@aperant/framework 0.6.7 → 0.7.3

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 (205) hide show
  1. package/CHANGELOG.md +240 -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 +147 -3
  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 +119 -3
  19. package/dist/cli/commands/health-check.mjs.map +1 -1
  20. package/dist/cli/commands/init.d.mts +19 -1
  21. package/dist/cli/commands/init.d.mts.map +1 -1
  22. package/dist/cli/commands/init.mjs +143 -8
  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/route.d.mts.map +1 -1
  32. package/dist/cli/commands/route.mjs +37 -2
  33. package/dist/cli/commands/route.mjs.map +1 -1
  34. package/dist/cli/commands/task.d.mts.map +1 -1
  35. package/dist/cli/commands/task.mjs +132 -5
  36. package/dist/cli/commands/task.mjs.map +1 -1
  37. package/dist/cli/commands/validate-evidence.d.mts +24 -2
  38. package/dist/cli/commands/validate-evidence.d.mts.map +1 -1
  39. package/dist/cli/commands/validate-evidence.mjs +154 -17
  40. package/dist/cli/commands/validate-evidence.mjs.map +1 -1
  41. package/dist/cli/commands/vitest-doctor.d.mts +2 -0
  42. package/dist/cli/commands/vitest-doctor.d.mts.map +1 -0
  43. package/dist/cli/commands/vitest-doctor.mjs +168 -0
  44. package/dist/cli/commands/vitest-doctor.mjs.map +1 -0
  45. package/dist/cli/config/gitignore-drift.d.mts +23 -1
  46. package/dist/cli/config/gitignore-drift.d.mts.map +1 -1
  47. package/dist/cli/config/gitignore-drift.mjs +81 -3
  48. package/dist/cli/config/gitignore-drift.mjs.map +1 -1
  49. package/dist/cli/config/load.d.mts +56 -2
  50. package/dist/cli/config/load.d.mts.map +1 -1
  51. package/dist/cli/config/load.mjs +192 -2
  52. package/dist/cli/config/load.mjs.map +1 -1
  53. package/dist/cli/consistency/parse-review.mjs +7 -0
  54. package/dist/cli/consistency/parse-review.mjs.map +1 -1
  55. package/dist/cli/dispatch.d.mts.map +1 -1
  56. package/dist/cli/dispatch.mjs +24 -2
  57. package/dist/cli/dispatch.mjs.map +1 -1
  58. package/dist/cli/gate/gates/gitignore-in-sync.d.mts +1 -1
  59. package/dist/cli/gate/gates/gitignore-in-sync.d.mts.map +1 -1
  60. package/dist/cli/gate/gates/gitignore-in-sync.mjs +5 -2
  61. package/dist/cli/gate/gates/gitignore-in-sync.mjs.map +1 -1
  62. package/dist/cli/gate/gates/review-clean.d.mts +5 -1
  63. package/dist/cli/gate/gates/review-clean.d.mts.map +1 -1
  64. package/dist/cli/gate/gates/review-clean.mjs +23 -18
  65. package/dist/cli/gate/gates/review-clean.mjs.map +1 -1
  66. package/dist/cli/gate/gates/verify-approved.d.mts +49 -1
  67. package/dist/cli/gate/gates/verify-approved.d.mts.map +1 -1
  68. package/dist/cli/gate/gates/verify-approved.mjs +93 -14
  69. package/dist/cli/gate/gates/verify-approved.mjs.map +1 -1
  70. package/dist/cli/help.d.mts.map +1 -1
  71. package/dist/cli/help.mjs +8 -2
  72. package/dist/cli/help.mjs.map +1 -1
  73. package/dist/cli/host/detect.d.mts +122 -9
  74. package/dist/cli/host/detect.d.mts.map +1 -1
  75. package/dist/cli/host/detect.mjs +132 -0
  76. package/dist/cli/host/detect.mjs.map +1 -1
  77. package/dist/cli/install/legacy-paths.d.mts +38 -0
  78. package/dist/cli/install/legacy-paths.d.mts.map +1 -0
  79. package/dist/cli/install/legacy-paths.mjs +69 -0
  80. package/dist/cli/install/legacy-paths.mjs.map +1 -0
  81. package/dist/cli/install/runtime-detect.d.mts +13 -0
  82. package/dist/cli/install/runtime-detect.d.mts.map +1 -1
  83. package/dist/cli/install/runtime-detect.mjs +9 -0
  84. package/dist/cli/install/runtime-detect.mjs.map +1 -1
  85. package/dist/cli/install/runtime-migrate.d.mts +84 -0
  86. package/dist/cli/install/runtime-migrate.d.mts.map +1 -0
  87. package/dist/cli/install/runtime-migrate.mjs +244 -0
  88. package/dist/cli/install/runtime-migrate.mjs.map +1 -0
  89. package/dist/cli/route/drift-detect.d.mts +20 -0
  90. package/dist/cli/route/drift-detect.d.mts.map +1 -0
  91. package/dist/cli/route/drift-detect.mjs +107 -0
  92. package/dist/cli/route/drift-detect.mjs.map +1 -0
  93. package/dist/cli/task/index-md.d.mts.map +1 -1
  94. package/dist/cli/task/index-md.mjs +14 -2
  95. package/dist/cli/task/index-md.mjs.map +1 -1
  96. package/dist/cli/util/aperant-section.d.mts +34 -0
  97. package/dist/cli/util/aperant-section.d.mts.map +1 -0
  98. package/dist/cli/util/aperant-section.mjs +127 -0
  99. package/dist/cli/util/aperant-section.mjs.map +1 -0
  100. package/dist/cli/util/copy.d.mts +28 -1
  101. package/dist/cli/util/copy.d.mts.map +1 -1
  102. package/dist/cli/util/copy.mjs +43 -55
  103. package/dist/cli/util/copy.mjs.map +1 -1
  104. package/dist/cli/util/semver.d.mts +17 -0
  105. package/dist/cli/util/semver.d.mts.map +1 -0
  106. package/dist/cli/util/semver.mjs +29 -0
  107. package/dist/cli/util/semver.mjs.map +1 -0
  108. package/dist/cli/util/skill-installs.d.mts +65 -9
  109. package/dist/cli/util/skill-installs.d.mts.map +1 -1
  110. package/dist/cli/util/skill-installs.mjs +130 -21
  111. package/dist/cli/util/skill-installs.mjs.map +1 -1
  112. package/dist/cli/util/version-preflight.d.mts +44 -0
  113. package/dist/cli/util/version-preflight.d.mts.map +1 -0
  114. package/dist/cli/util/version-preflight.mjs +66 -0
  115. package/dist/cli/util/version-preflight.mjs.map +1 -0
  116. package/dist/plugin/.claude-plugin/plugin.json +11 -2
  117. package/dist/plugin/agents/apt-improver.md +99 -0
  118. package/dist/plugin/agents/apt-planner.md +127 -10
  119. package/dist/plugin/agents/apt-pr-review-fixer.md +13 -9
  120. package/dist/plugin/skills/apt/SKILL.md +1 -0
  121. package/dist/plugin/skills/apt-close-task/SKILL.md +63 -1
  122. package/dist/plugin/skills/apt-debug/SKILL.md +39 -6
  123. package/dist/plugin/skills/apt-debug/appendices/diagnose-discipline.md +119 -0
  124. package/dist/plugin/skills/apt-diagram/SKILL.md +378 -0
  125. package/dist/plugin/skills/apt-diagram/appendices/design-discipline.md +97 -0
  126. package/dist/plugin/skills/apt-discuss/SKILL.md +72 -5
  127. package/dist/plugin/skills/apt-discuss/appendices/grill-discipline.md +104 -0
  128. package/dist/plugin/skills/apt-discuss/appendices/zoom-out-helper.md +79 -0
  129. package/dist/plugin/skills/apt-execute/SKILL.md +57 -5
  130. package/dist/plugin/skills/apt-execute/appendices/tdd-mode.md +107 -0
  131. package/dist/plugin/skills/apt-improve/DEEPENING.md +84 -0
  132. package/dist/plugin/skills/apt-improve/INTERFACE-DESIGN.md +97 -0
  133. package/dist/plugin/skills/apt-improve/LANGUAGE.md +104 -0
  134. package/dist/plugin/skills/apt-improve/SKILL.md +141 -0
  135. package/dist/plugin/skills/apt-plan/SKILL.md +171 -4
  136. package/dist/plugin/skills/apt-plan/adapters/conductor.md +98 -0
  137. package/dist/plugin/skills/apt-pr-review/SKILL.md +57 -18
  138. package/dist/plugin/skills/apt-prototype/LOGIC.md +109 -0
  139. package/dist/plugin/skills/apt-prototype/SKILL.md +143 -0
  140. package/dist/plugin/skills/apt-prototype/UI.md +90 -0
  141. package/dist/plugin/skills/apt-quick/SKILL.md +49 -8
  142. package/dist/plugin/skills/apt-release-notes/SKILL.md +193 -0
  143. package/dist/plugin/skills/apt-release-notes/appendices/persona-voice.md +59 -0
  144. package/dist/plugin/skills/apt-review/SKILL.md +2 -0
  145. package/dist/plugin/skills/apt-run/SKILL.md +32 -4
  146. package/dist/plugin/skills/apt-setup/SKILL.md +308 -6
  147. package/dist/plugin/skills/apt-ship/SKILL.md +122 -1
  148. package/dist/plugin/skills/apt-spar/SKILL.md +315 -0
  149. package/dist/plugin/skills/apt-triage/AGENT-BRIEF.md +84 -0
  150. package/dist/plugin/skills/apt-triage/OUT-OF-SCOPE.md +75 -0
  151. package/dist/plugin/skills/apt-triage/SKILL.md +169 -0
  152. package/dist/plugin/skills/apt-update/SKILL.md +77 -10
  153. package/dist/plugin/skills/apt-verify/SKILL.md +3 -0
  154. package/dist/plugin/skills/apt-verify-proof/SKILL.md +10 -5
  155. package/dist/plugin/skills/apt-watch-ci/SKILL.md +166 -0
  156. package/dist/plugin/skills/apt-zoom-out/SKILL.md +130 -0
  157. package/package.json +133 -133
  158. package/prompts/conductor-framework-context.md +63 -0
  159. package/prompts/conductor-system.md +11 -0
  160. package/skills/apt-close-task/SKILL.md +6 -0
  161. package/skills/apt-discuss/SKILL.md +47 -5
  162. package/skills/apt-execute/SKILL.md +9 -0
  163. package/skills/apt-plan/SKILL.md +12 -0
  164. package/skills/apt-pr-review/SKILL.md +11 -2
  165. package/skills/apt-quick/SKILL.md +19 -8
  166. package/skills/apt-researcher.md +1 -0
  167. package/skills/apt-setup/SKILL.md +33 -2
  168. package/skills/apt-ship/SKILL.md +16 -4
  169. package/skills/apt-spar/SKILL.md +36 -11
  170. package/skills/apt-update/SKILL.md +26 -1
  171. package/skills/apt-verify-proof/SKILL.md +7 -5
  172. package/skills/apt-watch-ci/SKILL.md +4 -1
  173. package/src/cli/ci-watch/stop-matrix.mjs +17 -0
  174. package/src/cli/commands/ci-watch.mjs +152 -3
  175. package/src/cli/commands/features-audit.mjs +164 -5
  176. package/src/cli/commands/health-check.mjs +116 -3
  177. package/src/cli/commands/init.mjs +154 -6
  178. package/src/cli/commands/modes.mjs +11 -0
  179. package/src/cli/commands/pr-review-audit-fixer.mjs +18 -5
  180. package/src/cli/commands/route.mjs +38 -2
  181. package/src/cli/commands/task.mjs +132 -5
  182. package/src/cli/commands/validate-evidence.mjs +158 -17
  183. package/src/cli/commands/vitest-doctor.mjs +173 -0
  184. package/src/cli/config/gitignore-drift.mjs +74 -3
  185. package/src/cli/config/load.mjs +188 -2
  186. package/src/cli/consistency/parse-review.mjs +6 -0
  187. package/src/cli/dispatch.mjs +23 -2
  188. package/src/cli/gate/gates/gitignore-in-sync.mjs +5 -2
  189. package/src/cli/gate/gates/review-clean.mjs +24 -19
  190. package/src/cli/gate/gates/verify-approved.mjs +97 -14
  191. package/src/cli/help.mjs +8 -2
  192. package/src/cli/host/detect.mjs +135 -0
  193. package/src/cli/install/legacy-paths.mjs +69 -0
  194. package/src/cli/install/runtime-detect.mjs +9 -0
  195. package/src/cli/install/runtime-migrate.mjs +252 -0
  196. package/src/cli/route/drift-detect.mjs +107 -0
  197. package/src/cli/task/index-md.mjs +15 -2
  198. package/src/cli/util/aperant-section.mjs +136 -0
  199. package/src/cli/util/copy.mjs +43 -56
  200. package/src/cli/util/semver.mjs +28 -0
  201. package/src/cli/util/skill-installs.mjs +134 -21
  202. package/src/cli/util/version-preflight.mjs +65 -0
  203. package/templates/aperant-claude-md-appendix.md +37 -0
  204. package/templates/config.json +2 -7
  205. package/workflows/verify-proof.md +8 -3
@@ -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
 
@@ -412,19 +412,30 @@ ci_watch_armed=$(echo "$ship_update" | jq -r '.ci_watch_armed // false')
412
412
  ci_watch_skip_reason=$(echo "$ship_update" | jq -r '.ci_watch_skip_reason // empty')
413
413
  ci_watch_host=$(echo "$ship_update" | jq -r '.ci_watch.host // "unknown"')
414
414
  ci_watch_deadline=$(echo "$ship_update" | jq -r '.ci_watch.deadline // empty')
415
+ ci_watch_scheduler_handle=$(echo "$ship_update" | jq -r '.scheduler_handle // empty')
415
416
  host_cron=$(node packages/framework/bin/apt-tools.mjs host-detect | jq -r '.host.capabilities.cron')
416
417
  ```
417
418
 
418
419
  **Autonomy interaction** (apply BEFORE branching, same rule as 7.1): when `autonomy === 3` (YOLO), degrade `ask` → `auto`.
419
420
 
420
- **Branch on the task-update envelope:**
421
+ **Branch on the task-update envelope** (FRAMEWORK-BUG-018 — Track 1: distinguish state-recording from wake-registration):
421
422
 
422
- - **`ci_watch_armed=true` AND `host_cron=true`** — emit:
423
+ - **`ci_watch_armed=true` AND `host_cron=true` AND `ci_watch_scheduler_handle` non-empty** — emit:
423
424
 
424
425
  ```
425
426
  [APT:ship] CI watcher armed (host cron: {ci_watch_host}). Deadline: {ci_watch_deadline}. Stop: /apt:watch-ci --stop --pr {N}.
426
427
  ```
427
428
 
429
+ This is the only honest "armed" case — a scheduler handle proves the wake is registered.
430
+
431
+ - **`ci_watch_armed=true` AND `host_cron=true` AND `ci_watch_scheduler_handle` empty** — emit:
432
+
433
+ ```
434
+ [APT:ship] CI watcher state created (host: {ci_watch_host}). Register a wake-up to drive ticks: CronCreate every ~3min running '/apt:watch-ci --tick --pr {N}'. Deadline: {ci_watch_deadline}. Stop: /apt:watch-ci --stop --pr {N}.
435
+ ```
436
+
437
+ State is recorded but no scheduler is registered — the orchestrator MUST register a wake or ticks will never fire. Track 2 (auto-wake on arm) will populate `scheduler_handle` and route into the first branch instead.
438
+
428
439
  - **`ci_watch_armed=true` AND `host_cron=false`** — emit:
429
440
 
430
441
  ```
@@ -442,10 +453,11 @@ host_cron=$(node packages/framework/bin/apt-tools.mjs host-detect | jq -r '.host
442
453
  On `Y` (or empty enter), call:
443
454
 
444
455
  ```bash
445
- node packages/framework/bin/apt-tools.mjs ci-watch start . --pr {pr_number} --task-id {task-id} --host {cli}
456
+ arm_envelope=$(node packages/framework/bin/apt-tools.mjs ci-watch start . --pr {pr_number} --task-id {task-id} --host {cli})
457
+ ci_watch_scheduler_handle=$(echo "$arm_envelope" | jq -r '.scheduler_handle // empty')
446
458
  ```
447
459
 
448
- Then emit the same host-cron-aware success banner used for `ci_watch_armed=true`. On `N` or any other response, proceed silently to Section 8.
460
+ Then route into the same four-way logic above — branch 1 (armed with handle), branch 2 (state-created, needs wake), or branch 3 (host has no cron). On `N` or any other response, proceed silently to Section 8.
449
461
 
450
462
  - **`ci_watch_skip_reason=disabled`** — silent. Proceed directly to Section 8.
451
463
 
@@ -129,14 +129,26 @@ If `--with` resolves to the same CLI as the host (e.g. `--with claude` when `hos
129
129
 
130
130
  Do NOT proceed; self-sparring is sycophancy by construction (the host cannot objectively challenge itself). This check runs after `host-detect` and before any partner availability check.
131
131
 
132
- **Partner unavailable.** If the resolved partner CLI binary or script is not on `$PATH` or, for the codex-companion path, if `CLAUDE_PLUGIN_ROOT` is unset OR `${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs` does not exist print a clean fallback and stop:
132
+ **Partner transport ladder (FRAMEWORK-BUG-024).** Read `host.partner_transports.<partner>` from the host-detect envelope (`jq '.host.partner_transports.codex'`, etc.). Each partner has a 3-rung ladder evaluated in preference order **MCP-first** for cleaner tool-call integration, with automatic fallthrough to plugin shim then raw CLI:
133
+
134
+ 1. **Rung 1 — MCP tool** (`mcp__<partner>-mcp__*`, e.g. `mcp__codex-mcp__codex`). Detected if the host exposes its deferred-tool manifest via `CLAUDE_MCP_DEFERRED_TOOLS` and the tool is registered. When `detected: "runtime-host-only"` the framework process cannot probe the host's tool registry from outside — treat as candidate and attempt the tool call; if the tool is genuinely unavailable in the host's visible registry, fall through to rung 2.
135
+ 2. **Rung 2 — Plugin shim** (`${CLAUDE_PLUGIN_ROOT}/scripts/<partner>-companion.mjs`). Codex only (claude / gemini have no plugin shim).
136
+ 3. **Rung 3 — Raw CLI on `$PATH`** (`codex exec`, `claude -p`, `gemini -m`).
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."
139
+
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:
133
141
 
134
142
  ```
135
- [apt:spar] partner `<id>` unavailable. Either:
136
- - install the partner CLI (Codex: install the `codex` Claude Code plugin so `CLAUDE_PLUGIN_ROOT/scripts/codex-companion.mjs` resolves; Claude: install the `claude` CLI on $PATH; Gemini: install the `gemini` CLI on $PATH)
137
- - re-run with `--with <other-id>` to pick a different partner
143
+ [apt:spar] partner `<id>` unavailable. Tried:
144
+ - MCP tool `mcp__<partner>-mcp__*`: <not loaded in this host | runtime-host-only + timed out at <ms>ms>
145
+ - Plugin shim `${CLAUDE_PLUGIN_ROOT}/scripts/<partner>-companion.mjs`: <missing reason>
146
+ - Raw CLI `<bin>` on $PATH: not found
147
+ Install any one of the three or re-run with `--with <other-id>`.
138
148
  ```
139
149
 
150
+ The failure message MUST name all three transports tried (`mcp`, `plugin shim`, `raw CLI`) so the user can pick the cheapest fix.
151
+
140
152
  Do NOT silently degrade to the host (sparring with yourself is sycophancy by construction).
141
153
 
142
154
  ## 4. Frame the Round
@@ -159,13 +171,26 @@ The frame is one shot per round — no multi-turn conversation with the partner
159
171
 
160
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.
161
173
 
162
- The table below shows the canonical command shape; the actual invocation MUST use the heredoc pattern in the next subsection — never raw double-quoted interpolation.
163
-
164
- | Host | Command | Notes |
165
- |---|---|---|
166
- | `host=claude-code` (or `--with codex`) | `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. |
167
- | `host=codex` (or `--with claude`) | `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. |
168
- | `--with gemini` (any host) | `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. |
174
+ 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
+ <!--
177
+ Future contributor: the `codex exec` flag below is taken from the precedent
178
+ at `packages/framework/skills/apt-pr-review/SKILL.md:1084`. Codex CLI flag
179
+ surface evolves; verify with `codex --help` at execution time and update
180
+ this table if the non-interactive invocation shape changes.
181
+ -->
182
+
183
+ | Partner | Rung | Command | Notes |
184
+ |---|---|---|---|
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 | 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
+ | 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 | plugin | n/a | No plugin shim exists for Claude. Skip this rung. |
190
+ | 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 | plugin | n/a | No plugin shim exists for Gemini. Skip this rung. |
193
+ | 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. |
169
194
 
170
195
  Set the Bash tool's `timeout` parameter to `--timeout-ms` (default 90000).
171
196
 
@@ -15,7 +15,7 @@ execution_modes:
15
15
  - auto
16
16
  - step
17
17
  allowed-tools: "Bash, Read, Write"
18
- argument-hint: "apt:update"
18
+ argument-hint: "apt:update [--from-source]"
19
19
  gates: []
20
20
  ---
21
21
 
@@ -90,6 +90,30 @@ npm install --prefix .aperant/deps @aperant/framework@<latest_version> --no-save
90
90
 
91
91
  On `npm 404` (`E404` in stderr) treat it as `not_published` and skip the kernel bump (proceed to step 3). On any other npm failure, surface the npm stderr verbatim and STOP — do NOT proceed to init with a half-installed kernel.
92
92
 
93
+ ## 1.6. Self-host case — bump from local workspace (FRAMEWORK-BUG-017)
94
+
95
+ When the user invokes `/apt:update --from-source` OR the route envelope's `update_check.reason` mentions `workspace source ahead of installed kernel`, the source of truth is the local `packages/framework/` workspace — NOT npm. This is the Aperant-contributor self-host case: a same-PR framework fix bumps the workspace `packages/framework/package.json:version` but the kernel under `.aperant/deps/node_modules/@aperant/framework/` still reflects the previous version. Step 1.5's `npm view` would fetch the published version (still the OLD one), so we must skip it.
96
+
97
+ The reliable bump mechanism mirrors what `runKernelInstall` does today — `pnpm pack` produces a versioned tarball, then `npm install --prefix` consumes it. Symlinking is fragile across re-installs and `pnpm install --filter` doesn't relocate the published artifact into `.aperant/deps`.
98
+
99
+ Walk up from the project root looking for a sibling `packages/framework/package.json`. When found AND its `name === "@aperant/framework"`:
100
+
101
+ ```bash
102
+ # 1. Pack the workspace source into a versioned tarball.
103
+ cd <workspace-root>
104
+ pnpm --filter @aperant/framework pack --pack-destination /tmp
105
+
106
+ # 2. Install the tarball into the project's kernel root.
107
+ cd <project-root>
108
+ npm install --prefix .aperant/deps /tmp/aperant-framework-<version>.tgz --no-save --no-package-lock --no-audit --no-fund
109
+ ```
110
+
111
+ Replace `<version>` with the workspace `package.json:version` you just read. The tarball name follows npm's `pack` convention (`@aperant/framework@X.Y.Z` → `aperant-framework-X.Y.Z.tgz`).
112
+
113
+ If `--from-source` was passed but no `packages/framework/package.json` is found anywhere above the project root, surface a one-line error (`⚠ --from-source: no workspace package.json found — falling back to npm`) and proceed with the standard step 1.5 npm path.
114
+
115
+ Then proceed to step 3 to re-install the runtime files at the new kernel version. Skip step 4 (cache clear) — the cache is keyed on `latest_version` from npm, and a `--from-source` bump never wrote to it, so there's nothing to invalidate.
116
+
93
117
  ## 2. Classify the update scenario
94
118
 
95
119
  Pick ONE of three scenarios based on what you parsed in step 1:
@@ -206,5 +230,6 @@ Runtime files were still refreshed; `/apt` will still work from PATH if `apt-too
206
230
  - **Worker hasn't run yet**: the cache file is missing. That's fine — run check-version directly (step 1 is robust to a missing cache). Proceed normally.
207
231
  - **Re-install fails (non-zero exit from init)**: surface the JSON `error` field verbatim in a single line, then stop. Do NOT delete the cache file — the chip should remain visible so the user sees the failure persists.
208
232
  - **Kernel install fails but runtime install succeeds**: see Scenario D above. Do not treat this as a total failure; the user's skills still work via PATH fallback.
233
+ - **CLAUDE.md / AGENTS.md foreign rows (exit 2, `instruction-file.foreign-rows`)** — 0.7.0+ — init now refuses to silently clobber custom rows in the `<!-- APT:framework-* -->` block. If `init` exits 2 with `code: instruction-file.foreign-rows`, the `foreignRowIds[]` field lists the rows the generator did NOT produce. Two paths: (a) remove the foreign rows from CLAUDE.md / AGENTS.md by hand and re-run init, or (b) re-run with `--force-overwrite-catalog` to regenerate anyway. Unattended upgrade flows should detect this code and surface it to the operator — silent bypass is a regression.
209
234
 
210
235
  </edge_cases>
@@ -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
@@ -87,7 +87,10 @@ For the `--tick` mode, execute exactly the following sequence — once — insid
87
87
 
88
88
  When `action ∈ {green, merged, closed, attempts-cap, wall-cap, identical-log}`:
89
89
 
90
- - **green** — print "CI green for PR {N}." If `ci_watch.auto_merge_on_green` is `true` in `.aperant/config.json`, run `gh pr merge {N} --auto --squash` then suggest `/apt:close-task`. Else print the exact D-08 text:
90
+ - **green** — print "CI green for PR {N}." If `ci_watch.auto_merge_on_green` is `true` in `.aperant/config.json`, run `gh pr merge {N} --auto --squash` then suggest `/apt:close-task`.
91
+ - **DO NOT pass `--delete-branch`** to `gh pr merge` from this auto-merge path — ever, unconditionally. The watcher's cwd is the task worktree, where `gh`'s `--delete-branch` post-merge cleanup tries to `git switch <base>` and fails because `<base>` is checked out in the primary worktree. The remote merge has already succeeded by then; local branch + worktree cleanup is `/apt:close-task`'s job via `apt-tools task close-merged` → `computeWorktreeCleanup` (see FRAMEWORK-BUG-019).
92
+
93
+ Else print the exact D-08 text:
91
94
 
92
95
  ```
93
96
  ✅ CI green — merge in browser or run /apt:close-task post-merge
@@ -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,14 +24,36 @@
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
- import { resolve } from 'node:path'
30
+ import { dirname, resolve } from 'node:path'
31
+ import { fileURLToPath } from 'node:url'
30
32
  import { acquireLock, releaseLock } from '../ci-watch/lock.mjs'
31
33
  import { deleteState, listWatches, readState, writeState } from '../ci-watch/state.mjs'
32
34
  import { classify, isFailing } from '../ci-watch/stop-matrix.mjs'
33
35
  import { parseFlags } from '../util/args.mjs'
34
36
  import { err, exitWith, ok } from '../util/result.mjs'
37
+ import { requireMinVersion } from '../util/version-preflight.mjs'
38
+
39
+ /**
40
+ * Installed kernel version — read once at module scope, same pattern as
41
+ * commands/check-version.mjs:108-109. Used by the version-preflight in
42
+ * `doTick` to fail loud (instead of infra-backoff-loop) when a future
43
+ * framework fix introduces a new tick flag and the user is on a stale
44
+ * install. See FRAMEWORK-BUG-017 rung 2 + the helper docstring for the
45
+ * "future-guard" framing.
46
+ */
47
+ const INSTALLED_KERNEL_VERSION = (() => {
48
+ try {
49
+ const here = dirname(fileURLToPath(import.meta.url))
50
+ const pkgPath = resolve(here, '..', '..', '..', 'package.json')
51
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
52
+ return typeof pkg?.version === 'string' ? pkg.version : null
53
+ } catch {
54
+ return null
55
+ }
56
+ })()
35
57
 
36
58
  const DEFAULT_TICK_INTERVAL_MS = 300_000 // 5 minutes
37
59
  const MAX_TICK_INTERVAL_MS = 30 * 60 * 1000 // 30 min
@@ -73,7 +95,7 @@ function validatePr(pr) {
73
95
  */
74
96
  export function cmdCiWatch(subcommand, projectDir, extraArgs) {
75
97
  if (!subcommand) {
76
- 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]')
77
99
  }
78
100
  if (!projectDir) {
79
101
  return err(`Usage: apt-tools ci-watch ${subcommand} <project-dir> [flags]`)
@@ -91,9 +113,34 @@ export function cmdCiWatch(subcommand, projectDir, extraArgs) {
91
113
  return doStatus(root, flags)
92
114
  case 'tick':
93
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
+ )
94
120
  default:
95
- 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]')
96
140
  }
141
+ const root = resolve(projectDir)
142
+ const flags = parseFlags(extraArgs)
143
+ return await doSweep(root, flags)
97
144
  }
98
145
 
99
146
  // ---------------------------------------------------------------------------
@@ -119,12 +166,14 @@ function doStart(root, flags) {
119
166
  command: 'ci-watch-start',
120
167
  pr: Number(pr),
121
168
  reason: 'already-armed',
169
+ scheduler_handle: existingState.scheduler_handle ?? null,
122
170
  existing: {
123
171
  task_id: existingState.task_id,
124
172
  host: existingState.host,
125
173
  attempts: existingState.attempts,
126
174
  started_at: existingState.started_at,
127
175
  deadline: existingState.deadline,
176
+ scheduler_handle: existingState.scheduler_handle ?? null,
128
177
  },
129
178
  },
130
179
  2,
@@ -222,6 +271,91 @@ function doStop(root, flags) {
222
271
  })
223
272
  }
224
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
+
225
359
  // ---------------------------------------------------------------------------
226
360
  // status
227
361
  // ---------------------------------------------------------------------------
@@ -261,6 +395,21 @@ function summarize(s) {
261
395
  // ---------------------------------------------------------------------------
262
396
 
263
397
  function doTick(root, flags) {
398
+ // FRAMEWORK-BUG-017 rung 2 (future-guard): emit a structured
399
+ // version_drift envelope when a future framework fix requires a
400
+ // new tick flag and the installed kernel predates it. The current
401
+ // pin to 0.6.7 (the version that introduced --payload-file) is a
402
+ // no-op on every 0.6.7+ install — but the scaffolding is in place
403
+ // for the NEXT new-flag fix to bump `introducedIn` without
404
+ // re-discovering the silent-infra-backoff failure mode.
405
+ const versionGate = requireMinVersion({
406
+ feature: 'payload-file',
407
+ introducedIn: '0.6.7',
408
+ installedVersion: INSTALLED_KERNEL_VERSION,
409
+ missingFlag: '--payload-file',
410
+ })
411
+ if (versionGate) return exitWith(versionGate, 1)
412
+
264
413
  const prCheck = validatePr(flags.get('pr'))
265
414
  if (typeof prCheck !== 'string') return prCheck
266
415
  const pr = prCheck