Skip to content
2 changes: 1 addition & 1 deletion .claude/skills/design-doc-creator/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ The script creates a comprehensive template. Fill in:
- Overview: High-level approach
- Architecture: Technical design
- Implementation plan: Break into phases with tasks
- Files to modify: List new/changed files with LOC estimates
- Files to modify: List new/changed files with LOC estimates. Use one repo-relative path per top-level bullet and make it the first backticked token; continuation lines and later backticks are prose.

**Examples:**
- Show before/after code or workflows
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Complete reference for AILANG design documents.
**Priority**: P0 (High) | P1 (Medium) | P2 (Low)
**Estimated**: 3 days
**Dependencies**: None | Feature X, Feature Y
**Planner-Lane**: codex-ok | opus-required
```

**Purpose**: Quick metadata for understanding scope and priority.
Expand Down Expand Up @@ -297,6 +298,8 @@ Create a central builtin registry where all builtins are registered once with:
```markdown
### Files to Modify/Create

Use one repo-relative path per top-level bullet, with that path as the bullet's first backticked token; put explanatory prose and any later backticks after it.

**New files:**
- `internal/builtins/spec.go` (~300 LOC) - BuiltinSpec struct and registry
- `internal/builtins/validator.go` (~150 LOC) - Validation logic
Expand Down
41 changes: 40 additions & 1 deletion .claude/skills/mission-control/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,7 @@ model from the driver-exported env (defaults track the charter table):
|---|---|---|
| Controller (this session: triage/pick/record/retro) | `$MODEL` (session) | **Opus** (opus-first since 2026-07-16, Mark: the long orchestration session is mechanical work — it must NOT ride Fable) |
| Design-doc-creator | **ROTATION** (Mark 2026-07-17; `$MISSION_DESIGNER_MODEL` is the rotation SEED, not a fixed pin) | Rotate per new-doc iteration: `claude:claude-fable-5` → `codex:gpt-5.6-sol` → (gemini after G4) → repeat. State: `~/.ailang/state/mission-designer-rotation` holds the LAST-USED value; pick the next list entry (missing file = start at claude), write back after the designer run. Every design passes the quorum regardless of author — record `(designer, quorum outcome)` in the evidence row. A probe-failed designer falls to the NEXT in rotation (not to `$MODEL`), FLAGGED |
| Sprint-planner | `$MISSION_PLANNER_MODEL` | Opus (down-tier A/B = M3; keep Opus until evidence) |
| Sprint-planner | `$MISSION_PLANNER_MODEL` | `codex:gpt-5.6-sol` configured default; effective lane = `derive-planner-lane.sh` output, used VERBATIM; fail-closed to opus |
| Sprint-executor | `$MISSION_EXECUTOR_MODEL` | Opus |
| Sprint-evaluator | `$MISSION_EVALUATOR_MODEL` | **Sonnet** (default changed fable→sonnet 2026-07-16 iter 38, Mark directive #399: "default … gemini (if able to git clone the codebase etc)? otherwise sonnet-5"; gemini-managed_agents VERIFIED not-viable-today — server-side sandbox sees no worktree + backend timed out; sonnet ≠ opus executor → generator≠judge, and it's Agent-tool-PINNABLE unlike fable) |

Expand All @@ -586,6 +586,17 @@ session is NOT Fable, a fable pin is unenforceable — apply the generator≠jud
silently inherit. `provider:model` values (e.g. `codex:gpt-5.6-sol`) instead signal cross-provider
routing via `provider_executor` (fleet Phase C), not the Agent tool.

**Step 1b — derive the effective planner lane (MANDATORY; before ANY planner probe or spawn).**
Run `tools/launchd/derive-planner-lane.sh <the-picked-design-doc>` with the driver-exported
environment intact. Its output is exactly one line, `<lane> <reason-token>`; use that line
VERBATIM. If it begins `opus `, spawn the opus Agent path directly and do **not** perform a codex
probe or spawn for the planner role; copy the reason token VERBATIM into the Gate-4
routing-evidence row. Only `codex declared:codex-ok` enters the codex planner recipe below. If the
script is missing on disk, fail closed to opus **LOUDLY** and record the missing-script reason in
the same evidence row. This rule is mission-independent and live wherever this shared skill is
resolved: the step-0 environment pin protects missions configured for opus, and the missing-script
rule protects missions whose checkout has no derivation script (including Ailang World).

**Cross-provider spawn recipe (`provider:model`, M1b — currently `codex` only).** When a role's env
value matches `^([a-z_]+):(.+)$`, DO NOT use the Agent tool. Split it (`PROVIDER=${VAL%%:*}`,
`MODEL=${VAL#*:}`) and route:
Expand Down Expand Up @@ -691,6 +702,34 @@ value matches `^([a-z_]+):(.+)$`, DO NOT use the Agent tool. Split it (`PROVIDER
(d) proves the reconstruction faithful by sha256-manifesting the executor's final tree BEFORE
starting and `shasum -c` after the last commit — byte-identity or the reconstruction is wrong.
Two milestones that touch the SAME file are exactly why snapshots beat file-lists here.
2a. **Planner role — parameterize this executor recipe; do not fork it.** Apply every shared
probe, bounded-background-run, directive-delivery, stdin, sandbox, output-capture, hygiene,
timeout, and fallback guard above by reference (including the executor recipe's `exit 64`,
`< /dev/null`, and `run_in_background` guards). There are exactly four planner deltas:
- **Working directory:** first assert
`git status --porcelain -- <design-doc>` is empty. From local `HEAD`, create an ephemeral
detached worktree with `git worktree add --detach`, then pass its path with `-C`. The path
MUST be a SIBLING OF THIS MISSION'S REPO — DERIVE it, never hardcode it
(`"$(cd "$REPO/.." && pwd)/.planner-wt-iter<N>"`): this skill is shared by every mission on
the rig, so an absolute path baked in for one of them is wrong for the others. Worktrees
under `/tmp` are forbidden — CWD-relative path tests then fail for the LOCATION rather than
the code, and CI never reproduces that red.
Never use `-b` or base it on `origin/dev`: a committed-but-unpushed design doc must be
visible to the planner.
- **Directive:** use the per-iteration file
`/tmp/codex_planner_directive_iter<N>.txt`, carrying the executor recipe's identical
≥200-byte delivery assertion and closed-stdin behavior on both probe and run by reference.
- **Sandbox directories and evidence:** keep `--add-dir "$GOCACHE" --add-dir "$GOMODCACHE"`.
**In-sandbox gate verdicts are NOT evidence**: socket-touching checks
are `UNINFORMATIVE UNDER SANDBOX`, and the controller re-verifies load-bearing premises
outside the sandbox before handing the plan to the executor.
- **Post-run controller steps:** (1) assert both artifacts exist in the worktree and are
well-formed (`jq -e . sprint_<id>.json`; plan non-empty and names the design doc); (2) reject
placeholder vacuous-passes (`MILESTONE_ID` or `auto-parse failed`); (3) copy both artifacts
to their main-checkout paths, refusing to overwrite unexpected existing files; (4) remove
the planner worktree; (5) run
`ailang messages import-github --labels bug,feature,ailang-message` outside the sandbox;
(6) commit with `Co-Authored-By: codex <model>`.
3. **generator≠judge guard (HARD, constraint #3):** before spawning the evaluator, assert the
evaluator's PROVIDER ≠ the executor's PROVIDER. If the executor ran on codex, the evaluator MUST
NOT be a codex `provider:model` — if `$MISSION_EVALUATOR_MODEL` collides, re-route the evaluator
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ For the latest version, see [changelogs/v0.18-current.md](changelogs/v0.18-curre
unencodable chunk. See [#546](https://github.com/sunholo-data/ailang/issues/546)
and [arniwesth/ailang#2](https://github.com/arniwesth/ailang/pull/2).

### Mission infrastructure (no user-facing language or CLI change)

- The mission loop's sprint-planner now defaults to the ChatGPT-subscription codex
lane instead of opus, so opus stays controller-only. The configured default is not
the effective lane: a new `tools/launchd/derive-planner-lane.sh` reads the picked
design doc and fails **closed** to opus unless the doc declares
`**Planner-Lane**: codex-ok` and every path in its Files section is inside a narrow
infrastructure allowlist. Rollback is one commented line in
`~/.config/ailang/mission-<name>.env`. Design docs gain an optional
`**Planner-Lane**` header field. See
`design_docs/implemented/v1_0_0/m-planner-codex-lane.md`.

## Changelog Archives

The full changelog has been split into themed files for searchability and readability:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,23 +368,65 @@ of forking them, so a future guard fix lands once.
**Modified:**
- `tools/launchd/mission-control.sh` — role-generic probe loop (Bash 3.2 form) + gated default flip (~35 LOC net)
- `.claude/skills/mission-control/SKILL.md` — roles-table cell + derivation step + planner sub-bullet (~55 lines)
- `~/.claude/skills/mission-control/SKILL.md` — sync copy (L15)
- `~/dev/sunholo-data/ailang-world/tools/launchd/mission-control.sh` — sync copy (L15; see D6)
- `.claude/skills/design-doc-creator/SKILL.md` — one-line template addition: the
`**Planner-Lane**: codex-ok | opus-required` header field (D2 field supply)

**New:**
- `tools/launchd/derive-planner-lane.sh` — D2 stage-2 derivation (~60 LOC, Bash 3.2-compatible,
pure text — contains no codex invocation, provable by AC-D2's grep)
- `tools/launchd/testdata/planner-lane/` — eight tiny fixture docs for AC-D2 (rev 3, per
- `tools/launchd/testdata/planner-lane/` — twelve tiny fixture docs for AC-D2 (rev 3, per
gpt5-6-sol's R2 fixture list): (a) unlisted language path (declares `codex-ok`,
Files-to-Modify touches `internal/parser/…` — the lying-declaration case), (b) field-missing,
(c) clean infra `codex-ok`, (f) unknown FUTURE `internal/...` path (a dir that does not exist
Files-to-Modify touches internal/parser/… — the lying-declaration case), (b) field-missing,
(c) clean infra `codex-ok`, (f) unknown FUTURE internal/... path (a dir that does not exist
today), (g) mixed infra + language paths, (h) malformed Files-to-Modify section, (i) duplicate
Files-to-Modify sections
Files-to-Modify sections, (j) prose-first bullet, (k) alternate `## Files to Modify`
heading, (l) invalid Planner-Lane value, (m) `opus-required`, (n) a Files bullet carrying no
backticked token at all — the sentinel arm (j) does not reach

**No Go code. No AILANG code.**

### Landing Checklist

Completed at mission iteration 136 (2026-08-04). Each row records what was actually done.

- **`~/.claude` skill copy — NO-OP BY CONSTRUCTION, not skipped.** `~/.claude/skills/mission-control`
is a SYMLINK to this repo's `.claude/skills/mission-control` (`readlink` + same inode, measured).
The two "copies" are ONE FILE, so the plan's `diff -q` criterion is satisfied the moment the edit
reaches the MAIN checkout. It follows that a skill edit committed only from a worktree never
reaches the running skill — this sprint landed via PR and then fast-forwarded the main checkout.
- **World driver sync — DECLINED, deliberately, message sent.** The plan recommended syncing
`tools/launchd/mission-control.sh` into `~/dev/sunholo-data/ailang-world/` and pinning World's
planner to opus in `mission-world.env`, on the stated premise that World was kill-switched and so
"verifiable with zero live risk". THAT PREMISE NO LONGER HOLDS — World is armed and ran its
iteration 45 the same night. Declining is strictly lower risk and costs World nothing: its planner
resolves to opus either way, and now every layer agrees on opus rather than relying on a pin.
Cost: the World driver now LAGS this one (they were byte-identical; `diff -q` was the drift
detector and it is now tripped by design). Surfaced to Mark as a pending ops call.
- **`.agents/skills/mission-control/SKILL.md` — OUT OF SCOPE, stale by design**, tracked by `#544`
(44,067 B vs 92,077 B). Not synced; `.agents/` deliberately NOT added to the D2 allowlist.
- **`mission-v1.env` rollback line present and COMMENTED.** Created at
`~/.config/ailang/mission-v1.env`; proven inert while commented, and uncommenting was exercised
once (`planner=ROLLBACK-PROVEN`) then reverted.
- **`git worktree list | grep planner-wt-` — empty.** The ephemeral AC3a worktree was removed
(control: the sprint worktree is still listed, so the grep sees worktrees).

### Engagement-rate reality (§0.5, measured at landing — do NOT oversell this lane)

Of the planned design docs, **41 carry a `Files` heading** and **40 of them derive
`opus fail-closed:planner-lane-field-missing`**. Exactly **ONE** doc declares `**Planner-Lane**`
today: this sprint's own. So the codex planner lane engages only on NEWLY AUTHORED infra docs, and
the doc's success metric ("`planner=codex…` appears in the evidence row on infra iterations")
matures over weeks, not on day one. That is the fail-closed design working as intended.

### The revert is three-tier — D6's "one env var" is true only for M4

| Landed | Revert | Covered by D6's env var? |
|---|---|---|
| M1 script/fixtures/template | `rm` the script + revert 2 markdown files | n/a — zero runtime effect |
| **M2 driver probe loop + rollback plumbing** | `git checkout <sha> -- tools/launchd/mission-control.sh` — a CODE revert. An env var cannot un-break a driver that fails to parse. | **NO** |
| M3 skill | revert the skill (one file; the `~/.claude` copy follows by symlink) | **NO** (mitigated: derivation fails closed to opus if the script is gone) |
| M4 default flip | uncomment `MISSION_PLANNER_MODEL` in `~/.config/ailang/mission-<name>.env` | **YES** — and only because M2 first built the delivery mechanism D6 already claimed existed |

---

## Acceptance Criteria (D5 — non-vacuous by construction)
Expand Down
98 changes: 98 additions & 0 deletions tools/launchd/derive-planner-lane.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/bin/bash
# Derive the mission planner lane from a design document. Pure text only.

emit() {
printf '%s\n' "$1"
exit 0
}

# Step 0: an explicit non-codex planner pin always wins.
case "${MISSION_PLANNER_MODEL:-}" in
codex:*) ;;
*) emit "opus fail-closed:env-pin" ;;
esac

# Step 1: require one readable document argument.
doc=${1:-}
if [ -z "$doc" ] || [ ! -r "$doc" ]; then
printf '%s\n' "derive-planner-lane: design document is missing or unreadable" >&2
emit "opus fail-closed:no-doc"
fi

# Step 2: read and validate the declaration.
planner_line=$(grep -m1 -E '^\*\*Planner-Lane\*\*:' "$doc" 2>/dev/null)
if [ -z "$planner_line" ]; then
emit "opus fail-closed:planner-lane-field-missing"
fi
planner_value=${planner_line#\*\*Planner-Lane\*\*:}
planner_value=$(printf '%s\n' "$planner_value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
case "$planner_value" in
codex-ok|opus-required) ;;
*) emit "opus fail-closed:planner-lane-field-invalid" ;;
esac

# Step 3: an opus declaration needs no path analysis.
if [ "$planner_value" = "opus-required" ]; then
emit "opus declared:opus-required"
fi

# Step 4: locate exactly one Files section and extract the first backticked
# token from every top-level bullet in it. Continuations and later tokens are
# prose. The awk matcher is the portable ERE equivalent of Files\b.
section_count=$(awk '
BEGIN { count = 0 }
{
line = tolower($0)
if (line ~ /^#{2,4}[[:space:]]+files([^[:alnum:]_]|$)/) count++
}
END { print count }
' "$doc")
if [ "$section_count" -ne 1 ]; then
emit "opus fail-closed:no-files-section"
fi

paths=$(awk '
BEGIN { in_files = 0 }
{
line = tolower($0)
if (!in_files && line ~ /^#{2,4}[[:space:]]+files([^[:alnum:]_]|$)/) {
in_files = 1
next
}
if (in_files && ($0 ~ /^#{1,4}[[:space:]]/ || $0 ~ /^---$/)) exit
if (in_files && $0 ~ /^- /) {
if (match($0, /`[^`]+`/)) print substr($0, RSTART + 1, RLENGTH - 2)
else print "__UNPARSABLE_PATH_ENTRY__"
}
}
' "$doc")

if [ -z "$paths" ]; then
printf '%s\n' "derive-planner-lane: Files section has no path bullets" >&2
emit "opus fail-closed:unparsable-path-entry"
fi

old_ifs=$IFS
IFS='
'
for path in $paths; do
if [ -z "$path" ] || [ "$path" = "__UNPARSABLE_PATH_ENTRY__" ]; then
IFS=$old_ifs
emit "opus fail-closed:unparsable-path-entry"
fi
case "$path" in
*/*|*.md|*.sh|*.go|*.yml) ;;
*) IFS=$old_ifs; emit "opus fail-closed:unparsable-path-entry" ;;
esac
case "$path" in
/*|~*|*..*) IFS=$old_ifs; emit "opus fail-closed:path-not-in-codex-allowlist" ;;
esac
case "$path" in
tools/launchd/*|.claude/skills/mission-control/SKILL.md|.claude/skills/design-doc-creator/*) ;;
*) IFS=$old_ifs; emit "opus fail-closed:path-not-in-codex-allowlist" ;;
esac
done
IFS=$old_ifs

# Step 5: every declared path is approved infrastructure.
emit "codex declared:codex-ok"
Loading
Loading