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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path>` 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
Expand Down
2 changes: 1 addition & 1 deletion README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

<p align="center">
<a href="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml"><img src="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
<img src="https://img.shields.io/badge/tests-1%2C933_passing-brightgreen" alt="1,933 tests passing">
<img src="https://img.shields.io/badge/tests-1%2C961_passing-brightgreen" alt="1,961 tests passing">
<img src="https://img.shields.io/badge/skills-28-blue" alt="28 skills">
<img src="https://img.shields.io/badge/hooks-22-blue" alt="22 hooks">
<a href="https://lidge-jun.github.io/codexclaw/"><img src="https://img.shields.io/badge/docs-codexclaw-black" alt="Documentation"></a>
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

<p align="center">
<a href="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml"><img src="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
<img src="https://img.shields.io/badge/tests-1%2C933_passing-brightgreen" alt="1,933 tests passing">
<img src="https://img.shields.io/badge/tests-1%2C961_passing-brightgreen" alt="1,961 tests passing">
<img src="https://img.shields.io/badge/skills-28-blue" alt="28 skills">
<img src="https://img.shields.io/badge/hooks-22-blue" alt="22 hooks">
<a href="https://lidge-jun.github.io/codexclaw/"><img src="https://img.shields.io/badge/docs-codexclaw-black" alt="Documentation"></a>
Expand Down
2 changes: 1 addition & 1 deletion README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

<p align="center">
<a href="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml"><img src="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
<img src="https://img.shields.io/badge/tests-1%2C933_passing-brightgreen" alt="1,933 tests passing">
<img src="https://img.shields.io/badge/tests-1%2C961_passing-brightgreen" alt="1,961 tests passing">
<img src="https://img.shields.io/badge/skills-28-blue" alt="28 skills">
<img src="https://img.shields.io/badge/hooks-22-blue" alt="22 hooks">
<a href="https://lidge-jun.github.io/codexclaw/"><img src="https://img.shields.io/badge/docs-codexclaw-black" alt="Documentation"></a>
Expand Down
17 changes: 16 additions & 1 deletion bin/codexclaw.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
@@ -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.",
Expand Down
77 changes: 77 additions & 0 deletions devlog/_plan/260822_attest_win_parity/010_help_verbs.md
Original file line number Diff line number Diff line change
@@ -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 <id> is required
loop steer: --batch-json <path-or-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": "<unique>", "rationale": "<why>", "evidence": "<proof>",
"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.
66 changes: 66 additions & 0 deletions devlog/_plan/260822_attest_win_parity/020_session_split.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# 020 - issue #48: one session id, two FSMs

## The failure

Session files live at `<cwd>/.codexclaw/sessions/<id>.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/<id>.json phase=I 13:16Z
~/kim_wiki/.codexclaw/sessions/<id>.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 <path> 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 <id> # 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.
84 changes: 84 additions & 0 deletions devlog/_plan/260822_attest_win_parity/030_receipt_and_d_gates.md
Original file line number Diff line number Diff line change
@@ -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 <id> --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 "<slug>" 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.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion plugins/codexclaw/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
16 changes: 15 additions & 1 deletion plugins/codexclaw/bin/cxc.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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).`,
Expand Down
2 changes: 1 addition & 1 deletion plugins/codexclaw/components/config-guard/package.json
Original file line number Diff line number Diff line change
@@ -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.",
Expand Down
Loading
Loading