# Autonomy P1 — Foundations Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use /ship (recommended) or /executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Land the foundational layer every later phase builds on: config layering (X10), full taxonomy rev, wrapper-contract rev, engine preflight gate (A3), `review.passed` + `task.usage` journaling, commit trailers (X9), version-skew + secret redaction (I9/I10).

**Architecture:** Pure additions to the existing wave engine — no daemon, no resolver yet. Config becomes one merge chain (`resolveConfig`); taxonomy gains all 9 new classes in one rev; the wrapper contract rev is one atomic schema+wrappers change per `spec/WRAPPER-CONTRACT-AMENDMENTS.md`; preflight becomes an engine verb instead of skill prose.

**Tech Stack:** Node (engine), bash (wrappers), JSON Schema.

**Sequencing:** After `autonomy-p0-chaos-scaffold` lands. Handoff plan PART IV P1 + phase-mapping deltas (PART VI/VII/VIII). Read `spec/IMPLEMENTER-NOTES.md` FIRST. `src/runner.js` is still a monolith (split is P2a) — all runner-touching tasks serialize into separate waves by construction.

---

## Wave Plan

| Wave | Tasks | Files touched | Safe to parallelize? |
|------|-------|---------------|----------------------|
| 1 | Task 1, Task 2, Task 3 | src/config.js · src/fail-taxonomy.js · spec/events.schema.json, presets/_validate.mjs, presets/adapters.json, wrappers/ | ✅ no overlap |
| 2 | Task 4 | src/runner.js, bin/runplan | single task |
| 3 | Task 5 | src/runner.js | single task |
| 4 | Task 6 | src/runner.js | single task |
| 5 | Task 7 | src/runner.js, lib/journal.sh | single task |
| 6 | Task 8 | test/chaos/scenarios/ | single task |
| 7 | Task 9 | wrappers/pi.sh, presets/adapters.json | single task |

---

### Task 1: X10 config layering

**Wave:** 1 · **Blocks:** Task 4 · **Blocked by:** —

**Files:**
- Create: `src/config.js` — sole config resolution module + colocated `src/config.test.js`.

**Contract (handoff plan PART VIII X10):**
- Signature: `resolveConfig(cli, planMeta, repoRoot) -> mergedConfig`
- Merge chain, later wins: engine defaults → `~/.harness/config.json` → `<repo>/.harness.json` → plan `meta` → CLI flags.
- Unknown key at any layer → error naming the layer (fail-closed). Protected-path hard defaults non-removable at every layer.
- Provenance: merged result retains per-key source; `runplan config --show` (wired in Task 4) prints `key: value  (from <layer>)`.

**Behavior:** Schema-validated per layer; missing optional files = empty layer, malformed file = error. No consumer reads env/config files directly after this plan (grep-enforceable).

**Acceptance:**
- Run: `node --test src/config.test.js`
- Expected: PASS — table-driven merge incl. unknown-key rejection + provenance

- [ ] Write tests from contract · [ ] Implement · [ ] Acceptance green · [ ] Commit

### Task 2: Taxonomy rev — all 9 new classes

**Wave:** 1 · **Blocks:** Task 4 · **Blocked by:** —

**Files:**
- Modify: `src/fail-taxonomy.js` — one rev adding `FAIL_CLASSES` entries + RULES patterns.

**Contract:** New classes (handoff plan P1, exact names): `task-stalled`, `agent-idle`, `land-failed`, `land-blocked-unreviewed`, `protected-path-touched`, `provider-quota`, `budget-exceeded`, `artifacts-missing`, `adapter-contract-violation`. Band/scope/rung per their defining sections: X5 (`provider-quota`: transport band, NOT task-attributed, NOT fixer-repairable), V1 (`protected-path-touched`: prevent band, NOT fixer-repairable), X8 (`artifacts-missing`: gate band, fixer-repairable), V2 (`budget-exceeded`: task-scoped quarantine), I7 (`adapter-contract-violation`: attributed to ADAPTER). `spec/RESOLVER.md` policy table rows must resolve for every class — cross-check names verbatim.

**Acceptance:**
- Run: existing taxonomy tests + new cases (`bash run-tests.sh` taxonomy section)
- Expected: each new class classified from a representative message fixture; no existing classification changes

- [ ] Tests from spec tables · [ ] Implement · [ ] Acceptance · [ ] Commit

### Task 3: Wrapper contract rev

**Wave:** 1 · **Blocks:** Task 5 · **Blocked by:** —

**Files:**
- Modify: `spec/events.schema.json`, `presets/_validate.mjs`, `presets/adapters.json`, every wrapper in `wrappers/`.

**Contract:** `spec/WRAPPER-CONTRACT-AMENDMENTS.md` is canonical — apply sections A (usage event + `metered` flag), B (quota channel, `errorKind`, exit-75 supersession note), C (transcript tee via `$HARNESS_TRANSCRIPT_PATH`), D (`north` dialect + `_validate.mjs` cross-check) as ONE contract rev. Tests per that spec's Tests section.

**Acceptance:**
- Run: `node presets/_validate.mjs && bash run-tests.sh` (wrapper/schema sections)
- Expected: amendment-spec test list green; pre-amendment fixtures still validate (additive)

- [ ] Tests from amendments spec · [ ] Implement · [ ] Acceptance · [ ] Commit

### Task 4: Preflight gate (A3) in engine

**Wave:** 2 · **Blocks:** Task 5 · **Blocked by:** Task 1, Task 2

**Files:**
- Modify: `src/runner.js`, `bin/runplan` — new verbs `preflight <slug>` and `config --show`; launch-path integration.

**Contract (handoff plan PART II A3):**
- `runplan preflight <slug>`: exit 0 = clean; non-zero lists every blocker: OPEN `gated` records, unstamped/INVALID `land_mode`, missing preset, unresolved `requires_decision` links.
- Launch path: `--unattended` flag parsed and REFUSES to start on dirty preflight (attended mode prints findings, existing behavior otherwise unchanged).
- All config reads via `resolveConfig` (Task 1).

**Behavior:** Preflight is read-only — never mutates plan or journal. Findings machine-readable on stdout (one JSON line per finding: `{severity, id, rule, message}`).

**Acceptance:**
- Run: chaos scenario for preflight (`test/chaos/run.sh` — plan-with-OPEN-gated fixture)
- Expected: dirty plan → non-zero listing the gated id; clean plan → exit 0; `--unattended` refusal asserted

- [ ] Tests · [ ] Implement · [ ] Acceptance · [ ] Commit

### Task 4b: shared `task_state_seq` helper (root-cause refactor)

**Wave:** 3 · **Blocks:** Task 5 · **Blocked by:** Task 4

**Why:** `test/runner-integration.sh` (and `test/self-healing-integration.sh`, which sources it) has 24+ duplicated inline `jq 'select(.task==X) | .state'` filters, already inconsistent (some carry `(.kind|not)`, most don't). Every un-guarded copy breaks the moment its scenario emits a `kind`-tagged additive record — which Task 5 introduces. The fixer role is scope-blocked from editing test files, so any assertion the implement pass misses can never be fixer-repaired and always ends in quarantine. User-authorized standalone task to fix the class before Task 5 lands, rather than patch call sites piecemeal per recurrence.

**Files:**
- Modify: `test/runner-integration.sh` — add `task_state_seq <journal> <task>` (comma-joined `.state` sequence, always excludes `kind`-tagged lines via `(.kind|not)`); replace every inline occurrence with a call to it.
- Modify: `test/self-healing-integration.sh` — same replacement for any inline occurrences there.

**Acceptance:**
- `grep -nE 'select\(\.task==[^)]*\)[^|]*\| *\.state' test/runner-integration.sh test/self-healing-integration.sh` returns zero matches outside the helper's own definition.
- `bash test/runner-integration.sh` and `bash test/self-healing-integration.sh` run standalone with zero FAIL.

- [ ] Tests · [ ] Implement · [ ] Acceptance · [ ] Commit

### Task 5: `review.passed` + `task.usage` journaling

**Wave:** 3 · **Blocks:** Task 6 · **Blocked by:** Task 3, Task 4b

**Files:**
- Modify: `src/runner.js` — review path + attempt-completion path.
- Modify: `lib/journal.sh`, `lib/test-journal.sh` — additive-record support in the shell writer/reader + coverage.
- Modify: `src/test/runner-transcript.test.js`, `test/runner-integration.sh` — new-record tests using the Task 4b `task_state_seq` helper (do not reintroduce raw inline filters).

**Contract:**
- `review.passed {task, stage}` appended by the runner's review path on each passing review stage (handoff plan P1; the P2b land gate consumes it — journal, not memory).
- `task.usage {attemptKind: implement|fix|review, adapter, ...usage}` journaled per attempt from the wrapper `usage` event (amendments spec § A); `usage.unknown` journaled as-is.
- Journal kinds/payloads are frozen public API (`spec/IMPLEMENTER-NOTES.md` invariant 3) — additive only.
- Additive records (`kind` present, `state` absent) are not lifecycle-state transitions — use Task 4b's `task_state_seq` helper for any new `.state`-sequence assertion.

**Acceptance:**
- Run: chaos clean-run scenario
- Expected: journal contains `review.passed` per reviewed task per stage + one `task.usage` per attempt with `attemptKind`

- [ ] Tests · [ ] Implement · [ ] Acceptance · [ ] Commit

### Task 6: X9 commit trailers

**Wave:** 4 · **Blocks:** Task 7 · **Blocked by:** Task 5

**Files:**
- Modify: `src/runner.js` — engine commit sites + post-implement trailer check.
- Modify: `src/fail-taxonomy.js` — new `commit-trailers-missing` fail class, committed in THIS task (owns the class it introduces).
- Modify: `test/self-healing-integration.sh` — add the matching failure-injection coverage scenario for `commit-trailers-missing` in the same task (do not split into a prerequisite task — this companion belongs to this feature, not shared infra).
- Modify: `test/runner-integration.sh` — replace the `rg`-based routing self-check (added by Task 5's prerequisite) with a `grep -E` equivalent. `rg` is not guaranteed installed; do NOT add any runtime `rg` shim/fallback to `src/runner.js` to work around this — fix the test assertion itself.

**Contract (handoff plan PART VIII X9):** Trailers `Plan: <slug>`, `Task: <taskId>`, `Run: <runId>` on every engine/agent commit (`--trailer` at engine commit sites); post-implement gate verifies task-branch commits carry them — missing = fixable class (fixer amends). Merge commits get `Plan:`/`Run:`.

**Acceptance:**
- Run: chaos clean-run scenario, then `git log --grep 'Plan: '` in fixture
- Expected: every landed commit matched; trailer-less-commit fixture caught by gate
- `bash test/self-healing-integration.sh` reports `coverage includes commit-trailers-missing` as `ok`
- `bash test/runner-integration.sh` runs standalone with zero FAIL and does not invoke `rg`
- `grep -c rg src/runner.js` shows no new `spawnSync("rg"` / rg-shim-writing code added

- [ ] Tests · [ ] Implement · [ ] Acceptance · [ ] Commit

### Task 7: I9 version-skew + I10 secret redaction

**Wave:** 5 · **Blocks:** Task 8 · **Blocked by:** Task 6

**Files:**
- Modify: `src/runner.js`, `lib/journal.sh` — append boundary in both current writers (single-writer port is P2a; both writers get this now).

**Contract (handoff plan PART V):**
- I9: journal + runconfig carry `formatVersion`; engine REFUSES journals written by a newer version (escalate-style fatal error), older → explicit migration path only (v1 needs none yet — refusal side is the deliverable).
- I10: redaction pass at append boundary — env-derived denylist (values of `*_TOKEN`, `*_KEY`, `*_SECRET` env vars) + common token patterns; replaced with `[REDACTED]`.

**Acceptance:**
- Run: planted-fake-secret test (agent output containing a fake token) + newer-version journal fixture
- Expected: journal contains `[REDACTED]`, never the token; newer-version journal → fail-closed error, file untouched

- [ ] Tests · [ ] Implement · [ ] Acceptance · [ ] Commit

### Task 8: Ungate P1 chaos assertions

**Wave:** 6 · **Blocks:** — · **Blocked by:** Task 7

**Files:**
- Modify: `test/chaos/scenarios/` — flip P1-gated assertions live per `spec/CHAOS-SUITE.md` phase gate map.

**Acceptance:**
- Run: `bash run-tests.sh`
- Expected: full suite + all P0/P1 chaos assertions green, zero unexplained warnings

- [ ] Ungate, run, commit

### Task 9: `pi` engine wrapper + adapter registration

**Wave:** 7 · **Blocks:** — · **Blocked by:** Task 3

**Files:**
- Create: `wrappers/pi.sh`; Modify: `presets/adapters.json` (new adapter entry `id: "pi"`).

**Contract:** Implements the REVISED wrapper contract (`spec/WRAPPER-CONTRACT.md` + amendments A–D as landed by Task 3) for the Pi coding agent (`pi.dev`, `@earendil-works/pi-coding-agent`), driving Pi headless (non-interactive print/JSON mode — NEVER the TUI) against the `openai-codex` subscription provider:
- Fixed flag set `--workspace/--trust/--task-slug/--model/--timeout`; `--model` REQUIRED resolved (binding or flag) and pinned on run line + log line from ONE built array — Pi picks its own default otherwise.
- Precondition fail-closed: `pi` binary present AND `~/.pi/agent/auth.json` holds an `openai-codex` credential, else exit `3` (no dispatch). Auth check is read-only — NEVER trigger an interactive login flow.
- Foreground under `timeout -k 5`, stdin `/dev/null`; raw Pi event stream/JSON to per-run logfile (first lines = exact command + `%q` prompt); stdout = status line only.
- Exit taxonomy mapped: completion `0`; timeout `124`; usage error `2`; engine/auth down `3`; Pi provider rate-limit/quota → `75` with `resume_at` in status line when Pi reports reset time. Usage/metered event per contract amendment (Task 3 schema).
- Adapter entry: `{"id":"pi","wrapper":"wrappers/pi.sh","models":["gpt-5.5-high","gpt-5.5-medium","gpt-5.4-medium","gpt-5.4-low","gpt-5.4-mini-high","gpt-5.3-codex-spark-high"],...}` — model ids follow the codex adapter's `<model>-<effort>` naming; wrapper maps effort suffix to Pi's reasoning-effort setting.

**Acceptance:**
- Run: `bash wrappers/_contract-probe.sh wrappers/pi.sh && node presets/_validate.mjs`
- Expected: contract probe green (all exit-code branches exercised, incl. mocked auth-missing → 3 and mocked quota → 75); adapters.json validates; smoke dispatch of a trivial task through `--preset` binding `pi` completes with pinned model visible in logfile

- [ ] Tests (contract probe branches) · [ ] Implement · [ ] Acceptance · [ ] Commit
