shapeup-sdlc 1.6.3 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.example.json +5 -5
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +54 -107
- package/README.md +165 -151
- package/SECURITY.md +49 -27
- package/bin/init.mjs +93 -108
- package/bin/lib/grant.mjs +145 -0
- package/commands/build.md +20 -0
- package/commands/eval.md +5 -4
- package/commands/scopes.md +5 -4
- package/commands/shape.md +1 -1
- package/commands/ship.md +53 -7
- package/commands/wire.md +1 -1
- package/hooks/dispatch-receipt.mjs +195 -0
- package/hooks/gate-intake.mjs +16 -17
- package/hooks/gate-zerowork.mjs +107 -25
- package/hooks/hooks.json +9 -48
- package/hooks/lib/decision.mjs +38 -19
- package/hooks/safety-spine.mjs +4 -4
- package/hooks/sandbox-guard.mjs +130 -50
- package/{skills/tech-lead/scripts/compile-order.mjs → kernel/compile.mjs} +344 -55
- package/{skills/tech-lead/scripts/gate-answers.mjs → kernel/gate.mjs} +69 -26
- package/kernel/harness.mjs +134 -0
- package/{skills/tech-lead/scripts/fit-check.mjs → kernel/init/fit.mjs} +25 -22
- package/kernel/init/run.mjs +489 -0
- package/{skills/tech-lead/scripts → kernel}/lib/argv.mjs +52 -31
- package/{skills/tech-lead/scripts/lib/contract-md.mjs → kernel/lib/contract.mjs} +261 -17
- package/kernel/lib/paths.mjs +491 -0
- package/kernel/probe/concurrency.mjs +510 -0
- package/{skills/tech-lead/scripts/aegis-digest.mjs → kernel/probe/digest.mjs} +10 -11
- package/kernel/probe/eval.mjs +77 -0
- package/kernel/probe/leg.mjs +125 -0
- package/kernel/probe/resume.mjs +528 -0
- package/{skills/tech-lead/scripts → kernel/probe}/stats.mjs +102 -21
- package/kernel/probe/t0.mjs +66 -0
- package/{skills/ba-pitch-analyzer/scripts/board-derive.mjs → kernel/reduce/board.mjs} +104 -28
- package/kernel/reduce/graph.mjs +437 -0
- package/kernel/reduce/hill.mjs +152 -0
- package/kernel/reduce/ingest.mjs +633 -0
- package/{hooks/slop-cleaner.mjs → kernel/reduce/leftovers.mjs} +40 -57
- package/{skills/tech-lead/scripts/ship-report.mjs → kernel/reduce/ship.mjs} +93 -14
- package/{skills/tech-lead/scripts/run-snapshot.mjs → kernel/reduce/snapshot.mjs} +22 -21
- package/{skills/spec-evaluator/scripts/verdict-ledger.mjs → kernel/reduce/verdict.mjs} +14 -8
- package/kernel/report/export.mjs +325 -0
- package/kernel/report/facts.mjs +347 -0
- package/{skills/tech-lead/scripts/budget-check.mjs → kernel/verify/budget.mjs} +22 -25
- package/kernel/verify/dispatch.mjs +114 -0
- package/{skills/tech-lead/scripts/validate-envelope.mjs → kernel/verify/envelope.mjs} +20 -15
- package/{skills/tech-lead/scripts/lib → kernel/verify}/ratchet-tree.mjs +36 -14
- package/kernel/verify/skills.mjs +125 -0
- package/kernel/verify/spec.mjs +559 -0
- package/{skills/tech-lead/scripts/t0-verify.mjs → kernel/verify/t0.mjs} +138 -28
- package/{skills/tech-lead/scripts/trace-lint.mjs → kernel/verify/trace.mjs} +28 -13
- package/oracles/_shared.mjs +1 -1
- package/oracles/http-oracle.mjs +2 -2
- package/oracles/index.mjs +1 -1
- package/oracles/process-oracle.mjs +2 -2
- package/oracles/snapshot-oracle.mjs +2 -2
- package/oracles/test-oracle.mjs +2 -2
- package/package.json +11 -13
- package/skills/ba-pitch-analyzer/SKILL.md +24 -19
- package/skills/ba-pitch-analyzer/assets/templates/_index.tmpl.md +1 -1
- package/skills/ba-pitch-analyzer/assets/templates/api-feasibility.tmpl.md +12 -7
- package/skills/ba-pitch-analyzer/assets/templates/contracts/third-party-api.contract.tmpl.md +6 -3
- package/skills/ba-pitch-analyzer/assets/templates/cross-context/migration-plan.tmpl.md +5 -4
- package/skills/ba-pitch-analyzer/assets/templates/cross-context/team-handoff.tmpl.md +23 -13
- package/skills/ba-pitch-analyzer/assets/templates/scope-summary.tmpl.md +17 -12
- package/skills/ba-pitch-analyzer/assets/templates/synthesis.tmpl.md +17 -38
- package/skills/ba-pitch-analyzer/assets/templates/task.tmpl.md +3 -0
- package/skills/ba-pitch-analyzer/references/contract-patterns.md +8 -4
- package/skills/ba-pitch-analyzer/references/doc-schemas.md +3 -1
- package/skills/ba-pitch-analyzer/references/task-generation.md +8 -8
- package/skills/ba-pitch-analyzer/references/test-surface.md +1 -1
- package/skills/coach/SKILL.md +3 -3
- package/skills/orient/SKILL.md +2 -1
- package/skills/qa-edge-hunter/SKILL.md +15 -41
- package/skills/scope-architect/SKILL.md +57 -18
- package/skills/scope-hammer/SKILL.md +5 -5
- package/skills/shapeup/SKILL.md +3 -3
- package/skills/shapeup/resources/context-compaction.md +4 -3
- package/skills/solution-architect/SKILL.md +37 -15
- package/skills/spec-evaluator/SKILL.md +24 -7
- package/skills/spec-evaluator/references/dimensions/_registry.md +2 -2
- package/skills/spec-evaluator/references/dimensions/completeness.md +1 -1
- package/skills/spec-evaluator/references/dimensions/integration.md +98 -67
- package/skills/spec-evaluator/references/dimensions/spec-conformance.md +4 -4
- package/skills/spec-evaluator/references/dimensions/test-surface-conformance.md +1 -1
- package/skills/spec-evaluator/references/probing.md +42 -23
- package/skills/spec-evaluator/references/report-schema.md +2 -2
- package/skills/task-executor/SKILL.md +32 -18
- package/skills/tech-lead/SKILL.md +127 -438
- package/skills/tech-lead/references/gates.md +140 -49
- package/skills/tech-lead/references/protocol.md +832 -0
- package/skills/tech-lead/schemas/domain.schema.json +645 -198
- package/skills/tech-lead/schemas/gate-answers.schema.json +2 -2
- package/skills/tech-lead/schemas/work-order.schema.json +11 -2
- package/skills/tech-lead/schemas/work-result.schema.json +56 -18
- package/skills/tech-lead/workflows/shapeup-run.js +1474 -0
- package/skills/translator/SKILL.md +2 -2
- package/hooks/anti-rationalization.mjs +0 -244
- package/hooks/compact-snapshot.mjs +0 -47
- package/hooks/gate-deadline.mjs +0 -151
- package/hooks/gate-l2.mjs +0 -161
- package/hooks/session-rehydrate.mjs +0 -109
- package/skills/advisor-protocol/SKILL.md +0 -171
- package/skills/ba-pitch-analyzer/scripts/spec-lint.mjs +0 -239
- package/skills/spec-evaluator/README.md +0 -93
- package/skills/tech-lead/README.md +0 -71
- package/skills/tech-lead/references/delegation.md +0 -254
- package/skills/tech-lead/references/invocation.md +0 -45
- package/skills/tech-lead/references/ledger-schema.md +0 -214
- package/skills/tech-lead/references/round-protocol.md +0 -184
- package/skills/tech-lead/references/state-model.md +0 -66
- package/skills/tech-lead/scripts/ingest-result.mjs +0 -270
- package/skills/tech-lead/scripts/init-run.mjs +0 -326
- package/skills/tech-lead/scripts/lib/is-main.mjs +0 -82
- package/skills/tech-lead/scripts/lib/paths.mjs +0 -278
- package/skills/translator/README.md +0 -66
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// The orchestrator's pipeline sub-layer: assembles the structured input envelope a worker is
|
|
5
5
|
// dispatched with. Replaces tech-lead's hand-assembled `isolated_brief()` prose step and every
|
|
6
6
|
// worker's GATE A/B plumbing (path resolution, run-state parse, board glob-matching, dependency
|
|
7
|
-
// reads, mode detection). Deterministic, zero LLM tokens
|
|
7
|
+
// reads, mode detection). Deterministic, zero LLM tokens.
|
|
8
8
|
//
|
|
9
9
|
// A worker depends on its ORDER, never on filesystem topology — moving a directory again
|
|
10
10
|
// (the v3.2 lesson) touches this script, zero skills.
|
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
//
|
|
14
14
|
// Usage:
|
|
15
15
|
// Scope attempt (isolated attempt loop):
|
|
16
|
-
// node
|
|
16
|
+
// node kernel/harness.mjs compile --scope shapeup/<slug>/scopes/<id>.md \
|
|
17
17
|
// --round N --attempt M [--cwd <dir>] [--test-cmd "<cmd>"]
|
|
18
18
|
// Single task (no scope contracts — pre-v0.3.0 boards):
|
|
19
|
-
// node
|
|
20
|
-
// node
|
|
19
|
+
// node kernel/harness.mjs compile --task TASK-003 --slug <slug> [--worker task-executor]
|
|
20
|
+
// node kernel/harness.mjs compile --next --slug <slug>
|
|
21
21
|
// Non-build operation (planner/judge/QA dispatches; flag surface → operation + whitelist):
|
|
22
|
-
// node
|
|
22
|
+
// node kernel/harness.mjs compile --operation reconcile --slug <slug> [--round N]
|
|
23
23
|
// [--worker ba-pitch-analyzer] [--payload '<json>']
|
|
24
24
|
//
|
|
25
25
|
// Output: .shapeup/<slug>/orders/<order-file>.json (schema-validated before write; a
|
|
@@ -28,20 +28,25 @@
|
|
|
28
28
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from "node:fs";
|
|
29
29
|
import { resolve, join, dirname, basename } from "node:path";
|
|
30
30
|
import { fileURLToPath } from "node:url";
|
|
31
|
-
import { validate } from "./
|
|
32
|
-
import { readTrials } from "./t0
|
|
33
|
-
import { isMain } from "./lib/is-main.mjs";
|
|
31
|
+
import { validate } from "./verify/envelope.mjs";
|
|
32
|
+
import { readTrials } from "./verify/t0.mjs";
|
|
34
33
|
import { runArgs } from "./lib/argv.mjs";
|
|
34
|
+
import { readRunId } from "./lib/paths.mjs";
|
|
35
35
|
// `specDir` is aliased: this module has a local `let specDir` holding the resolved, possibly
|
|
36
36
|
// --spec-overridden directory, and the import is the convention-derived default.
|
|
37
37
|
import {
|
|
38
38
|
tasksDir, specDir as defaultSpecDir, roundLedger, trials, verdictsDir, ordersDir,
|
|
39
|
-
relShared, globLocal, globShared, relKnowledgeBase,
|
|
39
|
+
relShared, globLocal, globShared, relKnowledgeBase, resultsDir, scopesDir,
|
|
40
40
|
} from "./lib/paths.mjs";
|
|
41
|
-
import { readContract, SCOPE_CONTRACT } from "./lib/contract
|
|
41
|
+
import { readContract, tasksForScope, SCOPE_CONTRACT } from "./lib/contract.mjs";
|
|
42
|
+
import { writeActiveOrder } from "./probe/resume.mjs";
|
|
43
|
+
// The SAME matcher the sandbox hook enforces with. "Is this cited file inside this scope's
|
|
44
|
+
// substrate" has to mean exactly what the guard means, or a bug is addressed to a scope that is
|
|
45
|
+
// then denied the write that fixes it.
|
|
46
|
+
import { matchesAny } from "../hooks/sandbox-guard.mjs";
|
|
42
47
|
|
|
43
48
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
44
|
-
const ORDER_SCHEMA = JSON.parse(readFileSync(resolve(HERE, "
|
|
49
|
+
const ORDER_SCHEMA = JSON.parse(readFileSync(resolve(HERE, "./../skills/tech-lead/schemas/work-order.schema.json"), "utf8"));
|
|
45
50
|
|
|
46
51
|
// --- tiny frontmatter reader (scalar keys + [a, b] inline lists) --------------------------
|
|
47
52
|
/**
|
|
@@ -71,7 +76,7 @@ export function frontmatter(md) {
|
|
|
71
76
|
* Parse one TASK-NNN.md file into the task entry a WorkOrder carries.
|
|
72
77
|
* @param {string} path - Absolute path to the task Markdown file.
|
|
73
78
|
* @returns {{id:string, title:string, status:string, priority:number, depends_on:string[],
|
|
74
|
-
* use_case_refs:string[], body_path:string, acceptance_criteria:Array<string|{text:string,
|
|
79
|
+
* use_case_refs:string[], scope_id:string, body_path:string, acceptance_criteria:Array<string|{text:string,
|
|
75
80
|
* covers:string[]}>}} The task entry. Each acceptance criterion is the checkbox text
|
|
76
81
|
* byte-identical (so ingest can tick it back); a trailing `(covers: REQ-…)` clause yields
|
|
77
82
|
* {text, covers} instead of a plain string. `priority` defaults to 999 when unset.
|
|
@@ -98,6 +103,7 @@ export function parseTaskFile(path) {
|
|
|
98
103
|
priority: Number(fm.priority) || 999,
|
|
99
104
|
depends_on: Array.isArray(fm.depends_on) ? fm.depends_on : [],
|
|
100
105
|
use_case_refs: Array.isArray(fm.use_case_refs) ? fm.use_case_refs : [],
|
|
106
|
+
scope_id: fm.scope_id || "",
|
|
101
107
|
body_path: path,
|
|
102
108
|
acceptance_criteria,
|
|
103
109
|
};
|
|
@@ -143,11 +149,30 @@ export function ledgerDecisions(ledgerText, scopeId) {
|
|
|
143
149
|
return decisions;
|
|
144
150
|
}
|
|
145
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Operation → owning worker (mirrors `domain.schema.json#/$defs/Operation` ownership). Lets a
|
|
154
|
+
* non-build dispatch resolve its worker from the operation alone, without a redundant `--worker`.
|
|
155
|
+
*
|
|
156
|
+
* Exported and at module scope so it is ONE table. Every value here must be a member of
|
|
157
|
+
* `WorkerName`: an operation mapped to a name the enum does not carry compiles an order that fails
|
|
158
|
+
* its own schema, and the dispatch is then denied by the order gate for a reason that names the
|
|
159
|
+
* envelope rather than the typo. That parity is checked by the suite, which can only read a table
|
|
160
|
+
* it can import.
|
|
161
|
+
*/
|
|
162
|
+
export const OP_OWNER = {
|
|
163
|
+
analyze: "ba-pitch-analyzer", reconcile: "ba-pitch-analyzer",
|
|
164
|
+
"retrofit-surface": "ba-pitch-analyzer", coverage: "ba-pitch-analyzer",
|
|
165
|
+
"map-scopes": "scope-architect",
|
|
166
|
+
wire: "solution-architect", evaluate: "spec-evaluator", orient: "orient",
|
|
167
|
+
hunt: "qa-edge-hunter", translate: "translator",
|
|
168
|
+
hammer: "scope-hammer", coach: "coach",
|
|
169
|
+
};
|
|
170
|
+
|
|
146
171
|
/**
|
|
147
172
|
* Resolve the write-contract (sandbox substrate) for an operation — one whitelist template per
|
|
148
|
-
* operation, so mode/flag differences are enforced by the sandbox hook, not trusted to prose.
|
|
149
|
-
* @param {string} operation - The order's operation (execute|analyze|
|
|
150
|
-
* retrofit-surface|coverage|map-scopes|
|
|
173
|
+
* operation, so mode/flag differences are enforced by the sandbox hook reading the order's substrate, not trusted to prose.
|
|
174
|
+
* @param {string} operation - The order's operation (execute|fix|spike|analyze|reconcile|
|
|
175
|
+
* retrofit-surface|coverage|map-scopes|wire|evaluate|orient|hunt|translate|hammer|coach).
|
|
151
176
|
* @param {{slug?:string, specDir?:string, scope?:object}} [ctx] - slug (names LOCAL/SHARED roots),
|
|
152
177
|
* specDir (overrides the default spec path), scope (contract supplying allowed/shared substrates).
|
|
153
178
|
* @returns {{allowed:string[], shared?:string[], frozen?:string[], append_only?:string[]}} The
|
|
@@ -171,11 +196,7 @@ export function substrateFor(operation, { slug, specDir, scope } = {}) {
|
|
|
171
196
|
};
|
|
172
197
|
case "analyze":
|
|
173
198
|
return { allowed: [`${spec}/**`, `${local}/**`], frozen: [] };
|
|
174
|
-
|
|
175
|
-
return {
|
|
176
|
-
allowed: [`${local}/tasks/**`, `${spec}/scope-summary.md`, `${working}/**`],
|
|
177
|
-
frozen: [...FROZEN_SPEC_CORE, `${scopesDir}/**`],
|
|
178
|
-
};
|
|
199
|
+
|
|
179
200
|
case "reconcile":
|
|
180
201
|
return {
|
|
181
202
|
allowed: [`${local}/tasks/**`, `${spec}/scope-summary.md`, `${working}/**`],
|
|
@@ -184,14 +205,14 @@ export function substrateFor(operation, { slug, specDir, scope } = {}) {
|
|
|
184
205
|
};
|
|
185
206
|
case "retrofit-surface":
|
|
186
207
|
return { allowed: [], append_only: [`${spec}/usecases/*.md#Test Surface`], frozen: FROZEN_SPEC_CORE };
|
|
208
|
+
|
|
187
209
|
case "coverage":
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
case "map-scopes": case "remap": case "split-scope":
|
|
210
|
+
// The covers-closure input truth. Writes ONLY the derived registry: the REQ source it
|
|
211
|
+
// extracts from is frozen alongside the spec core, because a planner that may edit the
|
|
212
|
+
// requirements it is being measured against is not measuring anything.
|
|
213
|
+
return { allowed: [globShared(slug, "requirements.md")], frozen: FROZEN_SPEC_CORE };
|
|
214
|
+
|
|
215
|
+
case "map-scopes":
|
|
195
216
|
return {
|
|
196
217
|
allowed: [`${scopesDir}/*.md`, globShared(slug, "scope-board.md")],
|
|
197
218
|
frozen: [...FROZEN_SPEC_CORE, `${local}/tasks/**`],
|
|
@@ -205,10 +226,16 @@ export function substrateFor(operation, { slug, specDir, scope } = {}) {
|
|
|
205
226
|
};
|
|
206
227
|
case "evaluate":
|
|
207
228
|
return { allowed: [`${local}/evaluation/**`], frozen: [`${spec}/**`, `${local}/tasks/**`] };
|
|
208
|
-
case "hunt":
|
|
229
|
+
case "hunt":
|
|
209
230
|
return { allowed: [`${local}/qa/**`], frozen: [`${spec}/**`, `${local}/tasks/**`] };
|
|
210
231
|
case "orient":
|
|
211
232
|
return { allowed: [`${local}/orient/**`], frozen: [`${spec}/**`] };
|
|
233
|
+
case "translate":
|
|
234
|
+
return { allowed: [globShared(slug, "shaping/*.md"), globShared(slug, "glossary.md")] };
|
|
235
|
+
case "hammer":
|
|
236
|
+
return { allowed: [globShared(slug, "REPORT.md"), `${local}/reports/**`] };
|
|
237
|
+
case "coach":
|
|
238
|
+
return { allowed: [relKnowledgeBase("*")] };
|
|
212
239
|
default:
|
|
213
240
|
return { allowed: [`${local}/**`] };
|
|
214
241
|
}
|
|
@@ -278,7 +305,7 @@ export function selectTrialHistory(trials, { scopeId, round } = {}) {
|
|
|
278
305
|
}
|
|
279
306
|
|
|
280
307
|
/**
|
|
281
|
-
* The stagnation term of the inner circuit breaker
|
|
308
|
+
* The stagnation term of the inner circuit breaker.
|
|
282
309
|
*
|
|
283
310
|
* `attempt_budget` counts attempts; it cannot see that the last two produced nothing. One term
|
|
284
311
|
* joins it: `no_progress_k` consecutive non-`kept` trials ends the scope early and queues the
|
|
@@ -299,6 +326,162 @@ export function stagnation(trials, k = 2) {
|
|
|
299
326
|
return { stagnant: streak >= k && k > 0, streak, k };
|
|
300
327
|
}
|
|
301
328
|
|
|
329
|
+
// --- the FAIL verdict's bugs, carried into the round that must fix them ----------------------
|
|
330
|
+
//
|
|
331
|
+
// WHY THE KERNEL OWNS THIS AND NOT THE ORCHESTRATOR (measured twice, two different ways).
|
|
332
|
+
//
|
|
333
|
+
// `WorkOrderPayload.bugs` is defined as "the EVAL report's bug entries for this task — touch
|
|
334
|
+
// nothing else", and AGENTS.md states the regression rule as "bugs + full Test Surface of touched
|
|
335
|
+
// UC". Nothing ever populated it. The orchestrator's first fix threaded the evaluator's findings
|
|
336
|
+
// from memory into the build leg's `payload`, and it could not work, for two independent reasons:
|
|
337
|
+
//
|
|
338
|
+
// 1. A BUILD ORDER TAKES NO PAYLOAD FROM THE CALLER. Build legs override the compile line
|
|
339
|
+
// (`compile --scope … --round … --attempt …`) because an order is addressed by scope, round
|
|
340
|
+
// and attempt; only the generic fallback line serialises `--payload`. So the payload object
|
|
341
|
+
// the orchestrator built for a build leg was constructed, filtered, and dropped on the floor.
|
|
342
|
+
// 2. IT LIVED ONLY IN MEMORY. Round r's verdict is what round r+1 must act on, and a run killed
|
|
343
|
+
// after EVAL and relaunched — the normal case here — starts round r+1 in a fresh process with
|
|
344
|
+
// an empty variable. The verdict was on disk the whole time.
|
|
345
|
+
//
|
|
346
|
+
// Both vanish if the evidence is read where the order is written: the same read on a fresh round
|
|
347
|
+
// and a resumed one, no process boundary to survive, and no model in the path — an LLM courier
|
|
348
|
+
// transcribing a repro string is a repro string that no longer reproduces.
|
|
349
|
+
//
|
|
350
|
+
// The shape is also RICHER than the in-memory channel ever was: the ledgered verdict carries
|
|
351
|
+
// severity, file:line, repro, expected and actual per bug, where the structured return carried
|
|
352
|
+
// only `{id, criterion, evidence}`.
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* The previous round's cited defects, if that round returned FAIL.
|
|
356
|
+
*
|
|
357
|
+
* @param {string} cwd - Project root.
|
|
358
|
+
* @param {string} slug - Feature slug.
|
|
359
|
+
* @param {number} [round] - The round being compiled; round 1 has no predecessor.
|
|
360
|
+
* @returns {Array<object>} Bug entries, minus any the adversarial check overturned. Empty for a
|
|
361
|
+
* first round, a PASS, or an absent/unreadable result — a fix round with nothing to fix is a
|
|
362
|
+
* normal build round, never an error.
|
|
363
|
+
*/
|
|
364
|
+
export function verdictBugs(cwd, slug, round) {
|
|
365
|
+
if (!round || round < 2) return [];
|
|
366
|
+
const p = join(resultsDir(cwd, slug), `evaluate-r${round - 1}.json`);
|
|
367
|
+
if (!existsSync(p)) return [];
|
|
368
|
+
let v;
|
|
369
|
+
try { v = JSON.parse(readFileSync(p, "utf8"))?.verdict; } catch { return []; }
|
|
370
|
+
if (v?.overall !== "FAIL" || !Array.isArray(v.bugs)) return [];
|
|
371
|
+
// A refuted acceptance criterion is one the judge withdrew. Re-dispatching it would send a
|
|
372
|
+
// worker to "fix" behaviour that was found correct.
|
|
373
|
+
const refuted = new Set(
|
|
374
|
+
(Array.isArray(v.refuted) ? v.refuted : [])
|
|
375
|
+
.flatMap((r) => [r?.id, r?.ac_id, r?.criterion, typeof r === "string" ? r : null])
|
|
376
|
+
.filter(Boolean).map(String),
|
|
377
|
+
);
|
|
378
|
+
return v.bugs.filter((b) => !refuted.has(String(b?.id)) && !refuted.has(String(b?.criterion)));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Every repo-relative file a bug is cited against.
|
|
383
|
+
*
|
|
384
|
+
* A LOCATOR NAMES MORE THAN ONE SITE, ROUTINELY. The judge writes what it found, and what it finds
|
|
385
|
+
* is often the same defect at several lines, sometimes across files:
|
|
386
|
+
* `"bin/todo.js:53, bin/todo.js:85"`, or `"lib/parse-index.js:5, :13, :21 (rendered by
|
|
387
|
+
* bin/todo.js:66)"`. A parser that accepts only a lone `file:line` returns nothing for those, and
|
|
388
|
+
* "nothing" routes the bug to every scope as unowned — the safe direction, but it throws away an
|
|
389
|
+
* address the judge did supply. On the measured round-1 verdict that was two of five bugs.
|
|
390
|
+
*
|
|
391
|
+
* @param {object} bug - One bug entry.
|
|
392
|
+
* @returns {string[]} Distinct paths, `:line` suffixes and `./` prefixes stripped, in first-seen
|
|
393
|
+
* order. Empty when the entry carries no locator this can read.
|
|
394
|
+
*/
|
|
395
|
+
export function bugLocations(bug) {
|
|
396
|
+
const raw = String(bug?.location ?? bug?.file ?? "").trim();
|
|
397
|
+
// A path-looking token: at least one dot-extension, and no whitespace. The extension is what
|
|
398
|
+
// keeps prose out ("exit 1", "Node 20") without needing to know the project's layout.
|
|
399
|
+
const out = [];
|
|
400
|
+
for (const m of raw.matchAll(/(?:^|[\s(,[])([\w@][\w./-]*\.[A-Za-z][A-Za-z0-9]{0,5})(?::\d+)?/g)) {
|
|
401
|
+
const p = m[1].replace(/^\.\//, "");
|
|
402
|
+
if (!out.includes(p)) out.push(p);
|
|
403
|
+
}
|
|
404
|
+
return out;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Elect the ONE scope that should fix a defect cited against a given file.
|
|
409
|
+
*
|
|
410
|
+
* OWNERSHIP IS BY SUBSTRATE, because that is what the sandbox enforces: a scope is exactly the set
|
|
411
|
+
* of files its worker may write, so a scope whose substrate excludes the cited line cannot fix it
|
|
412
|
+
* however well it understands the bug.
|
|
413
|
+
*
|
|
414
|
+
* BUT A MATCH IS NOT AN ELECTION. An entry point is routinely SHARED — on the measured run
|
|
415
|
+
* `bin/todo.js` sits in five scopes' substrate at once — so "address it to every scope that
|
|
416
|
+
* matches" hands the same one-line fix to five workers building concurrently against one file.
|
|
417
|
+
* That is a write race the harness sets up itself, and four of the five fixes are waste even when
|
|
418
|
+
* it resolves. So: prefer a scope that owns the file EXCLUSIVELY (allowed, not shared), and among
|
|
419
|
+
* equals take the lowest scope id — a rule that needs no coordination to agree with itself, since
|
|
420
|
+
* each leg compiles its own order in its own process.
|
|
421
|
+
*
|
|
422
|
+
* @param {string} path - Repo-relative file the bug cites.
|
|
423
|
+
* @param {Array<{scope_id:string, allowed:string[], shared:string[]}>} scopes - Every scope.
|
|
424
|
+
* @returns {string|null} The elected scope id, or null when no scope may write that file.
|
|
425
|
+
*/
|
|
426
|
+
export function electOwner(path, scopes) {
|
|
427
|
+
const can = (scopes || []).filter((s) => matchesAny(path, s.allowed));
|
|
428
|
+
if (!can.length) return null;
|
|
429
|
+
const exclusive = can.filter((s) => !matchesAny(path, s.shared || []));
|
|
430
|
+
return (exclusive.length ? exclusive : can).map((s) => s.scope_id).sort()[0];
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Address each bug to the scope that must fix it.
|
|
435
|
+
*
|
|
436
|
+
* AN UNOWNED BUG GOES TO EVERYONE, MARKED. A cited defect matching no scope's substrate — or
|
|
437
|
+
* carrying no locator this can read — has no owner, and dropping it silently is precisely how a
|
|
438
|
+
* judged defect survives a fix round to reappear in the next verdict. Better one scope reads a bug
|
|
439
|
+
* it turns out not to own than the run forgets a defect it already paid to find.
|
|
440
|
+
*
|
|
441
|
+
* @param {Array<object>} bugs - The previous verdict's bugs (see {@link verdictBugs}).
|
|
442
|
+
* @param {string|undefined} scopeId - The scope being compiled for.
|
|
443
|
+
* @param {Array<{scope_id:string, allowed:string[], shared:string[]}>} scopes - Every scope.
|
|
444
|
+
* @returns {Array<object>} The subset this order should carry; unowned entries get `unowned: true`.
|
|
445
|
+
*/
|
|
446
|
+
export function bugsForScope(bugs, scopeId, scopes) {
|
|
447
|
+
const out = [];
|
|
448
|
+
for (const b of bugs || []) {
|
|
449
|
+
if (b?.scope_id) {
|
|
450
|
+
if (b.scope_id === scopeId) out.push(b);
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
const owners = bugLocations(b).map((p) => electOwner(p, scopes)).filter(Boolean);
|
|
454
|
+
if (owners.includes(scopeId)) out.push(b);
|
|
455
|
+
else if (!owners.length) out.push({ ...b, unowned: true });
|
|
456
|
+
}
|
|
457
|
+
return out;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Every scope's write substrate, for the election in {@link electOwner}.
|
|
462
|
+
*
|
|
463
|
+
* @param {string} cwd - Project root.
|
|
464
|
+
* @param {string} slug - Feature slug.
|
|
465
|
+
* @returns {Array<{scope_id:string, allowed:string[], shared:string[]}>} One entry per readable
|
|
466
|
+
* contract; `[]` when the scopes directory is absent or unreadable.
|
|
467
|
+
*/
|
|
468
|
+
export function scopeSubstrates(cwd, slug) {
|
|
469
|
+
const dir = scopesDir(cwd, slug);
|
|
470
|
+
let files;
|
|
471
|
+
try { files = readdirSync(dir).filter((f) => f.endsWith(".md") || f.endsWith(".json")); } catch { return []; }
|
|
472
|
+
const out = [];
|
|
473
|
+
for (const f of files) {
|
|
474
|
+
const c = readContract(join(dir, f), SCOPE_CONTRACT)?.contract;
|
|
475
|
+
if (!c?.scope_id || !Array.isArray(c.allowed_file_substrate)) continue;
|
|
476
|
+
out.push({
|
|
477
|
+
scope_id: c.scope_id,
|
|
478
|
+
allowed: c.allowed_file_substrate,
|
|
479
|
+
shared: Array.isArray(c.shared_substrate) ? c.shared_substrate : [],
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
return out;
|
|
483
|
+
}
|
|
484
|
+
|
|
302
485
|
/**
|
|
303
486
|
* Assemble a WorkOrder envelope. Pure given its inputs — the CLI wrapper does the disk reads.
|
|
304
487
|
* @param {object} opts - The order inputs (destructured):
|
|
@@ -313,22 +496,74 @@ export function stagnation(trials, k = 2) {
|
|
|
313
496
|
* @param {Array<{id:string,answer:string}>} [opts.decisions] - This scope's advisor answers.
|
|
314
497
|
* @param {Array<object>} [opts.digestedErrors] - Prior-attempt AEGIS triples (payload.digested_errors).
|
|
315
498
|
* @param {Array<object>} [opts.trialHistory] - Compacted trial rows (payload.trial_history).
|
|
499
|
+
* @param {Array<object>} [opts.bugs] - The previous round's cited defects (payload.bugs).
|
|
316
500
|
* @param {string} [opts.testCmd] - Verify command, recorded under payload.verify.test_cmd.
|
|
317
501
|
* @param {object} [opts.payloadExtra] - Extra payload fields merged last (spec_folder, feature, …).
|
|
318
502
|
* @param {string} [opts.specDir] - Spec directory, threaded into the substrate template.
|
|
319
503
|
* @param {object} [opts.interaction] - Interaction flags (e.g. {pause_gates}).
|
|
320
|
-
* @
|
|
321
|
-
*
|
|
322
|
-
*
|
|
504
|
+
* @param {string} [opts.runId] - The run key (`mintRunId` in `lib/paths.mjs`); omitted when no receipt is readable.
|
|
505
|
+
* @param {string} [opts.compiledAt] - ISO compile time; omitted rather than invented.
|
|
506
|
+
* @returns {object} A WorkOrder: {schema_version, order_id ("<slug>/<suffix>"), run_id?,
|
|
507
|
+
* compiled_at?, worker, mode, operation?, interaction?, substrate (from {@link substrateFor}),
|
|
508
|
+
* payload{…}}. A coachable worker also gets payload.kb_rules_path. Not validated here — the CLI
|
|
509
|
+
* validates before writing.
|
|
323
510
|
*/
|
|
324
511
|
export function compileOrder({
|
|
325
512
|
slug, worker, mode = "orchestrated", operation, round, attempt,
|
|
326
|
-
scope, tasks, decisions, digestedErrors, trialHistory, testCmd, payloadExtra, specDir, interaction,
|
|
513
|
+
scope, tasks, decisions, digestedErrors, trialHistory, bugs, testCmd, payloadExtra, specDir, interaction,
|
|
514
|
+
runId, compiledAt,
|
|
327
515
|
}) {
|
|
328
|
-
|
|
516
|
+
// A build order's id carries its SCOPE. Without it, `r<round>-a<attempt>` is the same name for
|
|
517
|
+
// every scope in a round, so scope 2's order file overwrites scope 1's the moment it is compiled
|
|
518
|
+
// — measured on the kill/resume probe, where it made `orders/ minus results/` read EMPTY on a run
|
|
519
|
+
// that was re-dispatching a completed phase. The contract row watching that property therefore
|
|
520
|
+
// passed on the exact failure it exists to catch. ``harness verify t0``'s verdict artifacts have always
|
|
521
|
+
// been self-identifying this way (`r<R>-a<A>-t<T>.json`, `wx`-created); orders now match, so
|
|
522
|
+
// `orders/` is an audit trail of dispatches rather than a rolling buffer of the last one.
|
|
523
|
+
//
|
|
524
|
+
// The shape stays `<slug>/<suffix>`: every consumer splits on the FIRST "/" (`harness reduce ingest`
|
|
525
|
+
// reads [0] as the slug and [1] as the file stem), and a scope id is already filename-safe
|
|
526
|
+
// because it is a scope contract's own basename.
|
|
527
|
+
// `scope` arrives as the PARSED contract object (the CLI reads the .md and embeds it), so the id
|
|
528
|
+
// comes from its own `scope_id` field; a caller that passes a path instead still works. The id is
|
|
529
|
+
// lowercased and stripped to the character class `work-order.schema.json` allows after the "/" —
|
|
530
|
+
// an order that cannot pass its own schema is refused before it is written, and a naming
|
|
531
|
+
// improvement must not be able to cause that.
|
|
532
|
+
const rawScopeId = scope && typeof scope === "object"
|
|
533
|
+
? scope.scope_id
|
|
534
|
+
: (scope ? String(scope).split("/").pop().replace(/\.(md|json)$/, "") : null);
|
|
535
|
+
const scopeId = String(rawScopeId || "").toLowerCase().replace(/[^a-z0-9.-]/g, "-").replace(/^[^a-z0-9]+/, "");
|
|
536
|
+
const buildSuffix = round && attempt ? (scopeId ? `${scopeId}-r${round}-a${attempt}` : `r${round}-a${attempt}`) : null;
|
|
537
|
+
// A non-BUILD order was suffixed by operation+round alone, with no per-leg discriminator — so two
|
|
538
|
+
// concurrent non-BUILD legs of the SAME operation and round, dispatched for different scopes
|
|
539
|
+
// (e.g. two scopes both running `evaluate` in the same round), compiled to the identical suffix
|
|
540
|
+
// and the second write clobbered the first order file on disk while the run read green. `scopeId`
|
|
541
|
+
// is already derived above for any operation, not only BUILD; folding it in here — only when a
|
|
542
|
+
// scope was actually passed — closes that collision. Operation-level dispatches (orient, analyze,
|
|
543
|
+
// wire, map-scopes, evaluate, hunt, hammer) never pass a scope, so `scopeId` is empty for them and
|
|
544
|
+
// this reduces to the unchanged `${operation}-r${round}` / `${operation}` shape.
|
|
545
|
+
const scopedRoundSuffix = scopeId
|
|
546
|
+
? (round ? `${operation}-${scopeId}-r${round}` : `${operation}-${scopeId}`)
|
|
547
|
+
: (round ? `${operation}-r${round}` : operation);
|
|
548
|
+
const suffix = buildSuffix || scopedRoundSuffix;
|
|
329
549
|
const order = {
|
|
330
550
|
schema_version: 1,
|
|
331
551
|
order_id: `${slug}/${suffix}`,
|
|
552
|
+
// THE TWO ANALYTIC FIELDS, and why they are on the ORDER rather than the result.
|
|
553
|
+
//
|
|
554
|
+
// `order_id` identifies a dispatch within a run and repeats across runs of the same slug, so
|
|
555
|
+
// the dispatch record needed a run key to be groupable at all. It is stamped here because this
|
|
556
|
+
// is the one place every lane passes through — the same reason the active-order pointer is
|
|
557
|
+
// published here — so the workflow lane, `--tiny`, the prose round loop and a standalone
|
|
558
|
+
// dispatch all carry it without four separate stamps to keep in step.
|
|
559
|
+
//
|
|
560
|
+
// The RESULT deliberately gets neither. It is written by the worker, and a field a worker has
|
|
561
|
+
// to remember to copy is a field that goes missing under exactly the conditions you most want
|
|
562
|
+
// the record: the run that went wrong. Results join to orders on `order_id`, which they already
|
|
563
|
+
// echo and `validate-envelope` already checks — so the key reaches the result leg through a
|
|
564
|
+
// join that is enforced, instead of through a worker's cooperation.
|
|
565
|
+
...(runId ? { run_id: runId } : {}),
|
|
566
|
+
...(compiledAt ? { compiled_at: compiledAt } : {}),
|
|
332
567
|
worker,
|
|
333
568
|
mode,
|
|
334
569
|
...(operation ? { operation } : {}),
|
|
@@ -340,6 +575,9 @@ export function compileOrder({
|
|
|
340
575
|
...(decisions?.length ? { decisions } : {}),
|
|
341
576
|
...(digestedErrors?.length ? { digested_errors: digestedErrors } : {}),
|
|
342
577
|
...(trialHistory?.length ? { trial_history: trialHistory } : {}),
|
|
578
|
+
// Before `payloadExtra`, so an explicit `--payload '{"bugs":…}'` still wins: an operator
|
|
579
|
+
// hand-addressing a defect outranks the derivation.
|
|
580
|
+
...(bugs?.length ? { bugs } : {}),
|
|
343
581
|
...(testCmd ? { verify: { test_cmd: testCmd, env: [] } } : {}),
|
|
344
582
|
...(payloadExtra || {}),
|
|
345
583
|
},
|
|
@@ -352,7 +590,7 @@ export function compileOrder({
|
|
|
352
590
|
// ---------------------------------------------------------------------------
|
|
353
591
|
/** The typed argv contract (see `./lib/argv.mjs`). */
|
|
354
592
|
export const ARGV_SPEC = {
|
|
355
|
-
usage: "
|
|
593
|
+
usage: "harness.mjs compile (--scope <contract.json> --round N --attempt M | --task TASK-NNN --slug <slug> " +
|
|
356
594
|
"| --next --slug <slug> | --operation <op> --slug <slug>) [--worker <skill>] [--payload '<json>'] " +
|
|
357
595
|
"[--spec <dir>] [--test-cmd \"<cmd>\"] [--cwd <dir>] [--pause-gates]",
|
|
358
596
|
_: { arity: 0, max: 0, name: "(no positional operands)" },
|
|
@@ -371,9 +609,15 @@ export const ARGV_SPEC = {
|
|
|
371
609
|
"pause-gates": { type: "flag" },
|
|
372
610
|
};
|
|
373
611
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
612
|
+
/**
|
|
613
|
+
* Compile a WorkOrder for one dispatch and publish the substrate pointer that fences it.
|
|
614
|
+
*
|
|
615
|
+
* @param {string[]} rawArgv - The subcommand's own arguments (harness.mjs strips the verb words).
|
|
616
|
+
* @returns {(Promise<void>|void)} Settles when the subcommand has written its output; most paths
|
|
617
|
+
* call `process.exit()` with the subcommand's documented code rather than returning.
|
|
618
|
+
*/
|
|
619
|
+
export async function cli(rawArgv) {
|
|
620
|
+
const argv = runArgs(ARGV_SPEC, rawArgv);
|
|
377
621
|
/**
|
|
378
622
|
* Read a parsed flag's value, normalising "absent" to null (the shape the body below expects).
|
|
379
623
|
* @param {string} name - Flag name without leading dashes.
|
|
@@ -409,17 +653,26 @@ if (isMainModule) {
|
|
|
409
653
|
|
|
410
654
|
const round = flag("round");
|
|
411
655
|
const attempt = flag("attempt");
|
|
412
|
-
|
|
413
|
-
//
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
656
|
+
|
|
657
|
+
// The fix round's inbound evidence. Derived here, from the ledgered verdict, for every lane —
|
|
658
|
+
// the workflow, `--tiny`, the prose round loop and a standalone `/build` all compile through
|
|
659
|
+
// this line, and none of them can pass a payload to a build order (see the banner above).
|
|
660
|
+
const bugs = scope ? bugsForScope(verdictBugs(cwd, slug, round), scope.scope_id, scopeSubstrates(cwd, slug)) : [];
|
|
661
|
+
|
|
662
|
+
// A ROUND CARRYING CITED DEFECTS IS A `fix`, AND THE ORDER HAS TO SAY SO.
|
|
663
|
+
//
|
|
664
|
+
// `payload.bugs` is the only field task-executor's input contract binds to a specific operation:
|
|
665
|
+
// "`fix` (only the bugs in `payload.bugs` — touch nothing else)". Dispatching a fix round as
|
|
666
|
+
// `execute` hands the worker a field its own contract associates with a different operation,
|
|
667
|
+
// against a Zero-memory rule that says to treat anything not in the order as unknown. Delivering
|
|
668
|
+
// evidence the receiving contract does not claim is half a delivery.
|
|
669
|
+
//
|
|
670
|
+
// Free of blast radius by construction: `substrateFor` returns the same whitelist for
|
|
671
|
+
// execute/fix/spike, so the sandbox contract is byte-identical, and the order id is addressed by
|
|
672
|
+
// scope/round/attempt rather than by operation, so no filename moves. An explicit `--operation`
|
|
673
|
+
// still wins — an operator naming the operation outranks the derivation.
|
|
674
|
+
let operation = flag("operation")
|
|
675
|
+
|| (scopePath || flag("task") || has("next") ? (bugs.length ? "fix" : "execute") : null);
|
|
423
676
|
const worker = flag("worker")
|
|
424
677
|
|| (scopePath || flag("task") || has("next") ? "task-executor" : null)
|
|
425
678
|
|| (operation ? OP_OWNER[operation] : null);
|
|
@@ -436,10 +689,18 @@ if (isMainModule) {
|
|
|
436
689
|
tasks = board.filter((t) => t.status === "ready" && t.depends_on.every((d) => doneIds.has(d))).slice(0, 1);
|
|
437
690
|
if (!tasks.length) { console.error("compile-order: no ready task with satisfied dependencies"); process.exit(2); }
|
|
438
691
|
} else if (scope) {
|
|
439
|
-
// Scope attempt: the
|
|
440
|
-
//
|
|
441
|
-
|
|
442
|
-
|
|
692
|
+
// Scope attempt: the board tasks anchored to this scope's use cases, else every not-done task
|
|
693
|
+
// on the board (the attempt loop owns sequencing, not the worker).
|
|
694
|
+
//
|
|
695
|
+
// The contract names USE CASES, never task ids: it is committed and the board is not, so a
|
|
696
|
+
// contract holding `TASK-004` pointed into a gitignored per-machine tier and resolved to
|
|
697
|
+
// nothing on a fresh clone — silently, because an empty filter and an absent board are the
|
|
698
|
+
// same empty array. A scope that anchors no use case still falls back to the whole open board
|
|
699
|
+
// rather than dispatching nothing; spec-lint's SCOPE-ANCHOR is what reports the missing anchor.
|
|
700
|
+
const anchored = tasksForScope(board, scope);
|
|
701
|
+
tasks = anchored.length || (scope.use_cases || []).length
|
|
702
|
+
? anchored
|
|
703
|
+
: board.filter((t) => t.status !== "done");
|
|
443
704
|
}
|
|
444
705
|
|
|
445
706
|
// Ledger decisions for this scope.
|
|
@@ -450,7 +711,7 @@ if (isMainModule) {
|
|
|
450
711
|
|
|
451
712
|
// inspect() — the attempt loop's history (see selectTrialHistory above).
|
|
452
713
|
//
|
|
453
|
-
// NON-REGRESSION
|
|
714
|
+
// NON-REGRESSION. With no `trials.jsonl` on disk this falls back to exactly the pre-ratchet
|
|
454
715
|
// read — the previous attempt's verdict artifact, AEGIS triples only, byte-for-byte the same
|
|
455
716
|
// order. Every new arm is skipped when its artifact is absent, per the ✦/✚ convention.
|
|
456
717
|
let digestedErrors = [];
|
|
@@ -484,9 +745,14 @@ if (isMainModule) {
|
|
|
484
745
|
if (!payloadExtra.feature) payloadExtra.feature = slug;
|
|
485
746
|
|
|
486
747
|
const order = compileOrder({
|
|
487
|
-
slug, worker, operation, round, attempt, scope, tasks, decisions, digestedErrors, trialHistory,
|
|
748
|
+
slug, worker, operation, round, attempt, scope, tasks, decisions, digestedErrors, trialHistory, bugs,
|
|
488
749
|
testCmd: flag("test-cmd"), payloadExtra, specDir,
|
|
489
750
|
interaction: has("pause-gates") ? { pause_gates: true } : { pause_gates: false },
|
|
751
|
+
// Read off the receipt rather than passed in: a standalone `compile-order` invocation gets the
|
|
752
|
+
// same key as one the workflow drove, and a dispatch in a workspace with no open run simply
|
|
753
|
+
// carries no key instead of failing — the analytic field must never be able to block a build.
|
|
754
|
+
runId: readRunId(cwd, slug),
|
|
755
|
+
compiledAt: new Date().toISOString(),
|
|
490
756
|
});
|
|
491
757
|
|
|
492
758
|
const { valid, errors } = validate(order, ORDER_SCHEMA);
|
|
@@ -501,6 +767,29 @@ if (isMainModule) {
|
|
|
501
767
|
writeFileSync(outPath, JSON.stringify(order, null, 2) + "\n");
|
|
502
768
|
console.log(outPath);
|
|
503
769
|
|
|
770
|
+
// POINT THE SANDBOX AT THIS ORDER, HERE, because this is the only place every lane passes
|
|
771
|
+
// through.
|
|
772
|
+
//
|
|
773
|
+
// `hooks/sandbox-guard.mjs` enforces the order's own `substrate` block — allowed/shared,
|
|
774
|
+
// append_only, frozen — and it finds the order through `.shapeup/active-order`. Until this
|
|
775
|
+
// write existed the pointer had exactly one author, the workflow script, so the guard fenced
|
|
776
|
+
// the workflow lane and DEFERRED everywhere else: `--tiny`, the prose round loop, and a
|
|
777
|
+
// standalone `/build` all compiled an order carrying a write contract that nothing enforced.
|
|
778
|
+
// A substrate that is only enforced on the lane that also happens to be the most supervised
|
|
779
|
+
// one is the wrong way round.
|
|
780
|
+
//
|
|
781
|
+
// Compiling an order is the moment the write contract comes into existence, so it is the
|
|
782
|
+
// correct moment to publish it. The workflow script still sets the pointer explicitly before
|
|
783
|
+
// dispatch (it interleaves phases and must be exact about which order is live); this write
|
|
784
|
+
// makes the SAME mechanism cover callers that never reach that code.
|
|
785
|
+
//
|
|
786
|
+
// Best-effort, on stderr, and never fatal: a compiled order that cannot publish its pointer is
|
|
787
|
+
// still a valid order, and stdout belongs to the order path the caller consumes. The guard
|
|
788
|
+
// fails open on a missing pointer by design, so the failure mode is "unfenced", which is
|
|
789
|
+
// exactly what a warning is for.
|
|
790
|
+
const ptr = writeActiveOrder(cwd, slug, outPath);
|
|
791
|
+
if (!ptr.ok) console.error(`compile-order: warning — ${ptr.reason} (this order's substrate will not be enforced)`);
|
|
792
|
+
|
|
504
793
|
// The stagnation breaker reports on stderr, never on stdout: stdout is the order path the
|
|
505
794
|
// orchestrator consumes, and a breaker that corrupts the pipeline's own output would be worse
|
|
506
795
|
// than the flailing it detects. It advises; the orchestrator queues the GATE H proposal.
|