@haiyangbg/buildbeat 3.3.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.en.md +20 -124
- package/README.md +20 -124
- package/SKILL.md +73 -84
- package/docs/CAPABILITY-MATRIX.md +30 -39
- package/docs/MIGRATION.md +93 -0
- package/docs/README.md +8 -37
- package/docs/RELEASING.md +4 -3
- package/docs/v2/RFC-0001-product-definition.md +3 -0
- package/docs/v2/RFC-0002-domain-model.md +3 -0
- package/docs/v2/RFC-0003-workflow-policy.md +3 -0
- package/docs/v2/SPEC-0001-events-v1.md +3 -0
- package/docs/v2/guide/00-how-to-talk.en.md +12 -58
- package/docs/v2/guide/00-how-to-talk.md +12 -58
- package/docs/v2/guide/01-quickstart.en.md +41 -112
- package/docs/v2/guide/01-quickstart.md +41 -112
- package/docs/v2/guide/02-workflow-guide.en.md +13 -123
- package/docs/v2/guide/02-workflow-guide.md +14 -122
- package/docs/v2/guide/03-policy-guide.en.md +6 -50
- package/docs/v2/guide/03-policy-guide.md +6 -50
- package/docs/v2/guide/04-adapter-guide.en.md +7 -60
- package/docs/v2/guide/04-adapter-guide.md +7 -60
- package/docs/v2/guide/05-worker-contract.en.md +6 -53
- package/docs/v2/guide/05-worker-contract.md +7 -52
- package/docs/v2/guide/06-evidence-guide.en.md +8 -44
- package/docs/v2/guide/06-evidence-guide.md +9 -43
- package/docs/v2/guide/07-approval-guide.en.md +8 -110
- package/docs/v2/guide/07-approval-guide.md +8 -121
- package/docs/v2/guide/09-security-boundaries.en.md +13 -37
- package/docs/v2/guide/09-security-boundaries.md +13 -37
- package/docs/v2/guide/10-recovery.en.md +12 -96
- package/docs/v2/guide/10-recovery.md +12 -96
- package/docs/v2/guide/11-session-handoff.en.md +7 -78
- package/docs/v2/guide/11-session-handoff.md +7 -78
- package/docs/v2/guide/12-without-runtime.en.md +46 -0
- package/docs/v2/guide/12-without-runtime.md +46 -0
- package/docs/v2/guide/README.en.md +16 -36
- package/docs/v2/guide/README.md +16 -36
- package/example/AGENTS.md +15 -69
- package/example/README.md +8 -23
- package/example/delivery/envelope/prompts/builder.md +3 -1
- package/example/delivery/envelope/prompts/fixer.md +3 -1
- package/example/delivery/envelope/prompts/reviewer.md +3 -1
- package/example//346/214/207/346/214/245/345/217/260.md +8 -40
- package/package.json +2 -1
- package/src/v2/adapters/notification-payload.js +40 -0
- package/src/v2/adapters/shell.js +3 -1
- package/src/v2/cli/run-config-check.js +129 -45
- package/src/v2/cli/run.js +821 -252
- package/src/v2/engine/reducer.js +2 -0
- package/src/v2/engine/risk-preset.js +14 -47
- package/src/v2/engine/workflow.js +177 -185
- package/src/v2/evidence/collector.js +12 -1
- package/src/v2/evidence/image.js +346 -0
- package/src/v2/policy/policy.js +219 -286
- package/src/v2/presets/software-delivery.yaml +1 -16
- package/src/v2/runtime/cache.js +22 -1
- package/src/v2/runtime/decisions.js +185 -31
- package/src/v2/runtime/findings.js +4 -1
- package/src/v2/runtime/notify.js +42 -36
- package/src/v2/runtime/orchestrator.js +811 -297
- package/src/v2/runtime/overview.js +163 -34
- package/src/v2/runtime/release.js +233 -0
- package/src/v2/runtime/run-record.js +1 -0
- package/src/v2/workspace/workspace-manager.js +8 -0
- package/templates/v2/AGENTS.md +15 -69
- package/templates/v2/BUILDBEAT.md +2 -13
- package/templates/v2/envelope/prompts/builder.md +3 -1
- package/templates/v2/envelope/prompts/fixer.md +3 -1
- package/templates/v2/envelope/prompts/reviewer.md +3 -1
- package/templates/v2/run-config.example.yaml +7 -5
- package/templates/v2/work.example.md +16 -0
- package/templates/v2//346/214/207/346/214/245/345/217/260.md +8 -40
- package/docs/v2/skill/01-principles.md +0 -26
- package/docs/v2/skill/02-project-layout.md +0 -31
- package/docs/v2/skill/03-collaboration-rules.md +0 -45
- package/docs/v2/skill/04-rhythm-and-rituals.md +0 -104
- package/docs/v2/skill/05-red-lines.md +0 -13
- package/docs/v2/skill/06-bootstrap-and-takeover.md +0 -84
- package/docs/v2/skill/07-templates-and-lessons.md +0 -24
- package/example/.buildbeat/observe.yaml +0 -31
- package/src/v2/observe/observe-config.js +0 -194
- package/src/v2/observe/observe-reducer.js +0 -117
- package/src/v2/observe/observe.js +0 -427
- package/src/v2/presets/observe.yaml +0 -45
- package/src/v2/presets/policies/ui-render-gate.yaml +0 -13
- package/src/v2/presets/release-readback.yaml +0 -36
- package/src/v2/presets/risk/controlled.yaml +0 -39
- package/src/v2/presets/risk/fast.yaml +0 -19
- package/src/v2/presets/risk/release.yaml +0 -21
- package/src/v2/presets/risk/standard.yaml +0 -28
- package/templates/ARCHITECTURE.md +0 -50
- package/templates/contracts/PROTOCOL.md +0 -28
- package/templates/pm/adr/ADR-0000-template.md +0 -25
- package/templates/pm/adr/README.md +0 -15
- package/templates/pm/decisions.md +0 -11
- package/templates/standards/CODE.md +0 -23
- package/templates/standards/DESIGN.md +0 -36
- package/templates/standards/REVIEW.md +0 -20
- package/templates/standards/STACK.md +0 -31
package/src/v2/engine/reducer.js
CHANGED
|
@@ -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
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
//
|
|
2
|
-
//
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
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 (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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:
|
|
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 (
|
|
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";
|