shapeup-sdlc 1.6.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 (135) hide show
  1. package/.claude/settings.local.example.json +20 -0
  2. package/.claude-plugin/marketplace.json +16 -0
  3. package/.claude-plugin/plugin.json +18 -0
  4. package/.env.shapeup.example +14 -0
  5. package/AGENTS.md +133 -0
  6. package/LICENSE +21 -0
  7. package/README.md +362 -0
  8. package/SECURITY.md +72 -0
  9. package/bin/init.mjs +329 -0
  10. package/commands/build.md +14 -0
  11. package/commands/eval.md +15 -0
  12. package/commands/hammer.md +12 -0
  13. package/commands/orient.md +11 -0
  14. package/commands/qa.md +12 -0
  15. package/commands/retro.md +13 -0
  16. package/commands/scopes.md +14 -0
  17. package/commands/shape.md +12 -0
  18. package/commands/ship.md +53 -0
  19. package/commands/wire.md +11 -0
  20. package/hooks/anti-rationalization.mjs +244 -0
  21. package/hooks/compact-snapshot.mjs +47 -0
  22. package/hooks/gate-deadline.mjs +151 -0
  23. package/hooks/gate-intake.mjs +110 -0
  24. package/hooks/gate-l2.mjs +161 -0
  25. package/hooks/gate-zerowork.mjs +264 -0
  26. package/hooks/hooks.json +118 -0
  27. package/hooks/lib/decision.mjs +183 -0
  28. package/hooks/safety-spine.mjs +296 -0
  29. package/hooks/sandbox-guard.mjs +172 -0
  30. package/hooks/session-rehydrate.mjs +109 -0
  31. package/hooks/slop-cleaner.mjs +176 -0
  32. package/oracles/_shared.mjs +46 -0
  33. package/oracles/http-oracle.mjs +155 -0
  34. package/oracles/index.mjs +36 -0
  35. package/oracles/process-oracle.mjs +146 -0
  36. package/oracles/snapshot-oracle.mjs +119 -0
  37. package/oracles/test-oracle.mjs +138 -0
  38. package/package.json +49 -0
  39. package/skills/advisor-protocol/SKILL.md +171 -0
  40. package/skills/ba-pitch-analyzer/SKILL.md +175 -0
  41. package/skills/ba-pitch-analyzer/assets/templates/_index.tmpl.md +84 -0
  42. package/skills/ba-pitch-analyzer/assets/templates/api-feasibility.tmpl.md +65 -0
  43. package/skills/ba-pitch-analyzer/assets/templates/assess-report.tmpl.md +127 -0
  44. package/skills/ba-pitch-analyzer/assets/templates/contracts/be-service.contract.tmpl.md +62 -0
  45. package/skills/ba-pitch-analyzer/assets/templates/contracts/offline-storage.contract.tmpl.md +92 -0
  46. package/skills/ba-pitch-analyzer/assets/templates/contracts/third-party-api.contract.tmpl.md +66 -0
  47. package/skills/ba-pitch-analyzer/assets/templates/cross-context/context-map.tmpl.md +64 -0
  48. package/skills/ba-pitch-analyzer/assets/templates/cross-context/event-choreography.tmpl.md +77 -0
  49. package/skills/ba-pitch-analyzer/assets/templates/cross-context/migration-plan.tmpl.md +104 -0
  50. package/skills/ba-pitch-analyzer/assets/templates/cross-context/team-handoff.tmpl.md +79 -0
  51. package/skills/ba-pitch-analyzer/assets/templates/domain-model.tmpl.md +79 -0
  52. package/skills/ba-pitch-analyzer/assets/templates/feedback.tmpl.md +87 -0
  53. package/skills/ba-pitch-analyzer/assets/templates/integration.tmpl.md +64 -0
  54. package/skills/ba-pitch-analyzer/assets/templates/run-state.tmpl.md +77 -0
  55. package/skills/ba-pitch-analyzer/assets/templates/scope-summary.tmpl.md +105 -0
  56. package/skills/ba-pitch-analyzer/assets/templates/synthesis.tmpl.md +215 -0
  57. package/skills/ba-pitch-analyzer/assets/templates/task-board.tmpl.md +35 -0
  58. package/skills/ba-pitch-analyzer/assets/templates/task-spike.tmpl.md +86 -0
  59. package/skills/ba-pitch-analyzer/assets/templates/task.tmpl.md +84 -0
  60. package/skills/ba-pitch-analyzer/assets/templates/usecase.tmpl.md +105 -0
  61. package/skills/ba-pitch-analyzer/assets/templates/ux-behavior.tmpl.md +65 -0
  62. package/skills/ba-pitch-analyzer/references/contract-patterns.md +152 -0
  63. package/skills/ba-pitch-analyzer/references/ddd-patterns.md +173 -0
  64. package/skills/ba-pitch-analyzer/references/doc-schemas.md +275 -0
  65. package/skills/ba-pitch-analyzer/references/integration-analysis.md +152 -0
  66. package/skills/ba-pitch-analyzer/references/task-generation.md +623 -0
  67. package/skills/ba-pitch-analyzer/references/test-surface.md +102 -0
  68. package/skills/ba-pitch-analyzer/references/ux-behavior-patterns.md +269 -0
  69. package/skills/ba-pitch-analyzer/scripts/board-derive.mjs +241 -0
  70. package/skills/ba-pitch-analyzer/scripts/spec-lint.mjs +239 -0
  71. package/skills/coach/SKILL.md +202 -0
  72. package/skills/orient/SKILL.md +262 -0
  73. package/skills/qa-edge-hunter/SKILL.md +369 -0
  74. package/skills/scope-architect/SKILL.md +111 -0
  75. package/skills/scope-hammer/SKILL.md +186 -0
  76. package/skills/shapeup/SKILL.md +403 -0
  77. package/skills/shapeup/resources/breadboard-reflection.md +88 -0
  78. package/skills/shapeup/resources/breadboarding.md +334 -0
  79. package/skills/shapeup/resources/context-compaction.md +240 -0
  80. package/skills/shapeup/resources/framing-doc.md +92 -0
  81. package/skills/shapeup/resources/kickoff-doc.md +95 -0
  82. package/skills/shapeup/resources/shaping.md +194 -0
  83. package/skills/shapeup/resources/spike.md +86 -0
  84. package/skills/solution-architect/SKILL.md +129 -0
  85. package/skills/spec-evaluator/README.md +93 -0
  86. package/skills/spec-evaluator/SKILL.md +213 -0
  87. package/skills/spec-evaluator/references/anti-leniency.md +50 -0
  88. package/skills/spec-evaluator/references/dimension-contract.md +126 -0
  89. package/skills/spec-evaluator/references/dimensions/_registry.md +40 -0
  90. package/skills/spec-evaluator/references/dimensions/completeness.md +102 -0
  91. package/skills/spec-evaluator/references/dimensions/integration.md +129 -0
  92. package/skills/spec-evaluator/references/dimensions/performance.md +48 -0
  93. package/skills/spec-evaluator/references/dimensions/security.md +60 -0
  94. package/skills/spec-evaluator/references/dimensions/spec-conformance.md +134 -0
  95. package/skills/spec-evaluator/references/dimensions/tdd-surface.md +110 -0
  96. package/skills/spec-evaluator/references/dimensions/test-surface-conformance.md +100 -0
  97. package/skills/spec-evaluator/references/probing.md +177 -0
  98. package/skills/spec-evaluator/references/report-schema.md +101 -0
  99. package/skills/spec-evaluator/references/verdict-ledger.md +92 -0
  100. package/skills/spec-evaluator/scripts/verdict-ledger.mjs +166 -0
  101. package/skills/task-executor/SKILL.md +194 -0
  102. package/skills/tech-lead/README.md +71 -0
  103. package/skills/tech-lead/SKILL.md +459 -0
  104. package/skills/tech-lead/references/delegation.md +254 -0
  105. package/skills/tech-lead/references/gates.md +379 -0
  106. package/skills/tech-lead/references/invocation.md +45 -0
  107. package/skills/tech-lead/references/ledger-schema.md +214 -0
  108. package/skills/tech-lead/references/round-protocol.md +184 -0
  109. package/skills/tech-lead/references/state-model.md +66 -0
  110. package/skills/tech-lead/references/tiny-lane.md +52 -0
  111. package/skills/tech-lead/schemas/domain.schema.json +2294 -0
  112. package/skills/tech-lead/schemas/gate-answers.schema.json +92 -0
  113. package/skills/tech-lead/schemas/work-order.schema.json +21 -0
  114. package/skills/tech-lead/schemas/work-result.schema.json +40 -0
  115. package/skills/tech-lead/scripts/aegis-digest.mjs +124 -0
  116. package/skills/tech-lead/scripts/budget-check.mjs +156 -0
  117. package/skills/tech-lead/scripts/compile-order.mjs +518 -0
  118. package/skills/tech-lead/scripts/fit-check.mjs +196 -0
  119. package/skills/tech-lead/scripts/gate-answers.mjs +338 -0
  120. package/skills/tech-lead/scripts/ingest-result.mjs +270 -0
  121. package/skills/tech-lead/scripts/init-run.mjs +326 -0
  122. package/skills/tech-lead/scripts/lib/argv.mjs +224 -0
  123. package/skills/tech-lead/scripts/lib/contract-md.mjs +481 -0
  124. package/skills/tech-lead/scripts/lib/is-main.mjs +82 -0
  125. package/skills/tech-lead/scripts/lib/paths.mjs +278 -0
  126. package/skills/tech-lead/scripts/lib/ratchet-tree.mjs +112 -0
  127. package/skills/tech-lead/scripts/run-snapshot.mjs +269 -0
  128. package/skills/tech-lead/scripts/ship-report.mjs +275 -0
  129. package/skills/tech-lead/scripts/stats.mjs +423 -0
  130. package/skills/tech-lead/scripts/t0-verify.mjs +470 -0
  131. package/skills/tech-lead/scripts/trace-lint.mjs +367 -0
  132. package/skills/tech-lead/scripts/validate-envelope.mjs +283 -0
  133. package/skills/translator/README.md +66 -0
  134. package/skills/translator/SKILL.md +258 -0
  135. package/skills/translator/references/preservation-rules.md +102 -0
@@ -0,0 +1,270 @@
1
+ #!/usr/bin/env node
2
+ // WorkResult ingester (pure-skill architecture v1.0, plan P1).
3
+ //
4
+ // The other half of the orchestrator's pipeline sub-layer — and the mechanism that finally
5
+ // closes D6: workers no longer write shared state; they RETURN data (a WorkResult envelope)
6
+ // and this script performs every shared-state write, deterministically, in one place:
7
+ //
8
+ // task_results[] → tick AC boxes, flip task frontmatter status, append Execution Log,
9
+ // update tasks/_index.md row, propagate unblocks (old P3.1–P3.6)
10
+ // discoveries[] → append to .shapeup/<slug>/discovery/ledger.md (old P3.7 / QA H.3)
11
+ // verdict.criteria[] → append evaluation/.verdicts-<target>.jsonl (old evaluator B.0)
12
+ // verdict.refuted[] → un-tick refuted AC boxes + set eval_verdict frontmatter (old B.2/B.2b)
13
+ // escalates[] → queue .shapeup/<slug>/escalates/<order>.json for the orchestrator
14
+ //
15
+ // Zero dependencies, zero network, schema-validated input (a malformed result never mutates
16
+ // the board). Single-writer becomes mechanically true, not aspirational.
17
+ //
18
+ // Usage: node skills/tech-lead/scripts/ingest-result.mjs <result.json> [--cwd <dir>]
19
+ // Exit: 0 = ingested, 1 = result rejected (schema) or a write failed.
20
+
21
+ import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, readdirSync } from "node:fs";
22
+ import { resolve, join, dirname } from "node:path";
23
+ import { fileURLToPath } from "node:url";
24
+ import { validate } from "./validate-envelope.mjs";
25
+ import { isMain } from "./lib/is-main.mjs";
26
+ import { runArgs } from "./lib/argv.mjs";
27
+ import { tasksDir, localRoot } from "./lib/paths.mjs";
28
+
29
+ const HERE = dirname(fileURLToPath(import.meta.url));
30
+ const RESULT_SCHEMA = JSON.parse(readFileSync(resolve(HERE, "../schemas/work-result.schema.json"), "utf8"));
31
+
32
+ /**
33
+ * @returns {string} Today's date as an ISO `YYYY-MM-DD` string (UTC), for log/frontmatter stamps.
34
+ */
35
+ const today = () => new Date().toISOString().slice(0, 10);
36
+
37
+ /**
38
+ * Locate a task file on the LOCAL board by id.
39
+ * @param {string} cwd - Working-directory root.
40
+ * @param {string} slug - Feature slug.
41
+ * @param {string} taskId - Task id prefix to match (e.g. "TASK-001").
42
+ * @returns {string|null} Absolute path of the first `<taskId>*.md` file, or null when the tasks
43
+ * directory or a matching file does not exist.
44
+ */
45
+ export function findTaskFile(cwd, slug, taskId) {
46
+ const dir = tasksDir(cwd, slug);
47
+ if (!existsSync(dir)) return null;
48
+ const f = readdirSync(dir).find((n) => n.startsWith(taskId) && n.endsWith(".md"));
49
+ return f ? join(dir, f) : null;
50
+ }
51
+
52
+ /**
53
+ * Set a scalar frontmatter field in a task-file body, adding it when absent.
54
+ * @param {string} body - Full task-file text.
55
+ * @param {string} key - Frontmatter key to set.
56
+ * @param {(string|number)} value - Value to write (stringified inline).
57
+ * @returns {string} The body with `key: value` set; returned unchanged when the body has no
58
+ * frontmatter block.
59
+ */
60
+ export function setFrontmatter(body, key, value) {
61
+ const m = body.match(/^---\r?\n([\s\S]*?)\r?\n---/);
62
+ if (!m) return body;
63
+ const re = new RegExp(`^${key}:.*$`, "m");
64
+ const fm = re.test(m[1]) ? m[1].replace(re, `${key}: ${value}`) : `${m[1]}\n${key}: ${value}`;
65
+ return body.replace(m[1], fm);
66
+ }
67
+
68
+ /**
69
+ * Tick or un-tick the first acceptance-criterion checkbox whose text matches `ac`.
70
+ * @param {string} body - Full task-file text.
71
+ * @param {string} ac - Criterion text to match (case/space-insensitive substring, either direction).
72
+ * @param {boolean} checked - true to write `[x]`, false to write `[ ]`.
73
+ * @returns {{body:string, hit:boolean}} The updated body and whether a checkbox matched (only the
74
+ * first match is changed).
75
+ */
76
+ export function setCheckbox(body, ac, checked) {
77
+ const needle = ac.toLowerCase().replace(/\s+/g, " ").trim();
78
+ const lines = body.split(/\r?\n/);
79
+ let hit = false;
80
+ const out = lines.map((line) => {
81
+ const m = line.match(/^(\s*- \[)([ x])(\]\s+)(.*)$/);
82
+ if (!m || hit) return line;
83
+ const text = m[4].toLowerCase().replace(/\s+/g, " ").trim();
84
+ if (text.includes(needle) || needle.includes(text)) {
85
+ hit = true;
86
+ return `${m[1]}${checked ? "x" : " "}${m[3]}${m[4]}`;
87
+ }
88
+ return line;
89
+ });
90
+ return { body: out.join("\n"), hit };
91
+ }
92
+
93
+ /**
94
+ * Flip a task's row in `tasks/_index.md` to a done state.
95
+ * @param {string} indexBody - Full board-index text.
96
+ * @param {string} taskId - Task id whose row to update.
97
+ * @param {boolean} done - When true, rewrite the row's status emoji/word to done; false is a no-op.
98
+ * @returns {string} The board text with the matching row updated (unchanged when no row matches).
99
+ */
100
+ export function updateBoardRow(indexBody, taskId, done) {
101
+ return indexBody.split(/\r?\n/).map((line) => {
102
+ if (!line.includes(taskId) || !line.includes("|")) return line;
103
+ if (done) return line.replace(/⬜|🔄|⏳|🚫/g, "✅").replace(/\b(ready|in-progress|blocked)\b/gi, "done");
104
+ return line;
105
+ }).join("\n");
106
+ }
107
+
108
+ /**
109
+ * Apply one validated WorkResult to the working tree — the single-writer step (D6): ticks AC
110
+ * boxes, flips task status, appends the Execution Log, propagates unblocks, appends discoveries,
111
+ * writes the verdict ledger + un-ticks refuted boxes, and queues escalates.
112
+ * @param {object} result - A schema-valid WorkResult (order_id, task_results[], discoveries[],
113
+ * verdict{criteria[],refuted[]}, escalates[]).
114
+ * @param {{cwd:string}} opts - cwd: working-directory root every LOCAL path resolves against.
115
+ * @returns {{slug:string, tasks_updated:string[], acs_ticked:number, unblocked:string[],
116
+ * discoveries_appended:number, refuted_unticked:number, verdict_lines:number,
117
+ * escalates_queued:number}} A summary of every write performed.
118
+ * @throws {Error} If a task/board/ledger file it must write is not writable (fs error propagates).
119
+ * Side effects: writes task files, `tasks/_index.md`, `discovery/ledger.md`,
120
+ * `evaluation/.verdicts-*.jsonl`, and `escalates/*.json` under `.shapeup/<slug>/`.
121
+ */
122
+ export function applyResult(result, { cwd }) {
123
+ const slug = result.order_id.split("/")[0];
124
+ const local = localRoot(cwd, slug);
125
+ const summary = { slug, tasks_updated: [], acs_ticked: 0, unblocked: [], discoveries_appended: 0, refuted_unticked: 0, verdict_lines: 0, escalates_queued: 0 };
126
+
127
+ // 1. Task results → task files + board (old task-executor P3.1/P3.2/P3.6).
128
+ const boardIndex = join(local, "tasks", "_index.md");
129
+ for (const tr of result.task_results || []) {
130
+ const path = findTaskFile(cwd, slug, tr.task_id);
131
+ if (!path) continue;
132
+ let body = readFileSync(path, "utf8");
133
+ for (const acr of tr.ac_results || []) {
134
+ if (acr.result === "pass") {
135
+ const r = setCheckbox(body, acr.ac, true);
136
+ body = r.body;
137
+ if (r.hit) summary.acs_ticked++;
138
+ }
139
+ }
140
+ if (tr.status === "done") {
141
+ body = setFrontmatter(body, "status", "done");
142
+ body = setFrontmatter(body, "completed_at", today());
143
+ } else if (tr.status === "partial" || tr.status === "failed") {
144
+ body = setFrontmatter(body, "status", "in-progress");
145
+ }
146
+ // Execution Log (append; the checkbox list must never disagree with it).
147
+ const logLines = (tr.ac_results || []).map((a) => `- ${a.ac}: ${a.result}${a.evidence ? ` (${a.evidence})` : ""}`).join("\n");
148
+ body += `\n\n## Execution Log — ${today()} (${result.order_id})\n- executor: ${result.worker || "task-executor"} via ingest-result\n- status: ${tr.status}\n${logLines}${tr.notes ? `\n- notes: ${tr.notes}` : ""}\n`;
149
+ writeFileSync(path, body);
150
+ summary.tasks_updated.push(tr.task_id);
151
+ if (tr.status === "done" && existsSync(boardIndex)) {
152
+ writeFileSync(boardIndex, updateBoardRow(readFileSync(boardIndex, "utf8"), tr.task_id, true));
153
+ }
154
+ }
155
+
156
+ // 2. Unblock propagation (old P3.4): any blocked task whose dependencies are all done → ready.
157
+ const tasksDir = join(local, "tasks");
158
+ if (existsSync(tasksDir)) {
159
+ const files = readdirSync(tasksDir).filter((f) => /^TASK-[\w.-]+\.md$/i.test(f));
160
+ const statusOf = {};
161
+ const parsed = files.map((f) => {
162
+ const body = readFileSync(join(tasksDir, f), "utf8");
163
+ const id = (body.match(/^id:\s*(TASK-[\w.-]+)/im) || [])[1] || f.replace(/\.md$/, "");
164
+ const status = (body.match(/^status:\s*(\S+)/im) || [])[1] || "unknown";
165
+ const deps = (body.match(/^depends_on:\s*\[([^\]]*)\]/im) || [, ""])[1]
166
+ .split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
167
+ statusOf[id] = status;
168
+ return { f, id, status, deps, body };
169
+ });
170
+ for (const t of parsed) {
171
+ if (t.status === "blocked" && t.deps.length && t.deps.every((d) => statusOf[d] === "done")) {
172
+ writeFileSync(join(tasksDir, t.f), setFrontmatter(t.body, "status", "ready"));
173
+ summary.unblocked.push(t.id);
174
+ if (existsSync(boardIndex)) {
175
+ const idx = readFileSync(boardIndex, "utf8").split(/\r?\n/).map((line) =>
176
+ line.includes(t.id) && line.includes("|")
177
+ ? line.replace(/🚫|⏳/g, "⬜").replace(/\bblocked\b/gi, "ready")
178
+ : line).join("\n");
179
+ writeFileSync(boardIndex, idx);
180
+ }
181
+ }
182
+ }
183
+ }
184
+
185
+ // 3. Discoveries → the ledger (old P3.7 / QA H.3). Single writer: this script.
186
+ if (result.discoveries?.length) {
187
+ const ledgerDir = join(local, "discovery");
188
+ mkdirSync(ledgerDir, { recursive: true });
189
+ const ledger = join(ledgerDir, "ledger.md");
190
+ if (!existsSync(ledger)) writeFileSync(ledger, `---\nfeature: ${slug}\n---\n# Discovery Ledger — ${slug}\n`);
191
+ const lines = result.discoveries.map((d) => {
192
+ const tags = [d.lens ? `[lens:${d.lens}]` : "", d.severity_hint ? `severity-hint: ${d.severity_hint}` : "", d.test_gap ? `test-gap: ${d.test_gap}` : "", d.contradicts ? `contradicts: ${d.contradicts}` : "", d.traces_to?.length ? `traces_to: ${d.traces_to.join(", ")}` : ""].filter(Boolean);
193
+ return `${d.marker} ${d.lens ? tags[0] + " " : ""}${d.line}${d.repro ? `\n repro: ${d.repro}` : ""}${tags.slice(d.lens ? 1 : 0).map((t) => `\n ${t}`).join("")}`;
194
+ }).join("\n");
195
+ appendFileSync(ledger, `\n## Discovered — ${result.order_id} (${today()})\n${lines}\n`);
196
+ summary.discoveries_appended = result.discoveries.length;
197
+ }
198
+
199
+ // 4. Verdict bookkeeping (old evaluator B.0/B.2/B.2b) — judge returns data, ingest writes.
200
+ if (result.verdict) {
201
+ const evalDir = join(local, "evaluation");
202
+ mkdirSync(evalDir, { recursive: true });
203
+ if (result.verdict.criteria?.length) {
204
+ const target = result.order_id.split("/")[1] || "run";
205
+ const ledger = join(evalDir, `.verdicts-${target}.jsonl`);
206
+ let run = 1;
207
+ if (existsSync(ledger)) {
208
+ const prior = readFileSync(ledger, "utf8").trim().split(/\n/).filter(Boolean).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
209
+ run = prior.reduce((mx, r) => Math.max(mx, r.run || 0), 0) + 1;
210
+ }
211
+ const lines = result.verdict.criteria.map((c) => JSON.stringify({
212
+ run, dimension: c.dimension || "spec-conformance", criterion: c.criterion,
213
+ verdict: c.verdict, confidence: c.confidence, reprobed: !!c.reprobed,
214
+ evidence: c.evidence || "", at: new Date().toISOString(),
215
+ })).join("\n");
216
+ appendFileSync(ledger, lines + "\n");
217
+ summary.verdict_lines = result.verdict.criteria.length;
218
+ }
219
+ for (const ref of result.verdict.refuted || []) {
220
+ const path = findTaskFile(cwd, slug, ref.task_id);
221
+ if (!path) continue;
222
+ let body = readFileSync(path, "utf8");
223
+ const r = setCheckbox(body, ref.ac, false);
224
+ if (r.hit) summary.refuted_unticked++;
225
+ body = setFrontmatter(r.body, "eval_verdict", "fail");
226
+ body = setFrontmatter(body, "eval_at", today());
227
+ writeFileSync(path, body);
228
+ }
229
+ }
230
+
231
+ // 5. Escalates → a queue file the orchestrator adjudicates via advisor-protocol.
232
+ if (result.escalates?.length) {
233
+ const escDir = join(local, "escalates");
234
+ mkdirSync(escDir, { recursive: true });
235
+ const out = join(escDir, `${result.order_id.split("/")[1] || "run"}.json`);
236
+ writeFileSync(out, JSON.stringify(result.escalates, null, 2) + "\n");
237
+ summary.escalates_queued = result.escalates.length;
238
+ }
239
+
240
+ return summary;
241
+ }
242
+
243
+ // ---------------------------------------------------------------------------
244
+ /** The typed argv contract (see `./lib/argv.mjs`). */
245
+ export const ARGV_SPEC = {
246
+ usage: "ingest-result.mjs <result.json> [--cwd <dir>]",
247
+ _: { arity: 1, max: 1, name: "result.json" },
248
+ cwd: { type: "path" },
249
+ };
250
+
251
+ const isMainModule = isMain(import.meta.url);
252
+ if (isMainModule) {
253
+ const args = runArgs(ARGV_SPEC);
254
+ const file = args._[0];
255
+ const cwd = resolve(args.cwd || process.cwd());
256
+
257
+ let result;
258
+ try { result = JSON.parse(readFileSync(resolve(file), "utf8")); }
259
+ catch (e) { console.error(` ✗ result unreadable: ${e.message}`); process.exit(1); }
260
+
261
+ const { valid, errors } = validate(result, RESULT_SCHEMA);
262
+ if (!valid) {
263
+ console.error("ingest-result: result rejected — a malformed result never mutates the board:");
264
+ for (const e of errors) console.error(` ✗ ${e}`);
265
+ process.exit(1);
266
+ }
267
+ const s = applyResult(result, { cwd });
268
+ console.log(`✅ ingested ${result.order_id} — tasks: [${s.tasks_updated.join(", ")}] · ACs ticked: ${s.acs_ticked} · unblocked: [${s.unblocked.join(", ")}] · discoveries: ${s.discoveries_appended} · verdict lines: ${s.verdict_lines} · refuted un-ticked: ${s.refuted_unticked} · escalates queued: ${s.escalates_queued}`);
269
+ if (s.escalates_queued) process.exitCode = 0; // escalates are data, not failure
270
+ }
@@ -0,0 +1,326 @@
1
+ #!/usr/bin/env node
2
+ // GATE L0.1 — START THE RUN. The orchestrator's first tool call, before any prose.
3
+ //
4
+ // WHY THIS EXISTS (measured, not theorized).
5
+ //
6
+ // On the SDD harness benchmark (`sdd-harness-bench`, F2, Haiku 4.5, n=5, zero variance) the
7
+ // orchestrator was dispatched with a valid spec and did this:
8
+ //
9
+ // TOOL Skill(tech-lead, "--unattended --rounds 3\n\n# F2 — category budgets…")
10
+ // TEXT "The tech-lead skill is orchestrating the full Shape Up harness. It will: 1. …"
11
+ // FINAL (same text — session ends)
12
+ //
13
+ // It loaded a 450-line instruction file describing eleven gates and returned a description of
14
+ // eleven gates. No code, no board, no gate artifacts — and prose that reads exactly like a
15
+ // successful run. 29% acceptance, 10 escaped defects, five times out of five.
16
+ //
17
+ // Two guards existed and neither could see it:
18
+ // • `gate-intake.mjs` (L0.0) fires on an EMPTY intake. Intake was valid here. Correct no-op.
19
+ // • `anti-rationalization.mjs` fires when a completion claim contradicts run facts. It is
20
+ // scoped to an ACTIVE run — and a run that never started produces none of the files it
21
+ // reads — and its claim detector matches past-tense completion ("done", "shipped"), while
22
+ // narration is future-tense ("it will"). Two independent misses on the same transcript.
23
+ //
24
+ // The root cause of BOTH misses is the same: **whether a run had started was not a fact on
25
+ // disk.** It was an inference from artifacts that only appear later. So this script exists to
26
+ // make starting a run a mechanical event with a receipt, at t=0:
27
+ //
28
+ // • It is the orchestrator's FIRST action, stated in the first screen of SKILL.md. Everything
29
+ // emitted before a tool call is narration surface; this shrinks that surface to zero.
30
+ // • It writes `receipt.json` — the fact "this run started, with THIS intake, at THIS time".
31
+ // `gate-zerowork.mjs` (Stop) blocks a session that invoked tech-lead and produced no
32
+ // receipt. Narration now has a detector that does not depend on what the narration says.
33
+ // • It writes `active-scope`, which is the precondition every downstream guard already
34
+ // assumed someone had established. Previously that someone was the model, deciding to.
35
+ // An invariant that depends on the model choosing to establish it is a prompt, not a gate.
36
+ //
37
+ // It also HASHES the intake into the receipt. The benchmark's first (wrong) diagnosis was that
38
+ // requirement text was dropped on the hand-off. It was not, on re-run — but nothing on disk
39
+ // could have settled that either way. Now it can: the intake that reached the orchestrator is
40
+ // recorded verbatim next to its digest, so "the spec was dropped" is checkable, not arguable.
41
+ //
42
+ // USAGE
43
+ // node init-run.mjs --slug <slug> --intake-file <path> [options] <- prefer this
44
+ // node init-run.mjs --slug <slug> --intake-text "<requirement>" [options]
45
+ // cat spec.md | node init-run.mjs --slug <slug> --intake-stdin [options]
46
+ //
47
+ // PREFER --intake-file. A multi-line requirement inlined into a shell argument is where this step
48
+ // goes wrong: quoting breaks, a `#` after a newline trips path validation, and the run spends six
49
+ // turns fighting its own command line instead of starting. Measured, on this project's benchmark.
50
+ //
51
+ // --auto-level interactive | auto | unattended (default: interactive)
52
+ // --lens lite | standard | cross-context (default: standard)
53
+ // --max-rounds N outer circuit breaker (default: 3)
54
+ // --attempts N inner per-scope T0 budget (default: 5)
55
+ // --spec-folder SHARED spec deliverable path (default: shapeup/<slug>/spec/)
56
+ // --gate-answers path | preset name (see gate-answers.mjs; recorded, not read)
57
+ // --wall-clock-budget N deadline breaker, seconds (off by default; see budget-check.mjs)
58
+ // --cwd project root (default: process.cwd())
59
+ // --force re-init over an existing run receipt
60
+ //
61
+ // Prints a JSON receipt on stdout. Exit 0 on success, 2 on a usage error, 3 when a live run
62
+ // already exists and --force was not given.
63
+ //
64
+ // EXIT 3 IS THE RESUME PATH, not a dead end. It prints the file-derived RunSnapshot for the run that
65
+ // is already open — slug, status, round, attempt, board counts, pending orders — so the orchestrator
66
+ // continues from the phase the files report instead of re-opening the run or restarting the pipeline
67
+ // from phase 1. It previously said "Resume it (`--from <slug>`)", and `--from` is a /tech-lead flag
68
+ // that takes a phase, not an init-run flag that takes a slug: the one instruction available at the
69
+ // one moment it mattered named a mechanism that does not parse.
70
+
71
+ import { mkdirSync, writeFileSync, readFileSync, existsSync } from "node:fs";
72
+ import { join, dirname } from "node:path";
73
+ import { createHash } from "node:crypto";
74
+ import { decideLane, treeSize } from "./fit-check.mjs";
75
+ import { isMain } from "./lib/is-main.mjs";
76
+ import { runArgs } from "./lib/argv.mjs";
77
+ import { deriveSnapshot } from "./run-snapshot.mjs";
78
+ import { localRoot, activeScope, globLocal, globShared } from "./lib/paths.mjs";
79
+
80
+ export const RECEIPT_VERSION = 1;
81
+
82
+ const AUTO_LEVELS = new Set(["interactive", "auto", "unattended"]);
83
+ const LENSES = new Set(["lite", "standard", "cross-context"]);
84
+
85
+ /** Slugify a free-text feature name into a filesystem-safe run id. */
86
+ export function slugify(text) {
87
+ return String(text || "")
88
+ .toLowerCase()
89
+ .replace(/[^a-z0-9]+/g, "-")
90
+ .replace(/^-+|-+$/g, "")
91
+ .slice(0, 48) || "run";
92
+ }
93
+
94
+ export function digest(text) {
95
+ return createHash("sha256").update(String(text ?? ""), "utf8").digest("hex");
96
+ }
97
+
98
+ /**
99
+ * Build the receipt record. Pure — takes resolved inputs, returns the object that gets written.
100
+ * Kept separate from I/O so the structural tests can assert its shape without a filesystem.
101
+ */
102
+ export function buildReceipt({ slug, intake, config, startedAt }) {
103
+ const intakeText = String(intake ?? "");
104
+ return {
105
+ receipt_version: RECEIPT_VERSION,
106
+ type: "harness-run-receipt",
107
+ slug,
108
+ started_at: startedAt,
109
+ intake_sha256: digest(intakeText),
110
+ intake_chars: intakeText.length,
111
+ intake_lines: intakeText ? intakeText.split("\n").length : 0,
112
+ // The single fact that separates "the harness ran" from "the harness described itself".
113
+ // Written before any gate, so its ABSENCE at Stop is unambiguous.
114
+ started: true,
115
+ config,
116
+ };
117
+ }
118
+
119
+ /** The `harness-run.md` frontmatter block, per references/ledger-schema.md. */
120
+ export function runFrontmatter({ slug, config, startedAt }) {
121
+ return [
122
+ "---",
123
+ "type: harness-run",
124
+ `feature: ${slug}`,
125
+ `spec_folder: ${config.spec_folder}`,
126
+ `lens: ${config.lens}`,
127
+ "eval_dimensions: [spec-conformance]",
128
+ `max_rounds: ${config.max_rounds}`,
129
+ `attempt_budget: ${config.attempt_budget}`,
130
+ `wall_clock_budget_s: ${config.wall_clock_budget_s ?? "~"}`,
131
+ `auto_level: ${config.auto_level}`,
132
+ `gate_answers: ${config.gate_answers ?? "~"}`,
133
+ `lane: ${config.fit?.lane ?? "full"}${config.fit?.overridden_from ? ` (overridden from ${config.fit.overridden_from})` : ""}`,
134
+ "status: orienting",
135
+ "final_verdict: ~",
136
+ "rounds_used: 0",
137
+ "discovered_rounds: 0",
138
+ "deploy: ~",
139
+ `started_at: ${startedAt}`,
140
+ "closed_at: ~",
141
+ "---",
142
+ "",
143
+ `# Harness run — ${slug}`,
144
+ "",
145
+ "Opened by `init-run.mjs` (GATE L0.1). The tech lead is the sole writer from here on.",
146
+ "",
147
+ "## Rounds",
148
+ "",
149
+ "| Phase | Round | Result | Duration | Notes |",
150
+ "|-------|-------|--------|----------|-------|",
151
+ "| Init | — | run opened | — | intake recorded, receipt written |",
152
+ "",
153
+ "## Decisions log",
154
+ "",
155
+ "| Gate | Decision | Source | Note |",
156
+ "|------|----------|--------|------|",
157
+ "",
158
+ ].join("\n");
159
+ }
160
+
161
+ // ---- CLI -------------------------------------------------------------------
162
+
163
+ /** The typed argv contract (see `./lib/argv.mjs`). */
164
+ export const ARGV_SPEC = {
165
+ usage: 'init-run.mjs (--intake-file <path> | --intake-text "<req>" | --intake-stdin) ' +
166
+ "[--slug <slug>] [--auto-level interactive|auto|unattended] [--lens <lens>] " +
167
+ "[--max-rounds N] [--attempts N] [--spec-folder <dir>] [--gate-answers <preset|path>] " +
168
+ "[--lane full|tiny] [--tiny] [--wall-clock-budget <seconds>] [--cwd <dir>] [--force]",
169
+ _: { arity: 0, max: 0, name: "(no positional operands)" },
170
+ cwd: { type: "path" },
171
+ "intake-text": { type: "str" },
172
+ "intake-file": { type: "str" }, // "-" is a legitimate value here (stdin), so not type "path"
173
+ "intake-stdin": { type: "flag" },
174
+ slug: { type: "str" },
175
+ "auto-level": { type: "str" },
176
+ lens: { type: "str" },
177
+ "max-rounds": { type: "int", min: 1 },
178
+ attempts: { type: "int", min: 1 },
179
+ "spec-folder": { type: "path" },
180
+ "gate-answers": { type: "str" },
181
+ lane: { type: "str" },
182
+ tiny: { type: "flag" },
183
+ "wall-clock-budget": { type: "int", min: 1 },
184
+ force: { type: "flag" },
185
+ };
186
+
187
+ function fail(code, msg) {
188
+ console.error(msg);
189
+ process.exit(code);
190
+ }
191
+
192
+ export function main() {
193
+ const args = runArgs(ARGV_SPEC);
194
+ const cwd = args.cwd || process.cwd();
195
+
196
+ let intake = args.intakeText ?? null;
197
+ const intakeFile = args.intakeFile ?? null;
198
+ // `--intake-file -` is the shape everyone reaches for; accept it rather than erroring on a
199
+ // file literally named "-". (Measured: an agent tried exactly this on its second attempt.)
200
+ if (args.intakeStdin || intakeFile === "-") {
201
+ try { intake = readFileSync(0, "utf8"); } catch { fail(2, "--intake-stdin: nothing on stdin"); }
202
+ } else if (intakeFile) {
203
+ const p = intakeFile.startsWith("/") ? intakeFile : join(cwd, intakeFile);
204
+ if (!existsSync(p)) fail(2, `--intake-file not found: ${p}`);
205
+ intake = readFileSync(p, "utf8");
206
+ }
207
+ if (!intake || !intake.trim()) {
208
+ fail(2, [
209
+ "✋ init-run: no intake. Pass --intake-text \"<the requirement>\" or --intake-file <path>.",
210
+ "",
211
+ "An orchestrator with no spec has nothing to orchestrate — this is the same precondition",
212
+ "GATE L0.0 (hooks/gate-intake.mjs) enforces at dispatch, re-checked here where the run is",
213
+ "actually opened.",
214
+ ].join("\n"));
215
+ }
216
+
217
+ const slug = (args.slug ?? null) || slugify(intake.split("\n").find((l) => l.trim()) || "run");
218
+
219
+ const auto_level = args.autoLevel ?? "interactive";
220
+ if (!AUTO_LEVELS.has(auto_level)) fail(2, `--auto-level must be one of: ${[...AUTO_LEVELS].join(", ")}`);
221
+ const lens = args.lens ?? "standard";
222
+ if (!LENSES.has(lens)) fail(2, `--lens must be one of: ${[...LENSES].join(", ")}`);
223
+
224
+ const config = {
225
+ auto_level,
226
+ lens,
227
+ max_rounds: args.maxRounds ?? 3,
228
+ attempt_budget: args.attempts ?? 5,
229
+ spec_folder: args.specFolder ?? `${globShared(slug, "spec")}/`,
230
+ gate_answers: args.gateAnswers ?? null,
231
+ tiny_lane: !!args.tiny,
232
+ // GATE L0.3 — the lane, computed rather than judged (see fit-check.mjs). Recorded with its
233
+ // evidence so a heavy lane on a small change is visible instead of accidental. An explicit
234
+ // --lane or --tiny is honoured and marked as an override, because a measured recommendation
235
+ // fitted on three features must not outrank a human who knows the codebase.
236
+ fit: (() => {
237
+ const auto = decideLane({ intake, files: treeSize(cwd) });
238
+ const forced = (args.lane ?? null) || (args.tiny ? "tiny" : null);
239
+ return forced && forced !== auto.lane
240
+ ? { ...auto, lane: forced, overridden_from: auto.lane, override_source: args.tiny ? "--tiny" : "--lane" }
241
+ : auto;
242
+ })(),
243
+ // The third breaker (see scripts/budget-check.mjs). Null = off, which is the default and
244
+ // keeps every existing run behaving exactly as before. Set it in any lane with a hard clock
245
+ // — CI, a benchmark, an overnight run — so the harness trips its own breaker and ships what
246
+ // is green, instead of being killed from outside and shipping nothing.
247
+ wall_clock_budget_s: args.wallClockBudget ?? null,
248
+ };
249
+
250
+ const runRoot = localRoot(cwd, slug);
251
+ const receiptPath = join(runRoot, "receipt.json");
252
+ // A RUN IS ALREADY OPEN. This is the resume path, and it used to be a dead end.
253
+ //
254
+ // The refusal is right: silently re-initialising would discard the round history the circuit
255
+ // breaker counts against. What was wrong was the instruction it gave — "Resume it (`--from
256
+ // <slug>`)". `--from` is not an init-run flag at all; it is a `/tech-lead` flag, and it takes a
257
+ // PHASE (`--from build`), not a slug. So at the one moment the orchestrator most needs a next
258
+ // step, the runtime named a mechanism that does not exist, on a script whose failure mode was
259
+ // already invisible (see lib/is-main.mjs — under a symlinked install this whole body did not run).
260
+ //
261
+ // Measured consequence, on the SDD harness benchmark's F4 handoff: a fresh session in a workspace
262
+ // with an open run spent 82–120 turns before its first write, largely on forensics against this
263
+ // step, and closed 0/3 of the gap.
264
+ //
265
+ // So the refusal now DOES the resume work instead of describing it. It emits the derived snapshot
266
+ // — the same file-only derivation `hooks/session-rehydrate.mjs` injects — so the orchestrator gets
267
+ // slug, status, round, attempt, board counts and pending orders in THIS tool call rather than
268
+ // needing to discover that it needs another one. Exit 3 still means "do not proceed as if you
269
+ // opened a run"; it now also means "here is the run you are actually in".
270
+ if (existsSync(receiptPath) && !args.force) {
271
+ let resume = null;
272
+ try { resume = deriveSnapshot(cwd); } catch { /* a broken run must still produce the refusal */ }
273
+ fail(3, [
274
+ `✋ init-run: a run is ALREADY OPEN — receipt exists at ${receiptPath}.`,
275
+ "",
276
+ "Do NOT re-initialise and do NOT restart the pipeline from phase 1. Re-opening would discard",
277
+ "the round history the circuit breaker counts against, and the board, ledger and receipt below",
278
+ "already hold the run's real state. RESUME from the phase these files report.",
279
+ "",
280
+ resume
281
+ ? `RESUME STATE (derived from files, never from memory):\n${JSON.stringify(resume, null, 2)}`
282
+ : [
283
+ "The receipt exists but no run state could be derived, which means the run root is",
284
+ "incomplete. Inspect it before deciding:",
285
+ ` ls -R ${runRoot}`,
286
+ ].join("\n"),
287
+ "",
288
+ "To re-derive this at any time:",
289
+ " node <plugin>/skills/tech-lead/scripts/run-snapshot.mjs --cwd <dir>",
290
+ "To abandon the open run and start over, deliberately: --force",
291
+ ].join("\n"));
292
+ }
293
+
294
+ const startedAt = new Date().toISOString();
295
+ const receipt = buildReceipt({ slug, intake, config, startedAt });
296
+
297
+ mkdirSync(runRoot, { recursive: true });
298
+ mkdirSync(join(runRoot, "orders"), { recursive: true });
299
+ mkdirSync(join(runRoot, "results"), { recursive: true });
300
+ mkdirSync(join(runRoot, "discovery"), { recursive: true });
301
+
302
+ // The intake, verbatim. So "the spec was dropped on the hand-off" is a checkable claim.
303
+ writeFileSync(join(runRoot, "intake.md"), intake.endsWith("\n") ? intake : intake + "\n", "utf8");
304
+ writeFileSync(receiptPath, JSON.stringify(receipt, null, 2) + "\n", "utf8");
305
+ writeFileSync(join(runRoot, "harness-run.md"), runFrontmatter({ slug, config, startedAt }), "utf8");
306
+
307
+ // The pointer every downstream guard reads to answer "is a run active?".
308
+ const pointer = activeScope(cwd);
309
+ mkdirSync(dirname(pointer), { recursive: true });
310
+ writeFileSync(pointer, JSON.stringify({ slug, started_at: startedAt }, null, 2) + "\n", "utf8");
311
+
312
+ console.log(JSON.stringify({
313
+ ok: true,
314
+ slug,
315
+ run_root: globLocal(slug),
316
+ receipt: globLocal(slug, "receipt.json"),
317
+ intake_sha256: receipt.intake_sha256,
318
+ intake_chars: receipt.intake_chars,
319
+ config,
320
+ next: "GATE L0 — pin the run config, emit the gate block, then ORIENT.",
321
+ }, null, 2));
322
+ }
323
+
324
+ if (isMain(import.meta.url)) {
325
+ main();
326
+ }