diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4580ad..fa95741 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,12 +13,14 @@ 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: Herdr adapter contracts (no agents or LLM calls) run: python3 test/herdr.py - 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/AGENTS.md b/AGENTS.md index d3e246c..50a6384 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,193 +1,23 @@ -# agent-toolkit +# agent-toolkit — maintainer instructions -A reusable, project-agnostic version of the planner → implement → review → -test multi-agent pipeline. `bin/init.sh` scaffolds it into any target repo: -Codex or Codex as lead, Codex/Codex planning, OpenCode (any vendor) for -cross-vendor implement/review/test, a state file (`.agents/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. +This file is for any AI tool (Codex, OpenCode, Claude Code) that is *changing the +toolkit's own code*. It is not the `AGENTS.md` that `bin/init.sh` generates into +target projects; that comes from `templates/codex/AGENTS.md.tmpl`. -It was distilled from real multi-agent pipeline runs and hardened there -over time, so the same setup — permissions, session-reuse policy, -cross-vendor independence rules, the state-file contract, the -context-discipline rules — doesn't get re-invented and re-debugged from -scratch in every new repo. Read -`README.md` first for the pitch, the file tree, and the documented design -tradeoffs; this file is for someone (human or agent) actively changing the -toolkit's own code. +The maintainer guide lives in **`CLAUDE.md`** at this repository root. Read it +in full before editing; it is the single source of truth, and is deliberately +not duplicated here so the two cannot drift. Its "Claude" wording refers to the +lead tool whose command files are the canonical flow; it does not require the +Claude CLI. -## Commands - -No build step, no dependencies, no LLM calls in the test path. Verify a -change with the automated smoke run: +Minimum checks before claiming a change works: ```bash -bash test/smoke.sh # scaffolder guarantees + the two structural scripts -bash test/invariants.sh # every load-bearing rule present in every copy -python3 test/herdr.py # optional host adapter; fake Herdr, no agents or LLM calls +bash test/smoke.sh +bash test/invariants.sh +bash test/codex.sh +python3 test/herdr.py ``` -`smoke.sh` scaffolds into a throwaway directory and asserts the core -guarantees: every placeholder substituted, no file clobbered on re-run, -the provenance stamp written once and never touched by `--update`, -`--update` triage (summary-first, exit 0 clean / 1 drift, hunks behind -`--diff`/`--only`, flags defaulted from the stamp), the findings-promotion -traversal guard, the loop-cap and budget checks fire, `verify-state.sh` -refuses `done` while an acceptance criterion is open, and `verify-spec.sh` -rejects an unfilled spec while accepting a filled one. - -`invariants.sh` is the other half: it greps each load-bearing rule against -every hand-synced copy that must carry it. Run it after **any** change to -the pipeline's rules — it is what the "keep the copies in sync" convention -below finally has behind it. CI runs both plus shellcheck on every push -(`.github/workflows/ci.yml`). For an eyeball-level pass you can still do -the manual version: - -```bash -mkdir -p /tmp/toolkit-smoke && bash bin/init.sh \ - --target /tmp/toolkit-smoke --project-name x \ - --builder-model a/b --reviewer-model a/c \ - --reviewer-fallback-model d/e --tester-model a/b -grep -rl '__[A-Z_]*__' /tmp/toolkit-smoke # must print nothing — no unfilled placeholder -rm -rf /tmp/toolkit-smoke -``` - -Re-run `init.sh` a second time against the same target to confirm existing -files are skipped, not clobbered (`render()`'s core guarantee). - -## Architecture - -| Path | Role | -| --- | --- | -| `bin/init.sh` | The scaffolder. `render()` copies a `.tmpl` file to a destination with `sed` placeholder substitution, skipping any file that already exists | -| `bin/release.sh` | Deterministic releaser: validates clean tree / main / changelog heading / no duplicate tag, then annotated tag + push. Judgment (changelog drafting, version choice) lives in `skills/toolkit-release/`, never in the script | -| `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 | -| `integrations/herdr/` | Optional Herdr plugin for adopting an existing lead and showing task records/support panes; Python standard library, independent of the bash scaffolder | -| `bin/dashboard` | Standalone read-only visual task viewer for any terminal; reuses the renderer without calling Herdr | -| `integrations/herdr/dashboard.py` | Canonical host-independent viewer; copied directly by init to `scripts/dashboard`, also imported by the Herdr adapter. Python is required only to run this optional UI, not to scaffold | -| `.github/workflows/ci.yml` | Runs `test/smoke.sh` + shellcheck (`bin/init.sh`, the test, and every `templates/scripts/*.tmpl`) | -| `templates/Codex/agents/` | `planner.md.tmpl`, `senior-dev.md.tmpl` — Codex subagent role definitions | -| `templates/Codex/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 Codex command flow | -| `templates/opencode/agent/` | `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 | -| `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 | -| `skills/status-board/` | Keeps a project-wide status board in sync with per-task state files — independent of `init.sh` | -| `skills/karpathy-guidelines/` | Behavioral defaults loaded by the lead via `feature.md`; inlined into senior-dev/builder rather than granted Skill access | -| `skills/self-improvement/` | Optional, off by default: lead-only capture of user corrections into its own instruction files. Never auto-loaded | -| `SYSTEM.md` | Tool-agnostic one-pager meant to be handed to any AI ("recreate this system with yourself as lead") | -| `CHANGELOG.md` | Impact-tagged per-release entries (`[contract]` › `[safety]` › `[process]` › `[docs]`). Append an entry in the same commit as any template/script change — this is what lets a downstream project triage an update without reading raw diffs | -| `migrations/` | Numbered, hand-appliable notes for `[contract]` changes only. No migration runner, by design. Write one in the same commit as the contract change it describes | -| `docs/UPGRADING.md` | The downstream-update plan (provenance stamp → tags/changelog → `--update` triage → migrations → `/toolkit-update`); stages 1–5 are implemented in `bin/init.sh` + `CHANGELOG.md` + `migrations/` | -| `REVIEW.md` / `REVIEW-2.md` | Point-in-time honest reviews; see each file's status section for applied vs open items | -| `REVIEW-2.md` | Third review pass — the delivery contract (acceptance criteria are never marked met by anyone), the status state machine, the missing team-charter layer. Does not repeat `REVIEW.md`; its Part D re-ranks and prunes that file's backlog | -| `docs/UPGRADING.md` | Staged plan for keeping an already-scaffolded downstream project current: provenance stamp, tagged releases + impact-classified changelog, `--update` triage, AI-assisted merge | - -## Conventions - -- **Zero dependencies, bash + sed only.** Do not add Node/npm tooling to the - scaffolder itself, even for something `bin/init.sh` would find easier - with them. -- **`render()` never overwrites an existing file in the target.** That's - what makes re-running `init.sh` safe and lets a project's own - customizations survive a re-scaffold. Don't change that default. -- **Templates never hardcode project-specific constraints.** Every - generated role file says "read this project's own `AGENTS.md` / - `AGENTS.md` first" rather than guessing at a target project's security - posture, banned patterns, or style. The toolkit owns the *process*; the - target project's own guidance file owns the *content*. If you're tempted - to bake in a specific rule (e.g. "never use `innerHTML`"), it belongs in - an example project's own `AGENTS.md`, not here. -- **Every generated role's reply to the lead is short by design** — a - verdict line, a pass/fail count, the Latest-handoff line — never a copy - of the diff, findings, or test output it already wrote to the state - file. This is deliberate: it's what keeps the orchestrating lead's - context small across a long pipeline run. When editing a role template's - "Report"/"Output" section, preserve that shape rather than reverting to - "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 `.Codex/` + `.opencode/` + - `.agents/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 - rest is still by hand — `diff` the two sides after any pipeline-mechanism - change. -- **The lead's flow exists in three hand-synced copies — re-diff all - three when the pipeline sequence changes:** - `templates/Codex/commands/feature.md.tmpl`, `SYSTEM.md`, and - `skills/dev-team-generator/reference/flow-example.md`. They serve three - audiences (generated project / any-AI-as-lead / generate-anything skill) - but must carry the same sequence and stop-and-ask rules. These have - diverged silently before — twice, on consecutive commits, *after* this - bullet was written, which is why the rule alone was never enough. - **`test/invariants.sh` now enforces it**: add the rule to its table in the - same turn you add it to a flow copy, and CI fails when a copy is missing - it. The table is presence-only (it cannot catch wording drift), so still - read the sibling copies — but omission, the failure that actually keeps - happening, is now caught for you. Check all three plus - `lessons-learned.md` per the next bullet. -- **Mirror every new hardening lesson into - `skills/dev-team-generator/reference/lessons-learned.md`, in the same - turn.** This toolkit's real lessons — a live-verified gotcha, a - reliability fix, an incident and what actually fixed it — mostly surface - first as a change to one specific template (`oc.sh.tmpl` gaining - session-abort-on-timeout, `senior-dev.md.tmpl`'s testing rhythm getting - scoped, and so on) or in a project applying this toolkit. Whenever that - happens, add or update the corresponding entry in - `lessons-learned.md` too, written generalized — the mechanism and the - reason, not the originating tool's specific flag/endpoint name (that - file already explains why: it exists so a lesson earned once against - OpenCode, say, doesn't have to be re-earned by a future project running - some other tool entirely). This is a distinct sync step from the bullet - above: that one keeps the Codex+OpenCode *mechanism* consistent across - `templates/` and its applied copies; this one keeps the *generalized - knowledge* available to `dev-team-generator`'s any-tool generation path, - which doesn't read `templates/` at all. A change that's purely - mechanical (a typo fix, a formatting change) doesn't need this; a change - that exists *because* something failed, or because live verification - proved or disproved an assumption, does. - -## Gotchas - -- `render()` prints `skip (exists)` and leaves the file alone when the - 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 - true *before* any `render()` call ran (checked via whether - `.Codex/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 - 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 - 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 - `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" - — 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 - enforced by the runtime. This toolkit's safe-default posture exists - because that assumption failed once in a real pipeline. Any change to a - permission block in a template should carry a note - to verify it live (dispatch the agent, try the disallowed action, - confirm it's refused) rather than just reading the config. - -## Status - -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* -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. +Runtime state for scaffolded projects is `.pipeline/`; skills stay in +`.agents/skills/`. Never move task records back under `.agents/`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 592eeaf..8868cea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,106 @@ 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]` OpenCode sessions are scoped **per role per task**, not per + task. The state file's single *OpenCode session id* becomes *OpenCode + builder session id*, *OpenCode reviewer session id* and *OpenCode tester + session id* (plus *Codex tester thread id*); each role's first call starts + its own session and only its own retry reuses it. Sharing one session broke + on opencode v2 (a session pins the opening agent's tool set, so the tester + could not write its results) and fed the reviewer the builder's reasoning. + SYSTEM.md, the invariant rule, the dev-team-generator references, `oc.sh`, + and the OpenCode lead follow. Migration: `migrations/04-per-role-sessions.md`. +- `[process]` Every task gets its own branch (`T--`, keeping any + project prefix) before an implementer runs, so each merge gate proposes + exactly one PR. +- `[process]` `init.sh --builder-auto ask|on` stamps `builder_auto` in + `.pipeline/.toolkit-version`. `on` is a standing user decision: builder + dispatches pass `--auto` without a per-task question; `ask` (default) keeps + the spec-time question. The flow now names the builder's `deny` list as the + only thing `--auto` does not approve. + +- `[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. +- `[process]` A `codex/` tester is now a supported role: + `--tester-model codex/` scaffolds `.codex/agents/tester.toml` + (report-only tester, model id without the vendor prefix); + `verify-models.sh` skips `codex/*` like `claude/*`; `codex-preflight.sh` + requires the tester agent when the manifest names one. +- `[process]` The OpenCode lead dispatches a `claude/*` reviewer through + `scripts/claude-review.sh`, waits through `bg-dispatch.sh`, and stops roles + with `oc.sh --interrupt`. Generated `AGENTS.md` asks the project to record + whether sending review material to Anthropic is authorized. + +- `[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`, + and the Herdr plugin read and write `.pipeline/` (task records) while + skills stay in `.agents/skills/`. Upstream's `.opencode/agent/` worker + directory stays retired in favor of `.opencode/agents/`. See below for + upstream's own v0.10.0 notes, which still say `.agents/` where they + describe the unmodified upstream. +- `[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 instructs the lead to verify (against `/api/agent`) that + required roles and their ordered permission rules loaded after restarts, + upgrades, migrations, or permission edits — the script checks models, the + role check is a lead-run step. 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 + `.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.10.0 — 2026-10-06 - `[safety]` Corrected earlier permission-verification conclusions in worker diff --git a/CLAUDE.md b/CLAUDE.md index d5b1fe6..a2cddaf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,8 +2,8 @@ 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 +Claude, Codex, or OpenCode as lead, matching-tool planning, OpenCode (any vendor) for +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,15 @@ 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 +python3 test/herdr.py # optional host adapter; fake Herdr, no agents or LLM calls ``` `smoke.sh` scaffolds into a throwaway directory and asserts the core @@ -63,13 +66,18 @@ 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 | +| `integrations/herdr/` | Optional Herdr plugin for adopting an existing lead and showing task records/support panes; Python standard library, independent of the bash scaffolder | +| `bin/dashboard` | Standalone read-only visual task viewer for any terminal; reuses the renderer without calling Herdr | +| `integrations/herdr/dashboard.py` | Canonical host-independent viewer; copied directly by init to `scripts/dashboard`, also imported by the Herdr adapter. Reads `.pipeline/` (falls back to a not-yet-migrated `.agents/` runtime). Python is required only to run this optional UI, not to scaffold | | `.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 | | `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 worker role definitions; `leader.md.tmpl`, `planner.md.tmpl` — native V2 adapters for an OpenCode lead | +| `templates/opencode/commands/` | `feature.md.tmpl`, `toolkit-update.md.tmpl` — direct OpenCode lead commands that point at the canonical Claude flow files | | `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 | @@ -108,7 +116,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,20 +163,20 @@ 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 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 - `.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 +190,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 + Herdr adapter + 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 ca08df9..ee79ac9 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ A reusable version of the planner → implement → review → test multi-agent pipeline: Claude, Codex, or OpenCode as the lead, matching-tool 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 @@ -16,16 +16,22 @@ select a `leader` adapter, and its `planner` references the canonical role instructions. No Claude or Codex CLI is required; the generated `.claude/` files remain shared instruction sources, not executable dependencies. -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 ### Standalone visual dashboard -From any project with `.agents/` task records, run: +From any project with `.pipeline/` task records, run: ```bash ./scripts/dashboard @@ -86,7 +92,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 @@ -94,7 +100,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 @@ -117,20 +123,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. @@ -157,19 +163,21 @@ 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 - opencode/agent/ builder.md.tmpl, reviewer.md.tmpl, tester.md.tmpl - opencode/agents/ leader.md.tmpl, planner.md.tmpl — native V2 adapters + codex/ AGENTS.md.tmpl, a project-scoped planner agent, reviewed + lifecycle hooks, and feature/toolkit-update skills + opencode/agents/ builder, reviewer, tester (workers); leader, planner — + native V2 lead adapters opencode/commands/ feature.md.tmpl, toolkit-update.md.tmpl — direct lead commands 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 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/) @@ -263,13 +271,18 @@ 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. + ### OpenCode-only lead After scaffolding a target project, start OpenCode normally: @@ -301,8 +314,7 @@ written by `team.sh`). Do not start the lead with `--auto`. OpenCode starts a fresh lead chat by default; `--continue` might resume a worker instead. To resume a known lead explicitly, set `TEAM_OPENCODE_SESSION=ses_...` when launching `team.sh`. `--fresh` ignores it. This does not change the -canonical worker-session policy or eliminate its documented shared-context -tradeoff. Verify live agent discovery and denied actions before trusting +canonical per-role worker-session policy. Verify live agent discovery and denied actions before trusting permission controls; the smoke suite does not make that guarantee. For an already-stamped project, a plain `bin/init.sh --target ` adds @@ -311,7 +323,7 @@ the four missing OpenCode files without overwriting existing files. Use ### 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 @@ -330,8 +342,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 @@ -340,8 +354,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). @@ -359,13 +373,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 @@ -383,7 +400,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 @@ -395,6 +413,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 @@ -419,7 +440,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. @@ -447,11 +468,11 @@ exactly how to verify before loosening it (dispatch the agent, try to make it edit a source file, confirm it's refused). Do not trust "the reviewer can't touch source" without having run that check once against your actual OpenCode server. -- **Session reuse (implement → review → test in one OpenCode session) is -documented as a real tradeoff, not a free win.** It saves reload cost but -feeds the reviewer the implementer's full read/edit trace, which can be -larger than the diff it's meant to review. Measure it before assuming -it's cheaper. +- **One OpenCode session per role per task, not one per task.** Sharing one +session across implement, review and test was the earlier policy; it fed +the reviewer the implementer's full trace instead of the diff, and on +opencode v2 it left the tester without file-edit tools (a session pins the +opening agent's tool set). A retry still reuses its own role's session. - **Implementation/review/test role files are self-contained, one full copy per tool — not a canonical file with thin per-tool shims.** `senior-dev` (Claude) and @@ -542,8 +563,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 @@ -581,6 +602,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 @@ -593,7 +617,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..51a4b92 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 @@ -147,10 +147,13 @@ Record **who decided** (`agent` | `human`) next to it — and record, don't act: a class never changes session policy, permissions, or budgets mid-task. -**Session scope is one task, for all sub-agents alike:** same task → same -session (implement, review, test continue it); new task → new session. A -fresh session per reviewer/tester re-reads every file from scratch — that -burns the context reuse exists to save. +**Session scope is one task per role:** same task and same role → same +session (a retry continues it); another role → its own session, recorded in +its own state-file field; new task → new sessions. Sharing one session across +implement, review and test broke on opencode v2 (a session pins the tool set +of the agent that opened it, so a tester in the builder's session could not +write its results) and fed the reviewer the builder's reasoning instead of +just the diff. **A second review pass closes the first, it does not restart it:** every earlier finding marked fixed, withdrawn, disputed (answering the author's @@ -221,7 +224,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. @@ -240,13 +243,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 @@ -254,7 +257,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 +281,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 @@ -289,3 +299,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 11d67c6..1ecbf75 100755 --- a/bin/init.sh +++ b/bin/init.sh @@ -13,6 +13,13 @@ # [--reviewer-fallback-model ] \ # [--tester-model ] \ # [--claude-model sonnet] [--test-dir e2e] +# [--codex-model ] [--codex-reasoning ] +# [--codex-planner-model ] [--codex-planner-reasoning ] +# [--builder-auto ] +# Codex settings default to inherit: preserve local/parent choices. +# --builder-auto records the project's standing answer to OpenCode `--auto` +# for builder dispatches: `ask` (default) asks at every spec approval; `on` +# is a standing user decision, recorded per task without asking again. # # The four model flags and --claude-model/--test-dir all default (see # apply_defaults() below) to the lineup two independent real projects @@ -24,7 +31,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 +48,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,11 +71,16 @@ 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="" TESTER_MODEL="" TEST_DIR="" +BUILDER_AUTO="" UPDATE=0 SHOW_DIFF=0 ONLY="" @@ -86,11 +98,16 @@ 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 ;; --tester-model) [ $# -ge 2 ] || die "--tester-model needs a value"; TESTER_MODEL="$2"; shift 2 ;; --test-dir) [ $# -ge 2 ] || die "--test-dir needs a value"; TEST_DIR="$2"; shift 2 ;; + --builder-auto) [ $# -ge 2 ] || die "--builder-auto needs a value"; BUILDER_AUTO="$2"; shift 2 ;; --update) UPDATE=1; shift ;; --diff) SHOW_DIFF=1; shift ;; --only) [ $# -ge 2 ] || die "--only needs a value"; ONLY="$2"; shift 2 ;; @@ -137,6 +154,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 @@ -151,7 +169,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) @@ -168,10 +188,21 @@ 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 +# Worker files, not the directory: an upstream scaffold already has .opencode/agents/ +# (leader/planner) beside the legacy .opencode/agent/ workers. +if [ "$UPDATE" -eq 0 ] && [ -f "$TARGET/.opencode/agent/builder.md" ] && [ ! -f "$TARGET/.opencode/agents/builder.md" ]; 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=23 +RENDER_TOTAL=31 write_stamp() { local sha tag @@ -183,11 +214,16 @@ 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" printf 'tester_model: %s\n' "$TESTER_MODEL" printf 'test_dir: %s\n' "$TEST_DIR" + printf 'builder_auto: %s\n' "$BUILDER_AUTO" } > "$STAMP.tmp" mv "$STAMP.tmp" "$STAMP" } @@ -200,7 +236,20 @@ 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" + [ -n "$BUILDER_AUTO" ] || BUILDER_AUTO="ask" + case "$BUILDER_AUTO" in ask|on) ;; *) die "invalid --builder-auto: $BUILDER_AUTO (ask or on)" ;; esac # Lineup two independent real projects converged on. Not a guarantee these # exact ids exist on your OpenCode server — run `opencode models` and pass # explicit flags when they don't. @@ -211,18 +260,23 @@ 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" \ "tester_model|TESTER_MODEL" \ - "test_dir|TEST_DIR"; do + "test_dir|TEST_DIR" \ + "builder_auto|BUILDER_AUTO"; do key="${spec%%|*}"; var="${spec##*|}" if [ -z "${!var}" ]; then load_stamp_value "$key" @@ -238,19 +292,30 @@ 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" + [ -f "$agent_dir/builder.md" ] || 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__`" + # 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) - 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) + 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' \ @@ -258,16 +323,16 @@ 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 } -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 +340,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 +382,16 @@ 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\"" +# A `codex/` tester runs as a native Codex subagent, not through oc.sh: +# it gets .codex/agents/tester.toml (model id without the vendor prefix). +CODEX_TESTER_MODEL="" +case "$TESTER_MODEL" in codex/*) CODEX_TESTER_MODEL="${TESTER_MODEL#codex/}" ;; esac + # 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,10 +404,15 @@ 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" -e "s|__TESTER_MODEL__|$TESTER_MODEL|g" + -e "s|__CODEX_TESTER_MODEL__|$CODEX_TESTER_MODEL|g" -e "s|__TEST_DIR__|$TEST_DIR|g" ) @@ -379,28 +463,40 @@ 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" + [ -z "$CODEX_TESTER_MODEL" ] || check_pair "$TEMPLATES/codex/agents/tester.toml.tmpl" "$TARGET/.codex/agents/tester.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/opencode/agents/planner.md.tmpl" "$TARGET/.opencode/agents/planner.md" - check_pair "$TEMPLATES/opencode/agents/leader.md.tmpl" "$TARGET/.opencode/agents/leader.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/opencode/agents/planner.md.tmpl" "$TARGET/.opencode/agents/planner.md" + check_pair "$TEMPLATES/opencode/agents/leader.md.tmpl" "$TARGET/.opencode/agents/leader.md" check_pair "$TEMPLATES/opencode/commands/feature.md.tmpl" "$TARGET/.opencode/commands/feature.md" check_pair "$TEMPLATES/opencode/commands/toolkit-update.md.tmpl" "$TARGET/.opencode/commands/toolkit-update.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 "$TOOLKIT_ROOT/integrations/herdr/dashboard.py" "$TARGET/scripts/dashboard" 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" + 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 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 @@ -445,31 +541,45 @@ 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" +[ -z "$CODEX_TESTER_MODEL" ] || render "$TEMPLATES/codex/agents/tester.toml.tmpl" "$TARGET/.codex/agents/tester.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/opencode/agents/planner.md.tmpl" "$TARGET/.opencode/agents/planner.md" -render "$TEMPLATES/opencode/agents/leader.md.tmpl" "$TARGET/.opencode/agents/leader.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/opencode/agents/planner.md.tmpl" "$TARGET/.opencode/agents/planner.md" +render "$TEMPLATES/opencode/agents/leader.md.tmpl" "$TARGET/.opencode/agents/leader.md" render "$TEMPLATES/opencode/commands/feature.md.tmpl" "$TARGET/.opencode/commands/feature.md" render "$TEMPLATES/opencode/commands/toolkit-update.md.tmpl" "$TARGET/.opencode/commands/toolkit-update.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 "$TOOLKIT_ROOT/integrations/herdr/dashboard.py" "$TARGET/scripts/dashboard" 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" +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/promote-findings.sh" 2>/dev/null || true + "$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 -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, @@ -481,45 +591,58 @@ 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 + # 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' - 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 @@ -529,7 +652,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. @@ -542,11 +665,14 @@ Next steps: then OpenCode. 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 lead integration plus an explicitly unpopulated guidance section; fill it when no project CLAUDE.md already supplies constraints. 5. On the first feature run, the selected 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. View task progress anytime: $TARGET/scripts/dashboard (Python 3.8+; no Herdr or model calls; --once prints a snapshot). diff --git a/docs/ADDING-A-TOOL.md b/docs/ADDING-A-TOOL.md index 807972a..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 @@ -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..0d9ff8d --- /dev/null +++ b/docs/CODEX.md @@ -0,0 +1,144 @@ +# 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. + +## 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 +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/OC-TIMEOUT-WATCHDOG.md b/docs/OC-TIMEOUT-WATCHDOG.md index 6770186..4716cb4 100644 --- a/docs/OC-TIMEOUT-WATCHDOG.md +++ b/docs/OC-TIMEOUT-WATCHDOG.md @@ -43,7 +43,7 @@ aborted only when **either**: 2. the absolute `OC_TIMEOUT` ceiling (default **2400**) is reached while the turn is still running. -On abort it `POST $OC_SERVER/session//abort`s (verified live: 200, +On abort it `POST`s `/api/session//interrupt` (opencode v1 called this `/session//abort`; verified live on v1: 200, idempotent) and exits **124**, with a stderr message that says which of the two fired. A run that keeps making progress is never touched, however long it takes. diff --git a/docs/TEAM.md b/docs/TEAM.md index 2086078..4cf365e 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`, then `opencode`), 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 @@ -41,7 +41,7 @@ the password file, without putting its contents in the pane 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 @@ -61,10 +61,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). OpenCode deliberately does **not** use `--continue`: the latest session may be a worker, not the lead. It starts a fresh lead chat unless you supply a known @@ -83,12 +86,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. @@ -101,7 +104,7 @@ From the project root, open the read-only visual dashboard: ./scripts/dashboard ``` -It refreshes `.agents/T-*.md` every two seconds, showing stage graphs, task +It refreshes `.pipeline/T-*.md` every two seconds, showing stage graphs, task cards, acceptance ticks, blockers, and latest handoffs. Use arrows or `j/k` to scroll, `b` to filter blocked tasks, `r` to refresh, and `q` to close. diff --git a/docs/UPGRADING.md b/docs/UPGRADING.md index 00212bf..245fe96 100644 --- a/docs/UPGRADING.md +++ b/docs/UPGRADING.md @@ -43,12 +43,21 @@ 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. + +A project whose state template still has a single *OpenCode session id* field +needs [migration 04](../migrations/04-per-role-sessions.md): one session per +role per task, and a `builder_auto` decision in the stamp. + ```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 @@ -79,7 +88,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 @@ -93,7 +102,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 ``` ``` @@ -118,7 +127,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 @@ -170,8 +179,8 @@ 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 + [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 ``` @@ -193,8 +202,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:** @@ -236,7 +245,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/integrations/herdr/README.md b/integrations/herdr/README.md index f69fcdf..d18b851 100644 --- a/integrations/herdr/README.md +++ b/integrations/herdr/README.md @@ -21,7 +21,7 @@ panes to task records without replacing either system. - Herdr **0.9.1+**, macOS or Linux. - Python **3.8+**, standard library only. No build, npm, jq, or plugin SDK. -- A project already scaffolded by `bin/init.sh`, with `.agents/TEMPLATE.md` +- A project already scaffolded by `bin/init.sh`, with `.pipeline/TEMPLATE.md` and `.claude/commands/feature.md`. Adoption never scaffolds or updates files. From a local toolkit checkout: @@ -130,7 +130,7 @@ same project/workspace can be found even if its terminal/pane id changed. Herdr's OpenCode V2 lifecycle integration runs in the **full TUI**, not headless/Mini clients. Headless workers therefore remain ordinary processes; -their work is recorded in `.agents/T-.md`, not inferred from agent badges. +their work is recorded in `.pipeline/T-.md`, not inferred from agent badges. ### Live visual dashboard — no model required @@ -160,7 +160,7 @@ T-001 — testing — ledger 2/3 ticked (recorded) ``` Stage counts, highlighted per-task graphs, blocker summaries, handoffs, recorded -acceptance bars, and loop counters are computed directly from `.agents/T-*.md`. +acceptance bars, and loop counters are computed directly from `.pipeline/T-*.md`. **No model, model API, Herdr status query, or paid call generates the dashboard.** It works without adopting a lead. Narrow panes wrap automatically. Blocked or unknown statuses do not get a guessed stage; graphs do not claim prior gates diff --git a/integrations/herdr/dashboard.py b/integrations/herdr/dashboard.py index 971abed..bed43cb 100644 --- a/integrations/herdr/dashboard.py +++ b/integrations/herdr/dashboard.py @@ -31,9 +31,23 @@ def stage_graph(status): return " -> ".join("[" + stage + "]" if stage == active else stage for stage in STAGES) +def runtime_dir(root): + """Task-record directory: `.pipeline/`, or a not-yet-migrated `.agents/` runtime.""" + current = root / ".pipeline" + if current.is_dir(): + return current + legacy = root / ".agents" + # `.agents/` also holds Codex skills in migrated projects, so only treat it + # as a runtime when it carries task records or the task template. + if legacy.is_dir() and ((legacy / "TEMPLATE.md").is_file() or any(legacy.glob("T-*.md"))): + return legacy + return None + + def task_records(root): records, warnings = [], [] - for task in sorted((root / ".agents").glob("T-*.md")): + directory = runtime_dir(root) + for task in sorted(directory.glob("T-*.md") if directory else ()): if root not in task.resolve().parents: warnings.append(clean(task.name) + " — external symlink skipped") continue @@ -234,9 +248,9 @@ def main(argv=None): parser.add_argument("--once", action="store_true", help="Print a plain-text snapshot") args = parser.parse_args(argv) start = Path(args.project).expanduser().resolve() - root = next((p for p in (start, *start.parents) if (p / ".agents").is_dir()), None) + root = next((p for p in (start, *start.parents) if runtime_dir(p)), None) if root is None: - parser.error("No .agents/ directory found. Run inside a toolkit project or pass --project.") + parser.error("No .pipeline/ directory found. Run inside a toolkit project or pass --project.") if args.once or not sys.stdout.isatty() or not sys.stdin.isatty() or os.environ.get("TERM") in (None, "dumb"): print(board_text(root)) else: diff --git a/integrations/herdr/plugin.py b/integrations/herdr/plugin.py index 35d0960..5c5c6e5 100644 --- a/integrations/herdr/plugin.py +++ b/integrations/herdr/plugin.py @@ -62,7 +62,8 @@ def project_root(start): raise PluginError("No absolute project cwd supplied by Herdr; select a project pane.") path = Path(start).resolve() for root in (path, *path.parents): - if (root / ".agents/TEMPLATE.md").is_file() and (root / ".claude/commands/feature.md").is_file(): + template = (root / ".pipeline/TEMPLATE.md").is_file() or (root / ".agents/TEMPLATE.md").is_file() + if template and (root / ".claude/commands/feature.md").is_file(): return root raise PluginError("Project is not scaffolded. Run bin/init.sh explicitly first; adoption never scaffolds.") @@ -211,7 +212,7 @@ def brief_leader(root, workspace, binding): prompt = ( "Act as this project's agent-toolkit lead in this existing session, not as an implementer. " "Read the project's AGENTS.md and CLAUDE.md when present. " + instruction + " " - "Read .claude/commands/feature.md in full; keep the pipeline in .agents/T-.md. " + "Read .claude/commands/feature.md in full; keep the pipeline in .pipeline/T-.md. " "This role briefing is not a feature request, spec approval, auto-mode consent, or merge approval. " "Do not dispatch work or modify files yet. Confirm readiness briefly and ask for the task. " "Herdr hosts terminals only; keep reviewer independence, loop budgets, permission checks, " 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/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/migrations/04-per-role-sessions.md b/migrations/04-per-role-sessions.md new file mode 100644 index 0000000..4058d8f --- /dev/null +++ b/migrations/04-per-role-sessions.md @@ -0,0 +1,31 @@ +# Migration 04 — One OpenCode session per role + +Apply at a task boundary. Earlier scaffolds shared one OpenCode session across +implement, review and test for a task, recorded in a single *OpenCode session +id* field. On opencode v2 a session pins the tool set of the agent that opened +it, so a tester dispatched into the builder's session could not write its +results, and a shared session fed the reviewer the builder's reasoning instead +of just the diff. Each role now gets its own session and its own field. + +1. Confirm no task is between implement and test. A task already in review or + testing can finish on the old policy; record that in its Decisions log. +2. In `.pipeline/TEMPLATE.md`, replace the `**OpenCode session id:**` line with + the three per-role lines from `templates/agents-state/TEMPLATE.md.tmpl` + (*OpenCode builder session id*, *OpenCode reviewer session id*, *OpenCode + tester session id*), and add *Codex tester thread id* if the project uses a + `codex/*` tester. Leave historical `T-*.md` files as they are. +3. Merge the new session-policy section, the review and test commands (no + `--session` on a role's first call), and the per-task branch paragraph from + `templates/claude/commands/feature.md.tmpl` into + `.claude/commands/feature.md`. Keep any project-specific branch prefix. +4. Decide the project's standing answer to builder `--auto`: add + `builder_auto: ask` (ask per task, the old behavior) or `builder_auto: on` + (a standing user decision) to `.pipeline/.toolkit-version`. Only the user + can choose `on`; record when and who in the status board or a decision note. +5. Merge the session lines in `scripts/oc.sh`'s header and the OpenCode + `leader` agent's worker-session bullet. +6. Run `bash scripts/verify-state.sh` on any open task and the project + preflight. + +The first builder, reviewer and tester call on the next task each omit +`--session`; only a retry by the same role reuses its own recorded id. 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/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 5477f74..f07d889 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. @@ -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 @@ -146,11 +153,14 @@ stopped and say so. ### Dispatch-tool session policy If the dispatched tool supports continuing a session, scope a session to -one task, not one call — **same task → same session, new task → new -session, for all sub-agents alike**: implement, review, and test for the -same task continue a single session instead of each starting cold. A fresh -session per reviewer/tester would mean re-reading every file from scratch -on every dispatch — that burns the context the reuse exists to save. +one task **per role**, not one call — **same task and same role → same +session, another role → its own session, new task → new sessions**. A +builder retry continues the builder's session; the reviewer and tester each +start fresh and reuse only their own on a later pass or rerun. One session +shared by implement, review and test looked cheaper but broke in practice: +a tool that pins a session's tool set to the agent that opened it left the +tester unable to write its results, and the reviewer inherited the +builder's reasoning instead of judging the diff. Record whatever session identifier the tool's dispatch prints, and pass it on subsequent calls for the same task. @@ -303,3 +313,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 12bc250..fe1676f 100644 --- a/skills/dev-team-generator/reference/lessons-learned.md +++ b/skills/dev-team-generator/reference/lessons-learned.md @@ -437,16 +437,19 @@ fewest rules about removing it. of internal, and give the human something concrete to push back on other than the planner's own framing. -## 21. Session scope is one task, not one sub-agent - -When a dispatch tool supports session continuity it is tempting to give -every dispatched role a fresh session "for independence". That trades a -real cost for a partial benefit: a fresh reviewer/tester must re-read every -file the implementer already read — on a large implementation that burns -more context than the independence is worth. The rule that held up: **same -task → same session, new task → new session, for all sub-agents alike.** -The implementer, reviewer, and tester of one task share its single -session; nothing carries across tasks. +## 21. Session scope is one task per role + +This lesson was first written the other way round — "same task → same +session for all sub-agents alike", on the theory that a fresh reviewer or +tester re-reading every file costs more than the independence is worth. It +did not hold up. On opencode v2 a session pins the tool set of the agent +that opened it: a tester dispatched into the builder's session reported +"file-edit tool is not exposed" twice and could not record its results, +while a fresh session wrote them first time. A shared session also handed +the reviewer the builder's in-session reasoning instead of just the diff. +The rule that held up: **same task and same role → same session; another +role → its own session; new task → new sessions.** Each role's id lives in +its own state-file field, and only that role's retry reuses it. ## 22. Track classifications with their decision source @@ -569,7 +572,54 @@ path before its project integration exists. stamped scaffold and confirming a flag-free run restores only those files, preserves the stamp, and does not retrigger first-run behavior. -## 29. A structural checker that flattens nested config lies, and a scratch copy is not the artifact +## 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. + +## 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. + +## 33. A structural checker that flattens nested config lies, and a scratch copy is not the artifact Two related traps surfaced while chasing a suspected permission-enforcement bug: a "does this key repeat?" script written against nested config @@ -599,7 +649,7 @@ under test. shortcut as unconfirmed until re-run against the real file with a structure-aware check. -## 30. Direct lead support needs an adapter, not another copy of the pipeline +## 34. Direct lead support needs an adapter, not another copy of the pipeline - A tool being able to run workers does not make it a supported lead. Supply its native entrypoints, a planner, approval routing, launcher/authentication, @@ -627,7 +677,7 @@ under test. Use least privilege, disclose the boundary, and verify resolved rules and refused actions against the exact generated artifact in a fresh runtime. -## 31. Hosting an existing lead is not launching or approving a pipeline +## 35. Hosting an existing lead is not launching or approving a pipeline - A terminal/workspace integration must adopt a user-started lead, not require its own launcher or silently create a replacement conversation. Make layout, diff --git a/skills/dev-team-generator/reference/state-file-example.md b/skills/dev-team-generator/reference/state-file-example.md index bb559ae..e7e4eeb 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,7 +13,8 @@ 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 -**Dispatch session id:** none yet — set after the first dispatch call, reused for every later call on this task +**Planner thread id:** none yet — when the planner has a separate thread, record/reuse it for corrections on this task +**Dispatch session ids:** builder none yet · reviewer none yet · tester none yet — each role's first dispatch on this task starts its own session; only that role's retry reuses it **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 **Test-fix loops:** 0 / 2 @@ -154,11 +155,13 @@ reads between steps instead of the whole file.> re-derive "what's the state of this task" by reading the whole file — one line, updated by whoever finishes a step, is the entire between-steps read. -- **Dispatch session id** exists only because some tools support continuing - a session across multiple dispatches within one task (see - `flow-example.md`'s "Dispatch-tool session policy") — if the tool(s) in - use don't have that concept, drop this field rather than leaving a - perpetually-empty one. +- **Dispatch session ids** exist only because some tools support continuing + a session across dispatches within one task (see `flow-example.md`'s + "Dispatch-tool session policy"). One id per role, never one shared id: a + shared session handed the reviewer the builder's reasoning and, on a tool + that pins a session's tool set, left the tester unable to write. If the + tool(s) in use don't have that concept, drop the field rather than leaving + it perpetually empty. - **Wide-auto-approve mode** exists specifically so this decision is recorded per-task, not inferred from a dispatch command someone has to go find — see `lessons-learned.md` entry 1 for why this can never default 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/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 b5ec177..9c25be3 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 @@ -44,18 +47,18 @@ Ask these up front rather than guessing — they shape every generated file: --test-dir "" ``` -It writes `.claude/agents/`, `.opencode/agent/`, `.opencode/agents/`, -`.opencode/commands/`, `.claude/commands/{feature.md, +It writes `.claude/agents/`, `.opencode/agents/`, `.opencode/commands/`, +`.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 -`scripts/{oc.sh,team.sh,verify-state.sh,verify-spec.sh,promote-findings.sh}` +`.pipeline/{TEMPLATE.md,.toolkit-version}`, and +`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. 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 @@ -107,6 +110,9 @@ user at `SYSTEM.md` at the toolkit root. prefers Claude, then Codex, then OpenCode. 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..690ba5d 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 @@ -15,8 +15,12 @@ with a one-line why; the lead asks the human only when genuinely torn> **Class decided by:** agent | human **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 -**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. +**OpenCode builder session id:** none yet — set from the builder's first `oc.sh` call; reused only by a builder retry on this task. Never another role's session. +**OpenCode reviewer session id:** none yet — set from the OpenCode reviewer's first call (a fresh session, never the builder's); reused only by its later passes on this task. Stays `none` when the reviewer is a Claude or Codex subagent. +**OpenCode tester session id:** none yet — set from the OpenCode tester's first call (a fresh session, never the builder's); reused only by a tester retry on this task. Stays `none` when the tester is a Codex subagent. +**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 +**Codex tester thread id:** none yet — set after the first dispatch when the tester is a Codex subagent (`tester_model: codex/…`); reused only for tester retries on this task +**OpenCode auto mode:** off — only meaningful when Implementer is `builder`. On means every builder `oc.sh` call for this task passes `--auto`. Set at spec approval: `on` without asking when `.pipeline/.toolkit-version` says `builder_auto: on` (a standing user decision), otherwise the user's answer to the spec-time question. Never turned on mid-task. **Review loop count:** 0 / 2 **Test-fix loops:** 0 / 2 **Spec bounces:** 0 / 1 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..c1dcef8 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. @@ -51,11 +54,33 @@ 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 `.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 + `.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 @@ -64,13 +89,28 @@ 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. +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 @@ -82,8 +122,8 @@ 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 - in `.opencode/agent/*.md`). The lead's job is to **verify placement and + `.pipeline/T-.md` / `.pipeline/T-.diff` (see each role's write scope + 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 @@ -117,7 +157,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 +165,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 @@ -175,17 +215,26 @@ call, not yours to default either way — a large task run unsplit under `--auto` compounds the risk of both (a wide, hard-to-review diff and unattended permission approval across it) at once. -**In the same message, also ask whether to enable OpenCode's `--auto` for -this task** — only relevant if `builder` ends up implementing it below. +**OpenCode's `--auto` for builder dispatches — read the manifest first.** `--auto` auto-approves any permission not explicitly `deny`d in -`builder.md`'s own permission block (its `deny` entries — `rm -rf /*`, -`sudo *`, `curl *` — still block; verified live, this is not a blanket -bypass), which is what ends the "hangs on an un-allowlisted bash command, -someone notices, widens the allowlist, retries" cycle. It's OpenCode's own -flag, and its own `--help` calls it "dangerous" — default is off, and stays -off for this task unless the user says yes here. Record the answer in -*OpenCode auto mode* in the state file regardless of which implementer is -actually chosen. +`builder.md`'s own permission block, which is what ends the "hangs on an +un-allowlisted bash command, someone notices, widens the allowlist, retries" +cycle. It's OpenCode's own flag, and its own `--help` calls it "dangerous". + +- `builder_auto: on` in `.pipeline/.toolkit-version` is a **standing user + decision**: set *OpenCode auto mode* to `on` and do not ask again per task. +- `builder_auto: ask` (the default, or no key) means **ask in the same + spec-approval message** — only relevant if `builder` ends up implementing. + Off unless the user says yes here. + +Either way, record the value in *OpenCode auto mode* regardless of which +implementer is chosen. **What `--auto` does not survive is exactly that +block's `deny` list** — read it from `builder.md` and name it when you ask, +rather than gesturing at it. Nothing outside it is protected: a command that +matches no `deny` falls to the block's own `ask`, and `--auto` approves it. +It waives nothing else: spec approval and merge consent are workflow gates, +and a sandbox runtime approval is a separate control. A user asking for a +task to run without it overrides the standing decision; log that. **Task class — the model proposes, the human hears only when it's uncertain.** The planner fills *Task class* (`routine` | `standard` | @@ -199,7 +248,22 @@ permissions, or budgets mid-task. ## 2 — Implement -Once approved, set Status to `spec-approved` and record who implements: +**Give the task its own branch before any implementer runs.** A task's diff +has to be readable on its own, and the merge gate at step 5 has to be able to +propose exactly one PR: + +```bash +git switch -c "T--" +``` + +`` is the kebab-case tail of the state file's title. Projects with a +branch-prefix convention keep it (`codex/T--`). If the branch +already exists, `git switch` onto it — never re-create it, and never carry one +task's work onto another's branch. Cut it from an up-to-date base. This is a +lead action: no implementer permission block grants branch creation. + +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,18 +271,18 @@ 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 - file is `on` — i.e. only if the user said yes to it at spec approval - above. Never add it on your own judgement mid-task, even after a timeout; - go back and ask instead. + Add `--auto` to that command (and every builder retry) only if *OpenCode + auto mode* in the state file is `on`. Never add it on your own judgement + mid-task, even after a timeout, and never to a non-builder role. - This is the task's first `oc.sh` call, so it starts a new session — read + This is the builder'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. + builder session id*. Only a builder retry reuses it; review and test start + their own (see the session policy below). - **`senior-dev`** (Claude subagent, `__CLAUDE_MODEL__`) — when the user explicitly asks for Claude to implement instead, for this task. @@ -278,31 +342,31 @@ rate limit, usage cap, quota, or `429`: ### OpenCode session policy -**Session scope is one feature (one `T-`), not one call.** Same task -→ same session; new task → new session. This applies to **all** sub-agents -alike: implement, review, and test for the same task continue a single -OpenCode session instead of each starting cold, and no sub-agent ever -carries a session across tasks. A fresh session per reviewer/tester would -mean re-reading every file from scratch on every dispatch — that burns the -context the reuse exists to save. - -- The **first** `oc.sh` call on a task (implement) omits `--session` — that - 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`. -- Every **later** call for the same task (review, test, and any retry) - passes `--session ` read from that field. +**Session scope is one task per role, not one call and not one task.** Same +task and same role → same session; another role → its own session; new task +→ new sessions and a new branch. Reuse exists so a *retry* does not re-read +every file from scratch. It never crosses roles and never crosses tasks. + +Each OpenCode role records its own id in its own state-file field — +*OpenCode builder session id*, *OpenCode reviewer session id*, *OpenCode +tester session id*: + +- A role's **first** `oc.sh` call on a task omits `--session` — that starts a + new session. Every call prints `oc.sh: session=ses_...` to stderr on any + outcome; copy it into that role's field. +- A role's **retry** (a CHANGES_REQUESTED or test-fix loop for the builder, a + second pass for the reviewer, a rerun for the tester) passes `--session` + read from **that role's** field. Never hand one role another role's id. - Do not pass `--fork`. -**Accepted tradeoff:** because review continues the same session, the -reviewer sees the implementer's in-session reasoning trace, not just the -diff — a partial dent in independence beyond switching model/vendor. This -does not remove the requirement to switch the reviewer to a different -model family below. Weigh this against the token cost it's meant to save: -the reviewer inherits the full contents of every file the implementer -read, which can outweigh the saving on a large implementation. Measure it -on the first task you run this way rather than assuming it's a net win. +**Why not one shared session per task** (this toolkit's earlier policy): on +opencode v2 a session pins the tool set of the agent that opened it, so a +tester dispatched into the builder's session reported "file-edit tool is not +exposed" and could not record its results, while a fresh session wrote them +first time. A shared session also handed the reviewer the builder's +in-session reasoning, not just the diff — a dent in the independence the +vendor split exists for. A fresh reviewer reads `.pipeline/T-.diff` and +the state file on disk, like every subagent reviewer already does. On a **timeout** (exit 124), `oc.sh` already aborted the server-side turn before it exited — verified live: the session's message shows a @@ -310,22 +374,67 @@ 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: `curl -X POST $OC_SERVER/session//abort`.) +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-.