Conversation
…ease Records the two wrong designs and why measuring beat reasoning both times, plus the release audit that caught badge drift which would have failed cxc release verify closed at 1961 vs 1995.
A subagent dispatched read-only cannot write a receipt under .codexclaw/evidence/, so hasValidReceipt() is false forever; past MAX_ATTEMPTS the gate returned a block on every subsequent SubagentStop with no terminal release. A real transcript shows 15+ identical escalation blocks. Three read-only research lanes plus three adversarial audit rounds (FAIL, FAIL, NEAR-PASS) shaped the design: - permission_mode cannot carry the signal: it is derived from approval policy and only ever emits default or bypassPermissions, so a branch on it would be dead code. - A dispatch-capability marker file is forgeable — hook and child run as the same host user. Deleted from the plan. - A hook-authored artifact in a new evidence-unverified/ directory could be aliased into evidence/ via a symlinked directory. Deleted from the plan. - The terminal record is a tombstone in session State, consumed by the already shipped GOAL-COMPLETE-GATE-01, which denies update_goal complete while unresolved. 005 records the experiment that re-scoped the unit: one junk byte in the evidence directory already releases a worker on call 1, so bounded release opens no new opt-out. The gate can prove a receipt exists; it cannot prove verification ran. Docs only. No runtime change in this commit.
delete_branch_on_merge reads as complete branch hygiene, and a repository with it enabled looks solved. It is not: the setting fires only on merge, so a pull request closed WITHOUT merging keeps its head branch forever. OpenCodex had the setting on and still carried 59 dead remote branches for exactly that reason. Nothing reports the gap, so the branch list quietly degrades until triage stops using it. Add §2.9 to the router with four STRICT rules, and references/branch-lifecycle.md for the mechanics: - DEVOPS-BRANCH-AUTODELETE-01: enable host-side deletion AND close the closed-PR gap with scheduled automation. - DEVOPS-BRANCH-DELETE-EVIDENCE-01: never bulk-prune; prove per branch that it is not protected, not an open PR head, not the base of an open PR, not a fork head, and not carrying unique commits. A name pattern is not evidence. - DEVOPS-BRANCH-SNAPSHOT-01: snapshot for-each-ref before the first deletion, since a deleted remote branch is restorable only while you hold its SHA. - DEVOPS-WORKTREE-DIRTY-01: audit worktrees for uncommitted work, and remove worktrees before their branches. Two traps get their own treatment because both silently destroy work. A stacked child PR targets its parent's head, so deleting a closed parent closes the open child — "the PR that owned this branch is closed" is not sufficient grounds. And fork identity is by repo id, not name: forks commonly reuse upstream branch names, so name comparison misclassifies them as local. The reference also records why git branch -r --merged is not merge proof under squash-merge, and why the cleanup workflow must be schedule-only rather than workflow_dispatch-triggered with contents: write. Source: OpenCodex cleanup 2026-08-26 (101 remote / 228 local / 67 worktrees -> 59 / 177 / 59 removed) and lidge-jun/opencodex#2664, merged as bae100aa7. Verification: npm run gate (OK, no status drift or inventory drift).
A subagent dispatched read-only cannot create a receipt under the parent's
.codexclaw/evidence/, so hasValidReceipt() was false forever. Past MAX_ATTEMPTS
the gate returned decision:"block" on EVERY subsequent SubagentStop with no
terminal release: a real transcript shows 15+ identical escalation blocks while
the child kept re-sending the same report.
Blocking forever was never the safeguard it looked like. The child provably
cannot comply, so re-prompting it adds no safety - it only hides the outcome
from the parent, who is the one actor that can do something about it.
The budget is now terminal. At the cap the gate records an unresolved verdict
in session state and releases the child; GOAL-COMPLETE-GATE-01 then denies
update_goal{status:"complete"} until it is settled with a valid receipt via the
new 'cxc evidence resolve'. Fail-closed moved from the control flow to the
verdict. update_goal{status:"blocked"} stays available as the honest escape.
Defence in depth, because a verdict must not be lost:
- the tombstone is written under a session lock that re-reads inside it
(writeState is a read-modify-write; concurrent stops would erase each other)
- unreadable session state DENIES rather than reading as a clean default
- a corrupt or overflowed record sets a sentinel that also denies
- if nothing can be written, an exclusive-create marker is left instead
- the spent retry counter is itself a durable signal, so a verdict survives a
transient failure even when the filesystem later recovers
- counters are keyed by (agent, turn) with a raw-value UTF-16 digest, so no two
identities can share one budget
Rejected during review: keying on permission_mode (it only ever emits default or
bypassPermissions, so the branch would be dead code); a dispatch-capability
marker file (hook and child run as the same user - forgeable); a hook-authored
artifact directory (a child could symlink it into evidence/ and turn it into a
valid receipt); and a --override CLI flag (an agent could erase its own verdict).
Read-only lanes belong on agent_type:"explorer", which the gate never touches.
The block directive now says so on attempt 1 instead of after three confusing
retries.
12 adversarial review rounds; final verdict NEAR-PASS with no remaining
silent-allow path. Residuals are recorded in the devlog closeout.
npm test: 2026/2026 pass, 0 fail.
Bumps all 12 version surfaces to 0.2.14 and stamps the plugin manifest with a fresh +codex cachebuster so a local install actually repopulates the cache directory. Regenerates inventory.json and the EN/KO/ZH README badges against a measured 2026-test suite. Ships the read-only evidence-gate fix: the retry budget is terminal, the verdict is enforced at goal completion instead of by blocking a child that cannot comply, and 'cxc evidence resolve' settles it with a real receipt.
The release audit ran a resolve without --session and got only a session-id complaint, which reads as though the receipt were optional. Sequential checks made the error depend on argument order. Now all missing required arguments are collected and reported together, so --receipt is always named.
codex features enable only reaches booleans inside [features]. Keys in other tables have no persisted CLI setter, so memories.dedicated_tools was unreachable — which is what blocked the memory-upgrade unit's wp5. - toml-edit.ts owns the component's single TOML table/key grammar and adds setTableKey/restoreTableKey/readTableKey as pure string transforms. - multi-agent-v2.ts drops its local tomlTableBody copy and imports the shared one, so the component no longer carries two grammars. - managed-keys.ts holds the whitelist. Every entry is autoEnable:false: install never flips a user-owned switch, matching the multi_agent_v2 precedent. - Refuses value forms it cannot rewrite safely (multi-line/literal strings, arrays, inline tables, unterminated quotes) instead of guessing, and always leaves a [memories] header in place when removing our key. Roadmap and audit trail: devlog/_plan/260829_config-autopilot/
The uninstall path hashed all of config.toml and refused to revert anything when the file had changed at all. codexclaw is not that file's only writer, so a single unrelated edit after install disabled uninstall permanently, leaving flags the user never chose enabled forever. Each item is now judged on its own: revert when our value is still live, leave it when someone else changed or removed it, and report what was skipped and why. The whole-file hash survives as a reported fileDrifted signal and still gates the one destructive case (removing a key absent before install), which additionally needs the activation backup to agree -- value equality on a boolean is one bit of provenance, not proof we wrote it. - InstallManifest v2 records per-key priorValue/appliedValue/setByCodexclaw; v1 manifests still read, and parseInstallManifest replaces a bare cast since this repo has no tsc step to catch a malformed one. - Table-key restores run as one read-modify-write BEFORE the flag pass, so the features CLI (which re-reads from disk) preserves them. - Reading live flag state can no longer fail the uninstall: it falls back to the manifest and says so. - The fake codex in tests now edits in place like real toml_edit; regenerating the file from scratch let a foreign-line-clobbering bug pass its own test.
The Interview only fired when the user named it, so the phase whose job is catching misunderstandings never ran on the request most likely to hold one -- the first "plan this" of a new unit. Promotion is advisory: it injects the interview directive and leaves the FSM exactly where it was. Writing phase:"I" would have trapped the user, since a session promoted that way has no interview tracker, the I->P soft gate blocks it, and reset only makes the next prompt promote again -- no keyboard-reachable path back to P. - interview-policy.ts: off | new-unit | always, read from a committed codexclaw.json at the repo root. Default new-unit: advise on a fresh plan request, stay quiet mid-cycle. - Only the P trigger promotes. A/B/C are excluded because mayEnter deliberately refuses to enter a cycle on 구현해/검증해 (TRIGGER-AUTHORITY-01), and those are ordinary Korean verbs that would drag one-line asks into an interview. - Goal-mode suppression still wins, and the goal lookup stays behind the P check so an ordinary prompt opens no sqlite. - A test walks every policy/trigger/state combination asserting the phase always equals the raw trigger, so the wedge cannot reappear.
The setter and policy from the previous cycles had no user-facing entry point. Both bins dropped every argument after the verb for config-guard, so a nested "config set <key> <value>" could not have carried its payload. - config-set.ts does backup, write, manifest record and hash refresh in ONE path. Recording is not bookkeeping: deactivate reverts only keys present in the manifest, so a write that skipped it would be permanently unrevertable. With no manifest it refuses and points at cxc enable rather than writing something cxc disable could never undo. - Repeated sets keep the user's original priorValue, so history cannot be rewritten into "the prior value was our own value". - config set prints the key's side effect BEFORE writing, and a key outside the whitelist is refused without touching the file -- this is not a general TOML editor. - config interview routes to pabcd-state, which owns codexclaw.json; a writer in config-guard would drift from the hook that reads it on every prompt. Writing preserves unrelated keys in that file. - config joins the --help exit-0 contract, since a new verb with nested subcommands is the shape most likely to fail that unnoticed.
The #49 --generated exclusion was written into the receipt but never read back. validateCheckReceipt re-captured the tree with { excludeCodexclawArtifacts: true } only, so a receipt captured WITH an exclusion was compared against a tree captured WITHOUT it. While any declared path kept changing, the edge was structurally unpassable: re-running the check could not help, because the mismatch lived in the comparison rather than in the tree. - source-receipt.ts parses generatedPaths back off the receipt, dropping malformed entries instead of rejecting an otherwise valid receipt - check-gate.ts passes them to the re-capture so both sides share one exclusion set - new check-gate-generated.test.ts covers validateCheckReceipt, which had no test at all; verified red without the fix and green with it Also lands the plan unit for the request_user_input autopilot work (wp1 of devlog/_plan/260829_request-user-input-autopilot), whose D transition is what surfaced this gate bug.
features.default_mode_request_user_input is what exposes request_user_input in
Default collaboration mode, and it was the one declared flag whose enable failure
could not fail anything: SOFT_FEATURES made activate continue, and the only trace
was a parenthetical on the success line.
- features.ts: the old rationale claimed the flag "may fail because it is
under-development". That is false - codex-rs validate_feature checks only
is_known_feature_key and never the stage, and under-development merely prints a
warning after a successful write. The real failure mode is a build that does not
know the key, which is exactly what the user must hear about. Adds
SOFT_FEATURE_IMPACT so the lost capability is named next to the membership decision.
- activate.ts: FlagRecord gains an optional failure {exitCode, message}, recorded for
hard failures too before the throw. parseInstallManifest reads it back leniently,
so a malformed field drops itself rather than voiding a manifest the revert path needs.
- cli.ts: renderSoftFailureWarning is pure and testable; main writes it to stderr with
the impact, the codex message and both recovery commands. Exit stays 0 because
activation itself succeeded.
Verified with a fake codex on PATH in a throwaway CODEX_HOME that fails only the soft
flag: the warning names exit 2 and the impact, and the manifest carries the failure.
New soft-failure-visibility.test.ts 7/7; full suite 2099 pass / 0 fail.
Plan: devlog/_plan/260829_request-user-input-autopilot/010_soft_silence.md (wp2)
…gate goalplan grew a real dependency graph this cycle: dependsOn on tasks and work-phases, an outcome recorded at close, readiness-based selection, graph rejection before anything trusts it, and a shared write lock that makes D-close idempotent. The subagent evidence gate stopped trapping read-only children. Every version surface moves to 0.2.15, the inventory is regenerated, and the changelog gains a 0.2.15 section.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 05db9d0707
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| flags, | ||
| // Installation never writes a managed key: every CONFIG_MANAGED_KEYS entry is | ||
| // autoEnable:false, so this starts empty and only `cxc config set` adds to it. | ||
| tableKeys: {}, |
There was a problem hiding this comment.
Preserve managed-key records when enabling again
When a user runs cxc config set memories.dedicated_tools true and later reruns the otherwise idempotent cxc enable, activate() overwrites the existing version-2 manifest with tableKeys: {}. This discards the recorded priorValue and ownership needed by deactivate(), so a subsequent cxc disable leaves the managed setting enabled instead of restoring it. Merge the existing manifest's managed-key records when re-enabling rather than resetting them.
Useful? React with 👍 / 👎.
| if (process.argv[1] !== undefined && process.argv[1].endsWith("capture-goalplan-baseline.mjs")) { | ||
| captureBaseline(); |
There was a problem hiding this comment.
Prevent the generator from running as a test
When the workspace package's advertised npm test script runs node --test, Node discovers this .mjs file under test/fixtures, and this direct-execution check is also true for the test-runner process. It therefore invokes captureBaseline() against the package working directory, where .codexclaw/goalplans is absent, and the suite fails with ENOENT; I reproduced this as 1096 passing tests and one failure at this file. Keep the executable generator outside automatic test discovery or use a guard that distinguishes an intentional generator invocation.
Useful? React with 👍 / 👎.
…able A brand-new goalplan cannot be completed today. buildGoalplan() declares schemaVersion 3, every version >= 2 requires an approved finalGate, and no shipped verb opens a final_gate review round - review-round open hardcodes plan_audit and --lane is parsed nowhere. So validateGoalplan always reports a reason the user cannot discharge, and update_goal complete stays denied. This is the docs-only roadmap cycle for the fix: research in 000-002, and one diff-level doc per implementation phase (010 default flip, 020 remediation wording, 030 release). Two independent reviewers audited the plan. Round 1 verified the causal chain and found a test that fails on contact plus a CLI hunk that would not compile. Round 2 caught what round 1 missed: flipping the default would also silently drop the v3 task-outcome checks. 010 now unversions those two checks instead, so the default path keeps that coverage. Both rounds' claims were re-verified at file:line before acceptance, and two reviewer errors are rebutted in 002.
…leted buildGoalplan() declared SUPPORTED_MAX_SCHEMA_VERSION, so every plan created by cxc loop init claimed v3. Every version >= 2 requires an approved finalGate, and no shipped verb opens a final_gate review round: review-round open hardcodes purpose plan_audit and never parses a lane. So a fresh plan validated with a reason its owner could not discharge, and GOAL-COMPLETE-GATE-01 denied update_goal complete for all of them. The suite had already noticed - several tests overrode schemaVersion to 1 by hand to keep their assertions meaningful. New plans now declare DEFAULT_NEW_SCHEMA_VERSION (1), whose rules are all reachable. Callers that want the stricter schema ask for it: buildGoalplan takes schemaVersion, and cxc loop init takes --schema-version <n>, clamped into the range this build can read so it cannot mint a file it refuses to reopen. The gate itself is untouched. A plan declaring v2 or v3 still fails without an approved finalGate - verified on the built artifact, along with the v1 default passing and the clamp behaviour.
… not exist The gateless-v2 reason told the reader to run `cxc review-round open --lane final_gate --session <id>`. No parser in this repo accepts --lane, so the flag was ignored, the round opened as plan_audit, and roundReasons then refused it for being a plan audit. Following the instruction produced a second, more confusing failure. The comment above it already admitted no final-gate verb exists. The reason now states the true situation and names the escape that works. It also reports the version it actually saw - a v3 plan used to be told "schemaVersion 2 requires". final-gate.test.ts asserts on the reason the caller receives, including that it contains no --lane, so the phantom flag cannot return. Three test comments that explained their version pin by citing the old v3 default now state the pin as a deliberate premise.
Every plan cxc loop init created declared schemaVersion 3, and every version at or above 2 demands an approved finalGate that no shipped verb can open. So a fresh plan always validated with a reason its owner could not discharge, and GOAL-COMPLETE-GATE-01 denied update_goal complete for all of them. New plans now declare 1; 2 and 3 are opt-in via --schema-version. The gate is unchanged for plans that declare it, and the reason it prints no longer names a --lane flag that never existed. All 14 version surfaces move to 0.2.16 and the inventory is regenerated. Note that check-versions.mjs covers 12 of them - cli/package.json and plugins/codexclaw/gui/package.json are outside collectSurfaces() and were read back by hand.
Three tests in subagent-evidence.test.ts have been red on Windows since 0.2.14,
for reasons that are about the test harness rather than the code under test.
The tombstone race test resolved its src path with
`new URL(".", import.meta.url).pathname`, which yields "/D:/a/..." on Windows -
not a path any loader accepts. Both spawned children failed to import, so the
race read as lost. Now it uses fileURLToPath, which is what every other suite
in this component already does.
The other two stage a premise Windows cannot create: chmod write bits are
advisory on directories there, and a file standing in a directory position does
not raise ENOTDIR. They now skip on win32 with the reason stated, rather than
asserting a condition the platform refuses to produce. The rules themselves are
platform-independent and still fully exercised elsewhere - the suite reports
skipped 0 on macOS, so nothing was quietly disabled.
…g their errors The previous commit fixed the path but not the specifier. The child imports the module by ESM specifier, and a specifier must be a file:// URL - a bare "D:\\a\\..." does not resolve, so both racers died on Windows before recording anything and the assertion saw an empty list. `stdio: "ignore"` is what made this expensive: a child that never started was indistinguishable from a lost verdict, so a spawn bug read as a lock bug. The test now keeps stderr and asserts the racers actually ran before judging the race, with the child's own error in the message. Verified non-vacuous by pointing the specifier at a nonexistent path: the new assertion reports the dead child instead of a bare empty array.
Promotes
devtomain.mainsits at 0.2.13, so this carries the 0.2.14 and0.2.15 releases along with 0.2.16.
0.2.16 — a new goalplan you can actually finish
buildGoalplan()declaredSUPPORTED_MAX_SCHEMA_VERSION(3), so every plancxc loop initcreated claimed v3. Every version at or above 2 requires anapproved
finalGate, and no shipped verb opens afinal_gatereview round:review-round openhardcodespurpose: "plan_audit"and its parser neveraccepts
--lane. So a fresh plan always validated with a reason its owner couldnot discharge, and
GOAL-COMPLETE-GATE-01deniedupdate_goal completefor allof them. The suite had already noticed — several tests overrode
schemaVersionto1 by hand to keep their assertions meaningful.
New plans declare
DEFAULT_NEW_SCHEMA_VERSION(1), whose rules are allreachable. The stricter schema is opt-in via
buildGoalplan({schemaVersion})orcxc loop init --schema-version <n>, clamped to the range this build can read soit cannot mint a file it refuses to reopen.
The gate is untouched: a plan declaring 2 or 3 still fails without an approved
gate. The reason it prints no longer names a flag that never existed, and now
reports the version it actually saw instead of always saying 2.
Verified
Measured through the installed CLI in an isolated cwd, not from source:
loop validate: OKwith nofinalGateFAILboth — gate standsdependsOnintegrity applies at both--lane; saysschemaVersion 3for a v3 plannpm testnpm run build0.2.16+codex.260830094500New regressions were mutation-tested rather than assumed: reverting the default to
3 turns the v1-default test red, and reintroducing
--laneturnsfinal-gate.test.tsred. Source was restored byte-identical afterwards.Two corrections worth reading
An audit round recommended making the task-outcome checks version-independent so
the v1 default would not lose them. That was implemented, then reverted: two
tests encode a deliberate v1/v2 exemption, and applying the rule retroactively
made an existing legacy plan un-completable — the same class of surprise blocker
this PR removes. Rationale in
012_second_plan_correction.md.This unit's own goalplan was created before the fix and was stranded at v3. It now
declares 1, with a field-level diff proving only
schemaVersionandupdatedAtmoved. Reasoning, including the reversal of an earlier refusal, is in
013_self_referential_leftover.md.Blocked / not included
The full
--lane final_gatelifecycle is still unimplemented — a lane flag, afinal_gatebranch inopenRound, and observer support for a second purpose. Itis deliberately out of scope: it is the larger change and would not fix the
reported bug, since every ordinary run would still owe a dispatched reviewer
before completion.