diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 6691270..3af2af6 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -89,7 +89,7 @@ jobs: - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 with: repository: mattpocock/skills - ref: 49dd158d1076134a641b33efb035946536778336 + ref: 4ae2317ddb22b046e26455e4f9c9b3aaaf62212d path: .upstream/mattpocock-skills - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: diff --git a/README.md b/README.md index 50ce3a7..06f3bdc 100644 --- a/README.md +++ b/README.md @@ -458,23 +458,31 @@ the remote fixture and temporary credentials were removed afterward. ### Reviewed harness versions -The latest stable releases were reviewed on 2026-10-30. Release metadata and -CLI help checks confirmed the existing runtime arguments. This review did not -repeat the live prompts or skill-discovery checks recorded above and in the -[adapter reference](./docs/harness-adapters.md). +The latest stable releases were reviewed on 2026-11-12. Release metadata and +CLI help checks confirmed the runtime arguments for every Harness. OpenCode 2 +removed `run --pure`, so `profiles/harnesses.json` now runs `opencode run` +without it, and the `recall` history reader selects its OpenCode commands by +major version. This review repeated live prompts only for OpenCode; the other +live and skill-discovery checks recorded above and in the +[adapter reference](./docs/harness-adapters.md) keep their original versions. | Harness | Latest stable | Verification | | --- | --- | --- | -| Codex CLI | [0.166.0](https://github.com/openai/codex/releases/tag/rust-v0.166.0) | npm metadata, local version, and `exec --help` | -| Claude Code | [2.1.314](https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md) | npm metadata, local version, and `--help` | -| OpenCode | [1.19.5](https://github.com/anomalyco/opencode/releases/tag/v1.19.5) | npm metadata and release, local 1.19.4 `run --help` | -| pi | [1.4.0](https://github.com/earendil-works/pi/releases/tag/v1.4.0) | npm metadata, local version, and `--help` | -| Antigravity | [1.4.11](https://antigravity-cli-auto-updater-974169037036.us-central1.run.app/manifests/windows_amd64.json) | Official manifest, binary SHA512, downloaded binary version, and `--help` | -| Grok Build | 1.0.70 | Local version, `--help`, and `grok update --check --json` | - -The current pi npm package is `@earendil-works/pi-coding-agent`. The old -`@mariozechner/pi-coding-agent` package stops at 0.73.1. Antigravity's manifest -comes from its [official CLI installer](https://antigravity.google/cli). +| Codex CLI | [0.167.2](https://github.com/openai/codex/releases/tag/rust-v0.167.2) | npm metadata and release, `exec --help` from the npm package | +| Claude Code | [2.1.322](https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md) | npm metadata, local version, and `--help` | +| OpenCode 2 | [2.2.7](https://github.com/anomalyco/opencode/releases/tag/v2.2.7) | npm metadata, local version, `run --help`, live `run --agent plan`, `recall` list and read, and `api skill.list` | +| OpenCode 1 | [1.19.5](https://github.com/anomalyco/opencode/releases/tag/v1.19.5) | npm metadata and release, live `run --agent plan`, and `recall` list and read from the npm package | +| pi | [1.5.2](https://github.com/earendil-works/pi/releases/tag/v1.5.2) | npm metadata and release, `--help` from the npm package | +| Antigravity | [1.5.6](https://antigravity-cli-auto-updater-974169037036.us-central1.run.app/manifests/windows_amd64.json) | Official manifest, binary SHA512, downloaded binary version, and `--help` | +| Grok Build | 1.0.77 | `grok update --check --json`; `--help` from the local 1.0.70 | + +OpenCode 2 is published as `@opencode/cli` and is the install the +[OpenCode documentation](https://opencode.ai/docs) gives. The `opencode-ai` +package and the release GitHub marks as latest remain on the 1.19 line, so both +majors are supported. The current pi npm package is +`@earendil-works/pi-coding-agent`. The old `@mariozechner/pi-coding-agent` +package stops at 0.73.1. Antigravity's manifest comes from its +[official CLI installer](https://antigravity.google/cli). ## Recover recent context diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 86f4471..9274cfa 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -100,7 +100,7 @@ SOFTWARE. The following skills are adapted from [`mattpocock/skills`](https://github.com/mattpocock/skills), pinned during the port at commit -[`49dd158d1076134a641b33efb035946536778336`](https://github.com/mattpocock/skills/commit/49dd158d1076134a641b33efb035946536778336): +[`4ae2317ddb22b046e26455e4f9c9b3aaaf62212d`](https://github.com/mattpocock/skills/commit/4ae2317ddb22b046e26455e4f9c9b3aaaf62212d): - `writing-for-agents` - `codebase-design` diff --git a/docs/harness-adapters.md b/docs/harness-adapters.md index 6ce7c23..e0de421 100644 --- a/docs/harness-adapters.md +++ b/docs/harness-adapters.md @@ -3,11 +3,12 @@ This page records the vendor rules that shape mstack's adapters. The links point to the official documentation used for each entry. -On 2026-10-30, release metadata and CLI help were checked against the -[reviewed harness versions](../README.md#reviewed-harness-versions). The runtime -arguments in `profiles/harnesses.json` remain supported. That review did not -repeat live prompts or skill discovery. The behavior observations below keep -their original tested versions. +On 2026-11-12, release metadata and CLI help were checked against the +[reviewed harness versions](../README.md#reviewed-harness-versions). OpenCode 2 +rejects `--pure`, so its runtime arguments dropped that flag; the other runtime +arguments in `profiles/harnesses.json` remain supported. Live prompts and +skill discovery were repeated only for OpenCode. The other behavior +observations below keep their original tested versions. ## Skill locations and frontmatter @@ -76,13 +77,17 @@ the `name` field and a file named `SKILL.md`, which the canonical skills already satisfy, so `adapters/antigravity.json` and `adapters/grok.json` are empty. Grok also scans `~/.claude/skills/`; skills with the same name collapse to one entry. Claude output omits frontmatter `name`; Claude's documented fallback -uses the directory name, so `/meta-mode` keeps its name. OpenCode requires an +uses the directory name, so `/meta-mode` keeps its name. OpenCode 1 requires an explicit name before registration and skips the Claude copy. This behavior was checked with OpenCode 1.19.3 using a named Claude control skill and an -unnamed Claude copy alongside the canonical shared skill. Claude Code 2.1.267 +unnamed Claude copy alongside the canonical shared skill. OpenCode 2 falls back +to the directory name, so it also reads the unnamed Claude copy, but it keys +skills by directory name and loads `.agents` roots after `.claude` roots. With +OpenCode 2.2.7, `opencode api skill.list` returned one entry per mstack skill, +each from `~/.agents/skills/`, while `~/.claude/skills/` held the unnamed copies. +Claude Code 2.1.267 also returned the directory-derived command name during an isolated -stream-JSON initialization without a user prompt or model request. Recheck this adapter -if OpenCode adds a directory-name fallback for external skills. +stream-JSON initialization without a user prompt or model request. pi keeps the first same-name skill and reports a collision for later independent copies. This was observed with pi 0.85.1, and the pi 1.3.0 skills diff --git a/docs/upstream-sync-matt-4ae2317.md b/docs/upstream-sync-matt-4ae2317.md new file mode 100644 index 0000000..cef8780 --- /dev/null +++ b/docs/upstream-sync-matt-4ae2317.md @@ -0,0 +1,30 @@ +# Upstream sync to mattpocock/skills 4ae2317 + +The mattpocock/skills pin moves from `49dd158` to +[`4ae2317`](https://github.com/mattpocock/skills/commit/4ae2317ddb22b046e26455e4f9c9b3aaaf62212d). +One commit between the pins touches the three adapted skills: +[`7739eae`](https://github.com/mattpocock/skills/commit/7739eaed7f8f3915437a8def198a6d0d7206dbc7), +"Start subagents with a fresh context window, not a fork". + +## Ported + +- `codebase-design` starts each design-alternative subagent with a fresh + context window instead of a fork of the current conversation. Upstream notes + that some Harnesses, Codex among them, fork by default, and a fork carries the + parent context the subagent is meant to escape. + +## Not ported + +- The same sentence in upstream skills that mstack does not adapt + (`implement-spec`, `code-review`, `improve-codebase-architecture`, `wayfinder`, + `research`, `grilling`, `chief-of-staff`). +- Upstream's repository rule in `CLAUDE.md`. mstack's subagent briefs live in + pstack-derived skills, which follow pstack rather than this upstream. + +`diagnosing-bugs` and `writing-for-agents` have no upstream change between the +pins. + +## Verification + +`skill-baseline --check` passes against both pinned sources. The repository +tests do not show that a model follows the changed instruction. diff --git a/profiles/harnesses.json b/profiles/harnesses.json index 430bb07..dc9fa0a 100644 --- a/profiles/harnesses.json +++ b/profiles/harnesses.json @@ -33,7 +33,7 @@ "suffix": ["skills"], "runtime": { "command": "opencode", - "headlessArgs": ["run", "--pure"], + "headlessArgs": ["run"], "modelArgs": ["--model", "{model}"], "readOnlyArgs": ["--agent", "plan"] } diff --git a/profiles/skill-manifest.json b/profiles/skill-manifest.json index c5348e9..a023585 100644 --- a/profiles/skill-manifest.json +++ b/profiles/skill-manifest.json @@ -2,7 +2,7 @@ "version": 1, "sources": { "pstack": "4dd1d05818d19792e7cd6e818f1667f95f7e1dbe", - "mattpocock/skills": "49dd158d1076134a641b33efb035946536778336" + "mattpocock/skills": "4ae2317ddb22b046e26455e4f9c9b3aaaf62212d" }, "files": { "skills/architect/references/design-red-flags.md": { @@ -69,8 +69,8 @@ "skills/codebase-design/references/design-alternatives.md": { "upstream": "mattpocock/skills", "source": "skills/engineering/codebase-design/DESIGN-IT-TWICE.md", - "sourceDigest": "52b52e75655a27aea59c6966b410bac82448968ad46ef1ebb9f07c274e3ae010", - "targetDigest": "fe64a751fc7e1a5399126886b1c0832b484364cfeaa7929d1c8e77b6a49eb208", + "sourceDigest": "c222ab5a82e0405672eb9062d62496139a08828e45d07fc5f18e4952c10fac1a", + "targetDigest": "ff896de577b462ff28a3152e154788e1a8d922426f951444ec1b0dd5723b1f3b", "reason": "The adapted design comparison workflow lives under references." }, "skills/codebase-design/SKILL.md": { @@ -536,7 +536,7 @@ "skills/recall/references/history-sources.md": { "upstream": null, "reason": "Local portability guidance or implementation; review alongside its owning skill.", - "targetDigest": "181e7a56f8ef5514a1cae4841366bf62c708fd44be8bcee3c129c50182e0b3bc" + "targetDigest": "b95748234ef2196269ffba0638a439a6fee225420eca23da57923f04a3546922" }, "skills/recall/references/hosts/t3code.md": { "upstream": null, @@ -546,7 +546,7 @@ "skills/recall/scripts/history.mjs": { "upstream": null, "reason": "Local portability guidance or implementation; review alongside its owning skill.", - "targetDigest": "fe3284380308ecd0cf232072c7bc8e59c0f56ffecc42851d1fddb2c382ab2683" + "targetDigest": "c3fb58b7816bb58acb5c134ebeaa2fe40a4716b936977221c8f2b42910c7da47" }, "skills/recall/SKILL.md": { "upstream": "pstack", diff --git a/profiles/upstreams.json b/profiles/upstreams.json index 7a82875..ef28e12 100644 --- a/profiles/upstreams.json +++ b/profiles/upstreams.json @@ -200,6 +200,6 @@ }, "mattpocock/skills": { "repository": "https://github.com/mattpocock/skills", - "commit": "49dd158d1076134a641b33efb035946536778336" + "commit": "4ae2317ddb22b046e26455e4f9c9b3aaaf62212d" } } diff --git a/scripts/context.test.mjs b/scripts/context.test.mjs index f402f0c..ce8ccf2 100644 --- a/scripts/context.test.mjs +++ b/scripts/context.test.mjs @@ -219,7 +219,7 @@ test("run-role resolves a configured model override", () => { assert.equal(result.status, 0, result.stderr); const plan = JSON.parse(result.stdout); assert.equal(plan.model, "remote-model"); - assert.deepEqual(plan.args, ["run", "--pure", "--model", "remote-model", "inspect the repository"]); + assert.deepEqual(plan.args, ["run", "--model", "remote-model", "inspect the repository"]); } finally { rmSync(root, { recursive: true, force: true }); } diff --git a/scripts/environment.test.mjs b/scripts/environment.test.mjs index bd4ba96..0a383d3 100644 --- a/scripts/environment.test.mjs +++ b/scripts/environment.test.mjs @@ -133,7 +133,7 @@ test("OpenCode read-only plans select the built-in plan agent", () => { ], { cwd: resolve("."), encoding: "utf8" }); assert.equal(result.status, 0, result.stderr); const plan = JSON.parse(result.stdout); - assert.deepEqual(plan.args, ["run", "--pure", "inspect the repository", "--agent", "plan"]); + assert.deepEqual(plan.args, ["run", "inspect the repository", "--agent", "plan"]); }); test("SSH invocation rejects an unsafe host before spawning", () => { diff --git a/scripts/history.test.mjs b/scripts/history.test.mjs index 4ac0d1a..2465803 100644 --- a/scripts/history.test.mjs +++ b/scripts/history.test.mjs @@ -120,7 +120,7 @@ test("pi default storage uses only the requested workspace slug and keeps branch assert.deepEqual(result.warnings, []); }); -test("OpenCode filters list metadata before exporting one sanitized session", async (t) => { +test("OpenCode 1 filters list metadata before exporting one sanitized session", async (t) => { const { root, workspace } = fixture(t); const calls = []; const runOpenCode = (args, cwd) => { @@ -132,7 +132,7 @@ test("OpenCode filters list metadata before exporting one sanitized session", as ]; return { info: { id: "wanted", directory: "[redacted:session-directory:wanted]" }, messages: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Sanitized answer" }, { type: "tool", text: "Do not emit" }] }] }; }; - const options = { harness: "opencode", workspace, runOpenCode, env: {} }; + const options = { harness: "opencode", workspace, runOpenCode, openCodeMajor: () => 1, env: {} }; const listing = await history({ ...options, command: "list" }); assert.deepEqual(listing.sessions.map((session) => session.id), ["wanted"]); assert.equal(calls.length, 1); @@ -148,6 +148,42 @@ test("OpenCode filters list metadata before exporting one sanitized session", as } }); +test("OpenCode 2 uses session export and reads its message schema", async (t) => { + const { root, workspace } = fixture(t); + const calls = []; + const runOpenCode = (args, cwd) => { + calls.push({ args, cwd }); + if (args[1] === "list") return [ + { id: "other", directory: join(root, "other"), updated: Date.now() }, + { id: "wanted", directory: workspace, updated: Date.now() }, + ]; + return { info: { id: "wanted", location: { directory: "/[redacted:session-directory:wanted]" } }, messages: [ + { type: "user", text: "Sanitized prompt" }, + { type: "assistant", content: [{ type: "reasoning", text: "Do not emit" }, { type: "text", text: "Sanitized answer" }] }, + { type: "compaction", status: "completed", summary: "Earlier work" }, + { type: "shell", command: "Do not emit" }, + ] }; + }; + const options = { harness: "opencode", workspace, runOpenCode, openCodeMajor: () => 2, env: {} }; + const listing = await history({ ...options, command: "list" }); + assert.deepEqual(calls.at(-1).args, ["session", "list", "--format", "json"]); + assert.deepEqual(listing.sessions.map((session) => session.id), ["wanted"]); + const reading = await history({ ...options, command: "read", session: "wanted" }); + assert.deepEqual(calls.at(-1), { args: ["session", "export", "wanted", "--sanitize"], cwd: workspace }); + assert.deepEqual(reading.messages, [ + { role: "user", text: "Sanitized prompt" }, + { role: "assistant", text: "Sanitized answer" }, + { role: "summary", text: "Earlier work" }, + ]); + await history({ ...options, command: "read", session: "wanted", localText: true }); + assert.deepEqual(calls.at(-1).args, ["session", "export", "wanted"]); + for (const directory of [join(root, "other"), "/[redacted:session-directory:other]"]) { + const wrongExport = (args, cwd) => args[1] === "list" ? runOpenCode(args, cwd) + : { info: { id: "wanted", location: { directory } }, messages: [] }; + await assert.rejects(history({ ...options, runOpenCode: wrongExport, command: "read", session: "wanted" }), /does not match/); + } +}); + test("missing stores and malformed records produce controlled diagnostics without raw content", async (t) => { const { root, workspace } = fixture(t); const options = { harness: "codex", workspace, root: join(root, "missing"), command: "list", env: {} }; @@ -170,7 +206,7 @@ test("OpenCode warns when sanitizer removes text and requires an explicit local- const runOpenCode = (args) => args[0] === "session" ? [{ id: "session", directory: workspace, updated: Date.now() }] : { info: { id: "session", directory: args.includes("--sanitize") ? "[redacted:session-directory:session]" : workspace }, messages: [{ info: { role: "user" }, parts: [{ type: "text", text: args.includes("--sanitize") ? "[redacted:text:part]" : "Local fixture prompt" }] }] }; - const options = { harness: "opencode", workspace, runOpenCode, command: "read", session: "session" }; + const options = { harness: "opencode", workspace, runOpenCode, openCodeMajor: () => 1, command: "read", session: "session" }; const sanitized = await history(options); assert.equal(sanitized.sanitized, true); assert.match(sanitized.warnings[0], /redacted message text/); diff --git a/skills/codebase-design/references/design-alternatives.md b/skills/codebase-design/references/design-alternatives.md index 0dcdd6e..28e3803 100644 --- a/skills/codebase-design/references/design-alternatives.md +++ b/skills/codebase-design/references/design-alternatives.md @@ -18,7 +18,7 @@ Show this to the user, then immediately proceed to Step 2. The user reads and th ### 2. Spawn sub-agents -Use the active Harness's native delegation to assign three independent candidates, within its available concurrency. Each must produce a **radically different** interface for the deepened module. If delegation is unavailable, run separate design passes with these same briefs and disclose that they share one agent's context. Keep the passes read-only; design alternatives do not need to edit a shared checkout. +Use the active Harness's native delegation to assign three independent candidates, within its available concurrency. Start each one with a fresh context window rather than a fork of this conversation; some Harnesses fork by default, and a fork carries over the context the candidate exists to escape. Each must produce a **radically different** interface for the deepened module. If delegation is unavailable, run separate design passes with these same briefs and disclose that they share one agent's context. Keep the passes read-only; design alternatives do not need to edit a shared checkout. Prompt each sub-agent with a separate technical brief (file paths, coupling details, dependency category from [Deepening](deepening.md), what sits behind the seam). The brief is independent of the user-facing problem-space explanation in Step 1. Give each agent a different design constraint: diff --git a/skills/recall/references/history-sources.md b/skills/recall/references/history-sources.md index 8e0bcf3..d57ad06 100644 --- a/skills/recall/references/history-sources.md +++ b/skills/recall/references/history-sources.md @@ -80,11 +80,16 @@ the helper does not fall back to scanning all project directories. ## OpenCode -Prefer the stable CLI surface: +Prefer the stable CLI surface. The helper runs `opencode --version` first, +because OpenCode 2 removed `--pure` and moved `export` under `session`: ```bash +# OpenCode 1 (npm package opencode-ai) opencode session list --format json --pure opencode export --sanitize --pure +# OpenCode 2 (npm package @opencode/cli) +opencode session list --format json +opencode session export --sanitize ``` Use `--sanitize` whenever exported data will leave the local machine. Do not @@ -92,9 +97,14 @@ depend on OpenCode's internal database layout unless the CLI cannot provide the required authorized record. The helper runs these commands with the requested workspace as its working directory. It filters list entries by exact `directory` and excludes `parentID` sessions before exporting the selected ID, then verifies -the export's `info.id` and `info.directory`. Sanitized exports may replace the -directory with `[redacted:session-directory:]`; the helper accepts -only the marker for the already selected session. It inherits OpenCode's configured +the export's `info.id` and directory: `info.directory` in OpenCode 1 and +`info.location.directory` in OpenCode 2. Sanitized exports may replace the +directory with `[redacted:session-directory:]`, which OpenCode 2 +prefixes with `/`; the helper accepts only the marker for the already selected +session. OpenCode 2 exports user prompts as `text`, assistant replies as +`content` parts, and compaction summaries as `summary`; the helper reads those +and skips tool, shell, and reasoning entries. OpenCode 2 lists only top-level +sessions, at most 100 by default. It inherits OpenCode's configured environment, including `XDG_DATA_HOME`; `--root` is not supported. A missing CLI or unsupported JSON schema is an error, not an empty-history result. diff --git a/skills/recall/scripts/history.mjs b/skills/recall/scripts/history.mjs index d6b5e86..143af04 100644 --- a/skills/recall/scripts/history.mjs +++ b/skills/recall/scripts/history.mjs @@ -85,7 +85,7 @@ async function metadata(path, harness) { } } -function openCode(args, cwd, env) { +function openCodeOutput(args, cwd, env) { let command = "opencode"; let commandArgs = args; if (process.platform === "win32") { @@ -96,17 +96,50 @@ function openCode(args, cwd, env) { } const result = spawnSync(command, commandArgs, { cwd, env, encoding: "utf8", maxBuffer: MAX_FILE_BYTES, timeout: 30_000, windowsHide: true }); if (result.error || result.status !== 0) throw new Error("OpenCode history command failed; check CLI availability and its configured data directory."); + return result.stdout; +} + +function openCode(args, cwd, env) { try { - return JSON.parse(result.stdout); - } catch { - throw new Error("OpenCode returned unsupported JSON output."); + return JSON.parse(openCodeOutput(args, cwd, env)); + } catch (error) { + if (error instanceof SyntaxError) throw new Error("OpenCode returned unsupported JSON output."); + throw error; } } +function openCodeMajor(cwd, env) { + const match = /(\d+)\.\d+\.\d+/.exec(openCodeOutput(["--version"], cwd, env)); + if (!match) throw new Error("OpenCode returned an unsupported version string."); + return Number(match[1]); +} + +// OpenCode 2 removed `--pure` and the top-level `export` command, and changed the export schema. +// Its parser treats an unknown first word as a directory and opens the TUI, so the major version picks the commands. +async function openCodeCommands(options) { + options.openCodeMajorVersion ??= await options.openCodeMajor(options.workspace, options.env); + const legacy = options.openCodeMajorVersion < 2; + return { + list: ["session", "list", "--format", "json", ...(legacy ? ["--pure"] : [])], + export: (id, sanitize) => legacy + ? ["export", id, ...(sanitize ? ["--sanitize"] : []), "--pure"] + : ["session", "export", id, ...(sanitize ? ["--sanitize"] : [])], + legacy, + }; +} + +function openCodeMessages(data, legacy) { + if (legacy) return data.messages.map((row) => ({ role: row.info?.role, text: textContent(row.parts) })); + return data.messages.map((row) => row?.type === "user" ? { role: "user", text: typeof row.text === "string" ? row.text : "" } + : row?.type === "assistant" ? { role: "assistant", text: textContent(row.content) } + : row?.type === "compaction" ? { role: "summary", text: typeof row.summary === "string" ? row.summary : "" } + : {}); +} + async function discover(options, warnings) { const { harness, workspace } = options; if (harness === "opencode") { - const rows = await options.runOpenCode(["session", "list", "--format", "json", "--pure"], workspace, options.env); + const rows = await options.runOpenCode((await openCodeCommands(options)).list, workspace, options.env); if (!Array.isArray(rows)) throw new Error("OpenCode session list must return a JSON array."); return rows.filter((row) => row && sameWorkspace(row.directory, workspace)).map((row) => ({ id: row.id, workspace: row.directory, updated: row.updated ?? row.time?.updated, child: Boolean(row.parentID), @@ -162,15 +195,16 @@ function conversationBranch(rows, harness, leaf) { async function readMessages(session, options, warnings) { if (options.harness === "opencode") { - const args = ["export", session.id, ...(options.localText ? [] : ["--sanitize"]), "--pure"]; - const data = await options.runOpenCode(args, options.workspace, options.env); - const directory = data?.info?.directory; - // Sanitized exports replace the directory after list metadata established the workspace. - const matchesDirectory = sameWorkspace(directory, options.workspace) || directory === `[redacted:session-directory:${session.id}]`; + const commands = await openCodeCommands(options); + const data = await options.runOpenCode(commands.export(session.id, !options.localText), options.workspace, options.env); + const directory = commands.legacy ? data?.info?.directory : data?.info?.location?.directory; + // Sanitized exports replace the directory after list metadata established the workspace. OpenCode 2 prefixes the marker with "/". + const marker = `[redacted:session-directory:${session.id}]`; + const matchesDirectory = sameWorkspace(directory, options.workspace) || directory === marker || directory === `/${marker}`; if (data?.info?.id !== session.id || !matchesDirectory || !Array.isArray(data.messages)) { throw new Error("OpenCode export does not match the selected workspace and session."); } - const messages = data.messages.map((row) => ({ role: row.info?.role, text: textContent(row.parts) })); + const messages = openCodeMessages(data, commands.legacy); if (!options.localText && messages.some((message) => /\[redacted:text:/.test(message.text))) { warnings.push("OpenCode sanitized export redacted message text. Use --local-text for private local recovery of the selected session."); } @@ -206,7 +240,7 @@ async function readMessages(session, options, warnings) { } export async function history(input) { - const options = { env: process.env, runOpenCode: openCode, limit: 20, maxChars: 12_000, ...input }; + const options = { env: process.env, runOpenCode: openCode, openCodeMajor, limit: 20, maxChars: 12_000, ...input }; if (!["list", "read"].includes(options.command)) throw new Error("Command must be list or read."); if (!HARNESSES.includes(options.harness)) throw new Error(`--harness must be one of ${HARNESSES.join(", ")}.`); if (typeof options.workspace !== "string" || !options.workspace.trim()) throw new Error("--workspace is required.");