From d39fa3d3710562014b67a9d39dd39ca93f555438 Mon Sep 17 00:00:00 2001 From: abdoharb Date: Sun, 4 Oct 2026 22:17:07 +0300 Subject: [PATCH 1/8] feat: harden Codex lead parity and migrate pipeline state Make the generated Codex lead recover the correct team session, preserve planner continuity and approvals, and detect runtime permission failures before delegating work. Align the shared Claude/Codex pipeline contract with Codex's protected configuration directories. Runtime state and migration: - Move generated task records, artifacts, provenance, server credentials, session pins and logs from .agents/ to .pipeline/. Keep discoverable skills under .agents/skills/ and Codex configuration under .codex/. - Synchronize scripts, role instructions, flow references and ignore rules. Read legacy provenance for update triage, but refuse normal bootstrap while legacy task state remains to prevent parallel runtime directories. - Document the breaking path change in migration 02 and the changelog. Existing projects require a task-boundary migration that preserves history, customizations and skills; no automatic migration is performed. Exact lead and planner recovery: - Replace latest-session fallback with a dedicated launcher and per-team session pin captured by SessionStart/UserPromptSubmit hooks. Run without the shared daemon so hooks receive the selected team's launch context. - Validate hook inputs and project boundaries, ignore unrelated Codex chats, prevent pin replacement and concurrent leads, and stop on failed resume or missing session capture. Require explicit --fresh for a reset. - Record the planner thread in task state; reuse it for same-task fixes, recover from task records and decision history, and release it on task completion. Preserve recorded workflow approvals across recovery. Configuration and preflight: - Add optional lead/planner model and reasoning flags, defaulting to inherit, with validation and provenance round-tripping. - Install delegate, status-board and karpathy-guidelines supporting skills without overwriting local files. Expand update guards for permissions, hooks, agent tools, MCP and skills while allowing deliberate local drift. - Check required CLIs, feature capabilities, generated files, actual .pipeline write access and authenticated OpenCode connectivity using the same endpoint/password precedence as oc.sh. Replace the raw flow curl check so custom endpoints work consistently. - Keep workflow consent separate from runtime permissions. Preserve hook review/trust requirements and avoid automatic permission broadening. Documentation and validation: - Add the Codex setup/recovery guide, manual live-verification checklist, migration instructions and synchronized generator lessons/references. - Add no-model behavioral coverage for launch/resume isolation, fresh and failure paths, hook validation, locks, preflight failures, settings, legacy migration guards and customization preservation; run it in CI. - Add an opt-in real Codex OS sandbox check demonstrating writable .pipeline state and protected .agents/skills, .codex and .git paths. Validation passed: smoke (25 checks), invariants (113 rule/file pairs), Codex behavior (8 groups), real Codex 0.160.0 sandbox probe, Bash syntax checks and staged diff whitespace checks. ShellCheck was unavailable locally; CI retains its optional check. No live model-driven pipeline or native planner/skill discovery run was performed. The planner's source write boundary remains instructional under classic workspace-write. --- .github/workflows/ci.yml | 4 +- CHANGELOG.md | 27 +++ CLAUDE.md | 22 ++- README.md | 68 +++++--- SYSTEM.md | 10 +- bin/init.sh | 143 +++++++++++---- docs/ADDING-A-TOOL.md | 10 +- docs/CODEX.md | 125 ++++++++++++++ docs/TEAM.md | 19 +- docs/UPGRADING.md | 21 ++- migrations/02-pipeline-directory.md | 55 ++++++ skills/delegate/SKILL.md | 2 +- .../reference/flow-example.md | 6 +- .../reference/lessons-learned.md | 32 ++++ .../reference/state-file-example.md | 3 +- skills/status-board/SKILL.md | 2 +- skills/toolkit-init/SKILL.md | 12 +- templates/agents-state/TEMPLATE.md.tmpl | 3 +- templates/claude/agents/planner.md.tmpl | 6 +- templates/claude/agents/senior-dev.md.tmpl | 8 +- templates/claude/commands/feature.md.tmpl | 50 +++--- .../claude/commands/toolkit-update.md.tmpl | 21 ++- templates/codex/AGENTS.md.tmpl | 9 +- templates/codex/agents/planner.toml.tmpl | 11 +- templates/codex/hooks.json.tmpl | 7 + templates/codex/hooks/session.py.tmpl | 49 ++++++ templates/codex/skills/feature/SKILL.md.tmpl | 37 +++- .../codex/skills/toolkit-update/SKILL.md.tmpl | 5 + templates/opencode/agent/builder.md.tmpl | 10 +- templates/opencode/agent/reviewer.md.tmpl | 16 +- templates/opencode/agent/tester.md.tmpl | 12 +- templates/scripts/codex-lead.sh.tmpl | 46 +++++ templates/scripts/codex-preflight.sh.tmpl | 57 ++++++ templates/scripts/oc.sh.tmpl | 22 +-- templates/scripts/promote-findings.sh.tmpl | 4 +- templates/scripts/team.sh.tmpl | 40 ++--- templates/scripts/verify-spec.sh.tmpl | 2 +- templates/scripts/verify-state.sh.tmpl | 4 +- test/codex-sandbox.sh | 19 ++ test/codex.sh | 163 ++++++++++++++++++ test/invariants.sh | 18 +- test/smoke.sh | 52 +++--- 42 files changed, 1015 insertions(+), 217 deletions(-) create mode 100644 docs/CODEX.md create mode 100644 migrations/02-pipeline-directory.md create mode 100644 templates/codex/hooks.json.tmpl create mode 100644 templates/codex/hooks/session.py.tmpl create mode 100644 templates/scripts/codex-lead.sh.tmpl create mode 100644 templates/scripts/codex-preflight.sh.tmpl create mode 100644 test/codex-sandbox.sh create mode 100644 test/codex.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7745f1d..3f150b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,10 +13,12 @@ jobs: run: bash test/smoke.sh - name: Cross-file invariants run: bash test/invariants.sh + - name: Codex adapter behavior (no model calls) + run: bash test/codex.sh - name: ShellCheck run: | shellcheck --version >/dev/null 2>&1 || { echo "shellcheck not installed; skipping"; exit 0; } - shellcheck bin/init.sh bin/release.sh test/smoke.sh test/invariants.sh + shellcheck bin/init.sh bin/release.sh test/smoke.sh test/invariants.sh test/codex.sh test/codex-sandbox.sh for f in templates/scripts/*.tmpl; do shellcheck --shell=bash "$f" done diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c444ec..c6ad024 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,33 @@ update workflow. Version bumps mean: **MAJOR** = state-file contract / role authority / script interface changed; **MINOR** = new template, script, flag, or role rule; **PATCH** = prose and docs. +## Unreleased + +- `[contract]` Writable task records, outputs, server credentials, session pins, + and logs move from `.agents/` to `.pipeline/` for both leads. Codex protects + `.agents/` under its default workspace policy; skills remain at + `.agents/skills/`. Migration: `migrations/02-pipeline-directory.md`. + Read-only update triage accepts legacy provenance stamps; a plain bootstrap + refuses an unmigrated runtime rather than silently creating another one. +- `[process]` Codex leads capture exact session IDs through reviewed lifecycle + hooks, pin them per team name, and resume through `codex-lead.sh`. Failed + resumes, missing capture, and concurrent launches stop explicitly. The task + template gains an optional Codex planner thread ID for corrections/recovery. +- `[process]` Added Codex lead/planner model and reasoning flags, defaulting to + inheritance and preserved through the provenance stamp. Supporting delegate, + status-board, and karpathy-guidelines skills are now installed deterministically. +- `[safety]` Codex preflight probes writable state and authenticated OpenCode + access from the active sandbox. The adapter distinguishes workflow consent + from runtime permission approval and preserves task/thread state on failure. + Update consent explicitly covers Codex permissions, execution rules, and hooks. +- `[process]` Added no-model behavioral tests for launcher/hook/preflight and + migration failures, plus an opt-in real Codex sandbox test. CI runs the + behavioral suite. Live model workflow verification remains a documented + manual check; these tests do not claim to establish model-level parity. +- `[docs]` Added `docs/CODEX.md`, migration/recovery guidance, and generalized + lessons about protected instruction directories, exact-session identity, + lifecycle trust, and separate workflow/runtime approvals. + ## v0.9.0 — 2026-09-27 - `[safety]` Synced this toolkit's own opencode v2 migration into the diff --git a/CLAUDE.md b/CLAUDE.md index d5b1fe6..eaf500a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,7 +3,7 @@ A reusable, project-agnostic version of the planner → implement → review → test multi-agent pipeline. `bin/init.sh` scaffolds it into any target repo: Claude or Codex as lead, Claude/Codex planning, OpenCode (any vendor) for -cross-vendor implement/review/test, a state file (`.agents/T-.md`) as the +cross-vendor implement/review/test, a state file (`.pipeline/T-.md`) as the one handoff surface between roles, and a `delegate` skill that keeps the orchestrating lead's own context small across a long run. @@ -18,12 +18,14 @@ toolkit's own code. ## Commands -No build step, no dependencies, no LLM calls in the test path. Verify a +No build step or package dependencies, no LLM calls in the default test path. +Tests use Bash plus Python's standard library. Verify a change with the automated smoke run: ```bash bash test/smoke.sh # scaffolder guarantees + the two structural scripts bash test/invariants.sh # every load-bearing rule present in every copy +bash test/codex.sh # generated launcher/hooks/preflight, no model calls ``` `smoke.sh` scaffolds into a throwaway directory and asserts the core @@ -63,6 +65,7 @@ files are skipped, not clobbered (`render()`'s core guarantee). | `skills/toolkit-release/` | Conversational release flow: classify commits since the last tag into `[contract]›[safety]›[process]›[docs]`, propose a semver-ish version, get user approval, write the CHANGELOG section, run `bin/release.sh` | | `test/smoke.sh` | The automated smoke run (see Commands). CI runs it plus shellcheck on every push | | `test/invariants.sh` | Cross-file rule presence check: one grep per (rule, file) pair over the hand-synced copies. Add a rule = one line in its table | +| `test/codex.sh` | Behavioral checks for the generated launcher, lifecycle hooks, preflight, inheritance, and migration failures, using CLI doubles and no model calls | | `.github/workflows/ci.yml` | Runs `test/smoke.sh` + shellcheck (`bin/init.sh`, the test, and every `templates/scripts/*.tmpl`) | | `templates/claude/agents/` | `planner.md.tmpl`, `senior-dev.md.tmpl` — Claude subagent role definitions | | `templates/claude/commands/` | `feature.md.tmpl` — the `/feature` pipeline command (the lead's own instructions); `toolkit-update.md.tmpl` — the `/toolkit-update` merge command for already-scaffolded projects | @@ -108,7 +111,7 @@ files are skipped, not clobbered (`render()`'s core guarantee). "report your findings/output to the lead." - **Keep `templates/` and any applied copy of this toolkit in sync** (any repo that has scaffolded it, with its own `.claude/` + `.opencode/` + - `.agents/TEMPLATE.md`) when one side gets a + `.pipeline/TEMPLATE.md`) when one side gets a structural fix — a new state-file field, a new script, a report-back change. They're meant to be the same mechanism, generic vs. applied. `test/invariants.sh` covers the state-file contract's two copies; the @@ -155,11 +158,11 @@ files are skipped, not clobbered (`render()`'s core guarantee). destination already exists — if an edit to a `.tmpl` file doesn't show up after a re-run against an existing target, that's why, not a bug in `init.sh`. Use `init.sh --update` to see what changed instead. -- `.agents/.needs-customization` is written only when `FRESH_SCAFFOLD` was +- `.pipeline/.needs-customization` is written only when `FRESH_SCAFFOLD` was true *before* any `render()` call ran (checked via whether `.claude/commands/feature.md` already existed) — never on `--update`, and never again once deleted. The provenance stamp - (`.agents/.toolkit-version`) follows the same ordering rule: written + (`.pipeline/.toolkit-version`) follows the same ordering rule: written exactly once on a fresh scaffold, never by `--update` (that would erase the baseline it exists to record), rewritten only by an explicit `--refresh-stamp` after a merge is accepted. If you add a new @@ -168,7 +171,7 @@ files are skipped, not clobbered (`render()`'s core guarantee). - `templates/opencode/agent/reviewer.md.tmpl` defaults to blanket `edit: deny` / `write: deny`, unlike a project that has since widened its own copy (e.g. an applied copy's reviewer allows - `.agents/**`). That gap is intentional — see README's "Design decisions" + `.pipeline/**`). That gap is intentional — see README's "Design decisions" — not drift to fix by copying the widened version back over the template. - A permission block that reads correctly in YAML is not proof it's @@ -182,8 +185,13 @@ files are skipped, not clobbered (`render()`'s core guarantee). Actively developed; history in git. The scaffolder's core guarantees are covered by `test/smoke.sh`, and cross-copy rule presence by -`test/invariants.sh` (CI: both + shellcheck). What's *not* +`test/invariants.sh` (CI: smoke + invariants + Codex behavior + shellcheck). What's *not* automated: a live end-to-end pipeline run against a real OpenCode server, and live permission-enforcement verification — those stay manual per README's "Design decisions". Treat README.md's "Known gaps" and REVIEW.md's open items as the backlog. + +`test/codex.sh` adds behavioral coverage of the generated Codex integration. +`test/codex-sandbox.sh` is an opt-in real CLI sandbox check without model calls. +Live planner discovery and full workflow execution remain manual; do not call +the CLI-double suite an end-to-end model run. diff --git a/README.md b/README.md index 1514034..b62de61 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ A reusable version of the planner → implement → review → test multi-agent pipeline: Claude or Codex as the lead, Claude/Codex planning, OpenCode (any vendor) for cross-vendor implement/review/test, a state file -(`.agents/T-.md`) as the single handoff surface between roles, and a +(`.pipeline/T-.md`) as the single handoff surface between roles, and a `delegate` skill so the lead's own context stays small across a long run. It was distilled from real multi-agent pipeline runs and hardened there @@ -11,11 +11,17 @@ over time, so the same setup — permissions, session-reuse policy, cross-vendor independence rules, the state-file contract — doesn't get re-invented and re-debugged from scratch in every new repo. -Codex is a supported fallback lead: the scaffold includes project instructions, +Codex is a supported lead: the scaffold includes project instructions, a `feature` skill, a `toolkit-update` skill, and a Codex planner. For any other lead tool, read [`SYSTEM.md`](SYSTEM.md) — one tool-agnostic page meant to be handed to an AI ("recreate this system, with yourself as the lead"). +Both leads store writable task records and runtime files in `.pipeline/`; +Codex skills remain in `.agents/skills/`. Existing projects must apply +[the directory migration](migrations/02-pipeline-directory.md) at a task boundary. +See [Codex lead operation](docs/CODEX.md) for session pins, hook trust, +permissions, recovery, and live verification. + ## How it flows ```mermaid @@ -40,7 +46,7 @@ flowchart TD Every arrow into or out of a role is really a write to, or a read from, -`.agents/T-.md` — see below. +`.pipeline/T-.md` — see below. ### Context stays small, by construction @@ -48,7 +54,7 @@ Every arrow into or out of a role is really a write to, or a read from, sequenceDiagram participant Lead participant Role as Role (any) - participant File as .agents/T-id.md + participant File as .pipeline/T-id.md Lead->>Role: dispatch (task id, short prompt) Role->>File: full detail - diff, Decisions log,
verdict, test results @@ -71,20 +77,20 @@ and test log it dispatched. | Role | Reads | Writes | Notes | | -------------------------------------- | ---------------------- | ------------------------------------------- | ------------------------------------------------------- | | Lead | state file | the acceptance-criteria ledger, Status | the only role that records whether a criterion was met | -| Planner | whole repo | `.agents/T-.md` only | never touches source; owns criteria *text*, not outcome | -| Implementer (`senior-dev` / `builder`) | whole repo | source + `.agents/T-.diff` + state file | the only roles that edit source | +| Planner | whole repo | `.pipeline/T-.md` only | never touches source; owns criteria *text*, not outcome | +| Implementer (`senior-dev` / `builder`) | whole repo | source + `.pipeline/T-.diff` + state file | the only roles that edit source | | Reviewer | whole repo (read-only) | state file only, or nothing — see below | blanket `edit`/`write: deny` by default in this toolkit | | Tester | whole repo (read-only) | `/**` + state file only | never fixes, only reports | The reviewer template ships **safer than it has to be** — blanket deny, not -scoped-allow on `.agents/**` — because a permission block that reads +scoped-allow on `.pipeline/**` — because a permission block that reads correctly in YAML isn't proof it's enforced by the runtime. Loosen it only after verifying that live against your own OpenCode server (see "Design decisions" below). Codex's planner uses a `workspace-write` sandbox because it must create the -state file. Codex cannot scope that sandbox to `.agents/**` alone, so its +state file. That sandbox does not scope writes to `.pipeline/**` alone, so its source-read-only boundary is explicit role instruction rather than filesystem enforcement. This limitation is stated in the generated planner file rather than hidden. @@ -109,11 +115,12 @@ templates/ every generated file, with __PLACEHOLDER__ tokens claude/agents/ planner.md.tmpl, senior-dev.md.tmpl claude/commands/ feature.md.tmpl — the /feature pipeline command; toolkit-update.md.tmpl — the /toolkit-update merge command - codex/ AGENTS.md.tmpl, a project-scoped planner agent, and - feature/toolkit-update skills for a Codex lead + codex/ AGENTS.md.tmpl, a project-scoped planner agent, reviewed + lifecycle hooks, and feature/toolkit-update skills opencode/agent/ builder.md.tmpl, reviewer.md.tmpl, tester.md.tmpl agents-state/ TEMPLATE.md.tmpl — the T- state file shape - scripts/ oc.sh.tmpl (OpenCode CLI wrapper), team.sh.tmpl (tmux + scripts/ oc.sh.tmpl (OpenCode CLI wrapper), Codex launcher/preflight, + team.sh.tmpl (tmux layout — resumes the lead by default, --port for running a second project at once, see docs/TEAM.md), team-completion.bash.tmpl (optional shell completion @@ -213,16 +220,21 @@ templates into a temp file and compares each one against what's already in `--target`, printing a drift **summary** first (`exit 0` = clean, `exit 1` = something to merge; full hunks behind `--diff`, one file via `--only `). On any scaffold after v0.3.0, flags default from -`.agents/.toolkit-version` — the provenance stamp written at init — so +`.pipeline/.toolkit-version` — the provenance stamp written at init — so usually just `--update --target .` is needed. Merge deliberately (or run the generated `/toolkit-update` command and let your lead reconcile, triaging against the impact-tagged `CHANGELOG.md`), then refresh the baseline: `bin/init.sh --refresh-stamp --target .`. Full workflow: [`docs/UPGRADING.md`](docs/UPGRADING.md). +For existing scaffolds whose runtime is still under `.agents/`, triage reads +the legacy stamp there. Apply [migration 02](migrations/02-pipeline-directory.md) +before the plain missing-file bootstrap; it otherwise refuses to split the +runtime between two directories. + ### Updating a project scaffolded before v0.3.0 -Older scaffolds have no `.agents/.toolkit-version` stamp. One-time +These scaffolds have no provenance stamp at either location. One-time migration — in the *target* project: `/toolkit-update` doesn't exist in the target yet at this point (step 3 @@ -241,8 +253,10 @@ against the *toolkit checkout*, not the target's own commands: selection past the standard single-model-plus-fallback shape will need `--reviewer-fallback-model` by hand). This prints the drift summary and **doesn't write anything yet**. Before merging, check - `.agents/T-*.md` for any `Status:` that isn't `done` — merge at a task + `.pipeline/T-*.md` and legacy `.agents/T-*.md` for any `Status:` that isn't `done` — merge at a task boundary, not mid-flight. + Apply migration 02 before the next step if runtime files still live under + `.agents/`; keep the recovered flags for the first bootstrap. 3. Run the **same command again with the same flags, minus `--update`** (i.e. plain `bin/init.sh --target --project-name ... [...]`) — skip-if-exists makes this safe. This is what actually adds the files @@ -251,8 +265,8 @@ against the *toolkit checkout*, not the target's own commands: against an already-current project is — you still need the values from step 2, because this run doesn't attempt recovery itself. 4. Merge in `CHANGELOG.md` impact order. Coming from ≤ v0.2.x also apply - `migrations/01-delivery-contract.md` to `.agents/TEMPLATE.md` and any - in-flight `.agents/T-*.md` (bare `blocked` still validates; nothing + `migrations/01-delivery-contract.md` to `.pipeline/TEMPLATE.md` and any + in-flight `.pipeline/T-*.md` (bare `blocked` still validates; nothing breaks if you skip it — you just don't get the new guarantees). 5. Create the baseline: `bin/init.sh --refresh-stamp --target ` (same flags again). @@ -270,13 +284,16 @@ existing files. A Codex-only user on an older scaffold does not have `$toolkit-update` yet, so that skill cannot bootstrap itself. After pulling the toolkit checkout, run: +Apply migration 02 between the two commands when `.agents/` contains the old +runtime. Existing hooks and project instructions require a deliberate merge. + ```bash /bin/init.sh --update --target # preview only /bin/init.sh --target # add missing files only ``` For a stamped project the second command loads all original values from -`.agents/.toolkit-version`. It installs the Codex skill and planner without +`.pipeline/.toolkit-version`. It installs the Codex skill and planner without touching customized files; then open Codex and run `$toolkit-update` to reconcile any reported changes to files that already existed. For a pre-v0.3.0 project with no stamp, follow the flag-recovery procedure above and pass those @@ -294,7 +311,8 @@ open Claude Code in the target repo and run: That runs the generated `.claude/commands/feature.md` — the lead reads it, dispatches `planner` first, and walks the flow in "How it flows" above. -If Claude Code is not installed, open Codex in the target repo and invoke: +To use Codex, start `scripts/team.sh --lead codex` (or open Codex directly), +review/trust the session capture hooks with `/hooks`, then invoke: ``` $feature @@ -306,6 +324,9 @@ Codex discovers the generated root `AGENTS.md`, the repository-scoped skill at but uses the Codex planner and OpenCode builder instead of Claude roles. `scripts/team.sh` makes this choice automatically: Claude when installed, otherwise Codex. Override it with `--lead claude` or `--lead codex`. +The supporting skills are installed under `.agents/skills/`. The generated +Codex preflight checks writes and authenticated API access from the active +sandbox; see [the Codex guide](docs/CODEX.md) for required capability checks. Two things need to be true first: - `opencode serve` must be reachable — `scripts/team.sh` starts it in a @@ -330,7 +351,7 @@ not just correct-looking YAML. The first `/feature` run on a freshly-scaffolded project also asks, once, whether to fill the generated role files' generic "what this codebase will punish you for" sections with real specifics from your actual codebase — -gated by a `.agents/.needs-customization` marker that `init.sh` drops only +gated by a `.pipeline/.needs-customization` marker that `init.sh` drops only on a genuinely fresh scaffold, deleted the moment it's asked either way. See `feature.md.tmpl`'s Preflight step 1; the Codex feature skill executes the same check. @@ -453,8 +474,8 @@ mechanism. Full behavior and guardrails: `skills/self-improvement/ SKILL.md`. 1. Copy the file in: `cp /path/to/agent-toolkit/skills/self-improvement/SKILL.md .claude/skills/self-improvement/SKILL.md` (or wherever your tool discovers skills from — same as `delegate` and - `karpathy-guidelines`, this toolkit's skills aren't rendered by - `init.sh`, they're copied in on request). + `karpathy-guidelines`). The three supporting skills are scaffolded under + `.agents/skills/`; `self-improvement` remains opt-in and is not installed. 2. Add one line to that project's own `.claude/commands/feature.md`, next to the existing `delegate`/`karpathy-guidelines` line: `If the "self-improvement" skill is available, load it now.` 3. Read the guardrails in the skill file once before relying on it — it's @@ -492,6 +513,9 @@ project on its own. ## Known gaps +- Codex launcher/hooks/preflight have no-model behavioral coverage, and an + opt-in real sandbox check. Custom planner discovery and the full model-driven + workflow remain manual checks in [docs/CODEX.md](docs/CODEX.md). - `test/smoke.sh` covers the scaffolder's core guarantees (placeholder substitution, never-clobber on re-run, `--update` diffing, the findings-path traversal guard, the loop-cap and budget checks, the @@ -504,7 +528,7 @@ project on its own. enforcement in particular still needs the manual verification described under "Design decisions", and remains the single biggest unverified assumption in this toolkit.** -- The checks are structural by design. They can tell you a spec is +- The spec/state checks are structural by design. They can tell you a spec is unfinished, a budget is blown, or a criterion was closed without evidence; they cannot tell you the spec is *wrong* or the evidence is *good*. That judgement is still the reviewer's, the tester's, and yours diff --git a/SYSTEM.md b/SYSTEM.md index d6c14c4..096dd1f 100644 --- a/SYSTEM.md +++ b/SYSTEM.md @@ -40,7 +40,7 @@ actually enforce, rather than pasting a file in and hoping. Never fixes anything, never edits source. **The state file is the only handoff surface.** One file per task/feature — -`.agents/T-.md`, shape at `templates/agents-state/TEMPLATE.md.tmpl` — +`.pipeline/T-.md`, shape at `templates/agents-state/TEMPLATE.md.tmpl` — holding Status, Goal, Acceptance criteria, Files in scope, a Decisions log, Review verdicts, Test results, Findings for docs, and Open questions. Nothing is passed between roles by prose alone: if a fact isn't written in @@ -240,13 +240,13 @@ mechanism. You need three things: 0. **First, check whether this project was just scaffolded.** If a first-run customization marker exists (this toolkit writes - `.agents/.needs-customization`), the role files still carry generic + `.pipeline/.needs-customization`), the role files still carry generic pitfalls/hard-rules text rather than this codebase's real ones — do that customization pass with the human before running anything, then delete the marker. It is written once, on a fresh scaffold only, and the check otherwise lives in a Claude-Code-specific command file that you, as a different lead, will never execute. -1. **A way to read and write `.agents/T-.md`** — any tool with file +1. **A way to read and write `.pipeline/T-.md`** — any tool with file access can do this. 2. **A way to run each worker role** — either do the work yourself inline (weaker: no cross-vendor independence for review), or shell out to a @@ -289,3 +289,7 @@ root `AGENTS.md`, a project-scoped planner, and `feature` / `toolkit-update` skills; it executes the same canonical flow as Claude. For any other tool, this file plus `docs/ADDING-A-TOOL.md` is the path — there's no flag for "generate my tool's shim," so research its real discovery and permission surfaces first. + +Record approvals in the Decisions log. On restart or compaction, recover +status, counters, approvals, and worker activity from the task record before +dispatching. Do not repeat recorded approvals or start a competing writer. diff --git a/bin/init.sh b/bin/init.sh index 1bcd15d..8a9eb1b 100755 --- a/bin/init.sh +++ b/bin/init.sh @@ -13,6 +13,9 @@ # [--reviewer-fallback-model ] \ # [--tester-model ] \ # [--claude-model sonnet] [--test-dir e2e] +# [--codex-model ] [--codex-reasoning ] +# [--codex-planner-model ] [--codex-planner-reasoning ] +# Codex settings default to inherit: preserve local/parent choices. # # The four model flags and --claude-model/--test-dir all default (see # apply_defaults() below) to the lineup two independent real projects @@ -24,7 +27,7 @@ # guarantee those exact ids still exist. --project-name has no default # and is still required. # -# On a fresh scaffold this also writes .agents/.toolkit-version — a stamp +# On a fresh scaffold this also writes .pipeline/.toolkit-version — a stamp # recording the toolkit SHA/tag and every flag used. It is committed (it # describes the project, not the laptop); never written again by --update. # @@ -41,7 +44,7 @@ # then refresh the stamp: # # bin/init.sh --refresh-stamp [--target ] [flags] -# Rewrites .agents/.toolkit-version after you have accepted a merge. +# Rewrites .pipeline/.toolkit-version after you have accepted a merge. # This is the ONLY thing that updates the stamp besides a fresh scaffold. # # See docs/UPGRADING.md for the full workflow, CHANGELOG.md for what changed @@ -64,6 +67,10 @@ TEMPLATES="$TOOLKIT_ROOT/templates" TARGET="" PROJECT_NAME="" CLAUDE_MODEL="" +CODEX_MODEL="" +CODEX_REASONING="" +CODEX_PLANNER_MODEL="" +CODEX_PLANNER_REASONING="" BUILDER_MODEL="" REVIEWER_MODEL="" REVIEWER_FALLBACK_MODEL="" @@ -86,6 +93,10 @@ while [ $# -gt 0 ]; do --target) [ $# -ge 2 ] || die "--target needs a value"; TARGET="$2"; shift 2 ;; --project-name) [ $# -ge 2 ] || die "--project-name needs a value"; PROJECT_NAME="$2"; shift 2 ;; --claude-model) [ $# -ge 2 ] || die "--claude-model needs a value"; CLAUDE_MODEL="$2"; shift 2 ;; + --codex-model) [ $# -ge 2 ] || die "--codex-model needs a value"; CODEX_MODEL="$2"; shift 2 ;; + --codex-reasoning) [ $# -ge 2 ] || die "--codex-reasoning needs a value"; CODEX_REASONING="$2"; shift 2 ;; + --codex-planner-model) [ $# -ge 2 ] || die "--codex-planner-model needs a value"; CODEX_PLANNER_MODEL="$2"; shift 2 ;; + --codex-planner-reasoning) [ $# -ge 2 ] || die "--codex-planner-reasoning needs a value"; CODEX_PLANNER_REASONING="$2"; shift 2 ;; --builder-model) [ $# -ge 2 ] || die "--builder-model needs a value"; BUILDER_MODEL="$2"; shift 2 ;; --reviewer-model) [ $# -ge 2 ] || die "--reviewer-model needs a value"; REVIEWER_MODEL="$2"; shift 2 ;; --reviewer-fallback-model) [ $# -ge 2 ] || die "--reviewer-fallback-model needs a value"; REVIEWER_FALLBACK_MODEL="$2"; shift 2 ;; @@ -168,10 +179,16 @@ check_opencode_version() { } check_opencode_version -STAMP="$TARGET/.agents/.toolkit-version" +STAMP="$TARGET/.pipeline/.toolkit-version" +STAMP_SOURCE="$STAMP" +[ -f "$STAMP_SOURCE" ] || STAMP_SOURCE="$TARGET/.agents/.toolkit-version" +# Never bootstrap a half-migrated runtime or move customized state implicitly. +if [ "$UPDATE" -eq 0 ] && { [ -f "$TARGET/.agents/TEMPLATE.md" ] || compgen -G "$TARGET/.agents/T-*.md" >/dev/null; }; then + die "legacy .agents runtime state: complete migrations/02-pipeline-directory.md at a task boundary before bootstrapping" +fi # Keep in sync with the number of check_pair/render lines below. -RENDER_TOTAL=18 +RENDER_TOTAL=25 write_stamp() { local sha tag @@ -183,6 +200,10 @@ write_stamp() { printf 'scaffolded: %s\n' "$(date +%F)" printf 'project_name: %s\n' "$PROJECT_NAME" printf 'claude_model: %s\n' "$CLAUDE_MODEL" + printf 'codex_model: %s\n' "$CODEX_MODEL" + printf 'codex_reasoning: %s\n' "$CODEX_REASONING" + printf 'codex_planner_model: %s\n' "$CODEX_PLANNER_MODEL" + printf 'codex_planner_reasoning: %s\n' "$CODEX_PLANNER_REASONING" printf 'builder_model: %s\n' "$BUILDER_MODEL" printf 'reviewer_model: %s\n' "$REVIEWER_MODEL" printf 'reviewer_fallback_model: %s\n' "$REVIEWER_FALLBACK_MODEL" @@ -200,6 +221,17 @@ write_stamp() { apply_defaults() { [ -n "$CLAUDE_MODEL" ] || CLAUDE_MODEL="sonnet" + [ -n "$CODEX_MODEL" ] || CODEX_MODEL="inherit" + [ -n "$CODEX_REASONING" ] || CODEX_REASONING="inherit" + [ -n "$CODEX_PLANNER_MODEL" ] || CODEX_PLANNER_MODEL="inherit" + [ -n "$CODEX_PLANNER_REASONING" ] || CODEX_PLANNER_REASONING="inherit" + + for value in "$CODEX_MODEL" "$CODEX_PLANNER_MODEL"; do + [[ "$value" =~ ^[a-zA-Z0-9][a-zA-Z0-9._/-]*$ ]] || die "invalid Codex model id: $value" + done + for value in "$CODEX_REASONING" "$CODEX_PLANNER_REASONING"; do + case "$value" in inherit|minimal|low|medium|high|xhigh|max|ultra) ;; *) die "invalid Codex reasoning effort: $value" ;; esac + done [ -n "$TEST_DIR" ] || TEST_DIR="e2e" # Lineup two independent real projects converged on. Not a guarantee these # exact ids exist on your OpenCode server — run `opencode models` and pass @@ -211,13 +243,17 @@ apply_defaults() { } load_stamp_value() { # $1 = key, sets REPLY - REPLY="$(sed -n "s/^$1: *//p" "$STAMP" | head -1)" + REPLY="$(sed -n "s/^$1: *//p" "$STAMP_SOURCE" | head -1)" } load_flags_from_stamp() { for spec in \ "project_name|PROJECT_NAME" \ "claude_model|CLAUDE_MODEL" \ + "codex_model|CODEX_MODEL" \ + "codex_reasoning|CODEX_REASONING" \ + "codex_planner_model|CODEX_PLANNER_MODEL" \ + "codex_planner_reasoning|CODEX_PLANNER_REASONING" \ "builder_model|BUILDER_MODEL" \ "reviewer_model|REVIEWER_MODEL" \ "reviewer_fallback_model|REVIEWER_FALLBACK_MODEL" \ @@ -251,6 +287,14 @@ recover_from_target() { # $1 = key REPLY="$(sed -n 's/^model: //p' "$TARGET/.opencode/agent/tester.md" | head -1)" ;; claude_model) REPLY="$(sed -n 's/^model: //p' "$TARGET/.claude/agents/planner.md" 2>/dev/null | head -1)" ;; + codex_model) + REPLY="$(sed -n 's/^MODEL="\(.*\)"/\1/p' "$TARGET/scripts/codex-lead.sh" 2>/dev/null | head -1)" ;; + codex_reasoning) + REPLY="$(sed -n 's/^REASONING="\(.*\)"/\1/p' "$TARGET/scripts/codex-lead.sh" 2>/dev/null | head -1)" ;; + codex_planner_model) + REPLY="$(sed -n 's/^model = "\(.*\)"/\1/p' "$TARGET/.codex/agents/planner.toml" 2>/dev/null | head -1)" ;; + codex_planner_reasoning) + REPLY="$(sed -n 's/^model_reasoning_effort = "\(.*\)"/\1/p' "$TARGET/.codex/agents/planner.toml" 2>/dev/null | head -1)" ;; project_name) # team.sh: SESSION="__PROJECT_NAME__" REPLY="$(sed -n 's/^SESSION="\(.*\)"/\1/p' \ @@ -262,12 +306,12 @@ recover_from_target() { # $1 = key esac } -if [ -f "$STAMP" ]; then +if [ -f "$STAMP_SOURCE" ]; then load_flags_from_stamp fi if [ "$UPDATE" -eq 1 ]; then - if [ ! -f "$STAMP" ]; then + if [ ! -f "$STAMP_SOURCE" ]; then # No stamp (pre-v0.3.0 scaffold): recover the original values from the # target's own scaffolded files — they carry the substituted forms of the # same tokens. Anything still missing falls back to defaults, then to an @@ -275,6 +319,10 @@ if [ "$UPDATE" -eq 1 ]; then for spec in \ "project_name|PROJECT_NAME" \ "claude_model|CLAUDE_MODEL" \ + "codex_model|CODEX_MODEL" \ + "codex_reasoning|CODEX_REASONING" \ + "codex_planner_model|CODEX_PLANNER_MODEL" \ + "codex_planner_reasoning|CODEX_PLANNER_REASONING" \ "builder_model|BUILDER_MODEL" \ "reviewer_model|REVIEWER_MODEL" \ "reviewer_fallback_model|REVIEWER_FALLBACK_MODEL" \ @@ -313,6 +361,12 @@ else [ -n "$PROJECT_NAME" ] || die "--project-name is required" fi +apply_defaults +CODEX_PLANNER_MODEL_LINE="# model inherits from the lead" +CODEX_PLANNER_REASONING_LINE="# reasoning effort inherits from the lead" +[ "$CODEX_PLANNER_MODEL" = inherit ] || CODEX_PLANNER_MODEL_LINE="model = \"$CODEX_PLANNER_MODEL\"" +[ "$CODEX_PLANNER_REASONING" = inherit ] || CODEX_PLANNER_REASONING_LINE="model_reasoning_effort = \"$CODEX_PLANNER_REASONING\"" + # Captured before any render() call touches the target, so it reflects # whether this is the very first scaffold of this project — used below to # decide whether to drop the first-run customization marker and write the @@ -325,6 +379,10 @@ FRESH_SCAFFOLD=0 SED_ARGS=( -e "s|__PROJECT_NAME__|$PROJECT_NAME|g" -e "s|__CLAUDE_MODEL__|$CLAUDE_MODEL|g" + -e "s|__CODEX_MODEL__|$CODEX_MODEL|g" + -e "s|__CODEX_REASONING__|$CODEX_REASONING|g" + -e "s|__CODEX_PLANNER_MODEL_LINE__|$CODEX_PLANNER_MODEL_LINE|g" + -e "s|__CODEX_PLANNER_REASONING_LINE__|$CODEX_PLANNER_REASONING_LINE|g" -e "s|__BUILDER_MODEL__|$BUILDER_MODEL|g" -e "s|__REVIEWER_MODEL__|$REVIEWER_MODEL|g" -e "s|__REVIEWER_FALLBACK_MODEL__|$REVIEWER_FALLBACK_MODEL|g" @@ -379,12 +437,19 @@ if [ "$UPDATE" -eq 1 ]; then check_pair "$TEMPLATES/claude/commands/toolkit-update.md.tmpl" "$TARGET/.claude/commands/toolkit-update.md" check_pair "$TEMPLATES/codex/AGENTS.md.tmpl" "$TARGET/AGENTS.md" check_pair "$TEMPLATES/codex/agents/planner.toml.tmpl" "$TARGET/.codex/agents/planner.toml" + check_pair "$TEMPLATES/codex/hooks.json.tmpl" "$TARGET/.codex/hooks.json" + check_pair "$TEMPLATES/codex/hooks/session.py.tmpl" "$TARGET/scripts/codex-session.py" + check_pair "$TEMPLATES/scripts/codex-lead.sh.tmpl" "$TARGET/scripts/codex-lead.sh" + check_pair "$TEMPLATES/scripts/codex-preflight.sh.tmpl" "$TARGET/scripts/codex-preflight.sh" + check_pair "$TOOLKIT_ROOT/skills/delegate/SKILL.md" "$TARGET/.agents/skills/delegate/SKILL.md" + check_pair "$TOOLKIT_ROOT/skills/status-board/SKILL.md" "$TARGET/.agents/skills/status-board/SKILL.md" + check_pair "$TOOLKIT_ROOT/skills/karpathy-guidelines/SKILL.md" "$TARGET/.agents/skills/karpathy-guidelines/SKILL.md" check_pair "$TEMPLATES/codex/skills/feature/SKILL.md.tmpl" "$TARGET/.agents/skills/feature/SKILL.md" check_pair "$TEMPLATES/codex/skills/toolkit-update/SKILL.md.tmpl" "$TARGET/.agents/skills/toolkit-update/SKILL.md" check_pair "$TEMPLATES/opencode/agent/builder.md.tmpl" "$TARGET/.opencode/agent/builder.md" check_pair "$TEMPLATES/opencode/agent/reviewer.md.tmpl" "$TARGET/.opencode/agent/reviewer.md" check_pair "$TEMPLATES/opencode/agent/tester.md.tmpl" "$TARGET/.opencode/agent/tester.md" - check_pair "$TEMPLATES/agents-state/TEMPLATE.md.tmpl" "$TARGET/.agents/TEMPLATE.md" + check_pair "$TEMPLATES/agents-state/TEMPLATE.md.tmpl" "$TARGET/.pipeline/TEMPLATE.md" check_pair "$TEMPLATES/scripts/oc.sh.tmpl" "$TARGET/scripts/oc.sh" check_pair "$TEMPLATES/scripts/team.sh.tmpl" "$TARGET/scripts/team.sh" check_pair "$TEMPLATES/scripts/team-completion.bash.tmpl" "$TARGET/scripts/team-completion.bash" @@ -395,7 +460,7 @@ if [ "$UPDATE" -eq 1 ]; then CUR_SHA="$(git -C "$TOOLKIT_ROOT" rev-parse --short HEAD 2>/dev/null || echo unknown)" printf 'init.sh: toolkit is at %s; checking %s rendered file(s)%s\n' \ "$CUR_SHA" "$TOTAL" "${ONLY:+ (filtered by --only '$ONLY'; full set is $RENDER_TOTAL)}" - if [ -f "$STAMP" ]; then + if [ -f "$STAMP_SOURCE" ]; then load_stamp_value toolkit_sha STAMPED_SHA="$REPLY" load_stamp_value scaffolded @@ -440,12 +505,19 @@ render "$TEMPLATES/claude/commands/feature.md.tmpl" "$TARGET/.claude/commands/f render "$TEMPLATES/claude/commands/toolkit-update.md.tmpl" "$TARGET/.claude/commands/toolkit-update.md" render "$TEMPLATES/codex/AGENTS.md.tmpl" "$TARGET/AGENTS.md" render "$TEMPLATES/codex/agents/planner.toml.tmpl" "$TARGET/.codex/agents/planner.toml" +render "$TEMPLATES/codex/hooks.json.tmpl" "$TARGET/.codex/hooks.json" +render "$TEMPLATES/codex/hooks/session.py.tmpl" "$TARGET/scripts/codex-session.py" +render "$TEMPLATES/scripts/codex-lead.sh.tmpl" "$TARGET/scripts/codex-lead.sh" +render "$TEMPLATES/scripts/codex-preflight.sh.tmpl" "$TARGET/scripts/codex-preflight.sh" +render "$TOOLKIT_ROOT/skills/delegate/SKILL.md" "$TARGET/.agents/skills/delegate/SKILL.md" +render "$TOOLKIT_ROOT/skills/status-board/SKILL.md" "$TARGET/.agents/skills/status-board/SKILL.md" +render "$TOOLKIT_ROOT/skills/karpathy-guidelines/SKILL.md" "$TARGET/.agents/skills/karpathy-guidelines/SKILL.md" render "$TEMPLATES/codex/skills/feature/SKILL.md.tmpl" "$TARGET/.agents/skills/feature/SKILL.md" render "$TEMPLATES/codex/skills/toolkit-update/SKILL.md.tmpl" "$TARGET/.agents/skills/toolkit-update/SKILL.md" render "$TEMPLATES/opencode/agent/builder.md.tmpl" "$TARGET/.opencode/agent/builder.md" render "$TEMPLATES/opencode/agent/reviewer.md.tmpl" "$TARGET/.opencode/agent/reviewer.md" render "$TEMPLATES/opencode/agent/tester.md.tmpl" "$TARGET/.opencode/agent/tester.md" -render "$TEMPLATES/agents-state/TEMPLATE.md.tmpl" "$TARGET/.agents/TEMPLATE.md" +render "$TEMPLATES/agents-state/TEMPLATE.md.tmpl" "$TARGET/.pipeline/TEMPLATE.md" render "$TEMPLATES/scripts/oc.sh.tmpl" "$TARGET/scripts/oc.sh" render "$TEMPLATES/scripts/team.sh.tmpl" "$TARGET/scripts/team.sh" render "$TEMPLATES/scripts/team-completion.bash.tmpl" "$TARGET/scripts/team-completion.bash" @@ -455,11 +527,12 @@ render "$TEMPLATES/scripts/promote-findings.sh.tmpl" "$TARGET/scripts/promote-fi chmod +x "$TARGET/scripts/oc.sh" "$TARGET/scripts/team.sh" \ "$TARGET/scripts/verify-state.sh" "$TARGET/scripts/verify-spec.sh" \ - "$TARGET/scripts/promote-findings.sh" 2>/dev/null || true + "$TARGET/scripts/promote-findings.sh" "$TARGET/scripts/codex-lead.sh" \ + "$TARGET/scripts/codex-preflight.sh" 2>/dev/null || true -mkdir -p "$TARGET/.agents" +mkdir -p "$TARGET/.pipeline" if [ "$FRESH_SCAFFOLD" -eq 1 ]; then - touch "$TARGET/.agents/.needs-customization" + touch "$TARGET/.pipeline/.needs-customization" fi # Provenance stamp — written exactly once, on a genuine fresh scaffold, @@ -471,45 +544,50 @@ if [ "$FRESH_SCAFFOLD" -eq 1 ] && [ ! -f "$STAMP" ]; then printf 'init.sh: wrote %s\n' "$STAMP" fi -# .agents/.oc-port and .agents/.claude-session-id.* are local machine state +# .pipeline/.oc-port and .pipeline/.claude-session-id.* are local machine state # (which port scripts/team.sh last bound; which Claude conversation each -# tmux session name is pinned to), never something to commit. Codex resumes -# its most recent conversation scoped to the current project and needs no -# toolkit-owned session-id file. +# tmux session name is pinned to), never something to commit. Codex also +# pins the exact lead id through its reviewed lifecycle hooks. # Append-if-missing when the target is a git repo — additive only, in # keeping with this script's never-overwrite stance; a project that ignores # these differently is left alone. if [ -d "$TARGET/.git" ]; then GITIGNORE="$TARGET/.gitignore" - if ! grep -qxF '.agents/.oc-port' "$GITIGNORE" 2>/dev/null; then + if ! grep -qxF '.pipeline/.oc-port' "$GITIGNORE" 2>/dev/null; then { printf '\n# local opencode server port written by scripts/team.sh\n' - printf '.agents/.oc-port\n' + printf '.pipeline/.oc-port\n' } >> "$GITIGNORE" - printf 'init.sh: added .agents/.oc-port to %s\n' "$GITIGNORE" + printf 'init.sh: added .pipeline/.oc-port to %s\n' "$GITIGNORE" fi # opencode v2's `serve` always requires a password (v1 had none); this is # the secret scripts/team.sh generates for it — never commit it. - if ! grep -qxF '.agents/.oc-password' "$GITIGNORE" 2>/dev/null; then + if ! grep -qxF '.pipeline/.oc-password' "$GITIGNORE" 2>/dev/null; then { printf '\n# local opencode server password written by scripts/team.sh\n' - printf '.agents/.oc-password\n' + printf '.pipeline/.oc-password\n' } >> "$GITIGNORE" - printf 'init.sh: added .agents/.oc-password to %s\n' "$GITIGNORE" + printf 'init.sh: added .pipeline/.oc-password to %s\n' "$GITIGNORE" fi - if ! grep -qxF '.agents/.claude-session-id.*' "$GITIGNORE" 2>/dev/null; then + if ! grep -qxF '.pipeline/.claude-session-id.*' "$GITIGNORE" 2>/dev/null; then { printf '\n# local Claude session ids pinned per tmux session name by scripts/team.sh\n' - printf '.agents/.claude-session-id.*\n' + printf '.pipeline/.claude-session-id.*\n' } >> "$GITIGNORE" - printf 'init.sh: added .agents/.claude-session-id.* to %s\n' "$GITIGNORE" + printf 'init.sh: added .pipeline/.claude-session-id.* to %s\n' "$GITIGNORE" fi - if ! grep -qxF '.agents/logs/' "$GITIGNORE" 2>/dev/null; then + for pattern in '.pipeline/.codex-session-id.*' '.pipeline/.codex-started.*' '.pipeline/.codex-lead-lock.*'; do + if ! grep -qxF "$pattern" "$GITIGNORE" 2>/dev/null; then + printf '\n%s\n' "$pattern" >> "$GITIGNORE" + printf 'init.sh: added %s to %s\n' "$pattern" "$GITIGNORE" + fi + done + if ! grep -qxF '.pipeline/logs/' "$GITIGNORE" 2>/dev/null; then { printf '\n# per-call pipeline telemetry written by scripts/oc.sh\n' - printf '.agents/logs/\n' + printf '.pipeline/logs/\n' } >> "$GITIGNORE" - printf 'init.sh: added .agents/logs/ to %s\n' "$GITIGNORE" + printf 'init.sh: added .pipeline/logs/ to %s\n' "$GITIGNORE" fi fi @@ -530,11 +608,14 @@ Next steps: \$feature). scripts/team.sh makes the same Claude-first choice. Load the "delegate" skill when available; it is the context-discipline half of this. - 4. Make sure $TARGET has real project-specific guidance. The generated + 4. In Codex, review/trust the generated hooks with /hooks (existing hooks + are never overwritten). Run scripts/codex-preflight.sh from the lead + sandbox. Git/config writes may need runtime approval after pipeline consent. + Make sure $TARGET has real project-specific guidance. The generated AGENTS.md is Codex integration plus an explicitly unpopulated guidance section; fill it when no project CLAUDE.md already supplies constraints. 5. On the first feature run, either lead will notice - .agents/.needs-customization and ask whether to fill the role files' + .pipeline/.needs-customization and ask whether to fill the role files' generic pitfalls/hard-rules sections with this project's real ones. 6. Later, once the toolkit itself has moved on: bin/init.sh --update --target $TARGET shows a drift summary (exit 1 = something to merge), diff --git a/docs/ADDING-A-TOOL.md b/docs/ADDING-A-TOOL.md index 807972a..5687598 100644 --- a/docs/ADDING-A-TOOL.md +++ b/docs/ADDING-A-TOOL.md @@ -72,14 +72,14 @@ anticipation of it): consider collapsing the now-duplicated prose into one shared file, so a future behavioral fix is one edit instead of N. 1. Extract the actual instructions — not the frontmatter — into - `.agents/roles/.md` (rendered from a new + `.pipeline/roles/.md` (rendered from a new `templates/agents-state/roles/.md.tmpl`), written generically: no tool name in the prose itself, since two or more tools now read it. 2. Shrink every existing tool-specific file for that role down to: its own real frontmatter/permission block (this can never be shared — it's the one thing that's genuinely different per tool), a short paragraph on what's tool-specific about running this role under this tool, and one - line: "Read `.agents/roles/.md` now, in full, before doing + line: "Read `.pipeline/roles/.md` now, in full, before doing anything else." 3. Add the new canonical file's `render` line to `bin/init.sh`. 4. Update README to note the exception — this toolkit's default is one @@ -97,6 +97,12 @@ root: a single tool-agnostic file meant to be handed directly to that AI this toolkit generates for it the way `bin/init.sh` generates worker-role shims. +Claude and Codex lead adapters are already generated by `init.sh`; the recipe +above applies to additional runtimes. Keep one canonical workflow and adapt +dispatch, exact-session recovery, protected paths, and runtime approval using +the new tool's actual capabilities. See [Codex lead operation](CODEX.md) for +the existing integration and verification boundary. + **The tradeoff, stated plainly, so it's a real decision and not a reflex:** an extra file read before the role can do anything, and content that's genuinely tool-specific (not all of it) has to be judgment-called into diff --git a/docs/CODEX.md b/docs/CODEX.md new file mode 100644 index 0000000..46a1913 --- /dev/null +++ b/docs/CODEX.md @@ -0,0 +1,125 @@ +# Codex lead operation + +Codex and Claude use the same canonical feature and update workflow files. +Codex's adapters supply its dispatch, permissions, and recovery behavior; +OpenCode remains the implementer/reviewer/tester runtime. Both leads use +`.pipeline/` for writable task records, outputs, local server state, and logs. +Skills stay in `.agents/skills/` and agent/hook definitions in `.codex/`. + +The CLI integration was checked against Codex 0.160.0. The OS sandbox and +project/skill/agent discovery must also be checked in the runtime actually +used. File presence and valid TOML are not proof of agent discovery. + +## Setup + +Scaffold normally, optionally choosing independent settings: + +```bash +bash /path/to/agent-toolkit/bin/init.sh --target . --project-name example \ + --codex-model --codex-reasoning high \ + --codex-planner-model --codex-planner-reasoning medium +``` + +All four Codex options default to `inherit`. Omitted planner settings inherit +from the lead; the launcher preserves the user's configured lead settings. +Explicit choices are recorded in the provenance stamp. Use an available model +and a reasoning effort it supports; the scaffolder does not query accounts. + +Ensure project guidance is populated. Existing `AGENTS.md` is preserved: +review/merge the generated routing instructions if that file already existed. +Existing hooks are preserved too: merge the two capture handlers, rather than +replacing unrelated hooks. Review and trust them with `/hooks` in a trusted +project. Hook changes require trust review again. The toolkit never bypasses +hook trust. + +```bash +scripts/team.sh --lead codex example +``` + +Alternatively run `bash scripts/codex-lead.sh example` from any directory. +The wrapper starts Codex without the shared daemon so the hook inherits this +launcher's team key. SessionStart captures the exact lead ID; UserPromptSubmit +also captures it after initial hook trust. Ordinary Codex chats have no team +key and do not replace pins. Each team name has its own pin and process lock. +The launcher supplies the repository root to the hook command explicitly, so +capture also works in a nested checkout or a project without a Git root. + +## Permissions and preflight + +Before `$feature`, the lead runs `scripts/codex-preflight.sh` in its **current +sandbox**. It checks runtime-file writes, required files/tools, and authenticated +OpenCode API connectivity using the same server/password precedence as `oc.sh`. +It checks enabled hook/multi-agent capabilities and the launcher CLI option, +and prints the installed Codex version; verify custom planner and skill discovery +in Codex before dispatch. Running it from an unrestricted terminal alone does +not establish the lead's sandbox access. + +If policy blocks a command, request runtime approval for that exact operation. +Local OpenCode access may need network approval; the user-approved commit may +need approval to write `.git`; toolkit updates may need approval to write +protected `.codex/` or `.agents/skills/` files. Never disable the sandbox or +approval controls to get parity. OpenCode `--auto` applies only to OpenCode. +Spec approval and merge consent are separate workflow gates. + +The planner still has workspace-write access to ordinary source paths. Its +source-read-only rule is a disclosed role instruction, not path isolation. It +tests a temporary write under `.pipeline/` before planning and reports a blocked +operation to the lead. Changing the runtime directory does not make that +source-write boundary enforced. + +## Recovery + +Resuming uses `.pipeline/.codex-session-id.`, never `--last`. A failed +resume keeps the pin and stops. Missing capture after a prior launch also stops: +trust the hooks and submit a prompt, or recover the exact conversation ID from +`/status` and save it to the pin. Use `--fresh` only to deliberately reset that +team's lead conversation. After a crash, check that the process is stopped before +removing its stale `.pipeline/.codex-lead-lock.` directory. + +The lead records its planner thread ID in the task file. Corrections and the +single spec bounce reuse that thread. After interruption, check worker status +and the task record before dispatching; never start a competing writer. If the +planner thread has disappeared, record why a replacement was necessary and +reconstruct it from the task file. After compaction/restart, recover status, +counters, approval decisions, and worker activity from disk. + +If a quota failure occurs, preserve the task and thread IDs. Use a scheduled +wakeup only when the runtime actually offers one; otherwise leave a resumable +handoff and report that automatic retry is unavailable. + +## Verification + +`bash test/codex.sh` exercises real generated launcher/hook/preflight code using +CLI doubles: exact resume, isolated teams, explicit reset, failed resume, missing +capture, concurrent writers, unsafe event data, blocked writes/network access, +model-setting round trips, and legacy migration triage. CI runs it without any +model calls. Python 3.11+ is required for the test's TOML parser; runtime hooks +use only the Python standard library and do not require TOML parsing. + +`bash test/codex-sandbox.sh` is an opt-in real local Codex sandbox check, also +without model calls. It verifies writable runtime state and refusal to write +instruction/configuration/Git paths. Run outside another sandbox when the OS +disallows nested sandbox creation. + +Live workflow checks are manual, using a disposable repository and a real +OpenCode server. Verify each scenario before claiming model-level parity: + +1. Invoke `$feature` on a small change. Confirm the custom planner is discovered, + writes a spec passing `verify-spec.sh`, and stops for spec approval. +2. Request a spec correction; confirm reuse of the recorded planner thread. +3. Approve implementation. Exercise a review rejection and a failed test, then + confirm counters, evidence, status-board updates, and independent review. +4. Interrupt planning/implementation, restart the same team after an unrelated + Codex conversation, and confirm exact lead/thread recovery with no duplicate + writer and no repeated recorded approval. +5. Confirm the lead reports results and stops before an unapproved merge. After + approval, confirm it stages the task record with the code and tags the commit. +6. Change a sandbox or hook setting in a scratch update; confirm it is surfaced + for explicit permission-change consent and renewed hook trust. + +Do not label the no-model tests as a completed live multi-agent run. + +Sources: [protected paths and approvals](https://learn.chatgpt.com/docs/agent-approvals-security), +[exact-session resume](https://learn.chatgpt.com/docs/developer-commands#codex-resume), +[lifecycle hooks and trust](https://learn.chatgpt.com/docs/hooks), and +[custom agents](https://learn.chatgpt.com/docs/agent-configuration/subagents). diff --git a/docs/TEAM.md b/docs/TEAM.md index 5986a15..2800431 100644 --- a/docs/TEAM.md +++ b/docs/TEAM.md @@ -3,7 +3,7 @@ A tmux layout for running the pipeline: pane 0 is the lead (`claude` when installed, otherwise `codex`), pane 1 is `opencode serve` (what `scripts/oc.sh` attaches to), pane 2 tails -`.agents/` for activity, pane 3 is free. +`.pipeline/` for activity, pane 3 is free. ## Usage @@ -33,7 +33,7 @@ drives the same pipeline as Claude's `/feature` command. ## Resuming is the default Pane 0 resumes the lead's own conversation — not a fresh one — but it does -so by pinning to a stored UUID (`.agents/.claude-session-id.`, +so by pinning to a stored UUID (`.pipeline/.claude-session-id.`, written the first time that session name runs) rather than `claude --continue`. `--continue` only means "the most recent conversation in this directory," with no notion of *which* tmux session started it, so @@ -53,10 +53,13 @@ Pass `--fresh` on the rare run where you actually want a clean slate instead — this mints a new pinned id, so later resumes follow the new conversation, not the old one. -Codex has no create-with-session-id equivalent. Without `--fresh`, the launcher -uses `codex resume --last`, whose lookup is scoped to the current repository; -if no saved session exists it starts `codex`. With `--fresh`, it starts a new -Codex conversation directly. +Codex captures its actual session ID using reviewed SessionStart and +UserPromptSubmit hooks. `scripts/codex-lead.sh` stores it per team name under +`.pipeline/.codex-session-id.` and resumes that exact conversation. +It never uses `--last` or starts a fresh conversation after a failed resume. +Review/trust the generated hooks with `/hooks`; existing hooks need a hand-merge. +Use `--fresh` to deliberately reset a conversation. Missing capture or a stale +process lock requires explicit recovery; see [Codex lead operation](CODEX.md). ## Running two projects at once @@ -69,12 +72,12 @@ the second project its own port: scripts/team.sh --port 4097 ``` -That port gets written to `.agents/.oc-port` in this repo. `scripts/oc.sh` +That port gets written to `.pipeline/.oc-port` in this repo. `scripts/oc.sh` reads it automatically (when `OC_SERVER` isn't already set), so every `oc.sh` call in this project just uses the right port — you don't export `OC_SERVER` by hand for every call. -**`.agents/.oc-port` is gitignored automatically by `bin/init.sh`** when +**`.pipeline/.oc-port` is gitignored automatically by `bin/init.sh`** when the target is a git repo — it's local machine state (which port happened to be free on your laptop today), not something to commit. If your project predates that, or ignores it differently, add it by hand. diff --git a/docs/UPGRADING.md b/docs/UPGRADING.md index 99479f4..22bf038 100644 --- a/docs/UPGRADING.md +++ b/docs/UPGRADING.md @@ -20,12 +20,17 @@ An update command cannot bootstrap itself into a project that does not have it yet. After pulling the toolkit checkout, an existing stamped project can add newly introduced template files without repeating its original flags: +For a runtime still under `.agents/`, apply +[migration 02](../migrations/02-pipeline-directory.md) between inspection and +bootstrap. Update triage reads the legacy stamp; a plain run refuses to create +new runtime state before migration is complete. + ```bash /bin/init.sh --update --target # inspect first /bin/init.sh --target # missing files only ``` -The plain run now loads values from `.agents/.toolkit-version` and retains +The plain run now loads values from `.pipeline/.toolkit-version` and retains `render()`'s skip-if-exists guarantee. A Claude user whose older scaffold already has `/toolkit-update` can let that command perform these steps. A Codex-only user upgrading from before Codex support must run the shell bootstrap @@ -56,7 +61,7 @@ four problems in a row: 4. **No migrations.** When the state-file contract gains a field or a section — as it has now, substantially (the acceptance-criteria ledger, three budget counters, a split blocked status) — existing - `.agents/T-*.md` files in flight silently lack it, and nothing says so. + `.pipeline/T-*.md` files in flight silently lack it, and nothing says so. Stage 4 below now carries the real migration note for that change. Consequence: `--update` is technically correct and practically unused. A @@ -70,7 +75,7 @@ lesson this toolkit earns after the scaffold date stays upstream. **The single highest-value change.** `init.sh` writes, on a real scaffold: ``` -.agents/.toolkit-version +.pipeline/.toolkit-version ``` ``` @@ -95,7 +100,7 @@ Then: (2026-08-26); toolkit is now at `a1b2c3d` (14 commits ahead)". Problem 2 disappears. - A successful merge updates the stamp; `--update` alone never writes it. -- It is committed (unlike `.agents/.oc-port`) — it describes the project, +- It is committed (unlike `.pipeline/.oc-port`) — it describes the project, not the laptop. Follow the existing conventions: written only when `FRESH_SCAFFOLD` was @@ -148,7 +153,7 @@ With Stages 1–2 in place, `--update` gets three cheap upgrades: ``` init.sh: 3 of 12 files differ from toolkit v0.3.0 (you are on v0.2.0) [safety] .opencode/agent/builder.md — permission block - [contract] .agents/TEMPLATE.md — 2 new fields + [contract] .pipeline/TEMPLATE.md — 2 new fields [docs] scripts/team.sh — comment only run with --diff to see hunks, or --diff for one file ``` @@ -170,8 +175,8 @@ state file more than anything since the toolkit was extracted: ```markdown # 01 — Delivery contract (ledger, budgets, split blocked status) -Applies to: .agents/TEMPLATE.md, scripts/verify-state.sh, every role file, - and any in-flight .agents/T-*.md +Applies to: .pipeline/TEMPLATE.md, scripts/verify-state.sh, every role file, + and any in-flight .pipeline/T-*.md New header fields (add under **Implementer for this task:**): **Reviewer for this task:** / **Tester for this task:** @@ -213,7 +218,7 @@ The AI-assisted merge path the README already gestures at ("hand the diff to your AI lead and ask it to reconcile"), made into a real, templated command/skill rendered by `init.sh` alongside each lead's feature entry point: -1. Read `.agents/.toolkit-version` for the baseline. +1. Read `.pipeline/.toolkit-version` for the baseline. 2. Run `bin/init.sh --update` (flags come from the stamp). 3. Read the toolkit's `CHANGELOG.md` for entries after the baseline. 4. Work the diffs **in impact order** — `contract`, then `safety`, then diff --git a/migrations/02-pipeline-directory.md b/migrations/02-pipeline-directory.md new file mode 100644 index 0000000..303c3d3 --- /dev/null +++ b/migrations/02-pipeline-directory.md @@ -0,0 +1,55 @@ +# Writable pipeline records and Codex lead recovery + +Apply at a task boundary. This changes the handoff location for **both** leads, +from `.agents/` to `.pipeline/`; `.agents/skills/` remains the skill-discovery +directory. Codex protects `.agents/`, `.codex/`, and `.git` under its standard +workspace sandbox. The previous planner configuration did not make task +records under `.agents/` writable. + +1. Finish or explicitly park all tasks. Stop workers and the tmux team before + moving runtime files; do not change a live writer's paths. +2. Read the old `.agents/.toolkit-version` as the update baseline. Run the + toolkit's `bin/init.sh --update --target ` to review changes. It + reads legacy stamps without modifying them; a plain bootstrap refuses an + unmigrated `.agents/TEMPLATE.md`. +3. Create `.pipeline/` and move only runtime files, without overwriting any + existing destination. Review collisions instead of merging automatically: + `TEMPLATE.md`, `T-*.md`, task diffs/JSONL outputs, `.toolkit-version`, + `.needs-customization`, `.oc-port`, `.oc-password`, `.claude-session-id.*`, + and `logs/`. Preserve permissions on the password. **Leave `.agents/skills/` + in place.** No automatic migration runner is supplied. +4. Reconcile every active reference to the old runtime path in role files, + commands, project instructions, task links, tracking docs, scripts, and + locally customized permission scopes. Take the new structural scripts + together with the new task template. Keep historical changelog entries as + historical records. Never widen permissions silently. +5. Run `bin/init.sh --target ` to add missing files. Existing files, + including `AGENTS.md` and `.codex/hooks.json`, are preserved. Hand-merge the + Codex routing block into existing instructions and the SessionStart plus + UserPromptSubmit capture handlers into existing hooks. Runtime approval may + be needed for protected configuration writes; user acceptance of the update + is a separate decision. The three supporting skills are now installed under + `.agents/skills/`; Claude can read these files directly too. +6. Add `.pipeline/.oc-port`, `.pipeline/.oc-password`, + `.pipeline/.claude-session-id.*`, `.pipeline/.codex-session-id.*`, + `.pipeline/.codex-started.*`, `.pipeline/.codex-lead-lock.*`, and + `.pipeline/logs/` to the project's ignore rules. A plain bootstrap adds + these rules in Git repos. Old ignore entries may remain harmlessly. +7. In Codex review/trust the new or changed hooks with `/hooks`. The launcher + captures the actual session ID, never the newest repository conversation. + Recover an existing Codex lead's ID from `/status` and save it to + `.pipeline/.codex-session-id.` before resuming, or deliberately + use `scripts/team.sh --lead codex --fresh `. Missing pins and + failed resumes stop; they do not silently create another conversation. +8. Run `scripts/codex-preflight.sh` from the lead's actual sandbox, verify + discovery of `planner` and the supporting skills, and run the structural + scripts against a completed migrated task. Optional Codex model/reasoning + flags default to `inherit`. Add the optional **Codex planner thread id** + field to existing tasks only when a Codex planner is dispatched; older + task files remain valid without it. +9. Refresh the provenance stamp after accepting the changes. An update may + still report deliberate local customizations: review and record them rather + than forcing template equality. Verify the changed permission scopes live. + +See [Codex lead operation](../docs/CODEX.md) for the runtime checks and recovery +scenarios. The toolkit's tests never run paid model calls automatically. diff --git a/skills/delegate/SKILL.md b/skills/delegate/SKILL.md index 68566bd..a38d4c8 100644 --- a/skills/delegate/SKILL.md +++ b/skills/delegate/SKILL.md @@ -15,7 +15,7 @@ programmatically from inside the loop. **Everything a delegate produces lands on disk, not in your context.** A subagent's chat reply is a receipt, not the record. The record is the file -it wrote — a state file (`.agents/T-.md`), a diff, a test report. Read +it wrote — a state file (`.pipeline/T-.md`), a diff, a test report. Read the file to verify placement and content; do not ask the delegate to repeat itself into the conversation, and do not re-paste what is already on disk. diff --git a/skills/dev-team-generator/reference/flow-example.md b/skills/dev-team-generator/reference/flow-example.md index 5477f74..b065186 100644 --- a/skills/dev-team-generator/reference/flow-example.md +++ b/skills/dev-team-generator/reference/flow-example.md @@ -24,7 +24,7 @@ file — it's the one line each role updates when it finishes, naming what happened and who's next. Open the full file only when that line, a verification-script failure, or a real decision point tells you to. -Everything passes through the state file (`.agents/T-.md` or +Everything passes through the state file (`.pipeline/T-.md` or equivalent). Context is never carried between roles by memory alone — if it isn't written there, the next role doesn't know it. @@ -303,3 +303,7 @@ granting anything standing edit rights over its own instructions. - A third review loop, a third test-fix loop, or a second spec bounce. - Any budget the structural check reports as exceeded. - Before any merge. + +Record approvals in the Decisions log. On restart or compaction, recover +status, counters, approvals, and worker activity from the task record before +dispatching. Do not repeat recorded approvals or start a competing writer. diff --git a/skills/dev-team-generator/reference/lessons-learned.md b/skills/dev-team-generator/reference/lessons-learned.md index 82b4f52..70abd84 100644 --- a/skills/dev-team-generator/reference/lessons-learned.md +++ b/skills/dev-team-generator/reference/lessons-learned.md @@ -568,3 +568,35 @@ path before its project integration exists. - Test the exact bootstrap state by removing the newly introduced files from a stamped scaffold and confirming a flag-free run restores only those files, preserves the stamp, and does not retrigger first-run behavior. + +## 29. Keep writable handoff records outside protected instruction directories + +A writable workspace can still contain recursively protected instruction, +configuration, or version-control directories. A syntactically valid worker +sandbox setting does not prove it can write the handoff there. Keep task +records, logs, and runtime markers separate from skill/config discovery, and +probe the real OS sandbox before relying on a path. Move every role and checker +together at a task boundary; retain a read-only legacy baseline and refuse +bootstrap that would silently create two runtimes. + +## 30. Resume exact identities, and fail when identity cannot be recovered + +The latest conversation in a repository is not the conversation associated +with a particular team. Capture the runtime's authoritative session identity, +pin it per team, and resume that exact identity. Capture through a reviewed +lifecycle event rather than guessing from timestamps or arbitrary transcript +files. A failed resume or a skipped/untrusted capture must stop for recovery; +starting fresh silently loses decisions. Keep a process lock to prevent two +leads using one pin. Worker corrections reuse the task's recorded worker thread; +record a replacement when that thread is no longer recoverable. + +## 31. Workflow consent and runtime permission approval are separate controls + +Approval of a spec or merge is a decision about the work. It does not remove +filesystem, network, execution-rule, or hook-trust boundaries. Check actual +state writes and authenticated worker-runtime access before dispatching. When +blocked, report the precise operation and request the narrow runtime approval +it needs, rather than disabling protection. Classify permission-sensitive +updates by behavior across every tool's schema, including hooks and TOML, not +only by the first tool's YAML field names. Configuration/skill presence is not +proof of discovery; fake-CLI tests are not live model workflow evidence. diff --git a/skills/dev-team-generator/reference/state-file-example.md b/skills/dev-team-generator/reference/state-file-example.md index bb559ae..fda4021 100644 --- a/skills/dev-team-generator/reference/state-file-example.md +++ b/skills/dev-team-generator/reference/state-file-example.md @@ -2,7 +2,7 @@ The single handoff surface. Every role reads this file and appends to it — nothing is passed between roles by prose alone. Copy this shape to -`.agents/T-.md` (or wherever this project keeps per-task state) and +`.pipeline/T-.md` (or wherever this project keeps per-task state) and fill it in per task. ```markdown @@ -13,6 +13,7 @@ fill it in per task. **Implementer for this task:** **Reviewer for this task:** none yet — set by the lead before dispatching review; a model/vendor independent from the implementer **Tester for this task:** none yet — set by the lead before dispatching test +**Planner thread id:** none yet — when the planner has a separate thread, record/reuse it for corrections on this task **Dispatch session id:** none yet — set after the first dispatch call, reused for every later call on this task **Wide-auto-approve mode:** off — only meaningful if the implementer's tool has one (see lessons-learned.md entry 1). On means it's used for this task's implement call, per the user's answer at spec approval. Off is the default; never turn this on without asking. **Review loop count:** 0 / 2 diff --git a/skills/status-board/SKILL.md b/skills/status-board/SKILL.md index 1b81681..db566c5 100644 --- a/skills/status-board/SKILL.md +++ b/skills/status-board/SKILL.md @@ -5,7 +5,7 @@ description: Keep a project's top-level status board in sync with per-task state # Status board -A per-task state file (`.agents/T-.md`, or whatever a project calls it) +A per-task state file (`.pipeline/T-.md`, or whatever a project calls it) is easy to keep current — each role updates its own file as it works. The project-wide status board (a top-level `README.md`, `STATUS.md`, or `TASKS.md` checklist) is not: nothing forces anyone to open every task file diff --git a/skills/toolkit-init/SKILL.md b/skills/toolkit-init/SKILL.md index 68a506e..4a3519e 100644 --- a/skills/toolkit-init/SKILL.md +++ b/skills/toolkit-init/SKILL.md @@ -16,6 +16,9 @@ Ask these up front rather than guessing — they shape every generated file: 1. **Project name** — used in a few descriptions and the tmux session name. 2. **Claude model** for planner/senior-dev (default `sonnet`). + If using Codex, offer lead/planner model and reasoning settings separately; + `--codex-model`, `--codex-reasoning`, `--codex-planner-model`, and + `--codex-planner-reasoning` default to `inherit`, preserving local settings. 3. **Cross-vendor provider and models.** Run `opencode models` in the target project and show the user the list rather than assuming what's configured. You need three: builder, reviewer, and a reviewer-fallback @@ -47,14 +50,14 @@ Ask these up front rather than guessing — they shape every generated file: It writes `.claude/agents/`, `.opencode/agent/`, `.claude/commands/{feature.md, toolkit-update.md}`, Codex project instructions/skills/planner under `AGENTS.md`, `.agents/skills/`, and `.codex/agents/`, -`.agents/{TEMPLATE.md,.toolkit-version}`, and +`.pipeline/{TEMPLATE.md,.toolkit-version}`, and `scripts/{oc.sh,team.sh,verify-state.sh,verify-spec.sh,promote-findings.sh}` into the target. It does **not** overwrite a file that already exists — it prints what it skipped so you can diff and merge by hand. When the toolkit itself has moved on since this project was scaffolded, run the same command with `--update` instead (flags default from -`.agents/.toolkit-version`, so `--update --target .` is usually enough). +`.pipeline/.toolkit-version`, so `--update --target .` is usually enough). It writes nothing — it prints a drift summary and exits 1 when files differ; add `--diff`/`--only ` for hunks. Triage against the toolkit's impact-tagged `CHANGELOG.md`, merge deliberately, then refresh @@ -87,7 +90,7 @@ a different tool: research-then-build step live for this project regardless of whether that offer is taken. -Codex is already supported as a fallback lead. For a different AI to be the +Codex is already supported as a lead. For a different AI to be the lead itself — not a worker role dispatched by an existing lead — point the user at `SYSTEM.md` at the toolkit root. @@ -105,6 +108,9 @@ user at `SYSTEM.md` at the toolkit root. `$feature`. `scripts/team.sh` chooses that fallback automatically. Tell the user to load the `delegate` skill at the start of the lead's own session — it's the context-discipline half of this, not the workflow half. + With Codex, review/trust the generated lifecycle hooks with `/hooks`, merge + them if hooks already existed, and run `scripts/codex-preflight.sh` from the + lead sandbox. Missing pins and failed resumes require explicit recovery. 4. The generated `.claude/commands/feature.md` and agent files reference "this project's own guidance file" for constraints — make sure the project actually has a `CLAUDE.md`/`AGENTS.md` before relying on that, diff --git a/templates/agents-state/TEMPLATE.md.tmpl b/templates/agents-state/TEMPLATE.md.tmpl index 16bc9c5..12a1244 100644 --- a/templates/agents-state/TEMPLATE.md.tmpl +++ b/templates/agents-state/TEMPLATE.md.tmpl @@ -4,7 +4,7 @@ > **Nothing is passed between roles by prose alone.** If a fact is not > written here, the next role does not know it. > -> Copy to `.agents/T-.md` and fill in. Never delete another role's +> Copy to `.pipeline/T-.md` and fill in. Never delete another role's > section. **Status:** draft | spec-approved | in-progress | blocked:question | blocked:spec | in-review | changes-requested | testing | done @@ -16,6 +16,7 @@ with a one-line why; the lead asks the human only when genuinely torn> **Reviewer for this task:** none yet — set by the lead before dispatching review; a model/vendor independent from the implementer (see `.claude/commands/feature.md` step 3's rotation policy) **Tester for this task:** none yet — set by the lead before dispatching test **OpenCode session id:** none yet — set after the first `oc.sh` call, reused for every later call on this task +**Codex planner thread id:** none yet — Codex lead sets this after the first planner dispatch; reuse for corrections on this task, new thread for a new task **OpenCode auto mode:** off — only meaningful when Implementer is `builder`. On means `oc.sh --auto` is used for this task's implement call, per the user's answer at spec approval. Off is the default; never turn this on without asking. **Review loop count:** 0 / 2 **Test-fix loops:** 0 / 2 diff --git a/templates/claude/agents/planner.md.tmpl b/templates/claude/agents/planner.md.tmpl index 9fad605..dffa59a 100644 --- a/templates/claude/agents/planner.md.tmpl +++ b/templates/claude/agents/planner.md.tmpl @@ -1,6 +1,6 @@ --- name: planner -description: Read-only spec writer for __PROJECT_NAME__. Given a feature request, explores the repo and produces a filled .agents/T-.md with checkable acceptance criteria and file-level scope. Use at the start of every /feature run, before any code is written. +description: Read-only spec writer for __PROJECT_NAME__. Given a feature request, explores the repo and produces a filled .pipeline/T-.md with checkable acceptance criteria and file-level scope. Use at the start of every /feature run, before any code is written. tools: Read, Grep, Glob, Bash, Write model: __CLAUDE_MODEL__ --- @@ -11,7 +11,7 @@ anything. ## Your one output -A filled `.agents/T-.md`, copied from `.agents/TEMPLATE.md`. That file is +A filled `.pipeline/T-.md`, copied from `.pipeline/TEMPLATE.md`. That file is the entire handoff. If a fact is not in it, the implementer does not know it. Set **Status** to `draft` when you start writing it, and leave it there — @@ -21,7 +21,7 @@ that file has exactly one owner; yours is this one. ## Hard rules **You never modify source.** Your `Write` tool exists for exactly one -purpose: creating and updating `.agents/T-.md`. Writing to any other path +purpose: creating and updating `.pipeline/T-.md`. Writing to any other path is a failure of your role, not a shortcut. **You do not design the implementation.** Scope is file-level: *which* files diff --git a/templates/claude/agents/senior-dev.md.tmpl b/templates/claude/agents/senior-dev.md.tmpl index 5de941b..a9a86a3 100644 --- a/templates/claude/agents/senior-dev.md.tmpl +++ b/templates/claude/agents/senior-dev.md.tmpl @@ -1,6 +1,6 @@ --- name: senior-dev -description: Implements an approved spec for __PROJECT_NAME__. Reads .agents/T-.md, writes the code, records the diff and its decisions back into the state file. The only Claude-side role permitted to edit source. Use after the lead approves a planner spec. +description: Implements an approved spec for __PROJECT_NAME__. Reads .pipeline/T-.md, writes the code, records the diff and its decisions back into the state file. The only Claude-side role permitted to edit source. Use after the lead approves a planner spec. tools: Read, Grep, Glob, Bash, Edit, Write model: __CLAUDE_MODEL__ --- @@ -10,8 +10,8 @@ spec. You are the only Claude-side role that edits source. ## Your inputs and outputs -- **Input:** `.agents/T-.md`, already marked `spec-approved`. -- **Outputs:** the code, `.agents/T-.diff`, and appended entries in the +- **Input:** `.pipeline/T-.md`, already marked `spec-approved`. +- **Outputs:** the code, `.pipeline/T-.diff`, and appended entries in the state file's *Decisions log*. ## Hard rules @@ -76,7 +76,7 @@ In general, regardless of project: that's the dedicated `tester` role's job, later in this same pipeline; running it yourself there would just delay your handoff without saving anything, since the tester runs it anyway as the verified record. -5. Write `.agents/T-.diff` (`git diff` against the branch point). +5. Write `.pipeline/T-.diff` (`git diff` against the branch point). 6. Append to *Decisions log*: what you chose and **why** — a decision without a reason is a preference, and the reviewer will treat it as one. 7. Set Status to `in-review`. diff --git a/templates/claude/commands/feature.md.tmpl b/templates/claude/commands/feature.md.tmpl index 049fcab..5c7082b 100644 --- a/templates/claude/commands/feature.md.tmpl +++ b/templates/claude/commands/feature.md.tmpl @@ -14,7 +14,7 @@ the roles that produced it or to a deterministic script, not to you. You do **not** write feature code yourself — if you find yourself editing source directly, you have stepped out of your role. -Between steps, read `**Latest handoff:**` at the top of `.agents/T-.md`, +Between steps, read `**Latest handoff:**` at the top of `.pipeline/T-.md`, not the whole file — it is the one line each role updates when it finishes, naming what happened and who's next. Open the full file only when that line, a `scripts/verify-state.sh` failure, or a real decision point tells you to. @@ -27,9 +27,12 @@ no duplicates, no unfilled placeholder) cannot hallucinate a pass. If the if it's available — it applies to your own orchestration decisions, not only to code a role writes; dispatched roles already carry an inlined copy of the same behavioral defaults in their own role files (`senior-dev.md` / -`builder.md`), so this is deliberately not re-taught to them here. +`builder.md`), so this is deliberately not re-taught to them here. Fresh +scaffolds install the supporting skill files under `.agents/skills/`; read +`delegate/SKILL.md`, `karpathy-guidelines/SKILL.md`, and +`status-board/SKILL.md` there directly if your tool does not discover them. -Everything passes through `.agents/T-.md`. Context is never carried +Everything passes through `.pipeline/T-.md`. Context is never carried between roles by memory alone. If it is not written there, the next role does not know it. @@ -53,7 +56,7 @@ does not know it. ## Preflight -1. **First run only:** if `.agents/.needs-customization` exists, this +1. **First run only:** if `.pipeline/.needs-customization` exists, this project was just scaffolded — the role files under `.claude/agents/` and `.opencode/agent/` still carry generic pitfalls/hard-rules text, not this codebase's actual ones. Ask the user: read the real source tree and this @@ -64,10 +67,10 @@ does not know it. the acceptance-criteria/scope rules, those are structural, not project content. Either answer, delete the marker file once asked — this only happens once. -2. Pick the next free id: `ls .agents/T-*.md`. +2. Pick the next free id: `ls .pipeline/T-*.md`. 3. Confirm the OpenCode server is up, resolving the port the same way - `scripts/oc.sh` does (`OC_SERVER`, then `.agents/.oc-port`, then 4096): - `curl -sS -m 5 "http://localhost:$(cat .agents/.oc-port 2>/dev/null || echo 4096)"`. + `scripts/oc.sh` does (`OC_SERVER`, then `.pipeline/.oc-port`, then 4096): + `curl -sS -m 5 "http://localhost:$(cat .pipeline/.oc-port 2>/dev/null || echo 4096)"`. If not up, tell the user to run `scripts/team.sh` (or `opencode serve`) and stop. 4. Confirm the working tree is clean enough to produce a meaningful diff. @@ -82,7 +85,7 @@ calls. one. The harness already auto-summarizes long conversations in the background; no manual step is needed. - Implementer, reviewer, and tester write their own outputs directly into - `.agents/T-.md` / `.agents/T-.diff` (see each role's write scope + `.pipeline/T-.md` / `.pipeline/T-.diff` (see each role's write scope in `.opencode/agent/*.md`). The lead's job is to **verify placement and content**, not to retype or re-paste what a role already wrote to disk. - Don't read a role's raw `--raw-out .jsonl` event stream as a matter of @@ -117,7 +120,7 @@ calls. ## 1 — Spec Dispatch the `planner` subagent with the request and the task id. It writes -`.agents/T-.md` from `.agents/TEMPLATE.md`. +`.pipeline/T-.md` from `.pipeline/TEMPLATE.md`. **Assess whether this task actually needs the planner — completeness of an existing spec is the test, not size or triviality:** @@ -125,10 +128,10 @@ existing spec is the test, not size or triviality:** - **Not yet planned, or genuinely ambiguous about scope/files/acceptance criteria** → dispatch the planner. This is the default. - **Already fully documented** — an existing `tasks/*/TASKS.md` entry, a - prior `.agents/T-.md` covering the same ground, or a request specific + prior `.pipeline/T-.md` covering the same ground, or a request specific enough that you can state the Goal, acceptance criteria, and files in scope yourself with no invention — skip the planner. Write those fields - into `.agents/T-.md` yourself before dispatching an implementer; + into `.pipeline/T-.md` yourself before dispatching an implementer; approval, review, and test below still apply unchanged. A large feature with a complete existing spec skips the planner exactly the same way a trivial one-liner does — completeness is what's being tested, not @@ -199,7 +202,8 @@ permissions, or budgets mid-task. ## 2 — Implement -Once approved, set Status to `spec-approved` and record who implements: +Once approved, record the approval and any split/permission decisions in the +Decisions log, set Status to `spec-approved`, and record who implements: - **`builder`** (OpenCode, `__BUILDER_MODEL__`) — the default when a cross-vendor implementation is wanted, or when Claude capacity is @@ -207,8 +211,8 @@ Once approved, set Status to `spec-approved` and record who implements: ```bash scripts/oc.sh --agent builder --model __BUILDER_MODEL__ --text \ - --raw-out .agents/T-.build.jsonl \ - --prompt "Implement .agents/T-.md. Read this project's own guidance file first." + --raw-out .pipeline/T-.build.jsonl \ + --prompt "Implement .pipeline/T-.md. Read this project's own guidance file first." ``` Add `--auto` to that command only if *OpenCode auto mode* in the state @@ -218,7 +222,7 @@ Once approved, set Status to `spec-approved` and record who implements: This is the task's first `oc.sh` call, so it starts a new session — read the `oc.sh: session=ses_...` line from stderr and record it in *OpenCode - session id* in `.agents/T-.md`. Review and test below reuse it. + session id* in `.pipeline/T-.md`. Review and test below reuse it. - **`senior-dev`** (Claude subagent, `__CLAUDE_MODEL__`) — when the user explicitly asks for Claude to implement instead, for this task. @@ -290,7 +294,7 @@ context the reuse exists to save. starts a new session. - Every `oc.sh` call, on any outcome, prints the session id it used to stderr: `oc.sh: session=ses_...`. After the first call, copy that id into - this task's *OpenCode session id* field in `.agents/T-.md`. + this task's *OpenCode session id* field in `.pipeline/T-.md`. - Every **later** call for the same task (review, test, and any retry) passes `--session ` read from that field. - Do not pass `--fork`. @@ -318,8 +322,8 @@ yourself before reusing it: `curl -X POST $OC_SERVER/session//abort`.) ```bash scripts/oc.sh --agent reviewer --model __REVIEWER_MODEL__ --text \ --session "" \ - --raw-out .agents/T-.review.jsonl \ - --prompt "Review .agents/T-.diff against .agents/T-.md. Both passes." + --raw-out .pipeline/T-.review.jsonl \ + --prompt "Review .pipeline/T-.diff against .pipeline/T-.md. Both passes." ``` Always pass `--text`. Without it the JSON event stream inlines the full @@ -340,7 +344,7 @@ separate record from the Decisions-log substitution note above: that's a one-time entry explaining *why*, this field is the durable answer to *who*, for anyone reading the task later. -If the reviewer's permission block allows writing `.agents/**`, it writes +If the reviewer's permission block allows writing `.pipeline/**`, it writes its own verdict into *Review verdicts*. Either way, **verify with the script, not by eye:** run `scripts/verify-state.sh T-` — it fails loudly on a misplaced or duplicated `### Pass N` heading, an unfilled @@ -399,8 +403,8 @@ before dispatching, same as the reviewer's field above. ```bash scripts/oc.sh --agent tester --model __TESTER_MODEL__ --text \ --session "" \ - --raw-out .agents/T-.test.jsonl \ - --prompt "Run the suites for .agents/T-.md and triage failures." + --raw-out .pipeline/T-.test.jsonl \ + --prompt "Run the suites for .pipeline/T-.md and triage failures." ``` The tester reports; it never fixes. Real failures go back to the @@ -468,7 +472,7 @@ any role in this pipeline performs.** Every implementer's permission block hard-denies `git commit`/`git push` for exactly this reason: one point of control, only after the human has said yes. -- Stage the changed files from *Files in scope*, plus `.agents/T-.md` +- Stage the changed files from *Files in scope*, plus `.pipeline/T-.md` itself. Committing the state file alongside the code means `git show` on this commit carries the spec, the Decisions log, the review findings, and the test results in one place — not scattered across a file nobody @@ -481,7 +485,7 @@ control, only after the human has said yes. `git log --oneline` and `git blame` show, and what makes `git log --all --grep '\[T-\]'` find every commit for this task — the path back from "something about this commit needs investigating" to - the full record in `.agents/T-.md`. + the full record in `.pipeline/T-.md`. **Then ask, once: is there anything about *how this ran* worth writing down?** Not about the code — that is the reviewer's job and *Findings for diff --git a/templates/claude/commands/toolkit-update.md.tmpl b/templates/claude/commands/toolkit-update.md.tmpl index 6c44a44..946d00d 100644 --- a/templates/claude/commands/toolkit-update.md.tmpl +++ b/templates/claude/commands/toolkit-update.md.tmpl @@ -12,14 +12,22 @@ project's own copies. ## Hard gates -- **Task boundary only.** If any `.agents/T-*.md` is in flight (Status not - `done`), stop and tell the user to finish or park it first — changing +- **Task boundary only.** If any `.pipeline/T-*.md` (or legacy `.agents/T-*.md`) + is in flight (Status not `done`), stop and tell the user to finish or park it + first — changing the state-file contract under a running task is how an in-flight task ends up half on each contract. - **Permission blocks are never auto-applied.** Any hunk touching a `permission:` or `tools:` block is shown to the user and applied only on their explicit yes — and carries the standing rule: verify it live against the real OpenCode server before trusting it. +- **Codex permission changes follow the same gate.** Treat `sandbox_mode`, + `sandbox_workspace_write`, approval policy/reviewer, writable roots, network + policy, `.rules` files, hook commands, and agent tool/MCP/skill configuration + as permission-sensitive. Review and get explicit consent before applying; + re-verify in the real Codex runtime. Hook edits require renewed `/hooks` + trust. Runtime approval to write protected configuration is separate from + the user's choice to accept an upstream change; never bypass protection. - **Never overwrite a local customization silently.** Where upstream and local both changed, present three options: take upstream, keep local, or hand-merge. The user picks. @@ -44,7 +52,7 @@ project's own copies. `git -C pull` — or clone it first if it isn't on disk: `git clone https://github.com/MShokry/agent-toolkit`. Report the SHA/branch you landed on. -2. Read `.agents/.toolkit-version` — note `toolkit_sha`/`toolkit_tag` and +2. Read `.pipeline/.toolkit-version` — note `toolkit_sha`/`toolkit_tag` and `scaffolded`. This is your baseline — the diff is always *current toolkit templates vs your live files*, and the changelog entries that matter are those added after this baseline. If it doesn't exist, this @@ -52,6 +60,10 @@ project's own copies. `--update` infers them from the target's files and prints them; verify they look right before merging, and expect to create the stamp at step 7. + If the stamp/runtime is still under `.agents/`, read the legacy stamp there + and complete `migrations/02-pipeline-directory.md` before adding missing + files. Check legacy task files for in-flight work too. Never create a second + runtime alongside an unmigrated first one. 3. Run `bin/init.sh --update --target .` (from the toolkit checkout path). Exit 0 means nothing to do — report that and stop. Exit 1 lists the differing/new files. @@ -77,7 +89,8 @@ project's own copies. `bin/init.sh --refresh-stamp --target .` (on a pre-v0.3.0 project this writes the stamp for the first time — future updates then need no flags). -8. Re-run `bin/init.sh --update --target .` — it must now exit 0. Then run +8. Re-run `bin/init.sh --update --target .`. Resolve unexpected drift; record + deliberately kept local customizations rather than forcing equality. Then run `scripts/verify-state.sh` on the most recent task's state file as a smoke check that the contract still parses. 9. Report: merged / kept-local / deferred (with reasons), and record one diff --git a/templates/codex/AGENTS.md.tmpl b/templates/codex/AGENTS.md.tmpl index 8be20f0..94704d6 100644 --- a/templates/codex/AGENTS.md.tmpl +++ b/templates/codex/AGENTS.md.tmpl @@ -10,8 +10,13 @@ an agent-toolkit scaffold update, invoke the `toolkit-update` skill. Do not run either workflow from memory: load the skill and follow its referenced canonical flow. -The task state file under `.agents/` is the only handoff surface between roles. -Keep role replies short and leave full details in that file. +The task state file under `.pipeline/` is the only handoff surface between roles. +Keep role replies short and leave full details in that file. Run +`scripts/codex-preflight.sh` from your sandbox before feature work. +`.agents/skills/` and `.codex/` contain protected instructions/configuration; +`.pipeline/` contains writable runtime records. Never store runtime output in +the protected instruction directories. Spec/merge consent does not replace +runtime permission approval for network, Git, or protected-config writes. ## Project-specific guidance diff --git a/templates/codex/agents/planner.toml.tmpl b/templates/codex/agents/planner.toml.tmpl index dd61f16..fe26f07 100644 --- a/templates/codex/agents/planner.toml.tmpl +++ b/templates/codex/agents/planner.toml.tmpl @@ -1,12 +1,17 @@ name = "planner" description = "Read-only source explorer and spec writer for __PROJECT_NAME__. Use when the feature pipeline needs a new or corrected task specification." +__CODEX_PLANNER_MODEL_LINE__ +__CODEX_PLANNER_REASONING_LINE__ sandbox_mode = "workspace-write" developer_instructions = """ Read this project's root CLAUDE.md or AGENTS.md first, then read .claude/agents/planner.md in full. Follow that planner contract exactly. Your workspace-write sandbox is required only because your output is -.agents/T-.md. Codex cannot enforce a narrower path-only write scope here, -so never modify source or any other path. Return only the short receipt required -by the planner contract; the state file is the record. +.pipeline/T-.md. This sandbox does not enforce a narrower path-only write scope, +so never modify source or any other path. Create a temporary write probe in +.pipeline/ and remove it before planning; if policy blocks it, report the exact +operation to the lead rather than writing elsewhere. Do not dispatch agents. +Return only the short receipt required by the planner contract; the state file +is the record. """ diff --git a/templates/codex/hooks.json.tmpl b/templates/codex/hooks.json.tmpl new file mode 100644 index 0000000..79b6d9b --- /dev/null +++ b/templates/codex/hooks.json.tmpl @@ -0,0 +1,7 @@ +{ + "description": "Capture the exact Codex lead id for scripts/team.sh. Review and trust with /hooks.", + "hooks": { + "SessionStart": [{"hooks": [{"type": "command", "command": "if [ -n \"${TEAM_CODEX_SESSION:-}\" ]; then python3 \"$TEAM_CODEX_ROOT/scripts/codex-session.py\"; fi", "timeout": 5}]}], + "UserPromptSubmit": [{"hooks": [{"type": "command", "command": "if [ -n \"${TEAM_CODEX_SESSION:-}\" ]; then python3 \"$TEAM_CODEX_ROOT/scripts/codex-session.py\"; fi", "timeout": 5}]}] + } +} diff --git a/templates/codex/hooks/session.py.tmpl b/templates/codex/hooks/session.py.tmpl new file mode 100644 index 0000000..e048e27 --- /dev/null +++ b/templates/codex/hooks/session.py.tmpl @@ -0,0 +1,49 @@ +"""Capture only the team launcher's main Codex session, from hook JSON.""" +import json +import os +from pathlib import Path +import re +import sys + + +def main(): + team = os.environ.get("TEAM_CODEX_SESSION", "") + if not team: + return # Ordinary Codex conversations have no toolkit-owned pin. + if not re.fullmatch(r"[a-zA-Z0-9_-]+", team): + raise ValueError("invalid team name") + event = json.load(sys.stdin) + if not isinstance(event, dict): + raise ValueError("hook input must be an object") + if event.get("hook_event_name") not in ("SessionStart", "UserPromptSubmit"): + return + root = Path(__file__).resolve().parent.parent + cwd = Path(event["cwd"]).resolve() + if cwd != root and root not in cwd.parents: + raise ValueError("hook cwd is outside this repository") + session = event["session_id"] + if not isinstance(session, str) or not re.fullmatch(r"[a-zA-Z0-9_-]+", session): + raise ValueError("invalid Codex session id") + directory = root / ".pipeline" + if directory.is_symlink(): + raise ValueError("runtime directory must stay inside the repository") + directory.mkdir(exist_ok=True) + pin = directory / f".codex-session-id.{team}" + if pin.is_symlink(): + raise ValueError("session pin must not be a symlink") + # Exclusive creation avoids concurrent hooks replacing another thread's id. + try: + with pin.open("x") as output: + output.write(session + "\n") + except FileExistsError: + if pin.read_text().strip() != session: + raise ValueError("session differs from the pinned lead; use a different team name") + print("Team lead session captured. Use $feature for feature work; runtime records live in .pipeline/.") + + +if __name__ == "__main__": + try: + main() + except (KeyError, TypeError, ValueError, OSError) as error: + print(f"codex-session: {error}", file=sys.stderr) + sys.exit(2) diff --git a/templates/codex/skills/feature/SKILL.md.tmpl b/templates/codex/skills/feature/SKILL.md.tmpl index 3982d8c..138c46e 100644 --- a/templates/codex/skills/feature/SKILL.md.tmpl +++ b/templates/codex/skills/feature/SKILL.md.tmpl @@ -13,18 +13,47 @@ Claude frontmatter and `$ARGUMENTS` placeholder are syntax, not extra rules. Apply these Codex adaptations: - The feature request is the user's request that invoked this skill. +- Run `scripts/codex-preflight.sh` from your current sandbox before canonical + Preflight. Verify that Codex discovers the project `planner` agent and all + three supporting skills; a file-presence check cannot establish discovery. + On failure, report the exact blocked operation. Request narrowly scoped + runtime approval when needed; never switch to full access or disable approvals. + Its authenticated API check replaces canonical Preflight's step 3 server + probe, so `OC_SERVER` overrides and password-gated servers are handled once. - When the canonical flow says to dispatch `planner`, spawn the project-scoped Codex `planner` subagent. Wait for it and use its short reply as a receipt; - its full output belongs in `.agents/T-.md`. + its full output belongs in `.pipeline/T-.md`. +- Record **Codex planner thread id** in the task file after the first dispatch. + Same task -> same planner thread: send corrections, verify-spec failures, and + the one permitted spec bounce to that thread. Wait for completion before any + follow-up; after interruption inspect its status and the state file first. + Never start a second writer while the previous planner is active. If the + thread is gone, record that fact and start a replacement using the task file + and written decisions. New task -> new thread; release the thread when the + task ends, keeping the recorded id for audit. Respect the model/reasoning + settings in the planner file; otherwise inherit the lead's settings. - Use OpenCode `builder` as the implementer. The Claude `senior-dev` option is unavailable in a Codex-led fallback run. All review-independence rules still apply. -- Codex can invoke repository skills directly. Load `delegate`, - `karpathy-guidelines`, and `status-board` when they are available and the - canonical flow calls for them. +- Load the installed `delegate`, `karpathy-guidelines`, and `status-board` + repository skills when the canonical flow calls for them. - Do not replace the state-file handoff with Codex subagent chat context. Read `Latest handoff` between steps and use the structural scripts at the exact gates named by the canonical flow. +- OpenCode auto mode applies only to OpenCode dispatch. It never authorizes + a Codex permission change. Spec approval and merge consent are workflow + gates; runtime approval for sandbox/network/Git access is a separate control. + At the approved merge step, request runtime approval for the precise Git + command if needed. Configuration updates under `.codex/` or `.agents/skills/` + may also need runtime approval; never move those files to evade protection. +- At a restart or compaction, recover the active task from `.pipeline/`, read + its status, counters, approvals in the Decisions log, and Latest handoff, + and verify existing worker activity before dispatching. Do not repeat an + approval already recorded, and do not infer approval from silence. +- On a quota failure, preserve the task and thread ids and report the failing + step. Retry through an available scheduled wakeup only; if this runtime + cannot schedule one, state that limitation and leave a resumable handoff. + Never claim a retry has been scheduled when no mechanism exists. If the Codex planner cannot be spawned, stop and tell the user; do not silently plan and implement in the lead thread. diff --git a/templates/codex/skills/toolkit-update/SKILL.md.tmpl b/templates/codex/skills/toolkit-update/SKILL.md.tmpl index f854707..47abaf6 100644 --- a/templates/codex/skills/toolkit-update/SKILL.md.tmpl +++ b/templates/codex/skills/toolkit-update/SKILL.md.tmpl @@ -15,3 +15,8 @@ toolkit's plain, flag-free `bin/init.sh --target .` bootstrap has added it. Never update during an in-flight task, never auto-apply permission changes, and never overwrite a local customization without the user's explicit choice. + +Apply the canonical flow's Codex permission-change gate to TOML, execution +rules, and lifecycle hooks too. Protected config/skill writes may require +runtime approval. Preserve existing hooks and hand-merge the two session +capture handlers; review/trust changed handlers with `/hooks` afterward. diff --git a/templates/opencode/agent/builder.md.tmpl b/templates/opencode/agent/builder.md.tmpl index fab0797..d282346 100644 --- a/templates/opencode/agent/builder.md.tmpl +++ b/templates/opencode/agent/builder.md.tmpl @@ -1,5 +1,5 @@ --- -description: Cross-vendor implementer for __PROJECT_NAME__. Reads an approved .agents/T-.md and writes the code, as an alternative to the Claude-side senior-dev. Use when Claude capacity is unavailable or a second implementation vendor is wanted. +description: Cross-vendor implementer for __PROJECT_NAME__. Reads an approved .pipeline/T-.md and writes the code, as an alternative to the Claude-side senior-dev. Use when Claude capacity is unavailable or a second implementation vendor is wanted. mode: primary model: __BUILDER_MODEL__ temperature: 0.1 @@ -87,8 +87,8 @@ what the pipeline is parked on without the lead's conversation. ## Inputs and outputs -- **Input:** `.agents/T-.md`, marked `spec-approved`. -- **Outputs:** the code, `.agents/T-.diff`, appended *Decisions log* +- **Input:** `.pipeline/T-.md`, marked `spec-approved`. +- **Outputs:** the code, `.pipeline/T-.diff`, appended *Decisions log* entries, and Status set to `in-review`. ## Read first, every run @@ -119,7 +119,7 @@ the files in scope. end. Claiming a criterion met is not your call to make about your own work — that is the whole reason there are other roles. - **Never `git commit` or `git push`, `--auto` or not.** Write the diff to - `.agents/T-.diff`; leave the working tree uncommitted. Only the lead + `.pipeline/T-.diff`; leave the working tree uncommitted. Only the lead commits, and only once the user has said so — this is enforced at the permission layer below too (`git commit*`/`git push*` are hard `deny`), but say it here as well since a shared understanding beats a single point @@ -141,7 +141,7 @@ the files in scope. that's the dedicated `tester` role's job, later in this same pipeline; running it yourself there would just delay your handoff without saving anything, since the tester runs it anyway as the verified record. -4. Write `.agents/T-.diff` from `git diff` against the branch point. +4. Write `.pipeline/T-.diff` from `git diff` against the branch point. 5. Append decisions **with reasons** — a decision without a reason reads as a preference and the reviewer will treat it as such. 6. Set Status to `in-review`. diff --git a/templates/opencode/agent/reviewer.md.tmpl b/templates/opencode/agent/reviewer.md.tmpl index 207aa64..79a7aa7 100644 --- a/templates/opencode/agent/reviewer.md.tmpl +++ b/templates/opencode/agent/reviewer.md.tmpl @@ -9,11 +9,11 @@ temperature: 0.1 # # You can instead let the reviewer write its own verdict directly by # scoping edit/write to the state-file directory, e.g.: -# edit: { "*": deny, ".agents/**": allow } -# write: { "*": deny, ".agents/**": allow } +# edit: { "*": deny, ".pipeline/**": allow } +# write: { "*": deny, ".pipeline/**": allow } # Do NOT do this on faith. Verify it live first: dispatch this agent once and # have it try to edit a source file (must be refused) and edit a scratch file -# under .agents/ (must succeed). A permission block that looks right in YAML +# under .pipeline/ (must succeed). A permission block that looks right in YAML # is not the same as a permission block that is actually enforced by the # runtime — confirm before you rely on "the reviewer cannot touch source" as # a guarantee. @@ -80,12 +80,12 @@ from the repository, record it as a finding at severity `info` and move on. ## Inputs -- `.agents/T-.md` — the spec. Acceptance criteria are the contract. +- `.pipeline/T-.md` — the spec. Acceptance criteria are the contract. Read its *Decisions log* too, not only the criteria: that is where the implementer wrote **why** it made each call, and where it answers findings from your previous pass. Reviewing a diff without reading the author's stated reasoning is how a review turns into a rewrite request. -- `.agents/T-.diff` — what to review. +- `.pipeline/T-.diff` — what to review. - This project's own guidance file (`CLAUDE.md` / `AGENTS.md` / README) and any architecture/security docs it points to. @@ -146,7 +146,7 @@ pass code you believe is broken: a real blocker is a real blocker, and ## Output format If your permission block allows writing to the state-file directory, append -your verdict directly into `.agents/T-.md` under the existing *Review +your verdict directly into `.pipeline/T-.md` under the existing *Review verdicts* heading, replacing the placeholder for the pass you just ran — do not create a new heading elsewhere, and do not touch any other section. That entry is the full record: @@ -190,10 +190,10 @@ to edit `docs/` yourself, and typically have no permission to; a script copy mechanically once the lead runs it. Most reviews have nothing to add here — an empty section is the expected common case, not a gap to fill. -If your permission block does not allow writing `.agents/**`, mention the +If your permission block does not allow writing `.pipeline/**`, mention the finding in your reply instead so the lead can add it. -Before you stop, if your permission block allows writing `.agents/**`, set +Before you stop, if your permission block allows writing `.pipeline/**`, set **Latest handoff** at the top of the state file, e.g. `reviewer → PASS, 0 findings → next: tester` or `reviewer → CHANGES_REQUESTED, 2 findings → next: implementer (loop 1/2)`. diff --git a/templates/opencode/agent/tester.md.tmpl b/templates/opencode/agent/tester.md.tmpl index 8bf7b4b..145ab82 100644 --- a/templates/opencode/agent/tester.md.tmpl +++ b/templates/opencode/agent/tester.md.tmpl @@ -12,11 +12,11 @@ permission: edit: "*": deny "__TEST_DIR__/**": allow - ".agents/**": allow + ".pipeline/**": allow write: "*": deny "__TEST_DIR__/**": allow - ".agents/**": allow + ".pipeline/**": allow patch: deny question: deny todowrite: allow @@ -48,7 +48,7 @@ permission: # "npm install *" is allowed on purpose: a missing dev dependency is an # environmental failure the tester must be able to resolve to run the # suite at all. The tradeoff (postinstall code execution) is accepted - # because this role's write scope is bounded to __TEST_DIR__/ + .agents/ + # because this role's write scope is bounded to __TEST_DIR__/ + .pipeline/ # and every call lands in --raw-out. "*": ask "node *": allow @@ -72,13 +72,13 @@ You are the **tester** for __PROJECT_NAME__. You run tests, you triage what fails, and you write that down. **You never fix anything.** `question: deny` is set because you run non-interactively — never block on a -prompt. Your write access is restricted to `__TEST_DIR__/` and `.agents/`; +prompt. Your write access is restricted to `__TEST_DIR__/` and `.pipeline/`; everything else is off-limits even when the fix is obvious. Seeing the fix and reporting it precisely *is* your job. Making it is someone else's. ## Read the acceptance criteria first -Open `.agents/T-.md` and read the *Acceptance criteria* before you run +Open `.pipeline/T-.md` and read the *Acceptance criteria* before you run anything. They are what this task is actually measured against — a green suite that exercises none of them tells the lead nothing, and "12/12 pass" against criteria nobody checked is a false signal, not a weak one. @@ -136,7 +136,7 @@ unmentioned is one the task cannot close on. ## Output -Append to *Test results* in `.agents/T-.md`: the command, the result, +Append to *Test results* in `.pipeline/T-.md`: the command, the result, the authorship line, the coverage table, and failures with reproduction steps. **That entry is the full record — your reply to the lead is not a second copy of it.** diff --git a/templates/scripts/codex-lead.sh.tmpl b/templates/scripts/codex-lead.sh.tmpl new file mode 100644 index 0000000..6275c0a --- /dev/null +++ b/templates/scripts/codex-lead.sh.tmpl @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Start/resume one Codex lead per team name. SessionStart/UserPromptSubmit +# hooks capture its actual id; never infer it from the latest conversation. +set -eu +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$REPO_ROOT" +SESSION="${1:?usage: codex-lead.sh [--fresh]}" +FRESH="${2:-}" +die() { printf 'codex-lead.sh: %s\n' "$*" >&2; exit 1; } +[[ "$SESSION" =~ ^[a-zA-Z0-9_-]+$ ]] || die "invalid team name" +[ -z "$FRESH" ] || [ "$FRESH" = --fresh ] || die "expected --fresh" +[ "$#" -le 2 ] || die "too many arguments" +command -v codex >/dev/null 2>&1 || die "codex not installed" +command -v python3 >/dev/null 2>&1 || die "python3 is required for session hooks" +[ ! -L .pipeline ] || die "runtime directory must stay inside the repository" +mkdir -p .pipeline +PIN=".pipeline/.codex-session-id.$SESSION" +STARTED=".pipeline/.codex-started.$SESSION" +LOCK=".pipeline/.codex-lead-lock.$SESSION" +mkdir "$LOCK" 2>/dev/null || die "lead already running (or stale lock: $LOCK); check the process before removing the lock" +trap 'rmdir "$LOCK"' EXIT +trap 'exit 130' INT +trap 'exit 143' TERM +if [ "$FRESH" = --fresh ]; then + rm -f "$PIN" "$STARTED" +fi +ARGS=(--no-daemon) +MODEL="__CODEX_MODEL__" +REASONING="__CODEX_REASONING__" +[ "$MODEL" = inherit ] || ARGS+=(--model "$MODEL") +[ "$REASONING" = inherit ] || ARGS+=(-c "model_reasoning_effort=\"$REASONING\"") +export TEAM_CODEX_SESSION="$SESSION" +export TEAM_CODEX_ROOT="$REPO_ROOT" +[ ! -L "$PIN" ] || die "session pin must not be a symlink" +if [ -f "$PIN" ]; then + ID="$(cat "$PIN")" + [[ "$ID" =~ ^[a-zA-Z0-9_-]+$ ]] || die "invalid stored session id: $PIN" + # A failed resume leaves the pin intact. Explicit --fresh is the only reset. + codex resume "${ARGS[@]}" "$ID" +else + [ ! -f "$STARTED" ] || die "previous launch has no captured id; recover it from /status and write $PIN, or explicitly use --fresh" + touch "$STARTED" + printf 'codex-lead.sh: review/trust the session hooks with /hooks; capture must succeed before relying on resume.\n' >&2 + codex "${ARGS[@]}" + [ -f "$PIN" ] || die "session id not captured; trust the hooks and submit a prompt, or recover the id from /status before restarting" +fi diff --git a/templates/scripts/codex-preflight.sh.tmpl b/templates/scripts/codex-preflight.sh.tmpl new file mode 100644 index 0000000..313fe4b --- /dev/null +++ b/templates/scripts/codex-preflight.sh.tmpl @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Run from the lead's CURRENT sandbox before feature work. An outside-shell +# pass is not proof that the lead or a planner has the same capabilities. +set -eu +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$REPO_ROOT" +die() { printf 'codex-preflight.sh: %s\n' "$*" >&2; exit 1; } +for dependency in codex opencode python3; do + command -v "$dependency" >/dev/null 2>&1 || die "$dependency not installed" +done +VERSION="$(codex --version)" +printf 'codex-preflight.sh: %s (toolkit baseline: 0.160.0)\n' "$VERSION" +FEATURES="$(codex features list)" || die "cannot inspect Codex runtime capabilities" +for feature in hooks multi_agent; do + printf '%s\n' "$FEATURES" | grep -qE "^$feature[[:space:]].*[[:space:]]true$" \ + || die "Codex $feature capability is unavailable or disabled; review your runtime configuration before starting" +done +codex resume --help | grep -q -- '--no-daemon' \ + || die "installed Codex lacks the launcher environment isolation option --no-daemon; update Codex before starting" +for path in .codex/agents/planner.toml .codex/hooks.json scripts/codex-session.py \ + .agents/skills/feature/SKILL.md .agents/skills/delegate/SKILL.md \ + .agents/skills/status-board/SKILL.md .agents/skills/karpathy-guidelines/SKILL.md \ + .pipeline/TEMPLATE.md; do + [ -f "$path" ] || die "missing $path; finish scaffold/update before starting" +done +grep -q '^name = "planner"' .codex/agents/planner.toml || die "planner definition has no planner name" +if [ -f .agents/TEMPLATE.md ] || compgen -G ".agents/T-*.md" >/dev/null; then + die "legacy .agents runtime state remains; complete the pipeline-directory migration first" +fi +PROBE="$(mktemp .pipeline/.write-probe.XXXXXX)" || die "cannot write .pipeline; request approval for this path" +rm -f "$PROBE" +if [ -n "${OC_SERVER:-}" ]; then + SERVER="$OC_SERVER" +elif [ -f .pipeline/.oc-port ]; then + SERVER="http://localhost:$(cat .pipeline/.oc-port)" +else + SERVER="http://localhost:4096" +fi +if [ -n "${OC_PASSWORD:-}" ]; then + OPENCODE_PASSWORD="$OC_PASSWORD" +elif [ -f .pipeline/.oc-password ]; then + OPENCODE_PASSWORD="$(cat .pipeline/.oc-password)" +else + OPENCODE_PASSWORD="${OPENCODE_PASSWORD:-}" +fi +export OPENCODE_PASSWORD +if command -v timeout >/dev/null 2>&1; then + TIMEOUT=timeout +elif command -v gtimeout >/dev/null 2>&1; then + TIMEOUT=gtimeout +else + die "timeout/gtimeout not installed; install coreutils for the OpenCode wrapper" +fi +"$TIMEOUT" 10 opencode api GET /api/session --server "$SERVER" --param limit=1 >/dev/null 2>&1 \ + || die "OpenCode API unavailable at $SERVER (server, authentication, or sandbox network policy); authorize this exact operation if policy blocks it" +printf 'codex-preflight.sh: state writes and authenticated OpenCode access passed; verify planner discovery in Codex before dispatch.\n' +printf 'codex-preflight.sh: git writes and protected config updates may require runtime approval; spec/merge consent remains a separate gate.\n' diff --git a/templates/scripts/oc.sh.tmpl b/templates/scripts/oc.sh.tmpl index fbc11ea..8b3e9da 100755 --- a/templates/scripts/oc.sh.tmpl +++ b/templates/scripts/oc.sh.tmpl @@ -24,7 +24,7 @@ # # Usage: # scripts/oc.sh --agent reviewer --model hcnsec/DeepSeek-V4-Pro \ -# --prompt-file .agents/T-001.review-request.md +# --prompt-file .pipeline/T-001.review-request.md # scripts/oc.sh --agent tester --model hcnsec/auto --prompt "run the suite" # echo "..." | scripts/oc.sh --agent reviewer --model hcnsec/glm-5.3 # @@ -153,9 +153,9 @@ # # --- Pipeline telemetry ----------------------------------------------------- # -# Every call appends one JSON line to .agents/logs/pipeline.jsonl (the dir is +# Every call appends one JSON line to .pipeline/logs/pipeline.jsonl (the dir is # created on first use, and init.sh adds it to a target repo's .gitignore — -# it is machine state, like .agents/.oc-port). Fields: ts, agent, model, +# it is machine state, like .pipeline/.oc-port). Fields: ts, agent, model, # session, wall_s, status, kill_reason, and — best-effort from # GET /session/ — token counts and cost. This is what makes the # pipeline's *own* $/min per role answerable after the fact, instead of only @@ -166,10 +166,10 @@ # Env: # OC_SERVER default http://localhost:4096, or the port # scripts/team.sh last started (read from -# .agents/.oc-port) if that file exists and OC_SERVER +# .pipeline/.oc-port) if that file exists and OC_SERVER # isn't set — see scripts/team.sh's own header for why. # OC_PASSWORD the OPENCODE_PASSWORD to authenticate with, or read from -# .agents/.oc-password (written by scripts/team.sh) if +# .pipeline/.oc-password (written by scripts/team.sh) if # that file exists and OC_PASSWORD isn't set. Required — # opencode v2 always auth-gates the API (v1 had none). # OC_TIMEOUT absolute ceiling, seconds. Default 2400. A run still @@ -198,16 +198,16 @@ REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" if [ -n "${OC_SERVER:-}" ]; then SERVER="$OC_SERVER" -elif [ -f "$REPO_ROOT/.agents/.oc-port" ]; then - SERVER="http://localhost:$(cat "$REPO_ROOT/.agents/.oc-port")" +elif [ -f "$REPO_ROOT/.pipeline/.oc-port" ]; then + SERVER="http://localhost:$(cat "$REPO_ROOT/.pipeline/.oc-port")" else SERVER="http://localhost:4096" fi if [ -n "${OC_PASSWORD:-}" ]; then OPENCODE_PASSWORD="$OC_PASSWORD" -elif [ -f "$REPO_ROOT/.agents/.oc-password" ]; then - OPENCODE_PASSWORD="$(cat "$REPO_ROOT/.agents/.oc-password")" +elif [ -f "$REPO_ROOT/.pipeline/.oc-password" ]; then + OPENCODE_PASSWORD="$(cat "$REPO_ROOT/.pipeline/.oc-password")" else # Preserve a password the caller already exported (e.g. hand-launched # `opencode serve` with a chosen OPENCODE_PASSWORD) instead of clobbering @@ -288,7 +288,7 @@ fi # reachability probe alone would not have caught a missing/wrong password. if ! "$TIMEOUT_BIN" 10 opencode api GET /api/session --server "$SERVER" --param limit=1 >/dev/null 2>&1; then die "no opencode server (or bad OPENCODE_PASSWORD) at $SERVER — start one with: - OPENCODE_PASSWORD=\"\$(cat .agents/.oc-password)\" opencode serve --port 4096 + OPENCODE_PASSWORD=\"\$(cat .pipeline/.oc-password)\" opencode serve --port 4096 (or run scripts/team.sh, which starts it in pane 1 with the password wired up)" fi @@ -462,7 +462,7 @@ USED_SESSION="$WATCH_SID" # --- pipeline telemetry (best-effort; never fails the run) ---------------- { - TELEMETRY_DIR="$REPO_ROOT/.agents/logs" + TELEMETRY_DIR="$REPO_ROOT/.pipeline/logs" if mkdir -p "$TELEMETRY_DIR" 2>/dev/null; then "$TIMEOUT_BIN" 10 opencode api GET "/api/session/${USED_SESSION:-none}" --server "$SERVER" 2>/dev/null | python3 -c ' import json, sys, datetime diff --git a/templates/scripts/promote-findings.sh.tmpl b/templates/scripts/promote-findings.sh.tmpl index 6117142..623d4d0 100755 --- a/templates/scripts/promote-findings.sh.tmpl +++ b/templates/scripts/promote-findings.sh.tmpl @@ -13,7 +13,7 @@ # # Findings are written as: # - [docs/SOME-DOC.md] one-line finding text -# under a "## Findings for docs" heading in the task's .agents/T-.md. +# under a "## Findings for docs" heading in the task's .pipeline/T-.md. # # Usage: scripts/promote-findings.sh T-03 @@ -26,7 +26,7 @@ REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$REPO_ROOT" ID="${1:?usage: promote-findings.sh }" -FILE=".agents/${ID}.md" +FILE=".pipeline/${ID}.md" [ -f "$FILE" ] || { echo "promote-findings.sh: no such file: $FILE" >&2; exit 1; } LINES="$(awk '/^## Findings for docs/{f=1;next} /^## /{f=0} f && /^- \[/' "$FILE")" diff --git a/templates/scripts/team.sh.tmpl b/templates/scripts/team.sh.tmpl index aff63fc..5ed2e46 100755 --- a/templates/scripts/team.sh.tmpl +++ b/templates/scripts/team.sh.tmpl @@ -5,7 +5,7 @@ # # pane 0 (left, big) claude/codex <- you talk to the lead here # pane 1 (top right) opencode serve <- the server oc.sh attaches to -# pane 2 (mid right) .agents/ activity tail +# pane 2 (mid right) .pipeline/ activity tail # pane 3 (bottom right) free pane # # Usage: @@ -20,7 +20,7 @@ # # Resuming is the default, not an opt-in. For Claude, pane 0 resumes the # lead's conversation pinned to this session name via a stored UUID (see -# .agents/.claude-session-id.) rather than `claude --continue` +# .pipeline/.claude-session-id.) rather than `claude --continue` # (which just means "the most recent conversation in this directory" — with # no notion of *which* tmux session started it, so a second team.sh session # name in the same repo, or even a one-off `claude` run there, could steal @@ -32,26 +32,26 @@ # scripts/team.sh again, the lead picks up its own thread — never someone # else's. Pass --fresh on the rare run where you actually want a clean # slate (this also mints a new pinned id, so future resumes follow it). Codex -# has no create-with-id equivalent, so it resumes the latest conversation -# scoped to this project and --fresh starts a new one. +# captures its actual id with reviewed lifecycle hooks and resumes that exact +# id through codex-lead.sh. Missing/failed pins require explicit recovery. # # Running two projects at once: opencode's server binds one port per # process, so two projects both defaulting to --port 4096 collide — the # second scripts/team.sh's `opencode serve` fails to bind. Give the second # project its own port: `scripts/team.sh --port 4097`. That port is written -# to .agents/.oc-port in this repo, and scripts/oc.sh reads it automatically +# to .pipeline/.oc-port in this repo, and scripts/oc.sh reads it automatically # (when OC_SERVER isn't set) — so oc.sh calls in this project just work # without you having to export OC_SERVER by hand every time. bin/init.sh -# adds .agents/.oc-port to this project's .gitignore; it's local machine +# adds .pipeline/.oc-port to this project's .gitignore; it's local machine # state, not something to commit. # # opencode v2's `serve` always auth-gates the API (v1 had none): unless # OPENCODE_PASSWORD/OPENCODE_SERVER_PASSWORD is set, it mints a random one # and only prints it once to the pane. This script generates one instead and -# writes it to .agents/.oc-password (same gitignore treatment as +# writes it to .pipeline/.oc-password (same gitignore treatment as # .oc-port) so scripts/oc.sh can read it on every call without you copying # it out of pane 1 by hand. The server is launched with a command that reads -# the file at runtime (`$(cat .agents/.oc-password)`) rather than the literal +# the file at runtime (`$(cat .pipeline/.oc-password)`) rather than the literal # secret, so the password itself never appears in the pane's typed-command # echo or tmux scrollback. # @@ -144,9 +144,9 @@ gen_password() { fi } -mkdir -p "$REPO_ROOT/.agents" -printf '%s' "$PORT" > "$REPO_ROOT/.agents/.oc-port" -PASSWORD_FILE="$REPO_ROOT/.agents/.oc-password" +mkdir -p "$REPO_ROOT/.pipeline" +printf '%s' "$PORT" > "$REPO_ROOT/.pipeline/.oc-port" +PASSWORD_FILE="$REPO_ROOT/.pipeline/.oc-password" # umask before the write, not chmod after, so the file is never briefly # world/group-readable between creation and permission-fixing. ( umask 077; gen_password > "$PASSWORD_FILE" ) @@ -154,7 +154,7 @@ PASSWORD_FILE="$REPO_ROOT/.agents/.oc-password" if [ "$LEAD" = "claude" ]; then # Pin pane 0 to one Claude conversation per tmux session name (see usage # comment above for why --continue alone isn't enough). - SESSION_ID_FILE="$REPO_ROOT/.agents/.claude-session-id.$SESSION" + SESSION_ID_FILE="$REPO_ROOT/.pipeline/.claude-session-id.$SESSION" [ "$FRESH" -eq 1 ] && rm -f "$SESSION_ID_FILE" if [ -f "$SESSION_ID_FILE" ]; then @@ -168,13 +168,9 @@ if [ "$LEAD" = "claude" ]; then LEAD_CMD="claude --session-id $CLAUDE_UUID" fi else - # Codex has no create-with-id equivalent. Its --last lookup is scoped to - # the current project, so resume that unless --fresh requested a new chat. - if [ "$FRESH" -eq 1 ]; then - LEAD_CMD="codex" - else - LEAD_CMD="codex resume --last || codex" - fi + [[ "$SESSION" =~ ^[a-zA-Z0-9_-]+$ ]] || { echo "team.sh: invalid Codex team name" >&2; exit 1; } + LEAD_CMD="bash scripts/codex-lead.sh $SESSION" + [ "$FRESH" -eq 0 ] || LEAD_CMD="$LEAD_CMD --fresh" fi tmux new-session -d -s "$SESSION" -c "$REPO_ROOT" -n team @@ -185,10 +181,10 @@ tmux send-keys -t "$SESSION:team.1" \ tmux split-window -v -t "$SESSION:team.1" -c "$REPO_ROOT" tmux send-keys -t "$SESSION:team.2" \ - "echo '--- .agents/ activity ---'; ls -t .agents/ 2>/dev/null; \ + "echo '--- .pipeline/ activity ---'; ls -t .pipeline/ 2>/dev/null; \ command -v fswatch >/dev/null 2>&1 \ - && fswatch -o .agents | while read -r _; do date '+%H:%M:%S'; ls -t .agents/ | head -5; done \ - || while true; do sleep 5; ls -t .agents/ 2>/dev/null | head -5; done" C-m + && fswatch -o .pipeline | while read -r _; do date '+%H:%M:%S'; ls -t .pipeline/ | head -5; done \ + || while true; do sleep 5; ls -t .pipeline/ 2>/dev/null | head -5; done" C-m tmux split-window -v -t "$SESSION:team.2" -c "$REPO_ROOT" diff --git a/templates/scripts/verify-spec.sh.tmpl b/templates/scripts/verify-spec.sh.tmpl index 08e07db..cd73c7a 100644 --- a/templates/scripts/verify-spec.sh.tmpl +++ b/templates/scripts/verify-spec.sh.tmpl @@ -28,7 +28,7 @@ REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$REPO_ROOT" ID="${1:?usage: verify-spec.sh }" -FILE=".agents/${ID}.md" +FILE=".pipeline/${ID}.md" [ -f "$FILE" ] || { echo "verify-spec.sh: no such file: $FILE" >&2; exit 1; } diff --git a/templates/scripts/verify-state.sh.tmpl b/templates/scripts/verify-state.sh.tmpl index 5c03d3f..99d412c 100755 --- a/templates/scripts/verify-state.sh.tmpl +++ b/templates/scripts/verify-state.sh.tmpl @@ -21,13 +21,13 @@ set -eu -# Anchor to the repo root like oc.sh/team.sh do, so `.agents/` resolves the +# Anchor to the repo root like oc.sh/team.sh do, so `.pipeline/` resolves the # same way no matter where the lead runs this from. REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$REPO_ROOT" ID="${1:?usage: verify-state.sh }" -FILE=".agents/${ID}.md" +FILE=".pipeline/${ID}.md" [ -f "$FILE" ] || { echo "verify-state.sh: no such file: $FILE" >&2; exit 1; } diff --git a/test/codex-sandbox.sh b/test/codex-sandbox.sh new file mode 100644 index 0000000..e8460a9 --- /dev/null +++ b/test/codex-sandbox.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Opt-in real Codex sandbox check. No model calls. Run outside another sandbox +# if the OS refuses nesting. Requires the unified `codex sandbox` CLI (0.160). +set -eu +TMP="$(mktemp -d "${TMPDIR:-/tmp}/toolkit-sandbox.XXXXXX")" +trap 'rm -rf "$TMP"' EXIT +mkdir -p "$TMP/.pipeline" "$TMP/.agents/skills" "$TMP/.codex" "$TMP/.git" +codex sandbox --permission-profile :workspace -C "$TMP" -- /bin/bash -c ' + set -eu + printf probe > .pipeline/probe + for path in .agents/skills/probe .codex/probe .git/probe; do + if (printf probe > "$path") 2>/dev/null; then + printf "FAIL: protected path was writable: %s\n" "$path" >&2 + exit 1 + fi + done + printf "codex-sandbox: writable runtime and protected instructions/Git verified\n" +' +[ -f "$TMP/.pipeline/probe" ] diff --git a/test/codex.sh b/test/codex.sh new file mode 100644 index 0000000..1485365 --- /dev/null +++ b/test/codex.sh @@ -0,0 +1,163 @@ +#!/usr/bin/env bash +# Behavioral tests of the launcher/hooks/preflight. No model calls, daemon, +# authentication, or running OpenCode server; external CLIs are test doubles. +set -eu +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +TMP="$(mktemp -d "${TMPDIR:-/tmp}/toolkit-codex.XXXXXX")" +trap 'rm -rf "$TMP"' EXIT +fail() { printf 'codex: FAIL: %s\n' "$*" >&2; exit 1; } +ok() { printf 'codex: ok — %s\n' "$*"; } +bash "$ROOT/bin/init.sh" --target "$TMP" --project-name codex-test \ + --codex-model test-lead --codex-reasoning high \ + --codex-planner-model test-planner --codex-planner-reasoning medium > "$TMP/init.log" 2>&1 +python3 - "$TMP" <<'PY' +import json, pathlib, sys, tomllib +root = pathlib.Path(sys.argv[1]) +agent = tomllib.loads((root / '.codex/agents/planner.toml').read_text()) +assert agent['model'] == 'test-planner' +assert agent['model_reasoning_effort'] == 'medium' +assert agent['sandbox_mode'] == 'workspace-write' +hooks = json.loads((root / '.codex/hooks.json').read_text()) +assert set(hooks['hooks']) == {'SessionStart', 'UserPromptSubmit'} +assert '.pipeline/T-' in agent['developer_instructions'] +assert 'codex_planner_model: test-planner' in (root / '.pipeline/.toolkit-version').read_text() +PY +bash "$ROOT/bin/init.sh" --update --target "$TMP" > "$TMP/update.log" 2>&1 \ + || fail "Codex settings did not round-trip through the stamp" +ok "valid TOML/hooks and independent lead/planner settings round-trip" + +FAKEBIN="$TMP/fakebin" +mkdir "$FAKEBIN" +cat > "$FAKEBIN/codex" <<'SH' +#!/usr/bin/env bash +set -eu +if [ "${1:-}" = --version ]; then echo 'codex-cli 0.160.0'; exit 0; fi +if [ "${1:-}" = features ]; then + printf 'hooks stable %s\nmulti_agent stable true\n' "${FAKE_HOOKS:-true}"; exit 0 +fi +if [ "${1:-}" = resume ] && [ "${2:-}" = --help ]; then echo '--no-daemon'; exit 0; fi +printf '%s\n' "$@" > "$FAKE_ARGS" +if [ "${1:-}" = resume ]; then + [ "${FAKE_RESUME_FAIL:-0}" = 0 ] || exit 9 + export FAKE_SESSION_ID="${!#}" +fi +[ "${FAKE_NO_HOOK:-0}" = 0 ] || exit 0 +HOOK="$(python3 -c 'import json; print(json.load(open(".codex/hooks.json"))["hooks"]["SessionStart"][0]["hooks"][0]["command"])')" +python3 -c 'import json,os; print(json.dumps({"session_id":os.environ["FAKE_SESSION_ID"], "cwd":os.getcwd(), "hook_event_name":"SessionStart"}))' \ + | bash -c "$HOOK" +SH +cat > "$FAKEBIN/opencode" <<'SH' +#!/usr/bin/env bash +printf '%s\n' "$@" > "$FAKE_OC_ARGS" +exit "${FAKE_API_FAIL:-0}" +SH +cat > "$FAKEBIN/timeout" <<'SH' +#!/usr/bin/env bash +shift +exec "$@" +SH +chmod +x "$FAKEBIN/"* +export PATH="$FAKEBIN:$PATH" FAKE_ARGS="$TMP/args" FAKE_OC_ARGS="$TMP/oc-args" +export FAKE_SESSION_ID=thread-one +LEAD="$TMP/scripts/codex-lead.sh" +bash "$LEAD" first > "$TMP/lead.log" 2>&1 || fail "fresh lead did not capture its id" +[ "$(cat "$TMP/.pipeline/.codex-session-id.first")" = thread-one ] || fail "wrong captured id" +grep -qx test-lead "$FAKE_ARGS" || fail "lead model not passed" +grep -qx 'model_reasoning_effort="high"' "$FAKE_ARGS" || fail "reasoning not passed" +grep -qx -- --no-daemon "$FAKE_ARGS" || fail "hooks could inherit a different daemon environment" +export FAKE_SESSION_ID=unrelated-latest +bash "$LEAD" first > "$TMP/lead.log" 2>&1 || fail "pinned resume failed" +grep -qx resume "$FAKE_ARGS" && grep -qx thread-one "$FAKE_ARGS" || fail "did not resume the pinned thread" +! grep -qx -- --last "$FAKE_ARGS" || fail "launcher used latest-thread lookup" +ok "resume pins the exact thread despite unrelated repository conversations" + +export FAKE_SESSION_ID=thread-two +bash "$LEAD" second > "$TMP/lead.log" 2>&1 || fail "second team launch failed" +[ "$(cat "$TMP/.pipeline/.codex-session-id.second")" = thread-two ] || fail "teams share a pin" +bash "$LEAD" first --fresh > "$TMP/lead.log" 2>&1 || fail "explicit fresh failed" +[ "$(cat "$TMP/.pipeline/.codex-session-id.first")" = thread-two ] || fail "fresh did not replace its own pin" +ok "team names are isolated and explicit fresh replaces only the selected pin" + +if FAKE_RESUME_FAIL=1 bash "$LEAD" first > "$TMP/lead.log" 2>&1; then fail "failed resume silently started a fresh conversation"; fi +[ "$(cat "$TMP/.pipeline/.codex-session-id.first")" = thread-two ] || fail "failed resume destroyed its pin" +[ ! -d "$TMP/.pipeline/.codex-lead-lock.first" ] || fail "failed resume leaked its lock" +if FAKE_NO_HOOK=1 bash "$LEAD" unpinned > "$TMP/lead.log" 2>&1; then fail "missing hook capture was accepted"; fi +rm -f "$FAKE_ARGS" +if bash "$LEAD" unpinned > "$TMP/lead.log" 2>&1; then fail "unpinned restart accepted"; fi +[ ! -f "$FAKE_ARGS" ] || fail "unpinned restart reached Codex instead of requiring recovery" +mkdir "$TMP/.pipeline/.codex-lead-lock.locked" +if bash "$LEAD" locked > "$TMP/lead.log" 2>&1; then fail "concurrent writer accepted"; fi +if bash "$LEAD" '../escape' > "$TMP/lead.log" 2>&1; then fail "unsafe team name accepted"; fi +ok "failed resume, absent hooks, concurrent leads, and unsafe names fail closed" + +python3 - "$TMP" <<'PY' +import json, os, pathlib, subprocess, sys +root = pathlib.Path(sys.argv[1]) +def hook(event, team='hook-test'): + return subprocess.run([sys.executable, str(root/'scripts/codex-session.py')], + input=json.dumps(event), text=True, capture_output=True, + env={**os.environ, 'TEAM_CODEX_SESSION':team}) +event = {'session_id':'hook-one', 'cwd':str(root), 'hook_event_name':'UserPromptSubmit'} +assert hook(event).returncode == 0 +assert hook(event).returncode == 0 # idempotent across turns/compactions +assert hook({**event, 'session_id':'hook-two'}).returncode != 0 +assert hook({**event, 'cwd':'/'}).returncode != 0 +assert hook({**event, 'session_id':'../escape'}).returncode != 0 +assert hook(event, '').returncode == 0 +assert (root/'.pipeline/.codex-session-id.hook-test').read_text().strip() == 'hook-one' +PY +ok "real hook handler validates event data, refuses replacement, and ignores ordinary chats" + +PREFLIGHT="$TMP/scripts/codex-preflight.sh" +OC_SERVER=http://localhost:4999 bash "$PREFLIGHT" > "$TMP/preflight.log" 2>&1 || fail "valid preflight failed" +grep -qx http://localhost:4999 "$FAKE_OC_ARGS" || fail "preflight ignored OC_SERVER" +if FAKE_API_FAIL=1 bash "$PREFLIGHT" > "$TMP/preflight.log" 2>&1; then fail "preflight ignored inaccessible API"; fi +grep -q 'authentication, or sandbox network policy' "$TMP/preflight.log" || fail "preflight obscured the blocked operation" +if FAKE_HOOKS=false bash "$PREFLIGHT" > "$TMP/preflight.log" 2>&1; then fail "preflight ignored disabled hooks"; fi +grep -q 'hooks capability is unavailable or disabled' "$TMP/preflight.log" || fail "disabled capability not identified" +cat > "$FAKEBIN/mktemp" <<'SH' +#!/usr/bin/env bash +exit 1 +SH +chmod +x "$FAKEBIN/mktemp" +if bash "$PREFLIGHT" > "$TMP/preflight.log" 2>&1; then fail "preflight accepted blocked state writes"; fi +grep -q 'cannot write .pipeline' "$TMP/preflight.log" || fail "write failure did not name its path" +rm "$FAKEBIN/mktemp" +ok "preflight checks actual writes and authenticated API access, including failures" + +# A legacy stamp remains readable for triage; init must not split the runtime. +mkdir -p "$TMP/.agents" +mv "$TMP/.pipeline/TEMPLATE.md" "$TMP/.agents/TEMPLATE.md" +mv "$TMP/.pipeline/.toolkit-version" "$TMP/.agents/.toolkit-version" +cp "$TMP/.agents/.toolkit-version" "$TMP/legacy-stamp" +if bash "$ROOT/bin/init.sh" --target "$TMP" > "$TMP/legacy.log" 2>&1; then fail "bootstrap accepted unmigrated runtime"; fi +grep -q '02-pipeline-directory' "$TMP/legacy.log" || fail "migration not identified" +rc=0 +bash "$ROOT/bin/init.sh" --update --target "$TMP" > "$TMP/legacy.log" 2>&1 || rc=$? +[ "$rc" = 1 ] || fail "legacy read-only triage did not report drift" +cmp -s "$TMP/legacy-stamp" "$TMP/.agents/.toolkit-version" || fail "triage changed the legacy baseline" +[ ! -f "$TMP/.pipeline/TEMPLATE.md" ] || fail "triage wrote a second runtime" +ok "legacy baseline is readable, triage is read-only, and unmigrated bootstrap is refused" + +# Defaults preserve local model choices; existing instructions/hooks/skills survive. +DEFAULT="$TMP/default" +mkdir "$DEFAULT" +bash "$ROOT/bin/init.sh" --target "$DEFAULT" --project-name defaults > "$TMP/default.log" 2>&1 +! grep -q '^model = ' "$DEFAULT/.codex/agents/planner.toml" || fail "default planner pins a model" +! grep -q '^model_reasoning_effort = ' "$DEFAULT/.codex/agents/planner.toml" || fail "default planner pins reasoning" +export FAKE_SESSION_ID=default-thread +bash "$DEFAULT/scripts/codex-lead.sh" defaults > "$TMP/default.log" 2>&1 || fail "inherited-settings launch failed" +! grep -qx -- --model "$FAKE_ARGS" || fail "default launcher overrides the configured model" +! grep -qx -- -c "$FAKE_ARGS" || fail "default launcher overrides configured reasoning" +printf '\nLOCAL RULE\n' >> "$DEFAULT/AGENTS.md" +printf '\n' >> "$DEFAULT/.codex/hooks.json" +printf '\nLOCAL SKILL RULE\n' >> "$DEFAULT/.agents/skills/delegate/SKILL.md" +cp "$DEFAULT/.codex/hooks.json" "$TMP/hooks-before" +bash "$ROOT/bin/init.sh" --target "$DEFAULT" > "$TMP/default.log" 2>&1 +grep -q 'LOCAL RULE' "$DEFAULT/AGENTS.md" || fail "existing project instructions were overwritten" +grep -q 'LOCAL SKILL RULE' "$DEFAULT/.agents/skills/delegate/SKILL.md" || fail "supporting skill customization was overwritten" +cmp -s "$TMP/hooks-before" "$DEFAULT/.codex/hooks.json" || fail "existing hooks were overwritten" +if bash "$ROOT/bin/init.sh" --target "$DEFAULT" --codex-planner-reasoning invalid > "$TMP/default.log" 2>&1; then fail "invalid reasoning accepted"; fi +ok "inheritance is preserved, invalid settings are rejected, and existing instructions/hooks/skills survive" + +printf '\ncodex: all behavioral checks passed\n' diff --git a/test/invariants.sh b/test/invariants.sh index d12a287..855fa57 100755 --- a/test/invariants.sh +++ b/test/invariants.sh @@ -42,6 +42,8 @@ LESSONS="skills/dev-team-generator/reference/lessons-learned.md" CODEX_AGENTS="templates/codex/AGENTS.md.tmpl" CODEX_FEATURE="templates/codex/skills/feature/SKILL.md.tmpl" CODEX_PLANNER="templates/codex/agents/planner.toml.tmpl" +CODEX_UPDATE="templates/codex/skills/toolkit-update/SKILL.md.tmpl" +UPDATE="templates/claude/commands/toolkit-update.md.tmpl" FAIL=0 CHECKS=0 @@ -115,13 +117,27 @@ rule "Codex project instructions route feature work through the skill" \ "$CODEX_AGENTS" rule "Codex planner discloses its non-enforced source-write boundary" \ - 'cannot enforce a narrower path-only write scope.*never modify source' \ + 'does not enforce a narrower path-only write scope.*never modify source' \ "$CODEX_PLANNER" rule "Codex planner delegates to the canonical planner contract" \ '\.claude/agents/planner\.md' \ "$CODEX_PLANNER" +rule "writable pipeline records are separate from skill discovery" \ + '\.pipeline/' \ + "$FEATURE" "$SYSTEM" "$FLOW" "$STATE" "$STATE_EX" "$PLANNER" "$CODEX_PLANNER" "$CODEX_AGENTS" +rule "Codex planner corrections reuse the task thread" \ + 'Same task.*same planner thread' "$CODEX_FEATURE" +rule "Codex separates workflow consent from runtime permissions" \ + 'runtime approval|runtime permission approval' "$CODEX_FEATURE" "$CODEX_AGENTS" "$CODEX_UPDATE" +rule "Codex update permission gate covers TOML and hooks" \ + 'sandbox_mode.*approval policy/reviewer' "$UPDATE" +rule "planner thread identity is recorded in both state contracts" \ + 'planner thread id' "$STATE" "$STATE_EX" +rule "approval decisions survive restart" \ + 'Record approvals|record the approval' "$FEATURE" "$SYSTEM" "$FLOW" + # --- the delivery contract -------------------------------------------------- rule "acceptance-criteria ledger closes the contract" \ 'ledger' \ diff --git a/test/smoke.sh b/test/smoke.sh index 99770c0..cae7ae3 100755 --- a/test/smoke.sh +++ b/test/smoke.sh @@ -52,7 +52,7 @@ ok "no unsubstituted __PLACEHOLDER__ tokens" wrote="$(grep -c '^init.sh: wrote ' "$TMP/run1.log" || true)" # Exclude this log and the customization marker; the provenance stamp is # counted separately since it is written once, not rendered per-template. -stamp_written=0; [ -f "$TMP/.agents/.toolkit-version" ] && stamp_written=1 +stamp_written=0; [ -f "$TMP/.pipeline/.toolkit-version" ] && stamp_written=1 files="$(find "$TMP" -type f -not -name run1.log \ -not -name .needs-customization -not -name .toolkit-version | wc -l | tr -d ' ')" [ "$wrote" = "$((files + stamp_written))" ] || fail "claimed $wrote writes but $((files + stamp_written)) files exist" @@ -77,10 +77,10 @@ grep -q 'never modify source' "$TMP/.codex/agents/planner.toml" \ || fail "Codex planner does not state its source-write boundary" ok "Codex lead instructions, skills, and planner were scaffolded" -[ -f "$TMP/.agents/.needs-customization" ] || fail ".needs-customization marker missing on fresh scaffold" +[ -f "$TMP/.pipeline/.needs-customization" ] || fail ".needs-customization marker missing on fresh scaffold" ok "first-run customization marker dropped" -STAMP="$TMP/.agents/.toolkit-version" +STAMP="$TMP/.pipeline/.toolkit-version" [ -f "$STAMP" ] || fail "provenance stamp missing on fresh scaffold" grep -q '^toolkit_sha:' "$STAMP" || fail "stamp lacks toolkit_sha" grep -q '^builder_model: a/b' "$STAMP" || fail "stamp does not record the init flags" @@ -90,7 +90,7 @@ ok "fresh scaffold wrote the provenance stamp with flags + toolkit SHA" # --- 2. second run skips, never clobbers ------------------------------------ cp "$TMP/.claude/commands/feature.md" "$TMP/feature.sentinel" printf 'LOCAL CUSTOMIZATION\n' >> "$TMP/.claude/commands/feature.md" -rm -f "$TMP/.agents/.needs-customization" +rm -f "$TMP/.pipeline/.needs-customization" bash "$ROOT/bin/init.sh" --target "$TMP" > "$TMP/run2.log" 2>&1 \ || fail "second init.sh run failed" skips="$(grep -c '^init.sh: skip (exists)' "$TMP/run2.log" || true)" @@ -98,8 +98,8 @@ skips="$(grep -c '^init.sh: skip (exists)' "$TMP/run2.log" || true)" ! grep -q '^init.sh: wrote ' "$TMP/run2.log" || fail "second run wrote something" grep -q 'LOCAL CUSTOMIZATION' "$TMP/.claude/commands/feature.md" \ || fail "second run clobbered a customized file" -[ ! -f "$TMP/.agents/.needs-customization" ] || fail "marker recreated on non-fresh run" -cmp -s "$TMP/stamp.bak" "$TMP/.agents/.toolkit-version" 2>/dev/null \ +[ ! -f "$TMP/.pipeline/.needs-customization" ] || fail "marker recreated on non-fresh run" +cmp -s "$TMP/stamp.bak" "$TMP/.pipeline/.toolkit-version" 2>/dev/null \ || fail "second run touched the provenance stamp" ok "re-run skipped all $files files, preserved local edits, marker + stamp untouched" # restore the pristine render so the --update checks below start clean @@ -116,9 +116,9 @@ bash "$ROOT/bin/init.sh" --target "$TMP" > "$TMP/bootstrap.log" 2>&1 \ bootstrap_writes="$(grep -c '^init.sh: wrote ' "$TMP/bootstrap.log" || true)" [ "$bootstrap_writes" = "4" ] \ || fail "missing-file bootstrap wrote $bootstrap_writes files, expected 4" -grep -q '^builder_model: a/b' "$TMP/.agents/.toolkit-version" \ +grep -q '^builder_model: a/b' "$TMP/.pipeline/.toolkit-version" \ || fail "missing-file bootstrap changed the provenance values" -[ ! -f "$TMP/.agents/.needs-customization" ] \ +[ ! -f "$TMP/.pipeline/.needs-customization" ] \ || fail "missing-file bootstrap recreated the first-run marker" ok "flag-free plain run installs newly added files from stamped values only" @@ -142,7 +142,7 @@ chmod +x "$FAKEBIN/codex" "$FAKEBIN/tmux" TEAM_LOG="$TMP/team.log" PATH="$FAKEBIN:/usr/bin:/bin" \ "$TMP/scripts/team.sh" --fresh codex-smoke >/dev/null 2>&1 \ || fail "team.sh failed its Codex fallback launch" -grep -q 'send-keys .* codex C-m' "$TMP/team.log" \ +grep -q 'send-keys .* scripts/codex-lead.sh codex-smoke --fresh C-m' "$TMP/team.log" \ || fail "team.sh did not put Codex in the lead pane when Claude was unavailable" ok "team.sh falls back to Codex when Claude is unavailable" @@ -163,7 +163,7 @@ grep -q "all $files checked files match" "$TMP/upd1.log" || fail "--update summa ok "--update: flag-free run defaults from the stamp; clean target exits 0" # --- 5. --update: drift is summarized, exit 1, hunks only behind --diff ------- -printf '\n' >> "$TMP/.agents/TEMPLATE.md" +printf '\n' >> "$TMP/.pipeline/TEMPLATE.md" rc=0 bash "$ROOT/bin/init.sh" --update --target "$TMP" > "$TMP/upd2.log" 2>&1 || rc=$? [ "$rc" = "1" ] || fail "--update exited $rc on a drifted target, expected 1" @@ -211,7 +211,7 @@ ok "init.sh refuses --target pointing at the toolkit itself" # --- 6. verify-state.sh ------------------------------------------------------ VS="$TMP/scripts/verify-state.sh" -cat > "$TMP/.agents/T-01.md" <<'EOF' +cat > "$TMP/.pipeline/T-01.md" <<'EOF' **Status:** blocked ## Review verdicts @@ -224,17 +224,17 @@ EOF ok "verify-state accepts valid state file incl. blocked Status" # 6a. Task class + decision source: absent = fine, junk = fails -cat > "$TMP/.agents/T-01.md" <<'EOF' +cat > "$TMP/.pipeline/T-01.md" <<'EOF' **Status:** blocked **Task class:** TBD **Class decided by:** maybe EOF "$VS" T-01 > /dev/null 2>&1 && fail "verify-state accepted bogus Task class / decision source" -printf '**Status:** blocked\n**Task class:** sensitive\n**Class decided by:** human\n' > "$TMP/.agents/T-01.md" +printf '**Status:** blocked\n**Task class:** sensitive\n**Class decided by:** human\n' > "$TMP/.pipeline/T-01.md" "$VS" T-01 > /dev/null 2>&1 || fail "verify-state rejected a valid class + decision source" ok "verify-state validates Task class + Class decided by (absent ok, junk fails)" -printf '\n### Pass 3 — 2026-08-26 — verdict: PASS\n\nnone\n' >> "$TMP/.agents/T-01.md" +printf '\n### Pass 3 — 2026-08-26 — verdict: PASS\n\nnone\n' >> "$TMP/.pipeline/T-01.md" if "$VS" T-01 > /dev/null 2>&1; then fail "verify-state accepted a Pass 3 (two-loop cap breached)" fi @@ -245,7 +245,7 @@ ok "verify-state fails loudly on a third review pass" # anti-thrash and delivery-contract rules; if they are advisory only, they # are not rules. Each is checked on a file that is otherwise valid, so a # failure here names exactly one cause. -cat > "$TMP/.agents/T-03.md" <<'EOF' +cat > "$TMP/.pipeline/T-03.md" <<'EOF' **Status:** in-review **Review loop count:** 1 / 2 **Test-fix loops:** 5 / 2 @@ -258,11 +258,11 @@ cat > "$TMP/.agents/T-03.md" <<'EOF' 1. [high] a.js:1 — finding EOF "$VS" T-03 2>&1 | grep -q 'test-fix loop budget exceeded' || fail "verify-state did not catch a blown test-fix budget" -sed -i.bak 's|^\*\*Test-fix loops:\*\* 5 / 2|**Test-fix loops:** 1 / 2|' "$TMP/.agents/T-03.md" +sed -i.bak 's|^\*\*Test-fix loops:\*\* 5 / 2|**Test-fix loops:** 1 / 2|' "$TMP/.pipeline/T-03.md" "$VS" T-03 > /dev/null 2>&1 || fail "verify-state rejected a file with in-budget counters" ok "verify-state enforces the loop budgets and passes when they are in range" -cat > "$TMP/.agents/T-04.md" <<'EOF' +cat > "$TMP/.pipeline/T-04.md" <<'EOF' **Status:** done **Review loop count:** 1 / 2 **Test-fix loops:** 0 / 2 @@ -290,7 +290,7 @@ if "$VS" T-04 > /dev/null 2>&1; then fail "verify-state accepted 'done' with an unticked, unwaived criterion" fi "$VS" T-04 2>&1 | grep -q "acceptance criteria are unticked" || fail "verify-state's done-gate failed for the wrong reason" -python3 - "$TMP/.agents/T-04.md" <<'PY' +python3 - "$TMP/.pipeline/T-04.md" <<'PY' import sys p=sys.argv[1]; s=open(p).read() s=s.replace('| AC2 | [ ] | Pass 1 — unverifiable from diff | no covering test |', @@ -302,7 +302,7 @@ ok "verify-state refuses 'done' on an open criterion, accepts an explicit waiver # 6c. done-gate also rejects a ticked ledger row with an empty evidence cell — # a tick TEMPLATE.md already forbids, now enforced instead of self-policed. -cat > "$TMP/.agents/T-06.md" <<'EOF' +cat > "$TMP/.pipeline/T-06.md" <<'EOF' **Status:** done **Review loop count:** 1 / 2 **Test-fix loops:** 0 / 2 @@ -326,7 +326,7 @@ none EOF "$VS" T-06 > /dev/null 2>&1 && fail "verify-state accepted 'done' with a ticked ledger row citing no test evidence" "$VS" T-06 2>&1 | grep -q 'cite no evidence' || fail "done-gate ledger-evidence check failed for the wrong reason" -python3 - "$TMP/.agents/T-06.md" <<'PY' +python3 - "$TMP/.pipeline/T-06.md" <<'PY' import sys p = sys.argv[1]; s = open(p).read() s = s.replace('| AC1 | [x] | Pass 1 — a.js:42 | |', @@ -338,7 +338,7 @@ ok "verify-state's done-gate rejects a ticked ledger row with a blank evidence c # 6d. once review has run, a missing/placeholder verdict fails from the file # side too — the mirror of the unfilled-placeholder check. -cat > "$TMP/.agents/T-07.md" <<'EOF' +cat > "$TMP/.pipeline/T-07.md" <<'EOF' **Status:** testing **Review loop count:** 1 / 2 **Test-fix loops:** 0 / 2 @@ -350,21 +350,21 @@ cat > "$TMP/.agents/T-07.md" <<'EOF' EOF "$VS" T-07 > /dev/null 2>&1 && fail "verify-state accepted Status 'testing' with no filled review verdict" "$VS" T-07 2>&1 | grep -q 'no Review verdicts pass records a filled' || fail "missing-verdict check failed for the wrong reason" -sed -i.bak 's/verdict: TBD/verdict: PASS/' "$TMP/.agents/T-07.md" +sed -i.bak 's/verdict: TBD/verdict: PASS/' "$TMP/.pipeline/T-07.md" "$VS" T-07 > /dev/null 2>&1 || fail "verify-state rejected 'testing' once a filled PASS verdict was present" ok "verify-state requires a filled review verdict once a task has reached review" # --- 7. verify-spec.sh ------------------------------------------------------- # The spec-side equivalent: structure only, run before the human sees a spec. VSPEC="$TMP/scripts/verify-spec.sh" -cp "$TMP/.agents/TEMPLATE.md" "$TMP/.agents/T-05.md" +cp "$TMP/.pipeline/TEMPLATE.md" "$TMP/.pipeline/T-05.md" if "$VSPEC" T-05 > /dev/null 2>&1; then fail "verify-spec passed the raw template, which is entirely boilerplate" fi "$VSPEC" T-05 2>&1 | grep -q 'still the template placeholder' || fail "verify-spec did not identify template boilerplate" ok "verify-spec rejects an unfilled spec" -python3 - "$TMP/.agents/T-05.md" <<'PY' +python3 - "$TMP/.pipeline/T-05.md" <<'PY' import sys NL = chr(10) p = sys.argv[1] @@ -400,7 +400,7 @@ PY "$VSPEC" T-05 > /dev/null 2>&1 || { "$VSPEC" T-05; fail "verify-spec rejected a properly filled spec"; } ok "verify-spec accepts a filled spec" -python3 - "$TMP/.agents/T-05.md" <<'PY' +python3 - "$TMP/.pipeline/T-05.md" <<'PY' import sys p=sys.argv[1]; s=open(p).read() s=s.replace('resolves identically to "https://x.test/a"','works well') @@ -414,7 +414,7 @@ ok "verify-spec catches an unmeasurable acceptance criterion" # --- 8. promote-findings.sh -------------------------------------------------- PF="$TMP/scripts/promote-findings.sh" mkdir -p "$TMP/docs" -cat > "$TMP/.agents/T-02.md" <<'EOF' +cat > "$TMP/.pipeline/T-02.md" <<'EOF' ## Findings for docs - [docs/GOTCHAS.md] real finding worth keeping From f74a705d4597d718ef614ee1922540eebb0235bd Mon Sep 17 00:00:00 2001 From: abdoharb Date: Mon, 5 Oct 2026 16:29:24 +0300 Subject: [PATCH 2/8] fix: make OpenCode v2 pipeline preflight reliable --- CHANGELOG.md | 10 ++ CLAUDE.md | 6 +- README.md | 3 +- SYSTEM.md | 17 ++- bin/init.sh | 34 +++-- docs/ADDING-A-TOOL.md | 4 +- docs/UPGRADING.md | 2 +- migrations/03-opencode-v2-agents-directory.md | 28 +++++ skills/dev-team-generator/SKILL.md | 2 +- .../reference/flow-example.md | 9 +- .../reference/lessons-learned.md | 15 +++ .../reference/tool-onboarding.md | 2 +- skills/toolkit-init/SKILL.md | 4 +- templates/claude/commands/feature.md.tmpl | 21 +++- .../{agent => agents}/builder.md.tmpl | 0 .../{agent => agents}/reviewer.md.tmpl | 0 .../opencode/{agent => agents}/tester.md.tmpl | 0 templates/scripts/oc.sh.tmpl | 2 +- templates/scripts/verify-models.sh.tmpl | 117 ++++++++++++++++++ test/codex.sh | 15 +++ test/invariants.sh | 14 ++- test/smoke.sh | 9 ++ 22 files changed, 277 insertions(+), 37 deletions(-) create mode 100644 migrations/03-opencode-v2-agents-directory.md rename templates/opencode/{agent => agents}/builder.md.tmpl (100%) rename templates/opencode/{agent => agents}/reviewer.md.tmpl (100%) rename templates/opencode/{agent => agents}/tester.md.tmpl (100%) create mode 100755 templates/scripts/verify-models.sh.tmpl diff --git a/CHANGELOG.md b/CHANGELOG.md index c6ad024..93de85c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,16 @@ script, flag, or role rule; **PATCH** = prose and docs. ## Unreleased +- `[contract]` OpenCode role templates move from the legacy singular + `.opencode/agent/` directory to V2's canonical `.opencode/agents/` path. + Plain bootstrap refuses to create a second role tree beside an unmigrated + one, while stamp-less update recovery can still read either layout. + Migration: `migrations/03-opencode-v2-agents-directory.md`. +- `[safety]` Fresh scaffolds now include `scripts/verify-models.sh`; feature + preflight resolves every OpenCode-dispatched model against the authenticated + live server and verifies that required roles and their ordered permission + rules actually loaded after restarts, upgrades, migrations, or permission + edits. Disk configuration alone is no longer treated as readiness evidence. - `[contract]` Writable task records, outputs, server credentials, session pins, and logs move from `.agents/` to `.pipeline/` for both leads. Codex protects `.agents/` under its default workspace policy; skills remain at diff --git a/CLAUDE.md b/CLAUDE.md index eaf500a..971db13 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -70,9 +70,9 @@ files are skipped, not clobbered (`render()`'s core guarantee). | `templates/claude/agents/` | `planner.md.tmpl`, `senior-dev.md.tmpl` — Claude subagent role definitions | | `templates/claude/commands/` | `feature.md.tmpl` — the `/feature` pipeline command (the lead's own instructions); `toolkit-update.md.tmpl` — the `/toolkit-update` merge command for already-scaffolded projects | | `templates/codex/` | Root `AGENTS.md`, a project-scoped planner agent, and `feature` / `toolkit-update` skills. Thin Codex lead adapter over the canonical Claude command flow | -| `templates/opencode/agent/` | `builder.md.tmpl`, `reviewer.md.tmpl`, `tester.md.tmpl` — OpenCode role definitions | +| `templates/opencode/agents/` | `builder.md.tmpl`, `reviewer.md.tmpl`, `tester.md.tmpl` — OpenCode role definitions | | `templates/agents-state/` | `TEMPLATE.md.tmpl` — the `T-` state-file shape every role reads and appends to | -| `templates/scripts/` | `oc.sh.tmpl` (OpenCode CLI wrapper), `team.sh.tmpl` (+ `team-completion.bash.tmpl`; tmux layout), `verify-state.sh.tmpl` (state file) / `verify-spec.sh.tmpl` (spec, before the approval gate) / `promote-findings.sh.tmpl` — all deterministic, no-LLM-call structural checks | +| `templates/scripts/` | `oc.sh.tmpl` (OpenCode CLI wrapper), `team.sh.tmpl` (+ `team-completion.bash.tmpl`; tmux layout), `verify-state.sh.tmpl` (state file) / `verify-spec.sh.tmpl` (spec, before the approval gate) / `verify-models.sh.tmpl` (authenticated live model check) / `promote-findings.sh.tmpl` — all deterministic, no-LLM-call checks | | `skills/delegate/` | Context-discipline rules for the lead — usable independently of `init.sh` | | `skills/toolkit-init/` | Thin skill wrapping `bin/init.sh`, for running the scaffold conversationally | | `skills/dev-team-generator/` | Self-contained, interview-driven alternative to `toolkit-init`: generates the team + flow live for whatever tool(s) are actually available, instead of stamping out `templates/`. Its own `reference/lessons-learned.md` is a generalized, tool-agnostic distillation of this toolkit's hardening history — see the Conventions bullet below | @@ -168,7 +168,7 @@ files are skipped, not clobbered (`render()`'s core guarantee). `--refresh-stamp` after a merge is accepted. If you add a new marker-gated behavior, keep that "computed once, before any write" ordering or a later non-fresh `init.sh` run will re-trigger it. -- `templates/opencode/agent/reviewer.md.tmpl` defaults to blanket +- `templates/opencode/agents/reviewer.md.tmpl` defaults to blanket `edit: deny` / `write: deny`, unlike a project that has since widened its own copy (e.g. an applied copy's reviewer allows `.pipeline/**`). That gap is intentional — see README's "Design decisions" diff --git a/README.md b/README.md index b62de61..1bcbd68 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ templates/ every generated file, with __PLACEHOLDER__ tokens toolkit-update.md.tmpl — the /toolkit-update merge command codex/ AGENTS.md.tmpl, a project-scoped planner agent, reviewed lifecycle hooks, and feature/toolkit-update skills - opencode/agent/ builder.md.tmpl, reviewer.md.tmpl, tester.md.tmpl + opencode/agents/ builder.md.tmpl, reviewer.md.tmpl, tester.md.tmpl agents-state/ TEMPLATE.md.tmpl — the T- state file shape scripts/ oc.sh.tmpl (OpenCode CLI wrapper), Codex launcher/preflight, team.sh.tmpl (tmux @@ -127,6 +127,7 @@ templates/ every generated file, with __PLACEHOLDER__ tokens for team.sh), verify-state.sh.tmpl (structural check on a task's state file — no LLM call), verify-spec.sh.tmpl (the same, on a spec, before the human approves it), + verify-models.sh.tmpl (authenticated live model check), promote-findings.sh.tmpl (copies tagged findings into project docs — no LLM call, no agent write access to docs/) diff --git a/SYSTEM.md b/SYSTEM.md index 096dd1f..aa60ec6 100644 --- a/SYSTEM.md +++ b/SYSTEM.md @@ -221,7 +221,7 @@ takes them as required arguments rather than assuming: 2. **Cross-vendor independence for review, specifically.** The reviewer must not share a vendor/model family with whoever implemented — ask for a reviewer model and a *fallback* reviewer model in a different family, - the same way `templates/opencode/agent/reviewer.md.tmpl`'s consumer + the same way `templates/opencode/agents/reviewer.md.tmpl`'s consumer does, for whenever the two would otherwise collide. 3. **Source directories you're allowed to touch as implementer**, and anything explicitly off-limits without an Open Question first. @@ -254,7 +254,14 @@ mechanism. You need three things: thin CLI wrapper that passes a role's instructions and a model choice to another agent process and gets its final text back, nothing more exotic than that. -3. **The discipline rules above**, actually followed — the short-reply +3. **A live preflight for the worker runtime** — resolve every configured + model id against the actual provider before dispatch. After a runtime + restart, upgrade, role-path migration, or permission edit, inspect the + live role/capability ruleset too: confirm every required role loaded and + that last-match-wins permission ordering preserves the intended write + boundary. Configuration that merely looks correct is not enforcement + evidence. +4. **The discipline rules above**, actually followed — the short-reply convention, the loop cap, the stop-and-ask list. For the exact orchestration *sequence* (preflight checks → dispatch planner @@ -271,9 +278,9 @@ reference**, not a copy target: - `templates/claude/agents/planner.md.tmpl` — planner - `templates/claude/agents/senior-dev.md.tmpl` or - `templates/opencode/agent/builder.md.tmpl` — implementer -- `templates/opencode/agent/reviewer.md.tmpl` — reviewer -- `templates/opencode/agent/tester.md.tmpl` — tester + `templates/opencode/agents/builder.md.tmpl` — implementer +- `templates/opencode/agents/reviewer.md.tmpl` — reviewer +- `templates/opencode/agents/tester.md.tmpl` — tester Adapt each to what your own tool can actually enforce or actually do — research your tool's real config and capability surface first (don't diff --git a/bin/init.sh b/bin/init.sh index 8a9eb1b..6dcd6c6 100755 --- a/bin/init.sh +++ b/bin/init.sh @@ -186,9 +186,12 @@ STAMP_SOURCE="$STAMP" if [ "$UPDATE" -eq 0 ] && { [ -f "$TARGET/.agents/TEMPLATE.md" ] || compgen -G "$TARGET/.agents/T-*.md" >/dev/null; }; then die "legacy .agents runtime state: complete migrations/02-pipeline-directory.md at a task boundary before bootstrapping" fi +if [ "$UPDATE" -eq 0 ] && [ -d "$TARGET/.opencode/agent" ] && [ ! -d "$TARGET/.opencode/agents" ]; then + die "legacy .opencode/agent role layout: complete migrations/03-opencode-v2-agents-directory.md at a task boundary before bootstrapping" +fi # Keep in sync with the number of check_pair/render lines below. -RENDER_TOTAL=25 +RENDER_TOTAL=26 write_stamp() { local sha tag @@ -274,17 +277,19 @@ load_flags_from_stamp() { # exactly one rendered file. Sets REPLY; empty if not found. recover_from_target() { # $1 = key REPLY="" + local agent_dir="$TARGET/.opencode/agents" + [ -d "$agent_dir" ] || agent_dir="$TARGET/.opencode/agent" case "$1" in builder_model) - REPLY="$(sed -n 's/^model: //p' "$TARGET/.opencode/agent/builder.md" | head -1)" ;; + REPLY="$(sed -n 's/^model: //p' "$agent_dir/builder.md" | head -1)" ;; reviewer_model) - REPLY="$(sed -n 's/^model: //p' "$TARGET/.opencode/agent/reviewer.md" | head -1)" ;; + REPLY="$(sed -n 's/^model: //p' "$agent_dir/reviewer.md" | head -1)" ;; reviewer_fallback_model) # feature.md: "switch to `__REVIEWER_FALLBACK_MODEL__`" REPLY="$(sed -n 's/.*switch to `\([^`]*\)`.*/\1/p' \ "$TARGET/.claude/commands/feature.md" | head -1)" ;; tester_model) - REPLY="$(sed -n 's/^model: //p' "$TARGET/.opencode/agent/tester.md" | head -1)" ;; + REPLY="$(sed -n 's/^model: //p' "$agent_dir/tester.md" | head -1)" ;; claude_model) REPLY="$(sed -n 's/^model: //p' "$TARGET/.claude/agents/planner.md" 2>/dev/null | head -1)" ;; codex_model) @@ -302,7 +307,7 @@ recover_from_target() { # $1 = key test_dir) # tester.md: "__TEST_DIR__/**": allow REPLY="$(sed -n 's/^ *"\(.*\)\/\*\*": allow.*/\1/p' \ - "$TARGET/.opencode/agent/tester.md" | head -1)" ;; + "$agent_dir/tester.md" | head -1)" ;; esac } @@ -446,15 +451,16 @@ if [ "$UPDATE" -eq 1 ]; then check_pair "$TOOLKIT_ROOT/skills/karpathy-guidelines/SKILL.md" "$TARGET/.agents/skills/karpathy-guidelines/SKILL.md" check_pair "$TEMPLATES/codex/skills/feature/SKILL.md.tmpl" "$TARGET/.agents/skills/feature/SKILL.md" check_pair "$TEMPLATES/codex/skills/toolkit-update/SKILL.md.tmpl" "$TARGET/.agents/skills/toolkit-update/SKILL.md" - check_pair "$TEMPLATES/opencode/agent/builder.md.tmpl" "$TARGET/.opencode/agent/builder.md" - check_pair "$TEMPLATES/opencode/agent/reviewer.md.tmpl" "$TARGET/.opencode/agent/reviewer.md" - check_pair "$TEMPLATES/opencode/agent/tester.md.tmpl" "$TARGET/.opencode/agent/tester.md" + check_pair "$TEMPLATES/opencode/agents/builder.md.tmpl" "$TARGET/.opencode/agents/builder.md" + check_pair "$TEMPLATES/opencode/agents/reviewer.md.tmpl" "$TARGET/.opencode/agents/reviewer.md" + check_pair "$TEMPLATES/opencode/agents/tester.md.tmpl" "$TARGET/.opencode/agents/tester.md" check_pair "$TEMPLATES/agents-state/TEMPLATE.md.tmpl" "$TARGET/.pipeline/TEMPLATE.md" check_pair "$TEMPLATES/scripts/oc.sh.tmpl" "$TARGET/scripts/oc.sh" check_pair "$TEMPLATES/scripts/team.sh.tmpl" "$TARGET/scripts/team.sh" check_pair "$TEMPLATES/scripts/team-completion.bash.tmpl" "$TARGET/scripts/team-completion.bash" check_pair "$TEMPLATES/scripts/verify-state.sh.tmpl" "$TARGET/scripts/verify-state.sh" check_pair "$TEMPLATES/scripts/verify-spec.sh.tmpl" "$TARGET/scripts/verify-spec.sh" + check_pair "$TEMPLATES/scripts/verify-models.sh.tmpl" "$TARGET/scripts/verify-models.sh" check_pair "$TEMPLATES/scripts/promote-findings.sh.tmpl" "$TARGET/scripts/promote-findings.sh" CUR_SHA="$(git -C "$TOOLKIT_ROOT" rev-parse --short HEAD 2>/dev/null || echo unknown)" @@ -514,20 +520,22 @@ render "$TOOLKIT_ROOT/skills/status-board/SKILL.md" "$TARGET/.agents/skills/stat render "$TOOLKIT_ROOT/skills/karpathy-guidelines/SKILL.md" "$TARGET/.agents/skills/karpathy-guidelines/SKILL.md" render "$TEMPLATES/codex/skills/feature/SKILL.md.tmpl" "$TARGET/.agents/skills/feature/SKILL.md" render "$TEMPLATES/codex/skills/toolkit-update/SKILL.md.tmpl" "$TARGET/.agents/skills/toolkit-update/SKILL.md" -render "$TEMPLATES/opencode/agent/builder.md.tmpl" "$TARGET/.opencode/agent/builder.md" -render "$TEMPLATES/opencode/agent/reviewer.md.tmpl" "$TARGET/.opencode/agent/reviewer.md" -render "$TEMPLATES/opencode/agent/tester.md.tmpl" "$TARGET/.opencode/agent/tester.md" +render "$TEMPLATES/opencode/agents/builder.md.tmpl" "$TARGET/.opencode/agents/builder.md" +render "$TEMPLATES/opencode/agents/reviewer.md.tmpl" "$TARGET/.opencode/agents/reviewer.md" +render "$TEMPLATES/opencode/agents/tester.md.tmpl" "$TARGET/.opencode/agents/tester.md" render "$TEMPLATES/agents-state/TEMPLATE.md.tmpl" "$TARGET/.pipeline/TEMPLATE.md" render "$TEMPLATES/scripts/oc.sh.tmpl" "$TARGET/scripts/oc.sh" render "$TEMPLATES/scripts/team.sh.tmpl" "$TARGET/scripts/team.sh" render "$TEMPLATES/scripts/team-completion.bash.tmpl" "$TARGET/scripts/team-completion.bash" render "$TEMPLATES/scripts/verify-state.sh.tmpl" "$TARGET/scripts/verify-state.sh" render "$TEMPLATES/scripts/verify-spec.sh.tmpl" "$TARGET/scripts/verify-spec.sh" +render "$TEMPLATES/scripts/verify-models.sh.tmpl" "$TARGET/scripts/verify-models.sh" render "$TEMPLATES/scripts/promote-findings.sh.tmpl" "$TARGET/scripts/promote-findings.sh" chmod +x "$TARGET/scripts/oc.sh" "$TARGET/scripts/team.sh" \ "$TARGET/scripts/verify-state.sh" "$TARGET/scripts/verify-spec.sh" \ - "$TARGET/scripts/promote-findings.sh" "$TARGET/scripts/codex-lead.sh" \ + "$TARGET/scripts/verify-models.sh" "$TARGET/scripts/promote-findings.sh" \ + "$TARGET/scripts/codex-lead.sh" \ "$TARGET/scripts/codex-preflight.sh" 2>/dev/null || true mkdir -p "$TARGET/.pipeline" @@ -597,7 +605,7 @@ init.sh: done. Next steps: 1. Read every generated file before trusting it — especially - .opencode/agent/reviewer.md's permission block. A blanket "deny" has + .opencode/agents/reviewer.md's permission block. A blanket "deny" has failed to actually block a write before in at least one real project; verify it against your real OpenCode server rather than assuming it from the YAML. diff --git a/docs/ADDING-A-TOOL.md b/docs/ADDING-A-TOOL.md index 5687598..d6cc4ba 100644 --- a/docs/ADDING-A-TOOL.md +++ b/docs/ADDING-A-TOOL.md @@ -11,12 +11,12 @@ second or third tool — not before. ## Case 1 — a role this toolkit already has, running under a tool it doesn't support yet -Example: `tester` exists only for OpenCode (`.opencode/agent/tester.md`); +Example: `tester` exists only for OpenCode (`.opencode/agents/tester.md`); you want it to also run under some other tool. 1. **Learn the new tool's own agent/rule format first** — don't guess it. Find: where it looks for custom agent/rule definitions (a directory - convention, like `.claude/agents/` or `.opencode/agent/`), what + convention, like `.claude/agents/` or `.opencode/agents/`), what frontmatter or config keys it reads (model selection, a permission or capability model, a system-prompt field), and how — or whether — it can be dispatched non-interactively from a script (OpenCode has `opencode diff --git a/docs/UPGRADING.md b/docs/UPGRADING.md index 22bf038..6f0bdc0 100644 --- a/docs/UPGRADING.md +++ b/docs/UPGRADING.md @@ -152,7 +152,7 @@ With Stages 1–2 in place, `--update` gets three cheap upgrades: ``` init.sh: 3 of 12 files differ from toolkit v0.3.0 (you are on v0.2.0) - [safety] .opencode/agent/builder.md — permission block + [safety] .opencode/agents/builder.md — permission block [contract] .pipeline/TEMPLATE.md — 2 new fields [docs] scripts/team.sh — comment only run with --diff to see hunks, or --diff for one file diff --git a/migrations/03-opencode-v2-agents-directory.md b/migrations/03-opencode-v2-agents-directory.md new file mode 100644 index 0000000..a0fc031 --- /dev/null +++ b/migrations/03-opencode-v2-agents-directory.md @@ -0,0 +1,28 @@ +# Migration 03 — OpenCode V2 agents directory + +Apply at a task boundary. OpenCode V2's canonical project role directory is +`.opencode/agents/`; older toolkit scaffolds wrote `.opencode/agent/`. Do not +bootstrap the new directory beside the old one: duplicate role ids make it +unclear which permission block the runtime loaded. + +1. Confirm no task state file is in flight. +2. Stop the OpenCode server. +3. Move the customized role files without rewriting their frontmatter: + + ```bash + mv .opencode/agent .opencode/agents + ``` + +4. Update current instructions and scripts that reference + `.opencode/agent/` to `.opencode/agents/`. Historical task records may keep + their original paths. +5. Restart OpenCode from the project root. +6. Query the authenticated live agent endpoint and confirm `builder`, + `reviewer`, and `tester` are present. Inspect the ordered permission rules; + the last matching edit rule must preserve each role's intended write scope. +7. Run `scripts/verify-models.sh`, then the project preflight. + +The migration deliberately does not convert legacy `permission:` frontmatter +to another schema. Directory discovery and permission-schema conversion are +separate changes; combining them makes a missing role indistinguishable from a +silently dropped ruleset. diff --git a/skills/dev-team-generator/SKILL.md b/skills/dev-team-generator/SKILL.md index a063ce6..bf4a20c 100644 --- a/skills/dev-team-generator/SKILL.md +++ b/skills/dev-team-generator/SKILL.md @@ -80,7 +80,7 @@ question tool if one is available) before writing anything: see Step 3. For any tool that isn't already a worked example in this folder's `reference/role-examples.md` **and** isn't already scaffolded in this - target project (check `.claude/agents/`, `.opencode/agent/`, or + target project (check `.claude/agents/`, `.opencode/agents/`, or whatever that tool's own convention is), go to **Step 2** before promising it works. 4. **Cross-vendor independence for review, specifically.** The reviewer diff --git a/skills/dev-team-generator/reference/flow-example.md b/skills/dev-team-generator/reference/flow-example.md index b065186..2b750ff 100644 --- a/skills/dev-team-generator/reference/flow-example.md +++ b/skills/dev-team-generator/reference/flow-example.md @@ -42,7 +42,14 @@ exceeded budget as an escalation, never one more lap. server/runtime is actually reachable — fail fast with a clear message rather than letting a dispatch silently fall back to a slow cold-start path or hang. -3. Confirm the working tree is clean enough to produce a meaningful diff. +3. Resolve every configured worker model against the live runtime before the + first dispatch. Configuration files agreeing with each other does not prove + that a provider actually offers those model ids. +4. After a runtime restart, upgrade, role-path migration, or permission edit, + inspect the live role/capability ruleset. Confirm every required role is + loaded and that last-match-wins permission ordering preserves the intended + write boundary; readable configuration alone is not enforcement evidence. +5. Confirm the working tree is clean enough to produce a meaningful diff. ## Token/context discipline diff --git a/skills/dev-team-generator/reference/lessons-learned.md b/skills/dev-team-generator/reference/lessons-learned.md index 70abd84..48312f3 100644 --- a/skills/dev-team-generator/reference/lessons-learned.md +++ b/skills/dev-team-generator/reference/lessons-learned.md @@ -600,3 +600,18 @@ it needs, rather than disabling protection. Classify permission-sensitive updates by behavior across every tool's schema, including hooks and TOML, not only by the first tool's YAML field names. Configuration/skill presence is not proof of discovery; fake-CLI tests are not live model workflow evidence. + +## 32. A compatible legacy config path is not a reliable discovery contract + +A runtime may document backward-compatible discovery of an old role directory +while its live agent endpoint loads only built-ins from that project. Treat the +current canonical project path as the contract for generated scaffolds, and +migrate old paths explicitly at a task boundary instead of depending on a +compatibility promise. Refuse a plain bootstrap that would create a second role +tree beside an unmigrated first one. + +Likewise, configuration agreement is not runtime readiness. Before dispatch, +resolve every configured model id against the live provider and inspect the +live role list after a restart, upgrade, path migration, or permission change. +A model name written consistently in three files can still be unavailable, and +a permission file that exists on disk can still correspond to no loaded role. diff --git a/skills/dev-team-generator/reference/tool-onboarding.md b/skills/dev-team-generator/reference/tool-onboarding.md index 1f086f7..2a94f82 100644 --- a/skills/dev-team-generator/reference/tool-onboarding.md +++ b/skills/dev-team-generator/reference/tool-onboarding.md @@ -12,7 +12,7 @@ needed, not maintaining an ever-growing library of pre-built adapters. Find, from the tool's own documentation or by experimenting with it directly: - Where it looks for custom agent/rule definitions — a per-repo - directory convention (like `.claude/agents/` or `.opencode/agent/`), + directory convention (like `.claude/agents/` or `.opencode/agents/`), a global (per-machine) config location, or no custom-agent concept at all. - What frontmatter or config keys it actually reads: model selection, a diff --git a/skills/toolkit-init/SKILL.md b/skills/toolkit-init/SKILL.md index 4a3519e..07b7664 100644 --- a/skills/toolkit-init/SKILL.md +++ b/skills/toolkit-init/SKILL.md @@ -47,11 +47,11 @@ Ask these up front rather than guessing — they shape every generated file: --test-dir "" ``` -It writes `.claude/agents/`, `.opencode/agent/`, `.claude/commands/{feature.md, +It writes `.claude/agents/`, `.opencode/agents/`, `.claude/commands/{feature.md, toolkit-update.md}`, Codex project instructions/skills/planner under `AGENTS.md`, `.agents/skills/`, and `.codex/agents/`, `.pipeline/{TEMPLATE.md,.toolkit-version}`, and -`scripts/{oc.sh,team.sh,verify-state.sh,verify-spec.sh,promote-findings.sh}` +`scripts/{oc.sh,team.sh,verify-state.sh,verify-spec.sh,verify-models.sh,promote-findings.sh}` into the target. It does **not** overwrite a file that already exists — it prints what it skipped so you can diff and merge by hand. diff --git a/templates/claude/commands/feature.md.tmpl b/templates/claude/commands/feature.md.tmpl index 5c7082b..d12eed2 100644 --- a/templates/claude/commands/feature.md.tmpl +++ b/templates/claude/commands/feature.md.tmpl @@ -58,7 +58,7 @@ does not know it. 1. **First run only:** if `.pipeline/.needs-customization` exists, this project was just scaffolded — the role files under `.claude/agents/` and - `.opencode/agent/` still carry generic pitfalls/hard-rules text, not this + `.opencode/agents/` still carry generic pitfalls/hard-rules text, not this codebase's actual ones. Ask the user: read the real source tree and this project's own CLAUDE.md/AGENTS.md, then fill those sections in with real specifics, now, before starting the requested feature? If yes, do that @@ -73,7 +73,22 @@ does not know it. `curl -sS -m 5 "http://localhost:$(cat .pipeline/.oc-port 2>/dev/null || echo 4096)"`. If not up, tell the user to run `scripts/team.sh` (or `opencode serve`) and stop. -4. Confirm the working tree is clean enough to produce a meaningful diff. +4. **Confirm every OpenCode-dispatched model resolves on the live server:** + `scripts/verify-models.sh`. It reads model ids from + `.pipeline/.toolkit-version`, queries the authenticated `/api/model` + endpoint, and skips models that run outside OpenCode. If it fails, bring + the invalid id and the provider's real model list to the user and stop — + do not silently switch model or vendor. +5. **After any OpenCode server restart, upgrade, role-path migration, or edit + to a role permission block, verify the live roles before dispatching.** + The Markdown is not the running ruleset: + `OPENCODE_PASSWORD="$(cat .pipeline/.oc-password)" opencode api GET /api/agent --server http://localhost:$(cat .pipeline/.oc-port) --param directory=$PWD`. + Write the response to a file rather than a pipe because large API output + may truncate. Confirm `builder`, `reviewer`, and `tester` are present. The + last matching `edit` rule for the tester must allow `.pipeline/**`, and + the builder must retain its intended source-write rule. Retry the endpoint + once after a cold start before concluding a role is missing. +6. Confirm the working tree is clean enough to produce a meaningful diff. ## Token discipline @@ -86,7 +101,7 @@ calls. background; no manual step is needed. - Implementer, reviewer, and tester write their own outputs directly into `.pipeline/T-.md` / `.pipeline/T-.diff` (see each role's write scope - in `.opencode/agent/*.md`). The lead's job is to **verify placement and + in `.opencode/agents/*.md`). The lead's job is to **verify placement and content**, not to retype or re-paste what a role already wrote to disk. - Don't read a role's raw `--raw-out .jsonl` event stream as a matter of course — that inlines every file the agent read and is the single diff --git a/templates/opencode/agent/builder.md.tmpl b/templates/opencode/agents/builder.md.tmpl similarity index 100% rename from templates/opencode/agent/builder.md.tmpl rename to templates/opencode/agents/builder.md.tmpl diff --git a/templates/opencode/agent/reviewer.md.tmpl b/templates/opencode/agents/reviewer.md.tmpl similarity index 100% rename from templates/opencode/agent/reviewer.md.tmpl rename to templates/opencode/agents/reviewer.md.tmpl diff --git a/templates/opencode/agent/tester.md.tmpl b/templates/opencode/agents/tester.md.tmpl similarity index 100% rename from templates/opencode/agent/tester.md.tmpl rename to templates/opencode/agents/tester.md.tmpl diff --git a/templates/scripts/oc.sh.tmpl b/templates/scripts/oc.sh.tmpl index 8b3e9da..da84185 100755 --- a/templates/scripts/oc.sh.tmpl +++ b/templates/scripts/oc.sh.tmpl @@ -531,7 +531,7 @@ if [ -n "$KILL_REASON" ] || [ "$STATUS" -eq 124 ]; then WHY="the session's last message did not change for ${IDLE_SECS}s while a turn was still open" HINT="That is what a wedged run looks like — almost always a permission prompt with no TTY to answer it: - - Widen .opencode/agent/$AGENT.md's shell permissions to cover the command + - Widen .opencode/agents/$AGENT.md's shell permissions to cover the command that stopped it, and set 'question: deny'. - --auto is a reasonable workaround here (see this script's own --auto comment above): re-verified live, it correctly auto-approves only diff --git a/templates/scripts/verify-models.sh.tmpl b/templates/scripts/verify-models.sh.tmpl new file mode 100755 index 0000000..b943043 --- /dev/null +++ b/templates/scripts/verify-models.sh.tmpl @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +# +# Deterministic check that every model this pipeline dispatches actually +# exists on the OpenCode server — before a task is planned, not after a +# dispatch dies on it. +# +# This exists because of T-005: `builder_model` was set to `openai/sol`, +# which is a model *family* name, not an id. The manifest and feature.md +# agreed with each other perfectly, so nothing looked wrong — but the +# server offers `gpt-5.6-sol`, and the builder dispatch failed instantly +# with an opaque `UnknownError: Unexpected server error`. Agreement between +# config files is not the same fact as the values resolving, and only one +# of those two was ever being checked. +# +# Reads the ids from `.pipeline/.toolkit-version` rather than taking them as +# arguments, so the thing being validated is the thing the pipeline will +# actually use. `claude/*` entries are skipped: Claude roles dispatch as +# subagents, never through oc.sh, so the OpenCode server has no opinion +# about them. +# +# Usage: scripts/verify-models.sh +# Exit 0 = every oc.sh-dispatched id resolves, prints one OK line. +# Exit 1 = the invalid ids and what the provider actually offers. + +set -eu + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$REPO_ROOT" + +MANIFEST=".pipeline/.toolkit-version" +[ -f "$MANIFEST" ] || { echo "verify-models.sh: no $MANIFEST" >&2; exit 1; } + +PORT="$(cat .pipeline/.oc-port 2>/dev/null || echo 4096)" +SERVER="${OC_SERVER:-http://localhost:$PORT}" + +# opencode v2 auth-gates the whole API and moved it under /api/*. Plain curl +# gets 401 here, so go through `opencode api` with the password scripts/team.sh +# wrote (same approach as scripts/oc.sh). +if [ -n "${OC_PASSWORD:-}" ]; then + OPENCODE_PASSWORD="$OC_PASSWORD" +elif [ -f .pipeline/.oc-password ]; then + OPENCODE_PASSWORD="$(cat .pipeline/.oc-password)" +fi +export OPENCODE_PASSWORD="${OPENCODE_PASSWORD:-}" + +# Right after an `opencode serve` (re)start the provider list loads lazily, so +# the first call can say a provider "is not on this server" when it simply has +# not loaded yet (seen on T-028). Retry that one case, up to 3 attempts 5s +# apart; a wrong model id (provider present, model absent) still fails at once. +attempt=1 +while :; do +# Write to a file, not a pipe: the model list is ~430 entries (>256 KB) and +# `opencode api` truncates its output mid-JSON when stdout is a pipe. +MODELS_JSON="$(mktemp)" +trap 'rm -f "$MODELS_JSON"' EXIT +opencode api GET /api/model --server "$SERVER" --param directory="$REPO_ROOT" >"$MODELS_JSON" 2>/dev/null || { + echo "verify-models.sh: cannot reach OpenCode server at $SERVER (or it rejected the password in .pipeline/.oc-password) — run scripts/team.sh first" >&2 + exit 1 +} + +set +e +MANIFEST="$MANIFEST" MODELS_JSON="$MODELS_JSON" python3 -c ' +import json, os, sys, re + +try: + doc = json.load(open(os.environ["MODELS_JSON"], encoding="utf-8")) +except Exception as e: + print(f"verify-models.sh: server returned unparseable JSON: {e}", file=sys.stderr) + sys.exit(1) + +# v2: GET /api/model returns {"data": [{"providerID", "modelID", ...}, ...]}. +have = {} +for m in doc.get("data", []): + have.setdefault(m.get("providerID"), set()).add(m.get("modelID")) + +wanted = [] +for line in open(os.environ["MANIFEST"], encoding="utf-8"): + m = re.match(r"^(\w*model\w*):\s*(\S+)", line.strip()) + if m: + wanted.append((m.group(1), m.group(2))) + +bad = [] +checked = 0 +for key, val in wanted: + if "/" not in val: + continue # bare name, e.g. claude_model: sonnet — not an oc.sh id + prov, model = val.split("/", 1) + if prov == "claude": + continue # Claude roles run as subagents, never through oc.sh + checked += 1 + if model not in have.get(prov, set()): + bad.append((key, val, prov)) + +if bad: + cold = any(not have.get(prov) for _, _, prov in bad) + for key, val, prov in bad: + offers = sorted(have.get(prov, set())) + model = val.split("/", 1)[1] + if not offers: + print(f"verify-models.sh: {key} = {val} — provider {prov!r} is not on this server", file=sys.stderr) + else: + offered = ", ".join(offers) + print(f"verify-models.sh: {key} = {val} — {prov} does not offer {model!r}; it offers: {offered}", file=sys.stderr) + sys.exit(3 if cold else 1) + +print(f"verify-models.sh: OK ({checked} oc.sh model id(s) resolve on the server)") +' +rc=$? +set -e +[ "$rc" -ne 3 ] && exit "$rc" +if [ "$attempt" -ge 3 ]; then + echo "verify-models.sh: a configured provider is still not on the server after $attempt attempts — if you just restarted it, check \`opencode api GET /api/provider\`; otherwise the provider is not configured" >&2 + exit 1 +fi +attempt=$((attempt + 1)) +sleep 5 +done diff --git a/test/codex.sh b/test/codex.sh index 1485365..4120ce1 100644 --- a/test/codex.sh +++ b/test/codex.sh @@ -139,6 +139,21 @@ cmp -s "$TMP/legacy-stamp" "$TMP/.agents/.toolkit-version" || fail "triage chang [ ! -f "$TMP/.pipeline/TEMPLATE.md" ] || fail "triage wrote a second runtime" ok "legacy baseline is readable, triage is read-only, and unmigrated bootstrap is refused" +# The OpenCode V2 role path migration must not create duplicate role trees. +LEGACY_AGENT="$TMP/legacy-agent" +mkdir "$LEGACY_AGENT" +bash "$ROOT/bin/init.sh" --target "$LEGACY_AGENT" --project-name legacy-agent > "$TMP/legacy-agent.log" 2>&1 +mv "$LEGACY_AGENT/.opencode/agents" "$LEGACY_AGENT/.opencode/agent" +if bash "$ROOT/bin/init.sh" --target "$LEGACY_AGENT" > "$TMP/legacy-agent.log" 2>&1; then fail "bootstrap accepted the legacy singular OpenCode role layout"; fi +grep -q '03-opencode-v2-agents-directory' "$TMP/legacy-agent.log" || fail "OpenCode role-path migration not identified" +mv "$LEGACY_AGENT/.pipeline/.toolkit-version" "$LEGACY_AGENT/stamp.hold" +rc=0 +bash "$ROOT/bin/init.sh" --update --target "$LEGACY_AGENT" > "$TMP/legacy-agent.log" 2>&1 || rc=$? +[ "$rc" = 1 ] || fail "legacy OpenCode role layout triage did not report drift" +grep -q 'BUILDER_MODEL: opencode-go/glm-5.3-flash' "$TMP/legacy-agent.log" || fail "stamp-less recovery did not read the legacy OpenCode role path" +[ ! -d "$LEGACY_AGENT/.opencode/agents" ] || fail "read-only triage created a second OpenCode role tree" +ok "legacy OpenCode role layout is readable for triage and refused for bootstrap" + # Defaults preserve local model choices; existing instructions/hooks/skills survive. DEFAULT="$TMP/default" mkdir "$DEFAULT" diff --git a/test/invariants.sh b/test/invariants.sh index 855fa57..b5529fc 100755 --- a/test/invariants.sh +++ b/test/invariants.sh @@ -35,9 +35,9 @@ STATE="templates/agents-state/TEMPLATE.md.tmpl" STATE_EX="skills/dev-team-generator/reference/state-file-example.md" PLANNER="templates/claude/agents/planner.md.tmpl" SENIOR="templates/claude/agents/senior-dev.md.tmpl" -BUILDER="templates/opencode/agent/builder.md.tmpl" -REVIEWER="templates/opencode/agent/reviewer.md.tmpl" -TESTER="templates/opencode/agent/tester.md.tmpl" +BUILDER="templates/opencode/agents/builder.md.tmpl" +REVIEWER="templates/opencode/agents/reviewer.md.tmpl" +TESTER="templates/opencode/agents/tester.md.tmpl" LESSONS="skills/dev-team-generator/reference/lessons-learned.md" CODEX_AGENTS="templates/codex/AGENTS.md.tmpl" CODEX_FEATURE="templates/codex/skills/feature/SKILL.md.tmpl" @@ -107,6 +107,14 @@ rule "check the spec with a script before approval" \ 'verify-spec|check the spec with a script|spec.{0,40}structural check' \ "$FEATURE" "$SYSTEM" "$FLOW" "$PLANNER" +rule "preflight resolves configured models against the live runtime" \ + 'model.{0,100}live (server|runtime)|model id against the actual provider' \ + "$FEATURE" "$SYSTEM" "$FLOW" + +rule "preflight verifies the live role permission ruleset" \ + 'live role|live.*capability ruleset' \ + "$FEATURE" "$SYSTEM" "$FLOW" + # --- Codex lead adapter ----------------------------------------------------- rule "Codex feature skill delegates to the canonical lead flow" \ '\.claude/commands/feature\.md' \ diff --git a/test/smoke.sh b/test/smoke.sh index cae7ae3..fe709cb 100755 --- a/test/smoke.sh +++ b/test/smoke.sh @@ -65,6 +65,15 @@ for codex_file in \ "$TMP/.agents/skills/toolkit-update/SKILL.md"; do [ -f "$codex_file" ] || fail "Codex scaffold file missing: $codex_file" done +for opencode_file in \ + "$TMP/.opencode/agents/builder.md" \ + "$TMP/.opencode/agents/reviewer.md" \ + "$TMP/.opencode/agents/tester.md"; do + [ -f "$opencode_file" ] || fail "OpenCode V2 role file missing: $opencode_file" +done +[ ! -d "$TMP/.opencode/agent" ] || fail "fresh scaffold wrote the legacy singular OpenCode role directory" +[ -x "$TMP/scripts/verify-models.sh" ] || fail "verify-models.sh missing or not executable" +ok "OpenCode V2 roles and live model verifier were scaffolded" grep -q '\.claude/commands/feature.md' "$TMP/.agents/skills/feature/SKILL.md" \ || fail "Codex feature skill does not point at the canonical lead flow" grep -q 'Not populated by agent-toolkit' "$TMP/AGENTS.md" \ From 92816bf6e3a6eb8f5125ef3a92675e5a37f4bd71 Mon Sep 17 00:00:00 2001 From: abdoharb Date: Mon, 5 Oct 2026 16:35:04 +0300 Subject: [PATCH 3/8] fix: hide transient model cold-start errors --- templates/scripts/verify-models.sh.tmpl | 9 ++++++-- test/smoke.sh | 28 +++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/templates/scripts/verify-models.sh.tmpl b/templates/scripts/verify-models.sh.tmpl index b943043..3bdfc11 100755 --- a/templates/scripts/verify-models.sh.tmpl +++ b/templates/scripts/verify-models.sh.tmpl @@ -32,6 +32,7 @@ MANIFEST=".pipeline/.toolkit-version" PORT="$(cat .pipeline/.oc-port 2>/dev/null || echo 4096)" SERVER="${OC_SERVER:-http://localhost:$PORT}" +RETRY_DELAY="${OC_MODEL_RETRY_DELAY:-5}" # opencode v2 auth-gates the whole API and moved it under /api/*. Plain curl # gets 401 here, so go through `opencode api` with the password scripts/team.sh @@ -59,7 +60,9 @@ opencode api GET /api/model --server "$SERVER" --param directory="$REPO_ROOT" >" } set +e -MANIFEST="$MANIFEST" MODELS_JSON="$MODELS_JSON" python3 -c ' +final_attempt=0 +[ "$attempt" -ge 3 ] && final_attempt=1 +MANIFEST="$MANIFEST" MODELS_JSON="$MODELS_JSON" FINAL_ATTEMPT="$final_attempt" python3 -c ' import json, os, sys, re try: @@ -93,6 +96,8 @@ for key, val in wanted: if bad: cold = any(not have.get(prov) for _, _, prov in bad) + if cold and os.environ.get("FINAL_ATTEMPT") != "1": + sys.exit(3) for key, val, prov in bad: offers = sorted(have.get(prov, set())) model = val.split("/", 1)[1] @@ -113,5 +118,5 @@ if [ "$attempt" -ge 3 ]; then exit 1 fi attempt=$((attempt + 1)) -sleep 5 +sleep "$RETRY_DELAY" done diff --git a/test/smoke.sh b/test/smoke.sh index fe709cb..59be251 100755 --- a/test/smoke.sh +++ b/test/smoke.sh @@ -74,6 +74,34 @@ done [ ! -d "$TMP/.opencode/agent" ] || fail "fresh scaffold wrote the legacy singular OpenCode role directory" [ -x "$TMP/scripts/verify-models.sh" ] || fail "verify-models.sh missing or not executable" ok "OpenCode V2 roles and live model verifier were scaffolded" + +MODEL_BIN="$TMP/modelbin" +mkdir "$MODEL_BIN" +cat > "$MODEL_BIN/opencode" <<'EOF' +#!/bin/sh +count=0 +[ ! -f "$MODEL_COUNT_FILE" ] || count="$(cat "$MODEL_COUNT_FILE")" +count=$((count + 1)) +printf '%s\n' "$count" > "$MODEL_COUNT_FILE" +if [ "$count" = 1 ]; then + printf '%s\n' '{"data":[]}' +else + printf '%s\n' '{"data":[{"providerID":"a","modelID":"b"},{"providerID":"a","modelID":"c"},{"providerID":"d","modelID":"e"}]}' +fi +EOF +chmod +x "$MODEL_BIN/opencode" +( + cd "$TMP" + MODEL_COUNT_FILE="$TMP/model-count" OC_MODEL_RETRY_DELAY=0 \ + PATH="$MODEL_BIN:/usr/bin:/bin" scripts/verify-models.sh > "$TMP/model-check.log" 2>&1 +) || fail "verify-models.sh did not recover from a cold provider list" +grep -q 'OK (4 oc.sh model id(s) resolve' "$TMP/model-check.log" \ + || fail "verify-models.sh did not report the resolved configured models" +! grep -q 'not on this server' "$TMP/model-check.log" \ + || fail "verify-models.sh leaked a transient cold-start error before succeeding" +[ "$(cat "$TMP/model-count")" = 2 ] || fail "verify-models.sh did not retry the cold provider list exactly once" +ok "verify-models retries cold providers without printing false failure diagnostics" + grep -q '\.claude/commands/feature.md' "$TMP/.agents/skills/feature/SKILL.md" \ || fail "Codex feature skill does not point at the canonical lead flow" grep -q 'Not populated by agent-toolkit' "$TMP/AGENTS.md" \ From 2845d1bc3d77f85456647ea96cdb859a11f02196 Mon Sep 17 00:00:00 2001 From: abdoHarb93 Date: Tue, 6 Oct 2026 16:46:41 +0300 Subject: [PATCH 4/8] ci: make ShellCheck pass on the merged tree CI's ShellCheck step failed (also on codex-lead-parity before the merge) and stopped before the templates loop ran. Fix every finding: - SC1087 in codex-preflight.sh.tmpl: brace the loop variable in the regex. - SC2207 in team-completion.bash.tmpl: intentional word-splitting; mapfile is unavailable in macOS bash 3.2, so disable for the function with a reason. - SC2015 in smoke.sh and codex.sh: split A && B || C into separate checks. - SC2016 in init.sh and tests: literal backticks in single quotes are intentional; annotate each site. Co-Authored-By: Claude Sonnet 5.5 --- bin/init.sh | 5 +++++ templates/scripts/codex-preflight.sh.tmpl | 2 +- templates/scripts/team-completion.bash.tmpl | 3 +++ test/codex-sandbox.sh | 1 + test/codex.sh | 3 ++- test/invariants.sh | 1 + test/smoke.sh | 4 +++- 7 files changed, 16 insertions(+), 3 deletions(-) diff --git a/bin/init.sh b/bin/init.sh index 6964e8f..113d443 100755 --- a/bin/init.sh +++ b/bin/init.sh @@ -148,6 +148,7 @@ parse_opencode_major() { check_opencode_version() { if ! command -v opencode >/dev/null 2>&1; then printf '\ninit.sh: NOTE — opencode CLI not found on PATH.\n' >&2 + # shellcheck disable=SC2016 # literal backticks/quotes are intentional printf 'init.sh: scripts/oc.sh and scripts/team.sh need opencode v2 installed to run the pipeline (not to scaffold it). Install it before your first /feature or $feature run.\n\n' >&2 return 0 fi @@ -162,7 +163,9 @@ check_opencode_version() { case "$major" in 1) printf '\ninit.sh: WARNING — installed opencode is v1 (%s); these templates assume opencode v2.\n' "$ver_str" >&2 + # shellcheck disable=SC2016 # literal backticks/quotes are intentional printf 'init.sh: v1 lacks v2'"'"'s password-gated `serve` (auth will just fail), the `shell` permission action name (v1 used `bash`, so deny/ask rules silently will not match), the `opencode api` CLI subcommand scripts/oc.sh polls with, and the `--server` flag scripts/oc.sh passes to `opencode run` (v1 used `--attach`/`--dir`, both removed in v2). Running this toolkit'"'"'s generated scripts against opencode v1 as-is will fail or silently under-enforce permissions.\n' >&2 + # shellcheck disable=SC2016 # literal backticks/quotes are intentional printf 'init.sh: upgrade opencode first — `opencode upgrade` (or however you installed it) — then re-run.\n\n' >&2 ;; 2) @@ -288,6 +291,7 @@ recover_from_target() { # $1 = key REPLY="$(sed -n 's/^model: //p' "$agent_dir/reviewer.md" | head -1)" ;; reviewer_fallback_model) # feature.md: "switch to `__REVIEWER_FALLBACK_MODEL__`" + # shellcheck disable=SC2016 # literal backticks/quotes are intentional REPLY="$(sed -n 's/.*switch to `\([^`]*\)`.*/\1/p' \ "$TARGET/.claude/commands/feature.md" | head -1)" ;; tester_model) @@ -471,6 +475,7 @@ if [ "$UPDATE" -eq 1 ]; then check_pair "$TEMPLATES/scripts/promote-findings.sh.tmpl" "$TARGET/scripts/promote-findings.sh" CUR_SHA="$(git -C "$TOOLKIT_ROOT" rev-parse --short HEAD 2>/dev/null || echo unknown)" + # shellcheck disable=SC2016 # literal backticks/quotes are intentional printf 'init.sh: toolkit is at %s; checking %s rendered file(s)%s\n' \ "$CUR_SHA" "$TOTAL" "${ONLY:+ (filtered by --only '$ONLY'; full set is $RENDER_TOTAL)}" if [ -f "$STAMP_SOURCE" ]; then diff --git a/templates/scripts/codex-preflight.sh.tmpl b/templates/scripts/codex-preflight.sh.tmpl index 479e698..8705324 100644 --- a/templates/scripts/codex-preflight.sh.tmpl +++ b/templates/scripts/codex-preflight.sh.tmpl @@ -16,7 +16,7 @@ if [ -n "$CODEX_NUMBER" ] && [ "$(printf '0.160.0\n%s\n' "$CODEX_NUMBER" | sort fi FEATURES="$(codex features list)" || die "cannot inspect Codex runtime capabilities" for feature in hooks multi_agent; do - printf '%s\n' "$FEATURES" | grep -qE "^$feature[[:space:]].*[[:space:]]true$" \ + printf '%s\n' "$FEATURES" | grep -qE "^${feature}[[:space:]].*[[:space:]]true$" \ || die "Codex $feature capability is unavailable or disabled; review your runtime configuration before starting" done codex resume --help | grep -q -- '--no-daemon' \ diff --git a/templates/scripts/team-completion.bash.tmpl b/templates/scripts/team-completion.bash.tmpl index e774e2d..d5882bf 100644 --- a/templates/scripts/team-completion.bash.tmpl +++ b/templates/scripts/team-completion.bash.tmpl @@ -4,6 +4,9 @@ # zsh users: `autoload -U +X bashcompinit && bashcompinit` before sourcing # this, zsh's own completion system doesn't load bash completions directly. +# Word-splitting the compgen output into COMPREPLY is the intended behavior, and +# mapfile is unavailable in the bash 3.2 that macOS ships. +# shellcheck disable=SC2207 _team_sh_complete() { local cur prev cur="${COMP_WORDS[COMP_CWORD]}" diff --git a/test/codex-sandbox.sh b/test/codex-sandbox.sh index e8460a9..3ccfb0e 100644 --- a/test/codex-sandbox.sh +++ b/test/codex-sandbox.sh @@ -5,6 +5,7 @@ set -eu TMP="$(mktemp -d "${TMPDIR:-/tmp}/toolkit-sandbox.XXXXXX")" trap 'rm -rf "$TMP"' EXIT mkdir -p "$TMP/.pipeline" "$TMP/.agents/skills" "$TMP/.codex" "$TMP/.git" +# shellcheck disable=SC2016 # literal backticks/quotes are intentional codex sandbox --permission-profile :workspace -C "$TMP" -- /bin/bash -c ' set -eu printf probe > .pipeline/probe diff --git a/test/codex.sh b/test/codex.sh index 4120ce1..d11b546 100644 --- a/test/codex.sh +++ b/test/codex.sh @@ -67,7 +67,8 @@ grep -qx 'model_reasoning_effort="high"' "$FAKE_ARGS" || fail "reasoning not pas grep -qx -- --no-daemon "$FAKE_ARGS" || fail "hooks could inherit a different daemon environment" export FAKE_SESSION_ID=unrelated-latest bash "$LEAD" first > "$TMP/lead.log" 2>&1 || fail "pinned resume failed" -grep -qx resume "$FAKE_ARGS" && grep -qx thread-one "$FAKE_ARGS" || fail "did not resume the pinned thread" +grep -qx resume "$FAKE_ARGS" || fail "did not resume the pinned thread" +grep -qx thread-one "$FAKE_ARGS" || fail "did not resume the pinned thread" ! grep -qx -- --last "$FAKE_ARGS" || fail "launcher used latest-thread lookup" ok "resume pins the exact thread despite unrelated repository conversations" diff --git a/test/invariants.sh b/test/invariants.sh index 4ab0bd7..b181441 100755 --- a/test/invariants.sh +++ b/test/invariants.sh @@ -125,6 +125,7 @@ rule "Codex feature skill delegates to the canonical lead flow" \ '\.claude/commands/feature\.md' \ "$CODEX_FEATURE" +# shellcheck disable=SC2016 # literal backticks/quotes are intentional rule "Codex project instructions route feature work through the skill" \ 'invoke the `feature` skill' \ "$CODEX_AGENTS" diff --git a/test/smoke.sh b/test/smoke.sh index d4635c3..2e672b0 100755 --- a/test/smoke.sh +++ b/test/smoke.sh @@ -111,6 +111,7 @@ grep -q '\.claude/commands/feature.md' "$TMP/.agents/skills/feature/SKILL.md" \ || fail "Codex feature skill does not point at the canonical lead flow" grep -q 'Not populated by agent-toolkit' "$TMP/AGENTS.md" \ || fail "generated AGENTS.md masks its missing project-specific guidance" +# shellcheck disable=SC2016 # literal backticks/quotes are intentional grep -q 'Codex `planner` subagent' "$TMP/.agents/skills/feature/SKILL.md" \ || fail "Codex feature skill does not select the Codex planner" grep -q 'sandbox_mode = "workspace-write"' "$TMP/.codex/agents/planner.toml" \ @@ -551,7 +552,8 @@ grep -q 'appended to docs/GOTCHAS.md' <<< "$out" || fail "legit finding not prom grep -q 'escaping the repo root' <<< "$out" || fail "../.. path was not refused" grep -q 'unsafe/invalid doc path' <<< "$out" || fail "absolute path was not refused" [ -f "$TMP/docs/GOTCHAS.md" ] || fail "docs/GOTCHAS.md not created" -[ ! -e "$TMP/../escaped.md" ] && [ ! -f "/tmp/escaped.md" ] || fail "a traversal write landed somewhere" +[ ! -e "$TMP/../escaped.md" ] || fail "a traversal write landed somewhere" +[ ! -f "/tmp/escaped.md" ] || fail "a traversal write landed somewhere" [ ! -f "$(cd "$TMP/.." && pwd)/escaped.md" ] || fail "traversal escaped above target" out="$("$PF" T-02 2>&1)" grep -q 'already present, skipped' <<< "$out" || fail "promotion not idempotent" From 78fbf4de65787956f5a984216c6282b30286c6fa Mon Sep 17 00:00:00 2001 From: abdoharb Date: Thu, 8 Oct 2026 18:10:36 +0300 Subject: [PATCH 5/8] feat: Codex lead parity with the Claude lead Give a Codex lead the mechanics the Claude harness supplies natively, and write down the lead lessons that only lived in a Claude session. - scripts/bg-dispatch.sh (start/wait): detached dispatch plus a bounded, repeatable wait, replacing the Claude-only Monitor watch. - scripts/claude-review.sh: headless `claude -p --agent reviewer` with a read-only tool list, so a non-Claude lead can dispatch a claude/* reviewer. - scripts/oc.sh: --status (v2 /api/session/active) and --interrupt (v2 /api/session//interrupt). - promote-findings.sh skips directory tags instead of aborting the run. - codex-preflight.sh checks the new wrappers and the Claude reviewer deps. - feature flow: status check, merge record, mid-task model switch, concurrent-writer claims, exact Pass heading, tester-cannot-be-spawned. - Codex feature skill: matching adaptations. - init.sh renders/updates the new scripts and ignores dispatch traces. - smoke: 3 new checks (36 total). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 26 ++++ bin/init.sh | 13 ++ docs/CODEX.md | 19 +++ templates/claude/commands/feature.md.tmpl | 109 +++++++++++++- templates/codex/skills/feature/SKILL.md.tmpl | 61 ++++++++ templates/scripts/bg-dispatch.sh.tmpl | 107 ++++++++++++++ templates/scripts/claude-review.sh.tmpl | 95 ++++++++++++ templates/scripts/codex-preflight.sh.tmpl | 10 +- templates/scripts/oc.sh.tmpl | 144 ++++++++++++++++--- templates/scripts/promote-findings.sh.tmpl | 12 ++ test/smoke.sh | 62 ++++++++ 11 files changed, 638 insertions(+), 20 deletions(-) create mode 100755 templates/scripts/bg-dispatch.sh.tmpl create mode 100755 templates/scripts/claude-review.sh.tmpl diff --git a/CHANGELOG.md b/CHANGELOG.md index dd12580..d6a30d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,32 @@ script, flag, or role rule; **PATCH** = prose and docs. ## Unreleased +- `[process]` Codex-lead parity with the Claude lead. New + `scripts/bg-dispatch.sh` (`start`/`wait`) is the detached-dispatch watch for + any lead — a Codex lead has no Monitor, so it loops on `wait` instead of + losing dispatches to its own exec timeout. New `scripts/claude-review.sh` + runs the project's Claude `reviewer` agent headlessly (read-only tool list, + Edit/Write refused) so a non-Claude lead can dispatch a `claude/*` reviewer. + `scripts/oc.sh` gains `--status` (v2 `/api/session/active`) and + `--interrupt` (v2 `/api/session//interrupt`); the feature flow now uses + them instead of raw `opencode api` calls. `codex-preflight.sh` checks the new + wrappers and, for a `claude/*` reviewer, the Claude CLI and agent file. +- `[process]` Canonical feature flow: a *Where the team stopped* status check + (board + handoffs + `gh pr list` reconciliation + stale-`main` check); the + merge record after a merge; the mid-task model/implementer switch procedure + (interrupt, ask keep-vs-discard, update manifest and role file, + `verify-models.sh`, fresh session); verifying a role's "concurrent writer" + claim before acting on it; the exact `### Pass N — — verdict: ` + heading `verify-state.sh` keys off; what to do when the lead cannot spawn + the configured tester. The Codex feature skill gains the matching + adaptations (no Monitor, no structured question tool, headless Claude + review, per-task branches, sandboxed `oc.sh`). +- `[safety]` `promote-findings.sh` skips a *Findings for docs* tag that names + a directory instead of aborting the run and silently dropping every later + finding. +- `[process]` `init.sh` ignores `.pipeline/T-*.{out,err,pid,brief}` dispatch + traces in a git target. + - `[contract]` Merged upstream v0.10.0 (OpenCode lead, standalone dashboard, Herdr plugin) onto this line without changing the Codex/`.pipeline/` contract: the new OpenCode `leader`/`planner`/commands, `scripts/dashboard`, diff --git a/bin/init.sh b/bin/init.sh index 113d443..9dc4c9e 100755 --- a/bin/init.sh +++ b/bin/init.sh @@ -473,6 +473,8 @@ if [ "$UPDATE" -eq 1 ]; then check_pair "$TEMPLATES/scripts/verify-spec.sh.tmpl" "$TARGET/scripts/verify-spec.sh" check_pair "$TEMPLATES/scripts/verify-models.sh.tmpl" "$TARGET/scripts/verify-models.sh" check_pair "$TEMPLATES/scripts/promote-findings.sh.tmpl" "$TARGET/scripts/promote-findings.sh" + check_pair "$TEMPLATES/scripts/bg-dispatch.sh.tmpl" "$TARGET/scripts/bg-dispatch.sh" + check_pair "$TEMPLATES/scripts/claude-review.sh.tmpl" "$TARGET/scripts/claude-review.sh" CUR_SHA="$(git -C "$TOOLKIT_ROOT" rev-parse --short HEAD 2>/dev/null || echo unknown)" # shellcheck disable=SC2016 # literal backticks/quotes are intentional @@ -548,10 +550,13 @@ render "$TEMPLATES/scripts/verify-state.sh.tmpl" "$TARGET/scripts/verify-sta render "$TEMPLATES/scripts/verify-spec.sh.tmpl" "$TARGET/scripts/verify-spec.sh" render "$TEMPLATES/scripts/verify-models.sh.tmpl" "$TARGET/scripts/verify-models.sh" render "$TEMPLATES/scripts/promote-findings.sh.tmpl" "$TARGET/scripts/promote-findings.sh" +render "$TEMPLATES/scripts/bg-dispatch.sh.tmpl" "$TARGET/scripts/bg-dispatch.sh" +render "$TEMPLATES/scripts/claude-review.sh.tmpl" "$TARGET/scripts/claude-review.sh" chmod +x "$TARGET/scripts/oc.sh" "$TARGET/scripts/team.sh" "$TARGET/scripts/dashboard" \ "$TARGET/scripts/verify-state.sh" "$TARGET/scripts/verify-spec.sh" \ "$TARGET/scripts/verify-models.sh" "$TARGET/scripts/promote-findings.sh" \ + "$TARGET/scripts/bg-dispatch.sh" "$TARGET/scripts/claude-review.sh" \ "$TARGET/scripts/codex-lead.sh" \ "$TARGET/scripts/codex-preflight.sh" 2>/dev/null || true @@ -607,6 +612,14 @@ if [ -d "$TARGET/.git" ]; then printf 'init.sh: added %s to %s\n' "$pattern" "$GITIGNORE" fi done + # scripts/bg-dispatch.sh's per-dispatch pid/output files and the lead's + # review briefs: runtime traces, not records (the state file is the record). + for pattern in '/.pipeline/T-*.out' '/.pipeline/T-*.err' '/.pipeline/T-*.pid' '/.pipeline/T-*.brief'; do + if ! grep -qxF "$pattern" "$GITIGNORE" 2>/dev/null; then + printf '\n%s\n' "$pattern" >> "$GITIGNORE" + printf 'init.sh: added %s to %s\n' "$pattern" "$GITIGNORE" + fi + done if ! grep -qxF '.pipeline/logs/' "$GITIGNORE" 2>/dev/null; then { printf '\n# per-call pipeline telemetry written by scripts/oc.sh\n' diff --git a/docs/CODEX.md b/docs/CODEX.md index 46a1913..0d9ff8d 100644 --- a/docs/CODEX.md +++ b/docs/CODEX.md @@ -67,6 +67,25 @@ tests a temporary write under `.pipeline/` before planning and reports a blocked operation to the lead. Changing the runtime directory does not make that source-write boundary enforced. +## Lead parity with Claude + +The Codex lead runs the same canonical flow as the Claude lead; what differs is +mechanics the Claude harness supplies and Codex does not: + +- **No Monitor.** Long dispatches go through `scripts/bg-dispatch.sh start`, + and the lead calls `scripts/bg-dispatch.sh wait … ` with a limit + below its own exec timeout until it reports `finished:` or `wrapper-exited:`. +- **No Claude subagents.** A `claude/*` reviewer runs headlessly through + `scripts/claude-review.sh`, which needs the `claude` CLI, the project's + `.claude/agents/reviewer.md`, and runtime approval to reach Anthropic. +- **No structured question tool.** A stop-and-ask is one plain message with + numbered options, the recommended one first. +- **Stopping a role** is `scripts/oc.sh --interrupt `, never a raw API call. + +The reverse gap exists too: a Claude lead cannot spawn a Codex `tester` +subagent. The canonical flow says to ask the user rather than substitute — for +example, park the task at `testing` for a Codex-led session. + ## Recovery Resuming uses `.pipeline/.codex-session-id.`, never `--last`. A failed diff --git a/templates/claude/commands/feature.md.tmpl b/templates/claude/commands/feature.md.tmpl index b0a2a9e..6252206 100644 --- a/templates/claude/commands/feature.md.tmpl +++ b/templates/claude/commands/feature.md.tmpl @@ -54,6 +54,28 @@ does not know it. `verify-state.sh` reports as exceeded is an escalation, never one more lap. +## Where the team stopped + +When the request is a status question ("where did the team stop?", "what's +next?") or this session is resuming cold, answer it before dispatching +anything: + +1. The status board, then the `Latest handoff`, `Status` and `Blocked since` + lines of every `T-*.md` that is not `done` **and** of the newest few that + are. +2. If the project uses GitHub, `gh pr list --state all --limit 10` — + reconcile it against the board. A PR that GitHub shows **merged** while + the state file or board still says "awaiting merge approval" is a missing + merge record (step 5), not an open task. +3. Local `main` against `origin/main`. A stale local `main` silently bases + the next task branch on old code; fast-forward it with + `git fetch . origin/main:main` (never a reset). +4. `scripts/oc.sh --status` — a running session means a role is still + working; say so instead of re-dispatching. + +Report the last finished task, anything half-recorded, the next planned task +(from deferred scope in the last spec), and the options — then stop. + ## Preflight 1. **First run only:** if `.pipeline/.needs-customization` exists, this @@ -329,8 +351,52 @@ before it exited — verified live: the session's message shows a a turn hanging open. Retrying the same session immediately is safe; you do not need to check whether the prior call is still active first. (This only holds when `oc.sh` itself did the killing. If a session was killed some -other way — you interrupted it by hand, the machine slept — abort it -yourself before reusing it: `OPENCODE_PASSWORD="$(cat .pipeline/.oc-password)" opencode api POST /api/session//interrupt --server $OC_SERVER`.) +other way — you interrupted it by hand, the machine slept — interrupt it +yourself before reusing it: `scripts/oc.sh --interrupt `. It resolves the +server and password the way every dispatch does.) + +**Launch every `oc.sh` dispatch detached, never as a plain shell call.** The +lead's own shell tool kills commands long before `oc.sh`'s 2400s ceiling +(Claude Code at 600s even when backgrounded; Codex at its exec timeout). A +killed wrapper prints no `oc.sh: session=` line and no reply while the turn +keeps running server-side — it looks dead and is not. So: + +```bash +scripts/bg-dispatch.sh start T- builder -- scripts/oc.sh --agent builder … +scripts/bg-dispatch.sh wait T- builder 540 --session ses_xxx +``` + +`start` does the `nohup … & disown`, writes `.pipeline/T-.