shapeup-sdlc 1.6.2 → 1.7.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.
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +36 -103
- package/README.md +60 -67
- package/SECURITY.md +31 -16
- package/bin/init.mjs +24 -69
- package/commands/build.md +19 -0
- package/commands/eval.md +5 -4
- package/commands/scopes.md +5 -4
- package/commands/shape.md +1 -1
- package/commands/ship.md +37 -4
- package/hooks/anti-rationalization.mjs +7 -13
- package/hooks/compact-snapshot.mjs +5 -5
- package/hooks/gate-deadline.mjs +12 -16
- package/hooks/gate-intake.mjs +13 -14
- package/hooks/gate-l2.mjs +3 -3
- package/hooks/gate-zerowork.mjs +97 -16
- package/hooks/lib/decision.mjs +21 -13
- package/hooks/safety-spine.mjs +1 -1
- package/hooks/sandbox-guard.mjs +83 -47
- package/hooks/session-rehydrate.mjs +8 -9
- package/hooks/slop-cleaner.mjs +1 -1
- package/oracles/_shared.mjs +1 -1
- package/oracles/http-oracle.mjs +1 -1
- package/oracles/index.mjs +1 -1
- package/oracles/process-oracle.mjs +1 -1
- package/oracles/snapshot-oracle.mjs +1 -1
- package/oracles/test-oracle.mjs +1 -1
- package/package.json +3 -7
- package/skills/ba-pitch-analyzer/SKILL.md +20 -15
- package/skills/ba-pitch-analyzer/references/doc-schemas.md +1 -1
- package/skills/ba-pitch-analyzer/references/task-generation.md +6 -6
- package/skills/ba-pitch-analyzer/references/test-surface.md +1 -1
- package/skills/ba-pitch-analyzer/scripts/board-derive.mjs +5 -5
- package/skills/ba-pitch-analyzer/scripts/spec-lint.mjs +2 -2
- package/skills/coach/SKILL.md +3 -3
- package/skills/orient/SKILL.md +2 -1
- package/skills/qa-edge-hunter/SKILL.md +9 -39
- package/skills/scope-architect/SKILL.md +17 -12
- package/skills/scope-hammer/SKILL.md +5 -5
- package/skills/shapeup/SKILL.md +3 -3
- package/skills/shapeup/resources/context-compaction.md +4 -3
- package/skills/solution-architect/SKILL.md +18 -11
- package/skills/spec-evaluator/SKILL.md +12 -5
- package/skills/spec-evaluator/references/dimensions/_registry.md +2 -2
- package/skills/spec-evaluator/references/dimensions/completeness.md +1 -1
- package/skills/spec-evaluator/references/dimensions/integration.md +98 -67
- package/skills/spec-evaluator/references/dimensions/spec-conformance.md +4 -4
- package/skills/spec-evaluator/references/dimensions/test-surface-conformance.md +1 -1
- package/skills/spec-evaluator/references/probing.md +42 -23
- package/skills/spec-evaluator/references/report-schema.md +2 -2
- package/skills/spec-evaluator/scripts/verdict-ledger.mjs +1 -1
- package/skills/task-executor/SKILL.md +25 -12
- package/skills/tech-lead/SKILL.md +115 -438
- package/skills/tech-lead/references/delegation.md +38 -31
- package/skills/tech-lead/references/gates.md +75 -33
- package/skills/tech-lead/references/hard-rules.md +34 -0
- package/skills/tech-lead/references/invocation.md +2 -2
- package/skills/tech-lead/references/ledger-schema.md +17 -18
- package/skills/tech-lead/references/round-protocol.md +77 -27
- package/skills/tech-lead/references/state-model.md +1 -1
- package/skills/tech-lead/schemas/domain.schema.json +320 -148
- package/skills/tech-lead/schemas/gate-answers.schema.json +2 -2
- package/skills/tech-lead/schemas/work-result.schema.json +55 -17
- package/skills/tech-lead/scripts/aegis-digest.mjs +1 -1
- package/skills/tech-lead/scripts/budget-check.mjs +7 -13
- package/skills/tech-lead/scripts/compile-order.mjs +69 -22
- package/skills/tech-lead/scripts/fit-check.mjs +10 -10
- package/skills/tech-lead/scripts/gate-answers.mjs +11 -12
- package/skills/tech-lead/scripts/hill-derive.mjs +133 -0
- package/skills/tech-lead/scripts/ingest-result.mjs +7 -19
- package/skills/tech-lead/scripts/init-run.mjs +60 -18
- package/skills/tech-lead/scripts/lib/argv.mjs +1 -1
- package/skills/tech-lead/scripts/lib/contract-md.mjs +10 -10
- package/skills/tech-lead/scripts/lib/is-main.mjs +11 -12
- package/skills/tech-lead/scripts/lib/paths.mjs +15 -13
- package/skills/tech-lead/scripts/resume-state.mjs +387 -0
- package/skills/tech-lead/scripts/run-snapshot.mjs +1 -6
- package/skills/tech-lead/scripts/run-workflow.mjs +381 -0
- package/skills/tech-lead/scripts/ship-report.mjs +33 -2
- package/skills/tech-lead/scripts/stats.mjs +9 -10
- package/skills/tech-lead/scripts/t0-verify.mjs +4 -4
- package/skills/tech-lead/scripts/trace-lint.mjs +2 -2
- package/skills/tech-lead/workflows/shapeup-run.js +911 -0
- package/skills/translator/SKILL.md +2 -2
- package/skills/advisor-protocol/SKILL.md +0 -171
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// RESUME STATE — the fast-forward derivation, as a script rather than as a string.
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS FILE EXISTS (observed, not theorized).
|
|
5
|
+
//
|
|
6
|
+
// `shapeup-run.js` derives "which phase do I resume at" from disk on every launch — that
|
|
7
|
+
// derivation is what retires the whole class of handoff where a fresh session rebuilds a pipeline
|
|
8
|
+
// already on disk. Until this file existed, it lived inside the workflow script as a
|
|
9
|
+
// `node --input-type=module -e "…"` blob passed to a courier agent. Three consequences, all of
|
|
10
|
+
// them realised:
|
|
11
|
+
//
|
|
12
|
+
// 1. IT COULD NOT BE TESTED. A Workflow script has no `import`, takes `args` as a runtime
|
|
13
|
+
// global, and is executed by the Workflow runtime — there is no seam a fixture can reach.
|
|
14
|
+
// So the derivation shipped unverified, and the kill/resume probe found it re-dispatching a
|
|
15
|
+
// COMPLETED ORIENT phase: three orient
|
|
16
|
+
// artifacts rewritten, a spike added, the discovery ledger and two task files mutated.
|
|
17
|
+
// The cause was one branch reading stored `status` instead of ORIENT's own artifacts,
|
|
18
|
+
// while WIRE and MAP SCOPES read artifacts and fast-forwarded correctly.
|
|
19
|
+
// 2. IT MATCHED NO PERMISSION GRANT. `permissions.allow` carries
|
|
20
|
+
// `Bash(node ${CLAUDE_PLUGIN_ROOT}/skills/tech-lead/scripts/:*)`; an inline `node -e` matches
|
|
21
|
+
// no entry and passes only at the safety classifier's discretion. As a script it is covered
|
|
22
|
+
// by the grant the installer already writes.
|
|
23
|
+
// 3. TWO WRITES HAD NO READER. `setRunStatus` and `writeActiveScope` were the only `mech()`
|
|
24
|
+
// call sites in the workflow whose return value was discarded — and they are the only two
|
|
25
|
+
// whose failure went unnoticed for two entire runs. `status` never left `orienting` across
|
|
26
|
+
// 46 dispatched agents, and `.shapeup/active-scope` still named scope 1 while scope 2 was
|
|
27
|
+
// being built, which is the pointer `hooks/sandbox-guard.mjs` reads to decide which
|
|
28
|
+
// substrate a worker may write. A courier write whose result nobody reads back is
|
|
29
|
+
// indistinguishable from one that succeeded. Both writes live here now, both refuse
|
|
30
|
+
// silently-wrong outcomes (a missing ledger, a status line that matched nothing), and both
|
|
31
|
+
// report a non-zero exit the caller is required to act on.
|
|
32
|
+
//
|
|
33
|
+
// WHAT THIS FILE DOES NOT DECIDE. It reports facts; the workflow decides phases. `next_phase` is
|
|
34
|
+
// offered as a derived convenience (and is what the fixture asserts over), but every underlying
|
|
35
|
+
// boolean travels too, so a caller is never forced to trust a summary it cannot check.
|
|
36
|
+
//
|
|
37
|
+
// WHY `--require` EXISTS (the same lesson, one layer up).
|
|
38
|
+
//
|
|
39
|
+
// Making the RESUME decision read artifacts left the COMPLETION decision reading
|
|
40
|
+
// nothing at all: shapeup-run.js dispatched a phase, ingested its result, and moved to the next
|
|
41
|
+
// gate without ever re-asking the predicate. A worker that returns `status: "escalated"` with
|
|
42
|
+
// `artifacts: []` — a legitimate outcome its own contract defines — satisfied that. So a phase
|
|
43
|
+
// that wrote no artifact was recorded as complete, the artifact-gated fast-forward then correctly
|
|
44
|
+
// found nothing on the next launch, re-dispatched it, and the worker escalated again: an
|
|
45
|
+
// unbounded loop, invisible inside one leg, which is why several runs and a status review never
|
|
46
|
+
// saw it.
|
|
47
|
+
//
|
|
48
|
+
// `--require <phase>` is the completion check, and it is deliberately the SAME derivation the
|
|
49
|
+
// fast-forward uses — not a second predicate that can drift from it. Two predicates that can
|
|
50
|
+
// disagree about "is this phase done" is the defect class itself.
|
|
51
|
+
//
|
|
52
|
+
// USAGE
|
|
53
|
+
// node resume-state.mjs --slug <slug> [--cwd <dir>] # derive, print ResumeState
|
|
54
|
+
// node resume-state.mjs --slug <slug> --require <phase> # post-condition: exit 6 if unmet
|
|
55
|
+
// node resume-state.mjs --slug <slug> --set-status <status> # write harness-run.md status
|
|
56
|
+
// node resume-state.mjs --slug <slug> --set-active-scope <id> # write the substrate pointer
|
|
57
|
+
//
|
|
58
|
+
// Exit: 0 ok · 2 malformed argv (nothing ran) · 3 the target the operation needs is not on disk ·
|
|
59
|
+
// 6 the required phase's artifact is NOT on disk (the phase did not complete).
|
|
60
|
+
|
|
61
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
62
|
+
import { dirname, join, resolve } from "node:path";
|
|
63
|
+
import { isMain } from "./lib/is-main.mjs";
|
|
64
|
+
import { runArgs } from "./lib/argv.mjs";
|
|
65
|
+
import { splitFrontmatter } from "./lib/contract-md.mjs";
|
|
66
|
+
import {
|
|
67
|
+
intake, harnessRun, wiringMap, projectProfile, scopesDir, resultsDir, ordersDir,
|
|
68
|
+
orientDir, activeScope, activeOrder, usecasesDir,
|
|
69
|
+
} from "./lib/paths.mjs";
|
|
70
|
+
|
|
71
|
+
/** The run-state values `references/ledger-schema.md` defines. A typo'd status is a rejection,
|
|
72
|
+
* not a write — the whole point of this file is that a write nobody validates is a write nobody
|
|
73
|
+
* can trust. */
|
|
74
|
+
export const RUN_STATUSES = ["orienting", "mapping", "building", "evaluating", "shipped", "escalated"];
|
|
75
|
+
|
|
76
|
+
/** ORIENT's four artifacts (skills/orient/SKILL.md §Outputs): three by exact name, plus a spike
|
|
77
|
+
* whose filename carries the area it spiked (`spike-<area>.md`, or `spike-not-needed.md` when
|
|
78
|
+
* the risk scan came back rank 0 — both count, because both are ORIENT having finished). */
|
|
79
|
+
export const ORIENT_REQUIRED = ["code-surface.md", "discovered-seed.md", "hill-signal.md"];
|
|
80
|
+
export const ORIENT_SPIKE = /^spike-.+\.md$/;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Parse a leading `---` frontmatter block into a flat object, through the ONE library that reads
|
|
84
|
+
* this file form (`lib/contract-md.mjs`).
|
|
85
|
+
*
|
|
86
|
+
* It used to be a private scalar-only regex — every value came back a string, including a
|
|
87
|
+
* `key: [a, b]` list. That is why `eval_dimensions` was pinned: the ledger could carry the set the
|
|
88
|
+
* PO asked for and the `Array.isArray(hr.eval_dimensions)` branch below could never be true, so the
|
|
89
|
+
* fallback fired on every read and the run graded spec-conformance whatever the file said. A second
|
|
90
|
+
* parser for a format that already has one is the defect; the dialects diverge silently and the
|
|
91
|
+
* reader that loses a value looks identical to a file that never carried it.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} text - Whole file contents.
|
|
94
|
+
* @returns {Object<string,*>} Frontmatter keys, coerced: `[a, b]` → string[], `~` → null,
|
|
95
|
+
* digits → number, true/false → boolean, everything else the unquoted string.
|
|
96
|
+
*/
|
|
97
|
+
export function parseFrontmatter(text) {
|
|
98
|
+
return splitFrontmatter(text).meta;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Has ORIENT actually produced its artifacts? This is the predicate the fast-forward's ORIENT
|
|
103
|
+
* branch was missing — the comment above that branch has always described it, and until now the
|
|
104
|
+
* code read a stored status field instead.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} cwd - Project root.
|
|
107
|
+
* @param {string} slug - Feature slug.
|
|
108
|
+
* @returns {boolean} True when all three named artifacts and at least one spike file exist.
|
|
109
|
+
*/
|
|
110
|
+
export function hasOrientArtifacts(cwd, slug) {
|
|
111
|
+
const dir = orientDir(cwd, slug);
|
|
112
|
+
if (!existsSync(dir)) return false;
|
|
113
|
+
let files;
|
|
114
|
+
try { files = readdirSync(dir); } catch { return false; }
|
|
115
|
+
const present = new Set(files);
|
|
116
|
+
return ORIENT_REQUIRED.every((f) => present.has(f)) && files.some((f) => ORIENT_SPIKE.test(f));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The use-case directory this run's spec tree lands in. The ledger may name a non-default
|
|
121
|
+
* `spec_folder` (`init-run.mjs --spec-folder`), so honour it when present and fall back to the
|
|
122
|
+
* registry path otherwise — never a spelled-out root (test #45).
|
|
123
|
+
*
|
|
124
|
+
* @param {string} cwd - Project root.
|
|
125
|
+
* @param {string} slug - Feature slug.
|
|
126
|
+
* @param {string|null} specFolder - The ledger's `spec_folder`, if it names one.
|
|
127
|
+
* @returns {string} Absolute path to `usecases/`.
|
|
128
|
+
*/
|
|
129
|
+
export function usecasesPath(cwd, slug, specFolder) {
|
|
130
|
+
return specFolder ? resolve(cwd, specFolder, "usecases") : usecasesDir(cwd, slug);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Has ANALYZE actually produced the spec tree? `_index.md` alone is a tree with no use cases in
|
|
135
|
+
* it, and a wiring map is written one entry PER use case — so the index does not count.
|
|
136
|
+
*
|
|
137
|
+
* This predicate is why ANALYZE is in the phase chain at all. WIRE was dispatched before it ran, so
|
|
138
|
+
* `usecases/` did not exist, so `solution-architect` had nothing to wire and escalated — honestly,
|
|
139
|
+
* and identically on every relaunch (skills/solution-architect/SKILL.md:43-44, :108).
|
|
140
|
+
*
|
|
141
|
+
* @param {string} cwd - Project root.
|
|
142
|
+
* @param {string} slug - Feature slug.
|
|
143
|
+
* @param {string|null} specFolder - The ledger's `spec_folder`, if it names one.
|
|
144
|
+
* @returns {boolean} True when at least one use case (not the index) is on disk.
|
|
145
|
+
*/
|
|
146
|
+
export function hasSpecTree(cwd, slug, specFolder) {
|
|
147
|
+
const dir = usecasesPath(cwd, slug, specFolder);
|
|
148
|
+
if (!existsSync(dir)) return false;
|
|
149
|
+
let files;
|
|
150
|
+
try { files = readdirSync(dir); } catch { return false; }
|
|
151
|
+
return files.some((f) => f.endsWith(".md") && f !== "_index.md");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Each dispatched phase and the artifact that IS its completion. One table, read by the resume
|
|
156
|
+
* decision (`nextPhase`) and by the post-condition check (`--require`) alike — see the banner.
|
|
157
|
+
*/
|
|
158
|
+
export const PHASE_ARTIFACT = {
|
|
159
|
+
orient: { fact: "has_orient_artifacts", artifact: "orient/{code-surface,discovered-seed,hill-signal}.md + spike-*.md" },
|
|
160
|
+
analyze: { fact: "has_spec_tree", artifact: "spec/usecases/*.md" },
|
|
161
|
+
wire: { fact: "has_wiring_map", artifact: "wiring-map.md" },
|
|
162
|
+
"map-scopes": { fact: "scope_files", artifact: "scopes/*.md" },
|
|
163
|
+
};
|
|
164
|
+
export const PHASES = Object.keys(PHASE_ARTIFACT);
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Is this phase's artifact on disk? The ONE reading of "complete" in this pipeline.
|
|
168
|
+
*
|
|
169
|
+
* @param {object} state - A derived ResumeState.
|
|
170
|
+
* @param {string} phase - One of {@link PHASES}.
|
|
171
|
+
* @returns {boolean} True when the phase's artifact exists.
|
|
172
|
+
*/
|
|
173
|
+
export function phaseSatisfied(state, phase) {
|
|
174
|
+
const v = state[PHASE_ARTIFACT[phase].fact];
|
|
175
|
+
return Array.isArray(v) ? v.length > 0 : Boolean(v);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The first phase whose artifacts are incomplete — the design doc's §4 fast-forward, stated once
|
|
180
|
+
* so the workflow and the fixture cannot disagree about it.
|
|
181
|
+
*
|
|
182
|
+
* ⟐ ANALYZE sits between ORIENT and WIRE. The pipeline used to dispatch
|
|
183
|
+
* WIRE first, which is the position solution-architect's own input contract excludes — it reads
|
|
184
|
+
* `usecases/`, and `analyze` is what writes them.
|
|
185
|
+
*
|
|
186
|
+
* @param {object} f - Facts (a derived ResumeState, or the subset the phase predicates read).
|
|
187
|
+
* @returns {"orient"|"analyze"|"wire"|"map-scopes"|"build"} The phase to resume at.
|
|
188
|
+
*/
|
|
189
|
+
export function nextPhase(f) {
|
|
190
|
+
for (const p of PHASES) if (!phaseSatisfied(f, p)) return p;
|
|
191
|
+
return "build";
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Derive every path/status fact the outer pipeline needs, from files alone.
|
|
196
|
+
*
|
|
197
|
+
* @param {string} cwd - Project root.
|
|
198
|
+
* @param {string} slug - Feature slug.
|
|
199
|
+
* @returns {object} The ResumeState record (domain.schema.json $defs/ResumeState).
|
|
200
|
+
*/
|
|
201
|
+
export function deriveResumeState(cwd, slug) {
|
|
202
|
+
const hrPath = harnessRun(cwd, slug);
|
|
203
|
+
const hr = existsSync(hrPath) ? parseFrontmatter(readFileSync(hrPath, "utf8")) : {};
|
|
204
|
+
|
|
205
|
+
// Resolved contract PATHS, not bare filenames: compile-order.mjs and t0-verify.mjs both resolve
|
|
206
|
+
// `--scope` against cwd, so a bare "SC-x.md" names a file that does not exist, compile-order
|
|
207
|
+
// exits 2, and the attempt loop reads that non-zero exit as the stagnation breaker — a resumed
|
|
208
|
+
// run would falsely trip the inner breaker and hammer-propose every scope instead of continuing.
|
|
209
|
+
const sdir = scopesDir(cwd, slug);
|
|
210
|
+
const scope_files = existsSync(sdir)
|
|
211
|
+
? readdirSync(sdir).filter((f) => f.endsWith(".md")).sort()
|
|
212
|
+
.map((f) => ({ scope_id: f.replace(/\.md$/, ""), path: join(sdir, f) }))
|
|
213
|
+
: [];
|
|
214
|
+
|
|
215
|
+
const resultFiles = existsSync(resultsDir(cwd, slug)) ? readdirSync(resultsDir(cwd, slug)) : [];
|
|
216
|
+
const orderFiles = existsSync(ordersDir(cwd, slug)) ? readdirSync(ordersDir(cwd, slug)) : [];
|
|
217
|
+
|
|
218
|
+
const facts = {
|
|
219
|
+
intake_path: intake(cwd, slug),
|
|
220
|
+
spec_folder: hr.spec_folder || null,
|
|
221
|
+
status: hr.status || null,
|
|
222
|
+
lens: hr.lens || null,
|
|
223
|
+
stack: hr.stack || null,
|
|
224
|
+
run_cmd: hr.run_cmd || null,
|
|
225
|
+
app_url: hr.app_url || null,
|
|
226
|
+
eval_dimensions: Array.isArray(hr.eval_dimensions) ? hr.eval_dimensions : ["spec-conformance"],
|
|
227
|
+
orient_dir: `.shapeup/${slug}/orient/`,
|
|
228
|
+
has_orient_artifacts: hasOrientArtifacts(cwd, slug),
|
|
229
|
+
has_spec_tree: hasSpecTree(cwd, slug, hr.spec_folder || null),
|
|
230
|
+
has_wiring_map: existsSync(wiringMap(cwd, slug)),
|
|
231
|
+
project_profile_path: projectProfile(cwd, slug),
|
|
232
|
+
has_project_profile: existsSync(projectProfile(cwd, slug)),
|
|
233
|
+
scope_files,
|
|
234
|
+
pending_orders: orderFiles.filter((f) => f.endsWith(".json") && !resultFiles.includes(f)),
|
|
235
|
+
eval_rounds_done: resultFiles
|
|
236
|
+
.filter((f) => /^evaluate-r\d+\.json$/.test(f))
|
|
237
|
+
.map((f) => Number(f.match(/\d+/)[0])),
|
|
238
|
+
};
|
|
239
|
+
return { ...facts, next_phase: nextPhase(facts) };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Rewrite `harness-run.md`'s status line. Refuses rather than silently no-ops: a ledger that is
|
|
244
|
+
* absent, or that carries no `status:` line to replace, is a fact the run must act on — that
|
|
245
|
+
* silent no-op is exactly what pinned a run at `orienting` for two complete legs.
|
|
246
|
+
*
|
|
247
|
+
* @param {string} cwd - Project root.
|
|
248
|
+
* @param {string} slug - Feature slug.
|
|
249
|
+
* @param {string} status - One of {@link RUN_STATUSES}.
|
|
250
|
+
* @returns {{ok: boolean, path: string, status: string, reason?: string}} Outcome record.
|
|
251
|
+
*/
|
|
252
|
+
export function setRunStatus(cwd, slug, status) {
|
|
253
|
+
const p = harnessRun(cwd, slug);
|
|
254
|
+
if (!existsSync(p)) {
|
|
255
|
+
return { ok: false, path: p, status, reason: `no harness-run.md for slug "${slug}" — open the run with init-run.mjs (GATE L0.1) before setting its status` };
|
|
256
|
+
}
|
|
257
|
+
const body = readFileSync(p, "utf8");
|
|
258
|
+
if (!/^status:.*$/m.test(body)) {
|
|
259
|
+
return { ok: false, path: p, status, reason: `harness-run.md carries no "status:" line to replace — the ledger's frontmatter is malformed (references/ledger-schema.md)` };
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
writeFileSync(p, body.replace(/^status:.*$/m, `status: ${status}`));
|
|
263
|
+
} catch (e) {
|
|
264
|
+
return { ok: false, path: p, status, reason: `could not write the ledger: ${e.message}` };
|
|
265
|
+
}
|
|
266
|
+
const after = parseFrontmatter(readFileSync(p, "utf8")).status;
|
|
267
|
+
if (after !== status) {
|
|
268
|
+
return { ok: false, path: p, status, reason: `wrote "status: ${status}" but the ledger reads "${after}" — the write did not take` };
|
|
269
|
+
}
|
|
270
|
+
return { ok: true, path: p, status };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Point the substrate pointer at the scope about to be built. `hooks/sandbox-guard.mjs` reads
|
|
275
|
+
* this to decide which write-whitelist a worker is held to, so a failed write here does not
|
|
276
|
+
* degrade gracefully — it silently enforces the WRONG scope's substrate.
|
|
277
|
+
*
|
|
278
|
+
* @param {string} cwd - Project root.
|
|
279
|
+
* @param {string} slug - Feature slug.
|
|
280
|
+
* @param {string} scopeId - Scope contract id.
|
|
281
|
+
* @returns {{ok: boolean, path: string, slug: string, scope_id: string, reason?: string}} Outcome.
|
|
282
|
+
*/
|
|
283
|
+
export function writeActiveScope(cwd, slug, scopeId) {
|
|
284
|
+
const p = activeScope(cwd);
|
|
285
|
+
// A throw here would exit 1 with a stack trace and an empty stdout — readable enough to the
|
|
286
|
+
// workflow (any non-zero aborts the scope), but the caller learns nothing it can log. Report the
|
|
287
|
+
// failure as the same outcome record every other operation returns.
|
|
288
|
+
try {
|
|
289
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
290
|
+
writeFileSync(p, `${JSON.stringify({ slug, scope_id: scopeId }, null, 2)}\n`);
|
|
291
|
+
} catch (e) {
|
|
292
|
+
return { ok: false, path: p, slug, scope_id: scopeId, reason: `could not write the substrate pointer: ${e.message}` };
|
|
293
|
+
}
|
|
294
|
+
let readBack;
|
|
295
|
+
try { readBack = JSON.parse(readFileSync(p, "utf8")); } catch { readBack = null; }
|
|
296
|
+
if (readBack?.scope_id !== scopeId || readBack?.slug !== slug) {
|
|
297
|
+
return { ok: false, path: p, slug, scope_id: scopeId, reason: `pointer read back as ${JSON.stringify(readBack)} — sandbox-guard would hold the next worker to the wrong substrate` };
|
|
298
|
+
}
|
|
299
|
+
return { ok: true, path: p, slug, scope_id: scopeId };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Point the substrate pointer at the order about to be executed.
|
|
304
|
+
*
|
|
305
|
+
* @param {string} cwd - Project root.
|
|
306
|
+
* @param {string} slug - Feature slug.
|
|
307
|
+
* @param {string} orderPath - Path to the active order.
|
|
308
|
+
* @returns {{ok: boolean, path: string, slug: string, order_path: string, reason?: string}} Outcome.
|
|
309
|
+
*/
|
|
310
|
+
export function writeActiveOrder(cwd, slug, orderPath) {
|
|
311
|
+
const p = activeOrder(cwd);
|
|
312
|
+
try {
|
|
313
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
314
|
+
writeFileSync(p, `${JSON.stringify({ slug, order_path: orderPath }, null, 2)}\n`);
|
|
315
|
+
} catch (e) {
|
|
316
|
+
return { ok: false, path: p, slug, order_path: orderPath, reason: `could not write the active-order pointer: ${e.message}` };
|
|
317
|
+
}
|
|
318
|
+
let readBack;
|
|
319
|
+
try { readBack = JSON.parse(readFileSync(p, "utf8")); } catch { readBack = null; }
|
|
320
|
+
if (readBack?.order_path !== orderPath || readBack?.slug !== slug) {
|
|
321
|
+
return { ok: false, path: p, slug, order_path: orderPath, reason: `pointer read back as ${JSON.stringify(readBack)}` };
|
|
322
|
+
}
|
|
323
|
+
return { ok: true, path: p, slug, order_path: orderPath };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** The typed argv contract (see `./lib/argv.mjs`). */
|
|
327
|
+
export const ARGV_SPEC = {
|
|
328
|
+
usage: "resume-state.mjs --slug <slug> [--cwd <dir>] [--require <phase> | --set-status <status> | --set-active-scope <scope-id> | --set-active-order <path>]",
|
|
329
|
+
_: { arity: 0, max: 0, name: "(no positional operands)" },
|
|
330
|
+
slug: { type: "str", required: true },
|
|
331
|
+
cwd: { type: "path" },
|
|
332
|
+
require: { type: "enum", values: PHASES },
|
|
333
|
+
"set-status": { type: "enum", values: RUN_STATUSES },
|
|
334
|
+
"set-active-scope": { type: "str" },
|
|
335
|
+
"set-active-order": { type: "str" },
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
export function main() {
|
|
339
|
+
const args = runArgs(ARGV_SPEC);
|
|
340
|
+
const cwd = args.cwd || process.cwd();
|
|
341
|
+
|
|
342
|
+
const ops = [args.require && "--require", args.setStatus && "--set-status", args.setActiveScope && "--set-active-scope", args.setActiveOrder && "--set-active-order"].filter(Boolean);
|
|
343
|
+
if (ops.length > 1) {
|
|
344
|
+
process.stderr.write(JSON.stringify({ error: "conflicting_flags", flags: ops, expected: "one operation per invocation" }) + "\n");
|
|
345
|
+
process.exit(2);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// The post-condition. It prints the SAME ResumeState the derivation prints — plus which phase was
|
|
349
|
+
// asked about and whether its artifact is there — so a caller that wants to act on the facts
|
|
350
|
+
// rather than on the exit code never has to make a second call.
|
|
351
|
+
if (args.require) {
|
|
352
|
+
const state = deriveResumeState(cwd, args.slug);
|
|
353
|
+
const satisfied = phaseSatisfied(state, args.require);
|
|
354
|
+
console.log(JSON.stringify({
|
|
355
|
+
...state,
|
|
356
|
+
required_phase: args.require,
|
|
357
|
+
required_artifact: PHASE_ARTIFACT[args.require].artifact,
|
|
358
|
+
satisfied,
|
|
359
|
+
}));
|
|
360
|
+
process.exit(satisfied ? 0 : 6);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (args.setStatus) {
|
|
364
|
+
const r = setRunStatus(cwd, args.slug, args.setStatus);
|
|
365
|
+
console.log(JSON.stringify(r));
|
|
366
|
+
process.exit(r.ok ? 0 : 3);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (args.setActiveScope) {
|
|
370
|
+
const r = writeActiveScope(cwd, args.slug, args.setActiveScope);
|
|
371
|
+
console.log(JSON.stringify(r));
|
|
372
|
+
process.exit(r.ok ? 0 : 3);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (args.setActiveOrder) {
|
|
376
|
+
const r = writeActiveOrder(cwd, args.slug, args.setActiveOrder);
|
|
377
|
+
console.log(JSON.stringify(r));
|
|
378
|
+
process.exit(r.ok ? 0 : 3);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
console.log(JSON.stringify(deriveResumeState(cwd, args.slug)));
|
|
382
|
+
process.exit(0);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (isMain(import.meta.url)) {
|
|
386
|
+
main();
|
|
387
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Run snapshot — the compaction-resilience derivation (v1.2
|
|
2
|
+
// Run snapshot — the compaction-resilience derivation (v1.2).
|
|
3
3
|
//
|
|
4
4
|
// Derives a RunSnapshot (domain.schema.json#/$defs/RunSnapshot) from FILES ONLY: the
|
|
5
5
|
// active-scope pointer, harness-run.md frontmatter, board task frontmatter, t0/verdicts
|
|
@@ -176,11 +176,6 @@ export function deriveSnapshot(cwd) {
|
|
|
176
176
|
if (board.total > 0) snapshot.board = board;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
const escDir = join(root, "escalates");
|
|
180
|
-
snapshot.open_escalates = existsSync(escDir)
|
|
181
|
-
? readdirSync(escDir).filter((f) => f.endsWith(".json")).length
|
|
182
|
-
: 0;
|
|
183
|
-
|
|
184
179
|
const ordersDir = join(root, "orders");
|
|
185
180
|
const resultsDir = join(root, "results");
|
|
186
181
|
const results = new Set(existsSync(resultsDir) ? readdirSync(resultsDir) : []);
|