@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,303 @@
1
+ /**
2
+ * @internal
3
+ * produce/saga.mjs — the plan→execute→verify→review→ship saga + FORWARD
4
+ * compensators (M4, ID-M4-01 / ID-M4-02, the ID-10 semantic).
5
+ *
6
+ * The loop is modeled as a saga: each phase declares a FORWARD compensator via
7
+ * `resolveCompensator(phase)`. The load-bearing invariant is that EVERY
8
+ * compensator carries `rolls_back_merged_pr: false` — a compensator is always a
9
+ * forward compensating transaction. A failed verify/review/ship SHIPS A REVERT
10
+ * (a new forward commit/branch) + reopens the task + re-plans; it NEVER un-merges
11
+ * history other agents/humans already built on. plan/execute are pre-merge and
12
+ * simply reopen + re-plan (no revert), still `rolls_back_merged_pr: false`.
13
+ *
14
+ * The CLI RECORDS the phase transitions + the forward-compensator DESCRIPTOR; it
15
+ * NEVER executes the revert / reopen / re-plan (that dispatch is OOS — the CLI
16
+ * records + computes, the skill/dispatch acts, exactly as M2 records
17
+ * oracle/probe results while the skill drives the LLM steps).
18
+ *
19
+ * `projectSaga` folds `produce.saga.*` ONLY into per-milestone phase state — a
20
+ * SEPARATE fold PARALLEL to the roadmap projection (projection.mjs still folds
21
+ * `produce.roadmap.*` ONLY, so the M1/M2/M3 blindness invariant holds: a saga
22
+ * compensation never mutates a milestone's `done` roadmap status).
23
+ */
24
+
25
+ import {
26
+ appendProduceEvent,
27
+ readImplementerFamily,
28
+ readProduceRunEventsMatching,
29
+ } from './events.mjs'
30
+ import { normalizeFamily, resolveProbeFamily } from './probe-family.mjs'
31
+
32
+ /** The ordered saga phases (ID-M4-01). Mirrors ALLOWED_SAGA_PHASES in event-schema.mjs. */
33
+ export const SAGA_PHASES = ['plan', 'execute', 'verify', 'review', 'ship']
34
+
35
+ /**
36
+ * The declared FORWARD-compensator table (ID-M4-02). EVERY descriptor carries
37
+ * `rolls_back_merged_pr: false` — a compensator is always forward. plan/execute
38
+ * are pre-merge (reopen + re-plan, no revert); verify/review/ship SHIP A REVERT
39
+ * + reopen + re-plan. The CLI records the descriptor kind; it never emits a
40
+ * rollback / history-rewrite instruction.
41
+ */
42
+ const COMPENSATORS = {
43
+ plan: {
44
+ kind: 'rewrite_plan',
45
+ reopens_task: true,
46
+ ships_revert: false,
47
+ rolls_back_merged_pr: false,
48
+ },
49
+ execute: {
50
+ kind: 'discard_branch_replan',
51
+ reopens_task: true,
52
+ ships_revert: false,
53
+ rolls_back_merged_pr: false,
54
+ },
55
+ verify: {
56
+ kind: 'revert_reopen_replan',
57
+ reopens_task: true,
58
+ ships_revert: true,
59
+ rolls_back_merged_pr: false,
60
+ },
61
+ review: {
62
+ kind: 'revert_reopen_replan',
63
+ reopens_task: true,
64
+ ships_revert: true,
65
+ rolls_back_merged_pr: false,
66
+ },
67
+ ship: {
68
+ kind: 'revert_reopen_replan',
69
+ reopens_task: true,
70
+ ships_revert: true,
71
+ rolls_back_merged_pr: false,
72
+ },
73
+ }
74
+
75
+ /**
76
+ * Resolve the declared FORWARD compensator for a saga phase. Returns a fresh
77
+ * copy (the table is never handed out mutable) or null for an out-of-vocabulary
78
+ * phase (fail-closed). The returned descriptor ALWAYS has
79
+ * `rolls_back_merged_pr: false` (ID-M4-02).
80
+ *
81
+ * @param {string} phase
82
+ * @returns {{ kind: string, reopens_task: boolean, ships_revert: boolean, rolls_back_merged_pr: boolean } | null}
83
+ */
84
+ export function resolveCompensator(phase) {
85
+ const descriptor = COMPENSATORS[phase]
86
+ if (!descriptor) return null
87
+ return { ...descriptor }
88
+ }
89
+
90
+ /**
91
+ * Append `produce.saga.phase_entered` for a milestone's saga phase. Validates
92
+ * the phase against SAGA_PHASES fail-closed (an out-of-vocabulary phase appends
93
+ * nothing).
94
+ *
95
+ * @param {string} projectDir
96
+ * @param {{ runId: string, milestoneId: string, phase: string }} args
97
+ * @returns {{ ok: true, phase: string } | { ok: false, reason: string }}
98
+ */
99
+ export function enterPhase(projectDir, { runId, milestoneId, phase }) {
100
+ if (!SAGA_PHASES.includes(phase)) return { ok: false, reason: 'invalid_phase' }
101
+ appendProduceEvent(projectDir, 'produce.saga.phase_entered', {
102
+ run_id: runId,
103
+ milestone_id: milestoneId,
104
+ phase,
105
+ })
106
+ return { ok: true, phase }
107
+ }
108
+
109
+ /**
110
+ * Append `produce.saga.phase_completed` for a milestone's saga phase. Validates
111
+ * the phase against SAGA_PHASES fail-closed.
112
+ *
113
+ * @param {string} projectDir
114
+ * @param {{ runId: string, milestoneId: string, phase: string }} args
115
+ * @returns {{ ok: true, phase: string } | { ok: false, reason: string }}
116
+ */
117
+ export function completePhase(projectDir, { runId, milestoneId, phase }) {
118
+ if (!SAGA_PHASES.includes(phase)) return { ok: false, reason: 'invalid_phase' }
119
+ appendProduceEvent(projectDir, 'produce.saga.phase_completed', {
120
+ run_id: runId,
121
+ milestone_id: milestoneId,
122
+ phase,
123
+ })
124
+ return { ok: true, phase }
125
+ }
126
+
127
+ /**
128
+ * Fire the FORWARD compensator for a failed saga phase (ID-M4-02). Resolves the
129
+ * declared compensator, then appends `produce.saga.compensated {run_id,
130
+ * milestone_id, phase, compensator: <descriptor.kind>, reason}` — carrying the
131
+ * descriptor KIND, never a rollback instruction. Validates the phase fail-closed.
132
+ * The CLI records the forward compensator; it does NOT execute the revert/reopen/
133
+ * re-plan (OOS dispatch) and NEVER mutates the roadmap milestone status.
134
+ *
135
+ * @param {string} projectDir
136
+ * @param {{ runId: string, milestoneId: string, phase: string, reason?: string }} args
137
+ * @returns {{ ok: true, phase: string, compensator: object } | { ok: false, reason: string }}
138
+ */
139
+ export function compensatePhase(projectDir, { runId, milestoneId, phase, reason }) {
140
+ if (!SAGA_PHASES.includes(phase)) return { ok: false, reason: 'invalid_phase' }
141
+ const compensator = resolveCompensator(phase)
142
+ appendProduceEvent(projectDir, 'produce.saga.compensated', {
143
+ run_id: runId,
144
+ milestone_id: milestoneId,
145
+ phase,
146
+ compensator: compensator.kind,
147
+ reason: reason || `compensate_${phase}`,
148
+ })
149
+ return { ok: true, phase, compensator }
150
+ }
151
+
152
+ /**
153
+ * Record a provider-diverse review verdict (ID-M4-04 / ID-08). The review-path
154
+ * analog of evidence-oracle.mjs::recordOracleVerdict. REFUSES fail-closed when
155
+ * the reviewer family equals the recorded implementer family (a same-family
156
+ * reviewer rubber-stamps its own family's work) or when no implementer family is
157
+ * recorded (ID-M2-02). On acceptance appends `produce.saga.reviewed {run_id,
158
+ * milestone_id, reviewer_family, verdict}`.
159
+ *
160
+ * @param {string} projectDir
161
+ * @param {{ runId: string, milestoneId: string, verdict: string, reviewerFamily: string }} args
162
+ * @returns {{ ok: true, verdict: string, reviewer_family: string, implementer_family: string }
163
+ * | { ok: false, reason: string, implementer_family?: string | null }}
164
+ */
165
+ export function recordReview(projectDir, { runId, milestoneId, verdict, reviewerFamily }) {
166
+ if (verdict !== 'pass' && verdict !== 'fail') return { ok: false, reason: 'invalid_verdict' }
167
+ if (!reviewerFamily) return { ok: false, reason: 'no_reviewer_family' }
168
+
169
+ const implementerFamily = readImplementerFamily(projectDir, runId, milestoneId)
170
+ if (!implementerFamily)
171
+ return { ok: false, reason: 'no_recorded_implementer', implementer_family: null }
172
+
173
+ // Cross-family enforcement (normalized so claude-code === claude).
174
+ if (normalizeFamily(reviewerFamily) === normalizeFamily(implementerFamily))
175
+ return { ok: false, reason: 'same_family_reviewer', implementer_family: implementerFamily }
176
+
177
+ appendProduceEvent(projectDir, 'produce.saga.reviewed', {
178
+ run_id: runId,
179
+ milestone_id: milestoneId,
180
+ reviewer_family: reviewerFamily,
181
+ verdict,
182
+ })
183
+
184
+ return {
185
+ ok: true,
186
+ verdict,
187
+ reviewer_family: reviewerFamily,
188
+ implementer_family: implementerFamily,
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Resolve a REVIEW-phase reviewer family DISJOINT from the recorded implementer
194
+ * (ID-M4-04 / ID-08), a THIN wrapper over the M2 `resolveProbeFamily` — NO
195
+ * duplicated cross-family / partner-map / model-resolution logic. Reads the
196
+ * implementer family from the recorded `produce.milestone.implemented` trail
197
+ * (fail-closed when absent). Returns the resolution with `probe_family` surfaced
198
+ * as `reviewer_family`. Provider is an OUTPUT; the model is resolved LIVE (null
199
+ * for self-resolving families) — never a hardcoded id.
200
+ *
201
+ * @param {string} projectDir
202
+ * @param {{ runId: string, milestoneId: string, hostFamily?: string | null, availableFamilies?: string[] }} args
203
+ * @returns {{ available: boolean, reviewer_family: string | null, implementer_family: string | null,
204
+ * transport_hint: object | null, model: string | null, source: string | null, reason: string | null }}
205
+ */
206
+ export function resolveReviewer(projectDir, { runId, milestoneId, hostFamily, availableFamilies }) {
207
+ const implementerFamily = readImplementerFamily(projectDir, runId, milestoneId)
208
+ if (!implementerFamily) {
209
+ return {
210
+ available: false,
211
+ reviewer_family: null,
212
+ implementer_family: null,
213
+ transport_hint: null,
214
+ model: null,
215
+ source: null,
216
+ reason: 'no_recorded_implementer',
217
+ }
218
+ }
219
+
220
+ const resolution = resolveProbeFamily({ implementerFamily, hostFamily, availableFamilies })
221
+ // Surface the disjoint probe family as the reviewer family (same selection).
222
+ const { probe_family, ...rest } = resolution
223
+ return { ...rest, reviewer_family: probe_family, implementer_family: implementerFamily }
224
+ }
225
+
226
+ /**
227
+ * Fold the run's `produce.saga.*` events into per-milestone saga state — a
228
+ * SEPARATE fold parallel to the roadmap projection (the roadmap projection folds
229
+ * `produce.roadmap.*` ONLY, so saga events never enter the roadmap the Showrunner
230
+ * re-reads each tick — blindness preserved, ID-M4-01). Deterministic: two folds
231
+ * of the same log yield the identical saga state; `updated_at` is the last folded
232
+ * event's timestamp.
233
+ *
234
+ * @param {string} projectDir
235
+ * @param {string} runId
236
+ * @returns {{
237
+ * run_id: string,
238
+ * milestones: Array<{
239
+ * milestone_id: string,
240
+ * current_phase: string | null,
241
+ * phase_status: 'entered' | 'completed' | null,
242
+ * phases: Record<string, 'entered' | 'completed'>,
243
+ * last_compensated: { phase: string | null, compensator: string | null, reason: string | null } | null,
244
+ * reviewed: { reviewer_family: string | null, verdict: string | null } | null,
245
+ * }>,
246
+ * updated_at: string | null,
247
+ * }}
248
+ */
249
+ export function projectSaga(projectDir, runId) {
250
+ const events = readProduceRunEventsMatching(projectDir, runId, (op) =>
251
+ op.startsWith('produce.saga.'),
252
+ )
253
+ const byId = new Map()
254
+ const order = []
255
+ let updatedAt = null
256
+
257
+ const ensure = (id) => {
258
+ if (!byId.has(id)) {
259
+ byId.set(id, {
260
+ milestone_id: id,
261
+ current_phase: null,
262
+ phase_status: null,
263
+ phases: {},
264
+ last_compensated: null,
265
+ reviewed: null,
266
+ })
267
+ order.push(id)
268
+ }
269
+ return byId.get(id)
270
+ }
271
+
272
+ for (const e of events) {
273
+ const d = e.data || {}
274
+ const id = typeof d.milestone_id === 'string' ? d.milestone_id : null
275
+ if (!id) continue
276
+ const m = ensure(id)
277
+ const phase = typeof d.phase === 'string' ? d.phase : null
278
+
279
+ if (e.op === 'produce.saga.phase_entered' && phase) {
280
+ m.current_phase = phase
281
+ m.phase_status = 'entered'
282
+ m.phases[phase] = 'entered'
283
+ } else if (e.op === 'produce.saga.phase_completed' && phase) {
284
+ m.current_phase = phase
285
+ m.phase_status = 'completed'
286
+ m.phases[phase] = 'completed'
287
+ } else if (e.op === 'produce.saga.compensated') {
288
+ m.last_compensated = {
289
+ phase,
290
+ compensator: typeof d.compensator === 'string' ? d.compensator : null,
291
+ reason: typeof d.reason === 'string' ? d.reason : null,
292
+ }
293
+ } else if (e.op === 'produce.saga.reviewed') {
294
+ m.reviewed = {
295
+ reviewer_family: typeof d.reviewer_family === 'string' ? d.reviewer_family : null,
296
+ verdict: typeof d.verdict === 'string' ? d.verdict : null,
297
+ }
298
+ }
299
+ if (typeof e.t === 'string') updatedAt = e.t
300
+ }
301
+
302
+ return { run_id: runId, milestones: order.map((id) => byId.get(id)), updated_at: updatedAt }
303
+ }