@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,676 @@
1
+ // Living Notes Packet 2 — the input gatherer (OPR.0.4.4.20).
2
+ //
3
+ // The impure shell around the pure composer: reads slice docs + proof
4
+ // artifacts from disk, attention/agent rows from SQLite, approval stamps
5
+ // from frontmatter (cross-checked against the Packet-1 audit-target
6
+ // contract), and git facts from the workspace's default repo. Every source
7
+ // that cannot be read degrades honestly (nulls / "unknown") — the composer
8
+ // renders the named degrade, never invented content.
9
+ import * as fs from "node:fs";
10
+ import * as path from "node:path";
11
+ import { execFileSync } from "node:child_process";
12
+ import YAML from "yaml";
13
+ import { composeMissionReview, composeRecordedGreenForSlice, composeSliceReview, deriveCandidateSha, extractMediaRefs, extractSection, parseC1Header, } from "./compose.js";
14
+ import { composeAgentsBand, composeRigAgents } from "./compose.js";
15
+ import { isHumanSeatSession } from "../human-route-enforcer.js";
16
+ const ACTIVE_STATES = ["pending", "in-progress", "claimed", "blocked", "handed-off"];
17
+ export class ReviewGatherer {
18
+ db;
19
+ indexer;
20
+ gitRepoPath;
21
+ activityStore;
22
+ now;
23
+ constructor(deps) {
24
+ this.db = deps.db;
25
+ this.indexer = deps.indexer;
26
+ this.gitRepoPath = deps.gitRepoPath ?? null;
27
+ this.activityStore = deps.activityStore ?? null;
28
+ this.now = deps.now ?? (() => new Date().toISOString());
29
+ }
30
+ composeSlice(name) {
31
+ const inputs = this.gatherSlice(name);
32
+ return inputs ? composeSliceReview(inputs) : null;
33
+ }
34
+ /** Composition plus the on-disk context the freeze renderer needs (FR-6). */
35
+ composeSliceWithContext(name) {
36
+ const slice = this.indexer.get(name);
37
+ const inputs = this.gatherSlice(name);
38
+ if (!slice || !inputs)
39
+ return null;
40
+ return { composed: composeSliceReview(inputs), sliceDir: slice.slicePath, mediaRefs: inputs.mediaRefs };
41
+ }
42
+ composeMission(mission) {
43
+ const slices = this.indexer.list().filter((s) => s.missionId === mission);
44
+ if (slices.length === 0 && !this.missionDirExists(mission))
45
+ return null;
46
+ const nowIso = this.now();
47
+ // The ledger's recorded-verdict green rides beside each composed review
48
+ // (FR-7): the gatherer holds the artifacts, so it derives the mission-
49
+ // altitude completion fact the slice contract no longer carries (§11).
50
+ const composed = slices
51
+ .map((s) => {
52
+ const inputs = this.gatherSlice(s.name);
53
+ if (!inputs)
54
+ return null;
55
+ return {
56
+ review: composeSliceReview(inputs),
57
+ green: composeRecordedGreenForSlice(inputs.artifacts).green,
58
+ };
59
+ })
60
+ .filter((s) => s !== null);
61
+ const missionMeta = this.readMissionMeta(mission);
62
+ return composeMissionReview({
63
+ mission: { name: mission, id: missionMeta.id, title: missionMeta.title, intent: missionMeta.intent },
64
+ slices: composed,
65
+ missionAttention: this.attentionForTag(`mission:${mission}`, `slice:`),
66
+ agents: this.agentsForSlices(slices.map((s) => s.name)),
67
+ nowIso,
68
+ });
69
+ }
70
+ /**
71
+ * OPR.0.4.4.22 — the composed rig-agents read root (FR-1..FR-4): NEEDS
72
+ * YOU + AGENTS (health line) + SETTLED at rig scope. Pure projection over
73
+ * queue + hook-activity; nothing contacts an agent. The roster is
74
+ * active-holders UNION recently-holding (transitions-on-scope-TODAY — the
75
+ * plan-review-ruled display window, named in provenance).
76
+ */
77
+ composeRig() {
78
+ const nowIso = this.now();
79
+ const todayStart = `${nowIso.slice(0, 10)}T00:00:00.000Z`;
80
+ const agents = this.withTelemetry(this.rigRoster(todayStart), nowIso);
81
+ const attention = this.attentionAll();
82
+ const overdue = this.overdueWork(nowIso);
83
+ const { settled, handoffsToday } = this.settledToday(todayStart);
84
+ return composeRigAgents({
85
+ agents,
86
+ overdue,
87
+ attention,
88
+ settled,
89
+ handoffsToday,
90
+ overdueCount: overdue.length,
91
+ rosterWindow: "today",
92
+ nowIso,
93
+ });
94
+ }
95
+ /** The scope-parameterized agents projection — ONE contract, all consumers. */
96
+ composeAgents(scope) {
97
+ const nowIso = this.now();
98
+ if (scope === "rig") {
99
+ return composeAgentsBand(this.agentsForSlices(null), scope, [], nowIso);
100
+ }
101
+ if (scope.startsWith("slice:")) {
102
+ const name = scope.slice("slice:".length);
103
+ if (!this.indexer.get(name))
104
+ return null;
105
+ return composeAgentsBand(this.agentsForSlices([name]), scope, [], nowIso);
106
+ }
107
+ const mission = scope.slice("mission:".length);
108
+ const slices = this.indexer.list().filter((s) => s.missionId === mission);
109
+ if (slices.length === 0 && !this.missionDirExists(mission))
110
+ return null;
111
+ return composeAgentsBand(this.agentsForSlices(slices.map((s) => s.name)), scope, [], nowIso);
112
+ }
113
+ // -------------------------------------------------------------------------
114
+ gatherSlice(name) {
115
+ const slice = this.indexer.get(name);
116
+ if (!slice)
117
+ return null;
118
+ const readme = this.readFile(path.join(slice.slicePath, "README.md"));
119
+ const prd = this.readFile(path.join(slice.slicePath, "IMPLEMENTATION-PRD.md"));
120
+ const proofMd = this.readFile(path.join(slice.slicePath, "PROOF.md"));
121
+ const artifacts = this.readProofArtifacts(slice.slicePath);
122
+ const attention = this.attentionForTag(`slice:${name}`);
123
+ const agents = this.agentsForSlices([name]);
124
+ const frontmatter = this.parseFrontmatter(readme);
125
+ const approval = this.gatherApproval(slice, frontmatter);
126
+ const candidateRef = deriveCandidateSha(artifacts);
127
+ return {
128
+ slice: {
129
+ name: slice.name,
130
+ id: typeof frontmatter["id"] === "string" ? frontmatter["id"] : null,
131
+ title: slice.displayName,
132
+ missionId: slice.missionId,
133
+ },
134
+ readme,
135
+ prd,
136
+ proofMd,
137
+ artifacts,
138
+ lockedArtifacts: this.parseLockedArtifacts(frontmatter),
139
+ mediaRefs: this.collectMediaRefs([readme, prd, proofMd]),
140
+ proofDirExists: fs.existsSync(path.join(slice.slicePath, "proof")),
141
+ attention,
142
+ agents,
143
+ activeQitemPresent: this.hasActiveQitem(name, slice),
144
+ git: this.gatherGitFacts(frontmatter, candidateRef),
145
+ approval,
146
+ nowIso: this.now(),
147
+ };
148
+ }
149
+ /** §3.1's ONLY genuinely new datum: the pinned plan set, a frontmatter READ
150
+ * (`locked-artifacts:` list on the slice README — the scope-fs pattern,
151
+ * zero new file kinds, zero new write machinery). Malformed entries are
152
+ * skipped, never invented. */
153
+ parseLockedArtifacts(fm) {
154
+ const raw = fm["locked-artifacts"];
155
+ if (!Array.isArray(raw))
156
+ return [];
157
+ const out = [];
158
+ for (const entry of raw) {
159
+ if (entry === null || typeof entry !== "object")
160
+ continue;
161
+ const e = entry;
162
+ const p = typeof e["path"] === "string" ? e["path"].trim() : "";
163
+ if (!p)
164
+ continue;
165
+ out.push({
166
+ name: typeof e["name"] === "string" && e["name"].trim() ? e["name"].trim() : p,
167
+ path: p,
168
+ kind: typeof e["kind"] === "string" && e["kind"].trim() ? e["kind"].trim() : "artifact",
169
+ });
170
+ }
171
+ return out;
172
+ }
173
+ readFile(p) {
174
+ try {
175
+ return fs.readFileSync(p, "utf8");
176
+ }
177
+ catch {
178
+ return null;
179
+ }
180
+ }
181
+ missionDirExists(mission) {
182
+ // The indexer knows slices; a slice-less mission still composes (empty board).
183
+ return this.indexer.list().some((s) => s.missionId === mission);
184
+ }
185
+ readMissionMeta(mission) {
186
+ const anySlice = this.indexer.list().find((s) => s.missionId === mission);
187
+ if (anySlice) {
188
+ const missionDir = path.dirname(path.dirname(anySlice.slicePath));
189
+ const readme = this.readFile(path.join(missionDir, "README.md"));
190
+ const fm = this.parseFrontmatter(readme);
191
+ // FR-8: the brief's "What & why" projects VERBATIM as the intent opener.
192
+ const brief = this.readFile(path.join(missionDir, "MISSION_BRIEF.md"));
193
+ return {
194
+ id: typeof fm["id"] === "string" ? fm["id"] : null,
195
+ title: typeof fm["title"] === "string" ? fm["title"] : mission,
196
+ intent: extractSection(brief, "What & why"),
197
+ missionDir,
198
+ };
199
+ }
200
+ return { id: null, title: mission, intent: null, missionDir: null };
201
+ }
202
+ /** FR-8 freeze-moment brief write target: {absolute path, current content}. */
203
+ missionBriefTarget(mission) {
204
+ const meta = this.readMissionMeta(mission);
205
+ if (!meta.missionDir)
206
+ return null;
207
+ const briefPath = path.join(meta.missionDir, "MISSION_BRIEF.md");
208
+ const content = this.readFile(briefPath);
209
+ return content === null ? null : { briefPath, content };
210
+ }
211
+ parseFrontmatter(content) {
212
+ if (!content)
213
+ return {};
214
+ const m = content.match(/^---\n([\s\S]*?)\n---/);
215
+ if (!m)
216
+ return {};
217
+ try {
218
+ return (YAML.parse(m[1]) ?? {});
219
+ }
220
+ catch {
221
+ return {};
222
+ }
223
+ }
224
+ readProofArtifacts(sliceDir) {
225
+ const proofDir = path.join(sliceDir, "proof");
226
+ let entries = [];
227
+ try {
228
+ entries = fs.readdirSync(proofDir).filter((f) => f.endsWith(".md"));
229
+ }
230
+ catch {
231
+ return [];
232
+ }
233
+ return entries
234
+ .sort()
235
+ .map((f) => {
236
+ const full = path.join(proofDir, f);
237
+ try {
238
+ const content = fs.readFileSync(full, "utf8");
239
+ const mtime = fs.statSync(full).mtime.toISOString();
240
+ return parseC1Header(content, `proof/${f}`, mtime);
241
+ }
242
+ catch {
243
+ return null;
244
+ }
245
+ })
246
+ .filter((a) => a !== null);
247
+ }
248
+ /** Markdown image/video refs across the composed sources (FR-5 defect scan). */
249
+ collectMediaRefs(sources) {
250
+ return sources.flatMap((s) => extractMediaRefs(s));
251
+ }
252
+ tableExists(name) {
253
+ try {
254
+ const row = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(name);
255
+ return row !== undefined;
256
+ }
257
+ catch {
258
+ return false;
259
+ }
260
+ }
261
+ columnExists(table, column) {
262
+ try {
263
+ const rows = this.db.prepare(`PRAGMA table_info(${table})`).all();
264
+ return rows.some((r) => r.name === column);
265
+ }
266
+ catch {
267
+ return false;
268
+ }
269
+ }
270
+ /** Human-routed attention rows carrying the given tag (the §5 predicate is
271
+ * Packet 1's; until it lands, human-tier/human-dest/park-on-human rows are
272
+ * selected with the same shape). */
273
+ attentionForTag(tag, excludeTagPrefix) {
274
+ if (!this.tableExists("queue_items"))
275
+ return [];
276
+ const hasEvidenceRef = this.columnExists("queue_items", "evidence_ref");
277
+ const summaryCol = this.columnExists("queue_items", "summary") ? "summary" : "NULL AS summary";
278
+ const rows = this.db
279
+ .prepare(`SELECT qitem_id, ts_created, ts_updated, destination_session, state, priority, tier, tags, ${summaryCol},
280
+ blocked_on, closure_required_at${hasEvidenceRef ? ", evidence_ref" : ""}
281
+ FROM queue_items
282
+ WHERE state IN (${ACTIVE_STATES.map(() => "?").join(",")})
283
+ AND tags LIKE ?
284
+ AND (tier = 'human-gate'
285
+ OR destination_session LIKE 'human%'
286
+ OR (state = 'blocked' AND blocked_on LIKE 'human%'))`)
287
+ .all(...ACTIVE_STATES, `%"${tag}"%`);
288
+ return rows
289
+ .filter((r) => {
290
+ if (!excludeTagPrefix)
291
+ return true;
292
+ try {
293
+ const tags = JSON.parse(r.tags ?? "[]") ?? [];
294
+ return !tags.some((t) => t.startsWith(excludeTagPrefix));
295
+ }
296
+ catch {
297
+ return true;
298
+ }
299
+ })
300
+ .map((r) => ({
301
+ qitemId: r.qitem_id,
302
+ summary: r.summary,
303
+ leg: r.state === "blocked" ? "park-on-human" : "human-routed",
304
+ where: r.destination_session,
305
+ createdAtIso: r.ts_created,
306
+ priority: r.priority,
307
+ tier: r.tier,
308
+ evidenceRef: r.evidence_ref ?? null,
309
+ unblocks: r.state === "blocked" ? r.qitem_id : null,
310
+ destinationSession: r.destination_session,
311
+ closureRequiredAtIso: r.closure_required_at,
312
+ }));
313
+ }
314
+ /** Sessions holding active work on the named slices (null = rig-wide).
315
+ * Region membership derives from work-on-scope, never rig co-residency.
316
+ * Runtime/idle telemetry is honest-unknown at v1 (queue-derived only). */
317
+ agentsForSlices(sliceNames) {
318
+ if (!this.tableExists("queue_items"))
319
+ return [];
320
+ const summaryCol = this.columnExists("queue_items", "summary") ? "summary" : "NULL AS summary";
321
+ const rows = this.db
322
+ .prepare(`SELECT qitem_id, ts_created, ts_updated, destination_session, state, priority, tier, tags, ${summaryCol},
323
+ blocked_on, closure_required_at
324
+ FROM queue_items
325
+ WHERE state IN (${ACTIVE_STATES.map(() => "?").join(",")})`)
326
+ .all(...ACTIVE_STATES);
327
+ const bySession = new Map();
328
+ for (const r of rows) {
329
+ if (isHumanSeatSession(r.destination_session))
330
+ continue;
331
+ let tags = [];
332
+ try {
333
+ tags = JSON.parse(r.tags ?? "[]") ?? [];
334
+ }
335
+ catch {
336
+ tags = [];
337
+ }
338
+ const rowSlices = tags.filter((t) => t.startsWith("slice:")).map((t) => t.slice("slice:".length));
339
+ if (sliceNames !== null && !rowSlices.some((s) => sliceNames.includes(s)))
340
+ continue;
341
+ const entry = bySession.get(r.destination_session) ?? { rows: [], slices: new Set() };
342
+ entry.rows.push(r);
343
+ for (const s of rowSlices)
344
+ entry.slices.add(s);
345
+ bySession.set(r.destination_session, entry);
346
+ }
347
+ return [...bySession.entries()]
348
+ .sort(([a], [b]) => a.localeCompare(b))
349
+ .map(([session, e]) => {
350
+ const latest = e.rows.reduce((acc, r) => (r.ts_updated > acc.ts_updated ? r : acc), e.rows[0]);
351
+ const parked = e.rows
352
+ .filter((r) => !!r.blocked_on)
353
+ .reduce((acc, r) => (!acc || r.ts_updated > acc.ts_updated ? r : acc), null);
354
+ return {
355
+ agentName: session.split("@")[0] ?? session,
356
+ sessionName: session,
357
+ runtime: "unknown",
358
+ parkedOn: parked?.blocked_on ?? null,
359
+ idle: null, // honest-unknown: queue rows alone cannot prove liveness
360
+ idleSinceIso: null,
361
+ doing: parked?.summary ?? latest.summary,
362
+ holdsCount: e.rows.length,
363
+ lastTransitionIso: (parked ?? latest).ts_updated,
364
+ slices: [...e.slices].sort(),
365
+ };
366
+ });
367
+ }
368
+ // --- OPR.0.4.4.22 rig-scope helpers (all synchronous SQLite reads) ---
369
+ /** FR-1 roster: agents HOLDING active work on any slice-tagged item, UNION
370
+ * agents RECENTLY holding (their slice-tagged items transitioned today —
371
+ * the ruled display window). Membership derives from work-on-scope,
372
+ * never rig co-residency. */
373
+ rigRoster(todayStartIso) {
374
+ const holders = this.agentsForSlices(null);
375
+ if (!this.tableExists("queue_items"))
376
+ return holders;
377
+ const summaryCol = this.columnExists("queue_items", "summary") ? "summary" : "NULL AS summary";
378
+ // Recently-holding: destination of a slice-tagged qitem whose latest
379
+ // update landed today but is no longer in an active state.
380
+ const rows = this.db
381
+ .prepare(`SELECT qitem_id, ts_created, ts_updated, destination_session, state, priority, tier, tags, ${summaryCol},
382
+ blocked_on, closure_required_at
383
+ FROM queue_items
384
+ WHERE ts_updated >= ?
385
+ AND state NOT IN (${ACTIVE_STATES.map(() => "?").join(",")})
386
+ AND tags LIKE '%"slice:%'`)
387
+ .all(todayStartIso, ...ACTIVE_STATES);
388
+ const known = new Set(holders.map((h) => h.sessionName));
389
+ const recent = new Map();
390
+ for (const r of rows) {
391
+ if (isHumanSeatSession(r.destination_session))
392
+ continue;
393
+ if (known.has(r.destination_session))
394
+ continue;
395
+ let tags = [];
396
+ try {
397
+ tags = JSON.parse(r.tags ?? "[]") ?? [];
398
+ }
399
+ catch {
400
+ tags = [];
401
+ }
402
+ const rowSlices = tags.filter((t) => t.startsWith("slice:")).map((t) => t.slice("slice:".length));
403
+ if (rowSlices.length === 0)
404
+ continue;
405
+ const entry = recent.get(r.destination_session) ?? { latest: r, slices: new Set() };
406
+ if (r.ts_updated > entry.latest.ts_updated)
407
+ entry.latest = r;
408
+ for (const s of rowSlices)
409
+ entry.slices.add(s);
410
+ recent.set(r.destination_session, entry);
411
+ }
412
+ const recentInputs = [...recent.entries()]
413
+ .sort(([a], [b]) => a.localeCompare(b))
414
+ .map(([session, e]) => ({
415
+ agentName: session.split("@")[0] ?? session,
416
+ sessionName: session,
417
+ runtime: "unknown",
418
+ parkedOn: null,
419
+ idle: null,
420
+ idleSinceIso: null,
421
+ // FR-1: an agent with no tracked ACTIVE item renders the truth —
422
+ // itself a coordination signal; nothing is invented.
423
+ doing: "no tracked work item",
424
+ holdsCount: 0,
425
+ lastTransitionIso: e.latest.ts_updated,
426
+ slices: [...e.slices].sort(),
427
+ }));
428
+ return [...holders, ...recentInputs].sort((a, b) => a.sessionName.localeCompare(b.sessionName));
429
+ }
430
+ /** FR-2 telemetry enrichment: runtime from the sessions/nodes tables, the
431
+ * active/idle light from recorded hook activity (AgentActivityStore).
432
+ * Anything unprovable stays honest-unknown — never guessed. */
433
+ withTelemetry(agents, nowIso) {
434
+ const runtimes = this.sessionRuntimes();
435
+ return agents.map((a) => {
436
+ const runtime = runtimes.get(a.sessionName) ?? "unknown";
437
+ let idle = a.idle;
438
+ let idleSinceIso = a.idleSinceIso;
439
+ if (this.activityStore) {
440
+ try {
441
+ const activity = this.activityStore.getLatestForNode({ sessionName: a.sessionName, now: new Date(nowIso) });
442
+ if (activity?.state === "running") {
443
+ idle = false;
444
+ idleSinceIso = null;
445
+ }
446
+ else if (activity?.state === "idle") {
447
+ idle = true;
448
+ idleSinceIso = activity.eventAt ?? activity.sampledAt ?? null;
449
+ }
450
+ // needs_input / unknown / null → stays honest-unknown.
451
+ }
452
+ catch {
453
+ // Telemetry read failure = unknown, never a guess.
454
+ }
455
+ }
456
+ const normalizedRuntime = runtime === "claude-code" || runtime === "codex" || runtime === "terminal" ? runtime : "unknown";
457
+ return { ...a, runtime: normalizedRuntime, idle, idleSinceIso };
458
+ });
459
+ }
460
+ sessionRuntimes() {
461
+ const out = new Map();
462
+ if (!this.tableExists("sessions") || !this.tableExists("nodes"))
463
+ return out;
464
+ try {
465
+ const rows = this.db
466
+ .prepare(`SELECT s.session_name AS session_name, n.runtime AS runtime
467
+ FROM sessions s JOIN nodes n ON n.id = s.node_id
468
+ WHERE s.session_name IS NOT NULL`)
469
+ .all();
470
+ for (const r of rows)
471
+ if (r.runtime)
472
+ out.set(r.session_name, r.runtime);
473
+ }
474
+ catch {
475
+ /* degrade to unknown runtimes */
476
+ }
477
+ return out;
478
+ }
479
+ /** Rig-scope NEEDS YOU: ALL human-routed active items (no tag filter). */
480
+ attentionAll() {
481
+ if (!this.tableExists("queue_items"))
482
+ return [];
483
+ const hasEvidenceRef = this.columnExists("queue_items", "evidence_ref");
484
+ const summaryCol = this.columnExists("queue_items", "summary") ? "summary" : "NULL AS summary";
485
+ const rows = this.db
486
+ .prepare(`SELECT qitem_id, ts_created, ts_updated, destination_session, state, priority, tier, tags, ${summaryCol},
487
+ blocked_on, closure_required_at${hasEvidenceRef ? ", evidence_ref" : ""}
488
+ FROM queue_items
489
+ WHERE state IN (${ACTIVE_STATES.map(() => "?").join(",")})
490
+ AND (tier = 'human-gate'
491
+ OR destination_session LIKE 'human%'
492
+ OR (state = 'blocked' AND blocked_on LIKE 'human%'))`)
493
+ .all(...ACTIVE_STATES);
494
+ return rows.map((r) => ({
495
+ qitemId: r.qitem_id,
496
+ summary: r.summary,
497
+ leg: r.state === "blocked" ? "park-on-human" : "human-routed",
498
+ where: r.destination_session,
499
+ createdAtIso: r.ts_created,
500
+ priority: r.priority,
501
+ tier: r.tier,
502
+ evidenceRef: r.evidence_ref ?? null,
503
+ unblocks: r.state === "blocked" ? r.qitem_id : null,
504
+ destinationSession: r.destination_session,
505
+ closureRequiredAtIso: r.closure_required_at,
506
+ }));
507
+ }
508
+ /** FR-4: today's closed handoffs from the transitions log — the SETTLED
509
+ * band and the health line's handoff count come from the SAME query
510
+ * (two renders, one computation). */
511
+ settledToday(todayStartIso) {
512
+ if (!this.tableExists("queue_transitions"))
513
+ return { settled: [], handoffsToday: 0 };
514
+ const summaryJoin = this.tableExists("queue_items") && this.columnExists("queue_items", "summary")
515
+ ? "LEFT JOIN queue_items q ON q.qitem_id = t.qitem_id"
516
+ : null;
517
+ try {
518
+ const rows = this.db
519
+ .prepare(`SELECT t.qitem_id AS qitem_id, t.ts AS ts, t.actor_session AS actor_session,
520
+ t.closure_target AS closure_target${summaryJoin ? ", q.summary AS summary" : ", NULL AS summary"}
521
+ FROM queue_transitions t
522
+ ${summaryJoin ?? ""}
523
+ WHERE t.closure_reason = 'handed_off_to' AND t.ts >= ?
524
+ ORDER BY t.ts DESC`)
525
+ .all(todayStartIso);
526
+ const settled = rows.map((r) => ({
527
+ fromSession: r.actor_session,
528
+ toSession: r.closure_target ?? "unknown",
529
+ summary: r.summary,
530
+ closedAtIso: r.ts,
531
+ qitemId: r.qitem_id,
532
+ }));
533
+ return { settled, handoffsToday: settled.length };
534
+ }
535
+ catch {
536
+ return { settled: [], handoffsToday: 0 };
537
+ }
538
+ }
539
+ /** FR-3/FR-4: in-progress slice work past closure_required_at, rig-wide.
540
+ * Feeds both the derived NEEDS YOU exception and the health count. */
541
+ overdueWork(nowIso) {
542
+ if (!this.tableExists("queue_items"))
543
+ return [];
544
+ try {
545
+ const summaryCol = this.columnExists("queue_items", "summary") ? "summary" : "NULL AS summary";
546
+ const rows = this.db
547
+ .prepare(`SELECT qitem_id, ts_created, ts_updated, destination_session, state, priority, tier, tags, ${summaryCol},
548
+ blocked_on, closure_required_at
549
+ FROM queue_items
550
+ WHERE state = 'in-progress'
551
+ AND closure_required_at IS NOT NULL
552
+ AND closure_required_at < ?
553
+ AND tags LIKE '%"slice:%'`)
554
+ .all(nowIso);
555
+ return rows.map((r) => ({
556
+ qitemId: r.qitem_id,
557
+ summary: r.summary,
558
+ leg: "overdue",
559
+ where: r.destination_session,
560
+ createdAtIso: r.ts_created,
561
+ priority: r.priority,
562
+ tier: r.tier,
563
+ evidenceRef: null,
564
+ unblocks: null,
565
+ destinationSession: r.destination_session,
566
+ closureRequiredAtIso: r.closure_required_at,
567
+ }));
568
+ }
569
+ catch {
570
+ return [];
571
+ }
572
+ }
573
+ hasActiveQitem(name, slice) {
574
+ if (!this.tableExists("queue_items"))
575
+ return false;
576
+ const row = this.db
577
+ .prepare(`SELECT 1 FROM queue_items WHERE state IN (${ACTIVE_STATES.map(() => "?").join(",")}) AND tags LIKE ? LIMIT 1`)
578
+ .get(...ACTIVE_STATES, `%"slice:${name}"%`);
579
+ if (row)
580
+ return true;
581
+ if (slice.qitemIds.length === 0)
582
+ return false;
583
+ const placeholders = slice.qitemIds.map(() => "?").join(",");
584
+ try {
585
+ return this.db
586
+ .prepare(`SELECT 1 FROM queue_items
587
+ WHERE qitem_id IN (${placeholders})
588
+ AND state IN (${ACTIVE_STATES.map(() => "?").join(",")})
589
+ LIMIT 1`)
590
+ .get(...slice.qitemIds, ...ACTIVE_STATES) !== undefined;
591
+ }
592
+ catch {
593
+ return false;
594
+ }
595
+ }
596
+ /** §4 — the two staged-approval stamps (arch F-A: the SHIPPED verb's
597
+ * frontmatter fields), each cross-checked against the pinned
598
+ * scope-approval audit shape (`approval_scope` inside audit_notes_json).
599
+ * A stamp with no matching row -> auditVerified false (UNVERIFIED stamp,
600
+ * rendered loudly — never a block). */
601
+ gatherApproval(slice, fm) {
602
+ const str = (k) => {
603
+ const v = fm[k];
604
+ if (typeof v === "string" && v.trim())
605
+ return v.trim();
606
+ if (v instanceof Date)
607
+ return v.toISOString();
608
+ return null;
609
+ };
610
+ const sliceId = str("id");
611
+ const auditRowPresent = (approvalScope) => {
612
+ if (!this.tableExists("mission_control_actions"))
613
+ return false;
614
+ try {
615
+ return (this.db
616
+ .prepare(`SELECT 1 FROM mission_control_actions
617
+ WHERE action_verb='approve'
618
+ AND audit_notes_json LIKE ?
619
+ AND (audit_notes_json LIKE ? OR audit_notes_json LIKE ?)
620
+ LIMIT 1`)
621
+ .get(`%"approval_scope":"${approvalScope}"%`,
622
+ // A null slice id must never match an empty scope_id row.
623
+ sliceId ? `%"scope_id":"${sliceId}"%` : `%"scope_id":"${slice.name}"%`, `%${slice.name}%`) !== undefined);
624
+ }
625
+ catch {
626
+ return false;
627
+ }
628
+ };
629
+ const stamp = (byKey, atKey, approvalScope) => {
630
+ const by = str(byKey);
631
+ const at = str(atKey);
632
+ if (!by || !at)
633
+ return null;
634
+ return { by, at, auditRowPresent: auditRowPresent(approvalScope) };
635
+ };
636
+ return {
637
+ spec: stamp("approved-spec-by", "approved-spec-at", "spec"),
638
+ delivery: stamp("approved-by", "approved-at", "delivery"),
639
+ };
640
+ }
641
+ git(args) {
642
+ if (!this.gitRepoPath)
643
+ return null;
644
+ try {
645
+ return execFileSync("git", args, { cwd: this.gitRepoPath, timeout: 4000, encoding: "utf8" }).trim();
646
+ }
647
+ catch {
648
+ return null;
649
+ }
650
+ }
651
+ gatherGitFacts(fm, candidateSha) {
652
+ const mainTip = this.git(["rev-parse", "--short", "HEAD"]) ?? "unknown";
653
+ const sliceId = typeof fm["id"] === "string" ? fm["id"] : null;
654
+ let mergeSha = null;
655
+ if (sliceId) {
656
+ const found = this.git(["log", "--fixed-strings", `--grep=Merge ${sliceId}`, "--format=%h", "-1"]);
657
+ mergeSha = found && found.length > 0 ? found : null;
658
+ }
659
+ let mergeIsAncestorOfTip = null;
660
+ if (mergeSha) {
661
+ const out = this.git(["merge-base", "--is-ancestor", mergeSha, "HEAD"]);
662
+ // exec throws (returns null) on non-ancestor exit 1; success returns "".
663
+ mergeIsAncestorOfTip = out !== null;
664
+ }
665
+ let candidateBehindTip = null;
666
+ if (!mergeSha && candidateSha) {
667
+ const base = this.git(["merge-base", candidateSha, "HEAD"]);
668
+ if (base) {
669
+ const count = this.git(["rev-list", "--count", `${base}..HEAD`]);
670
+ candidateBehindTip = count !== null && /^\d+$/.test(count) ? Number(count) : null;
671
+ }
672
+ }
673
+ return { mainTip, mergeSha, mergeIsAncestorOfTip, candidateBehindTip };
674
+ }
675
+ }
676
+ //# sourceMappingURL=gather.js.map