shapeup-sdlc 3.18.3 → 4.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.
- package/.claude/settings.local.example.json +2 -3
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +115 -56
- package/README.md +101 -143
- package/SECURITY.md +16 -25
- package/agents/orient.md +14 -0
- package/agents/qa-edge-hunter.md +13 -0
- package/agents/spec-evaluator.md +14 -0
- package/agents/task-executor.md +14 -0
- package/bin/init.mjs +13 -16
- package/bin/lib/grant.mjs +8 -19
- package/commands/build.md +9 -14
- package/commands/eval.md +3 -3
- package/commands/hammer.md +9 -5
- package/commands/retro.md +12 -11
- package/commands/scopes.md +1 -12
- package/commands/ship.md +45 -88
- package/commands/wire.md +1 -8
- package/hooks/dispatch-receipt.mjs +94 -8
- package/hooks/hooks.json +0 -41
- package/hooks/lib/decision.mjs +6 -9
- package/hooks/lib/tier.mjs +50 -0
- package/hooks/sandbox-guard.mjs +140 -64
- package/kernel/gate.mjs +203 -22
- package/kernel/harness.mjs +20 -67
- package/kernel/init/run.mjs +175 -523
- package/kernel/lib/contract.mjs +9 -93
- package/kernel/lib/floor.mjs +64 -0
- package/kernel/lib/paths.mjs +39 -112
- package/kernel/order.mjs +157 -0
- package/kernel/probe/digest.mjs +2 -38
- package/kernel/schemas/domain.schema.json +146 -1125
- package/kernel/schemas/work-order.schema.json +8 -4
- package/kernel/schemas/work-result.schema.json +1 -1
- package/kernel/verify/envelope.mjs +2 -2
- package/kernel/verify/skills.mjs +27 -8
- package/kernel/verify/t0.mjs +2 -2
- package/package.json +4 -2
- package/skills/building/SKILL.md +144 -0
- package/skills/building/resources/gates.md +76 -0
- package/skills/building/resources/guidance.md +23 -0
- package/skills/{translator/references/preservation-rules.md → building/resources/intake-preservation.md} +1 -1
- package/skills/building/resources/intake.md +157 -0
- package/skills/building/resources/orders.md +69 -0
- package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/doc-schemas.md +1 -1
- package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/task-generation.md +13 -13
- package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/test-surface.md +6 -7
- package/skills/building/resources/plan-scopes.md +95 -0
- package/skills/building/resources/plan-spec.md +102 -0
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/_index.tmpl.md +1 -1
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/assess-report.tmpl.md +10 -11
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/run-state.tmpl.md +4 -4
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/scope-summary.tmpl.md +2 -2
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/synthesis.tmpl.md +4 -4
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/usecase.tmpl.md +2 -2
- package/skills/building/resources/plan-wire.md +82 -0
- package/skills/building/resources/plan.md +98 -0
- package/skills/building/resources/retro-research.md +60 -0
- package/skills/building/resources/retro-scan.md +49 -0
- package/skills/building/resources/retro.md +225 -0
- package/skills/building/resources/rounds.md +78 -0
- package/skills/building/resources/stop.md +127 -0
- package/skills/orient/SKILL.md +37 -37
- package/skills/qa-edge-hunter/SKILL.md +14 -14
- package/skills/shapeup/SKILL.md +5 -5
- package/skills/shapeup/resources/breadboarding.md +1 -1
- package/skills/shapeup/resources/context-compaction.md +5 -5
- package/skills/shapeup/resources/shaping.md +1 -1
- package/skills/spec-evaluator/SKILL.md +17 -21
- package/skills/spec-evaluator/references/dimension-contract.md +1 -1
- package/skills/spec-evaluator/references/dimensions/completeness.md +3 -3
- package/skills/spec-evaluator/references/dimensions/spec-conformance.md +1 -1
- package/skills/spec-evaluator/references/dimensions/test-surface-conformance.md +5 -5
- package/skills/task-executor/SKILL.md +11 -15
- package/commands/hill.md +0 -10
- package/hooks/gate-intake.mjs +0 -109
- package/hooks/gate-zerowork.mjs +0 -349
- package/hooks/tier-guard.mjs +0 -162
- package/kernel/compile.mjs +0 -1419
- package/kernel/init/fit.mjs +0 -199
- package/kernel/init/run-args.mjs +0 -206
- package/kernel/lib/breadboard.mjs +0 -165
- package/kernel/probe/attempts.mjs +0 -161
- package/kernel/probe/concurrency.mjs +0 -535
- package/kernel/probe/eval.mjs +0 -405
- package/kernel/probe/hunt.mjs +0 -104
- package/kernel/probe/leg.mjs +0 -229
- package/kernel/probe/owner.mjs +0 -142
- package/kernel/probe/requirements.mjs +0 -328
- package/kernel/probe/resume.mjs +0 -1110
- package/kernel/probe/rounds.mjs +0 -115
- package/kernel/probe/staged.mjs +0 -83
- package/kernel/probe/stats.mjs +0 -477
- package/kernel/probe/t0.mjs +0 -115
- package/kernel/reduce/board.mjs +0 -350
- package/kernel/reduce/graph.mjs +0 -482
- package/kernel/reduce/hill.mjs +0 -322
- package/kernel/reduce/ingest.mjs +0 -767
- package/kernel/reduce/leftovers.mjs +0 -159
- package/kernel/reduce/ship.mjs +0 -529
- package/kernel/reduce/snapshot.mjs +0 -291
- package/kernel/reduce/verdict.mjs +0 -172
- package/kernel/report/export.mjs +0 -384
- package/kernel/report/facts.mjs +0 -234
- package/kernel/verify/budget.mjs +0 -153
- package/kernel/verify/build.mjs +0 -428
- package/kernel/verify/dispatch.mjs +0 -114
- package/kernel/verify/spec.mjs +0 -975
- package/kernel/verify/trace.mjs +0 -604
- package/skills/ba-pitch-analyzer/SKILL.md +0 -204
- package/skills/coach/SKILL.md +0 -397
- package/skills/hill-chart/SKILL.md +0 -154
- package/skills/hill-chart/assets/dashboard.template.html +0 -1432
- package/skills/scope-architect/SKILL.md +0 -174
- package/skills/scope-hammer/SKILL.md +0 -231
- package/skills/solution-architect/SKILL.md +0 -158
- package/skills/tech-lead/SKILL.md +0 -151
- package/skills/tech-lead/references/gates.md +0 -605
- package/skills/tech-lead/references/protocol.md +0 -889
- package/skills/tech-lead/references/tiny-lane.md +0 -53
- package/skills/tech-lead/workflows/shapeup-run.js +0 -2187
- package/skills/translator/SKILL.md +0 -258
- /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/contract-patterns.md +0 -0
- /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/ddd-patterns.md +0 -0
- /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/integration-analysis.md +0 -0
- /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/ux-behavior-patterns.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/api-feasibility.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/be-service.contract.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/offline-storage.contract.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/third-party-api.contract.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/context-map.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/event-choreography.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/migration-plan.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/team-handoff.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/domain-model.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/feedback.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/integration.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task-board.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task-spike.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/ux-behavior.tmpl.md +0 -0
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Run snapshot — the compaction-resilience derivation (v1.2).
|
|
3
|
-
//
|
|
4
|
-
// Derives a RunSnapshot (domain.schema.json#/$defs/RunSnapshot) from FILES ONLY: the
|
|
5
|
-
// active-scope pointer, harness-run.md frontmatter, board task frontmatter, t0/verdicts
|
|
6
|
-
// filenames, and the orders/ vs results/ diff. Never from conversation memory — that is the
|
|
7
|
-
// point: after a context compaction the summary may be lossy, but the files are not. The
|
|
8
|
-
// danger this record exists to prevent is the orchestrator continuing on a degraded summary:
|
|
9
|
-
// re-dispatching an already-ingested order, miscounting attempts (breaking the inner circuit
|
|
10
|
-
// breaker), or "remembering" a hill phase instead of re-deriving it.
|
|
11
|
-
//
|
|
12
|
-
// Consumers: `--write` persists it as an audit anchor; `--format text` prints the rehydrate_hint
|
|
13
|
-
// for whoever asks. Nothing fires it automatically — the orchestrator re-derives after a context
|
|
14
|
-
// loss by running it, which answers whenever the question is asked rather than only at the two
|
|
15
|
-
// moments a lifecycle hook happened to see.
|
|
16
|
-
//
|
|
17
|
-
// Output is self-validated against the registry before it is emitted — the same
|
|
18
|
-
// refuse-to-emit-schema-drift discipline as `harness compile`.
|
|
19
|
-
//
|
|
20
|
-
// Usage: node `harness reduce snapshot` [--cwd <dir>] [--format json|text] [--write]
|
|
21
|
-
// exit 0 with empty stdout when no run is active (fail-open), 1 on schema drift.
|
|
22
|
-
|
|
23
|
-
import { readFileSync, readdirSync, existsSync, writeFileSync } from "node:fs";
|
|
24
|
-
import { resolve, join } from "node:path";
|
|
25
|
-
import { validate } from "../verify/envelope.mjs";
|
|
26
|
-
import { runArgs } from "../lib/argv.mjs";
|
|
27
|
-
import { localDir, localRoot, relLocal, globLocal, runSnapshot as runSnapshotPath } from "../lib/paths.mjs";
|
|
28
|
-
import { deriveRounds } from "../probe/rounds.mjs";
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Read a JSON file, tolerating absence/parse errors.
|
|
32
|
-
* @param {string} p - Path to the JSON file.
|
|
33
|
-
* @returns {(*|null)} The parsed value, or null when the file is missing or not valid JSON.
|
|
34
|
-
*/
|
|
35
|
-
function readJSON(p) {
|
|
36
|
-
try { return JSON.parse(readFileSync(p, "utf8")); } catch { return null; }
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Parse a Markdown frontmatter block into a flat scalar map.
|
|
41
|
-
* @param {string} text - Full document text ("" / null tolerated).
|
|
42
|
-
* @returns {Object<string,string>} Top-level `key: value` pairs (quotes stripped); {} when absent.
|
|
43
|
-
*/
|
|
44
|
-
function frontmatter(text) {
|
|
45
|
-
const m = /^---\n([\s\S]*?)\n---/.exec(text || "");
|
|
46
|
-
if (!m) return {};
|
|
47
|
-
const fm = {};
|
|
48
|
-
for (const line of m[1].split("\n")) {
|
|
49
|
-
const kv = /^([A-Za-z_][\w-]*):\s*(.*)$/.exec(line.trim());
|
|
50
|
-
if (kv) fm[kv[1]] = kv[2].replace(/^['"]|['"]$/g, "");
|
|
51
|
-
}
|
|
52
|
-
return fm;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const MID_RUN = new Set(["orienting", "mapping", "building", "evaluating"]);
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Is this run still open, per its own ledger? Fails CLOSED — a missing or unreadable
|
|
59
|
-
* `harness-run.md` cannot be evidence that a run is in progress.
|
|
60
|
-
* @param {string} root - The `.shapeup` directory.
|
|
61
|
-
* @param {string} slug - Run slug (a child directory of root).
|
|
62
|
-
* @returns {boolean} True when the run's frontmatter status is one of MID_RUN.
|
|
63
|
-
*/
|
|
64
|
-
function isMidRun(root, slug) {
|
|
65
|
-
const runPath = join(root, slug, "harness-run.md");
|
|
66
|
-
if (!existsSync(runPath)) return false;
|
|
67
|
-
try {
|
|
68
|
-
return MID_RUN.has(frontmatter(readFileSync(runPath, "utf8")).status);
|
|
69
|
-
} catch {
|
|
70
|
-
return false; // unreadable ledger → cannot claim the run is open
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Find the active run from files alone — the active-scope pointer, else a mid-run harness-run.md.
|
|
76
|
-
* Either way the run must be OPEN per its own ledger; a pointer alone is not enough.
|
|
77
|
-
* @param {string} cwd - Working-directory root.
|
|
78
|
-
* @returns {({slug:string, scope_id?:string}|null)} The active run's slug (and scope when pointed
|
|
79
|
-
* at one), or null when no run is in progress.
|
|
80
|
-
*/
|
|
81
|
-
function findRun(cwd) {
|
|
82
|
-
const root = localDir(cwd);
|
|
83
|
-
if (!existsSync(root)) return null;
|
|
84
|
-
|
|
85
|
-
// The pointer names the run the sandbox guard is scoping, NOT necessarily a run still open.
|
|
86
|
-
// `.shapeup/active-scope` has existed since v0.3 and nothing has ever cleared it, because
|
|
87
|
-
// its original reader (sandbox-guard) fails OPEN on a stale one — a pointer at a finished run
|
|
88
|
-
// simply stops matching any substrate. `harness reduce graph --subgraph run` is the opposite kind of reader: it
|
|
89
|
-
// turns a pointer into "a run is ALREADY OPEN in this workspace … do NOT open a new run", and
|
|
90
|
-
// it now fires on `startup`/`clear`. Following the pointer without checking the run's status
|
|
91
|
-
// therefore made every cold session in every repo that had EVER run the harness open with a
|
|
92
|
-
// false claim about its own workspace — including sessions with nothing to do with the harness,
|
|
93
|
-
// and including the case where the user's next act is to open the run the injection forbids.
|
|
94
|
-
// This function's own contract (and `harness reduce graph --subgraph run`'s header) always said "a run only for an
|
|
95
|
-
// active-scope pointer or a mid-run harness-run.md"; the status check is what makes that true.
|
|
96
|
-
const pointer = readJSON(join(root, "active-scope"));
|
|
97
|
-
if (pointer?.slug && isMidRun(root, pointer.slug)) {
|
|
98
|
-
return { slug: pointer.slug, scope_id: pointer.scope_id };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
for (const entry of readdirSync(root)) {
|
|
102
|
-
if (isMidRun(root, entry)) return { slug: entry };
|
|
103
|
-
}
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Derive a RunSnapshot for the active run from FILES ONLY (never conversation memory).
|
|
109
|
-
* @param {string} cwd - Working-directory root.
|
|
110
|
-
* @returns {(object|null)} A RunSnapshot (domain.schema.json#/$defs/RunSnapshot): slug, optional
|
|
111
|
-
* scope_id/status/rounds, latest T0 verdict, board done/total + unfinished, open escalates,
|
|
112
|
-
* dispatched-not-ingested `pending_orders`, and a human `rehydrate_hint`. null when no run is active.
|
|
113
|
-
*/
|
|
114
|
-
export function deriveSnapshot(cwd) {
|
|
115
|
-
const run = findRun(cwd);
|
|
116
|
-
if (!run) return null;
|
|
117
|
-
const root = localRoot(cwd, run.slug);
|
|
118
|
-
|
|
119
|
-
const snapshot = {
|
|
120
|
-
schema_version: 1,
|
|
121
|
-
at: new Date().toISOString(),
|
|
122
|
-
slug: run.slug,
|
|
123
|
-
};
|
|
124
|
-
if (run.scope_id) snapshot.scope_id = run.scope_id;
|
|
125
|
-
|
|
126
|
-
const runPath = join(root, "harness-run.md");
|
|
127
|
-
if (existsSync(runPath)) {
|
|
128
|
-
try {
|
|
129
|
-
const fm = frontmatter(readFileSync(runPath, "utf8"));
|
|
130
|
-
// The terminal allowlist mirrors kernel/probe/resume.mjs's TERMINAL_STATUSES, not just the two
|
|
131
|
-
// members it used to carry — a schema/allowlist that disagrees with RUN_STATUSES is silent on
|
|
132
|
-
// both sides here (findRun only ever surfaces a MID_RUN run today, so this whole branch is
|
|
133
|
-
// unreachable in practice), but is exactly the divergence class this file's own imports exist
|
|
134
|
-
// to close elsewhere (deriveRounds, above). "aborted" is a real RUN_STATUSES member.
|
|
135
|
-
if (MID_RUN.has(fm.status) || ["shipped", "escalated", "aborted"].includes(fm.status)) snapshot.status = fm.status;
|
|
136
|
-
// MIGRATED to the same mechanical derivation `reduce ship` and `report export` already use
|
|
137
|
-
// (kernel/probe/rounds.mjs), rather than reading `harness-run.md`'s `rounds_used` literally.
|
|
138
|
-
// That frontmatter line is written ONCE, as 0, by `init run`, and nothing in the round loop
|
|
139
|
-
// ever rewrites it — so the third mechanical reader of "how many rounds did this run build"
|
|
140
|
-
// was the one still reporting the pre-fix number. Measured on a two-round, no-EVAL fixture:
|
|
141
|
-
// this line alone reported `rounds_used: 0` beside its own `round: 2` a few lines below,
|
|
142
|
-
// the exact disagreement `deriveRounds` exists to close. `fm.rounds_used` still travels in as
|
|
143
|
-
// the fallback for a run neither this fix nor deriveRounds can see evidence for (a `--tiny`
|
|
144
|
-
// lane, or a run from before any of these artifacts existed).
|
|
145
|
-
const derivedRounds = deriveRounds(cwd, run.slug, fm.rounds_used);
|
|
146
|
-
if (Number.isFinite(Number(derivedRounds.rounds_used))) snapshot.rounds_used = Number(derivedRounds.rounds_used);
|
|
147
|
-
// Kept as its OWN field, never folded into rounds_used — a round built is not a round judged,
|
|
148
|
-
// and collapsing the two into one number is exactly the ambiguity this migration removes.
|
|
149
|
-
// null (no EVAL result on disk yet) is a real answer and is not written at all, the same
|
|
150
|
-
// optional-field discipline every other snapshot field here follows.
|
|
151
|
-
if (Number.isFinite(derivedRounds.rounds_judged)) snapshot.rounds_judged = derivedRounds.rounds_judged;
|
|
152
|
-
if (/^\d+$/.test(fm.max_rounds || "")) snapshot.max_rounds = Number(fm.max_rounds);
|
|
153
|
-
if (fm.auto_level) snapshot.auto_level = fm.auto_level;
|
|
154
|
-
if (fm.spec_folder) snapshot.spec_folder = fm.spec_folder;
|
|
155
|
-
} catch { /* run ledger unreadable → snapshot stays partial; the files still win */ }
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const verdictsDir = join(root, "t0", "verdicts");
|
|
159
|
-
if (existsSync(verdictsDir)) {
|
|
160
|
-
let latest = null;
|
|
161
|
-
for (const f of readdirSync(verdictsDir)) {
|
|
162
|
-
const m = /^r(\d+)-a(\d+)\.json$/.exec(f);
|
|
163
|
-
if (!m) continue;
|
|
164
|
-
const round = Number(m[1]), attempt = Number(m[2]);
|
|
165
|
-
if (!latest || round > latest.round || (round === latest.round && attempt > latest.attempt)) {
|
|
166
|
-
latest = { round, attempt, file: f };
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
if (latest) {
|
|
170
|
-
snapshot.round = latest.round;
|
|
171
|
-
snapshot.attempt = latest.attempt;
|
|
172
|
-
const verdict = readJSON(join(verdictsDir, latest.file));
|
|
173
|
-
if (verdict?.overall === "green" || verdict?.overall === "red") {
|
|
174
|
-
snapshot.latest_t0 = {
|
|
175
|
-
path: relLocal(run.slug, "t0", "verdicts", latest.file),
|
|
176
|
-
overall: verdict.overall,
|
|
177
|
-
round: latest.round,
|
|
178
|
-
attempt: latest.attempt,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
if (snapshot.round === undefined && snapshot.rounds_used !== undefined) snapshot.round = snapshot.rounds_used;
|
|
184
|
-
|
|
185
|
-
const tasksDir = join(root, "tasks");
|
|
186
|
-
if (existsSync(tasksDir)) {
|
|
187
|
-
const board = { total: 0, done: 0, unfinished: [] };
|
|
188
|
-
for (const f of readdirSync(tasksDir)) {
|
|
189
|
-
if (!/^TASK-.*\.md$/.test(f)) continue;
|
|
190
|
-
board.total++;
|
|
191
|
-
try {
|
|
192
|
-
const fm = frontmatter(readFileSync(join(tasksDir, f), "utf8"));
|
|
193
|
-
if (fm.status === "done") board.done++;
|
|
194
|
-
else if (board.unfinished.length < 10) board.unfinished.push(fm.id || f.replace(/\.md$/, ""));
|
|
195
|
-
} catch { /* unreadable task counts as unfinished-unknown; skip naming it */ }
|
|
196
|
-
}
|
|
197
|
-
if (board.total > 0) snapshot.board = board;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const ordersDir = join(root, "orders");
|
|
201
|
-
const resultsDir = join(root, "results");
|
|
202
|
-
const results = new Set(existsSync(resultsDir) ? readdirSync(resultsDir) : []);
|
|
203
|
-
snapshot.pending_orders = existsSync(ordersDir)
|
|
204
|
-
? readdirSync(ordersDir).filter((f) => f.endsWith(".json") && !results.has(f))
|
|
205
|
-
: [];
|
|
206
|
-
|
|
207
|
-
snapshot.rehydrate_hint =
|
|
208
|
-
`mid-run harness state: slug "${snapshot.slug}"` +
|
|
209
|
-
(snapshot.scope_id ? `, scope "${snapshot.scope_id}"` : "") +
|
|
210
|
-
(snapshot.round !== undefined ? `, round ${snapshot.round}` : "") +
|
|
211
|
-
(snapshot.attempt !== undefined ? `, attempt ${snapshot.attempt}` : "") +
|
|
212
|
-
(snapshot.status ? `, status ${snapshot.status}` : "") +
|
|
213
|
-
(snapshot.board ? `, board ${snapshot.board.done}/${snapshot.board.total} done` : "") +
|
|
214
|
-
(snapshot.latest_t0 ? `, latest T0 ${snapshot.latest_t0.overall}` : "") +
|
|
215
|
-
(snapshot.pending_orders.length ? `, ${snapshot.pending_orders.length} dispatched-not-ingested order(s): ${snapshot.pending_orders.join(", ")}` : "") +
|
|
216
|
-
`. Re-read ${globLocal(snapshot.slug, "harness-run.md")} and the board before continuing — ` +
|
|
217
|
-
`trust the files, not the conversation summary. Never re-dispatch an order that already has a result; ` +
|
|
218
|
-
`re-derive round/attempt/hill from the files, never from memory.`;
|
|
219
|
-
|
|
220
|
-
return snapshot;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* @param {string} cwd - Working-directory root.
|
|
225
|
-
* @param {string} slug - Feature slug.
|
|
226
|
-
* @returns {string} The LOCAL run-trace path `.shapeup/<slug>/run-snapshot.json`.
|
|
227
|
-
*/
|
|
228
|
-
export function snapshotPath(cwd, slug) {
|
|
229
|
-
return runSnapshotPath(cwd, slug);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Validate then persist a snapshot to its LOCAL run-trace home.
|
|
234
|
-
* @param {string} cwd - Working-directory root.
|
|
235
|
-
* @param {object} snapshot - The RunSnapshot to persist (its `slug` names the path).
|
|
236
|
-
* @returns {void} Side effect: writes the snapshot JSON.
|
|
237
|
-
* @throws {Error} If the snapshot drifts from domain.schema.json#/$defs/RunSnapshot.
|
|
238
|
-
*/
|
|
239
|
-
export function writeSnapshot(cwd, snapshot) {
|
|
240
|
-
assertValid(snapshot);
|
|
241
|
-
writeFileSync(snapshotPath(cwd, snapshot.slug), JSON.stringify(snapshot, null, 2) + "\n");
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Assert a snapshot conforms to its registry definition.
|
|
246
|
-
* @param {object} snapshot - The RunSnapshot to check.
|
|
247
|
-
* @returns {void}
|
|
248
|
-
* @throws {Error} If it fails domain.schema.json#/$defs/RunSnapshot (message lists each error).
|
|
249
|
-
*/
|
|
250
|
-
function assertValid(snapshot) {
|
|
251
|
-
const { valid, errors } = validate(snapshot, { $ref: "domain.schema.json#/$defs/RunSnapshot" });
|
|
252
|
-
if (!valid) {
|
|
253
|
-
throw new Error(`run-snapshot drifted from domain.schema.json#/$defs/RunSnapshot:\n ${errors.join("\n ")}`);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// --- CLI -----------------------------------------------------------------------
|
|
258
|
-
|
|
259
|
-
/** The typed argv contract (see `./lib/argv.mjs`). */
|
|
260
|
-
export const ARGV_SPEC = {
|
|
261
|
-
usage: "harness.mjs reduce snapshot [--cwd <dir>] [--format json|text] [--write]",
|
|
262
|
-
_: { arity: 0, max: 0, name: "(no positional operands)" },
|
|
263
|
-
cwd: { type: "path" },
|
|
264
|
-
format: { type: "enum", values: ["json", "text"], default: "json" },
|
|
265
|
-
write: { type: "flag" },
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Derive the file-based run snapshot, and optionally freeze it to disk.
|
|
270
|
-
*
|
|
271
|
-
* @param {string[]} rawArgv - The subcommand's own arguments (harness.mjs strips the verb words).
|
|
272
|
-
* @returns {(Promise<void>|void)} Settles when the subcommand has written its output; most paths
|
|
273
|
-
* call `process.exit()` with the subcommand's documented code rather than returning.
|
|
274
|
-
*/
|
|
275
|
-
export async function cli(rawArgv) {
|
|
276
|
-
const args = runArgs(ARGV_SPEC, rawArgv);
|
|
277
|
-
const cwd = resolve(args.cwd || process.cwd());
|
|
278
|
-
const format = args.format;
|
|
279
|
-
|
|
280
|
-
const snapshot = deriveSnapshot(cwd);
|
|
281
|
-
if (!snapshot) process.exit(0); // no active run → nothing to say (fail-open)
|
|
282
|
-
|
|
283
|
-
try {
|
|
284
|
-
assertValid(snapshot);
|
|
285
|
-
if (args.write) writeSnapshot(cwd, snapshot);
|
|
286
|
-
} catch (e) {
|
|
287
|
-
console.error(` ✗ ${e.message}`);
|
|
288
|
-
process.exit(1);
|
|
289
|
-
}
|
|
290
|
-
console.log(format === "text" ? snapshot.rehydrate_hint : JSON.stringify(snapshot, null, 2));
|
|
291
|
-
}
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Verdict-ledger reference implementation — spec-evaluator's own skill-local
|
|
3
|
-
// reference impl (ships beside SKILL.md; not invoked at runtime).
|
|
4
|
-
//
|
|
5
|
-
// The `spec-evaluator` skill performs verdict re-probe / confidence / flip-detection as a
|
|
6
|
-
// self-contained procedure (`references/verdict-ledger.md`) using its own file tools — it does NOT
|
|
7
|
-
// call this file. This is the executable proof that the documented grammar is well-defined and
|
|
8
|
-
// actually discriminates an unstable judge from a stable one, the same way oracles/*
|
|
9
|
-
// prove the probing grammar. Structural test #15 exercises it.
|
|
10
|
-
//
|
|
11
|
-
// Ledger line shape (one per criterion per run), see verdict-ledger.md:
|
|
12
|
-
// { run, task, dimension, criterion, verdict:"PASS"|"FAIL", confidence, reprobed, flip, evidence, at }
|
|
13
|
-
//
|
|
14
|
-
// Library use:
|
|
15
|
-
// import { reconcile, detectFlips, stability } from "kernel/reduce/verdict.mjs";
|
|
16
|
-
// const { records, summary } = reconcile(priorLines, currentRecords);
|
|
17
|
-
|
|
18
|
-
// Most recent prior line for a (dimension, criterion), by highest run number.
|
|
19
|
-
import { runArgs } from "../lib/argv.mjs";
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Find the most recent prior ledger line for a record's (dimension, criterion), by highest run.
|
|
23
|
-
* @param {Array<{dimension:string, criterion:string, run:number, verdict:string}>} priorLines - Prior lines.
|
|
24
|
-
* @param {{dimension:string, criterion:string}} rec - The record to match.
|
|
25
|
-
* @returns {(object|null)} The highest-run matching prior line, or null when none exists.
|
|
26
|
-
*/
|
|
27
|
-
function priorFor(priorLines, rec) {
|
|
28
|
-
let best = null;
|
|
29
|
-
for (const p of priorLines) {
|
|
30
|
-
if (p.criterion !== rec.criterion || p.dimension !== rec.dimension) continue;
|
|
31
|
-
if (best === null || p.run > best.run) best = p;
|
|
32
|
-
}
|
|
33
|
-
return best;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Reconcile this run's records against the prior ledger: a verdict change vs the most recent prior
|
|
37
|
-
// line for the same criterion sets flip=true and FORCES confidence="low" (a flip means the oracle
|
|
38
|
-
// is unstable on that row, regardless of what confidence the judge proposed). Returns augmented
|
|
39
|
-
// records + a summary. Pure: no I/O, no clock (caller stamps `at`).
|
|
40
|
-
/**
|
|
41
|
-
* Reconcile this run's records against the prior ledger — a verdict change vs the most recent
|
|
42
|
-
* prior line for the same criterion sets flip=true and FORCES confidence="low". Pure: no I/O, no clock.
|
|
43
|
-
* @param {Array<object>} priorLines - Previously recorded ledger lines.
|
|
44
|
-
* @param {Array<{criterion:string, dimension:string, verdict:string, confidence:string}>}
|
|
45
|
-
* currentRecords - This run's per-criterion records.
|
|
46
|
-
* @returns {{records:Array<object>, summary:{total:number, flipped:number, stable:number,
|
|
47
|
-
* flips:Array<{criterion:string, dimension:string, to:string}>}}} The records augmented with
|
|
48
|
-
* `flip` (+ forced-low confidence) and a run summary.
|
|
49
|
-
*/
|
|
50
|
-
export function reconcile(priorLines, currentRecords) {
|
|
51
|
-
const records = currentRecords.map((rec) => {
|
|
52
|
-
const prior = priorFor(priorLines, rec);
|
|
53
|
-
const flip = !!prior && prior.verdict !== rec.verdict;
|
|
54
|
-
return { ...rec, flip, confidence: flip ? "low" : rec.confidence };
|
|
55
|
-
});
|
|
56
|
-
const flipped = records.filter((r) => r.flip);
|
|
57
|
-
return {
|
|
58
|
-
records,
|
|
59
|
-
summary: {
|
|
60
|
-
total: records.length,
|
|
61
|
-
flipped: flipped.length,
|
|
62
|
-
stable: records.length - flipped.length,
|
|
63
|
-
flips: flipped.map((r) => ({ criterion: r.criterion, dimension: r.dimension, to: r.verdict })),
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Walk the full ledger in run order; return every criterion whose verdict changed at any step,
|
|
69
|
-
// with the run pair and direction. Used for the report's stability block / audit of a whole task.
|
|
70
|
-
/**
|
|
71
|
-
* Walk the full ledger in run order and report every verdict change.
|
|
72
|
-
* @param {Array<{dimension:string, criterion:string, run:number, verdict:string}>} allLines - Every ledger line.
|
|
73
|
-
* @returns {Array<{criterion:string, dimension:string, from:string, to:string, runs:[number,number]}>}
|
|
74
|
-
* One entry per step where a criterion's verdict changed.
|
|
75
|
-
*/
|
|
76
|
-
export function detectFlips(allLines) {
|
|
77
|
-
const byKey = new Map();
|
|
78
|
-
for (const l of [...allLines].sort((a, b) => a.run - b.run)) {
|
|
79
|
-
const key = `${l.dimension}::${l.criterion}`;
|
|
80
|
-
const seq = byKey.get(key) || [];
|
|
81
|
-
seq.push(l);
|
|
82
|
-
byKey.set(key, seq);
|
|
83
|
-
}
|
|
84
|
-
const flips = [];
|
|
85
|
-
for (const [key, seq] of byKey) {
|
|
86
|
-
for (let i = 1; i < seq.length; i++) {
|
|
87
|
-
if (seq[i].verdict !== seq[i - 1].verdict) {
|
|
88
|
-
flips.push({
|
|
89
|
-
criterion: seq[i].criterion,
|
|
90
|
-
dimension: seq[i].dimension,
|
|
91
|
-
from: seq[i - 1].verdict,
|
|
92
|
-
to: seq[i].verdict,
|
|
93
|
-
runs: [seq[i - 1].run, seq[i].run],
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return flips;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Stability of the latest run vs the run before it: fraction of the latest run's criteria whose
|
|
102
|
-
// verdict matches their immediately-prior line. 1.0 when there is only one run (nothing to contradict).
|
|
103
|
-
/**
|
|
104
|
-
* Measure stability of the latest run vs the run before it.
|
|
105
|
-
* @param {Array<{run:number, dimension:string, criterion:string, verdict:string}>} allLines - Every ledger line.
|
|
106
|
-
* @returns {{runs:number, stable:number, total:number, ratio:number}} The max run number, how many
|
|
107
|
-
* of the latest run's criteria match their prior verdict (new criteria count as stable), the
|
|
108
|
-
* latest run's criterion count, and their ratio (1.0 when there is only one run).
|
|
109
|
-
*/
|
|
110
|
-
export function stability(allLines) {
|
|
111
|
-
if (allLines.length === 0) return { runs: 0, stable: 0, total: 0, ratio: 1 };
|
|
112
|
-
const maxRun = Math.max(...allLines.map((l) => l.run));
|
|
113
|
-
const latest = allLines.filter((l) => l.run === maxRun);
|
|
114
|
-
if (maxRun === 1) return { runs: 1, stable: latest.length, total: latest.length, ratio: 1 };
|
|
115
|
-
const prior = allLines.filter((l) => l.run < maxRun);
|
|
116
|
-
let stable = 0;
|
|
117
|
-
for (const rec of latest) {
|
|
118
|
-
const p = priorFor(prior, rec);
|
|
119
|
-
if (p && p.verdict === rec.verdict) stable++;
|
|
120
|
-
else if (!p) stable++; // a brand-new criterion can't have flipped
|
|
121
|
-
}
|
|
122
|
-
return { runs: maxRun, stable, total: latest.length, ratio: latest.length ? stable / latest.length : 1 };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Parse a .jsonl ledger string into records (blank lines ignored).
|
|
126
|
-
/**
|
|
127
|
-
* Parse a `.jsonl` verdict ledger into records.
|
|
128
|
-
* @param {string} text - The ledger file contents ("" / null → []).
|
|
129
|
-
* @returns {Array<object>} One parsed object per non-blank line.
|
|
130
|
-
* @throws {SyntaxError} If a non-blank line is not valid JSON.
|
|
131
|
-
*/
|
|
132
|
-
export function parseLedger(text) {
|
|
133
|
-
return (text || "")
|
|
134
|
-
.split(/\r?\n/)
|
|
135
|
-
.map((l) => l.trim())
|
|
136
|
-
.filter(Boolean)
|
|
137
|
-
.map((l) => JSON.parse(l));
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// --- CLI entry: summarize a ledger file -------------------------------------
|
|
141
|
-
/** The typed argv contract (see `kernel/lib/argv.mjs`). */
|
|
142
|
-
export const ARGV_SPEC = {
|
|
143
|
-
usage: "harness.mjs reduce verdict <.verdicts-TASK.jsonl>",
|
|
144
|
-
_: { arity: 1, max: 1, name: ".verdicts-TASK.jsonl" },
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Reconcile a verdict ledger and report its flips and stability.
|
|
149
|
-
*
|
|
150
|
-
* @param {string[]} rawArgv - The subcommand's own arguments (harness.mjs strips the verb words).
|
|
151
|
-
* @returns {(Promise<void>|void)} Settles when the subcommand has written its output; most paths
|
|
152
|
-
* call `process.exit()` with the subcommand's documented code rather than returning.
|
|
153
|
-
*/
|
|
154
|
-
export async function cli(rawArgv) {
|
|
155
|
-
const { readFileSync } = await import("node:fs");
|
|
156
|
-
const path = runArgs(ARGV_SPEC, rawArgv)._[0];
|
|
157
|
-
let lines;
|
|
158
|
-
try { lines = parseLedger(readFileSync(path, "utf8")); }
|
|
159
|
-
catch (e) { console.error(`cannot read ledger ${path}: ${e.message}`); process.exit(2); }
|
|
160
|
-
const flips = detectFlips(lines);
|
|
161
|
-
const s = stability(lines);
|
|
162
|
-
console.log(`ledger: ${lines.length} records over ${s.runs} run(s)`);
|
|
163
|
-
console.log(`stability (latest vs prior): ${s.stable}/${s.total} stable`);
|
|
164
|
-
if (flips.length) {
|
|
165
|
-
console.log(`⚠ ${flips.length} flip(s):`);
|
|
166
|
-
for (const f of flips) console.log(` ${f.dimension}/${f.criterion}: ${f.from}→${f.to} (runs ${f.runs.join("→")})`);
|
|
167
|
-
process.exit(1);
|
|
168
|
-
} else {
|
|
169
|
-
console.log("✅ no verdict flips");
|
|
170
|
-
process.exit(0);
|
|
171
|
-
}
|
|
172
|
-
}
|