shapeup-sdlc 1.6.3 → 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 (118) hide show
  1. package/.claude/settings.local.example.json +5 -5
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/AGENTS.md +54 -107
  4. package/README.md +165 -151
  5. package/SECURITY.md +49 -27
  6. package/bin/init.mjs +93 -108
  7. package/bin/lib/grant.mjs +145 -0
  8. package/commands/build.md +20 -0
  9. package/commands/eval.md +5 -4
  10. package/commands/scopes.md +5 -4
  11. package/commands/shape.md +1 -1
  12. package/commands/ship.md +53 -7
  13. package/commands/wire.md +1 -1
  14. package/hooks/dispatch-receipt.mjs +195 -0
  15. package/hooks/gate-intake.mjs +16 -17
  16. package/hooks/gate-zerowork.mjs +107 -25
  17. package/hooks/hooks.json +9 -48
  18. package/hooks/lib/decision.mjs +38 -19
  19. package/hooks/safety-spine.mjs +4 -4
  20. package/hooks/sandbox-guard.mjs +130 -50
  21. package/{skills/tech-lead/scripts/compile-order.mjs → kernel/compile.mjs} +344 -55
  22. package/{skills/tech-lead/scripts/gate-answers.mjs → kernel/gate.mjs} +69 -26
  23. package/kernel/harness.mjs +134 -0
  24. package/{skills/tech-lead/scripts/fit-check.mjs → kernel/init/fit.mjs} +25 -22
  25. package/kernel/init/run.mjs +489 -0
  26. package/{skills/tech-lead/scripts → kernel}/lib/argv.mjs +52 -31
  27. package/{skills/tech-lead/scripts/lib/contract-md.mjs → kernel/lib/contract.mjs} +261 -17
  28. package/kernel/lib/paths.mjs +491 -0
  29. package/kernel/probe/concurrency.mjs +510 -0
  30. package/{skills/tech-lead/scripts/aegis-digest.mjs → kernel/probe/digest.mjs} +10 -11
  31. package/kernel/probe/eval.mjs +77 -0
  32. package/kernel/probe/leg.mjs +125 -0
  33. package/kernel/probe/resume.mjs +528 -0
  34. package/{skills/tech-lead/scripts → kernel/probe}/stats.mjs +102 -21
  35. package/kernel/probe/t0.mjs +66 -0
  36. package/{skills/ba-pitch-analyzer/scripts/board-derive.mjs → kernel/reduce/board.mjs} +104 -28
  37. package/kernel/reduce/graph.mjs +437 -0
  38. package/kernel/reduce/hill.mjs +152 -0
  39. package/kernel/reduce/ingest.mjs +633 -0
  40. package/{hooks/slop-cleaner.mjs → kernel/reduce/leftovers.mjs} +40 -57
  41. package/{skills/tech-lead/scripts/ship-report.mjs → kernel/reduce/ship.mjs} +93 -14
  42. package/{skills/tech-lead/scripts/run-snapshot.mjs → kernel/reduce/snapshot.mjs} +22 -21
  43. package/{skills/spec-evaluator/scripts/verdict-ledger.mjs → kernel/reduce/verdict.mjs} +14 -8
  44. package/kernel/report/export.mjs +325 -0
  45. package/kernel/report/facts.mjs +347 -0
  46. package/{skills/tech-lead/scripts/budget-check.mjs → kernel/verify/budget.mjs} +22 -25
  47. package/kernel/verify/dispatch.mjs +114 -0
  48. package/{skills/tech-lead/scripts/validate-envelope.mjs → kernel/verify/envelope.mjs} +20 -15
  49. package/{skills/tech-lead/scripts/lib → kernel/verify}/ratchet-tree.mjs +36 -14
  50. package/kernel/verify/skills.mjs +125 -0
  51. package/kernel/verify/spec.mjs +559 -0
  52. package/{skills/tech-lead/scripts/t0-verify.mjs → kernel/verify/t0.mjs} +138 -28
  53. package/{skills/tech-lead/scripts/trace-lint.mjs → kernel/verify/trace.mjs} +28 -13
  54. package/oracles/_shared.mjs +1 -1
  55. package/oracles/http-oracle.mjs +2 -2
  56. package/oracles/index.mjs +1 -1
  57. package/oracles/process-oracle.mjs +2 -2
  58. package/oracles/snapshot-oracle.mjs +2 -2
  59. package/oracles/test-oracle.mjs +2 -2
  60. package/package.json +11 -13
  61. package/skills/ba-pitch-analyzer/SKILL.md +24 -19
  62. package/skills/ba-pitch-analyzer/assets/templates/_index.tmpl.md +1 -1
  63. package/skills/ba-pitch-analyzer/assets/templates/api-feasibility.tmpl.md +12 -7
  64. package/skills/ba-pitch-analyzer/assets/templates/contracts/third-party-api.contract.tmpl.md +6 -3
  65. package/skills/ba-pitch-analyzer/assets/templates/cross-context/migration-plan.tmpl.md +5 -4
  66. package/skills/ba-pitch-analyzer/assets/templates/cross-context/team-handoff.tmpl.md +23 -13
  67. package/skills/ba-pitch-analyzer/assets/templates/scope-summary.tmpl.md +17 -12
  68. package/skills/ba-pitch-analyzer/assets/templates/synthesis.tmpl.md +17 -38
  69. package/skills/ba-pitch-analyzer/assets/templates/task.tmpl.md +3 -0
  70. package/skills/ba-pitch-analyzer/references/contract-patterns.md +8 -4
  71. package/skills/ba-pitch-analyzer/references/doc-schemas.md +3 -1
  72. package/skills/ba-pitch-analyzer/references/task-generation.md +8 -8
  73. package/skills/ba-pitch-analyzer/references/test-surface.md +1 -1
  74. package/skills/coach/SKILL.md +3 -3
  75. package/skills/orient/SKILL.md +2 -1
  76. package/skills/qa-edge-hunter/SKILL.md +15 -41
  77. package/skills/scope-architect/SKILL.md +57 -18
  78. package/skills/scope-hammer/SKILL.md +5 -5
  79. package/skills/shapeup/SKILL.md +3 -3
  80. package/skills/shapeup/resources/context-compaction.md +4 -3
  81. package/skills/solution-architect/SKILL.md +37 -15
  82. package/skills/spec-evaluator/SKILL.md +24 -7
  83. package/skills/spec-evaluator/references/dimensions/_registry.md +2 -2
  84. package/skills/spec-evaluator/references/dimensions/completeness.md +1 -1
  85. package/skills/spec-evaluator/references/dimensions/integration.md +98 -67
  86. package/skills/spec-evaluator/references/dimensions/spec-conformance.md +4 -4
  87. package/skills/spec-evaluator/references/dimensions/test-surface-conformance.md +1 -1
  88. package/skills/spec-evaluator/references/probing.md +42 -23
  89. package/skills/spec-evaluator/references/report-schema.md +2 -2
  90. package/skills/task-executor/SKILL.md +32 -18
  91. package/skills/tech-lead/SKILL.md +127 -438
  92. package/skills/tech-lead/references/gates.md +140 -49
  93. package/skills/tech-lead/references/protocol.md +832 -0
  94. package/skills/tech-lead/schemas/domain.schema.json +645 -198
  95. package/skills/tech-lead/schemas/gate-answers.schema.json +2 -2
  96. package/skills/tech-lead/schemas/work-order.schema.json +11 -2
  97. package/skills/tech-lead/schemas/work-result.schema.json +56 -18
  98. package/skills/tech-lead/workflows/shapeup-run.js +1474 -0
  99. package/skills/translator/SKILL.md +2 -2
  100. package/hooks/anti-rationalization.mjs +0 -244
  101. package/hooks/compact-snapshot.mjs +0 -47
  102. package/hooks/gate-deadline.mjs +0 -151
  103. package/hooks/gate-l2.mjs +0 -161
  104. package/hooks/session-rehydrate.mjs +0 -109
  105. package/skills/advisor-protocol/SKILL.md +0 -171
  106. package/skills/ba-pitch-analyzer/scripts/spec-lint.mjs +0 -239
  107. package/skills/spec-evaluator/README.md +0 -93
  108. package/skills/tech-lead/README.md +0 -71
  109. package/skills/tech-lead/references/delegation.md +0 -254
  110. package/skills/tech-lead/references/invocation.md +0 -45
  111. package/skills/tech-lead/references/ledger-schema.md +0 -214
  112. package/skills/tech-lead/references/round-protocol.md +0 -184
  113. package/skills/tech-lead/references/state-model.md +0 -66
  114. package/skills/tech-lead/scripts/ingest-result.mjs +0 -270
  115. package/skills/tech-lead/scripts/init-run.mjs +0 -326
  116. package/skills/tech-lead/scripts/lib/is-main.mjs +0 -82
  117. package/skills/tech-lead/scripts/lib/paths.mjs +0 -278
  118. package/skills/translator/README.md +0 -66
@@ -1,326 +0,0 @@
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
- }
@@ -1,82 +0,0 @@
1
- // isMain — "was this module executed directly, or imported?"
2
- //
3
- // WHY THIS FILE EXISTS (measured on the SDD harness benchmark, not theorized).
4
- //
5
- // Eighteen of this plugin's scripts and hooks decided whether to do anything at all with:
6
- //
7
- // if (import.meta.url === `file://${process.argv[1]}`) main();
8
- //
9
- // That comparison is FALSE — and the whole body silently does nothing, exit 0, no output —
10
- // whenever the path the process was invoked with is not byte-identical to the resolved module
11
- // URL. Two extremely common cases make it false:
12
- //
13
- // 1. A SYMLINKED DIRECTORY ANYWHERE IN THE PATH. Node resolves `import.meta.url` through
14
- // symlinks; `process.argv[1]` is the string as typed. On macOS `/var` is a symlink to
15
- // `/private/var`, so EVERY path under the system temp directory mismatches — which is how
16
- // the benchmark installs this plugin (`/var/folders/…/package`). nvm, pnpm's content store,
17
- // Homebrew and any symlinked checkout do the same thing on every platform.
18
- //
19
- // 2. A SPACE OR OTHER URL-RESERVED CHARACTER IN THE PATH. `import.meta.url` is percent-encoded
20
- // (`My%20Plugins`); the template literal is not (`My Plugins`). So a plugin installed under
21
- // `~/Library/Application Support/…` or any directory with a space in its name mismatches too.
22
- //
23
- // WHAT THAT COST, measured. `init-run.mjs` is GATE L0.1 — the orchestrator's mandatory first tool
24
- // call, the script that writes the run receipt everything else is derived from. Under a `/var`
25
- // path it exited 0 with empty stdout and wrote no receipt. The orchestrator could not distinguish
26
- // "the run opened" from "nothing happened", and in the benchmark's F4 handoff rows it spent
27
- // 82–120 turns before its first write doing forensics on its own bootstrap — retrying the script
28
- // six ways, hitting five separate permission refusals trying to capture an exit code, and finally
29
- // running `find /` to look for its own skill. Session B cost $4.57–$10.36 and recovered 0/3 of
30
- // the gap while the artifact it needed sat on disk the entire time.
31
- //
32
- // The same guard sits in seven hooks, including `gate-zerowork`, `safety-spine` and
33
- // `sandbox-guard`. This project's stated organising idea is that "every invariant that matters
34
- // lives in the runtime, not in a prompt" — and under a symlinked install the runtime half was
35
- // inert, while every gate still reported success. A silent no-op is the single worst failure mode
36
- // an enforcement layer can have, because it is indistinguishable from working.
37
- //
38
- // THE FIX. Compare resolved URL to resolved URL, using `pathToFileURL` so encoding matches and
39
- // `realpathSync` so symlinks match. `tests/structural/11-is-main.mjs` asserts that no file
40
- // reintroduces the fragile form, and actually executes each entry point through a symlinked
41
- // directory AND through a directory with a space in its name to prove the guard holds — because
42
- // this defect was invisible to every test that invoked scripts by their real path.
43
-
44
- import { realpathSync } from "node:fs";
45
- import { pathToFileURL } from "node:url";
46
-
47
- /**
48
- * True when `moduleUrl` belongs to the module Node was asked to execute.
49
- *
50
- * Usage, at the bottom of an entry point:
51
- *
52
- * import { isMain } from "./lib/is-main.mjs";
53
- * if (isMain(import.meta.url)) main();
54
- *
55
- * @param {string} moduleUrl - The caller's `import.meta.url`.
56
- * @returns {boolean} true if executed directly, false if imported (or if there is no entry point,
57
- * e.g. `node --eval`, where nothing should auto-run).
58
- */
59
- export function isMain(moduleUrl) {
60
- const entry = process.argv[1];
61
- if (!entry || !moduleUrl) return false;
62
-
63
- // Cheap path first: correct encoding, no filesystem access. Handles spaces and unicode.
64
- let entryUrl;
65
- try { entryUrl = pathToFileURL(entry).href; } catch { return false; }
66
- if (entryUrl === moduleUrl) return true;
67
-
68
- // Then resolve symlinks on both sides. `import.meta.url` is already realpath-resolved by Node,
69
- // but resolving it again is harmless and covers the reverse case (a caller passing an
70
- // unresolved URL) rather than assuming Node's behaviour never changes.
71
- try {
72
- const realEntry = pathToFileURL(realpathSync(entry)).href;
73
- if (realEntry === moduleUrl) return true;
74
- return realEntry === pathToFileURL(realpathSync(new URL(moduleUrl))).href;
75
- } catch {
76
- // An unreadable or deleted entry point is not this function's problem to report. Returning
77
- // false means "do not auto-run", which is the safe direction for an imported module and is
78
- // never the direction that silently skips a gate — a directly-invoked script whose own path
79
- // cannot be stat'd has larger problems that will surface immediately.
80
- return false;
81
- }
82
- }