From dc454080fa21e8f0acd497106420fd740f9291ac Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Sat, 22 Aug 2026 23:01:56 +0900 Subject: [PATCH 1/5] fix(cli): give loop, scan and receipt the --help that orchestrate already had (#47) `cxc --help` points at the sibling commands, and following that pointer failed: --help was reported as an unknown verb on loop, scan and receipt, and `cxc --version` was an unknown command. orchestrate was fixed for exactly this in 260709_cxc_help_agent_ux; its siblings never were. The cost is not the error messages, which are individually fine. It is that discovery was only available through failure. Arming a goalplan in this session took six consecutive rejections to assemble one correct command: loop steer: --session is required loop steer: --batch-json is required loop steer: idempotencyKey is required and must be a non-empty string loop steer: rationale is required and must be a non-empty string loop steer: evidence is required and must be a non-empty string loop steer: ops must be a non-empty array help | --help | -h now print usage and exit 0 on all three, and the loop usage spells out the steer batch shape since that is the one nobody can guess. Unknown verbs still fail, but now name the way out. cxc --version reads the installed manifest; previously the only way to know which payload was live was to read the cache directory name. Also from the same issue: scan record now accepts --cwd, which orchestrate already documented. The reporter's answer ledger was in one tree and the process cwd in another, so --derive matched nothing and said so only as a warning. help-verbs.test.ts asserts the contract (exit 0 plus a Usage: block) rather than the wording, and pins the flags that were previously rejection-only. --- README.ko.md | 2 +- README.md | 2 +- README.zh.md | 2 +- .../010_help_verbs.md | 77 +++++++++++++++++++ plugins/codexclaw/bin/cxc.mjs | 16 +++- .../pabcd-state/dist/goalplan-cli.js | 40 +++++++++- .../pabcd-state/dist/receipt-cli.js | 30 +++++++- .../components/pabcd-state/dist/scan-cli.js | 44 ++++++++++- .../pabcd-state/src/goalplan-cli.ts | 42 +++++++++- .../components/pabcd-state/src/receipt-cli.ts | 32 +++++++- .../components/pabcd-state/src/scan-cli.ts | 46 ++++++++++- .../pabcd-state/test/help-verbs.test.ts | 77 +++++++++++++++++++ 12 files changed, 395 insertions(+), 15 deletions(-) create mode 100644 devlog/_plan/260822_attest_win_parity/010_help_verbs.md create mode 100644 plugins/codexclaw/components/pabcd-state/test/help-verbs.test.ts diff --git a/README.ko.md b/README.ko.md index 6215613..7d7532d 100644 --- a/README.ko.md +++ b/README.ko.md @@ -13,7 +13,7 @@

CI - 1,933 tests passing + 1,945 tests passing 28 skills 22 hooks Documentation diff --git a/README.md b/README.md index 47f155c..91e07b1 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@

CI - 1,933 tests passing + 1,945 tests passing 28 skills 22 hooks Documentation diff --git a/README.zh.md b/README.zh.md index 5da8a07..c7230b7 100644 --- a/README.zh.md +++ b/README.zh.md @@ -13,7 +13,7 @@

CI - 1,933 tests passing + 1,945 tests passing 28 skills 22 hooks Documentation diff --git a/devlog/_plan/260822_attest_win_parity/010_help_verbs.md b/devlog/_plan/260822_attest_win_parity/010_help_verbs.md new file mode 100644 index 0000000..e2084df --- /dev/null +++ b/devlog/_plan/260822_attest_win_parity/010_help_verbs.md @@ -0,0 +1,77 @@ +# 010 - issue #47: the sibling commands had no --help + +## Why this belongs in the same unit + +The attest fix was about codexclaw telling an agent to run something impossible. +This is the same theme one layer down: codexclaw telling an agent where to look +and then refusing to answer. + +``` +cxc --help # mentions loop / orchestrate --help +cxc orchestrate --help # OK +cxc loop --help # loop: unknown loop verb '--help' +cxc scan --help # scan: unknown scan action '--help' +cxc receipt --help # receipt: unknown receipt verb '--help' +cxc --version # codexclaw: unknown command '--version' +``` + +`orchestrate` was fixed long ago (`devlog/_fin/260709_cxc_help_agent_ux`); its +siblings never were. The top-level help points at them, so following the pointer +is what breaks. + +## Measured cost, from this session + +I hit this myself before the issue was filed. Arming the goalplan took six +rejections to get one command right: + +``` +loop steer: --session is required +loop steer: --batch-json is required +loop steer: idempotencyKey is required and must be a non-empty string +loop steer: rationale is required and must be a non-empty string +loop steer: evidence is required and must be a non-empty string +loop steer: ops must be a non-empty array +``` + +Each one is a good error message. Together they are a guessing game, because +there was no way to ask for the whole shape at once. That is the actual defect: +not that the errors are bad, but that discovery was only available through +failure. + +## Fix + +`help | --help | -h` on `loop`, `scan` and `receipt` now print usage and exit 0, +matching `orchestrate`'s contract. Unknown verbs still fail, but the message +names the way out (`run cxc loop --help`) rather than only listing verbs. + +The `loop` usage spells out the steer batch shape explicitly, since that is the +one nobody can guess: + +``` +{ "idempotencyKey": "", "rationale": "", "evidence": "", + "ops": [ { "kind": "annotate", "note": "..." } ] } +``` + +`cxc --version` reads the installed manifest. Previously the only way to know +which payload was live was to read the cache directory name. + +## `scan record --cwd` + +Reported in the same issue and fixed here: `orchestrate` documents and accepts +`--cwd`, `scan` rejected it outright. The reporter's session had its answer +ledger in one tree and its process cwd in another, so `--derive` silently +matched nothing: + +``` +derived=0 dimension(s) from the answer ledger — WARNING: nothing matched +``` + +That is a preview of issue #48, which is the same split seen from the FSM side. + +## Tests + +`help-verbs.test.ts` asserts the CONTRACT — exit 0 plus a `Usage:` block — rather +than the wording, so the text can be edited freely. It also pins the flags that +were previously discoverable only through rejection (`--session`, `--batch-json`, +`idempotencyKey`), asserts that unknown verbs now point at `--help`, and covers +`scan record --cwd` in both the explicit and defaulted forms. diff --git a/plugins/codexclaw/bin/cxc.mjs b/plugins/codexclaw/bin/cxc.mjs index 1d849be..51d43dd 100644 --- a/plugins/codexclaw/bin/cxc.mjs +++ b/plugins/codexclaw/bin/cxc.mjs @@ -18,7 +18,7 @@ * the payload path). Both print a pointer instead of failing cryptically. */ import { spawnSync } from "node:child_process"; -import { realpathSync } from "node:fs"; +import { readFileSync, realpathSync } from "node:fs"; import { dirname, join, resolve as resolvePath } from "node:path"; import { fileURLToPath } from "node:url"; @@ -117,6 +117,20 @@ if (isMain) { console.log(HELP); process.exit(0); } + // #47: `cxc --version` was reported as an unknown command, so the only way to + // learn which payload was installed was to read the cache path. + if (cmd === "version" || cmd === "--version" || cmd === "-v") { + try { + const manifest = JSON.parse( + readFileSync(join(payloadRoot, ".codex-plugin", "plugin.json"), "utf8"), + ); + console.log(manifest.version ?? "unknown"); + process.exit(0); + } catch (err) { + console.error(`cxc --version: could not read the plugin manifest (${err.code ?? err.message})`); + process.exit(1); + } + } if (cmd === "gui" || cmd === "map") { console.log( `cxc ${cmd}: available from a repo checkout only (github.com/lidge-jun/codexclaw — see README Development section).`, diff --git a/plugins/codexclaw/components/pabcd-state/dist/goalplan-cli.js b/plugins/codexclaw/components/pabcd-state/dist/goalplan-cli.js index 1b46765..0b4607a 100644 --- a/plugins/codexclaw/components/pabcd-state/dist/goalplan-cli.js +++ b/plugins/codexclaw/components/pabcd-state/dist/goalplan-cli.js @@ -73,9 +73,15 @@ const VERBS = new Set ([ /** Structural argv parse. argv excludes the `goalplan` kind token. */ export function parseGoalplanCliArgs(argv , cwd ) { const verb = (argv[0] ?? "").toLowerCase(); + // #47: `--help` on a sibling command used to be reported as an unknown verb, so an + // agent that followed `cxc --help`'s own pointer hit a non-zero exit and had to + // discover every flag one rejection at a time. Same contract as orchestrate. + if (verb === "help" || verb === "--help" || verb === "-h") { + return { verb: "help", cwd, criteria: [] }; + } if (!VERBS.has(verb)) { return { - error: `unknown loop verb '${argv[0] ?? ""}' (expected init|show|validate|steer|add-criterion|add-work-phase)`, + error: `unknown loop verb '${argv[0] ?? ""}' (expected init|show|validate|steer|add-criterion|add-work-phase); run cxc loop --help`, }; } const out = { verb: verb , cwd, criteria: [] }; @@ -276,7 +282,39 @@ function renderPlanLines(plan ) { return lines.join("\n"); } +/** + * #47: every flag below used to be discoverable only by running the command and + * reading the rejection, one missing argument at a time. The steer batch shape is + * spelled out for the same reason. + */ +export function renderGoalplanHelp() { + return [ + "cxc loop — durable goalplan for a multi-cycle PABCD loop", + "", + "Usage:", + " cxc loop init --objective --session [--criterion ]... [--cwd ]", + " cxc loop show (--slug | --objective ) [--cwd ]", + " cxc loop validate --slug [--cwd ]", + " cxc loop steer --session --slug --batch-json [--cwd ]", + " cxc loop add-work-phase --session --slug --id --title ", + " cxc loop add-criterion --session --slug --criterion [--surface logic|web|tui]", + " cxc loop --help", + "", + "Notes:", + " Mutating verbs require --session ; show and validate are read-only.", + " The goalplan lives at /.codexclaw/goalplans//goalplan.json, so --cwd", + " matters when the process cwd is not the workspace you are planning in.", + "", + "steer --batch-json expects an object with:", + ' { "idempotencyKey": "", "rationale": "", "evidence": "",', + ' "ops": [ { "kind": "annotate", "note": "..." } ] }', + " op kinds: annotate | add-criterion | add-work-phase (all additive — steering", + " cannot weaken a completion criterion).", + ].join("\n"); +} + export function runGoalplanCli(args ) { + if (args.verb === "help") return { output: renderGoalplanHelp(), code: 0 }; if (args.verb === "init") { const objective = (args.objective ?? "").trim(); if (objective.length === 0) { diff --git a/plugins/codexclaw/components/pabcd-state/dist/receipt-cli.js b/plugins/codexclaw/components/pabcd-state/dist/receipt-cli.js index 3a054cc..8834c01 100644 --- a/plugins/codexclaw/components/pabcd-state/dist/receipt-cli.js +++ b/plugins/codexclaw/components/pabcd-state/dist/receipt-cli.js @@ -31,7 +31,14 @@ import { STATE_DIR, sanitizeKey } from "./state.js"; /** Everything after `--` is the command; nothing before it is. */ export function parseReceiptCliArgs(argv , cwd ) { const verb = (argv[0] ?? "").toLowerCase(); - if (verb !== "test") return { error: `unknown receipt verb '${argv[0] ?? ""}' (expected test)` }; + // #47: --help was reported as an unknown verb, so the flags could only be learned + // from rejections. + if (verb === "help" || verb === "--help" || verb === "-h") { + return { verb: "help", cwd, command: [] }; + } + if (verb !== "test") { + return { error: `unknown receipt verb '${argv[0] ?? ""}' (expected test); run cxc receipt --help` }; + } const out = { verb: "test", cwd, command: [] }; let i = 1; for (; i < argv.length; i++) { @@ -54,6 +61,27 @@ export function receiptPathFor(cwd , sessionId ) { export function runReceiptCli(args ) { + if (args.verb === "help") { + return { + output: [ + "cxc receipt — record a check receipt that binds a command's result to a source tree", + "", + "Usage:", + " cxc receipt test --session [--cwd ] -- [args...]", + " cxc receipt --help", + "", + "Notes:", + " Everything after `--` is the command; nothing before it is.", + " The session must be at phase C — a receipt is produced during Check.", + " The receipt is written to /.codexclaw/evidence//test-receipt.json", + " and is refused if the command changes the source while it runs.", + "", + "Example:", + " cxc receipt test --session -- npm test", + ].join("\n"), + code: 0, + }; + } const session = (args.session ?? "").trim(); if (session.length === 0) return { output: "receipt test: --session is required", code: 1 }; if (args.command.length === 0) { diff --git a/plugins/codexclaw/components/pabcd-state/dist/scan-cli.js b/plugins/codexclaw/components/pabcd-state/dist/scan-cli.js index 7921416..5aaa390 100644 --- a/plugins/codexclaw/components/pabcd-state/dist/scan-cli.js +++ b/plugins/codexclaw/components/pabcd-state/dist/scan-cli.js @@ -83,8 +83,20 @@ export function parseScanCliArgs( cwd , ) { const [action, ...rest] = argv; + // #47: --help was an unknown action, and --cwd was rejected outright even though + // orchestrate accepts it — which stranded a live session whose answer ledger lived + // in a different tree from the process cwd. + if (action === "help" || action === "--help" || action === "-h") { + return { + action: "help", + sessionId: "", + contradictionCount: 0, + highContradictionCount: 0, + cwd, + }; + } if (action !== "record") { - return { error: `unknown scan action '${action ?? ""}' — usage: scan record --session [--contradictions N] [--high N]` }; + return { error: `unknown scan action '${action ?? ""}'; run cxc scan --help` }; } let sessionId = ""; let contradictionCount = 0; @@ -98,11 +110,17 @@ export function parseScanCliArgs( const known = []; const unknown = []; const confidence = {}; + let resolvedCwd = cwd; for (let i = 0; i < rest.length; i += 1) { const arg = rest[i]; if (arg === "--session") { sessionId = rest[i + 1] ?? ""; i += 1; + } else if (arg === "--cwd") { + // #47: orchestrate documents and accepts --cwd; scan rejected it, which + // stranded a session whose answer ledger was in a different tree. + resolvedCwd = rest[i + 1] ?? cwd; + i += 1; } else if (arg === "--contradictions") { contradictionCount = Number.parseInt(rest[i + 1] ?? "", 10); i += 1; @@ -173,7 +191,7 @@ export function parseScanCliArgs( sessionId, contradictionCount, highContradictionCount, - cwd, + cwd: resolvedCwd, ...(derive ? { derive } : {}), ...(Object.keys(map).length > 0 ? { map } : {}), ...(Object.keys(dims).length > 0 ? { dims } : {}), @@ -264,6 +282,28 @@ function deriveLevel(score ) { } export function runScanCli(args ) { + if (args.action === "help") { + return { + output: [ + "cxc scan — record an interview rescan round and fold answers into the tracker", + "", + "Usage:", + " cxc scan record --session [--cwd ] [--contradictions N] [--high N]", + " [--derive] [--map =]...", + " [--dim =]... [--known =]...", + " [--unknown =]... [--confidence =<0..1>]...", + " cxc scan --help", + "", + "Notes:", + " --session is required; there is no latest-session fallback for a mutating command.", + " --cwd matters when the answer ledger lives outside the process cwd:", + " answers are read from /.codexclaw/interviews/.jsonl.", + " --derive folds captured answers in; --map attributes a questionId to a dimension.", + " --dim cannot set 'max' — that level gates I->P and must be attested.", + ].join("\n"), + code: 0, + }; + } try { const state = readState(args.cwd, args.sessionId); const tracker = state.interview ?? defaultInterview(0); diff --git a/plugins/codexclaw/components/pabcd-state/src/goalplan-cli.ts b/plugins/codexclaw/components/pabcd-state/src/goalplan-cli.ts index b637867..4fd72ec 100644 --- a/plugins/codexclaw/components/pabcd-state/src/goalplan-cli.ts +++ b/plugins/codexclaw/components/pabcd-state/src/goalplan-cli.ts @@ -34,7 +34,7 @@ import { captureSourceIdentity, compareSource } from "./source-identity.ts"; import { parseSourceBoundReceipt } from "./source-receipt.ts"; import { applySteeringBatch } from "./steering.ts"; -export type GoalplanVerb = "init" | "show" | "validate" | "steer" | "add-criterion" | "add-work-phase"; +export type GoalplanVerb = "init" | "show" | "validate" | "steer" | "add-criterion" | "add-work-phase" | "help"; export interface GoalplanCliArgs { verb: GoalplanVerb; @@ -73,9 +73,15 @@ const VERBS: ReadonlySet = new Set([ /** Structural argv parse. argv excludes the `goalplan` kind token. */ export function parseGoalplanCliArgs(argv: string[], cwd: string): GoalplanCliArgs | GoalplanCliParseError { const verb = (argv[0] ?? "").toLowerCase(); + // #47: `--help` on a sibling command used to be reported as an unknown verb, so an + // agent that followed `cxc --help`'s own pointer hit a non-zero exit and had to + // discover every flag one rejection at a time. Same contract as orchestrate. + if (verb === "help" || verb === "--help" || verb === "-h") { + return { verb: "help", cwd, criteria: [] }; + } if (!VERBS.has(verb)) { return { - error: `unknown loop verb '${argv[0] ?? ""}' (expected init|show|validate|steer|add-criterion|add-work-phase)`, + error: `unknown loop verb '${argv[0] ?? ""}' (expected init|show|validate|steer|add-criterion|add-work-phase); run cxc loop --help`, }; } const out: GoalplanCliArgs = { verb: verb as GoalplanVerb, cwd, criteria: [] }; @@ -276,7 +282,39 @@ function renderPlanLines(plan: Goalplan): string { return lines.join("\n"); } +/** + * #47: every flag below used to be discoverable only by running the command and + * reading the rejection, one missing argument at a time. The steer batch shape is + * spelled out for the same reason. + */ +export function renderGoalplanHelp(): string { + return [ + "cxc loop — durable goalplan for a multi-cycle PABCD loop", + "", + "Usage:", + " cxc loop init --objective --session [--criterion ]... [--cwd ]", + " cxc loop show (--slug | --objective ) [--cwd ]", + " cxc loop validate --slug [--cwd ]", + " cxc loop steer --session --slug --batch-json [--cwd ]", + " cxc loop add-work-phase --session --slug --id --title ", + " cxc loop add-criterion --session --slug --criterion [--surface logic|web|tui]", + " cxc loop --help", + "", + "Notes:", + " Mutating verbs require --session ; show and validate are read-only.", + " The goalplan lives at /.codexclaw/goalplans//goalplan.json, so --cwd", + " matters when the process cwd is not the workspace you are planning in.", + "", + "steer --batch-json expects an object with:", + ' { "idempotencyKey": "", "rationale": "", "evidence": "",', + ' "ops": [ { "kind": "annotate", "note": "..." } ] }', + " op kinds: annotate | add-criterion | add-work-phase (all additive — steering", + " cannot weaken a completion criterion).", + ].join("\n"); +} + export function runGoalplanCli(args: GoalplanCliArgs): GoalplanCliResult { + if (args.verb === "help") return { output: renderGoalplanHelp(), code: 0 }; if (args.verb === "init") { const objective = (args.objective ?? "").trim(); if (objective.length === 0) { diff --git a/plugins/codexclaw/components/pabcd-state/src/receipt-cli.ts b/plugins/codexclaw/components/pabcd-state/src/receipt-cli.ts index 59d7450..50b88a5 100644 --- a/plugins/codexclaw/components/pabcd-state/src/receipt-cli.ts +++ b/plugins/codexclaw/components/pabcd-state/src/receipt-cli.ts @@ -20,7 +20,7 @@ import { captureSourceIdentity, compareSource } from "./source-identity.ts"; import { STATE_DIR, sanitizeKey } from "./state.ts"; export interface ReceiptCliArgs { - verb: "test"; + verb: "test" | "help"; cwd: string; session?: string; command: string[]; @@ -31,7 +31,14 @@ export interface ReceiptCliParseError { error: string } /** Everything after `--` is the command; nothing before it is. */ export function parseReceiptCliArgs(argv: string[], cwd: string): ReceiptCliArgs | ReceiptCliParseError { const verb = (argv[0] ?? "").toLowerCase(); - if (verb !== "test") return { error: `unknown receipt verb '${argv[0] ?? ""}' (expected test)` }; + // #47: --help was reported as an unknown verb, so the flags could only be learned + // from rejections. + if (verb === "help" || verb === "--help" || verb === "-h") { + return { verb: "help", cwd, command: [] }; + } + if (verb !== "test") { + return { error: `unknown receipt verb '${argv[0] ?? ""}' (expected test); run cxc receipt --help` }; + } const out: ReceiptCliArgs = { verb: "test", cwd, command: [] }; let i = 1; for (; i < argv.length; i++) { @@ -54,6 +61,27 @@ export function receiptPathFor(cwd: string, sessionId: string): string { export interface ReceiptCliResult { output: string; code: number } export function runReceiptCli(args: ReceiptCliArgs): ReceiptCliResult { + if (args.verb === "help") { + return { + output: [ + "cxc receipt — record a check receipt that binds a command's result to a source tree", + "", + "Usage:", + " cxc receipt test --session [--cwd ] -- [args...]", + " cxc receipt --help", + "", + "Notes:", + " Everything after `--` is the command; nothing before it is.", + " The session must be at phase C — a receipt is produced during Check.", + " The receipt is written to /.codexclaw/evidence//test-receipt.json", + " and is refused if the command changes the source while it runs.", + "", + "Example:", + " cxc receipt test --session -- npm test", + ].join("\n"), + code: 0, + }; + } const session = (args.session ?? "").trim(); if (session.length === 0) return { output: "receipt test: --session is required", code: 1 }; if (args.command.length === 0) { diff --git a/plugins/codexclaw/components/pabcd-state/src/scan-cli.ts b/plugins/codexclaw/components/pabcd-state/src/scan-cli.ts index a0d595e..7292901 100644 --- a/plugins/codexclaw/components/pabcd-state/src/scan-cli.ts +++ b/plugins/codexclaw/components/pabcd-state/src/scan-cli.ts @@ -44,7 +44,7 @@ import { readQaEvents } from "./interview-ledger.ts"; import { computeNextScanRound } from "./rescan-coordinator.ts"; export interface ScanCliArgs { - action: "record"; + action: "record" | "help"; sessionId: string; contradictionCount: number; highContradictionCount: number; @@ -83,8 +83,20 @@ export function parseScanCliArgs( cwd: string, ): ScanCliArgs | { error: string } { const [action, ...rest] = argv; + // #47: --help was an unknown action, and --cwd was rejected outright even though + // orchestrate accepts it — which stranded a live session whose answer ledger lived + // in a different tree from the process cwd. + if (action === "help" || action === "--help" || action === "-h") { + return { + action: "help", + sessionId: "", + contradictionCount: 0, + highContradictionCount: 0, + cwd, + }; + } if (action !== "record") { - return { error: `unknown scan action '${action ?? ""}' — usage: scan record --session [--contradictions N] [--high N]` }; + return { error: `unknown scan action '${action ?? ""}'; run cxc scan --help` }; } let sessionId = ""; let contradictionCount = 0; @@ -98,11 +110,17 @@ export function parseScanCliArgs( const known: Array<{ dimension: Dimension; text: string }> = []; const unknown: Array<{ dimension: Dimension; text: string }> = []; const confidence: Partial> = {}; + let resolvedCwd = cwd; for (let i = 0; i < rest.length; i += 1) { const arg = rest[i]; if (arg === "--session") { sessionId = rest[i + 1] ?? ""; i += 1; + } else if (arg === "--cwd") { + // #47: orchestrate documents and accepts --cwd; scan rejected it, which + // stranded a session whose answer ledger was in a different tree. + resolvedCwd = rest[i + 1] ?? cwd; + i += 1; } else if (arg === "--contradictions") { contradictionCount = Number.parseInt(rest[i + 1] ?? "", 10); i += 1; @@ -173,7 +191,7 @@ export function parseScanCliArgs( sessionId, contradictionCount, highContradictionCount, - cwd, + cwd: resolvedCwd, ...(derive ? { derive } : {}), ...(Object.keys(map).length > 0 ? { map } : {}), ...(Object.keys(dims).length > 0 ? { dims } : {}), @@ -264,6 +282,28 @@ function deriveLevel(score: DimensionScore): DimensionLevel { } export function runScanCli(args: ScanCliArgs): { output: string; code: number } { + if (args.action === "help") { + return { + output: [ + "cxc scan — record an interview rescan round and fold answers into the tracker", + "", + "Usage:", + " cxc scan record --session [--cwd ] [--contradictions N] [--high N]", + " [--derive] [--map =]...", + " [--dim =]... [--known =]...", + " [--unknown =]... [--confidence =<0..1>]...", + " cxc scan --help", + "", + "Notes:", + " --session is required; there is no latest-session fallback for a mutating command.", + " --cwd matters when the answer ledger lives outside the process cwd:", + " answers are read from /.codexclaw/interviews/.jsonl.", + " --derive folds captured answers in; --map attributes a questionId to a dimension.", + " --dim cannot set 'max' — that level gates I->P and must be attested.", + ].join("\n"), + code: 0, + }; + } try { const state: State = readState(args.cwd, args.sessionId); const tracker: InterviewTracker = state.interview ?? defaultInterview(0); diff --git a/plugins/codexclaw/components/pabcd-state/test/help-verbs.test.ts b/plugins/codexclaw/components/pabcd-state/test/help-verbs.test.ts new file mode 100644 index 0000000..6718838 --- /dev/null +++ b/plugins/codexclaw/components/pabcd-state/test/help-verbs.test.ts @@ -0,0 +1,77 @@ +/** + * help-verbs.test.ts — issue #47: `cxc orchestrate --help` worked while every + * sibling reported `--help` as an unknown verb and exited non-zero. The top-level + * help points at those commands, so an agent that followed the pointer hit a brick + * wall and had to learn each flag from a rejection. + * + * These assert the CONTRACT (exit 0 plus usage), not the wording, so the help text + * can be edited without churning the test. + */ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { parseGoalplanCliArgs, runGoalplanCli } from "../src/goalplan-cli.ts"; +import { parseReceiptCliArgs, runReceiptCli } from "../src/receipt-cli.ts"; +import { parseScanCliArgs, runScanCli } from "../src/scan-cli.ts"; + +const CWD = "/unused"; + +for (const token of ["help", "--help", "-h"]) { + test(`loop ${token} prints usage and exits 0`, () => { + const args = parseGoalplanCliArgs([token], CWD); + assert.ok(!("error" in args), `${token} must not be an unknown verb`); + const r = runGoalplanCli(args as never); + assert.equal(r.code, 0); + assert.match(r.output, /Usage:/); + // The flags that could previously only be discovered from rejections. + assert.match(r.output, /--session /); + assert.match(r.output, /--batch-json/); + assert.match(r.output, /idempotencyKey/); + }); + + test(`receipt ${token} prints usage and exits 0`, () => { + const args = parseReceiptCliArgs([token], CWD); + assert.ok(!("error" in args), `${token} must not be an unknown verb`); + const r = runReceiptCli(args as never); + assert.equal(r.code, 0); + assert.match(r.output, /Usage:/); + assert.match(r.output, /-- /); + }); + + test(`scan ${token} prints usage and exits 0`, () => { + const args = parseScanCliArgs([token], CWD); + assert.ok(!("error" in args), `${token} must not be an unknown action`); + const r = runScanCli(args as never); + assert.equal(r.code, 0); + assert.match(r.output, /Usage:/); + assert.match(r.output, /--cwd /); + }); +} + +// An unknown verb must still fail — but now it names the way out. +test("an unknown verb points at --help instead of just listing verbs", () => { + const loop = parseGoalplanCliArgs(["nope"], CWD); + assert.ok("error" in loop); + assert.match((loop as { error: string }).error, /cxc loop --help/); + + const receipt = parseReceiptCliArgs(["nope"], CWD); + assert.ok("error" in receipt); + assert.match((receipt as { error: string }).error, /cxc receipt --help/); + + const scan = parseScanCliArgs(["nope"], CWD); + assert.ok("error" in scan); + assert.match((scan as { error: string }).error, /cxc scan --help/); +}); + +// #47 also reported `scan record --cwd` as rejected while orchestrate accepts it. +// That stranded a session whose answer ledger lived outside the process cwd. +test("scan record accepts --cwd, like orchestrate does", () => { + const args = parseScanCliArgs(["record", "--session", "s1", "--cwd", "/elsewhere"], CWD); + assert.ok(!("error" in args), "scan record must accept --cwd"); + assert.equal((args as { cwd: string }).cwd, "/elsewhere"); +}); + +test("scan record without --cwd still resolves against the process cwd", () => { + const args = parseScanCliArgs(["record", "--session", "s1"], CWD); + assert.ok(!("error" in args)); + assert.equal((args as { cwd: string }).cwd, CWD); +}); From c757d99d9ff9a603392c2d48218e6d61c9aa0c19 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Sat, 22 Aug 2026 23:06:16 +0900 Subject: [PATCH 2/5] fix(cli): cxc --version also from the root entry point (#47) --- README.ko.md | 2 +- README.md | 2 +- README.zh.md | 2 +- bin/codexclaw.mjs | 17 +++++++++++++- plugins/codexclaw/test/cli-usage.test.mjs | 28 +++++++++++++++++++++++ 5 files changed, 47 insertions(+), 4 deletions(-) diff --git a/README.ko.md b/README.ko.md index 7d7532d..4dcc227 100644 --- a/README.ko.md +++ b/README.ko.md @@ -13,7 +13,7 @@

CI - 1,945 tests passing + 1,949 tests passing 28 skills 22 hooks Documentation diff --git a/README.md b/README.md index 91e07b1..7fa2625 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@

CI - 1,945 tests passing + 1,949 tests passing 28 skills 22 hooks Documentation diff --git a/README.zh.md b/README.zh.md index c7230b7..a0637cf 100644 --- a/README.zh.md +++ b/README.zh.md @@ -13,7 +13,7 @@

CI - 1,945 tests passing + 1,949 tests passing 28 skills 22 hooks Documentation diff --git a/bin/codexclaw.mjs b/bin/codexclaw.mjs index 0dd93cd..b1c1f1f 100755 --- a/bin/codexclaw.mjs +++ b/bin/codexclaw.mjs @@ -30,7 +30,7 @@ import { spawnSync } from "node:child_process"; import { delimiter, dirname, isAbsolute, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -import { existsSync, realpathSync, rmSync } from "node:fs"; +import { existsSync, readFileSync, realpathSync, rmSync } from "node:fs"; import { homedir } from "node:os"; const here = dirname(fileURLToPath(import.meta.url)); @@ -421,6 +421,21 @@ if (isMain) switch (cmd) { console.log(renderTopLevelHelp()); process.exit(0); break; + // #47: `cxc --version` was an unknown command, so the only way to learn which + // payload was live was to read the cache directory name. + case "version": + case "--version": + case "-v": { + try { + const manifestPath = join(here, "..", "plugins", "codexclaw", ".codex-plugin", "plugin.json"); + console.log(JSON.parse(readFileSync(manifestPath, "utf8")).version ?? "unknown"); + process.exit(0); + } catch (err) { + console.error(`cxc --version: could not read the plugin manifest (${err.code ?? err.message})`); + process.exit(1); + } + break; + } case "enable": process.exit(runConfigGuard("enable")); break; diff --git a/plugins/codexclaw/test/cli-usage.test.mjs b/plugins/codexclaw/test/cli-usage.test.mjs index a9da9f4..5484e1e 100644 --- a/plugins/codexclaw/test/cli-usage.test.mjs +++ b/plugins/codexclaw/test/cli-usage.test.mjs @@ -35,6 +35,34 @@ test("top-level CLI unknown command fails with recovery hint", () => { assert.match(res.stderr, /cxc --help/); }); +// #47: the top-level help points at the sibling commands, and following that +// pointer failed — --help was an unknown verb on loop/scan/receipt, and +// --version was an unknown command. These run the REAL binary, because the +// defect was in the argv dispatch rather than in any parser under test. +for (const command of ["loop", "scan", "receipt"]) { + test(`${command} --help exits 0 with usage, like orchestrate`, () => { + for (const flag of ["--help", "-h"]) { + const res = spawnSync("node", [cli, command, flag], { cwd: repoRoot, encoding: "utf8" }); + assert.equal(res.status, 0, `${command} ${flag} exited ${res.status}: ${res.stderr}`); + assert.match(res.stdout, /Usage:/); + assert.match(res.stdout, new RegExp(`cxc ${command}`)); + } + }); +} + +// There are TWO entry points — bin/codexclaw.mjs and plugins/codexclaw/bin/cxc.mjs — +// and fixing only one is exactly the mistake this case exists to catch. +test("--version prints the plugin version from both entry points", () => { + const entries = [cli, join(repoRoot, "plugins", "codexclaw", "bin", "cxc.mjs")]; + for (const entry of entries) { + for (const flag of ["--version", "-v", "version"]) { + const res = spawnSync("node", [entry, flag], { cwd: repoRoot, encoding: "utf8" }); + assert.equal(res.status, 0, `${entry} ${flag} exited ${res.status}: ${res.stderr}`); + assert.match(res.stdout.trim(), /^\d+\.\d+\.\d+/); + } + } +}); + test("top-level CLI delegates orchestrate help", () => { const res = spawnSync("node", [cli, "orchestrate", "--help"], { cwd: repoRoot, encoding: "utf8" }); assert.equal(res.status, 0); From 6ee835df7dcad1c4adad8b0730594d8e30e768ee Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Sat, 22 Aug 2026 23:12:41 +0900 Subject: [PATCH 3/5] fix(state): surface the cwd session split instead of silently picking a side (#48) Session files live at /.codexclaw/sessions/.json. The id is stable, the cwd is not, so a thread whose process cwd is one tree while its work is in another has two FSMs under one id and nothing says so. The reporter closed D in the wiki tree and the next turn re-injected Interview from the other copy. Pinning the store to the session-start workspace would invalidate every existing session file with no migration path, so this makes the split visible rather than flipping a coin. findForeignSessionCopies looks for the same id in plausible sibling roots and orchestrate status reports what it finds: session= phase=IDLE ... WARNING: this session id also has state in 1 other tree(s); the phase above describes THIS cwd only. also at: .../cxc-split-a/.codexclaw/sessions/.json Detection only - the other tree is never read from or written to. The candidate list is deliberately shallow because this is a warning on a read-only command, not a filesystem crawl. Also from the same issue: loop show --slug was cwd-only and printed "no plan found" from the wrong tree. loop init --session already binds the slug into the session file, so resolveSlug now falls back to that binding - which also makes the session the source of truth rather than whichever directory the shell was in. The underlying cwd-keyed storage is unchanged; you are now told about the split rather than misled by it. --- README.ko.md | 2 +- README.md | 2 +- README.zh.md | 2 +- .../020_session_split.md | 66 ++++++++++++++++ .../pabcd-state/dist/goalplan-cli.js | 13 +++- .../pabcd-state/dist/orchestrate-cli.js | 62 ++++++++++++++- .../components/pabcd-state/dist/state.js | 39 +++++++++- .../pabcd-state/src/goalplan-cli.ts | 13 +++- .../pabcd-state/src/orchestrate-cli.ts | 62 ++++++++++++++- .../components/pabcd-state/src/state.ts | 39 +++++++++- .../pabcd-state/test/session-split.test.ts | 77 +++++++++++++++++++ 11 files changed, 360 insertions(+), 17 deletions(-) create mode 100644 devlog/_plan/260822_attest_win_parity/020_session_split.md create mode 100644 plugins/codexclaw/components/pabcd-state/test/session-split.test.ts diff --git a/README.ko.md b/README.ko.md index 4dcc227..1ad1773 100644 --- a/README.ko.md +++ b/README.ko.md @@ -13,7 +13,7 @@

CI - 1,949 tests passing + 1,953 tests passing 28 skills 22 hooks Documentation diff --git a/README.md b/README.md index 7fa2625..2377564 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@

CI - 1,949 tests passing + 1,953 tests passing 28 skills 22 hooks Documentation diff --git a/README.zh.md b/README.zh.md index a0637cf..154d784 100644 --- a/README.zh.md +++ b/README.zh.md @@ -13,7 +13,7 @@

CI - 1,949 tests passing + 1,953 tests passing 28 skills 22 hooks Documentation diff --git a/devlog/_plan/260822_attest_win_parity/020_session_split.md b/devlog/_plan/260822_attest_win_parity/020_session_split.md new file mode 100644 index 0000000..c3a8603 --- /dev/null +++ b/devlog/_plan/260822_attest_win_parity/020_session_split.md @@ -0,0 +1,66 @@ +# 020 - issue #48: one session id, two FSMs + +## The failure + +Session files live at `/.codexclaw/sessions/.json`. The id is stable; +the cwd is not. A Codex thread whose process cwd is one tree while its work is in +another therefore has TWO FSMs under the same id, and nothing says so: + +``` +~/.cli-jaw/.codexclaw/sessions/.json phase=I 13:16Z +~/kim_wiki/.codexclaw/sessions/.json phase=IDLE 13:22Z +``` + +The reporter closed D in the wiki tree and the next turn re-injected Interview +from the cli-jaw copy. `scan record --derive` matched nothing because the answer +ledger was in the other tree, and reported it as a warning rather than an error. + +## Why not just move the store + +The obvious fix — pin session files to the session-start workspace — would +invalidate every session file that exists today, in every checkout, with no +migration path. The failure is bad but it is not worth a flag day. + +So the fix makes the split **visible** instead of silently picking one side. + +## Fix + +`findForeignSessionCopies(cwd, sessionId, candidates)` looks for the same id in +plausible sibling roots and returns the paths it finds. Detection only: the other +tree is never read from or written to. + +`orchestrate status` now reports it: + +``` +session=split-demo-0001 phase=IDLE interview=false auditPassed=false checkPassed=false +WARNING: this session id also has state in 1 other tree(s); the phase above describes THIS cwd only. + also at: C:\Users\super\cxc-split-a\.codexclaw\sessions\split-demo-0001.json + Pass --cwd to address a specific tree. +``` + +That is exactly the reporter's scenario: `phase=IDLE` here, a live cycle next +door, and now a line that says so. The `--json` form carries `alsoFoundAt`. + +The candidate list is deliberately shallow — immediate children of `$HOME` plus +the parent of cwd — because this is a warning on a read-only command, not a +filesystem crawl. `node_modules` and `AppData` are skipped as places a workspace +never lives. + +## `loop show --session` + +The same issue reported that `loop show --slug` is cwd-only and prints +`no plan found` from the wrong tree. `loop init --session` already binds the slug +into the session file, so `resolveSlug` now falls back to that binding: + +``` +cxc loop show --session # no 47-character slug to retype +``` + +This also makes the session the source of truth when the id exists in more than +one tree, rather than whichever directory the shell happened to be in. + +## Not fixed here + +The underlying cwd-keyed storage is unchanged, so two trees still diverge — you +are now told about it rather than misled by it. Making `--session` resolve to one +canonical store is a larger change that needs a migration story. diff --git a/plugins/codexclaw/components/pabcd-state/dist/goalplan-cli.js b/plugins/codexclaw/components/pabcd-state/dist/goalplan-cli.js index 0b4607a..a75aff4 100644 --- a/plugins/codexclaw/components/pabcd-state/dist/goalplan-cli.js +++ b/plugins/codexclaw/components/pabcd-state/dist/goalplan-cli.js @@ -110,6 +110,14 @@ export function parseGoalplanCliArgs(argv , cwd ) function resolveSlug(args ) { if (typeof args.slug === "string" && args.slug.length > 0) return deriveSlug(args.slug); if (typeof args.objective === "string" && args.objective.length > 0) return deriveSlug(args.objective); + // #48: `loop init --session` already binds the slug into the session file, so a + // later `show`/`validate` can recover it without the caller re-typing a + // 47-character derived slug. This also makes the session the source of truth + // when the same id has state in more than one tree. + if (typeof args.session === "string" && args.session.length > 0) { + const bound = readState(args.cwd, args.session).slug; + if (typeof bound === "string" && bound.length > 0) return bound; + } return null; } @@ -351,7 +359,10 @@ export function runGoalplanCli(args ) { const slug = resolveSlug(args); if (!slug) { - return { output: `loop ${args.verb}: --slug "" or --objective "" is required`, code: 1 }; + return { + output: `loop ${args.verb}: --slug "", --objective "", or --session (with a bound plan) is required`, + code: 1, + }; } const plan = readGoalplan(args.cwd, slug); if (!plan) { diff --git a/plugins/codexclaw/components/pabcd-state/dist/orchestrate-cli.js b/plugins/codexclaw/components/pabcd-state/dist/orchestrate-cli.js index 78dcf79..d5d0145 100644 --- a/plugins/codexclaw/components/pabcd-state/dist/orchestrate-cli.js +++ b/plugins/codexclaw/components/pabcd-state/dist/orchestrate-cli.js @@ -15,6 +15,7 @@ */ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; import { join, resolve } from "node:path"; +import { homedir } from "node:os"; import { coerceAttest, validateWorkPhaseBinding, GATED_TRANSITIONS, } from "./attest.js"; import { canEnter, transition } from "./fsm.js"; import { validatePlanArtifacts } from "./plan-gate.js"; @@ -108,6 +109,7 @@ import { appendLedger, readState, writeState, + findForeignSessionCopies, STATE_DIR, SESSIONS_SUBDIR, @@ -303,9 +305,26 @@ function renderPhaseContext(state , sessionId ) { return `current=${state.phase} session=${sessionId}`; } -function renderStatus(state , json ) { - if (json) return JSON.stringify({ phase: state.phase, flags: state.flags, sessionId: state.sessionId }); - return `session=${state.sessionId} phase=${state.phase} interview=${state.flags.interview} auditPassed=${state.flags.auditPassed} checkPassed=${state.flags.checkPassed}`; +function renderStatus(state , json , elsewhere = []) { + if (json) { + return JSON.stringify({ + phase: state.phase, + flags: state.flags, + sessionId: state.sessionId, + ...(elsewhere.length > 0 ? { alsoFoundAt: elsewhere } : {}), + }); + } + const line = `session=${state.sessionId} phase=${state.phase} interview=${state.flags.interview} auditPassed=${state.flags.auditPassed} checkPassed=${state.flags.checkPassed}`; + // #48: the same id in two trees means this line describes only ONE of them. + // Reporting IDLE for a cycle that is really in flight next door is the failure + // this warning exists to prevent. + if (elsewhere.length === 0) return line; + return [ + line, + `WARNING: this session id also has state in ${elsewhere.length} other tree(s); the phase above describes THIS cwd only.`, + ...elsewhere.map((p) => ` also at: ${p}`), + " Pass --cwd to address a specific tree.", + ].join("\n"); } export function renderOrchestrateParseError(error ) { @@ -334,7 +353,14 @@ export function runOrchestrateCli(args // status: read-only. With no session, report it (don't create one). if (args.verb === "status") { if (!sessionId) return { code: 0, output: "no active session" }; - return { code: 0, output: renderStatus(readState(args.cwd, sessionId), args.json) }; + return { + code: 0, + output: renderStatus( + readState(args.cwd, sessionId), + args.json, + findForeignSessionCopies(args.cwd, sessionId, siblingRoots(args.cwd)), + ), + }; } // G3 (fork-FSM collision, 260707): mutating verbs REQUIRE an explicit --session. @@ -651,3 +677,31 @@ export function runOrchestrateCli(args }); return { code: 0, output: `orchestrate ${args.verb}: current=${state.phase} -> ${result.state.phase} (${state.phase} → ${result.state.phase}, session ${sessionId})` }; } +/** + * #48: candidate trees to check for the SAME session id. Deliberately shallow — + * the immediate children of $HOME plus the parent of cwd — because this is a + * warning path on a read-only command, not a filesystem crawl. Anything deeper + * would cost more than the warning is worth. + */ +function siblingRoots(cwd ) { + const roots = []; + let home ; + try { + home = homedir(); + } catch { + return roots; + } + try { + for (const entry of readdirSync(home, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + // Skip the noisy ones a workspace never lives in. + if (entry.name === "node_modules" || entry.name === "AppData") continue; + roots.push(join(home, entry.name)); + } + } catch { + // an unreadable home is not an error for a warning path + } + const parent = resolve(cwd, ".."); + if (parent !== resolve(cwd)) roots.push(parent); + return roots; +} diff --git a/plugins/codexclaw/components/pabcd-state/dist/state.js b/plugins/codexclaw/components/pabcd-state/dist/state.js index 3e1c96c..38bfe92 100644 --- a/plugins/codexclaw/components/pabcd-state/dist/state.js +++ b/plugins/codexclaw/components/pabcd-state/dist/state.js @@ -1,6 +1,6 @@ -import { mkdirSync, readFileSync, writeFileSync, appendFileSync, linkSync, rmSync } from "node:fs"; +import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync, linkSync, rmSync } from "node:fs"; import { randomUUID } from "node:crypto"; -import { join } from "node:path"; +import { join, resolve } from "node:path"; import { renameWithRetry } from "./atomic-write.js"; import { reconstructInterview, normalizeInterview, isInterviewReady } from "./interview.js"; @@ -181,6 +181,41 @@ function statePath(cwd , sessionId ) { return join(sessionsDir(cwd), `${sanitizeKey(sessionId)}.json`); } +/** + * #48: session files live at `/.codexclaw/sessions/.json`, so the SAME + * `--session` id resolves to different state depending on where the process was + * started. A Codex thread whose cwd is one tree while its work is in another then + * interviews one FSM and orchestrates the other, and `status` reports IDLE for a + * cycle that is genuinely in flight elsewhere. + * + * Changing the storage location would break every existing session, so instead we + * make the split VISIBLE: look for the same id in the nearest plausible sibling + * roots and report what was found. Detection only — nothing is read from or + * written to the other tree. + */ +export function findForeignSessionCopies( + cwd , + sessionId , + candidates , +) { + const mine = resolve(statePath(cwd, sessionId)); + const seen = new Set ([mine]); + const found = []; + for (const root of candidates) { + if (typeof root !== "string" || root.length === 0) continue; + let candidate ; + try { + candidate = resolve(statePath(root, sessionId)); + } catch { + continue; + } + if (seen.has(candidate)) continue; + seen.add(candidate); + if (existsSync(candidate)) found.push(candidate); + } + return found; +} + /** * Materialize a fresh Codex session without resetting a resumed one. * diff --git a/plugins/codexclaw/components/pabcd-state/src/goalplan-cli.ts b/plugins/codexclaw/components/pabcd-state/src/goalplan-cli.ts index 4fd72ec..d0901d7 100644 --- a/plugins/codexclaw/components/pabcd-state/src/goalplan-cli.ts +++ b/plugins/codexclaw/components/pabcd-state/src/goalplan-cli.ts @@ -110,6 +110,14 @@ export interface GoalplanCliResult { function resolveSlug(args: GoalplanCliArgs): string | null { if (typeof args.slug === "string" && args.slug.length > 0) return deriveSlug(args.slug); if (typeof args.objective === "string" && args.objective.length > 0) return deriveSlug(args.objective); + // #48: `loop init --session` already binds the slug into the session file, so a + // later `show`/`validate` can recover it without the caller re-typing a + // 47-character derived slug. This also makes the session the source of truth + // when the same id has state in more than one tree. + if (typeof args.session === "string" && args.session.length > 0) { + const bound = readState(args.cwd, args.session).slug; + if (typeof bound === "string" && bound.length > 0) return bound; + } return null; } @@ -351,7 +359,10 @@ export function runGoalplanCli(args: GoalplanCliArgs): GoalplanCliResult { const slug = resolveSlug(args); if (!slug) { - return { output: `loop ${args.verb}: --slug "" or --objective "" is required`, code: 1 }; + return { + output: `loop ${args.verb}: --slug "", --objective "", or --session (with a bound plan) is required`, + code: 1, + }; } const plan = readGoalplan(args.cwd, slug); if (!plan) { diff --git a/plugins/codexclaw/components/pabcd-state/src/orchestrate-cli.ts b/plugins/codexclaw/components/pabcd-state/src/orchestrate-cli.ts index 77d38a8..14057f5 100644 --- a/plugins/codexclaw/components/pabcd-state/src/orchestrate-cli.ts +++ b/plugins/codexclaw/components/pabcd-state/src/orchestrate-cli.ts @@ -15,6 +15,7 @@ */ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; import { join, resolve } from "node:path"; +import { homedir } from "node:os"; import { coerceAttest, validateWorkPhaseBinding, GATED_TRANSITIONS, type Attestation } from "./attest.ts"; import { canEnter, transition } from "./fsm.ts"; import { validatePlanArtifacts } from "./plan-gate.ts"; @@ -108,6 +109,7 @@ import { appendLedger, readState, writeState, + findForeignSessionCopies, STATE_DIR, SESSIONS_SUBDIR, type Phase, @@ -303,9 +305,26 @@ function renderPhaseContext(state: State, sessionId: string): string { return `current=${state.phase} session=${sessionId}`; } -function renderStatus(state: State, json: boolean): string { - if (json) return JSON.stringify({ phase: state.phase, flags: state.flags, sessionId: state.sessionId }); - return `session=${state.sessionId} phase=${state.phase} interview=${state.flags.interview} auditPassed=${state.flags.auditPassed} checkPassed=${state.flags.checkPassed}`; +function renderStatus(state: State, json: boolean, elsewhere: string[] = []): string { + if (json) { + return JSON.stringify({ + phase: state.phase, + flags: state.flags, + sessionId: state.sessionId, + ...(elsewhere.length > 0 ? { alsoFoundAt: elsewhere } : {}), + }); + } + const line = `session=${state.sessionId} phase=${state.phase} interview=${state.flags.interview} auditPassed=${state.flags.auditPassed} checkPassed=${state.flags.checkPassed}`; + // #48: the same id in two trees means this line describes only ONE of them. + // Reporting IDLE for a cycle that is really in flight next door is the failure + // this warning exists to prevent. + if (elsewhere.length === 0) return line; + return [ + line, + `WARNING: this session id also has state in ${elsewhere.length} other tree(s); the phase above describes THIS cwd only.`, + ...elsewhere.map((p) => ` also at: ${p}`), + " Pass --cwd to address a specific tree.", + ].join("\n"); } export function renderOrchestrateParseError(error: CliParseError): string { @@ -334,7 +353,14 @@ export function runOrchestrateCli(args: OrchestrateCliArgs | OrchestrateCliHelpA // status: read-only. With no session, report it (don't create one). if (args.verb === "status") { if (!sessionId) return { code: 0, output: "no active session" }; - return { code: 0, output: renderStatus(readState(args.cwd, sessionId), args.json) }; + return { + code: 0, + output: renderStatus( + readState(args.cwd, sessionId), + args.json, + findForeignSessionCopies(args.cwd, sessionId, siblingRoots(args.cwd)), + ), + }; } // G3 (fork-FSM collision, 260707): mutating verbs REQUIRE an explicit --session. @@ -651,3 +677,31 @@ export function runOrchestrateCli(args: OrchestrateCliArgs | OrchestrateCliHelpA }); return { code: 0, output: `orchestrate ${args.verb}: current=${state.phase} -> ${result.state.phase} (${state.phase} → ${result.state.phase}, session ${sessionId})` }; } +/** + * #48: candidate trees to check for the SAME session id. Deliberately shallow — + * the immediate children of $HOME plus the parent of cwd — because this is a + * warning path on a read-only command, not a filesystem crawl. Anything deeper + * would cost more than the warning is worth. + */ +function siblingRoots(cwd: string): string[] { + const roots: string[] = []; + let home: string; + try { + home = homedir(); + } catch { + return roots; + } + try { + for (const entry of readdirSync(home, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + // Skip the noisy ones a workspace never lives in. + if (entry.name === "node_modules" || entry.name === "AppData") continue; + roots.push(join(home, entry.name)); + } + } catch { + // an unreadable home is not an error for a warning path + } + const parent = resolve(cwd, ".."); + if (parent !== resolve(cwd)) roots.push(parent); + return roots; +} diff --git a/plugins/codexclaw/components/pabcd-state/src/state.ts b/plugins/codexclaw/components/pabcd-state/src/state.ts index c317730..8f82430 100644 --- a/plugins/codexclaw/components/pabcd-state/src/state.ts +++ b/plugins/codexclaw/components/pabcd-state/src/state.ts @@ -1,6 +1,6 @@ -import { mkdirSync, readFileSync, writeFileSync, appendFileSync, linkSync, rmSync } from "node:fs"; +import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync, linkSync, rmSync } from "node:fs"; import { randomUUID } from "node:crypto"; -import { join } from "node:path"; +import { join, resolve } from "node:path"; import { renameWithRetry } from "./atomic-write.ts"; import { type InterviewTracker, reconstructInterview, normalizeInterview, isInterviewReady } from "./interview.ts"; import type { SourceIdentity } from "./source-identity.ts"; @@ -181,6 +181,41 @@ function statePath(cwd: string, sessionId: string): string { return join(sessionsDir(cwd), `${sanitizeKey(sessionId)}.json`); } +/** + * #48: session files live at `/.codexclaw/sessions/.json`, so the SAME + * `--session` id resolves to different state depending on where the process was + * started. A Codex thread whose cwd is one tree while its work is in another then + * interviews one FSM and orchestrates the other, and `status` reports IDLE for a + * cycle that is genuinely in flight elsewhere. + * + * Changing the storage location would break every existing session, so instead we + * make the split VISIBLE: look for the same id in the nearest plausible sibling + * roots and report what was found. Detection only — nothing is read from or + * written to the other tree. + */ +export function findForeignSessionCopies( + cwd: string, + sessionId: string, + candidates: string[], +): string[] { + const mine = resolve(statePath(cwd, sessionId)); + const seen = new Set([mine]); + const found: string[] = []; + for (const root of candidates) { + if (typeof root !== "string" || root.length === 0) continue; + let candidate: string; + try { + candidate = resolve(statePath(root, sessionId)); + } catch { + continue; + } + if (seen.has(candidate)) continue; + seen.add(candidate); + if (existsSync(candidate)) found.push(candidate); + } + return found; +} + /** * Materialize a fresh Codex session without resetting a resumed one. * diff --git a/plugins/codexclaw/components/pabcd-state/test/session-split.test.ts b/plugins/codexclaw/components/pabcd-state/test/session-split.test.ts new file mode 100644 index 0000000..9e60eee --- /dev/null +++ b/plugins/codexclaw/components/pabcd-state/test/session-split.test.ts @@ -0,0 +1,77 @@ +/** + * session-split.test.ts — issue #48: session files live at + * `/.codexclaw/sessions/.json`, so the same `--session` id resolves to + * different state depending on where the process started. + * + * Reported symptom: a thread whose cwd was `~/.cli-jaw` while its work was in + * `~/kim_wiki` interviewed one FSM and orchestrated the other. `status` said + * `phase=IDLE` for a cycle that had just closed D in the other tree, and the next + * turn re-injected Interview from the stale copy. + * + * Relocating the store would break every existing session, so the fix makes the + * split VISIBLE instead. Detection only: the other tree is never read or written. + */ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { findForeignSessionCopies, defaultState, STATE_DIR } from "../src/state.ts"; + +function treeWithSession(id: string): string { + const root = mkdtempSync(join(tmpdir(), "cxc-split-")); + mkdirSync(join(root, STATE_DIR, "sessions"), { recursive: true }); + writeFileSync( + join(root, STATE_DIR, "sessions", `${id}.json`), + JSON.stringify(defaultState(id), null, 2), + ); + return root; +} + +test("a session id present in a sibling tree is reported", () => { + const mine = treeWithSession("s-dup"); + const other = treeWithSession("s-dup"); + try { + const found = findForeignSessionCopies(mine, "s-dup", [other]); + assert.equal(found.length, 1); + assert.match(found[0], /s-dup\.json$/); + assert.ok(found[0].startsWith(other), "must name the OTHER tree, not this one"); + } finally { + rmSync(mine, { recursive: true, force: true }); + rmSync(other, { recursive: true, force: true }); + } +}); + +test("the caller's own tree is never reported as foreign", () => { + const mine = treeWithSession("s-self"); + try { + // Passing your own cwd as a candidate must not produce a self-warning. + assert.deepEqual(findForeignSessionCopies(mine, "s-self", [mine]), []); + } finally { + rmSync(mine, { recursive: true, force: true }); + } +}); + +test("a candidate without that session is not reported", () => { + const mine = treeWithSession("s-only"); + const bare = mkdtempSync(join(tmpdir(), "cxc-bare-")); + try { + assert.deepEqual(findForeignSessionCopies(mine, "s-only", [bare]), []); + } finally { + rmSync(mine, { recursive: true, force: true }); + rmSync(bare, { recursive: true, force: true }); + } +}); + +test("unreadable or missing candidates are skipped, not thrown", () => { + const mine = treeWithSession("s-safe"); + try { + const found = findForeignSessionCopies(mine, "s-safe", [ + join(tmpdir(), "cxc-does-not-exist-" + Date.now()), + "", + ]); + assert.deepEqual(found, []); + } finally { + rmSync(mine, { recursive: true, force: true }); + } +}); From d5e35fc8ccde4f9f9a0b74de25b3b936451d634a Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Sat, 22 Aug 2026 23:19:06 +0900 Subject: [PATCH 4/5] fix(receipt,d): stop two gates from forcing the forgery they exist to prevent (#49) receipt test refused a receipt when the check command dirtied the tree, even when the dirty files were the artifacts the check exists to rebuild. The ontology validator IS the documented gate for that repo. The reported workaround was to commit the generated files and run a no-op existence check instead - a receipt that certifies nothing. A forged receipt is strictly worse than a loose one, because it satisfies CHECK-BINDING-01 while proving less than no receipt at all. --generated declares paths the check rewrites by design. Repeatable, repo- relative, prefix-matched. Everything undeclared is still refused, verified four ways: undeclared rewrite refused, wrong path declared still refused, a declared FILE does not cover its siblings, and a missing value is a parse error rather than a silent skip. The refusal now names the flag so the next agent finds the sanctioned route instead of reinventing the no-op trick, and the receipt records generatedPaths so a reader sees what was permitted. orchestrate D refused a goalplan whose work-phases were all done, because advanceWorkPhase returns no_active for both 'plan is empty' and 'plan is complete'. The reported workaround was to write a finished phase back to in_progress purely to pass the gate - corrupting the record to satisfy a check about the record. D now closes over a complete plan, and the refusal names which real cause applies (empty, or everything blocked). The goalplan ledger says 'cycle closed over an already-complete plan' rather than 'closed null'. SOURCE-DELTA-01 was also raised. Left unchanged deliberately: I hit it twice in this session and both times it was right. A gate that occasionally annoys beats one that lets an empty B through. --- README.ko.md | 2 +- README.md | 2 +- README.zh.md | 2 +- .../030_receipt_and_d_gates.md | 84 ++++++++++++++ .../components/pabcd-state/dist/goalplan.js | 2 + .../pabcd-state/dist/orchestrate-cli.js | 19 +++- .../pabcd-state/dist/receipt-cli.js | 33 +++++- .../pabcd-state/dist/source-identity.js | 15 +++ .../components/pabcd-state/src/goalplan.ts | 4 +- .../pabcd-state/src/orchestrate-cli.ts | 19 +++- .../components/pabcd-state/src/receipt-cli.ts | 33 +++++- .../pabcd-state/src/source-identity.ts | 15 +++ .../pabcd-state/test/orchestrate-cli.test.ts | 59 +++++++++- .../test/receipt-generated.test.ts | 104 ++++++++++++++++++ 14 files changed, 372 insertions(+), 21 deletions(-) create mode 100644 devlog/_plan/260822_attest_win_parity/030_receipt_and_d_gates.md create mode 100644 plugins/codexclaw/components/pabcd-state/test/receipt-generated.test.ts diff --git a/README.ko.md b/README.ko.md index 1ad1773..807f2bc 100644 --- a/README.ko.md +++ b/README.ko.md @@ -13,7 +13,7 @@

CI - 1,953 tests passing + 1,961 tests passing 28 skills 22 hooks Documentation diff --git a/README.md b/README.md index 2377564..6f7e3fd 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@

CI - 1,953 tests passing + 1,961 tests passing 28 skills 22 hooks Documentation diff --git a/README.zh.md b/README.zh.md index 154d784..df1bce0 100644 --- a/README.zh.md +++ b/README.zh.md @@ -13,7 +13,7 @@

CI - 1,953 tests passing + 1,961 tests passing 28 skills 22 hooks Documentation diff --git a/devlog/_plan/260822_attest_win_parity/030_receipt_and_d_gates.md b/devlog/_plan/260822_attest_win_parity/030_receipt_and_d_gates.md new file mode 100644 index 0000000..64b7e43 --- /dev/null +++ b/devlog/_plan/260822_attest_win_parity/030_receipt_and_d_gates.md @@ -0,0 +1,84 @@ +# 030 - issue #49: two gates that fought legitimate work + +Both halves share a shape: a rule written to prevent forgery ended up forcing it. + +## 1. `receipt test` refused a validator that rebuilds its own artifacts + +``` +검증 통과 +graph.json 생성 — {nodes: 100, ...} +receipt test: the command changed the source while running (working tree went dirty); +no receipt written — a check cannot certify a tree it rewrote +``` + +`validate.py --build` IS the documented ontology gate for that repo. It rewrites +`graph.json` by design. The receipt runner treated that as contamination. + +The reported workaround is the damning part: commit the generated files, then run +`cxc receipt test -- test -f ...` — a no-op existence check. That produces a +receipt that certifies nothing. **A forged receipt is strictly worse than a loose +one**, because it satisfies CHECK-BINDING-01 while proving less than no receipt +at all. + +### Fix: declared, never inferred + +``` +cxc receipt test --session --generated build -- node validate.mjs +``` + +`--generated` is repeatable and takes a repo-relative path; a path covers that +file or that directory. Anything NOT declared is still refused, so the escape +hatch cannot be widened by accident: + +| scenario | result | +|---|---| +| declared path rewritten | receipt written | +| undeclared path rewritten | still refused | +| wrong path declared, real file rewritten | still refused | +| `build/graph.json` declared, `build/other.json` written | still refused | + +The refusal message now names the flag, so the next agent finds the sanctioned +route instead of inventing the `test -f` trick. The receipt records +`generatedPaths`, so a reader can see exactly which rewrites were permitted. + +Verified end to end on a throwaway git repo whose validator rewrites its own +output: refused without the flag, receipt written with it, still refused when the +wrong path is declared. + +## 2. `orchestrate D` refused a goalplan that was already finished + +``` +the bound goalplan "" has no active work-phase to close (CYCLE-COMPLETION-01). +``` + +`advanceWorkPhase` returns `no_active` for two very different situations, and the +gate treated them the same: + +- the plan is EMPTY, or everything is blocked — a real refusal +- every work-phase is `done` — the plan is **complete** + +The reported workaround was to write a finished phase back to `in_progress` just +to get past the gate. That is corrupting the record in order to satisfy a check +about the record. + +D now closes when every work-phase is done, and the refusal names which of the +two real causes applies: + +``` +... has no work-phase to close: the plan is empty — register workPhases[] first +... has no work-phase to close: every remaining work-phase is blocked or superseded +``` + +The goalplan ledger says `cycle closed over an already-complete plan` rather than +`closed null`. + +## 3. SOURCE-DELTA-01, documented rather than changed + +The issue also asks that a B-phase commit count as B work. It already does — I +hit `the source is unchanged since B began` twice in this session, and both times +the cause was real: I had entered B and then tried to leave it without touching +the tree, because the work had happened in an earlier cycle. + +The rule is correct. What was missing is that the message does not say what +counts. Left as-is here rather than loosened; a gate that occasionally annoys is +better than one that lets an empty B through. diff --git a/plugins/codexclaw/components/pabcd-state/dist/goalplan.js b/plugins/codexclaw/components/pabcd-state/dist/goalplan.js index a2d17a7..6039516 100644 --- a/plugins/codexclaw/components/pabcd-state/dist/goalplan.js +++ b/plugins/codexclaw/components/pabcd-state/dist/goalplan.js @@ -1008,6 +1008,8 @@ function identityReasons(plan , gate , ctx + + /** * Advance the goalplan's work-phase cursor: mark the current activeWorkPhaseId * as `done`, then set the next pending work-phase active. diff --git a/plugins/codexclaw/components/pabcd-state/dist/orchestrate-cli.js b/plugins/codexclaw/components/pabcd-state/dist/orchestrate-cli.js index d5d0145..84f1d23 100644 --- a/plugins/codexclaw/components/pabcd-state/dist/orchestrate-cli.js +++ b/plugins/codexclaw/components/pabcd-state/dist/orchestrate-cli.js @@ -569,10 +569,21 @@ export function runOrchestrateCli(args }; } if (advanced.kind === "no_active") { + // #49: "no active work-phase" has two very different causes. If every phase + // is already done, the plan is COMPLETE and refusing D strands the cycle — + // the reported workaround was to write a finished phase back to + // in_progress just to satisfy this gate, which corrupts the record to + // satisfy a check about the record. Only an EMPTY or fully blocked plan is + // a real refusal. + const closable = plan.workPhases.length > 0 && plan.workPhases.every((wp) => wp.status === "done"); + if (closable) { + advanced = { kind: "ok", closedId: null, plan }; + } else { return { code: 1, - output: `orchestrate D: ${renderPhaseContext(state, sessionId)}; the bound goalplan "${state.slug}" has no active work-phase to close (CYCLE-COMPLETION-01). Register or unblock a work-phase before closing a cycle. Nothing was written.`, + output: `orchestrate D: ${renderPhaseContext(state, sessionId)}; the bound goalplan "${state.slug}" has no work-phase to close (CYCLE-COMPLETION-01): ${plan.workPhases.length === 0 ? "the plan is empty — register workPhases[] first" : "every remaining work-phase is blocked or superseded — unblock one"}. Nothing was written.`, }; + } } } writeState(args.cwd, { ...clearedIdle(state), stopBlockPhase: null, stopBlockCount: 0 }); @@ -595,7 +606,11 @@ export function runOrchestrateCli(args event: "workphase_done", // 260714 wp4: log the EFFECTIVE closed id (implicit cursor may have // started from a null explicit cursor — "closed none" was a lie). - detail: `closed ${advanced.closedId}`, + // #49: a null id means the plan was already fully done and this cycle + // closed without advancing a cursor. Say that rather than "closed null". + detail: advanced.closedId + ? `closed ${advanced.closedId}` + : "cycle closed over an already-complete plan", }); if (advanced.plan.activeWorkPhaseId) { appendGoalplanLedger(args.cwd, state.slug, { diff --git a/plugins/codexclaw/components/pabcd-state/dist/receipt-cli.js b/plugins/codexclaw/components/pabcd-state/dist/receipt-cli.js index 8834c01..725648f 100644 --- a/plugins/codexclaw/components/pabcd-state/dist/receipt-cli.js +++ b/plugins/codexclaw/components/pabcd-state/dist/receipt-cli.js @@ -28,6 +28,8 @@ import { STATE_DIR, sanitizeKey } from "./state.js"; + + /** Everything after `--` is the command; nothing before it is. */ export function parseReceiptCliArgs(argv , cwd ) { const verb = (argv[0] ?? "").toLowerCase(); @@ -46,6 +48,13 @@ export function parseReceiptCliArgs(argv , cwd ) if (a === "--") { i++; break; } if (a === "--session") out.session = argv[++i]; else if (a === "--cwd") out.cwd = argv[++i] ?? cwd; + else if (a === "--generated") { + const v = argv[++i]; + if (typeof v !== "string" || v.length === 0) { + return { error: "receipt test: --generated needs a repo-relative path" }; + } + out.generated = [...(out.generated ?? []), v.replace(/\\/g, "/").replace(/^\.\//, "")]; + } else return { error: `unexpected argument '${a}' before --` }; } out.command = argv.slice(i).filter((a) => typeof a === "string" && a.length > 0); @@ -67,7 +76,7 @@ export function runReceiptCli(args ) { "cxc receipt — record a check receipt that binds a command's result to a source tree", "", "Usage:", - " cxc receipt test --session [--cwd ] -- [args...]", + " cxc receipt test --session [--cwd ] [--generated ]... -- [args...]", " cxc receipt --help", "", "Notes:", @@ -75,6 +84,8 @@ export function runReceiptCli(args ) { " The session must be at phase C — a receipt is produced during Check.", " The receipt is written to /.codexclaw/evidence//test-receipt.json", " and is refused if the command changes the source while it runs.", + " --generated declares paths the check REGENERATES by design (a validator that", + " rebuilds its own artifacts). Repeatable. Undeclared rewrites are still refused.", "", "Example:", " cxc receipt test --session -- npm test", @@ -102,7 +113,11 @@ export function runReceiptCli(args ) { // Clear first: a stale success must not survive a failing re-run. rmSync(path, { force: true }); - const before = captureSourceIdentity(args.cwd, { excludeCodexclawArtifacts: true }); + const capture = { + excludeCodexclawArtifacts: true, + ...(args.generated && args.generated.length > 0 ? { generatedPaths: args.generated } : {}), + }; + const before = captureSourceIdentity(args.cwd, capture); const [bin, ...rest] = args.command; // Issue #40: `npm` is the command people actually pass here, and a bare // shell-less spawn of it cannot work on Windows - the name alone skips PATHEXT @@ -116,7 +131,7 @@ export function runReceiptCli(args ) { shell: false, ...invocation.options, }); - const after = captureSourceIdentity(args.cwd, { excludeCodexclawArtifacts: true }); + const after = captureSourceIdentity(args.cwd, capture); if (run.error || typeof run.status !== "number") { return { output: `receipt test: the command did not run to completion (${run.error?.message ?? "terminated by signal"}); no receipt written`, code: 1 }; @@ -126,7 +141,15 @@ export function runReceiptCli(args ) { } const cmp = compareSource(before, after); if (cmp.kind === "different") { - return { output: `receipt test: the command changed the source while running (${cmp.detail}); no receipt written — a check cannot certify a tree it rewrote`, code: 1 }; + return { + output: [ + `receipt test: the command changed the source while running (${cmp.detail}); no receipt written — a check cannot certify a tree it rewrote.`, + "If the check REGENERATES artifacts by design, declare them:", + " cxc receipt test --session --generated -- ", + "(repeatable; a path covers that file or that directory. Undeclared rewrites are still refused.)", + ].join("\n"), + code: 1, + }; } if (cmp.kind === "unavailable") { return { output: `receipt test: git could not resolve the source identity (${cmp.reason}); no receipt written`, code: 1 }; @@ -140,6 +163,8 @@ export function runReceiptCli(args ) { createdAt: new Date().toISOString(), ownerSessionId: session, checkEpoch: state.checkEpoch, + // Recorded so a reader can see WHICH rewrites the check was allowed to make. + ...(args.generated && args.generated.length > 0 ? { generatedPaths: args.generated } : {}), }; mkdirSync(join(path, ".."), { recursive: true }); writeFileSync(path, `${JSON.stringify(receipt, null, 2)}\n`); diff --git a/plugins/codexclaw/components/pabcd-state/dist/source-identity.js b/plugins/codexclaw/components/pabcd-state/dist/source-identity.js index 65ae017..42960a6 100644 --- a/plugins/codexclaw/components/pabcd-state/dist/source-identity.js +++ b/plugins/codexclaw/components/pabcd-state/dist/source-identity.js @@ -154,6 +154,17 @@ const STATE_DIR_PREFIX = ".codexclaw/"; + + + + + + + + + + + export function captureSourceIdentity(cwd , options = {}) { const capturedAt = new Date().toISOString(); let commitSha = ""; @@ -173,6 +184,10 @@ export function captureSourceIdentity(cwd , options = {}) if (options.excludeCodexclawArtifacts) { records = records.filter((r) => !r.path.startsWith(STATE_DIR_PREFIX)); } + const generated = (options.generatedPaths ?? []).filter((p) => p.length > 0); + if (generated.length > 0) { + records = records.filter((r) => !generated.some((g) => r.path === g || r.path.startsWith(`${g}/`))); + } if (records.length === 0) return { kind: "resolved", commitSha, dirty: false, capturedAt }; return { kind: "resolved", commitSha, dirty: true, treeHash: hashRecords(cwd, records), capturedAt }; } diff --git a/plugins/codexclaw/components/pabcd-state/src/goalplan.ts b/plugins/codexclaw/components/pabcd-state/src/goalplan.ts index 538550b..ad4151a 100644 --- a/plugins/codexclaw/components/pabcd-state/src/goalplan.ts +++ b/plugins/codexclaw/components/pabcd-state/src/goalplan.ts @@ -1004,7 +1004,9 @@ function identityReasons(plan: Goalplan, gate: FinalGateState, ctx: GoalplanVali * refusal leaves all three untouched. */ export type AdvanceResult = - | { kind: "ok"; plan: Goalplan; closedId: string } + // closedId is null when the plan was ALREADY fully done and this cycle closes + // without advancing a cursor (#49). + | { kind: "ok"; plan: Goalplan; closedId: string | null } | { kind: "tasks_pending"; workPhaseId: string; pending: GoalplanTask[] } | { kind: "no_active" }; diff --git a/plugins/codexclaw/components/pabcd-state/src/orchestrate-cli.ts b/plugins/codexclaw/components/pabcd-state/src/orchestrate-cli.ts index 14057f5..f8314d9 100644 --- a/plugins/codexclaw/components/pabcd-state/src/orchestrate-cli.ts +++ b/plugins/codexclaw/components/pabcd-state/src/orchestrate-cli.ts @@ -569,10 +569,21 @@ export function runOrchestrateCli(args: OrchestrateCliArgs | OrchestrateCliHelpA }; } if (advanced.kind === "no_active") { + // #49: "no active work-phase" has two very different causes. If every phase + // is already done, the plan is COMPLETE and refusing D strands the cycle — + // the reported workaround was to write a finished phase back to + // in_progress just to satisfy this gate, which corrupts the record to + // satisfy a check about the record. Only an EMPTY or fully blocked plan is + // a real refusal. + const closable = plan.workPhases.length > 0 && plan.workPhases.every((wp) => wp.status === "done"); + if (closable) { + advanced = { kind: "ok", closedId: null, plan }; + } else { return { code: 1, - output: `orchestrate D: ${renderPhaseContext(state, sessionId)}; the bound goalplan "${state.slug}" has no active work-phase to close (CYCLE-COMPLETION-01). Register or unblock a work-phase before closing a cycle. Nothing was written.`, + output: `orchestrate D: ${renderPhaseContext(state, sessionId)}; the bound goalplan "${state.slug}" has no work-phase to close (CYCLE-COMPLETION-01): ${plan.workPhases.length === 0 ? "the plan is empty — register workPhases[] first" : "every remaining work-phase is blocked or superseded — unblock one"}. Nothing was written.`, }; + } } } writeState(args.cwd, { ...clearedIdle(state), stopBlockPhase: null, stopBlockCount: 0 }); @@ -595,7 +606,11 @@ export function runOrchestrateCli(args: OrchestrateCliArgs | OrchestrateCliHelpA event: "workphase_done", // 260714 wp4: log the EFFECTIVE closed id (implicit cursor may have // started from a null explicit cursor — "closed none" was a lie). - detail: `closed ${advanced.closedId}`, + // #49: a null id means the plan was already fully done and this cycle + // closed without advancing a cursor. Say that rather than "closed null". + detail: advanced.closedId + ? `closed ${advanced.closedId}` + : "cycle closed over an already-complete plan", }); if (advanced.plan.activeWorkPhaseId) { appendGoalplanLedger(args.cwd, state.slug, { diff --git a/plugins/codexclaw/components/pabcd-state/src/receipt-cli.ts b/plugins/codexclaw/components/pabcd-state/src/receipt-cli.ts index 50b88a5..4939f30 100644 --- a/plugins/codexclaw/components/pabcd-state/src/receipt-cli.ts +++ b/plugins/codexclaw/components/pabcd-state/src/receipt-cli.ts @@ -24,6 +24,8 @@ export interface ReceiptCliArgs { cwd: string; session?: string; command: string[]; + /** #49: repo-relative paths the check command is expected to rewrite. */ + generated?: string[]; } export interface ReceiptCliParseError { error: string } @@ -46,6 +48,13 @@ export function parseReceiptCliArgs(argv: string[], cwd: string): ReceiptCliArgs if (a === "--") { i++; break; } if (a === "--session") out.session = argv[++i]; else if (a === "--cwd") out.cwd = argv[++i] ?? cwd; + else if (a === "--generated") { + const v = argv[++i]; + if (typeof v !== "string" || v.length === 0) { + return { error: "receipt test: --generated needs a repo-relative path" }; + } + out.generated = [...(out.generated ?? []), v.replace(/\\/g, "/").replace(/^\.\//, "")]; + } else return { error: `unexpected argument '${a}' before --` }; } out.command = argv.slice(i).filter((a) => typeof a === "string" && a.length > 0); @@ -67,7 +76,7 @@ export function runReceiptCli(args: ReceiptCliArgs): ReceiptCliResult { "cxc receipt — record a check receipt that binds a command's result to a source tree", "", "Usage:", - " cxc receipt test --session [--cwd ] -- [args...]", + " cxc receipt test --session [--cwd ] [--generated ]... -- [args...]", " cxc receipt --help", "", "Notes:", @@ -75,6 +84,8 @@ export function runReceiptCli(args: ReceiptCliArgs): ReceiptCliResult { " The session must be at phase C — a receipt is produced during Check.", " The receipt is written to /.codexclaw/evidence//test-receipt.json", " and is refused if the command changes the source while it runs.", + " --generated declares paths the check REGENERATES by design (a validator that", + " rebuilds its own artifacts). Repeatable. Undeclared rewrites are still refused.", "", "Example:", " cxc receipt test --session -- npm test", @@ -102,7 +113,11 @@ export function runReceiptCli(args: ReceiptCliArgs): ReceiptCliResult { // Clear first: a stale success must not survive a failing re-run. rmSync(path, { force: true }); - const before = captureSourceIdentity(args.cwd, { excludeCodexclawArtifacts: true }); + const capture = { + excludeCodexclawArtifacts: true, + ...(args.generated && args.generated.length > 0 ? { generatedPaths: args.generated } : {}), + }; + const before = captureSourceIdentity(args.cwd, capture); const [bin, ...rest] = args.command; // Issue #40: `npm` is the command people actually pass here, and a bare // shell-less spawn of it cannot work on Windows - the name alone skips PATHEXT @@ -116,7 +131,7 @@ export function runReceiptCli(args: ReceiptCliArgs): ReceiptCliResult { shell: false, ...invocation.options, }); - const after = captureSourceIdentity(args.cwd, { excludeCodexclawArtifacts: true }); + const after = captureSourceIdentity(args.cwd, capture); if (run.error || typeof run.status !== "number") { return { output: `receipt test: the command did not run to completion (${run.error?.message ?? "terminated by signal"}); no receipt written`, code: 1 }; @@ -126,7 +141,15 @@ export function runReceiptCli(args: ReceiptCliArgs): ReceiptCliResult { } const cmp = compareSource(before, after); if (cmp.kind === "different") { - return { output: `receipt test: the command changed the source while running (${cmp.detail}); no receipt written — a check cannot certify a tree it rewrote`, code: 1 }; + return { + output: [ + `receipt test: the command changed the source while running (${cmp.detail}); no receipt written — a check cannot certify a tree it rewrote.`, + "If the check REGENERATES artifacts by design, declare them:", + " cxc receipt test --session --generated -- ", + "(repeatable; a path covers that file or that directory. Undeclared rewrites are still refused.)", + ].join("\n"), + code: 1, + }; } if (cmp.kind === "unavailable") { return { output: `receipt test: git could not resolve the source identity (${cmp.reason}); no receipt written`, code: 1 }; @@ -140,6 +163,8 @@ export function runReceiptCli(args: ReceiptCliArgs): ReceiptCliResult { createdAt: new Date().toISOString(), ownerSessionId: session, checkEpoch: state.checkEpoch, + // Recorded so a reader can see WHICH rewrites the check was allowed to make. + ...(args.generated && args.generated.length > 0 ? { generatedPaths: args.generated } : {}), }; mkdirSync(join(path, ".."), { recursive: true }); writeFileSync(path, `${JSON.stringify(receipt, null, 2)}\n`); diff --git a/plugins/codexclaw/components/pabcd-state/src/source-identity.ts b/plugins/codexclaw/components/pabcd-state/src/source-identity.ts index 8bd4a38..7258734 100644 --- a/plugins/codexclaw/components/pabcd-state/src/source-identity.ts +++ b/plugins/codexclaw/components/pabcd-state/src/source-identity.ts @@ -152,6 +152,17 @@ const STATE_DIR_PREFIX = ".codexclaw/"; export interface CaptureOptions { /** Drop `.codexclaw/` entries before hashing. Default false. */ excludeCodexclawArtifacts?: boolean; + /** + * #49: paths the check command is EXPECTED to rewrite. A validator that + * regenerates its own artifacts (`validate.py --build` writing graph.json) is + * the documented gate for some repos, and refusing its receipt forces agents to + * fake a `test -f` receipt instead — which defeats CHECK-BINDING-01 entirely. + * + * Prefix match on the repo-relative POSIX path, so `600_ontology` covers the + * whole directory and `graph.json` covers exactly that file. Declared by the + * caller, never inferred: an undeclared rewrite is still a refusal. + */ + generatedPaths?: string[]; } export function captureSourceIdentity(cwd: string, options: CaptureOptions = {}): SourceIdentity { @@ -173,6 +184,10 @@ export function captureSourceIdentity(cwd: string, options: CaptureOptions = {}) if (options.excludeCodexclawArtifacts) { records = records.filter((r) => !r.path.startsWith(STATE_DIR_PREFIX)); } + const generated = (options.generatedPaths ?? []).filter((p) => p.length > 0); + if (generated.length > 0) { + records = records.filter((r) => !generated.some((g) => r.path === g || r.path.startsWith(`${g}/`))); + } if (records.length === 0) return { kind: "resolved", commitSha, dirty: false, capturedAt }; return { kind: "resolved", commitSha, dirty: true, treeHash: hashRecords(cwd, records), capturedAt }; } diff --git a/plugins/codexclaw/components/pabcd-state/test/orchestrate-cli.test.ts b/plugins/codexclaw/components/pabcd-state/test/orchestrate-cli.test.ts index fd5d1c7..79e3b29 100644 --- a/plugins/codexclaw/components/pabcd-state/test/orchestrate-cli.test.ts +++ b/plugins/codexclaw/components/pabcd-state/test/orchestrate-cli.test.ts @@ -833,11 +833,15 @@ test("D-close on a bound session is refused when the goalplan cannot be read", ( assert.equal(ledgerLines(cwd).length, 0); }); -test("D-close is refused when the bound goalplan has no active work-phase", () => { +// #49: a plan whose work-phases are ALL done is complete, not broken. Refusing D +// here stranded the cycle, and the reported workaround was to write a finished +// phase back to in_progress purely to satisfy this gate — corrupting the record +// to satisfy a check about the record. It closes now. +test("D-close succeeds when every work-phase is already done", () => { const cwd = boundCwd(); - const id = "cycle-no-active"; - const slug = "cycle-gate-empty"; - const plan = buildGoalplan({ objective: "no active phase" }); + const id = "cycle-all-done"; + const slug = "cycle-gate-complete"; + const plan = buildGoalplan({ objective: "all phases done" }); plan.slug = slug; plan.workPhases = [{ id: "wp-1", title: "closed", status: "done", tasks: [], criteriaIds: [] }]; plan.activeWorkPhaseId = null; @@ -850,12 +854,57 @@ test("D-close is refused when the bound goalplan has no active work-phase", () = assert.ok(!("error" in args)); const r = runOrchestrateCli(args as never); + assert.equal(r.code, 0, r.output); + assert.equal(readState(cwd, id).phase, "IDLE"); + assert.equal(ledgerLines(cwd).length, 1); +}); + +// The gate still exists — it just names the two REAL failures instead. +test("D-close is refused when the bound goalplan is empty", () => { + const cwd = boundCwd(); + const id = "cycle-empty-plan"; + const slug = "cycle-gate-empty"; + const plan = buildGoalplan({ objective: "no phases registered" }); + plan.slug = slug; + plan.workPhases = []; + plan.activeWorkPhaseId = null; + writeGoalplan(cwd, plan); + const epoch = "c-test-epoch"; + writeState(cwd, { ...defaultState(id), phase: "C", slug, checkEpoch: epoch, flags: { interview: false, auditPassed: true, checkPassed: false } }); + seedReceipt(cwd, id, epoch); + + const args = parseOrchestrateCliArgs(["d", "--session", id, "--cwd", cwd, "--attest", dAttest(id)], cwd); + assert.ok(!("error" in args)); + const r = runOrchestrateCli(args as never); + assert.equal(r.code, 1); - assert.match(r.output, /no active work-phase/); + assert.match(r.output, /the plan is empty/); assert.equal(readState(cwd, id).phase, "C"); assert.equal(ledgerLines(cwd).length, 0); }); +test("D-close is refused when every remaining work-phase is blocked", () => { + const cwd = boundCwd(); + const id = "cycle-all-blocked"; + const slug = "cycle-gate-blocked"; + const plan = buildGoalplan({ objective: "everything blocked" }); + plan.slug = slug; + plan.workPhases = [{ id: "wp-1", title: "stuck", status: "blocked", tasks: [], criteriaIds: [] }]; + plan.activeWorkPhaseId = null; + writeGoalplan(cwd, plan); + const epoch = "c-test-epoch"; + writeState(cwd, { ...defaultState(id), phase: "C", slug, checkEpoch: epoch, flags: { interview: false, auditPassed: true, checkPassed: false } }); + seedReceipt(cwd, id, epoch); + + const args = parseOrchestrateCliArgs(["d", "--session", id, "--cwd", cwd, "--attest", dAttest(id)], cwd); + assert.ok(!("error" in args)); + const r = runOrchestrateCli(args as never); + + assert.equal(r.code, 1); + assert.match(r.output, /blocked or superseded/); + assert.equal(readState(cwd, id).phase, "C"); +}); + test("an unbound (HITL) session closes its cycle exactly as before", () => { const cwd = freshCwd(); const id = "cycle-hitl"; diff --git a/plugins/codexclaw/components/pabcd-state/test/receipt-generated.test.ts b/plugins/codexclaw/components/pabcd-state/test/receipt-generated.test.ts new file mode 100644 index 0000000..2b3097a --- /dev/null +++ b/plugins/codexclaw/components/pabcd-state/test/receipt-generated.test.ts @@ -0,0 +1,104 @@ +/** + * receipt-generated.test.ts — issue #49: `receipt test` refused to write a receipt + * when the check command dirtied the tree, even though the dirty files were the + * artifacts the check exists to rebuild. + * + * Reported consequence: the agent committed the generated files and ran + * `cxc receipt test -- test -f ...`, a no-op existence check, to get past D. A + * forged receipt is strictly worse than a loose one — it defeats CHECK-BINDING-01 + * while looking like it satisfied it. + * + * The fix is DECLARED, not inferred: an undeclared rewrite is still refused. + */ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { execFileSync } from "node:child_process"; +import { captureSourceIdentity, compareSource } from "../src/source-identity.ts"; +import { parseReceiptCliArgs } from "../src/receipt-cli.ts"; + +function repoWithGeneratedFile(): string { + const root = mkdtempSync(join(tmpdir(), "cxc-gen-")); + const git = (...a: string[]) => execFileSync("git", a, { cwd: root, encoding: "utf8" }); + git("init", "-q"); + git("config", "user.email", "t@t"); + git("config", "user.name", "t"); + mkdirSync(join(root, "build"), { recursive: true }); + writeFileSync(join(root, "build", "graph.json"), '{"nodes":0}'); + writeFileSync(join(root, "src.txt"), "source"); + git("add", "-A"); + git("commit", "-qm", "init"); + return root; +} + +test("a declared generated path does not count as a source change", () => { + const root = repoWithGeneratedFile(); + try { + const opts = { excludeCodexclawArtifacts: true, generatedPaths: ["build"] }; + const before = captureSourceIdentity(root, opts); + writeFileSync(join(root, "build", "graph.json"), '{"nodes":999}'); + const after = captureSourceIdentity(root, opts); + assert.equal(compareSource(before, after).kind, "same"); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("an UNdeclared rewrite is still a source change", () => { + const root = repoWithGeneratedFile(); + try { + const opts = { excludeCodexclawArtifacts: true, generatedPaths: ["build"] }; + const before = captureSourceIdentity(root, opts); + // src.txt was never declared — this must still be caught. + writeFileSync(join(root, "src.txt"), "rewritten"); + const after = captureSourceIdentity(root, opts); + assert.equal(compareSource(before, after).kind, "different"); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("declaring the wrong path does not launder a real change", () => { + const root = repoWithGeneratedFile(); + try { + const opts = { excludeCodexclawArtifacts: true, generatedPaths: ["docs"] }; + const before = captureSourceIdentity(root, opts); + writeFileSync(join(root, "build", "graph.json"), '{"nodes":999}'); + const after = captureSourceIdentity(root, opts); + assert.equal(compareSource(before, after).kind, "different"); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("a file path declares exactly that file, not its siblings", () => { + const root = repoWithGeneratedFile(); + try { + const opts = { excludeCodexclawArtifacts: true, generatedPaths: ["build/graph.json"] }; + const before = captureSourceIdentity(root, opts); + writeFileSync(join(root, "build", "graph.json"), '{"nodes":1}'); + assert.equal(compareSource(before, captureSourceIdentity(root, opts)).kind, "same"); + writeFileSync(join(root, "build", "other.json"), "{}"); + assert.equal(compareSource(before, captureSourceIdentity(root, opts)).kind, "different"); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("--generated is repeatable and normalizes separators", () => { + const args = parseReceiptCliArgs( + ["test", "--session", "s1", "--generated", "build", "--generated", ".\\docs\\api", "--", "npm", "test"], + "/unused", + ); + assert.ok(!("error" in args)); + assert.deepEqual((args as { generated?: string[] }).generated, ["build", "docs/api"]); + assert.deepEqual((args as { command: string[] }).command, ["npm", "test"]); +}); + +test("--generated without a value is a parse error, not a silent skip", () => { + const args = parseReceiptCliArgs(["test", "--session", "s1", "--generated"], "/unused"); + assert.ok("error" in args); + assert.match((args as { error: string }).error, /--generated needs a repo-relative path/); +}); From d5e1c00e399674243470e66ff7fd10d0c181c507 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Sat, 22 Aug 2026 23:22:08 +0900 Subject: [PATCH 5/5] release(0.2.12): bump every version surface for the #47/#48/#49 fixes --- CHANGELOG.md | 40 +++++++++++++++++++ cli/package.json | 2 +- package.json | 2 +- plugins/codexclaw/.codex-plugin/plugin.json | 2 +- .../components/config-guard/package.json | 2 +- .../codexclaw/components/cxc-ops/package.json | 2 +- .../components/messenger-bridge/package.json | 2 +- .../components/pabcd-state/package.json | 2 +- .../components/provider-bridge/package.json | 2 +- .../codexclaw/components/recall/package.json | 2 +- .../components/skill-search/package.json | 2 +- .../components/subagent-config/package.json | 2 +- plugins/codexclaw/gui/package.json | 2 +- plugins/codexclaw/inventory.json | 20 +++++----- 14 files changed, 62 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fe09d3..5112f95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,46 @@ All notable changes to codexclaw are documented here. The format follows ## [Unreleased] +## [0.2.12] — 2026-08-22 + +Three bugs filed against codexclaw, all of them cases where the tool obstructed +an agent that was following its own instructions. + +### Fixed + +- **`loop`, `scan` and `receipt` had no `--help` (#47).** The top-level help + points at those commands, and following that pointer failed — `--help` was + reported as an unknown verb, and `cxc --version` as an unknown command. + `orchestrate` was fixed for this long ago; its siblings never were. + + The individual error messages were fine. The problem was that discovery was + only available through failure: arming a goalplan took six consecutive + rejections to assemble one correct command. The `loop` usage now spells out the + steer batch shape, which is the one nobody can guess. + + Also from that issue: `scan record` now accepts `--cwd`, which `orchestrate` + already documented. + +- **The same `--session` id resolved to two different FSMs (#48).** Session files + live under the process cwd, so a thread whose cwd is one tree while its work is + in another silently interviews one FSM and orchestrates the other. `status` now + warns when the id exists elsewhere and names the paths, instead of reporting + `IDLE` for a cycle that is live next door. Detection only — the other tree is + never read or written. `loop show` also accepts `--session` and resolves the + slug the session already carries. + +- **Two gates forced the forgery they existed to prevent (#49).** `receipt test` + refused a receipt whenever the check dirtied the tree, including when the dirty + files were the artifacts the check exists to rebuild — so the reported + workaround was a no-op existence check, a receipt that certifies nothing. + `--generated ` now declares expected rewrites; everything undeclared is + still refused. + + `orchestrate D` refused a goalplan whose work-phases were all done, because + "complete" and "empty" produced the same internal result. The workaround was to + write a finished phase back to `in_progress` purely to pass the gate. D now + closes over a complete plan and the refusal names which real cause applies. + ## [0.2.11] — 2026-08-22 ### Fixed diff --git a/cli/package.json b/cli/package.json index 83291aa..f8b8a79 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@codexclaw/cli", - "version": "0.2.11", + "version": "0.2.12", "private": true, "type": "module", "description": "codexclaw CLI — status, subagent config, provider toggle, GUI launcher.", diff --git a/package.json b/package.json index 5f6227d..262fb97 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codexclaw", - "version": "0.2.11", + "version": "0.2.12", "private": true, "description": "cli-jaw-style dev discipline + multi-model subagents for the OpenAI Codex runtime.", "type": "module", diff --git a/plugins/codexclaw/.codex-plugin/plugin.json b/plugins/codexclaw/.codex-plugin/plugin.json index ab973e5..b65933f 100644 --- a/plugins/codexclaw/.codex-plugin/plugin.json +++ b/plugins/codexclaw/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "codexclaw", - "version": "0.2.11+codex.20260818121334", + "version": "0.2.12+codex.20260818121334", "description": "cli-jaw-style dev discipline (dev skills + PABCD) and multi-model subagents for the OpenAI Codex runtime, with optional opencodex provider routing.", "author": { "name": "lidge-jun", diff --git a/plugins/codexclaw/components/config-guard/package.json b/plugins/codexclaw/components/config-guard/package.json index b4c0672..1d88c48 100644 --- a/plugins/codexclaw/components/config-guard/package.json +++ b/plugins/codexclaw/components/config-guard/package.json @@ -1,6 +1,6 @@ { "name": "@codexclaw/config-guard", - "version": "0.2.11", + "version": "0.2.12", "private": true, "type": "module", "description": "Controlled feature-flag activation: enables only codexclaw's declared [features] flags via the official `codex features` CLI, with a revert manifest and backup.", diff --git a/plugins/codexclaw/components/cxc-ops/package.json b/plugins/codexclaw/components/cxc-ops/package.json index cfd4b3c..765ed77 100644 --- a/plugins/codexclaw/components/cxc-ops/package.json +++ b/plugins/codexclaw/components/cxc-ops/package.json @@ -1,6 +1,6 @@ { "name": "@codexclaw/cxc-ops", - "version": "0.2.11", + "version": "0.2.12", "private": true, "type": "module", "description": "codexclaw ops CLI — doctor (plugin health), reset (scoped state cleanup).", diff --git a/plugins/codexclaw/components/messenger-bridge/package.json b/plugins/codexclaw/components/messenger-bridge/package.json index 6ec2ade..0ce6e04 100644 --- a/plugins/codexclaw/components/messenger-bridge/package.json +++ b/plugins/codexclaw/components/messenger-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@codexclaw/messenger-bridge", - "version": "0.2.11", + "version": "0.2.12", "private": true, "type": "module", "description": "codexclaw messenger bridge — cxc serve HTTP server + SQLite state substrate (zero third-party deps).", diff --git a/plugins/codexclaw/components/pabcd-state/package.json b/plugins/codexclaw/components/pabcd-state/package.json index 02a8175..7e52052 100644 --- a/plugins/codexclaw/components/pabcd-state/package.json +++ b/plugins/codexclaw/components/pabcd-state/package.json @@ -1,6 +1,6 @@ { "name": "@codexclaw/pabcd-state", - "version": "0.2.11", + "version": "0.2.12", "private": true, "type": "module", "description": "IPABCD finite-state machine backed by per-session .codexclaw/sessions/.json + shared ledger.jsonl.", diff --git a/plugins/codexclaw/components/provider-bridge/package.json b/plugins/codexclaw/components/provider-bridge/package.json index f772f5e..824a096 100644 --- a/plugins/codexclaw/components/provider-bridge/package.json +++ b/plugins/codexclaw/components/provider-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@codexclaw/provider-bridge", - "version": "0.2.11", + "version": "0.2.12", "private": true, "type": "module", "description": "Detect-only opencodex (ocx) status probe at session start; graceful native path when absent.", diff --git a/plugins/codexclaw/components/recall/package.json b/plugins/codexclaw/components/recall/package.json index 0447644..67072dc 100644 --- a/plugins/codexclaw/components/recall/package.json +++ b/plugins/codexclaw/components/recall/package.json @@ -1,6 +1,6 @@ { "name": "@codexclaw/recall", - "version": "0.2.11", + "version": "0.2.12", "private": true, "type": "module", "description": "Read-only chat/memory recall search over the Codex session root (~/.codex): date-pruned rollout scan + thread/memory sqlite enrichment.", diff --git a/plugins/codexclaw/components/skill-search/package.json b/plugins/codexclaw/components/skill-search/package.json index a051cab..5401260 100644 --- a/plugins/codexclaw/components/skill-search/package.json +++ b/plugins/codexclaw/components/skill-search/package.json @@ -1,6 +1,6 @@ { "name": "@codexclaw/skill-search", - "version": "0.2.11", + "version": "0.2.12", "private": true, "type": "module", "description": "Remote dormant-skill search over cli-jaw-skills / Hermes / ClawHub / gh code search. Zero-dep, TTL-cached, adapter-preamble output. No local vendoring.", diff --git a/plugins/codexclaw/components/subagent-config/package.json b/plugins/codexclaw/components/subagent-config/package.json index 4b950b9..8beface 100644 --- a/plugins/codexclaw/components/subagent-config/package.json +++ b/plugins/codexclaw/components/subagent-config/package.json @@ -1,6 +1,6 @@ { "name": "@codexclaw/subagent-config", - "version": "0.2.11", + "version": "0.2.12", "private": true, "type": "module", "description": "Stores subagent model/prompt config; serves it to the GUI and an MCP tool.", diff --git a/plugins/codexclaw/gui/package.json b/plugins/codexclaw/gui/package.json index 490583d..c01c613 100644 --- a/plugins/codexclaw/gui/package.json +++ b/plugins/codexclaw/gui/package.json @@ -1,6 +1,6 @@ { "name": "@codexclaw/gui", - "version": "0.2.11", + "version": "0.2.12", "private": true, "type": "module", "description": "codexclaw local dashboard (Vite + React) — subagent config, prompts, provider link bar.", diff --git a/plugins/codexclaw/inventory.json b/plugins/codexclaw/inventory.json index 53b85c6..3ac8184 100644 --- a/plugins/codexclaw/inventory.json +++ b/plugins/codexclaw/inventory.json @@ -2,8 +2,8 @@ "schemaVersion": 1, "plugin": { "name": "codexclaw", - "manifestVersion": "0.2.11+codex.20260818121334", - "packageVersion": "0.2.11" + "manifestVersion": "0.2.12+codex.20260818121334", + "packageVersion": "0.2.12" }, "skills": [ { @@ -257,49 +257,49 @@ { "folder": "config-guard", "packageName": "@codexclaw/config-guard", - "version": "0.2.11", + "version": "0.2.12", "hasTests": true }, { "folder": "cxc-ops", "packageName": "@codexclaw/cxc-ops", - "version": "0.2.11", + "version": "0.2.12", "hasTests": true }, { "folder": "messenger-bridge", "packageName": "@codexclaw/messenger-bridge", - "version": "0.2.11", + "version": "0.2.12", "hasTests": true }, { "folder": "pabcd-state", "packageName": "@codexclaw/pabcd-state", - "version": "0.2.11", + "version": "0.2.12", "hasTests": true }, { "folder": "provider-bridge", "packageName": "@codexclaw/provider-bridge", - "version": "0.2.11", + "version": "0.2.12", "hasTests": true }, { "folder": "recall", "packageName": "@codexclaw/recall", - "version": "0.2.11", + "version": "0.2.12", "hasTests": true }, { "folder": "skill-search", "packageName": "@codexclaw/skill-search", - "version": "0.2.11", + "version": "0.2.12", "hasTests": true }, { "folder": "subagent-config", "packageName": "@codexclaw/subagent-config", - "version": "0.2.11", + "version": "0.2.12", "hasTests": true } ]