@aperant/framework 0.17.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 (152) hide show
  1. package/CHANGELOG.md +152 -0
  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/task.d.mts +16 -1
  15. package/dist/cli/commands/task.d.mts.map +1 -1
  16. package/dist/cli/commands/task.mjs +434 -266
  17. package/dist/cli/commands/task.mjs.map +1 -1
  18. package/dist/cli/coordination/auto-emit-artifact-ready.mjs +5 -5
  19. package/dist/cli/coordination/auto-emit-artifact-ready.mjs.map +1 -1
  20. package/dist/cli/coordination/event-schema.d.mts +5 -3
  21. package/dist/cli/coordination/event-schema.d.mts.map +1 -1
  22. package/dist/cli/coordination/event-schema.mjs +245 -21
  23. package/dist/cli/coordination/event-schema.mjs.map +1 -1
  24. package/dist/cli/coordination/store.d.mts +2 -2
  25. package/dist/cli/coordination/store.mjs +4 -4
  26. package/dist/cli/coordination/store.mjs.map +1 -1
  27. package/dist/cli/dispatch.d.mts.map +1 -1
  28. package/dist/cli/dispatch.mjs +2 -0
  29. package/dist/cli/dispatch.mjs.map +1 -1
  30. package/dist/cli/help.d.mts.map +1 -1
  31. package/dist/cli/help.mjs +30 -0
  32. package/dist/cli/help.mjs.map +1 -1
  33. package/dist/cli/install/legacy-paths.d.mts.map +1 -1
  34. package/dist/cli/install/legacy-paths.mjs +2 -0
  35. package/dist/cli/install/legacy-paths.mjs.map +1 -1
  36. package/dist/cli/produce/blind-probe.d.mts +85 -0
  37. package/dist/cli/produce/blind-probe.d.mts.map +1 -0
  38. package/dist/cli/produce/blind-probe.mjs +217 -0
  39. package/dist/cli/produce/blind-probe.mjs.map +1 -0
  40. package/dist/cli/produce/claim.d.mts +188 -0
  41. package/dist/cli/produce/claim.d.mts.map +1 -0
  42. package/dist/cli/produce/claim.mjs +518 -0
  43. package/dist/cli/produce/claim.mjs.map +1 -0
  44. package/dist/cli/produce/done-gate.d.mts +87 -0
  45. package/dist/cli/produce/done-gate.d.mts.map +1 -0
  46. package/dist/cli/produce/done-gate.mjs +200 -0
  47. package/dist/cli/produce/done-gate.mjs.map +1 -0
  48. package/dist/cli/produce/events.d.mts +77 -0
  49. package/dist/cli/produce/events.d.mts.map +1 -0
  50. package/dist/cli/produce/events.mjs +126 -0
  51. package/dist/cli/produce/events.mjs.map +1 -0
  52. package/dist/cli/produce/evidence-oracle.d.mts +63 -0
  53. package/dist/cli/produce/evidence-oracle.d.mts.map +1 -0
  54. package/dist/cli/produce/evidence-oracle.mjs +122 -0
  55. package/dist/cli/produce/evidence-oracle.mjs.map +1 -0
  56. package/dist/cli/produce/ledger.d.mts +140 -0
  57. package/dist/cli/produce/ledger.d.mts.map +1 -0
  58. package/dist/cli/produce/ledger.mjs +272 -0
  59. package/dist/cli/produce/ledger.mjs.map +1 -0
  60. package/dist/cli/produce/probe-family.d.mts +53 -0
  61. package/dist/cli/produce/probe-family.d.mts.map +1 -0
  62. package/dist/cli/produce/probe-family.mjs +160 -0
  63. package/dist/cli/produce/probe-family.mjs.map +1 -0
  64. package/dist/cli/produce/projection.d.mts +55 -0
  65. package/dist/cli/produce/projection.d.mts.map +1 -0
  66. package/dist/cli/produce/projection.mjs +97 -0
  67. package/dist/cli/produce/projection.mjs.map +1 -0
  68. package/dist/cli/produce/run-id.d.mts +42 -0
  69. package/dist/cli/produce/run-id.d.mts.map +1 -0
  70. package/dist/cli/produce/run-id.mjs +79 -0
  71. package/dist/cli/produce/run-id.mjs.map +1 -0
  72. package/dist/cli/produce/saga.d.mts +180 -0
  73. package/dist/cli/produce/saga.d.mts.map +1 -0
  74. package/dist/cli/produce/saga.mjs +290 -0
  75. package/dist/cli/produce/saga.mjs.map +1 -0
  76. package/dist/cli/produce/scheduler.d.mts +165 -0
  77. package/dist/cli/produce/scheduler.d.mts.map +1 -0
  78. package/dist/cli/produce/scheduler.mjs +399 -0
  79. package/dist/cli/produce/scheduler.mjs.map +1 -0
  80. package/dist/cli/produce/setpoint.d.mts +52 -0
  81. package/dist/cli/produce/setpoint.d.mts.map +1 -0
  82. package/dist/cli/produce/setpoint.mjs +113 -0
  83. package/dist/cli/produce/setpoint.mjs.map +1 -0
  84. package/dist/cli/produce/verification-ttl.d.mts +75 -0
  85. package/dist/cli/produce/verification-ttl.d.mts.map +1 -0
  86. package/dist/cli/produce/verification-ttl.mjs +169 -0
  87. package/dist/cli/produce/verification-ttl.mjs.map +1 -0
  88. package/dist/cli/roadmap/{conductor-view.d.mts → showrunner-view.d.mts} +3 -3
  89. package/dist/cli/roadmap/showrunner-view.d.mts.map +1 -0
  90. package/dist/cli/roadmap/{conductor-view.mjs → showrunner-view.mjs} +7 -7
  91. package/dist/cli/roadmap/showrunner-view.mjs.map +1 -0
  92. package/dist/plugin/.claude-plugin/plugin.json +2 -1
  93. package/dist/plugin/skills/apt/SKILL.md +112 -38
  94. package/dist/plugin/skills/apt-debug/SKILL.md +14 -24
  95. package/dist/plugin/skills/apt-fan-out/SKILL.md +4 -4
  96. package/dist/plugin/skills/apt-handoff/SKILL.md +1 -1
  97. package/dist/plugin/skills/apt-plan/SKILL.md +5 -5
  98. package/dist/plugin/skills/apt-plan/adapters/{conductor.md → showrunner.md} +16 -16
  99. package/dist/plugin/skills/apt-produce/SKILL.md +606 -0
  100. package/dist/plugin/skills/apt-quick/SKILL.md +14 -22
  101. package/dist/plugin/skills/apt-run/SKILL.md +126 -3
  102. package/dist/plugin/skills/apt-ship/SKILL.md +2 -0
  103. package/dist/plugin/skills/apt-spar/SKILL.md +5 -3
  104. package/dist/plugin/skills/apt-watch-ci/SKILL.md +4 -4
  105. package/package.json +4 -4
  106. package/prompts/{conductor-framework-context.md → showrunner-framework-context.md} +1 -1
  107. package/prompts/{conductor-status-check.md → showrunner-status-check.md} +1 -1
  108. package/prompts/{conductor-sub-agent.md → showrunner-sub-agent.md} +6 -6
  109. package/prompts/{conductor-system.md → showrunner-system.md} +8 -8
  110. package/skills/apt/SKILL.md +112 -38
  111. package/skills/apt-debug/SKILL.md +14 -24
  112. package/skills/apt-fan-out/SKILL.md +4 -4
  113. package/skills/apt-handoff/SKILL.md +1 -1
  114. package/skills/apt-plan/SKILL.md +5 -5
  115. package/skills/apt-plan/adapters/{conductor.md → showrunner.md} +16 -16
  116. package/skills/apt-produce/SKILL.md +606 -0
  117. package/skills/apt-quick/SKILL.md +14 -22
  118. package/skills/apt-run/SKILL.md +126 -3
  119. package/skills/apt-ship/SKILL.md +2 -0
  120. package/skills/apt-spar/SKILL.md +5 -3
  121. package/skills/apt-watch-ci/SKILL.md +4 -4
  122. package/src/cli/commands/ci-watch.mjs +51 -2
  123. package/src/cli/commands/commit.mjs +3 -3
  124. package/src/cli/commands/event.mjs +16 -16
  125. package/src/cli/commands/merge-integrate.mjs +3 -3
  126. package/src/cli/commands/produce.mjs +1466 -0
  127. package/src/cli/commands/task.mjs +482 -285
  128. package/src/cli/coordination/auto-emit-artifact-ready.mjs +5 -5
  129. package/src/cli/coordination/event-schema.d.ts +4 -2
  130. package/src/cli/coordination/event-schema.mjs +276 -21
  131. package/src/cli/coordination/store.mjs +4 -4
  132. package/src/cli/dispatch.mjs +2 -0
  133. package/src/cli/help.mjs +30 -0
  134. package/src/cli/install/legacy-paths.mjs +2 -0
  135. package/src/cli/produce/blind-probe.mjs +245 -0
  136. package/src/cli/produce/claim.mjs +543 -0
  137. package/src/cli/produce/done-gate.mjs +238 -0
  138. package/src/cli/produce/events.mjs +131 -0
  139. package/src/cli/produce/evidence-oracle.mjs +133 -0
  140. package/src/cli/produce/ledger.mjs +284 -0
  141. package/src/cli/produce/probe-family.mjs +168 -0
  142. package/src/cli/produce/projection.mjs +105 -0
  143. package/src/cli/produce/run-id.mjs +84 -0
  144. package/src/cli/produce/saga.mjs +303 -0
  145. package/src/cli/produce/scheduler.mjs +423 -0
  146. package/src/cli/produce/setpoint.mjs +122 -0
  147. package/src/cli/produce/verification-ttl.mjs +191 -0
  148. package/src/cli/roadmap/showrunner-view.d.ts +10 -0
  149. package/src/cli/roadmap/{conductor-view.mjs → showrunner-view.mjs} +6 -6
  150. package/dist/cli/roadmap/conductor-view.d.mts.map +0 -1
  151. package/dist/cli/roadmap/conductor-view.mjs.map +0 -1
  152. package/src/cli/roadmap/conductor-view.d.ts +0 -10
@@ -0,0 +1,1345 @@
1
+ /**
2
+ * commands/produce.mjs — `/apt:produce` M1 CLI substrate.
3
+ *
4
+ * Subcommands (mirrors commands/roadmap.mjs's (subcommand, projectDir, extraArgs)
5
+ * shape): start | add-milestone | set-milestone-status | status | tick.
6
+ *
7
+ * M1 is the outer-loop SKELETON: it freezes an immutable acceptance setpoint,
8
+ * seeds + projects the roadmap crash-safely from the append-only event log, and
9
+ * advances one wake tick per invocation. It does NOT adjudicate "done" — that
10
+ * is external-oracle-gated and deferred to M2 (ID-03).
11
+ */
12
+ import { existsSync, readFileSync } from 'node:fs';
13
+ import { createCoordinationStore } from '../coordination/store.mjs';
14
+ import { freezeProbeSet, recordProbeRun, selectSweepSample } from '../produce/blind-probe.mjs';
15
+ import { consumeClaims, freezeClaim, projectClaims } from '../produce/claim.mjs';
16
+ import { adjudicateDone, computeCanary } from '../produce/done-gate.mjs';
17
+ import { appendProduceEvent, readImplementerFamily, readProduceRunEventsMatching, } from '../produce/events.mjs';
18
+ import { prepareOracleInput, recordOracleVerdict } from '../produce/evidence-oracle.mjs';
19
+ import { closeCriteriaForShippedMilestone, computeOpenCriteria, computeProgress, evaluateDeclareGuard, mintCriterionId, } from '../produce/ledger.mjs';
20
+ import { resolveProbeFamily } from '../produce/probe-family.mjs';
21
+ import { projectRoadmap, writeProjectionCache } from '../produce/projection.mjs';
22
+ import { mintRunId, relativeSetpointPath, resolveRunDir, slugFromOutcome, } from '../produce/run-id.mjs';
23
+ import { compensatePhase, completePhase, enterPhase, projectSaga, recordReview, resolveReviewer, SAGA_PHASES, } from '../produce/saga.mjs';
24
+ import { computeSchedule, resolveWipLimit } from '../produce/scheduler.mjs';
25
+ import { freezeSetpoint, readSetpoint } from '../produce/setpoint.mjs';
26
+ import { listRegressions, recordTtlReverify, selectTtlReverifySample, } from '../produce/verification-ttl.mjs';
27
+ import { parseFlags } from '../util/args.mjs';
28
+ import { err, ok } from '../util/result.mjs';
29
+ /**
30
+ * Top-level dispatcher.
31
+ *
32
+ * @param {string} subcommand
33
+ * @param {string} projectDir
34
+ * @param {string[]} extraArgs
35
+ */
36
+ export async function cmdProduce(subcommand, projectDir, extraArgs) {
37
+ if (!subcommand)
38
+ return err('Usage: apt-tools produce <start|add-milestone|set-milestone-status|status|tick|declare-criteria|progress|record-implementer|resolve-probe-family|oracle|probe|canary|saga|ttl|schedule|claim> <project-dir>');
39
+ if (!projectDir)
40
+ return err(`Usage: apt-tools produce ${subcommand} <project-dir>`);
41
+ const flags = parseFlags(extraArgs);
42
+ switch (subcommand) {
43
+ case 'start':
44
+ return cmdStart(projectDir, flags);
45
+ case 'add-milestone':
46
+ return cmdAddMilestone(projectDir, flags);
47
+ case 'set-milestone-status':
48
+ return cmdSetMilestoneStatus(projectDir, flags);
49
+ case 'status':
50
+ return cmdStatus(projectDir, flags);
51
+ case 'tick':
52
+ return cmdTick(projectDir, flags);
53
+ // M3 — the Lyapunov acceptance-criteria ledger: declare an open criterion
54
+ // behind the write-time anti-windup guard (ID-M3-03 / ID-M3-04), and the
55
+ // read-time monotone-progress report (ID-M3-02 — flags, never rejects).
56
+ case 'declare-criteria':
57
+ return cmdDeclareCriteria(projectDir, flags);
58
+ case 'progress':
59
+ return cmdProgress(projectDir, flags);
60
+ // M2 — the cross-family disjointness anchor (ID-M2-02) + resolver (ST2).
61
+ case 'record-implementer':
62
+ return cmdRecordImplementer(projectDir, flags);
63
+ case 'resolve-probe-family':
64
+ return cmdResolveProbeFamily(projectDir, flags);
65
+ // M2 — two-level subcommands: `produce oracle <prepare|record>` (ST3) +
66
+ // `produce probe <freeze|record|sweep>` (ST4). `projectDir` here holds the
67
+ // sub-subcommand; the real dir is extraArgs[0].
68
+ case 'oracle':
69
+ return cmdOracle(projectDir, extraArgs);
70
+ case 'probe':
71
+ return cmdProbe(projectDir, extraArgs);
72
+ // M2 — the reward-hacking canary (ID-M2-06 / ST5).
73
+ case 'canary':
74
+ return cmdCanary(projectDir, flags);
75
+ // M4 — the saga phase model + FORWARD compensators + provider-diverse
76
+ // review, a two-level subcommand (same shape as oracle/probe: the
77
+ // sub-subcommand arrives as `projectDir`, the real dir is extraArgs[0]).
78
+ case 'saga':
79
+ return cmdSaga(projectDir, extraArgs);
80
+ // M4 — the verification-TTL: a rotating re-verify of OLD done milestones
81
+ // against HEAD (`ttl select`) + the cross-family re-verify RUN that flags +
82
+ // compensates regressions (`ttl reverify`). Two-level (same shape as saga).
83
+ case 'ttl':
84
+ return cmdTtl(projectDir, extraArgs);
85
+ // M5 — the WSJF/WIP scheduler: a read-only ranking of the ready milestones
86
+ // + a WIP-limited pick (dependency-aware, regression-aware). Single-level
87
+ // (mirrors cmdStatus/cmdCanary read-only shape); emits nothing (ID-M5-06).
88
+ case 'schedule':
89
+ return cmdSchedule(projectDir, flags);
90
+ // M6 — the Claim Envelope lifecycle, a two-level subcommand (same shape as
91
+ // oracle/probe/saga/ttl: the sub-subcommand arrives as `projectDir`, the
92
+ // real dir is extraArgs[0]).
93
+ case 'claim':
94
+ return cmdClaim(projectDir, extraArgs);
95
+ default:
96
+ return err(`Unknown produce subcommand: ${subcommand}`);
97
+ }
98
+ }
99
+ /** Require a frozen run; returns { setpoint } or an err() envelope. */
100
+ function requireRun(projectDir, runId, cmd) {
101
+ if (!runId)
102
+ return { error: err(`produce ${cmd} requires --run-id <id>`) };
103
+ const setpoint = readSetpoint(resolveRunDir(projectDir, runId));
104
+ if (!setpoint)
105
+ return {
106
+ error: err(`produce ${cmd}: no frozen setpoint for run ${runId} — run produce start first`),
107
+ };
108
+ return { setpoint };
109
+ }
110
+ /** Resolve a string flag or null. */
111
+ function strFlag(flags, name) {
112
+ const v = flags.get(name);
113
+ return typeof v === 'string' ? v : null;
114
+ }
115
+ /**
116
+ * `produce start . --outcome "<o>" [--run-id <id>]` — freeze the acceptance
117
+ * setpoint and, on a fresh freeze, emit produce.run.started +
118
+ * produce.setpoint.frozen. Re-running with the same --run-id is idempotent
119
+ * reuse (never overwrites — ID-02).
120
+ */
121
+ function cmdStart(projectDir, flags) {
122
+ const outcome = strFlag(flags, 'outcome');
123
+ const explicitRunId = strFlag(flags, 'run-id');
124
+ let runId = explicitRunId;
125
+ if (!runId) {
126
+ if (!outcome)
127
+ return err('produce start requires --outcome "<text>" (or --run-id <id> to reuse an existing run)');
128
+ runId = mintRunId(outcome);
129
+ }
130
+ const runDir = resolveRunDir(projectDir, runId);
131
+ const existing = readSetpoint(runDir);
132
+ if (!existing && !outcome)
133
+ return err(`produce start --run-id ${runId} has no frozen setpoint yet; pass --outcome "<text>" to freeze it`);
134
+ const outcomeForFreeze = existing ? existing.outcome : outcome;
135
+ const { reused, setpoint } = freezeSetpoint(runDir, { run_id: runId, outcome: outcomeForFreeze });
136
+ const setpointPath = relativeSetpointPath(runId);
137
+ // Emit the run-start + setpoint-frozen events only on a FRESH freeze;
138
+ // idempotent reuse never re-logs (ID-02).
139
+ if (!reused) {
140
+ const store = createCoordinationStore(projectDir);
141
+ store.appendEvent({
142
+ op: 'produce.run.started',
143
+ task: runId,
144
+ data: { run_id: runId, setpoint_hash: setpoint.setpoint_hash },
145
+ });
146
+ store.appendEvent({
147
+ op: 'produce.setpoint.frozen',
148
+ task: runId,
149
+ data: {
150
+ run_id: runId,
151
+ setpoint_hash: setpoint.setpoint_hash,
152
+ setpoint_path: setpointPath,
153
+ },
154
+ });
155
+ }
156
+ return ok({
157
+ status: 'ok',
158
+ command: 'produce-start',
159
+ reused,
160
+ run_id: runId,
161
+ outcome: setpoint.outcome,
162
+ frozen_at: setpoint.frozen_at,
163
+ setpoint_path: setpointPath,
164
+ setpoint_hash: setpoint.setpoint_hash,
165
+ });
166
+ }
167
+ /**
168
+ * `produce add-milestone . --run-id <id> --title "<t>" [--id <mid>]
169
+ * [--depends-on a,b,c] [--priority <int>] [--size <positive int>]` — append
170
+ * produce.roadmap.milestone_added, then refresh the derived roadmap.json cache.
171
+ * The milestone id is minted with a random suffix when not supplied so N
172
+ * concurrent auto-id writers never collide (parent TD-06 / subtask 4).
173
+ *
174
+ * M5 (ID-M5-03): --depends-on / --priority / --size ride as ADDITIVE OPTIONAL
175
+ * data fields on the EXISTING event (no schema change — run_id/milestone_id/title
176
+ * stay the only required fields). An M1-style add (no new flags) is
177
+ * byte-identical: the event carries none of the extra fields.
178
+ */
179
+ function cmdAddMilestone(projectDir, flags) {
180
+ const runId = strFlag(flags, 'run-id');
181
+ const title = strFlag(flags, 'title');
182
+ const gate = requireRun(projectDir, runId, 'add-milestone');
183
+ if (gate.error)
184
+ return gate.error;
185
+ if (!title)
186
+ return err('produce add-milestone requires --title "<text>"');
187
+ const milestoneId = strFlag(flags, 'id') || `${slugFromOutcome(title)}-${Math.random().toString(36).slice(2, 8)}`;
188
+ const data = { run_id: runId, milestone_id: milestoneId, title };
189
+ // Additive scheduling meta (M5) — only stamped when the flag is present, so an
190
+ // M1-style add stays byte-identical (ID-M5-03).
191
+ const dependsOnRaw = strFlag(flags, 'depends-on');
192
+ if (dependsOnRaw) {
193
+ const deps = dependsOnRaw
194
+ .split(',')
195
+ .map((s) => s.trim())
196
+ .filter(Boolean);
197
+ if (deps.length > 0)
198
+ data.depends_on = deps;
199
+ }
200
+ const priorityRaw = strFlag(flags, 'priority');
201
+ if (priorityRaw !== null) {
202
+ const p = Number.parseInt(priorityRaw, 10);
203
+ if (Number.isFinite(p))
204
+ data.priority = p;
205
+ }
206
+ const sizeRaw = strFlag(flags, 'size');
207
+ if (sizeRaw !== null) {
208
+ const s = Number.parseInt(sizeRaw, 10);
209
+ if (Number.isFinite(s) && s > 0)
210
+ data.size = s;
211
+ }
212
+ appendProduceEvent(projectDir, 'produce.roadmap.milestone_added', data);
213
+ // The event log is the authority; the cache is a derived convenience,
214
+ // folded + persisted under a single-writer lock (never lost-update).
215
+ const roadmap = writeProjectionCache(projectDir, runId);
216
+ return ok({
217
+ status: 'ok',
218
+ command: 'produce-add-milestone',
219
+ run_id: runId,
220
+ milestone_id: milestoneId,
221
+ title,
222
+ roadmap,
223
+ });
224
+ }
225
+ /**
226
+ * `produce set-milestone-status . --run-id <id> --milestone <mid> --status <s>`
227
+ * — append produce.roadmap.milestone_status_changed. `--status done` is the M2
228
+ * two-gate adjudication (ID-M2-04) — accepted ONLY when the visible oracle AND
229
+ * the blind probe both agree; a bare done with no recorded external evidence is
230
+ * STILL refused (the Showrunner never self-attests, ID-03). `planned` /
231
+ * `in_progress` are byte-identical to M1.
232
+ */
233
+ function cmdSetMilestoneStatus(projectDir, flags) {
234
+ const runId = strFlag(flags, 'run-id');
235
+ const milestoneId = strFlag(flags, 'milestone');
236
+ const status = strFlag(flags, 'status');
237
+ const gate = requireRun(projectDir, runId, 'set-milestone-status');
238
+ if (gate.error)
239
+ return gate.error;
240
+ if (!milestoneId)
241
+ return err('produce set-milestone-status requires --milestone <mid>');
242
+ if (!status)
243
+ return err('produce set-milestone-status requires --status <planned|in_progress|done>');
244
+ // M2 — `done` is the two-gate adjudication over recorded external evidence,
245
+ // replacing M1's blanket reject (ID-M2-04). See cmdSetMilestoneDone.
246
+ if (status === 'done')
247
+ return cmdSetMilestoneDone(projectDir, runId, milestoneId, flags);
248
+ if (status !== 'planned' && status !== 'in_progress')
249
+ return err(`produce set-milestone-status: invalid --status "${status}". Allowed: planned | in_progress | done.`);
250
+ const before = projectRoadmap(projectDir, runId);
251
+ if (!before.milestones.some((m) => m.id === milestoneId))
252
+ return err(`produce set-milestone-status: milestone ${milestoneId} not found in run ${runId}`);
253
+ appendProduceEvent(projectDir, 'produce.roadmap.milestone_status_changed', {
254
+ run_id: runId,
255
+ milestone_id: milestoneId,
256
+ status,
257
+ });
258
+ const roadmap = writeProjectionCache(projectDir, runId);
259
+ return ok({
260
+ status: 'ok',
261
+ command: 'produce-set-milestone-status',
262
+ run_id: runId,
263
+ milestone_id: milestoneId,
264
+ milestone_status: status,
265
+ roadmap,
266
+ });
267
+ }
268
+ /**
269
+ * `produce status . --run-id <id>` — read-only re-projection of the roadmap
270
+ * from the append-only event log (no in-memory authority — ID-01 / AC2). Does
271
+ * NOT write the cache (pure read).
272
+ */
273
+ function cmdStatus(projectDir, flags) {
274
+ const runId = strFlag(flags, 'run-id');
275
+ const gate = requireRun(projectDir, runId, 'status');
276
+ if (gate.error)
277
+ return gate.error;
278
+ const roadmap = projectRoadmap(projectDir, runId);
279
+ return ok({
280
+ status: 'ok',
281
+ command: 'produce-status',
282
+ run_id: runId,
283
+ setpoint_hash: gate.setpoint.setpoint_hash,
284
+ roadmap,
285
+ });
286
+ }
287
+ /**
288
+ * `produce tick . --run-id <id> [--wip N]` — run EXACTLY ONE wake tick: re-project
289
+ * the roadmap from disk (ID-01), compute the M5 WSJF/WIP schedule, emit
290
+ * produce.tick + ONE produce.schedule.computed, set next_action to the scheduler's
291
+ * top pick, and return a reschedule hint, then EXIT. There is no polling loop and
292
+ * no self-scheduling timer — the recurring beat is delivered by the host (ID-05).
293
+ *
294
+ * next_action is scheduler-driven but SHAPE-PRESERVING (ID-M5-08): an in_progress
295
+ * milestone → continue_milestone (unconditional — always continue in-flight work,
296
+ * regardless of WIP); else the top WSJF ready pick under the WIP budget →
297
+ * start_milestone; else idle (carrying the schedule's reason). It is advisory;
298
+ * the Showrunner never self-attests a milestone done (external-oracle-gated, M2 —
299
+ * ID-03).
300
+ */
301
+ function cmdTick(projectDir, flags) {
302
+ const runId = strFlag(flags, 'run-id');
303
+ const gate = requireRun(projectDir, runId, 'tick');
304
+ if (gate.error)
305
+ return gate.error;
306
+ // Re-read the roadmap from the append-only log — no in-memory authority.
307
+ const roadmap = projectRoadmap(projectDir, runId);
308
+ // M5 — fold the WSJF/WIP schedule (dependency-aware, regression-aware) under
309
+ // the resolved WIP budget. A pure read; the tick is the ONLY writer below.
310
+ const wipLimit = resolveWipLimit(projectDir, flags);
311
+ const schedule = computeSchedule(projectDir, runId, { wipLimit });
312
+ appendProduceEvent(projectDir, 'produce.tick', { run_id: runId });
313
+ // One recorded schedule fact per tick (ID-M5-06) — content-blind (picked ids +
314
+ // wip_limit only; the roadmap projection never folds produce.schedule.*).
315
+ appendProduceEvent(projectDir, 'produce.schedule.computed', {
316
+ run_id: runId,
317
+ picked: schedule.pick.map((p) => p.milestone_id).join(','),
318
+ wip_limit: String(wipLimit),
319
+ });
320
+ // Scheduler-driven next_action, shape-preserving (ID-M5-08). Continue an
321
+ // in-flight milestone unconditionally; else start the top WSJF ready pick under
322
+ // the WIP budget; else idle with the schedule's reason.
323
+ const inProgress = roadmap.milestones.find((m) => m.status === 'in_progress');
324
+ let nextAction;
325
+ if (inProgress) {
326
+ nextAction = {
327
+ kind: 'continue_milestone',
328
+ milestone_id: inProgress.id,
329
+ title: inProgress.title,
330
+ status: inProgress.status,
331
+ };
332
+ }
333
+ else if (schedule.pick.length > 0) {
334
+ const top = schedule.pick[0];
335
+ nextAction = {
336
+ kind: 'start_milestone',
337
+ milestone_id: top.milestone_id,
338
+ title: top.title,
339
+ status: top.status,
340
+ };
341
+ }
342
+ else {
343
+ nextAction = { kind: 'idle', reason: schedule.reason };
344
+ }
345
+ const envelope = {
346
+ status: 'ok',
347
+ command: 'produce-tick',
348
+ run_id: runId,
349
+ roadmap,
350
+ next_action: nextAction,
351
+ // M5 (ID-M5-08) — the full WSJF/WIP schedule object, an ADDITIVE field.
352
+ schedule,
353
+ next_tick_hint: 'One tick per invocation — reschedule via the host beat (cron-capable host → CronCreate; ' +
354
+ `non-cron host → re-run \`apt-tools produce tick . --run-id ${runId}\` when you return). No in-skill loop (ID-05).`,
355
+ };
356
+ // M4 (ID-M4-05) — an OPTIONAL --head-sha surfaces the additive per-tick TTL
357
+ // re-verify sample (the rotating STALE done milestones to re-verify this beat,
358
+ // AC13 "each tick"). When ABSENT the tick output is BYTE-IDENTICAL to M1/M2/M3
359
+ // (no field) and next_action is unchanged. Still exactly one tick, no loop.
360
+ const headSha = strFlag(flags, 'head-sha');
361
+ if (headSha) {
362
+ const sampleRaw = strFlag(flags, 'sample');
363
+ const sample = sampleRaw ? Number.parseInt(sampleRaw, 10) : 1;
364
+ envelope.ttl_reverify = selectTtlReverifySample(projectDir, runId, { headSha, sample });
365
+ }
366
+ return ok(envelope);
367
+ }
368
+ /**
369
+ * `produce declare-criteria . --run-id <id> --milestone <mid> --criterion "<text>"
370
+ * [--criterion-id <cid>] [--scope-change "<reason>"]` — open one acceptance
371
+ * criterion bound to a milestone (V += 1), gated by the write-time Lyapunov
372
+ * anti-windup guard (ID-M3-03). Before descent, declaration is FREE (the baseline
373
+ * scope-definition window). After descent (≥1 criterion already closed), it is
374
+ * REJECTED fail-closed (exit 1, nothing appended) unless --scope-change
375
+ * "<reason>" justifies the increase — the ONLY sanctioned post-descent V rise
376
+ * (ID-M3-04). When --scope-change is present, the command appends
377
+ * produce.scope.changed FIRST, then produce.criteria.declared — atomic
378
+ * self-justifying growth on the trail.
379
+ */
380
+ function cmdDeclareCriteria(projectDir, flags) {
381
+ const runId = strFlag(flags, 'run-id');
382
+ const milestoneId = strFlag(flags, 'milestone');
383
+ const criterion = strFlag(flags, 'criterion');
384
+ const gate = requireRun(projectDir, runId, 'declare-criteria');
385
+ if (gate.error)
386
+ return gate.error;
387
+ if (!milestoneId)
388
+ return err('produce declare-criteria requires --milestone <mid>');
389
+ if (!criterion)
390
+ return err('produce declare-criteria requires --criterion "<text>"');
391
+ // The milestone must exist in the run's roadmap (fail-closed).
392
+ const roadmap = projectRoadmap(projectDir, runId);
393
+ if (!roadmap.milestones.some((m) => m.id === milestoneId))
394
+ return err(`produce declare-criteria: milestone ${milestoneId} not found in run ${runId}`);
395
+ const scopeChangeReason = strFlag(flags, 'scope-change');
396
+ const criterionId = strFlag(flags, 'criterion-id') || mintCriterionId(criterion);
397
+ // Write-time Lyapunov anti-windup guard — the ONLY hard enforcement (ID-M3-03).
398
+ const guard = evaluateDeclareGuard(projectDir, runId, { hasScopeChange: !!scopeChangeReason });
399
+ if (!guard.allowed)
400
+ return err('Lyapunov violation: adding an open acceptance criterion after the run began closing criteria requires --scope-change "<reason>" (ID-M3-03/ID-M3-04)', 1, {
401
+ command: 'produce-declare-criteria',
402
+ run_id: runId,
403
+ milestone_id: milestoneId,
404
+ in_descent: guard.in_descent,
405
+ });
406
+ // Self-justifying growth: the scope-change lands FIRST, then the criterion
407
+ // (atomic — every post-descent increase carries its own logged justification).
408
+ if (scopeChangeReason)
409
+ appendProduceEvent(projectDir, 'produce.scope.changed', {
410
+ run_id: runId,
411
+ milestone_id: milestoneId,
412
+ reason: scopeChangeReason,
413
+ });
414
+ appendProduceEvent(projectDir, 'produce.criteria.declared', {
415
+ run_id: runId,
416
+ milestone_id: milestoneId,
417
+ criterion_id: criterionId,
418
+ text: criterion,
419
+ });
420
+ return ok({
421
+ status: 'ok',
422
+ command: 'produce-declare-criteria',
423
+ run_id: runId,
424
+ milestone_id: milestoneId,
425
+ criterion_id: criterionId,
426
+ open_count: computeOpenCriteria(projectDir, runId),
427
+ scope_changed: !!scopeChangeReason,
428
+ });
429
+ }
430
+ /**
431
+ * Read the LATEST recorded implementer family for a run across ALL milestones
432
+ * (used by `resolve-probe-family` when neither --implementer-family nor
433
+ * --milestone is supplied). Returns null when none is recorded (fail-closed).
434
+ *
435
+ * @param {string} projectDir
436
+ * @param {string} runId
437
+ * @returns {string | null}
438
+ */
439
+ function latestImplementerFamilyForRun(projectDir, runId) {
440
+ const events = readProduceRunEventsMatching(projectDir, runId, (op) => op === 'produce.milestone.implemented');
441
+ let family = null;
442
+ for (const e of events) {
443
+ const d = e.data || {};
444
+ if (typeof d.implementer_family === 'string' && d.implementer_family.length > 0) {
445
+ family = d.implementer_family;
446
+ }
447
+ }
448
+ return family;
449
+ }
450
+ /**
451
+ * `produce record-implementer . --run-id <id> --milestone <mid>
452
+ * --implementer-family <fam>` — record who implemented a milestone (ID-M2-02).
453
+ * This is the recorded disjointness anchor every cross-family check reads. The
454
+ * Showrunner records it once per milestone at implementation time; the CLI
455
+ * never dispatches execute in M2 (that is the post-M2 closed loop).
456
+ */
457
+ function cmdRecordImplementer(projectDir, flags) {
458
+ const runId = strFlag(flags, 'run-id');
459
+ const milestoneId = strFlag(flags, 'milestone');
460
+ const implementerFamily = strFlag(flags, 'implementer-family');
461
+ const gate = requireRun(projectDir, runId, 'record-implementer');
462
+ if (gate.error)
463
+ return gate.error;
464
+ if (!milestoneId)
465
+ return err('produce record-implementer requires --milestone <mid>');
466
+ if (!implementerFamily)
467
+ return err('produce record-implementer requires --implementer-family <fam>');
468
+ appendProduceEvent(projectDir, 'produce.milestone.implemented', {
469
+ run_id: runId,
470
+ milestone_id: milestoneId,
471
+ implementer_family: implementerFamily,
472
+ });
473
+ return ok({
474
+ status: 'ok',
475
+ command: 'produce-record-implementer',
476
+ run_id: runId,
477
+ milestone_id: milestoneId,
478
+ implementer_family: implementerFamily,
479
+ });
480
+ }
481
+ /**
482
+ * `produce resolve-probe-family . --run-id <id> [--implementer-family <fam>]
483
+ * [--milestone <mid>] [--host-family <fam>] [--available-families a,b,c]` —
484
+ * resolve a probe/oracle family DISJOINT from the implementer, with the model
485
+ * resolved LIVE via the reused spar primitive (no baked model id — ID-M2-07).
486
+ *
487
+ * The implementer family is taken from --implementer-family, else read from the
488
+ * recorded `produce.milestone.implemented` trail (by --milestone, else the
489
+ * latest for the run). If neither is present, adjudication is impossible — you
490
+ * cannot enforce disjointness against an unknown implementer (ID-M2-02,
491
+ * fail-closed).
492
+ */
493
+ function cmdResolveProbeFamily(projectDir, flags) {
494
+ const runId = strFlag(flags, 'run-id');
495
+ const gate = requireRun(projectDir, runId, 'resolve-probe-family');
496
+ if (gate.error)
497
+ return gate.error;
498
+ const milestoneId = strFlag(flags, 'milestone');
499
+ let implementerFamily = strFlag(flags, 'implementer-family');
500
+ if (!implementerFamily) {
501
+ implementerFamily = milestoneId
502
+ ? readImplementerFamily(projectDir, runId, milestoneId)
503
+ : latestImplementerFamilyForRun(projectDir, runId);
504
+ }
505
+ if (!implementerFamily)
506
+ return err('produce resolve-probe-family: no implementer family — pass --implementer-family <fam> or record it first via `produce record-implementer` (ID-M2-02, fail-closed).', 1, { command: 'produce-resolve-probe-family', run_id: runId });
507
+ const hostFamily = strFlag(flags, 'host-family');
508
+ const availRaw = strFlag(flags, 'available-families');
509
+ const availableFamilies = availRaw
510
+ ? availRaw
511
+ .split(',')
512
+ .map((s) => s.trim())
513
+ .filter(Boolean)
514
+ : undefined;
515
+ const resolution = resolveProbeFamily({
516
+ implementerFamily,
517
+ hostFamily,
518
+ availableFamilies,
519
+ });
520
+ return ok({
521
+ status: 'ok',
522
+ command: 'produce-resolve-probe-family',
523
+ run_id: runId,
524
+ milestone_id: milestoneId ?? null,
525
+ implementer_family: implementerFamily,
526
+ host_family: hostFamily ?? null,
527
+ ...resolution,
528
+ });
529
+ }
530
+ /**
531
+ * `produce oracle <prepare|record> <project-dir> ...` — two-level dispatch. The
532
+ * sub-subcommand arrives as the outer `projectDir` param (see cmdProduce); the
533
+ * real project dir is `args[0]` and the flags follow it.
534
+ *
535
+ * @param {string} subsub
536
+ * @param {string[]} args
537
+ */
538
+ function cmdOracle(subsub, args) {
539
+ const realProjectDir = Array.isArray(args) ? args[0] : undefined;
540
+ if (!realProjectDir || realProjectDir.startsWith('--'))
541
+ return err('Usage: apt-tools produce oracle <prepare|record> <project-dir> [flags]');
542
+ const flags = parseFlags(args.slice(1));
543
+ switch (subsub) {
544
+ case 'prepare':
545
+ return cmdOraclePrepare(realProjectDir, flags);
546
+ case 'record':
547
+ return cmdOracleRecord(realProjectDir, flags);
548
+ default:
549
+ return err(`Unknown produce oracle subcommand: ${subsub}. Expected prepare | record.`);
550
+ }
551
+ }
552
+ /**
553
+ * `produce oracle prepare . --run-id <id> --milestone <mid> --verdict-path <path>`
554
+ * — extract verify-proof's v2 verdict + tests[] evidence into a cross-family
555
+ * judge payload. Rejects (exit 1) when the verdict is not EXACTLY `approved`
556
+ * (ID-M2-05) so the skill never judges evidence a failed/unreadable gate never
557
+ * produced. The skill drives the actual cross-family judge LLM call; this only
558
+ * extracts.
559
+ */
560
+ function cmdOraclePrepare(projectDir, flags) {
561
+ const runId = strFlag(flags, 'run-id');
562
+ const milestoneId = strFlag(flags, 'milestone');
563
+ const verdictPath = strFlag(flags, 'verdict-path');
564
+ const gate = requireRun(projectDir, runId, 'oracle prepare');
565
+ if (gate.error)
566
+ return gate.error;
567
+ if (!milestoneId)
568
+ return err('produce oracle prepare requires --milestone <mid>');
569
+ if (!verdictPath)
570
+ return err('produce oracle prepare requires --verdict-path <path>');
571
+ const prepared = prepareOracleInput(verdictPath);
572
+ if (!prepared.approved)
573
+ return err(`produce oracle prepare: verify-proof verdict is not 'approved' (got: ${prepared.verdict ?? 'missing/unreadable'}) — not adjudicable (ID-M2-05, fail-closed).`, 1, {
574
+ command: 'produce-oracle-prepare',
575
+ run_id: runId,
576
+ milestone_id: milestoneId,
577
+ verdict: prepared.verdict,
578
+ reason: prepared.reason,
579
+ });
580
+ return ok({
581
+ status: 'ok',
582
+ command: 'produce-oracle-prepare',
583
+ run_id: runId,
584
+ milestone_id: milestoneId,
585
+ verdict: prepared.verdict,
586
+ tests: prepared.tests,
587
+ judge_input: prepared.judge_input,
588
+ });
589
+ }
590
+ /**
591
+ * `produce oracle record . --run-id <id> --milestone <mid> --verdict <pass|fail>
592
+ * --oracle-family <fam>` — record the cross-family evidence-quality judge's
593
+ * verdict. Refused fail-closed when the oracle family equals the recorded
594
+ * implementer family, or when no implementer family is recorded (ID-08 / ID-M2-02).
595
+ */
596
+ function cmdOracleRecord(projectDir, flags) {
597
+ const runId = strFlag(flags, 'run-id');
598
+ const milestoneId = strFlag(flags, 'milestone');
599
+ const verdict = strFlag(flags, 'verdict');
600
+ const oracleFamily = strFlag(flags, 'oracle-family');
601
+ const gate = requireRun(projectDir, runId, 'oracle record');
602
+ if (gate.error)
603
+ return gate.error;
604
+ if (!milestoneId)
605
+ return err('produce oracle record requires --milestone <mid>');
606
+ if (!verdict)
607
+ return err('produce oracle record requires --verdict <pass|fail>');
608
+ if (!oracleFamily)
609
+ return err('produce oracle record requires --oracle-family <fam>');
610
+ const res = recordOracleVerdict(projectDir, { runId, milestoneId, verdict, oracleFamily });
611
+ if (!res.ok)
612
+ return err(`produce oracle record refused: ${res.reason}`, 1, {
613
+ command: 'produce-oracle-record',
614
+ run_id: runId,
615
+ milestone_id: milestoneId,
616
+ reason: res.reason,
617
+ implementer_family: res.implementer_family ?? null,
618
+ });
619
+ return ok({
620
+ status: 'ok',
621
+ command: 'produce-oracle-record',
622
+ run_id: runId,
623
+ milestone_id: milestoneId,
624
+ verdict: res.verdict,
625
+ oracle_family: res.oracle_family,
626
+ implementer_family: res.implementer_family,
627
+ });
628
+ }
629
+ /**
630
+ * `produce probe <freeze|record|sweep> <project-dir> ...` — two-level dispatch
631
+ * (same shape as cmdOracle: the sub-subcommand arrives as `projectDir`).
632
+ *
633
+ * @param {string} subsub
634
+ * @param {string[]} args
635
+ */
636
+ function cmdProbe(subsub, args) {
637
+ const realProjectDir = Array.isArray(args) ? args[0] : undefined;
638
+ if (!realProjectDir || realProjectDir.startsWith('--'))
639
+ return err('Usage: apt-tools produce probe <freeze|record|sweep> <project-dir> [flags]');
640
+ const flags = parseFlags(args.slice(1));
641
+ switch (subsub) {
642
+ case 'freeze':
643
+ return cmdProbeFreeze(realProjectDir, flags);
644
+ case 'record':
645
+ return cmdProbeRecord(realProjectDir, flags);
646
+ case 'sweep':
647
+ return cmdProbeSweep(realProjectDir, flags);
648
+ default:
649
+ return err(`Unknown produce probe subcommand: ${subsub}. Expected freeze | record | sweep.`);
650
+ }
651
+ }
652
+ /**
653
+ * `produce probe freeze . --run-id <id> --milestone <mid> --probe-file <path>
654
+ * --author-family <fam>` — freeze an immutable held-out probe set authored by a
655
+ * family ≠ the implementer. The probe CONTENT is read from --probe-file (the
656
+ * skill wrote it in the disjoint author family) and stored in the frozen file
657
+ * ONLY — never the event log (ID-M2-03). Reads the recorded implementer family
658
+ * to enforce the cross-family rule (fail-closed if absent).
659
+ */
660
+ function cmdProbeFreeze(projectDir, flags) {
661
+ const runId = strFlag(flags, 'run-id');
662
+ const milestoneId = strFlag(flags, 'milestone');
663
+ const probeFile = strFlag(flags, 'probe-file');
664
+ const authorFamily = strFlag(flags, 'author-family');
665
+ const gate = requireRun(projectDir, runId, 'probe freeze');
666
+ if (gate.error)
667
+ return gate.error;
668
+ if (!milestoneId)
669
+ return err('produce probe freeze requires --milestone <mid>');
670
+ if (!probeFile)
671
+ return err('produce probe freeze requires --probe-file <path>');
672
+ if (!authorFamily)
673
+ return err('produce probe freeze requires --author-family <fam>');
674
+ if (!existsSync(probeFile))
675
+ return err(`produce probe freeze: --probe-file not found: ${probeFile}`);
676
+ // Read the probe content the skill authored. JSON when parseable, else raw
677
+ // text — the content is stored verbatim and never enters the event log.
678
+ const raw = readFileSync(probeFile, 'utf-8');
679
+ let content;
680
+ try {
681
+ content = JSON.parse(raw);
682
+ }
683
+ catch {
684
+ content = raw;
685
+ }
686
+ const implementerFamily = readImplementerFamily(projectDir, runId, milestoneId);
687
+ if (!implementerFamily)
688
+ return err('produce probe freeze: no recorded implementer family — record it first via `produce record-implementer` (ID-M2-02, fail-closed).', 1, { command: 'produce-probe-freeze', run_id: runId, milestone_id: milestoneId });
689
+ const res = freezeProbeSet(projectDir, runId, milestoneId, {
690
+ content,
691
+ authorFamily,
692
+ implementerFamily,
693
+ });
694
+ if (!res.ok)
695
+ return err(`produce probe freeze refused: ${res.reason}`, 1, {
696
+ command: 'produce-probe-freeze',
697
+ run_id: runId,
698
+ milestone_id: milestoneId,
699
+ reason: res.reason,
700
+ });
701
+ return ok({
702
+ status: 'ok',
703
+ command: 'produce-probe-freeze',
704
+ run_id: runId,
705
+ milestone_id: milestoneId,
706
+ reused: res.reused,
707
+ probe_set_hash: res.probe_set_hash,
708
+ author_family: res.author_family,
709
+ });
710
+ }
711
+ /**
712
+ * `produce probe record . --run-id <id> --milestone <mid> --result <pass|fail>
713
+ * --probe-family <fam> --head-sha <sha>` — record a blind-probe run result
714
+ * (result/family/head_sha only — never content). Refused fail-closed when the
715
+ * probe family equals the recorded implementer family (ID-08).
716
+ */
717
+ function cmdProbeRecord(projectDir, flags) {
718
+ const runId = strFlag(flags, 'run-id');
719
+ const milestoneId = strFlag(flags, 'milestone');
720
+ const result = strFlag(flags, 'result');
721
+ const probeFamily = strFlag(flags, 'probe-family');
722
+ const headSha = strFlag(flags, 'head-sha');
723
+ const gate = requireRun(projectDir, runId, 'probe record');
724
+ if (gate.error)
725
+ return gate.error;
726
+ if (!milestoneId)
727
+ return err('produce probe record requires --milestone <mid>');
728
+ if (!result)
729
+ return err('produce probe record requires --result <pass|fail>');
730
+ if (!probeFamily)
731
+ return err('produce probe record requires --probe-family <fam>');
732
+ if (!headSha)
733
+ return err('produce probe record requires --head-sha <sha>');
734
+ const res = recordProbeRun(projectDir, { runId, milestoneId, result, probeFamily, headSha });
735
+ if (!res.ok)
736
+ return err(`produce probe record refused: ${res.reason}`, 1, {
737
+ command: 'produce-probe-record',
738
+ run_id: runId,
739
+ milestone_id: milestoneId,
740
+ reason: res.reason,
741
+ });
742
+ return ok({
743
+ status: 'ok',
744
+ command: 'produce-probe-record',
745
+ run_id: runId,
746
+ milestone_id: milestoneId,
747
+ result: res.result,
748
+ probe_family: res.probe_family,
749
+ head_sha: res.head_sha,
750
+ });
751
+ }
752
+ /**
753
+ * `produce probe sweep . --run-id <id> [--sample N]` — return a deterministic
754
+ * rotating sample of already-done milestones selected for blind re-probing
755
+ * against HEAD (US-6). Read-only — selection, not a run.
756
+ */
757
+ function cmdProbeSweep(projectDir, flags) {
758
+ const runId = strFlag(flags, 'run-id');
759
+ const gate = requireRun(projectDir, runId, 'probe sweep');
760
+ if (gate.error)
761
+ return gate.error;
762
+ const sampleRaw = strFlag(flags, 'sample');
763
+ const sample = sampleRaw ? Number.parseInt(sampleRaw, 10) : 1;
764
+ const swept = selectSweepSample(projectDir, runId, { sample });
765
+ return ok({
766
+ status: 'ok',
767
+ command: 'produce-probe-sweep',
768
+ ...swept,
769
+ });
770
+ }
771
+ /**
772
+ * The M2 `--status done` path — the two-gate adjudication (ID-M2-04). Delegates
773
+ * to `adjudicateDone`: `done` flips ONLY when the visible oracle (verify-proof
774
+ * `approved` + recorded cross-family oracle pass) AND the blind probe (recorded
775
+ * cross-family probe pass + a cross-family frozen set) both agree. A
776
+ * visible-pass + blind-fail logs `produce.oracle.probe_gap` and stays open; a
777
+ * bare done with no recorded external evidence is refused — the Showrunner never
778
+ * self-attests (ID-03). `--verdict-path` supplies verify-proof's verdict file.
779
+ */
780
+ function cmdSetMilestoneDone(projectDir, runId, milestoneId, flags) {
781
+ const verdictPath = strFlag(flags, 'verdict-path');
782
+ const res = adjudicateDone({ projectDir, runId, milestoneId, verdictPath });
783
+ if (res.ok) {
784
+ // M3 (ID-M3-05) — the shipped milestone's still-open acceptance criteria
785
+ // close as a CONSEQUENCE of the ship, so V strictly decreases. Wired
786
+ // ADDITIVELY here (done-gate.mjs is untouched): a criteria-less milestone
787
+ // closes zero and is byte-identical to M2.
788
+ const closed = closeCriteriaForShippedMilestone(projectDir, runId, milestoneId);
789
+ return ok({
790
+ status: 'ok',
791
+ command: 'produce-set-milestone-status',
792
+ run_id: runId,
793
+ milestone_id: milestoneId,
794
+ milestone_status: 'done',
795
+ roadmap: res.roadmap,
796
+ closed_criteria: closed,
797
+ });
798
+ }
799
+ // Refused — the milestone stays open. Frame the reason honestly (done is
800
+ // external-oracle-gated: it requires recorded external evidence + a blind
801
+ // cross-family probe; the Showrunner never self-attests, ID-03).
802
+ const messages = {
803
+ no_recorded_implementer: 'no recorded implementer family — done is external-oracle-gated: record the implementer + a cross-family oracle + blind probe first (the Showrunner never self-attests, ID-03).',
804
+ not_adjudicable: 'not adjudicable — done is external-oracle-gated: it requires verify-proof `approved` AND a cross-family oracle pass (ID-03).',
805
+ probe_gap: 'probe_gap — the visible oracle passed but the held-out blind cross-family probe FAILED; the milestone stays open and a produce.oracle.probe_gap canary event was logged (AC9).',
806
+ };
807
+ return err(`produce set-milestone-status --status done refused: ${messages[res.reason] ?? res.reason}`, 1, {
808
+ command: 'produce-set-milestone-status',
809
+ run_id: runId,
810
+ milestone_id: milestoneId,
811
+ rejected_reason: res.reason,
812
+ visible_gate: res.visible_gate,
813
+ blind_gate: res.blind_gate,
814
+ detail: res.detail ?? null,
815
+ });
816
+ }
817
+ /**
818
+ * `produce progress . --run-id <id>` — the read-time monotone-progress report
819
+ * (M3, ID-M3-02 / AC6). Re-reads the ledger from the append-only log on every
820
+ * invocation (no in-memory authority — ID-01) and returns the V-series across
821
+ * shipped-milestone checkpoints, `strictly_decreasing`, and any hard
822
+ * `lyapunov_violations` (an un-sanctioned post-descent increase, detected
823
+ * defensively from the fold). It FLAGS — it never rejects (the rejection is the
824
+ * write-time guard in `declare-criteria`); a flat ship is a soft stall.
825
+ */
826
+ function cmdProgress(projectDir, flags) {
827
+ const runId = strFlag(flags, 'run-id');
828
+ const gate = requireRun(projectDir, runId, 'progress');
829
+ if (gate.error)
830
+ return gate.error;
831
+ return ok({
832
+ status: 'ok',
833
+ command: 'produce-progress',
834
+ ...computeProgress(projectDir, runId),
835
+ });
836
+ }
837
+ /**
838
+ * `produce canary . --run-id <id>` — the reward-hacking canary (ID-M2-06):
839
+ * green-on-visible MINUS green-on-blind over the run's milestones (from recorded
840
+ * seals), with a per-milestone breakdown. Each `produce.oracle.probe_gap` is one
841
+ * visible-pass + blind-fail near-miss. Read-only.
842
+ */
843
+ function cmdCanary(projectDir, flags) {
844
+ const runId = strFlag(flags, 'run-id');
845
+ const gate = requireRun(projectDir, runId, 'canary');
846
+ if (gate.error)
847
+ return gate.error;
848
+ const canary = computeCanary(projectDir, runId);
849
+ return ok({
850
+ status: 'ok',
851
+ command: 'produce-canary',
852
+ ...canary,
853
+ });
854
+ }
855
+ /**
856
+ * `produce schedule . --run-id <id> [--wip N]` — the M5 WSJF/WIP scheduler
857
+ * (ID-M5-01/AC1). READ-ONLY: re-projects the roadmap + ledger + regressions from
858
+ * the append-only log, folds them into a WSJF-ranked ready_queue, a dependency-
859
+ * aware blocked list, the in_progress WIP accounting, and a WIP-limited pick of
860
+ * the top wip_available ready milestones. Emits NO event (a pure read, mirroring
861
+ * status/progress/canary). WIP resolves --wip flag → config produce.wip →
862
+ * default 1.
863
+ */
864
+ function cmdSchedule(projectDir, flags) {
865
+ const runId = strFlag(flags, 'run-id');
866
+ const gate = requireRun(projectDir, runId, 'schedule');
867
+ if (gate.error)
868
+ return gate.error;
869
+ const wipLimit = resolveWipLimit(projectDir, flags);
870
+ const schedule = computeSchedule(projectDir, runId, { wipLimit });
871
+ return ok({
872
+ status: 'ok',
873
+ command: 'produce-schedule',
874
+ ...schedule,
875
+ });
876
+ }
877
+ /**
878
+ * `produce saga <enter|complete|compensate|status> <project-dir> ...` — two-level
879
+ * dispatch (same shape as cmdOracle/cmdProbe: the sub-subcommand arrives as the
880
+ * outer `projectDir` param; the real dir is `args[0]` and the flags follow it).
881
+ *
882
+ * @param {string} subsub
883
+ * @param {string[]} args
884
+ */
885
+ function cmdSaga(subsub, args) {
886
+ const realProjectDir = Array.isArray(args) ? args[0] : undefined;
887
+ if (!realProjectDir || realProjectDir.startsWith('--'))
888
+ return err('Usage: apt-tools produce saga <enter|complete|compensate|review|resolve-reviewer|status> <project-dir> [flags]');
889
+ const flags = parseFlags(args.slice(1));
890
+ switch (subsub) {
891
+ case 'enter':
892
+ return cmdSagaEnter(realProjectDir, flags);
893
+ case 'complete':
894
+ return cmdSagaComplete(realProjectDir, flags);
895
+ case 'compensate':
896
+ return cmdSagaCompensate(realProjectDir, flags);
897
+ case 'review':
898
+ return cmdSagaReview(realProjectDir, flags);
899
+ case 'resolve-reviewer':
900
+ return cmdSagaResolveReviewer(realProjectDir, flags);
901
+ case 'status':
902
+ return cmdSagaStatus(realProjectDir, flags);
903
+ default:
904
+ return err(`Unknown produce saga subcommand: ${subsub}. Expected enter | complete | compensate | review | resolve-reviewer | status.`);
905
+ }
906
+ }
907
+ /** Require the milestone to exist in the run's roadmap (fail-closed). */
908
+ function requireSagaMilestone(projectDir, runId, milestoneId, cmd) {
909
+ const roadmap = projectRoadmap(projectDir, runId);
910
+ if (!roadmap.milestones.some((m) => m.id === milestoneId))
911
+ return err(`produce saga ${cmd}: milestone ${milestoneId} not found in run ${runId}`);
912
+ return null;
913
+ }
914
+ /**
915
+ * `produce saga enter . --run-id <id> --milestone <mid> --phase <p>` — append
916
+ * produce.saga.phase_entered for a milestone's saga phase (phase ∈ SAGA_PHASES,
917
+ * validated fail-closed). A SEPARATE fold from the roadmap projection (blindness).
918
+ */
919
+ function cmdSagaEnter(projectDir, flags) {
920
+ const runId = strFlag(flags, 'run-id');
921
+ const milestoneId = strFlag(flags, 'milestone');
922
+ const phase = strFlag(flags, 'phase');
923
+ const gate = requireRun(projectDir, runId, 'saga enter');
924
+ if (gate.error)
925
+ return gate.error;
926
+ if (!milestoneId)
927
+ return err('produce saga enter requires --milestone <mid>');
928
+ if (!phase)
929
+ return err(`produce saga enter requires --phase <${SAGA_PHASES.join('|')}>`);
930
+ const missing = requireSagaMilestone(projectDir, runId, milestoneId, 'enter');
931
+ if (missing)
932
+ return missing;
933
+ const res = enterPhase(projectDir, { runId, milestoneId, phase });
934
+ if (!res.ok)
935
+ return err(`produce saga enter: invalid --phase "${phase}". Allowed: ${SAGA_PHASES.join(' | ')}.`, 1, { command: 'produce-saga-enter', run_id: runId, milestone_id: milestoneId });
936
+ return ok({
937
+ status: 'ok',
938
+ command: 'produce-saga-enter',
939
+ run_id: runId,
940
+ milestone_id: milestoneId,
941
+ phase,
942
+ });
943
+ }
944
+ /**
945
+ * `produce saga complete . --run-id <id> --milestone <mid> --phase <p>` — append
946
+ * produce.saga.phase_completed for a milestone's saga phase.
947
+ */
948
+ function cmdSagaComplete(projectDir, flags) {
949
+ const runId = strFlag(flags, 'run-id');
950
+ const milestoneId = strFlag(flags, 'milestone');
951
+ const phase = strFlag(flags, 'phase');
952
+ const gate = requireRun(projectDir, runId, 'saga complete');
953
+ if (gate.error)
954
+ return gate.error;
955
+ if (!milestoneId)
956
+ return err('produce saga complete requires --milestone <mid>');
957
+ if (!phase)
958
+ return err(`produce saga complete requires --phase <${SAGA_PHASES.join('|')}>`);
959
+ const missing = requireSagaMilestone(projectDir, runId, milestoneId, 'complete');
960
+ if (missing)
961
+ return missing;
962
+ const res = completePhase(projectDir, { runId, milestoneId, phase });
963
+ if (!res.ok)
964
+ return err(`produce saga complete: invalid --phase "${phase}". Allowed: ${SAGA_PHASES.join(' | ')}.`, 1, { command: 'produce-saga-complete', run_id: runId, milestone_id: milestoneId });
965
+ return ok({
966
+ status: 'ok',
967
+ command: 'produce-saga-complete',
968
+ run_id: runId,
969
+ milestone_id: milestoneId,
970
+ phase,
971
+ });
972
+ }
973
+ /**
974
+ * `produce saga compensate . --run-id <id> --milestone <mid> --phase <p>
975
+ * [--reason "<r>"]` — fire the FORWARD compensator for a failed saga phase
976
+ * (ID-M4-02). Records produce.saga.compensated carrying the descriptor KIND. The
977
+ * verify/review/ship compensator SHIPS A REVERT + reopens + re-plans; EVERY
978
+ * compensator is `rolls_back_merged_pr: false` — never a merged-PR rollback. The
979
+ * CLI records the descriptor; it does NOT execute the revert (OOS dispatch) and
980
+ * NEVER mutates the roadmap milestone status.
981
+ */
982
+ function cmdSagaCompensate(projectDir, flags) {
983
+ const runId = strFlag(flags, 'run-id');
984
+ const milestoneId = strFlag(flags, 'milestone');
985
+ const phase = strFlag(flags, 'phase');
986
+ const reason = strFlag(flags, 'reason');
987
+ const gate = requireRun(projectDir, runId, 'saga compensate');
988
+ if (gate.error)
989
+ return gate.error;
990
+ if (!milestoneId)
991
+ return err('produce saga compensate requires --milestone <mid>');
992
+ if (!phase)
993
+ return err(`produce saga compensate requires --phase <${SAGA_PHASES.join('|')}>`);
994
+ const missing = requireSagaMilestone(projectDir, runId, milestoneId, 'compensate');
995
+ if (missing)
996
+ return missing;
997
+ const res = compensatePhase(projectDir, { runId, milestoneId, phase, reason });
998
+ if (!res.ok)
999
+ return err(`produce saga compensate: invalid --phase "${phase}". Allowed: ${SAGA_PHASES.join(' | ')}.`, 1, { command: 'produce-saga-compensate', run_id: runId, milestone_id: milestoneId });
1000
+ return ok({
1001
+ status: 'ok',
1002
+ command: 'produce-saga-compensate',
1003
+ run_id: runId,
1004
+ milestone_id: milestoneId,
1005
+ phase,
1006
+ reason: reason ?? null,
1007
+ compensator: res.compensator,
1008
+ });
1009
+ }
1010
+ /**
1011
+ * `produce saga resolve-reviewer . --run-id <id> --milestone <mid>
1012
+ * [--host-family <fam>] [--available-families a,b,c]` — resolve a REVIEW-phase
1013
+ * reviewer family DISJOINT from the recorded implementer (ID-M4-04 / ID-08), a
1014
+ * THIN wrapper over the reused resolveProbeFamily (no duplicated cross-family or
1015
+ * model-resolution logic). The model is resolved LIVE (never a hardcoded id);
1016
+ * available:false when no disjoint family exists (single-family setup) or when no
1017
+ * implementer family is recorded (fail-closed).
1018
+ */
1019
+ function cmdSagaResolveReviewer(projectDir, flags) {
1020
+ const runId = strFlag(flags, 'run-id');
1021
+ const milestoneId = strFlag(flags, 'milestone');
1022
+ const gate = requireRun(projectDir, runId, 'saga resolve-reviewer');
1023
+ if (gate.error)
1024
+ return gate.error;
1025
+ if (!milestoneId)
1026
+ return err('produce saga resolve-reviewer requires --milestone <mid>');
1027
+ const hostFamily = strFlag(flags, 'host-family');
1028
+ const availRaw = strFlag(flags, 'available-families');
1029
+ const availableFamilies = availRaw
1030
+ ? availRaw
1031
+ .split(',')
1032
+ .map((s) => s.trim())
1033
+ .filter(Boolean)
1034
+ : undefined;
1035
+ const resolution = resolveReviewer(projectDir, {
1036
+ runId,
1037
+ milestoneId,
1038
+ hostFamily,
1039
+ availableFamilies,
1040
+ });
1041
+ return ok({
1042
+ status: 'ok',
1043
+ command: 'produce-saga-resolve-reviewer',
1044
+ run_id: runId,
1045
+ milestone_id: milestoneId,
1046
+ host_family: hostFamily ?? null,
1047
+ ...resolution,
1048
+ });
1049
+ }
1050
+ /**
1051
+ * `produce saga review . --run-id <id> --milestone <mid> --verdict <pass|fail>
1052
+ * --reviewer-family <fam>` — record the cross-family review verdict
1053
+ * (produce.saga.reviewed). REFUSED fail-closed when the reviewer family equals
1054
+ * the recorded implementer family, or when no implementer family is recorded
1055
+ * (ID-M4-04 / ID-08 — the review-path analog of `oracle record`).
1056
+ */
1057
+ function cmdSagaReview(projectDir, flags) {
1058
+ const runId = strFlag(flags, 'run-id');
1059
+ const milestoneId = strFlag(flags, 'milestone');
1060
+ const verdict = strFlag(flags, 'verdict');
1061
+ const reviewerFamily = strFlag(flags, 'reviewer-family');
1062
+ const gate = requireRun(projectDir, runId, 'saga review');
1063
+ if (gate.error)
1064
+ return gate.error;
1065
+ if (!milestoneId)
1066
+ return err('produce saga review requires --milestone <mid>');
1067
+ if (!verdict)
1068
+ return err('produce saga review requires --verdict <pass|fail>');
1069
+ if (!reviewerFamily)
1070
+ return err('produce saga review requires --reviewer-family <fam>');
1071
+ const res = recordReview(projectDir, { runId, milestoneId, verdict, reviewerFamily });
1072
+ if (!res.ok)
1073
+ return err(`produce saga review refused: ${res.reason}`, 1, {
1074
+ command: 'produce-saga-review',
1075
+ run_id: runId,
1076
+ milestone_id: milestoneId,
1077
+ reason: res.reason,
1078
+ implementer_family: res.implementer_family ?? null,
1079
+ });
1080
+ return ok({
1081
+ status: 'ok',
1082
+ command: 'produce-saga-review',
1083
+ run_id: runId,
1084
+ milestone_id: milestoneId,
1085
+ verdict: res.verdict,
1086
+ reviewer_family: res.reviewer_family,
1087
+ implementer_family: res.implementer_family,
1088
+ });
1089
+ }
1090
+ /**
1091
+ * `produce saga status . --run-id <id>` — read-only re-projection of the saga
1092
+ * fold from the append-only log (no in-memory authority — AC4). Reports the
1093
+ * per-milestone phase state + any pending compensation, WITHOUT mutating the
1094
+ * roadmap milestone status. Also reports V read-only via computeProgress for
1095
+ * context (light M3 ledger reuse — no structural coupling).
1096
+ */
1097
+ function cmdSagaStatus(projectDir, flags) {
1098
+ const runId = strFlag(flags, 'run-id');
1099
+ const gate = requireRun(projectDir, runId, 'saga status');
1100
+ if (gate.error)
1101
+ return gate.error;
1102
+ const saga = projectSaga(projectDir, runId);
1103
+ return ok({
1104
+ status: 'ok',
1105
+ command: 'produce-saga-status',
1106
+ run_id: runId,
1107
+ saga,
1108
+ v_open_now: computeProgress(projectDir, runId).v_open_now,
1109
+ // M4 — the TTL regression trail for context (read-only; no coupling).
1110
+ ttl_regressions: listRegressions(projectDir, runId),
1111
+ });
1112
+ }
1113
+ /**
1114
+ * `produce ttl <select|reverify> <project-dir> ...` — two-level dispatch (same
1115
+ * shape as cmdSaga/cmdOracle/cmdProbe: the sub-subcommand arrives as the outer
1116
+ * `projectDir` param; the real dir is `args[0]` and the flags follow it).
1117
+ *
1118
+ * @param {string} subsub
1119
+ * @param {string[]} args
1120
+ */
1121
+ function cmdTtl(subsub, args) {
1122
+ const realProjectDir = Array.isArray(args) ? args[0] : undefined;
1123
+ if (!realProjectDir || realProjectDir.startsWith('--'))
1124
+ return err('Usage: apt-tools produce ttl <select|reverify> <project-dir> [flags]');
1125
+ const flags = parseFlags(args.slice(1));
1126
+ switch (subsub) {
1127
+ case 'select':
1128
+ return cmdTtlSelect(realProjectDir, flags);
1129
+ case 'reverify':
1130
+ return cmdTtlReverify(realProjectDir, flags);
1131
+ default:
1132
+ return err(`Unknown produce ttl subcommand: ${subsub}. Expected select | reverify.`);
1133
+ }
1134
+ }
1135
+ /**
1136
+ * `produce ttl select . --run-id <id> --head-sha <sha> [--sample N]` — return a
1137
+ * deterministic rotating sample of already-done milestones STALE against HEAD
1138
+ * (reuses selectSweepSample; fresh milestones excluded). Read-only — selection,
1139
+ * not a run. Requires --head-sha (staleness fails closed without it).
1140
+ */
1141
+ function cmdTtlSelect(projectDir, flags) {
1142
+ const runId = strFlag(flags, 'run-id');
1143
+ const headSha = strFlag(flags, 'head-sha');
1144
+ const gate = requireRun(projectDir, runId, 'ttl select');
1145
+ if (gate.error)
1146
+ return gate.error;
1147
+ if (!headSha)
1148
+ return err('produce ttl select requires --head-sha <sha>');
1149
+ const sampleRaw = strFlag(flags, 'sample');
1150
+ const sample = sampleRaw ? Number.parseInt(sampleRaw, 10) : 1;
1151
+ const selected = selectTtlReverifySample(projectDir, runId, { headSha, sample });
1152
+ return ok({
1153
+ status: 'ok',
1154
+ command: 'produce-ttl-select',
1155
+ ...selected,
1156
+ });
1157
+ }
1158
+ /**
1159
+ * `produce ttl reverify . --run-id <id> --milestone <mid> --result <pass|fail>
1160
+ * --probe-family <fam> --head-sha <sha>` — record a cross-family TTL re-verify
1161
+ * run. Refused fail-closed when the probe family equals the recorded implementer
1162
+ * family (ID-08). On `fail` it appends produce.ttl.reverified +
1163
+ * produce.ttl.regression_detected AND fires the FORWARD compensator (a revert +
1164
+ * reopen + re-plan; NEVER a merged-PR rollback — ID-M4-03). A `pass` refreshes
1165
+ * verification against the new HEAD and logs no regression.
1166
+ */
1167
+ function cmdTtlReverify(projectDir, flags) {
1168
+ const runId = strFlag(flags, 'run-id');
1169
+ const milestoneId = strFlag(flags, 'milestone');
1170
+ const result = strFlag(flags, 'result');
1171
+ const probeFamily = strFlag(flags, 'probe-family');
1172
+ const headSha = strFlag(flags, 'head-sha');
1173
+ const gate = requireRun(projectDir, runId, 'ttl reverify');
1174
+ if (gate.error)
1175
+ return gate.error;
1176
+ if (!milestoneId)
1177
+ return err('produce ttl reverify requires --milestone <mid>');
1178
+ if (!result)
1179
+ return err('produce ttl reverify requires --result <pass|fail>');
1180
+ if (!probeFamily)
1181
+ return err('produce ttl reverify requires --probe-family <fam>');
1182
+ if (!headSha)
1183
+ return err('produce ttl reverify requires --head-sha <sha>');
1184
+ const res = recordTtlReverify(projectDir, { runId, milestoneId, result, probeFamily, headSha });
1185
+ if (!res.ok)
1186
+ return err(`produce ttl reverify refused: ${res.reason}`, 1, {
1187
+ command: 'produce-ttl-reverify',
1188
+ run_id: runId,
1189
+ milestone_id: milestoneId,
1190
+ reason: res.reason,
1191
+ });
1192
+ return ok({
1193
+ status: 'ok',
1194
+ command: 'produce-ttl-reverify',
1195
+ run_id: runId,
1196
+ milestone_id: milestoneId,
1197
+ result: res.result,
1198
+ probe_family: res.probe_family,
1199
+ head_sha: res.head_sha,
1200
+ regression: res.regression,
1201
+ compensator: res.compensator,
1202
+ });
1203
+ }
1204
+ /**
1205
+ * `produce claim <declare|list|expire|consume> <project-dir> ...` — two-level
1206
+ * dispatch (same shape as cmdSaga/cmdTtl: the sub-subcommand arrives as the outer
1207
+ * `subsub` param; the real dir is `args[0]` and the flags follow it). The Claim
1208
+ * Envelope lifecycle (M6): declare freezes + logs a validated envelope, list
1209
+ * re-projects the ledger, expire logs `produce.claim.expired`, consume emits the
1210
+ * structured delta.
1211
+ *
1212
+ * @param {string} subsub
1213
+ * @param {string[]} args
1214
+ */
1215
+ function cmdClaim(subsub, args) {
1216
+ const realProjectDir = Array.isArray(args) ? args[0] : undefined;
1217
+ if (!realProjectDir || realProjectDir.startsWith('--'))
1218
+ return err('Usage: apt-tools produce claim <declare|list|expire|consume> <project-dir> [flags]');
1219
+ const flags = parseFlags(args.slice(1));
1220
+ switch (subsub) {
1221
+ case 'declare':
1222
+ return cmdClaimDeclare(realProjectDir, flags);
1223
+ case 'list':
1224
+ return cmdClaimList(realProjectDir, flags);
1225
+ case 'expire':
1226
+ return cmdClaimExpire(realProjectDir, flags);
1227
+ case 'consume':
1228
+ return cmdClaimConsume(realProjectDir, flags);
1229
+ default:
1230
+ return err(`Unknown produce claim subcommand: ${subsub}. Expected declare | list | expire | consume.`);
1231
+ }
1232
+ }
1233
+ /**
1234
+ * `produce claim declare . --run-id <id> --profile <p> --claim-file <path>
1235
+ * [--claim-id <id>]` — read + parse the claim file, VALIDATE + freeze it to
1236
+ * claims/<claim-id>.json exclusive-create, and log `produce.claim.declared`
1237
+ * (content_hash only). An invalid envelope is REFUSED fail-closed with its reason.
1238
+ */
1239
+ function cmdClaimDeclare(projectDir, flags) {
1240
+ const runId = strFlag(flags, 'run-id');
1241
+ const profile = strFlag(flags, 'profile');
1242
+ const claimFile = strFlag(flags, 'claim-file');
1243
+ const claimId = strFlag(flags, 'claim-id');
1244
+ const gate = requireRun(projectDir, runId, 'claim declare');
1245
+ if (gate.error)
1246
+ return gate.error;
1247
+ if (!profile)
1248
+ return err('produce claim declare requires --profile <id>');
1249
+ if (!claimFile)
1250
+ return err('produce claim declare requires --claim-file <path>');
1251
+ if (!existsSync(claimFile))
1252
+ return err(`produce claim declare: claim file not found: ${claimFile}`);
1253
+ let envelope;
1254
+ try {
1255
+ envelope = JSON.parse(readFileSync(claimFile, 'utf-8'));
1256
+ }
1257
+ catch {
1258
+ return err(`produce claim declare: claim file is not valid JSON: ${claimFile}`);
1259
+ }
1260
+ const res = freezeClaim(projectDir, runId, { profile, envelope, claimId: claimId ?? undefined });
1261
+ if (!res.ok)
1262
+ return err(`produce claim declare refused: ${res.reason}`, 1, {
1263
+ command: 'produce-claim-declare',
1264
+ run_id: runId,
1265
+ reason: res.reason,
1266
+ });
1267
+ return ok({
1268
+ status: 'ok',
1269
+ command: 'produce-claim-declare',
1270
+ run_id: runId,
1271
+ claim_id: res.claim_id,
1272
+ profile: res.profile,
1273
+ content_hash: res.content_hash,
1274
+ reused: res.reused,
1275
+ });
1276
+ }
1277
+ /**
1278
+ * `produce claim list . --run-id <id>` — read-only re-projection of the claim
1279
+ * ledger (declared − expired) folded from the append-only log (no in-memory
1280
+ * authority). The `produce.claim.*` ops never enter the roadmap projection.
1281
+ */
1282
+ function cmdClaimList(projectDir, flags) {
1283
+ const runId = strFlag(flags, 'run-id');
1284
+ const gate = requireRun(projectDir, runId, 'claim list');
1285
+ if (gate.error)
1286
+ return gate.error;
1287
+ const ledger = projectClaims(projectDir, runId);
1288
+ return ok({ status: 'ok', command: 'produce-claim-list', ...ledger });
1289
+ }
1290
+ /**
1291
+ * `produce claim expire . --run-id <id> --claim-id <id> [--reason "<r>"]` —
1292
+ * append `produce.claim.expired`; the claim leaves the ledger's active set
1293
+ * (anti-corrosion manual expiry). Records the descriptor; the content file stays.
1294
+ */
1295
+ function cmdClaimExpire(projectDir, flags) {
1296
+ const runId = strFlag(flags, 'run-id');
1297
+ const claimId = strFlag(flags, 'claim-id');
1298
+ const reason = strFlag(flags, 'reason') || 'manual_expiry';
1299
+ const gate = requireRun(projectDir, runId, 'claim expire');
1300
+ if (gate.error)
1301
+ return gate.error;
1302
+ if (!claimId)
1303
+ return err('produce claim expire requires --claim-id <id>');
1304
+ appendProduceEvent(projectDir, 'produce.claim.expired', {
1305
+ run_id: runId,
1306
+ claim_id: claimId,
1307
+ reason,
1308
+ });
1309
+ return ok({
1310
+ status: 'ok',
1311
+ command: 'produce-claim-expire',
1312
+ run_id: runId,
1313
+ claim_id: claimId,
1314
+ reason,
1315
+ });
1316
+ }
1317
+ /**
1318
+ * `produce claim consume . --run-id <id> [--milestone <mid>]` — emit the
1319
+ * STRUCTURED delta `{claim_id, targets:[{kind, requirement, ref?}]}` for each
1320
+ * consumable active claim (derived from resolution_verifier — never a lesson-text
1321
+ * paste), skipping expired / HEAD-invalidated / precondition-unmatched claims.
1322
+ * Appends one `produce.claim.consumed {run_id, claim_id, target_kind, target_ref?}`
1323
+ * per emitted target for the forensic trail (target_kind only, never content).
1324
+ */
1325
+ function cmdClaimConsume(projectDir, flags) {
1326
+ const runId = strFlag(flags, 'run-id');
1327
+ const gate = requireRun(projectDir, runId, 'claim consume');
1328
+ if (gate.error)
1329
+ return gate.error;
1330
+ const milestone = strFlag(flags, 'milestone');
1331
+ const ctx = milestone ? { milestone, milestone_id: milestone } : {};
1332
+ const result = consumeClaims(projectDir, runId, ctx);
1333
+ // Forensic trail: one produce.claim.consumed per emitted target (ID-M6-07 —
1334
+ // target_kind only, never claim content).
1335
+ for (const c of result.consumed) {
1336
+ for (const t of c.targets) {
1337
+ const data = { run_id: runId, claim_id: c.claim_id, target_kind: t.kind };
1338
+ if (typeof t.ref === 'string' && t.ref)
1339
+ data.target_ref = t.ref;
1340
+ appendProduceEvent(projectDir, 'produce.claim.consumed', data);
1341
+ }
1342
+ }
1343
+ return ok({ status: 'ok', command: 'produce-claim-consume', run_id: runId, ...result });
1344
+ }
1345
+ //# sourceMappingURL=produce.mjs.map