@aperant/framework 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +12 -0
  3. package/dist/cli/commands/init.d.mts.map +1 -1
  4. package/dist/cli/commands/init.mjs +67 -27
  5. package/dist/cli/commands/init.mjs.map +1 -1
  6. package/dist/cli/design/frontmatter-schema.d.mts +4 -4
  7. package/dist/cli/install/cli-to-install-root.d.mts.map +1 -1
  8. package/dist/cli/install/cli-to-install-root.mjs +0 -2
  9. package/dist/cli/install/cli-to-install-root.mjs.map +1 -1
  10. package/dist/cli/install/runtime-detect.d.mts +2 -6
  11. package/dist/cli/install/runtime-detect.d.mts.map +1 -1
  12. package/dist/cli/install/runtime-detect.mjs +8 -3
  13. package/dist/cli/install/runtime-detect.mjs.map +1 -1
  14. package/dist/cli/install/transforms/pi.d.mts +6 -0
  15. package/dist/cli/install/transforms/pi.d.mts.map +1 -0
  16. package/dist/cli/install/transforms/pi.mjs +37 -0
  17. package/dist/cli/install/transforms/pi.mjs.map +1 -0
  18. package/dist/cli/personas/sidecar.d.mts +1 -1
  19. package/dist/cli/roadmap/lifecycle.d.mts +1 -1
  20. package/dist/cli/roadmap/rollup.d.mts +2 -2
  21. package/dist/plugin/.claude-plugin/plugin.json +2 -1
  22. package/dist/plugin/agents/apt-executor.md +12 -0
  23. package/dist/plugin/agents/apt-team-docs-narrator.md +11 -0
  24. package/dist/plugin/skills/apt/SKILL.md +3 -2
  25. package/dist/plugin/skills/apt-execute/SKILL.md +13 -0
  26. package/dist/plugin/skills/apt-fan-out/SKILL.md +124 -0
  27. package/dist/plugin/skills/apt-pr-review/SKILL.md +8 -1
  28. package/dist/plugin/skills/apt-review/SKILL.md +4 -0
  29. package/dist/plugin/skills/apt-run/SKILL.md +20 -5
  30. package/dist/plugin/skills/apt-spar/SKILL.md +12 -9
  31. package/dist/plugin/skills/apt-verify/SKILL.md +11 -0
  32. package/dist/plugin/skills/apt-watch-ci/SKILL.md +2 -2
  33. package/dist/schemas/quick-task.d.ts +17 -17
  34. package/package.json +1 -1
  35. package/skills/apt/SKILL.md +3 -2
  36. package/src/cli/commands/init.mjs +66 -27
  37. package/src/cli/install/cli-to-install-root.mjs +0 -2
  38. package/src/cli/install/runtime-detect.mjs +8 -3
  39. package/src/cli/install/transforms/pi.mjs +37 -0
@@ -209,7 +209,7 @@ export type PersonaEvidenceAnchor = {
209
209
  source: "codebase" | "project_md" | "app_wizard";
210
210
  file: string;
211
211
  line_range?: string | undefined;
212
- confidence?: "high" | "low" | "medium" | undefined;
212
+ confidence?: "medium" | "high" | "low" | undefined;
213
213
  };
214
214
  export type Persona = {
215
215
  id?: string | undefined;
@@ -45,7 +45,7 @@ export function applyLifecyclePhaseSideEffect(targetDir: any, task: any, spec: a
45
45
  commit: string | null;
46
46
  rollup: {
47
47
  before: any;
48
- after: "completed" | "in-progress" | "planned" | "blocked" | null;
48
+ after: "planned" | "completed" | "blocked" | "in-progress" | null;
49
49
  changed: boolean;
50
50
  milestone_id: any;
51
51
  };
@@ -16,7 +16,7 @@ export function rollupMilestoneStatus(milestone: object): "completed" | "in-prog
16
16
  */
17
17
  export function applyRollup(roadmap: any, milestoneId: any): {
18
18
  before: any;
19
- after: "in-progress" | "completed" | "planned" | "blocked" | null;
19
+ after: "planned" | "completed" | "blocked" | "in-progress" | null;
20
20
  changed: boolean;
21
21
  milestone_id: any;
22
22
  };
@@ -31,7 +31,7 @@ export function applyRollup(roadmap: any, milestoneId: any): {
31
31
  export function writeRollup(projectDir: any, scope: any, milestoneId: any): {
32
32
  committed: boolean;
33
33
  before: any;
34
- after: "in-progress" | "completed" | "planned" | "blocked" | null;
34
+ after: "planned" | "completed" | "blocked" | "in-progress" | null;
35
35
  changed: boolean;
36
36
  milestone_id: any;
37
37
  };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aperant",
3
- "version": "0.7.3",
3
+ "version": "0.8.1",
4
4
  "description": "AI coding framework — composable skills for planning, executing, verifying, and reviewing code with any LLM provider. Works as Claude Code commands, Codex tasks, or programmatically.",
5
5
  "author": {
6
6
  "name": "Mikalsen AI",
@@ -34,6 +34,7 @@
34
34
  "skills/apt-discuss",
35
35
  "skills/apt-docs",
36
36
  "skills/apt-execute",
37
+ "skills/apt-fan-out",
37
38
  "skills/apt-improve",
38
39
  "skills/apt-mockup",
39
40
  "skills/apt-pause",
@@ -46,6 +46,18 @@ node packages/framework/bin/apt-tools.mjs gate check . --task-id {task-id} --ski
46
46
 
47
47
  If `status == "fail"`, STOP. Report the failed gates verbatim and exit — do NOT auto-apply `--force`. Forcing is a human decision; the user can re-invoke with `--force --reason "<explanation>"` to bypass (the override is logged to `.aperant/events/{today}.jsonl`).
48
48
 
49
+ ## 0a. Fan-out worker identity (FRAMEWORK-BUG-033 defect 3)
50
+
51
+ If your spawn prompt contains both `batch_id` and `task_id` (i.e. you are a fan-out worker subagent dispatched by `/apt:fan-out`), export `APT_AGENT_ID` in EVERY `Bash` invocation that runs `apt-tools ...`:
52
+
53
+ ```bash
54
+ APT_AGENT_ID=fanout-${batch_id}-${task_id} node packages/framework/bin/apt-tools.mjs <command> ...
55
+ ```
56
+
57
+ This pins your `team-status/{agentId}.json` file across short-lived CLI invocations. Without it, each `apt-tools` call gets a fresh `hostname-pid` agentId (per `agent-identity.mjs:86`), the previous pid dies, and `listTeamStatus({ includeStale: false })` filters healthy workers out between invocations — the team-status dashboard flickers into emptiness instead of showing 4 stable fan-out workers.
58
+
59
+ Non-fan-out invocations of this agent receive no `batch_id` and skip this export — the legacy `hostname-pid` path remains correct for single-task work.
60
+
49
61
  ## 1. Load Context
50
62
 
51
63
  1. Read `CLAUDE.md` and `AGENTS.md` if they exist
@@ -72,6 +72,17 @@ If the parent spawned you without a mode you recognize, write an error line to `
72
72
  rendered table. If present, REPLACE the content between anchors (never
73
73
  append — this is how re-runs stay idempotent).
74
74
  7. Call `apt-tools roadmap sync . --reason 'narrator:phase-ship:<phase_id>'`.
75
+ 8. **Slice 5 / AC14 / ADR-0006 dual-write.** Write the rendered prose
76
+ block to a temp file (e.g. `mktemp -t narrator-prose-XXXXXX.md`), then
77
+ run
78
+ `apt-tools roadmap convex-mirror . --scope <scope> --phase <phase_id> --anchor-id phase-ship:<phase_id> --prose-path <tempfile>`
79
+ (add `--merged-pr <pr-number>` when known). The markdown write from
80
+ step 7 is canonical (ADR-0006); this step is the Convex mirror.
81
+ Fail-soft contract:
82
+ - exit 0 with `status:ok` or `status:queued` → continue normally
83
+ - exit non-zero with `status:error` → append the error message to
84
+ `.aperant/digests/errors.log` and continue (do NOT block on Convex
85
+ failures; the in-process cron drainer reconciles within 5 min)
75
86
 
76
87
  ## Mode 2: `decision-merge` — merge a recorded decision into the roadmap
77
88
 
@@ -41,8 +41,9 @@ you must follow when classifying.
41
41
  - **Config:** `.aperant/config.json` — project preferences
42
42
  - **State:** `.aperant/state.json` — execution state
43
43
  - **Task directories:** `.aperant/tasks/{task-id}/` — each task gets isolated artifacts
44
- - **Codex repo skills:** `.agents/skills/apt*/` — native Codex discovery surface installed by `apt-tools init`
45
- - **Claude agents:** `.claude/agents/apt-*.md` — specialized agent definitions for heavy workflows
44
+ - **Claude surface:** `.claude/skills/`, `.claude/agents/`
45
+ - **Codex surface:** `.codex/commands/`, `.codex/agents/`
46
+ - **Pi surface:** `.pi/prompts/`, `.pi/skills/`
46
47
  </your_environment>
47
48
 
48
49
  <mandatory_first_step>
@@ -453,6 +453,19 @@ After all subtasks are completed:
453
453
  node packages/framework/bin/apt-tools.mjs commit "build: all subtasks complete" --files implementation_plan.json build-progress.json
454
454
  ```
455
455
 
456
+ ### Mark lifecycle phase as `verifying`
457
+
458
+ Flip the task's lifecycle phase at the success-end boundary so the next stage
459
+ (`/apt:verify`) finds the task in the correct from-state. Mirror the same CLI
460
+ shape as the start-boundary call in Section 2 — quick tasks (no phase linkage)
461
+ silently no-op, and the update is idempotent so it is safe to re-run on
462
+ `--continue`. Skip this step if any subtask ended in `"blocked"` — only the
463
+ all-subtasks-completed branch should advance the phase.
464
+
465
+ ```bash
466
+ node packages/framework/bin/apt-tools.mjs task update . --id {task-id} --lifecycle-phase verifying
467
+ ```
468
+
456
469
  Output:
457
470
  ```
458
471
  Execution complete:
@@ -0,0 +1,124 @@
1
+ ---
2
+ name: apt:fan-out
3
+ description: "Cross-task fan-out — spawn N apt-executor workers from one conductor terminal, each in its own worktree"
4
+ apt-skill-version: {{APT_VERSION}}
5
+ stage: execute
6
+ intent: x-fan-out
7
+ when_to_use: "You have N pre-planned, independent tasks (no file overlap, no cross-dependencies) and want to run them concurrently from a single host CLI session."
8
+ user_invocable: true
9
+ internal: false
10
+ spawns_agent: true
11
+ agent_name: "apt-executor"
12
+ task_context: require-existing
13
+ default_execution_mode: auto
14
+ execution_modes:
15
+ - auto
16
+ - step
17
+ allowed-tools: "Bash, Read, Write, Task"
18
+ argument-hint: "apt:fan-out --tasks <id,id,id> [--phases <m/p,m/p>] [--unblocked]"
19
+ gates: []
20
+ ---
21
+ <objective>
22
+ Run N pre-planned Aperant tasks concurrently from one conductor terminal. This skill is a thin orchestrator over the generalized wave engine (`unit_kind: "task"` path, see ADR-0003) and the existing primitives: `apt-tools worktree create --task`, `apt-tools lock claim`, `apt-tools event append`, and the `parallelization.max_tasks` quota. No daemon, no job queue, no in-skill scheduling. The host CLI's main thread is the conductor (ADR-0001); each spawned apt-executor subagent runs in its own worktree (ADR-0002).
23
+
24
+ This is the "I have 16 unblocked tasks in this phase, run them all" surface. It replaces the old workaround of opening 16 sibling terminals manually.
25
+ </objective>
26
+
27
+ <your_environment>
28
+ - **Working directory:** Project root (where you were invoked).
29
+ - **apt-tools path:** `node packages/framework/bin/apt-tools.mjs` (or `node .aperant/deps/node_modules/@aperant/framework/bin/apt-tools.mjs` when running from an install mirror).
30
+ - **Concurrency cap:** `parallelization.max_tasks` from `.aperant/config.json` (default 4, no paternalism cap per D-07). Read via `cli/config/parallelization.mjs:getMaxTasks(config)`.
31
+ - **Worker identity:** Each spawned worker exports `APT_AGENT_ID=fanout-${batch_id}-${task_id}` before any `apt-tools ...` invocation (per FRAMEWORK-BUG-033 defect 3). This pins their `team-status/{agentId}.json` rows across short-lived CLI calls.
32
+ - **Constitution:** Read `AGENTS.md` if it exists.
33
+ </your_environment>
34
+
35
+ <state_files>
36
+ ## State Files
37
+
38
+ **Reads:**
39
+ - `.aperant/state.json` — `active_tasks[*]` filtered when `--phases` or `--unblocked` resolves the task list.
40
+ - `.aperant/roadmap/{scope}/milestones.json` — milestone/phase membership for `--phases`.
41
+ - `.aperant/config.json:parallelization.max_tasks` — concurrency cap.
42
+
43
+ **Writes (indirectly via spawned workers):**
44
+ - `.aperant/team-status/fanout-${batch_id}-${task_id}.json` — one stable file per worker (per FRAMEWORK-BUG-033 defect 3 fix).
45
+ - `.aperant/state.json:active_tasks[*].assigned_to` — workers stamp their `APT_AGENT_ID` on claim (per FRAMEWORK-BUG-033 defect 2 fix).
46
+ - `.aperant/events/{today}.jsonl` — `task-claimed`, `wave-started`, `wave-completed` events.
47
+
48
+ **No filesystem state of its own.** The skill body is pure orchestration glue; all heavy lifting reuses existing primitives.
49
+ </state_files>
50
+
51
+ <process>
52
+
53
+ ## 1. Resolve the batch — canonical task ID list
54
+
55
+ The three input forms (`--tasks`, `--phases`, `--unblocked`) all resolve to a canonical `task_ids: [...]` list via the apt-tools helper:
56
+
57
+ ```bash
58
+ node packages/framework/bin/apt-tools.mjs fan-out resolve . --tasks T1,T2,T3,T4
59
+ # OR
60
+ node packages/framework/bin/apt-tools.mjs fan-out resolve . --phases m1/p1,m1/p2
61
+ # OR
62
+ node packages/framework/bin/apt-tools.mjs fan-out resolve . --unblocked
63
+ ```
64
+
65
+ Returns:
66
+ ```json
67
+ {
68
+ "status": "ok",
69
+ "command": "fan-out-resolve",
70
+ "task_ids": ["T1", "T2", "T3", "T4"],
71
+ "source": "tasks",
72
+ "batch_id": "fanout-20260519-abc123"
73
+ }
74
+ ```
75
+
76
+ If `--tasks` IDs fail the `/^[a-z0-9_-]+$/i` validation regex, the helper returns `status: error` and the skill body MUST abort.
77
+
78
+ ## 2. Ensure each task has a worktree
79
+
80
+ For each `task_id` in the batch:
81
+
82
+ ```bash
83
+ node packages/framework/bin/apt-tools.mjs worktree create --task ${task_id}
84
+ ```
85
+
86
+ (Skip if `state.active_tasks[task_id].worktree_path` is already set — the create command is idempotent.)
87
+
88
+ ## 3. Spawn N apt-executor subagents
89
+
90
+ Read the `parallelization.max_tasks` cap from `.aperant/config.json` (or default 4). Slice `task_ids` into batches of size `min(cap, task_ids.length)`. For each task in the current batch, dispatch via the host CLI's `Task` tool with `run_in_background: true`:
91
+
92
+ - `subagent: apt-executor`
93
+ - `prompt` includes:
94
+ - `batch_id` (from step 1)
95
+ - `task_id`
96
+ - The worktree path for this task
97
+ - A reminder: "Export `APT_AGENT_ID=fanout-${batch_id}-${task_id}` in every Bash invocation that runs `apt-tools` — see `packages/framework/agents/apt-executor.md` §0a."
98
+
99
+ The executor agent's `<process>` §0a already documents the env-export rule; the spawn prompt just has to seed `batch_id` and `task_id`.
100
+
101
+ ## 4. Poll team-status until all workers reach a terminal state
102
+
103
+ ```bash
104
+ node packages/framework/bin/apt-tools.mjs team list .
105
+ ```
106
+
107
+ Each worker's row uses its `APT_AGENT_ID=fanout-${batch_id}-${task_id}`. A worker is "done" when its corresponding task's `lifecycle_phase` is one of `closed`, `shipped-pending-merge`, or `blocked`.
108
+
109
+ ## 5. Report
110
+
111
+ When all workers reach a terminal state, emit one consolidated summary: which tasks closed (verdict + PR URL), which are awaiting merge, which blocked. Recommend `/apt:close-task --all` for the post-merge sweep.
112
+
113
+ </process>
114
+
115
+ ## Design references
116
+
117
+ - **ADR-0001** — Host CLI main thread is the conductor; no daemon, no in-skill queue.
118
+ - **ADR-0002** — One worktree per task; workers run independent feature branches.
119
+ - **ADR-0003** — Wave engine accepts `unit_kind: "task"` so cross-task fan-out reuses the same primitives as intra-task subtask waves.
120
+
121
+ ## Config keys
122
+
123
+ - `parallelization.max_tasks` (default 4) — concurrency cap for the spawn loop.
124
+ - `parallelization.max_agents` (default 4) — global agent quota; the spawn loop respects whichever cap is lower.
@@ -1110,6 +1110,8 @@ For `AUTHORSHIP_MODE == "owned"`, continue normally.
1110
1110
 
1111
1111
  AI agents have near-zero marginal cost per fix. The framework fixes **all** confirmed findings — critical, high, medium, and low — regardless of autonomy level. There is no severity gate and no "report for manual resolution" path. If a finding was confirmed by the validator, it gets fixed.
1112
1112
 
1113
+ **Hard rule — context budget is NOT a valid skip reason.** The orchestrator MAY NOT defer, drop, downgrade, or "punt for follow-up" any confirmed finding because its own context window feels heavy, because the run has already produced many fixes, or because it judges marginal value low. The query loop auto-compacts on its own — trust it. Per-finding fix work happens inside fix agents (separate context windows), so orchestrator pressure does not justify cutting their inputs. The only valid skip reasons are the ones enumerated in Key Principle #2 (`fixable: false`, critical finding without user approval, file assigned to another fixer). Any other skip is an unauthorized policy deviation — emit all confirmed findings to fixers, then let the audit gate adjudicate outcomes.
1114
+
1113
1115
  | Autonomy | Action |
1114
1116
  |----------|--------|
1115
1117
  | `< 2` | Show findings summary before fixing. Then proceed with auto-fix. |
@@ -1647,7 +1649,7 @@ node packages/framework/bin/apt-tools.mjs workflow destroy . --id ${REVIEW_ID}
1647
1649
  Agents do NOT assign confidence percentages. Either they have evidence (actual code snippet from Read tool) or they do not have a finding. The validator independently verifies every finding against real code.
1648
1650
 
1649
1651
  ### 2. Fix Everything Found
1650
- If it was found and confirmed, fix it. ALL severities. "This is minor" is NOT a valid skip reason. The only valid skip reasons are: `fixable: false`, critical findings without user approval, or files assigned to another fixer.
1652
+ If it was found and confirmed, fix it. ALL severities. "This is minor" is NOT a valid skip reason. **Neither is "my context window is heavy" or "I've already done a lot this run"** — orchestrator context pressure never authorizes dropping a confirmed finding (see Phase 6 → Principle: Fix Everything). The only valid skip reasons are: `fixable: false`, critical findings without user approval, or files assigned to another fixer.
1651
1653
 
1652
1654
  ### 3. Findings Flow Through Files
1653
1655
  Agents write to `.aperant/pr-reviews/{review-id}/findings/`. The orchestrator reads with the Read tool. Fix agents read finding files directly. This keeps the orchestrator context under 30%.
@@ -1711,6 +1713,11 @@ Fix agents self-report their outcome via a status line (`FIXED: X | FAILED: Y |
1711
1713
  **Wrong:** Skipping Phase 6 entirely when autonomy < 2.
1712
1714
  **Right:** Always fix all confirmed findings. At autonomy < 2, show findings summary first, then proceed with auto-fix. The only difference is whether findings are displayed before fixing — not whether they get fixed.
1713
1715
 
1716
+ ### Mistake 7a: Dropping Findings Because Orchestrator Context Feels Heavy
1717
+
1718
+ **Wrong:** Mid-Phase-6, the orchestrator decides "my window is getting heavy after 6 specialist rounds + Round 1 fixes — I'll skip the minor findings and report a partial fix." This is an unauthorized policy deviation, even when phrased as "judgment call" or "context-budget reasons."
1719
+ **Right:** Spawn fix agents for **every** confirmed finding regardless of orchestrator context state. Fix work happens in the fixer's window, not yours. The query loop auto-compacts when it needs to — trust it. If you genuinely cannot fit the spawn calls, that is a framework bug to file (not a license to ship a partial fix); emit all findings to fixers in the smallest batches possible and let the audit gate adjudicate. Severity, "diminishing returns," and your own token pressure are NOT skip authorities — only the three reasons in Key Principle #2 are.
1720
+
1714
1721
  ### Mistake 8: Forgetting to Update metadata.json
1715
1722
 
1716
1723
  **Wrong:** Writing review.json but leaving metadata.json stuck at `"status": "in-progress"`.
@@ -333,6 +333,10 @@ Scores: logic={n} security={n} quality={n} architecture={n} performance={n}
333
333
  Report: {task_dir}/review.json
334
334
  ```
335
335
 
336
+ `lifecycle_phase` remains at `reviewing` after /apt:review completes — this is
337
+ the from-state /apt:ship expects for its atomic `reviewing → shipped-pending-merge`
338
+ flip, so no further `task update --lifecycle-phase` call is needed here.
339
+
336
340
  **Always end with a "What's Next" recommendation:**
337
341
 
338
342
  ```
@@ -159,6 +159,10 @@ Execute the apt:execute workflow inline:
159
159
  d. Commit atomically
160
160
  e. Update subtask status to "completed"
161
161
  3. Write build-progress.json
162
+ 4. Flip lifecycle to `verifying` so Stage 3 finds the task in the correct from-state (idempotent; quick tasks no-op):
163
+ ```bash
164
+ node packages/framework/bin/apt-tools.mjs task update . --id {task-id} --lifecycle-phase verifying
165
+ ```
162
166
 
163
167
  **Checkpoint state:** `build-progress.json` is written after every subtask. That file is the durable resume point — do NOT self-estimate context and do NOT voluntarily exit. Keep executing until the plan is complete or a risk gate stops you. If the harness auto-compacts mid-pipeline, re-read `implementation_plan.json` + `build-progress.json` and continue from the next pending subtask. The `--continue` flag exists for human-initiated resumes (`/apt:pause` + new session), not for agent-initiated bailouts.
164
168
 
@@ -189,6 +193,10 @@ Execute the apt:verify workflow inline:
189
193
  6. Score on 4 dimensions (completeness, correctness, quality, coverage)
190
194
  7. Fix all issues found — every severity, not just critical (up to 4 iterations: first 2 discovery, last 2 verification-only)
191
195
  8. Write qa_signoff.json
196
+ 9. If `status` is `approved` or `approved-with-notes`, flip lifecycle to `reviewing` so Stage 4 (or `/apt:ship` when review is skipped) finds the task in the correct from-state. Skip this step entirely on `rejected`:
197
+ ```bash
198
+ node packages/framework/bin/apt-tools.mjs task update . --id {task-id} --lifecycle-phase reviewing
199
+ ```
192
200
 
193
201
  **Output:** qa_signoff.json with status
194
202
 
@@ -224,6 +232,8 @@ Execute the apt:review workflow inline (see `apt-review/SKILL.md`):
224
232
  4. Update task documentation — review.json, qa_signoff.json (re-score if fixes changed quality), build-progress.json
225
233
  5. Write review.json with final findings and assessment. The `docs_policy` at `.aperant/config.json.docs_policy` controls per-surface behavior; defaults in `packages/framework/templates/docs-policy-defaults.json`.
226
234
 
235
+ `lifecycle_phase` stays at `reviewing` after this stage — /apt:ship handles the next transition (`reviewing → shipped-pending-merge`).
236
+
227
237
  **Output:** review.json (written to task directory)
228
238
 
229
239
  **This is the task review, not the PR review.** It runs inline without sub-agents. For heavy multi-agent PR review, use `/apt:pr-review` separately.
@@ -269,11 +279,16 @@ node packages/framework/bin/apt-tools.mjs commit "pipeline: complete {task}" --f
269
279
 
270
280
  ## 8. Lifecycle transitions
271
281
 
272
- Lifecycle phase transitions are handled by the child skills themselves:
273
- `apt:execute` calls `task update --lifecycle-phase executing` (which flips a
274
- phase-linked task from `planned` `in-progress`), and `apt:ship` calls
275
- `task close --verdict approved` (which flips it to `completed`). The
276
- pipeline wrapper does not need to record or drain anything.
282
+ Lifecycle phase transitions are handled by the child skills themselves.
283
+ `apt:execute` flips `planned` `in-progress` at start (lifecycle = `executing`)
284
+ and at success-end (lifecycle = `verifying`). `apt:verify` flips lifecycle =
285
+ `reviewing` on the approved verdict (skipped on `rejected`). `apt:review`
286
+ leaves lifecycle at `reviewing` the from-state `/apt:ship` expects for its
287
+ atomic `reviewing → shipped-pending-merge` flip. `/apt:ship` then flips
288
+ `reviewing → shipped-pending-merge`, and `/apt:close-task` later flips
289
+ `shipped-pending-merge → completed` after the PR merges. The pipeline wrapper
290
+ does not need to record or drain anything beyond ensuring each child-skill
291
+ stage runs to its success-end.
277
292
 
278
293
  </process>
279
294
  </output>
@@ -90,7 +90,7 @@ Parse `$ARGUMENTS`. Extract:
90
90
  `[apt:spar] --rounds clamped to 3 (hard cap, see SPAR-05)`
91
91
  and proceed with N=3. If N<1, clamp to 1 and print:
92
92
  `[apt:spar] --rounds clamped to 1 (minimum)`
93
- - `--timeout-ms N` — per-partner Bash timeout in milliseconds, default 90000 (90s). Capped at 600000 (10m, the Bash tool's own ceiling).
93
+ - `--timeout-ms N` — per-partner Bash timeout in milliseconds, default 300000 (5 min). Capped at 600000 (10 min, the Bash tool's own ceiling).
94
94
  - `[topic]` — the remaining text after flags. If empty, frame the round from the **last N conversation turns** as the context.
95
95
 
96
96
  Also read (best-effort, skip silently if missing):
@@ -135,7 +135,7 @@ Do NOT proceed; self-sparring is sycophancy by construction (the host cannot obj
135
135
  2. **Rung 2 — Plugin shim** (`${CLAUDE_PLUGIN_ROOT}/scripts/<partner>-companion.mjs`). Codex only (claude / gemini have no plugin shim).
136
136
  3. **Rung 3 — Raw CLI on `$PATH`** (`codex exec`, `claude -p`, `gemini -m`).
137
137
 
138
- Walk the ladder in order; pick the first rung with `detected: true` (or `"runtime-host-only"` for MCP). **MCP rung has a hard timeout** — default 60000ms, configurable via `apt-spar.mcp_timeout_ms` in `.aperant/config.json`. Empirical: the Codex MCP `claude_code` tool has been observed to hang past 120s. On MCP timeout, **automatically fall through to rung 2 or rung 3** — do NOT fail the whole spar. Treat MCP timeout as "rung unavailable for this invocation."
138
+ Walk the ladder in order; pick the first rung with `detected: true` (or `"runtime-host-only"` for MCP). **MCP rung has a hard timeout** — default 900000ms (15 min), configurable via `apt-spar.mcp_timeout_ms` in `.aperant/config.json`. The MCP rung is a structured tool call, NOT a Bash invocation, so the Bash tool's 600000ms ceiling does NOT apply — the MCP rung can carry heavy-research partner runs (e.g. Claude Code partner doing multiple minutes of Read/Grep/Bash) where the Bash rungs would hit the host ceiling. On MCP timeout, **automatically fall through to rung 2 or rung 3** — do NOT fail the whole spar. Treat MCP timeout as "rung unavailable for this invocation."
139
139
 
140
140
  If all three rungs are unavailable (every rung `detected: false`, or MCP `runtime-host-only` timed out AND rungs 2+3 are `detected: false`), print:
141
141
 
@@ -163,13 +163,14 @@ You are the sparring partner in an apt:spar round. Your job:
163
163
  - Identify the strongest objection.
164
164
  - Cite specific evidence where possible (file:line, test output, doc reference).
165
165
  - Be specific, not generic — "this might fail" is not a finding; "this fails when X because Y" is.
166
+ - After your main objection, provide NEXT_WEAKEST_POINT: one additional independent objection that would still matter if your first point is resolved. This populates the next round's frame.
166
167
  ```
167
168
 
168
169
  The frame is one shot per round — no multi-turn conversation with the partner inside a single round.
169
170
 
170
171
  ## 5. Invoke Partner (with timeout)
171
172
 
172
- **Wrap every partner Bash call with an explicit timeout** (default 90000ms, override via `--timeout-ms`). The empirical reason: the Codex MCP `claude_code` tool has been observed to hang past 120s while the `claude -p` CLI returns in seconds. A hung partner CLI must NOT stall the spar loop.
173
+ **Wrap every partner Bash call with an explicit timeout** (default 300000ms (5 min), override via `--timeout-ms`). The empirical reason: heavy-research partner CLIs (especially Claude Code in `--with claude` mode) routinely spend multiple minutes doing genuine Read/Grep/Bash investigation per round, and a hung partner CLI must NOT stall the spar loop indefinitely.
173
174
 
174
175
  The table below shows the canonical command shape per (partner × rung); the actual invocation for shell-based rungs (plugin shim, raw CLI) MUST use the heredoc pattern in the next subsection — never raw double-quoted interpolation. The MCP rung is a structured tool call — no shell quoting concern applies.
175
176
 
@@ -182,19 +183,19 @@ The table below shows the canonical command shape per (partner × rung); the act
182
183
 
183
184
  | Partner | Rung | Command | Notes |
184
185
  |---|---|---|---|
185
- | codex | mcp | tool call: `mcp__codex-mcp__codex({prompt})` | Preferred. 60s timeout (configurable `apt-spar.mcp_timeout_ms`); on timeout fall through to plugin then CLI. |
186
+ | codex | mcp | tool call: `mcp__codex-mcp__codex({prompt})` | Preferred. 900000ms (15 min) timeout (configurable `apt-spar.mcp_timeout_ms`); on timeout fall through to plugin then CLI. |
186
187
  | codex | plugin | `node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" task "$(cat <<'APT_SPAR_PROMPT_EOF' ... APT_SPAR_PROMPT_EOF\n)"` | **NO `--write`. NO `--effort` override.** Per SPAR-04 — spar is conversation, not patching. |
187
188
  | codex | cli | `codex exec "$(cat <<'APT_SPAR_PROMPT_EOF' ... APT_SPAR_PROMPT_EOF\n)"` | Precedent: `apt-pr-review/SKILL.md:1084`. Verify with `codex --help`. |
188
- | claude | mcp | tool call: `mcp__claude-code-mcp__claude_code({prompt})` | Preferred. Same 60s timeout + fallthrough rule. |
189
+ | claude | mcp | tool call: `mcp__claude-code-mcp__claude_code({prompt})` | Preferred. Same 900000ms (15 min) timeout + fallthrough rule. |
189
190
  | claude | plugin | n/a | No plugin shim exists for Claude. Skip this rung. |
190
191
  | claude | cli | `claude -p "$(cat <<'APT_SPAR_PROMPT_EOF' ... APT_SPAR_PROMPT_EOF\n)" --output-format text` | Symmetric. Claude has no `--write` equivalent — asymmetric flags are a smell. |
191
- | gemini | mcp | tool call: `mcp__gemini-mcp__*({prompt})` | Preferred. Same 60s timeout + fallthrough rule. Any `mcp__gemini-mcp__*` tool satisfies the rung. |
192
+ | gemini | mcp | tool call: `mcp__gemini-mcp__*({prompt})` | Preferred. Same 900000ms (15 min) timeout + fallthrough rule. Any `mcp__gemini-mcp__*` tool satisfies the rung. |
192
193
  | gemini | plugin | n/a | No plugin shim exists for Gemini. Skip this rung. |
193
194
  | gemini | cli | `gemini -m <model-from-router.llm.providers.gemini-cli> "$(cat <<'APT_SPAR_PROMPT_EOF' ... APT_SPAR_PROMPT_EOF\n)"` | Read the exact model name from `.aperant/config.json` at runtime — do not hardcode. |
194
195
 
195
- Set the Bash tool's `timeout` parameter to `--timeout-ms` (default 90000).
196
+ Set the Bash tool's `timeout` parameter to `--timeout-ms` (default 300000).
196
197
 
197
- **Timeout calibration.** The 90s default is a conservative ceiling sized for worst-case Codex hangs (empirical: the `claude_code` MCP tool has been observed timing out at 120s while `claude -p` returns in seconds). A single unified ceiling simplifies the loop-control logic and avoids per-partner special-casing in v1. For tighter budgets, pass `--timeout-ms` explicitly; future versions may introduce per-partner defaults.
198
+ **Timeout calibration.** The 5-min Bash-rung default and 15-min MCP-rung default are sized for heavy-research partner runs — modern partner CLIs commonly spend multiple minutes doing real Read/Grep/Bash investigation before producing a round response. The two defaults are intentionally asymmetric: the Bash rungs (plugin shim, raw CLI) are capped at the Claude Code Bash tool's 600000ms (10 min) host ceiling, while the MCP rung — a structured tool call rather than a Bash invocation — has no host ceiling and can carry the worst-case 15-min Claude Code partner. For tighter budgets (e.g. CI), pass `--timeout-ms` explicitly. The MCP-rung override is downward-only: if you observe consistent partner-CLI wedges (auth handshakes, hosted-API stalls, CI lanes wanting fast failure), set `apt-spar.mcp_timeout_ms` BELOW the 15-min default in `.aperant/config.json` (e.g. `120000` for a 2-min fast-fail budget) — raising it above 15 min is unnecessary because the default already covers the documented worst case. Future versions may introduce per-partner defaults.
198
199
 
199
200
  **Prompt-quoting safety.** The prompt is user-controlled (it comes from `[topic]` and/or conversation context) and may legitimately contain double quotes, dollar signs, backticks, or `$(...)` substitution that bash would otherwise expand or break on. NEVER interpolate `<prompt>` directly inside a double-quoted shell argument. Instead, pipe via a here-document so the shell treats the body as opaque text:
200
201
 
@@ -284,7 +285,9 @@ After each round, print a verdict block (one of these four, exact format):
284
285
 
285
286
  ## 8. Loop Control (max 3 rounds)
286
287
 
287
- - If `verdict=cede` **converge, terminate** unilaterally. The host has accepted the partner's prior claim with cited evidence; no second partner turn is needed.
288
+ - If `verdict=cede` AND `--rounds 1` was explicit terminate this round.
289
+ - If `verdict=cede` AND `--rounds >= 2` (default) → emit `next-round` regardless: convergence requires TWO consecutive rounds with the same disposition AND no materially new evidence. Round 1 alone cannot terminate as converged.
290
+ - The host's `next-round` payload for Round 2 MUST include: (a) the cited evidence that confirmed the partner's Round 1 point, (b) a request to challenge the host's cited evidence with the partner's NEXT_WEAKEST_POINT.
288
291
  - If `verdict=hold` → only triggers `next-round` (partner gets the new evidence). There is no terminal `hold` convergence in a single round. If the round cap is reached with hold still active, the held position stands and the loop exits via the round-cap path below.
289
292
  - If `verdict=next-round` → invoke the partner again with the new evidence the host just cited, go to Step 4.
290
293
  - If the round counter hits **3 rounds** (or whatever `--rounds N` clamped to) → **terminate** with:
@@ -302,6 +302,17 @@ The `status` field is derived from the minimum dimension score:
302
302
  node packages/framework/bin/apt-tools.mjs commit "qa: verification {status}" --files qa_signoff.json
303
303
  ```
304
304
 
305
+ ### Mark lifecycle phase as `reviewing` (approved path only)
306
+
307
+ If `status` is `approved` or `approved-with-notes`, flip the task's lifecycle
308
+ phase so the next stage (`/apt:review`, or `/apt:ship` if review is skipped)
309
+ finds the task in the correct from-state. Skip this transition entirely on
310
+ `rejected` — the task stays in `verifying` until the user re-runs after fixes.
311
+
312
+ ```bash
313
+ node packages/framework/bin/apt-tools.mjs task update . --id {task-id} --lifecycle-phase reviewing
314
+ ```
315
+
305
316
  ## 8. Report
306
317
 
307
318
  Output:
@@ -69,7 +69,7 @@ Watcher already running for PR {N} (held by sessionId={x}, pid={y}); exiting.
69
69
 
70
70
  For the `--tick` mode, execute exactly the following sequence — once — inside this single skill invocation:
71
71
 
72
- 1. **Poll.** Call `gh pr checks <N> --json name,bucket,state,conclusion,detailsUrl` and `gh pr view <N> --json state,reviewDecision`. Combine both JSON outputs into a single file shaped `{checks: <pr-checks-json>, prMetadata: <pr-view-json>}` (for example with `jq -s '{checks: .[0], prMetadata: .[1]}'`) and save it as `{captured.json}` (any path under the worktree is fine). On any `gh` failure, omit the file and let the cmd's `tick` route to `infra-backoff` (it surfaces the same action when no payload is available).
72
+ 1. **Poll.** Call `gh pr checks <N> --json name,bucket,state,link,workflow,completedAt` and `gh pr view <N> --json state,reviewDecision`. Combine both JSON outputs into a single file shaped `{checks: <pr-checks-json>, prMetadata: <pr-view-json>}` (for example with `jq -s '{checks: .[0], prMetadata: .[1]}'`) and save it as `{captured.json}` (any path under the worktree is fine). On any `gh` failure, omit the file and let the cmd's `tick` route to `infra-backoff` (it surfaces the same action when no payload is available). <!-- FRAMEWORK-BUG-016 (spec.md §D3): gh 2.65+'s `pr checks --json` schema dropped two legacy fields that the watcher previously requested; per-check status now lives in `state` (uppercase: SUCCESS / FAILURE / SKIPPED / IN_PROGRESS / …) and the failed-run URL lives in `link`. Do NOT re-introduce the legacy field names — gh exits non-zero ("Unknown JSON field") and the watcher falls into infra-backoff forever. -->
73
73
  2. **Classify + state update.** Call `apt-tools ci-watch tick {worktree} --pr {N} --payload-file {captured.json}`. The cmd reads `{captured.json}` from disk and runs the classifier — it does NOT shell out to `gh` itself (that boundary stays with the orchestrator skill; see commands/ci-watch.mjs:15). The cmd internally:
74
74
  - acquires the per-PR lock (BUSY → exit 2 → step §2);
75
75
  - reads the per-PR state file;
@@ -111,7 +111,7 @@ In all terminal cases:
111
111
 
112
112
  When `action === 'fix'` (or `diverged-rebased`/`diverged-bailed`):
113
113
 
114
- 1. **Fetch the failed-check log.** From the gh payload, find the failed check's `detailsUrl` and call `gh run view {runId} --log` to capture the log into a temp file.
114
+ 1. **Fetch the failed-check log.** From the gh payload, find the failed check's `link` field (see FRAMEWORK-BUG-016 / §D3 for the gh 2.65+ field migration) and call `gh run view {runId} --log` to capture the log into a temp file.
115
115
  2. **Shell out to the host CLI** to fix it. The watcher does NOT inline a custom fix prompt; it hands the worktree + failed-log to the same CLI binary that's running this skill (detected by `apt-tools host-detect`). Pseudocode:
116
116
 
117
117
  ```bash
@@ -1,8 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  declare const VerificationStatusEnum: z.ZodEnum<{
3
3
  skip: "skip";
4
- pass: "pass";
5
4
  fail: "fail";
5
+ pass: "pass";
6
6
  }>;
7
7
  declare const FileChangeSchema: z.ZodObject<{
8
8
  path: z.ZodString;
@@ -11,23 +11,23 @@ declare const FileChangeSchema: z.ZodObject<{
11
11
  declare const VerificationSchema: z.ZodObject<{
12
12
  typecheck: z.ZodEnum<{
13
13
  skip: "skip";
14
- pass: "pass";
15
14
  fail: "fail";
15
+ pass: "pass";
16
16
  }>;
17
17
  lint: z.ZodEnum<{
18
18
  skip: "skip";
19
- pass: "pass";
20
19
  fail: "fail";
20
+ pass: "pass";
21
21
  }>;
22
22
  visual: z.ZodEnum<{
23
23
  skip: "skip";
24
- pass: "pass";
25
24
  fail: "fail";
25
+ pass: "pass";
26
26
  }>;
27
27
  test: z.ZodEnum<{
28
28
  skip: "skip";
29
- pass: "pass";
30
29
  fail: "fail";
30
+ pass: "pass";
31
31
  }>;
32
32
  evidence_path: z.ZodOptional<z.ZodString>;
33
33
  }, z.core.$strip>;
@@ -50,23 +50,23 @@ export declare const QuickTaskSchema: z.ZodObject<{
50
50
  verification: z.ZodObject<{
51
51
  typecheck: z.ZodEnum<{
52
52
  skip: "skip";
53
- pass: "pass";
54
53
  fail: "fail";
54
+ pass: "pass";
55
55
  }>;
56
56
  lint: z.ZodEnum<{
57
57
  skip: "skip";
58
- pass: "pass";
59
58
  fail: "fail";
59
+ pass: "pass";
60
60
  }>;
61
61
  visual: z.ZodEnum<{
62
62
  skip: "skip";
63
- pass: "pass";
64
63
  fail: "fail";
64
+ pass: "pass";
65
65
  }>;
66
66
  test: z.ZodEnum<{
67
67
  skip: "skip";
68
- pass: "pass";
69
68
  fail: "fail";
69
+ pass: "pass";
70
70
  }>;
71
71
  evidence_path: z.ZodOptional<z.ZodString>;
72
72
  }, z.core.$strip>;
@@ -96,10 +96,10 @@ export declare function validateQuickTask(data: unknown): z.ZodSafeParseResult<{
96
96
  change: string;
97
97
  }[];
98
98
  verification: {
99
- typecheck: "skip" | "pass" | "fail";
100
- lint: "skip" | "pass" | "fail";
101
- visual: "skip" | "pass" | "fail";
102
- test: "skip" | "pass" | "fail";
99
+ typecheck: "skip" | "fail" | "pass";
100
+ lint: "skip" | "fail" | "pass";
101
+ visual: "skip" | "fail" | "pass";
102
+ test: "skip" | "fail" | "pass";
103
103
  evidence_path?: string | undefined;
104
104
  };
105
105
  commit: {
@@ -124,23 +124,23 @@ export declare const quickTaskJsonSchema: z.core.ZodStandardJSONSchemaPayload<z.
124
124
  verification: z.ZodObject<{
125
125
  typecheck: z.ZodEnum<{
126
126
  skip: "skip";
127
- pass: "pass";
128
127
  fail: "fail";
128
+ pass: "pass";
129
129
  }>;
130
130
  lint: z.ZodEnum<{
131
131
  skip: "skip";
132
- pass: "pass";
133
132
  fail: "fail";
133
+ pass: "pass";
134
134
  }>;
135
135
  visual: z.ZodEnum<{
136
136
  skip: "skip";
137
- pass: "pass";
138
137
  fail: "fail";
138
+ pass: "pass";
139
139
  }>;
140
140
  test: z.ZodEnum<{
141
141
  skip: "skip";
142
- pass: "pass";
143
142
  fail: "fail";
143
+ pass: "pass";
144
144
  }>;
145
145
  evidence_path: z.ZodOptional<z.ZodString>;
146
146
  }, z.core.$strip>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aperant/framework",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "AI coding framework — composable skills for planning, executing, verifying, and reviewing code with any LLM provider. Works as Claude Code commands, Codex tasks, or programmatically.",
5
5
  "author": "Mikalsen AI <hello@mikalsen.ai>",
6
6
  "type": "module",
@@ -41,8 +41,9 @@ you must follow when classifying.
41
41
  - **Config:** `.aperant/config.json` — project preferences
42
42
  - **State:** `.aperant/state.json` — execution state
43
43
  - **Task directories:** `.aperant/tasks/{task-id}/` — each task gets isolated artifacts
44
- - **Codex repo skills:** `.agents/skills/apt*/` — native Codex discovery surface installed by `apt-tools init`
45
- - **Claude agents:** `.claude/agents/apt-*.md` — specialized agent definitions for heavy workflows
44
+ - **Claude surface:** `.claude/skills/`, `.claude/agents/`
45
+ - **Codex surface:** `.codex/commands/`, `.codex/agents/`
46
+ - **Pi surface:** `.pi/prompts/`, `.pi/skills/`
46
47
  </your_environment>
47
48
 
48
49
  <mandatory_first_step>