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,518 @@
1
+ #!/usr/bin/env node
2
+ // WorkOrder compiler (pure-skill architecture v1.0, plan P1).
3
+ //
4
+ // The orchestrator's pipeline sub-layer: assembles the structured input envelope a worker is
5
+ // dispatched with. Replaces tech-lead's hand-assembled `isolated_brief()` prose step and every
6
+ // worker's GATE A/B plumbing (path resolution, run-state parse, board glob-matching, dependency
7
+ // reads, mode detection). Deterministic, zero LLM tokens (DD-7).
8
+ //
9
+ // A worker depends on its ORDER, never on filesystem topology — moving a directory again
10
+ // (the v3.2 lesson) touches this script, zero skills.
11
+ //
12
+ // Zero dependencies, zero network.
13
+ //
14
+ // Usage:
15
+ // Scope attempt (isolated attempt loop):
16
+ // node skills/tech-lead/scripts/compile-order.mjs --scope shapeup/<slug>/scopes/<id>.md \
17
+ // --round N --attempt M [--cwd <dir>] [--test-cmd "<cmd>"]
18
+ // Single task (no scope contracts — pre-v0.3.0 boards):
19
+ // node skills/tech-lead/scripts/compile-order.mjs --task TASK-003 --slug <slug> [--worker task-executor]
20
+ // node skills/tech-lead/scripts/compile-order.mjs --next --slug <slug>
21
+ // Non-build operation (planner/judge/QA dispatches; flag surface → operation + whitelist):
22
+ // node skills/tech-lead/scripts/compile-order.mjs --operation reconcile --slug <slug> [--round N]
23
+ // [--worker ba-pitch-analyzer] [--payload '<json>']
24
+ //
25
+ // Output: .shapeup/<slug>/orders/<order-file>.json (schema-validated before write; a
26
+ // pretty-printed envelope, colocated so audits can read it). Prints the path on stdout.
27
+
28
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from "node:fs";
29
+ import { resolve, join, dirname, basename } from "node:path";
30
+ import { fileURLToPath } from "node:url";
31
+ import { validate } from "./validate-envelope.mjs";
32
+ import { readTrials } from "./t0-verify.mjs";
33
+ import { isMain } from "./lib/is-main.mjs";
34
+ import { runArgs } from "./lib/argv.mjs";
35
+ // `specDir` is aliased: this module has a local `let specDir` holding the resolved, possibly
36
+ // --spec-overridden directory, and the import is the convention-derived default.
37
+ import {
38
+ tasksDir, specDir as defaultSpecDir, roundLedger, trials, verdictsDir, ordersDir,
39
+ relShared, globLocal, globShared, relKnowledgeBase,
40
+ } from "./lib/paths.mjs";
41
+ import { readContract, SCOPE_CONTRACT } from "./lib/contract-md.mjs";
42
+
43
+ const HERE = dirname(fileURLToPath(import.meta.url));
44
+ const ORDER_SCHEMA = JSON.parse(readFileSync(resolve(HERE, "../schemas/work-order.schema.json"), "utf8"));
45
+
46
+ // --- tiny frontmatter reader (scalar keys + [a, b] inline lists) --------------------------
47
+ /**
48
+ * Parse a task/spec Markdown frontmatter block into scalar keys and inline `[a, b]` lists.
49
+ * @param {string} md - Full Markdown document text.
50
+ * @returns {Object<string,(string|string[])>} A flat map of top-level frontmatter keys; a value
51
+ * written as `[x, y]` becomes a trimmed string[], every other value stays a string. Returns
52
+ * {} when the document has no leading `---`-delimited block.
53
+ */
54
+ export function frontmatter(md) {
55
+ const m = md.match(/^---\r?\n([\s\S]*?)\r?\n---/);
56
+ if (!m) return {};
57
+ const meta = {};
58
+ for (const line of m[1].split(/\r?\n/)) {
59
+ const c = line.indexOf(":");
60
+ if (c === -1 || /^\s/.test(line)) continue;
61
+ const key = line.slice(0, c).trim();
62
+ let val = line.slice(c + 1).trim().replace(/^["']|["']$/g, "");
63
+ if (/^\[.*\]$/.test(val)) {
64
+ meta[key] = val.slice(1, -1).split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
65
+ } else meta[key] = val;
66
+ }
67
+ return meta;
68
+ }
69
+
70
+ /**
71
+ * Parse one TASK-NNN.md file into the task entry a WorkOrder carries.
72
+ * @param {string} path - Absolute path to the task Markdown file.
73
+ * @returns {{id:string, title:string, status:string, priority:number, depends_on:string[],
74
+ * use_case_refs:string[], body_path:string, acceptance_criteria:Array<string|{text:string,
75
+ * covers:string[]}>}} The task entry. Each acceptance criterion is the checkbox text
76
+ * byte-identical (so ingest can tick it back); a trailing `(covers: REQ-…)` clause yields
77
+ * {text, covers} instead of a plain string. `priority` defaults to 999 when unset.
78
+ * @throws {Error} If `path` is not readable.
79
+ */
80
+ export function parseTaskFile(path) {
81
+ const body = readFileSync(path, "utf8");
82
+ const fm = frontmatter(body);
83
+ const acceptance_criteria = [];
84
+ for (const line of body.split(/\r?\n/)) {
85
+ const m = line.match(/^\s*- \[[ x]\]\s+(.*)$/);
86
+ if (!m) continue;
87
+ const text = m[1].trim(); // byte-identical to the checkbox — ingest ticks by matching it back
88
+ // Additive covers-closure anchor (spine v1.3): a trailing `(covers: REQ-3, REQ-7)` clause on
89
+ // the AC line yields {text, covers}; a plain line stays a string (non-regression on legacy boards).
90
+ const cov = text.match(/\(covers:\s*([^)]*)\)/i);
91
+ const covers = cov ? cov[1].split(",").map((s) => s.trim()).filter((s) => /^REQ-\d+$/.test(s)) : [];
92
+ acceptance_criteria.push(covers.length ? { text, covers } : text);
93
+ }
94
+ return {
95
+ id: fm.id || basename(path).match(/TASK-[\w.-]+?(?=-|\.md)/)?.[0] || basename(path, ".md"),
96
+ title: fm.title || "",
97
+ status: fm.status || "unknown",
98
+ priority: Number(fm.priority) || 999,
99
+ depends_on: Array.isArray(fm.depends_on) ? fm.depends_on : [],
100
+ use_case_refs: Array.isArray(fm.use_case_refs) ? fm.use_case_refs : [],
101
+ body_path: path,
102
+ acceptance_criteria,
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Read every task entry on the LOCAL board for a slug, sorted by ascending priority.
108
+ * @param {string} cwd - Working-directory root the `.shapeup/<slug>/tasks` path resolves against.
109
+ * @param {string} slug - Feature slug naming the run.
110
+ * @returns {Array<object>} The parsed task entries (see {@link parseTaskFile}); [] when the tasks
111
+ * directory does not exist.
112
+ */
113
+ export function readBoard(cwd, slug) {
114
+ const dir = tasksDir(cwd, slug);
115
+ if (!existsSync(dir)) return [];
116
+ return readdirSync(dir)
117
+ .filter((f) => /^TASK-[\w.-]+\.md$/i.test(f))
118
+ .map((f) => parseTaskFile(join(dir, f)))
119
+ .sort((a, b) => a.priority - b.priority);
120
+ }
121
+
122
+ /**
123
+ * Extract a scope's advisor-answer rows from the committed round ledger's Decisions table.
124
+ * @param {string} ledgerText - Full `round-ledger.md` text ("" / null → no decisions).
125
+ * @param {string} [scopeId] - When given, keep only rows whose cells name this scope; omit to keep all.
126
+ * @returns {Array<{id:string, answer:string}>} One entry per `ESC-N` row (id = the ESC id, answer =
127
+ * the row's last cell); [] when the Decisions section is absent or empty.
128
+ */
129
+ export function ledgerDecisions(ledgerText, scopeId) {
130
+ const decisions = [];
131
+ let inDecisions = false;
132
+ for (const line of (ledgerText || "").split(/\r?\n/)) {
133
+ if (/^#+\s*Decisions/i.test(line)) { inDecisions = true; continue; }
134
+ if (inDecisions && /^#+\s/.test(line)) inDecisions = false;
135
+ if (!inDecisions || !line.trim().startsWith("|")) continue;
136
+ const cells = line.split("|").map((s) => s.trim()).filter(Boolean);
137
+ if (cells.length < 2 || /^[-: ]+$/.test(cells[0])) continue;
138
+ const id = cells.find((c) => /^ESC-\d+/i.test(c));
139
+ if (!id) continue;
140
+ if (scopeId && !cells.some((c) => c === scopeId)) continue;
141
+ decisions.push({ id, answer: cells[cells.length - 1] });
142
+ }
143
+ return decisions;
144
+ }
145
+
146
+ /**
147
+ * Resolve the write-contract (sandbox substrate) for an operation — one whitelist template per
148
+ * operation, so mode/flag differences are enforced by the sandbox hook, not trusted to prose.
149
+ * @param {string} operation - The order's operation (execute|analyze|generate-board|reconcile|
150
+ * retrofit-surface|coverage|map-scopes|remap|split-scope|wire|evaluate|hunt|recheck|orient|…).
151
+ * @param {{slug?:string, specDir?:string, scope?:object}} [ctx] - slug (names LOCAL/SHARED roots),
152
+ * specDir (overrides the default spec path), scope (contract supplying allowed/shared substrates).
153
+ * @returns {{allowed:string[], shared?:string[], frozen?:string[], append_only?:string[]}} The
154
+ * substrate contract: globs the worker may write (`allowed`), shared-write globs, read-only
155
+ * `frozen` globs, and `append_only` globs. An unknown operation returns a LOCAL-only default.
156
+ */
157
+ export function substrateFor(operation, { slug, specDir, scope } = {}) {
158
+ const local = globLocal(slug);
159
+ const spec = specDir || globShared(slug, "spec");
160
+ const scopesDir = globShared(slug, "scopes");
161
+ // Working notes live in the LOCAL tier since ADR-0001: the committed `spec/` keeps only what the
162
+ // evaluator grades against and a reviewer needs. `synthesis.md`, `assess-report.md`,
163
+ // `feedback.md`, `api-feasibility.md` and `integration.md` are analysis, not contract.
164
+ const working = `${local}/working`;
165
+ const FROZEN_SPEC_CORE = [`${spec}/domain-model.md`, `${spec}/usecases/*.md#Steps`, `${spec}/contracts/**`, `${spec}/ux-behavior.md`];
166
+ switch (operation) {
167
+ case "execute": case "fix": case "spike":
168
+ return {
169
+ allowed: [...(scope?.allowed_file_substrate || []), `${local}/spikes/**`],
170
+ shared: scope?.shared_substrate || [],
171
+ };
172
+ case "analyze":
173
+ return { allowed: [`${spec}/**`, `${local}/**`], frozen: [] };
174
+ case "generate-board":
175
+ return {
176
+ allowed: [`${local}/tasks/**`, `${spec}/scope-summary.md`, `${working}/**`],
177
+ frozen: [...FROZEN_SPEC_CORE, `${scopesDir}/**`],
178
+ };
179
+ case "reconcile":
180
+ return {
181
+ allowed: [`${local}/tasks/**`, `${spec}/scope-summary.md`, `${working}/**`],
182
+ append_only: [`${spec}/usecases/*.md#Invariants`, `${spec}/usecases/*.md#Test Surface`],
183
+ frozen: FROZEN_SPEC_CORE,
184
+ };
185
+ case "retrofit-surface":
186
+ return { allowed: [], append_only: [`${spec}/usecases/*.md#Test Surface`], frozen: FROZEN_SPEC_CORE };
187
+ case "coverage":
188
+ // ba-pitch-analyzer writes the SHARED requirement registry only — the REQ source and the
189
+ // spec core stay frozen (the registry is a separate derived file, never an edit of the source).
190
+ return {
191
+ allowed: [globShared(slug, "requirements.md")],
192
+ frozen: [...FROZEN_SPEC_CORE, `${scopesDir}/**`, `${local}/tasks/**`],
193
+ };
194
+ case "map-scopes": case "remap": case "split-scope":
195
+ return {
196
+ allowed: [`${scopesDir}/*.md`, globShared(slug, "scope-board.md")],
197
+ frozen: [...FROZEN_SPEC_CORE, `${local}/tasks/**`],
198
+ };
199
+ case "wire":
200
+ // solution-architect writes the SHARED wiring map DIRECTLY (precedent: scope-architect
201
+ // writes scopes/*.md). The spec core, the scopes, and the profile stay frozen.
202
+ return {
203
+ allowed: [globShared(slug, "wiring-map.md")],
204
+ frozen: [...FROZEN_SPEC_CORE, `${scopesDir}/**`, globShared(slug, "project-profile.md")],
205
+ };
206
+ case "evaluate":
207
+ return { allowed: [`${local}/evaluation/**`], frozen: [`${spec}/**`, `${local}/tasks/**`] };
208
+ case "hunt": case "recheck":
209
+ return { allowed: [`${local}/qa/**`], frozen: [`${spec}/**`, `${local}/tasks/**`] };
210
+ case "orient":
211
+ return { allowed: [`${local}/orient/**`], frozen: [`${spec}/**`] };
212
+ default:
213
+ return { allowed: [`${local}/**`] };
214
+ }
215
+ }
216
+
217
+ // --- inspect(): the attempt loop's history ------------------------------------------------
218
+ //
219
+ // WHAT THIS REPLACED. The entire history mechanism used to be one read of one file:
220
+ //
221
+ // const prev = join(cwd, ".shapeup", slug, "t0", "verdicts", `r${round}-a${attempt-1}.json`);
222
+ // digestedErrors = JSON.parse(readFileSync(prev)).discovered_tasks || [];
223
+ //
224
+ // Three bounds followed, and each is a way the loop can repeat itself. It read ONE ATTEMPT BACK,
225
+ // not attempts 1…N−1 — so with the default `attempt_budget: 5`, attempt 4 could re-propose a change
226
+ // that already failed at attempt 1. It read ONLY WITHIN THE CURRENT ROUND — round 2 attempt 1 began
227
+ // blind to everything round 1 learned. And it carried ONLY AEGIS error triples: what was tried,
228
+ // whether it helped, and whether the tree was kept were not in the envelope because no field held
229
+ // them. Excluding the prior TRANSCRIPT is right and deliberate (zero-memory handoff). Excluding the
230
+ // structured TRIAL RECORD is the opposite thing, and the two were dropped together.
231
+ //
232
+ // TOKEN DISCIPLINE. `compactTrial` strips stdout/stderr and truncates the digest to three triples.
233
+ // Eight rows ≈ 600 tokens against an `attempt_budget` of 5 — cheaper than one re-proposed failed
234
+ // change.
235
+
236
+ /** How many trial rows an order carries. Matches `WorkOrderPayload.trial_history.maxItems`. */
237
+ export const TRIAL_HISTORY_MAX = 8;
238
+
239
+ /**
240
+ * Reduce a trial ledger row to what a worker can act on.
241
+ *
242
+ * Drops the fields a worker cannot use — `artifact`, `sha256`, `at`, `tree_ref`, `baseline_trial`
243
+ * — and truncates the digest. Keeps `schema_version` and `scope_id` so the row stays a valid
244
+ * `TrialRow`: the registry defines each cross-boundary record ONCE, and a payload carrying a
245
+ * near-TrialRow that needs its own definition would be exactly the ad-hoc field the registry
246
+ * exists to prevent.
247
+ *
248
+ * @param {object} t - A row from `t0/trials.jsonl`.
249
+ * @returns {{schema_version:number, trial:number, round:number, attempt:number, scope_id:string,
250
+ * score:object, status:string, delta:string, digest:Array<object>}} The compacted row.
251
+ */
252
+ export function compactTrial(t) {
253
+ return {
254
+ schema_version: t.schema_version ?? 1,
255
+ trial: t.trial, round: t.round, attempt: t.attempt, scope_id: t.scope_id,
256
+ score: t.score, status: t.status, delta: t.delta ?? "",
257
+ digest: (t.digest || []).slice(0, 3),
258
+ };
259
+ }
260
+
261
+ /**
262
+ * Select the trial rows an order should carry: this scope, this round or the one before it,
263
+ * most recent {@link TRIAL_HISTORY_MAX} in write order.
264
+ *
265
+ * Crossing the round boundary is the point — a fix round that starts blind to the build round is
266
+ * how the same failed change gets proposed twice.
267
+ *
268
+ * @param {Array<object>} trials - All rows read from `t0/trials.jsonl`.
269
+ * @param {{scopeId?:string, round?:number}} sel - Scope id and current round to select against.
270
+ * @returns {Array<object>} The compacted rows, oldest first; [] when nothing matches.
271
+ */
272
+ export function selectTrialHistory(trials, { scopeId, round } = {}) {
273
+ return (trials || [])
274
+ .filter((t) => !scopeId || t.scope_id === scopeId)
275
+ .filter((t) => !round || t.round === round || t.round === round - 1)
276
+ .slice(-TRIAL_HISTORY_MAX)
277
+ .map(compactTrial);
278
+ }
279
+
280
+ /**
281
+ * The stagnation term of the inner circuit breaker (plan §2.6).
282
+ *
283
+ * `attempt_budget` counts attempts; it cannot see that the last two produced nothing. One term
284
+ * joins it: `no_progress_k` consecutive non-`kept` trials ends the scope early and queues the
285
+ * EXISTING GATE H proposal rather than blocking the round — composing with the three-level breaker
286
+ * instead of adding a fourth. On a flailing scope this saves three of five attempts.
287
+ *
288
+ * @param {Array<object>} trials - This scope's trial rows, oldest first.
289
+ * @param {number} [k=2] - Consecutive non-`kept` trials that trip the breaker.
290
+ * @returns {{stagnant:boolean, streak:number, k:number}} The streak of trailing non-`kept` trials
291
+ * and whether it has reached `k`.
292
+ */
293
+ export function stagnation(trials, k = 2) {
294
+ let streak = 0;
295
+ for (let i = (trials || []).length - 1; i >= 0; i--) {
296
+ if (trials[i].status === "kept") break;
297
+ streak++;
298
+ }
299
+ return { stagnant: streak >= k && k > 0, streak, k };
300
+ }
301
+
302
+ /**
303
+ * Assemble a WorkOrder envelope. Pure given its inputs — the CLI wrapper does the disk reads.
304
+ * @param {object} opts - The order inputs (destructured):
305
+ * @param {string} opts.slug - Feature slug (names the order_id and substrate roots).
306
+ * @param {string} opts.worker - Target worker skill (e.g. "task-executor").
307
+ * @param {string} [opts.mode="orchestrated"] - Dispatch mode written onto the order.
308
+ * @param {string} [opts.operation] - Operation the worker runs (drives the substrate template).
309
+ * @param {number} [opts.round] - Round number (for the order_id suffix + attempt pairing).
310
+ * @param {number} [opts.attempt] - Attempt number within the round.
311
+ * @param {object} [opts.scope] - Scope contract, carried as payload.scope_contract + substrate source.
312
+ * @param {Array<object>} [opts.tasks] - Task entries to include in the payload.
313
+ * @param {Array<{id:string,answer:string}>} [opts.decisions] - This scope's advisor answers.
314
+ * @param {Array<object>} [opts.digestedErrors] - Prior-attempt AEGIS triples (payload.digested_errors).
315
+ * @param {Array<object>} [opts.trialHistory] - Compacted trial rows (payload.trial_history).
316
+ * @param {string} [opts.testCmd] - Verify command, recorded under payload.verify.test_cmd.
317
+ * @param {object} [opts.payloadExtra] - Extra payload fields merged last (spec_folder, feature, …).
318
+ * @param {string} [opts.specDir] - Spec directory, threaded into the substrate template.
319
+ * @param {object} [opts.interaction] - Interaction flags (e.g. {pause_gates}).
320
+ * @returns {object} A WorkOrder: {schema_version, order_id ("<slug>/<suffix>"), worker, mode,
321
+ * operation?, interaction?, substrate (from {@link substrateFor}), payload{…}}. A coachable
322
+ * worker also gets payload.kb_rules_path. Not validated here — the CLI validates before writing.
323
+ */
324
+ export function compileOrder({
325
+ slug, worker, mode = "orchestrated", operation, round, attempt,
326
+ scope, tasks, decisions, digestedErrors, trialHistory, testCmd, payloadExtra, specDir, interaction,
327
+ }) {
328
+ const suffix = round && attempt ? `r${round}-a${attempt}` : round ? `${operation}-r${round}` : operation;
329
+ const order = {
330
+ schema_version: 1,
331
+ order_id: `${slug}/${suffix}`,
332
+ worker,
333
+ mode,
334
+ ...(operation ? { operation } : {}),
335
+ ...(interaction ? { interaction } : {}),
336
+ substrate: substrateFor(operation, { slug, specDir, scope }),
337
+ payload: {
338
+ ...(scope ? { scope_contract: scope } : {}),
339
+ ...(tasks?.length ? { tasks } : {}),
340
+ ...(decisions?.length ? { decisions } : {}),
341
+ ...(digestedErrors?.length ? { digested_errors: digestedErrors } : {}),
342
+ ...(trialHistory?.length ? { trial_history: trialHistory } : {}),
343
+ ...(testCmd ? { verify: { test_cmd: testCmd, env: [] } } : {}),
344
+ ...(payloadExtra || {}),
345
+ },
346
+ };
347
+ const kbByWorker = { "task-executor": "task-executor", "ba-pitch-analyzer": "ba-pitch-analyzer", "qa-edge-hunter": "qa-edge-hunter" };
348
+ if (kbByWorker[worker]) order.payload.kb_rules_path = relKnowledgeBase(kbByWorker[worker]);
349
+ return order;
350
+ }
351
+
352
+ // ---------------------------------------------------------------------------
353
+ /** The typed argv contract (see `./lib/argv.mjs`). */
354
+ export const ARGV_SPEC = {
355
+ usage: "compile-order.mjs (--scope <contract.json> --round N --attempt M | --task TASK-NNN --slug <slug> " +
356
+ "| --next --slug <slug> | --operation <op> --slug <slug>) [--worker <skill>] [--payload '<json>'] " +
357
+ "[--spec <dir>] [--test-cmd \"<cmd>\"] [--cwd <dir>] [--pause-gates]",
358
+ _: { arity: 0, max: 0, name: "(no positional operands)" },
359
+ cwd: { type: "path" },
360
+ slug: { type: "str" },
361
+ spec: { type: "path" },
362
+ scope: { type: "path" },
363
+ round: { type: "int", min: 1 },
364
+ attempt: { type: "int", min: 1 },
365
+ operation: { type: "str" },
366
+ worker: { type: "str" },
367
+ task: { type: "str" },
368
+ next: { type: "flag" },
369
+ payload: { type: "json" },
370
+ "test-cmd": { type: "str" },
371
+ "pause-gates": { type: "flag" },
372
+ };
373
+
374
+ const isMainModule = isMain(import.meta.url);
375
+ if (isMainModule) {
376
+ const argv = runArgs(ARGV_SPEC);
377
+ /**
378
+ * Read a parsed flag's value, normalising "absent" to null (the shape the body below expects).
379
+ * @param {string} name - Flag name without leading dashes.
380
+ * @returns {*} The parsed value, or null when the flag was not given.
381
+ */
382
+ const flag = (name) => argv[name.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] ?? null;
383
+ /**
384
+ * Test whether a boolean flag was given.
385
+ * @param {string} name - Flag name without leading dashes.
386
+ * @returns {boolean} True when `--<name>` appears in argv.
387
+ */
388
+ const has = (name) => !!flag(name);
389
+ const cwd = resolve(flag("cwd") || process.cwd());
390
+
391
+ let slug = flag("slug");
392
+ let scope = null;
393
+ let specDir = flag("spec") || null;
394
+ const scopePath = flag("scope");
395
+ if (scopePath) {
396
+ // Markdown on disk, JSON on the wire (ADR-0001): the contract is parsed here and the resulting
397
+ // OBJECT is embedded in payload.scope_contract, so the envelope and its schema are unchanged.
398
+ const abs = resolve(cwd, scopePath);
399
+ const found = readContract(abs, SCOPE_CONTRACT);
400
+ if (!found) { console.error(`compile-order: no scope contract at ${abs} (.md or .json)`); process.exit(2); }
401
+ scope = found.contract;
402
+ // shapeup/<slug>/scopes/<id>.md → slug
403
+ if (!slug) slug = basename(dirname(dirname(abs)));
404
+ }
405
+ if (!slug) { console.error("compile-order: --slug (or a --scope path it derives from) is required"); process.exit(2); }
406
+ if (!specDir && existsSync(defaultSpecDir(cwd, slug))) {
407
+ specDir = relShared(slug, "spec");
408
+ }
409
+
410
+ const round = flag("round");
411
+ const attempt = flag("attempt");
412
+ // Operation → owning worker (mirrors domain.schema.json $defs/Operation ownership). Lets a
413
+ // non-build dispatch resolve its worker from the operation alone, without a redundant --worker.
414
+ const OP_OWNER = {
415
+ analyze: "ba-pitch-analyzer", "generate-board": "ba-pitch-analyzer", reconcile: "ba-pitch-analyzer",
416
+ "retrofit-surface": "ba-pitch-analyzer", coverage: "ba-pitch-analyzer",
417
+ "map-scopes": "scope-architect", remap: "scope-architect", "split-scope": "scope-architect",
418
+ wire: "solution-architect", evaluate: "spec-evaluator", orient: "orient",
419
+ hunt: "qa-edge-hunter", recheck: "qa-edge-hunter", translate: "translator",
420
+ hammer: "scope-hammer", coach: "coach", adjudicate: "advisor-protocol",
421
+ };
422
+ let operation = flag("operation") || (scopePath || flag("task") || has("next") ? "execute" : null);
423
+ const worker = flag("worker")
424
+ || (scopePath || flag("task") || has("next") ? "task-executor" : null)
425
+ || (operation ? OP_OWNER[operation] : null);
426
+ if (!worker || !operation) { console.error("compile-order: could not resolve --worker/--operation"); process.exit(2); }
427
+
428
+ // Task selection.
429
+ let tasks;
430
+ const board = readBoard(cwd, slug);
431
+ if (flag("task")) {
432
+ tasks = board.filter((t) => t.id === flag("task"));
433
+ if (!tasks.length) { console.error(`compile-order: ${flag("task")} not found on the ${slug} board`); process.exit(2); }
434
+ } else if (has("next")) {
435
+ const doneIds = new Set(board.filter((t) => t.status === "done").map((t) => t.id));
436
+ tasks = board.filter((t) => t.status === "ready" && t.depends_on.every((d) => doneIds.has(d))).slice(0, 1);
437
+ if (!tasks.length) { console.error("compile-order: no ready task with satisfied dependencies"); process.exit(2); }
438
+ } else if (scope) {
439
+ // Scope attempt: the scope's own task list if the contract names one, else every
440
+ // not-done task on the board (the attempt loop owns sequencing, not the worker).
441
+ const named = new Set(scope.tasks || []);
442
+ tasks = board.filter((t) => (named.size ? named.has(t.id) : t.status !== "done"));
443
+ }
444
+
445
+ // Ledger decisions for this scope.
446
+ const ledgerPath = roundLedger(cwd, slug);
447
+ const decisions = existsSync(ledgerPath)
448
+ ? ledgerDecisions(readFileSync(ledgerPath, "utf8"), scope?.scope_id)
449
+ : [];
450
+
451
+ // inspect() — the attempt loop's history (see selectTrialHistory above).
452
+ //
453
+ // NON-REGRESSION (plan §7). With no `trials.jsonl` on disk this falls back to exactly today's
454
+ // read — the previous attempt's verdict artifact, AEGIS triples only, byte-for-byte the same
455
+ // order. Every new arm is skipped when its artifact is absent, per the ✦/✚ convention.
456
+ let digestedErrors = [];
457
+ let trialHistory = [];
458
+ const trialsPath = trials(cwd, slug);
459
+ const allTrials = readTrials(trialsPath);
460
+ if (allTrials.length) {
461
+ trialHistory = selectTrialHistory(allTrials, { scopeId: scope?.scope_id, round });
462
+ const lastRed = [...trialHistory].reverse().find((t) => t.digest?.length);
463
+ digestedErrors = lastRed?.digest ?? [];
464
+ } else if (round && attempt && attempt > 1) {
465
+ const dir = verdictsDir(cwd, slug);
466
+ // Immutable addressing (v1.5) writes `r<R>-a<A>-t<T>.json`; the pre-v1.5 unsuffixed name is
467
+ // still read so artifacts already on disk keep working.
468
+ const suffixed = existsSync(dir)
469
+ ? readdirSync(dir)
470
+ .filter((f) => new RegExp(`^r${round}-a${attempt - 1}-t\\d+\\.json$`).test(f))
471
+ .sort((a, b) => Number(b.match(/-t(\d+)\./)[1]) - Number(a.match(/-t(\d+)\./)[1])) // newest first
472
+ .map((f) => join(dir, f))
473
+ : [];
474
+ const prev = [...suffixed, join(dir, `r${round}-a${attempt - 1}.json`)].find((p) => existsSync(p));
475
+ if (prev) {
476
+ try { digestedErrors = JSON.parse(readFileSync(prev, "utf8")).discovered_tasks || []; } catch { /* stale artifact → none */ }
477
+ }
478
+ }
479
+
480
+ // `--payload` is coerced and rejected at the argv boundary now (type "json"), so a malformed
481
+ // payload never reaches this point — the same discipline validate-envelope applies to an order.
482
+ let payloadExtra = flag("payload") || {};
483
+ if (specDir && !payloadExtra.spec_folder) payloadExtra.spec_folder = specDir;
484
+ if (!payloadExtra.feature) payloadExtra.feature = slug;
485
+
486
+ const order = compileOrder({
487
+ slug, worker, operation, round, attempt, scope, tasks, decisions, digestedErrors, trialHistory,
488
+ testCmd: flag("test-cmd"), payloadExtra, specDir,
489
+ interaction: has("pause-gates") ? { pause_gates: true } : { pause_gates: false },
490
+ });
491
+
492
+ const { valid, errors } = validate(order, ORDER_SCHEMA);
493
+ if (!valid) {
494
+ console.error("compile-order: produced an order that fails its own schema — refusing to write:");
495
+ for (const e of errors) console.error(` ✗ ${e}`);
496
+ process.exit(1);
497
+ }
498
+ const outDir = ordersDir(cwd, slug);
499
+ mkdirSync(outDir, { recursive: true });
500
+ const outPath = join(outDir, `${order.order_id.split("/")[1]}.json`);
501
+ writeFileSync(outPath, JSON.stringify(order, null, 2) + "\n");
502
+ console.log(outPath);
503
+
504
+ // The stagnation breaker reports on stderr, never on stdout: stdout is the order path the
505
+ // orchestrator consumes, and a breaker that corrupts the pipeline's own output would be worse
506
+ // than the flailing it detects. It advises; the orchestrator queues the GATE H proposal.
507
+ if (scope?.scope_id) {
508
+ const k = Number(scope.no_progress_k ?? payloadExtra.no_progress_k ?? 2);
509
+ const st = stagnation(allTrials.filter((t) => t.scope_id === scope.scope_id), k);
510
+ if (st.stagnant) {
511
+ console.error(JSON.stringify({
512
+ breaker: "stagnation", scope_id: scope.scope_id, streak: st.streak, no_progress_k: st.k,
513
+ action: "queue a GATE H proposal for this scope and move on — do NOT block the round",
514
+ reason: `${st.streak} consecutive non-kept trials: the loop is not ratcheting on this scope.`,
515
+ }));
516
+ }
517
+ }
518
+ }