@aperant/framework 0.17.0 → 0.20.0

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 (242) hide show
  1. package/CHANGELOG.md +261 -0
  2. package/agents/apt-executor.md +6 -0
  3. package/agents/apt-planner.md +3 -2
  4. package/bin/features-reconcile-check.mjs +77 -0
  5. package/dist/cli/artifacts/self-stage.d.mts.map +1 -1
  6. package/dist/cli/artifacts/self-stage.mjs +2 -1
  7. package/dist/cli/artifacts/self-stage.mjs.map +1 -1
  8. package/dist/cli/cli-wrappers/features-reconcile.d.mts +2 -0
  9. package/dist/cli/cli-wrappers/features-reconcile.d.mts.map +1 -0
  10. package/dist/cli/cli-wrappers/features-reconcile.mjs +9 -0
  11. package/dist/cli/cli-wrappers/features-reconcile.mjs.map +1 -0
  12. package/dist/cli/commands/ci-watch.mjs +49 -2
  13. package/dist/cli/commands/ci-watch.mjs.map +1 -1
  14. package/dist/cli/commands/commit.mjs +3 -3
  15. package/dist/cli/commands/commit.mjs.map +1 -1
  16. package/dist/cli/commands/event.mjs +16 -16
  17. package/dist/cli/commands/event.mjs.map +1 -1
  18. package/dist/cli/commands/features-reconcile.d.mts +2 -0
  19. package/dist/cli/commands/features-reconcile.d.mts.map +1 -0
  20. package/dist/cli/commands/features-reconcile.mjs +143 -0
  21. package/dist/cli/commands/features-reconcile.mjs.map +1 -0
  22. package/dist/cli/commands/health-check.d.mts +2 -2
  23. package/dist/cli/commands/health-check.mjs +2 -2
  24. package/dist/cli/commands/merge-integrate.mjs +3 -3
  25. package/dist/cli/commands/merge-integrate.mjs.map +1 -1
  26. package/dist/cli/commands/produce.d.mts +9 -0
  27. package/dist/cli/commands/produce.d.mts.map +1 -0
  28. package/dist/cli/commands/produce.mjs +1345 -0
  29. package/dist/cli/commands/produce.mjs.map +1 -0
  30. package/dist/cli/commands/task.d.mts +16 -1
  31. package/dist/cli/commands/task.d.mts.map +1 -1
  32. package/dist/cli/commands/task.mjs +434 -266
  33. package/dist/cli/commands/task.mjs.map +1 -1
  34. package/dist/cli/coordination/auto-emit-artifact-ready.mjs +5 -5
  35. package/dist/cli/coordination/auto-emit-artifact-ready.mjs.map +1 -1
  36. package/dist/cli/coordination/event-schema.d.mts +5 -3
  37. package/dist/cli/coordination/event-schema.d.mts.map +1 -1
  38. package/dist/cli/coordination/event-schema.mjs +245 -21
  39. package/dist/cli/coordination/event-schema.mjs.map +1 -1
  40. package/dist/cli/coordination/store.d.mts +2 -2
  41. package/dist/cli/coordination/store.mjs +4 -4
  42. package/dist/cli/coordination/store.mjs.map +1 -1
  43. package/dist/cli/dispatch.d.mts.map +1 -1
  44. package/dist/cli/dispatch.mjs +4 -2
  45. package/dist/cli/dispatch.mjs.map +1 -1
  46. package/dist/cli/features/area-shape.d.mts +11 -0
  47. package/dist/cli/features/area-shape.d.mts.map +1 -0
  48. package/dist/cli/features/area-shape.mjs +141 -0
  49. package/dist/cli/features/area-shape.mjs.map +1 -0
  50. package/dist/cli/features/delta.d.mts +78 -0
  51. package/dist/cli/features/delta.d.mts.map +1 -0
  52. package/dist/cli/features/delta.mjs +591 -0
  53. package/dist/cli/features/delta.mjs.map +1 -0
  54. package/dist/cli/features/reconcile.d.mts +236 -0
  55. package/dist/cli/features/reconcile.d.mts.map +1 -0
  56. package/dist/cli/features/reconcile.mjs +1126 -0
  57. package/dist/cli/features/reconcile.mjs.map +1 -0
  58. package/dist/cli/features/surface-map.d.mts +63 -0
  59. package/dist/cli/features/surface-map.d.mts.map +1 -0
  60. package/dist/cli/features/surface-map.mjs +191 -0
  61. package/dist/cli/features/surface-map.mjs.map +1 -0
  62. package/dist/cli/{commands/features-audit.d.mts → features/write-root.d.mts} +4 -3
  63. package/dist/cli/features/write-root.d.mts.map +1 -0
  64. package/dist/cli/features/write-root.mjs +137 -0
  65. package/dist/cli/features/write-root.mjs.map +1 -0
  66. package/dist/cli/gate/gates/features-reconciled.d.mts +36 -0
  67. package/dist/cli/gate/gates/features-reconciled.d.mts.map +1 -0
  68. package/dist/cli/gate/gates/features-reconciled.mjs +67 -0
  69. package/dist/cli/gate/gates/features-reconciled.mjs.map +1 -0
  70. package/dist/cli/gate/gates/index.mjs +1 -0
  71. package/dist/cli/gate/gates/index.mjs.map +1 -1
  72. package/dist/cli/help.d.mts.map +1 -1
  73. package/dist/cli/help.mjs +31 -1
  74. package/dist/cli/help.mjs.map +1 -1
  75. package/dist/cli/install/legacy-paths.d.mts.map +1 -1
  76. package/dist/cli/install/legacy-paths.mjs +2 -0
  77. package/dist/cli/install/legacy-paths.mjs.map +1 -1
  78. package/dist/cli/produce/blind-probe.d.mts +85 -0
  79. package/dist/cli/produce/blind-probe.d.mts.map +1 -0
  80. package/dist/cli/produce/blind-probe.mjs +217 -0
  81. package/dist/cli/produce/blind-probe.mjs.map +1 -0
  82. package/dist/cli/produce/claim.d.mts +188 -0
  83. package/dist/cli/produce/claim.d.mts.map +1 -0
  84. package/dist/cli/produce/claim.mjs +518 -0
  85. package/dist/cli/produce/claim.mjs.map +1 -0
  86. package/dist/cli/produce/done-gate.d.mts +87 -0
  87. package/dist/cli/produce/done-gate.d.mts.map +1 -0
  88. package/dist/cli/produce/done-gate.mjs +200 -0
  89. package/dist/cli/produce/done-gate.mjs.map +1 -0
  90. package/dist/cli/produce/events.d.mts +77 -0
  91. package/dist/cli/produce/events.d.mts.map +1 -0
  92. package/dist/cli/produce/events.mjs +126 -0
  93. package/dist/cli/produce/events.mjs.map +1 -0
  94. package/dist/cli/produce/evidence-oracle.d.mts +63 -0
  95. package/dist/cli/produce/evidence-oracle.d.mts.map +1 -0
  96. package/dist/cli/produce/evidence-oracle.mjs +122 -0
  97. package/dist/cli/produce/evidence-oracle.mjs.map +1 -0
  98. package/dist/cli/produce/ledger.d.mts +140 -0
  99. package/dist/cli/produce/ledger.d.mts.map +1 -0
  100. package/dist/cli/produce/ledger.mjs +272 -0
  101. package/dist/cli/produce/ledger.mjs.map +1 -0
  102. package/dist/cli/produce/probe-family.d.mts +53 -0
  103. package/dist/cli/produce/probe-family.d.mts.map +1 -0
  104. package/dist/cli/produce/probe-family.mjs +160 -0
  105. package/dist/cli/produce/probe-family.mjs.map +1 -0
  106. package/dist/cli/produce/projection.d.mts +55 -0
  107. package/dist/cli/produce/projection.d.mts.map +1 -0
  108. package/dist/cli/produce/projection.mjs +97 -0
  109. package/dist/cli/produce/projection.mjs.map +1 -0
  110. package/dist/cli/produce/run-id.d.mts +42 -0
  111. package/dist/cli/produce/run-id.d.mts.map +1 -0
  112. package/dist/cli/produce/run-id.mjs +79 -0
  113. package/dist/cli/produce/run-id.mjs.map +1 -0
  114. package/dist/cli/produce/saga.d.mts +180 -0
  115. package/dist/cli/produce/saga.d.mts.map +1 -0
  116. package/dist/cli/produce/saga.mjs +290 -0
  117. package/dist/cli/produce/saga.mjs.map +1 -0
  118. package/dist/cli/produce/scheduler.d.mts +165 -0
  119. package/dist/cli/produce/scheduler.d.mts.map +1 -0
  120. package/dist/cli/produce/scheduler.mjs +399 -0
  121. package/dist/cli/produce/scheduler.mjs.map +1 -0
  122. package/dist/cli/produce/setpoint.d.mts +52 -0
  123. package/dist/cli/produce/setpoint.d.mts.map +1 -0
  124. package/dist/cli/produce/setpoint.mjs +113 -0
  125. package/dist/cli/produce/setpoint.mjs.map +1 -0
  126. package/dist/cli/produce/verification-ttl.d.mts +75 -0
  127. package/dist/cli/produce/verification-ttl.d.mts.map +1 -0
  128. package/dist/cli/produce/verification-ttl.mjs +169 -0
  129. package/dist/cli/produce/verification-ttl.mjs.map +1 -0
  130. package/dist/cli/release-notes/ship-autodraft.mjs +2 -2
  131. package/dist/cli/roadmap/{conductor-view.d.mts → showrunner-view.d.mts} +3 -3
  132. package/dist/cli/roadmap/showrunner-view.d.mts.map +1 -0
  133. package/dist/cli/roadmap/{conductor-view.mjs → showrunner-view.mjs} +7 -7
  134. package/dist/cli/roadmap/showrunner-view.mjs.map +1 -0
  135. package/dist/cli/variant/gallery.d.mts.map +1 -1
  136. package/dist/cli/variant/gallery.mjs +7 -3
  137. package/dist/cli/variant/gallery.mjs.map +1 -1
  138. package/dist/plugin/.claude-plugin/plugin.json +2 -1
  139. package/dist/plugin/agents/apt-executor.md +6 -0
  140. package/dist/plugin/agents/apt-planner.md +3 -2
  141. package/dist/plugin/skills/apt/SKILL.md +112 -38
  142. package/dist/plugin/skills/apt-close-task/SKILL.md +2 -2
  143. package/dist/plugin/skills/apt-debug/SKILL.md +22 -24
  144. package/dist/plugin/skills/apt-execute/SKILL.md +35 -26
  145. package/dist/plugin/skills/apt-fan-out/SKILL.md +4 -4
  146. package/dist/plugin/skills/apt-handoff/SKILL.md +1 -1
  147. package/dist/plugin/skills/apt-mockup/SKILL.md +1 -1
  148. package/dist/plugin/skills/apt-plan/SKILL.md +5 -5
  149. package/dist/plugin/skills/apt-plan/adapters/{conductor.md → showrunner.md} +16 -16
  150. package/dist/plugin/skills/apt-produce/SKILL.md +606 -0
  151. package/dist/plugin/skills/apt-quick/SKILL.md +27 -33
  152. package/dist/plugin/skills/apt-review/SKILL.md +14 -11
  153. package/dist/plugin/skills/apt-run/SKILL.md +132 -3
  154. package/dist/plugin/skills/apt-ship/SKILL.md +29 -5
  155. package/dist/plugin/skills/apt-spar/SKILL.md +5 -3
  156. package/dist/plugin/skills/apt-watch-ci/SKILL.md +6 -4
  157. package/dist/schemas/feature-registry.d.ts.map +1 -1
  158. package/dist/schemas/feature-registry.js +34 -3
  159. package/dist/schemas/feature-registry.js.map +1 -1
  160. package/dist/types/config.d.ts +8 -5
  161. package/dist/types/config.d.ts.map +1 -1
  162. package/package.json +4 -4
  163. package/prompts/{conductor-framework-context.md → showrunner-framework-context.md} +1 -1
  164. package/prompts/{conductor-status-check.md → showrunner-status-check.md} +1 -1
  165. package/prompts/{conductor-sub-agent.md → showrunner-sub-agent.md} +6 -6
  166. package/prompts/{conductor-system.md → showrunner-system.md} +8 -8
  167. package/skills/apt/SKILL.md +112 -38
  168. package/skills/apt-close-task/SKILL.md +2 -2
  169. package/skills/apt-debug/SKILL.md +22 -24
  170. package/skills/apt-execute/SKILL.md +35 -26
  171. package/skills/apt-fan-out/SKILL.md +4 -4
  172. package/skills/apt-handoff/SKILL.md +1 -1
  173. package/skills/apt-mockup/SKILL.md +1 -1
  174. package/skills/apt-plan/SKILL.md +5 -5
  175. package/skills/apt-plan/adapters/{conductor.md → showrunner.md} +16 -16
  176. package/skills/apt-produce/SKILL.md +606 -0
  177. package/skills/apt-quick/SKILL.md +27 -33
  178. package/skills/apt-review/SKILL.md +14 -11
  179. package/skills/apt-run/SKILL.md +132 -3
  180. package/skills/apt-ship/SKILL.md +29 -5
  181. package/skills/apt-spar/SKILL.md +5 -3
  182. package/skills/apt-watch-ci/SKILL.md +6 -4
  183. package/src/cli/artifacts/self-stage.mjs +2 -1
  184. package/src/cli/cli-wrappers/features-reconcile.mjs +9 -0
  185. package/src/cli/commands/ci-watch.mjs +51 -2
  186. package/src/cli/commands/commit.mjs +3 -3
  187. package/src/cli/commands/event.mjs +16 -16
  188. package/src/cli/commands/features-reconcile.mjs +157 -0
  189. package/src/cli/commands/health-check.mjs +2 -2
  190. package/src/cli/commands/merge-integrate.mjs +3 -3
  191. package/src/cli/commands/produce.mjs +1466 -0
  192. package/src/cli/commands/task.mjs +482 -285
  193. package/src/cli/coordination/auto-emit-artifact-ready.mjs +5 -5
  194. package/src/cli/coordination/event-schema.d.ts +4 -2
  195. package/src/cli/coordination/event-schema.mjs +276 -21
  196. package/src/cli/coordination/store.mjs +4 -4
  197. package/src/cli/dispatch.mjs +4 -2
  198. package/src/cli/features/area-shape.mjs +140 -0
  199. package/src/cli/features/delta.mjs +625 -0
  200. package/src/cli/features/reconcile.mjs +1169 -0
  201. package/src/cli/features/surface-map.mjs +192 -0
  202. package/src/cli/features/write-root.mjs +140 -0
  203. package/src/cli/gate/gates/features-reconciled.mjs +70 -0
  204. package/src/cli/gate/gates/index.mjs +1 -0
  205. package/src/cli/help.mjs +31 -1
  206. package/src/cli/install/legacy-paths.mjs +2 -0
  207. package/src/cli/produce/blind-probe.mjs +245 -0
  208. package/src/cli/produce/claim.mjs +543 -0
  209. package/src/cli/produce/done-gate.mjs +238 -0
  210. package/src/cli/produce/events.mjs +131 -0
  211. package/src/cli/produce/evidence-oracle.mjs +133 -0
  212. package/src/cli/produce/ledger.mjs +284 -0
  213. package/src/cli/produce/probe-family.mjs +168 -0
  214. package/src/cli/produce/projection.mjs +105 -0
  215. package/src/cli/produce/run-id.mjs +84 -0
  216. package/src/cli/produce/saga.mjs +303 -0
  217. package/src/cli/produce/scheduler.mjs +423 -0
  218. package/src/cli/produce/setpoint.mjs +122 -0
  219. package/src/cli/produce/verification-ttl.mjs +191 -0
  220. package/src/cli/release-notes/ship-autodraft.mjs +2 -2
  221. package/src/cli/roadmap/showrunner-view.d.ts +10 -0
  222. package/src/cli/roadmap/{conductor-view.mjs → showrunner-view.mjs} +6 -6
  223. package/src/cli/variant/gallery.mjs +7 -3
  224. package/templates/aperant-claude-md-appendix.md +2 -2
  225. package/workflows/scan-features.md +17 -11
  226. package/dist/cli/cli-wrappers/features-audit.d.mts +0 -2
  227. package/dist/cli/cli-wrappers/features-audit.d.mts.map +0 -1
  228. package/dist/cli/cli-wrappers/features-audit.mjs +0 -8
  229. package/dist/cli/cli-wrappers/features-audit.mjs.map +0 -1
  230. package/dist/cli/commands/features-audit.d.mts.map +0 -1
  231. package/dist/cli/commands/features-audit.mjs +0 -293
  232. package/dist/cli/commands/features-audit.mjs.map +0 -1
  233. package/dist/cli/features/registry-audit.d.mts +0 -56
  234. package/dist/cli/features/registry-audit.d.mts.map +0 -1
  235. package/dist/cli/features/registry-audit.mjs +0 -264
  236. package/dist/cli/features/registry-audit.mjs.map +0 -1
  237. package/dist/cli/roadmap/conductor-view.d.mts.map +0 -1
  238. package/dist/cli/roadmap/conductor-view.mjs.map +0 -1
  239. package/src/cli/cli-wrappers/features-audit.mjs +0 -8
  240. package/src/cli/commands/features-audit.mjs +0 -302
  241. package/src/cli/features/registry-audit.mjs +0 -254
  242. package/src/cli/roadmap/conductor-view.d.ts +0 -10
@@ -0,0 +1,191 @@
1
+ /**
2
+ * @internal
3
+ * produce/verification-ttl.mjs — verification-TTL: a rotating sample of OLD done
4
+ * milestones re-verified against HEAD each tick, regressions flagged +
5
+ * compensated (M4, ID-M4-03 / AC13).
6
+ *
7
+ * Verification is not permanent — a milestone marked `done` at HEAD-sha `A` can
8
+ * silently regress as HEAD advances to `B` (a later milestone's change breaks an
9
+ * earlier one). This module gives verification a TTL:
10
+ *
11
+ * - `selectTtlReverifySample` REUSES M2's `selectSweepSample` for the rotating
12
+ * done-milestone candidates (do NOT invent a parallel rotation) and annotates
13
+ * staleness against the supplied HEAD sha — a done milestone is STALE when its
14
+ * last recorded `produce.ttl.reverified`/`produce.probe.run` head_sha ≠ the
15
+ * current HEAD (or it has never been re-verified since done); a milestone last
16
+ * re-verified against the current HEAD is EXCLUDED as FRESH.
17
+ * - `recordTtlReverify` REUSES the M2 cross-family guard (refuses a same-family
18
+ * re-verify), appends `produce.ttl.reverified`, and on `fail` ALSO appends
19
+ * `produce.ttl.regression_detected` AND fires the FORWARD compensator via
20
+ * saga.mjs::compensatePhase({phase:'verify'}) — a revert-branch + reopen +
21
+ * re-plan, NEVER a merged-PR rollback (ID-M4-03).
22
+ *
23
+ * `ttl.reverified` is a DISTINCT op from `probe.run` (a scheduled post-`done` TTL
24
+ * re-check vs a first-time done-gate probe) so the forensic trail stays clean —
25
+ * only the ROTATION and the CROSS-FAMILY GUARD are reused. This module records +
26
+ * computes; the actual re-verify RUN + the revert-ship/reopen/re-plan EXECUTION
27
+ * is the OOS dispatch layer.
28
+ */
29
+
30
+ import { selectSweepSample } from './blind-probe.mjs'
31
+ import {
32
+ appendProduceEvent,
33
+ readImplementerFamily,
34
+ readProduceRunEventsMatching,
35
+ } from './events.mjs'
36
+ import { normalizeFamily } from './probe-family.mjs'
37
+ import { projectRoadmap } from './projection.mjs'
38
+ import { compensatePhase } from './saga.mjs'
39
+
40
+ /**
41
+ * Build the per-milestone latest-recorded head_sha map from the
42
+ * `produce.ttl.reverified` + `produce.probe.run` events (both carry `head_sha`).
43
+ * Chronological fold — the last recorded run for a milestone wins.
44
+ *
45
+ * @param {string} projectDir
46
+ * @param {string} runId
47
+ * @returns {Map<string, { head_sha: string, at: string | null }>}
48
+ */
49
+ function latestReverifyByMilestone(projectDir, runId) {
50
+ const events = readProduceRunEventsMatching(
51
+ projectDir,
52
+ runId,
53
+ (op) => op === 'produce.ttl.reverified' || op === 'produce.probe.run',
54
+ )
55
+ const map = new Map()
56
+ for (const e of events) {
57
+ const d = e.data || {}
58
+ const id = typeof d.milestone_id === 'string' ? d.milestone_id : null
59
+ const sha = typeof d.head_sha === 'string' ? d.head_sha : null
60
+ if (!id || !sha) continue
61
+ map.set(id, { head_sha: sha, at: typeof e.t === 'string' ? e.t : null })
62
+ }
63
+ return map
64
+ }
65
+
66
+ /**
67
+ * Select a DETERMINISTIC rotating sample of already-`done` milestones STALE
68
+ * against HEAD (ID-M4-03 / AC7). REUSES `selectSweepSample` for the rotation
69
+ * order (fewest/oldest last-probe-run first), then annotates each candidate with
70
+ * staleness against `headSha` and FILTERS OUT the FRESH ones (last re-verified
71
+ * against the current HEAD). Returns the first `sample` STALE candidates. An
72
+ * absent HEAD sha fails closed (nothing selected).
73
+ *
74
+ * @param {string} projectDir
75
+ * @param {string} runId
76
+ * @param {{ headSha: string, sample?: number }} args
77
+ * @returns {{ run_id: string, head_sha: string | null, sample_size: number,
78
+ * selected: Array<{ milestone_id: string, title: string, last_reverify_head_sha: string | null, last_reverify_at: string | null, stale: boolean }> }}
79
+ */
80
+ export function selectTtlReverifySample(projectDir, runId, { headSha, sample = 1 } = {}) {
81
+ if (!headSha) return { run_id: runId, head_sha: null, sample_size: 0, selected: [] }
82
+
83
+ // The FULL rotation-ordered pool of done milestones (reuse selectSweepSample;
84
+ // pass the done count so it returns every done milestone in rotation order).
85
+ const doneCount = projectRoadmap(projectDir, runId).milestones.filter(
86
+ (m) => m.status === 'done',
87
+ ).length
88
+ const pool = selectSweepSample(projectDir, runId, { sample: doneCount || 1 }).selected
89
+
90
+ const latest = latestReverifyByMilestone(projectDir, runId)
91
+ const n = Number.isFinite(sample) && sample > 0 ? Math.floor(sample) : 1
92
+
93
+ const stale = []
94
+ for (const candidate of pool) {
95
+ const last = latest.get(candidate.milestone_id) || null
96
+ // STALE when never re-verified since done, or last re-verified against a sha
97
+ // ≠ the current HEAD. FRESH (last re-verify head_sha === HEAD) is excluded.
98
+ const isStale = !last || last.head_sha !== headSha
99
+ if (!isStale) continue
100
+ stale.push({
101
+ milestone_id: candidate.milestone_id,
102
+ title: candidate.title,
103
+ last_reverify_head_sha: last?.head_sha ?? null,
104
+ last_reverify_at: last?.at ?? null,
105
+ stale: true,
106
+ })
107
+ }
108
+
109
+ const selected = stale.slice(0, n)
110
+ return { run_id: runId, head_sha: headSha, sample_size: selected.length, selected }
111
+ }
112
+
113
+ /**
114
+ * Record a scheduled TTL re-verify RUN against HEAD (ID-M4-03). REUSES the M2
115
+ * cross-family guard (refuses a same-family re-verify, or when no implementer
116
+ * family is recorded). Appends `produce.ttl.reverified {run_id, milestone_id,
117
+ * head_sha, result}`; on `result === 'fail'` ALSO appends
118
+ * `produce.ttl.regression_detected` AND fires the FORWARD compensator via
119
+ * saga.mjs::compensatePhase({phase:'verify', reason:'ttl_regression@<sha>'}) — a
120
+ * revert-branch + reopen + re-plan, NEVER a merged-PR rollback. A `pass`
121
+ * refreshes verification against the new HEAD and logs no regression.
122
+ *
123
+ * @param {string} projectDir
124
+ * @param {{ runId: string, milestoneId: string, result: string, probeFamily: string, headSha: string }} args
125
+ * @returns {{ ok: true, result: string, probe_family: string, head_sha: string, regression: boolean, compensator: object | null }
126
+ * | { ok: false, reason: string }}
127
+ */
128
+ export function recordTtlReverify(
129
+ projectDir,
130
+ { runId, milestoneId, result, probeFamily, headSha },
131
+ ) {
132
+ if (result !== 'pass' && result !== 'fail') return { ok: false, reason: 'invalid_result' }
133
+ if (!headSha) return { ok: false, reason: 'no_head_sha' }
134
+ if (!probeFamily) return { ok: false, reason: 'no_probe_family' }
135
+
136
+ const implementerFamily = readImplementerFamily(projectDir, runId, milestoneId)
137
+ if (!implementerFamily) return { ok: false, reason: 'no_recorded_implementer' }
138
+ // Cross-family enforcement (normalized so claude-code === claude).
139
+ if (normalizeFamily(probeFamily) === normalizeFamily(implementerFamily))
140
+ return { ok: false, reason: 'same_family_reverify' }
141
+
142
+ appendProduceEvent(projectDir, 'produce.ttl.reverified', {
143
+ run_id: runId,
144
+ milestone_id: milestoneId,
145
+ head_sha: headSha,
146
+ result,
147
+ })
148
+
149
+ let regression = false
150
+ let compensator = null
151
+ if (result === 'fail') {
152
+ // The regression flags the milestone AND fires the FORWARD compensator — a
153
+ // revert-branch + reopen + re-plan, never a merged-PR rollback (ID-M4-03).
154
+ appendProduceEvent(projectDir, 'produce.ttl.regression_detected', {
155
+ run_id: runId,
156
+ milestone_id: milestoneId,
157
+ head_sha: headSha,
158
+ })
159
+ const comp = compensatePhase(projectDir, {
160
+ runId,
161
+ milestoneId,
162
+ phase: 'verify',
163
+ reason: `ttl_regression@${headSha}`,
164
+ })
165
+ regression = true
166
+ compensator = comp.ok ? comp.compensator : null
167
+ }
168
+
169
+ return { ok: true, result, probe_family: probeFamily, head_sha: headSha, regression, compensator }
170
+ }
171
+
172
+ /**
173
+ * Read-only fold of the run's `produce.ttl.regression_detected` events (for saga
174
+ * status / canary context). Chronological order.
175
+ *
176
+ * @param {string} projectDir
177
+ * @param {string} runId
178
+ * @returns {Array<{ milestone_id: string | null, head_sha: string | null, t: string | null }>}
179
+ */
180
+ export function listRegressions(projectDir, runId) {
181
+ const events = readProduceRunEventsMatching(
182
+ projectDir,
183
+ runId,
184
+ (op) => op === 'produce.ttl.regression_detected',
185
+ )
186
+ return events.map((e) => ({
187
+ milestone_id: e.data && typeof e.data.milestone_id === 'string' ? e.data.milestone_id : null,
188
+ head_sha: e.data && typeof e.data.head_sha === 'string' ? e.data.head_sha : null,
189
+ t: typeof e.t === 'string' ? e.t : null,
190
+ }))
191
+ }
@@ -6,8 +6,8 @@
6
6
  * should grow a `Release note: …` line OR ship should block.
7
7
  *
8
8
  * This module is intentionally pure — the apt-ship SKILL.md body shells
9
- * `apt-tools features-audit . --diff-since <base> --json` and feeds the
10
- * envelope here. Keeping the decision pure makes TD-4 trivial: fixture in,
9
+ * `apt-tools features-reconcile . --draft --dry-run --base "$base"` and feeds
10
+ * the envelope here. Keeping the decision pure makes TD-4 trivial: fixture in,
11
11
  * decision out, no I/O.
12
12
  */
13
13
 
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Type declarations for the .mjs showrunner-view helper.
3
+ * Hand-authored — the import path points at src/.mjs directly.
4
+ */
5
+
6
+ export interface ShowrunnerRoadmapView {
7
+ scopes: Array<{ scope: string; roadmap: unknown }>
8
+ }
9
+
10
+ export function buildShowrunnerRoadmapView(projectRoot: string): ShowrunnerRoadmapView
@@ -1,26 +1,26 @@
1
1
  /**
2
- * conductor-view.mjs — read-only roadmap surface for the Aperant
3
- * Conductor's `getRoadmap` tool.
2
+ * showrunner-view.mjs — read-only roadmap surface for the Aperant
3
+ * Showrunner's `getRoadmap` tool.
4
4
  *
5
- * Wraps `loadRoadmapForScope` + `listRoadmapScopes` so the Conductor sees
5
+ * Wraps `loadRoadmapForScope` + `listRoadmapScopes` so the Showrunner sees
6
6
  * the same overlay-aware view every framework command sees. Direct reads
7
7
  * of `.aperant/roadmap/*\/phases/*.json` or `milestones.json` would miss
8
8
  * the dual-write overlay (seed_id from legacy blob, etc.) — Codex
9
9
  * roundtable correction #3.
10
10
  *
11
- * Step 5 of Conductor v2 ship order.
11
+ * Step 5 of Showrunner v2 ship order.
12
12
  */
13
13
 
14
14
  import { loadRoadmapForScope } from './io.mjs'
15
15
  import { listRoadmapScopes } from './paths.mjs'
16
16
 
17
17
  /**
18
- * Build a flat per-scope roadmap snapshot for the Conductor.
18
+ * Build a flat per-scope roadmap snapshot for the Showrunner.
19
19
  *
20
20
  * @param {string} projectRoot
21
21
  * @returns {{ scopes: Array<{ scope: string, roadmap: unknown }> }}
22
22
  */
23
- export function buildConductorRoadmapView(projectRoot) {
23
+ export function buildShowrunnerRoadmapView(projectRoot) {
24
24
  const scopes = listRoadmapScopes(projectRoot)
25
25
  const out = []
26
26
  for (const scope of scopes) {
@@ -12,6 +12,10 @@
12
12
  * no privilege for that). Picking happens by running a terminal
13
13
  * command — the gallery's job is to make the letter obvious and the
14
14
  * command copy-pasteable.
15
+ * - Each card links to its variant file via a plain relative anchor
16
+ * with target="_blank", so the user can open any variant full-size
17
+ * in a new tab straight from the gallery (relative hrefs resolve
18
+ * against the gallery's own file:// URL — no server needed).
15
19
  * - Responsive grid: 1 column on narrow, 2 on medium, N on wide.
16
20
  * - Each variant iframe is rendered at full device-width-feel, not a
17
21
  * thumbnail, so the user can actually read the content.
@@ -80,11 +84,11 @@ export function renderGallery(input) {
80
84
  ` <div class="truncate text-xs text-slate-400">${description}</div>`,
81
85
  ' </div>',
82
86
  ' </div>',
83
- ` <code class="shrink-0 rounded bg-slate-800 px-2 py-1 text-xs text-slate-300">${filename}</code>`,
87
+ ` <a href="${filename}" target="_blank" rel="noopener" title="Open variant ${letter} in a new tab" class="shrink-0 rounded bg-slate-800 px-2 py-1 text-xs text-slate-300 hover:bg-slate-700 hover:text-slate-100"><code>${filename}</code> ↗</a>`,
84
88
  ' </header>',
85
89
  ` <iframe src="${filename}" title="Variant ${letter}" loading="lazy" class="block h-[720px] w-full bg-white"></iframe>`,
86
90
  ' <footer class="flex items-center justify-between gap-3 border-t border-slate-800 px-4 py-3">',
87
- ` <div class="text-xs text-slate-500">Pick this one:</div>`,
91
+ ` <a href="${filename}" target="_blank" rel="noopener" class="text-xs font-medium ${ACCENT} hover:underline">Open full size ↗</a>`,
88
92
  ` <code class="rounded bg-slate-800 px-2 py-1 text-xs text-slate-100">/apt:mockup --pick ${letter.toLowerCase()}</code>`,
89
93
  ' </footer>',
90
94
  ' </article>',
@@ -112,7 +116,7 @@ export function renderGallery(input) {
112
116
  ' <div>',
113
117
  ` <div class="text-xs uppercase tracking-[0.2em] text-slate-500">Variant gallery · iteration ${iterationEsc}</div>`,
114
118
  ` <h1 class="mt-1 text-2xl font-semibold text-slate-100">${featureEsc}</h1>`,
115
- ` <p class="mt-1 text-sm text-slate-400">${variants.length} parallel generations. Open this page via <code>file://</code>, scan the iframes, pick the letter that lands.</p>`,
119
+ ` <p class="mt-1 text-sm text-slate-400">${variants.length} parallel generations. Open this page via <code>file://</code>, scan the iframes, click “Open full size” to view any variant in its own tab, then pick the letter that lands.</p>`,
116
120
  ' </div>',
117
121
  ' <div class="flex flex-col gap-2 text-xs text-slate-400 sm:items-end">',
118
122
  ` <div>Directory: <code class="text-slate-300">${taskDirEsc}/mockups/</code></div>`,
@@ -23,9 +23,9 @@ The feature registry (`.aperant/features/`) is a living inventory of every inter
23
23
  feature in the app. It's the source of truth for what to verify and review.
24
24
 
25
25
  - **Created by:** `/apt:scan` (initial scan or full refresh)
26
- - **Updated by:** `/apt:execute` post-step (`apt-tools features-audit --apply-stubs` auto-registers new UI/core/hook/IPC files per the docs-coherence policy)
26
+ - **Updated by:** per-writer DELTAS committed with the work (`apt-tools features-reconcile --draft` at the executor/quick/debug commit step), materialized into the area JSONs + a durable receipt by `/apt:ship` §0.0 (`--write`)
27
27
  - **Consumed by:** `/apt:verify-proof` (generates test cases from registry)
28
- - **Checked by:** `/apt:review` Pass 6 (flags unregistered new components, applies stubs for auto-update policies)
28
+ - **Checked by:** the blocking `features-reconciled` ship gate, `/apt:review` Pass 6 (`features-reconcile --check`), and the always-on `features-reconcile` CI check on every PR
29
29
 
30
30
  Run `/apt:scan` after setting up Aperant on a brownfield project, or after major changes.
31
31
 
@@ -171,7 +171,7 @@ After all agents complete (or at the end of each wave), verify each expected out
171
171
  if (!result.success) { /* re-launch with error message */ }
172
172
  ```
173
173
  4. For orchestrators running in pure-markdown context (no TS runtime), perform a **lightweight structural check**: parse the JSON and verify the top-level keys include `area`, `route`, `components`, `last_scanned`, `features`, and that `features` is an array where each entry has `id`, `name`, `component`, `sub_features`. Flag deviations in the output report.
174
- 5. On validation failure, the orchestrator may: (a) re-launch the agent with the validation error as feedback, OR (b) mark the file as non-conforming and note it in REGISTRY.md. Prefer (a) up to 2 retries, then fall back to (b).
174
+ 5. On validation failure, re-launch the agent with the validation error as feedback, up to 2 retries. After 2 failed retries the orchestrator MUST mark the file as **must-fix** in REGISTRY.md (the `## Non-conforming files (must fix)` section) AND report it as a failure line in the final scan output — a non-conforming file is NEVER silently accepted (`features-reconcile --check` clause (b) will fail red on it until fixed).
175
175
  </step>
176
176
 
177
177
  <step name="synthesize_registry">
@@ -283,16 +283,22 @@ Next: /apt:verify-proof {area} — verify features with visual evidence
283
283
 
284
284
  ### How other skills update the registry
285
285
 
286
- **`/apt:execute` post-step:**
287
- After completing a task that adds or modifies UI features, the executor should:
288
- 1. Check if `.aperant/features/REGISTRY.md` exists
289
- 2. If yes: read the relevant `{area}.json` file
290
- 3. Add new entries for any features that were created
291
- 4. Update `wired` status for any features that were connected
292
- 5. Update `last_scanned` timestamp
293
-
294
- This is a lightweight update — NOT a full rescan. The executor reads its own diff
295
- to know what changed.
286
+ **`/apt:execute` post-step (delta-emit contract):**
287
+ After each subtask commit, the executor emits a per-writer registry DELTA
288
+ instead of editing area JSONs directly:
289
+ 1. `apt-tools features-reconcile . --draft --task-id <id> --subtask-id <sid>`
290
+ scaffolds `.aperant/features/deltas/<id>-<sid>.json` from the diff.
291
+ 2. The executor enriches the scaffold in-window: fills dispositions
292
+ (`upsert` / `remove` / `wiring-transition` / `no-semantic-change` + reason),
293
+ assigns any `area: null`, and writes real `sub_features` with
294
+ `wired`/`handler`/`test_hint`.
295
+ 3. The delta is committed with the work; `/apt:ship` §0.0 materializes all
296
+ pending deltas into the area JSONs + a durable receipt (`--write`), and the
297
+ `features-reconciled` ship gate + the always-on CI check verify coverage.
298
+
299
+ This is a lightweight capture — NOT a full rescan. The executor reads its own
300
+ diff to know what changed; area JSONs are only ever written by the
301
+ materializer (or this full-scan workflow).
296
302
 
297
303
  **`/apt:verify-proof` pre-step:**
298
304
  Before generating tests, verify-proof should:
@@ -1,2 +0,0 @@
1
- export { cmdFeaturesAudit as cliFeaturesAudit } from "../commands/features-audit.mjs";
2
- //# sourceMappingURL=features-audit.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"features-audit.d.mts","sourceRoot":"","sources":["../../../src/cli/cli-wrappers/features-audit.mjs"],"names":[],"mappings":""}
@@ -1,8 +0,0 @@
1
- /**
2
- * cli-wrappers/features-audit.mjs — Layer 2 wrapper for the features-audit cmd.
3
- *
4
- * Today this is a passthrough — DISPATCH calls wrap() directly.
5
- * Re-exporting keeps the file-per-cmd structural seam.
6
- */
7
- export { cmdFeaturesAudit as cliFeaturesAudit } from '../commands/features-audit.mjs';
8
- //# sourceMappingURL=features-audit.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"features-audit.mjs","sourceRoot":"","sources":["../../../src/cli/cli-wrappers/features-audit.mjs"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,gBAAgB,IAAI,gBAAgB,EAAE,MAAM,gCAAgC,CAAA"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"features-audit.d.mts","sourceRoot":"","sources":["../../../src/cli/commands/features-audit.mjs"],"names":[],"mappings":"AAoCA;;;;;;;;;;GAUG;AACH,qEAHW;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAC,GAChE;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,qBAAqB,EAAE,MAAM,EAAE,CAAA;KAAC,CAAA;CAAC,CA0E3I;AAoCD,8GAqFC"}
@@ -1,293 +0,0 @@
1
- /**
2
- * commands/features-audit.mjs — CLI entry point for the features-registry
3
- * audit (C33).
4
- *
5
- * Modes:
6
- * - report only (default): print `new_components`, `unregistered`, `stub_patches`
7
- * - `--apply-stubs`: additionally write the stub patches to disk.
8
- * This is what the /apt:execute post-step calls.
9
- *
10
- * Diff source precedence:
11
- * 1. `--diff-files <a,b,c>` explicit CSV (highest priority)
12
- * 2. `--since <ref>` → runs `git diff --name-only <ref>..HEAD`
13
- * 3. fallback: `git diff HEAD --name-only` (current WIP)
14
- *
15
- * BUG-025b worktree-redirect: features-audit historically wrote to the
16
- * invoking cwd's `.aperant/features/`, leaking generated `*.json` into
17
- * main when the executor invoked it from a worktree-isolated task. The
18
- * fix applies a 4-step resolution order (see resolveWriteRoot below):
19
- * (1) cwd IS a worktree root → write there (current behaviour)
20
- * (2) --task-id maps to an active non-terminal worktree task →
21
- * redirect writes to that worktree's `.aperant/features/`
22
- * (3) multiple active worktree-isolated tasks + no --task-id → error
23
- * (4) `--no-worktree-redirect` opt-out preserves layer (1) behaviour
24
- */
25
- import { execFileSync, spawnSync } from 'node:child_process';
26
- import { existsSync, readFileSync } from 'node:fs';
27
- import { join, resolve, sep } from 'node:path';
28
- import { loadDocsPolicy, matchSurfaceToPolicy } from '../docs-audit/policy.mjs';
29
- import { applyStubPatches, auditFeatureRegistry } from '../features/registry-audit.mjs';
30
- import { parseFlags } from '../util/args.mjs';
31
- import { err, ok } from '../util/result.mjs';
32
- import { resolveMainRepoRoot } from '../util/state-paths.mjs';
33
- const TERMINAL_PHASES = new Set(['completed', 'cancelled', 'closed', 'shipped', 'merged']);
34
- /**
35
- * BUG-025b — resolve where features-audit should write `*.json`.
36
- * See module header for the 4-step order. Returns `{ writeRoot, redirect_notice }`
37
- * on success or a synthetic error envelope (`{ error: { ... } }`) on the
38
- * ambiguous-worktree case.
39
- *
40
- * @internal — exported for unit testing.
41
- *
42
- * @param {{projectDir: string, taskId?: string | null, noRedirect?: boolean}} args
43
- * @returns {{writeRoot?: string, redirect_notice?: string | null, error?: {kind: string, message: string, active_worktree_tasks: string[]}}}
44
- */
45
- export function resolveWriteRoot({ projectDir, taskId, noRedirect }) {
46
- const root = resolve(projectDir);
47
- if (noRedirect === true) {
48
- return { writeRoot: root, redirect_notice: null };
49
- }
50
- // Layer (1): cwd IS a worktree root. Worktrees have `git rev-parse
51
- // --git-dir` != `git rev-parse --git-common-dir`. Falling through to
52
- // the existing behaviour writes to this worktree's .aperant/features.
53
- // Fast path: git sets GIT_DIR and GIT_COMMON_DIR in the environment
54
- // when running inside a worktree hook context — if both are set and
55
- // differ, we are in a worktree. This env-var check is ONLY valid at
56
- // Layer (1) where we ask "is this process in a worktree?"; it must
57
- // NOT be used to validate an untrusted external path (Layer 2 below).
58
- const envDir = process.env.GIT_DIR;
59
- const envCommonDir = process.env.GIT_COMMON_DIR;
60
- if (envDir && envCommonDir) {
61
- if (resolve(envDir) !== resolve(envCommonDir)) {
62
- return { writeRoot: root, redirect_notice: null };
63
- }
64
- }
65
- else if (isWorktreeRoot(root)) {
66
- return { writeRoot: root, redirect_notice: null };
67
- }
68
- // Layer (2): --task-id maps to an active non-terminal worktree-isolated
69
- // task → redirect.
70
- const stateActive = readActiveTasks(root);
71
- if (taskId) {
72
- const task = stateActive[taskId];
73
- if (task &&
74
- !TERMINAL_PHASES.has(task.lifecycle_phase) &&
75
- typeof task.worktree_path === 'string' &&
76
- existsSync(task.worktree_path)) {
77
- const resolvedWorktree = resolve(task.worktree_path);
78
- const worktreesDir = resolve(root, '.aperant', 'worktrees') + sep;
79
- const isUnderProject = resolvedWorktree.startsWith(root + sep) || resolvedWorktree === root;
80
- const isUnderWorktrees = resolvedWorktree.startsWith(worktreesDir);
81
- // Accept only paths under the project root, under .aperant/worktrees/,
82
- // or confirmed as a real git worktree by git itself. Rejects arbitrary
83
- // paths injected via a tampered state.json while preserving test
84
- // fixtures that use ad-hoc worktree paths outside the project tree.
85
- if (!isUnderProject && !isUnderWorktrees && !isWorktreeRoot(resolvedWorktree)) {
86
- return { writeRoot: root, redirect_notice: null };
87
- }
88
- return {
89
- writeRoot: resolvedWorktree,
90
- redirect_notice: `features-audit redirected writes to worktree ${resolvedWorktree} (--task-id ${taskId})`,
91
- };
92
- }
93
- }
94
- // Layer (3): multiple active worktree-isolated tasks AND no --task-id
95
- // → actionable error. Single-worktree case is OK to default to root
96
- // (the executor will set up the worktree's registry on first scan).
97
- const activeWorktreeTasks = Object.entries(stateActive).filter(([_id, t]) => {
98
- return (t &&
99
- !TERMINAL_PHASES.has(t.lifecycle_phase) &&
100
- typeof t.worktree_path === 'string' &&
101
- existsSync(t.worktree_path));
102
- });
103
- if (!taskId && activeWorktreeTasks.length > 1) {
104
- return {
105
- error: {
106
- kind: 'ambiguous-worktree-context',
107
- message: 'ambiguous worktree context; pass --task-id <id> or --no-worktree-redirect',
108
- active_worktree_tasks: activeWorktreeTasks.map(([id]) => id),
109
- },
110
- };
111
- }
112
- // Default: current behaviour (write into cwd).
113
- return { writeRoot: root, redirect_notice: null };
114
- }
115
- function isWorktreeRoot(projectDir) {
116
- try {
117
- const dirRes = spawnSync('git', ['rev-parse', '--git-dir'], {
118
- cwd: projectDir,
119
- encoding: 'utf-8',
120
- stdio: ['ignore', 'pipe', 'ignore'],
121
- });
122
- const commonRes = spawnSync('git', ['rev-parse', '--git-common-dir'], {
123
- cwd: projectDir,
124
- encoding: 'utf-8',
125
- stdio: ['ignore', 'pipe', 'ignore'],
126
- });
127
- if (dirRes.status !== 0 || commonRes.status !== 0)
128
- return false;
129
- return (dirRes.stdout || '').trim() !== (commonRes.stdout || '').trim();
130
- }
131
- catch {
132
- return false;
133
- }
134
- }
135
- function readActiveTasks(projectDir) {
136
- // state-fork-fix: read the AUTHORITATIVE main board — readActiveTasks finds
137
- // each task's worktree_path for the BUG-025b feature-write redirect, so it
138
- // MUST read main or the redirect-target lookup itself forks. (The feature
139
- // WRITE path / resolveWriteRoot stays untouched.)
140
- const statePath = join(resolveMainRepoRoot(projectDir), '.aperant', 'state.json');
141
- if (!existsSync(statePath))
142
- return {};
143
- try {
144
- const parsed = JSON.parse(readFileSync(statePath, 'utf-8'));
145
- return parsed?.active_tasks ?? {};
146
- }
147
- catch {
148
- return {};
149
- }
150
- }
151
- export function cmdFeaturesAudit(projectDir, extraArgs) {
152
- if (!projectDir)
153
- return err('Usage: apt-tools features-audit <project-dir> [flags]');
154
- const root = resolve(projectDir);
155
- const flags = parseFlags(extraArgs);
156
- // BUG-025b: redirect writes to the active task's worktree when the
157
- // caller is in a non-worktree cwd. See resolveWriteRoot for the
158
- // 4-step resolution order.
159
- const taskIdFlag = flags.get('task-id');
160
- const taskId = typeof taskIdFlag === 'string' && taskIdFlag.trim() ? taskIdFlag.trim() : null;
161
- const noRedirect = flags.get('no-worktree-redirect') === true;
162
- const writeRootResult = resolveWriteRoot({ projectDir: root, taskId, noRedirect });
163
- if (writeRootResult.error) {
164
- return err(writeRootResult.error.message, 1, {
165
- kind: writeRootResult.error.kind,
166
- active_worktree_tasks: writeRootResult.error.active_worktree_tasks,
167
- });
168
- }
169
- const writeRoot = writeRootResult.writeRoot;
170
- const redirectNotice = writeRootResult.redirect_notice;
171
- if (redirectNotice) {
172
- // Surface the redirect once on stderr so the executor's log carries
173
- // the audit trail without polluting stdout JSON.
174
- process.stderr.write(`[features-audit] ${redirectNotice}\n`);
175
- }
176
- const diffFiles = resolveDiffFiles(root, flags);
177
- const applyStubs = flags.has('apply-stubs');
178
- const format = (flags.get('format') || 'json').toLowerCase();
179
- const result = auditFeatureRegistry({ projectDir: writeRoot, diffFiles });
180
- // Honor `.aperant/config.json.docs_policy`: only apply stubs for targets
181
- // whose resolved policy is `auto-update`. This closes the skill-vs-code
182
- // gap where `apt-execute` §3e-features promised `--apply-stubs` would
183
- // no-op on surfaces the user opted out of. Patches whose target matches
184
- // `skip` / `flag-drift` (or nothing) are surfaced in `stub_patches` but
185
- // never written to disk.
186
- let applied_patches = null;
187
- let skipped_patches = null;
188
- if (applyStubs && result.stub_patches.length > 0) {
189
- const policy = loadDocsPolicy(root);
190
- const eligible = [];
191
- const skipped = [];
192
- for (const patch of result.stub_patches) {
193
- const policyValue = matchSurfaceToPolicy(patch.target, policy);
194
- if (policyValue === 'auto-update') {
195
- eligible.push(patch);
196
- }
197
- else {
198
- skipped.push({ target: patch.target, policy: policyValue });
199
- }
200
- }
201
- if (eligible.length > 0) {
202
- // BUG-025b: stubs are written to the resolved `writeRoot` (which
203
- // may be the active task's worktree) rather than the invoking cwd.
204
- applied_patches = applyStubPatches(writeRoot, eligible);
205
- }
206
- if (skipped.length > 0) {
207
- skipped_patches = skipped;
208
- }
209
- }
210
- if (format === 'md') {
211
- return ok({
212
- status: 'ok',
213
- command: 'features-audit',
214
- format: 'md',
215
- markdown: renderMarkdown(result, applied_patches, skipped_patches),
216
- write_root: writeRoot,
217
- ...(redirectNotice ? { redirect_notice: redirectNotice } : {}),
218
- ...result,
219
- ...(applied_patches ? { applied_patches } : {}),
220
- ...(skipped_patches ? { skipped_patches } : {}),
221
- });
222
- }
223
- return ok({
224
- status: 'ok',
225
- command: 'features-audit',
226
- write_root: writeRoot,
227
- ...(redirectNotice ? { redirect_notice: redirectNotice } : {}),
228
- ...result,
229
- ...(applied_patches ? { applied_patches } : {}),
230
- ...(skipped_patches ? { skipped_patches } : {}),
231
- });
232
- }
233
- function resolveDiffFiles(root, flags) {
234
- const explicit = flags.get('diff-files');
235
- if (explicit && typeof explicit === 'string') {
236
- return explicit
237
- .split(',')
238
- .map((s) => s.trim())
239
- .filter(Boolean);
240
- }
241
- const sinceRaw = flags.get('since');
242
- const since = typeof sinceRaw === 'string' && /^[A-Za-z0-9/_.-]+$/.test(sinceRaw) && !sinceRaw.startsWith('-')
243
- ? sinceRaw
244
- : null;
245
- try {
246
- const args = since ? ['diff', '--name-only', `${since}..HEAD`] : ['diff', 'HEAD', '--name-only'];
247
- const out = execFileSync('git', args, {
248
- cwd: root,
249
- encoding: 'utf-8',
250
- stdio: ['ignore', 'pipe', 'ignore'],
251
- });
252
- return out
253
- .split('\n')
254
- .map((s) => s.trim())
255
- .filter(Boolean);
256
- }
257
- catch {
258
- // Not a git repo or HEAD doesn't exist — clean slate.
259
- return [];
260
- }
261
- }
262
- function renderMarkdown(result, applied, skipped) {
263
- const lines = ['### Features audit', ''];
264
- if (result.new_components.length === 0) {
265
- lines.push('_No new feature-qualifying files in diff._');
266
- }
267
- else {
268
- lines.push('**New candidates:**');
269
- for (const c of result.new_components) {
270
- lines.push(`- \`${c.file}\` — kind: \`${c.kind}\`, area: \`${c.area}\``);
271
- }
272
- }
273
- if (result.unregistered.length > 0) {
274
- lines.push('', '**Unregistered:**');
275
- for (const u of result.unregistered) {
276
- lines.push(`- \`${u.file}\` (area: \`${u.area}\`)`);
277
- }
278
- }
279
- if (applied && applied.length > 0) {
280
- lines.push('', '**Applied patches:**');
281
- for (const p of applied) {
282
- lines.push(`- ${p.target} — ${p.applied ? p.reason : `FAILED: ${p.reason}`}`);
283
- }
284
- }
285
- if (skipped && skipped.length > 0) {
286
- lines.push('', '**Skipped patches (policy opt-out):**');
287
- for (const s of skipped) {
288
- lines.push(`- ${s.target} — policy: \`${s.policy ?? 'unmatched'}\``);
289
- }
290
- }
291
- return lines.join('\n');
292
- }
293
- //# sourceMappingURL=features-audit.mjs.map