Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/agent-skills.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"pi-lossless": patch
"opencode-lossless": patch
"lossless-core": patch
---

Ship a versioned Intent skill with each package (`skills/<name>/SKILL.md` in the published tarball): install, command and recall vocabulary, config essentials, and common mistakes, stamped with the package version on release. Adds `skills` to each package's published files and validates skills on publish and in CI.
83 changes: 83 additions & 0 deletions .github/workflows/check-skills.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: Check Skills

on:
pull_request:
paths:
- "packages/*/skills/**"
release:
types: [published]
workflow_dispatch: {}

jobs:
validate:
name: Validate intent skills
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false

- name: Setup
uses: ./.github/actions/setup

- name: Validate skills
run: bun run intent:validate -- --github-summary

review:
name: Check intent skill coverage
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0

- name: Setup
uses: ./.github/actions/setup

- name: Check skills
id: stale
run: |
bun run intent:stale -- --github-review

- name: Open or update review PR
if: steps.stale.outputs.has_review == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
VERSION="${RELEASE_TAG:-manual}"
BRANCH="skills/review-${VERSION}"
BASE_BRANCH="$DEFAULT_BRANCH"

git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

git fetch origin "$BRANCH" || true
if git show-ref --verify --quiet "refs/remotes/origin/$BRANCH"; then
git checkout -B "$BRANCH" "origin/$BRANCH"
else
git checkout -b "$BRANCH"
git commit --allow-empty -m "chore: review intent skills for ${VERSION}"
git push origin "$BRANCH"
fi

PR_URL="$(gh pr list --head "$BRANCH" --json url --jq '.[0].url')"
if [ -n "$PR_URL" ]; then
gh pr edit "$PR_URL" --body-file pr-body.md
else
gh pr create \
--title "Review intent skills (${VERSION})" \
--body-file pr-body.md \
--head "$BRANCH" \
--base "$BASE_BRANCH"
fi
9 changes: 6 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
"check": "vp check",
"format": "vp fmt",
"format:check": "vp fmt --check",
"intent:stale": "intent stale",
"intent:validate": "intent validate",
"lint:ci": "vp lint --quiet",
"test": "vp test",
"test:coverage": "vp test --coverage --coverage.reporter=lcov",
Expand All @@ -20,13 +22,14 @@
"opencode:compact": "bun scripts/opencode-compact.ts",
"pack": "vp run --filter ./packages/core prepack && vp run --filter ./packages/pi prepack && vp run --filter ./packages/opencode prepack",
"changeset": "changeset",
"version": "changeset version && vp fmt packages/*/CHANGELOG.md",
"version": "changeset version && bun scripts/sync-skill-versions.ts && vp fmt packages/*/CHANGELOG.md",
"release": "changeset publish",
"prepare": "vp config || true",
"eval": "bun scripts/eval-long-context.ts"
},
"devDependencies": {
"@changesets/cli": "^3.0.3",
"@tanstack/intent": "0.5.6",
"@types/node": "^26.6.5",
"@vitest/coverage-v8": "catalog:",
"typescript": "^7.0.2",
Expand Down
8 changes: 5 additions & 3 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"lcm",
"llm",
"memory",
"retrieval"
"retrieval",
"tanstack-intent"
],
"homepage": "https://github.com/stainless-code/lossless#readme",
"bugs": "https://github.com/stainless-code/lossless/issues",
Expand All @@ -21,7 +22,8 @@
},
"files": [
"dist",
"CHANGELOG.md"
"CHANGELOG.md",
"skills"
],
"type": "module",
"main": "./dist/index.mjs",
Expand All @@ -35,7 +37,7 @@
"scripts": {
"pack:watch": "vp pack --watch",
"prepack": "vp pack",
"prepublishOnly": "vp check && vp test"
"prepublishOnly": "vp check && vp test && intent validate"
},
"dependencies": {
"typebox": ">=1.4.3 <2"
Expand Down
91 changes: 91 additions & 0 deletions packages/core/skills/lossless-core/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
---
name: lossless-core
description: "Use when wiring a host to lossless-core or working on the engine itself: implement ModelHost or Reader, run a compaction, retrieval, or heal pass, or read the store directly with LcmStore."
license: MIT
metadata:
type: core
library: "lossless-core"
library_version: "0.1.1"
sources:
- stainless-code/lossless:packages/core/src/host.ts
- stainless-code/lossless:packages/core/src/index.ts
- stainless-code/lossless:packages/core/README.md
---

# Lossless core

Use it when implementing a host adapter or touching the engine. Do not use it
for adapter commands: Pi subcommands live in the pi-lossless skill,
`lossless-*` commands in the opencode-lossless skill.

The engine behind `pi-lossless` and `opencode-lossless`: Lossless Context
Management for a long-running agent. Every message is stored verbatim,
aged context is replaced with searchable pointers, and the original text
stays readable by id, entry, or search.

## Minimal wiring

```ts
import { LcmStore, runCompaction, runHeal, runRetrieval } from "lossless-core";
import type { ModelHost, Reader } from "lossless-core";
```

Call shapes (full signatures in `src/index.ts`, port contracts in `src/host.ts`):

```ts
const store = new LcmStore(dbPath);
await runCompaction(store, input, llm); // input: CompactionInput (span, budgets)
await runRetrieval(store, request, reader); // request: RetrievalRequest (scope, query)
await runHeal(store, { dryRun: true }, llm); // HealInput: maxNodes, maxCalls, dryRun plan mode
```

The engine has no agent SDK dependency. A host supplies two ports and the
engine does the rest. Pi implements both; the OpenCode v2 adapter implements
`ModelHost` and runs retrieval in a host child session instead of `Reader`.

## What it does

- **Ingest.** Every message lands in SQLite with its entry id, role,
timestamp, and any file handle the host attached.
- **Compaction.** Past a swap ratio, a pass summarizes spans into leaf and
condensed nodes and leaves a pointer in place. Nothing is deleted.
- **Retrieval.** A reader answers by calling `lcm_grep`, `lcm_describe`,
and `lcm_expand`, so the answer cites what was read.
- **Repair.** `runHeal` re-derives a truncated summary and its ancestors
from the provenance rows they still point at.
- **Redaction.** Secret-shaped text is masked on the way to a model, never
at the door, so an export or read still returns what was stored.

## The two ports

`src/host.ts` documents both. `ModelHost` is what the engine calls a model
through: `session()`, `find()`, `complete()`, plus where notices go.
`Reader` is one retrieval conversation: `begin()` returns a `Reading`
whose `next()` appends results and returns the model reply.

| Type | Question it answers |
| ----------- | ----------------------------------------------------- |
| `ModelHost` | Which model, and what did it answer |
| `Reader` | What did the reader ask for, and what did it conclude |

## Boundary rule

No module under `src/` imports an agent SDK, a bare specifier other than
`node:*` and `typebox`, or a file outside the engine. The boundary test
fails any violation. Import the package root only.

## Common mistakes

- **Importing engine source deep paths** (`lossless-core/dist/...` or
relative `src/...` files) instead of the package root — the boundary test
fails deep imports; `import { LcmStore } from "lossless-core"` is the only entry.
- **Expecting a host SDK in the bundle** - the host always supplies
`ModelHost` (and `Reader`, unless the host runs retrieval itself, as the OpenCode adapter does); implement the port, never `npm install` an SDK.
- **Assuming compaction deletes** - pointers replace text in context,
the verbatim rows stay queryable via the recall views.
- **Redacting at write time** - masking happens on the model path only; a stored row is never rewritten.

## API surface

- `LcmStore`, `runCompaction`, `runRetrieval`, `runHeal` — exported from `src/index.ts`; pass and heal options live next to their runners.
- `ModelHost`, `Reader`, `Reading` (types, `src/host.ts`) — the two port contracts; implement these, never a host SDK.
13 changes: 1 addition & 12 deletions packages/core/test/package-boundary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,10 @@ test("package: the engine depends on its schema library and nothing else", () =>
test("package: the engine is publishable, and ships dist plus the docs it owns", () => {
assert.equal(manifest.name, "lossless-core");
assert.equal(manifest.private, undefined);
// npm always includes README and LICENSE from the package directory, so the
// file list names only what it has to be told about. changesets writes the
// changelog in place, as it does for the adapter.
assert.deepEqual(manifest.files, ["dist", "CHANGELOG.md"]);
assert.deepEqual(manifest.files, ["dist", "CHANGELOG.md", "skills"]);
for (const name of ["README.md", "LICENSE", "CHANGELOG.md"]) {
assert.ok(existsSync(join(PACKAGE_DIR, name)), `${name} is not in the package`);
}
// The root keeps the repo's licence; the package ships the same bytes.
assert.deepEqual(
readFileSync(join(PACKAGE_DIR, "LICENSE")),
readFileSync(join(REPO_ROOT, "LICENSE")),
Expand All @@ -88,9 +84,6 @@ test("package: the engine is publishable, and ships dist plus the docs it owns",

const SHIPPED_DOCS = ["README.md", "CHANGELOG.md"];

/** The links in a shipped doc that resolve outside the package or to a file it
* does not carry. An npm reader has the tarball and nothing around it, so both
* fail for them. */
function deadLinks(source: string): string[] {
const dead: string[] = [];
for (const match of source.matchAll(/\]\(([^)\s]+)\)/g)) {
Expand All @@ -105,8 +98,6 @@ function deadLinks(source: string): string[] {
}

test("package: a shipped doc links only to what its tarball carries", () => {
// A planted violation, so a walk that finds nothing is known to work rather
// than known to be silent, and an inside link it has to leave alone.
assert.deepEqual(deadLinks("[the roadmap](../../docs/roadmap.md)"), ["../../docs/roadmap.md"]);
assert.deepEqual(deadLinks("[the changelog](./CHANGELOG.md)"), []);
const problems = SHIPPED_DOCS.flatMap((name) =>
Expand Down Expand Up @@ -138,8 +129,6 @@ test("package: every name the engine's README promises is exported", () => {
const values = imports
.filter(([, kind]) => kind === undefined)
.flatMap(([, , names]) => namedIn(names!));
// A type import is erased at runtime, so it is checked against the source
// that declares it; `bun run typecheck` reads the import itself.
const types = imports
.filter(([, kind]) => kind !== undefined)
.flatMap(([, , names]) => namedIn(names!));
Expand Down
8 changes: 5 additions & 3 deletions packages/opencode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"context",
"lcm",
"memory",
"opencode"
"opencode",
"tanstack-intent"
],
"homepage": "https://github.com/stainless-code/lossless#readme",
"bugs": {
Expand All @@ -22,7 +23,8 @@
},
"files": [
"dist",
"CHANGELOG.md"
"CHANGELOG.md",
"skills"
],
"type": "module",
"main": "./dist/index.mjs",
Expand All @@ -35,7 +37,7 @@
"scripts": {
"pack:watch": "vp pack --watch",
"prepack": "vp pack",
"prepublishOnly": "vp check && vp test"
"prepublishOnly": "vp check && vp test && intent validate"
},
"devDependencies": {
"@opencode/ai": "2.1.4",
Expand Down
Loading
Loading