From 05f5ba45d879572fc420096be0cae45b2fa31050 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Wed, 1 Jul 2026 19:50:56 -0300 Subject: [PATCH 1/4] fix(session-context): select top-level opencode sessions, add --root flag opencode stores sub-agent runs as child sessions (parent_id set) that share the parent's directory, so recency-based selection with --skip returned sub-agent transcripts instead of the main development session. opencode skills also run inline in the calling session (they do not create their own session row), so the shipped '--skip 1' guidance skipped the very session it was meant to read. - Only top-level sessions (parent_id IS NULL) participate in recency selection; --session-id can still read any session, incl. children - New --root flag resolves a --session-id child session to its top-level ancestor by walking the parent_id chain (cycle-guarded) - Skip-overflow error and available list now reflect top-level sessions - Docs updated (en/nb/pt-br): accurate directory-matching semantics, new --root row, reworded --skip Verified end-to-end against a real opencode.db where '--skip 1' returned a '@general subagent' child session instead of the parent development session. Claude-Session: https://claude.ai/code/session_011LcL4xUi6VWYRyz7LpiC6L Signed-off-by: Rhuan Barreto --- .../agent-memory/archgate-developer/MEMORY.md | 3 + docs/public/llms-full.txt | 13 +- .../docs/nb/reference/cli/session-context.mdx | 13 +- .../pt-br/reference/cli/session-context.mdx | 13 +- .../docs/reference/cli/session-context.mdx | 13 +- src/commands/session-context/opencode.ts | 7 +- src/helpers/session-context-opencode.ts | 62 +++- .../commands/session-context/opencode.test.ts | 1 + .../helpers/session-context-opencode.test.ts | 304 +++++++++--------- 9 files changed, 243 insertions(+), 186 deletions(-) diff --git a/.claude/agent-memory/archgate-developer/MEMORY.md b/.claude/agent-memory/archgate-developer/MEMORY.md index 633a4d91..ff0a4315 100644 --- a/.claude/agent-memory/archgate-developer/MEMORY.md +++ b/.claude/agent-memory/archgate-developer/MEMORY.md @@ -43,6 +43,9 @@ Non-enforceable lessons — environment/CI/platform quirks no static rule can re - **`oxfmt` formats markdown too — run `bun run format` after editing any `.md`, including ADRs** — `format:check` (part of `bun run validate` and the CI "Lint, Test & Check" job) runs `oxfmt --check .` over ALL files, not just `.ts`. It normalizes markdown (e.g., emphasis `*word*` → `_word_`). The `adr-author` skill writes ADR markdown but does NOT auto-format it, so ADR edits frequently fail CI `format:check` even when the TS changes are clean. Always run `bun run format` before committing ADR/markdown edits. Tripped CI on PR #372 (ARCH-005 edit). - **YAML double-quoted strings require escaped backslashes for Windows paths in tests** — YAML interprets `\` as an escape character inside double-quoted strings. Writing `cwd: "E:\project"` silently corrupts the parsed value because `\p` is not a valid escape sequence. Fix: use `JSON.stringify(path)` to produce properly escaped YAML values (e.g., `cwd: ${JSON.stringify(cwd)}`). JSON and YAML double-quoted strings share the same escape syntax. Encountered in Copilot CLI session-context tests (`workspace.yaml` with Windows paths). - **`Bun.Glob.match()` triggers oxlint `prefer-regexp-test`** — `Bun.Glob.match()` returns a boolean (not a RegExp), but oxlint can't tell. Suppress with `// oxlint-disable-next-line prefer-regexp-test -- Bun.Glob.match() returns boolean, not RegExp`. +- **Live `opencode.db` can't be opened `readonly: true` while opencode runs** — `new Database(path, { readonly: true })` fails with `SQLITE_CANTOPEN` (errno 14) on the live WAL-mode DB (`~/.local/share/opencode/opencode.db`). To inspect real data, copy `opencode.db` + `.db-wal` + `.db-shm` to a temp dir and open the copy. Data model facts load-bearing for `session-context opencode`: sub-agent runs are child sessions (`parent_id` set) sharing the parent's `directory`, and opencode skills run INLINE in the calling session (no own session row) — recency selection must filter `parent_id IS NULL` (fixed 2026-07-01; `--root` resolves a `--session-id` child to its top-level ancestor). +- **opencode plugin SKILL.md references CLI flags — sync contracts across repos** — `/e/archgate/plugins/opencode/.../archgate-lessons-learned/SKILL.md` now instructs `archgate session-context opencode --root` (was `--skip 1`, which read sub-agent transcripts). When changing session-context CLI semantics, update the plugins-repo skill in the same effort AND sequence releases: CLI ships the flag first, plugin follows — a skill referencing a flag the installed CLI lacks dies with "unknown option". Don't hand-edit the installed copy under `~/.config/opencode/skills/` before the CLI release. +- **Verify haiku reviewer findings against the cited ADR's text before blocking** — A haiku review sub-agent reported "await on a synchronous helper" as an ARCH-012 violation; ARCH-012 only mandates try-catch boundaries/exit codes and its automated rules passed. Re-read the cited ADR's Decision/Do's before accepting a FAIL; overrule misattributed style nits but still surface them as non-ADR notes. - **oxlint `no-negated-condition`** — Always write ternaries with the positive condition first: `x === null ? A : B` not `x !== null ? B : A`. Applies to both `if/else` blocks and ternary expressions. - **oxlint `no-unused-vars` on catch parameters** — Use bare `catch { }` (no parameter) when the caught error is not used. `catch (err) { }` with unused `err` triggers the rule. - **oxlint `no-await-in-loop`** — Sequential `await` inside a `for` loop is flagged (warning). When the sequential order is intentional (e.g., build steps with per-step output), suppress with `// oxlint-disable-next-line no-await-in-loop -- `. diff --git a/docs/public/llms-full.txt b/docs/public/llms-full.txt index 38f93ebb..55ba0018 100644 --- a/docs/public/llms-full.txt +++ b/docs/public/llms-full.txt @@ -4388,17 +4388,18 @@ archgate session-context cursor [options] ### archgate session-context opencode -Read the opencode session transcript for the project. Sessions are matched by the project `path` field in session metadata. +Read the opencode session transcript for the project. Sessions are matched by comparing the session `directory` field to the project root. opencode records sub-agent runs as child sessions that share the parent's directory — these are excluded from recency selection, so the most recent top-level session is always the main development session. Use `--session-id` to read a specific session, including a sub-agent child session. ```bash archgate session-context opencode [options] ``` -| Option | Description | -| ------------------- | ------------------------------------------------------------------ | -| `--max-entries ` | Maximum entries to return (default: 200) | -| `--skip ` | Skip the N most recent sessions (useful when running as sub-agent) | -| `--session-id ` | Specific session ID to read | +| Option | Description | +| ------------------- | ------------------------------------------------------------------------------------------------------ | +| `--max-entries ` | Maximum entries to return (default: 200) | +| `--skip ` | Skip the N most recent top-level sessions (sub-agent sessions always excluded) | +| `--session-id ` | Specific session ID to read | +| `--root` | Resolve to the top-level (root) session — with `--session-id`, walks up from a sub-agent child session | ## Examples diff --git a/docs/src/content/docs/nb/reference/cli/session-context.mdx b/docs/src/content/docs/nb/reference/cli/session-context.mdx index 95e9b72d..5e65b64f 100644 --- a/docs/src/content/docs/nb/reference/cli/session-context.mdx +++ b/docs/src/content/docs/nb/reference/cli/session-context.mdx @@ -54,17 +54,18 @@ archgate session-context cursor [options] ### archgate session-context opencode -Les opencode-sesjonsloggen for prosjektet. Sesjoner matches etter prosjektets `path`-felt i sesjonsmetadataene. +Les opencode-sesjonsloggen for prosjektet. Sesjoner matches ved å sammenligne sesjonens `directory`-felt med prosjektroten. opencode lagrer underagent-kjøringer som barnesesjoner som deler foreldrenes katalog — disse ekskluderes fra utvalget etter nylighet, slik at den nyeste toppnivåsesjonen alltid er hovedutviklingssesjonen. Bruk `--session-id` for å lese en bestemt sesjon, inkludert en underagent-barnesesjon. ```bash archgate session-context opencode [options] ``` -| Valg | Beskrivelse | -| ------------------- | ------------------------------------------------------------------- | -| `--max-entries ` | Maksimalt antall oppføringer å returnere (standard: 200) | -| `--skip ` | Hopp over de N nyeste sesjonene (nyttig ved kjøring som underagent) | -| `--session-id ` | Spesifikk sesjons-ID å lese | +| Valg | Beskrivelse | +| ------------------- | ----------------------------------------------------------------------------------------------------- | +| `--max-entries ` | Maksimalt antall oppføringer å returnere (standard: 200) | +| `--skip ` | Hopp over de N nyeste toppnivåsesjonene (underagentsesjoner ekskluderes alltid) | +| `--session-id ` | Spesifikk sesjons-ID å lese | +| `--root` | Løs opp til toppnivåsesjonen (rot) — med `--session-id` gås det oppover fra en underagent-barnesesjon | ## Eksempler diff --git a/docs/src/content/docs/pt-br/reference/cli/session-context.mdx b/docs/src/content/docs/pt-br/reference/cli/session-context.mdx index 23afb3c0..0c386cff 100644 --- a/docs/src/content/docs/pt-br/reference/cli/session-context.mdx +++ b/docs/src/content/docs/pt-br/reference/cli/session-context.mdx @@ -54,17 +54,18 @@ archgate session-context cursor [options] ### archgate session-context opencode -Lê a transcrição de sessão do opencode para o projeto. Sessões são correspondidas pelo campo `path` do projeto nos metadados da sessão. +Lê a transcrição de sessão do opencode para o projeto. Sessões são correspondidas comparando o campo `directory` da sessão com a raiz do projeto. O opencode registra execuções de sub-agentes como sessões filhas que compartilham o diretório da sessão pai — elas são excluídas da seleção por recência, de modo que a sessão de nível superior mais recente é sempre a sessão principal de desenvolvimento. Use `--session-id` para ler uma sessão específica, incluindo uma sessão filha de sub-agente. ```bash archgate session-context opencode [options] ``` -| Opção | Descrição | -| ------------------- | ------------------------------------------------------------------- | -| `--max-entries ` | Máximo de entradas a retornar (padrão: 200) | -| `--skip ` | Pular as N sessões mais recentes (útil ao executar como sub-agente) | -| `--session-id ` | ID específico da sessão a ser lida | +| Opção | Descrição | +| ------------------- | -------------------------------------------------------------------------------------------------------------------- | +| `--max-entries ` | Máximo de entradas a retornar (padrão: 200) | +| `--skip ` | Pular as N sessões de nível superior mais recentes (sessões de sub-agente são excluídas) | +| `--session-id ` | ID específico da sessão a ser lida | +| `--root` | Resolve para a sessão de nível superior (raiz) — com `--session-id`, sobe a partir de uma sessão filha de sub-agente | ## Exemplos diff --git a/docs/src/content/docs/reference/cli/session-context.mdx b/docs/src/content/docs/reference/cli/session-context.mdx index 624a9d6a..0be319de 100644 --- a/docs/src/content/docs/reference/cli/session-context.mdx +++ b/docs/src/content/docs/reference/cli/session-context.mdx @@ -54,17 +54,18 @@ archgate session-context cursor [options] ### archgate session-context opencode -Read the opencode session transcript for the project. Sessions are matched by the project `path` field in session metadata. +Read the opencode session transcript for the project. Sessions are matched by comparing the session `directory` field to the project root. opencode records sub-agent runs as child sessions that share the parent's directory — these are excluded from recency selection, so the most recent top-level session is always the main development session. Use `--session-id` to read a specific session, including a sub-agent child session. ```bash archgate session-context opencode [options] ``` -| Option | Description | -| ------------------- | ------------------------------------------------------------------ | -| `--max-entries ` | Maximum entries to return (default: 200) | -| `--skip ` | Skip the N most recent sessions (useful when running as sub-agent) | -| `--session-id ` | Specific session ID to read | +| Option | Description | +| ------------------- | ------------------------------------------------------------------------------------------------------ | +| `--max-entries ` | Maximum entries to return (default: 200) | +| `--skip ` | Skip the N most recent top-level sessions (sub-agent sessions always excluded) | +| `--session-id ` | Specific session ID to read | +| `--root` | Resolve to the top-level (root) session — with `--session-id`, walks up from a sub-agent child session | ## Examples diff --git a/src/commands/session-context/opencode.ts b/src/commands/session-context/opencode.ts index 7e1a7d18..d530c767 100644 --- a/src/commands/session-context/opencode.ts +++ b/src/commands/session-context/opencode.ts @@ -16,7 +16,7 @@ const maxEntriesOption = new Option( const skipOption = new Option( "--skip ", - "skip the N most recent sessions (useful when running as a sub-agent)" + "skip the N most recent top-level sessions (sub-agent sessions are always excluded)" ) .argParser((val) => Math.trunc(Number(val))) .default(0); @@ -28,6 +28,10 @@ export function registerOpencodeSessionContextCommand(parent: Command) { .addOption(maxEntriesOption) .addOption(skipOption) .option("--session-id ", "Specific session ID to read") + .option( + "--root", + "resolve to the top-level (root) session — with --session-id, walks up from a sub-agent child session" + ) .action(async (opts) => { try { const projectRoot = findProjectRoot(); @@ -35,6 +39,7 @@ export function registerOpencodeSessionContextCommand(parent: Command) { maxEntries: opts.maxEntries, skip: opts.skip, sessionId: opts.sessionId, + root: opts.root, }); if (!result.ok) { diff --git a/src/helpers/session-context-opencode.ts b/src/helpers/session-context-opencode.ts index f4311df5..e57573f9 100644 --- a/src/helpers/session-context-opencode.ts +++ b/src/helpers/session-context-opencode.ts @@ -23,6 +23,13 @@ interface OpencodeSessionSummary { interface ReadOpencodeSessionOptions extends ReadSessionOptions { sessionId?: string; + /** + * Resolve to the top-level (root) session. Without `sessionId` this is + * the default behavior already; combined with a `sessionId` that names a + * sub-agent child session, walks the `parent_id` chain up to the + * top-level ancestor. + */ + root?: boolean; } type OpencodeSessionResult = @@ -45,11 +52,20 @@ function normalizePath(p: string): string { * Opencode stores data in a SQLite database at * `$XDG_DATA_HOME/opencode/opencode.db` (default `~/.local/share/opencode/opencode.db`): * - `session` table — session metadata with `directory` for project matching + * and `parent_id` linking sub-agent sessions to their parent * - `message` table — messages with `role` in the `data` JSON column * - `part` table — content parts with `type` and `text` in the `data` JSON column * * Sessions are matched by comparing the `directory` field in session rows * to the provided project root. + * + * Sub-agent runs are stored as child sessions (`parent_id` set) that share + * the parent's `directory`, so recency-based selection (`skip`) only + * considers top-level sessions — otherwise sub-agent transcripts shadow the + * main session. Note that opencode skills run inline in the calling session + * (they do NOT create their own session), so no `skip` is needed to read + * the current development session. An explicit `sessionId` can still read + * any session, including sub-agent children. */ export function readOpencodeSession( projectRoot: string | null, @@ -79,11 +95,12 @@ export function readOpencodeSession( interface SessionRow { id: string; directory: string; + parent_id: string | null; time_updated: number; } const allSessions = db .query( - "SELECT id, directory, time_updated FROM session ORDER BY time_updated DESC" + "SELECT id, directory, parent_id, time_updated FROM session ORDER BY time_updated DESC" ) .all(); @@ -105,17 +122,44 @@ export function readOpencodeSession( }; } - // 3. Select session by ID or most recent (with optional skip) + // 3. Select session by ID or most recent (with optional skip). + // Recency selection only considers top-level sessions: sub-agent runs + // are child sessions (parent_id set) sharing the parent's directory, + // and would otherwise shadow the main session in the skip index. + // An explicit --session-id can read any session, including children. + const topLevel = matching.filter((s) => s.parent_id === null); const skip = options?.skip ?? 0; const target = options?.sessionId ? matching.find((s) => s.id === options.sessionId) - : matching[skip]; + : topLevel[skip]; if (!target) { - const error = options?.sessionId - ? `Session not found: ${options.sessionId}` - : `Only ${String(matching.length)} session(s) available but --skip ${String(skip)} requested`; - return { ok: false, error, available: matching.map((s) => s.id) }; + if (options?.sessionId) { + return { + ok: false, + error: `Session not found: ${options.sessionId}`, + available: matching.map((s) => s.id), + }; + } + return { + ok: false, + error: `Only ${String(topLevel.length)} top-level session(s) available but --skip ${String(skip)} requested`, + available: topLevel.map((s) => s.id), + }; + } + + // 3b. With --root, walk the parent_id chain up to the top-level + // ancestor (relevant when --session-id names a sub-agent child). + let selected = target; + if (options?.root === true) { + const byId = new Map(allSessions.map((s) => [s.id, s])); + const seen = new Set(); + while (selected.parent_id !== null && !seen.has(selected.id)) { + seen.add(selected.id); + const parent = byId.get(selected.parent_id); + if (!parent) break; + selected = parent; + } } // 4. Read messages for the session @@ -127,7 +171,7 @@ export function readOpencodeSession( .query( "SELECT id, json_extract(data, '$.role') as role FROM message WHERE session_id = ? ORDER BY time_created" ) - .all(target.id); + .all(selected.id); if (messages.length === 0) { return { @@ -176,7 +220,7 @@ export function readOpencodeSession( return { ok: true, data: { - sessionId: target.id, + sessionId: selected.id, totalEntries: messages.length, relevantEntries: relevant.length, transcript: trimmed, diff --git a/tests/commands/session-context/opencode.test.ts b/tests/commands/session-context/opencode.test.ts index 42af1992..85e677b5 100644 --- a/tests/commands/session-context/opencode.test.ts +++ b/tests/commands/session-context/opencode.test.ts @@ -184,6 +184,7 @@ describe("opencode action handler", () => { maxEntries: undefined, skip: 0, sessionId: undefined, + root: undefined, }); }); }); diff --git a/tests/helpers/session-context-opencode.test.ts b/tests/helpers/session-context-opencode.test.ts index 705fcb65..9636224b 100644 --- a/tests/helpers/session-context-opencode.test.ts +++ b/tests/helpers/session-context-opencode.test.ts @@ -88,12 +88,13 @@ describe("readOpencodeSession", () => { sessionId: string, sessionDir: string, messages?: Array<{ id: string; role: string; content: string }>, - timeUpdated?: number + timeUpdated?: number, + parentId?: string ): void { const now = timeUpdated ?? Date.now(); db.run( - "INSERT INTO session (id, directory, time_created, time_updated) VALUES (?, ?, ?, ?)", - [sessionId, sessionDir, now, now] + "INSERT INTO session (id, parent_id, directory, time_created, time_updated) VALUES (?, ?, ?, ?, ?)", + [sessionId, parentId ?? null, sessionDir, now, now] ); if (!messages) return; @@ -125,6 +126,53 @@ describe("readOpencodeSession", () => { } } + /** Insert a session under `projectRoot` with a single user message. */ + function makeSimpleSession( + db: Database, + id: string, + content: string, + timeUpdated: number, + parentId?: string + ): void { + makeSession( + db, + id, + projectRoot, + [{ id: `msg_${id}`, role: "user", content }], + timeUpdated, + parentId + ); + } + + /** Insert a raw message row with the given role. */ + function insertMessage( + db: Database, + id: string, + sessionId: string, + t: number, + role: string + ): void { + db.run( + "INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?)", + [id, sessionId, t, t, JSON.stringify({ role })] + ); + } + + /** Insert a raw part row with arbitrary JSON data. */ + function insertPart( + db: Database, + id: string, + messageId: string, + sessionId: string, + t: number, + data: Record + ): void { + db.run( + "INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?, ?)", + [id, messageId, sessionId, t, t, JSON.stringify(data)] + ); + } + test("returns data for most recent session matching project", async () => { const db = createDb(); const sessionId = `ses_${uniqueId}_1`; @@ -156,20 +204,8 @@ describe("readOpencodeSession", () => { const sessionId1 = `ses_${uniqueId}_first`; const sessionId2 = `ses_${uniqueId}_second`; - makeSession( - db, - sessionId1, - projectRoot, - [{ id: "msg_001", role: "user", content: "first session" }], - 1000 - ); - makeSession( - db, - sessionId2, - projectRoot, - [{ id: "msg_002", role: "user", content: "second session" }], - 2000 - ); + makeSimpleSession(db, sessionId1, "first session", 1000); + makeSimpleSession(db, sessionId2, "second session", 2000); db.close(); const result = await readOpencodeSession(projectRoot, { @@ -185,9 +221,7 @@ describe("readOpencodeSession", () => { test("returns error when sessionId not found (with available list)", async () => { const db = createDb(); const sessionId = `ses_${uniqueId}_real`; - makeSession(db, sessionId, projectRoot, [ - { id: "msg_001", role: "user", content: "real" }, - ]); + makeSimpleSession(db, sessionId, "real", 1000); db.close(); const result = await readOpencodeSession(projectRoot, { @@ -206,36 +240,17 @@ describe("readOpencodeSession", () => { // Insert messages with various roles — system and tool should be filtered out const now = Date.now(); - db.run( - "INSERT INTO session (id, directory, time_created, time_updated) VALUES (?, ?, ?, ?)", - [sessionId, projectRoot, now, now] - ); + makeSession(db, sessionId, projectRoot, undefined, now); const roles = ["system", "tool", "user", "assistant"]; const contents = ["system msg", "tool output", "visible", "also visible"]; for (let i = 0; i < roles.length; i++) { const msgId = `msg_${String(i).padStart(3, "0")}`; - db.run( - "INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?)", - [ - msgId, - sessionId, - now + i + 1, - now + i + 1, - JSON.stringify({ role: roles[i] }), - ] - ); - db.run( - "INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?, ?)", - [ - `prt_${msgId}`, - msgId, - sessionId, - now + i + 1, - now + i + 1, - JSON.stringify({ type: "text", text: contents[i] }), - ] - ); + insertMessage(db, msgId, sessionId, now + i + 1, roles[i] ?? ""); + insertPart(db, `prt_${msgId}`, msgId, sessionId, now + i + 1, { + type: "text", + text: contents[i], + }); } db.close(); @@ -338,46 +353,19 @@ describe("readOpencodeSession", () => { const db = createDb(); const sessionId = `ses_${uniqueId}_synthetic`; const now = Date.now(); - - db.run( - "INSERT INTO session (id, directory, time_created, time_updated) VALUES (?, ?, ?, ?)", - [sessionId, projectRoot, now, now] - ); - - // User message with a synthetic part and a real part - const msgId = "msg_syn_001"; - db.run( - "INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?)", - [msgId, sessionId, now + 1, now + 1, JSON.stringify({ role: "user" })] - ); - // Synthetic part (editor context) - db.run( - "INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?, ?)", - [ - "prt_syn_001", - msgId, - sessionId, - now + 1, - now + 1, - JSON.stringify({ - type: "text", - text: "Note: The user opened the file", - synthetic: true, - }), - ] - ); - // Real part (user input) - db.run( - "INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?, ?)", - [ - "prt_real_001", - msgId, - sessionId, - now + 2, - now + 2, - JSON.stringify({ type: "text", text: "actual user question" }), - ] - ); + makeSession(db, sessionId, projectRoot, undefined, now); + + // User message with a synthetic part (editor context) and a real part + insertMessage(db, "msg_syn", sessionId, now + 1, "user"); + insertPart(db, "prt_syn", "msg_syn", sessionId, now + 1, { + type: "text", + text: "Note: The user opened the file", + synthetic: true, + }); + insertPart(db, "prt_real", "msg_syn", sessionId, now + 2, { + type: "text", + text: "actual user question", + }); db.close(); const result = await readOpencodeSession(projectRoot); @@ -393,94 +381,106 @@ describe("readOpencodeSession", () => { ); }); - test("skip=1 reads the parent session instead of the sub-agent session", async () => { + test("excludes sub-agent child sessions from recency selection", async () => { const db = createDb(); - makeSession( - db, - "ses_parent", - projectRoot, - [{ id: "msg_p1", role: "user", content: "parent question" }], - 1000 - ); - makeSession( - db, - "ses_subagent", - projectRoot, - [{ id: "msg_s1", role: "user", content: "sub-agent init" }], - 2000 - ); + makeSimpleSession(db, "ses_parent", "parent question", 1000); + // Child session is newer — it must NOT shadow the parent session + makeSimpleSession(db, "ses_sub", "sub-agent init", 2000, "ses_parent"); + db.close(); + + const result = await readOpencodeSession(projectRoot); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.sessionId).toBe("ses_parent"); + expect(result.data.transcript[0]?.contentPreview).toBe("parent question"); + } + }); + + test("sessionId can read a sub-agent child session explicitly", async () => { + const db = createDb(); + makeSimpleSession(db, "ses_parent", "parent question", 1000); + makeSimpleSession(db, "ses_sub", "sub-agent init", 2000, "ses_parent"); + db.close(); + + const result = await readOpencodeSession(projectRoot, { + sessionId: "ses_sub", + }); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.sessionId).toBe("ses_sub"); + expect(result.data.transcript[0]?.contentPreview).toBe("sub-agent init"); + } + }); + + test("skip indexes over top-level sessions only", async () => { + const db = createDb(); + makeSimpleSession(db, "ses_older", "older top-level", 1000); + makeSimpleSession(db, "ses_newer", "newer top-level", 2000); + // Child session between the two — must not consume a skip index + makeSimpleSession(db, "ses_child", "child", 1500, "ses_older"); db.close(); const noSkip = await readOpencodeSession(projectRoot); expect(noSkip.ok).toBe(true); - if (noSkip.ok) expect(noSkip.data.sessionId).toBe("ses_subagent"); + if (noSkip.ok) expect(noSkip.data.sessionId).toBe("ses_newer"); const skipped = await readOpencodeSession(projectRoot, { skip: 1 }); expect(skipped.ok).toBe(true); - if (skipped.ok) expect(skipped.data.sessionId).toBe("ses_parent"); + if (skipped.ok) expect(skipped.data.sessionId).toBe("ses_older"); }); - test("skip beyond available matching sessions returns error", async () => { + test("root resolves to the top-level ancestor session", async () => { const db = createDb(); - makeSession( - db, - "ses_only", - projectRoot, - [{ id: "msg_001", role: "user", content: "only" }], - 1000 - ); + makeSimpleSession(db, "ses_parent", "parent question", 1000); + makeSimpleSession(db, "ses_sub", "sub-agent init", 2000, "ses_parent"); + // Grandchild — root must walk the whole parent_id chain + makeSimpleSession(db, "ses_grand", "grandchild", 3000, "ses_sub"); db.close(); - const result = await readOpencodeSession(projectRoot, { skip: 3 }); + const viaChild = await readOpencodeSession(projectRoot, { + sessionId: "ses_grand", + root: true, + }); + expect(viaChild.ok).toBe(true); + if (viaChild.ok) expect(viaChild.data.sessionId).toBe("ses_parent"); + + const noId = await readOpencodeSession(projectRoot, { root: true }); + expect(noId.ok).toBe(true); + if (noId.ok) expect(noId.data.sessionId).toBe("ses_parent"); + }); + + test("skip beyond available top-level sessions returns error", async () => { + const db = createDb(); + makeSimpleSession(db, "ses_only", "only", 1000); + // Child sessions must not count toward the top-level total + makeSimpleSession(db, "ses_child", "child", 2000, "ses_only"); + db.close(); + + const result = await readOpencodeSession(projectRoot, { skip: 1 }); expect(result.ok).toBe(false); - if (!result.ok) expect(result.error).toContain("--skip 3 requested"); + if (!result.ok) { + expect(result.error).toContain( + "Only 1 top-level session(s) available but --skip 1 requested" + ); + expect(result.available).toEqual(["ses_only"]); + } }); test("includes tool parts as [tool: name]", async () => { const db = createDb(); const sessionId = `ses_${uniqueId}_tools`; const now = Date.now(); + makeSession(db, sessionId, projectRoot, undefined, now); - db.run( - "INSERT INTO session (id, directory, time_created, time_updated) VALUES (?, ?, ?, ?)", - [sessionId, projectRoot, now, now] - ); - - const msgId = "msg_tool_001"; - db.run( - "INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?)", - [ - msgId, - sessionId, - now + 1, - now + 1, - JSON.stringify({ role: "assistant" }), - ] - ); - // Text part - db.run( - "INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?, ?)", - [ - "prt_t1", - msgId, - sessionId, - now + 1, - now + 1, - JSON.stringify({ type: "text", text: "Let me check that." }), - ] - ); - // Tool part - db.run( - "INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?, ?)", - [ - "prt_t2", - msgId, - sessionId, - now + 2, - now + 2, - JSON.stringify({ type: "tool", tool: "glob" }), - ] - ); + insertMessage(db, "msg_tool", sessionId, now + 1, "assistant"); + insertPart(db, "prt_t1", "msg_tool", sessionId, now + 1, { + type: "text", + text: "Let me check that.", + }); + insertPart(db, "prt_t2", "msg_tool", sessionId, now + 2, { + type: "tool", + tool: "glob", + }); db.close(); const result = await readOpencodeSession(projectRoot); From 918c0870091f6d8fa41744424f6cb43d90f889a4 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Wed, 1 Jul 2026 20:58:39 -0300 Subject: [PATCH 2/4] fix(session-context): incorporate review feedback from PR #444 - Port the sibling fan-out regression test from #444, adapted to the new semantics (--skip 1 errors honestly; default and --root resolve the parent) with unconditional control assertions - Enrich the root option JSDoc with the inline-skill / sibling fan-out rationale - Document the multi-top-level-session caveat and deterministic resolution via --session-id --root in the CLI reference (en/nb/pt-br) and regenerate llms-full.txt - pt-br: add the missing "sempre" nuance to the --skip row - Port agent-memory captures from #444 (generated plugins skill files; session-context skip/root fix incl. the open claude-code/cursor follow-up) Claude-Session: https://claude.ai/code/session_01SGjSjrywTnZDcUqgHWGrTj Signed-off-by: Rhuan Barreto --- .../agent-memory/archgate-developer/MEMORY.md | 5 ++++ .../project_plugins_generated_skill_files.md | 12 ++++++++ .../project_session_context_skip_root_fix.md | 16 ++++++++++ docs/public/llms-full.txt | 8 +++++ .../docs/nb/reference/cli/session-context.mdx | 8 +++++ .../pt-br/reference/cli/session-context.mdx | 10 ++++++- .../docs/reference/cli/session-context.mdx | 8 +++++ src/helpers/session-context-opencode.ts | 14 +++++++-- .../helpers/session-context-opencode.test.ts | 30 +++++++++++++++++++ 9 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 .claude/agent-memory/archgate-developer/project_plugins_generated_skill_files.md create mode 100644 .claude/agent-memory/archgate-developer/project_session_context_skip_root_fix.md diff --git a/.claude/agent-memory/archgate-developer/MEMORY.md b/.claude/agent-memory/archgate-developer/MEMORY.md index ff0a4315..a429482d 100644 --- a/.claude/agent-memory/archgate-developer/MEMORY.md +++ b/.claude/agent-memory/archgate-developer/MEMORY.md @@ -83,6 +83,11 @@ Non-enforceable lessons — environment/CI/platform quirks no static rule can re - **`archgate` command = `bun run cli`** — This is the CLI repo itself, so the `archgate` binary is not installed in PATH. Use `bun run cli ` (e.g., `bun run cli check`, `bun run cli adr list`) instead of `archgate `. The `bun run cli` script maps to `bun run src/cli.ts`. +## Sibling Repo: archgate/plugins + +- [Skill files for opencode/cursor are GENERATED, not hand-authored](project_plugins_generated_skill_files.md) — canonical source is `claude-code/plugins/archgate/skills//SKILL.md`; edit build scripts (`scripts/build--plugin.ts`) for platform-specific wording, not the generated copies directly +- [session-context opencode --skip 1 bug fixed (top-level default + --root)](project_session_context_skip_root_fix.md) — same bug class likely still open for claude-code/cursor/copilot, live-reproduced for claude-code but not fixed + ## Distribution / Packaging - **npm shim + GitHub Releases** — The npm package is a thin shim (`bin/archgate.cjs`). On first run, the shim downloads the platform binary from GitHub Releases and caches it to `~/.archgate/bin/`. No platform-specific npm packages. diff --git a/.claude/agent-memory/archgate-developer/project_plugins_generated_skill_files.md b/.claude/agent-memory/archgate-developer/project_plugins_generated_skill_files.md new file mode 100644 index 00000000..204dabd6 --- /dev/null +++ b/.claude/agent-memory/archgate-developer/project_plugins_generated_skill_files.md @@ -0,0 +1,12 @@ +--- +name: project-plugins-generated-skill-files +description: archgate/plugins skill files for opencode/cursor-plugin/cursor-install are GENERATED from claude-code source — never hand-edit them directly +metadata: + type: project +--- + +The sibling repo `E:\archgate\plugins` (separate git repo, `github.com/archgate/plugins`) holds the AI-facing skill/agent markdown consumed by the `archgate` Claude Code plugin, opencode, Cursor, VS Code Copilot, etc. `claude-code/plugins/archgate/skills//SKILL.md` is the **only** canonical source (per that repo's ARCH-003 "Platform Distribution Build Pattern"). Every other platform's copy — `opencode/plugins/archgate/skills/archgate-/SKILL.md`, `cursor-plugin/...`, `cursor-install/...` — is a build artifact generated by `scripts/build--plugin.ts`, which applies regex-based text transformations (tool-name rewrites, `@mention` syntax, editor-specific `session-context ` substitutions, etc.) and is checked into git for diff review, but is NOT hand-authored. + +**Why:** Hand-editing a generated copy (e.g. `opencode/plugins/archgate/skills/archgate-lessons-learned/SKILL.md`) works until the next `bun scripts/build-opencode-plugin.ts` (or `build-artifacts.ts`) run, which silently overwrites it back to whatever the regex transform of the canonical claude-code source produces. Learned this the hard way on 2026-07-01: edited the opencode SKILL.md directly to fix the `--skip 1` bug (see [[project-session-context-skip-root-fix]]), then ran the build script to regenerate the tarball artifact — it reverted my edit instantly. The same mistake was made independently in plugins PR #143 (closed in favor of #142, which fixed the generator). + +**How to apply:** To change platform-specific wording that should NOT apply to all editors (e.g. opencode needing `--root` while claude-code/cursor keep `--skip 1`), add a new transformation step inside that platform's `adaptForOpencode()`-equivalent function in `scripts/build--plugin.ts` (do a literal string/regex replace keyed off the exact canonical source text), NOT a direct edit to the generated file. To change wording that SHOULD apply to all editors, edit the canonical `claude-code/plugins/archgate/skills//SKILL.md` source instead. Either way, finish with `bun scripts/build-artifacts.ts` (regenerates all platform artifacts + embedded bridge files in one pass) then `bun scripts/check-artifacts.ts` to confirm everything is back in sync before considering the change done. That repo's `package.json` `validate` script and ARCH-003 are the authoritative references — read ARCH-003 in full via `archgate adr show ARCH-003` in that repo before making build-script changes. diff --git a/.claude/agent-memory/archgate-developer/project_session_context_skip_root_fix.md b/.claude/agent-memory/archgate-developer/project_session_context_skip_root_fix.md new file mode 100644 index 00000000..5776a351 --- /dev/null +++ b/.claude/agent-memory/archgate-developer/project_session_context_skip_root_fix.md @@ -0,0 +1,16 @@ +--- +name: project-session-context-skip-root-fix +description: opencode session-context --skip 1 sibling/inline-skill bug fixed 2026-07-01 (top-level default + --root ancestry walk); same class of bug likely still open for claude-code/cursor/copilot +metadata: + type: project +--- + +Fixed a real, reproduced bug in `archgate session-context opencode --skip 1`: it could return a completely unrelated sibling sub-agent's session transcript instead of the true parent/development session. Fix shipped 2026-07-01 (see `src/helpers/session-context-opencode.ts` and `src/commands/session-context/opencode.ts`): recency selection now only considers **top-level sessions** (`parent_id IS NULL`) by default, and a new `--root` flag walks the `parent_id` chain from a `--session-id` child session up to its top-level ancestor (cycle-guarded). Without `--session-id`, `--root` is an explicit alias for the new default. + +**Why:** `readOpencodeSession`'s `--skip N` selected the Nth-most-recently-updated session sharing a project directory, ignoring opencode's real `session.parent_id` column entirely. This breaks in two ways: (1) the opencode `Skill` tool runs inline in the current session (no new session row), so "skip past my own session" skips past the actual parent instead; (2) sibling sub-agent sessions fanned out from the same parent (e.g. the `archgate:reviewer` skill's parallel domain-review agents) interleave in recency order with the parent, so `--skip 1` can land on any sibling. Verified this concretely against the user's real `~/.local/share/opencode/opencode.db`: a live `archgate-lessons-learned` skill invocation's `--skip 1` call returned the "General process ADR review" sub-agent's private transcript instead of the parent session. + +Top-level filtering fixes the common case regardless of nesting depth or sibling count; `--session-id --root` gives deterministic ancestry resolution when the caller knows a session inside the right conversation tree (relevant when several top-level sessions share a directory — recency alone can pick a different conversation's root). The `archgate-lessons-learned` skill's opencode variant was updated to use `--root` instead of `--skip 1` — see [[project-plugins-generated-skill-files]] for where that source actually lives. + +**Open/unresolved:** The _same class_ of bug likely still affects the other 3 editors' lessons-learned skill guidance (`archgate session-context claude-code --skip 1`, `cursor --skip 1`). Live-reproduced for Claude Code: running `archgate session-context claude-code --skip 1` from within an inline Skill invocation returned an unrelated, unconnected past session (`6ee6f0a5-...` about "ExitWorktree"), not the current/parent conversation — because Claude Code's Skill tool also runs inline, so `--skip 1` skips past the correct (current) session file. Unlike opencode, Claude Code/Cursor have no `parent_id`-equivalent — sessions are flat per-conversation JSONL/directory files with no structural parent link, so the `--root` fix pattern doesn't directly transfer; a different fix (e.g. detecting "am I inline vs a real sub-agent" some other way, or defaulting to `--skip 0` for inline skill runs) would be needed. Not investigated for Copilot. This has NOT been fixed — flag to the user before doing more lessons-learned/reviewer work that depends on `--skip 1` being reliable for non-opencode editors. + +**How to apply:** If asked to investigate "session context returns wrong data" for claude-code, cursor, or copilot, start here — this is the same architectural flaw already fixed once for opencode, not a new mystery. diff --git a/docs/public/llms-full.txt b/docs/public/llms-full.txt index 55ba0018..ce4149ca 100644 --- a/docs/public/llms-full.txt +++ b/docs/public/llms-full.txt @@ -4390,6 +4390,8 @@ archgate session-context cursor [options] Read the opencode session transcript for the project. Sessions are matched by comparing the session `directory` field to the project root. opencode records sub-agent runs as child sessions that share the parent's directory — these are excluded from recency selection, so the most recent top-level session is always the main development session. Use `--session-id` to read a specific session, including a sub-agent child session. +When several top-level sessions exist for the same directory, recency selection picks the most recently updated one — which may not be the conversation you are part of. If you know a session ID inside the right conversation tree (for example, a sub-agent knows its own child session ID), pass `--session-id --root` to resolve its top-level ancestor deterministically instead of relying on recency. + ```bash archgate session-context opencode [options] ``` @@ -4433,6 +4435,12 @@ Read the parent session (skip the sub-agent's own session): archgate session-context claude-code --skip 1 ``` +Resolve an opencode sub-agent child session to its top-level ancestor: + +```bash +archgate session-context opencode --session-id ses_child123 --root +``` + --- ## Reference: archgate telemetry diff --git a/docs/src/content/docs/nb/reference/cli/session-context.mdx b/docs/src/content/docs/nb/reference/cli/session-context.mdx index 5e65b64f..8786fda5 100644 --- a/docs/src/content/docs/nb/reference/cli/session-context.mdx +++ b/docs/src/content/docs/nb/reference/cli/session-context.mdx @@ -56,6 +56,8 @@ archgate session-context cursor [options] Les opencode-sesjonsloggen for prosjektet. Sesjoner matches ved å sammenligne sesjonens `directory`-felt med prosjektroten. opencode lagrer underagent-kjøringer som barnesesjoner som deler foreldrenes katalog — disse ekskluderes fra utvalget etter nylighet, slik at den nyeste toppnivåsesjonen alltid er hovedutviklingssesjonen. Bruk `--session-id` for å lese en bestemt sesjon, inkludert en underagent-barnesesjon. +Når flere toppnivåsesjoner finnes for samme katalog, velger nylighetsutvalget den som sist ble oppdatert — som kanskje ikke er samtalen du er en del av. Hvis du kjenner en sesjons-ID i riktig samtaletre (for eksempel kjenner en underagent sin egen barnesesjons-ID), send `--session-id --root` for å løse opp toppnivåforelderen deterministisk i stedet for å stole på nylighet. + ```bash archgate session-context opencode [options] ``` @@ -98,3 +100,9 @@ Les foreldresesjonen (hopp over underagentens egen sesjon): ```bash archgate session-context claude-code --skip 1 ``` + +Løs opp en underagent-barnesesjon i opencode til dens toppnivåforelder: + +```bash +archgate session-context opencode --session-id ses_child123 --root +``` diff --git a/docs/src/content/docs/pt-br/reference/cli/session-context.mdx b/docs/src/content/docs/pt-br/reference/cli/session-context.mdx index 0c386cff..8247e4f3 100644 --- a/docs/src/content/docs/pt-br/reference/cli/session-context.mdx +++ b/docs/src/content/docs/pt-br/reference/cli/session-context.mdx @@ -56,6 +56,8 @@ archgate session-context cursor [options] Lê a transcrição de sessão do opencode para o projeto. Sessões são correspondidas comparando o campo `directory` da sessão com a raiz do projeto. O opencode registra execuções de sub-agentes como sessões filhas que compartilham o diretório da sessão pai — elas são excluídas da seleção por recência, de modo que a sessão de nível superior mais recente é sempre a sessão principal de desenvolvimento. Use `--session-id` para ler uma sessão específica, incluindo uma sessão filha de sub-agente. +Quando existem várias sessões de nível superior para o mesmo diretório, a seleção por recência escolhe a atualizada mais recentemente — que pode não ser a conversa da qual você faz parte. Se você conhece um ID de sessão dentro da árvore de conversa correta (por exemplo, um sub-agente conhece o ID da sua própria sessão filha), passe `--session-id --root` para resolver o ancestral de nível superior de forma determinística em vez de depender da recência. + ```bash archgate session-context opencode [options] ``` @@ -63,7 +65,7 @@ archgate session-context opencode [options] | Opção | Descrição | | ------------------- | -------------------------------------------------------------------------------------------------------------------- | | `--max-entries ` | Máximo de entradas a retornar (padrão: 200) | -| `--skip ` | Pular as N sessões de nível superior mais recentes (sessões de sub-agente são excluídas) | +| `--skip ` | Pular as N sessões de nível superior mais recentes (sessões de sub-agente são sempre excluídas) | | `--session-id ` | ID específico da sessão a ser lida | | `--root` | Resolve para a sessão de nível superior (raiz) — com `--session-id`, sobe a partir de uma sessão filha de sub-agente | @@ -98,3 +100,9 @@ Ler a sessão pai (pular a sessão do sub-agente): ```bash archgate session-context claude-code --skip 1 ``` + +Resolver uma sessão filha de sub-agente do opencode para seu ancestral de nível superior: + +```bash +archgate session-context opencode --session-id ses_child123 --root +``` diff --git a/docs/src/content/docs/reference/cli/session-context.mdx b/docs/src/content/docs/reference/cli/session-context.mdx index 0be319de..131b4886 100644 --- a/docs/src/content/docs/reference/cli/session-context.mdx +++ b/docs/src/content/docs/reference/cli/session-context.mdx @@ -56,6 +56,8 @@ archgate session-context cursor [options] Read the opencode session transcript for the project. Sessions are matched by comparing the session `directory` field to the project root. opencode records sub-agent runs as child sessions that share the parent's directory — these are excluded from recency selection, so the most recent top-level session is always the main development session. Use `--session-id` to read a specific session, including a sub-agent child session. +When several top-level sessions exist for the same directory, recency selection picks the most recently updated one — which may not be the conversation you are part of. If you know a session ID inside the right conversation tree (for example, a sub-agent knows its own child session ID), pass `--session-id --root` to resolve its top-level ancestor deterministically instead of relying on recency. + ```bash archgate session-context opencode [options] ``` @@ -98,3 +100,9 @@ Read the parent session (skip the sub-agent's own session): ```bash archgate session-context claude-code --skip 1 ``` + +Resolve an opencode sub-agent child session to its top-level ancestor: + +```bash +archgate session-context opencode --session-id ses_child123 --root +``` diff --git a/src/helpers/session-context-opencode.ts b/src/helpers/session-context-opencode.ts index e57573f9..c850b914 100644 --- a/src/helpers/session-context-opencode.ts +++ b/src/helpers/session-context-opencode.ts @@ -25,9 +25,19 @@ interface ReadOpencodeSessionOptions extends ReadSessionOptions { sessionId?: string; /** * Resolve to the top-level (root) session. Without `sessionId` this is - * the default behavior already; combined with a `sessionId` that names a - * sub-agent child session, walks the `parent_id` chain up to the + * an explicit alias for the default behavior (recency selection already + * only considers top-level sessions); combined with a `sessionId` that + * names a sub-agent child session, walks the `parent_id` chain up to the * top-level ancestor. + * + * Opencode is the only session-context backend with a real parent/child + * session graph, so this option lives here rather than in the shared + * `ReadSessionOptions`. A recency-based guess (the old bare `skip: 1`) + * cannot distinguish the true parent from a sibling sub-agent session + * once more than one sibling exists — and an inline Skill invocation + * creates no session row at all, so there is nothing to skip past. + * Ancestry via `parent_id` is correct regardless of nesting depth or + * sibling fan-out. */ root?: boolean; } diff --git a/tests/helpers/session-context-opencode.test.ts b/tests/helpers/session-context-opencode.test.ts index 9636224b..11a47f9b 100644 --- a/tests/helpers/session-context-opencode.test.ts +++ b/tests/helpers/session-context-opencode.test.ts @@ -449,6 +449,36 @@ describe("readOpencodeSession", () => { if (noId.ok) expect(noId.data.sessionId).toBe("ses_parent"); }); + test("selects the true parent when sibling sub-agents fan out and are more recent", async () => { + // Real-world fan-out reproduced from a live incident: one parent session + // spawns several sibling sub-agent sessions against the same directory + // (e.g. the reviewer skill's parallel domain reviews). Every sibling + // sorts ahead of the parent by recency; none of them may shadow it. + // The old recency-based `--skip 1` landed on whichever sibling sat + // second in recency order instead of the parent. + const db = createDb(); + makeSimpleSession(db, "ses_parent", "parent development session", 1000); + makeSimpleSession(db, "ses_sib_a", "domain review a", 2000, "ses_parent"); + makeSimpleSession(db, "ses_sib_b", "domain review b", 3000, "ses_parent"); + makeSimpleSession(db, "ses_sib_c", "domain review c", 4000, "ses_parent"); + makeSimpleSession(db, "ses_sib_d", "domain review d", 5000, "ses_parent"); + db.close(); + + // skip: 1 now errors honestly — only one top-level session exists, so + // there is no second one to skip to (previously it returned a sibling). + const skipped = await readOpencodeSession(projectRoot, { skip: 1 }); + expect(skipped.ok).toBe(false); + + // Default selection and explicit root both resolve to the parent. + const byDefault = await readOpencodeSession(projectRoot); + expect(byDefault.ok).toBe(true); + if (byDefault.ok) expect(byDefault.data.sessionId).toBe("ses_parent"); + + const rooted = await readOpencodeSession(projectRoot, { root: true }); + expect(rooted.ok).toBe(true); + if (rooted.ok) expect(rooted.data.sessionId).toBe("ses_parent"); + }); + test("skip beyond available top-level sessions returns error", async () => { const db = createDb(); makeSimpleSession(db, "ses_only", "only", 1000); From 9f38594443109afc50f0b84d7238d168ea3e61f9 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Wed, 1 Jul 2026 21:08:27 -0300 Subject: [PATCH 3/4] chore(memory): capture lessons from PR consolidation session - Haiku reviewer await-on-sync false positive recurred (ARCH-NONE) - Git Bash /tmp paths are invisible to Windows-native tools - plugins-repo validate fails locally on Windows (service test /tmp sqlite path); use build/check-artifacts scripts locally instead Claude-Session: https://claude.ai/code/session_01SGjSjrywTnZDcUqgHWGrTj Signed-off-by: Rhuan Barreto --- .claude/agent-memory/archgate-developer/MEMORY.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.claude/agent-memory/archgate-developer/MEMORY.md b/.claude/agent-memory/archgate-developer/MEMORY.md index a429482d..9b430920 100644 --- a/.claude/agent-memory/archgate-developer/MEMORY.md +++ b/.claude/agent-memory/archgate-developer/MEMORY.md @@ -45,7 +45,8 @@ Non-enforceable lessons — environment/CI/platform quirks no static rule can re - **`Bun.Glob.match()` triggers oxlint `prefer-regexp-test`** — `Bun.Glob.match()` returns a boolean (not a RegExp), but oxlint can't tell. Suppress with `// oxlint-disable-next-line prefer-regexp-test -- Bun.Glob.match() returns boolean, not RegExp`. - **Live `opencode.db` can't be opened `readonly: true` while opencode runs** — `new Database(path, { readonly: true })` fails with `SQLITE_CANTOPEN` (errno 14) on the live WAL-mode DB (`~/.local/share/opencode/opencode.db`). To inspect real data, copy `opencode.db` + `.db-wal` + `.db-shm` to a temp dir and open the copy. Data model facts load-bearing for `session-context opencode`: sub-agent runs are child sessions (`parent_id` set) sharing the parent's `directory`, and opencode skills run INLINE in the calling session (no own session row) — recency selection must filter `parent_id IS NULL` (fixed 2026-07-01; `--root` resolves a `--session-id` child to its top-level ancestor). - **opencode plugin SKILL.md references CLI flags — sync contracts across repos** — `/e/archgate/plugins/opencode/.../archgate-lessons-learned/SKILL.md` now instructs `archgate session-context opencode --root` (was `--skip 1`, which read sub-agent transcripts). When changing session-context CLI semantics, update the plugins-repo skill in the same effort AND sequence releases: CLI ships the flag first, plugin follows — a skill referencing a flag the installed CLI lacks dies with "unknown option". Don't hand-edit the installed copy under `~/.config/opencode/skills/` before the CLI release. -- **Verify haiku reviewer findings against the cited ADR's text before blocking** — A haiku review sub-agent reported "await on a synchronous helper" as an ARCH-012 violation; ARCH-012 only mandates try-catch boundaries/exit codes and its automated rules passed. Re-read the cited ADR's Decision/Do's before accepting a FAIL; overrule misattributed style nits but still surface them as non-ADR notes. +- **Verify haiku reviewer findings against the cited ADR's text before blocking** — A haiku review sub-agent reported "await on a synchronous helper" as an ARCH-012 violation; ARCH-012 only mandates try-catch boundaries/exit codes and its automated rules passed. Re-read the cited ADR's Decision/Do's before accepting a FAIL; overrule misattributed style nits but still surface them as non-ADR notes. Recurred 2026-07-01: the same await-on-sync-helper nit came back self-labeled "ARCH-NONE" — a finding citing no ADR can never block. +- **Git Bash `/tmp` is invisible to Windows-native tools** — A bash redirect to `/tmp/x` writes into Git Bash's virtual mount, but Windows-native python/node then fail with `FileNotFoundError` on that path (gh.exe survives only because Git Bash rewrites path-looking arguments). When piping a file between bash and Windows-native tools, use a repo-relative path (and clean it up) or `$TMPDIR`. - **oxlint `no-negated-condition`** — Always write ternaries with the positive condition first: `x === null ? A : B` not `x !== null ? B : A`. Applies to both `if/else` blocks and ternary expressions. - **oxlint `no-unused-vars` on catch parameters** — Use bare `catch { }` (no parameter) when the caught error is not used. `catch (err) { }` with unused `err` triggers the rule. - **oxlint `no-await-in-loop`** — Sequential `await` inside a `for` loop is flagged (warning). When the sequential order is intentional (e.g., build steps with per-step output), suppress with `// oxlint-disable-next-line no-await-in-loop -- `. @@ -87,6 +88,7 @@ Non-enforceable lessons — environment/CI/platform quirks no static rule can re - [Skill files for opencode/cursor are GENERATED, not hand-authored](project_plugins_generated_skill_files.md) — canonical source is `claude-code/plugins/archgate/skills//SKILL.md`; edit build scripts (`scripts/build--plugin.ts`) for platform-specific wording, not the generated copies directly - [session-context opencode --skip 1 bug fixed (top-level default + --root)](project_session_context_skip_root_fix.md) — same bug class likely still open for claude-code/cursor/copilot, live-reproduced for claude-code but not fixed +- **plugins-repo `bun run validate` fails locally on Windows** — `service/backend/src/test-setup.ts` hardcodes `/tmp/archgate-test-.db`, which bun:sqlite cannot open on Windows (`SQLITE_CANTOPEN`, all 14 service tests error). Pre-existing; CI runs on Linux and is green. For plugin/build-script changes, validate locally with `bun scripts/build-artifacts.ts` + `bun scripts/check-artifacts.ts` and let CI cover the service tests. ## Distribution / Packaging From 57d1a2dcf7ba61a90c1b7a14997335897d6e9419 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Thu, 2 Jul 2026 05:06:29 -0300 Subject: [PATCH 4/4] chore(memory): remove private-repo internals from public agent memory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The plugins repo is private while this repo is public — the ported memory files exposed its internals (repo paths, build scripts, service test structure). Remove the generated-skill-files memory and the Sibling Repo section, and sanitize the remaining notes to only reference the publicly distributed plugin behavior. The detailed memories move to the private plugins repo. Claude-Session: https://claude.ai/code/session_01SGjSjrywTnZDcUqgHWGrTj Signed-off-by: Rhuan Barreto --- .claude/agent-memory/archgate-developer/MEMORY.md | 8 +------- .../project_plugins_generated_skill_files.md | 12 ------------ .../project_session_context_skip_root_fix.md | 2 +- 3 files changed, 2 insertions(+), 20 deletions(-) delete mode 100644 .claude/agent-memory/archgate-developer/project_plugins_generated_skill_files.md diff --git a/.claude/agent-memory/archgate-developer/MEMORY.md b/.claude/agent-memory/archgate-developer/MEMORY.md index 9b430920..71e8d24e 100644 --- a/.claude/agent-memory/archgate-developer/MEMORY.md +++ b/.claude/agent-memory/archgate-developer/MEMORY.md @@ -44,7 +44,7 @@ Non-enforceable lessons — environment/CI/platform quirks no static rule can re - **YAML double-quoted strings require escaped backslashes for Windows paths in tests** — YAML interprets `\` as an escape character inside double-quoted strings. Writing `cwd: "E:\project"` silently corrupts the parsed value because `\p` is not a valid escape sequence. Fix: use `JSON.stringify(path)` to produce properly escaped YAML values (e.g., `cwd: ${JSON.stringify(cwd)}`). JSON and YAML double-quoted strings share the same escape syntax. Encountered in Copilot CLI session-context tests (`workspace.yaml` with Windows paths). - **`Bun.Glob.match()` triggers oxlint `prefer-regexp-test`** — `Bun.Glob.match()` returns a boolean (not a RegExp), but oxlint can't tell. Suppress with `// oxlint-disable-next-line prefer-regexp-test -- Bun.Glob.match() returns boolean, not RegExp`. - **Live `opencode.db` can't be opened `readonly: true` while opencode runs** — `new Database(path, { readonly: true })` fails with `SQLITE_CANTOPEN` (errno 14) on the live WAL-mode DB (`~/.local/share/opencode/opencode.db`). To inspect real data, copy `opencode.db` + `.db-wal` + `.db-shm` to a temp dir and open the copy. Data model facts load-bearing for `session-context opencode`: sub-agent runs are child sessions (`parent_id` set) sharing the parent's `directory`, and opencode skills run INLINE in the calling session (no own session row) — recency selection must filter `parent_id IS NULL` (fixed 2026-07-01; `--root` resolves a `--session-id` child to its top-level ancestor). -- **opencode plugin SKILL.md references CLI flags — sync contracts across repos** — `/e/archgate/plugins/opencode/.../archgate-lessons-learned/SKILL.md` now instructs `archgate session-context opencode --root` (was `--skip 1`, which read sub-agent transcripts). When changing session-context CLI semantics, update the plugins-repo skill in the same effort AND sequence releases: CLI ships the flag first, plugin follows — a skill referencing a flag the installed CLI lacks dies with "unknown option". Don't hand-edit the installed copy under `~/.config/opencode/skills/` before the CLI release. +- **The distributed opencode lessons-learned skill references session-context CLI flags — sequence releases** — the skill instructs `archgate session-context opencode --root` (was `--skip 1`, which read sub-agent transcripts). When changing session-context CLI semantics, update the shipped skill in the same effort AND sequence releases: the CLI ships the flag first, the plugin follows — a skill referencing a flag the installed CLI lacks dies with "unknown option". Don't hand-edit the installed copy under `~/.config/opencode/skills/` before the CLI release. - **Verify haiku reviewer findings against the cited ADR's text before blocking** — A haiku review sub-agent reported "await on a synchronous helper" as an ARCH-012 violation; ARCH-012 only mandates try-catch boundaries/exit codes and its automated rules passed. Re-read the cited ADR's Decision/Do's before accepting a FAIL; overrule misattributed style nits but still surface them as non-ADR notes. Recurred 2026-07-01: the same await-on-sync-helper nit came back self-labeled "ARCH-NONE" — a finding citing no ADR can never block. - **Git Bash `/tmp` is invisible to Windows-native tools** — A bash redirect to `/tmp/x` writes into Git Bash's virtual mount, but Windows-native python/node then fail with `FileNotFoundError` on that path (gh.exe survives only because Git Bash rewrites path-looking arguments). When piping a file between bash and Windows-native tools, use a repo-relative path (and clean it up) or `$TMPDIR`. - **oxlint `no-negated-condition`** — Always write ternaries with the positive condition first: `x === null ? A : B` not `x !== null ? B : A`. Applies to both `if/else` blocks and ternary expressions. @@ -84,12 +84,6 @@ Non-enforceable lessons — environment/CI/platform quirks no static rule can re - **`archgate` command = `bun run cli`** — This is the CLI repo itself, so the `archgate` binary is not installed in PATH. Use `bun run cli ` (e.g., `bun run cli check`, `bun run cli adr list`) instead of `archgate `. The `bun run cli` script maps to `bun run src/cli.ts`. -## Sibling Repo: archgate/plugins - -- [Skill files for opencode/cursor are GENERATED, not hand-authored](project_plugins_generated_skill_files.md) — canonical source is `claude-code/plugins/archgate/skills//SKILL.md`; edit build scripts (`scripts/build--plugin.ts`) for platform-specific wording, not the generated copies directly -- [session-context opencode --skip 1 bug fixed (top-level default + --root)](project_session_context_skip_root_fix.md) — same bug class likely still open for claude-code/cursor/copilot, live-reproduced for claude-code but not fixed -- **plugins-repo `bun run validate` fails locally on Windows** — `service/backend/src/test-setup.ts` hardcodes `/tmp/archgate-test-.db`, which bun:sqlite cannot open on Windows (`SQLITE_CANTOPEN`, all 14 service tests error). Pre-existing; CI runs on Linux and is green. For plugin/build-script changes, validate locally with `bun scripts/build-artifacts.ts` + `bun scripts/check-artifacts.ts` and let CI cover the service tests. - ## Distribution / Packaging - **npm shim + GitHub Releases** — The npm package is a thin shim (`bin/archgate.cjs`). On first run, the shim downloads the platform binary from GitHub Releases and caches it to `~/.archgate/bin/`. No platform-specific npm packages. diff --git a/.claude/agent-memory/archgate-developer/project_plugins_generated_skill_files.md b/.claude/agent-memory/archgate-developer/project_plugins_generated_skill_files.md deleted file mode 100644 index 204dabd6..00000000 --- a/.claude/agent-memory/archgate-developer/project_plugins_generated_skill_files.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -name: project-plugins-generated-skill-files -description: archgate/plugins skill files for opencode/cursor-plugin/cursor-install are GENERATED from claude-code source — never hand-edit them directly -metadata: - type: project ---- - -The sibling repo `E:\archgate\plugins` (separate git repo, `github.com/archgate/plugins`) holds the AI-facing skill/agent markdown consumed by the `archgate` Claude Code plugin, opencode, Cursor, VS Code Copilot, etc. `claude-code/plugins/archgate/skills//SKILL.md` is the **only** canonical source (per that repo's ARCH-003 "Platform Distribution Build Pattern"). Every other platform's copy — `opencode/plugins/archgate/skills/archgate-/SKILL.md`, `cursor-plugin/...`, `cursor-install/...` — is a build artifact generated by `scripts/build--plugin.ts`, which applies regex-based text transformations (tool-name rewrites, `@mention` syntax, editor-specific `session-context ` substitutions, etc.) and is checked into git for diff review, but is NOT hand-authored. - -**Why:** Hand-editing a generated copy (e.g. `opencode/plugins/archgate/skills/archgate-lessons-learned/SKILL.md`) works until the next `bun scripts/build-opencode-plugin.ts` (or `build-artifacts.ts`) run, which silently overwrites it back to whatever the regex transform of the canonical claude-code source produces. Learned this the hard way on 2026-07-01: edited the opencode SKILL.md directly to fix the `--skip 1` bug (see [[project-session-context-skip-root-fix]]), then ran the build script to regenerate the tarball artifact — it reverted my edit instantly. The same mistake was made independently in plugins PR #143 (closed in favor of #142, which fixed the generator). - -**How to apply:** To change platform-specific wording that should NOT apply to all editors (e.g. opencode needing `--root` while claude-code/cursor keep `--skip 1`), add a new transformation step inside that platform's `adaptForOpencode()`-equivalent function in `scripts/build--plugin.ts` (do a literal string/regex replace keyed off the exact canonical source text), NOT a direct edit to the generated file. To change wording that SHOULD apply to all editors, edit the canonical `claude-code/plugins/archgate/skills//SKILL.md` source instead. Either way, finish with `bun scripts/build-artifacts.ts` (regenerates all platform artifacts + embedded bridge files in one pass) then `bun scripts/check-artifacts.ts` to confirm everything is back in sync before considering the change done. That repo's `package.json` `validate` script and ARCH-003 are the authoritative references — read ARCH-003 in full via `archgate adr show ARCH-003` in that repo before making build-script changes. diff --git a/.claude/agent-memory/archgate-developer/project_session_context_skip_root_fix.md b/.claude/agent-memory/archgate-developer/project_session_context_skip_root_fix.md index 5776a351..d2e4b95e 100644 --- a/.claude/agent-memory/archgate-developer/project_session_context_skip_root_fix.md +++ b/.claude/agent-memory/archgate-developer/project_session_context_skip_root_fix.md @@ -9,7 +9,7 @@ Fixed a real, reproduced bug in `archgate session-context opencode --skip 1`: it **Why:** `readOpencodeSession`'s `--skip N` selected the Nth-most-recently-updated session sharing a project directory, ignoring opencode's real `session.parent_id` column entirely. This breaks in two ways: (1) the opencode `Skill` tool runs inline in the current session (no new session row), so "skip past my own session" skips past the actual parent instead; (2) sibling sub-agent sessions fanned out from the same parent (e.g. the `archgate:reviewer` skill's parallel domain-review agents) interleave in recency order with the parent, so `--skip 1` can land on any sibling. Verified this concretely against the user's real `~/.local/share/opencode/opencode.db`: a live `archgate-lessons-learned` skill invocation's `--skip 1` call returned the "General process ADR review" sub-agent's private transcript instead of the parent session. -Top-level filtering fixes the common case regardless of nesting depth or sibling count; `--session-id --root` gives deterministic ancestry resolution when the caller knows a session inside the right conversation tree (relevant when several top-level sessions share a directory — recency alone can pick a different conversation's root). The `archgate-lessons-learned` skill's opencode variant was updated to use `--root` instead of `--skip 1` — see [[project-plugins-generated-skill-files]] for where that source actually lives. +Top-level filtering fixes the common case regardless of nesting depth or sibling count; `--session-id --root` gives deterministic ancestry resolution when the caller knows a session inside the right conversation tree (relevant when several top-level sessions share a directory — recency alone can pick a different conversation's root). The distributed archgate editor plugin's opencode lessons-learned skill was updated to use `--root` instead of `--skip 1`. **Open/unresolved:** The _same class_ of bug likely still affects the other 3 editors' lessons-learned skill guidance (`archgate session-context claude-code --skip 1`, `cursor --skip 1`). Live-reproduced for Claude Code: running `archgate session-context claude-code --skip 1` from within an inline Skill invocation returned an unrelated, unconnected past session (`6ee6f0a5-...` about "ExitWorktree"), not the current/parent conversation — because Claude Code's Skill tool also runs inline, so `--skip 1` skips past the correct (current) session file. Unlike opencode, Claude Code/Cursor have no `parent_id`-equivalent — sessions are flat per-conversation JSONL/directory files with no structural parent link, so the `--root` fix pattern doesn't directly transfer; a different fix (e.g. detecting "am I inline vs a real sub-agent" some other way, or defaulting to `--skip 0` for inline skill runs) would be needed. Not investigated for Copilot. This has NOT been fixed — flag to the user before doing more lessons-learned/reviewer work that depends on `--skip 1` being reliable for non-opencode editors.