@haiyangbg/buildbeat 3.3.1 → 4.1.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.
Files changed (102) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.en.md +20 -124
  3. package/README.md +20 -124
  4. package/SKILL.md +73 -84
  5. package/docs/CAPABILITY-MATRIX.md +30 -39
  6. package/docs/MIGRATION.md +93 -0
  7. package/docs/README.md +8 -37
  8. package/docs/RELEASING.md +4 -3
  9. package/docs/v2/RFC-0001-product-definition.md +3 -0
  10. package/docs/v2/RFC-0002-domain-model.md +3 -0
  11. package/docs/v2/RFC-0003-workflow-policy.md +3 -0
  12. package/docs/v2/SPEC-0001-events-v1.md +3 -0
  13. package/docs/v2/guide/00-how-to-talk.en.md +12 -56
  14. package/docs/v2/guide/00-how-to-talk.md +12 -56
  15. package/docs/v2/guide/01-quickstart.en.md +42 -111
  16. package/docs/v2/guide/01-quickstart.md +42 -111
  17. package/docs/v2/guide/02-workflow-guide.en.md +13 -123
  18. package/docs/v2/guide/02-workflow-guide.md +14 -122
  19. package/docs/v2/guide/03-policy-guide.en.md +6 -50
  20. package/docs/v2/guide/03-policy-guide.md +6 -50
  21. package/docs/v2/guide/04-adapter-guide.en.md +7 -60
  22. package/docs/v2/guide/04-adapter-guide.md +7 -60
  23. package/docs/v2/guide/05-worker-contract.en.md +6 -53
  24. package/docs/v2/guide/05-worker-contract.md +7 -52
  25. package/docs/v2/guide/06-evidence-guide.en.md +8 -44
  26. package/docs/v2/guide/06-evidence-guide.md +9 -43
  27. package/docs/v2/guide/07-approval-guide.en.md +8 -110
  28. package/docs/v2/guide/07-approval-guide.md +8 -121
  29. package/docs/v2/guide/09-security-boundaries.en.md +13 -37
  30. package/docs/v2/guide/09-security-boundaries.md +13 -37
  31. package/docs/v2/guide/10-recovery.en.md +12 -96
  32. package/docs/v2/guide/10-recovery.md +12 -96
  33. package/docs/v2/guide/11-session-handoff.en.md +10 -75
  34. package/docs/v2/guide/11-session-handoff.md +10 -75
  35. package/docs/v2/guide/12-without-runtime.en.md +46 -0
  36. package/docs/v2/guide/12-without-runtime.md +46 -0
  37. package/docs/v2/guide/README.en.md +16 -36
  38. package/docs/v2/guide/README.md +16 -36
  39. package/example/AGENTS.md +15 -69
  40. package/example/README.md +8 -23
  41. package/example/delivery/envelope/prompts/builder.md +3 -1
  42. package/example/delivery/envelope/prompts/fixer.md +3 -1
  43. package/example/delivery/envelope/prompts/reviewer.md +3 -1
  44. package/example/delivery/work/WORK-EXPORT-DATE-FILTER/run-config.yaml +1 -1
  45. package/example//346/214/207/346/214/245/345/217/260.md +8 -40
  46. package/package.json +2 -1
  47. package/src/v2/adapters/notification-payload.js +40 -0
  48. package/src/v2/adapters/shell.js +3 -1
  49. package/src/v2/cli/run-config-check.js +129 -45
  50. package/src/v2/cli/run.js +853 -254
  51. package/src/v2/engine/reducer.js +2 -0
  52. package/src/v2/engine/risk-preset.js +14 -47
  53. package/src/v2/engine/workflow.js +177 -185
  54. package/src/v2/evidence/collector.js +12 -1
  55. package/src/v2/evidence/image.js +346 -0
  56. package/src/v2/policy/policy.js +219 -286
  57. package/src/v2/presets/software-delivery.yaml +1 -16
  58. package/src/v2/runtime/cache.js +22 -1
  59. package/src/v2/runtime/decisions.js +185 -31
  60. package/src/v2/runtime/findings.js +4 -1
  61. package/src/v2/runtime/notify.js +42 -36
  62. package/src/v2/runtime/orchestrator.js +811 -297
  63. package/src/v2/runtime/overview-repos.js +79 -0
  64. package/src/v2/runtime/overview.js +279 -47
  65. package/src/v2/runtime/release.js +233 -0
  66. package/src/v2/runtime/run-record.js +1 -0
  67. package/src/v2/workspace/workspace-manager.js +8 -0
  68. package/templates/v2/AGENTS.md +20 -73
  69. package/templates/v2/BUILDBEAT.md +2 -13
  70. package/templates/v2/envelope/prompts/builder.md +3 -1
  71. package/templates/v2/envelope/prompts/fixer.md +3 -1
  72. package/templates/v2/envelope/prompts/reviewer.md +3 -1
  73. package/templates/v2/run-config.example.yaml +9 -7
  74. package/templates/v2/work.example.md +16 -0
  75. package/templates/v2//346/214/207/346/214/245/345/217/260.md +8 -40
  76. package/docs/v2/skill/01-principles.md +0 -26
  77. package/docs/v2/skill/02-project-layout.md +0 -31
  78. package/docs/v2/skill/03-collaboration-rules.md +0 -45
  79. package/docs/v2/skill/04-rhythm-and-rituals.md +0 -104
  80. package/docs/v2/skill/05-red-lines.md +0 -13
  81. package/docs/v2/skill/06-bootstrap-and-takeover.md +0 -84
  82. package/docs/v2/skill/07-templates-and-lessons.md +0 -24
  83. package/example/.buildbeat/observe.yaml +0 -31
  84. package/src/v2/observe/observe-config.js +0 -194
  85. package/src/v2/observe/observe-reducer.js +0 -117
  86. package/src/v2/observe/observe.js +0 -427
  87. package/src/v2/presets/observe.yaml +0 -45
  88. package/src/v2/presets/policies/ui-render-gate.yaml +0 -13
  89. package/src/v2/presets/release-readback.yaml +0 -36
  90. package/src/v2/presets/risk/controlled.yaml +0 -39
  91. package/src/v2/presets/risk/fast.yaml +0 -19
  92. package/src/v2/presets/risk/release.yaml +0 -21
  93. package/src/v2/presets/risk/standard.yaml +0 -28
  94. package/templates/ARCHITECTURE.md +0 -50
  95. package/templates/contracts/PROTOCOL.md +0 -28
  96. package/templates/pm/adr/ADR-0000-template.md +0 -25
  97. package/templates/pm/adr/README.md +0 -15
  98. package/templates/pm/decisions.md +0 -11
  99. package/templates/standards/CODE.md +0 -23
  100. package/templates/standards/DESIGN.md +0 -36
  101. package/templates/standards/REVIEW.md +0 -20
  102. package/templates/standards/STACK.md +0 -31
@@ -70,6 +70,7 @@ export function applyEvent(state, event) {
70
70
  entry: data.entry ?? null,
71
71
  planDigest: data.planDigest ?? "UNVERIFIED",
72
72
  intentDigest: data.intentDigest ?? "UNVERIFIED",
73
+ ...(data.deliveryChecks ? { deliveryChecks: data.deliveryChecks } : {}),
73
74
  };
74
75
  break;
75
76
  }
@@ -162,6 +163,7 @@ export function applyEvent(state, event) {
162
163
  ? { suppressedFingerprints: data.suppressedFingerprints }
163
164
  : {}),
164
165
  ...(data.cacheKey ? { cacheKey: data.cacheKey } : {}),
166
+ ...(data.source ? { source: data.source } : {}),
165
167
  ...(data.reused ? { reused: data.reused } : {}),
166
168
  });
167
169
  break;
@@ -1,48 +1,15 @@
1
- // Risk presets per docs/v2/RFC-0003-workflow-policy.md §7: fast / standard /
2
- // controlled, plus release. A preset is data —
3
- // human boundaries (stopAt) and a policy set — never kernel-fixed gates.
4
-
5
- import { readFileSync } from "node:fs";
6
- import { join } from "node:path";
7
- import { fileURLToPath } from "node:url";
8
-
9
- import { PolicyError, parsePolicyDoc } from "../policy/policy.js";
10
- import { parseYamlSubset } from "./yaml-subset.js";
11
-
12
- const PRESET_DIR = fileURLToPath(new URL("../presets/risk/", import.meta.url));
13
- const PRESET_FIELDS = new Set(["kind", "version", "name", "stopAt", "policies"]);
14
-
15
- export function parseRiskPreset(doc) {
16
- if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
17
- throw new PolicyError("risk preset must be a map");
18
- }
19
- for (const key of Object.keys(doc)) {
20
- if (!PRESET_FIELDS.has(key)) {
21
- throw new PolicyError(`unknown field "${key}" in risk preset ${doc.name ?? "(unnamed)"}`);
22
- }
23
- }
24
- if (doc.kind !== "risk-preset" || doc.version !== 1) {
25
- throw new PolicyError("risk preset requires kind: risk-preset and version: 1");
26
- }
27
- if (typeof doc.name !== "string" || doc.name.length === 0) {
28
- throw new PolicyError("risk preset needs a name");
29
- }
30
- const stopAt = doc.stopAt ?? [];
31
- if (!Array.isArray(stopAt) || stopAt.some((step) => typeof step !== "string")) {
32
- throw new PolicyError(`risk preset ${doc.name}: stopAt must be a list of step ids`);
33
- }
34
- const policies = (doc.policies ?? []).map((policy) => parsePolicyDoc(policy));
35
- return { name: doc.name, stopAt, policies };
36
- }
37
-
38
- export function loadRiskPreset(name) {
39
- if (!/^[a-z0-9-]+$/.test(name)) {
40
- throw new PolicyError(`invalid risk preset name: ${name}`);
41
- }
42
- const text = readFileSync(join(PRESET_DIR, `${name}.yaml`), "utf8");
43
- const preset = parseRiskPreset(parseYamlSubset(text));
44
- if (preset.name !== name) {
45
- throw new PolicyError(`risk preset file ${name}.yaml declares name ${preset.name}`);
46
- }
47
- return preset;
1
+ // Compatibility names translate to fixed safeguards. No preset DSL is loaded.
2
+ import { PolicyError, deliveryChecks } from "../policy/policy.js";
3
+ export function loadRiskPreset(name, { artifact = "plan" } = {}) {
4
+ if (!["fast", "standard", "controlled"].includes(name))
5
+ throw new PolicyError(
6
+ `unsupported risk preset ${name}; release/custom presets are retired; see docs/MIGRATION.md`,
7
+ );
8
+ const checks = {
9
+ artifact,
10
+ requireAcceptance: name !== "fast",
11
+ requireIntent: name === "controlled",
12
+ maxSeverity: name === "controlled" ? "P3" : "P2",
13
+ };
14
+ return { name, stopAt: [], checks, policies: deliveryChecks(checks) };
48
15
  }
@@ -1,208 +1,200 @@
1
- // Minimal workflow parser per docs/v2/RFC-0003-workflow-policy.md §1.
2
- // The kernel is phase-agnostic: business steps come entirely from the
3
- // workflow file. Movement combines the declared step order (each step chains
4
- // to the next on "succeeded") with explicit transitions, which override the
5
- // default edge for the same (from, on) pair. Unknown fields are rejected.
6
-
1
+ // Fixed delivery graph. Legacy official files are decoded only to preserve
2
+ // their exact pinned digest and safeguards; arbitrary workflow authoring is retired.
7
3
  import { readFileSync } from "node:fs";
8
-
9
4
  import { parseYamlSubset } from "./yaml-subset.js";
10
5
 
11
- export class WorkflowError extends Error {
12
- constructor(message) {
13
- super(message);
14
- this.name = "WorkflowError";
15
- }
16
- }
17
-
18
- const ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
19
- const TOP_FIELDS = new Set([
20
- "kind",
21
- "version",
22
- "name",
23
- "entry",
24
- "steps",
25
- "transitions",
26
- "terminal",
27
- "policies",
28
- "budgets",
6
+ export class WorkflowError extends Error {}
7
+ export const DELIVERY_TEXT = `kind: workflow
8
+ version: 1
9
+ name: software-delivery
10
+ entry: build
11
+ steps:
12
+ - id: build
13
+ worker: builder
14
+ - id: verify
15
+ worker: verifier
16
+ - id: review
17
+ worker: reviewer
18
+ readonly: true
19
+ - id: wait-merge
20
+ - id: fix
21
+ worker: fixer
22
+ transitions:
23
+ - from: verify
24
+ on: failed
25
+ to: fix
26
+ - from: fix
27
+ on: succeeded
28
+ to: verify
29
+ - from: review
30
+ on: findings-blocking
31
+ to: fix
32
+ terminal:
33
+ - wait-merge
34
+ `;
35
+ const CORE = ["build", "verify", "review", "wait-merge", "fix"];
36
+ const LEGACY = ["intent", "spec", "plan", ...CORE];
37
+ const ROLES = {
38
+ intent: "planner",
39
+ spec: "planner",
40
+ plan: "planner",
41
+ build: "builder",
42
+ verify: "verifier",
43
+ review: "reviewer",
44
+ fix: "fixer",
45
+ };
46
+ const EDGES = new Map([
47
+ ["build|succeeded", "verify"],
48
+ ["verify|succeeded", "review"],
49
+ ["verify|failed", "fix"],
50
+ ["review|succeeded", "wait-merge"],
51
+ ["review|findings-blocking", "fix"],
52
+ ["fix|succeeded", "verify"],
29
53
  ]);
30
- const STEP_FIELDS = new Set(["id", "worker", "optional", "requiredWhen", "readonly", "grade"]);
31
- const EVIDENCE_GRADES = new Set(["L0", "L1", "L2", "L3", "L4"]);
32
- const TRANSITION_FIELDS = new Set(["from", "on", "to"]);
33
-
34
- function rejectUnknownFields(object, allowed, where) {
35
- for (const key of Object.keys(object)) {
36
- if (!allowed.has(key)) {
37
- throw new WorkflowError(`unknown field "${key}" in ${where}`);
38
- }
39
- }
40
- }
41
-
42
- function requireId(value, where) {
43
- if (typeof value !== "string" || !ID_PATTERN.test(value)) {
44
- throw new WorkflowError(`${where} must be a lowercase id, got: ${JSON.stringify(value)}`);
45
- }
46
- return value;
54
+ const RETIRED =
55
+ "custom workflows and the release lane are retired; finish active runs with 3.3.1, then migrate using docs/MIGRATION.md";
56
+ function reject(detail) {
57
+ throw new WorkflowError(`${detail}; ${RETIRED}`);
47
58
  }
48
-
49
59
  export function parseWorkflow(text) {
50
60
  const doc = parseYamlSubset(text);
51
- if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
52
- throw new WorkflowError("workflow document must be a map");
53
- }
54
- rejectUnknownFields(doc, TOP_FIELDS, "workflow");
55
- if (doc.kind !== "workflow") {
56
- throw new WorkflowError(`kind must be "workflow", got: ${JSON.stringify(doc.kind)}`);
57
- }
58
- if (doc.version !== 1) {
59
- throw new WorkflowError(`unsupported workflow version: ${JSON.stringify(doc.version)}`);
60
- }
61
- const name = requireId(doc.name, "name");
62
- if (!Array.isArray(doc.steps) || doc.steps.length === 0) {
63
- throw new WorkflowError("steps must be a non-empty list");
64
- }
65
-
66
- const steps = [];
67
- const stepIds = new Set();
68
- for (const raw of doc.steps) {
69
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
70
- throw new WorkflowError("each step must be a map");
71
- }
72
- rejectUnknownFields(raw, STEP_FIELDS, `step ${JSON.stringify(raw.id)}`);
73
- const id = requireId(raw.id, "step id");
74
- if (stepIds.has(id)) {
75
- throw new WorkflowError(`duplicate step id: ${id}`);
76
- }
77
- if (raw.worker !== undefined) {
78
- requireId(raw.worker, `step ${id} worker`);
79
- }
80
- if (raw.optional !== undefined && typeof raw.optional !== "boolean") {
81
- throw new WorkflowError(`step ${id} optional must be a boolean`);
82
- }
83
- if (raw.readonly !== undefined && typeof raw.readonly !== "boolean") {
84
- throw new WorkflowError(`step ${id} readonly must be a boolean`);
85
- }
86
- // Evidence grade a step's command evidence is recorded at (iteration 08:
87
- // release readbacks run against the real environment and are L4).
88
- if (raw.grade !== undefined && !EVIDENCE_GRADES.has(raw.grade)) {
89
- throw new WorkflowError(`step ${id} grade must be one of L0-L4`);
90
- }
91
- stepIds.add(id);
92
- steps.push({
93
- id,
61
+ if (
62
+ !doc ||
63
+ doc.kind !== "workflow" ||
64
+ doc.version !== 1 ||
65
+ doc.name !== "software-delivery"
66
+ )
67
+ reject("expected the fixed software-delivery workflow");
68
+ for (const key of Object.keys(doc))
69
+ if (
70
+ ![
71
+ "kind",
72
+ "version",
73
+ "name",
74
+ "entry",
75
+ "steps",
76
+ "transitions",
77
+ "terminal",
78
+ "budgets",
79
+ "policies",
80
+ ].includes(key)
81
+ )
82
+ reject(`unknown workflow field ${key}`);
83
+ if (!Array.isArray(doc.steps)) reject("steps must be a list");
84
+ const ids = doc.steps.map((step) => step?.id);
85
+ const legacy = JSON.stringify(ids) === JSON.stringify(LEGACY);
86
+ if (!legacy && JSON.stringify(ids) !== JSON.stringify(CORE))
87
+ reject("delivery step order changed");
88
+ if (doc.entry !== (legacy ? "intent" : "build"))
89
+ reject("workflow entry changed");
90
+ if (JSON.stringify(doc.terminal) !== '["wait-merge"]')
91
+ reject("delivery terminal changed");
92
+ if (
93
+ doc.policies !== undefined &&
94
+ (!Array.isArray(doc.policies) || doc.policies.length)
95
+ )
96
+ reject("workflow policies are unsupported");
97
+ if (doc.budgets !== undefined) {
98
+ if (
99
+ !doc.budgets ||
100
+ typeof doc.budgets !== "object" ||
101
+ Array.isArray(doc.budgets) ||
102
+ Object.keys(doc.budgets).some((key) => key !== "maxAttempts")
103
+ )
104
+ reject("unsupported workflow budget");
105
+ const attempts = doc.budgets.maxAttempts ?? {};
106
+ if (
107
+ !attempts ||
108
+ typeof attempts !== "object" ||
109
+ Array.isArray(attempts) ||
110
+ Object.entries(attempts).some(
111
+ ([step, value]) =>
112
+ !ids.includes(step) || !Number.isInteger(value) || value < 1,
113
+ )
114
+ )
115
+ reject("invalid workflow attempt budget");
116
+ }
117
+ const steps = doc.steps.map((raw) => {
118
+ for (const key of Object.keys(raw))
119
+ if (
120
+ ![
121
+ "id",
122
+ "worker",
123
+ "readonly",
124
+ "optional",
125
+ "requiredWhen",
126
+ "grade",
127
+ ].includes(key)
128
+ )
129
+ reject(`unknown step field ${key}`);
130
+ const spec = legacy && raw.id === "spec";
131
+ if (
132
+ (raw.worker ?? null) !== (ROLES[raw.id] ?? null) ||
133
+ (raw.readonly ?? false) !== (raw.id === "review") ||
134
+ (raw.optional ?? false) !== spec ||
135
+ (raw.requiredWhen ?? null) !== (spec ? "ui-delivery" : null)
136
+ )
137
+ reject(`delivery role or safeguard changed at ${raw.id}`);
138
+ if (
139
+ raw.grade !== undefined &&
140
+ !["L0", "L1", "L2", "L3", "L4"].includes(raw.grade)
141
+ )
142
+ reject("grade must be one of L0-L4");
143
+ return {
144
+ id: raw.id,
94
145
  worker: raw.worker ?? null,
95
- optional: raw.optional ?? false,
146
+ readonly: raw.id === "review",
147
+ optional: spec,
96
148
  requiredWhen: raw.requiredWhen ?? null,
97
- readonly: raw.readonly ?? false,
98
149
  grade: raw.grade ?? "L2",
99
- });
100
- }
101
-
102
- const entry = requireId(doc.entry, "entry");
103
- if (!stepIds.has(entry)) {
104
- throw new WorkflowError(`entry references unknown step: ${entry}`);
150
+ };
151
+ });
152
+ const expected = new Map([
153
+ ["verify|failed", "fix"],
154
+ ["fix|succeeded", "verify"],
155
+ ["review|findings-blocking", "fix"],
156
+ ]);
157
+ if (
158
+ !Array.isArray(doc.transitions) ||
159
+ doc.transitions.length !== expected.size
160
+ )
161
+ reject("delivery transitions changed");
162
+ const seen = new Set();
163
+ for (const row of doc.transitions) {
164
+ if (!row || typeof row !== "object" || Array.isArray(row))
165
+ reject("invalid delivery transition");
166
+ const key = `${row.from}|${row.on}`;
167
+ if (
168
+ Object.keys(row).some((key) => !["from", "on", "to"].includes(key)) ||
169
+ expected.get(key) !== row.to ||
170
+ seen.has(key)
171
+ )
172
+ reject("delivery transitions changed");
173
+ seen.add(key);
174
+ }
175
+ const edges = new Map(EDGES);
176
+ if (legacy) {
177
+ edges.set("intent|succeeded", "spec");
178
+ edges.set("spec|succeeded", "plan");
179
+ edges.set("plan|succeeded", "build");
105
180
  }
106
- if (!Array.isArray(doc.terminal) || doc.terminal.length === 0) {
107
- throw new WorkflowError("terminal must be a non-empty list");
108
- }
109
- const terminal = new Set();
110
- for (const id of doc.terminal) {
111
- if (!stepIds.has(id)) {
112
- throw new WorkflowError(`terminal references unknown step: ${id}`);
113
- }
114
- terminal.add(id);
115
- }
116
-
117
- const edges = new Map();
118
- const explicit = doc.transitions ?? [];
119
- if (!Array.isArray(explicit)) {
120
- throw new WorkflowError("transitions must be a list");
121
- }
122
- for (const raw of explicit) {
123
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
124
- throw new WorkflowError("each transition must be a map");
125
- }
126
- rejectUnknownFields(raw, TRANSITION_FIELDS, "transition");
127
- const from = requireId(raw.from, "transition from");
128
- const to = requireId(raw.to, "transition to");
129
- if (!stepIds.has(from) || !stepIds.has(to)) {
130
- throw new WorkflowError(`transition references unknown step: ${from} -> ${to}`);
131
- }
132
- if (typeof raw.on !== "string" || raw.on.length === 0) {
133
- throw new WorkflowError(`transition ${from} -> ${to} needs a non-empty "on"`);
134
- }
135
- const key = `${from}|${raw.on}`;
136
- if (edges.has(key)) {
137
- throw new WorkflowError(`duplicate transition for (${from}, ${raw.on})`);
138
- }
139
- edges.set(key, to);
140
- }
141
-
142
- for (const [index, step] of steps.entries()) {
143
- const key = `${step.id}|succeeded`;
144
- if (terminal.has(step.id) || edges.has(key)) {
145
- continue;
146
- }
147
- const next = steps[index + 1];
148
- if (next) {
149
- edges.set(key, next.id);
150
- }
151
- }
152
-
153
- const outgoing = new Map();
154
- for (const [key, to] of edges) {
155
- const from = key.split("|")[0];
156
- if (!outgoing.has(from)) {
157
- outgoing.set(from, []);
158
- }
159
- outgoing.get(from).push(to);
160
- }
161
-
162
- const reachable = new Set([entry]);
163
- const queue = [entry];
164
- while (queue.length > 0) {
165
- const current = queue.pop();
166
- for (const to of outgoing.get(current) ?? []) {
167
- if (!reachable.has(to)) {
168
- reachable.add(to);
169
- queue.push(to);
170
- }
171
- }
172
- }
173
- const reachesTerminal = new Set(terminal);
174
- let grew = true;
175
- while (grew) {
176
- grew = false;
177
- for (const [from, targets] of outgoing) {
178
- if (!reachesTerminal.has(from) && targets.some((to) => reachesTerminal.has(to))) {
179
- reachesTerminal.add(from);
180
- grew = true;
181
- }
182
- }
183
- }
184
- for (const id of reachable) {
185
- if (!reachesTerminal.has(id)) {
186
- throw new WorkflowError(`step "${id}" has no path to a terminal step`);
187
- }
188
- }
189
-
190
181
  return {
191
- name,
192
- entry,
182
+ name: doc.name,
183
+ entry: doc.entry,
193
184
  steps,
194
- stepIds,
195
- terminal,
185
+ stepIds: new Set(ids),
186
+ terminal: new Set(["wait-merge"]),
196
187
  edges,
197
- policies: doc.policies ?? [],
188
+ policies: [],
198
189
  budgets: doc.budgets ?? {},
199
190
  };
200
191
  }
201
-
202
192
  export function loadWorkflow(filePath) {
203
193
  return parseWorkflow(readFileSync(filePath, "utf8"));
204
194
  }
205
-
195
+ export function deliveryWorkflow() {
196
+ return parseWorkflow(DELIVERY_TEXT);
197
+ }
206
198
  export function nextStep(workflow, from, outcome) {
207
199
  return workflow.edges.get(`${from}|${outcome}`) ?? null;
208
200
  }
@@ -39,6 +39,12 @@ export function collectCommandEvidence({
39
39
  `signal: ${execResult.signal}`,
40
40
  `timedOut: ${execResult.timedOut}`,
41
41
  `spawnError: ${execResult.spawnError}`,
42
+ ...(execResult.requirementFailure
43
+ ? [`requirement: ${execResult.requirementFailure}`]
44
+ : []),
45
+ ...(execResult.screenshotsRejected?.length
46
+ ? [`screenshots rejected: ${execResult.screenshotsRejected.join(", ")}`]
47
+ : []),
42
48
  "--- stdout ---",
43
49
  scrub(execResult.stdout),
44
50
  "--- stderr ---",
@@ -50,7 +56,12 @@ export function collectCommandEvidence({
50
56
  let status;
51
57
  if (execResult.spawnError) {
52
58
  status = "unverified";
53
- } else if (execResult.exitCode === 0 && !execResult.timedOut && !execResult.signal) {
59
+ } else if (
60
+ execResult.exitCode === 0 &&
61
+ !execResult.timedOut &&
62
+ !execResult.signal &&
63
+ !execResult.requirementFailure
64
+ ) {
54
65
  status = "passed";
55
66
  } else {
56
67
  status = "failed";