@aperant/framework 0.16.0 → 0.19.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 (174) hide show
  1. package/CHANGELOG.md +185 -1
  2. package/dist/cli/commands/ci-watch.mjs +49 -2
  3. package/dist/cli/commands/ci-watch.mjs.map +1 -1
  4. package/dist/cli/commands/commit.mjs +3 -3
  5. package/dist/cli/commands/commit.mjs.map +1 -1
  6. package/dist/cli/commands/event.mjs +16 -16
  7. package/dist/cli/commands/event.mjs.map +1 -1
  8. package/dist/cli/commands/merge-integrate.mjs +3 -3
  9. package/dist/cli/commands/merge-integrate.mjs.map +1 -1
  10. package/dist/cli/commands/produce.d.mts +9 -0
  11. package/dist/cli/commands/produce.d.mts.map +1 -0
  12. package/dist/cli/commands/produce.mjs +1345 -0
  13. package/dist/cli/commands/produce.mjs.map +1 -0
  14. package/dist/cli/commands/spar-resolve-partner.d.mts +9 -0
  15. package/dist/cli/commands/spar-resolve-partner.d.mts.map +1 -0
  16. package/dist/cli/commands/spar-resolve-partner.mjs +82 -0
  17. package/dist/cli/commands/spar-resolve-partner.mjs.map +1 -0
  18. package/dist/cli/commands/task.d.mts +16 -1
  19. package/dist/cli/commands/task.d.mts.map +1 -1
  20. package/dist/cli/commands/task.mjs +434 -266
  21. package/dist/cli/commands/task.mjs.map +1 -1
  22. package/dist/cli/config/gitignore-drift.d.mts.map +1 -1
  23. package/dist/cli/config/gitignore-drift.mjs +1 -0
  24. package/dist/cli/config/gitignore-drift.mjs.map +1 -1
  25. package/dist/cli/config/post-merge-sweep.d.mts.map +1 -1
  26. package/dist/cli/config/post-merge-sweep.mjs +1 -0
  27. package/dist/cli/config/post-merge-sweep.mjs.map +1 -1
  28. package/dist/cli/coordination/auto-emit-artifact-ready.mjs +5 -5
  29. package/dist/cli/coordination/auto-emit-artifact-ready.mjs.map +1 -1
  30. package/dist/cli/coordination/event-schema.d.mts +5 -3
  31. package/dist/cli/coordination/event-schema.d.mts.map +1 -1
  32. package/dist/cli/coordination/event-schema.mjs +245 -21
  33. package/dist/cli/coordination/event-schema.mjs.map +1 -1
  34. package/dist/cli/coordination/store.d.mts +2 -2
  35. package/dist/cli/coordination/store.mjs +4 -4
  36. package/dist/cli/coordination/store.mjs.map +1 -1
  37. package/dist/cli/dispatch.d.mts.map +1 -1
  38. package/dist/cli/dispatch.mjs +4 -0
  39. package/dist/cli/dispatch.mjs.map +1 -1
  40. package/dist/cli/help.d.mts.map +1 -1
  41. package/dist/cli/help.mjs +31 -0
  42. package/dist/cli/help.mjs.map +1 -1
  43. package/dist/cli/host/codex-config.d.mts +78 -0
  44. package/dist/cli/host/codex-config.d.mts.map +1 -0
  45. package/dist/cli/host/codex-config.mjs +169 -0
  46. package/dist/cli/host/codex-config.mjs.map +1 -0
  47. package/dist/cli/install/legacy-paths.d.mts.map +1 -1
  48. package/dist/cli/install/legacy-paths.mjs +2 -0
  49. package/dist/cli/install/legacy-paths.mjs.map +1 -1
  50. package/dist/cli/produce/blind-probe.d.mts +85 -0
  51. package/dist/cli/produce/blind-probe.d.mts.map +1 -0
  52. package/dist/cli/produce/blind-probe.mjs +217 -0
  53. package/dist/cli/produce/blind-probe.mjs.map +1 -0
  54. package/dist/cli/produce/claim.d.mts +188 -0
  55. package/dist/cli/produce/claim.d.mts.map +1 -0
  56. package/dist/cli/produce/claim.mjs +518 -0
  57. package/dist/cli/produce/claim.mjs.map +1 -0
  58. package/dist/cli/produce/done-gate.d.mts +87 -0
  59. package/dist/cli/produce/done-gate.d.mts.map +1 -0
  60. package/dist/cli/produce/done-gate.mjs +200 -0
  61. package/dist/cli/produce/done-gate.mjs.map +1 -0
  62. package/dist/cli/produce/events.d.mts +77 -0
  63. package/dist/cli/produce/events.d.mts.map +1 -0
  64. package/dist/cli/produce/events.mjs +126 -0
  65. package/dist/cli/produce/events.mjs.map +1 -0
  66. package/dist/cli/produce/evidence-oracle.d.mts +63 -0
  67. package/dist/cli/produce/evidence-oracle.d.mts.map +1 -0
  68. package/dist/cli/produce/evidence-oracle.mjs +122 -0
  69. package/dist/cli/produce/evidence-oracle.mjs.map +1 -0
  70. package/dist/cli/produce/ledger.d.mts +140 -0
  71. package/dist/cli/produce/ledger.d.mts.map +1 -0
  72. package/dist/cli/produce/ledger.mjs +272 -0
  73. package/dist/cli/produce/ledger.mjs.map +1 -0
  74. package/dist/cli/produce/probe-family.d.mts +53 -0
  75. package/dist/cli/produce/probe-family.d.mts.map +1 -0
  76. package/dist/cli/produce/probe-family.mjs +160 -0
  77. package/dist/cli/produce/probe-family.mjs.map +1 -0
  78. package/dist/cli/produce/projection.d.mts +55 -0
  79. package/dist/cli/produce/projection.d.mts.map +1 -0
  80. package/dist/cli/produce/projection.mjs +97 -0
  81. package/dist/cli/produce/projection.mjs.map +1 -0
  82. package/dist/cli/produce/run-id.d.mts +42 -0
  83. package/dist/cli/produce/run-id.d.mts.map +1 -0
  84. package/dist/cli/produce/run-id.mjs +79 -0
  85. package/dist/cli/produce/run-id.mjs.map +1 -0
  86. package/dist/cli/produce/saga.d.mts +180 -0
  87. package/dist/cli/produce/saga.d.mts.map +1 -0
  88. package/dist/cli/produce/saga.mjs +290 -0
  89. package/dist/cli/produce/saga.mjs.map +1 -0
  90. package/dist/cli/produce/scheduler.d.mts +165 -0
  91. package/dist/cli/produce/scheduler.d.mts.map +1 -0
  92. package/dist/cli/produce/scheduler.mjs +399 -0
  93. package/dist/cli/produce/scheduler.mjs.map +1 -0
  94. package/dist/cli/produce/setpoint.d.mts +52 -0
  95. package/dist/cli/produce/setpoint.d.mts.map +1 -0
  96. package/dist/cli/produce/setpoint.mjs +113 -0
  97. package/dist/cli/produce/setpoint.mjs.map +1 -0
  98. package/dist/cli/produce/verification-ttl.d.mts +75 -0
  99. package/dist/cli/produce/verification-ttl.d.mts.map +1 -0
  100. package/dist/cli/produce/verification-ttl.mjs +169 -0
  101. package/dist/cli/produce/verification-ttl.mjs.map +1 -0
  102. package/dist/cli/roadmap/{conductor-view.d.mts → showrunner-view.d.mts} +3 -3
  103. package/dist/cli/roadmap/showrunner-view.d.mts.map +1 -0
  104. package/dist/cli/roadmap/{conductor-view.mjs → showrunner-view.mjs} +7 -7
  105. package/dist/cli/roadmap/showrunner-view.mjs.map +1 -0
  106. package/dist/cli/wfrun/run.d.mts.map +1 -1
  107. package/dist/cli/wfrun/run.mjs +1 -0
  108. package/dist/cli/wfrun/run.mjs.map +1 -1
  109. package/dist/plugin/.claude-plugin/plugin.json +2 -1
  110. package/dist/plugin/skills/apt/SKILL.md +112 -38
  111. package/dist/plugin/skills/apt-debug/SKILL.md +14 -24
  112. package/dist/plugin/skills/apt-fan-out/SKILL.md +4 -4
  113. package/dist/plugin/skills/apt-handoff/SKILL.md +1 -1
  114. package/dist/plugin/skills/apt-plan/SKILL.md +5 -5
  115. package/dist/plugin/skills/apt-plan/adapters/{conductor.md → showrunner.md} +16 -16
  116. package/dist/plugin/skills/apt-produce/SKILL.md +606 -0
  117. package/dist/plugin/skills/apt-quick/SKILL.md +14 -22
  118. package/dist/plugin/skills/apt-run/SKILL.md +126 -3
  119. package/dist/plugin/skills/apt-ship/SKILL.md +2 -0
  120. package/dist/plugin/skills/apt-spar/SKILL.md +19 -9
  121. package/dist/plugin/skills/apt-watch-ci/SKILL.md +4 -4
  122. package/package.json +4 -4
  123. package/prompts/{conductor-framework-context.md → showrunner-framework-context.md} +1 -1
  124. package/prompts/{conductor-status-check.md → showrunner-status-check.md} +1 -1
  125. package/prompts/{conductor-sub-agent.md → showrunner-sub-agent.md} +6 -6
  126. package/prompts/{conductor-system.md → showrunner-system.md} +8 -8
  127. package/skills/apt/SKILL.md +112 -38
  128. package/skills/apt-debug/SKILL.md +14 -24
  129. package/skills/apt-fan-out/SKILL.md +4 -4
  130. package/skills/apt-handoff/SKILL.md +1 -1
  131. package/skills/apt-plan/SKILL.md +5 -5
  132. package/skills/apt-plan/adapters/{conductor.md → showrunner.md} +16 -16
  133. package/skills/apt-produce/SKILL.md +606 -0
  134. package/skills/apt-quick/SKILL.md +14 -22
  135. package/skills/apt-run/SKILL.md +126 -3
  136. package/skills/apt-ship/SKILL.md +2 -0
  137. package/skills/apt-spar/SKILL.md +19 -9
  138. package/skills/apt-watch-ci/SKILL.md +4 -4
  139. package/src/cli/commands/ci-watch.mjs +51 -2
  140. package/src/cli/commands/commit.mjs +3 -3
  141. package/src/cli/commands/event.mjs +16 -16
  142. package/src/cli/commands/merge-integrate.mjs +3 -3
  143. package/src/cli/commands/produce.mjs +1466 -0
  144. package/src/cli/commands/spar-resolve-partner.mjs +85 -0
  145. package/src/cli/commands/task.mjs +482 -285
  146. package/src/cli/config/gitignore-drift.mjs +1 -0
  147. package/src/cli/config/post-merge-sweep.mjs +1 -0
  148. package/src/cli/coordination/auto-emit-artifact-ready.mjs +5 -5
  149. package/src/cli/coordination/event-schema.d.ts +4 -2
  150. package/src/cli/coordination/event-schema.mjs +276 -21
  151. package/src/cli/coordination/store.mjs +4 -4
  152. package/src/cli/dispatch.mjs +4 -0
  153. package/src/cli/help.mjs +31 -0
  154. package/src/cli/host/codex-config.mjs +165 -0
  155. package/src/cli/install/legacy-paths.mjs +2 -0
  156. package/src/cli/produce/blind-probe.mjs +245 -0
  157. package/src/cli/produce/claim.mjs +543 -0
  158. package/src/cli/produce/done-gate.mjs +238 -0
  159. package/src/cli/produce/events.mjs +131 -0
  160. package/src/cli/produce/evidence-oracle.mjs +133 -0
  161. package/src/cli/produce/ledger.mjs +284 -0
  162. package/src/cli/produce/probe-family.mjs +168 -0
  163. package/src/cli/produce/projection.mjs +105 -0
  164. package/src/cli/produce/run-id.mjs +84 -0
  165. package/src/cli/produce/saga.mjs +303 -0
  166. package/src/cli/produce/scheduler.mjs +423 -0
  167. package/src/cli/produce/setpoint.mjs +122 -0
  168. package/src/cli/produce/verification-ttl.mjs +191 -0
  169. package/src/cli/roadmap/showrunner-view.d.ts +10 -0
  170. package/src/cli/roadmap/{conductor-view.mjs → showrunner-view.mjs} +6 -6
  171. package/src/cli/wfrun/run.mjs +1 -0
  172. package/dist/cli/roadmap/conductor-view.d.mts.map +0 -1
  173. package/dist/cli/roadmap/conductor-view.mjs.map +0 -1
  174. package/src/cli/roadmap/conductor-view.d.ts +0 -10
@@ -0,0 +1,238 @@
1
+ /**
2
+ * @internal
3
+ * produce/done-gate.mjs — the real two-gate done adjudication + canary (M2,
4
+ * ID-M2-04 / AC8-AC11).
5
+ *
6
+ * A milestone flips to `done` ONLY when the VISIBLE oracle AND the BLIND probe
7
+ * both agree — otherwise it stays open and the reward-hacking canary logs the
8
+ * gap. The Showrunner still cannot self-attest: `done` is reachable ONLY through
9
+ * recorded external evidence + a blind cross-family probe (ID-03 preserved). Any
10
+ * missing / same-family / stale seal fails closed.
11
+ *
12
+ * The CLI is deterministic — it reads recorded evidence (verify-proof's verdict
13
+ * file + the recorded `produce.oracle.judged` / `produce.probe.run` /
14
+ * `produce.probe.frozen` seals) and computes the gate. The (LLM) oracle-judge +
15
+ * probe-run steps are driven cross-family by the skill; the CLI records + gates.
16
+ */
17
+
18
+ import {
19
+ appendProduceEvent,
20
+ readImplementerFamily,
21
+ readProduceRunEventsMatching,
22
+ } from './events.mjs'
23
+ import { prepareOracleInput } from './evidence-oracle.mjs'
24
+ import { normalizeFamily } from './probe-family.mjs'
25
+ import { projectRoadmap, writeProjectionCache } from './projection.mjs'
26
+
27
+ /** The last event matching `matchOp` for a milestone, or null. */
28
+ function latestForMilestone(projectDir, runId, milestoneId, matchOp) {
29
+ const events = readProduceRunEventsMatching(projectDir, runId, matchOp)
30
+ let last = null
31
+ for (const e of events) {
32
+ if (e.data && e.data.milestone_id === milestoneId) last = e
33
+ }
34
+ return last
35
+ }
36
+
37
+ /** Whether any event matching `matchOp` (with a cross-family predicate) exists. */
38
+ function anyForMilestone(projectDir, runId, milestoneId, matchOp, predicate) {
39
+ const events = readProduceRunEventsMatching(projectDir, runId, matchOp)
40
+ return events.some((e) => e.data && e.data.milestone_id === milestoneId && predicate(e.data))
41
+ }
42
+
43
+ /**
44
+ * Evaluate the RECORDED gate seals for a milestone (no verify-proof file read —
45
+ * that live half is added by `adjudicateDone`). Pure read.
46
+ *
47
+ * @param {string} projectDir
48
+ * @param {string} runId
49
+ * @param {string} milestoneId
50
+ * @returns {{
51
+ * implementer_family: string | null,
52
+ * oracle_pass_cross_family: boolean,
53
+ * probe_pass_cross_family: boolean,
54
+ * frozen_cross_family: boolean,
55
+ * blind_gate: boolean,
56
+ * }}
57
+ */
58
+ export function evaluateRecordedGates(projectDir, runId, milestoneId) {
59
+ const implementer = readImplementerFamily(projectDir, runId, milestoneId)
60
+ const impl = implementer ? normalizeFamily(implementer) : null
61
+
62
+ const oracle = latestForMilestone(
63
+ projectDir,
64
+ runId,
65
+ milestoneId,
66
+ (op) => op === 'produce.oracle.judged',
67
+ )
68
+ const oracle_pass_cross_family =
69
+ !!impl &&
70
+ !!oracle &&
71
+ oracle.data.verdict === 'pass' &&
72
+ normalizeFamily(oracle.data.oracle_family) !== impl
73
+
74
+ const probeRun = latestForMilestone(
75
+ projectDir,
76
+ runId,
77
+ milestoneId,
78
+ (op) => op === 'produce.probe.run',
79
+ )
80
+ const probe_pass_cross_family =
81
+ !!impl &&
82
+ !!probeRun &&
83
+ probeRun.data.result === 'pass' &&
84
+ normalizeFamily(probeRun.data.probe_family) !== impl
85
+
86
+ const frozen_cross_family =
87
+ !!impl &&
88
+ anyForMilestone(
89
+ projectDir,
90
+ runId,
91
+ milestoneId,
92
+ (op) => op === 'produce.probe.frozen',
93
+ (d) => normalizeFamily(d.author_family) !== impl,
94
+ )
95
+
96
+ return {
97
+ implementer_family: implementer,
98
+ oracle_pass_cross_family,
99
+ probe_pass_cross_family,
100
+ frozen_cross_family,
101
+ blind_gate: probe_pass_cross_family && frozen_cross_family,
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Adjudicate a milestone `done` (ID-M2-04). `done` accepted iff:
107
+ * visible_gate = verify-proof verdict === 'approved' AND recorded
108
+ * produce.oracle.judged.verdict === 'pass' with oracle_family !== implementer;
109
+ * blind_gate = recorded produce.probe.run.result === 'pass' with
110
+ * probe_family !== implementer AND a produce.probe.frozen exists with
111
+ * author_family !== implementer.
112
+ * visible && blind → append produce.oracle.verdict{done} +
113
+ * produce.roadmap.milestone_status_changed{done} + refresh the cache → ok.
114
+ * visible && !blind → append produce.oracle.probe_gap → err (stays open).
115
+ * !visible → err not-adjudicable. Absent implementer → err fail-closed.
116
+ *
117
+ * @param {{ projectDir: string, runId: string, milestoneId: string, verdictPath: string }} args
118
+ * @returns {{ ok: true, milestone_id: string, roadmap: object }
119
+ * | { ok: false, reason: string, visible_gate: boolean, blind_gate: boolean, detail?: object }}
120
+ */
121
+ export function adjudicateDone({ projectDir, runId, milestoneId, verdictPath }) {
122
+ const gates = evaluateRecordedGates(projectDir, runId, milestoneId)
123
+ if (!gates.implementer_family)
124
+ return { ok: false, reason: 'no_recorded_implementer', visible_gate: false, blind_gate: false }
125
+
126
+ // The live half of the visible gate: verify-proof's v2 verdict === 'approved'.
127
+ const prepared = prepareOracleInput(verdictPath)
128
+ const visible_gate = prepared.approved && gates.oracle_pass_cross_family
129
+ const blind_gate = gates.blind_gate
130
+
131
+ if (!visible_gate)
132
+ return {
133
+ ok: false,
134
+ reason: 'not_adjudicable',
135
+ visible_gate,
136
+ blind_gate,
137
+ detail: {
138
+ verify_proof_verdict: prepared.verdict,
139
+ verify_proof_approved: prepared.approved,
140
+ oracle_pass_cross_family: gates.oracle_pass_cross_family,
141
+ },
142
+ }
143
+
144
+ if (!blind_gate) {
145
+ // Visible passed, blind failed — the reward-hacking near-miss. Log the
146
+ // canary event; the milestone STAYS OPEN (AC9).
147
+ appendProduceEvent(projectDir, 'produce.oracle.probe_gap', {
148
+ run_id: runId,
149
+ milestone_id: milestoneId,
150
+ })
151
+ return {
152
+ ok: false,
153
+ reason: 'probe_gap',
154
+ visible_gate,
155
+ blind_gate,
156
+ detail: {
157
+ probe_pass_cross_family: gates.probe_pass_cross_family,
158
+ frozen_cross_family: gates.frozen_cross_family,
159
+ },
160
+ }
161
+ }
162
+
163
+ // Both gates agree — the ONLY path to done. Record the verdict + flip status.
164
+ appendProduceEvent(projectDir, 'produce.oracle.verdict', {
165
+ run_id: runId,
166
+ milestone_id: milestoneId,
167
+ verdict: 'done',
168
+ })
169
+ appendProduceEvent(projectDir, 'produce.roadmap.milestone_status_changed', {
170
+ run_id: runId,
171
+ milestone_id: milestoneId,
172
+ status: 'done',
173
+ })
174
+ const roadmap = writeProjectionCache(projectDir, runId)
175
+ return { ok: true, milestone_id: milestoneId, roadmap }
176
+ }
177
+
178
+ /**
179
+ * The reward-hacking canary (ID-M2-06 / AC10): green-on-visible MINUS
180
+ * green-on-blind over the run's milestones, from RECORDED seals. Each
181
+ * `produce.oracle.probe_gap` is one visible-pass + blind-fail near-miss. The
182
+ * metric surfaces the divergence — it does not average it away.
183
+ *
184
+ * @param {string} projectDir
185
+ * @param {string} runId
186
+ * @returns {{
187
+ * run_id: string,
188
+ * green_on_visible: number,
189
+ * green_on_blind: number,
190
+ * gap: number,
191
+ * probe_gap_events: number,
192
+ * per_milestone: Array<{ milestone_id: string, title: string, status: string, visible: boolean, blind: boolean, probe_gaps: number }>,
193
+ * }}
194
+ */
195
+ export function computeCanary(projectDir, runId) {
196
+ const roadmap = projectRoadmap(projectDir, runId)
197
+ const gapEvents = readProduceRunEventsMatching(
198
+ projectDir,
199
+ runId,
200
+ (op) => op === 'produce.oracle.probe_gap',
201
+ )
202
+ const gapCountByMilestone = new Map()
203
+ for (const e of gapEvents) {
204
+ const id = e.data && typeof e.data.milestone_id === 'string' ? e.data.milestone_id : null
205
+ if (id) gapCountByMilestone.set(id, (gapCountByMilestone.get(id) || 0) + 1)
206
+ }
207
+
208
+ let greenOnVisible = 0
209
+ let greenOnBlind = 0
210
+ const perMilestone = roadmap.milestones.map((m) => {
211
+ const gates = evaluateRecordedGates(projectDir, runId, m.id)
212
+ // The recorded (append-log) half of the visible gate — a recorded
213
+ // cross-family oracle pass. In the production flow the oracle is recorded
214
+ // only after verify-proof `approved` (the skill gates `oracle prepare` on
215
+ // it), so this is the durable visible-gate signal for the canary.
216
+ const visible = gates.oracle_pass_cross_family
217
+ const blind = gates.blind_gate
218
+ if (visible) greenOnVisible += 1
219
+ if (blind) greenOnBlind += 1
220
+ return {
221
+ milestone_id: m.id,
222
+ title: m.title,
223
+ status: m.status,
224
+ visible,
225
+ blind,
226
+ probe_gaps: gapCountByMilestone.get(m.id) || 0,
227
+ }
228
+ })
229
+
230
+ return {
231
+ run_id: runId,
232
+ green_on_visible: greenOnVisible,
233
+ green_on_blind: greenOnBlind,
234
+ gap: greenOnVisible - greenOnBlind,
235
+ probe_gap_events: gapEvents.length,
236
+ per_milestone: perMilestone,
237
+ }
238
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * @internal
3
+ * produce/events.mjs — append + read the run-scoped produce event trail (M1).
4
+ *
5
+ * Writes go through `createCoordinationStore(dir).appendEvent` → the O_APPEND
6
+ * `.aperant/events/*.jsonl` log, schema-gated by event-schema.mjs (ID-04). The
7
+ * append is atomic across concurrent writers (POSIX O_APPEND under PIPE_BUF),
8
+ * so N parallel `add-milestone` writers never lose updates (parent TD-06).
9
+ *
10
+ * Reads fold the run's `produce.roadmap.*` events across the day buckets from
11
+ * the run's start day (setpoint.json.frozen_at) to today (UTC), filtered by
12
+ * `data.run_id`. Bounded + deterministic (ID-06).
13
+ */
14
+
15
+ import { readLifecycleEventsForDay } from '../coordination/event-log.mjs'
16
+ import { createCoordinationStore } from '../coordination/store.mjs'
17
+ import { resolveRunDir } from './run-id.mjs'
18
+ import { readSetpoint } from './setpoint.mjs'
19
+
20
+ /** The op-prefix the projection folds. */
21
+ export const ROADMAP_OP_PREFIX = 'produce.roadmap.'
22
+
23
+ /**
24
+ * Append one produce event through the schema-gated, O_APPEND coordination
25
+ * store. `data.run_id` also tags the event's top-level `task` field so the
26
+ * forensic trail is queryable by run. Returns the store's append result (null
27
+ * on schema violation — fail-closed).
28
+ *
29
+ * @param {string} projectDir
30
+ * @param {string} op
31
+ * @param {Record<string, unknown>} data
32
+ */
33
+ export function appendProduceEvent(projectDir, op, data) {
34
+ const store = createCoordinationStore(projectDir)
35
+ const runId = data && typeof data.run_id === 'string' ? data.run_id : null
36
+ return store.appendEvent({ op, task: runId, data: data || {} })
37
+ }
38
+
39
+ /** Inclusive list of 'YYYY-MM-DD' UTC day buckets from startDay to endDay. */
40
+ function dayBucketsInclusive(startDay, endDay) {
41
+ const cur = new Date(`${startDay}T00:00:00.000Z`)
42
+ const end = new Date(`${endDay}T00:00:00.000Z`)
43
+ // Inverted / malformed range → just today's bucket (defensive).
44
+ if (Number.isNaN(cur.getTime()) || Number.isNaN(end.getTime()) || cur > end) return [endDay]
45
+ const out = []
46
+ while (cur <= end) {
47
+ out.push(cur.toISOString().slice(0, 10))
48
+ cur.setUTCDate(cur.getUTCDate() + 1)
49
+ // Safety cap — a real run's window is days, not a decade.
50
+ if (out.length > 3660) break
51
+ }
52
+ return out
53
+ }
54
+
55
+ /**
56
+ * Fold the run's produce events matching `opMatches(op)` in chronological
57
+ * (append) order. The window starts at the setpoint's frozen day and ends today
58
+ * (UTC). Events are filtered to `data.run_id === runId`.
59
+ *
60
+ * This is the shared reader over the run's day-bucket window. `matchOp` selects
61
+ * which produce ops to fold — the roadmap projection folds ONLY the
62
+ * `produce.roadmap.*` prefix (see `readProduceEventsForRun`), while the M2
63
+ * oracle/probe consumers select `produce.milestone.implemented` /
64
+ * `produce.probe.*` / `produce.oracle.*`. It reads ONLY the append-only event
65
+ * log — it NEVER opens the frozen probe-content file (probe-blindness, ID-M2-03).
66
+ *
67
+ * @param {string} projectDir
68
+ * @param {string} runId
69
+ * @param {(op: string) => boolean} matchOp
70
+ * @returns {Array<{ op: string, data: Record<string, unknown>, t?: string }>}
71
+ */
72
+ export function readProduceRunEventsMatching(projectDir, runId, matchOp) {
73
+ const setpoint = readSetpoint(resolveRunDir(projectDir, runId))
74
+ const today = new Date().toISOString().slice(0, 10)
75
+ const startDay =
76
+ setpoint && typeof setpoint.frozen_at === 'string' ? setpoint.frozen_at.slice(0, 10) : today
77
+ const out = []
78
+ for (const day of dayBucketsInclusive(startDay, today)) {
79
+ for (const e of readLifecycleEventsForDay(projectDir, day)) {
80
+ if (typeof e.op !== 'string') continue
81
+ if (!matchOp(e.op)) continue
82
+ if (!e.data || e.data.run_id !== runId) continue
83
+ out.push(e)
84
+ }
85
+ }
86
+ return out
87
+ }
88
+
89
+ /**
90
+ * Fold the run's `produce.roadmap.*` events in chronological (append) order.
91
+ * The projection consumes ONLY this reader, so the roadmap the Showrunner
92
+ * re-reads each tick can never surface a `produce.probe.*` / `produce.oracle.*`
93
+ * event (structural probe-blindness, AC5).
94
+ *
95
+ * @param {string} projectDir
96
+ * @param {string} runId
97
+ * @returns {Array<{ op: string, data: Record<string, unknown>, t?: string }>}
98
+ */
99
+ export function readProduceEventsForRun(projectDir, runId) {
100
+ return readProduceRunEventsMatching(projectDir, runId, (op) => op.startsWith(ROADMAP_OP_PREFIX))
101
+ }
102
+
103
+ /**
104
+ * Read the recorded implementer family for a run + milestone from the
105
+ * append-only `produce.milestone.implemented` trail (ID-M2-02 — the recorded
106
+ * disjointness anchor the cross-family rule is enforced against). Returns the
107
+ * LATEST recorded `implementer_family` for the milestone, or null when none is
108
+ * recorded (fail-closed — you cannot enforce disjointness against an unknown
109
+ * implementer).
110
+ *
111
+ * @param {string} projectDir
112
+ * @param {string} runId
113
+ * @param {string} milestoneId
114
+ * @returns {string | null}
115
+ */
116
+ export function readImplementerFamily(projectDir, runId, milestoneId) {
117
+ const events = readProduceRunEventsMatching(
118
+ projectDir,
119
+ runId,
120
+ (op) => op === 'produce.milestone.implemented',
121
+ )
122
+ let family = null
123
+ for (const e of events) {
124
+ const d = e.data || {}
125
+ if (d.milestone_id !== milestoneId) continue
126
+ if (typeof d.implementer_family === 'string' && d.implementer_family.length > 0) {
127
+ family = d.implementer_family
128
+ }
129
+ }
130
+ return family
131
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * @internal
3
+ * produce/evidence-oracle.mjs — the independent evidence-quality oracle (M2, AC7).
4
+ *
5
+ * verify-proof issues a deterministic two-gate verdict over captured evidence;
6
+ * the evidence-oracle is a SECOND opinion — a cross-family judge deciding
7
+ * whether that captured evidence actually supports "done" (US-3 / ID-06). The
8
+ * CLI is deterministic: `prepareOracleInput` extracts the verdict + the tests[]
9
+ * evidence into a structured judge payload the skill sends to the cross-family
10
+ * model; `recordOracleVerdict` records the judge's pass/fail, refusing
11
+ * fail-closed when the oracle family equals the implementer family (the oracle
12
+ * that judged the work must not be the family that produced it — ID-08).
13
+ *
14
+ * The verify-proof verdict is read as the v2 closed-set `approved` ONLY
15
+ * (ID-M2-05): a legacy middle-band verdict, `needs_human`, `rejected`, or a
16
+ * missing/unreadable artifact is NOT `approved` → not adjudicable (fail-closed).
17
+ */
18
+
19
+ import { readJsonIfExists } from '../util/io.mjs'
20
+ import { appendProduceEvent, readImplementerFamily } from './events.mjs'
21
+ import { normalizeFamily } from './probe-family.mjs'
22
+
23
+ /** The single verdict verify-proof v2 treats as a pass (ID-M2-05). */
24
+ const APPROVED = 'approved'
25
+
26
+ /**
27
+ * Read verify-proof's `verification.json` at `verdictPath` and extract the
28
+ * v2 verdict + the tests[] {surface, evidence, result} rows into a structured
29
+ * judge-input payload. The verdict is read from `meta.verdict` (or a top-level
30
+ * `verdict`) and must be EXACTLY `approved` — anything else (or a
31
+ * missing/unreadable artifact) is NOT approved (fail-closed).
32
+ *
33
+ * @param {string} verdictPath — absolute/relative path to verification.json.
34
+ * @returns {{
35
+ * ok: boolean,
36
+ * approved: boolean,
37
+ * verdict: string | null,
38
+ * reason: string | null,
39
+ * tests: Array<{ id: string | null, surface: string | null, evidence: string | null, result: string | null }>,
40
+ * judge_input: { verdict: string | null, task_id: string | null, spec: string | null, tests: Array<object> } | null,
41
+ * }}
42
+ */
43
+ export function prepareOracleInput(verdictPath) {
44
+ if (!verdictPath)
45
+ return {
46
+ ok: false,
47
+ approved: false,
48
+ verdict: null,
49
+ reason: 'no_verdict_path',
50
+ tests: [],
51
+ judge_input: null,
52
+ }
53
+
54
+ const doc = readJsonIfExists(verdictPath)
55
+ if (!doc || typeof doc !== 'object')
56
+ return {
57
+ ok: false,
58
+ approved: false,
59
+ verdict: null,
60
+ reason: 'verdict_unreadable',
61
+ tests: [],
62
+ judge_input: null,
63
+ }
64
+
65
+ const meta = doc.meta && typeof doc.meta === 'object' ? doc.meta : {}
66
+ const verdict =
67
+ typeof meta.verdict === 'string'
68
+ ? meta.verdict
69
+ : typeof doc.verdict === 'string'
70
+ ? doc.verdict
71
+ : null
72
+
73
+ const tests = Array.isArray(doc.tests)
74
+ ? doc.tests.map((t) => ({
75
+ id: t && typeof t.id === 'string' ? t.id : null,
76
+ surface: t && typeof t.surface === 'string' ? t.surface : null,
77
+ evidence: t && typeof t.evidence === 'string' ? t.evidence : null,
78
+ result: t && typeof t.result === 'string' ? t.result : null,
79
+ }))
80
+ : []
81
+
82
+ // EXACTLY `approved` — the legacy middle-band verdict is NOT
83
+ // approved (verify-proof v2 deleted it; ID-M2-05).
84
+ const approved = verdict === APPROVED
85
+
86
+ return {
87
+ ok: approved,
88
+ approved,
89
+ verdict,
90
+ reason: approved ? null : 'verdict_not_approved',
91
+ tests,
92
+ judge_input: {
93
+ verdict,
94
+ task_id: typeof meta.task_id === 'string' ? meta.task_id : null,
95
+ spec: typeof meta.spec === 'string' ? meta.spec : null,
96
+ tests,
97
+ },
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Record the independent evidence-quality oracle's verdict. REFUSES fail-closed
103
+ * when the oracle family equals the recorded implementer family (cross-family
104
+ * enforcement, ID-08) or when no implementer family is recorded (ID-M2-02). On
105
+ * acceptance appends `produce.oracle.judged {run_id, milestone_id, verdict,
106
+ * oracle_family}`.
107
+ *
108
+ * @param {string} projectDir
109
+ * @param {{ runId: string, milestoneId: string, verdict: string, oracleFamily: string }} args
110
+ * @returns {{ ok: true, verdict: string, oracle_family: string, implementer_family: string }
111
+ * | { ok: false, reason: string, implementer_family?: string | null }}
112
+ */
113
+ export function recordOracleVerdict(projectDir, { runId, milestoneId, verdict, oracleFamily }) {
114
+ if (verdict !== 'pass' && verdict !== 'fail') return { ok: false, reason: 'invalid_verdict' }
115
+ if (!oracleFamily) return { ok: false, reason: 'no_oracle_family' }
116
+
117
+ const implementerFamily = readImplementerFamily(projectDir, runId, milestoneId)
118
+ if (!implementerFamily)
119
+ return { ok: false, reason: 'no_recorded_implementer', implementer_family: null }
120
+
121
+ // Cross-family enforcement (normalized so claude-code === claude).
122
+ if (normalizeFamily(oracleFamily) === normalizeFamily(implementerFamily))
123
+ return { ok: false, reason: 'same_family_oracle', implementer_family: implementerFamily }
124
+
125
+ appendProduceEvent(projectDir, 'produce.oracle.judged', {
126
+ run_id: runId,
127
+ milestone_id: milestoneId,
128
+ verdict,
129
+ oracle_family: oracleFamily,
130
+ })
131
+
132
+ return { ok: true, verdict, oracle_family: oracleFamily, implementer_family: implementerFamily }
133
+ }