shapeup-sdlc 3.18.2 → 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 +29 -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
package/kernel/verify/budget.mjs
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// THE THIRD BREAKER — wall clock. Derived, never claimed; checked at every round boundary.
|
|
3
|
-
//
|
|
4
|
-
// WHY THIS EXISTS (and it corrects an earlier diagnosis).
|
|
5
|
-
//
|
|
6
|
-
// A run killed at an external time cap produces nothing scoreable, and the obvious reading is that
|
|
7
|
-
// it stalled at a gate with no human to sign off.
|
|
8
|
-
//
|
|
9
|
-
// Reading the transcript with transcript-level metrics often says otherwise, and not narrowly:
|
|
10
|
-
// steady turns, steady tool calls, steady file writes, gate markers advancing, a low narration
|
|
11
|
-
// ratio and zero stall signals — a run killed mid-loop at GATE L3, Verdict & Loop. It was not
|
|
12
|
-
// waiting. It ran out of clock while genuinely working.
|
|
13
|
-
//
|
|
14
|
-
// That makes the defect specific: **both existing breakers count events, not time.**
|
|
15
|
-
// `round_budget` decrements once per round; `attempt_budget` decrements once per T0 attempt.
|
|
16
|
-
// Neither can observe that round 1 has been running for twenty-nine minutes. A run can burn its
|
|
17
|
-
// entire wall-clock budget without either breaker moving a single tick, and when the harness is
|
|
18
|
-
// killed from outside, it ships nothing — not even the scopes that were already green.
|
|
19
|
-
//
|
|
20
|
-
// So the fix is not more gate automation (there was no stall to automate away). It is a third
|
|
21
|
-
// breaker on the axis nobody was watching:
|
|
22
|
-
//
|
|
23
|
-
// OUTER round_budget — rounds, the six-week-timebox analog
|
|
24
|
-
// INNER attempt_budget — T0 attempts per scope
|
|
25
|
-
// DEADLINE wall_clock_budget — elapsed seconds, this file
|
|
26
|
-
//
|
|
27
|
-
// Tripping the deadline breaker does NOT kill the run. It routes to GATE H (scope-hammer): the
|
|
28
|
-
// census runs, the cut list is compared against the baseline, and whatever is green ships. That
|
|
29
|
-
// is the entire difference between an external SIGTERM and a circuit breaker — one produces a
|
|
30
|
-
// dash in every column, the other produces a smaller feature that works.
|
|
31
|
-
//
|
|
32
|
-
// Non-regression by construction: with no `wall_clock_budget_s` in the run config this reports
|
|
33
|
-
// `off` and nothing changes. Existing runs behave exactly as before.
|
|
34
|
-
//
|
|
35
|
-
// USAGE
|
|
36
|
-
// node `harness verify budget` [--slug <slug>] [--cwd <root>] [--at <ISO>] # status, exit 0
|
|
37
|
-
// node `harness verify budget` --strict … # exit 6 when the breaker has tripped
|
|
38
|
-
//
|
|
39
|
-
// Output: { status, elapsed_s, budget_s, remaining_s, used_fraction, action }
|
|
40
|
-
// status ∈ off | ok | warn | trip
|
|
41
|
-
// action = the sentence the orchestrator must act on at the round boundary.
|
|
42
|
-
|
|
43
|
-
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
|
44
|
-
import { join } from "node:path";
|
|
45
|
-
import { runArgs } from "../lib/argv.mjs";
|
|
46
|
-
import { localDir } from "../lib/paths.mjs";
|
|
47
|
-
|
|
48
|
-
/** Fraction of the budget at which the run should stop STARTING work it cannot finish. */
|
|
49
|
-
export const WARN_AT = 0.75;
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Decide the breaker state from elapsed time against a budget. Pure, so the structural tests can
|
|
53
|
-
* assert every boundary without waiting for a clock.
|
|
54
|
-
*
|
|
55
|
-
* @param {number} elapsedS - Seconds since the run receipt was written.
|
|
56
|
-
* @param {number|null} budgetS - Configured wall-clock budget, or null/0 when the breaker is off.
|
|
57
|
-
* @returns {{status: string, elapsed_s: number, budget_s: number|null, remaining_s: number|null,
|
|
58
|
-
* used_fraction: number|null, action: string}}
|
|
59
|
-
*/
|
|
60
|
-
export function evaluateBudget(elapsedS, budgetS) {
|
|
61
|
-
const elapsed = Math.max(0, Math.round(elapsedS));
|
|
62
|
-
if (!budgetS || budgetS <= 0) {
|
|
63
|
-
return {
|
|
64
|
-
status: "off", elapsed_s: elapsed, budget_s: null, remaining_s: null, used_fraction: null,
|
|
65
|
-
action: "No wall-clock budget configured — the deadline breaker is off. Round and attempt budgets still apply.",
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
const remaining = Math.round(budgetS - elapsed);
|
|
69
|
-
const used = Number((elapsed / budgetS).toFixed(3));
|
|
70
|
-
|
|
71
|
-
if (remaining <= 0) {
|
|
72
|
-
return {
|
|
73
|
-
status: "trip", elapsed_s: elapsed, budget_s: budgetS, remaining_s: remaining, used_fraction: used,
|
|
74
|
-
action:
|
|
75
|
-
"DEADLINE BREAKER TRIPPED. Do not start another build round or another scope. Go straight to " +
|
|
76
|
-
"GATE H (delegate scope-hammer with --breaker deadline): run the census, compare the shippable " +
|
|
77
|
-
"subset against the baseline, and ship what is green. A run killed from outside ships nothing; " +
|
|
78
|
-
"a run that trips its own breaker ships the part that works.",
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
if (used >= WARN_AT) {
|
|
82
|
-
return {
|
|
83
|
-
status: "warn", elapsed_s: elapsed, budget_s: budgetS, remaining_s: remaining, used_fraction: used,
|
|
84
|
-
action:
|
|
85
|
-
`${remaining}s of ${budgetS}s remain (${Math.round(used * 100)}% spent). Finish and T0-verify the ` +
|
|
86
|
-
"scope in flight; do NOT open a new scope or a new build round. The next boundary is where the " +
|
|
87
|
-
"breaker trips, and work started now will be cut at GATE H rather than shipped.",
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
return {
|
|
91
|
-
status: "ok", elapsed_s: elapsed, budget_s: budgetS, remaining_s: remaining, used_fraction: used,
|
|
92
|
-
action: `${remaining}s remain of ${budgetS}s. Proceed.`,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** Locate the active run's receipt — the only place `started_at` is recorded. */
|
|
97
|
-
export function findRun(cwd, slug = null) {
|
|
98
|
-
const root = localDir(cwd);
|
|
99
|
-
if (!existsSync(root)) return null;
|
|
100
|
-
if (!slug) {
|
|
101
|
-
try {
|
|
102
|
-
const ptr = JSON.parse(readFileSync(join(root, "active-scope"), "utf8"));
|
|
103
|
-
slug = ptr?.slug || null;
|
|
104
|
-
} catch { /* no pointer — fall through to a scan */ }
|
|
105
|
-
}
|
|
106
|
-
const candidates = slug ? [slug] : (() => { try { return readdirSync(root); } catch { return []; } })();
|
|
107
|
-
for (const entry of candidates) {
|
|
108
|
-
const p = join(root, entry, "receipt.json");
|
|
109
|
-
if (!existsSync(p)) continue;
|
|
110
|
-
try { return { slug: entry, receipt: JSON.parse(readFileSync(p, "utf8")) }; }
|
|
111
|
-
catch { /* unreadable receipt is not a run */ }
|
|
112
|
-
}
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** The typed argv contract (see `./lib/argv.mjs`). */
|
|
117
|
-
export const ARGV_SPEC = {
|
|
118
|
-
usage: "harness.mjs verify budget [--slug <slug>] [--cwd <dir>] [--at <iso8601>] [--strict]",
|
|
119
|
-
_: { arity: 0, max: 0, name: "(no positional operands)" },
|
|
120
|
-
slug: { type: "str" },
|
|
121
|
-
cwd: { type: "path" },
|
|
122
|
-
at: { type: "str" },
|
|
123
|
-
strict: { type: "flag" },
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Check the wall-clock budget for the open run and report the breaker's verdict.
|
|
128
|
-
*
|
|
129
|
-
* @param {string[]} rawArgv - The subcommand's own arguments (harness.mjs strips the verb words).
|
|
130
|
-
* @returns {(Promise<void>|void)} Settles when the subcommand has written its output; most paths
|
|
131
|
-
* call `process.exit()` with the subcommand's documented code rather than returning.
|
|
132
|
-
*/
|
|
133
|
-
export function cli(rawArgv) {
|
|
134
|
-
const args = runArgs(ARGV_SPEC, rawArgv);
|
|
135
|
-
const cwd = args.cwd || process.cwd();
|
|
136
|
-
const run = findRun(cwd, args.slug ?? null);
|
|
137
|
-
if (!run) {
|
|
138
|
-
console.error("no run receipt found — open the run with `harness init run` first (GATE L0.1).");
|
|
139
|
-
process.exit(2);
|
|
140
|
-
}
|
|
141
|
-
const startedAt = Date.parse(run.receipt.started_at || "");
|
|
142
|
-
if (Number.isNaN(startedAt)) {
|
|
143
|
-
console.error(`receipt for "${run.slug}" has no parseable started_at — cannot derive elapsed time.`);
|
|
144
|
-
process.exit(2);
|
|
145
|
-
}
|
|
146
|
-
const now = args.at ? Date.parse(args.at) : Date.now();
|
|
147
|
-
const budget = Number(run.receipt.config?.wall_clock_budget_s || 0) || null;
|
|
148
|
-
const result = evaluateBudget((now - startedAt) / 1000, budget);
|
|
149
|
-
|
|
150
|
-
console.log(JSON.stringify({ slug: run.slug, started_at: run.receipt.started_at, ...result }, null, 2));
|
|
151
|
-
process.exit(args.strict && result.status === "trip" ? 6 : 0);
|
|
152
|
-
}
|
|
153
|
-
|
package/kernel/verify/build.mjs
DELETED
|
@@ -1,428 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// The round build gate — does the FEATURE build and launch, before the judge is asked about it.
|
|
3
|
-
//
|
|
4
|
-
// WHY A GATE ABOVE T0. A T0 verdict is one scope's fixtures, run inside that scope's substrate, and
|
|
5
|
-
// the fixtures are the scope-architect's to write. Nothing in that layer proves the feature as a
|
|
6
|
-
// whole compiles or starts. Measured on a live mobile run: every one of 30 T0 trials went green on
|
|
7
|
-
// its first try — the fixtures were TypeScript stand-ins, structural greps and a test-suite wrapper
|
|
8
|
-
// that never compiled its sources — while the ledger's own `run_cmd` failed, and once the compiler
|
|
9
|
-
// could actually reach the new files it reported 58 errors. Three rounds of EVAL then graded a
|
|
10
|
-
// blank screen: nothing in the loop had ever installed or launched the app. The hill shards, derived
|
|
11
|
-
// from those T0 verdicts, all read DOWNHILL_EXECUTION.
|
|
12
|
-
//
|
|
13
|
-
// Two lessons, and this gate is both:
|
|
14
|
-
//
|
|
15
|
-
// 1. A green exit code from the build is not proof the feature compiled. Some toolchains compile
|
|
16
|
-
// only what is reachable from an entry point, so a scope's files can sit outside the compiled
|
|
17
|
-
// set and the build stays green. The exit code is the necessary half; `build_probe` is where a
|
|
18
|
-
// project asserts the other half (the artifact covers what the run wrote), in whatever way its
|
|
19
|
-
// toolchain makes possible.
|
|
20
|
-
// 2. Nothing else in the loop launches the app. `launch_probe` is where a project installs, starts
|
|
21
|
-
// and asserts a first screen. For the mobile archetype its absence is called out every round,
|
|
22
|
-
// because "on-device install unverified" was an L0 risk with no owner for three rounds.
|
|
23
|
-
//
|
|
24
|
-
// WHAT RUNS, in order, each stopping the rest on failure: the ledger's `run_cmd` (the build),
|
|
25
|
-
// then the profile's `build_probe`, then its `launch_probe`. All three are commands the tech lead
|
|
26
|
-
// pinned at GATE L0 — this script invents none of them, and a run that declares none of them gets
|
|
27
|
-
// exit 3 and no artifact, which every reader treats as "no gate declared" rather than as green.
|
|
28
|
-
//
|
|
29
|
-
// THE RED GATE IS THE NEXT ROUND'S BUG LIST. `harness compile` reads the latest gate artifact for
|
|
30
|
-
// the previous round and addresses each failing step to the scopes whose substrate contains the
|
|
31
|
-
// files the output names (unowned → every scope, marked), exactly as it does for an EVAL verdict.
|
|
32
|
-
// `reduce hill` reads the same artifact: a T0-green verdict from a round whose gate is red moves no
|
|
33
|
-
// scope downhill. Both are the same file, written once here — zero LLM tokens, same as T0.
|
|
34
|
-
//
|
|
35
|
-
// Usage:
|
|
36
|
-
// node "${CLAUDE_PLUGIN_ROOT}/kernel/harness.mjs" verify build --slug <slug> --round N [--cwd <dir>]
|
|
37
|
-
//
|
|
38
|
-
// Exit code: 0 = green, 1 = red, 2 = bad argv, 3 = nothing declared (no run_cmd, no probes).
|
|
39
|
-
|
|
40
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from "node:fs";
|
|
41
|
-
import { join, resolve } from "node:path";
|
|
42
|
-
import { spawnSync } from "node:child_process";
|
|
43
|
-
import { createHash } from "node:crypto";
|
|
44
|
-
import { runArgs, isMain } from "../lib/argv.mjs";
|
|
45
|
-
import { harnessRun, projectProfile, roundBuildDir, localRoot, runIdFromRoot, readRunId } from "../lib/paths.mjs";
|
|
46
|
-
import { readContract, readAllContracts, PROJECT_PROFILE, SCOPE_CONTRACT, splitFrontmatter } from "../lib/contract.mjs";
|
|
47
|
-
import { scopesDir } from "../lib/paths.mjs";
|
|
48
|
-
import { digest } from "../probe/digest.mjs";
|
|
49
|
-
|
|
50
|
-
/** The three steps, in the order they run. */
|
|
51
|
-
export const STEPS = ["run_cmd", "build_probe", "launch_probe"];
|
|
52
|
-
|
|
53
|
-
/** Archetypes whose product cannot be judged without being launched on a device or simulator. */
|
|
54
|
-
export const LAUNCH_REQUIRED_ARCHETYPES = new Set(["mobile"]);
|
|
55
|
-
|
|
56
|
-
const TAIL_BYTES = 8 * 1024;
|
|
57
|
-
const tail = (s) => (s || "").length > TAIL_BYTES ? (s || "").slice(-TAIL_BYTES) : (s || "");
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* The commands this gate runs for a feature, read from the artifacts that declare them.
|
|
61
|
-
*
|
|
62
|
-
* `run_cmd` comes from the run ledger's frontmatter (`harness-run.md`, written at GATE L0 and
|
|
63
|
-
* already what every EVAL order carries); the two probes come from the committed project profile.
|
|
64
|
-
* Nothing is inferred: an absent field is an absent step.
|
|
65
|
-
*
|
|
66
|
-
* @param {string} cwd - Project root.
|
|
67
|
-
* @param {string} slug - Feature slug.
|
|
68
|
-
* @returns {{archetype:(string|null), steps:Array<{kind:string, cmd:string}>, warnings:string[]}}
|
|
69
|
-
* The declared steps in run order, the profile's archetype, and the warnings a reader should
|
|
70
|
-
* see — currently one: a launch-required archetype with no `launch_probe`.
|
|
71
|
-
*/
|
|
72
|
-
export function declaredSteps(cwd, slug) {
|
|
73
|
-
const warnings = [];
|
|
74
|
-
let runCmd = null;
|
|
75
|
-
try {
|
|
76
|
-
const hr = splitFrontmatter(readFileSync(harnessRun(cwd, slug), "utf8")).meta || {};
|
|
77
|
-
runCmd = typeof hr.run_cmd === "string" && hr.run_cmd.trim() ? hr.run_cmd.trim() : null;
|
|
78
|
-
} catch { /* no ledger — no run_cmd */ }
|
|
79
|
-
|
|
80
|
-
let profile = null;
|
|
81
|
-
const pp = projectProfile(cwd, slug);
|
|
82
|
-
if (existsSync(pp)) profile = readContract(pp, PROJECT_PROFILE)?.contract || null;
|
|
83
|
-
const archetype = typeof profile?.archetype === "string" ? profile.archetype : null;
|
|
84
|
-
const pick = (k) => (typeof profile?.[k] === "string" && profile[k].trim() ? profile[k].trim() : null);
|
|
85
|
-
|
|
86
|
-
const steps = [];
|
|
87
|
-
if (runCmd) { steps.push({ kind: "run_cmd", cmd: runCmd }); warnings.push(...fixtureCoverageWarnings(cwd, slug, runCmd)); }
|
|
88
|
-
else warnings.push("the run ledger declares no run_cmd — the build itself is not part of this gate");
|
|
89
|
-
const buildProbe = pick("build_probe");
|
|
90
|
-
if (buildProbe) steps.push({ kind: "build_probe", cmd: buildProbe });
|
|
91
|
-
const launchProbe = pick("launch_probe");
|
|
92
|
-
if (launchProbe) steps.push({ kind: "launch_probe", cmd: launchProbe });
|
|
93
|
-
else if (archetype && LAUNCH_REQUIRED_ARCHETYPES.has(archetype)) {
|
|
94
|
-
warnings.push(`archetype ${archetype} declares no launch_probe in project-profile.md — nothing in the loop ` +
|
|
95
|
-
`launches the app, so a blank first screen survives every round. Give the install/launch risk an owner: ` +
|
|
96
|
-
`a command that installs the built artifact, starts it, asserts the first screen and fails on fatal logs.`);
|
|
97
|
-
}
|
|
98
|
-
return { archetype, steps, warnings };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* The executable a run command actually invokes — its basename, past any `cd …&&`, `env`, or
|
|
103
|
-
* `VAR=value` prefix. `cd app && DEVECO_SDK_HOME=/x /tools/hvigor/bin/hvigorw assembleHap` → `hvigorw`.
|
|
104
|
-
* @param {string} runCmd - The ledger's run command.
|
|
105
|
-
* @returns {(string|null)} The tool's basename, or null when none can be read.
|
|
106
|
-
*/
|
|
107
|
-
export function buildTool(runCmd) {
|
|
108
|
-
const segments = String(runCmd || "").split(/&&|;|\|\|/).map((s) => s.trim()).filter(Boolean);
|
|
109
|
-
for (const seg of segments) {
|
|
110
|
-
const tokens = seg.split(/\s+/).filter((t) => t && !/^[A-Za-z_][A-Za-z0-9_]*=/.test(t) && t !== "env" && t !== "cd");
|
|
111
|
-
if (seg.startsWith("cd ")) continue;
|
|
112
|
-
if (!tokens.length) continue;
|
|
113
|
-
const base = tokens[0].split(/[\\/]/).pop();
|
|
114
|
-
if (base) return base;
|
|
115
|
-
}
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Scopes whose fixtures never invoke the build tool `run_cmd` names — ADVISORY.
|
|
121
|
-
*
|
|
122
|
-
* A T0 layer can be entirely green on a feature that does not compile when every fixture is a
|
|
123
|
-
* grep or a stand-in compiler (measured: 13 of 15 fixtures on one run). The harness cannot know
|
|
124
|
-
* what a project's fixtures should run, but it can see when none of a scope's fixture commands
|
|
125
|
-
* mention the tool the ledger builds with, and say so beside the gate's verdict. A warning, not a
|
|
126
|
-
* failure: a library scope with no compiled surface is a legitimate reason for the mismatch.
|
|
127
|
-
*
|
|
128
|
-
* @param {string} cwd - Project root.
|
|
129
|
-
* @param {string} slug - Feature slug.
|
|
130
|
-
* @param {(string|null)} runCmd - The ledger's run command.
|
|
131
|
-
* @returns {string[]} One warning per scope whose fixtures name the tool nowhere.
|
|
132
|
-
*/
|
|
133
|
-
export function fixtureCoverageWarnings(cwd, slug, runCmd) {
|
|
134
|
-
const tool = buildTool(runCmd);
|
|
135
|
-
if (!tool) return [];
|
|
136
|
-
const out = [];
|
|
137
|
-
let contracts = [];
|
|
138
|
-
try { contracts = readAllContracts(scopesDir(cwd, slug), SCOPE_CONTRACT).map((x) => x.contract); } catch { return []; }
|
|
139
|
-
for (const c of contracts) {
|
|
140
|
-
const fixtures = Array.isArray(c.e2e_verification_fixtures) ? c.e2e_verification_fixtures : [];
|
|
141
|
-
if (!fixtures.length || fixtures.some((f) => String(f).includes(tool))) continue;
|
|
142
|
-
out.push(`scope ${c.scope_id}: none of its ${fixtures.length} fixture(s) invoke ${tool} (the ledger's build tool) — ` +
|
|
143
|
-
`its T0 green is not evidence the scope compiles; only this gate's run_cmd is.`);
|
|
144
|
-
}
|
|
145
|
-
return out;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Run one gate command and capture its outcome (10-minute timeout), output tails kept for the digest.
|
|
150
|
-
* @param {{kind:string, cmd:string}} step - The step to run.
|
|
151
|
-
* @param {string} cwd - Working directory.
|
|
152
|
-
* @returns {object} `{kind, cmd, exit, pass, stdout_tail, stderr_tail, error?}`.
|
|
153
|
-
*/
|
|
154
|
-
export function runStep(step, cwd) {
|
|
155
|
-
const r = spawnSync(step.cmd, { shell: true, cwd, encoding: "utf8", timeout: 10 * 60 * 1000 });
|
|
156
|
-
const error = r.error ? String(r.error.message || r.error) : null;
|
|
157
|
-
return {
|
|
158
|
-
kind: step.kind, cmd: step.cmd,
|
|
159
|
-
exit: r.status ?? 1, pass: r.status === 0,
|
|
160
|
-
stdout_tail: tail(r.stdout), stderr_tail: tail(r.stderr),
|
|
161
|
-
...(error ? { error } : {}),
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Run the declared steps in order, stopping at the first failure — a probe over a build that did
|
|
167
|
-
* not complete answers nothing, and would only bury the real error under a second one.
|
|
168
|
-
*
|
|
169
|
-
* @param {Array<{kind:string, cmd:string}>} steps - From {@link declaredSteps}.
|
|
170
|
-
* @param {string} cwd - Working directory.
|
|
171
|
-
* @returns {{overall:("green"|"red"), steps:Array<object>}} Every declared step, the ones not
|
|
172
|
-
* reached marked `skipped: true`.
|
|
173
|
-
*/
|
|
174
|
-
export function runGate(steps, cwd) {
|
|
175
|
-
const out = [];
|
|
176
|
-
let failed = false;
|
|
177
|
-
for (const step of steps) {
|
|
178
|
-
if (failed) { out.push({ kind: step.kind, cmd: step.cmd, skipped: true }); continue; }
|
|
179
|
-
const r = runStep(step, cwd);
|
|
180
|
-
out.push(r);
|
|
181
|
-
if (!r.pass) failed = true;
|
|
182
|
-
}
|
|
183
|
-
return { overall: failed ? "red" : "green", steps: out };
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Every gate artifact for a round, oldest first.
|
|
188
|
-
* @param {string} cwd - Project root.
|
|
189
|
-
* @param {string} slug - Feature slug.
|
|
190
|
-
* @param {number} round - Round number.
|
|
191
|
-
* @returns {Array<{path:string, trial:number, body:object}>} Parsed artifacts; unreadable ones skipped.
|
|
192
|
-
*/
|
|
193
|
-
export function roundBuildArtifacts(cwd, slug, round) {
|
|
194
|
-
const dir = roundBuildDir(cwd, slug);
|
|
195
|
-
let files;
|
|
196
|
-
try { files = readdirSync(dir); } catch { return []; }
|
|
197
|
-
const out = [];
|
|
198
|
-
for (const f of files) {
|
|
199
|
-
const m = f.match(/^r(\d+)-t(\d+)\.json$/);
|
|
200
|
-
if (!m || Number(m[1]) !== Number(round)) continue;
|
|
201
|
-
try { out.push({ path: join(dir, f), trial: Number(m[2]), body: JSON.parse(readFileSync(join(dir, f), "utf8")) }); }
|
|
202
|
-
catch { /* skip */ }
|
|
203
|
-
}
|
|
204
|
-
return out.sort((a, b) => a.trial - b.trial);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* The gate's latest word on a round — the artifact readers branch on.
|
|
209
|
-
*
|
|
210
|
-
* Latest, because the gate may run again after a hand fix between launches, and the run must act on
|
|
211
|
-
* the current state of the build, not the first one recorded. Every earlier artifact stays on disk.
|
|
212
|
-
*
|
|
213
|
-
* @param {string} cwd - Project root.
|
|
214
|
-
* @param {string} slug - Feature slug.
|
|
215
|
-
* @param {number} round - Round number.
|
|
216
|
-
* @returns {(object|null)} The latest artifact body, or null when the gate never ran for this round.
|
|
217
|
-
*/
|
|
218
|
-
export function latestRoundBuild(cwd, slug, round) {
|
|
219
|
-
const all = roundBuildArtifacts(cwd, slug, round);
|
|
220
|
-
return all.length ? all[all.length - 1].body : null;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* The gate artifact an evaluate order should name — the newest one THIS run wrote, with the file it
|
|
225
|
-
* lives in.
|
|
226
|
-
*
|
|
227
|
-
* {@link latestRoundBuild} answers "what did the gate say about round N" and hands back a body; an
|
|
228
|
-
* order has to point at a file, so the path travels with it here. Scoped to the current run when a
|
|
229
|
-
* receipt is readable: a prior run over the same slug leaves its own `r1-t1.json` on disk, and the
|
|
230
|
-
* judge must not be pointed at another run's launch evidence. Scoped to one round when given; with
|
|
231
|
-
* no round it is the newest of any round — a standalone evaluation has none, the same reading
|
|
232
|
-
* `t0ArtifactsFor` uses for the T0 verdicts.
|
|
233
|
-
*
|
|
234
|
-
* @param {string} cwd - Project root.
|
|
235
|
-
* @param {string} slug - Feature slug.
|
|
236
|
-
* @param {number} [round] - The round being evaluated.
|
|
237
|
-
* @returns {({path:string, body:object}|null)} null when the gate never ran, or nothing readable
|
|
238
|
-
* belongs to this run. Never throws: an order must compile without it.
|
|
239
|
-
*/
|
|
240
|
-
export function latestRoundBuildFile(cwd, slug, round) {
|
|
241
|
-
let files;
|
|
242
|
-
const dir = roundBuildDir(cwd, slug);
|
|
243
|
-
try { files = readdirSync(dir); } catch { return null; }
|
|
244
|
-
const runId = readRunId(cwd, slug);
|
|
245
|
-
let best = null;
|
|
246
|
-
for (const f of files) {
|
|
247
|
-
const m = f.match(/^r(\d+)-t(\d+)\.json$/);
|
|
248
|
-
if (!m) continue;
|
|
249
|
-
const r = Number(m[1]), t = Number(m[2]);
|
|
250
|
-
if (round != null && r !== Number(round)) continue;
|
|
251
|
-
let body;
|
|
252
|
-
try { body = JSON.parse(readFileSync(join(dir, f), "utf8")); } catch { continue; }
|
|
253
|
-
if (runId && body?.run_id && body.run_id !== runId) continue;
|
|
254
|
-
if (!best || r > best.r || (r === best.r && t > best.t)) best = { r, t, path: join(dir, f), body };
|
|
255
|
-
}
|
|
256
|
-
return best && { path: best.path, body: best.body };
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* The command the project declared for bringing the built app up — `launch_probe` in the committed
|
|
261
|
-
* profile: install the artifact, start it, assert the first screen. Read on its own rather than
|
|
262
|
-
* through {@link declaredSteps}, which also reads the ledger and computes warnings an order has no
|
|
263
|
-
* use for.
|
|
264
|
-
*
|
|
265
|
-
* @param {string} cwd - Project root.
|
|
266
|
-
* @param {string} slug - Feature slug.
|
|
267
|
-
* @returns {(string|null)} The command, or null when the profile is absent or declares none.
|
|
268
|
-
* Never throws.
|
|
269
|
-
*/
|
|
270
|
-
export function launchProbeFor(cwd, slug) {
|
|
271
|
-
try {
|
|
272
|
-
const pp = projectProfile(cwd, slug);
|
|
273
|
-
if (!existsSync(pp)) return null;
|
|
274
|
-
const v = readContract(pp, PROJECT_PROFILE)?.contract?.launch_probe;
|
|
275
|
-
return typeof v === "string" && v.trim() ? v.trim() : null;
|
|
276
|
-
} catch { return null; }
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* The rounds whose latest gate artifact is red — the set `reduce hill` subtracts from.
|
|
281
|
-
* @param {string} cwd - Project root.
|
|
282
|
-
* @param {string} slug - Feature slug.
|
|
283
|
-
* @returns {Set<number>} Round numbers; empty when the gate never ran (non-regression: every
|
|
284
|
-
* T0 verdict then counts exactly as it did before the gate existed).
|
|
285
|
-
*/
|
|
286
|
-
export function redBuildRounds(cwd, slug) {
|
|
287
|
-
const latest = new Map();
|
|
288
|
-
let files;
|
|
289
|
-
try { files = readdirSync(roundBuildDir(cwd, slug)); } catch { return new Set(); }
|
|
290
|
-
// This run's gates only: a prior run's red round over the same slug must not hold this run's dot.
|
|
291
|
-
const runId = readRunId(cwd, slug);
|
|
292
|
-
for (const f of files) {
|
|
293
|
-
const m = f.match(/^r(\d+)-t(\d+)\.json$/);
|
|
294
|
-
if (!m) continue;
|
|
295
|
-
if (runId) {
|
|
296
|
-
try { const g = JSON.parse(readFileSync(join(roundBuildDir(cwd, slug), f), "utf8")); if (g?.run_id && g.run_id !== runId) continue; }
|
|
297
|
-
catch { continue; }
|
|
298
|
-
}
|
|
299
|
-
const round = Number(m[1]), trial = Number(m[2]);
|
|
300
|
-
if (!latest.has(round) || latest.get(round).trial < trial) latest.set(round, { trial, file: f });
|
|
301
|
-
}
|
|
302
|
-
const red = new Set();
|
|
303
|
-
for (const [round, { file }] of latest) {
|
|
304
|
-
try {
|
|
305
|
-
if (JSON.parse(readFileSync(join(roundBuildDir(cwd, slug), file), "utf8")).overall === "red") red.add(round);
|
|
306
|
-
} catch { /* unreadable → not proven red */ }
|
|
307
|
-
}
|
|
308
|
-
return red;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Write the gate artifact immutably (`wx`, next ordinal on collision — the T0 convention).
|
|
313
|
-
* @param {string} cwd - Project root.
|
|
314
|
-
* @param {string} slug - Feature slug.
|
|
315
|
-
* @param {number} round - Round number.
|
|
316
|
-
* @param {object} body - Artifact fields.
|
|
317
|
-
* @returns {{path:string, sha256:string, trial:number}} Where it landed and its digest.
|
|
318
|
-
*/
|
|
319
|
-
export function writeRoundBuild(cwd, slug, round, body) {
|
|
320
|
-
const dir = roundBuildDir(cwd, slug);
|
|
321
|
-
mkdirSync(dir, { recursive: true });
|
|
322
|
-
const existing = roundBuildArtifacts(cwd, slug, round);
|
|
323
|
-
for (let trial = (existing.length ? existing[existing.length - 1].trial : 0) + 1; ; trial++) {
|
|
324
|
-
const path = join(dir, `r${round}-t${trial}.json`);
|
|
325
|
-
const text = JSON.stringify({ schema_version: 1, round, trial, at: new Date().toISOString(), ...body }, null, 2);
|
|
326
|
-
try {
|
|
327
|
-
writeFileSync(path, text, { flag: "wx" });
|
|
328
|
-
return { path, sha256: createHash("sha256").update(text).digest("hex"), trial };
|
|
329
|
-
} catch (e) { if (e.code !== "EEXIST") throw e; }
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
export const ARGV_SPEC = {
|
|
334
|
-
usage: "harness.mjs verify build --slug <slug> (--round <N> | --preflight) [--cwd <dir>]",
|
|
335
|
-
_: { arity: 0, max: 0, name: "(no positional operands)" },
|
|
336
|
-
slug: { type: "str", required: true },
|
|
337
|
-
round: { type: "int", min: 1 },
|
|
338
|
-
preflight: { type: "flag" },
|
|
339
|
-
cwd: { type: "path" },
|
|
340
|
-
};
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Classify a preflight: the declared steps run, nothing is written.
|
|
344
|
-
*
|
|
345
|
-
* WHY A PREFLIGHT, AND WHY IT WRITES NOTHING. Every environment fault a run has died of was
|
|
346
|
-
* discoverable in seconds and discovered after most of an hour: a checkout missing its package
|
|
347
|
-
* install or its local SDK pointer fails the build in about a second, a probe outside the session's
|
|
348
|
-
* grant is refused, a device that is not attached leaves every `[ui]` row ungraded. Each surfaced
|
|
349
|
-
* only at the first round gate, after planning had been paid for. The same commands, run once before
|
|
350
|
-
* anything is dispatched, turn that into one report up front. It is not a round, so it writes no
|
|
351
|
-
* round artifact: a red preflight read back as round 0's gate would reach round 1's orders as bugs,
|
|
352
|
-
* handing a feature worker an environment fault it cannot fix.
|
|
353
|
-
*
|
|
354
|
-
* A step that exits 2 is a probe that COULD NOT RUN — the fixture convention, e.g. no device
|
|
355
|
-
* attached — which is a gap in the environment rather than a red build, and is reported apart.
|
|
356
|
-
*
|
|
357
|
-
* @param {{overall:string, steps:Array<{kind:string, exit?:number, pass?:boolean, skipped?:boolean}>}} gate
|
|
358
|
-
* What {@link runGate} returned.
|
|
359
|
-
* @returns {{status:("green"|"red"|"cannot-run"), failed_step:(string|null)}} `cannot-run` when the
|
|
360
|
-
* first failing step exited 2.
|
|
361
|
-
*/
|
|
362
|
-
export function classifyPreflight(gate) {
|
|
363
|
-
const failing = gate.steps.find((s) => !s.skipped && !s.pass);
|
|
364
|
-
if (!failing) return { status: "green", failed_step: null };
|
|
365
|
-
return { status: failing.exit === 2 ? "cannot-run" : "red", failed_step: failing.kind };
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* Run the round build gate and write its artifact.
|
|
370
|
-
*
|
|
371
|
-
* @param {string[]} rawArgv - The subcommand's own arguments (harness.mjs strips the verb words).
|
|
372
|
-
* @returns {Promise<void>} Exits 0 green, 1 red, 3 nothing declared, 2 bad argv.
|
|
373
|
-
*/
|
|
374
|
-
export async function cli(rawArgv) {
|
|
375
|
-
const args = runArgs(ARGV_SPEC, rawArgv);
|
|
376
|
-
const cwd = resolve(args.cwd || process.cwd());
|
|
377
|
-
const { slug, round } = args;
|
|
378
|
-
if (Boolean(args.preflight) === (round != null)) {
|
|
379
|
-
console.error("verify build: pass exactly one of --round <N> (the round gate, writes its artifact) or --preflight (runs the same steps, writes nothing)");
|
|
380
|
-
process.exit(2);
|
|
381
|
-
}
|
|
382
|
-
const { archetype, steps, warnings } = declaredSteps(cwd, slug);
|
|
383
|
-
for (const w of warnings) console.error(`build-gate: ${w}`);
|
|
384
|
-
|
|
385
|
-
if (args.preflight) {
|
|
386
|
-
if (steps.length === 0) {
|
|
387
|
-
console.log(JSON.stringify({ preflight: true, status: "undeclared", steps: [], warnings }, null, 2));
|
|
388
|
-
process.exit(3);
|
|
389
|
-
}
|
|
390
|
-
const gate = runGate(steps, cwd);
|
|
391
|
-
const { status, failed_step } = classifyPreflight(gate);
|
|
392
|
-
const failing = gate.steps.find((s) => s.kind === failed_step);
|
|
393
|
-
console.log(JSON.stringify({
|
|
394
|
-
preflight: true, status, archetype, warnings,
|
|
395
|
-
steps: gate.steps.map((s) => (s.skipped ? { kind: s.kind, skipped: true } : { kind: s.kind, exit: s.exit, pass: s.pass })),
|
|
396
|
-
...(failing ? { failed_step, stderr_tail: `${failing.stdout_tail || ""}\n${failing.stderr_tail || ""}`.trim().slice(-1200) } : {}),
|
|
397
|
-
}, null, 2));
|
|
398
|
-
process.exit(status === "green" ? 0 : status === "cannot-run" ? 4 : 1);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
if (steps.length === 0) {
|
|
402
|
-
console.log(JSON.stringify({ round, overall: "skipped", steps: [], warnings,
|
|
403
|
-
reason: "nothing declared — no run_cmd in the run ledger, no build_probe or launch_probe in project-profile.md" }, null, 2));
|
|
404
|
-
process.exit(3);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const gate = runGate(steps, cwd);
|
|
408
|
-
const failing = gate.steps.filter((s) => !s.skipped && !s.pass);
|
|
409
|
-
const discovered = failing.flatMap((s) => digest(`${s.stdout_tail}\n${s.stderr_tail}`));
|
|
410
|
-
const runId = runIdFromRoot(localRoot(cwd, slug));
|
|
411
|
-
const { path, sha256, trial } = writeRoundBuild(cwd, slug, round, {
|
|
412
|
-
...(runId ? { run_id: runId } : {}),
|
|
413
|
-
archetype,
|
|
414
|
-
overall: gate.overall,
|
|
415
|
-
steps: gate.steps,
|
|
416
|
-
warnings,
|
|
417
|
-
discovered_tasks: discovered.slice(0, 16),
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
console.log(JSON.stringify({
|
|
421
|
-
path, sha256, trial, round, overall: gate.overall, warnings,
|
|
422
|
-
steps: gate.steps.map((s) => (s.skipped ? { kind: s.kind, skipped: true } : { kind: s.kind, exit: s.exit, pass: s.pass })),
|
|
423
|
-
...(failing.length ? { failed_step: failing[0].kind, stderr_tail: failing[0].stderr_tail.slice(-1200) } : {}),
|
|
424
|
-
}, null, 2));
|
|
425
|
-
process.exit(gate.overall === "green" ? 0 : 1);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
if (isMain(import.meta.url)) cli(process.argv.slice(2));
|