cyber-sdd 0.7.0 → 0.7.2

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 (150) hide show
  1. package/.claude-plugin/plugin.json +1 -2
  2. package/.codex-plugin/plugin.json +1 -2
  3. package/CHANGELOG.md +320 -0
  4. package/agents/sdd-scanner.md +1 -1
  5. package/package.json +11 -12
  6. package/plugin.json +55 -0
  7. package/skills/align-spec/SKILL.md +1 -1
  8. package/skills/align-spec/scripts/align-spec.mjs +6755 -12
  9. package/skills/blast-estimate/README.md +3 -3
  10. package/skills/blast-estimate/scripts/blast-estimate.mjs +601 -12
  11. package/skills/check-field-mandates/scripts/check-field-mandates.mjs +253 -12
  12. package/skills/check-partition-quality/scripts/check-partition-quality.mjs +235 -12
  13. package/skills/check-plan-safety/SKILL.md +2 -3
  14. package/skills/check-plan-safety/scripts/check-plan-safety.mjs +109 -12
  15. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mjs +250 -12
  16. package/skills/check-project-specs/SKILL.md +1 -1
  17. package/skills/check-project-specs/scripts/check-project-specs.mjs +593 -12
  18. package/skills/check-retired-terms/SKILL.md +2 -3
  19. package/skills/check-retired-terms/scripts/check-retired-terms.mjs +186 -12
  20. package/skills/check-scenario-overlap/SKILL.md +1 -2
  21. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mjs +177 -12
  22. package/skills/check-spec-references/SKILL.md +2 -2
  23. package/skills/check-spec-references/scripts/check-spec-references.mjs +287 -12
  24. package/skills/check-spec-structure/SKILL.md +1 -2
  25. package/skills/check-spec-structure/scripts/check-spec-structure.mjs +243 -12
  26. package/skills/collision-ladder/README.md +2 -2
  27. package/skills/collision-ladder/scripts/collision-ladder.mjs +7047 -12
  28. package/skills/combat-log-governance/SKILL.md +1 -1
  29. package/skills/concept-index/SKILL.md +1 -1
  30. package/skills/concept-index/scripts/concept-index.mjs +183 -12
  31. package/skills/discover-plans/README.md +3 -3
  32. package/skills/discover-plans/SKILL.md +1 -1
  33. package/skills/discover-plans/scripts/discover-plans.mjs +137 -12
  34. package/skills/discover-specs/README.md +3 -3
  35. package/skills/discover-specs/SKILL.md +1 -1
  36. package/skills/discover-specs/scripts/discover-specs.mjs +321 -12
  37. package/skills/doctrine-loop/SKILL.md +1 -1
  38. package/skills/gate-validation-governance/SKILL.md +1 -1
  39. package/skills/init/README.md +2 -2
  40. package/skills/init/scripts/wire-statusline.mjs +168 -12
  41. package/skills/manage-ignore/README.md +3 -3
  42. package/skills/manage-ignore/SKILL.md +1 -2
  43. package/skills/manage-ignore/scripts/manage-ignore.mjs +285 -12
  44. package/skills/manage-scenario-bridge/README.md +3 -3
  45. package/skills/manage-scenario-bridge/SKILL.md +1 -1
  46. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mjs +239 -12
  47. package/skills/manage-spec-anchors/README.md +3 -3
  48. package/skills/manage-spec-anchors/SKILL.md +1 -1
  49. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mjs +310 -12
  50. package/skills/mission-graph/README.md +2 -2
  51. package/skills/mission-graph/SKILL.md +1 -1
  52. package/skills/mission-graph/scripts/mission-graph.mjs +1040 -12
  53. package/skills/pause-mission/SKILL.md +1 -1
  54. package/skills/place-node/SKILL.md +2 -2
  55. package/skills/place-node/scripts/place-node.mjs +117 -12
  56. package/skills/plan-retirement/README.md +2 -2
  57. package/skills/plan-retirement/scripts/retire-plans.mjs +123 -12
  58. package/skills/resolve-governances/README.md +3 -3
  59. package/skills/resolve-governances/SKILL.md +1 -1
  60. package/skills/resolve-governances/scripts/resolve-governances.mjs +352 -12
  61. package/skills/resolve-tracking/SKILL.md +1 -1
  62. package/skills/resolve-tracking/scripts/resolve-tracking.mjs +152 -12
  63. package/skills/scaffold-project-spec/SKILL.md +1 -1
  64. package/skills/spec-gate/README.md +12 -9
  65. package/skills/spec-gate/SKILL.md +2 -2
  66. package/skills/spec-gate/scripts/check-spec-state.mjs +422 -12
  67. package/skills/spec-gate/scripts/check-suite.mjs +6500 -12
  68. package/skills/spec-gate/scripts/classify-edit-class.mjs +6579 -12
  69. package/skills/spec-producer-governance/SKILL.md +2 -2
  70. package/skills/start-mission/SKILL.md +1 -1
  71. package/skills/suite-format-governance/README.md +1 -1
  72. package/skills/suite-format-governance/SKILL.md +1 -1
  73. package/skills/touch-set-correction/README.md +5 -5
  74. package/skills/touch-set-correction/SKILL.md +1 -1
  75. package/skills/touch-set-correction/scripts/touch-set-correction.mjs +6663 -12
  76. package/skills/verify-scenarios/README.md +6 -6
  77. package/skills/verify-scenarios/scripts/verify-scenarios.mjs +6539 -12
  78. package/.plugin/pins.json +0 -3
  79. package/.plugin/plugin.json +0 -19
  80. package/dist/align-spec/scripts/align-spec.js +0 -102
  81. package/dist/check-project-specs/scripts/check-project-specs.js +0 -312
  82. package/dist/check-scenario-overlap/scripts/check-scenario-overlap.js +0 -175
  83. package/dist/check-spec-references/scripts/check-spec-references.js +0 -285
  84. package/dist/check-spec-structure/scripts/check-spec-structure.js +0 -241
  85. package/dist/concept-index/scripts/concept-index.js +0 -181
  86. package/dist/discover-specs/scripts/discover-specs.js +0 -318
  87. package/dist/spec-gate/scripts/check-spec-state.js +0 -420
  88. package/dist/spec-gate/scripts/check-suite.js +0 -317
  89. package/dist/spec-gate/scripts/classify-edit-class.js +0 -263
  90. package/dist/spec-gate/vendor/gherkin-cli.js +0 -6369
  91. package/skills/align-spec/scripts/align-spec.bundle.mjs +0 -6753
  92. package/skills/align-spec/scripts/align-spec.mts +0 -188
  93. package/skills/blast-estimate/scripts/blast-estimate.bundle.mjs +0 -593
  94. package/skills/blast-estimate/scripts/blast-estimate.mts +0 -586
  95. package/skills/check-field-mandates/scripts/check-field-mandates.bundle.mjs +0 -251
  96. package/skills/check-field-mandates/scripts/check-field-mandates.mts +0 -337
  97. package/skills/check-partition-quality/scripts/check-partition-quality.bundle.mjs +0 -233
  98. package/skills/check-partition-quality/scripts/check-partition-quality.mts +0 -338
  99. package/skills/check-plan-safety/scripts/check-plan-safety.bundle.mjs +0 -107
  100. package/skills/check-plan-safety/scripts/check-plan-safety.mts +0 -148
  101. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.bundle.mjs +0 -248
  102. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mts +0 -303
  103. package/skills/check-project-specs/scripts/check-project-specs.bundle.mjs +0 -584
  104. package/skills/check-project-specs/scripts/check-project-specs.mts +0 -370
  105. package/skills/check-retired-terms/scripts/check-retired-terms.bundle.mjs +0 -184
  106. package/skills/check-retired-terms/scripts/check-retired-terms.mts +0 -293
  107. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.bundle.mjs +0 -175
  108. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +0 -250
  109. package/skills/check-spec-references/scripts/check-spec-references.bundle.mjs +0 -285
  110. package/skills/check-spec-references/scripts/check-spec-references.mts +0 -422
  111. package/skills/check-spec-structure/scripts/check-spec-structure.bundle.mjs +0 -241
  112. package/skills/check-spec-structure/scripts/check-spec-structure.mts +0 -347
  113. package/skills/collision-ladder/scripts/collision-ladder.bundle.mjs +0 -7041
  114. package/skills/collision-ladder/scripts/collision-ladder.mts +0 -662
  115. package/skills/concept-index/scripts/concept-index.bundle.mjs +0 -181
  116. package/skills/concept-index/scripts/concept-index.mts +0 -246
  117. package/skills/discover-plans/scripts/discover-plans.bundle.mjs +0 -135
  118. package/skills/discover-plans/scripts/discover-plans.mts +0 -215
  119. package/skills/discover-specs/scripts/discover-specs.bundle.mjs +0 -319
  120. package/skills/discover-specs/scripts/discover-specs.mts +0 -420
  121. package/skills/init/scripts/wire-statusline.bundle.mjs +0 -166
  122. package/skills/init/scripts/wire-statusline.mts +0 -279
  123. package/skills/manage-ignore/scripts/manage-ignore.bundle.mjs +0 -244
  124. package/skills/manage-ignore/scripts/manage-ignore.mts +0 -297
  125. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.bundle.mjs +0 -237
  126. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +0 -159
  127. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.bundle.mjs +0 -308
  128. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +0 -331
  129. package/skills/mission-graph/scripts/mission-graph.bundle.mjs +0 -1038
  130. package/skills/mission-graph/scripts/mission-graph.mts +0 -1333
  131. package/skills/place-node/scripts/place-node.bundle.mjs +0 -115
  132. package/skills/place-node/scripts/place-node.mts +0 -158
  133. package/skills/plan-retirement/scripts/retire-plans.bundle.mjs +0 -121
  134. package/skills/plan-retirement/scripts/retire-plans.mts +0 -205
  135. package/skills/resolve-governances/scripts/resolve-governances.bundle.mjs +0 -350
  136. package/skills/resolve-governances/scripts/resolve-governances.mts +0 -518
  137. package/skills/resolve-tracking/scripts/resolve-tracking.bundle.mjs +0 -150
  138. package/skills/resolve-tracking/scripts/resolve-tracking.mts +0 -214
  139. package/skills/spec-gate/scripts/check-spec-state.bundle.mjs +0 -420
  140. package/skills/spec-gate/scripts/check-spec-state.mts +0 -637
  141. package/skills/spec-gate/scripts/check-suite.bundle.mjs +0 -6498
  142. package/skills/spec-gate/scripts/check-suite.mts +0 -538
  143. package/skills/spec-gate/scripts/classify-edit-class.bundle.mjs +0 -6577
  144. package/skills/spec-gate/scripts/classify-edit-class.mts +0 -414
  145. package/skills/spec-gate/vendor/gherkin-cli.d.mts +0 -2
  146. package/skills/spec-gate/vendor/gherkin-cli.mjs +0 -6545
  147. package/skills/touch-set-correction/scripts/touch-set-correction.bundle.mjs +0 -6655
  148. package/skills/touch-set-correction/scripts/touch-set-correction.mts +0 -429
  149. package/skills/verify-scenarios/scripts/verify-scenarios.bundle.mjs +0 -6537
  150. package/skills/verify-scenarios/scripts/verify-scenarios.mts +0 -470
@@ -1,1038 +0,0 @@
1
- #!/usr/bin/env node
2
- import { execFileSync } from "node:child_process";
3
- import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync } from "node:fs";
4
- import { dirname, join } from "node:path";
5
- import { pathToFileURL } from "node:url";
6
- //#region skills/mission-graph/scripts/mission-graph.mts
7
- /** The shared default project — a REAL project every project-less node belongs to together, not a
8
- * wildcard that matches every project. */
9
- const DEFAULT_PROJECT = "";
10
- const NODE_DEFAULTS = {
11
- kind: "mission",
12
- status: "open",
13
- blast: "unknown",
14
- hitlOrAfk: "hitl",
15
- modelTier: "unspecified",
16
- briefPointer: "",
17
- project: "",
18
- barrier: false
19
- };
20
- function edgeKey(kind, from, to) {
21
- return JSON.stringify([
22
- kind,
23
- from,
24
- to
25
- ]);
26
- }
27
- /**
28
- * fold — read the whole append-only event log and work out the current picture: the latest
29
- * fields of each node win, a tombstone drops its target (node or edge) from the result, and a
30
- * field the reader doesn't recognize rides along unread — no known derivation looks at it, so a
31
- * later additive schema field never needs a migration or breaks the fold.
32
- */
33
- function fold(events) {
34
- const nodeFields = /* @__PURE__ */ new Map();
35
- const tombstonedNodes = /* @__PURE__ */ new Set();
36
- const edges = /* @__PURE__ */ new Map();
37
- for (const ev of events) if (ev.type === "node") {
38
- const merged = { ...nodeFields.get(ev.id) ?? {} };
39
- for (const [key, value] of Object.entries(ev)) {
40
- if (key === "type" || value === void 0) continue;
41
- merged[key] = value;
42
- }
43
- nodeFields.set(ev.id, merged);
44
- } else if (ev.type === "edge") edges.set(edgeKey(ev.kind, ev.from, ev.to), {
45
- kind: ev.kind,
46
- from: ev.from,
47
- to: ev.to
48
- });
49
- else if (ev.type === "tombstone") {
50
- if (ev.target === "node" && ev.id !== void 0) tombstonedNodes.add(ev.id);
51
- else if (ev.target === "edge" && ev.kind !== void 0 && ev.from !== void 0 && ev.to !== void 0) edges.delete(edgeKey(ev.kind, ev.from, ev.to));
52
- }
53
- const nodes = /* @__PURE__ */ new Map();
54
- for (const [id, fields] of nodeFields) {
55
- if (tombstonedNodes.has(id)) continue;
56
- nodes.set(id, {
57
- id,
58
- kind: fields.kind ?? NODE_DEFAULTS.kind,
59
- status: fields.status ?? NODE_DEFAULTS.status,
60
- touchSet: Array.isArray(fields.touchSet) ? [...fields.touchSet] : [],
61
- blast: typeof fields.blast === "string" ? fields.blast : NODE_DEFAULTS.blast,
62
- hitlOrAfk: fields.hitlOrAfk ?? NODE_DEFAULTS.hitlOrAfk,
63
- modelTier: typeof fields.modelTier === "string" ? fields.modelTier : NODE_DEFAULTS.modelTier,
64
- briefPointer: typeof fields.briefPointer === "string" ? fields.briefPointer : NODE_DEFAULTS.briefPointer,
65
- origin: Array.isArray(fields.origin) ? [...fields.origin] : [],
66
- capstone: typeof fields.capstone === "string" ? fields.capstone : void 0,
67
- project: typeof fields.project === "string" ? fields.project : NODE_DEFAULTS.project,
68
- barrier: typeof fields.barrier === "boolean" ? fields.barrier : NODE_DEFAULTS.barrier,
69
- schemaVersion: typeof fields.v === "number" ? fields.v : 1
70
- });
71
- }
72
- return {
73
- nodes,
74
- edges: [...edges.values()].filter((e) => nodes.has(e.from) && nodes.has(e.to))
75
- };
76
- }
77
- function extractNumber(id) {
78
- const m = /(\d+)/.exec(id);
79
- return m ? Number(m[1]) : null;
80
- }
81
- /**
82
- * compareIds — the pinned mission-ref tie-break: numeric-aware so "#136" sorts before "#137" and
83
- * "M2" before "M10" (a plain string sort would get the latter backwards); falls back to plain
84
- * string order when either side carries no digits. Deterministic, never random.
85
- */
86
- function compareIds(a, b) {
87
- const na = extractNumber(a);
88
- const nb = extractNumber(b);
89
- if (na !== null && nb !== null && na !== nb) return na - nb;
90
- return a < b ? -1 : a > b ? 1 : 0;
91
- }
92
- function intersects(a, b) {
93
- if (a.length === 0 || b.length === 0) return false;
94
- const set = new Set(a);
95
- return b.some((x) => set.has(x));
96
- }
97
- /** Every node id with a RAW edge pointing at `id` (its direct "must finish before" predecessors). */
98
- function rawPredecessors(graph, id) {
99
- return graph.edges.filter((e) => e.kind === "RAW" && e.to === id).map((e) => e.from);
100
- }
101
- function rawAdjacency(graph) {
102
- const adj = /* @__PURE__ */ new Map();
103
- for (const id of graph.nodes.keys()) adj.set(id, []);
104
- for (const e of graph.edges) {
105
- if (e.kind !== "RAW") continue;
106
- const list = adj.get(e.from) ?? [];
107
- list.push(e.to);
108
- adj.set(e.from, list);
109
- }
110
- return adj;
111
- }
112
- /** Tarjan's strongly-connected-components pass, restricted to RAW edges. */
113
- function computeSCCs(graph) {
114
- const adj = rawAdjacency(graph);
115
- let index = 0;
116
- const indices = /* @__PURE__ */ new Map();
117
- const lowlink = /* @__PURE__ */ new Map();
118
- const onStack = /* @__PURE__ */ new Set();
119
- const stack = [];
120
- const sccs = [];
121
- function strongconnect(v) {
122
- indices.set(v, index);
123
- lowlink.set(v, index);
124
- index += 1;
125
- stack.push(v);
126
- onStack.add(v);
127
- for (const w of adj.get(v) ?? []) if (!indices.has(w)) {
128
- strongconnect(w);
129
- lowlink.set(v, Math.min(lowlink.get(v) ?? 0, lowlink.get(w) ?? 0));
130
- } else if (onStack.has(w)) lowlink.set(v, Math.min(lowlink.get(v) ?? 0, indices.get(w) ?? 0));
131
- if (lowlink.get(v) === indices.get(v)) {
132
- const component = [];
133
- let w;
134
- do {
135
- w = stack.pop();
136
- if (w === void 0) break;
137
- onStack.delete(w);
138
- component.push(w);
139
- } while (w !== v);
140
- sccs.push(component);
141
- }
142
- }
143
- for (const v of [...graph.nodes.keys()].sort(compareIds)) if (!indices.has(v)) strongconnect(v);
144
- return sccs;
145
- }
146
- /**
147
- * cycles — reports every RAW strongly-connected component of size > 1, plus any single-node RAW
148
- * self-loop, as a repair item. An acyclic graph reports none. Never throws — a knotted plan is
149
- * surfaced, not crashed on.
150
- */
151
- function cycles(graph) {
152
- const sccs = computeSCCs(graph);
153
- const selfLoop = new Set(graph.edges.filter((e) => e.kind === "RAW" && e.from === e.to).map((e) => e.from));
154
- const items = [];
155
- for (const comp of sccs) {
156
- if (!(comp.length > 1 || selfLoop.has(comp[0]))) continue;
157
- items.push({ members: [...comp].sort(compareIds) });
158
- }
159
- return items.sort((a, b) => compareIds(a.members[0], b.members[0]));
160
- }
161
- /** Every mission id sitting on a RAW cycle — excluded from `ready` and from unblocking anything
162
- * that depends on it; the only way clear is repairing the plan (a merge, or a tombstoned edge). */
163
- function quarantinedIds(graph) {
164
- const ids = /* @__PURE__ */ new Set();
165
- for (const item of cycles(graph)) for (const m of item.members) ids.add(m);
166
- return ids;
167
- }
168
- function canReach(graph, start, target) {
169
- const visited = /* @__PURE__ */ new Set();
170
- const stack = [start];
171
- while (stack.length > 0) {
172
- const cur = stack.pop();
173
- if (cur === void 0) continue;
174
- if (cur === target) return true;
175
- if (visited.has(cur)) continue;
176
- visited.add(cur);
177
- for (const e of graph.edges) if (e.kind === "RAW" && e.from === cur) stack.push(e.to);
178
- }
179
- return false;
180
- }
181
- /** Would appending this edge close a RAW cycle? Only RAW ("wait for") edges can — parent-child
182
- * and discovered-from are exempt from the guard entirely. */
183
- function wouldCloseCycle(graph, proposed) {
184
- if (proposed.kind !== "RAW") return false;
185
- if (proposed.from === proposed.to) return true;
186
- return canReach(graph, proposed.to, proposed.from);
187
- }
188
- /**
189
- * proposeEdge — the gentle write-time guard: rejects a RAW edge that would close a cycle unless
190
- * the writer passes `override: true` to record a genuinely-discovered mutual dependency.
191
- */
192
- function proposeEdge(graph, proposed, override = false) {
193
- if (!override && wouldCloseCycle(graph, proposed)) return {
194
- accepted: false,
195
- reason: `would close a RAW cycle (${proposed.to} already reaches ${proposed.from})`
196
- };
197
- if (proposed.kind === "RAW") {
198
- const violation = checkBarrierInvariants({
199
- nodes: graph.nodes,
200
- edges: [...graph.edges, {
201
- kind: proposed.kind,
202
- from: proposed.from,
203
- to: proposed.to
204
- }]
205
- });
206
- if (violation) return {
207
- accepted: false,
208
- reason: violation
209
- };
210
- }
211
- return { accepted: true };
212
- }
213
- /**
214
- * checkBarrierInvariants — runs INV-1/INV-2/INV-3 over an already-simulated (post-append) graph.
215
- * Returns the first violation's reason, or null when the graph is clean.
216
- * INV-1: only a Mission may carry a barrier marking.
217
- * INV-2: no un-retired barrier may have a RAW predecessor that is an Operation (an Operation
218
- * never retires, so such a barrier would fence its project forever, invisibly to `cycles`).
219
- * INV-3: no two un-retired barriers of one project may both be `claimed` at once.
220
- */
221
- function checkBarrierInvariants(graph) {
222
- for (const n of graph.nodes.values()) if (n.barrier && n.kind !== "mission") return "INV-1: a barrier marking is only valid on a mission node";
223
- for (const n of graph.nodes.values()) {
224
- if (!n.barrier || n.status === "retired") continue;
225
- for (const predId of rawPredecessors(graph, n.id)) if (graph.nodes.get(predId)?.kind === "operation") return "INV-2: an un-retired barrier may not have a RAW predecessor that is an operation";
226
- }
227
- const claimedBarriersByProject = /* @__PURE__ */ new Map();
228
- for (const n of graph.nodes.values()) if (n.barrier && n.status === "claimed") claimedBarriersByProject.set(n.project, (claimedBarriersByProject.get(n.project) ?? 0) + 1);
229
- for (const count of claimedBarriersByProject.values()) if (count > 1) return "INV-3: at most one barrier of a project may be claimed at once";
230
- return null;
231
- }
232
- /**
233
- * applyNodeEventSimulated — shallow-merges one more node event onto an already-folded graph's
234
- * existing node (or NODE_DEFAULTS when absent), mirroring `fold`'s per-field merge semantics
235
- * without re-folding the whole event log — the guard only needs the ONE resulting node.
236
- */
237
- function applyNodeEventSimulated(graph, event) {
238
- const prev = graph.nodes.get(event.id);
239
- const merged = {
240
- id: event.id,
241
- kind: event.kind ?? prev?.kind ?? NODE_DEFAULTS.kind,
242
- status: event.status ?? prev?.status ?? NODE_DEFAULTS.status,
243
- touchSet: event.touchSet ? [...event.touchSet] : prev?.touchSet ?? [],
244
- blast: event.blast ?? prev?.blast ?? NODE_DEFAULTS.blast,
245
- hitlOrAfk: event.hitlOrAfk ?? prev?.hitlOrAfk ?? NODE_DEFAULTS.hitlOrAfk,
246
- modelTier: event.modelTier ?? prev?.modelTier ?? NODE_DEFAULTS.modelTier,
247
- briefPointer: event.briefPointer ?? prev?.briefPointer ?? NODE_DEFAULTS.briefPointer,
248
- origin: event.origin ? [...event.origin] : prev?.origin ?? [],
249
- capstone: event.capstone ?? prev?.capstone,
250
- project: event.project ?? prev?.project ?? NODE_DEFAULTS.project,
251
- barrier: event.barrier ?? prev?.barrier ?? NODE_DEFAULTS.barrier,
252
- schemaVersion: typeof event.v === "number" ? event.v : prev?.schemaVersion ?? 1
253
- };
254
- const nodes = new Map(graph.nodes);
255
- nodes.set(event.id, merged);
256
- return {
257
- nodes,
258
- edges: graph.edges
259
- };
260
- }
261
- /**
262
- * proposeNode — the write-time guard for a node event: simulates the merge onto the already-
263
- * folded graph, then rejects (INV-1/INV-2/INV-3) if the resulting state is invalid. Fold-then-
264
- * check, exactly like `proposeEdge` — so marking a node a barrier AFTER an Operation RAW edge
265
- * already exists is caught exactly like the reverse order.
266
- */
267
- function proposeNode(graph, event) {
268
- const violation = checkBarrierInvariants(applyNodeEventSimulated(graph, event));
269
- if (violation) return {
270
- accepted: false,
271
- reason: violation
272
- };
273
- return { accepted: true };
274
- }
275
- function declaredMembers(graph, operationId) {
276
- return graph.edges.filter((e) => e.kind === "parent-child" && e.from === operationId).map((e) => e.to);
277
- }
278
- /** The Operation a Mission is declared under (via a parent-child edge), or null when it is its
279
- * own Mission with no group. */
280
- function operationOf(graph, missionId) {
281
- const edge = graph.edges.find((e) => e.kind === "parent-child" && e.to === missionId);
282
- return edge ? edge.from : null;
283
- }
284
- /** Every Operation node and its declared Mission members, sorted for stable reading. */
285
- function listOperations(graph) {
286
- const ops = [];
287
- for (const n of graph.nodes.values()) {
288
- if (n.kind !== "operation") continue;
289
- ops.push({
290
- id: n.id,
291
- capstone: n.capstone,
292
- members: declaredMembers(graph, n.id).sort(compareIds)
293
- });
294
- }
295
- return ops.sort((a, b) => compareIds(a.id, b.id));
296
- }
297
- function rawClosure(graph, start) {
298
- const seen = /* @__PURE__ */ new Set();
299
- const stack = [start];
300
- while (stack.length > 0) {
301
- const cur = stack.pop();
302
- if (cur === void 0 || seen.has(cur)) continue;
303
- seen.add(cur);
304
- for (const e of graph.edges) if (e.kind === "RAW" && e.to === cur) stack.push(e.from);
305
- }
306
- return seen;
307
- }
308
- /**
309
- * checkOperation — the shippable-group view for one Operation: dependency-closed iff the
310
- * capstone's RAW closure is a subset of the declared set (a missing prerequisite is flagged; a
311
- * declared support member outside the closure is legal and never flagged); the release floor is
312
- * the capstone's closure alone (support members never gate release); progress is completed/total
313
- * over the declared Missions only.
314
- */
315
- function checkOperation(graph, operationId) {
316
- const declared = declaredMembers(graph, operationId);
317
- const declaredSet = new Set(declared);
318
- const capstone = graph.nodes.get(operationId)?.capstone;
319
- const closure = capstone !== void 0 ? rawClosure(graph, capstone) : /* @__PURE__ */ new Set();
320
- const missingPrereqs = [...closure].filter((id) => !declaredSet.has(id)).sort(compareIds);
321
- const completed = declared.filter((id) => graph.nodes.get(id)?.status === "retired").length;
322
- return {
323
- dependencyClosed: missingPrereqs.length === 0,
324
- missingPrereqs,
325
- releaseFloor: [...closure].sort(compareIds),
326
- progress: {
327
- completed,
328
- total: declared.length
329
- }
330
- };
331
- }
332
- function isRawSatisfied(graph, quarantined, id) {
333
- return rawPredecessors(graph, id).every((p) => {
334
- if (quarantined.has(p)) return false;
335
- return graph.nodes.get(p)?.status === "retired";
336
- });
337
- }
338
- function whyReady(graph, id) {
339
- const preds = rawPredecessors(graph, id);
340
- if (preds.length === 0) return "no RAW predecessors";
341
- return `RAW-satisfied: ${[...preds].sort(compareIds).join(", ")} retired`;
342
- }
343
- /** An un-retired barrier — open or claimed — is "live": it grants exemption (clause 1), fences its
344
- * project (clause 3), and occupies its project's at-most-one slot (clause 2). */
345
- function isLiveBarrier(n) {
346
- return n.barrier && n.status !== "retired";
347
- }
348
- /**
349
- * fenceExempt — clause 1's exempt set: every NON-barrier mission in the STRICT RAW-predecessor
350
- * closure of any live (un-retired) barrier, graph-global (not scoped to the barrier's own
351
- * project — an acyclic two-project deadlock needs the exemption to cross project lines). Barriers
352
- * are NEVER exempt, so a barrier that is itself a RAW predecessor of another project's barrier
353
- * cannot use exemption to dodge its own project's at-most-one cap (clause 2).
354
- */
355
- function fenceExempt(graph) {
356
- const exempt = /* @__PURE__ */ new Set();
357
- for (const b of graph.nodes.values()) {
358
- if (!isLiveBarrier(b)) continue;
359
- for (const id of rawClosure(graph, b.id)) {
360
- if (id === b.id) continue;
361
- const m = graph.nodes.get(id);
362
- if (m && !m.barrier) exempt.add(id);
363
- }
364
- }
365
- return exempt;
366
- }
367
- /**
368
- * barrierHeldByCap — clause 2, the at-most-one-barrier-per-project offer cap, evaluated over
369
- * UN-RETIRED barriers of `candidate`'s project (open ∪ claimed, never just the open `candidates`
370
- * set — a claimed barrier still fills the slot even though it's no longer itself a candidate).
371
- * Holds `candidate` when either another un-retired barrier of the project is already claimed, or a
372
- * lower-id OPEN RAW-satisfied barrier of the project exists (a quarantined or RAW-blocked one never
373
- * fills the slot).
374
- */
375
- function barrierHeldByCap(graph, quarantined, liveBarriersOfProject, candidate) {
376
- if (liveBarriersOfProject.some((b) => b.id !== candidate.id && b.status === "claimed")) return true;
377
- return liveBarriersOfProject.some((b) => b.id !== candidate.id && b.status === "open" && !quarantined.has(b.id) && isRawSatisfied(graph, quarantined, b.id) && compareIds(b.id, candidate.id) < 0);
378
- }
379
- /**
380
- * ready — reads the graph without changing it and returns every Mission that is both RAW-satisfied
381
- * (every RAW predecessor retired, transitively — realized here as each direct predecessor's own
382
- * recorded status, which composes: a blocked predecessor can never itself be retired) and not
383
- * WAW-held. Two WAW triggers: (1) an open candidate whose touch-set intersects an in-flight
384
- * (claimed) mission's is held; (2) among the remaining RAW-satisfied candidates, an intersecting
385
- * pair never both surface — the pinned lowest-id tie-break (compareIds) admits one and holds the
386
- * rest, guaranteeing the frontier never contains a WAW pair. A cycle-quarantined mission (and
387
- * anything depending on it) is excluded regardless of its recorded status.
388
- *
389
- * Before the WAW-mutex, the RAW-satisfied non-quarantined candidates pass through the barrier
390
- * fence (fold-time, never a stored edge — see the .feature "ready — the barrier fence" block):
391
- * clause 1 lifts a non-barrier in any live barrier's strict RAW-predecessor closure from every
392
- * fence (graph-global); clause 2 caps each project to at most one offered barrier (counting
393
- * open+claimed, so an in-flight barrier still fills the slot); clause 3 holds every other
394
- * non-barrier, non-exempt candidate of a fenced project outright, regardless of touch-set (an
395
- * empty-touch-set barrier still fences — the fence never delegates to the WAW-mutex).
396
- *
397
- * Deterministic: same graph in, same frontier in the same order out. Read-only: never mutates
398
- * `graph`.
399
- */
400
- function ready(graph) {
401
- const quarantined = quarantinedIds(graph);
402
- const missions = [...graph.nodes.values()].filter((n) => n.kind === "mission");
403
- const inFlightTouchSets = missions.filter((n) => n.status === "claimed").map((n) => n.touchSet);
404
- const rawSatisfiedOpen = missions.filter((n) => n.status === "open" && !quarantined.has(n.id) && isRawSatisfied(graph, quarantined, n.id));
405
- const liveBarriers = missions.filter(isLiveBarrier);
406
- const exempt = fenceExempt(graph);
407
- const fencedProjects = new Set(liveBarriers.map((b) => b.project));
408
- const sorted = [...rawSatisfiedOpen.filter((c) => {
409
- if (c.barrier) {
410
- const ofProject = liveBarriers.filter((b) => b.project === c.project);
411
- return !barrierHeldByCap(graph, quarantined, ofProject, c);
412
- }
413
- if (exempt.has(c.id)) return true;
414
- if (fencedProjects.has(c.project)) return false;
415
- return true;
416
- }).filter((n) => !inFlightTouchSets.some((t) => intersects(t, n.touchSet)))].sort((a, b) => compareIds(a.id, b.id));
417
- const admitted = [];
418
- const claimedTouch = [];
419
- for (const n of sorted) {
420
- if (intersects(n.touchSet, claimedTouch)) continue;
421
- admitted.push(n);
422
- claimedTouch.push(...n.touchSet);
423
- }
424
- return admitted.sort((a, b) => compareIds(a.id, b.id)).map((n) => ({
425
- id: n.id,
426
- node: n.kind,
427
- operation: operationOf(graph, n.id),
428
- blast: n.blast,
429
- hitlOrAfk: n.hitlOrAfk,
430
- modelTier: n.modelTier,
431
- briefPointer: n.briefPointer,
432
- whyReady: whyReady(graph, n.id)
433
- }));
434
- }
435
- const STORE_RELATIVE_PATH = [
436
- ".agents",
437
- "mission-graph",
438
- "events.jsonl"
439
- ];
440
- function storePath(root) {
441
- return join(root, ...STORE_RELATIVE_PATH);
442
- }
443
- /** Read the in-tree JSONL store's raw (trimmed, non-empty) lines — used by both the in-tree
444
- * backend and `migrate` (which copies these verbatim, never re-serialized). */
445
- function readInTreeLines(root) {
446
- const path = storePath(root);
447
- if (!existsSync(path)) return [];
448
- return readFileSync(path, "utf8").split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
449
- }
450
- function readInTreeEvents(root) {
451
- return readInTreeLines(root).map((line) => JSON.parse(line));
452
- }
453
- function appendInTreeEvent(root, event) {
454
- const path = storePath(root);
455
- mkdirSync(dirname(path), { recursive: true });
456
- appendFileSync(path, `${JSON.stringify(event)}\n`);
457
- }
458
- const ORPHAN_REF = "refs/sdd/mission-graph";
459
- const ORPHAN_STORE_FILE = "events.jsonl";
460
- function git(root, args, input) {
461
- return execFileSync("git", args, {
462
- cwd: root,
463
- input,
464
- encoding: "utf8"
465
- }).trim();
466
- }
467
- function isGitWorkTree(root) {
468
- try {
469
- return git(root, ["rev-parse", "--is-inside-work-tree"]) === "true";
470
- } catch {
471
- return false;
472
- }
473
- }
474
- function orphanRefExists(root) {
475
- try {
476
- git(root, [
477
- "rev-parse",
478
- "--verify",
479
- "--quiet",
480
- ORPHAN_REF
481
- ]);
482
- return true;
483
- } catch {
484
- return false;
485
- }
486
- }
487
- function orphanHead(root) {
488
- if (!orphanRefExists(root)) return null;
489
- return git(root, ["rev-parse", ORPHAN_REF]);
490
- }
491
- function readOrphanLines(root) {
492
- if (!orphanRefExists(root)) return [];
493
- return git(root, [
494
- "cat-file",
495
- "-p",
496
- `${ORPHAN_REF}:${ORPHAN_STORE_FILE}`
497
- ]).split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
498
- }
499
- function readOrphanEvents(root) {
500
- return readOrphanLines(root).map((line) => JSON.parse(line));
501
- }
502
- /** blob -> tree -> commit -> update-ref, compare-and-swap on `expectedOld` (the ref value the
503
- * caller last observed). `null` means "the ref must not yet exist"; a stale/mismatched old value
504
- * makes `update-ref` throw — that throw IS the CAS rejection, and the ref is left unchanged. */
505
- function commitOrphan(root, lines, expectedOld) {
506
- const commitArgs = ["commit-tree", git(root, ["mktree"], `100644 blob ${git(root, [
507
- "hash-object",
508
- "-w",
509
- "--stdin"
510
- ], lines.length ? `${lines.join("\n")}\n` : "")}\t${ORPHAN_STORE_FILE}\n`)];
511
- if (expectedOld !== null) commitArgs.push("-p", expectedOld);
512
- const commit = git(root, commitArgs, "mission-graph: append");
513
- git(root, [
514
- "update-ref",
515
- ORPHAN_REF,
516
- commit,
517
- expectedOld ?? ""
518
- ]);
519
- return commit;
520
- }
521
- function appendOrphanEvent(root, event) {
522
- const old = orphanHead(root);
523
- const lines = readOrphanLines(root);
524
- lines.push(JSON.stringify(event));
525
- commitOrphan(root, lines, old);
526
- }
527
- /**
528
- * resolveBackend — an explicit `MISSION_GRAPH_STORE` override wins outright; otherwise a
529
- * non-git-work-tree always stays in-tree (the orphan-ref backend needs git plumbing); inside a
530
- * work-tree, an existing orphan ref wins (already migrated); otherwise an existing in-tree store
531
- * keeps the in-tree backend so a pre-migrate v1 adopter is never silently orphaned; only a fresh
532
- * work-tree with neither picks the orphan-ref backend outright.
533
- */
534
- function resolveBackend(root, env = process.env) {
535
- const override = env.MISSION_GRAPH_STORE;
536
- if (override === "in-tree" || override === "orphan-ref") return override;
537
- if (!isGitWorkTree(root)) return "in-tree";
538
- if (orphanRefExists(root)) return "orphan-ref";
539
- if (existsSync(storePath(root))) return "in-tree";
540
- return "orphan-ref";
541
- }
542
- /** Read the append-only event log back. An absent store reads as the empty log (a fresh project). */
543
- function readEvents(root) {
544
- if (resolveBackend(root) === "orphan-ref") return readOrphanEvents(root);
545
- return readInTreeEvents(root);
546
- }
547
- /** Append one event line. The one writer for now is a person at the CLI; later, one automated
548
- * coordinator — never split across writers, so there is nothing to reconcile. */
549
- function appendEvent(root, event) {
550
- if (resolveBackend(root) === "orphan-ref") {
551
- appendOrphanEvent(root, event);
552
- return;
553
- }
554
- appendInTreeEvent(root, event);
555
- }
556
- function relativeStorePath() {
557
- return STORE_RELATIVE_PATH.join("/");
558
- }
559
- /** Every raw seed line must already be present among the orphan ref's lines — the guard that
560
- * keeps retirement from ever being the data loss the migration exists to prevent. An empty seed
561
- * vacuously counts as fully carried (nothing to lose). */
562
- function seedFullyCarried(root) {
563
- const seedLines = readInTreeLines(root);
564
- if (seedLines.length === 0) return true;
565
- const orphanLines = new Set(readOrphanLines(root));
566
- return seedLines.every((line) => orphanLines.has(line));
567
- }
568
- /**
569
- * retireSeed — migrate's final, guarded act: once (and only once) the orphan ref already carries
570
- * every line of the in-tree seed, deletes the seed file and stages the deletion (never commits
571
- * it — landing that commit is the caller's next step, and is what actually makes the retirement
572
- * reach other clones). Refuses when the ref does not yet carry every seed line. Idempotent and
573
- * safe to re-run: a pre-fix project whose seed was left behind by an earlier migrate is repaired
574
- * simply by calling migrate again.
575
- */
576
- function retireSeed(root) {
577
- const path = storePath(root);
578
- if (!existsSync(path)) return {
579
- retired: false,
580
- retiredReason: "no in-tree seed remains to retire"
581
- };
582
- if (!seedFullyCarried(root)) return {
583
- retired: false,
584
- retiredReason: "will not retire a seed the orphan ref does not carry"
585
- };
586
- rmSync(path);
587
- try {
588
- git(root, [
589
- "add",
590
- "--",
591
- relativeStorePath()
592
- ]);
593
- } catch {}
594
- return {
595
- retired: true,
596
- retiredReason: "seed deleted; the staged deletion must be committed to reach other clones"
597
- };
598
- }
599
- /**
600
- * migrate — one-time, idempotent: seeds the orphan ref from the existing in-tree store, copying
601
- * its raw lines verbatim (never parsed and re-serialized, so exact bytes/order are preserved),
602
- * then retires the seed (see `retireSeed`) as its final, guarded act — a seed left tracked would
603
- * travel to every clone and shadow the (never-fetched) ref there. A no-op on the seeding half
604
- * (never throws) when there is nothing to seed from, or the ref is already seeded; the retirement
605
- * half still runs either way, so a pre-fix project (ref seeded, seed left behind) is repaired by
606
- * simply re-running migrate.
607
- */
608
- function migrate(root) {
609
- if (!isGitWorkTree(root)) return {
610
- migrated: false,
611
- reason: "not a git work-tree",
612
- count: 0,
613
- retired: false,
614
- retiredReason: "not a git work-tree"
615
- };
616
- let migrated = false;
617
- let reason;
618
- let count;
619
- if (orphanRefExists(root)) {
620
- reason = "orphan ref already seeded";
621
- count = readOrphanLines(root).length;
622
- } else {
623
- const rawLines = readInTreeLines(root);
624
- if (rawLines.length === 0) return {
625
- migrated: false,
626
- reason: "no in-tree store to seed from",
627
- count: 0,
628
- retired: false,
629
- retiredReason: "no in-tree seed remains to retire"
630
- };
631
- commitOrphan(root, rawLines, null);
632
- migrated = true;
633
- reason = "seeded orphan ref from in-tree store";
634
- count = rawLines.length;
635
- }
636
- const { retired, retiredReason } = retireSeed(root);
637
- return {
638
- migrated,
639
- reason,
640
- count,
641
- retired,
642
- retiredReason
643
- };
644
- }
645
- function refTransferSpec(refName) {
646
- return `${refName}:${refName}`;
647
- }
648
- /** A remote NAME is configured at all (distinct from being reachable — see `remoteReachable`). */
649
- function remoteConfigured(root, remote) {
650
- try {
651
- git(root, [
652
- "remote",
653
- "get-url",
654
- remote
655
- ]);
656
- return true;
657
- } catch {
658
- return false;
659
- }
660
- }
661
- /** Whether the remote can actually be talked to right now. Deliberately checked SEPARATELY from
662
- * "does it have the ref" — `git ls-remote` reports an unreachable remote and an empty one the
663
- * same way, so establishing reachability first (and failing loudly when it cannot be) is the
664
- * only way to avoid conflating the two. */
665
- function remoteReachable(root, remote) {
666
- try {
667
- git(root, ["ls-remote", remote]);
668
- return true;
669
- } catch {
670
- return false;
671
- }
672
- }
673
- /** The remote's current orphan-ref tip, or null when the remote has no such ref (but IS
674
- * reachable — reachability is checked by the caller before this is ever consulted). */
675
- function remoteOrphanHead(root, remote) {
676
- const out = git(root, [
677
- "ls-remote",
678
- remote,
679
- ORPHAN_REF
680
- ]);
681
- if (out === "") return null;
682
- const [hash] = out.split("\n")[0].split(" ");
683
- return hash ?? null;
684
- }
685
- function gitFetchOrphanRef(root, remote) {
686
- git(root, [
687
- "fetch",
688
- remote,
689
- refTransferSpec(ORPHAN_REF)
690
- ]);
691
- }
692
- function gitPushOrphanRef(root, remote) {
693
- git(root, [
694
- "push",
695
- remote,
696
- refTransferSpec(ORPHAN_REF)
697
- ]);
698
- }
699
- function isAncestor(root, ancestor, descendant) {
700
- try {
701
- execFileSync("git", [
702
- "merge-base",
703
- "--is-ancestor",
704
- ancestor,
705
- descendant
706
- ], { cwd: root });
707
- return true;
708
- } catch {
709
- return false;
710
- }
711
- }
712
- /**
713
- * syncStore — carries the orphan ref between this clone and `remote`, fast-forward only. Two
714
- * prechecks run first, in this exact order: (1) backend — under the in-tree backend, sync is a
715
- * no-op that reports the active backend, even when the remote is unreachable (an in-tree project
716
- * must not fail merely for being offline); (2) reachability — an unreachable remote, or no remote
717
- * configured at all, is a loud non-zero failure, never reported as "no list there". Only once both
718
- * are settled does the 7-case partition of (local ref, remote ref) apply — see the README's sync
719
- * table. Writes no git configuration, ever; never uses the forced (`+`-prefixed) refspec form.
720
- */
721
- function syncStore(root, remote = "origin") {
722
- const backend = resolveBackend(root);
723
- if (backend !== "orphan-ref") return {
724
- backend,
725
- ok: true,
726
- action: "no-op",
727
- message: `the ${backend} backend is in use; sync only carries the shared orphan ref, so this is a no-op`,
728
- localHead: null,
729
- remoteHead: null
730
- };
731
- if (!remoteConfigured(root, remote)) return {
732
- backend,
733
- ok: false,
734
- action: "no-remote",
735
- message: `no remote '${remote}' is configured to reach`,
736
- localHead: orphanHead(root),
737
- remoteHead: null
738
- };
739
- if (!remoteReachable(root, remote)) return {
740
- backend,
741
- ok: false,
742
- action: "unreachable",
743
- message: `could not reach remote '${remote}'`,
744
- localHead: orphanHead(root),
745
- remoteHead: null
746
- };
747
- const localHead = orphanHead(root);
748
- const remoteHead = remoteOrphanHead(root, remote);
749
- if (localHead === null && remoteHead === null) return {
750
- backend,
751
- ok: true,
752
- action: "both-empty",
753
- message: "neither side holds a store ref",
754
- localHead: null,
755
- remoteHead: null
756
- };
757
- if (localHead !== null && remoteHead === null) {
758
- gitPushOrphanRef(root, remote);
759
- return {
760
- backend,
761
- ok: true,
762
- action: "push",
763
- message: "published the local-only store ref to the remote",
764
- localHead,
765
- remoteHead: localHead
766
- };
767
- }
768
- if (localHead === null && remoteHead !== null) {
769
- gitFetchOrphanRef(root, remote);
770
- return {
771
- backend,
772
- ok: true,
773
- action: "collect",
774
- message: "collected the remote store ref",
775
- localHead: remoteHead,
776
- remoteHead
777
- };
778
- }
779
- if (localHead === remoteHead) return {
780
- backend,
781
- ok: true,
782
- action: "agree",
783
- message: "the store refs already agree",
784
- localHead,
785
- remoteHead
786
- };
787
- git(root, [
788
- "fetch",
789
- remote,
790
- ORPHAN_REF
791
- ]);
792
- if (isAncestor(root, localHead, remoteHead)) {
793
- gitFetchOrphanRef(root, remote);
794
- return {
795
- backend,
796
- ok: true,
797
- action: "collect",
798
- message: "fast-forwarded the local store ref to the remote",
799
- localHead: remoteHead,
800
- remoteHead
801
- };
802
- }
803
- if (isAncestor(root, remoteHead, localHead)) {
804
- gitPushOrphanRef(root, remote);
805
- return {
806
- backend,
807
- ok: true,
808
- action: "push",
809
- message: "published the ahead store ref to the remote",
810
- localHead,
811
- remoteHead: localHead
812
- };
813
- }
814
- return {
815
- backend,
816
- ok: false,
817
- action: "refuse",
818
- message: `refused: the store refs have diverged (local ${localHead}, remote ${remoteHead}) — see the node README's "Getting out of a refusal"`,
819
- localHead,
820
- remoteHead
821
- };
822
- }
823
- /** Append a proposed RAW/parent-child/discovered-from edge through the write-time cycle guard;
824
- * only appends when the guard accepts (or the caller overrides it). */
825
- function appendEdgeChecked(root, proposed, override = false) {
826
- const decision = proposeEdge(fold(readEvents(root)), proposed, override);
827
- if (decision.accepted) appendEvent(root, {
828
- v: 1,
829
- type: "edge",
830
- ...proposed
831
- });
832
- return decision;
833
- }
834
- /** Append a node event through the write-time barrier guards (INV-1/INV-2/INV-3); only appends
835
- * when the guard accepts. */
836
- function appendNodeChecked(root, event) {
837
- const decision = proposeNode(fold(readEvents(root)), event);
838
- if (decision.accepted) appendEvent(root, event);
839
- return decision;
840
- }
841
- function toonQuote(v) {
842
- if (v === "" || /[",;]/.test(v) || v !== v.trim()) return `"${v.replace(/"/g, "\"\"")}"`;
843
- return v;
844
- }
845
- function renderFrontierToon(entries) {
846
- return [`ready[${entries.length}]{id,node,operation,blast,hitlOrAfk,modelTier,briefPointer,whyReady}:`, ...entries.map((e) => [
847
- e.id,
848
- e.node,
849
- e.operation ?? "",
850
- e.blast,
851
- e.hitlOrAfk,
852
- e.modelTier,
853
- e.briefPointer,
854
- e.whyReady
855
- ].map((v) => toonQuote(v)).join(",")).map((r) => ` ${r}`)].join("\n");
856
- }
857
- function renderCyclesToon(items) {
858
- return [`cycles[${items.length}]{scc,members}:`, ...items.map((item, i) => ` scc-${i + 1},"${item.members.join(";")}"`)].join("\n");
859
- }
860
- function renderSyncToon(result) {
861
- return [
862
- `backend: ${result.backend}`,
863
- `action: ${result.action}`,
864
- `ok: ${result.ok}`,
865
- `message: ${result.message}`,
866
- `localHead: ${result.localHead ?? ""}`,
867
- `remoteHead: ${result.remoteHead ?? ""}`
868
- ].join("\n");
869
- }
870
- function renderOperationToon(id, check) {
871
- return [
872
- `operation: ${id}`,
873
- `dependencyClosed: ${check.dependencyClosed}`,
874
- `missingPrereqs: ${check.missingPrereqs.join(";")}`,
875
- `releaseFloor: ${check.releaseFloor.join(";")}`,
876
- `progress: ${check.progress.completed}/${check.progress.total}`
877
- ].join("\n");
878
- }
879
- function flag(argv, name) {
880
- const i = argv.indexOf(name);
881
- return i === -1 ? void 0 : argv[i + 1];
882
- }
883
- function hasFlag(argv, name) {
884
- return argv.includes(name);
885
- }
886
- function splitCsv(v) {
887
- if (v === void 0 || v === "") return [];
888
- return v.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
889
- }
890
- function isEdgeKind(v) {
891
- return v === "RAW" || v === "parent-child" || v === "discovered-from";
892
- }
893
- function runAppendNode(rest, root) {
894
- const id = flag(rest, "--id");
895
- if (id === void 0) {
896
- process.stderr.write("mission-graph append node: --id is required\n");
897
- return 1;
898
- }
899
- const event = {
900
- v: 1,
901
- type: "node",
902
- id
903
- };
904
- const nodeKind = flag(rest, "--kind");
905
- if (nodeKind === "mission" || nodeKind === "operation") event.kind = nodeKind;
906
- const status = flag(rest, "--status");
907
- if (status === "open" || status === "claimed" || status === "retired") event.status = status;
908
- const touchSet = flag(rest, "--touch-set");
909
- if (touchSet !== void 0) event.touchSet = splitCsv(touchSet);
910
- const blast = flag(rest, "--blast");
911
- if (blast !== void 0) event.blast = blast;
912
- if (hasFlag(rest, "--hitl")) event.hitlOrAfk = "hitl";
913
- if (hasFlag(rest, "--afk")) event.hitlOrAfk = "afk";
914
- const modelTier = flag(rest, "--model-tier");
915
- if (modelTier !== void 0) event.modelTier = modelTier;
916
- const briefPointer = flag(rest, "--brief-pointer");
917
- if (briefPointer !== void 0) event.briefPointer = briefPointer;
918
- const capstone = flag(rest, "--capstone");
919
- if (capstone !== void 0) event.capstone = capstone;
920
- const project = flag(rest, "--project");
921
- if (project !== void 0) event.project = project;
922
- if (hasFlag(rest, "--barrier")) event.barrier = true;
923
- const decision = appendNodeChecked(root, event);
924
- if (!decision.accepted) {
925
- process.stderr.write(`mission-graph append node: rejected — ${decision.reason}\n`);
926
- return 1;
927
- }
928
- process.stdout.write(`mission-graph: appended node ${id}\n`);
929
- return 0;
930
- }
931
- function runAppendEdge(rest, root) {
932
- const edgeKind = flag(rest, "--kind");
933
- const from = flag(rest, "--from");
934
- const to = flag(rest, "--to");
935
- if (!isEdgeKind(edgeKind) || from === void 0 || to === void 0) {
936
- process.stderr.write("mission-graph append edge: --kind <RAW|parent-child|discovered-from> --from <id> --to <id> are required\n");
937
- return 1;
938
- }
939
- const decision = appendEdgeChecked(root, {
940
- kind: edgeKind,
941
- from,
942
- to
943
- }, hasFlag(rest, "--override"));
944
- if (!decision.accepted) {
945
- process.stderr.write(`mission-graph append edge: rejected — ${decision.reason}\n`);
946
- return 1;
947
- }
948
- process.stdout.write(`mission-graph: appended edge ${edgeKind} ${from} -> ${to}\n`);
949
- return 0;
950
- }
951
- function runAppendTombstone(rest, root) {
952
- const target = flag(rest, "--target");
953
- if (target === "node") {
954
- const id = flag(rest, "--id");
955
- if (id === void 0) {
956
- process.stderr.write("mission-graph append tombstone: --id is required for a node tombstone\n");
957
- return 1;
958
- }
959
- appendEvent(root, {
960
- v: 1,
961
- type: "tombstone",
962
- target: "node",
963
- id
964
- });
965
- process.stdout.write(`mission-graph: tombstoned node ${id}\n`);
966
- return 0;
967
- }
968
- if (target === "edge") {
969
- const edgeKind = flag(rest, "--kind");
970
- const from = flag(rest, "--from");
971
- const to = flag(rest, "--to");
972
- if (!isEdgeKind(edgeKind) || from === void 0 || to === void 0) {
973
- process.stderr.write("mission-graph append tombstone: --kind --from --to are required for an edge tombstone\n");
974
- return 1;
975
- }
976
- appendEvent(root, {
977
- v: 1,
978
- type: "tombstone",
979
- target: "edge",
980
- kind: edgeKind,
981
- from,
982
- to
983
- });
984
- process.stdout.write(`mission-graph: tombstoned edge ${edgeKind} ${from} -> ${to}\n`);
985
- return 0;
986
- }
987
- process.stderr.write("mission-graph append tombstone: --target <node|edge> is required\n");
988
- return 1;
989
- }
990
- function runAppend(argv, root) {
991
- const [kind, ...rest] = argv;
992
- if (kind === "node") return runAppendNode(rest, root);
993
- if (kind === "edge") return runAppendEdge(rest, root);
994
- if (kind === "tombstone") return runAppendTombstone(rest, root);
995
- process.stderr.write("mission-graph append: usage: append <node|edge|tombstone> ...\n");
996
- return 1;
997
- }
998
- function main(argv) {
999
- const [cmd, ...rest] = argv;
1000
- const root = flag(rest, "--root") ?? ".";
1001
- const format = flag(rest, "--format") === "json" ? "json" : "toon";
1002
- if (cmd === "ready") {
1003
- const entries = ready(fold(readEvents(root)));
1004
- process.stdout.write(`${format === "json" ? JSON.stringify(entries, null, 2) : renderFrontierToon(entries)}\n`);
1005
- return 0;
1006
- }
1007
- if (cmd === "cycles") {
1008
- const items = cycles(fold(readEvents(root)));
1009
- process.stdout.write(`${format === "json" ? JSON.stringify(items, null, 2) : renderCyclesToon(items)}\n`);
1010
- return 0;
1011
- }
1012
- if (cmd === "operation") {
1013
- const id = flag(rest, "--id");
1014
- if (id === void 0) {
1015
- process.stderr.write("mission-graph operation: --id <operation-id> is required\n");
1016
- return 1;
1017
- }
1018
- const check = checkOperation(fold(readEvents(root)), id);
1019
- process.stdout.write(`${format === "json" ? JSON.stringify(check, null, 2) : renderOperationToon(id, check)}\n`);
1020
- return 0;
1021
- }
1022
- if (cmd === "append") return runAppend(rest, root);
1023
- if (cmd === "migrate") {
1024
- const result = migrate(root);
1025
- process.stdout.write(`${format === "json" ? JSON.stringify(result, null, 2) : `migrated: ${result.migrated}\nreason: ${result.reason}\ncount: ${result.count}\nretired: ${result.retired}\nretiredReason: ${result.retiredReason}`}\n`);
1026
- return result.reason === "not a git work-tree" ? 1 : 0;
1027
- }
1028
- if (cmd === "sync") {
1029
- const result = syncStore(root, flag(rest, "--remote") ?? "origin");
1030
- process.stdout.write(`${format === "json" ? JSON.stringify(result, null, 2) : renderSyncToon(result)}\n`);
1031
- return result.ok ? 0 : 1;
1032
- }
1033
- process.stderr.write("mission-graph: usage: ready | cycles | operation --id <id> | append <node|edge|tombstone> ... | migrate | sync [--remote <name>]\n");
1034
- return 1;
1035
- }
1036
- if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
1037
- //#endregion
1038
- export { DEFAULT_PROJECT, ORPHAN_REF, appendEdgeChecked, appendEvent, appendNodeChecked, appendOrphanEvent, checkOperation, commitOrphan, compareIds, computeSCCs, cycles, fold, listOperations, main, migrate, operationOf, orphanHead, proposeEdge, proposeNode, quarantinedIds, rawPredecessors, readEvents, readOrphanLines, ready, renderCyclesToon, renderFrontierToon, renderOperationToon, renderSyncToon, resolveBackend, syncStore, wouldCloseCycle };