shapeup-sdlc 1.7.0 → 3.0.0

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 (98) hide show
  1. package/.claude/settings.local.example.json +5 -5
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/AGENTS.md +17 -3
  4. package/README.md +124 -103
  5. package/SECURITY.md +37 -30
  6. package/bin/init.mjs +77 -47
  7. package/bin/lib/grant.mjs +145 -0
  8. package/commands/build.md +7 -6
  9. package/commands/ship.md +29 -16
  10. package/commands/wire.md +1 -1
  11. package/hooks/dispatch-receipt.mjs +195 -0
  12. package/hooks/gate-intake.mjs +3 -3
  13. package/hooks/gate-zerowork.mjs +25 -24
  14. package/hooks/hooks.json +9 -48
  15. package/hooks/lib/decision.mjs +19 -8
  16. package/hooks/safety-spine.mjs +3 -3
  17. package/hooks/sandbox-guard.mjs +84 -40
  18. package/{skills/tech-lead/scripts/compile-order.mjs → kernel/compile.mjs} +283 -41
  19. package/{skills/tech-lead/scripts/gate-answers.mjs → kernel/gate.mjs} +58 -14
  20. package/kernel/harness.mjs +134 -0
  21. package/{skills/tech-lead/scripts/fit-check.mjs → kernel/init/fit.mjs} +15 -12
  22. package/{skills/tech-lead/scripts/init-run.mjs → kernel/init/run.mjs} +152 -31
  23. package/{skills/tech-lead/scripts → kernel}/lib/argv.mjs +52 -31
  24. package/{skills/tech-lead/scripts/lib/contract-md.mjs → kernel/lib/contract.mjs} +251 -7
  25. package/kernel/lib/paths.mjs +491 -0
  26. package/kernel/probe/concurrency.mjs +510 -0
  27. package/{skills/tech-lead/scripts/aegis-digest.mjs → kernel/probe/digest.mjs} +9 -10
  28. package/kernel/probe/eval.mjs +77 -0
  29. package/kernel/probe/leg.mjs +125 -0
  30. package/{skills/tech-lead/scripts/resume-state.mjs → kernel/probe/resume.mjs} +204 -63
  31. package/{skills/tech-lead/scripts → kernel/probe}/stats.mjs +93 -11
  32. package/kernel/probe/t0.mjs +66 -0
  33. package/{skills/ba-pitch-analyzer/scripts/board-derive.mjs → kernel/reduce/board.mjs} +99 -23
  34. package/kernel/reduce/graph.mjs +437 -0
  35. package/{skills/tech-lead/scripts/hill-derive.mjs → kernel/reduce/hill.mjs} +28 -9
  36. package/kernel/reduce/ingest.mjs +633 -0
  37. package/{hooks/slop-cleaner.mjs → kernel/reduce/leftovers.mjs} +40 -57
  38. package/{skills/tech-lead/scripts/ship-report.mjs → kernel/reduce/ship.mjs} +61 -13
  39. package/{skills/tech-lead/scripts/run-snapshot.mjs → kernel/reduce/snapshot.mjs} +21 -15
  40. package/{skills/spec-evaluator/scripts/verdict-ledger.mjs → kernel/reduce/verdict.mjs} +13 -7
  41. package/kernel/report/export.mjs +325 -0
  42. package/kernel/report/facts.mjs +347 -0
  43. package/{skills/tech-lead/scripts/budget-check.mjs → kernel/verify/budget.mjs} +15 -12
  44. package/kernel/verify/dispatch.mjs +114 -0
  45. package/{skills/tech-lead/scripts/validate-envelope.mjs → kernel/verify/envelope.mjs} +20 -15
  46. package/{skills/tech-lead/scripts/lib → kernel/verify}/ratchet-tree.mjs +36 -14
  47. package/kernel/verify/skills.mjs +125 -0
  48. package/kernel/verify/spec.mjs +559 -0
  49. package/{skills/tech-lead/scripts/t0-verify.mjs → kernel/verify/t0.mjs} +134 -24
  50. package/{skills/tech-lead/scripts/trace-lint.mjs → kernel/verify/trace.mjs} +26 -11
  51. package/oracles/http-oracle.mjs +1 -1
  52. package/oracles/process-oracle.mjs +1 -1
  53. package/oracles/snapshot-oracle.mjs +1 -1
  54. package/oracles/test-oracle.mjs +1 -1
  55. package/package.json +11 -9
  56. package/skills/ba-pitch-analyzer/SKILL.md +5 -5
  57. package/skills/ba-pitch-analyzer/assets/templates/_index.tmpl.md +1 -1
  58. package/skills/ba-pitch-analyzer/assets/templates/api-feasibility.tmpl.md +12 -7
  59. package/skills/ba-pitch-analyzer/assets/templates/contracts/third-party-api.contract.tmpl.md +6 -3
  60. package/skills/ba-pitch-analyzer/assets/templates/cross-context/migration-plan.tmpl.md +5 -4
  61. package/skills/ba-pitch-analyzer/assets/templates/cross-context/team-handoff.tmpl.md +23 -13
  62. package/skills/ba-pitch-analyzer/assets/templates/scope-summary.tmpl.md +17 -12
  63. package/skills/ba-pitch-analyzer/assets/templates/synthesis.tmpl.md +17 -38
  64. package/skills/ba-pitch-analyzer/assets/templates/task.tmpl.md +3 -0
  65. package/skills/ba-pitch-analyzer/references/contract-patterns.md +8 -4
  66. package/skills/ba-pitch-analyzer/references/doc-schemas.md +2 -0
  67. package/skills/ba-pitch-analyzer/references/task-generation.md +2 -2
  68. package/skills/qa-edge-hunter/SKILL.md +7 -3
  69. package/skills/scope-architect/SKILL.md +40 -6
  70. package/skills/solution-architect/SKILL.md +19 -4
  71. package/skills/spec-evaluator/SKILL.md +12 -2
  72. package/skills/task-executor/SKILL.md +7 -6
  73. package/skills/tech-lead/SKILL.md +50 -38
  74. package/skills/tech-lead/references/gates.md +69 -20
  75. package/skills/tech-lead/references/protocol.md +832 -0
  76. package/skills/tech-lead/schemas/domain.schema.json +341 -66
  77. package/skills/tech-lead/schemas/work-order.schema.json +11 -2
  78. package/skills/tech-lead/schemas/work-result.schema.json +1 -1
  79. package/skills/tech-lead/workflows/shapeup-run.js +1313 -750
  80. package/hooks/anti-rationalization.mjs +0 -238
  81. package/hooks/compact-snapshot.mjs +0 -47
  82. package/hooks/gate-deadline.mjs +0 -147
  83. package/hooks/gate-l2.mjs +0 -161
  84. package/hooks/session-rehydrate.mjs +0 -108
  85. package/skills/ba-pitch-analyzer/scripts/spec-lint.mjs +0 -239
  86. package/skills/spec-evaluator/README.md +0 -93
  87. package/skills/tech-lead/README.md +0 -71
  88. package/skills/tech-lead/references/delegation.md +0 -261
  89. package/skills/tech-lead/references/hard-rules.md +0 -34
  90. package/skills/tech-lead/references/invocation.md +0 -45
  91. package/skills/tech-lead/references/ledger-schema.md +0 -213
  92. package/skills/tech-lead/references/round-protocol.md +0 -234
  93. package/skills/tech-lead/references/state-model.md +0 -66
  94. package/skills/tech-lead/scripts/ingest-result.mjs +0 -258
  95. package/skills/tech-lead/scripts/lib/is-main.mjs +0 -81
  96. package/skills/tech-lead/scripts/lib/paths.mjs +0 -280
  97. package/skills/tech-lead/scripts/run-workflow.mjs +0 -381
  98. package/skills/translator/README.md +0 -66
@@ -7,7 +7,7 @@
7
7
  // structurally invalid. No agent can fabricate this file's contents
8
8
  // because it is produced by actually running the commands.
9
9
  //
10
- // Zero dependencies, zero network — same discipline as oracles/* and gate-l2.mjs.
10
+ // Zero dependencies, zero network — same discipline as oracles/* and the GATE L2 block.
11
11
  //
12
12
  // THE PAWL (v1.5). This script also owns the ratchet's comparison and its history. The attempt
13
13
  // loop used to be a BUDGETED RETRY LOOP wearing a ratchet's shape: `computeVerdict` returned four
@@ -29,7 +29,7 @@
29
29
  // N+1 build on attempt N instead of restarting from unexplained code.
30
30
  //
31
31
  // Usage:
32
- // node "${CLAUDE_PLUGIN_ROOT}/skills/tech-lead/scripts/t0-verify.mjs" <scope-contract.json> \
32
+ // node "${CLAUDE_PLUGIN_ROOT}/kernel/harness.mjs" verify t0 <scope-contract.json> \
33
33
  // --round N --attempt M [--cwd <dir>] [--out <dir>] [--seesaw-registry <path>] [--no-seesaw]
34
34
  // [--no-ratchet]
35
35
  //
@@ -39,11 +39,28 @@ import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, rea
39
39
  import { join, dirname } from "node:path";
40
40
  import { spawnSync } from "node:child_process";
41
41
  import { createHash } from "node:crypto";
42
- import { digest } from "./aegis-digest.mjs";
43
- import { isMain } from "./lib/is-main.mjs";
44
- import { runArgs } from "./lib/argv.mjs";
45
- import { snapshot, restore, keptRef } from "./lib/ratchet-tree.mjs";
46
- import { readContract, SCOPE_CONTRACT } from "./lib/contract-md.mjs";
42
+ import { digest } from "../probe/digest.mjs";
43
+ import { runArgs } from "../lib/argv.mjs";
44
+ import { snapshot, restore, keptRef } from "./ratchet-tree.mjs";
45
+ import { readContract, SCOPE_CONTRACT } from "../lib/contract.mjs";
46
+ import { runIdFromRoot, localRoot, SHARED } from "../lib/paths.mjs";
47
+
48
+ /**
49
+ * The feature slug a scope contract belongs to, from its path.
50
+ *
51
+ * Scope contracts live at `<SHARED>/<slug>/scopes/<id>.md`, so the slug is the segment two levels
52
+ * above the file. Derived rather than flagged because every caller already names the contract, and
53
+ * a second way to say the same thing is a second thing to get wrong.
54
+ *
55
+ * @param {string} contractPath - Path to the scope contract, absolute or relative.
56
+ * @returns {string} The slug, or "" when the path does not have the expected shape — in which case
57
+ * the caller's `--out` is the only sensible answer and its absence is a usage error.
58
+ */
59
+ export function slugFromContractPath(contractPath) {
60
+ const parts = String(contractPath).split(/[/\\]/);
61
+ const i = parts.lastIndexOf("scopes");
62
+ return i > 0 ? parts[i - 1] : "";
63
+ }
47
64
 
48
65
  /**
49
66
  * Run one shell command and capture its outcome (10-minute timeout).
@@ -71,7 +88,24 @@ function runCommand(cmd, cwd) {
71
88
  */
72
89
  export function runFixtures(fixtures, cwd) {
73
90
  const results = (fixtures || []).map((cmd) => runCommand(cmd, cwd));
74
- return { pass: results.every((r) => r.pass), results };
91
+ // AN ABSENCE IS NOT A PASS, and this is the whole "measured, not claimed" invariant in one line.
92
+ //
93
+ // `[].every(...)` is `true`, so a scope whose contract declared no fixtures — or whose fixtures
94
+ // failed to PARSE, which is how it actually happened — scored `pass: true` and the verdict came
95
+ // back `overall: "green"`. Measured directly:
96
+ //
97
+ // runFixtures(undefined) -> pass=true, results=0
98
+ // computeVerdict(…) -> {"overall":"green"}
99
+ //
100
+ // Nothing ran, and the scope was certified. That is not a weaker version of the guarantee, it is
101
+ // the inverse of it: the one layer the evaluator is required to cite, green on zero evidence. The
102
+ // same reasoning the decision ledger states one directory over — "an absence proves nothing and
103
+ // must not sharpen the message".
104
+ //
105
+ // `ran` is reported rather than folded into `pass` so the two facts stay distinguishable: a scope
106
+ // with nothing to run is a contract that is not finished, which is different from a scope whose
107
+ // fixtures ran and failed, and the ratchet should be able to tell them apart.
108
+ return { pass: results.length > 0 && results.every((r) => r.pass), ran: results.length > 0, results };
75
109
  }
76
110
 
77
111
  /**
@@ -183,9 +217,43 @@ export function better(next, current) {
183
217
  if (next.regressions !== current.regressions) return next.regressions < current.regressions;
184
218
  if (next.fixtures_passed !== current.fixtures_passed) return next.fixtures_passed > current.fixtures_passed;
185
219
  if (next.db_probe !== current.db_probe) return (next.db_probe ?? 0) > (current.db_probe ?? 0);
220
+ // EVERY COMPONENT TIES. What that means depends entirely on whether the incumbent was green.
221
+ //
222
+ // On a RED scope a tie is the sawtooth the pawl exists to stop, and it stays `false` — that is the
223
+ // case the rule above was written for and the case the guard fixtures assert (2/5 vs 2/5).
224
+ //
225
+ // On a GREEN scope there is no score left to win. Every further attempt ties BY CONSTRUCTION, so
226
+ // `false` restores the tree every time and the scope can never change again: the pawl stops being
227
+ // a pawl and becomes a weld. That is exactly the state a spec-conformance round is in. EVAL
228
+ // returns FAIL citing bugs T0's fixtures do not test — a bare stack trace, an error message that
229
+ // does not match the catalogue, validation ordered after a load — the next round's workers fix
230
+ // them, T0 scores 1/1 again because its fixtures never tested the prose, and the ratchet throws
231
+ // the fix away.
232
+ //
233
+ // Measured on a live run: round 2 produced six trials, 0 kept, 6 reverted, `bin/todo.js` byte-for-
234
+ // byte unchanged, and all three cited bugs reproducing. The FAIL → fix → re-evaluate loop — the
235
+ // harness's central promise — is inert for precisely the defects EVAL is best at finding.
236
+ //
237
+ // So: a green tie KEEPS. The tree may move when the score cannot, because by then the work being
238
+ // done is work T0 does not measure, and refusing it does not protect the scope — it freezes it.
239
+ if (isGreenScore(next) && isGreenScore(current)) return true;
186
240
  return false;
187
241
  }
188
242
 
243
+ /**
244
+ * Is this score a clean pass — every fixture passing, none of them absent, no outstanding regression?
245
+ *
246
+ * `fixtures_total > 0` is load-bearing: a scope with no fixtures has nothing to be green ABOUT, and
247
+ * treating its empty score as a pass is the same absence-reads-as-success mistake `runFixtures`
248
+ * made one function above.
249
+ *
250
+ * @param {{regressions:number, fixtures_passed:number, fixtures_total:number}} s - A trial score.
251
+ * @returns {boolean} True when the score represents a real, complete pass.
252
+ */
253
+ function isGreenScore(s) {
254
+ return s.regressions === 0 && s.fixtures_total > 0 && s.fixtures_passed === s.fixtures_total;
255
+ }
256
+
189
257
  /**
190
258
  * The one rule that replaces the protocol's two red branches.
191
259
  *
@@ -327,7 +395,7 @@ function sha256(text) {
327
395
  *
328
396
  * An `existsSync` guard would be check-then-write: still racy, and still a policy expressed in code
329
397
  * rather than a property of the store. `flag: "wx"` makes overwriting IMPOSSIBLE — the filesystem
330
- * refuses with EEXIST and the loop moves to the next ordinal. Same class of move as `lib/is-main.mjs`:
398
+ * refuses with EEXIST and the loop moves to the next ordinal. Same class of move as the entry-point guard:
331
399
  * replace a fragile comparison with one that cannot silently be wrong.
332
400
  *
333
401
  * The evaluator's citation contract is unaffected: it re-hashes whatever path it is handed, and
@@ -364,7 +432,7 @@ export function writeArtifact(outDir, round, attempt, verdictBody) {
364
432
 
365
433
  /** The typed argv contract (see `./lib/argv.mjs`). */
366
434
  export const ARGV_SPEC = {
367
- usage: "t0-verify.mjs <scope-contract.json> --round N --attempt M [--cwd <dir>] [--out <dir>] " +
435
+ usage: "harness.mjs verify t0 <scope-contract.json> --round N --attempt M [--cwd <dir>] [--out <dir>] " +
368
436
  "[--seesaw-registry <path>] [--no-seesaw] [--no-ratchet]",
369
437
  _: { arity: 1, max: 1, name: "scope-contract.json" },
370
438
  round: { type: "int", min: 1, required: true },
@@ -377,25 +445,45 @@ export const ARGV_SPEC = {
377
445
  };
378
446
 
379
447
  /**
380
- * CLI entry: run a scope's fixtures + probe + seesaw, write the verdict artifact, print it, and
381
- * exit 0 (green) / 1 (red) / 2 (usage).
382
- * @returns {Promise<void>} Resolves after writing the artifact; the process exit code carries the verdict.
448
+ * Run the T0 evidence layer for one attempt and write the verdict artifact the judge cites.
449
+ *
450
+ * @param {string[]} rawArgv - The subcommand's own arguments (harness.mjs strips the verb words).
451
+ * @returns {(Promise<void>|void)} Settles when the subcommand has written its output; most paths
452
+ * call `process.exit()` with the subcommand's documented code rather than returning.
383
453
  */
384
- async function main() {
385
- const args = runArgs(ARGV_SPEC);
454
+ export async function cli(rawArgv) {
455
+ const args = runArgs(ARGV_SPEC, rawArgv);
386
456
  const contractPath = args._[0];
387
- // Markdown first, legacy JSON second (ADR-0001, lib/contract-md.mjs).
457
+ // Markdown first, legacy JSON second (ADR-0001, lib/contract.mjs).
388
458
  const found = readContract(contractPath, SCOPE_CONTRACT);
389
459
  if (!found) { console.error(`t0-verify: no scope contract at ${contractPath} (.md or .json)`); process.exit(2); }
390
460
  const contract = found.contract;
391
461
  const cwd = args.cwd || process.cwd();
392
- const outDir = args.out || dirname(dirname(contractPath)); // default: <slug>/ (parent of scopes/)
462
+ // WHERE THE VERDICT LANDS, and the one thing about this file that must not be derived from the
463
+ // contract's own location.
464
+ //
465
+ // This used to be `dirname(dirname(contractPath))` — "the parent of scopes/" — which was correct
466
+ // while scope contracts and the run trace shared a root. ADR-0001 split the tiers and moved scope
467
+ // contracts to COMMITTED `shapeup/<slug>/scopes/`, so the default silently followed them: every
468
+ // T0 verdict and trial row was written to `shapeup/<slug>/t0/` while `probe t0` — the ONLY reader,
469
+ // and the one the build round's confirm stage asks — resolves `verdictsDir()` to
470
+ // `.shapeup/<slug>/t0/`. AGENTS.md is unambiguous that T0 artifacts are LOCAL tier.
471
+ //
472
+ // Measured on a live run: six verdicts and six trial rows were written, `foundation` among them
473
+ // with `fixtures_passed: 2/2, status: kept` — a real green — and the round still reported ZERO
474
+ // green scopes, tripped the inner breaker and returned `gate_h`. The evidence existed, was
475
+ // correct, and was invisible to the only thing that reads it. That is the same silent disconnect
476
+ // `hooks/lib/decision.mjs` records in its own header, one directory over.
477
+ //
478
+ // `--out` still wins: `verify t0` is also called with an explicit run root, and that caller knows
479
+ // better than any derivation.
480
+ const outDir = args.out || localRoot(cwd, slugFromContractPath(contractPath));
393
481
  const round = args.round;
394
482
  const attempt = args.attempt;
395
483
 
396
484
  const fixtures = runFixtures(contract.e2e_verification_fixtures, cwd);
397
485
  const dbProbe = runDbProbe(contract.db_probe, cwd);
398
- // --seesaw-registry is expected explicitly (tech-lead always passes it, delegation.md 3c);
486
+ // --seesaw-registry is expected explicitly (tech-lead always passes it, protocol.md 3c);
399
487
  // standalone CLI use without it simply skips the seesaw check rather than guessing a path.
400
488
  const seesawRegistry = args.noSeesaw ? null : args.seesawRegistry || null;
401
489
  const seesaw = args.noSeesaw || fixtures.pass === false
@@ -417,7 +505,13 @@ async function main() {
417
505
  const crashed = fixtures.results.some((r) => r.error) || !!dbProbe?.error;
418
506
  const { status, action } = decideStatus(verdictBetter, crashed);
419
507
 
508
+ // The run key, read from the receipt that lives in the run root this script was pointed at.
509
+ // `--out` IS that root, so identity comes from the receipt rather than from parsing a slug back
510
+ // out of a directory name; a caller that points `--out` somewhere else simply gets no key.
511
+ const runId = runIdFromRoot(outDir);
512
+
420
513
  const { path, sha256: hash, trial } = writeArtifact(outDir, round, attempt, {
514
+ ...(runId ? { run_id: runId } : {}),
421
515
  scope_id: contract.scope_id,
422
516
  fixtures: fixtures.results.map(({ cmd, exit, pass }) => ({ cmd, exit, pass })),
423
517
  db_probe: dbProbe && { cmd: dbProbe.cmd, exit: dbProbe.exit, pass: dbProbe.pass },
@@ -431,18 +525,37 @@ async function main() {
431
525
  // the pre-v1.5 behaviour, kept for standalone CLI use and for any caller managing its own tree.
432
526
  let tree = { ok: false, reason: "--no-ratchet" };
433
527
  if (!args.noRatchet) {
434
- tree = action === "keep" ? snapshot(contract.scope_id, cwd) : restore(contract.scope_id, cwd);
528
+ // The revert is bounded by what this scope was allowed to write. Unbounded, it rolls back every
529
+ // other scope building alongside it — see `restore`.
530
+ const own = contract.allowed_file_substrate || [];
531
+ tree = action === "keep" ? snapshot(contract.scope_id, cwd) : restore(contract.scope_id, cwd, own);
435
532
  // First trial with nothing to restore to: there is no kept tree yet BY DEFINITION. Take one,
436
533
  // so trial 2 has a floor to fall back to instead of inheriting the "no revert at all" defect.
437
- if (action === "restore" && !tree.ok) tree = { ...snapshot(contract.scope_id, cwd), fell_back_to: "snapshot" };
534
+ // Gated on `!baseline` (no prior kept/rebased trial exists at all), NOT on `!tree.ok` alone —
535
+ // a genuine restore failure with a real baseline to have restored to must NOT be silently
536
+ // promoted as the new kept tree; it stays `tree.ok === false` so the trial row records
537
+ // `tree_ref: null` and the failure stays visible instead of quietly becoming the baseline.
538
+ if (action === "restore" && !tree.ok && !baseline) tree = { ...snapshot(contract.scope_id, cwd), fell_back_to: "snapshot" };
438
539
  }
439
540
 
440
541
  // `baseline_trial` is the parent link — the experiment DAG (lineage, PARENT_OF and a genuine
441
542
  // SUPERSEDES edge) delivered as one field, with no graph store behind it.
442
- const trialNo = readTrials(trialsPath).length + 1;
543
+ //
544
+ // COUNTED WITHIN THE SCOPE, NEVER ACROSS THE FILE. This used to be
545
+ // `readTrials(trialsPath).length + 1` — every row in the run, whoever wrote it — which is a
546
+ // read-modify-write counter over a file that concurrent scopes append to. Scopes build in
547
+ // parallel, so several `verify t0` processes reach this line at once, each reads the same length
548
+ // and each writes the same ordinal: four concurrent scopes produced ordinals [1,1,1,4] and
549
+ // [1,1,3,3]. The ordinal is only ever consumed WITHIN a scope — `baseline` is picked from
550
+ // `priorTrials`, which is already filtered by `scope_id`, and the ratchet report groups by scope
551
+ // before it sorts — so counting the scope's own prior trials is both the race-free answer and the
552
+ // one the readers actually want. Two attempts of the SAME scope cannot race: one worker leg owns
553
+ // a scope and runs its attempts in sequence.
554
+ const trialNo = priorTrials.length + 1;
443
555
  const row = {
444
556
  schema_version: 1,
445
557
  trial: trialNo,
558
+ ...(runId ? { run_id: runId } : {}),
446
559
  round, attempt,
447
560
  scope_id: contract.scope_id,
448
561
  at: new Date().toISOString(),
@@ -465,6 +578,3 @@ async function main() {
465
578
  process.exit(verdict.overall === "green" ? 0 : 1);
466
579
  }
467
580
 
468
- if (isMain(import.meta.url)) {
469
- main();
470
- }
@@ -30,18 +30,17 @@
30
30
  // Writes LOCAL .shapeup/<slug>/trace/report.json (regenerated each run)
31
31
  // LOCAL .shapeup/<slug>/trace/wiring.mmd (Mermaid view of the checked graph)
32
32
  //
33
- // Zero dependencies, zero network — same discipline as t0-verify.mjs / compile-order.mjs.
33
+ // Zero dependencies, zero network — same discipline as `harness verify t0` / `harness compile`.
34
34
  //
35
- // Usage: node skills/tech-lead/scripts/trace-lint.mjs --slug <slug> [--cwd <dir>] [--gate] [--quiet]
35
+ // Usage: node kernel/harness.mjs verify trace --slug <slug> [--cwd <dir>] [--gate] [--quiet]
36
36
  // Exit: advisory (default) → always 0. --gate → 1 when overall is red.
37
37
 
38
38
  import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from "node:fs";
39
39
  import { resolve, join, dirname, relative, isAbsolute } from "node:path";
40
- import { readBoard } from "./compile-order.mjs";
41
- import { isMain } from "./lib/is-main.mjs";
42
- import { runArgs } from "./lib/argv.mjs";
43
- import { sharedRoot, traceDir, relLocal } from "./lib/paths.mjs";
44
- import { readContract, unreadableReason, WIRING_MAP, PROJECT_PROFILE } from "./lib/contract-md.mjs";
40
+ import { readBoard } from "../compile.mjs";
41
+ import { runArgs } from "../lib/argv.mjs";
42
+ import { sharedRoot, traceDir, relLocal } from "../lib/paths.mjs";
43
+ import { readContract, unreadableReason, LEGACY_LAYOUT, WIRING_MAP, PROJECT_PROFILE } from "../lib/contract.mjs";
45
44
 
46
45
  // --- requirements.md registry parser -----------------------------------------
47
46
  // A committed markdown table: | REQ-id | clause (verbatim) | source | status | note |
@@ -273,6 +272,16 @@ export function traceLint(slug, { cwd, gate = false }) {
273
272
  skipped_reason: `wiring-map.md is present but unreadable (${wiringUnreadable}) — reachability cannot be claimed.` };
274
273
  }
275
274
 
275
+ // A map that parsed only through the migration reader still WORKS — but silence about that is
276
+ // how a temporary fallback becomes a permanent second format. Warn, so the next `wire` converges
277
+ // the file on the canonical table rather than the reader carrying the old shape indefinitely.
278
+ if (wiringMap && wiringMap[LEGACY_LAYOUT]) {
279
+ findings.push({ severity: "warn", code: "WIRING-LEGACY-LAYOUT", message:
280
+ "wiring-map.md is in the pre-canonical per-use-case layout (`### UC-xx` + a `| Field | Value |` table). " +
281
+ "It reads correctly through the migration reader, but the canonical form is one `## Wiring` table — " +
282
+ "re-run the wire operation to converge it, so this artifact stops having two shapes." });
283
+ }
284
+
276
285
  if (wiringMap) {
277
286
  let profileFound = null;
278
287
  try { profileFound = readContract(profilePath, PROJECT_PROFILE); }
@@ -329,7 +338,7 @@ export function traceLint(slug, { cwd, gate = false }) {
329
338
  // ---------------------------------------------------------------------------
330
339
  /** The typed argv contract (see `./lib/argv.mjs`). */
331
340
  export const ARGV_SPEC = {
332
- usage: "trace-lint.mjs --slug <slug> [--cwd <dir>] [--gate] [--quiet]",
341
+ usage: "harness.mjs verify trace --slug <slug> [--cwd <dir>] [--gate] [--quiet]",
333
342
  _: { arity: 0, max: 0, name: "(no positional operands)" },
334
343
  slug: { type: "str", required: true },
335
344
  cwd: { type: "path" },
@@ -337,9 +346,15 @@ export const ARGV_SPEC = {
337
346
  quiet: { type: "flag" },
338
347
  };
339
348
 
340
- const isMainModule = isMain(import.meta.url);
341
- if (isMainModule) {
342
- const args = runArgs(ARGV_SPEC);
349
+ /**
350
+ * Run the traceability oracle over the spine artifacts and write its report.
351
+ *
352
+ * @param {string[]} rawArgv - The subcommand's own arguments (harness.mjs strips the verb words).
353
+ * @returns {(Promise<void>|void)} Settles when the subcommand has written its output; most paths
354
+ * call `process.exit()` with the subcommand's documented code rather than returning.
355
+ */
356
+ export async function cli(rawArgv) {
357
+ const args = runArgs(ARGV_SPEC, rawArgv);
343
358
  const cwd = resolve(args.cwd || process.cwd());
344
359
  const slug = args.slug;
345
360
  const gate = !!args.gate;
@@ -34,7 +34,7 @@ import { spawn } from "node:child_process";
34
34
  import { createServer } from "node:net";
35
35
  import { readFileSync } from "node:fs";
36
36
  import { matchNum, toRegExp, formatReport } from "./_shared.mjs";
37
- import { isMain } from "../skills/tech-lead/scripts/lib/is-main.mjs";
37
+ import { isMain } from "../kernel/lib/argv.mjs";
38
38
 
39
39
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
40
40
 
@@ -25,7 +25,7 @@ import { spawnSync } from "node:child_process";
25
25
  import { mkdtempSync, writeFileSync, rmSync, readFileSync } from "node:fs";
26
26
  import { tmpdir } from "node:os";
27
27
  import { join } from "node:path";
28
- import { isMain } from "../skills/tech-lead/scripts/lib/is-main.mjs";
28
+ import { isMain } from "../kernel/lib/argv.mjs";
29
29
 
30
30
  const CRASH_RE = /at\s+.*:\d+:\d+|Traceback|panic:|unhandled|Segmentation fault/i;
31
31
 
@@ -28,7 +28,7 @@ import { spawnSync } from "node:child_process";
28
28
  import { readFileSync, existsSync } from "node:fs";
29
29
  import { dirname, isAbsolute, join } from "node:path";
30
30
  import { CRASH_RE, formatReport } from "./_shared.mjs";
31
- import { isMain } from "../skills/tech-lead/scripts/lib/is-main.mjs";
31
+ import { isMain } from "../kernel/lib/argv.mjs";
32
32
 
33
33
  // Normalize so trailing spaces and final-newline differences never cause a spurious FAIL.
34
34
  function normalize(s) {
@@ -31,7 +31,7 @@
31
31
  import { spawnSync } from "node:child_process";
32
32
  import { readFileSync } from "node:fs";
33
33
  import { matchNum, toRegExp, formatReport } from "./_shared.mjs";
34
- import { isMain } from "../skills/tech-lead/scripts/lib/is-main.mjs";
34
+ import { isMain } from "../kernel/lib/argv.mjs";
35
35
 
36
36
  // Parse common test-runner output into { total, pass, fail, failing[] }.
37
37
  function parseSuite(out) {
package/package.json CHANGED
@@ -1,21 +1,22 @@
1
1
  {
2
2
  "name": "shapeup-sdlc",
3
- "version": "1.7.0",
3
+ "version": "3.0.0",
4
4
  "description": "Shape Up for coding agents \u2014 with gates the agent can't talk its way past. Harness for Claude Code.",
5
5
  "bin": {
6
6
  "shapeup-sdlc": "bin/init.mjs"
7
7
  },
8
8
  "files": [
9
+ ".claude-plugin/",
10
+ ".claude/settings.local.example.json",
11
+ ".env.shapeup.example",
12
+ "AGENTS.md",
13
+ "SECURITY.md",
9
14
  "bin/",
10
- "skills/",
11
- "hooks/",
12
15
  "commands/",
16
+ "hooks/",
17
+ "kernel/",
13
18
  "oracles/",
14
- ".claude-plugin/",
15
- "AGENTS.md",
16
- ".env.shapeup.example",
17
- ".claude/settings.local.example.json",
18
- "SECURITY.md"
19
+ "skills/"
19
20
  ],
20
21
  "repository": {
21
22
  "type": "git",
@@ -33,7 +34,8 @@
33
34
  },
34
35
  "scripts": {
35
36
  "test": "node tests/structural.mjs",
36
- "demo": "node tools/demo/record-demo.mjs"
37
+ "demo": "node tools/demo/record-demo.mjs",
38
+ "test:grant": "node tests/grant/executing-grant.mjs"
37
39
  },
38
40
  "keywords": [
39
41
  "shapeup",
@@ -50,7 +50,7 @@ its phase; templates live in `assets/templates/`.
50
50
  2 DDD bounded contexts, aggregates (new vs extended), value objects, domain events,
51
51
  repository interfaces → domain-model.md [references/ddd-patterns.md]
52
52
  2b CONTRACTS (standard lens) typed Request/Response/Error per repository; two-pass rule:
53
- unresolvable at spec time → `⏳ TBD — verify in TASK-NNN-spike-…`, resolved
53
+ unresolvable at spec time → `⏳ TBD — verify in the [UC-x] spike`, resolved
54
54
  post-SPIKE with citation → contracts/ [references/contract-patterns.md]
55
55
  3 UX per screen: state table (idle→loading→error→success), error cases with
56
56
  message+action, ASCII flows → ux-behavior.md [references/ux-behavior-patterns.md]
@@ -64,10 +64,10 @@ its phase; templates live in `assets/templates/`.
64
64
  6 TASKS atomic, ordered, executable → tasks/ (LOCAL root; the one uncommitted branch
65
65
  of the tree — regenerable, machine-local) [references/task-generation.md]
66
66
  7 DERIVE+LINT mechanical, not yours to grade:
67
- node "${CLAUDE_PLUGIN_ROOT}/skills/ba-pitch-analyzer/scripts/board-derive.mjs" --slug <slug> --write
67
+ node "${CLAUDE_PLUGIN_ROOT}/kernel/harness.mjs" reduce board --slug <slug> --write
68
68
  (unlocks = depends_on inverse; Σ hours; critical path; appetite arithmetic —
69
69
  overflow is a fact you REPORT for the caller's HAMMER gate, never resolve)
70
- node "${CLAUDE_PLUGIN_ROOT}/skills/ba-pitch-analyzer/scripts/spec-lint.mjs" --slug <slug>
70
+ node "${CLAUDE_PLUGIN_ROOT}/kernel/harness.mjs" verify spec --slug <slug>
71
71
  (structure, wikilinks, edge symmetry — fix reds, then re-run; you never
72
72
  self-grade with a hand-walked checklist)
73
73
  → scope-summary.md + synthesis.md (traceability matrix, risk register,
@@ -165,7 +165,7 @@ status flips for built work (ingest's job), scope contracts (scope-architect's),
165
165
  /ba-pitch-analyzer --order .shapeup/checkout-vnpay/orders/analyze.json
166
166
 
167
167
  # Standalone — the preamble shim compiles the order (mode: standalone, pause_gates: true):
168
- # node "${CLAUDE_PLUGIN_ROOT}/skills/tech-lead/scripts/compile-order.mjs" --operation analyze --slug <slug> \
168
+ # node "${CLAUDE_PLUGIN_ROOT}/kernel/harness.mjs" compile --operation analyze --slug <slug> \
169
169
  # --worker ba-pitch-analyzer --payload '{"pitch": "docs/pitch.md", "lens": "standard"}'
170
170
  /ba-pitch-analyzer docs/pitch.md # operation: analyze, lens judged
171
171
  /ba-pitch-analyzer --lens standard docs/pitch.md # lens pinned
@@ -175,6 +175,6 @@ Standalone keeps exactly two flags: the pitch input and `--lens`. Every retired
175
175
  caller context: `--tasks-only`/`--from-discovered` → a reconcile order, `--surface-only` → a
176
176
  retrofit-surface order, `--coverage` → a coverage order, `--remap`/`--split` → a
177
177
  scope-architect `map-scopes` order,
178
- `--status` → read `spec-lint.mjs`/`board-derive.mjs` output (zero LLM tokens),
178
+ `--status` → read `harness verify spec`/`harness reduce board` output (zero LLM tokens),
179
179
  `--auto`/`--skip-gate*` → `interaction.pause_gates`, `--upgrade` → an analyze order with the
180
180
  standard lens over an existing lite tree (reconciliation pass: extend, never overwrite Steps).
@@ -64,7 +64,7 @@ audit_rules_version: "2.5"
64
64
 
65
65
  ## Audit Report
66
66
 
67
- *Generated from spec-lint.mjs output — do not edit manually.*
67
+ *Generated from harness verify spec output — do not edit manually.*
68
68
  *skill_version: 2.1 | audit_rules_version: 2.1*
69
69
 
70
70
  ### Score Summary
@@ -14,9 +14,12 @@ dependent implementation tasks can proceed.*
14
14
 
15
15
  ## Detected Third-Party Dependencies
16
16
 
17
- | # | Service | Capability Claimed | Status | SPIKE Task |
18
- |---|---------|-------------------|--------|------------|
19
- | API-01 | [ServiceName] | [What pitch claims this API can do] | ❓ UNVERIFIED | [[TASK-001-spike-[api]]] |
17
+ | # | Service | Capability Claimed | Status | Spike |
18
+ |---|---------|-------------------|--------|-------|
19
+ | API-01 | [ServiceName] | [What pitch claims this API can do] | ❓ UNVERIFIED | open |
20
+
21
+ <!-- The Spike column records STATUS (⏳ open / ✅ done / — none), never a task id.
22
+ This document is committed; the board is not, and its ids renumber per machine. -->
20
23
 
21
24
  ---
22
25
 
@@ -49,10 +52,12 @@ Specifically: [quote or paraphrase relevant pitch sentence].
49
52
  | Partially supported | [Describe workaround] | +[N]h |
50
53
  | Not supported | [Describe alternative or de-scope] | [TBD with PO] |
51
54
 
52
- **Dependent Tasks:**
53
- <!-- List all tasks blocked until this investigation is complete -->
54
- - [[TASK-NNN]] [reason blocked]
55
- - [[TASK-NNN]] [reason blocked]
55
+ **Blocked Use Cases:**
56
+ <!-- What cannot be built until this investigation resolves. Name the committed UC, never
57
+ a task id: this doc is committed and task ids are machine-local. The tasks are found
58
+ by inverting use_case_refs over the LOCAL board. -->
59
+ - [[usecases/UC-Name]] — [reason blocked]
60
+ - [[usecases/UC-Name]] — [reason blocked]
56
61
 
57
62
  **Decision Gate:**
58
63
  - All questions answered + capability confirmed → update `contracts/[repo].contract.md`, remove ⏳ TBD fields, notify SPIKE complete
@@ -5,7 +5,9 @@ feature: "[feature-slug]"
5
5
  repository: "[RepoName]"
6
6
  service: "[ServiceName]"
7
7
  feasibility_ref: "API-[NN]"
8
- spike_task: "[[TASK-NNN-spike-[api]]]"
8
+ spike_status: open # open | done — STATUS, never a task id: this contract is
9
+ # committed and board ids renumber per machine. The spike is
10
+ # reached through feasibility_ref above.
9
11
  status: speculative # → confirmed after SPIKE done
10
12
  skill_version: "2.3"
11
13
  ---
@@ -14,13 +16,14 @@ skill_version: "2.3"
14
16
 
15
17
  ## Source Type: `third-party-api`
16
18
  ## Service: [ServiceName] ([REST API / SDK])
17
- ## Feasibility: ⏳ UNVERIFIED → see [[api-feasibility#API-NN]] + [[TASK-NNN-spike-[api]]]
19
+ ## Feasibility: ⏳ UNVERIFIED → see [[api-feasibility#API-NN]]
18
20
 
19
21
  ---
20
22
 
21
23
  > ⚠️ **SPECULATIVE CONTRACT**
22
24
  > Fields marked `⏳ TBD` are derived from pitch assumptions and community references.
23
- > All `⏳ TBD` entries MUST be confirmed and replaced during [[TASK-NNN-spike-[api]]].
25
+ > All `⏳ TBD` entries MUST be confirmed and replaced by the spike tracked at
26
+ > [[api-feasibility#API-NN]].
24
27
  > Implementation tasks depending on this repository are **blocked** until no `⏳ TBD` remains.
25
28
 
26
29
  ---
@@ -13,7 +13,8 @@ status: draft
13
13
  # Migration Plan: FEATURE TITLE
14
14
 
15
15
  > Generated only when feature modifies existing production schema.
16
- > Each migration task maps to a TASK-M0N in tasks/_index.md.
16
+ > Each migration step anchors to a committed use case; the tasks that carry it are found
17
+ > by inverting `use_case_refs` over the LOCAL board, never linked from here.
17
18
 
18
19
  ---
19
20
 
@@ -31,7 +32,7 @@ status: draft
31
32
 
32
33
  ## Migration Tasks
33
34
 
34
- ### TASK-M01 — [description, e.g. Add nullable columns]
35
+ ### STEP-1 — [description, e.g. Add nullable columns]
35
36
 
36
37
  ```sql
37
38
  -- UP
@@ -46,7 +47,7 @@ Safe to run: during deploy / after deploy / off-hours only
46
47
 
47
48
  ---
48
49
 
49
- ### TASK-M02 — [description, e.g. Backfill existing rows]
50
+ ### STEP-2 — [description, e.g. Backfill existing rows]
50
51
 
51
52
  ```sql
52
53
  -- UP (run in batches of 1000 to avoid lock escalation)
@@ -66,7 +67,7 @@ Estimated rows: ~N | Estimated time: ~Ns | Lock: none
66
67
 
67
68
  ---
68
69
 
69
- ### TASK-M03 — [description, e.g. Enforce NOT NULL constraint]
70
+ ### STEP-3 — [description, e.g. Enforce NOT NULL constraint]
70
71
 
71
72
  ```sql
72
73
  -- UP — only run after M02 verified
@@ -21,19 +21,29 @@ status: draft
21
21
 
22
22
  | From Team | To Team | Artifact | Ready When | Blocker Risk |
23
23
  |-----------|---------|----------|-----------|-------------|
24
- | [Team A] (API) | [Team B] (Mobile) | [[contracts/[repo].contract.md]] | TASK-00N done | 🔴 blocks Mobile TASK-00N |
25
- | [Team B] (API) | [Team A] (API) | `EventName` schema | TASK-00N done | 🟡 blocks integration test |
26
- | [Team B] (Mobile) | QA | E2E test cases | TASK-00N done | 🟡 blocks QA sprint entry |
24
+ | [Team A] (API) | [Team B] (Mobile) | [[contracts/[repo].contract.md]] | [[usecases/UC-Name]] green | 🔴 blocks Mobile [[usecases/UC-Name]] |
25
+ | [Team B] (API) | [Team A] (API) | `EventName` schema | [[usecases/UC-Name]] green | 🟡 blocks integration test |
26
+ | [Team B] (Mobile) | QA | E2E test cases | [[usecases/UC-Name]] green | 🟡 blocks QA sprint entry |
27
+
28
+ <!-- Readiness is stated against a committed UC (or a scope_id), never a task id: this
29
+ register is committed and shared across teams, and board ids are per-machine. -->
27
30
 
28
31
  ---
29
32
 
30
33
  ## Blocking Dependencies
31
34
 
35
+ <!--
36
+ Key every row on a USE CASE or a scope_id, never a task id. This document is committed and
37
+ crosses a team boundary; board ids live in the gitignored tier and each team's board numbers
38
+ its own, so one board id names a different piece of work on every machine that reads this.
39
+ spec-lint reds a board id anywhere in the committed tree (TIER-DIRECTION).
40
+ -->
41
+
32
42
  ```
33
- [Team B] CANNOT start TASK-00N ([description])
34
- until [Team A] completes TASK-00N ([description])
43
+ [Team B] CANNOT start [UC-x] ([description])
44
+ until [Team A] completes [UC-y] ([description])
35
45
  Mitigation: use contract stub from [[contracts/[repo].contract.md]]
36
- Stub ready: TASK-00N (unblocked — no dependency)
46
+ Ready now: [UC-z] (unblocked — no dependency)
37
47
 
38
48
  [Team C] CANNOT start integration tests
39
49
  until [Team A] EventName schema is stable
@@ -46,12 +56,12 @@ status: draft
46
56
 
47
57
  ```
48
58
  Wave 1 — No cross-team dependencies (start immediately):
49
- [Team A]: TASK-001, TASK-002, TASK-003
50
- [Team B]: TASK-004, TASK-005 (using contract stub)
59
+ [Team A]: [UC-a], [UC-b], [UC-c]
60
+ [Team B]: [UC-d], [UC-e] (using contract stub)
51
61
 
52
- Wave 2 — After Team A TASK-003 done:
53
- [Team B]: TASK-006 (replace stub with real contract)
54
- [Team C]: TASK-007 (integration tests real contract available)
62
+ Wave 2 — After Team A [UC-c] done:
63
+ [Team B]: [UC-f] (replace stub with real contract)
64
+ [Team C]: integration tests (real contract available)
55
65
 
56
66
  Wave 3 — After Wave 2 complete:
57
67
  QA: E2E test suite
@@ -64,7 +74,7 @@ Wave 3 — After Wave 2 complete:
64
74
  | Trigger | Owner | Notify | Channel |
65
75
  |---------|-------|--------|---------|
66
76
  | Contract changes after stub distributed | [Team A] | [Team B], QA | [channel] |
67
- | TASK-00N delayed > 1 day | [Team A] | [Team B] | [channel] |
77
+ | A wave-1 use case delayed > 1 day | [Team A] | [Team B] | [channel] |
68
78
  | Schema breaking change detected | Any | All teams | [channel] |
69
79
 
70
80
  ---
@@ -72,7 +82,7 @@ Wave 3 — After Wave 2 complete:
72
82
  ## Definition of Done (Cross-Context)
73
83
 
74
84
  Feature is complete when ALL of the following are true:
75
- - [ ] All TASK-MNN migrations run and verified in staging
85
+ - [ ] Every migration STEP run and verified in staging
76
86
  - [ ] All contracts have no remaining ⏳ TBD fields
77
87
  - [ ] Event choreography happy path verified end-to-end
78
88
  - [ ] All dead-letter scenarios have runbooks