@openrig/cli 0.4.3 → 0.4.4

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 (287) hide show
  1. package/daemon/assets/guidance/openrig-start.md +6 -0
  2. package/daemon/assets/plugins/openrig-core/skills/mission-slice-sop/SKILL.md +49 -21
  3. package/daemon/assets/plugins/openrig-core/skills/openrig-user/SKILL.md +12 -12
  4. package/daemon/dist/build-info.d.ts +12 -0
  5. package/daemon/dist/build-info.d.ts.map +1 -0
  6. package/daemon/dist/build-info.js +11 -0
  7. package/daemon/dist/build-info.js.map +1 -0
  8. package/daemon/dist/db/migrations/048_queue_item_evidence_ref.d.ts +18 -0
  9. package/daemon/dist/db/migrations/048_queue_item_evidence_ref.d.ts.map +1 -0
  10. package/daemon/dist/db/migrations/048_queue_item_evidence_ref.js +22 -0
  11. package/daemon/dist/db/migrations/048_queue_item_evidence_ref.js.map +1 -0
  12. package/daemon/dist/domain/feed/attention-aggregator.d.ts +31 -0
  13. package/daemon/dist/domain/feed/attention-aggregator.d.ts.map +1 -0
  14. package/daemon/dist/domain/feed/attention-aggregator.js +119 -0
  15. package/daemon/dist/domain/feed/attention-aggregator.js.map +1 -0
  16. package/daemon/dist/domain/files/file-write-service.d.ts +11 -2
  17. package/daemon/dist/domain/files/file-write-service.d.ts.map +1 -1
  18. package/daemon/dist/domain/files/file-write-service.js +77 -0
  19. package/daemon/dist/domain/files/file-write-service.js.map +1 -1
  20. package/daemon/dist/domain/hosts/fanout-contract.d.ts +33 -0
  21. package/daemon/dist/domain/hosts/fanout-contract.d.ts.map +1 -0
  22. package/daemon/dist/domain/hosts/fanout-contract.js +23 -0
  23. package/daemon/dist/domain/hosts/fanout-contract.js.map +1 -0
  24. package/daemon/dist/domain/hosts/hosts-registry-reader.d.ts +49 -0
  25. package/daemon/dist/domain/hosts/hosts-registry-reader.d.ts.map +1 -0
  26. package/daemon/dist/domain/hosts/hosts-registry-reader.js +165 -0
  27. package/daemon/dist/domain/hosts/hosts-registry-reader.js.map +1 -0
  28. package/daemon/dist/domain/hosts/remote-daemon-http.d.ts +35 -0
  29. package/daemon/dist/domain/hosts/remote-daemon-http.d.ts.map +1 -0
  30. package/daemon/dist/domain/hosts/remote-daemon-http.js +105 -0
  31. package/daemon/dist/domain/hosts/remote-daemon-http.js.map +1 -0
  32. package/daemon/dist/domain/human-route-enforcer.d.ts +71 -0
  33. package/daemon/dist/domain/human-route-enforcer.d.ts.map +1 -0
  34. package/daemon/dist/domain/human-route-enforcer.js +80 -0
  35. package/daemon/dist/domain/human-route-enforcer.js.map +1 -0
  36. package/daemon/dist/domain/mission-control/audit-browse.d.ts +8 -0
  37. package/daemon/dist/domain/mission-control/audit-browse.d.ts.map +1 -1
  38. package/daemon/dist/domain/mission-control/audit-browse.js +19 -0
  39. package/daemon/dist/domain/mission-control/audit-browse.js.map +1 -1
  40. package/daemon/dist/domain/mission-control/mission-control-action-log.d.ts +1 -1
  41. package/daemon/dist/domain/mission-control/mission-control-action-log.d.ts.map +1 -1
  42. package/daemon/dist/domain/mission-control/mission-control-action-log.js +4 -0
  43. package/daemon/dist/domain/mission-control/mission-control-action-log.js.map +1 -1
  44. package/daemon/dist/domain/mission-control/mission-control-fleet-cli-capability.js +1 -1
  45. package/daemon/dist/domain/mission-control/mission-control-fleet-cli-capability.js.map +1 -1
  46. package/daemon/dist/domain/mission-control/mission-control-read-layer.js +1 -1
  47. package/daemon/dist/domain/mission-control/mission-control-read-layer.js.map +1 -1
  48. package/daemon/dist/domain/mission-control/mission-control-write-contract.d.ts +28 -0
  49. package/daemon/dist/domain/mission-control/mission-control-write-contract.d.ts.map +1 -1
  50. package/daemon/dist/domain/mission-control/mission-control-write-contract.js +125 -0
  51. package/daemon/dist/domain/mission-control/mission-control-write-contract.js.map +1 -1
  52. package/daemon/dist/domain/ps-projection.d.ts +28 -1
  53. package/daemon/dist/domain/ps-projection.d.ts.map +1 -1
  54. package/daemon/dist/domain/ps-projection.js +39 -0
  55. package/daemon/dist/domain/ps-projection.js.map +1 -1
  56. package/daemon/dist/domain/queue-repository.d.ts +25 -1
  57. package/daemon/dist/domain/queue-repository.d.ts.map +1 -1
  58. package/daemon/dist/domain/queue-repository.js +111 -7
  59. package/daemon/dist/domain/queue-repository.js.map +1 -1
  60. package/daemon/dist/domain/review/brief-spine.d.ts +19 -0
  61. package/daemon/dist/domain/review/brief-spine.d.ts.map +1 -0
  62. package/daemon/dist/domain/review/brief-spine.js +86 -0
  63. package/daemon/dist/domain/review/brief-spine.js.map +1 -0
  64. package/daemon/dist/domain/review/compose.d.ts +228 -0
  65. package/daemon/dist/domain/review/compose.d.ts.map +1 -0
  66. package/daemon/dist/domain/review/compose.js +773 -0
  67. package/daemon/dist/domain/review/compose.js.map +1 -0
  68. package/daemon/dist/domain/review/freeze.d.ts +31 -0
  69. package/daemon/dist/domain/review/freeze.d.ts.map +1 -0
  70. package/daemon/dist/domain/review/freeze.js +257 -0
  71. package/daemon/dist/domain/review/freeze.js.map +1 -0
  72. package/daemon/dist/domain/review/gather.d.ts +100 -0
  73. package/daemon/dist/domain/review/gather.d.ts.map +1 -0
  74. package/daemon/dist/domain/review/gather.js +676 -0
  75. package/daemon/dist/domain/review/gather.js.map +1 -0
  76. package/daemon/dist/domain/review/types.d.ts +258 -0
  77. package/daemon/dist/domain/review/types.d.ts.map +1 -0
  78. package/daemon/dist/domain/review/types.js +35 -0
  79. package/daemon/dist/domain/review/types.js.map +1 -0
  80. package/daemon/dist/domain/scope/scope-approve.d.ts +64 -0
  81. package/daemon/dist/domain/scope/scope-approve.d.ts.map +1 -0
  82. package/daemon/dist/domain/scope/scope-approve.js +153 -0
  83. package/daemon/dist/domain/scope/scope-approve.js.map +1 -0
  84. package/daemon/dist/domain/scope/scope-audit.d.ts +9 -1
  85. package/daemon/dist/domain/scope/scope-audit.d.ts.map +1 -1
  86. package/daemon/dist/domain/scope/scope-audit.js +173 -1
  87. package/daemon/dist/domain/scope/scope-audit.js.map +1 -1
  88. package/daemon/dist/domain/topology/multi-rig-launcher.d.ts +49 -0
  89. package/daemon/dist/domain/topology/multi-rig-launcher.d.ts.map +1 -0
  90. package/daemon/dist/domain/topology/multi-rig-launcher.js +132 -0
  91. package/daemon/dist/domain/topology/multi-rig-launcher.js.map +1 -0
  92. package/daemon/dist/domain/topology/remote-up-leaf.d.ts +29 -0
  93. package/daemon/dist/domain/topology/remote-up-leaf.d.ts.map +1 -0
  94. package/daemon/dist/domain/topology/remote-up-leaf.js +60 -0
  95. package/daemon/dist/domain/topology/remote-up-leaf.js.map +1 -0
  96. package/daemon/dist/domain/topology/topology-manifest.d.ts +34 -0
  97. package/daemon/dist/domain/topology/topology-manifest.d.ts.map +1 -0
  98. package/daemon/dist/domain/topology/topology-manifest.js +195 -0
  99. package/daemon/dist/domain/topology/topology-manifest.js.map +1 -0
  100. package/daemon/dist/domain/types.d.ts +5 -0
  101. package/daemon/dist/domain/types.d.ts.map +1 -1
  102. package/daemon/dist/domain/types.js.map +1 -1
  103. package/daemon/dist/domain/up-command-router.d.ts +1 -1
  104. package/daemon/dist/domain/up-command-router.d.ts.map +1 -1
  105. package/daemon/dist/domain/up-command-router.js +34 -2
  106. package/daemon/dist/domain/up-command-router.js.map +1 -1
  107. package/daemon/dist/domain/user-settings/settings-store.d.ts +17 -0
  108. package/daemon/dist/domain/user-settings/settings-store.d.ts.map +1 -1
  109. package/daemon/dist/domain/user-settings/settings-store.js +109 -0
  110. package/daemon/dist/domain/user-settings/settings-store.js.map +1 -1
  111. package/daemon/dist/domain/workspace/default-workspace-scaffold.d.ts.map +1 -1
  112. package/daemon/dist/domain/workspace/default-workspace-scaffold.js +51 -6
  113. package/daemon/dist/domain/workspace/default-workspace-scaffold.js.map +1 -1
  114. package/daemon/dist/domain/workspace/workspace-doctor.d.ts +11 -0
  115. package/daemon/dist/domain/workspace/workspace-doctor.d.ts.map +1 -1
  116. package/daemon/dist/domain/workspace/workspace-doctor.js +82 -0
  117. package/daemon/dist/domain/workspace/workspace-doctor.js.map +1 -1
  118. package/daemon/dist/routes/config.d.ts.map +1 -1
  119. package/daemon/dist/routes/config.js +15 -6
  120. package/daemon/dist/routes/config.js.map +1 -1
  121. package/daemon/dist/routes/files.d.ts.map +1 -1
  122. package/daemon/dist/routes/files.js +45 -1
  123. package/daemon/dist/routes/files.js.map +1 -1
  124. package/daemon/dist/routes/mission-control.d.ts.map +1 -1
  125. package/daemon/dist/routes/mission-control.js +67 -2
  126. package/daemon/dist/routes/mission-control.js.map +1 -1
  127. package/daemon/dist/routes/queue.d.ts +8 -2
  128. package/daemon/dist/routes/queue.d.ts.map +1 -1
  129. package/daemon/dist/routes/queue.js +44 -4
  130. package/daemon/dist/routes/queue.js.map +1 -1
  131. package/daemon/dist/routes/review.d.ts +3 -0
  132. package/daemon/dist/routes/review.d.ts.map +1 -0
  133. package/daemon/dist/routes/review.js +169 -0
  134. package/daemon/dist/routes/review.js.map +1 -0
  135. package/daemon/dist/routes/scope-approve.d.ts +3 -0
  136. package/daemon/dist/routes/scope-approve.d.ts.map +1 -0
  137. package/daemon/dist/routes/scope-approve.js +55 -0
  138. package/daemon/dist/routes/scope-approve.js.map +1 -0
  139. package/daemon/dist/routes/scope-audit.d.ts.map +1 -1
  140. package/daemon/dist/routes/scope-audit.js +28 -1
  141. package/daemon/dist/routes/scope-audit.js.map +1 -1
  142. package/daemon/dist/routes/slices.d.ts.map +1 -1
  143. package/daemon/dist/routes/slices.js +49 -9
  144. package/daemon/dist/routes/slices.js.map +1 -1
  145. package/daemon/dist/routes/terminal-ws.d.ts.map +1 -1
  146. package/daemon/dist/routes/terminal-ws.js +58 -21
  147. package/daemon/dist/routes/terminal-ws.js.map +1 -1
  148. package/daemon/dist/routes/up.d.ts.map +1 -1
  149. package/daemon/dist/routes/up.js +84 -0
  150. package/daemon/dist/routes/up.js.map +1 -1
  151. package/daemon/dist/routes/workspace.js +2 -2
  152. package/daemon/dist/server.d.ts +1 -0
  153. package/daemon/dist/server.d.ts.map +1 -1
  154. package/daemon/dist/server.js +14 -1
  155. package/daemon/dist/server.js.map +1 -1
  156. package/daemon/dist/startup.d.ts.map +1 -1
  157. package/daemon/dist/startup.js +18 -3
  158. package/daemon/dist/startup.js.map +1 -1
  159. package/daemon/docs/reference/agent-startup-guide.md +1 -1
  160. package/daemon/docs/reference/product-factory-vps-runbook.md +132 -0
  161. package/daemon/docs/reference/sdlc-conventions.md +187 -0
  162. package/daemon/specs/agents/conveyor/builder/agent.yaml +1 -1
  163. package/daemon/specs/agents/conveyor/builder/guidance/role.md +1 -0
  164. package/daemon/specs/agents/conveyor/lead/agent.yaml +1 -1
  165. package/daemon/specs/agents/conveyor/lead/guidance/role.md +1 -0
  166. package/daemon/specs/agents/conveyor/planner/agent.yaml +1 -1
  167. package/daemon/specs/agents/conveyor/planner/guidance/role.md +1 -0
  168. package/daemon/specs/agents/conveyor/reviewer/agent.yaml +1 -1
  169. package/daemon/specs/agents/conveyor/reviewer/guidance/role.md +1 -0
  170. package/daemon/specs/agents/design/product-designer/agent.yaml +1 -1
  171. package/daemon/specs/agents/design/product-designer/guidance/role.md +1 -0
  172. package/daemon/specs/agents/development/implementer/agent.yaml +1 -1
  173. package/daemon/specs/agents/development/implementer/guidance/role.md +1 -0
  174. package/daemon/specs/agents/development/qa/agent.yaml +1 -1
  175. package/daemon/specs/agents/development/qa/guidance/role.md +1 -0
  176. package/daemon/specs/agents/orchestration/orchestrator/agent.yaml +1 -1
  177. package/daemon/specs/agents/orchestration/orchestrator/guidance/role.md +1 -0
  178. package/daemon/specs/agents/product-management/pm/agent.yaml +1 -0
  179. package/daemon/specs/agents/product-management/pm/guidance/role.md +13 -0
  180. package/daemon/specs/agents/review/independent-reviewer/agent.yaml +1 -1
  181. package/daemon/specs/agents/review/independent-reviewer/guidance/role.md +1 -0
  182. package/daemon/specs/agents/shared/agent.yaml +2 -0
  183. package/daemon/specs/agents/shared/skills/core/mission-slice-sop/SKILL.md +115 -0
  184. package/daemon/specs/agents/shared/skills/core/openrig-operator/SKILL.md +4 -4
  185. package/daemon/specs/agents/shared/skills/core/openrig-user/SKILL.md +69 -23
  186. package/daemon/specs/agents/shared/skills/pods/orchestration-team/SKILL.md +2 -2
  187. package/daemon/specs/rigs/focused/pm-team/CULTURE.md +4 -0
  188. package/daemon/specs/rigs/launch/kernel/agents/operator/agent/guidance/role.md +1 -1
  189. package/daemon/specs/rigs/launch/kernel/agents/operator/agent/startup/context.md +1 -1
  190. package/daemon/specs/rigs/preview/product-team/CULTURE.md +4 -0
  191. package/dist/build-info.d.ts +8 -0
  192. package/dist/build-info.d.ts.map +1 -0
  193. package/dist/build-info.js +11 -0
  194. package/dist/build-info.js.map +1 -0
  195. package/dist/commands/config-init-workspace.d.ts.map +1 -1
  196. package/dist/commands/config-init-workspace.js +20 -4
  197. package/dist/commands/config-init-workspace.js.map +1 -1
  198. package/dist/commands/doctor.js +2 -2
  199. package/dist/commands/doctor.js.map +1 -1
  200. package/dist/commands/expand.js +1 -1
  201. package/dist/commands/expand.js.map +1 -1
  202. package/dist/commands/file.d.ts +7 -0
  203. package/dist/commands/file.d.ts.map +1 -0
  204. package/dist/commands/file.js +77 -0
  205. package/dist/commands/file.js.map +1 -0
  206. package/dist/commands/host.d.ts +34 -0
  207. package/dist/commands/host.d.ts.map +1 -0
  208. package/dist/commands/host.js +448 -0
  209. package/dist/commands/host.js.map +1 -0
  210. package/dist/commands/proof.d.ts +54 -0
  211. package/dist/commands/proof.d.ts.map +1 -0
  212. package/dist/commands/proof.js +311 -0
  213. package/dist/commands/proof.js.map +1 -0
  214. package/dist/commands/ps.d.ts +41 -7
  215. package/dist/commands/ps.d.ts.map +1 -1
  216. package/dist/commands/ps.js +343 -131
  217. package/dist/commands/ps.js.map +1 -1
  218. package/dist/commands/queue.d.ts.map +1 -1
  219. package/dist/commands/queue.js +76 -0
  220. package/dist/commands/queue.js.map +1 -1
  221. package/dist/commands/scope.d.ts.map +1 -1
  222. package/dist/commands/scope.js +140 -3
  223. package/dist/commands/scope.js.map +1 -1
  224. package/dist/commands/send.js +13 -4
  225. package/dist/commands/send.js.map +1 -1
  226. package/dist/commands/up.d.ts +2 -0
  227. package/dist/commands/up.d.ts.map +1 -1
  228. package/dist/commands/up.js +69 -2
  229. package/dist/commands/up.js.map +1 -1
  230. package/dist/commands/workspace.js +6 -6
  231. package/dist/commands/workspace.js.map +1 -1
  232. package/dist/config-store.d.ts +4 -0
  233. package/dist/config-store.d.ts.map +1 -1
  234. package/dist/config-store.js +68 -0
  235. package/dist/config-store.js.map +1 -1
  236. package/dist/cross-host-executor.d.ts +1 -0
  237. package/dist/cross-host-executor.d.ts.map +1 -1
  238. package/dist/cross-host-executor.js +4 -1
  239. package/dist/cross-host-executor.js.map +1 -1
  240. package/dist/daemon-lifecycle.d.ts.map +1 -1
  241. package/dist/daemon-lifecycle.js +4 -1
  242. package/dist/daemon-lifecycle.js.map +1 -1
  243. package/dist/host-registry.d.ts +9 -0
  244. package/dist/host-registry.d.ts.map +1 -1
  245. package/dist/host-registry.js +32 -2
  246. package/dist/host-registry.js.map +1 -1
  247. package/dist/index.d.ts.map +1 -1
  248. package/dist/index.js +7 -0
  249. package/dist/index.js.map +1 -1
  250. package/dist/lib/file-transfer.d.ts +89 -0
  251. package/dist/lib/file-transfer.d.ts.map +1 -0
  252. package/dist/lib/file-transfer.js +300 -0
  253. package/dist/lib/file-transfer.js.map +1 -0
  254. package/dist/lib/hosts/fanout-contract.d.ts +33 -0
  255. package/dist/lib/hosts/fanout-contract.d.ts.map +1 -0
  256. package/dist/lib/hosts/fanout-contract.js +23 -0
  257. package/dist/lib/hosts/fanout-contract.js.map +1 -0
  258. package/dist/lib/scope/scope-audit.d.ts +9 -1
  259. package/dist/lib/scope/scope-audit.d.ts.map +1 -1
  260. package/dist/lib/scope/scope-audit.js +173 -1
  261. package/dist/lib/scope/scope-audit.js.map +1 -1
  262. package/dist/lib/scope/templates.d.ts +1 -0
  263. package/dist/lib/scope/templates.d.ts.map +1 -1
  264. package/dist/lib/scope/templates.js +9 -0
  265. package/dist/lib/scope/templates.js.map +1 -1
  266. package/dist/lib/scope-templates/backlog-deprecation.md +16 -0
  267. package/dist/lib/scope-templates/backlog-tech-debt.md +16 -0
  268. package/dist/lib/scope-templates/bug-fix.md +16 -0
  269. package/dist/lib/scope-templates/implementation-prd.md +33 -0
  270. package/dist/lib/scope-templates/mission-placeholder.md +1 -1
  271. package/dist/lib/scope-templates/mission-progress.md +1 -1
  272. package/dist/lib/scope-templates/mission-release.md +1 -1
  273. package/dist/lib/scope-templates/placeholder.md +11 -7
  274. package/dist/lib/scope-templates/proof.md +5 -1
  275. package/dist/lib/scope-templates/release-feature.md +14 -6
  276. package/dist/lib/scope-templates/research.md +16 -0
  277. package/dist/lib/scope-templates/slice-progress.md +1 -1
  278. package/dist/version.d.ts +5 -0
  279. package/dist/version.d.ts.map +1 -1
  280. package/dist/version.js +11 -1
  281. package/dist/version.js.map +1 -1
  282. package/package.json +1 -1
  283. package/ui/dist/assets/index-D69ZhNIr.js +598 -0
  284. package/ui/dist/assets/index-DJYun-8d.css +32 -0
  285. package/ui/dist/index.html +2 -2
  286. package/ui/dist/assets/index-DYZniQcc.js +0 -597
  287. package/ui/dist/assets/index-trcb4Yf_.css +0 -32
@@ -0,0 +1,773 @@
1
+ // Living Notes — the deterministic composer (OPR.0.4.4.20, rebuilt per the
2
+ // CORRECTIVE REDESIGN of 2026-07-05).
3
+ //
4
+ // PURE: `(gathered inputs) -> composed doc`. Same inputs (including the
5
+ // caller-supplied view-time facts nowIso/mainTip/git facts) -> byte-identical
6
+ // output. Every section has a named SSOT and a degrade value; a missing
7
+ // source renders its degrade, never invented content. The gatherer that
8
+ // assembles inputs from disk/queue/git lives beside this file; keeping the
9
+ // core pure is what makes the idempotence AC hold by construction.
10
+ //
11
+ // CORRECTIVE §3.1 — the composer builds ONE renderable structure per slice:
12
+ // the INTENT → PLAN → DELIVERED stack. It no longer emits `sections`,
13
+ // `acceptance`, `compare`, `join`, or a coequal `green` field. The
14
+ // recorded-verdict rigor behind the old green lives on in two places only:
15
+ // the per-deliverable `verified` signal (§11) and the mission ledger's
16
+ // completion green (FR-7 — a mission-altitude fact, not a slice structure).
17
+ import YAML from "yaml";
18
+ import * as posixPath from "node:path/posix";
19
+ import { renderBriefSpine } from "./brief-spine.js";
20
+ import { C1_ARTIFACT_TYPES, C1_VERDICTS, GATE_ROLES, PHASE_LANE_LABELS, } from "./types.js";
21
+ // --- Fixed, visible v1 thresholds (markdown-steered tuning is a named fast-follow) ---
22
+ export const IDLE_WITH_WORK_THRESHOLD_MIN = 30;
23
+ // ---------------------------------------------------------------------------
24
+ // Media refs (shared shape helpers — pure string work, no filesystem)
25
+ // ---------------------------------------------------------------------------
26
+ const IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"]);
27
+ const VIDEO_EXTS = new Set([".mp4", ".webm", ".mov", ".m4v"]);
28
+ function extOf(ref) {
29
+ const clean = ref.split(/[?#]/)[0];
30
+ const i = clean.lastIndexOf(".");
31
+ return i === -1 ? "" : clean.slice(i).toLowerCase();
32
+ }
33
+ export function mediaKind(ref) {
34
+ const ext = extOf(ref);
35
+ if (IMAGE_EXTS.has(ext))
36
+ return "image";
37
+ if (VIDEO_EXTS.has(ext))
38
+ return "video";
39
+ return null;
40
+ }
41
+ /** Markdown/HTML media refs in a source string (http(s) refs excluded —
42
+ * media is co-located slice content, FR-5). */
43
+ export function extractMediaRefs(markdown) {
44
+ if (!markdown)
45
+ return [];
46
+ const refs = [];
47
+ const re = /!\[[^\]]*\]\(([^)]+)\)|<(?:video|img|source)[^>]*\ssrc="([^"]+)"/g;
48
+ for (const m of markdown.matchAll(re)) {
49
+ const ref = (m[1] ?? m[2] ?? "").trim();
50
+ if (ref && !ref.startsWith("http"))
51
+ refs.push(ref);
52
+ }
53
+ return refs;
54
+ }
55
+ /** Normalizes a media ref written relative to `baseDir` (slice-relative "")
56
+ * into a slice-relative path. Returns null when the ref is absolute or
57
+ * escapes the slice dir — the caller records the defect (FR-5), never a
58
+ * silent drop. */
59
+ export function sliceRelativeMediaPath(ref, baseDir) {
60
+ if (ref.startsWith("/"))
61
+ return null;
62
+ const joined = baseDir ? posixPath.join(baseDir, ref) : ref;
63
+ const normalized = posixPath.normalize(joined);
64
+ if (normalized === ".." || normalized.startsWith("../"))
65
+ return null;
66
+ return normalized;
67
+ }
68
+ function toReviewMedia(ref, baseDir) {
69
+ const kind = mediaKind(ref);
70
+ if (!kind)
71
+ return null;
72
+ const src = sliceRelativeMediaPath(ref, baseDir);
73
+ if (!src)
74
+ return null;
75
+ return { kind, src, caption: ref };
76
+ }
77
+ function dedupMedia(media) {
78
+ const seen = new Set();
79
+ return media.filter((m) => {
80
+ if (seen.has(m.src))
81
+ return false;
82
+ seen.add(m.src);
83
+ return true;
84
+ });
85
+ }
86
+ // ---------------------------------------------------------------------------
87
+ // C1 header parsing
88
+ // ---------------------------------------------------------------------------
89
+ /** Parses a proof artifact's YAML frontmatter into a ProofArtifact.
90
+ * Out-of-set / missing verdicts become null (a present artifact is not a
91
+ * verdict — FR-2); the parse never throws on malformed input. Body media
92
+ * refs are captured for the §3.4 curated-proof projection. */
93
+ export function parseC1Header(content, relPath, droppedAtIso) {
94
+ const out = {
95
+ relPath,
96
+ slice: null,
97
+ candidateSha: null,
98
+ artifactType: null,
99
+ verdict: null,
100
+ moneyEvidence: null,
101
+ evidences: [],
102
+ selfCheck: null,
103
+ mediaRefs: [],
104
+ droppedAt: droppedAtIso,
105
+ };
106
+ const m = content.match(/^---\n([\s\S]*?)\n---/);
107
+ out.mediaRefs = extractMediaRefs(m ? content.slice(m[0].length) : content);
108
+ if (!m)
109
+ return out;
110
+ let fm;
111
+ try {
112
+ fm = (YAML.parse(m[1]) ?? {});
113
+ }
114
+ catch {
115
+ return out;
116
+ }
117
+ const str = (v) => (typeof v === "string" && v.trim() ? v.trim() : v != null && typeof v !== "object" ? String(v) : null);
118
+ out.slice = str(fm["slice"]);
119
+ out.candidateSha = str(fm["candidate_sha"]);
120
+ const at = str(fm["artifact_type"]);
121
+ out.artifactType = at && C1_ARTIFACT_TYPES.includes(at) ? at : null;
122
+ const v = str(fm["verdict"]);
123
+ out.verdict = v && C1_VERDICTS.includes(v) ? v : null;
124
+ out.moneyEvidence = str(fm["money_evidence"]);
125
+ out.selfCheck = str(fm["self_check"]);
126
+ const ev = fm["evidences"];
127
+ if (Array.isArray(ev))
128
+ out.evidences = ev.map((e) => String(e)).filter((e) => e.trim().length > 0);
129
+ return out;
130
+ }
131
+ // ---------------------------------------------------------------------------
132
+ // FR-2 — verdict selection, pass-mapping, lineage (KEEP)
133
+ // ---------------------------------------------------------------------------
134
+ /** The pinned pass-mapping ("passing" is never left to interpretation). */
135
+ export function isPassing(artifactType, verdict) {
136
+ if (verdict === null)
137
+ return false;
138
+ if (artifactType === "qa")
139
+ return verdict === "PASS";
140
+ if (artifactType === "adjudication")
141
+ return verdict === "CLEAR" || verdict === "PASS";
142
+ // guard, rev1-r1, rev1-r2
143
+ return verdict === "CLEAR";
144
+ }
145
+ function toneFor(artifactType, verdict) {
146
+ if (verdict === null)
147
+ return "unknown";
148
+ return isPassing(artifactType, verdict) ? "pass" : "fail";
149
+ }
150
+ /** Latest-wins per (candidate_sha, artifact_type) — the ratified C1 selection
151
+ * rule. A non-passing verdict is superseded only by a LATER artifact of the
152
+ * SAME tuple, never by adjacent artifacts, presence, or approval. */
153
+ export function selectWinning(artifacts, candidateSha) {
154
+ const winning = new Map();
155
+ for (const a of artifacts) {
156
+ if (!a.artifactType)
157
+ continue;
158
+ if (candidateSha !== null && a.candidateSha !== candidateSha)
159
+ continue;
160
+ const prev = winning.get(a.artifactType);
161
+ if (!prev || a.droppedAt > prev.droppedAt || (a.droppedAt === prev.droppedAt && a.relPath > prev.relPath)) {
162
+ winning.set(a.artifactType, a);
163
+ }
164
+ }
165
+ return winning;
166
+ }
167
+ /** The candidate under judgment: the candidate_sha of the latest-dropped gate
168
+ * artifact (deterministic; ties broken by relPath). Null when no artifact
169
+ * carries one. */
170
+ export function deriveCandidateSha(artifacts) {
171
+ let best = null;
172
+ for (const a of artifacts) {
173
+ if (!a.candidateSha || !a.artifactType || a.artifactType === "adjudication")
174
+ continue;
175
+ if (!best || a.droppedAt > best.droppedAt || (a.droppedAt === best.droppedAt && a.relPath > best.relPath))
176
+ best = a;
177
+ }
178
+ return best?.candidateSha ?? null;
179
+ }
180
+ export function deriveGateCells(artifacts, candidateSha) {
181
+ const winning = selectWinning(artifacts, candidateSha);
182
+ return GATE_ROLES.map((role) => {
183
+ const a = winning.get(role);
184
+ if (!a || a.verdict === null) {
185
+ return { role, recordedToken: null, tone: "unknown", state: "missing", source: a?.relPath ?? null };
186
+ }
187
+ return {
188
+ role,
189
+ recordedToken: a.verdict,
190
+ tone: toneFor(role, a.verdict),
191
+ state: isPassing(role, a.verdict) ? "passing" : "non-passing",
192
+ source: a.relPath,
193
+ };
194
+ });
195
+ }
196
+ /** Near-tip tolerance for the pre-merge fresh label. */
197
+ const FRESH_BEHIND_TOLERANCE = 3;
198
+ export function composeLineage(candidateSha, git, gateCells) {
199
+ let freshness = "unknown";
200
+ let staleBehind = null;
201
+ if (git.mergeSha !== null) {
202
+ if (git.mergeIsAncestorOfTip !== null)
203
+ freshness = git.mergeIsAncestorOfTip ? "fresh" : "stale";
204
+ }
205
+ else if (candidateSha !== null && git.candidateBehindTip !== null) {
206
+ if (git.candidateBehindTip <= FRESH_BEHIND_TOLERANCE) {
207
+ freshness = "fresh";
208
+ }
209
+ else {
210
+ freshness = "stale";
211
+ staleBehind = git.candidateBehindTip;
212
+ }
213
+ }
214
+ return { candidateSha, mergeSha: git.mergeSha, mainTip: git.mainTip, freshness, staleBehind, gateCells };
215
+ }
216
+ export function lockFrom(stamp) {
217
+ if (!stamp)
218
+ return null;
219
+ return { by: stamp.by, at: stamp.at, auditVerified: stamp.auditRowPresent };
220
+ }
221
+ export function computeRecordedGreen(gateCells, artifacts, candidateSha) {
222
+ if (gateCells.every((c) => c.state === "passing"))
223
+ return { green: true, regime: 1 };
224
+ const adjudication = selectWinning(artifacts, candidateSha).get("adjudication");
225
+ if (adjudication && isPassing("adjudication", adjudication.verdict))
226
+ return { green: true, regime: 2 };
227
+ return { green: false, regime: null };
228
+ }
229
+ /** Convenience for the mission gatherer: derive the ledger green straight
230
+ * from a slice's artifacts (candidate + gate cells derived internally). */
231
+ export function composeRecordedGreenForSlice(artifacts) {
232
+ const candidateSha = deriveCandidateSha(artifacts);
233
+ return computeRecordedGreen(deriveGateCells(artifacts, candidateSha), artifacts, candidateSha);
234
+ }
235
+ // ---------------------------------------------------------------------------
236
+ // FR-1 — markdown structure extraction (KEEP the extractors; the sections/
237
+ // acceptance/compare structures they used to feed are gone)
238
+ // ---------------------------------------------------------------------------
239
+ /** Extracts a `## <heading>` section body, verbatim (character-identical). */
240
+ export function extractSection(markdown, heading) {
241
+ if (!markdown)
242
+ return null;
243
+ const re = new RegExp(`^##\\s+${heading}\\s*$`, "im");
244
+ const m = re.exec(markdown);
245
+ if (!m)
246
+ return null;
247
+ const start = m.index + m[0].length;
248
+ const rest = markdown.slice(start);
249
+ const next = rest.search(/^##?\s/m);
250
+ const body = next === -1 ? rest : rest.slice(0, next);
251
+ return body.replace(/^\n+/, "").replace(/\s+$/, "");
252
+ }
253
+ /** The pinned mini-requirements tier at the PRD top (plan.concise.text). */
254
+ export function extractMiniReqs(prd) {
255
+ return extractSection(prd, "Mini-requirements(?:[^\\n]*)?");
256
+ }
257
+ export function extractProofContract(prd) {
258
+ const body = extractSection(prd, "Proof contract");
259
+ if (!body)
260
+ return [];
261
+ const items = [];
262
+ for (const line of body.split("\n")) {
263
+ const m = line.match(/^\s*-?\s*\[(?:\s|x|X)\]\s+(.+)$/);
264
+ if (!m)
265
+ continue;
266
+ let text = m[1].trim();
267
+ let plannedRef = null;
268
+ const img = text.match(/!\[[^\]]*\]\(([^)]+)\)/);
269
+ if (img) {
270
+ plannedRef = img[1].trim();
271
+ text = text.replace(img[0], "").replace(/\s{2,}/g, " ").trim();
272
+ }
273
+ items.push({ text, plannedRef });
274
+ }
275
+ return items;
276
+ }
277
+ /** Top-down by precedence — locked > review > building > spec > intent —
278
+ * stated explicitly because one signal can satisfy two lanes. */
279
+ export function derivePhase(s) {
280
+ if (s.approved)
281
+ return "locked";
282
+ if (s.verdictOrEvidenceSetPresent)
283
+ return "review";
284
+ if (s.proofArtifactPresent || s.activeQitemPresent)
285
+ return "building";
286
+ if (s.prdPresent)
287
+ return "spec";
288
+ return "intent";
289
+ }
290
+ // ---------------------------------------------------------------------------
291
+ // §3.1 DELIVERED — the redesigned join: planned ↔ curated proof ↔ verified
292
+ // ---------------------------------------------------------------------------
293
+ /** An `evidences:` ref matches a promised item by exact text or 1-based index. */
294
+ function refMatches(ref, promised, index) {
295
+ const trimmed = ref.trim();
296
+ return trimmed === promised[index].text || trimmed === String(index + 1);
297
+ }
298
+ /**
299
+ * `delivered.items` IS the join, reframed (§3.1): each `## Proof contract`
300
+ * deliverable pairs with the CURATED proof media of the artifacts covering it
301
+ * and QA's recorded comparison signal.
302
+ *
303
+ * `verified` binds to the SHIPPED C1 fields (arch F3), never presence:
304
+ * verified — a covering qa|adjudication artifact records the comparison
305
+ * (self_check) AND its recorded verdict is passing;
306
+ * unverified — some covering artifact exists but no passing recorded QA
307
+ * comparison (QA's why-kicked-back note still surfaces);
308
+ * missing — promised, nothing delivered.
309
+ * Fail-open by construction: these are render states, never blocks.
310
+ */
311
+ export function composeDelivered(promised, artifacts) {
312
+ const escaping = new Set();
313
+ const mediaOf = (a) => {
314
+ const out = [];
315
+ for (const ref of a.mediaRefs) {
316
+ const m = toReviewMedia(ref, "proof");
317
+ if (m)
318
+ out.push(m);
319
+ else if (mediaKind(ref))
320
+ escaping.add(ref);
321
+ }
322
+ return out;
323
+ };
324
+ const byLatest = (a, b) => a.droppedAt < b.droppedAt ? 1 : a.droppedAt > b.droppedAt ? -1 : a.relPath.localeCompare(b.relPath);
325
+ const covered = new Set();
326
+ const items = promised.map((p, i) => {
327
+ const covering = artifacts.filter((a) => a.evidences.some((ref) => refMatches(ref, promised, i))).sort(byLatest);
328
+ covering.forEach((a) => covered.add(a.relPath));
329
+ const qaCovering = covering.filter((a) => a.artifactType === "qa" || a.artifactType === "adjudication");
330
+ const verifiedBy = qaCovering.find((a) => a.selfCheck !== null && a.artifactType !== null && isPassing(a.artifactType, a.verdict));
331
+ const noteSource = qaCovering.find((a) => a.selfCheck !== null) ?? covering.find((a) => a.selfCheck !== null);
332
+ const plannedRef = p.plannedRef ? toReviewMedia(p.plannedRef, "") : null;
333
+ if (p.plannedRef && !plannedRef && mediaKind(p.plannedRef))
334
+ escaping.add(p.plannedRef);
335
+ const item = {
336
+ promised: plannedRef ? { text: p.text, plannedRef } : { text: p.text },
337
+ proof: dedupMedia(covering.flatMap(mediaOf)),
338
+ verified: verifiedBy ? "verified" : covering.length > 0 ? "unverified" : "missing",
339
+ };
340
+ const note = noteSource?.selfCheck ?? null;
341
+ if (note)
342
+ item.note = note;
343
+ return item;
344
+ });
345
+ // Helpful-but-unmapped artifacts (§6): their media renders bounded under
346
+ // the extraProof label — visible, never dropped, never a primary-view pile.
347
+ const extraProof = dedupMedia(artifacts
348
+ .filter((a) => !covered.has(a.relPath))
349
+ .sort(byLatest)
350
+ .flatMap(mediaOf));
351
+ return {
352
+ items,
353
+ extraProof,
354
+ missingCount: items.filter((it) => it.verified === "missing").length,
355
+ escapingRefs: [...escaping].sort(),
356
+ };
357
+ }
358
+ function minutesBetween(aIso, bIso) {
359
+ return Math.floor((Date.parse(bIso) - Date.parse(aIso)) / 60_000);
360
+ }
361
+ /** The four ▲ exception rules over captured signals. Every row carries its
362
+ * evidence + crossed threshold; no evidence -> no exception (never a bare
363
+ * accusation). A ▲ is information for the human, invisible to the flagged
364
+ * agent's workflow. */
365
+ export function deriveExceptions(agents, attention, delivered, scopeLabel, nowIso, latestArtifactIso, governingStampIso) {
366
+ const items = [];
367
+ const push = (identity, summary, d) => {
368
+ items.push({
369
+ source: "derived",
370
+ identity,
371
+ summary,
372
+ leg: d.kind,
373
+ where: scopeLabel,
374
+ ageIso: null,
375
+ priority: null,
376
+ tier: null,
377
+ evidenceRef: null,
378
+ unblocks: null,
379
+ qitemId: null,
380
+ destinationSession: null,
381
+ derived: d,
382
+ });
383
+ };
384
+ for (const a of agents) {
385
+ if (a.idle === true && a.holdsCount > 0 && a.idleSinceIso) {
386
+ const idleMin = minutesBetween(a.idleSinceIso, nowIso);
387
+ if (idleMin >= IDLE_WITH_WORK_THRESHOLD_MIN) {
388
+ push(`${a.sessionName}|stuck|${a.idleSinceIso}`, `${a.agentName} looks stuck`, {
389
+ kind: "stuck",
390
+ evidence: `idle ${idleMin}m >= ${IDLE_WITH_WORK_THRESHOLD_MIN}m default · holds ${a.holdsCount}`,
391
+ threshold: `idle-with-work >= ${IDLE_WITH_WORK_THRESHOLD_MIN}m`,
392
+ });
393
+ }
394
+ }
395
+ }
396
+ for (const q of attention) {
397
+ if (q.closureRequiredAtIso && q.closureRequiredAtIso < nowIso) {
398
+ push(`${q.qitemId}|overdue|${q.closureRequiredAtIso}`, `${q.summary ?? q.qitemId} is overdue`, {
399
+ kind: "overdue",
400
+ evidence: `closure required at ${q.closureRequiredAtIso} · now ${nowIso}`,
401
+ threshold: "past closure_required_at",
402
+ });
403
+ }
404
+ }
405
+ if (delivered.promisedCount > 0 && delivered.missingCount > 0) {
406
+ push(`${scopeLabel}|insufficient-proof|${delivered.missingCount}`, `insufficient proof: ${delivered.missingCount}/${delivered.promisedCount} promised items missing`, {
407
+ kind: "insufficient-proof",
408
+ evidence: `${delivered.missingCount} of ${delivered.promisedCount} promised deliverables have no delivered evidence`,
409
+ threshold: "delivered.items MISSING count > 0",
410
+ });
411
+ }
412
+ if (latestArtifactIso && governingStampIso && latestArtifactIso > governingStampIso) {
413
+ push(`${scopeLabel}|stale-after-change|${latestArtifactIso}`, "artifacts changed after the governing stamp", {
414
+ kind: "stale-after-change",
415
+ evidence: `artifact at ${latestArtifactIso} is newer than the stamp at ${governingStampIso}`,
416
+ threshold: "artifact newer than governing stamp",
417
+ });
418
+ }
419
+ return items;
420
+ }
421
+ export function composeNeedsYou(attention, derived, confirmFaithful, computedOver, nowIso) {
422
+ const agentItems = attention.map((q) => ({
423
+ source: "agent",
424
+ identity: q.qitemId,
425
+ summary: q.summary ?? q.qitemId,
426
+ leg: q.leg,
427
+ where: q.where,
428
+ ageIso: q.createdAtIso,
429
+ priority: q.priority,
430
+ tier: q.tier,
431
+ evidenceRef: q.evidenceRef,
432
+ unblocks: q.unblocks,
433
+ qitemId: q.qitemId,
434
+ destinationSession: q.destinationSession,
435
+ derived: null,
436
+ }));
437
+ // One-count identity rule: distinct identities within this scope only.
438
+ const seen = new Set();
439
+ const all = [...agentItems, ...confirmFaithful, ...derived].filter((i) => {
440
+ if (seen.has(i.identity))
441
+ return false;
442
+ seen.add(i.identity);
443
+ return true;
444
+ });
445
+ // Priority-ordered, most-consequential-first: explicit priority rank, then age.
446
+ const rank = { urgent: 0, high: 1, normal: 2, low: 3 };
447
+ all.sort((a, b) => {
448
+ const ra = rank[a.priority ?? "normal"] ?? 2;
449
+ const rb = rank[b.priority ?? "normal"] ?? 2;
450
+ if (ra !== rb)
451
+ return ra - rb;
452
+ return (a.ageIso ?? "9999") < (b.ageIso ?? "9999") ? -1 : 1;
453
+ });
454
+ return {
455
+ items: all,
456
+ provenance: all.length === 0
457
+ ? `0 attention items · 0 parks · 0 unconfirmed proofs — computed from ${computedOver} at ${nowIso}`
458
+ : `computed from ${computedOver} at ${nowIso}`,
459
+ };
460
+ }
461
+ // --- OPR.0.4.4.22 (slice 22): agent-scope exceptions + the rig read root (KEEP) ---
462
+ /** The third NAMED ▲ heuristic's visible v1 default (slice-22 FR-3:
463
+ * too-long-in-state — no transition beyond threshold). Lives HERE because
464
+ * compose.ts is the single threshold home (P2 arch note N2): changes land
465
+ * once and every altitude inherits. Same markdown-steered tuning
466
+ * fast-follow as IDLE_WITH_WORK_THRESHOLD_MIN. */
467
+ export const TOO_LONG_IN_STATE_THRESHOLD_MIN = 120;
468
+ /**
469
+ * Slice-22 FR-3 — the agent-scope ▲ set: exactly the three NAMED heuristics
470
+ * (idle-with-assigned-work, overdue handoff, too-long-in-state). The first
471
+ * two REUSE deriveExceptions' rules (called with zero delivered counts and
472
+ * no artifact/stamp facts, so the slice-only insufficient-proof /
473
+ * stale-after-change rules cannot fire); too-long-in-state is added here —
474
+ * additively, so slice-scope composition is byte-unchanged. No evidence →
475
+ * no exception (unknown is not idle; unknown lastTransition is not
476
+ * too-long).
477
+ */
478
+ export function deriveAgentScopeExceptions(agents, attention, scopeLabel, nowIso) {
479
+ const items = deriveExceptions(agents, attention, { promisedCount: 0, missingCount: 0 }, scopeLabel, nowIso, null, null);
480
+ for (const a of agents) {
481
+ if (a.holdsCount > 0 && a.lastTransitionIso) {
482
+ const sinceMin = minutesBetween(a.lastTransitionIso, nowIso);
483
+ if (sinceMin >= TOO_LONG_IN_STATE_THRESHOLD_MIN) {
484
+ items.push({
485
+ source: "derived",
486
+ identity: `${a.sessionName}|too-long-in-state|${a.lastTransitionIso}`,
487
+ summary: `${a.agentName} has not transitioned in ${sinceMin}m`,
488
+ leg: "stuck",
489
+ where: scopeLabel,
490
+ ageIso: null,
491
+ priority: null,
492
+ tier: null,
493
+ evidenceRef: null,
494
+ unblocks: null,
495
+ qitemId: null,
496
+ destinationSession: null,
497
+ derived: {
498
+ kind: "stuck",
499
+ evidence: `no transition for ${sinceMin}m >= ${TOO_LONG_IN_STATE_THRESHOLD_MIN}m default · holds ${a.holdsCount}`,
500
+ threshold: `too-long-in-state >= ${TOO_LONG_IN_STATE_THRESHOLD_MIN}m`,
501
+ },
502
+ });
503
+ }
504
+ }
505
+ }
506
+ return items;
507
+ }
508
+ /** Slice-22 FR-1..FR-4 — the rig-scope composition root. PURE: same inputs,
509
+ * byte-identical output (idempotence is a money proof). */
510
+ export function composeRigAgents(inputs) {
511
+ const { nowIso } = inputs;
512
+ const scopeLabel = "rig";
513
+ const derived = deriveAgentScopeExceptions(inputs.agents, inputs.overdue, scopeLabel, nowIso);
514
+ const needsYou = composeNeedsYou(inputs.attention, derived, [], `queue+ps (rig scope) · window: ${inputs.rosterWindow}`, nowIso);
515
+ const band = composeAgentsBand(inputs.agents, "rig", derived, nowIso);
516
+ return {
517
+ scope: "rig",
518
+ needsYou,
519
+ agents: {
520
+ ...band,
521
+ provenance: band.rows.length === 0
522
+ ? `no agents holding or recently holding work — computed from queue+ps · window: ${inputs.rosterWindow} · at ${nowIso}`
523
+ : `computed from queue+ps · window: ${inputs.rosterWindow} · at ${nowIso}`,
524
+ // FR-4: one health line per scope, from the transitions log.
525
+ coordinationHealth: `${inputs.handoffsToday} handoffs today · ${inputs.overdueCount} overdue`,
526
+ },
527
+ settled: inputs.settled,
528
+ settledProvenance: inputs.settled.length === 0
529
+ ? `0 handoffs today — computed from queue transitions · window: today · at ${nowIso}`
530
+ : `computed from queue transitions · window: today · at ${nowIso}`,
531
+ composedAt: nowIso,
532
+ };
533
+ }
534
+ /** Region membership derives from work-on-THIS-scope, never rig co-residency. */
535
+ export function composeAgentsBand(agents, scope, exceptions, nowIso) {
536
+ const rows = agents.map((a) => {
537
+ const ex = exceptions.find((e) => e.derived && e.identity.startsWith(`${a.sessionName}|`));
538
+ return {
539
+ agentName: a.agentName,
540
+ runtime: a.runtime,
541
+ stateGlyph: a.parkedOn ? "parked" : a.idle === null ? "unknown" : a.idle ? "idle" : "active",
542
+ doing: a.doing,
543
+ holdsCount: a.holdsCount,
544
+ lastTransitionIso: a.lastTransitionIso,
545
+ exception: ex?.derived ?? null,
546
+ sessionName: a.sessionName,
547
+ slices: a.slices,
548
+ };
549
+ });
550
+ return {
551
+ scope,
552
+ rows,
553
+ provenance: rows.length === 0
554
+ ? `no agents holding or recently holding work — computed from queue at ${nowIso}`
555
+ : `computed from queue at ${nowIso}`,
556
+ coordinationHealth: null,
557
+ };
558
+ }
559
+ /** A self-asserted PASS in the slice's own PROOF.md (never a verdict). */
560
+ export function proofClaimsPass(proofMd) {
561
+ if (!proofMd)
562
+ return false;
563
+ return proofMd
564
+ .split(/\r?\n/)
565
+ .some((line) => /^\s*(?:Closed by:.*\bVerdict:|Verdict:|Result:)\s*PASS\b/i.test(line));
566
+ }
567
+ function sectionMedia(sectionBody, escaping) {
568
+ const out = [];
569
+ for (const ref of extractMediaRefs(sectionBody)) {
570
+ const m = toReviewMedia(ref, "");
571
+ if (m)
572
+ out.push(m);
573
+ else if (mediaKind(ref))
574
+ escaping.add(ref);
575
+ }
576
+ return out;
577
+ }
578
+ export function composeSliceReview(inputs) {
579
+ const { slice, nowIso } = inputs;
580
+ const sliceRef = `${slice.missionId ?? "?"}/slices/${slice.name}`;
581
+ const candidateSha = deriveCandidateSha(inputs.artifacts);
582
+ const gateCells = deriveGateCells(inputs.artifacts, candidateSha);
583
+ const lineage = composeLineage(candidateSha, inputs.git, gateCells);
584
+ const planLock = lockFrom(inputs.approval.spec);
585
+ const deliveredLock = lockFrom(inputs.approval.delivery);
586
+ const escaping = new Set();
587
+ const intentText = extractSection(inputs.readme, "Intent");
588
+ const intentMedia = sectionMedia(intentText, escaping);
589
+ const miniReqs = extractMiniReqs(inputs.prd);
590
+ const planMedia = dedupMedia([
591
+ ...sectionMedia(miniReqs, escaping),
592
+ ...inputs.lockedArtifacts
593
+ .map((a) => (mediaKind(a.path) ? toReviewMedia(a.path, "") : null))
594
+ .filter((m) => m !== null),
595
+ ]);
596
+ const promised = extractProofContract(inputs.prd);
597
+ const delivered = composeDelivered(promised, inputs.artifacts);
598
+ delivered.escapingRefs.forEach((r) => escaping.add(r));
599
+ const claimedPass = proofClaimsPass(inputs.proofMd);
600
+ const anyRecordedVerdict = gateCells.some((c) => c.state !== "missing");
601
+ const evidencePresent = inputs.artifacts.length > 0 || claimedPass;
602
+ const phase = derivePhase({
603
+ prdPresent: inputs.prd !== null,
604
+ proofArtifactPresent: inputs.artifacts.length > 0 || inputs.proofMd !== null,
605
+ activeQitemPresent: inputs.activeQitemPresent,
606
+ verdictOrEvidenceSetPresent: anyRecordedVerdict || claimedPass,
607
+ approved: deliveredLock !== null,
608
+ });
609
+ // Regime 2: evidence present, NO recorded passing state -> confirm-faithful.
610
+ const recordedGreen = computeRecordedGreen(gateCells, inputs.artifacts, candidateSha);
611
+ const confirmFaithful = [];
612
+ if (!recordedGreen.green && evidencePresent && claimedPass) {
613
+ confirmFaithful.push({
614
+ source: "agent",
615
+ identity: `${slice.name}|confirm-faithful|${candidateSha ?? "no-sha"}`,
616
+ summary: "confirm this proof is faithful",
617
+ leg: "confirm-faithful",
618
+ where: sliceRef,
619
+ ageIso: null,
620
+ priority: "high",
621
+ tier: null,
622
+ evidenceRef: `${sliceRef}/PROOF.md`,
623
+ unblocks: `${slice.name} green (regime 2)`,
624
+ qitemId: null,
625
+ destinationSession: null,
626
+ derived: null,
627
+ });
628
+ }
629
+ const latestArtifactIso = inputs.artifacts.reduce((acc, a) => (acc === null || a.droppedAt > acc ? a.droppedAt : acc), null);
630
+ const derived = deriveExceptions(inputs.agents, inputs.attention, { promisedCount: promised.length, missingCount: delivered.missingCount }, sliceRef, nowIso, latestArtifactIso, inputs.approval.delivery?.at ?? null);
631
+ const needsYou = composeNeedsYou(inputs.attention, derived, confirmFaithful, "queue+artifacts", nowIso);
632
+ const agents = composeAgentsBand(inputs.agents, `slice:${slice.name}`, derived, nowIso);
633
+ // FR-5: an out-of-slice media ref = a defect finding, never a silent
634
+ // no-render. Absolute paths AND ../ traversal segments both escape the
635
+ // co-located contract (rev1 fixback at d6135921 — slice-19 containment class).
636
+ const defects = [
637
+ ...inputs.mediaRefs
638
+ .filter((r) => r.startsWith("/") || /(^|\/)\.\.(\/|$)/.test(r))
639
+ .map((r) => r.startsWith("/")
640
+ ? `absolute media path (must be slice-relative): ${r}`
641
+ : `media ref escapes the slice dir (must be co-located): ${r}`),
642
+ ...[...escaping]
643
+ .sort()
644
+ .map((r) => r.startsWith("/")
645
+ ? `absolute media path (must be slice-relative): ${r}`
646
+ : `media ref escapes the slice dir (must be co-located): ${r}`),
647
+ ];
648
+ return {
649
+ slice: slice.name,
650
+ sliceId: slice.id,
651
+ title: slice.title,
652
+ missionId: slice.missionId,
653
+ phase,
654
+ laneLabel: PHASE_LANE_LABELS[phase],
655
+ intent: {
656
+ text: intentText,
657
+ media: intentMedia,
658
+ ssotPath: inputs.readme !== null ? `${sliceRef}/README.md` : null,
659
+ degrade: intentText === null ? "no intent recorded" : null,
660
+ },
661
+ plan: {
662
+ concise: { text: miniReqs, media: planMedia },
663
+ lockedArtifacts: inputs.lockedArtifacts,
664
+ lock: planLock,
665
+ ssotPath: inputs.prd !== null ? `${sliceRef}/IMPLEMENTATION-PRD.md` : null,
666
+ },
667
+ delivered: {
668
+ items: delivered.items,
669
+ extraProof: delivered.extraProof,
670
+ lock: deliveredLock,
671
+ proofDirPath: inputs.proofDirExists ? `${sliceRef}/proof` : null,
672
+ },
673
+ needsYou,
674
+ agents,
675
+ lineage,
676
+ defects,
677
+ composedAt: nowIso,
678
+ };
679
+ }
680
+ export function composeMissionReview(inputs) {
681
+ const { nowIso } = inputs;
682
+ const board = inputs.slices.map(({ review: s, green }) => {
683
+ let stageCell;
684
+ switch (s.phase) {
685
+ case "spec":
686
+ stageCell = s.plan.lock ? `spec-approved ${s.plan.lock.at}` : "spec unstamped";
687
+ break;
688
+ case "building":
689
+ stageCell =
690
+ s.delivered.items.length > 0
691
+ ? `${s.delivered.items.filter((it) => it.verified !== "missing").length}/${s.delivered.items.length} proofs`
692
+ : "building";
693
+ break;
694
+ case "review":
695
+ stageCell = `${green ? "GREEN" : "not green"} · ${s.lineage.mergeSha ?? "UNMERGED"}`;
696
+ break;
697
+ case "locked":
698
+ stageCell = `stamped ${s.delivered.lock?.at ?? "?"}`;
699
+ break;
700
+ default:
701
+ stageCell = "intent";
702
+ }
703
+ const changedSinceStamp = s.needsYou.items.some((i) => i.derived?.kind === "stale-after-change");
704
+ return {
705
+ slice: s.slice,
706
+ title: s.title,
707
+ phase: s.phase,
708
+ laneLabel: s.laneLabel,
709
+ agentsCount: s.agents.rows.length,
710
+ stageCell,
711
+ changedSinceStamp,
712
+ attentionWorthy: s.needsYou.items.length > 0 || changedSinceStamp,
713
+ };
714
+ });
715
+ // The completion ledger — a query over the mission's slice set, never an
716
+ // authored list (omission-proof by construction).
717
+ const ledger = inputs.slices.map(({ review: s, green }) => ({
718
+ slice: s.slice,
719
+ candidateSha: s.lineage.candidateSha,
720
+ gateCells: s.lineage.gateCells,
721
+ mergeSha: s.lineage.mergeSha,
722
+ needsHumanCount: s.needsYou.items.length,
723
+ green,
724
+ }));
725
+ // Cut-complete: TRUE only when EVERY in-cut slice is (a) green, (b) merged,
726
+ // (c) zero open needs-human items — never asserted from a status field.
727
+ const incomplete = ledger.filter((r) => !(r.green && r.mergeSha !== null && r.needsHumanCount === 0));
728
+ const cutComplete = ledger.length > 0 && incomplete.length === 0;
729
+ // Mission NEEDS YOU = the union query (slice ∪ mission ∪ ▲), distinct
730
+ // identities — an item at N altitudes is one item seen from N heights.
731
+ const seen = new Set();
732
+ const unionItems = [];
733
+ for (const { review: s } of inputs.slices) {
734
+ for (const i of s.needsYou.items) {
735
+ if (!seen.has(i.identity)) {
736
+ seen.add(i.identity);
737
+ unionItems.push(i);
738
+ }
739
+ }
740
+ }
741
+ const missionBand = composeNeedsYou(inputs.missionAttention, [], [], "mission queue+slice unions", nowIso);
742
+ for (const i of missionBand.items) {
743
+ if (!seen.has(i.identity)) {
744
+ seen.add(i.identity);
745
+ unionItems.push(i);
746
+ }
747
+ }
748
+ const agents = composeAgentsBand(inputs.agents, `mission:${inputs.mission.name}`, [], nowIso);
749
+ const composed = {
750
+ mission: inputs.mission.name,
751
+ missionId: inputs.mission.id,
752
+ title: inputs.mission.title,
753
+ intent: inputs.mission.intent ?? null,
754
+ briefSpine: { building: "", progress: "", proven: "", needsYou: "" },
755
+ board,
756
+ ledger,
757
+ cutComplete,
758
+ cutCompleteBasis: cutComplete
759
+ ? `all ${ledger.length} in-cut slices green + merged + zero needs-human · computed at ${nowIso}`
760
+ : `${incomplete.length} of ${ledger.length} slices not cut-complete (${incomplete.map((r) => r.slice).join(", ") || "none"}) · computed at ${nowIso}`,
761
+ needsYou: {
762
+ items: unionItems,
763
+ provenance: unionItems.length === 0
764
+ ? `0 attention items across ${inputs.slices.length} slices — computed from queue+artifacts at ${nowIso}`
765
+ : `union of ${inputs.slices.length} slice scopes + mission scope · computed at ${nowIso}`,
766
+ },
767
+ agents,
768
+ composedAt: nowIso,
769
+ };
770
+ composed.briefSpine = renderBriefSpine(composed);
771
+ return composed;
772
+ }
773
+ //# sourceMappingURL=compose.js.map