Skip to content

release(0.2.16): a new goalplan you can actually finish - #60

Merged
lidge-jun merged 128 commits into
mainfrom
dev
Aug 30, 2026
Merged

release(0.2.16): a new goalplan you can actually finish#60
lidge-jun merged 128 commits into
mainfrom
dev

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Promotes dev to main. main sits at 0.2.13, so this carries the 0.2.14 and
0.2.15 releases along with 0.2.16.

0.2.16 — a new goalplan you can actually finish

buildGoalplan() declared SUPPORTED_MAX_SCHEMA_VERSION (3), so every plan
cxc loop init created claimed v3. Every version at or above 2 requires an
approved finalGate, and no shipped verb opens a final_gate review round:
review-round open hardcodes purpose: "plan_audit" and its parser never
accepts --lane. 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. The suite had already noticed — several tests overrode schemaVersion to
1 by hand to keep their assertions meaningful.

New plans declare DEFAULT_NEW_SCHEMA_VERSION (1), whose rules are all
reachable. The stricter schema is opt-in via buildGoalplan({schemaVersion}) or
cxc loop init --schema-version <n>, clamped to the range this build can read so
it 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:

Check Result
fresh plan, all phases and criteria satisfied loop validate: OK with no finalGate
same plan, only the version changed to 2 / 3 FAIL both — gate stands
v3 opt-in task-outcome rules fire at v3, not v1; dependsOn integrity applies at both
remediation text no --lane; says schemaVersion 3 for a v3 plan
npm test 2273/2273
pabcd-state suite 1098/1098
npm run build exit 0
installed runtime 0.2.16+codex.260830094500

New regressions were mutation-tested rather than assumed: reverting the default to
3 turns the v1-default test red, and reintroducing --lane turns
final-gate.test.ts red. 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 schemaVersion and updatedAt
moved. Reasoning, including the reversal of an earlier refusal, is in
013_self_referential_leftover.md.

Blocked / not included

The full --lane final_gate lifecycle is still unimplemented — a lane flag, a
final_gate branch in openRound, and observer support for a second purpose. It
is 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.

…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.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 29, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-29T23:26:04.422664Z 05db9d0 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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: {},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +160 to +161
if (process.argv[1] !== undefined && process.argv[1].endsWith("capture-goalplan-baseline.mjs")) {
captureBaseline();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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.
@lidge-jun lidge-jun changed the title release(0.2.15): dependency-aware goalplan control plane release(0.2.16): a new goalplan you can actually finish Aug 30, 2026
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.
@lidge-jun
lidge-jun merged commit aaf0e70 into main Aug 30, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant