shapeup-sdlc 1.6.2 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +36 -103
- package/README.md +60 -67
- package/SECURITY.md +31 -16
- package/bin/init.mjs +24 -69
- package/commands/build.md +19 -0
- package/commands/eval.md +5 -4
- package/commands/scopes.md +5 -4
- package/commands/shape.md +1 -1
- package/commands/ship.md +37 -4
- package/hooks/anti-rationalization.mjs +7 -13
- package/hooks/compact-snapshot.mjs +5 -5
- package/hooks/gate-deadline.mjs +12 -16
- package/hooks/gate-intake.mjs +13 -14
- package/hooks/gate-l2.mjs +3 -3
- package/hooks/gate-zerowork.mjs +97 -16
- package/hooks/lib/decision.mjs +21 -13
- package/hooks/safety-spine.mjs +1 -1
- package/hooks/sandbox-guard.mjs +83 -47
- package/hooks/session-rehydrate.mjs +8 -9
- package/hooks/slop-cleaner.mjs +1 -1
- package/oracles/_shared.mjs +1 -1
- package/oracles/http-oracle.mjs +1 -1
- package/oracles/index.mjs +1 -1
- package/oracles/process-oracle.mjs +1 -1
- package/oracles/snapshot-oracle.mjs +1 -1
- package/oracles/test-oracle.mjs +1 -1
- package/package.json +3 -7
- package/skills/ba-pitch-analyzer/SKILL.md +20 -15
- package/skills/ba-pitch-analyzer/references/doc-schemas.md +1 -1
- package/skills/ba-pitch-analyzer/references/task-generation.md +6 -6
- package/skills/ba-pitch-analyzer/references/test-surface.md +1 -1
- package/skills/ba-pitch-analyzer/scripts/board-derive.mjs +5 -5
- package/skills/ba-pitch-analyzer/scripts/spec-lint.mjs +2 -2
- package/skills/coach/SKILL.md +3 -3
- package/skills/orient/SKILL.md +2 -1
- package/skills/qa-edge-hunter/SKILL.md +9 -39
- package/skills/scope-architect/SKILL.md +17 -12
- package/skills/scope-hammer/SKILL.md +5 -5
- package/skills/shapeup/SKILL.md +3 -3
- package/skills/shapeup/resources/context-compaction.md +4 -3
- package/skills/solution-architect/SKILL.md +18 -11
- package/skills/spec-evaluator/SKILL.md +12 -5
- package/skills/spec-evaluator/references/dimensions/_registry.md +2 -2
- package/skills/spec-evaluator/references/dimensions/completeness.md +1 -1
- package/skills/spec-evaluator/references/dimensions/integration.md +98 -67
- package/skills/spec-evaluator/references/dimensions/spec-conformance.md +4 -4
- package/skills/spec-evaluator/references/dimensions/test-surface-conformance.md +1 -1
- package/skills/spec-evaluator/references/probing.md +42 -23
- package/skills/spec-evaluator/references/report-schema.md +2 -2
- package/skills/spec-evaluator/scripts/verdict-ledger.mjs +1 -1
- package/skills/task-executor/SKILL.md +25 -12
- package/skills/tech-lead/SKILL.md +115 -438
- package/skills/tech-lead/references/delegation.md +38 -31
- package/skills/tech-lead/references/gates.md +75 -33
- package/skills/tech-lead/references/hard-rules.md +34 -0
- package/skills/tech-lead/references/invocation.md +2 -2
- package/skills/tech-lead/references/ledger-schema.md +17 -18
- package/skills/tech-lead/references/round-protocol.md +77 -27
- package/skills/tech-lead/references/state-model.md +1 -1
- package/skills/tech-lead/schemas/domain.schema.json +320 -148
- package/skills/tech-lead/schemas/gate-answers.schema.json +2 -2
- package/skills/tech-lead/schemas/work-result.schema.json +55 -17
- package/skills/tech-lead/scripts/aegis-digest.mjs +1 -1
- package/skills/tech-lead/scripts/budget-check.mjs +7 -13
- package/skills/tech-lead/scripts/compile-order.mjs +69 -22
- package/skills/tech-lead/scripts/fit-check.mjs +10 -10
- package/skills/tech-lead/scripts/gate-answers.mjs +11 -12
- package/skills/tech-lead/scripts/hill-derive.mjs +133 -0
- package/skills/tech-lead/scripts/ingest-result.mjs +7 -19
- package/skills/tech-lead/scripts/init-run.mjs +60 -18
- package/skills/tech-lead/scripts/lib/argv.mjs +1 -1
- package/skills/tech-lead/scripts/lib/contract-md.mjs +10 -10
- package/skills/tech-lead/scripts/lib/is-main.mjs +11 -12
- package/skills/tech-lead/scripts/lib/paths.mjs +15 -13
- package/skills/tech-lead/scripts/resume-state.mjs +387 -0
- package/skills/tech-lead/scripts/run-snapshot.mjs +1 -6
- package/skills/tech-lead/scripts/run-workflow.mjs +381 -0
- package/skills/tech-lead/scripts/ship-report.mjs +33 -2
- package/skills/tech-lead/scripts/stats.mjs +9 -10
- package/skills/tech-lead/scripts/t0-verify.mjs +4 -4
- package/skills/tech-lead/scripts/trace-lint.mjs +2 -2
- package/skills/tech-lead/workflows/shapeup-run.js +911 -0
- package/skills/translator/SKILL.md +2 -2
- package/skills/advisor-protocol/SKILL.md +0 -171
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
3
|
"$id": "https://github.com/nguyenvanphituoc/shapeup-sdlc-plugin/schemas/gate-answers.schema.json",
|
|
4
4
|
"title": "GateAnswerSet",
|
|
5
|
-
"description": "A pre-recorded set of PO decisions, one per gate, that lets a run cross its gates without a human in the loop. This is NOT 'gates off' — every gate still emits its block and still records a decision in the ledger; the decision's SOURCE becomes this file instead of a live human. That distinction is the whole design: an unattended run with no answer set stalls at the first ⏸ and burns its wall-clock budget
|
|
5
|
+
"description": "A pre-recorded set of PO decisions, one per gate, that lets a run cross its gates without a human in the loop. This is NOT 'gates off' — every gate still emits its block and still records a decision in the ledger; the decision's SOURCE becomes this file instead of a live human. That distinction is the whole design: an unattended run with no answer set stalls at the first ⏸ and burns its whole wall-clock budget having built nothing, while an unattended run with one produces the same audit trail an interactive run does.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": ["version", "preset", "answers"],
|
|
8
8
|
"additionalProperties": false,
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"escalation": {
|
|
27
27
|
"type": "object",
|
|
28
28
|
"additionalProperties": false,
|
|
29
|
-
"description": "Policy for a worker
|
|
29
|
+
"description": "RESERVED. Policy for a worker-raised question, which would be a gate in all but name. Inert today: WorkResult carries no escalates field, so no worker can raise one. Kept as a declared key because the answer-set schema forbids additional properties, and removing it would invalidate every gate-answers.json already on disk.",
|
|
30
30
|
"properties": {
|
|
31
31
|
"default": {
|
|
32
32
|
"type": "string",
|
|
@@ -3,38 +3,76 @@
|
|
|
3
3
|
"title": "WorkResult",
|
|
4
4
|
"description": "The worker → orchestrator envelope (pure-skill architecture v1.0). Everything a worker used to write into shared files it now RETURNS as data; skills/tech-lead/scripts/ingest-result.mjs performs the writes (tick AC boxes, flip board status, append execution log, propagate unblocks, append discoveries) deterministically, in one place. Path: .shapeup/<slug>/results/r<N>-a<M>.json. Every record type is DEFINED CENTRALLY in domain.schema.json — this file only shapes the envelope.",
|
|
5
5
|
"type": "object",
|
|
6
|
-
"required": [
|
|
6
|
+
"required": [
|
|
7
|
+
"schema_version",
|
|
8
|
+
"order_id",
|
|
9
|
+
"status"
|
|
10
|
+
],
|
|
7
11
|
"properties": {
|
|
8
|
-
"schema_version": {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
"schema_version": {
|
|
13
|
+
"type": "integer",
|
|
14
|
+
"enum": [
|
|
15
|
+
1
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
"order_id": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"description": "Copied verbatim from the answered WorkOrder — the 1:1 join key."
|
|
21
|
+
},
|
|
22
|
+
"worker": {
|
|
23
|
+
"type": "string"
|
|
24
|
+
},
|
|
25
|
+
"status": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"enum": [
|
|
28
|
+
"done",
|
|
29
|
+
"partial",
|
|
30
|
+
"escalated",
|
|
31
|
+
"failed"
|
|
32
|
+
]
|
|
33
|
+
},
|
|
12
34
|
"task_results": {
|
|
13
35
|
"type": "array",
|
|
14
36
|
"description": "Per-task outcome. ingest-result flips the board row, ticks GATE-verified AC boxes, and appends the Execution Log from these entries.",
|
|
15
|
-
"items": {
|
|
37
|
+
"items": {
|
|
38
|
+
"$ref": "domain.schema.json#/$defs/TaskResult"
|
|
39
|
+
}
|
|
16
40
|
},
|
|
17
41
|
"files_touched": {
|
|
18
42
|
"type": "array",
|
|
19
|
-
"items": {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"type": "array",
|
|
23
|
-
"description": "advisor-protocol ESCALATE blocks — the worker's one outward port.",
|
|
24
|
-
"items": { "$ref": "domain.schema.json#/$defs/Escalate" }
|
|
43
|
+
"items": {
|
|
44
|
+
"$ref": "domain.schema.json#/$defs/FileTouched"
|
|
45
|
+
}
|
|
25
46
|
},
|
|
26
47
|
"discoveries": {
|
|
27
48
|
"type": "array",
|
|
28
49
|
"description": "Raw discovered lines. ingest-result appends them to the discovery ledger — the worker never touches the ledger itself.",
|
|
29
|
-
"items": {
|
|
50
|
+
"items": {
|
|
51
|
+
"$ref": "domain.schema.json#/$defs/Discovery"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"verdict": {
|
|
55
|
+
"$ref": "domain.schema.json#/$defs/Verdict"
|
|
30
56
|
},
|
|
31
|
-
"verdict": { "$ref": "domain.schema.json#/$defs/Verdict" },
|
|
32
57
|
"artifacts": {
|
|
33
58
|
"type": "array",
|
|
34
59
|
"description": "Domain artifacts the worker wrote inside its substrate (spec docs, orient/ files, reports).",
|
|
35
|
-
"items": {
|
|
60
|
+
"items": {
|
|
61
|
+
"type": "string"
|
|
62
|
+
}
|
|
36
63
|
},
|
|
37
|
-
"assumptions": {
|
|
38
|
-
|
|
64
|
+
"assumptions": {
|
|
65
|
+
"type": "array",
|
|
66
|
+
"items": {
|
|
67
|
+
"type": "string"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"deviations": {
|
|
71
|
+
"type": "array",
|
|
72
|
+
"items": {
|
|
73
|
+
"type": "string"
|
|
74
|
+
},
|
|
75
|
+
"description": "When status is \"escalated\", the FIRST entry is the blocker: one specific, answerable question with the context needed to answer it. There is no escalates[] field, so this is the only channel a blocked worker has."
|
|
76
|
+
}
|
|
39
77
|
}
|
|
40
78
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// AEGIS digester
|
|
2
|
+
// AEGIS digester — compresses a failed verification into the triples the next attempt reads.
|
|
3
3
|
//
|
|
4
4
|
// Distills raw build/test/Playwright logs into {file, line, core_message} triples so a
|
|
5
5
|
// task-executor's next attempt gets a few lines of signal instead of a full stack-trace dump.
|
|
@@ -1,21 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// THE THIRD BREAKER — wall clock. Derived, never claimed; checked at every round boundary.
|
|
3
3
|
//
|
|
4
|
-
// WHY THIS EXISTS (
|
|
4
|
+
// WHY THIS EXISTS (and it corrects an earlier diagnosis).
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// was that it had stalled at a gate with no human to sign off.
|
|
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.
|
|
9
8
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// 19 gate markers, last gate reached L3 · narration_ratio 0.047 · stall_signals 0
|
|
15
|
-
//
|
|
16
|
-
// It was not waiting. It was the LEAST talkative shapeup run in the whole matrix and one of the
|
|
17
|
-
// busiest rows in the dataset, and it got killed mid-loop at GATE L3 — Verdict & Loop. It ran
|
|
18
|
-
// out of clock while genuinely working.
|
|
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.
|
|
19
13
|
//
|
|
20
14
|
// That makes the defect specific: **both existing breakers count events, not time.**
|
|
21
15
|
// `round_budget` decrements once per round; `attempt_budget` decrements once per T0 attempt.
|
|
@@ -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.
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
relShared, globLocal, globShared, relKnowledgeBase,
|
|
40
40
|
} from "./lib/paths.mjs";
|
|
41
41
|
import { readContract, SCOPE_CONTRACT } from "./lib/contract-md.mjs";
|
|
42
|
+
import { writeActiveOrder } from "./resume-state.mjs";
|
|
42
43
|
|
|
43
44
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
44
45
|
const ORDER_SCHEMA = JSON.parse(readFileSync(resolve(HERE, "../schemas/work-order.schema.json"), "utf8"));
|
|
@@ -145,9 +146,9 @@ export function ledgerDecisions(ledgerText, scopeId) {
|
|
|
145
146
|
|
|
146
147
|
/**
|
|
147
148
|
* 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|
|
|
149
|
+
* operation, so mode/flag differences are enforced by the sandbox hook reading the order's substrate, not trusted to prose.
|
|
150
|
+
* @param {string} operation - The order's operation (execute|fix|spike|analyze|reconcile|
|
|
151
|
+
* retrofit-surface|coverage|map-scopes|wire|evaluate|orient|hunt|translate|hammer|coach).
|
|
151
152
|
* @param {{slug?:string, specDir?:string, scope?:object}} [ctx] - slug (names LOCAL/SHARED roots),
|
|
152
153
|
* specDir (overrides the default spec path), scope (contract supplying allowed/shared substrates).
|
|
153
154
|
* @returns {{allowed:string[], shared?:string[], frozen?:string[], append_only?:string[]}} The
|
|
@@ -171,11 +172,7 @@ export function substrateFor(operation, { slug, specDir, scope } = {}) {
|
|
|
171
172
|
};
|
|
172
173
|
case "analyze":
|
|
173
174
|
return { allowed: [`${spec}/**`, `${local}/**`], frozen: [] };
|
|
174
|
-
|
|
175
|
-
return {
|
|
176
|
-
allowed: [`${local}/tasks/**`, `${spec}/scope-summary.md`, `${working}/**`],
|
|
177
|
-
frozen: [...FROZEN_SPEC_CORE, `${scopesDir}/**`],
|
|
178
|
-
};
|
|
175
|
+
|
|
179
176
|
case "reconcile":
|
|
180
177
|
return {
|
|
181
178
|
allowed: [`${local}/tasks/**`, `${spec}/scope-summary.md`, `${working}/**`],
|
|
@@ -184,14 +181,14 @@ export function substrateFor(operation, { slug, specDir, scope } = {}) {
|
|
|
184
181
|
};
|
|
185
182
|
case "retrofit-surface":
|
|
186
183
|
return { allowed: [], append_only: [`${spec}/usecases/*.md#Test Surface`], frozen: FROZEN_SPEC_CORE };
|
|
184
|
+
|
|
187
185
|
case "coverage":
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
case "map-scopes": case "remap": case "split-scope":
|
|
186
|
+
// The covers-closure input truth. Writes ONLY the derived registry: the REQ source it
|
|
187
|
+
// extracts from is frozen alongside the spec core, because a planner that may edit the
|
|
188
|
+
// requirements it is being measured against is not measuring anything.
|
|
189
|
+
return { allowed: [globShared(slug, "requirements.md")], frozen: FROZEN_SPEC_CORE };
|
|
190
|
+
|
|
191
|
+
case "map-scopes":
|
|
195
192
|
return {
|
|
196
193
|
allowed: [`${scopesDir}/*.md`, globShared(slug, "scope-board.md")],
|
|
197
194
|
frozen: [...FROZEN_SPEC_CORE, `${local}/tasks/**`],
|
|
@@ -205,10 +202,16 @@ export function substrateFor(operation, { slug, specDir, scope } = {}) {
|
|
|
205
202
|
};
|
|
206
203
|
case "evaluate":
|
|
207
204
|
return { allowed: [`${local}/evaluation/**`], frozen: [`${spec}/**`, `${local}/tasks/**`] };
|
|
208
|
-
case "hunt":
|
|
205
|
+
case "hunt":
|
|
209
206
|
return { allowed: [`${local}/qa/**`], frozen: [`${spec}/**`, `${local}/tasks/**`] };
|
|
210
207
|
case "orient":
|
|
211
208
|
return { allowed: [`${local}/orient/**`], frozen: [`${spec}/**`] };
|
|
209
|
+
case "translate":
|
|
210
|
+
return { allowed: [globShared(slug, "shaping/*.md"), globShared(slug, "glossary.md")] };
|
|
211
|
+
case "hammer":
|
|
212
|
+
return { allowed: [globShared(slug, "REPORT.md"), `${local}/reports/**`] };
|
|
213
|
+
case "coach":
|
|
214
|
+
return { allowed: [relKnowledgeBase("*")] };
|
|
212
215
|
default:
|
|
213
216
|
return { allowed: [`${local}/**`] };
|
|
214
217
|
}
|
|
@@ -325,7 +328,28 @@ export function compileOrder({
|
|
|
325
328
|
slug, worker, mode = "orchestrated", operation, round, attempt,
|
|
326
329
|
scope, tasks, decisions, digestedErrors, trialHistory, testCmd, payloadExtra, specDir, interaction,
|
|
327
330
|
}) {
|
|
328
|
-
|
|
331
|
+
// A build order's id carries its SCOPE. Without it, `r<round>-a<attempt>` is the same name for
|
|
332
|
+
// every scope in a round, so scope 2's order file overwrites scope 1's the moment it is compiled
|
|
333
|
+
// — measured on the kill/resume probe, where it made `orders/ minus results/` read EMPTY on a run
|
|
334
|
+
// that was re-dispatching a completed phase. The contract row watching that property therefore
|
|
335
|
+
// passed on the exact failure it exists to catch. `t0-verify.mjs`'s verdict artifacts have always
|
|
336
|
+
// been self-identifying this way (`r<R>-a<A>-t<T>.json`, `wx`-created); orders now match, so
|
|
337
|
+
// `orders/` is an audit trail of dispatches rather than a rolling buffer of the last one.
|
|
338
|
+
//
|
|
339
|
+
// The shape stays `<slug>/<suffix>`: every consumer splits on the FIRST "/" (ingest-result.mjs
|
|
340
|
+
// reads [0] as the slug and [1] as the file stem), and a scope id is already filename-safe
|
|
341
|
+
// because it is a scope contract's own basename.
|
|
342
|
+
// `scope` arrives as the PARSED contract object (the CLI reads the .md and embeds it), so the id
|
|
343
|
+
// comes from its own `scope_id` field; a caller that passes a path instead still works. The id is
|
|
344
|
+
// lowercased and stripped to the character class `work-order.schema.json` allows after the "/" —
|
|
345
|
+
// an order that cannot pass its own schema is refused before it is written, and a naming
|
|
346
|
+
// improvement must not be able to cause that.
|
|
347
|
+
const rawScopeId = scope && typeof scope === "object"
|
|
348
|
+
? scope.scope_id
|
|
349
|
+
: (scope ? String(scope).split("/").pop().replace(/\.(md|json)$/, "") : null);
|
|
350
|
+
const scopeId = String(rawScopeId || "").toLowerCase().replace(/[^a-z0-9.-]/g, "-").replace(/^[^a-z0-9]+/, "");
|
|
351
|
+
const buildSuffix = round && attempt ? (scopeId ? `${scopeId}-r${round}-a${attempt}` : `r${round}-a${attempt}`) : null;
|
|
352
|
+
const suffix = buildSuffix || (round ? `${operation}-r${round}` : operation);
|
|
329
353
|
const order = {
|
|
330
354
|
schema_version: 1,
|
|
331
355
|
order_id: `${slug}/${suffix}`,
|
|
@@ -412,12 +436,12 @@ if (isMainModule) {
|
|
|
412
436
|
// Operation → owning worker (mirrors domain.schema.json $defs/Operation ownership). Lets a
|
|
413
437
|
// non-build dispatch resolve its worker from the operation alone, without a redundant --worker.
|
|
414
438
|
const OP_OWNER = {
|
|
415
|
-
analyze: "ba-pitch-analyzer",
|
|
439
|
+
analyze: "ba-pitch-analyzer", reconcile: "ba-pitch-analyzer",
|
|
416
440
|
"retrofit-surface": "ba-pitch-analyzer", coverage: "ba-pitch-analyzer",
|
|
417
|
-
"map-scopes": "scope-architect",
|
|
441
|
+
"map-scopes": "scope-architect",
|
|
418
442
|
wire: "solution-architect", evaluate: "spec-evaluator", orient: "orient",
|
|
419
|
-
hunt: "qa-edge-hunter",
|
|
420
|
-
hammer: "scope-hammer", coach: "coach",
|
|
443
|
+
hunt: "qa-edge-hunter", translate: "translator",
|
|
444
|
+
hammer: "scope-hammer", coach: "coach",
|
|
421
445
|
};
|
|
422
446
|
let operation = flag("operation") || (scopePath || flag("task") || has("next") ? "execute" : null);
|
|
423
447
|
const worker = flag("worker")
|
|
@@ -501,6 +525,29 @@ if (isMainModule) {
|
|
|
501
525
|
writeFileSync(outPath, JSON.stringify(order, null, 2) + "\n");
|
|
502
526
|
console.log(outPath);
|
|
503
527
|
|
|
528
|
+
// POINT THE SANDBOX AT THIS ORDER, HERE, because this is the only place every lane passes
|
|
529
|
+
// through.
|
|
530
|
+
//
|
|
531
|
+
// `hooks/sandbox-guard.mjs` enforces the order's own `substrate` block — allowed/shared,
|
|
532
|
+
// append_only, frozen — and it finds the order through `.shapeup/active-order`. Until this
|
|
533
|
+
// write existed the pointer had exactly one author, the workflow script, so the guard fenced
|
|
534
|
+
// the workflow lane and DEFERRED everywhere else: `--tiny`, the prose round loop, and a
|
|
535
|
+
// standalone `/build` all compiled an order carrying a write contract that nothing enforced.
|
|
536
|
+
// A substrate that is only enforced on the lane that also happens to be the most supervised
|
|
537
|
+
// one is the wrong way round.
|
|
538
|
+
//
|
|
539
|
+
// Compiling an order is the moment the write contract comes into existence, so it is the
|
|
540
|
+
// correct moment to publish it. The workflow script still sets the pointer explicitly before
|
|
541
|
+
// dispatch (it interleaves phases and must be exact about which order is live); this write
|
|
542
|
+
// makes the SAME mechanism cover callers that never reach that code.
|
|
543
|
+
//
|
|
544
|
+
// Best-effort, on stderr, and never fatal: a compiled order that cannot publish its pointer is
|
|
545
|
+
// still a valid order, and stdout belongs to the order path the caller consumes. The guard
|
|
546
|
+
// fails open on a missing pointer by design, so the failure mode is "unfenced", which is
|
|
547
|
+
// exactly what a warning is for.
|
|
548
|
+
const ptr = writeActiveOrder(cwd, slug, outPath);
|
|
549
|
+
if (!ptr.ok) console.error(`compile-order: warning — ${ptr.reason} (this order's substrate will not be enforced)`);
|
|
550
|
+
|
|
504
551
|
// The stagnation breaker reports on stderr, never on stdout: stdout is the order path the
|
|
505
552
|
// orchestrator consumes, and a breaker that corrupts the pipeline's own output would be worse
|
|
506
553
|
// than the flailing it detects. It advises; the orchestrator queues the GATE H proposal.
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// GATE L0.3 — FIT CHECK. Which lane does this change belong in? Decided by measurement.
|
|
3
3
|
//
|
|
4
|
-
// WHY THIS EXISTS (
|
|
4
|
+
// WHY THIS EXISTS (and it is the root cause the other three fixes did not touch).
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
6
|
+
// A change of the size "add a `summary` command to a six-file CLI" — one new module plus one
|
|
7
|
+
// dispatcher wiring — run through the full eleven-gate pipeline does not finish. Not once, across
|
|
8
|
+
// repeated attempts: killed at the time cap mid-build; then, with a wall-clock breaker fitted,
|
|
9
|
+
// killed at ship-triage; then producing an honest report saying its two must-haves were 0%
|
|
10
|
+
// started.
|
|
11
11
|
//
|
|
12
12
|
// Every one of those fixes made the FAILURE better. None of them made the RUN finish, because
|
|
13
13
|
// none addressed why a three-file change was consuming half an hour: **the ceremony was not
|
|
14
14
|
// sized to the change.**
|
|
15
15
|
//
|
|
16
|
-
// The harness already knew. In
|
|
16
|
+
// The harness already knew. In those transcripts tech-lead identified the change at GATE L0 as
|
|
17
17
|
// "about as small as they come… squarely inside the --tiny lane" — and then ran the full pipeline
|
|
18
18
|
// anyway, because the lane was a judgment the model was free to talk itself out of. That is the
|
|
19
19
|
// same class of defect as narration and as prose consent: an invariant living somewhere a model
|
|
@@ -121,7 +121,7 @@ export function decideLane({ intake, files }) {
|
|
|
121
121
|
};
|
|
122
122
|
|
|
123
123
|
// CONJUNCTIVE, and it must be. The first version asked "is there evidence this is BIG?" and
|
|
124
|
-
// defaulted to tiny when it found none — which classified
|
|
124
|
+
// defaulted to tiny when it found none — which classified every trial feature as tiny,
|
|
125
125
|
// including the five-seam one that genuinely needs the pipeline and used two evaluation rounds
|
|
126
126
|
// to pass. A router that confident and that wrong is worse than no router: it would skip review
|
|
127
127
|
// on exactly the change that needed it.
|
|
@@ -152,7 +152,7 @@ export function decideLane({ intake, files }) {
|
|
|
152
152
|
// Say it on every invocation rather than burying it in a doc. These thresholds are calibrated
|
|
153
153
|
// against THREE features. That is enough to stop the router being obviously wrong and nowhere
|
|
154
154
|
// near enough to trust it silently, so it recommends and never denies.
|
|
155
|
-
calibration: "advisory — thresholds
|
|
155
|
+
calibration: "advisory — thresholds are coarse and `full` is the default; the PO may override with --lane",
|
|
156
156
|
};
|
|
157
157
|
}
|
|
158
158
|
|
|
@@ -186,7 +186,7 @@ export function main() {
|
|
|
186
186
|
...result,
|
|
187
187
|
// The sentence the orchestrator must act on, so the decision is not re-derived from the JSON.
|
|
188
188
|
action: result.lane === "tiny"
|
|
189
|
-
? "TINY LANE. Run: orient (light) → single-task board → build → T0 → ⏸ L4. Skip WIRE, scope contracts, spec tree, EVAL and QA. The full pipeline on a change this size is
|
|
189
|
+
? "TINY LANE. Run: orient (light) → single-task board → build → T0 → ⏸ L4. Skip WIRE, scope contracts, spec tree, EVAL and QA. The full pipeline on a change this size is why a run burns its whole clock without finishing."
|
|
190
190
|
: "FULL LANE. Run the complete pipeline: ORIENT → WIRE → MAP SCOPES → BUILD → EVAL → QA → GATE H → L4.",
|
|
191
191
|
}, null, 2));
|
|
192
192
|
}
|
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// GATE ANSWER SET — cross a gate with a pre-recorded decision instead of a live human.
|
|
3
3
|
//
|
|
4
|
-
// WHY THIS EXISTS (
|
|
4
|
+
// WHY THIS EXISTS (observed, not theorized).
|
|
5
5
|
//
|
|
6
6
|
// This harness pauses at every ⏸ gate for PO sign-off by default. That is the point of it. But
|
|
7
|
-
//
|
|
8
|
-
//
|
|
7
|
+
// unattended it produces two distinct failures, both of which look like the harness being slow or
|
|
8
|
+
// broken rather than the harness being safe:
|
|
9
9
|
//
|
|
10
|
-
// 1. STALL → TIMEOUT.
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
10
|
+
// 1. STALL → TIMEOUT. The run is killed at its time cap having produced nothing scoreable,
|
|
11
|
+
// on a feature that takes minutes to build by hand. A run with no human at the keyboard
|
|
12
|
+
// sitting at a gate does not fail — it waits, and a wait is indistinguishable from work
|
|
13
|
+
// until the budget runs out.
|
|
14
14
|
// 2. CONSENT-BY-PROSE. The workaround was a paragraph of English in the prompt ("treat this
|
|
15
|
-
// message as advance sign-off for every gate").
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// that can be re-summarized instead of acted on.
|
|
15
|
+
// message as advance sign-off for every gate"). Some models act on it. Others read the
|
|
16
|
+
// paragraph, read the gate list, and narrate the pipeline instead of running it. Consent
|
|
17
|
+
// carried in prose is consent that can be re-summarized instead of acted on.
|
|
19
18
|
//
|
|
20
19
|
// The organising rule of this project is that every invariant that matters lives in the runtime,
|
|
21
20
|
// not in a prompt. Gate sign-off was the last big one still living in a prompt. So it becomes a
|
|
@@ -79,7 +78,7 @@ export const VALID_BY_GATE = {
|
|
|
79
78
|
const CI_NOTE = "Pre-approved for a headless lane. No human is present; the decision is recorded here so the ledger still names a source.";
|
|
80
79
|
|
|
81
80
|
export const PRESETS = {
|
|
82
|
-
// Everything pre-approved. The lane a CI step
|
|
81
|
+
// Everything pre-approved. The lane a CI step runs in.
|
|
83
82
|
ci: {
|
|
84
83
|
version: 1,
|
|
85
84
|
preset: "ci",
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Hill Phase Derivation — phases are derived from facts, never authored
|
|
3
|
+
// Mechanical derivation of a scope's hill position based on facts (T0 verdicts, T1 evaluation, and discovery ledger).
|
|
4
|
+
// Writes to shapeup/<slug>/hill/<scope-id>.yml
|
|
5
|
+
|
|
6
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from "node:fs";
|
|
7
|
+
import { resolve, join } from "node:path";
|
|
8
|
+
import { isMain } from "./lib/is-main.mjs";
|
|
9
|
+
import { runArgs } from "./lib/argv.mjs";
|
|
10
|
+
import { scopesDir, hillDir, verdictsDir, evaluationDir, discoveryLedger } from "./lib/paths.mjs";
|
|
11
|
+
import { readAllContracts, SCOPE_CONTRACT } from "./lib/contract-md.mjs";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Derive and write the hill phase for all scopes mechanically based on T0, T1, and ledger facts.
|
|
15
|
+
*
|
|
16
|
+
* The derived phase follows these progression rules (facts move dots, not authors):
|
|
17
|
+
* - UPHILL_UNKNOWN: open unknowns > 0 in the ledger for this scope
|
|
18
|
+
* - UPHILL_SOLVED: unknowns 0, no T0-green yet
|
|
19
|
+
* - DOWNHILL_EXECUTION: ≥1 T0-green; T1/seesaw pending
|
|
20
|
+
* - FINISHED: T1 PASS ∧ seesaw green
|
|
21
|
+
*
|
|
22
|
+
* @param {string} cwd - The project root directory.
|
|
23
|
+
* @param {string} slug - The feature slug being built.
|
|
24
|
+
* @returns {Array<{scope_id: string, phase: string, changed: boolean}>} A report of all scopes processed, their derived phase, and whether the hill shard on disk was modified.
|
|
25
|
+
* Side effects: writes to `shapeup/<slug>/hill/<scope-id>.yml` for each scope.
|
|
26
|
+
*/
|
|
27
|
+
export function deriveHill(cwd, slug) {
|
|
28
|
+
const scopes = readAllContracts(scopesDir(cwd, slug), SCOPE_CONTRACT).map((x) => x.contract);
|
|
29
|
+
const vDir = verdictsDir(cwd, slug);
|
|
30
|
+
const evalDir = evaluationDir(cwd, slug);
|
|
31
|
+
const ledgerPath = discoveryLedger(cwd, slug);
|
|
32
|
+
const hDir = hillDir(cwd, slug);
|
|
33
|
+
|
|
34
|
+
if (!existsSync(hDir)) mkdirSync(hDir, { recursive: true });
|
|
35
|
+
|
|
36
|
+
// 1. Check if T1 Evaluation passed (spec-conformance === PASS for the most recent run)
|
|
37
|
+
let t1Pass = false;
|
|
38
|
+
const evalFile = join(evalDir, ".verdicts-run.jsonl");
|
|
39
|
+
if (existsSync(evalFile)) {
|
|
40
|
+
const lines = readFileSync(evalFile, "utf8").trim().split(/\n/).filter(Boolean);
|
|
41
|
+
let maxRun = 0;
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(line);
|
|
45
|
+
if (parsed.run >= maxRun) {
|
|
46
|
+
maxRun = parsed.run;
|
|
47
|
+
if (parsed.dimension === "spec-conformance") {
|
|
48
|
+
t1Pass = (parsed.verdict === "PASS");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch (e) {
|
|
52
|
+
// ignore parse errors
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 2. T0 facts per scope: has it achieved a green overall verdict? was seesaw also green?
|
|
58
|
+
const t0Facts = {};
|
|
59
|
+
if (existsSync(vDir)) {
|
|
60
|
+
for (const f of readdirSync(vDir)) {
|
|
61
|
+
if (!f.endsWith(".json")) continue;
|
|
62
|
+
try {
|
|
63
|
+
const b = JSON.parse(readFileSync(join(vDir, f), "utf8"));
|
|
64
|
+
if (!t0Facts[b.scope_id]) t0Facts[b.scope_id] = { hasGreen: false, seesawGreen: false };
|
|
65
|
+
if (b.overall === "green") {
|
|
66
|
+
t0Facts[b.scope_id].hasGreen = true;
|
|
67
|
+
// If regression is false on a green T0, it means seesaw was also green
|
|
68
|
+
if (b.regression === false) {
|
|
69
|
+
t0Facts[b.scope_id].seesawGreen = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch (e) {
|
|
73
|
+
// ignore parse errors
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 3. Ledger unknowns per scope
|
|
79
|
+
const scopeUnknowns = {};
|
|
80
|
+
if (existsSync(ledgerPath)) {
|
|
81
|
+
const lines = readFileSync(ledgerPath, "utf8").split("\n");
|
|
82
|
+
let currentScope = null;
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
const m = line.match(/^## Discovered — .*?:([\w.-]+)-a\d+/);
|
|
85
|
+
if (m) {
|
|
86
|
+
currentScope = m[1];
|
|
87
|
+
}
|
|
88
|
+
if (currentScope && line.startsWith("~ ")) {
|
|
89
|
+
scopeUnknowns[currentScope] = (scopeUnknowns[currentScope] || 0) + 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const report = [];
|
|
95
|
+
for (const s of scopes) {
|
|
96
|
+
const id = s.scope_id;
|
|
97
|
+
const t0 = t0Facts[id] || { hasGreen: false, seesawGreen: false };
|
|
98
|
+
const unknowns = scopeUnknowns[id] || 0;
|
|
99
|
+
|
|
100
|
+
let phase = "UPHILL_UNKNOWN";
|
|
101
|
+
if (t1Pass && t0.hasGreen && t0.seesawGreen) {
|
|
102
|
+
phase = "FINISHED";
|
|
103
|
+
} else if (t0.hasGreen) {
|
|
104
|
+
phase = "DOWNHILL_EXECUTION";
|
|
105
|
+
} else if (unknowns === 0) {
|
|
106
|
+
phase = "UPHILL_SOLVED";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const yaml = `scope_id: ${id}\nphase: ${phase}\n`;
|
|
110
|
+
const out = join(hDir, `${id}.yml`);
|
|
111
|
+
let changed = false;
|
|
112
|
+
if (!existsSync(out) || readFileSync(out, "utf8") !== yaml) {
|
|
113
|
+
writeFileSync(out, yaml);
|
|
114
|
+
changed = true;
|
|
115
|
+
}
|
|
116
|
+
report.push({ scope_id: id, phase, changed });
|
|
117
|
+
}
|
|
118
|
+
return report;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const ARGV_SPEC = {
|
|
122
|
+
usage: "hill-derive.mjs --slug <slug> [--cwd <dir>]",
|
|
123
|
+
_: { arity: 0, max: 0, name: "(no positional operands)" },
|
|
124
|
+
slug: { type: "str", required: true },
|
|
125
|
+
cwd: { type: "path" },
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
if (isMain(import.meta.url)) {
|
|
129
|
+
const args = runArgs(ARGV_SPEC);
|
|
130
|
+
const cwd = resolve(args.cwd || process.cwd());
|
|
131
|
+
const report = deriveHill(cwd, args.slug);
|
|
132
|
+
console.log(JSON.stringify(report, null, 2));
|
|
133
|
+
}
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
// discoveries[] → append to .shapeup/<slug>/discovery/ledger.md (old P3.7 / QA H.3)
|
|
11
11
|
// verdict.criteria[] → append evaluation/.verdicts-<target>.jsonl (old evaluator B.0)
|
|
12
12
|
// verdict.refuted[] → un-tick refuted AC boxes + set eval_verdict frontmatter (old B.2/B.2b)
|
|
13
|
-
// escalates[] → queue .shapeup/<slug>/escalates/<order>.json for the orchestrator
|
|
14
13
|
//
|
|
15
14
|
// Zero dependencies, zero network, schema-validated input (a malformed result never mutates
|
|
16
15
|
// the board). Single-writer becomes mechanically true, not aspirational.
|
|
@@ -108,21 +107,18 @@ export function updateBoardRow(indexBody, taskId, done) {
|
|
|
108
107
|
/**
|
|
109
108
|
* Apply one validated WorkResult to the working tree — the single-writer step (D6): ticks AC
|
|
110
109
|
* boxes, flips task status, appends the Execution Log, propagates unblocks, appends discoveries,
|
|
111
|
-
* writes the verdict ledger + un-ticks refuted boxes
|
|
112
|
-
*
|
|
113
|
-
* verdict{criteria[],refuted[]}, escalates[]).
|
|
110
|
+
* writes the verdict ledger + un-ticks refuted boxes.
|
|
111
|
+
* verdict{criteria[],refuted[]}).
|
|
114
112
|
* @param {{cwd:string}} opts - cwd: working-directory root every LOCAL path resolves against.
|
|
115
113
|
* @returns {{slug:string, tasks_updated:string[], acs_ticked:number, unblocked:string[],
|
|
116
|
-
* discoveries_appended:number, refuted_unticked:number, verdict_lines:number
|
|
117
|
-
* escalates_queued:number}} A summary of every write performed.
|
|
114
|
+
* discoveries_appended:number, refuted_unticked:number, verdict_lines:number}} A summary of every write performed.
|
|
118
115
|
* @throws {Error} If a task/board/ledger file it must write is not writable (fs error propagates).
|
|
119
|
-
*
|
|
120
|
-
* `evaluation/.verdicts-*.jsonl`, and `escalates/*.json` under `.shapeup/<slug>/`.
|
|
116
|
+
* `evaluation/.verdicts-*.jsonl` under `.shapeup/<slug>/`.
|
|
121
117
|
*/
|
|
122
118
|
export function applyResult(result, { cwd }) {
|
|
123
119
|
const slug = result.order_id.split("/")[0];
|
|
124
120
|
const local = localRoot(cwd, slug);
|
|
125
|
-
const summary = { slug, tasks_updated: [], acs_ticked: 0, unblocked: [], discoveries_appended: 0, refuted_unticked: 0, verdict_lines: 0
|
|
121
|
+
const summary = { slug, tasks_updated: [], acs_ticked: 0, unblocked: [], discoveries_appended: 0, refuted_unticked: 0, verdict_lines: 0 };
|
|
126
122
|
|
|
127
123
|
// 1. Task results → task files + board (old task-executor P3.1/P3.2/P3.6).
|
|
128
124
|
const boardIndex = join(local, "tasks", "_index.md");
|
|
@@ -228,14 +224,7 @@ export function applyResult(result, { cwd }) {
|
|
|
228
224
|
}
|
|
229
225
|
}
|
|
230
226
|
|
|
231
|
-
|
|
232
|
-
if (result.escalates?.length) {
|
|
233
|
-
const escDir = join(local, "escalates");
|
|
234
|
-
mkdirSync(escDir, { recursive: true });
|
|
235
|
-
const out = join(escDir, `${result.order_id.split("/")[1] || "run"}.json`);
|
|
236
|
-
writeFileSync(out, JSON.stringify(result.escalates, null, 2) + "\n");
|
|
237
|
-
summary.escalates_queued = result.escalates.length;
|
|
238
|
-
}
|
|
227
|
+
|
|
239
228
|
|
|
240
229
|
return summary;
|
|
241
230
|
}
|
|
@@ -265,6 +254,5 @@ if (isMainModule) {
|
|
|
265
254
|
process.exit(1);
|
|
266
255
|
}
|
|
267
256
|
const s = applyResult(result, { cwd });
|
|
268
|
-
console.log(`✅ ingested ${result.order_id} — tasks: [${s.tasks_updated.join(", ")}] · ACs ticked: ${s.acs_ticked} · unblocked: [${s.unblocked.join(", ")}] · discoveries: ${s.discoveries_appended} · verdict lines: ${s.verdict_lines} · refuted un-ticked: ${s.refuted_unticked}
|
|
269
|
-
if (s.escalates_queued) process.exitCode = 0; // escalates are data, not failure
|
|
257
|
+
console.log(`✅ ingested ${result.order_id} — tasks: [${s.tasks_updated.join(", ")}] · ACs ticked: ${s.acs_ticked} · unblocked: [${s.unblocked.join(", ")}] · discoveries: ${s.discoveries_appended} · verdict lines: ${s.verdict_lines} · refuted un-ticked: ${s.refuted_unticked}`);
|
|
270
258
|
}
|