diff --git a/.agents/README.md b/.agents/README.md index ed361d6..9ea1fb1 100644 --- a/.agents/README.md +++ b/.agents/README.md @@ -41,6 +41,7 @@ This decides where things go, so it is worth getting right: | Repo-root stub (tools / humans) | [`AGENTS.md`](../AGENTS.md) → this README | | Verify each step (gates) | [`skills/verify-after-each-step`](skills/verify-after-each-step/SKILL.md) | | Docs governance, comparison claims | [`skills/docs-governance`](skills/docs-governance/SKILL.md) | +| Docs drift audit and factual refresh | [`skills/docs-sync`](skills/docs-sync/SKILL.md) | | Docs-site voice and build order | [`skills/docs-voice`](skills/docs-voice/SKILL.md) | | Past corrections | [`lessons.md`](lessons.md) | | Writing style (every prose surface) | [`skills/unslop`](skills/unslop/SKILL.md) | diff --git a/.agents/skills/docs-governance/SKILL.md b/.agents/skills/docs-governance/SKILL.md index 58cabd8..1727fa5 100644 --- a/.agents/skills/docs-governance/SKILL.md +++ b/.agents/skills/docs-governance/SKILL.md @@ -1,30 +1,38 @@ --- name: docs-governance -description: Doc lanes for this repo. Use when creating or editing README.md, docs/roadmap.md, CHANGELOG.md, COMPARISON.md, a package README, a changeset, or any file under .agents/; each doc has one job and content never gets copied between them. +description: Doc lanes for this repo. Use when creating or editing README.md, docs/roadmap.md, CHANGELOG.md, the comparison sources, a package README, a changeset, or any file under .agents/; each doc has one job and content never gets copied between them. --- # Docs governance Each doc has exactly one job, keep content in its lane: -| File | Is for | Never contains | -| ------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | -| `README.md` | Repo map: the two packages, where to work, the gates, doc links | Install, config or usage sections; version history; roadmap; internal rationale | -| `packages/pi/README.md` | The adapter's npm landing: what it does, install, commands, config pointers | Version history, roadmap, repo map, engine internals | -| `packages/*/CHANGELOG.md` | Each package's version history, written by changesets | Marketing prose, roadmap | -| `apps/docs/COMPARISON.md` | Peer comparison with verified cells, the source the site's comparison page generates from | Feature marketing | -| `packages/core/README.md` | The engine's API: the entry, the two ports, the boundary rule | Pi-specific behaviour, the split's rationale | -| `docs/roadmap.md` | Forward-looking work, evidence gates, rejected items | Shipped-feature history | -| `.agents/**` | Agent rules, skills, lessons | Product docs | +| File | Is for | Never contains | +| ----------------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| `README.md` | Repo map: the three packages, where to work, the gates, doc links | Install, config or usage sections; version history; roadmap; internal rationale | +| `packages/pi/README.md` | The adapter's npm landing: what it does, install, commands, config pointers | Version history, roadmap, repo map, engine internals | +| `packages/opencode/README.md` | The OpenCode adapter's npm landing: what it does, install, commands, tools, live loop | Version history, roadmap, repo map, engine internals | +| `packages/*/CHANGELOG.md` | Each package's version history, written by changesets | Marketing prose, roadmap | +| `apps/docs/content/reference/pi/comparison.mdx` | Audited Pi peer comparison; the page and the source are one file | Feature marketing, unverified cells | +| `apps/docs/content/reference/opencode/comparison.mdx` | Audited OpenCode v2 peer comparison; the page and the source are one file | Feature marketing, unverified cells | +| `apps/docs/content/_snippets/**` | Shared Markdown or MDX fragments, spliced into the per-harness wrapper pages with `` | Frontmatter, a second full copy of a page, or the other harness's content | +| `packages/core/README.md` | The engine's API: the entry, the two ports, the boundary rule | Pi-specific behaviour, the split's rationale | +| `docs/roadmap.md` | Forward-looking work, evidence gates, rejected items | Shipped-feature history | +| `.agents/**` | Agent rules, skills, lessons | Product docs | ## Rules 1. **Never duplicate content across these docs**, package READMEs included, link - instead. + instead. The per-harness guide and reference pages are the one exception in + form, not in content: `apps/docs/content/_snippets/**` holds the shared + fragments, plus one partial per harness where the page differs. The wrappers + under `apps/docs/content/{guides,reference}/{pi,opencode}/*.mdx` hold + frontmatter and `` statements only. Edit the snippet, never the + wrapper body. 2. **Version bumps flow through changesets**, run `bun run changeset` with your change; `bun run version` generates the CHANGELOG entry. Every changeset - names the package it bumps in the frontmatter, the engine included: both - packages publish, so both take changesets. Never hand-bump `package.json` + names the package it bumps in the frontmatter, the engine included: all + three packages publish, so all three take changesets. Never hand-bump `package.json` version for shipped work. The generator in `.changeset/config.json` must stay `@changesets/cli/changelog`: the `changelog-github` generator aborts `bun run version` without a @@ -39,31 +47,32 @@ Each doc has exactly one job, keep content in its lane: never cut. `apps/docs` is private and ships nothing to npm, so a site-only change takes no changeset. 3. **Comparison claims are verified against tarballs, dated, and peer-named - only in `COMPARISON.md`.** Check a peer package against its published + only in the comparison sources.** Check a peer package against its published tarball and npm metadata (`npm view peerDependencies dependencies -time.modified`), never against its README, and stamp `COMPARISON.md` with + time.modified`), never against its README, and stamp each source with "Last verified: …" before treating a cell as settled. Credit a peer's win in the cell: a comparison that shows only our wins is marketing. - Peer package names appear in `COMPARISON.md` and in the page generated - from it (`apps/docs/content/reference/comparison.mdx`, written by - `apps/docs/scripts/build-comparison.ts`), and nowhere else in this repo or - on the site (owner policy). The generated page restates no cell - and is never hand-edited: `COMPARISON.md` is the only writer's source. There - are no inspiration credits: the design cites the LCM paper (algorithmic + Peer package names appear in + `apps/docs/content/reference/{pi,opencode}/comparison.mdx`, and nowhere else + in this repo or on the site (owner policy). Those two pages are both the + audited source and the rendered page: edit a cell there, with its + verification note and date, never in a second copy. There are no inspiration + credits: the design cites the LCM paper (algorithmic spec) and the smart-zone framing (Matt Pocock) only, because the code is an original implementation. The docs site carries no credit line; a concept page cites the paper where a mechanism comes from, as [how it works](../../../apps/docs/content/concepts/how-it-works.mdx) does. - Attribution lives in `packages/*/README.md`, `apps/docs/COMPARISON.md`, and + Attribution lives in `packages/*/README.md`, the comparison sources, and the licence preamble (owner decision). - `COMPARISON.md` is not a spec: a design decision cites a paper section or a + The comparison sources are not a spec: a design decision cites a paper section or a measured result, never a comparison cell, and a cell that scores a mechanism as a peer's cost is not a reason to lack it. -4. **Each published package owns its landing docs.** `packages/pi/README.md` is - the landing npm shows, `packages/pi/CHANGELOG.md` is the version history - changesets writes in place, and `packages/pi/LICENSE` is a copy of the root - licence. The comparison lives beside the docs site (`apps/docs/COMPARISON.md`) - and generates the site's comparison page, so the tarball no longer ships it. The +4. **Each published package owns its landing docs.** `packages//README.md` is + the landing npm shows, `packages//CHANGELOG.md` is the version history + changesets writes in place, and `packages//LICENSE` is a copy of the root + licence. The comparisons live in + `apps/docs/content/reference/{pi,opencode}/comparison.mdx`, so the tarball + no longer ships them. The holder named there is the org brand, `stainless-code`, as in every other stainless-code package, never a personal name. npm always includes `README` and `LICENSE` from the package directory, so the diff --git a/.agents/skills/docs-sync/SKILL.md b/.agents/skills/docs-sync/SKILL.md new file mode 100644 index 0000000..34ab2ac --- /dev/null +++ b/.agents/skills/docs-sync/SKILL.md @@ -0,0 +1,51 @@ +--- +name: docs-sync +description: Audit documentation against shipped behavior and update only factually stale pages. Use after a release or a command, config, CLI, API, default, path, or workflow change; when asked to check docs for drift, refresh stale docs, or run a scheduled docs audit. +--- + +# Docs sync + +Keep `apps/docs`, the package READMEs, and the root docs true to what shipped. This is maintenance, not authorship: find claims the code no longer supports, fix exactly those, and verify the docs site. [`docs-governance`](../docs-governance/SKILL.md) owns which doc holds what; [`docs-voice`](../docs-voice/SKILL.md) owns voice, anchors, and the site build order. + +## Ground rules + +- Document only behavior that is in the repository now. Work behind a disabled flag is not public documentation. +- Facts over polish: a wrong default, flag, path, version, limit, provider, or required step is a docs bug; a wording preference is not. +- Copy exact identifiers and values from code. Do not paraphrase a command, key, default, or environment variable. +- Touch the fewest pages that remove the drift. Preserve the surrounding structure, components, and anchors. +- Do not edit generated output (`dist/`, `.blume/`). Do not overwrite unrelated local changes. +- A run that finds nothing actionable ends with a short report and no commit. + +## Workflow + +1. **Set the scope.** Read `AGENTS.md`, [`docs-governance`](../docs-governance/SKILL.md), and [`docs-voice`](../docs-voice/SKILL.md). Honor a lookback window, docs path, or report-only request from the user. With no window, compare against changes since the last docs update or the last stable milestone. + Sources: `git log`, `.changeset/**`, `packages/*/CHANGELOG.md`, package entry points and config schemas, CLI help, environment variables, routes, and the docs under `apps/docs/content/**`, `_snippets/**`, package READMEs, `README.md`, and `docs/roadmap.md`. + Done when every changed public fact has a source and either a docs target or a recorded reason it is internal. + +2. **Find drift.** Compare facts, not prose: + - A command, flag, config key, environment variable, route, prop, exported API, default, path, or version changed. + - A shipped capability is absent from the page that should describe it. + - A page promises a provider, adapter, integration, or workflow the code no longer supports. + - A required step disappeared, or an example no longer runs. + - A linked page or heading moved, so the link or anchor is stale. + - An external link the page depends on points at a dead or superseded primary source. + - A release or changeset needs a user-facing entry. + + Skip subjective rewording, voice work, speculative future work, duplicated content, and formatting churn. Send wording-only changes through [`docs-voice`](../docs-voice/SKILL.md). + +3. **Edit the owning surface.** Update the snippet or source file, never a generated wrapper. Keep package READMEs in their own lane. Do not copy the same fact into several docs; link instead. Preserve published anchors by pinning the old slug when a linked heading must change. + +4. **Verify.** + - From the repo root: `vp check`. + - From `apps/docs`: `bun run check --isolated` while a dev server may be running. A missing `GITHUB_TOKEN` warning is an environment warning, not a docs failure; report it as such. + - If the change adds, removes, or renames a page or heading, or changes site navigation, follow the full build, validate, and audit order in [`docs-voice`](../docs-voice/SKILL.md). + - Fix failures caused by this edit. Report pre-existing failures separately. + +5. **Deliver.** This repo commits on `main`, one commit per stable milestone, with a changeset for user-visible package change. A report-only audit or a no-op creates no branch and no commit. When edits are needed, make one focused commit and state what was checked, what changed, what was verified, what was skipped, and what risk remains. + +## Done means + +- Every changed public fact is either corrected in its owning doc or explicitly recorded as internal. +- The diff is the smallest set of factual corrections. +- `vp check` passes and the docs check passes, or the remaining failure is named and shown to predate the edit. +- A no-op run has no edits and reports the areas checked. diff --git a/.agents/skills/docs-voice/SKILL.md b/.agents/skills/docs-voice/SKILL.md index a9a1264..ce488de 100644 --- a/.agents/skills/docs-voice/SKILL.md +++ b/.agents/skills/docs-voice/SKILL.md @@ -11,14 +11,15 @@ description: Voice and pipeline for the published docs site (apps/docs): heading ## Surfaces and their modes -| Surface | Mode | Boundary | -| ------------------------------------ | ----------- | ------------------------------------------------------------------------- | -| `pages/_home/*`, `pages/index.astro` | copy | Opinion and pitch are allowed. No claim the page cannot show. | -| `content/concepts/**` | explanation | Opinion allowed. A heading tolerates "About ..." in front. | -| `content/guides/**` | how-to | Commands, conditions, expected output. No teaching. | -| `content/reference/**` | reference | Dry. No "you", no persuasion, spec tables only. | -| `content/reference/comparison.mdx` | generated | Never hand-edited; `apps/docs/COMPARISON.md` is the only writer's source. | -| `content/**/index.mdx` | routing | Help the reader choose, do not summarize. | +| Surface | Mode | Boundary | +| ------------------------------------------------ | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `pages/_home/*`, `pages/index.astro` | copy | Opinion and pitch are allowed. No claim the page cannot show. | +| `content/concepts/**` | explanation | Opinion allowed. A heading tolerates "About ..." in front. | +| `content/guides/**` | how-to | Commands, conditions, expected output. No teaching. | +| `content/reference/**` | reference | Dry. No "you", no persuasion, spec tables only. | +| `content/reference/{pi,opencode}/comparison.mdx` | reference | Audited peer comparison; the page is the source, so the verification note and date live with the cells. | +| `apps/docs/content/_snippets/**` | source | Shared fragments, plus one partial per harness where the page differs. The wrapper pages under `content/{guides,reference}/` hold frontmatter and includes only. | +| `content/**/index.mdx` | routing | Help the reader choose, do not summarize. | ## Headings carry the point @@ -65,8 +66,8 @@ If anything links to it, take the new text and pin the slug. `blume audit` reads the built `apps/docs/dist`, and an isolated build omits `robots.txt` and `llms.txt`, so an isolated build cannot be audited. A non-isolated build refuses to run while `blume dev` is up. 1. Stop the dev server. -2. From `apps/docs`: `rm -rf dist && bunx blume build`. -3. `bunx blume validate --strict`. +2. From `apps/docs`: `rm -rf dist && bun run build`. +3. `bun run validate --strict`. 4. `bunx blume audit --fail-on error`. 5. Restart `bunx blume dev`. diff --git a/.agents/skills/typescript-best-practices/PATTERNS.md b/.agents/skills/typescript-best-practices/PATTERNS.md index 220ef50..569bb26 100644 --- a/.agents/skills/typescript-best-practices/PATTERNS.md +++ b/.agents/skills/typescript-best-practices/PATTERNS.md @@ -31,7 +31,9 @@ type DiffState = { loading: boolean; diff?: GitDiff; error?: string }; // Do. Only valid states exist. type DiffState = - { kind: "loading" } | { kind: "ready"; diff: GitDiff } | { kind: "error"; error: string }; + | { kind: "loading" } + | { kind: "ready"; diff: GitDiff } + | { kind: "error"; error: string }; ``` Pick one discriminant name (`kind`, `type`, `tag`) and stick to it. diff --git a/.changeset/README.md b/.changeset/README.md index fb84d04..8415d15 100644 --- a/.changeset/README.md +++ b/.changeset/README.md @@ -5,9 +5,8 @@ generated `.changeset/*.md` file on `main` in the same commit as the change. At release time: `bun run version` (bumps the version and writes the changelog from the pending changesets), then `bun run pack`. -Two packages, each published from its own directory, so a changeset names the -package it bumps in the frontmatter (`"pi-lossless": patch`). The engine, -`lossless-core`, takes no changeset until it publishes. The changelog text comes -from `@changesets/cli/changelog`, which writes it locally from the changeset -bodies. This repo has no remote and no pull requests, so the GitHub changelog -generator is never used. +Each package publishes from its own directory, so a changeset names the package +it bumps in the frontmatter (`"pi-lossless": patch`). The engine, +`lossless-core`, takes a changeset like any other published package. The +changelog text comes from `@changesets/cli/changelog`, which writes it locally +from the changeset bodies; the GitHub changelog generator is never used. diff --git a/.changeset/core-host-seams.md b/.changeset/core-host-seams.md new file mode 100644 index 0000000..ac9d49d --- /dev/null +++ b/.changeset/core-host-seams.md @@ -0,0 +1,5 @@ +--- +"lossless-core": patch +--- + +Add the three host seams the OpenCode adapter uses: `renderableFrontier` renders a store's DAG frontier for a host checkpoint, `grepView` accepts an `expandTool` name so each adapter can name its own expansion tool, and `LcmStore.entryCountWithPrefix` counts a family of rows without a table scan. diff --git a/.changeset/dependency-refresh.md b/.changeset/dependency-refresh.md new file mode 100644 index 0000000..3636715 --- /dev/null +++ b/.changeset/dependency-refresh.md @@ -0,0 +1,7 @@ +--- +"lossless-core": patch +"pi-lossless": patch +"opencode-lossless": patch +--- + +Refresh every external dependency to its newest published version: Vite+ 1.4.0 and Vitest 5.0.5, TypeScript 7.0.2, `@types/node` 26.6.5, `@changesets/cli` 3.0.3, Pi 1.4.0, OpenCode 2.1.4, Typebox 1.4.3, Blume 2.2.5, and Bun 1.4.4. Pi host packages now use the `*` peer range its package guide documents for packages the host supplies. diff --git a/.changeset/heal-source-window.md b/.changeset/heal-source-window.md new file mode 100644 index 0000000..e8a2a98 --- /dev/null +++ b/.changeset/heal-source-window.md @@ -0,0 +1,7 @@ +--- +"lossless-core": patch +"pi-lossless": patch +"opencode-lossless": patch +--- + +Derive the heal source window from the summary target instead of clipping every source message at a fixed 3,000 tokens. The window is clamped to the summarizer request budget and has an explicit `healSourceTokens` override in Pi and OpenCode. A leaf that exceeds one window is mapped through up to eight exact windows and reduced once, so a large tool result can become clean without one enormous prompt. A source beyond the bounded plan is blocked without another model call, while a mechanical node gets one improvement. Heal reports now separate clean repairs from improvements and record source-blocked nodes. diff --git a/.changeset/observability-and-heal.md b/.changeset/observability-and-heal.md new file mode 100644 index 0000000..42fca53 --- /dev/null +++ b/.changeset/observability-and-heal.md @@ -0,0 +1,7 @@ +--- +"lossless-core": patch +"pi-lossless": patch +"opencode-lossless": patch +--- + +Add per-node summary timing, session-correlated engine rows, and adapter-routed metrics. OpenCode now writes engine and summarizer rows to its sidecar log, exposes the summarizer call and pass budgets, marks character-derived usage as estimated, and reports summary latency, cap outcomes, and abandoned rows. Add persistent summary quality: clean, clipped, mechanical, and derived. Existing stores migrate to generation 2, and a failed parent stays visible as derived. Add a bounded heal command in both adapters. It finds tainted summaries and their ancestors, re-derives them from stored source bottom-up, rejects another mechanical result, and supports a dry run. A heal owns its store connection so a plugin reload cannot finalize it, and one session allows one heal at a time. Mechanical work runs before a clipped retry, so a bounded run keeps making progress. diff --git a/.changeset/opencode-lossless.md b/.changeset/opencode-lossless.md new file mode 100644 index 0000000..70823ce --- /dev/null +++ b/.changeset/opencode-lossless.md @@ -0,0 +1,5 @@ +--- +"opencode-lossless": patch +--- + +Add the OpenCode v2 adapter. It mirrors prompts, assistant text, and tool traffic verbatim into a per-session SQLite sidecar; registers `lcm_grep`, `lcm_describe`, `lcm_expand`, and the `/lossless-status`, `/lossless-report`, `/lossless-pass`, `/lossless-backup`, `/lossless-export`, `/lossless-import`, `/lossless-diagnose`, `/lossless-migrate`, `/lossless-gc`, and `/lossless-heal` commands; and records the host's compaction boundaries and summaries beside the rows they replace. `assembly` defaults to true: it builds the leaf and condensed DAG, projects the aged frontier into `session.context`, and hands the same frontier to the host as its compaction checkpoint, marked so the next request re-applies it. `assembly: false` leaves compaction to the host and omits `/lossless-pass` and `lcm_expand_query`. `lcm_expand_query` runs a child session narrowed to the three recall tools, reads the parent's sidecar, returns findings only, and removes the child with its transcript sidecar. Every seam writes a diagnostic row to `metrics.jsonl` (projection, checkpoint, pass, recall, reader), and `/lossless-report` summarizes this session's rows. `lcm_grep` gains `scope` and `lcm_expand` gains `session`, which read other sessions' sidecars read-only; OpenCode keeps one global sidecar directory, so `sessions` and `all_sessions` are equivalent and the reader remains session-scoped. `/lossless-diagnose` builds a redacted bundle and can repair the full-text index. `/lossless-gc` is age-based because the plugin API cannot list host sessions; it exports each transcript before deleting and never touches the current or open sidecar. There is no cockpit in V2 scope: the host TUI extension point exists, so a separate TUI plugin could reuse the same command core later. The adapter has no runtime dependencies, and sidecar files and their WAL are owner-only (0600) with a 0700 directory. diff --git a/.changeset/owner-only-store-files.md b/.changeset/owner-only-store-files.md new file mode 100644 index 0000000..a8e8768 --- /dev/null +++ b/.changeset/owner-only-store-files.md @@ -0,0 +1,5 @@ +--- +"lossless-core": patch +--- + +Create store files owner-only before SQLite opens them, so the database, WAL, and shared-memory files are 0600 from the first write, and repair an existing world-readable database on open. diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 93018be..609f81d 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -7,17 +7,17 @@ runs: - name: Setup Bun (the install, the audit, and the node-based scripts) uses: oven-sh/setup-bun@v2 with: - bun-version: 1.4.2 + bun-version: 1.4.4 - name: Setup Node (the npm publish dry runs and the node-based scripts) - uses: actions/setup-node@v4 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7 with: node-version: 22 # Vite+'s installer is what puts `vp` on PATH for a bare `vp run` step; the # two manager flags keep setup-bun and setup-node the tools the jobs use. - name: Setup Vite+ (vp on PATH, cached dependencies) - uses: voidzero-dev/setup-vp@v1.21.1 + uses: voidzero-dev/setup-vp@v1.21.2 with: node-manager: false package-manager: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1ef685..4284066 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false @@ -32,7 +32,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false @@ -47,7 +47,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false @@ -62,7 +62,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false @@ -77,7 +77,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false @@ -95,12 +95,25 @@ jobs: working-directory: packages/core run: npm publish --dry-run + - name: Verify the OpenCode adapter's publish tarball (dry run) + working-directory: packages/opencode + run: npm publish --dry-run + + - name: Verify the OpenCode bundle and declarations name no host SDK + run: | + if grep -qE '@opencode/|lossless-core/src/' \ + packages/opencode/dist/index.mjs \ + packages/opencode/dist/index.d.mts; then + echo "the published artifact names a host SDK or the engine source" >&2 + exit 1 + fi + docs: name: 📖 Docs site runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false # blume --strict fails on BLUME_SHALLOW_GIT_HISTORY: lastmod reads git log per file. @@ -109,9 +122,6 @@ jobs: - name: Setup uses: ./.github/actions/setup - - name: Generate the comparison page from COMPARISON.md - run: bun apps/docs/scripts/build-comparison.ts - - name: Validate docs links run: vp run --filter ./apps/docs validate --strict @@ -129,7 +139,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false @@ -144,34 +154,23 @@ jobs: # act on it. Capture the exit code directly; a pipe into tee would # report tee's status, not bun's. # - # Ignored: fifteen highs in the private docs workspace via the Blume - # 2.1.3 subtree and two @earendil-works peer packages kept external. - # Pinned versions come from that subtree and peer ranges, so - # `bun audit fix` clears 0 without upstream bumps. Reassess on every - # Blume bump; any other path still blocks. + # Ignored: two highs that live only outside a published tarball. + # lodash-es comes from Blume 2.2.4's mermaid subtree (dagre-d3-es pins + # 4.17.23), and path-to-regexp 6.1.0 from @astrojs/vercel's + # routing-utils; the other path-to-regexp instance sits inside the + # @earendil-works/pi-ai peer subtree the host supplies. Both bundles + # keep their peers external. Reassess on every Blume or Pi bump; any + # other path still blocks. set +e out=$(NO_COLOR=1 bun audit --audit-level=high \ --ignore=GHSA-r5fr-rjxr-66jc \ --ignore=GHSA-9wv6-86v2-598j \ - --ignore=GHSA-4cwx-7wf7-3272 \ - --ignore=GHSA-vmh5-mc38-953g \ - --ignore=GHSA-vxpw-j846-p89q \ - --ignore=GHSA-hm92-r4w5-c3mj \ - --ignore=GHSA-qhr7-859c-m2p7 \ - --ignore=GHSA-6j4f-fj2g-mc7p \ - --ignore=GHSA-j22f-vq7h-c4qm \ - --ignore=GHSA-mcm9-63f2-9j32 \ - --ignore=GHSA-x5rw-q4pp-hg5g \ - --ignore=GHSA-ch52-4w7c-c8xp \ - --ignore=GHSA-68fv-2mgg-jv7q \ - --ignore=GHSA-rfgv-xxqx-mfg5 \ - --ignore=GHSA-w293-vg96-wgc3 \ 2>&1) code=$? set -e printf '%s\n' "$out" if [ "$code" -eq 0 ]; then exit 0; fi - # bun 1.4.2 wording: "error: POST - ConnectionRefused", + # bun 1.4.4 wording: "error: POST - ConnectionRefused", # " did not answer the audit request; skipped", # " returned a non-JSON audit response". if ! grep -qE '[0-9]+ vulnerabilit' <<<"$out" && diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 9beb2fb..82eb7cd 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false # A closed PR checkout defaults to a dead merge ref, so pin the merge commit on main. @@ -38,9 +38,6 @@ jobs: - name: Setup Bun uses: ./.github/actions/setup - - name: Generate the comparison page from COMPARISON.md - run: bun apps/docs/scripts/build-comparison.ts - - name: Validate docs links run: vp run --filter ./apps/docs validate --strict diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5bbb087 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,61 @@ +# [Changesets](https://github.com/changesets/changesets): open a "Version packages" PR when +# `.changeset/*.md` files land on `main`, then publish to npm when that PR merges via npm +# trusted publishing (GitHub OIDC — no NPM_TOKEN; provenance auto-generated). +name: Release + +on: + push: + branches: + - main + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +permissions: + contents: write + pull-requests: write + id-token: write + +jobs: + release: + name: Release + runs-on: ubuntu-latest + environment: release + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + fetch-depth: 0 + + # npm trusted publishing (OIDC) needs npm >= 11.5.1 (docs.npmjs.com/trusted-publishers). + # setup-bun leaves the runner's npm 10.x in PATH, so `npm publish` can't detect OIDC + # and fails with ENEEDAUTH. Node 24 ships npm 11.x; npm then auto-authenticates via OIDC + # (no token). This repo keeps zero native dependencies (node:sqlite), so unlike native-addon + # repos there is no node-gyp ABI concern with switching Node before `bun install`. + - name: Setup Node + npm for trusted publishing + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7 + with: + node-version: "24" + registry-url: "https://registry.npmjs.org" + + - name: Setup Bun and install + uses: ./.github/actions/setup + + # Opens/updates the Version PR when `.changeset/*.md` exist. When there are none, the action + # can still run `publish` (see changesets/action) so a just-merged Version PR can ship. + # `changeset publish` should exit 0 when there is nothing new to publish. If this job fails, + # read the step log: common causes are OIDC/trusted-publishing misconfig (`id-token: write`, + # the `release` environment, or the npm trusted-publisher binding), registry/network errors, + # or a real publish failure — not assumed from the runner alone. + - name: Create Release Pull Request or Publish to npm + id: changesets + uses: changesets/action@ae32849d5ba541f9ae29e40e22a623bc13562f51 # v2.1.2 + with: + # Runs `changeset version` then formats CHANGELOG.md so `format:check` passes + # (see the `version` script in package.json). + version-script: bun run version + publish-script: bun run release + pr-title: "chore: version packages" + commit-message: "chore: version packages" + create-github-releases: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md index 675ad34..654db28 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,12 +2,12 @@ This file loads every turn. What belongs in it, and which channels load at all, are defined in [`.agents/README.md`](.agents/README.md). Skills under `.agents/skills/` load by description; everything else is reached by link. -This repo is two packages: `packages/core` is the engine (`lossless-core`) and `packages/pi` is the Pi adapter (`pi-lossless`). [docs/roadmap.md](docs/roadmap.md) holds forward-looking decisions, and [`packages/pi/CHANGELOG.md`](packages/pi/CHANGELOG.md) holds the extension's shipped ones. +This repo holds the engine in `packages/core` (`lossless-core`) and the harness adapters under `packages/` (`pi` and `opencode`). [docs/roadmap.md](docs/roadmap.md) holds forward-looking decisions, and [`packages/pi/CHANGELOG.md`](packages/pi/CHANGELOG.md) holds the shipped ones. ## Non-negotiables - After each step (slice, plan item, fix, doc edit), run `vp check`; on `packages/*/src/**` or `packages/*/test/**` changes also `bun run test` and `bun run typecheck`. Never carry a red gate forward. `bun run test` (`vp test`, Vitest through Vite+) is the only test runner here, and the repo keeps no Bun test config: a bare `bun test` is not a supported way to check a change. Details: [`verify-after-each-step`](.agents/skills/verify-after-each-step/SKILL.md). -- After any turn that changed code under `packages/*/src/`, run `bun run pack`; live Pi sessions keep the old `dist/` until restarted. +- After any turn that changed code under `packages/*/src/`, run `bun run pack`; live Pi sessions keep the old `dist/` until restarted, and a running OpenCode server reloads the plugin when its configured directory changes. - No em dashes in any prose this repo ships or replies with. Apply [`unslop`](.agents/skills/unslop/SKILL.md) to every prose surface. - Prose names the agent **Pi**. Lowercase forms are identifiers: the `pi` command, `pi-lossless`, `pi-*` package names, `~/.pi/` paths, and version labels that sit beside `node`. Verbatim message text, fixture payloads and quoted host output keep the casing they came with. - New skills go under `.agents/skills/`. Policy that must fire is a skill or a line in this file, never a file reached only by link ([`.agents/README.md`](.agents/README.md)). diff --git a/README.md b/README.md index 34c3a2e..219278f 100644 --- a/README.md +++ b/README.md @@ -4,34 +4,39 @@ Lossless Context Management for coding agents: every message is stored verbatim, aged context is replaced by summaries a model can navigate, and the original text stays searchable. Nothing is lost, and the model sees less. -This repository holds the engine and the Pi extension that consumes it: +This repository holds the engine and the adapters that consume it: -| Package | Directory | What it is | -| ------------------------------------------ | --------------- | ------------------------------------------------------------------------------------------------------------- | -| [`pi-lossless`](packages/pi/README.md) | `packages/pi` | The extension a Pi user installs (`pi install npm:pi-lossless`): the recall tools, the cockpit, the commands. | -| [`lossless-core`](packages/core/README.md) | `packages/core` | The engine: ingest, compaction and retrieval behind a two-port host contract, naming no agent SDK. | +| Package | Directory | What it is | +| -------------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------ | +| [`pi-lossless`](packages/pi/README.md) | `packages/pi` | The Pi extension (`pi install npm:pi-lossless`): recall tools, cockpit, commands. | +| [`opencode-lossless`](packages/opencode/README.md) | `packages/opencode` | The OpenCode v2 plugin: recall tools, lifecycle commands, DAG projection, per-session sidecar. | +| [`lossless-core`](packages/core/README.md) | `packages/core` | The engine: ingest, compaction, and retrieval behind a two-port host contract, with no agent SDK dependency. | -`lossless-core` is a workspace `devDependency` of the adapter, and the pack -inlines it into `dist/index.mjs`, so the installed package stays one -self-contained artifact with no runtime dependencies. A harness other than Pi -implements `ModelHost` and `Reader` and drives the engine from -[`lossless-core`](https://npmx.dev/package/lossless-core). +`lossless-core` is a workspace `devDependency` of the Pi and OpenCode +adapters, and the pack inlines it into `dist/index.mjs`, so each published +package is one self-contained artifact with no runtime dependencies. The Pi +adapter implements both host ports. The OpenCode adapter implements `ModelHost` +and runs retrieval in a host child session, because its plugin API has no +in-process tool-calling completion. ## Where to work -| Path | Holds | -| ----------------------- | ---------------------------------------------------------------------------- | -| `packages/core/src/**` | the engine; the port in `src/host.ts` is its only host-facing surface | -| `packages/core/test/**` | engine tests, over port doubles rather than the Pi adapter | -| `packages/pi/src/**` | the Pi adapter, the only Pi-aware code in the tree | -| `packages/pi/test/**` | adapter tests, the Pi-shaped doubles, and the end-to-end spawn | -| `apps/docs/**` | the documentation site (Blume) | -| `scripts/**` | repo tools: the long-context evaluation, the metrics report, the peer census | -| `.agents/**` | agent rules, skills and lessons | +| Path | Holds | +| -------------------------------- | -------------------------------------------------------------------------------------------------------- | +| `packages/core/src/**` | the engine; the port in `src/host.ts` is its only host-facing surface | +| `packages/core/test/**` | engine tests, over port doubles rather than the Pi adapter | +| `packages/pi/src/**` | the Pi adapter, the only Pi-aware code in the tree | +| `packages/pi/test/**` | adapter tests, the Pi-shaped doubles, and the end-to-end spawn | +| `packages/opencode/src/**` | the OpenCode v2 adapter, the only OpenCode-aware code in the tree | +| `packages/opencode/test/**` | adapter tests, the host-shaped doubles, and the host-compat guard | +| `apps/docs/**` | the documentation site (Blume), its generated pages, and the per-harness page source | +| `apps/docs/content/_snippets/**` | shared guide and reference fragments; the per-harness wrappers include them | +| `scripts/**` | repo tools: the long-context evaluation, the metrics report, the peer census, and the OpenCode live loop | +| `.agents/**` | agent rules, skills and lessons | Test support files under `packages/core/test/` are shared on purpose: one HOME -guard, one pass-outcome reader, one temp-home helper, referenced by both -packages' test configs. +guard, one pass-outcome reader, one temp-home helper, referenced by every +package's test config. Every package declares its own needs. A library the package calls at runtime is a `dependencies` entry; a host package the bundle imports rather than inlines is @@ -42,20 +47,23 @@ own scripts run. ## Gates -Run these from the root. Every step is green by itself, and no gate is carried -red into the next one. +Run these from the root. A red gate stops the next step. ```bash bun install -bun run test # both suites, the only runner (vp test under the hood) +bun run test # all suites (vp test) bun run typecheck # one program over the tree vp check # format and lint -bun run pack # both publish bundles, dist included +bun run pack # all publish bundles, dist included bun run eval # the procedural long-context ladder, on demand ``` `bun run pack` writes the `dist/` a live Pi session loads, so after a change -under `packages/*/src` run it and restart Pi. The docs site has its own gates: +under `packages/*/src` run it and restart Pi. A running OpenCode server reloads +the plugin when its configured directory changes; its loop is +`bun run opencode:check`, `bun run opencode:smoke`, and `bun run opencode:compact` +(see [packages/opencode/README.md](packages/opencode/README.md#live-loop)). +The docs site has its own gates: ```bash cd apps/docs && bun run build && bun run validate --strict && bun run audit @@ -63,15 +71,17 @@ cd apps/docs && bun run build && bun run validate --strict && bun run audit ## Docs -| | | -| ----------------------------------------------------- | ------------------------------------------------------------------------------- | -| [The docs site](https://stainless-code.com/lossless/) | How LCM behaves in a session, the config keys, the limits | -| [pi-lossless](packages/pi/README.md) | Install, the commands, and the three recall tools | -| [lossless-core](packages/core/README.md) | The engine's API and the two ports a host implements | -| [COMPARISON.md](apps/docs/COMPARISON.md) | Peer comparison with verified cells, the source behind the site comparison page | -| [docs/roadmap.md](docs/roadmap.md) | Forward-looking work, evidence gates, and rejected items | -| [CHANGELOG.md](packages/pi/CHANGELOG.md) | Version history, generated from changesets | -| [AGENTS.md](AGENTS.md) | Agent rules, with the skills and lessons under `.agents/` | +| | | +| ----------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | +| [The docs site](https://stainless-code.com/lossless/) | How LCM behaves in a session, the config keys, the limits | +| [pi-lossless](packages/pi/README.md) | Install, the commands, and the three recall tools | +| [opencode-lossless](packages/opencode/README.md) | Install, the commands, the four recall tools, and the live loop | +| [lossless-core](packages/core/README.md) | The engine's API and the ports a host supplies | +| [pi/comparison.mdx](apps/docs/content/reference/pi/comparison.mdx) | Audited Pi peer comparison, page and source in one file | +| [opencode/comparison.mdx](apps/docs/content/reference/opencode/comparison.mdx) | Audited OpenCode v2 peer comparison, page and source in one file | +| [docs/roadmap.md](docs/roadmap.md) | Forward-looking work, evidence gates, and rejected items | +| [pi CHANGELOG.md](packages/pi/CHANGELOG.md), [core CHANGELOG.md](packages/core/CHANGELOG.md), [opencode CHANGELOG.md](packages/opencode/CHANGELOG.md) | Per-package version history, generated from changesets | +| [AGENTS.md](AGENTS.md) | Agent rules, with the skills and lessons under `.agents/` | ## License diff --git a/apps/docs/.gitignore b/apps/docs/.gitignore index 502bae8..60905e0 100644 --- a/apps/docs/.gitignore +++ b/apps/docs/.gitignore @@ -1,6 +1,3 @@ .blume/ .blume-verify/ dist/ - -# Generated from COMPARISON.md by scripts/build-comparison.ts; that file is the source -content/reference/comparison.mdx diff --git a/apps/docs/blume.config.ts b/apps/docs/blume.config.ts index 30c5d33..e6c0c3e 100644 --- a/apps/docs/blume.config.ts +++ b/apps/docs/blume.config.ts @@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url"; import { defineConfig } from "blume"; import { orama } from "blume/search"; -import { filesystem } from "blume/sources"; +import { filesystem, githubReleases } from "blume/sources"; import { CURATED_POPULAR } from "./components/curated-popular.js"; @@ -71,36 +71,39 @@ export default defineConfig({ lastModified: "git", content: { - sources: [filesystem({ root: "content" })], + sources: [ + filesystem({ root: "content" }), + githubReleases({ + owner: "stainless-code", + repo: "lossless", + prefix: "changelog", + }), + ], + }, + + changelog: { + title: "Changelog", + description: "Every Lossless package release, newest first.", }, integrations: [webAppHeadIntegration], navigation: { tabs: [ - { label: "Guides", path: "/guides" }, + { label: "Guides", path: "/guides", href: "/guides/pi/getting-started" }, { label: "Concepts", path: "/concepts" }, - { label: "Reference", path: "/reference" }, - ], - featured: [ - { - label: "Comparison", - href: "/reference/comparison", - icon: "scale", - }, - { - label: "GitHub", - href: "https://github.com/stainless-code/lossless", - icon: "github", - }, + { label: "Reference", path: "/reference", href: "/reference/pi/tools" }, + { label: "Roadmap", path: "/roadmap" }, + { label: "Changelog", path: "/changelog" }, ], sidebar: { display: "flat" }, }, footer: { links: [ - { label: "Comparison", href: "/reference/comparison" }, - { label: "Roadmap", href: "/reference/roadmap" }, + { label: "Comparison", href: "/reference/pi/comparison" }, + { label: "Roadmap", href: "/roadmap" }, + { label: "Changelog", href: "/changelog" }, { label: "npm", href: "https://npmx.dev/package/pi-lossless" }, { label: "MIT license", diff --git a/apps/docs/components.ts b/apps/docs/components.ts index 2849626..6be7530 100644 --- a/apps/docs/components.ts +++ b/apps/docs/components.ts @@ -2,15 +2,19 @@ import { defineComponents } from "blume"; export default defineComponents({ layout: { - // No layout.Footer: the homepage slots one, docs pages stay empty. - // String paths, so the `.astro` files stay out of this TypeScript project: - // Vite+'s type program has no `*.astro` module declaration and Blume - // resolves a path against the project root the same way. + // Docs pages have no site footer; the landing page slots its own through + // PageLayout, and the 404 page keeps the default footer. String paths, so + // the `.astro` files stay out of this TypeScript project: Vite+'s type + // program has no `*.astro` module declaration and Blume resolves a path + // against the project root the same way. Header: "./components/blume/Header.astro", + Footer: "./components/blume/EmptyFooter.astro", Pagination: "./components/blume/Pagination.astro", + Sidebar: "./components/blume/Sidebar.astro", }, mdx: { FaqSection: "./components/seo/FaqSection.astro", + HarnessCards: "./components/mdx/HarnessCards.astro", DagFigure: "./components/visuals/DagFigure.astro", EscalationLadder: "./components/visuals/EscalationLadder.astro", RecallFlow: "./components/visuals/RecallFlow.astro", diff --git a/apps/docs/components/blume/EmptyFooter.astro b/apps/docs/components/blume/EmptyFooter.astro new file mode 100644 index 0000000..0c8936f --- /dev/null +++ b/apps/docs/components/blume/EmptyFooter.astro @@ -0,0 +1,4 @@ +--- +// Docs pages intentionally render no site footer. The landing page slots its +// own footer through PageLayout, and the 404 page keeps the default one. +--- diff --git a/apps/docs/components/blume/Footer.astro b/apps/docs/components/blume/Footer.astro index f503fbf..90f3249 100644 --- a/apps/docs/components/blume/Footer.astro +++ b/apps/docs/components/blume/Footer.astro @@ -30,61 +30,58 @@ const groups: { { label: "Docs", links: [ - { href: contentHref("/guides"), label: "Guides" }, + { href: contentHref("/guides/pi/getting-started"), label: "Guides" }, { href: contentHref("/concepts"), label: "Concepts" }, - { href: contentHref("/reference"), label: "Reference" }, - { href: contentHref("/reference/comparison"), label: "Comparison" }, + { href: contentHref("/reference/pi/tools"), label: "Reference" }, + { href: contentHref("/reference/pi/comparison"), label: "Comparison" }, + { href: contentHref("/roadmap"), label: "Roadmap" }, + { href: contentHref("/changelog"), label: "Changelog" }, ], }, { label: "Packages", links: [ - { href: "https://npmx.dev/package/pi-lossless", label: "pi-lossless", external: true }, { href: "https://npmx.dev/package/lossless-core", label: "lossless-core", external: true }, - { href: `${repoUrl}/blob/main/docs/roadmap.md`, label: "Roadmap", external: true }, + { href: "https://npmx.dev/package/pi-lossless", label: "pi-lossless", external: true }, + { href: "https://npmx.dev/package/opencode-lossless", label: "opencode-lossless", external: true }, ], }, { label: "Project", links: [ { href: repoUrl, label: "GitHub", external: true }, - { href: "https://npmx.dev/package/pi-lossless", label: "npm", external: true }, { href: `${repoUrl}/blob/main/LICENSE`, label: "MIT license", external: true }, - { href: "https://github.com/stainless-code", label: "stainless-code", external: true }, ], }, ]; ---