@aperant/framework 0.7.0 → 0.7.4

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 (151) hide show
  1. package/CHANGELOG.md +205 -0
  2. package/agents/apt-planner.md +12 -0
  3. package/agents/apt-pr-review-fixer.md +13 -9
  4. package/bin/apt-tools.mjs +7 -0
  5. package/dist/cli/ci-watch/stop-matrix.d.mts.map +1 -1
  6. package/dist/cli/ci-watch/stop-matrix.mjs +16 -0
  7. package/dist/cli/ci-watch/stop-matrix.mjs.map +1 -1
  8. package/dist/cli/commands/ci-watch.d.mts +11 -0
  9. package/dist/cli/commands/ci-watch.d.mts.map +1 -1
  10. package/dist/cli/commands/ci-watch.mjs +108 -2
  11. package/dist/cli/commands/ci-watch.mjs.map +1 -1
  12. package/dist/cli/commands/features-audit.d.mts +24 -0
  13. package/dist/cli/commands/features-audit.d.mts.map +1 -1
  14. package/dist/cli/commands/features-audit.mjs +159 -5
  15. package/dist/cli/commands/features-audit.mjs.map +1 -1
  16. package/dist/cli/commands/health-check.d.mts +16 -0
  17. package/dist/cli/commands/health-check.d.mts.map +1 -1
  18. package/dist/cli/commands/health-check.mjs +118 -2
  19. package/dist/cli/commands/health-check.mjs.map +1 -1
  20. package/dist/cli/commands/init.d.mts +9 -0
  21. package/dist/cli/commands/init.d.mts.map +1 -1
  22. package/dist/cli/commands/init.mjs +49 -4
  23. package/dist/cli/commands/init.mjs.map +1 -1
  24. package/dist/cli/commands/modes.d.mts.map +1 -1
  25. package/dist/cli/commands/modes.mjs +11 -0
  26. package/dist/cli/commands/modes.mjs.map +1 -1
  27. package/dist/cli/commands/pr-review-audit-fixer.d.mts +13 -0
  28. package/dist/cli/commands/pr-review-audit-fixer.d.mts.map +1 -1
  29. package/dist/cli/commands/pr-review-audit-fixer.mjs +18 -5
  30. package/dist/cli/commands/pr-review-audit-fixer.mjs.map +1 -1
  31. package/dist/cli/commands/task.d.mts.map +1 -1
  32. package/dist/cli/commands/task.mjs +84 -5
  33. package/dist/cli/commands/task.mjs.map +1 -1
  34. package/dist/cli/commands/validate-evidence.d.mts +24 -2
  35. package/dist/cli/commands/validate-evidence.d.mts.map +1 -1
  36. package/dist/cli/commands/validate-evidence.mjs +154 -17
  37. package/dist/cli/commands/validate-evidence.mjs.map +1 -1
  38. package/dist/cli/commands/vitest-doctor.d.mts +2 -0
  39. package/dist/cli/commands/vitest-doctor.d.mts.map +1 -0
  40. package/dist/cli/commands/vitest-doctor.mjs +168 -0
  41. package/dist/cli/commands/vitest-doctor.mjs.map +1 -0
  42. package/dist/cli/config/gitignore-drift.d.mts +23 -1
  43. package/dist/cli/config/gitignore-drift.d.mts.map +1 -1
  44. package/dist/cli/config/gitignore-drift.mjs +81 -3
  45. package/dist/cli/config/gitignore-drift.mjs.map +1 -1
  46. package/dist/cli/config/load.d.mts +56 -2
  47. package/dist/cli/config/load.d.mts.map +1 -1
  48. package/dist/cli/config/load.mjs +192 -2
  49. package/dist/cli/config/load.mjs.map +1 -1
  50. package/dist/cli/consistency/parse-review.mjs +7 -0
  51. package/dist/cli/consistency/parse-review.mjs.map +1 -1
  52. package/dist/cli/dispatch.d.mts.map +1 -1
  53. package/dist/cli/dispatch.mjs +24 -2
  54. package/dist/cli/dispatch.mjs.map +1 -1
  55. package/dist/cli/gate/gates/gitignore-in-sync.d.mts +1 -1
  56. package/dist/cli/gate/gates/gitignore-in-sync.d.mts.map +1 -1
  57. package/dist/cli/gate/gates/gitignore-in-sync.mjs +5 -2
  58. package/dist/cli/gate/gates/gitignore-in-sync.mjs.map +1 -1
  59. package/dist/cli/gate/gates/review-clean.d.mts +5 -1
  60. package/dist/cli/gate/gates/review-clean.d.mts.map +1 -1
  61. package/dist/cli/gate/gates/review-clean.mjs +23 -18
  62. package/dist/cli/gate/gates/review-clean.mjs.map +1 -1
  63. package/dist/cli/gate/gates/verify-approved.d.mts +49 -1
  64. package/dist/cli/gate/gates/verify-approved.d.mts.map +1 -1
  65. package/dist/cli/gate/gates/verify-approved.mjs +93 -14
  66. package/dist/cli/gate/gates/verify-approved.mjs.map +1 -1
  67. package/dist/cli/help.d.mts.map +1 -1
  68. package/dist/cli/help.mjs +7 -2
  69. package/dist/cli/help.mjs.map +1 -1
  70. package/dist/cli/install/runtime-detect.d.mts +13 -0
  71. package/dist/cli/install/runtime-detect.d.mts.map +1 -1
  72. package/dist/cli/install/runtime-detect.mjs +9 -0
  73. package/dist/cli/install/runtime-detect.mjs.map +1 -1
  74. package/dist/cli/task/index-md.d.mts.map +1 -1
  75. package/dist/cli/task/index-md.mjs +14 -2
  76. package/dist/cli/task/index-md.mjs.map +1 -1
  77. package/dist/plugin/.claude-plugin/plugin.json +11 -2
  78. package/dist/plugin/agents/apt-improver.md +99 -0
  79. package/dist/plugin/agents/apt-planner.md +127 -10
  80. package/dist/plugin/agents/apt-pr-review-fixer.md +13 -9
  81. package/dist/plugin/skills/apt/SKILL.md +1 -0
  82. package/dist/plugin/skills/apt-close-task/SKILL.md +63 -1
  83. package/dist/plugin/skills/apt-debug/SKILL.md +39 -6
  84. package/dist/plugin/skills/apt-debug/appendices/diagnose-discipline.md +119 -0
  85. package/dist/plugin/skills/apt-diagram/SKILL.md +378 -0
  86. package/dist/plugin/skills/apt-diagram/appendices/design-discipline.md +97 -0
  87. package/dist/plugin/skills/apt-discuss/SKILL.md +72 -5
  88. package/dist/plugin/skills/apt-discuss/appendices/grill-discipline.md +104 -0
  89. package/dist/plugin/skills/apt-discuss/appendices/zoom-out-helper.md +79 -0
  90. package/dist/plugin/skills/apt-execute/SKILL.md +57 -5
  91. package/dist/plugin/skills/apt-execute/appendices/tdd-mode.md +107 -0
  92. package/dist/plugin/skills/apt-improve/DEEPENING.md +84 -0
  93. package/dist/plugin/skills/apt-improve/INTERFACE-DESIGN.md +97 -0
  94. package/dist/plugin/skills/apt-improve/LANGUAGE.md +104 -0
  95. package/dist/plugin/skills/apt-improve/SKILL.md +141 -0
  96. package/dist/plugin/skills/apt-plan/SKILL.md +171 -4
  97. package/dist/plugin/skills/apt-plan/adapters/conductor.md +98 -0
  98. package/dist/plugin/skills/apt-pr-review/SKILL.md +57 -18
  99. package/dist/plugin/skills/apt-prototype/LOGIC.md +109 -0
  100. package/dist/plugin/skills/apt-prototype/SKILL.md +143 -0
  101. package/dist/plugin/skills/apt-prototype/UI.md +90 -0
  102. package/dist/plugin/skills/apt-quick/SKILL.md +49 -8
  103. package/dist/plugin/skills/apt-release-notes/SKILL.md +193 -0
  104. package/dist/plugin/skills/apt-release-notes/appendices/persona-voice.md +59 -0
  105. package/dist/plugin/skills/apt-review/SKILL.md +2 -0
  106. package/dist/plugin/skills/apt-run/SKILL.md +32 -4
  107. package/dist/plugin/skills/apt-setup/SKILL.md +308 -6
  108. package/dist/plugin/skills/apt-ship/SKILL.md +122 -1
  109. package/dist/plugin/skills/apt-spar/SKILL.md +315 -0
  110. package/dist/plugin/skills/apt-triage/AGENT-BRIEF.md +84 -0
  111. package/dist/plugin/skills/apt-triage/OUT-OF-SCOPE.md +75 -0
  112. package/dist/plugin/skills/apt-triage/SKILL.md +169 -0
  113. package/dist/plugin/skills/apt-update/SKILL.md +77 -10
  114. package/dist/plugin/skills/apt-verify/SKILL.md +3 -0
  115. package/dist/plugin/skills/apt-verify-proof/SKILL.md +10 -5
  116. package/dist/plugin/skills/apt-watch-ci/SKILL.md +166 -0
  117. package/dist/plugin/skills/apt-zoom-out/SKILL.md +130 -0
  118. package/package.json +1 -1
  119. package/prompts/conductor-framework-context.md +63 -0
  120. package/prompts/conductor-system.md +11 -0
  121. package/skills/apt-close-task/SKILL.md +1 -0
  122. package/skills/apt-discuss/SKILL.md +47 -5
  123. package/skills/apt-execute/SKILL.md +9 -0
  124. package/skills/apt-plan/SKILL.md +12 -0
  125. package/skills/apt-pr-review/SKILL.md +19 -3
  126. package/skills/apt-quick/SKILL.md +19 -8
  127. package/skills/apt-researcher.md +1 -0
  128. package/skills/apt-setup/SKILL.md +33 -2
  129. package/skills/apt-verify-proof/SKILL.md +7 -5
  130. package/src/cli/ci-watch/stop-matrix.mjs +17 -0
  131. package/src/cli/commands/ci-watch.mjs +113 -2
  132. package/src/cli/commands/features-audit.mjs +164 -5
  133. package/src/cli/commands/health-check.mjs +115 -2
  134. package/src/cli/commands/init.mjs +52 -4
  135. package/src/cli/commands/modes.mjs +11 -0
  136. package/src/cli/commands/pr-review-audit-fixer.mjs +18 -5
  137. package/src/cli/commands/task.mjs +83 -5
  138. package/src/cli/commands/validate-evidence.mjs +158 -17
  139. package/src/cli/commands/vitest-doctor.mjs +173 -0
  140. package/src/cli/config/gitignore-drift.mjs +74 -3
  141. package/src/cli/config/load.mjs +188 -2
  142. package/src/cli/consistency/parse-review.mjs +6 -0
  143. package/src/cli/dispatch.mjs +23 -2
  144. package/src/cli/gate/gates/gitignore-in-sync.mjs +5 -2
  145. package/src/cli/gate/gates/review-clean.mjs +24 -19
  146. package/src/cli/gate/gates/verify-approved.mjs +97 -14
  147. package/src/cli/help.mjs +7 -2
  148. package/src/cli/install/runtime-detect.mjs +9 -0
  149. package/src/cli/task/index-md.mjs +15 -2
  150. package/templates/config.json +2 -7
  151. package/workflows/verify-proof.md +8 -3
@@ -17,7 +17,7 @@ execution_modes:
17
17
  - step
18
18
  - research
19
19
  allowed-tools: "Bash, Read, Grep, Glob, Write"
20
- argument-hint: "apt:discuss [--brainstorm] [topic]"
20
+ argument-hint: "apt:discuss [--brainstorm] [--grill] [topic]"
21
21
  gates: []
22
22
  ---
23
23
  <objective>
@@ -183,10 +183,11 @@ If any file is missing, proceed with what's available. The framework works with
183
183
  Extract the task description from `$ARGUMENTS`:
184
184
 
185
185
  1. Strip mode flags (`--assumptions`, `--power`, `--auto`, `--review-auto`, `--continue`) from arguments
186
- 2. The remaining text is the task description
187
- 3. If task description is empty but `state.json` has an `active_task`, use that task's description
188
- 4. If still empty, ask: "What are you working on?"
189
- 5. Generate a `task-id` slug from the description: lowercase, hyphens, max 40 characters
186
+ 2. Strip the **`--grill`** overlay flag from arguments (orthogonal to the mode flags — see Section 6.5). `--grill` may combine with any mode; standalone `--grill` defaults to Interactive.
187
+ 3. The remaining text is the task description
188
+ 4. If task description is empty but `state.json` has an `active_task`, use that task's description
189
+ 5. If still empty, ask: "What are you working on?"
190
+ 6. Generate a `task-id` slug from the description: lowercase, hyphens, max 40 characters
190
191
  - Example: "Add OAuth login with Google" -> `add-oauth-login-with-google`
191
192
 
192
193
  ## 3.5. Brainstorm (skip if you picked decision-lock in Section 1)
@@ -564,6 +565,47 @@ Review-Auto is Auto Mode **plus one human checkpoint at the batch end**. It does
564
565
 
565
566
  Do NOT introduce per-decision staging ("show me D-01, wait, show me D-02, wait...") — that collapses review-auto into spaced interactive mode and defeats the point of joint reasoning up front. Do NOT skip the pause ("autonomy is high, let's just write it") — that collapses review-auto into auto. The one pause at the end is the whole identity of this mode.
566
567
 
568
+ ## 6.5. Grill Mode (`--grill` flag — Pocock `grill-with-docs` overlay)
569
+
570
+ **Activate when:** the user invoked the skill with `--grill`. This is an **overlay**, not a mode — it stacks on top of whichever Section 6 mode is active (Interactive by default; combine with `--auto` etc. as desired). The overlay does two things: (1) it applies Pocock's 4 grill moves during decision resolution; (2) it fires inline writes to CONTEXT.md and ADRs as terms resolve and decisions pass the Nygard 3-gate.
571
+
572
+ The discipline this overlay loads is already in [`appendices/grill-discipline.md`](./appendices/grill-discipline.md) — re-read the reframe sentence and the autonomy degradation table before each grill turn. **Do NOT duplicate that content here.** What follows is only the moves + writer-call instructions specific to the overlay.
573
+
574
+ ### The 4 grill moves (apply during gray-area resolution)
575
+
576
+ 1. **Challenge against the glossary.** When the user uses a term that conflicts with an existing CONTEXT.md term-block, call it out immediately: *"Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?"* Read `CONTEXT.md` (or per-context CONTEXT files via `CONTEXT-MAP.md`) when the overlay activates; skip silently if absent.
577
+ 2. **Sharpen fuzzy language.** When the user uses vague or overloaded terms, propose a precise canonical term: *"You're saying 'account' — do you mean Customer or User? Those are different things."*
578
+ 3. **Stress-test with concrete scenarios.** When domain relationships are being discussed, invent specific scenarios that probe edge cases and force precision about the boundaries between concepts.
579
+ 4. **Cross-reference with code.** When the user states how something works, check whether the code agrees. If you find a contradiction, surface it: *"Your code cancels entire Orders, but you just said partial cancellation is possible — which is right?"*
580
+
581
+ ### Inline writer calls (NOT batched)
582
+
583
+ When a term resolves with confidence during the grill, invoke `apt-tools context write` to upsert the term-block into CONTEXT.md **right there** — do NOT batch:
584
+
585
+ ```bash
586
+ node .aperant/deps/node_modules/@aperant/framework/bin/apt-tools.mjs context write . \
587
+ --term "Plan thread" \
588
+ --definition "<one-line definition>" \
589
+ --aliases-to-avoid "Plan session,plan chat" \
590
+ --cardinality "one-to-many"
591
+ ```
592
+
593
+ **Lazy creation:** if no `CONTEXT.md` exists at the project root (or in the relevant per-context directory when `CONTEXT-MAP.md` is present), the first resolved term creates the file. Do NOT create CONTEXT.md upfront with placeholder content.
594
+
595
+ **Glossary-only rule:** CONTEXT.md is a glossary of domain terms — NO implementation details, NO spec content, NO scratch-pad notes. Implementation lives in `spec.md` / `implementation_plan.json`; scratch lives in `.aperant/context/notes/{task-id}.md`. Term-blocks are user-facing concepts (e.g. "Plan thread", "Sparring round"), not code identifiers (e.g. "ChatSession", "useTaskStore").
596
+
597
+ When a decision passes the **Nygard 3-gate**, offer to write an ADR via `apt-tools adr write .`. The 3-gate (ALL three must hold — miss one and skip the ADR):
598
+
599
+ 1. **Hard to reverse** — the cost of changing your mind later is meaningful.
600
+ 2. **Surprising without context** — a future reader will wonder *"why did they do it this way?"*.
601
+ 3. **Result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons.
602
+
603
+ If only one or two hold, the decision belongs in `.aperant/context/notes/{task-id}.md` (locked-decisions list), not in an ADR.
604
+
605
+ ### Autonomy interaction
606
+
607
+ `--grill` does not change the autonomy degradation already specified in [`appendices/grill-discipline.md`](./appendices/grill-discipline.md). Autonomy 1 grills every term/decision interactively; Autonomy 2 batches recommendations; Autonomy 3 auto-locks with the three escalation triggers (conflict-with-locked, critical-path file, no-defensible-default). The overlay simply ensures that whichever autonomy path runs, the writer CLIs fire as side-effects of the resolution work.
608
+
567
609
  ## 7. Advanced Elicitation (DEEP Tasks Only)
568
610
 
569
611
  After all decisions are locked, check the task track. If the track is DEEP (from state.json or router context), behavior depends on the resolved mode from Section 6's precedence chain (D-07):
@@ -383,11 +383,20 @@ executor commits introduce a new feature surface without registering it.
383
383
 
384
384
  ```bash
385
385
  node packages/framework/bin/apt-tools.mjs features-audit . \
386
+ --task-id "$TASK_ID" \
386
387
  --diff-files "$(git diff --name-only HEAD^ HEAD | tr '\n' ',')" \
387
388
  --apply-stubs
388
389
  ```
389
390
 
390
391
  Notes:
392
+ - `--task-id $TASK_ID` (BUG-025b) is mandatory when the executor runs
393
+ from the main-repo cwd instead of inside the worktree (the
394
+ orchestrator's common path). features-audit resolves the worktree's
395
+ `.aperant/features/` from the active task record and writes there,
396
+ preventing generated `*.json` from leaking into main. When invoked
397
+ from the worktree cwd directly, `--task-id` is a no-op (layer 1
398
+ detection short-circuits). Pass `--no-worktree-redirect` only when
399
+ you intentionally want to write to the invoking cwd.
391
400
  - `--apply-stubs` only runs when the matched surface's policy is
392
401
  `auto-update` (the default for `.aperant/features/*.json`). Users who
393
402
  opted out via `/apt:setup` Batch 7 get a no-op — the command still
@@ -99,6 +99,18 @@ For STANDARD and COMPLEX tasks:
99
99
  - Test files that will need updates
100
100
  - Configuration that may need changes
101
101
 
102
+ ### 4.4. React state guidance — sync-store vs render-committed ref (BUG-025-planner)
103
+
104
+ When a planned event handler fires a flush/persist immediately after mutating a Zustand/Jotai/refs-stored store, the spec MUST recommend reading from `store.getState()` (or the equivalent direct-read primitive) on the synchronous path, NOT from a `useRef`-populated-by-`useEffect`.
105
+
106
+ Why: refs populated via `useEffect` lag the store by exactly one render cycle. A handler that mutates the store and immediately reads the ref sees the pre-mutation value, silently corrupting persists and order-of-operations contracts. Reference pattern: `apps/desktop/src/renderer/src/App.tsx::runUpsertForIds` (post-fix commit `4c263856`).
107
+
108
+ Investigation checklist when the task touches a Zustand/Jotai store or any handler that does flush-after-mutate:
109
+
110
+ - Grep for `useRef(` references that cache store state (e.g. `const fooRef = useRef(foo)` populated by `useEffect(() => { fooRef.current = foo }, [foo])`).
111
+ - If the spec or planned subtasks read from such a ref on a synchronous path that ALSO mutates the store, recommend `store.getState()` (or `useFooStore.getState()`) as the read primitive.
112
+ - Note the caveat explicitly in the spec's Implementation Decisions section so the executor doesn't silently revert it.
113
+
102
114
  ### 4.5. Consume design.md (C24)
103
115
 
104
116
  If `{task_dir}/design.md` exists (emitted by a prior `/apt:design` run), load it before decomposing subtasks:
@@ -257,7 +257,7 @@ When the result is `unknown`, treat it as `external` for safety and prompt the u
257
257
  Authorship could not be auto-detected for PR #${PR_NUMBER}. Treat as [owned / external]? (default: external)
258
258
  ```
259
259
 
260
- If `autonomy_level >= 3` (yolo), skip the prompt and lock in `external`. Persist the user's answer to BOTH `pr-state.json:authorship` AND `config.json:pr_review.authorship_overrides['pr-${PR_NUMBER}']` so subsequent rounds don't re-ask.
260
+ If `autonomy_level >= 3` (yolo), skip the prompt and lock in `external`. Persist the user's answer to `pr-state.json:authorship` so subsequent rounds don't re-ask. (Per C56 A3 — authorship overrides moved from `config.json:pr_review.authorship_overrides` to per-PR `pr-state.json` so they GC with the pr-review round. Legacy `config.json` entries are still read with a deprecation warning + auto-promoted to pr-state on next `init-pr`; do NOT write to `config.json` from new code.)
261
261
 
262
262
  Store the result as `AUTHORSHIP_MODE` and use it as a gate at every later phase:
263
263
 
@@ -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. |
@@ -1195,6 +1197,12 @@ Take all confirmed, fixable findings and group them into up to 3 independent gro
1195
1197
 
1196
1198
  #### Step 3: Spawn Fix Agents
1197
1199
 
1200
+ **Capture the iteration-start SHA BEFORE spawning fixers** (used by both the Step 4b audit gate AND the per-fixer Pre-Return Self-Audit added in BUG-016 — fixers now commit atomically per finding, and the audit window is scoped by this SHA):
1201
+
1202
+ ```bash
1203
+ ITERATION_START_SHA=$(cd "${WORKTREE_PATH}" && git rev-parse HEAD)
1204
+ ```
1205
+
1198
1206
  For each fix group, spawn a fix agent. Read the agent definition from `.claude/agents/apt-pr-review-fixer.md` and fill in:
1199
1207
 
1200
1208
  | Placeholder | Value |
@@ -1207,6 +1215,7 @@ For each fix group, spawn a fix agent. Read the agent definition from `.claude/a
1207
1215
  | `[CRITICAL_APPROVAL]` | `true` or `false` for each critical finding in this group |
1208
1216
  | `[PARALLEL_FIXERS]` | `true` if more than 1 fixer is spawned this iteration |
1209
1217
  | `[WORKTREE_PATH]` | The worktree path — fix agents edit files HERE, not in the user's working directory |
1218
+ | `[ITERATION_START_SHA]` | The SHA captured BEFORE spawning fixers (see Step 4b's `ITERATION_START_SHA=$(...)` capture). Required for BUG-016 commit-discipline Pre-Return Self-Audit. |
1210
1219
 
1211
1220
  ```
1212
1221
  Agent(
@@ -1260,7 +1269,9 @@ If `--since-sha` is **omitted**, the CLI falls back to `git merge-base HEAD orig
1260
1269
 
1261
1270
  Rule of thumb: pass `--since-sha "${ITERATION_START_SHA}"` on every audit invocation. The merge-base fallback exists only so a missing flag doesn't flip every standard-branch-workflow repo into a false-positive hallucination — it is NOT a replacement for the explicit flag.
1262
1271
 
1263
- The CLI returns a JSON envelope with `verdict` ∈ `verified | hallucinated | skipped`, writes `iterations/${ITERATION}/audit.json` (append-only), and bumps `metadata.json.fixer_hallucinations_total` on every `hallucinated` outcome. The envelope's `observed.since_sha` + `observed.since_sha_source` fields record which SHA was used and where it came from (`flag` | `merge-base` | `null`). The same envelope is used for the fixer and the self-reviewer (Phase 7) — `--fixer self-reviewer` reads from `self-review.md` instead of `fixes-applied-group{N}.md`.
1272
+ The CLI returns a JSON envelope with `verdict` ∈ `verified | hallucinated | skipped`, writes `iterations/${ITERATION}/audit.json` (append-only), and bumps `metadata.json.fixer_hallucinations_total` on every `hallucinated` outcome. The envelope's `observed.since_sha` + `observed.since_sha_source` fields record which SHA was used and where it came from (`flag` | `merge-base` | `null`). The same envelope is used for the fixer and the self-reviewer (Phase 7) — `--fixer self-reviewer` reads from `self-review.md`.
1273
+
1274
+ **Canonical fix-report filename per fixer (BUG-030):** each fixer agent writes to `fixes-applied-${FIXER_NAME}.md` (e.g. `fixes-applied-fixer-1.md`). The audit CLI resolves candidates in strict order: `fixes-applied-group{N}.md` (legacy group-named, pre-BUG-030 reviews) → `fixes-applied-fixer-{N}.md` (NEW fixer-named) → `fixes-applied-${FIXER_NAME}.md` (exact match, preferred for non-numeric fixer names) → `fixes-applied.md` (legacy unscoped, single-fixer reviews only). Writing the canonical name eliminates the cross-fixer false-positive `hallucinated` verdict that fired when two parallel fixers shared `fixes-applied.md`.
1264
1275
 
1265
1276
  | Verdict | Orchestrator Action |
1266
1277
  |---------|---------------------|
@@ -1638,7 +1649,7 @@ node packages/framework/bin/apt-tools.mjs workflow destroy . --id ${REVIEW_ID}
1638
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.
1639
1650
 
1640
1651
  ### 2. Fix Everything Found
1641
- 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.
1642
1653
 
1643
1654
  ### 3. Findings Flow Through Files
1644
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%.
@@ -1702,6 +1713,11 @@ Fix agents self-report their outcome via a status line (`FIXED: X | FAILED: Y |
1702
1713
  **Wrong:** Skipping Phase 6 entirely when autonomy < 2.
1703
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.
1704
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
+
1705
1721
  ### Mistake 8: Forgetting to Update metadata.json
1706
1722
 
1707
1723
  **Wrong:** Writing review.json but leaving metadata.json stuck at `"status": "in-progress"`.
@@ -279,18 +279,29 @@ skill MUST exit silently regardless of the config value, so spawned subagents
279
279
  never block on a prompt that no human will answer.
280
280
 
281
281
  ```bash
282
- # Pseudocode the model executes inline:
282
+ # Pseudocode the model executes inline.
283
+ # AUDIT-001: `preferences.quick_task_post_verify` is per-device — read from
284
+ # the MERGED config (shared `.aperant/config.json` + local
285
+ # `.aperant/config.local.json`). Local wins on collision. Reading raw
286
+ # `config.json` only would miss per-device overrides.
283
287
  node -e '
284
288
  const fs = require("fs");
285
289
  const path = require("path");
286
- const cfgPath = path.join(process.cwd(), ".aperant/config.json");
290
+ const readJsonOrEmpty = (p) => { try { return JSON.parse(fs.readFileSync(p, "utf-8")) ?? {} } catch { return {} } };
291
+ const shared = readJsonOrEmpty(path.join(process.cwd(), ".aperant/config.json"));
292
+ const local = readJsonOrEmpty(path.join(process.cwd(), ".aperant/config.local.json"));
293
+ function merge(a, b) {
294
+ if (a === null || typeof a !== "object" || Array.isArray(a)) return b;
295
+ if (b === null || typeof b !== "object" || Array.isArray(b)) return b;
296
+ const out = { ...a };
297
+ for (const k of Object.keys(b)) out[k] = (k in a) ? merge(a[k], b[k]) : b[k];
298
+ return out;
299
+ }
300
+ const cfg = merge(shared, local);
287
301
  let mode = "ask";
288
- try {
289
- const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
290
- if (cfg && cfg.preferences && cfg.preferences.quick_task_post_verify) {
291
- mode = cfg.preferences.quick_task_post_verify;
292
- }
293
- } catch {}
302
+ if (cfg.preferences && cfg.preferences.quick_task_post_verify) {
303
+ mode = cfg.preferences.quick_task_post_verify;
304
+ }
294
305
  if (mode === "never" || !process.stdin.isTTY) process.exit(0);
295
306
  if (mode === "always") { /* invoke apt:verify {task-id} */ process.exit(0); }
296
307
  // mode === "ask": readline prompt with 30s timeout, default = skip.
@@ -41,6 +41,7 @@ Parse the investigation query. Identify:
41
41
  3. Read key files to understand the full picture
42
42
  4. Follow import chains to map dependencies
43
43
  5. Check tests for behavioral documentation
44
+ 6. **Report which refs cache store state on a render-cycle delay** (BUG-025-planner): when investigating React state or any Zustand/Jotai store touched by handlers, grep for `useRef(` patterns populated via `useEffect`. Flag them explicitly — they lag the store by one render and any sync-mutate-then-read code path against them silently corrupts. Reference pattern: `apps/desktop/src/renderer/src/App.tsx::runUpsertForIds` (post-fix commit `4c263856`).
44
45
 
45
46
  ## 3. Report Findings
46
47
  Produce a clear, structured answer:
@@ -1111,6 +1111,34 @@ The selective-edit pass through Batch 9 MUST respect the §1b dispatch — it do
1111
1111
 
1112
1112
  ## 4. Map Answers to Config
1113
1113
 
1114
+ **AUDIT-001 — file-routing contract for per-device fields (UPDATED 0.7.2):** the
1115
+ following five fields are PER-DEVICE and MUST land in `.aperant/config.local.json`
1116
+ (gitignored), NOT the shared `.aperant/config.json`:
1117
+ - `verification.open_report` — covered in the "Auto-Open" mapping below.
1118
+ - `multi_model.cross_verification.tools` — covered in the "Cross-Verification" mapping below.
1119
+ - `preferences.date_format` — covered in the "Date Format" mapping below.
1120
+ - `preferences.time_format` — covered in the "Time Format" mapping below.
1121
+ - `preferences.quick_task_post_verify` — covered in apt-quick SKILL config block.
1122
+
1123
+ All other fields (including `verification.evidence_capture`,
1124
+ `multi_model.cross_verification.enabled`, `multi_model.video_review.*`,
1125
+ `preferences.task_index`, every `orchestration.*` value) stay in shared
1126
+ `.aperant/config.json`.
1127
+
1128
+ Implementation: use the framework's `writeLocalConfigField(targetDir, dotPath, value)`
1129
+ helper from `packages/framework/src/cli/config/load.mjs` (or its shell-out
1130
+ equivalent via `apt-tools config write . --local --key <dotpath> --value <json>`)
1131
+ for the five fields above. NEVER hand-roll `JSON.parse(readFileSync('.aperant/config.json'))`
1132
+ followed by `writeFileSync('.aperant/config.json', ...)` for these fields — it
1133
+ will land per-device data in the shared file and re-introduce the
1134
+ shared-config trap AUDIT-001 was designed to close.
1135
+
1136
+ Reads of these fields anywhere in the framework MUST go through
1137
+ `loadMergedProjectConfig(targetDir)` or `readLocalOrSharedField(targetDir, dotPath)`
1138
+ so per-device overrides are honored.
1139
+
1140
+ ---
1141
+
1114
1142
  Map the user's selections to config values:
1115
1143
 
1116
1144
  **Verify Mode:**
@@ -1180,13 +1208,16 @@ Note: task ids are slug-first since v0.6.6 — the date-format setting controls
1180
1208
 
1181
1209
  ## 5. Write Config
1182
1210
 
1183
- Write merged config to `.aperant/config.json`:
1211
+ Write merged config to two files per AUDIT-001 routing (see Step 4 block):
1212
+
1213
+ - Shared `.aperant/config.json` — all project policy values (the bulk of the answers).
1214
+ - Per-device `.aperant/config.local.json` — the five AUDIT-001 fields enumerated in Step 4.
1184
1215
 
1185
1216
  ```bash
1186
1217
  mkdir -p .aperant
1187
1218
  ```
1188
1219
 
1189
- Write the JSON file with 2-space indentation. Bump `version` to 4 if upgrading from an older config (versions 2/3 load forward-compatibly — the new `tdd` block is optional).
1220
+ Write each JSON file with 2-space indentation. Bump shared `config.json`'s `version` to 4 if upgrading from an older config (versions 2/3 load forward-compatibly — the new `tdd` block is optional). `config.local.json` is gitignored by `.aperant/.gitignore` — do not check it in.
1190
1221
 
1191
1222
  ## 6. Save as Global Defaults (Optional)
1192
1223
 
@@ -89,8 +89,10 @@ For multi-model verification details:
89
89
 
90
90
  ## Config
91
91
 
92
- Settings in `.aperant/config.json` control behavior (run `/apt:setup` to configure):
93
- - `verification.evidence_capture` — capture screenshots + terminal output (default: true)
94
- - `verification.open_report` — open HTML in browser when done (default: true)
95
- - `multi_model.enabled` — run multi-model verification (default: false)
96
- - `multi_model.gemini.model` — Gemini preset: default/thorough/fast
92
+ Settings live in the MERGED Aperant config (shared `.aperant/config.json` + per-device `.aperant/config.local.json`; local wins). Run `/apt:setup` to configure. Read via `apt-tools config read .` or the framework's `loadMergedProjectConfig()` — NEVER raw `JSON.parse(readFileSync('.aperant/config.json'))`, which misses per-device overrides.
93
+
94
+ - `verification.evidence_capture` — shared. capture screenshots + terminal output (default: true)
95
+ - `verification.open_report` — **per-device (config.local.json)** per AUDIT-001. open HTML in browser when done (default: true)
96
+ - `multi_model.enabled` — shared. run multi-model verification (default: false)
97
+ - `multi_model.cross_verification.tools` — **per-device (config.local.json)** per AUDIT-001. which CLIs to cross-verify with.
98
+ - `multi_model.gemini.model` — shared. Gemini preset: default/thorough/fast
@@ -106,6 +106,23 @@ export function classify({ checks, state, prMetadata }) {
106
106
  }
107
107
  }
108
108
 
109
+ // 6b. BUG-031 defensive wait branch — if no checks are actually
110
+ // failing (everything is success / skipped / in_progress / neutral),
111
+ // never fall through to the default-red 'fix' at the bottom. Placed
112
+ // AFTER reviewer-pause so CHANGES_REQUESTED + no-failures still
113
+ // produces 'reviewer-pause' (locks existing semantics), BEFORE the
114
+ // remaining checks (identical-log, flake-skip, infra-backoff, fix)
115
+ // which all assume at least one failing row. Pre-fix, a PR with a
116
+ // success + a SKIPPED conditional CI job fell through to 'fix' and
117
+ // burned a fix attempt every tick on a non-existent failure.
118
+ if (failed.length === 0) {
119
+ return {
120
+ action: 'wait',
121
+ counts_toward_budget: false,
122
+ reason: 'no failing checks; awaiting in-flight or skipped',
123
+ }
124
+ }
125
+
109
126
  // 7. Identical-log → non-converging fixer.
110
127
  // Compare aggregate fingerprints (same format as the write side in ci-watch.mjs).
111
128
  if (attempts > 0 && st.last_status && failed.length > 0) {
@@ -24,6 +24,7 @@
24
24
  * itself never shells out to gh.
25
25
  */
26
26
 
27
+ import { spawnSync } from 'node:child_process'
27
28
  import { randomBytes } from 'node:crypto'
28
29
  import { existsSync, readFileSync } from 'node:fs'
29
30
  import { dirname, resolve } from 'node:path'
@@ -94,7 +95,7 @@ function validatePr(pr) {
94
95
  */
95
96
  export function cmdCiWatch(subcommand, projectDir, extraArgs) {
96
97
  if (!subcommand) {
97
- return err('Usage: apt-tools ci-watch <start|stop|status|tick> <project-dir> [flags]')
98
+ return err('Usage: apt-tools ci-watch <start|stop|status|tick|sweep> <project-dir> [flags]')
98
99
  }
99
100
  if (!projectDir) {
100
101
  return err(`Usage: apt-tools ci-watch ${subcommand} <project-dir> [flags]`)
@@ -112,9 +113,34 @@ export function cmdCiWatch(subcommand, projectDir, extraArgs) {
112
113
  return doStatus(root, flags)
113
114
  case 'tick':
114
115
  return doTick(root, flags)
116
+ case 'sweep':
117
+ return err(
118
+ 'sweep is async; call cmdCiWatchSweep directly (the apt-tools dispatcher routes it separately)',
119
+ )
115
120
  default:
116
- return err(`Unknown ci-watch subcommand: ${subcommand}. Expected: start, stop, status, tick`)
121
+ return err(
122
+ `Unknown ci-watch subcommand: ${subcommand}. Expected: start, stop, status, tick, sweep`,
123
+ )
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Async sibling for the `sweep` subcommand (the rate-limited orphaned-state
129
+ * walker). Split out from the sync `cmdCiWatch` dispatcher so the latter's
130
+ * return type stays `CommandResult` (not a `CommandResult | Promise` union)
131
+ * and the synchronous tests + callers don't need to be wrapped in `await`.
132
+ *
133
+ * @param {string} projectDir
134
+ * @param {string[]} extraArgs
135
+ * @returns {Promise<CommandResult>}
136
+ */
137
+ export async function cmdCiWatchSweep(projectDir, extraArgs) {
138
+ if (!projectDir) {
139
+ return err('Usage: apt-tools ci-watch sweep <project-dir> [flags]')
117
140
  }
141
+ const root = resolve(projectDir)
142
+ const flags = parseFlags(extraArgs)
143
+ return await doSweep(root, flags)
118
144
  }
119
145
 
120
146
  // ---------------------------------------------------------------------------
@@ -245,6 +271,91 @@ function doStop(root, flags) {
245
271
  })
246
272
  }
247
273
 
274
+ // ---------------------------------------------------------------------------
275
+ // sweep — BUG-027 (c): manual GC for orphaned ci-watch state files.
276
+ //
277
+ // Enumerates all watcher state files via `listWatches(root)`, shells out
278
+ // to `gh pr view --json state` for each one (rate-limited to ≤5/sec),
279
+ // and removes the state files whose PR state is MERGED or CLOSED. Never
280
+ // auto-runs on startup — opt-in manual subcommand only, per brief.
281
+ // Honors APT_GH_FIXTURE_JSON for hermetic tests (returns the same body
282
+ // for every PR — fine since the test fixtures we ship cover each
283
+ // branch in isolation).
284
+ // ---------------------------------------------------------------------------
285
+
286
+ async function doSweep(root, flags) {
287
+ if (flags.get('orphaned') !== true) {
288
+ return err(
289
+ 'ci-watch sweep currently supports only `--orphaned`. Usage: apt-tools ci-watch sweep <project-dir> --orphaned',
290
+ )
291
+ }
292
+ const dryRun = flags.get('dry-run') === true
293
+ const watches = listWatches(root)
294
+ const removed = []
295
+ const skipped = []
296
+ const ghFixture = process.env.APT_GH_FIXTURE_JSON
297
+ for (let i = 0; i < watches.length; i++) {
298
+ const watch = watches[i]
299
+ const pr = watch.pr
300
+ const prUrl = watch.pr_url ?? null
301
+ // Validate pr_url shape before passing to gh: must be an https:// URL
302
+ // or absent. An invalid value (e.g. a flag-injection string from a
303
+ // tampered state file) falls back to the integer pr number.
304
+ const safeUrl =
305
+ typeof prUrl === 'string' && /^https:\/\/.+/.test(prUrl) && !prUrl.startsWith('-')
306
+ ? prUrl
307
+ : null
308
+ // Need a PR URL to call `gh pr view`; if absent, fall back to the
309
+ // integer pr arg which gh accepts for the active repo.
310
+ const target = safeUrl ?? String(pr)
311
+ let body = null
312
+ if (ghFixture) {
313
+ body = ghFixture
314
+ } else {
315
+ try {
316
+ const res = spawnSync('gh', ['pr', 'view', target, '--json', 'state'], {
317
+ cwd: root,
318
+ encoding: 'utf-8',
319
+ })
320
+ if (res.status === 0 && res.stdout) body = res.stdout
321
+ } catch {
322
+ /* gh transport error — skip this entry */
323
+ }
324
+ }
325
+ if (!body) {
326
+ skipped.push({ pr, reason: 'gh-unavailable' })
327
+ continue
328
+ }
329
+ let parsed
330
+ try {
331
+ parsed = JSON.parse(body)
332
+ } catch {
333
+ skipped.push({ pr, reason: 'malformed-gh-json' })
334
+ continue
335
+ }
336
+ const state = parsed?.state ?? null
337
+ if (state !== 'MERGED' && state !== 'CLOSED') {
338
+ skipped.push({ pr, reason: `pr-state-${state ?? 'unknown'}` })
339
+ continue
340
+ }
341
+ if (!dryRun) deleteState(root, pr)
342
+ removed.push({ pr, pr_state: state })
343
+ // Rate-limit ≤5/sec per brief — sleep 200ms between live gh calls.
344
+ // Use async setTimeout (non-blocking) rather than Atomics.wait.
345
+ if (!ghFixture && i < watches.length - 1) {
346
+ await new Promise((resolve) => setTimeout(resolve, 200))
347
+ }
348
+ }
349
+ return ok({
350
+ status: 'ok',
351
+ command: 'ci-watch-sweep',
352
+ dry_run: dryRun,
353
+ removed,
354
+ skipped,
355
+ total_watches: watches.length,
356
+ })
357
+ }
358
+
248
359
  // ---------------------------------------------------------------------------
249
360
  // status
250
361
  // ---------------------------------------------------------------------------