shapeup-sdlc 1.6.2
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 +20 -0
- package/.claude-plugin/marketplace.json +16 -0
- package/.claude-plugin/plugin.json +18 -0
- package/.env.shapeup.example +14 -0
- package/AGENTS.md +133 -0
- package/LICENSE +21 -0
- package/README.md +362 -0
- package/SECURITY.md +72 -0
- package/bin/init.mjs +329 -0
- package/commands/build.md +14 -0
- package/commands/eval.md +15 -0
- package/commands/hammer.md +12 -0
- package/commands/orient.md +11 -0
- package/commands/qa.md +12 -0
- package/commands/retro.md +13 -0
- package/commands/scopes.md +14 -0
- package/commands/shape.md +12 -0
- package/commands/ship.md +53 -0
- package/commands/wire.md +11 -0
- package/hooks/anti-rationalization.mjs +244 -0
- package/hooks/compact-snapshot.mjs +47 -0
- package/hooks/gate-deadline.mjs +151 -0
- package/hooks/gate-intake.mjs +110 -0
- package/hooks/gate-l2.mjs +161 -0
- package/hooks/gate-zerowork.mjs +264 -0
- package/hooks/hooks.json +118 -0
- package/hooks/lib/decision.mjs +183 -0
- package/hooks/safety-spine.mjs +296 -0
- package/hooks/sandbox-guard.mjs +172 -0
- package/hooks/session-rehydrate.mjs +109 -0
- package/hooks/slop-cleaner.mjs +176 -0
- package/oracles/_shared.mjs +46 -0
- package/oracles/http-oracle.mjs +155 -0
- package/oracles/index.mjs +36 -0
- package/oracles/process-oracle.mjs +146 -0
- package/oracles/snapshot-oracle.mjs +119 -0
- package/oracles/test-oracle.mjs +138 -0
- package/package.json +49 -0
- package/skills/advisor-protocol/SKILL.md +171 -0
- package/skills/ba-pitch-analyzer/SKILL.md +175 -0
- package/skills/ba-pitch-analyzer/assets/templates/_index.tmpl.md +84 -0
- package/skills/ba-pitch-analyzer/assets/templates/api-feasibility.tmpl.md +65 -0
- package/skills/ba-pitch-analyzer/assets/templates/assess-report.tmpl.md +127 -0
- package/skills/ba-pitch-analyzer/assets/templates/contracts/be-service.contract.tmpl.md +62 -0
- package/skills/ba-pitch-analyzer/assets/templates/contracts/offline-storage.contract.tmpl.md +92 -0
- package/skills/ba-pitch-analyzer/assets/templates/contracts/third-party-api.contract.tmpl.md +66 -0
- package/skills/ba-pitch-analyzer/assets/templates/cross-context/context-map.tmpl.md +64 -0
- package/skills/ba-pitch-analyzer/assets/templates/cross-context/event-choreography.tmpl.md +77 -0
- package/skills/ba-pitch-analyzer/assets/templates/cross-context/migration-plan.tmpl.md +104 -0
- package/skills/ba-pitch-analyzer/assets/templates/cross-context/team-handoff.tmpl.md +79 -0
- package/skills/ba-pitch-analyzer/assets/templates/domain-model.tmpl.md +79 -0
- package/skills/ba-pitch-analyzer/assets/templates/feedback.tmpl.md +87 -0
- package/skills/ba-pitch-analyzer/assets/templates/integration.tmpl.md +64 -0
- package/skills/ba-pitch-analyzer/assets/templates/run-state.tmpl.md +77 -0
- package/skills/ba-pitch-analyzer/assets/templates/scope-summary.tmpl.md +105 -0
- package/skills/ba-pitch-analyzer/assets/templates/synthesis.tmpl.md +215 -0
- package/skills/ba-pitch-analyzer/assets/templates/task-board.tmpl.md +35 -0
- package/skills/ba-pitch-analyzer/assets/templates/task-spike.tmpl.md +86 -0
- package/skills/ba-pitch-analyzer/assets/templates/task.tmpl.md +84 -0
- package/skills/ba-pitch-analyzer/assets/templates/usecase.tmpl.md +105 -0
- package/skills/ba-pitch-analyzer/assets/templates/ux-behavior.tmpl.md +65 -0
- package/skills/ba-pitch-analyzer/references/contract-patterns.md +152 -0
- package/skills/ba-pitch-analyzer/references/ddd-patterns.md +173 -0
- package/skills/ba-pitch-analyzer/references/doc-schemas.md +275 -0
- package/skills/ba-pitch-analyzer/references/integration-analysis.md +152 -0
- package/skills/ba-pitch-analyzer/references/task-generation.md +623 -0
- package/skills/ba-pitch-analyzer/references/test-surface.md +102 -0
- package/skills/ba-pitch-analyzer/references/ux-behavior-patterns.md +269 -0
- package/skills/ba-pitch-analyzer/scripts/board-derive.mjs +241 -0
- package/skills/ba-pitch-analyzer/scripts/spec-lint.mjs +239 -0
- package/skills/coach/SKILL.md +202 -0
- package/skills/orient/SKILL.md +262 -0
- package/skills/qa-edge-hunter/SKILL.md +369 -0
- package/skills/scope-architect/SKILL.md +111 -0
- package/skills/scope-hammer/SKILL.md +186 -0
- package/skills/shapeup/SKILL.md +403 -0
- package/skills/shapeup/resources/breadboard-reflection.md +88 -0
- package/skills/shapeup/resources/breadboarding.md +334 -0
- package/skills/shapeup/resources/context-compaction.md +240 -0
- package/skills/shapeup/resources/framing-doc.md +92 -0
- package/skills/shapeup/resources/kickoff-doc.md +95 -0
- package/skills/shapeup/resources/shaping.md +194 -0
- package/skills/shapeup/resources/spike.md +86 -0
- package/skills/solution-architect/SKILL.md +129 -0
- package/skills/spec-evaluator/README.md +93 -0
- package/skills/spec-evaluator/SKILL.md +213 -0
- package/skills/spec-evaluator/references/anti-leniency.md +50 -0
- package/skills/spec-evaluator/references/dimension-contract.md +126 -0
- package/skills/spec-evaluator/references/dimensions/_registry.md +40 -0
- package/skills/spec-evaluator/references/dimensions/completeness.md +102 -0
- package/skills/spec-evaluator/references/dimensions/integration.md +129 -0
- package/skills/spec-evaluator/references/dimensions/performance.md +48 -0
- package/skills/spec-evaluator/references/dimensions/security.md +60 -0
- package/skills/spec-evaluator/references/dimensions/spec-conformance.md +134 -0
- package/skills/spec-evaluator/references/dimensions/tdd-surface.md +110 -0
- package/skills/spec-evaluator/references/dimensions/test-surface-conformance.md +100 -0
- package/skills/spec-evaluator/references/probing.md +177 -0
- package/skills/spec-evaluator/references/report-schema.md +101 -0
- package/skills/spec-evaluator/references/verdict-ledger.md +92 -0
- package/skills/spec-evaluator/scripts/verdict-ledger.mjs +166 -0
- package/skills/task-executor/SKILL.md +194 -0
- package/skills/tech-lead/README.md +71 -0
- package/skills/tech-lead/SKILL.md +459 -0
- package/skills/tech-lead/references/delegation.md +254 -0
- package/skills/tech-lead/references/gates.md +379 -0
- package/skills/tech-lead/references/invocation.md +45 -0
- package/skills/tech-lead/references/ledger-schema.md +214 -0
- package/skills/tech-lead/references/round-protocol.md +184 -0
- package/skills/tech-lead/references/state-model.md +66 -0
- package/skills/tech-lead/references/tiny-lane.md +52 -0
- package/skills/tech-lead/schemas/domain.schema.json +2294 -0
- package/skills/tech-lead/schemas/gate-answers.schema.json +92 -0
- package/skills/tech-lead/schemas/work-order.schema.json +21 -0
- package/skills/tech-lead/schemas/work-result.schema.json +40 -0
- package/skills/tech-lead/scripts/aegis-digest.mjs +124 -0
- package/skills/tech-lead/scripts/budget-check.mjs +156 -0
- package/skills/tech-lead/scripts/compile-order.mjs +518 -0
- package/skills/tech-lead/scripts/fit-check.mjs +196 -0
- package/skills/tech-lead/scripts/gate-answers.mjs +338 -0
- package/skills/tech-lead/scripts/ingest-result.mjs +270 -0
- package/skills/tech-lead/scripts/init-run.mjs +326 -0
- package/skills/tech-lead/scripts/lib/argv.mjs +224 -0
- package/skills/tech-lead/scripts/lib/contract-md.mjs +481 -0
- package/skills/tech-lead/scripts/lib/is-main.mjs +82 -0
- package/skills/tech-lead/scripts/lib/paths.mjs +278 -0
- package/skills/tech-lead/scripts/lib/ratchet-tree.mjs +112 -0
- package/skills/tech-lead/scripts/run-snapshot.mjs +269 -0
- package/skills/tech-lead/scripts/ship-report.mjs +275 -0
- package/skills/tech-lead/scripts/stats.mjs +423 -0
- package/skills/tech-lead/scripts/t0-verify.mjs +470 -0
- package/skills/tech-lead/scripts/trace-lint.mjs +367 -0
- package/skills/tech-lead/scripts/validate-envelope.mjs +283 -0
- package/skills/translator/README.md +66 -0
- package/skills/translator/SKILL.md +258 -0
- package/skills/translator/references/preservation-rules.md +102 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://github.com/nguyenvanphituoc/shapeup-sdlc-plugin/schemas/gate-answers.schema.json",
|
|
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 (measured: shapeup-sdlc DNF at 1800s on benchmark F3), while an unattended run with one produces the same audit trail an interactive run does.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["version", "preset", "answers"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"version": { "const": 1 },
|
|
11
|
+
"preset": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"enum": ["ci", "guarded", "interactive", "custom"],
|
|
14
|
+
"description": "Provenance of this set. 'ci' = every gate pre-approved (headless); 'guarded' = low-risk gates pre-approved, judgment gates still ask; 'interactive' = nothing pre-approved (the default lane); 'custom' = hand-authored."
|
|
15
|
+
},
|
|
16
|
+
"authorized_by": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "Who pre-approved these decisions. Free text, recorded verbatim in the round ledger so a shipped run can always name the human behind its sign-off."
|
|
19
|
+
},
|
|
20
|
+
"on_missing": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"enum": ["ask", "proceed", "abort"],
|
|
23
|
+
"default": "ask",
|
|
24
|
+
"description": "What to do at a gate this set has no answer for. 'abort' is the correct value for headless lanes: a fast attributable failure beats a silent stall that reads as a slow run."
|
|
25
|
+
},
|
|
26
|
+
"escalation": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"additionalProperties": false,
|
|
29
|
+
"description": "Policy for a worker's structured ESCALATE (advisor-protocol), which is a gate in all but name.",
|
|
30
|
+
"properties": {
|
|
31
|
+
"default": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"enum": ["ask", "assume-and-record", "abort"],
|
|
34
|
+
"default": "ask"
|
|
35
|
+
},
|
|
36
|
+
"budget_per_scope_per_round": { "type": "integer", "minimum": 0, "default": 2 }
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"answers": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"description": "Gate id → decision. Ids match the ⏸ GATE blocks the orchestrator emits.",
|
|
42
|
+
"additionalProperties": false,
|
|
43
|
+
"properties": {
|
|
44
|
+
"L0": { "$ref": "#/definitions/answer" },
|
|
45
|
+
"L1a": { "$ref": "#/definitions/answer" },
|
|
46
|
+
"L1a.5": { "$ref": "#/definitions/answer" },
|
|
47
|
+
"L1b": { "$ref": "#/definitions/answer" },
|
|
48
|
+
"L2": { "$ref": "#/definitions/answer" },
|
|
49
|
+
"L3": { "$ref": "#/definitions/answer" },
|
|
50
|
+
"QA": { "$ref": "#/definitions/answer" },
|
|
51
|
+
"H": { "$ref": "#/definitions/answer" },
|
|
52
|
+
"L4": { "$ref": "#/definitions/answer" },
|
|
53
|
+
"COACH-1":{ "$ref": "#/definitions/answer" }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"definitions": {
|
|
58
|
+
"answer": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"required": ["decision"],
|
|
61
|
+
"additionalProperties": false,
|
|
62
|
+
"properties": {
|
|
63
|
+
"decision": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"enum": [
|
|
66
|
+
"proceed",
|
|
67
|
+
"ask",
|
|
68
|
+
"abort",
|
|
69
|
+
"loop",
|
|
70
|
+
"stop",
|
|
71
|
+
"ship",
|
|
72
|
+
"hold",
|
|
73
|
+
"accept-cut-list",
|
|
74
|
+
"ship-all",
|
|
75
|
+
"run",
|
|
76
|
+
"skip"
|
|
77
|
+
],
|
|
78
|
+
"description": "proceed/ask/abort apply to review gates (L0, L1a, L1a.5, L1b, L2). L3 takes loop|stop|ask. QA takes run|skip. H takes accept-cut-list|ship-all|ask. L4 takes ship|hold|ask. COACH-1 takes skip|ask."
|
|
79
|
+
},
|
|
80
|
+
"note": {
|
|
81
|
+
"type": "string",
|
|
82
|
+
"description": "Recorded in the ledger next to the decision. This is where the pre-approval's reasoning lives, and it is the difference between an audited bypass and a rubber stamp."
|
|
83
|
+
},
|
|
84
|
+
"max_rounds": {
|
|
85
|
+
"type": "integer",
|
|
86
|
+
"minimum": 1,
|
|
87
|
+
"description": "L3 only — caps the loop decision. Redundant with the run config on purpose: an answer set handed to a run must not be able to authorize more rounds than it names."
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "work-order.schema.json",
|
|
3
|
+
"title": "WorkOrder",
|
|
4
|
+
"description": "The orchestrator → worker envelope (pure-skill architecture v1.0). Compiled by skills/tech-lead/scripts/compile-order.mjs, validated by validate-envelope.mjs before any worker dispatch. A worker depends only on this envelope — never on filesystem topology, run-state format, board schema, or another worker. Path: .shapeup/<slug>/orders/r<N>-a<M>.json (or <slug>/orders/<operation>.json for non-attempt work). Every record type and payload field is DEFINED CENTRALLY in domain.schema.json ($defs + x-payload-by-worker) — this file only shapes the envelope; it never re-defines a domain entity.",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["schema_version", "order_id", "worker", "mode", "payload"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"schema_version": { "type": "integer", "enum": [1] },
|
|
9
|
+
"order_id": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "\"<slug>/r<N>-a<M>\" for build attempts, \"<slug>/<operation>[-r<N>]\" otherwise",
|
|
12
|
+
"pattern": "^[a-z0-9][a-z0-9-]*/[a-z0-9][A-Za-z0-9.-]*$"
|
|
13
|
+
},
|
|
14
|
+
"worker": { "$ref": "domain.schema.json#/$defs/WorkerName" },
|
|
15
|
+
"mode": { "type": "string", "enum": ["orchestrated", "standalone"] },
|
|
16
|
+
"operation": { "$ref": "domain.schema.json#/$defs/Operation" },
|
|
17
|
+
"interaction": { "$ref": "domain.schema.json#/$defs/Interaction" },
|
|
18
|
+
"substrate": { "$ref": "domain.schema.json#/$defs/Substrate" },
|
|
19
|
+
"payload": { "$ref": "domain.schema.json#/$defs/WorkOrderPayload" }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "work-result.schema.json",
|
|
3
|
+
"title": "WorkResult",
|
|
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
|
+
"type": "object",
|
|
6
|
+
"required": ["schema_version", "order_id", "status"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"schema_version": { "type": "integer", "enum": [1] },
|
|
9
|
+
"order_id": { "type": "string", "description": "Copied verbatim from the answered WorkOrder — the 1:1 join key." },
|
|
10
|
+
"worker": { "type": "string" },
|
|
11
|
+
"status": { "type": "string", "enum": ["done", "partial", "escalated", "failed"] },
|
|
12
|
+
"task_results": {
|
|
13
|
+
"type": "array",
|
|
14
|
+
"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": { "$ref": "domain.schema.json#/$defs/TaskResult" }
|
|
16
|
+
},
|
|
17
|
+
"files_touched": {
|
|
18
|
+
"type": "array",
|
|
19
|
+
"items": { "$ref": "domain.schema.json#/$defs/FileTouched" }
|
|
20
|
+
},
|
|
21
|
+
"escalates": {
|
|
22
|
+
"type": "array",
|
|
23
|
+
"description": "advisor-protocol ESCALATE blocks — the worker's one outward port.",
|
|
24
|
+
"items": { "$ref": "domain.schema.json#/$defs/Escalate" }
|
|
25
|
+
},
|
|
26
|
+
"discoveries": {
|
|
27
|
+
"type": "array",
|
|
28
|
+
"description": "Raw discovered lines. ingest-result appends them to the discovery ledger — the worker never touches the ledger itself.",
|
|
29
|
+
"items": { "$ref": "domain.schema.json#/$defs/Discovery" }
|
|
30
|
+
},
|
|
31
|
+
"verdict": { "$ref": "domain.schema.json#/$defs/Verdict" },
|
|
32
|
+
"artifacts": {
|
|
33
|
+
"type": "array",
|
|
34
|
+
"description": "Domain artifacts the worker wrote inside its substrate (spec docs, orient/ files, reports).",
|
|
35
|
+
"items": { "type": "string" }
|
|
36
|
+
},
|
|
37
|
+
"assumptions": { "type": "array", "items": { "type": "string" } },
|
|
38
|
+
"deviations": { "type": "array", "items": { "type": "string" } }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// AEGIS digester (design spec v1.1 §3.4/§4.5, PA6 countermeasure).
|
|
3
|
+
//
|
|
4
|
+
// Distills raw build/test/Playwright logs into {file, line, core_message} triples so a
|
|
5
|
+
// task-executor's next attempt gets a few lines of signal instead of a full stack-trace dump.
|
|
6
|
+
// Script-first by design: regex over known log formats is free (no model tokens); an
|
|
7
|
+
// unrecognized line becomes a "raw" triple (file/line unknown) rather than being silently
|
|
8
|
+
// dropped, so a Sonnet fallback (or a human) still has something to look at — this module never
|
|
9
|
+
// invents a file:line it didn't find in the text.
|
|
10
|
+
//
|
|
11
|
+
// Zero dependencies, zero network — same discipline as oracles/*.
|
|
12
|
+
|
|
13
|
+
import { isMain } from "./lib/is-main.mjs";
|
|
14
|
+
import { runArgs } from "./lib/argv.mjs";
|
|
15
|
+
|
|
16
|
+
const PATTERNS = [
|
|
17
|
+
// Node stack frame: " at fn (path/to/file.js:12:34)" or " at path/to/file.js:12:34"
|
|
18
|
+
{ re: /^\s*at\s+(?:[\w.$<>\[\] ]+\s+\()?(.+?):(\d+):\d+\)?\s*$/, kind: "stack-frame" },
|
|
19
|
+
// TAP/test-runner failure: "✗ some test name (path/to/file.js:12)" or "not ok N - msg (file:line)"
|
|
20
|
+
{ re: /^(?:✗|not ok\b.*?)[^()]*\((.+?):(\d+)\)\s*$/, kind: "test-failure" },
|
|
21
|
+
// ESLint/tsc style: "path/to/file.ts:12:34 - error TS2345: message"
|
|
22
|
+
{ re: /^(.+?):(\d+):\d+\s*[-–]\s*(?:error|warning)\b.*$/, kind: "compiler-diagnostic" },
|
|
23
|
+
// Generic "Error: message" line followed later by a stack — capture the message alone.
|
|
24
|
+
{ re: /^\s*(?:Error|TypeError|ReferenceError|AssertionError)\s*:\s*(.+)$/, kind: "error-message" },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Normalize a log line into a compact core message.
|
|
29
|
+
* @param {string} line - A raw log line.
|
|
30
|
+
* @returns {string} The line trimmed, whitespace-collapsed, and capped at 200 chars.
|
|
31
|
+
*/
|
|
32
|
+
function coreMessage(line) {
|
|
33
|
+
return line.trim().replace(/\s+/g, " ").slice(0, 200);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Distill raw build/test/Playwright log text into deduped {file,line,core_message,kind} triples —
|
|
38
|
+
* never inventing a file:line it did not find in the text.
|
|
39
|
+
* @param {string} rawText - The raw log output ("" / null tolerated).
|
|
40
|
+
* @returns {Array<{file:(string|null), line:(number|null), core_message:string, kind:string}>}
|
|
41
|
+
* One triple per recognized failure/diagnostic (kind ∈ stack-frame|test-failure|
|
|
42
|
+
* compiler-diagnostic|error-message); unmatched error messages become file-less triples.
|
|
43
|
+
*/
|
|
44
|
+
export function digest(rawText) {
|
|
45
|
+
const lines = (rawText || "").split(/\r?\n/);
|
|
46
|
+
const triples = [];
|
|
47
|
+
let pendingMessage = null;
|
|
48
|
+
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
if (!line.trim()) continue;
|
|
51
|
+
|
|
52
|
+
let matched = false;
|
|
53
|
+
for (const { re, kind } of PATTERNS) {
|
|
54
|
+
const m = line.match(re);
|
|
55
|
+
if (!m) continue;
|
|
56
|
+
matched = true;
|
|
57
|
+
if (kind === "error-message") {
|
|
58
|
+
pendingMessage = coreMessage(m[1]);
|
|
59
|
+
continue; // wait for the stack frame that follows to get a file:line
|
|
60
|
+
}
|
|
61
|
+
const file = m[1]?.trim();
|
|
62
|
+
const lineNo = m[2] ? Number(m[2]) : null;
|
|
63
|
+
triples.push({
|
|
64
|
+
file: file || null,
|
|
65
|
+
line: lineNo,
|
|
66
|
+
core_message: pendingMessage || coreMessage(line),
|
|
67
|
+
kind,
|
|
68
|
+
});
|
|
69
|
+
pendingMessage = null;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
if (!matched && pendingMessage) {
|
|
73
|
+
// an error message with no stack frame ever followed — keep it as file-less signal
|
|
74
|
+
triples.push({ file: null, line: null, core_message: pendingMessage, kind: "error-message" });
|
|
75
|
+
pendingMessage = null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (pendingMessage) {
|
|
79
|
+
triples.push({ file: null, line: null, core_message: pendingMessage, kind: "error-message" });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// De-duplicate identical (file, line, core_message) triples — repeated retries/log noise.
|
|
83
|
+
const seen = new Set();
|
|
84
|
+
return triples.filter((t) => {
|
|
85
|
+
const key = `${t.file}:${t.line}:${t.core_message}`;
|
|
86
|
+
if (seen.has(key)) return false;
|
|
87
|
+
seen.add(key);
|
|
88
|
+
return true;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --- CLI ---------------------------------------------------------------------
|
|
93
|
+
/** The typed argv contract (see `./lib/argv.mjs`). A bare `-`, or nothing, means stdin. */
|
|
94
|
+
export const ARGV_SPEC = {
|
|
95
|
+
usage: "aegis-digest.mjs [<log-file>|-] (no file → stdin)",
|
|
96
|
+
_: { arity: 0, max: 1, name: "log-file" },
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* CLI entry: read a log from a file arg or stdin, print the digested triples as JSON, exit 0.
|
|
101
|
+
* @returns {Promise<void>} Resolves after printing.
|
|
102
|
+
*/
|
|
103
|
+
async function main() {
|
|
104
|
+
const arg = runArgs(ARGV_SPEC)._[0];
|
|
105
|
+
let raw;
|
|
106
|
+
if (arg && arg !== "-") {
|
|
107
|
+
const { readFileSync } = await import("node:fs");
|
|
108
|
+
raw = readFileSync(arg, "utf8");
|
|
109
|
+
} else {
|
|
110
|
+
raw = await new Promise((resolve) => {
|
|
111
|
+
let d = "";
|
|
112
|
+
process.stdin.on("data", (c) => (d += c));
|
|
113
|
+
process.stdin.on("end", () => resolve(d));
|
|
114
|
+
process.stdin.on("error", () => resolve(""));
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const triples = digest(raw);
|
|
118
|
+
console.log(JSON.stringify(triples, null, 2));
|
|
119
|
+
process.exit(0);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (isMain(import.meta.url)) {
|
|
123
|
+
main();
|
|
124
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// THE THIRD BREAKER — wall clock. Derived, never claimed; checked at every round boundary.
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS EXISTS (measured, and it corrects an earlier diagnosis).
|
|
5
|
+
//
|
|
6
|
+
// SDD harness benchmark, F3 (wiring trap, Sonnet 5): this harness was killed at the declared
|
|
7
|
+
// 1800 s cap and published as a DNF. The obvious reading — the one the first write-up implied —
|
|
8
|
+
// was that it had stalled at a gate with no human to sign off.
|
|
9
|
+
//
|
|
10
|
+
// Re-reading the retained transcript with transcript-level metrics says otherwise, and the
|
|
11
|
+
// numbers are not close:
|
|
12
|
+
//
|
|
13
|
+
// 327 assistant turns · 262 tool calls · 130 work calls · 37 file writes
|
|
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.
|
|
19
|
+
//
|
|
20
|
+
// That makes the defect specific: **both existing breakers count events, not time.**
|
|
21
|
+
// `round_budget` decrements once per round; `attempt_budget` decrements once per T0 attempt.
|
|
22
|
+
// Neither can observe that round 1 has been running for twenty-nine minutes. A run can burn its
|
|
23
|
+
// entire wall-clock budget without either breaker moving a single tick, and when the harness is
|
|
24
|
+
// killed from outside, it ships nothing — not even the scopes that were already green.
|
|
25
|
+
//
|
|
26
|
+
// So the fix is not more gate automation (there was no stall to automate away). It is a third
|
|
27
|
+
// breaker on the axis nobody was watching:
|
|
28
|
+
//
|
|
29
|
+
// OUTER round_budget — rounds, the six-week-timebox analog
|
|
30
|
+
// INNER attempt_budget — T0 attempts per scope
|
|
31
|
+
// DEADLINE wall_clock_budget — elapsed seconds, this file
|
|
32
|
+
//
|
|
33
|
+
// Tripping the deadline breaker does NOT kill the run. It routes to GATE H (scope-hammer): the
|
|
34
|
+
// census runs, the cut list is compared against the baseline, and whatever is green ships. That
|
|
35
|
+
// is the entire difference between an external SIGTERM and a circuit breaker — one produces a
|
|
36
|
+
// dash in every column, the other produces a smaller feature that works.
|
|
37
|
+
//
|
|
38
|
+
// Non-regression by construction: with no `wall_clock_budget_s` in the run config this reports
|
|
39
|
+
// `off` and nothing changes. Existing runs behave exactly as before.
|
|
40
|
+
//
|
|
41
|
+
// USAGE
|
|
42
|
+
// node budget-check.mjs [--slug <slug>] [--cwd <root>] [--at <ISO>] # status, exit 0
|
|
43
|
+
// node budget-check.mjs --strict … # exit 6 when the breaker has tripped
|
|
44
|
+
//
|
|
45
|
+
// Output: { status, elapsed_s, budget_s, remaining_s, used_fraction, action }
|
|
46
|
+
// status ∈ off | ok | warn | trip
|
|
47
|
+
// action = the sentence the orchestrator must act on at the round boundary.
|
|
48
|
+
|
|
49
|
+
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
|
50
|
+
import { join } from "node:path";
|
|
51
|
+
import { isMain } from "./lib/is-main.mjs";
|
|
52
|
+
import { runArgs } from "./lib/argv.mjs";
|
|
53
|
+
import { localDir } from "./lib/paths.mjs";
|
|
54
|
+
|
|
55
|
+
/** Fraction of the budget at which the run should stop STARTING work it cannot finish. */
|
|
56
|
+
export const WARN_AT = 0.75;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Decide the breaker state from elapsed time against a budget. Pure, so the structural tests can
|
|
60
|
+
* assert every boundary without waiting for a clock.
|
|
61
|
+
*
|
|
62
|
+
* @param {number} elapsedS - Seconds since the run receipt was written.
|
|
63
|
+
* @param {number|null} budgetS - Configured wall-clock budget, or null/0 when the breaker is off.
|
|
64
|
+
* @returns {{status: string, elapsed_s: number, budget_s: number|null, remaining_s: number|null,
|
|
65
|
+
* used_fraction: number|null, action: string}}
|
|
66
|
+
*/
|
|
67
|
+
export function evaluateBudget(elapsedS, budgetS) {
|
|
68
|
+
const elapsed = Math.max(0, Math.round(elapsedS));
|
|
69
|
+
if (!budgetS || budgetS <= 0) {
|
|
70
|
+
return {
|
|
71
|
+
status: "off", elapsed_s: elapsed, budget_s: null, remaining_s: null, used_fraction: null,
|
|
72
|
+
action: "No wall-clock budget configured — the deadline breaker is off. Round and attempt budgets still apply.",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const remaining = Math.round(budgetS - elapsed);
|
|
76
|
+
const used = Number((elapsed / budgetS).toFixed(3));
|
|
77
|
+
|
|
78
|
+
if (remaining <= 0) {
|
|
79
|
+
return {
|
|
80
|
+
status: "trip", elapsed_s: elapsed, budget_s: budgetS, remaining_s: remaining, used_fraction: used,
|
|
81
|
+
action:
|
|
82
|
+
"DEADLINE BREAKER TRIPPED. Do not start another build round or another scope. Go straight to " +
|
|
83
|
+
"GATE H (delegate scope-hammer with --breaker deadline): run the census, compare the shippable " +
|
|
84
|
+
"subset against the baseline, and ship what is green. A run killed from outside ships nothing; " +
|
|
85
|
+
"a run that trips its own breaker ships the part that works.",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (used >= WARN_AT) {
|
|
89
|
+
return {
|
|
90
|
+
status: "warn", elapsed_s: elapsed, budget_s: budgetS, remaining_s: remaining, used_fraction: used,
|
|
91
|
+
action:
|
|
92
|
+
`${remaining}s of ${budgetS}s remain (${Math.round(used * 100)}% spent). Finish and T0-verify the ` +
|
|
93
|
+
"scope in flight; do NOT open a new scope or a new build round. The next boundary is where the " +
|
|
94
|
+
"breaker trips, and work started now will be cut at GATE H rather than shipped.",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
status: "ok", elapsed_s: elapsed, budget_s: budgetS, remaining_s: remaining, used_fraction: used,
|
|
99
|
+
action: `${remaining}s remain of ${budgetS}s. Proceed.`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Locate the active run's receipt — the only place `started_at` is recorded. */
|
|
104
|
+
export function findRun(cwd, slug = null) {
|
|
105
|
+
const root = localDir(cwd);
|
|
106
|
+
if (!existsSync(root)) return null;
|
|
107
|
+
if (!slug) {
|
|
108
|
+
try {
|
|
109
|
+
const ptr = JSON.parse(readFileSync(join(root, "active-scope"), "utf8"));
|
|
110
|
+
slug = ptr?.slug || null;
|
|
111
|
+
} catch { /* no pointer — fall through to a scan */ }
|
|
112
|
+
}
|
|
113
|
+
const candidates = slug ? [slug] : (() => { try { return readdirSync(root); } catch { return []; } })();
|
|
114
|
+
for (const entry of candidates) {
|
|
115
|
+
const p = join(root, entry, "receipt.json");
|
|
116
|
+
if (!existsSync(p)) continue;
|
|
117
|
+
try { return { slug: entry, receipt: JSON.parse(readFileSync(p, "utf8")) }; }
|
|
118
|
+
catch { /* unreadable receipt is not a run */ }
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The typed argv contract (see `./lib/argv.mjs`). */
|
|
124
|
+
export const ARGV_SPEC = {
|
|
125
|
+
usage: "budget-check.mjs [--slug <slug>] [--cwd <dir>] [--at <iso8601>] [--strict]",
|
|
126
|
+
_: { arity: 0, max: 0, name: "(no positional operands)" },
|
|
127
|
+
slug: { type: "str" },
|
|
128
|
+
cwd: { type: "path" },
|
|
129
|
+
at: { type: "str" },
|
|
130
|
+
strict: { type: "flag" },
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export function main() {
|
|
134
|
+
const args = runArgs(ARGV_SPEC);
|
|
135
|
+
const cwd = args.cwd || process.cwd();
|
|
136
|
+
const run = findRun(cwd, args.slug ?? null);
|
|
137
|
+
if (!run) {
|
|
138
|
+
console.error("no run receipt found — open the run with init-run.mjs first (GATE L0.1).");
|
|
139
|
+
process.exit(2);
|
|
140
|
+
}
|
|
141
|
+
const startedAt = Date.parse(run.receipt.started_at || "");
|
|
142
|
+
if (Number.isNaN(startedAt)) {
|
|
143
|
+
console.error(`receipt for "${run.slug}" has no parseable started_at — cannot derive elapsed time.`);
|
|
144
|
+
process.exit(2);
|
|
145
|
+
}
|
|
146
|
+
const now = args.at ? Date.parse(args.at) : Date.now();
|
|
147
|
+
const budget = Number(run.receipt.config?.wall_clock_budget_s || 0) || null;
|
|
148
|
+
const result = evaluateBudget((now - startedAt) / 1000, budget);
|
|
149
|
+
|
|
150
|
+
console.log(JSON.stringify({ slug: run.slug, started_at: run.receipt.started_at, ...result }, null, 2));
|
|
151
|
+
process.exit(args.strict && result.status === "trip" ? 6 : 0);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (isMain(import.meta.url)) {
|
|
155
|
+
main();
|
|
156
|
+
}
|