shapeup-sdlc 3.6.0 → 3.7.1
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 +6 -4
- package/README.md +11 -4
- package/SECURITY.md +3 -2
- package/hooks/hooks.json +10 -0
- package/hooks/tier-guard.mjs +162 -0
- package/kernel/compile.mjs +52 -2
- package/kernel/gate.mjs +67 -2
- package/kernel/harness.mjs +8 -2
- package/kernel/probe/attempts.mjs +152 -0
- package/kernel/probe/resume.mjs +183 -21
- package/kernel/reduce/ship.mjs +39 -3
- package/kernel/schemas/domain.schema.json +15 -2
- package/kernel/verify/spec.mjs +52 -20
- package/kernel/verify/t0.mjs +67 -2
- package/package.json +1 -1
- package/skills/ba-pitch-analyzer/SKILL.md +1 -1
- package/skills/ba-pitch-analyzer/references/doc-schemas.md +10 -0
- package/skills/scope-hammer/SKILL.md +10 -2
- package/skills/tech-lead/references/tiny-lane.md +2 -1
- package/skills/tech-lead/workflows/shapeup-run.js +38 -10
package/kernel/verify/spec.mjs
CHANGED
|
@@ -235,11 +235,56 @@ export function lintScopes(scopes, repoFiles) {
|
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
/** Text forms worth scanning; anything else in a committed tree is not a reference carrier. */
|
|
238
|
-
const SCANNED = /\.(md|markdown|yml|yaml|json|txt)$/i;
|
|
238
|
+
export const SCANNED = /\.(md|markdown|yml|yaml|json|txt)$/i;
|
|
239
239
|
|
|
240
240
|
/** A machine-local board id. Strict on purpose: a committed tree has no reason to carry one at all. */
|
|
241
241
|
const TASK_ID = /\bTASK-[A-Za-z0-9][\w.-]*/;
|
|
242
242
|
|
|
243
|
+
/**
|
|
244
|
+
* The tier-direction violations one piece of text carries — the whole rule, on a string.
|
|
245
|
+
*
|
|
246
|
+
* EXPORTED BECAUSE IT HAS TWO ENFORCEMENT POINTS NOW, and they must not be allowed to drift.
|
|
247
|
+
* `lintCommittedTier` below walks files at GATE L1b; `hooks/tier-guard.mjs` refuses the same text
|
|
248
|
+
* at the moment a tool writes it, minutes earlier, while the writer still holds the context needed
|
|
249
|
+
* to rephrase. Four producers wrote committed files this rule reds and none of them learned from
|
|
250
|
+
* the lint, because by the time it speaks the dispatch that wrote the line is over. A second
|
|
251
|
+
* enforcement point is only worth having if it enforces the SAME predicate, so both call this.
|
|
252
|
+
*
|
|
253
|
+
* The detail strings are the message the writer reads, so they carry the remedy, not just the
|
|
254
|
+
* verdict: a board id resolves on the machine that wrote it and nowhere else, and a path into the
|
|
255
|
+
* gitignored tier dangles on every clone.
|
|
256
|
+
*
|
|
257
|
+
* @param {string} text - The file body, or the fragment a tool is about to write.
|
|
258
|
+
* @returns {Array<{kind:("board-id"|"local-path"), token:string, line:number, detail:string}>}
|
|
259
|
+
* One entry per offending line and form, in file order; [] when clean.
|
|
260
|
+
*/
|
|
261
|
+
export function tierLeaks(text) {
|
|
262
|
+
// Built from the LOCAL constant, never a literal — the storage roots have exactly one home.
|
|
263
|
+
const esc = LOCAL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
264
|
+
// `\S+` where the walk's own rule is `\S`: the same LINES match either way (a path with one
|
|
265
|
+
// non-space character after the slash has at least one), and the longer form yields the token to
|
|
266
|
+
// quote back at the writer. Trailing punctuation the prose wrapped it in is trimmed off the
|
|
267
|
+
// quote only — never off the test.
|
|
268
|
+
const localPath = new RegExp(`${esc}/\\S+`);
|
|
269
|
+
const leaks = [];
|
|
270
|
+
String(text ?? "").split(/\r?\n/).forEach((line, i) => {
|
|
271
|
+
const task = line.match(TASK_ID);
|
|
272
|
+
if (task) {
|
|
273
|
+
leaks.push({ kind: "board-id", token: task[0], line: i + 1, detail:
|
|
274
|
+
`names ${task[0]} — a committed file cannot carry a board id. Boards live in ${LOCAL}/ ` +
|
|
275
|
+
"(gitignored) and renumber on every regeneration, so this resolves on the machine that wrote it " +
|
|
276
|
+
"and nowhere else. Cite the use case or the scope_id, which are stable." });
|
|
277
|
+
}
|
|
278
|
+
const path = line.match(localPath);
|
|
279
|
+
if (path) {
|
|
280
|
+
leaks.push({ kind: "local-path", token: path[0].replace(/[`)\]},.;:'"]+$/, ""), line: i + 1, detail:
|
|
281
|
+
`points into ${LOCAL}/ — a committed file cannot reference the gitignored tier; the path ` +
|
|
282
|
+
"dangles on every other clone. Name the committed artifact, or describe the tier without a path." });
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
return leaks;
|
|
286
|
+
}
|
|
287
|
+
|
|
243
288
|
/**
|
|
244
289
|
* Lint the WHOLE committed tree for references into the gitignored tier.
|
|
245
290
|
*
|
|
@@ -265,28 +310,15 @@ const TASK_ID = /\bTASK-[A-Za-z0-9][\w.-]*/;
|
|
|
265
310
|
export function lintCommittedTier({ cwd, slug }) {
|
|
266
311
|
const root = sharedRoot(cwd, slug);
|
|
267
312
|
if (!existsSync(root)) return [];
|
|
268
|
-
// Built from the LOCAL constant, never a literal — the storage roots have exactly one home.
|
|
269
|
-
const localPath = new RegExp(`${LOCAL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/\\S`);
|
|
270
313
|
const findings = [];
|
|
271
314
|
for (const rel of walkFiles(root)) {
|
|
272
315
|
if (!SCANNED.test(rel)) continue;
|
|
273
|
-
let
|
|
274
|
-
try {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
findings.push({ rule: "TIER-DIRECTION", level: "red", detail:
|
|
280
|
-
`${at} names ${task[0]} — a committed file cannot carry a board id. Boards live in ${LOCAL}/ ` +
|
|
281
|
-
"(gitignored) and renumber on every regeneration, so this resolves on the machine that wrote it " +
|
|
282
|
-
"and nowhere else. Cite the use case or the scope_id, which are stable." });
|
|
283
|
-
}
|
|
284
|
-
if (localPath.test(line)) {
|
|
285
|
-
findings.push({ rule: "TIER-DIRECTION", level: "red", detail:
|
|
286
|
-
`${at} points into ${LOCAL}/ — a committed file cannot reference the gitignored tier; the path ` +
|
|
287
|
-
"dangles on every other clone. Name the committed artifact, or describe the tier without a path." });
|
|
288
|
-
}
|
|
289
|
-
});
|
|
316
|
+
let text;
|
|
317
|
+
try { text = readFileSync(join(root, rel), "utf8"); } catch { continue; }
|
|
318
|
+
for (const leak of tierLeaks(text)) {
|
|
319
|
+
findings.push({ rule: "TIER-DIRECTION", level: "red",
|
|
320
|
+
detail: `${relative(cwd, join(root, rel))}:${leak.line} ${leak.detail}` });
|
|
321
|
+
}
|
|
290
322
|
}
|
|
291
323
|
return findings;
|
|
292
324
|
}
|
package/kernel/verify/t0.mjs
CHANGED
|
@@ -79,6 +79,69 @@ function runCommand(cmd, cwd) {
|
|
|
79
79
|
return { cmd, exit: r.status ?? 1, pass: r.status === 0, stdout, stderr, ...(error ? { error } : {}) };
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* How much of each stream the persisted record keeps, per command.
|
|
84
|
+
*
|
|
85
|
+
* A bound rather than the whole stream, because one chatty fixture would otherwise make every
|
|
86
|
+
* reader of the run trace pay for it — and a bound at the END rather than the start, because that
|
|
87
|
+
* is where a stack trace, an assertion diff and a test summary all land.
|
|
88
|
+
*/
|
|
89
|
+
export const EVIDENCE_TAIL_CHARS = 4000;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The last `limit` characters of a stream, marked when anything was dropped.
|
|
93
|
+
*
|
|
94
|
+
* THE MARKER IS NOT DECORATION. A truncated tail that does not say so is a partial stream that
|
|
95
|
+
* reads as a complete one, which is the same class of defect as the one this whole change fixes:
|
|
96
|
+
* a record that overstates what it actually holds.
|
|
97
|
+
*
|
|
98
|
+
* @param {(string|null|undefined)} text - The captured stream.
|
|
99
|
+
* @param {number} [limit] - Characters to keep.
|
|
100
|
+
* @returns {(string|null)} The kept tail, or null when the stream was empty (the field is then
|
|
101
|
+
* omitted rather than stored as "", so "nothing was printed" stays visibly different from
|
|
102
|
+
* "nothing was kept").
|
|
103
|
+
*/
|
|
104
|
+
export function boundedTail(text, limit = EVIDENCE_TAIL_CHARS) {
|
|
105
|
+
const s = String(text ?? "");
|
|
106
|
+
if (!s) return null;
|
|
107
|
+
if (s.length <= limit) return s;
|
|
108
|
+
return `[truncated: kept the last ${limit} of ${s.length} characters]\n${s.slice(-limit)}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* One command's outcome as the verdict artifact stores it — the evidence, not just the score.
|
|
113
|
+
*
|
|
114
|
+
* WHAT THIS FIXES. The artifact used to keep `{cmd, exit, pass}`, and `runCommand` maps a command
|
|
115
|
+
* that never started onto `exit: 1` — the same number a genuine failure returns. So a refused or
|
|
116
|
+
* timed-out command and a broken build were the SAME RECORD everywhere downstream: the digest, the
|
|
117
|
+
* hill, the report, the evaluator's citation and anyone reading the trace back afterwards. The
|
|
118
|
+
* kernel computed the difference (`error`, which the ratchet grades as `crash`) and discarded it
|
|
119
|
+
* one line later.
|
|
120
|
+
*
|
|
121
|
+
* `exit` is deliberately left as it is. Mapping a crash to some other number would change what the
|
|
122
|
+
* ratchet compares and what every existing reader parses; the distinction travels in `error`, which
|
|
123
|
+
* is the field that actually means "this never ran", and which the crash branch already reads.
|
|
124
|
+
*
|
|
125
|
+
* Output is kept for PASSING commands too, and that direction is not an afterthought: a fixture
|
|
126
|
+
* that exits 0 having run zero tests is the false green this evidence layer exists to catch, and
|
|
127
|
+
* its stdout is the only place that shows.
|
|
128
|
+
*
|
|
129
|
+
* @param {({cmd:string, exit:number, pass:boolean, stdout?:string, stderr?:string, error?:string}|null)} r
|
|
130
|
+
* A `runCommand` result, or null when no command was declared.
|
|
131
|
+
* @returns {(object|null)} The record to persist; null passes through unchanged.
|
|
132
|
+
*/
|
|
133
|
+
export function commandEvidence(r) {
|
|
134
|
+
if (!r) return null;
|
|
135
|
+
const stdout = boundedTail(r.stdout);
|
|
136
|
+
const stderr = boundedTail(r.stderr);
|
|
137
|
+
return {
|
|
138
|
+
cmd: r.cmd, exit: r.exit, pass: r.pass,
|
|
139
|
+
...(r.error ? { error: r.error } : {}),
|
|
140
|
+
...(stdout ? { stdout_tail: stdout } : {}),
|
|
141
|
+
...(stderr ? { stderr_tail: stderr } : {}),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
82
145
|
/**
|
|
83
146
|
* Run every e2e fixture command for a scope.
|
|
84
147
|
* @param {string[]} fixtures - Fixture command lines (null/empty → no commands).
|
|
@@ -513,8 +576,10 @@ export async function cli(rawArgv) {
|
|
|
513
576
|
const { path, sha256: hash, trial } = writeArtifact(outDir, round, attempt, {
|
|
514
577
|
...(runId ? { run_id: runId } : {}),
|
|
515
578
|
scope_id: contract.scope_id,
|
|
516
|
-
|
|
517
|
-
|
|
579
|
+
// The evidence, not just the score — see `commandEvidence` for what the three-field record
|
|
580
|
+
// could not tell apart, and why `exit` still reads the way it always did.
|
|
581
|
+
fixtures: fixtures.results.map((r) => commandEvidence(r)),
|
|
582
|
+
db_probe: commandEvidence(dbProbe),
|
|
518
583
|
seesaw,
|
|
519
584
|
...verdict,
|
|
520
585
|
score: s,
|
package/package.json
CHANGED
|
@@ -121,7 +121,7 @@ covered AC is a requirement the run can be measured against.
|
|
|
121
121
|
|---|---|---|
|
|
122
122
|
| `reconcile` | Verify `ledger.feature == payload.feature` (mismatch → STOP). Map each `[+]` Keep item → its owning UC; new task continues numbering (never renumber); `~`/Cut → synthesis "Hammered Out" row, no file. A Keep item asserting a new invariant → APPEND `[INV-NN]` + TS-INV row to that UC (append-only sections in your substrate). A new actor/action with no UC → `status: "escalated"` + a `deviations[]` spec-ambiguity entry: spawning a UC mid-cycle is silent re-shaping, the PO decides. Finish with board-derive (appetite overflow → report) + spec-lint | re-run phases 1–5; edit UC Steps; resolve the appetite HAMMER yourself |
|
|
123
123
|
| `retrofit-surface` | Append `## Test Surface` (derived rows only, after Error Cases) to each UC of a pre-surface spec; an all-sources-empty UC gets the explicit empty-sources line | touch anything else — append-only substrate |
|
|
124
|
-
| `coverage` | Extract **atomic** customer requirement clauses from `payload.requirements` (default: the pitch) and write the SHARED `shapeup/<slug>/requirements.md` registry: one `\| REQ-id \| clause (verbatim) \| source \| status \| note \|` row per clause. Split compound sentences into one testable clause each — a clause lost *inside* a bigger sentence is a requirement nothing can be traced to. **Assign REQ-ids ONCE and freeze them** (they behave like scope_id, never TASK-NNN — every `covers:` link rots otherwise): re-running, append new clauses with fresh ids, mark a removed clause `CUT (PO-approved)`, never renumber or delete. Status starts `covered` (a live requirement); only the PO sets `CUT`. The REQ source itself is frozen — the registry is a separate derived file. **Numbering.** A source clause already carrying an `R<n>` keeps its number — `R12` → `REQ-12` — and its `source` cell records where it came from verbatim (`shaping.md R12`), because that cell is the only thing that survives a re-run. A clause with no R-id takes the next free number ABOVE the highest `R<n>` in the source, so it can never collide with one added later. Splitting a compound clause keeps `REQ-12` for the first atomic part and records `shaping.md R12 (split 2/3)` for the rest — a requirement graded in parts is why splitting matters at all. On a re-run, match an existing id by its frozen `source` cell and clause text, **never** by re-deriving the number from the source's current order | edit the REQ source; renumber existing REQ-ids; delete a dropped clause instead of marking it CUT; invent a requirement not in the source; re-point an existing REQ-id because the source's R-numbers shifted |
|
|
124
|
+
| `coverage` | Extract **atomic** customer requirement clauses from `payload.requirements` (default: the pitch) and write the SHARED `shapeup/<slug>/requirements.md` registry: one `\| REQ-id \| clause (verbatim) \| source \| status \| note \|` row per clause. **The `source` cell, and any other prose in this file, cites the committed pitch or shaping doc — never the gitignored run-tier path** (`.shapeup/<slug>/intake.md`, or any other `.shapeup/` path): spec-lint's `TIER-DIRECTION` rule reds a committed file naming that path in ANY form, a bare path in a sentence exactly as much as a `[[tasks/...]]` wikilink, because it dangles on every other clone. When the intake has no committed original to name, describe the run tier without a path (`"the pitch staged for this run"`) rather than citing where it actually lives. Split compound sentences into one testable clause each — a clause lost *inside* a bigger sentence is a requirement nothing can be traced to. **Assign REQ-ids ONCE and freeze them** (they behave like scope_id, never TASK-NNN — every `covers:` link rots otherwise): re-running, append new clauses with fresh ids, mark a removed clause `CUT (PO-approved)`, never renumber or delete. Status starts `covered` (a live requirement); only the PO sets `CUT`. The REQ source itself is frozen — the registry is a separate derived file. **Numbering.** A source clause already carrying an `R<n>` keeps its number — `R12` → `REQ-12` — and its `source` cell records where it came from verbatim (`shaping.md R12`), because that cell is the only thing that survives a re-run. A clause with no R-id takes the next free number ABOVE the highest `R<n>` in the source, so it can never collide with one added later. Splitting a compound clause keeps `REQ-12` for the first atomic part and records `shaping.md R12 (split 2/3)` for the rest — a requirement graded in parts is why splitting matters at all. On a re-run, match an existing id by its frozen `source` cell and clause text, **never** by re-deriving the number from the source's current order | edit the REQ source; renumber existing REQ-ids; delete a dropped clause instead of marking it CUT; invent a requirement not in the source; re-point an existing REQ-id because the source's R-numbers shifted |
|
|
125
125
|
---
|
|
126
126
|
|
|
127
127
|
## Anti-rationalization table
|
|
@@ -275,6 +275,16 @@ Always use wikilinks (double brackets), never relative paths like `../domain-mod
|
|
|
275
275
|
`.shapeup/` is gitignored, so a committed task link dangles on every fresh clone.
|
|
276
276
|
spec-lint flags it as a red `TIER-DIRECTION` finding. Coverage views (synthesis
|
|
277
277
|
traceability) record derived counts/status, not task ids.
|
|
278
|
+
- **The rule is not only about wikilinks.** `TIER-DIRECTION` reds *any* line in a SHARED
|
|
279
|
+
doc that names a `.shapeup/` path — a bare path cited in a sentence or a table cell,
|
|
280
|
+
not only a `[[tasks/...]]` link. A provenance sentence that names its real source
|
|
281
|
+
(`"extracted from .shapeup/<slug>/intake.md"`) reds for the same reason a task
|
|
282
|
+
wikilink does: the path dangles on every other clone. Cite the committed pitch or
|
|
283
|
+
shaping doc instead, or describe the run tier without a path.
|
|
284
|
+
- **You will be stopped at the write, not at the gate.** The same rule is enforced as a
|
|
285
|
+
refusal on the Write/Edit itself, quoting the offending token back. There is nothing to
|
|
286
|
+
appeal: rephrase the line and write it again. Waiting for spec-lint to tell you at Board
|
|
287
|
+
Review costs the whole phase, which is what it used to cost every time.
|
|
278
288
|
- `[[tasks/...]]` wikilinks are valid only inside LOCAL documents (task files, the board,
|
|
279
289
|
EVAL reports), where they resolve against the LOCAL root (`.shapeup/<slug>/`);
|
|
280
290
|
every wikilink in a SHARED doc stays `spec_folder`-relative.
|
|
@@ -67,8 +67,16 @@ H0.0 Ownership is DERIVED, never stated. Before the census says "no scope owns
|
|
|
67
67
|
H0.1 Unresolved scopes (breaker cases only):
|
|
68
68
|
- uphill/downhill scopes when round_budget hit 0 → CARRY candidates (their own hill
|
|
69
69
|
phase + open unknowns, from hill/<scope-id>.yml)
|
|
70
|
-
- scopes with hammer_proposals (attempt_budget exhausted) → CARRY candidates
|
|
71
|
-
|
|
70
|
+
- scopes with hammer_proposals (attempt_budget exhausted) → CARRY candidates. Exhaustion
|
|
71
|
+
is DERIVED, never read off `t0/verdicts/*.json` directly — a compiled order or a T0
|
|
72
|
+
verdict is writable by the very scope being judged and proves nothing on its own. Run
|
|
73
|
+
node "${CLAUDE_PLUGIN_ROOT}/kernel/harness.mjs" probe attempts --slug <slug> \
|
|
74
|
+
--scope <scope-id> --round <n> --attempt-budget <n>
|
|
75
|
+
and cite its `spent`/`tripped` fields (exit 1 = tripped) — an attempt counts only when a
|
|
76
|
+
dispatch receipt AND either a leg-completion row or a WorkResult attest it, so a leg still
|
|
77
|
+
in flight holds it open rather than reading as exhausted. This is the SAME derivation the
|
|
78
|
+
round loop's own inner breaker reads, so the census and the breaker cannot disagree about
|
|
79
|
+
the same exhaustion the way a live run once measured.
|
|
72
80
|
H0.2 QA findings (qa-edge-hunter's hunt-report.md, when present) — all `~` by default.
|
|
73
81
|
H0.3 Discovered-task ledger entries still open (discovery/ledger.md, `[+]`/`~` unresolved).
|
|
74
82
|
H0.4 Attempt-budget hammer proposals (scopes that exhausted their T0 attempts during BUILD).
|
|
@@ -26,7 +26,8 @@ scales down, its *verification floor* does not.
|
|
|
26
26
|
ingest-result dispatch. Tiny never means "just edit the file inline".
|
|
27
27
|
- **T0 verification.** A tiny change still proves itself by running — never by claim. If there
|
|
28
28
|
is no runnable check at all, that is a fit-check failure, not a reason to skip T0.
|
|
29
|
-
- **The safety
|
|
29
|
+
- **The machine guards — safety spine, substrate sandbox, tier guard.** They do not scale down,
|
|
30
|
+
and a tiny lane is where a committed file is most likely to be written by hand.
|
|
30
31
|
- **The discovery ledger.** `lane: tiny` is recorded, so a later reader knows exactly what was
|
|
31
32
|
NOT checked (no EVAL verdict, no QA charter, no wiring assertion).
|
|
32
33
|
|
|
@@ -391,6 +391,9 @@ const CMD = {
|
|
|
391
391
|
// from `detail` on purpose: `detail` is prose for a human to read, this is a token the control
|
|
392
392
|
// plane branches on, and collapsing the two is what made every gate comparison silently false.
|
|
393
393
|
decision: { type: "string" },
|
|
394
|
+
// A close's own advisory failure (kernel/probe/resume.mjs's `exportOnClose`) — copied verbatim, the same discipline as `decision`, so "the export failed" is a
|
|
395
|
+
// fact `closeIfTerminal` can act on rather than a line buried inside free-text `detail`.
|
|
396
|
+
export_warning: { type: "string" },
|
|
394
397
|
},
|
|
395
398
|
required: ["exit_code", "ok"],
|
|
396
399
|
};
|
|
@@ -617,7 +620,9 @@ async function cmd(verbs, phaseName, label) {
|
|
|
617
620
|
`Report its exit code as exit_code, ok=true if and only if exit_code is 0, and one line of ` +
|
|
618
621
|
`detail. If the command printed JSON carrying a top-level "decision" key, copy that value into ` +
|
|
619
622
|
`decision EXACTLY as it appears — one bare token, no sentence, no quotes, no rephrasing. ` +
|
|
620
|
-
`Otherwise omit decision.
|
|
623
|
+
`Otherwise omit decision. If the command printed JSON carrying a top-level "export_warning" ` +
|
|
624
|
+
`key with a non-empty string value, copy that string into export_warning verbatim. Otherwise ` +
|
|
625
|
+
`omit export_warning. Do not interpret, summarise or act on the command's output beyond that.\n\n` +
|
|
621
626
|
`If the tool call itself is refused or blocked before the command ever runs — a permission or ` +
|
|
622
627
|
`policy denial, not the command's own exit — that is NOT an exit code, and you must never invent ` +
|
|
623
628
|
`one to fill the field: report exit_code as -1 and put the denial's own wording verbatim in ` +
|
|
@@ -968,11 +973,18 @@ async function setRunStatus(status, phaseName) {
|
|
|
968
973
|
const causeArg = (s) => (String(s ?? "").replace(/[`"'$\\\n\r]/g, " ").replace(/\s+/g, " ").trim().slice(0, 300) || "no reason recorded");
|
|
969
974
|
|
|
970
975
|
// A terminal RunReturn closes the run's own ledger — a terminal status, its cause, and a close
|
|
971
|
-
// timestamp, in one write
|
|
972
|
-
//
|
|
973
|
-
//
|
|
974
|
-
//
|
|
975
|
-
//
|
|
976
|
+
// timestamp, in one write. WHICH arms are terminal, and what status each closes as, is no longer
|
|
977
|
+
// decided here: `probe resume --close-arm <ret.status>` hands the kernel the RunReturn arm itself
|
|
978
|
+
// and it answers from `RUN_RETURN_CLOSE` (kernel/probe/resume.mjs), derived from the schema's own
|
|
979
|
+
// enum rather than a pair of statuses this file used to compare `ret.status` against by hand — a
|
|
980
|
+
// literal comparison that could not see a new arm go unhandled, because it never consulted the
|
|
981
|
+
// kernel's own list of terminal statuses at all. `gate_h` now closes the run as `escalated` — the
|
|
982
|
+
// breaker that tripped travels in the close's cause text, never as a status of its own; the
|
|
983
|
+
// tech-lead skill's own GATE H → L4 orchestration still runs the census and the ship decision, it
|
|
984
|
+
// just no longer finds the ledger's close fields unset when it gets there. `paused` and `ok` remain
|
|
985
|
+
// explicitly non-terminal — the kernel says so, this call site no longer needs to know why.
|
|
986
|
+
// Best-effort, the same discipline as `setRunStatus` above: a lost write degrades the trace's own
|
|
987
|
+
// record of why the run ended, it does not change what this return reports.
|
|
976
988
|
//
|
|
977
989
|
// A close can also come back `ok:true` and still be a degraded outcome: `closeRun`
|
|
978
990
|
// (kernel/probe/resume.mjs) refuses a DIFFERENT terminal status outright (that failure already hits
|
|
@@ -986,11 +998,15 @@ const causeArg = (s) => (String(s ?? "").replace(/[`"'$\\\n\r]/g, " ").replace(/
|
|
|
986
998
|
// reports — the ledger already folded the prior cause in — but the run's own RunReturn must say the
|
|
987
999
|
// trace is degraded.
|
|
988
1000
|
async function closeIfTerminal(ret) {
|
|
989
|
-
if (ret.status !== "aborted" && ret.status !== "shipped") return;
|
|
990
1001
|
const cause = ret.status === "aborted"
|
|
991
1002
|
? `${ret.aborted_at || "?"}: ${ret.reason || "no reason recorded"}`
|
|
1003
|
+
: ret.status === "gate_h"
|
|
1004
|
+
? `breaker=${ret.breaker ?? "?"} green_scopes=${Array.isArray(ret.green_scopes) ? ret.green_scopes.length : "?"} hammer_proposals=${Array.isArray(ret.hammer_proposals) ? ret.hammer_proposals.length : "?"}`
|
|
992
1005
|
: `verdict=${ret.verdict ?? "?"} rounds=${ret.rounds_used ?? "?"} qa_findings=${ret.qa_findings ?? "?"}`;
|
|
993
|
-
|
|
1006
|
+
// `--close-arm` hands the kernel the arm itself (not a status this file decided was terminal) —
|
|
1007
|
+
// a non-terminal arm (`paused`, `ok`) still exits 0 with no "decision" key, so the branches below
|
|
1008
|
+
// stay silent for it exactly as they did when this file's own guard returned early.
|
|
1009
|
+
const r = await cmd(`probe resume --slug ${slug} --close-arm ${ret.status} --cause "${causeArg(cause)}"`, "Ship", `close:${ret.status}`);
|
|
994
1010
|
if (!r.ok) {
|
|
995
1011
|
const why = (r.detail || `exit ${r.exit_code}`).trim();
|
|
996
1012
|
log(`RUN STATE — close(${ret.status}) did not take: ${why}. This return's own status and reason still ` +
|
|
@@ -1004,6 +1020,14 @@ async function closeIfTerminal(ret) {
|
|
|
1004
1020
|
`own close_cause line; this return's trace is degraded, not corrupted.`);
|
|
1005
1021
|
stateWarnings.push(`close(${ret.status}) superseded an earlier close of this run_id — see harness-run.md's close_cause for both reasons`);
|
|
1006
1022
|
}
|
|
1023
|
+
// The close itself took (r.ok above) — an export_warning here is `exportOnClose`
|
|
1024
|
+
// (kernel/probe/resume.mjs) reporting it could not project this run's
|
|
1025
|
+
// fact tables. Advisory, same as every other line in this function: the close stands, the run's
|
|
1026
|
+
// own return says the trace is short one export rather than swallowing the fact.
|
|
1027
|
+
if (r.export_warning) {
|
|
1028
|
+
log(`RUN STATE — close(${ret.status}) took, but its export did not: ${r.export_warning}`);
|
|
1029
|
+
stateWarnings.push(`close(${ret.status}): ${r.export_warning}`);
|
|
1030
|
+
}
|
|
1007
1031
|
}
|
|
1008
1032
|
const withWarnings = async (ret) => {
|
|
1009
1033
|
await closeIfTerminal(ret);
|
|
@@ -1739,11 +1763,15 @@ async function buildScope(scope, roundNo) {
|
|
|
1739
1763
|
`and keep T0 green. An entry marked \`unowned\` cites no file any scope owns — fix it only ` +
|
|
1740
1764
|
`if it falls inside your substrate. `
|
|
1741
1765
|
: "") +
|
|
1742
|
-
`Re-compile the order for every attempt after the first, with --attempt <n>. ` +
|
|
1766
|
+
`Re-compile the order for every attempt after the first, with --attempt <n>. An attempt will ` +
|
|
1767
|
+
`be REFUSED (exit 3) while the previous one is unanswered — dispatched with no leg row and no ` +
|
|
1768
|
+
`WorkResult — because grading a tree the previous attempt may still be writing counts an ` +
|
|
1769
|
+
`attempt nobody ran. Let it come back rather than opening the next one. ` +
|
|
1743
1770
|
`Run the attempt ratchet for THIS scope only: up to ${attemptBudget} attempts of implement → ` +
|
|
1744
1771
|
`\`node "${KERNEL}" verify t0 "${scope.path}" --round ${roundNo} --attempt <n>\`, each scored against ` +
|
|
1745
1772
|
`the last kept trial. Stop on the first green T0, or when the attempt budget or the stagnation ` +
|
|
1746
1773
|
`breaker trips. Write only inside this scope's substrate whitelist — the sandbox hook enforces it. ` +
|
|
1747
|
-
`Report green, attempts_used, which breaker (if any) tripped, and the T0 artifact path
|
|
1774
|
+
`Report green, attempts_used, which breaker (if any) tripped, and the T0 artifact path — ` +
|
|
1775
|
+
`attempts_used is what the attested channels carry, not a count of the orders you compiled.`,
|
|
1748
1776
|
});
|
|
1749
1777
|
}
|