Skip to content

mission: planner codex lane — derivation gate, role-generic driver probe, default flip (m-planner-codex-lane) - #580

Merged
sunholo-voight-kampff merged 6 commits into
devfrom
sprint/m-planner-codex-lane-iter136
Aug 3, 2026
Merged

mission: planner codex lane — derivation gate, role-generic driver probe, default flip (m-planner-codex-lane)#580
sunholo-voight-kampff merged 6 commits into
devfrom
sprint/m-planner-codex-lane-iter136

Conversation

@sunholo-voight-kampff

Copy link
Copy Markdown
Collaborator

Executes the plan-ready m-planner-codex-lane sprint (Mark's quota-offload #1, [NEXT #2]).
Routes MISSION_PLANNER_MODEL through the hardened codex lane so the mission sprint-planner
rides the ChatGPT-subscription bucket and opus stays controller-only.

CI relevance is ZERO for the load-bearing half. tools/launchd/ has no workflow reference,
no shellcheck, no bash -n gate, no test — verified, with a positive control. A green CI here
says nothing about the driver. The acceptance criteria are the real gate, and every one was
re-run by the controller outside the codex sandbox.

Milestones

  • M1 derive-planner-lane.sh (Bash 3.2 only, pure text, no network, no codex invocation) +
    12 fixtures + the **Planner-Lane** design-doc template field. Zero runtime effect.
  • M2 ⚠ point of no return — the driver's executor-only probe case becomes the role-generic
    D3 loop, plus the two-line mission-<name>.env source that gives D6's documented rollback an
    actual delivery mechanism (D6 previously claimed one existed; it did not).
  • M3 mandatory Gate-3 step 1b (derive before any planner probe/spawn) + a planner sub-bullet
    that parameterizes the executor recipe rather than forking it.
  • M4 AC3a rehearsal (a real codex planner run, validated by the repo's own validator, rc=0),
    then the default flip.

Three things worth reading

  1. A mutation survived. Neutering the __UNPARSABLE_PATH_ENTRY__ sentinel changed nothing:
    fixture (j) is caught one arm earlier by the path-shape check, so a whole arm had zero coverage
    while the matrix read green. Fixture (n) closes it and now kills that mutation.
  2. The plan's AC9 is vacuous. It pins opus via the rollback file and expects planner=opus
    — but pre-flip opus is also the built-in default, so it passes identically whether the file is
    sourced or not. Only a sentinel value makes it a measurement.
  3. Probe dedupe is proven, not asserted. Same bad model on both roles → ONE "unusable" line
    and TWO fallback lines; two different bad models → TWO "unusable" lines. The counter
    discriminates.

Safety

Rollback is one commented line in ~/.config/ailang/mission-<name>.env, exercised end to end.
The derivation fails closed to opus on every ambiguity, and today exactly one design doc
in the repo qualifies — the lane engages on newly authored infra docs only.

Sibling mission: ~/.claude/skills/mission-control is a symlink to this repo's copy and
Ailang World has no repo-local skills directory, so M3 reaches World at its next fire. It fails
closed to opus there via two independent guards. World's driver was deliberately not synced
(the plan's recommendation assumed World was kill-switched; it is live) — cross-mission message
sent, and it is flagged to Mark as a pending ops call.

🤖 Generated with Claude Code

sunholo-voight-kampff and others added 6 commits August 4, 2026 01:12
…mplate field

M-PLANNER-CODEX-LANE milestone 1. Zero runtime effect: nothing invokes
derive-planner-lane.sh until M3 edits the mission-control skill.

- tools/launchd/derive-planner-lane.sh — Bash 3.2 only, pure text, no network and
  no codex invocation. Six-step contract in strict order (env-pin, no-doc,
  Planner-Lane field present/valid, opus-required short-circuit, D2 allowlist
  cross-check, codex-ok). Exactly one line on stdout, always exit 0; the caller
  reads the line, not the rc.
- 11 fixtures under tools/launchd/testdata/planner-lane/.
- design-doc-creator: **Planner-Lane** template field + the one-path-per-bullet,
  path-first convention that the §0.4 extractor depends on.
- m-planner-codex-lane.md: controller ruling encoded — the two ~/absolute sync-copy
  bullets move out of Files-to-Modify into a new Landing Checklist, and the
  internal/parser prose in the fixture bullet is de-backticked. The doc now
  self-qualifies (AC-D0).

Acceptance re-run first-party by the controller OUTSIDE the codex sandbox — AC7b,
AC8(a,b,c,f,g,h,i,j,k,l,m), AC8d, AC8-env, AC-D0 all as expected, /bin/bash 3.2.57.
Known-positive controls (c) and (k) both return codex declared:codex-ok, so the
negative sweep is a measurement rather than a claim.

PLAN DEFECT FOUND — AC8(i) contradicts binding task 1. Task 1 step 4 specifies
"section absent / found more than once -> opus fail-closed:no-files-section", while
the AC8 list groups (i) with (f)(g)(h) under path-not-in-codex-allowlist. Both are
fail-closed to opus so the SAFETY behaviour is identical; only the reason token
differs. Resolved in favour of task 1: §0's corrections are binding, and a duplicate
section means the parser cannot know which one to read, which is what
no-files-section says. AC8(i)'s expected token is corrected accordingly.

Sanity sweep over every planned doc carrying a Files heading: 41 docs, 40
planner-lane-field-missing, 1 codex-ok (this sprint's own doc). Zero hard stops —
no doc touching internal/ derives codex.

Co-Authored-By: codex <gpt-5.6-sol>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t uncovered

Controller mutation probe on M1's derivation script. Four mutations, each PROVEN
applied by cmp against the original before its result was read (a mutation that
never applied and a mutation that does not discriminate are the same output):

  MUT1 allowlist arm neutered      -> (a)(f)(g) flip to codex-ok   KILLED
  MUT2 duplicate-section guard -ne 1 -> -lt 1  -> (i) flips        KILLED
  MUT3 __UNPARSABLE_PATH_ENTRY__ sentinel neutered                SURVIVED
  MUT4 step-0 env-pin removed      -> AC8-env flips to codex-ok    KILLED

MUT3 survived because no committed fixture reaches that arm. Diagnosed by
relabelling the two candidate arms separately: fixture (j)'s first backticked token
EXISTS but is not path-shaped, so (j) is caught one arm later by the path-shape
check — it never touches the sentinel. The sentinel covers a different input class:
a Files bullet with no backticks at all. Both arms are correct and both emit
unparsable-path-entry, which is exactly why the gap was invisible — the matrix read
green while a whole arm went unexercised.

Fixture (n) supplies that input. Re-running MUT3 with (n) in the matrix now yields
codex declared:codex-ok from the mutant against opus fail-closed:unparsable-path-entry
from the real script, so the mutation is killed.

derive-planner-lane.sh is byte-identical to M1 (sha256 026553562c93…) — restored
from the executor's .snap/M1/ snapshot after an earlier mutation harness of mine
clobbered its own backup. Design doc fixture list corrected eight -> twelve;
AC-D0 re-verified after the edit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ing in the driver

M-PLANNER-CODEX-LANE milestone 2, the sprint's point of no return: this edits
tools/launchd/mission-control.sh, the launchd driver that fires the mission loop
every 90 minutes. It has ZERO CI coverage — no shellcheck, no bash -n gate, no
test — so a green CI says nothing about this change and the acceptance criteria
below are the only real gate.

Two edits, nothing else:
- The executor-only codex probe `case` block becomes the design doc's D3
  role-generic loop, adopted VERBATIM (Bash 3.2 form: ':'-delimited string sets
  instead of `declare -A`, `tr` instead of `${role,,}` — both 4.0-isms crash the
  driver's actual /bin/bash 3.2.57). One probe per DISTINCT model, so the
  post-flip both-roles-codex default still costs ONE probe, not two.
- A two-line source of ~/.config/ailang/mission-${MISSION_NAME}.env after
  MISSION_NAME resolution, so D6's documented one-env-var rollback has a delivery
  mechanism for V1 (whose plist sets no MISSION_PROFILE). D6 previously claimed
  this existed; it did not.

The planner default on line 286 is deliberately UNTOUCHED — the flip is M4.

VERIFIED BY THE CONTROLLER OUTSIDE THE CODEX SANDBOX (mandatory: the executor
correctly labelled AC1/AC2/AC9 UNINFORMATIVE UNDER SANDBOX, its nested codex
probes failing at app-server init rather than on the model, and ~/.config writes
being denied outright):
  (a) /bin/bash -n                                            rc=0
  (b) acheck dry-run, zero probes eligible     planner=opus  rc=0
  AC1  planner=codex:gpt-5.6-sol pinned        planner=codex:gpt-5.6-sol, no fallback
  AC2  planner=codex:no-such-model             400 "'no-such-model' model is not
       supported when using Codex with a ChatGPT account" -> "codex planner lane ->
       falling back to opus" -> planner=opus. The failure is caused BY --model.
  #486 non-regression control: the same probe WITHOUT --model returns rc=0 "ok",
       so a model-less probe would still false-green the lane. The guard is
       load-bearing, measured rather than asserted.
  AC7  /bin/bash 3.2.57 banner + roles line     rc=0
  AC9  three-sided: file pinning a SENTINEL -> planner=SENTINEL-FROM-FILE; file
       deleted -> planner=opus; explicit command-line pin -> planner=opus.

AC9 AS WRITTEN IN THE PLAN IS VACUOUS. It pins `opus` via the env file and expects
planner=opus — but pre-flip `opus` is ALSO the built-in default, so it passes
identically whether the file is sourced or not. Only substituting a sentinel value
turns it into a measurement. Same vacuous-pass class this mission has closed three
times before. The sentinel form is what is recorded above.

Also recorded: an earlier sentinel run read as "the rollback plumbing is broken"
because this controller session's OWN shell exports MISSION_PLANNER_MODEL=opus
(the driver exports it into every tool shell), so the file's ${VAR:-...} correctly
deferred to it. The instrument was contaminated, not the plumbing. Every AC9 side
above is run under `env -u MISSION_PLANNER_MODEL`. M4's post-flip AC1 needs the
same precaution or it will read the session's stale pin as the new default.

The v1 namespace cannot be dry-run while the loop is live — it yields at the
overlap guard on this iteration's own pid. That is exactly why §0.1 mandates the
acheck namespace; the sourced path is namespace-generic.

~/.config/ailang/mission-v1.env created with the rollback line COMMENTED OUT and
proven inert; uncommenting it was exercised once (planner=ROLLBACK-PROVEN) and
reverted.

Co-Authored-By: codex <gpt-5.6-sol>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…codex recipe

M-PLANNER-CODEX-LANE milestone 3. Runtime effect: the NEXT controller session of
EVERY mission on this rig.

- Roles table: Sprint-planner default becomes `codex:gpt-5.6-sol`, with the cell
  stating that the EFFECTIVE lane is derive-planner-lane.sh's output used verbatim,
  fail-closed to opus. Supersedes the stale "down-tier A/B = M3; keep Opus until
  evidence" note, which this sprint is the resolution of.
- New Gate-3 step 1b: derivation is MANDATORY and runs before any planner probe or
  spawn. Output beginning `opus ` routes straight to the opus Agent path with NO
  codex probe for the planner role, and the reason token is copied verbatim into
  the Gate-4 evidence row. A missing script fails closed to opus, loudly.
- New planner sub-bullet under PROVIDER=codex, PARAMETERIZING the executor recipe
  rather than forking it — the four deltas only (detached sibling-of-repo worktree
  from local HEAD; per-iteration directive file; GOCACHE/GOMODCACHE plus the
  in-sandbox-verdicts-are-not-evidence caveat; six post-run controller steps).
  Every shared guard is a REFERENCE, so a future guard fix still lands once.

WHY THIS IS SAFE FOR THE SIBLING MISSION, stated because it is NOT optional: the
edit is live for Ailang World the moment it reaches the main checkout, since
~/.claude/skills/mission-control is a SYMLINK to this repo's copy and World has no
repo-local skills directory at all. Two independent guards make that degrade
safely: step 0 of the script returns `opus fail-closed:env-pin` whenever
MISSION_PLANNER_MODEL is not `codex:*` (World's driver still defaults to opus), and
derive-planner-lane.sh does not exist in the World checkout, so the missing-script
rule fails closed. Expect fail-closed tokens in World's evidence rows; that is the
design working, not a break.

CONTROLLER RULINGS APPLIED (the plan's open questions, answered):
- Task 5 "sync to ~/.claude" is a NO-OP BY CONSTRUCTION, not a skipped step. The
  two paths are ONE FILE (readlink + same inode, measured). The plan's `diff -q`
  criterion is satisfied by construction once the edit reaches the main checkout —
  and `diff -q` reporting a difference from a WORKTREE is the expected reading, not
  a failure.
- Task 6: .agents/skills/mission-control/SKILL.md stays OUT OF SCOPE, stale by
  design, tracked by #544. Not synced, and .agents/ is deliberately NOT added to
  the D2 allowlist.

CONTROLLER CORRECTION to the executor's text: it hardcoded an absolute
/Users/voightkampff/... planner-worktree path into a skill that three missions
share. Replaced with a DERIVED sibling-of-repo path and the reason spelled out.
The /tmp prohibition is kept — the plan's own M4 still says /tmp, which predates
the iter-133 rule; deviation recorded.

A SUSPICION I HAD, REPRODUCED AND REFUTED: post-run step (5),
`ailang messages import-github --labels bug,feature,ailang-message`, reads as a
non-sequitur in a planner recipe and I took it for an invented step. It is verbatim
from the design doc's D4 step (5) (compensating for a possibly-skipped in-sandbox
sync, L9/L18) and the subcommand exists in the CLI. The executor was right.

Verified first-party outside the sandbox: AC8e fixture (a) derives
`opus fail-closed:path-not-in-codex-allowlist`; the planner bullet mentions
`exit 64` / `< /dev/null` / `run_in_background` only as references (control: the
executor recipe carries 4 `exit 64` hits, so the grep discriminates); the stale
note is gone (0 hits, control `Sprint-planner` = 1); no hardcoded absolute path
remains in the planner bullet; only SKILL.md changed.

Co-Authored-By: codex <gpt-5.6-sol>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…codex, doc landed

M-PLANNER-CODEX-LANE milestone 4, the last landing. The sprint-planner default is now
`codex:gpt-5.6-sol` (Mark quota-offload #1), so opus stays controller-only.

THE FLIP WAS GATED, NOT ASSUMED. The plan permits it only after M1-M3 and AC3a are
green. AC3a is a REAL codex planner run — full D4 recipe, detached worktree, sprint
id REHEARSAL-ITER136 — and every criterion was re-checked by the controller in the
worktree: `jq -e .` rc=0; 4 features; ZERO `MILESTONE_ID` / `auto-parse failed`
(control: `sprint_id` = 1 hit, so the grep reads the file); all `estimated_loc` > 0;
the plan names the input doc; the `-o` final message is a plan summary, NOT "What
would you like me to work on?". The repo's OWN validator then passed it rc=0. The
codex planner lane works end to end.

Post-flip measurements, all under `env -u MISSION_PLANNER_MODEL`:
  AC1 post-flip, no env pin              planner=codex:gpt-5.6-sol
  the same command WITHOUT `env -u`      planner=opus  <- the trap, shown for contrast
  D6 rollback file uncommented           planner=opus  <- rollback beats the flip
  AC5 directive absent                   rc=64, no codex spawn
  AC5b directive truncated to 50 B       rc=64 ("suspected truncation")
  AC5 control: real 2951 B directive     assertion passes, so 64 is not the only answer
  AC4 `git worktree list | grep planner-wt-`  0  (control: sprint worktree still listed)

PROBE DEDUPE PROVEN WITH A DISCRIMINATING CONTROL, not asserted. Both roles pinned to
the SAME bad model: ONE "codex model 'no-such-model' unusable" line and TWO
"lane -> falling back" lines, both roles landing on opus. Both roles pinned to
DIFFERENT bad models: TWO "unusable" lines. So the counter really does distinguish one
probe from two, and the post-flip both-roles-codex default costs ONE probe per fire.

WORLD-SYNC DECISION — DECLINED, and the reason is a CHANGED PREMISE, not laziness. The
plan recommended syncing this driver into the World checkout "verifiable with zero live
risk, World is currently kill-switched". World is armed and ran its iteration 45 the
same night. Declining costs World nothing — its planner resolves to opus either way,
via two independent guards (the script's step-0 env pin, and the script being absent
from the World checkout so the skill's missing-script rule fails closed). Every layer
now agrees on opus instead of relying on a pin. The cost is real and recorded: the World
driver LAGS this one, and `diff -q` — which was the drift detector — is now tripped by
design. Cross-mission message sent (msg_20260804_012921_899f0e6a); surfaced to Mark as
a pending ops call.

Landing checklist written into the design doc with what was ACTUALLY done, including
the engagement-rate reality the doc must not oversell: 41 planned docs carry a Files
heading, 40 derive `fail-closed:planner-lane-field-missing`, and exactly ONE declares
`**Planner-Lane**` today — this sprint's own. The lane engages on newly authored infra
docs only; the success metric matures over weeks, not on day one.

Also recorded there: D6's "rollback = one env var" is true ONLY for M4, and only
because M2 first built the delivery mechanism D6 already claimed existed. M2 and M3
need code reverts. Three-tier table in the doc.

Design doc + sprint plan moved to design_docs/implemented/v1_0_0/ (plans travel with
their doc). AC-D0 re-verified after the landing-checklist edit — the doc still
self-qualifies `codex declared:codex-ok`.

Co-Authored-By: codex <gpt-5.6-sol>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cture

Per .claude/rules/coding-standards.md — every change gets a CHANGELOG entry. Scoped
as mission infrastructure and explicitly marked as no user-facing language or CLI
change, since tools/launchd/ and .claude/skills/ ship to nobody.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Aug 3, 2026

Copy link
Copy Markdown

@sunholo-voight-kampff
sunholo-voight-kampff merged commit cd76499 into dev Aug 3, 2026
20 checks passed
@sunholo-voight-kampff
sunholo-voight-kampff deleted the sprint/m-planner-codex-lane-iter136 branch August 3, 2026 23:50
sunholo-voight-kampff added a commit that referenced this pull request Aug 3, 2026
…E and the default is flipped

All four milestones of m-planner-codex-lane landed (PR #580 -> squash cd76499, 20 checks
0 failures SHA-addressed) and the sprint-planner default is now codex:gpt-5.6-sol, so opus
is controller-only. Charter queue row -> LANDED; #498 Lane B becomes the queue head.

The three findings worth the record, all from re-running the executor's work first-party
outside its sandbox:

- A MUTATION SURVIVED. Neutering the __UNPARSABLE_PATH_ENTRY__ sentinel changed nothing,
  because fixture (j) is caught one arm earlier by the path-shape check and never reaches
  the sentinel. Both arms are correct and emit the SAME token, which is exactly why a whole
  arm went unexercised while the matrix read green. Fixture (n) closes it.
- THE PLAN'S OWN AC9 IS VACUOUS: it pins `opus` via the rollback file and expects
  planner=opus, but pre-flip `opus` is also the built-in default, so it passes identically
  whether the file is sourced or not. Only a sentinel value makes it a measurement.
- I CONTAMINATED MY OWN INSTRUMENT: the sentinel first read as "the rollback plumbing is
  broken"; in fact this controller session exports MISSION_PLANNER_MODEL=opus, so the file's
  ${VAR:-...} correctly deferred to it. Everything now runs under `env -u`, with the
  contaminated form recorded beside the clean one because the two look identical.

Two of my own suspicions were REFUTED by measurement and are recorded as such: the
`ailang messages import-github` post-run step I took for a hallucination is verbatim from
the design doc's D4 and the subcommand exists; and awk interval expressions {2,4} do work
on this macOS awk, proven with controls in both directions.

STATUS rotation scripted with the line-count assertion the skill demands: before=1732
after=1732 expected=1732, exactly one stamp archived, rotation invariant 3, and a known
queue row re-grepped after the edit (the iter-127 damage was invisible in the STATUS block
itself). git diff --stat read before any git add.

FLAGGED, not buried: no evaluator ran this iteration. tools/launchd/ has zero CI coverage
by design so the ACs were the only real gate and I ran all of them, but generator!=judge was
not exercised. A sonnet judge is owed and is named as the first task of iter-137.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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