@openrig/cli 0.3.4-rc.0 → 0.4.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 (250) hide show
  1. package/daemon/assets/plugins/openrig-core/hooks/scripts/activity-relay.cjs +34 -0
  2. package/daemon/dist/adapters/codex-resume.d.ts +2 -1
  3. package/daemon/dist/adapters/codex-resume.d.ts.map +1 -1
  4. package/daemon/dist/adapters/codex-resume.js +18 -6
  5. package/daemon/dist/adapters/codex-resume.js.map +1 -1
  6. package/daemon/dist/adapters/codex-runtime-adapter.d.ts +5 -1
  7. package/daemon/dist/adapters/codex-runtime-adapter.d.ts.map +1 -1
  8. package/daemon/dist/adapters/codex-runtime-adapter.js +58 -5
  9. package/daemon/dist/adapters/codex-runtime-adapter.js.map +1 -1
  10. package/daemon/dist/adapters/tmux.d.ts +28 -14
  11. package/daemon/dist/adapters/tmux.d.ts.map +1 -1
  12. package/daemon/dist/adapters/tmux.js +65 -30
  13. package/daemon/dist/adapters/tmux.js.map +1 -1
  14. package/daemon/dist/db/migrations/043_resume_provenance.d.ts +3 -0
  15. package/daemon/dist/db/migrations/043_resume_provenance.d.ts.map +1 -0
  16. package/daemon/dist/db/migrations/043_resume_provenance.js +5 -0
  17. package/daemon/dist/db/migrations/043_resume_provenance.js.map +1 -0
  18. package/daemon/dist/domain/agent-activity-store.d.ts +11 -1
  19. package/daemon/dist/domain/agent-activity-store.d.ts.map +1 -1
  20. package/daemon/dist/domain/agent-activity-store.js +11 -2
  21. package/daemon/dist/domain/agent-activity-store.js.map +1 -1
  22. package/daemon/dist/domain/native-resume-probe.d.ts +2 -1
  23. package/daemon/dist/domain/native-resume-probe.d.ts.map +1 -1
  24. package/daemon/dist/domain/native-resume-probe.js +21 -2
  25. package/daemon/dist/domain/native-resume-probe.js.map +1 -1
  26. package/daemon/dist/domain/node-inventory.d.ts.map +1 -1
  27. package/daemon/dist/domain/node-inventory.js +70 -46
  28. package/daemon/dist/domain/node-inventory.js.map +1 -1
  29. package/daemon/dist/domain/node-launcher.d.ts +7 -7
  30. package/daemon/dist/domain/node-launcher.d.ts.map +1 -1
  31. package/daemon/dist/domain/node-launcher.js +0 -22
  32. package/daemon/dist/domain/node-launcher.js.map +1 -1
  33. package/daemon/dist/domain/profile-resolver.d.ts +4 -5
  34. package/daemon/dist/domain/profile-resolver.d.ts.map +1 -1
  35. package/daemon/dist/domain/profile-resolver.js.map +1 -1
  36. package/daemon/dist/domain/queue-repository.d.ts +4 -0
  37. package/daemon/dist/domain/queue-repository.d.ts.map +1 -1
  38. package/daemon/dist/domain/queue-repository.js +21 -2
  39. package/daemon/dist/domain/queue-repository.js.map +1 -1
  40. package/daemon/dist/domain/restore-check-service.d.ts +14 -0
  41. package/daemon/dist/domain/restore-check-service.d.ts.map +1 -1
  42. package/daemon/dist/domain/restore-check-service.js +78 -8
  43. package/daemon/dist/domain/restore-check-service.js.map +1 -1
  44. package/daemon/dist/domain/restore-orchestrator.d.ts.map +1 -1
  45. package/daemon/dist/domain/restore-orchestrator.js +3 -3
  46. package/daemon/dist/domain/restore-orchestrator.js.map +1 -1
  47. package/daemon/dist/domain/resume-metadata-refresher.js +1 -1
  48. package/daemon/dist/domain/resume-metadata-refresher.js.map +1 -1
  49. package/daemon/dist/domain/resume-token-validation.d.ts +17 -0
  50. package/daemon/dist/domain/resume-token-validation.d.ts.map +1 -0
  51. package/daemon/dist/domain/resume-token-validation.js +46 -0
  52. package/daemon/dist/domain/resume-token-validation.js.map +1 -0
  53. package/daemon/dist/domain/rigspec-instantiator.d.ts.map +1 -1
  54. package/daemon/dist/domain/rigspec-instantiator.js +3 -6
  55. package/daemon/dist/domain/rigspec-instantiator.js.map +1 -1
  56. package/daemon/dist/domain/runtime-adapter.d.ts.map +1 -1
  57. package/daemon/dist/domain/runtime-adapter.js +1 -0
  58. package/daemon/dist/domain/runtime-adapter.js.map +1 -1
  59. package/daemon/dist/domain/scope/dot-id.d.ts +43 -0
  60. package/daemon/dist/domain/scope/dot-id.d.ts.map +1 -0
  61. package/daemon/dist/domain/scope/dot-id.js +165 -0
  62. package/daemon/dist/domain/scope/dot-id.js.map +1 -0
  63. package/daemon/dist/domain/scope/scope-audit.d.ts +26 -0
  64. package/daemon/dist/domain/scope/scope-audit.d.ts.map +1 -0
  65. package/daemon/dist/domain/scope/scope-audit.js +96 -0
  66. package/daemon/dist/domain/scope/scope-audit.js.map +1 -0
  67. package/daemon/dist/domain/scope/types.d.ts +61 -0
  68. package/daemon/dist/domain/scope/types.d.ts.map +1 -0
  69. package/daemon/dist/domain/scope/types.js +38 -0
  70. package/daemon/dist/domain/scope/types.js.map +1 -0
  71. package/daemon/dist/domain/seat-attention-reconciler.d.ts +12 -0
  72. package/daemon/dist/domain/seat-attention-reconciler.d.ts.map +1 -1
  73. package/daemon/dist/domain/seat-attention-reconciler.js +116 -88
  74. package/daemon/dist/domain/seat-attention-reconciler.js.map +1 -1
  75. package/daemon/dist/domain/session-registry.d.ts +15 -1
  76. package/daemon/dist/domain/session-registry.d.ts.map +1 -1
  77. package/daemon/dist/domain/session-registry.js +46 -4
  78. package/daemon/dist/domain/session-registry.js.map +1 -1
  79. package/daemon/dist/domain/skill-audit.d.ts +47 -0
  80. package/daemon/dist/domain/skill-audit.d.ts.map +1 -0
  81. package/daemon/dist/domain/skill-audit.js +218 -0
  82. package/daemon/dist/domain/skill-audit.js.map +1 -0
  83. package/daemon/dist/domain/skill-discovery.d.ts +15 -0
  84. package/daemon/dist/domain/skill-discovery.d.ts.map +1 -1
  85. package/daemon/dist/domain/skill-discovery.js +70 -0
  86. package/daemon/dist/domain/skill-discovery.js.map +1 -1
  87. package/daemon/dist/domain/skill-mirror-drift.d.ts +10 -0
  88. package/daemon/dist/domain/skill-mirror-drift.d.ts.map +1 -0
  89. package/daemon/dist/domain/skill-mirror-drift.js +24 -0
  90. package/daemon/dist/domain/skill-mirror-drift.js.map +1 -0
  91. package/daemon/dist/domain/startup-orchestrator.js +1 -1
  92. package/daemon/dist/domain/startup-orchestrator.js.map +1 -1
  93. package/daemon/dist/domain/types.d.ts +38 -14
  94. package/daemon/dist/domain/types.d.ts.map +1 -1
  95. package/daemon/dist/domain/types.js.map +1 -1
  96. package/daemon/dist/domain/user-settings/settings-store.d.ts +1 -1
  97. package/daemon/dist/domain/user-settings/settings-store.d.ts.map +1 -1
  98. package/daemon/dist/domain/user-settings/settings-store.js +4 -0
  99. package/daemon/dist/domain/user-settings/settings-store.js.map +1 -1
  100. package/daemon/dist/domain/whoami-service.d.ts +1 -0
  101. package/daemon/dist/domain/whoami-service.d.ts.map +1 -1
  102. package/daemon/dist/domain/whoami-service.js +8 -4
  103. package/daemon/dist/domain/whoami-service.js.map +1 -1
  104. package/daemon/dist/index.d.ts.map +1 -1
  105. package/daemon/dist/index.js +25 -3
  106. package/daemon/dist/index.js.map +1 -1
  107. package/daemon/dist/routes/activity.d.ts.map +1 -1
  108. package/daemon/dist/routes/activity.js +29 -0
  109. package/daemon/dist/routes/activity.js.map +1 -1
  110. package/daemon/dist/routes/queue.d.ts.map +1 -1
  111. package/daemon/dist/routes/queue.js +8 -0
  112. package/daemon/dist/routes/queue.js.map +1 -1
  113. package/daemon/dist/routes/restore-check.js +33 -2
  114. package/daemon/dist/routes/restore-check.js.map +1 -1
  115. package/daemon/dist/routes/scope-audit.d.ts +3 -0
  116. package/daemon/dist/routes/scope-audit.d.ts.map +1 -0
  117. package/daemon/dist/routes/scope-audit.js +148 -0
  118. package/daemon/dist/routes/scope-audit.js.map +1 -0
  119. package/daemon/dist/routes/sessions.d.ts.map +1 -1
  120. package/daemon/dist/routes/sessions.js +67 -3
  121. package/daemon/dist/routes/sessions.js.map +1 -1
  122. package/daemon/dist/routes/skills.d.ts.map +1 -1
  123. package/daemon/dist/routes/skills.js +43 -0
  124. package/daemon/dist/routes/skills.js.map +1 -1
  125. package/daemon/dist/routes/terminal-ws.d.ts +7 -0
  126. package/daemon/dist/routes/terminal-ws.d.ts.map +1 -0
  127. package/daemon/dist/routes/terminal-ws.js +132 -0
  128. package/daemon/dist/routes/terminal-ws.js.map +1 -0
  129. package/daemon/dist/routes/transport.d.ts +3 -1
  130. package/daemon/dist/routes/transport.d.ts.map +1 -1
  131. package/daemon/dist/routes/transport.js +4 -1
  132. package/daemon/dist/routes/transport.js.map +1 -1
  133. package/daemon/dist/routes/whoami.d.ts.map +1 -1
  134. package/daemon/dist/routes/whoami.js +5 -0
  135. package/daemon/dist/routes/whoami.js.map +1 -1
  136. package/daemon/dist/server.d.ts +6 -0
  137. package/daemon/dist/server.d.ts.map +1 -1
  138. package/daemon/dist/server.js +29 -2
  139. package/daemon/dist/server.js.map +1 -1
  140. package/daemon/dist/startup.d.ts +8 -0
  141. package/daemon/dist/startup.d.ts.map +1 -1
  142. package/daemon/dist/startup.js +27 -8
  143. package/daemon/dist/startup.js.map +1 -1
  144. package/daemon/dist/terminal/TerminalSessionBroker.d.ts +173 -0
  145. package/daemon/dist/terminal/TerminalSessionBroker.d.ts.map +1 -0
  146. package/daemon/dist/terminal/TerminalSessionBroker.js +570 -0
  147. package/daemon/dist/terminal/TerminalSessionBroker.js.map +1 -0
  148. package/daemon/specs/agents/shared/skills/core/openrig-user/SKILL.md +131 -2
  149. package/daemon/specs/agents/shared/skills/pods/orchestration-team/SKILL.md +29 -0
  150. package/dist/client.d.ts +1 -0
  151. package/dist/client.d.ts.map +1 -1
  152. package/dist/client.js +23 -0
  153. package/dist/client.js.map +1 -1
  154. package/dist/commands/capture.d.ts.map +1 -1
  155. package/dist/commands/capture.js +6 -6
  156. package/dist/commands/capture.js.map +1 -1
  157. package/dist/commands/context.d.ts.map +1 -1
  158. package/dist/commands/context.js +41 -5
  159. package/dist/commands/context.js.map +1 -1
  160. package/dist/commands/down.d.ts.map +1 -1
  161. package/dist/commands/down.js +27 -0
  162. package/dist/commands/down.js.map +1 -1
  163. package/dist/commands/launch.d.ts.map +1 -1
  164. package/dist/commands/launch.js +34 -0
  165. package/dist/commands/launch.js.map +1 -1
  166. package/dist/commands/ps.d.ts.map +1 -1
  167. package/dist/commands/ps.js +361 -53
  168. package/dist/commands/ps.js.map +1 -1
  169. package/dist/commands/queue.d.ts.map +1 -1
  170. package/dist/commands/queue.js +60 -3
  171. package/dist/commands/queue.js.map +1 -1
  172. package/dist/commands/restore-check.d.ts.map +1 -1
  173. package/dist/commands/restore-check.js +80 -2
  174. package/dist/commands/restore-check.js.map +1 -1
  175. package/dist/commands/scope.d.ts.map +1 -1
  176. package/dist/commands/scope.js +688 -4
  177. package/dist/commands/scope.js.map +1 -1
  178. package/dist/commands/seat.d.ts +3 -1
  179. package/dist/commands/seat.d.ts.map +1 -1
  180. package/dist/commands/seat.js +61 -1
  181. package/dist/commands/seat.js.map +1 -1
  182. package/dist/commands/send.d.ts.map +1 -1
  183. package/dist/commands/send.js +6 -6
  184. package/dist/commands/send.js.map +1 -1
  185. package/dist/commands/skill.d.ts +4 -0
  186. package/dist/commands/skill.d.ts.map +1 -0
  187. package/dist/commands/skill.js +77 -0
  188. package/dist/commands/skill.js.map +1 -0
  189. package/dist/commands/up.d.ts.map +1 -1
  190. package/dist/commands/up.js +27 -0
  191. package/dist/commands/up.js.map +1 -1
  192. package/dist/commands/whoami.d.ts.map +1 -1
  193. package/dist/commands/whoami.js +208 -20
  194. package/dist/commands/whoami.js.map +1 -1
  195. package/dist/cross-host-executor.d.ts +1 -1
  196. package/dist/cross-host-executor.d.ts.map +1 -1
  197. package/dist/cross-host-executor.js +3 -2
  198. package/dist/cross-host-executor.js.map +1 -1
  199. package/dist/cross-host-types.d.ts +2 -0
  200. package/dist/cross-host-types.d.ts.map +1 -0
  201. package/dist/cross-host-types.js +2 -0
  202. package/dist/cross-host-types.js.map +1 -0
  203. package/dist/host-registry.d.ts +27 -5
  204. package/dist/host-registry.d.ts.map +1 -1
  205. package/dist/host-registry.js +86 -19
  206. package/dist/host-registry.js.map +1 -1
  207. package/dist/index.d.ts +1 -0
  208. package/dist/index.d.ts.map +1 -1
  209. package/dist/index.js +2 -0
  210. package/dist/index.js.map +1 -1
  211. package/dist/lib/scope/progress-edit.d.ts +42 -0
  212. package/dist/lib/scope/progress-edit.d.ts.map +1 -0
  213. package/dist/lib/scope/progress-edit.js +171 -0
  214. package/dist/lib/scope/progress-edit.js.map +1 -0
  215. package/dist/lib/scope/scope-audit.d.ts +26 -0
  216. package/dist/lib/scope/scope-audit.d.ts.map +1 -0
  217. package/dist/lib/scope/scope-audit.js +96 -0
  218. package/dist/lib/scope/scope-audit.js.map +1 -0
  219. package/dist/lib/scope/templates.d.ts +2 -0
  220. package/dist/lib/scope/templates.d.ts.map +1 -1
  221. package/dist/lib/scope/templates.js +8 -0
  222. package/dist/lib/scope/templates.js.map +1 -1
  223. package/dist/lib/scope/trust.d.ts +30 -0
  224. package/dist/lib/scope/trust.d.ts.map +1 -0
  225. package/dist/lib/scope/trust.js +80 -0
  226. package/dist/lib/scope/trust.js.map +1 -0
  227. package/dist/lib/scope/types.d.ts +6 -0
  228. package/dist/lib/scope/types.d.ts.map +1 -1
  229. package/dist/lib/scope/types.js +8 -0
  230. package/dist/lib/scope/types.js.map +1 -1
  231. package/dist/lib/scope-templates/backlog-deprecation.md +1 -0
  232. package/dist/lib/scope-templates/backlog-tech-debt.md +1 -0
  233. package/dist/lib/scope-templates/bug-fix.md +1 -0
  234. package/dist/lib/scope-templates/mission-placeholder.md +1 -0
  235. package/dist/lib/scope-templates/mission-progress.md +12 -0
  236. package/dist/lib/scope-templates/mission-release.md +1 -0
  237. package/dist/lib/scope-templates/placeholder.md +1 -0
  238. package/dist/lib/scope-templates/release-feature.md +1 -0
  239. package/dist/lib/scope-templates/research.md +1 -0
  240. package/dist/lib/scope-templates/slice-progress.md +11 -0
  241. package/dist/remote-host-ops.d.ts +24 -0
  242. package/dist/remote-host-ops.d.ts.map +1 -0
  243. package/dist/remote-host-ops.js +53 -0
  244. package/dist/remote-host-ops.js.map +1 -0
  245. package/package.json +1 -1
  246. package/ui/dist/assets/index-DMI6jeAX.css +32 -0
  247. package/ui/dist/assets/index-FQgCB6mC.js +580 -0
  248. package/ui/dist/index.html +2 -2
  249. package/ui/dist/assets/index-C_Q5FczU.css +0 -1
  250. package/ui/dist/assets/index-De6vv0yO.js +0 -552
@@ -8,10 +8,12 @@
8
8
  import fs from "node:fs";
9
9
  import path from "node:path";
10
10
  import { Command } from "commander";
11
- import { CLOSE_REASONS, MISSION_TEMPLATE_KINDS, SLICE_TEMPLATE_KINDS, ScopeCliError, } from "../lib/scope/types.js";
11
+ import { CLOSE_REASONS, MISSION_TEMPLATE_KINDS, SLICE_TEMPLATE_KINDS, STAGE_VALUES, ScopeCliError, } from "../lib/scope/types.js";
12
12
  import { DEFAULT_PROJECT_PREFIX, inferMissionDotId, isMissionDotId, nextEscapeBandOrdinal, sliceIdFromMission, } from "../lib/scope/dot-id.js";
13
- import { ensureMissionIdPersisted, findMission, findSlice, listMissions, listSlices, moveSlice, nextSliceNN, pad2, resolveMissionsRoot, splitFrontmatter, todayDateISO, updateFrontmatter, } from "../lib/scope/scope-fs.js";
14
- import { renderMissionNotesTemplate, renderMissionTemplate, renderSliceTemplate, titleFromSlug, } from "../lib/scope/templates.js";
13
+ import { ensureMissionId, ensureMissionIdPersisted, findMission, findSlice, listMissions, listSlices, moveSlice, nextSliceNN, pad2, readFrontmatter, resolveMissionsRoot, splitFrontmatter, todayDateISO, updateFrontmatter, } from "../lib/scope/scope-fs.js";
14
+ import { renderMissionNotesTemplate, renderMissionProgressTemplate, renderMissionTemplate, renderSliceProgressTemplate, renderSliceTemplate, titleFromSlug, } from "../lib/scope/templates.js";
15
+ import { addProgressRow, DEFAULT_PROGRESS_SECTION, parseStatus, PROGRESS_STATUSES, setProgressRow, } from "../lib/scope/progress-edit.js";
16
+ import { deriveScopeTrust } from "../lib/scope/trust.js";
15
17
  function makeStdout() {
16
18
  return { write: (text) => process.stdout.write(text) };
17
19
  }
@@ -63,6 +65,16 @@ function getOpts(cmd) {
63
65
  }
64
66
  return {};
65
67
  }
68
+ /** FR-5: a one-line human render of the derived stage. Shows the declared
69
+ * stage, and (when a weak `verified` downgrades it) the effective stage +
70
+ * why. Derived at read time; nothing is written. */
71
+ function formatTrustLine(trust) {
72
+ const declared = trust.declaredStage || "—";
73
+ if (trust.downgraded) {
74
+ return ` stage: ${declared} (effective: ${trust.effectiveStage} — ${trust.verified.status})\n`;
75
+ }
76
+ return ` stage: ${declared}\n`;
77
+ }
66
78
  // ---------------------------------------------------------------------
67
79
  // rig scope slice ls
68
80
  // ---------------------------------------------------------------------
@@ -131,6 +143,8 @@ function buildSliceShowCommand() {
131
143
  const readme = slice.readmePath ? fs.readFileSync(slice.readmePath, "utf8") : null;
132
144
  const children = fs.readdirSync(slice.absPath, { withFileTypes: true })
133
145
  .map((e) => ({ name: e.name, kind: e.isDirectory() ? "dir" : "file" }));
146
+ // FR-5: derive read-time trust from (stage x verified) — NEVER stored.
147
+ const trust = deriveScopeTrust(slice.frontmatter);
134
148
  const payload = {
135
149
  ok: true,
136
150
  slice: {
@@ -140,6 +154,7 @@ function buildSliceShowCommand() {
140
154
  status: slice.status,
141
155
  path: slice.absPath,
142
156
  frontmatter: slice.frontmatter,
157
+ trust,
143
158
  readme,
144
159
  children,
145
160
  },
@@ -151,6 +166,7 @@ function buildSliceShowCommand() {
151
166
  out.write(`Slice: ${slice.missionName}/${slice.name}\n`);
152
167
  out.write(` id: ${slice.id ?? "—"}\n`);
153
168
  out.write(` status: ${slice.status ?? "—"}\n`);
169
+ out.write(formatTrustLine(trust));
154
170
  out.write(` path: ${slice.absPath}\n`);
155
171
  out.write(` children: ${children.length}\n`);
156
172
  if (readme) {
@@ -176,6 +192,7 @@ function buildSliceCreateCommand() {
176
192
  .argument("<slug>", "Short slug (becomes the folder name's suffix)")
177
193
  .option("--template <kind>", `Template: ${SLICE_TEMPLATE_KINDS.join(" | ")}`, "placeholder")
178
194
  .option("--title <text>", "Display title (defaults to titlecased slug)")
195
+ .option("--readme-only", "Write progress_rail: readme-only in README frontmatter instead of scaffolding PROGRESS.md")
179
196
  .option("--json", "Machine-readable output")
180
197
  .action(async (missionName, rawSlug, opts, command) => {
181
198
  const out = makeStdout();
@@ -226,7 +243,16 @@ function buildSliceCreateCommand() {
226
243
  });
227
244
  fs.mkdirSync(sliceAbs, { recursive: true });
228
245
  const readmePath = path.join(sliceAbs, "README.md");
229
- fs.writeFileSync(readmePath, body, "utf8");
246
+ const readmeOnly = Boolean(opts.readmeOnly);
247
+ if (readmeOnly) {
248
+ const markerBody = body.replace(/^(---\n)/, `---\nprogress_rail: readme-only\n`);
249
+ fs.writeFileSync(readmePath, markerBody, "utf8");
250
+ }
251
+ else {
252
+ fs.writeFileSync(readmePath, body, "utf8");
253
+ const progressPath = path.join(sliceAbs, "PROGRESS.md");
254
+ fs.writeFileSync(progressPath, renderSliceProgressTemplate(title), "utf8");
255
+ }
230
256
  const payload = {
231
257
  ok: true,
232
258
  slice: {
@@ -464,6 +490,8 @@ function buildMissionShowCommand() {
464
490
  const slices = listSlices(mission, "all").map((s) => ({
465
491
  name: s.name, id: s.id, status: s.status, nn: s.nn,
466
492
  }));
493
+ // FR-5: derive read-time trust from (stage x verified) — NEVER stored.
494
+ const trust = deriveScopeTrust(mission.frontmatter);
467
495
  const payload = {
468
496
  ok: true,
469
497
  mission: {
@@ -473,6 +501,7 @@ function buildMissionShowCommand() {
473
501
  activeSliceCount: mission.activeSliceCount,
474
502
  closedSliceCount: mission.closedSliceCount,
475
503
  frontmatter: mission.frontmatter,
504
+ trust,
476
505
  readme,
477
506
  slices,
478
507
  },
@@ -483,6 +512,7 @@ function buildMissionShowCommand() {
483
512
  else {
484
513
  out.write(`Mission: ${mission.name}\n`);
485
514
  out.write(` id: ${mission.id ?? "—"}\n`);
515
+ out.write(formatTrustLine(trust));
486
516
  out.write(` active slices: ${mission.activeSliceCount}\n`);
487
517
  out.write(` closed slices: ${mission.closedSliceCount}\n`);
488
518
  out.write(` path: ${mission.absPath}\n`);
@@ -593,11 +623,14 @@ function buildMissionCreateCommand() {
593
623
  });
594
624
  missionNotesRendered = { rendered: r.rendered, resolvedFrom: r.resolvedFrom };
595
625
  }
626
+ const progressBody = renderMissionProgressTemplate(title);
596
627
  // All renders succeeded — safe to touch the filesystem.
597
628
  fs.mkdirSync(absPath, { recursive: true });
598
629
  fs.mkdirSync(path.join(absPath, "slices"), { recursive: true });
599
630
  const readmePath = path.join(absPath, "README.md");
600
631
  fs.writeFileSync(readmePath, readmeBody, "utf8");
632
+ const progressPath = path.join(absPath, "PROGRESS.md");
633
+ fs.writeFileSync(progressPath, progressBody, "utf8");
601
634
  let missionNotesPath = null;
602
635
  let missionNotesResolvedFrom = null;
603
636
  if (missionNotesRendered) {
@@ -633,6 +666,648 @@ function buildMissionCreateCommand() {
633
666
  });
634
667
  }
635
668
  // ---------------------------------------------------------------------
669
+ // Audit (B2 — read-only scope audit)
670
+ // ---------------------------------------------------------------------
671
+ function buildAuditCommand() {
672
+ return new Command("audit")
673
+ .description("Read-only scope audit: flag missing/malformed progress rails and registration ghosts")
674
+ .requiredOption("--mission <name>", "Mission to audit")
675
+ .option("--json", "Machine-readable JSON output")
676
+ .action(async (opts, command) => {
677
+ const out = makeStdout();
678
+ const json = Boolean(opts.json);
679
+ try {
680
+ const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
681
+ const { classifyScopeItem } = await import("../lib/scope/scope-audit.js");
682
+ const missionName = opts.mission;
683
+ const missionDir = path.join(missionsRoot, missionName);
684
+ if (!fs.existsSync(missionDir)) {
685
+ throw new ScopeCliError({ fact: `Mission "${missionName}" not found at ${missionDir}.`, consequence: "Cannot audit.", action: "Check the mission name." });
686
+ }
687
+ const missionReadme = path.join(missionDir, "README.md");
688
+ const missionProgress = path.join(missionDir, "PROGRESS.md");
689
+ const missionReadmeExists = fs.existsSync(missionReadme);
690
+ const missionProgressExists = fs.existsSync(missionProgress);
691
+ let missionResult;
692
+ if (!missionReadmeExists && missionProgressExists) {
693
+ missionResult = {
694
+ railStatus: "malformed",
695
+ findings: [{
696
+ kind: "orphan_progress",
697
+ severity: "high",
698
+ path: missionDir,
699
+ message: `PROGRESS.md exists but no README.md (orphan progress rail, no backing scope item)`,
700
+ remediation: `Add a README.md with frontmatter id, or remove the orphan PROGRESS.md`,
701
+ }],
702
+ frontmatterError: null,
703
+ };
704
+ }
705
+ else {
706
+ const missionFm = missionReadmeExists
707
+ ? extractFrontmatterRaw(fs.readFileSync(missionReadme, "utf-8"))
708
+ : null;
709
+ missionResult = classifyScopeItem({
710
+ id: null,
711
+ path: missionDir,
712
+ readmeFrontmatterRaw: missionFm,
713
+ progressFileExists: missionProgressExists,
714
+ readmeOnlyMarker: false,
715
+ isActiveRelease: true,
716
+ level: "mission",
717
+ });
718
+ }
719
+ const slicesDir = path.join(missionDir, "slices");
720
+ const sliceResults = [];
721
+ if (fs.existsSync(slicesDir)) {
722
+ for (const entry of fs.readdirSync(slicesDir)) {
723
+ const sliceDir = path.join(slicesDir, entry);
724
+ if (!fs.statSync(sliceDir).isDirectory())
725
+ continue;
726
+ const sliceReadme = path.join(sliceDir, "README.md");
727
+ const sliceProgress = path.join(sliceDir, "PROGRESS.md");
728
+ if (!fs.existsSync(sliceReadme)) {
729
+ if (fs.existsSync(sliceProgress)) {
730
+ sliceResults.push({
731
+ name: entry,
732
+ result: {
733
+ railStatus: "malformed",
734
+ findings: [{
735
+ kind: "orphan_progress",
736
+ severity: "high",
737
+ path: sliceDir,
738
+ message: `PROGRESS.md exists but no README.md (orphan progress rail, no backing scope item)`,
739
+ remediation: `Add a README.md with frontmatter id, or remove the orphan PROGRESS.md`,
740
+ }],
741
+ frontmatterError: null,
742
+ },
743
+ });
744
+ }
745
+ else {
746
+ const noReadmeResult = classifyScopeItem({
747
+ id: null,
748
+ path: sliceDir,
749
+ readmeFrontmatterRaw: null,
750
+ progressFileExists: false,
751
+ readmeOnlyMarker: false,
752
+ isActiveRelease: true,
753
+ level: "slice",
754
+ });
755
+ sliceResults.push({ name: entry, result: noReadmeResult });
756
+ }
757
+ continue;
758
+ }
759
+ const sliceFm = extractFrontmatterRaw(fs.readFileSync(sliceReadme, "utf-8"));
760
+ const readmeOnlyMarker = sliceFm !== null && /^progress_rail\s*:\s*readme-only/m.test(sliceFm);
761
+ const sliceResult = classifyScopeItem({
762
+ id: null,
763
+ path: sliceDir,
764
+ readmeFrontmatterRaw: sliceFm,
765
+ progressFileExists: fs.existsSync(sliceProgress),
766
+ readmeOnlyMarker,
767
+ isActiveRelease: true,
768
+ level: "slice",
769
+ });
770
+ if (!/^\d{2}-/.test(entry)) {
771
+ sliceResult.findings.push({
772
+ kind: "id_convention_violation",
773
+ severity: "high",
774
+ path: sliceDir,
775
+ message: `Directory "${entry}" does not match the NN-slug slice naming convention (e.g. 01-my-slice)`,
776
+ remediation: `Rename to NN-slug format or move out of slices/`,
777
+ });
778
+ }
779
+ sliceResults.push({ name: entry, result: sliceResult });
780
+ }
781
+ }
782
+ const allFindings = [
783
+ ...missionResult.findings.map((f) => ({ ...f, scope: "mission", scopeName: missionName })),
784
+ ...sliceResults.flatMap((s) => s.result.findings.map((f) => ({ ...f, scope: "slice", scopeName: s.name }))),
785
+ ];
786
+ if (json) {
787
+ out.write(JSON.stringify({
788
+ ok: allFindings.length === 0,
789
+ mission: { name: missionName, railStatus: missionResult.railStatus, frontmatterError: missionResult.frontmatterError, findings: missionResult.findings },
790
+ slices: sliceResults.map((s) => ({ name: s.name, railStatus: s.result.railStatus, frontmatterError: s.result.frontmatterError, findings: s.result.findings })),
791
+ totalFindings: allFindings.length,
792
+ }, null, 2));
793
+ out.write("\n");
794
+ if (allFindings.length > 0)
795
+ process.exitCode = 1;
796
+ return;
797
+ }
798
+ out.write(`Scope audit: ${missionName}\n`);
799
+ out.write(`Mission rail: ${missionResult.railStatus}\n`);
800
+ out.write(`Slices: ${sliceResults.length} total\n\n`);
801
+ if (allFindings.length > 0) {
802
+ out.write("FINDINGS:\n");
803
+ for (const f of allFindings) {
804
+ out.write(` [${f.severity}] [${f.kind}] ${f.scope}/${f.scopeName}\n`);
805
+ out.write(` ${f.message}\n`);
806
+ out.write(` fix: ${f.remediation}\n`);
807
+ }
808
+ out.write(`\nFAIL: ${allFindings.length} finding(s)\n`);
809
+ process.exitCode = 1;
810
+ }
811
+ else {
812
+ out.write("PASS: all scope items have valid rails\n");
813
+ }
814
+ }
815
+ catch (err) {
816
+ if (err instanceof ScopeCliError) {
817
+ fail(err, json, out);
818
+ }
819
+ throw err;
820
+ }
821
+ });
822
+ }
823
+ function extractFrontmatterRaw(content) {
824
+ if (!content.startsWith("---"))
825
+ return null;
826
+ const match = /^---\s*\n([\s\S]*?)\n---/.exec(content);
827
+ return match ? match[1] : null;
828
+ }
829
+ // ---------------------------------------------------------------------
830
+ // rig scope <tier> progress (OPR.0.4.0.33 FR-3 — deterministic update)
831
+ // ---------------------------------------------------------------------
832
+ /** Resolve which file a progress update edits for a scope dir: the
833
+ * PROGRESS.md when present, else the README's rail for a readme-only
834
+ * scope, else an error directing to create/repair (the verb UPDATES an
835
+ * existing surface; it does not scaffold). */
836
+ function resolveProgressTarget(scopeDir, level) {
837
+ const progressPath = path.join(scopeDir, "PROGRESS.md");
838
+ if (fs.existsSync(progressPath))
839
+ return { targetPath: progressPath, kind: "progress" };
840
+ const readmePath = path.join(scopeDir, "README.md");
841
+ if (fs.existsSync(readmePath)) {
842
+ const fm = readFrontmatter(readmePath);
843
+ if (String(fm.progress_rail ?? "") === "readme-only") {
844
+ return { targetPath: readmePath, kind: "readme-only" };
845
+ }
846
+ }
847
+ throw new ScopeCliError({
848
+ fact: `${level} at ${scopeDir} has no progress surface (no PROGRESS.md and no readme-only rail).`,
849
+ consequence: "The progress verb updates an existing surface; it does not scaffold.",
850
+ action: `Backfill it with: rig scope ${level} repair <target> (creates PROGRESS.md), or rig scope ${level} create.`,
851
+ });
852
+ }
853
+ /** Shared body for slice/mission progress: validate the mutually
854
+ * exclusive --add/--set modes, edit the resolved surface, write only
855
+ * on change. */
856
+ function runProgressUpdate(scopeDir, level, scopeName, opts, out, json) {
857
+ const hasAdd = typeof opts.add === "string";
858
+ const hasSet = typeof opts.set === "string";
859
+ if (hasAdd === hasSet) {
860
+ throw new ScopeCliError({
861
+ fact: hasAdd
862
+ ? "Both --add and --set were given."
863
+ : "Neither --add nor --set was given.",
864
+ consequence: "No progress update was made.",
865
+ action: 'Pass exactly one of --add "<row text>" or --set "<row text>".',
866
+ });
867
+ }
868
+ const status = parseStatus(opts.status ?? "active");
869
+ const { targetPath, kind } = resolveProgressTarget(scopeDir, level);
870
+ const before = fs.readFileSync(targetPath, "utf8");
871
+ let result;
872
+ let operation;
873
+ if (hasAdd) {
874
+ operation = "add";
875
+ result = addProgressRow(before, {
876
+ section: opts.section ?? DEFAULT_PROGRESS_SECTION,
877
+ text: opts.add,
878
+ status,
879
+ });
880
+ }
881
+ else {
882
+ operation = "set";
883
+ result = setProgressRow(before, { text: opts.set, status });
884
+ }
885
+ if (result.changed)
886
+ fs.writeFileSync(targetPath, result.content, "utf8");
887
+ emit(out, {
888
+ ok: true,
889
+ progress: {
890
+ scope: level,
891
+ name: scopeName,
892
+ target: targetPath,
893
+ kind,
894
+ operation,
895
+ status,
896
+ changed: result.changed,
897
+ },
898
+ }, json, [
899
+ `${result.changed ? "Updated" : "No change"} ${level} ${scopeName} progress (${operation})`,
900
+ ` target: ${targetPath}`,
901
+ ` status: ${status}`,
902
+ ]);
903
+ }
904
+ function buildSliceProgressCommand() {
905
+ return new Command("progress")
906
+ .description("Update a slice's progress rail deterministically (append a row, or set a row's status)")
907
+ .argument("<slice-path>", "Slice path (absolute, relative, or NN-slug)")
908
+ .option("--mission <name>", "Hint mission when slice-path is just NN-slug")
909
+ .option("--add <text>", "Append a checkbox row with this text")
910
+ .option("--set <text>", "Set the status of the row whose trimmed text exactly matches")
911
+ .option("--section <heading>", `Section heading for --add (default: ${DEFAULT_PROGRESS_SECTION})`)
912
+ .option("--status <status>", `Row status: ${PROGRESS_STATUSES.join(" | ")}`, "active")
913
+ .option("--json", "Machine-readable output")
914
+ .action(async (slicePath, opts, command) => {
915
+ const out = makeStdout();
916
+ const json = Boolean(opts.json);
917
+ try {
918
+ const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
919
+ const slice = findSlice(missionsRoot, slicePath, opts.mission ?? null);
920
+ runProgressUpdate(slice.absPath, "slice", slice.name, opts, out, json);
921
+ }
922
+ catch (err) {
923
+ fail(err, json, out);
924
+ }
925
+ });
926
+ }
927
+ function buildMissionProgressCommand() {
928
+ return new Command("progress")
929
+ .description("Update a mission's progress rail deterministically (append a row, or set a row's status)")
930
+ .argument("<mission>", "Mission name")
931
+ .option("--add <text>", "Append a checkbox row with this text")
932
+ .option("--set <text>", "Set the status of the row whose trimmed text exactly matches")
933
+ .option("--section <heading>", `Section heading for --add (default: ${DEFAULT_PROGRESS_SECTION})`)
934
+ .option("--status <status>", `Row status: ${PROGRESS_STATUSES.join(" | ")}`, "active")
935
+ .option("--json", "Machine-readable output")
936
+ .action(async (missionName, opts, command) => {
937
+ const out = makeStdout();
938
+ const json = Boolean(opts.json);
939
+ try {
940
+ const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
941
+ const mission = findMission(missionsRoot, missionName);
942
+ runProgressUpdate(mission.absPath, "mission", mission.name, opts, out, json);
943
+ }
944
+ catch (err) {
945
+ fail(err, json, out);
946
+ }
947
+ });
948
+ }
949
+ /** Mirror the create-time title derivation so a backfilled PROGRESS.md
950
+ * is byte-identical to what create would have written. */
951
+ function backfillTitle(level, scopeDir) {
952
+ const base = path.basename(scopeDir);
953
+ return level === "mission"
954
+ ? titleFromSlug(base.replace(/^release-/, ""))
955
+ : titleFromSlug(base.replace(/^\d+-/, ""));
956
+ }
957
+ /** Create a missing PROGRESS.md for a single scope dir. Idempotent
958
+ * (skips when one exists) and non-clobbering (skips an intentional
959
+ * readme-only scope, and skips README-less dirs that are not declared
960
+ * scopes). */
961
+ function backfillScopeProgress(scopeDir, level) {
962
+ const name = path.basename(scopeDir);
963
+ const readmePath = path.join(scopeDir, "README.md");
964
+ if (!fs.existsSync(readmePath)) {
965
+ return { scope: level, name, created: false, reason: "no-readme (not a declared scope)", path: null };
966
+ }
967
+ const progressPath = path.join(scopeDir, "PROGRESS.md");
968
+ if (fs.existsSync(progressPath)) {
969
+ return { scope: level, name, created: false, reason: "already-present", path: progressPath };
970
+ }
971
+ const fm = readFrontmatter(readmePath);
972
+ if (String(fm.progress_rail ?? "") === "readme-only") {
973
+ return { scope: level, name, created: false, reason: "readme-only (intentional opt-out)", path: null };
974
+ }
975
+ const title = backfillTitle(level, scopeDir);
976
+ const body = level === "mission"
977
+ ? renderMissionProgressTemplate(title)
978
+ : renderSliceProgressTemplate(title);
979
+ fs.writeFileSync(progressPath, body, "utf8");
980
+ return { scope: level, name, created: true, reason: "backfilled", path: progressPath };
981
+ }
982
+ // ---------------------------------------------------------------------
983
+ // OPR.0.4.1.6 — stage + verified verbs (deterministic §2 maturity edits)
984
+ // ---------------------------------------------------------------------
985
+ /** Validate a stage against the §2 enum, rejecting invented values. */
986
+ function validateStage(raw) {
987
+ if (!STAGE_VALUES.includes(raw)) {
988
+ throw new ScopeCliError({
989
+ fact: `Invalid stage "${raw}".`,
990
+ consequence: "Stage not changed.",
991
+ action: `Use one of: ${STAGE_VALUES.join(" | ")}.`,
992
+ });
993
+ }
994
+ return raw;
995
+ }
996
+ /** Surgically set `stage` (+ `superseded-by` when superseded) on a scope
997
+ * README, enforcing the §2 superseded-needs-successor rule. */
998
+ function applyStage(readmePath, stage, successor) {
999
+ const updates = { stage };
1000
+ if (stage === "superseded") {
1001
+ const id = typeof successor === "string" ? successor.trim() : "";
1002
+ if (!id) {
1003
+ throw new ScopeCliError({
1004
+ fact: "stage 'superseded' requires a successor.",
1005
+ consequence: "Stage not changed (a superseded scope must name what replaces it, per scope-and-versioning §2).",
1006
+ action: "Re-run with --successor <id>, e.g. --successor OPR.0.4.1.7.",
1007
+ });
1008
+ }
1009
+ updates["superseded-by"] = id;
1010
+ }
1011
+ updateFrontmatter(readmePath, updates);
1012
+ }
1013
+ /** `retired` is an exit, not a rung — warn (do not block). */
1014
+ function warnRetired(stage) {
1015
+ if (stage === "retired") {
1016
+ process.stderr.write("Warning: stage 'retired' means do-not-use (an exit, not a maturity rung).\n");
1017
+ }
1018
+ }
1019
+ /** Validate a --against provenance: mandatory, non-empty, non-whitespace
1020
+ * (the §2 "no bare timestamp" rule). Returns the trimmed source. */
1021
+ function validateAgainst(raw) {
1022
+ const source = typeof raw === "string" ? raw.trim() : "";
1023
+ if (!source) {
1024
+ throw new ScopeCliError({
1025
+ fact: "--against provenance is empty or missing.",
1026
+ consequence: "verified not stamped — scope-and-versioning §2 forbids a bare timestamp without a named source.",
1027
+ action: 'Provide what it was verified against, e.g. --against "runtime (npm+tag+origin)".',
1028
+ });
1029
+ }
1030
+ return source;
1031
+ }
1032
+ function buildSliceStageCommand() {
1033
+ return new Command("stage")
1034
+ .description(`Set a slice's epistemic stage (${STAGE_VALUES.join(" | ")}); superseded needs --successor`)
1035
+ .argument("<slice-path>", "Slice path (absolute, relative, or NN-slug)")
1036
+ .argument("<new-stage>", `New stage: ${STAGE_VALUES.join(" | ")}`)
1037
+ .option("--successor <id>", "Successor scope id — REQUIRED when new-stage is superseded")
1038
+ .option("--mission <name>", "Hint mission when slice-path is just NN-slug")
1039
+ .option("--json", "Machine-readable output")
1040
+ .action(async (slicePath, newStage, opts, command) => {
1041
+ const out = makeStdout();
1042
+ const json = Boolean(opts.json);
1043
+ try {
1044
+ const stage = validateStage(newStage);
1045
+ const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
1046
+ const slice = findSlice(missionsRoot, slicePath, opts.mission ?? null);
1047
+ if (!slice.readmePath) {
1048
+ throw new ScopeCliError({
1049
+ fact: `Slice ${slice.name} has no README.md.`,
1050
+ consequence: "Stage is a frontmatter field on the README; nothing to write.",
1051
+ action: "Create the slice README (rig scope slice create) before setting its stage.",
1052
+ });
1053
+ }
1054
+ applyStage(slice.readmePath, stage, opts.successor);
1055
+ warnRetired(stage);
1056
+ const supersededBy = stage === "superseded" ? String(opts.successor).trim() : undefined;
1057
+ emit(out, { ok: true, scope: { tier: "slice", mission: slice.missionName, name: slice.name, id: slice.id, stage, ...(supersededBy ? { supersededBy } : {}) } }, json, [
1058
+ `Set ${slice.missionName}/${slice.name} stage: ${stage}`,
1059
+ ...(supersededBy ? [` superseded-by: ${supersededBy}`] : []),
1060
+ ]);
1061
+ }
1062
+ catch (err) {
1063
+ fail(err, json, out);
1064
+ }
1065
+ });
1066
+ }
1067
+ function buildMissionStageCommand() {
1068
+ return new Command("stage")
1069
+ .description(`Set a mission's epistemic stage (${STAGE_VALUES.join(" | ")}); superseded needs --successor`)
1070
+ .argument("<mission>", "Mission name")
1071
+ .argument("<new-stage>", `New stage: ${STAGE_VALUES.join(" | ")}`)
1072
+ .option("--successor <id>", "Successor scope id — REQUIRED when new-stage is superseded")
1073
+ .option("--json", "Machine-readable output")
1074
+ .action(async (missionName, newStage, opts, command) => {
1075
+ const out = makeStdout();
1076
+ const json = Boolean(opts.json);
1077
+ try {
1078
+ const stage = validateStage(newStage);
1079
+ const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
1080
+ const mission = findMission(missionsRoot, missionName);
1081
+ if (!mission.readmePath) {
1082
+ throw new ScopeCliError({
1083
+ fact: `Mission ${mission.name} has no README.md.`,
1084
+ consequence: "Stage is a frontmatter field on the README; nothing to write.",
1085
+ action: "Create the mission README before setting its stage.",
1086
+ });
1087
+ }
1088
+ applyStage(mission.readmePath, stage, opts.successor);
1089
+ warnRetired(stage);
1090
+ const supersededBy = stage === "superseded" ? String(opts.successor).trim() : undefined;
1091
+ emit(out, { ok: true, scope: { tier: "mission", name: mission.name, id: mission.id, stage, ...(supersededBy ? { supersededBy } : {}) } }, json, [
1092
+ `Set ${mission.name} stage: ${stage}`,
1093
+ ...(supersededBy ? [` superseded-by: ${supersededBy}`] : []),
1094
+ ]);
1095
+ }
1096
+ catch (err) {
1097
+ fail(err, json, out);
1098
+ }
1099
+ });
1100
+ }
1101
+ function buildSliceVerifiedCommand() {
1102
+ return new Command("verified")
1103
+ .description("Stamp a slice's verified line: <today> against <source> (provenance mandatory; overwrites the prior line)")
1104
+ .argument("<slice-path>", "Slice path (absolute, relative, or NN-slug)")
1105
+ .option("--against <source>", "What it was verified against — MANDATORY (no bare timestamps)")
1106
+ .option("--mission <name>", "Hint mission when slice-path is just NN-slug")
1107
+ .option("--json", "Machine-readable output")
1108
+ .action(async (slicePath, opts, command) => {
1109
+ const out = makeStdout();
1110
+ const json = Boolean(opts.json);
1111
+ try {
1112
+ const source = validateAgainst(opts.against);
1113
+ const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
1114
+ const slice = findSlice(missionsRoot, slicePath, opts.mission ?? null);
1115
+ if (!slice.readmePath) {
1116
+ throw new ScopeCliError({
1117
+ fact: `Slice ${slice.name} has no README.md.`,
1118
+ consequence: "verified is a frontmatter field on the README; nothing to write.",
1119
+ action: "Create the slice README before stamping verified.",
1120
+ });
1121
+ }
1122
+ const verified = `${todayDateISO()} against ${source}`;
1123
+ updateFrontmatter(slice.readmePath, { verified });
1124
+ emit(out, { ok: true, scope: { tier: "slice", mission: slice.missionName, name: slice.name, id: slice.id, verified } }, json, [
1125
+ `Stamped ${slice.missionName}/${slice.name} verified: ${verified}`,
1126
+ ]);
1127
+ }
1128
+ catch (err) {
1129
+ fail(err, json, out);
1130
+ }
1131
+ });
1132
+ }
1133
+ function buildMissionVerifiedCommand() {
1134
+ return new Command("verified")
1135
+ .description("Stamp a mission's verified line: <today> against <source> (provenance mandatory; overwrites the prior line)")
1136
+ .argument("<mission>", "Mission name")
1137
+ .option("--against <source>", "What it was verified against — MANDATORY (no bare timestamps)")
1138
+ .option("--json", "Machine-readable output")
1139
+ .action(async (missionName, opts, command) => {
1140
+ const out = makeStdout();
1141
+ const json = Boolean(opts.json);
1142
+ try {
1143
+ const source = validateAgainst(opts.against);
1144
+ const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
1145
+ const mission = findMission(missionsRoot, missionName);
1146
+ if (!mission.readmePath) {
1147
+ throw new ScopeCliError({
1148
+ fact: `Mission ${mission.name} has no README.md.`,
1149
+ consequence: "verified is a frontmatter field on the README; nothing to write.",
1150
+ action: "Create the mission README before stamping verified.",
1151
+ });
1152
+ }
1153
+ const verified = `${todayDateISO()} against ${source}`;
1154
+ updateFrontmatter(mission.readmePath, { verified });
1155
+ emit(out, { ok: true, scope: { tier: "mission", name: mission.name, id: mission.id, verified } }, json, [
1156
+ `Stamped ${mission.name} verified: ${verified}`,
1157
+ ]);
1158
+ }
1159
+ catch (err) {
1160
+ fail(err, json, out);
1161
+ }
1162
+ });
1163
+ }
1164
+ /** Map a legacy `status:` to a §4 migration stage. Default `wip` when absent
1165
+ * or unmapped (the safe floor). */
1166
+ function mapLegacyStatusToStage(status) {
1167
+ const s = typeof status === "string" ? status.toLowerCase().trim() : "";
1168
+ if (s === "placeholder")
1169
+ return "wip";
1170
+ if (s === "draft" || s === "draft-for-comms")
1171
+ return "wip";
1172
+ if (s === "active" || s === "in-flight")
1173
+ return "established";
1174
+ if (s.startsWith("shipped") || s.startsWith("closed"))
1175
+ return "established";
1176
+ if (s === "ready-for-mission" || s === "ready-for-orch-dispatch")
1177
+ return "provisional";
1178
+ return "wip";
1179
+ }
1180
+ /** Idempotently conform a scope README's mandatory frontmatter. Only ADDS
1181
+ * absent fields — never clobbers an existing id/stage/verified. `mintId` is
1182
+ * called only when `id` is absent (it may persist a parent id per §1 lazy
1183
+ * adoption). */
1184
+ function conformReadmeFrontmatter(readmePath, mintId) {
1185
+ const fm = readFrontmatter(readmePath);
1186
+ const updates = {};
1187
+ let idAdded = null;
1188
+ const hasId = typeof fm.id === "string" && fm.id.length > 0;
1189
+ if (!hasId) {
1190
+ const minted = mintId();
1191
+ if (minted) {
1192
+ idAdded = minted;
1193
+ updates.id = minted;
1194
+ }
1195
+ }
1196
+ let stageAdded = null;
1197
+ if (!fm.stage) {
1198
+ stageAdded = mapLegacyStatusToStage(fm.status);
1199
+ updates.stage = stageAdded;
1200
+ }
1201
+ let verifiedAdded = null;
1202
+ if (!fm.verified) {
1203
+ verifiedAdded = `${todayDateISO()} against backfill (rig scope repair)`;
1204
+ updates.verified = verifiedAdded;
1205
+ }
1206
+ const changed = Object.keys(updates).length > 0;
1207
+ if (changed)
1208
+ updateFrontmatter(readmePath, updates);
1209
+ return { idAdded, stageAdded, verifiedAdded, changed };
1210
+ }
1211
+ /** Mint a slice's id from its (persisted) parent mission id + NN — the §1
1212
+ * lazy parent-ID adoption site. Null when the folder has no NN. */
1213
+ function mintSliceIdClosure(slice, missionsRoot) {
1214
+ return () => {
1215
+ if (slice.nn == null)
1216
+ return null;
1217
+ const mission = findMission(missionsRoot, slice.missionName);
1218
+ const missionId = ensureMissionIdPersisted(mission, missionsRoot);
1219
+ return sliceIdFromMission(missionId, slice.nn);
1220
+ };
1221
+ }
1222
+ function conformLines(scope, r) {
1223
+ if (!r.changed)
1224
+ return [` frontmatter: conformant (no change)`];
1225
+ const parts = [];
1226
+ if (r.idAdded)
1227
+ parts.push(`id=${r.idAdded}`);
1228
+ if (r.stageAdded)
1229
+ parts.push(`stage=${r.stageAdded}`);
1230
+ if (r.verifiedAdded)
1231
+ parts.push(`verified=${r.verifiedAdded}`);
1232
+ return [` frontmatter conformed: ${parts.join(", ")}`];
1233
+ }
1234
+ function buildSliceRepairCommand() {
1235
+ return new Command("repair")
1236
+ .description("Backfill a slice's missing PROGRESS.md + conform mandatory frontmatter (id/stage/verified); idempotent")
1237
+ .argument("<slice-path>", "Slice path (absolute, relative, or NN-slug)")
1238
+ .option("--mission <name>", "Hint mission when slice-path is just NN-slug")
1239
+ .option("--json", "Machine-readable output")
1240
+ .action(async (slicePath, opts, command) => {
1241
+ const out = makeStdout();
1242
+ const json = Boolean(opts.json);
1243
+ try {
1244
+ const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
1245
+ const slice = findSlice(missionsRoot, slicePath, opts.mission ?? null);
1246
+ const result = backfillScopeProgress(slice.absPath, "slice");
1247
+ const frontmatter = slice.readmePath
1248
+ ? conformReadmeFrontmatter(slice.readmePath, mintSliceIdClosure(slice, missionsRoot))
1249
+ : { idAdded: null, stageAdded: null, verifiedAdded: null, changed: false };
1250
+ emit(out, { ok: true, result, frontmatter }, json, [
1251
+ `${result.created ? "Backfilled" : "Skipped"} ${slice.name}: ${result.reason}`,
1252
+ ...(result.path ? [` path: ${result.path}`] : []),
1253
+ ...conformLines("slice", frontmatter),
1254
+ ]);
1255
+ }
1256
+ catch (err) {
1257
+ fail(err, json, out);
1258
+ }
1259
+ });
1260
+ }
1261
+ function buildMissionRepairCommand() {
1262
+ return new Command("repair")
1263
+ .description("Backfill missing PROGRESS.md + conform mandatory frontmatter (id/stage/verified) for a mission and its slices; idempotent")
1264
+ .argument("<mission>", "Mission name")
1265
+ .option("--json", "Machine-readable output")
1266
+ .action(async (missionName, opts, command) => {
1267
+ const out = makeStdout();
1268
+ const json = Boolean(opts.json);
1269
+ try {
1270
+ const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
1271
+ const mission = findMission(missionsRoot, missionName);
1272
+ const results = [];
1273
+ results.push(backfillScopeProgress(mission.absPath, "mission"));
1274
+ const slicesDir = path.join(mission.absPath, "slices");
1275
+ if (fs.existsSync(slicesDir)) {
1276
+ for (const entry of fs.readdirSync(slicesDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
1277
+ if (!entry.isDirectory() || !/^\d+-/.test(entry.name))
1278
+ continue;
1279
+ results.push(backfillScopeProgress(path.join(slicesDir, entry.name), "slice"));
1280
+ }
1281
+ }
1282
+ // FR-4: conform mandatory frontmatter — mission first (mints+persists
1283
+ // the mission id), then each slice (child ids derive from the now-
1284
+ // persisted parent id).
1285
+ const conformed = [];
1286
+ if (mission.readmePath) {
1287
+ const fm = conformReadmeFrontmatter(mission.readmePath, () => ensureMissionId(mission, missionsRoot));
1288
+ conformed.push({ scope: "mission", name: mission.name, frontmatter: fm });
1289
+ }
1290
+ const freshMission = findMission(missionsRoot, mission.name);
1291
+ for (const slice of listSlices(freshMission, "all")) {
1292
+ if (!slice.readmePath)
1293
+ continue;
1294
+ const fm = conformReadmeFrontmatter(slice.readmePath, mintSliceIdClosure(slice, missionsRoot));
1295
+ conformed.push({ scope: "slice", name: slice.name, frontmatter: fm });
1296
+ }
1297
+ const created = results.filter((r) => r.created);
1298
+ const fmChanged = conformed.filter((c) => c.frontmatter.changed);
1299
+ emit(out, { ok: true, mission: mission.name, created, results, conformed }, json, [
1300
+ `Repaired ${mission.name}: ${created.length} PROGRESS.md backfilled, ${fmChanged.length} frontmatter conformed`,
1301
+ ...created.map((r) => ` + PROGRESS ${r.scope}/${r.name}`),
1302
+ ...fmChanged.map((c) => ` ~ frontmatter ${c.scope}/${c.name}`),
1303
+ ]);
1304
+ }
1305
+ catch (err) {
1306
+ fail(err, json, out);
1307
+ }
1308
+ });
1309
+ }
1310
+ // ---------------------------------------------------------------------
636
1311
  // Aggregate
637
1312
  // ---------------------------------------------------------------------
638
1313
  export function scopeCommand() {
@@ -646,12 +1321,21 @@ export function scopeCommand() {
646
1321
  slice.addCommand(buildSliceShipCommand());
647
1322
  slice.addCommand(buildSliceCloseCommand());
648
1323
  slice.addCommand(buildSliceMoveCommand());
1324
+ slice.addCommand(buildSliceProgressCommand());
1325
+ slice.addCommand(buildSliceRepairCommand());
1326
+ slice.addCommand(buildSliceStageCommand());
1327
+ slice.addCommand(buildSliceVerifiedCommand());
649
1328
  cmd.addCommand(slice);
650
1329
  const mission = new Command("mission").description("Mission-tier commands");
651
1330
  mission.addCommand(buildMissionLsCommand());
652
1331
  mission.addCommand(buildMissionShowCommand());
653
1332
  mission.addCommand(buildMissionCreateCommand());
1333
+ mission.addCommand(buildMissionProgressCommand());
1334
+ mission.addCommand(buildMissionRepairCommand());
1335
+ mission.addCommand(buildMissionStageCommand());
1336
+ mission.addCommand(buildMissionVerifiedCommand());
654
1337
  cmd.addCommand(mission);
1338
+ cmd.addCommand(buildAuditCommand());
655
1339
  return cmd;
656
1340
  }
657
1341
  // Re-exports for tests.