@tea-agent/loop-agent 0.14.0 → 0.16.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/AGENTS.md +1 -1
- package/CHANGELOG.md +98 -11
- package/README.md +24 -5
- package/dist/application/evaluation/alias.js +184 -0
- package/dist/application/evaluation/budget.js +192 -0
- package/dist/application/evaluation/campaign-hash.js +47 -0
- package/dist/application/evaluation/campaign-matrix.js +372 -0
- package/dist/application/evaluation/campaign-scorecard.js +135 -0
- package/dist/application/evaluation/campaign.js +370 -0
- package/dist/application/evaluation/candidate.js +23 -6
- package/dist/application/evaluation/corpus-hash.js +38 -0
- package/dist/application/evaluation/corpus.js +56 -0
- package/dist/application/evaluation/experiment.js +294 -0
- package/dist/application/evaluation/ignition.js +198 -0
- package/dist/application/evaluation/integrity-audit.js +162 -0
- package/dist/application/evaluation/outer-loop.js +132 -0
- package/dist/application/evaluation/pi-cell-executor.js +39 -0
- package/dist/application/evaluation/private-verifier.js +46 -0
- package/dist/application/evaluation/promotion-policy.js +151 -0
- package/dist/application/evaluation/proposer.js +98 -0
- package/dist/application/evaluation/types.js +522 -0
- package/dist/cli/command-definitions.js +19 -3
- package/dist/commands/eval.js +1176 -13
- package/dist/commands/init.js +4 -1
- package/dist/infrastructure/evaluation/alias-store.js +199 -0
- package/dist/infrastructure/evaluation/campaign-store.js +154 -0
- package/dist/infrastructure/evaluation/corpus-store.js +181 -0
- package/dist/infrastructure/evaluation/experiment-store.js +124 -0
- package/dist/infrastructure/evaluation/ignition-store.js +82 -0
- package/dist/infrastructure/evaluation/private-verifier-store.js +145 -0
- package/dist/infrastructure/evaluation/proposer-store.js +78 -0
- package/dist/worker/cli.js +6 -3
- package/dist/worker/delivery/final-verification.js +96 -8
- package/dist/worker/delivery/package.js +23 -4
- package/dist/worker/delivery/verification-bundle.js +521 -0
- package/dist/worker/feature/fullstack-validate.js +337 -0
- package/dist/worker/feature/profile-schema.js +44 -0
- package/dist/worker/feature/ready-plan-projection.js +1 -0
- package/dist/worker/feature/reducer.js +2 -0
- package/dist/worker/feature/review.js +106 -11
- package/dist/worker/materialize/harness-task-materializer.js +5 -0
- package/dist/worker/observability/read-model.js +7 -0
- package/dist/worker/observe/static/views/task.js +1 -0
- package/dist/worker/outcomes/adapters.js +144 -0
- package/dist/worker/outcomes/evidence-tokens.js +29 -0
- package/dist/worker/outcomes/gate.js +40 -0
- package/dist/worker/outcomes/projector.js +185 -0
- package/dist/worker/outcomes/registry.js +1 -0
- package/dist/worker/outcomes/store.js +131 -0
- package/dist/worker/outcomes/types.js +79 -0
- package/dist/worker/report/morning-report.js +4 -3
- package/dist/worker/run-task/run-task.js +85 -2
- package/dist/worker/runner/run-ready.js +32 -1
- package/dist/worker/task-graph/acceptance-schema.js +12 -0
- package/dist/worker/task-graph/ready-planner.js +131 -0
- package/dist/worker/task-graph/task-graph-schema.js +31 -0
- package/dist/worker/task-graph/validate.js +44 -4
- package/dist/worker/task-spec/schema.js +9 -0
- package/dist/worker/task-spec/validate.js +39 -0
- package/dist/worker/task-spec/workflow-routing.js +149 -0
- package/dist/workflows/dag/budget-enforcement.js +67 -0
- package/dist/workflows/dag/context-policy.js +137 -0
- package/dist/workflows/dag/init-hybrid.js +27 -11
- package/dist/workflows/dag/knowledge-curator.js +3 -0
- package/dist/workflows/dag/node-execution.js +11 -4
- package/dist/workflows/dag/prompt.js +1 -1
- package/dist/workflows/dag/runner.js +43 -16
- package/dist/workflows/dag/skill-snapshot.js +11 -7
- package/dist/workflows/dag/types.js +18 -0
- package/docs/README.md +1 -0
- package/docs/init-surface.manifest.json +7 -7
- package/docs/templates/branch-merge-report.md +0 -1
- package/docs/templates/evaluation/campaign-budget-v1.json +12 -0
- package/docs/templates/evaluation/campaign-dogfood-v0.json +24 -0
- package/docs/templates/evaluation/campaign-evidence-v1.json +44 -0
- package/docs/templates/evaluation/context-policy-baseline-v1.json +17 -0
- package/docs/templates/evaluation/context-policy-role-specialized-v1.json +28 -0
- package/docs/templates/evaluation/corpus-dogfood-v0.manifest.json +118 -0
- package/docs/templates/evaluation/matrix-dag-dry-run-v1.json +21 -0
- package/docs/templates/evaluation/matrix-fixture-v1.json +10 -0
- package/docs/templates/evaluation/private-verifier-dogfood-v0.json +16 -0
- package/docs/templates/product-line/AGENTS.md +1 -0
- package/docs/templates/product-line/README.md +17 -0
- package/docs/templates/product-line/acceptance.yaml +9 -0
- package/docs/templates/product-line/feature.yaml +11 -0
- package/docs/templates/product-line/task-graph.yaml +8 -0
- package/docs/templates/product-line/task.yaml +4 -0
- package/package.json +6 -16
- package/skills/browser-tools/SKILL.md +2 -2
- package/skills/frontend-design-review/references/review-checklist.md +27 -45
- package/skills/frontend-implementation/references/node-contracts.md +4 -4
- package/skills/frontend-review/SKILL.md +3 -1
- package/skills/frontend-review/references/review-findings.md +2 -1
- package/skills/frontend-verification/SKILL.md +3 -1
- package/skills/frontend-verification/references/verification-checklist.md +13 -22
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/docs/agent-dag-recovery-playbook.md +0 -195
- package/docs/agent-dag-runner.md +0 -67
- package/docs/cursor-prompt-sidecar.md +0 -36
- package/docs/decisions/README.md +0 -18
- package/docs/design/README.md +0 -167
- package/docs/development-principles.md +0 -73
- package/docs/exec-plans/README.md +0 -6
- package/docs/exec-plans/active/README.md +0 -13
- package/docs/exec-plans/completed/README.md +0 -108
- package/docs/feature-workflow.md +0 -414
- package/docs/loop-agent-harness.md +0 -142
- package/docs/production-readiness.md +0 -96
- package/docs/progress/README.md +0 -81
- package/docs/reports/README.md +0 -163
- package/docs/verification-matrix.md +0 -70
- package/scripts/check-product-line-docs.sh +0 -29
- package/scripts/check-task-pool-root.sh +0 -32
- package/scripts/kb-graph-incremental-prepare.sh +0 -5
- package/scripts/kb-graph-materialize.sh +0 -4
- package/scripts/kb-graph-promote.sh +0 -4
- package/scripts/kb-query.sh +0 -5
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { resolveWorkflow } from "../task-spec/workflow-routing.js";
|
|
2
|
+
export function resolveNodeWorkflows(graph, taskSpecs) {
|
|
3
|
+
const index = new Map();
|
|
4
|
+
for (const node of graph.nodes) {
|
|
5
|
+
const spec = taskSpecs.get(node.id);
|
|
6
|
+
if (spec)
|
|
7
|
+
index.set(node.id, { workflow: resolveWorkflow(spec).workflow, type: spec.type });
|
|
8
|
+
}
|
|
9
|
+
return index;
|
|
10
|
+
}
|
|
11
|
+
export function validateFullstackStructure(input) {
|
|
12
|
+
const errors = [];
|
|
13
|
+
const { profile, acceptance, graph, taskSpecs } = input;
|
|
14
|
+
const nodeIds = new Set(graph.nodes.map((node) => node.id));
|
|
15
|
+
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
16
|
+
const workflows = resolveNodeWorkflows(graph, taskSpecs);
|
|
17
|
+
// Gate 1: authority/acceptance non-empty is already enforced by the acceptance
|
|
18
|
+
// schema (.min(1)) before fullstack gates run; nothing to add here.
|
|
19
|
+
// Gate 2: each AC must declare a contract source; required ACs require both
|
|
20
|
+
// implementation and verification evidence, plus the kinds they expect.
|
|
21
|
+
for (const item of acceptance.acceptance) {
|
|
22
|
+
const implRefs = item.verification.implementation_task_refs ?? [];
|
|
23
|
+
const verRefs = item.verification.verification_task_refs ?? [];
|
|
24
|
+
if (implRefs.length === 0 && verRefs.length === 0) {
|
|
25
|
+
// `expected_task_refs` only is allowed as legacy fallback, but for a
|
|
26
|
+
// fullstack-v1 AC we require explicit contract separation.
|
|
27
|
+
errors.push({
|
|
28
|
+
code: "fullstack-contract-source-missing",
|
|
29
|
+
message: `acceptance ${item.id} must declare implementation_task_refs and/or verification_task_refs for a fullstack-v1 packet`,
|
|
30
|
+
path: `acceptance.${item.id}.verification`,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (item.priority === "must" && implRefs.length === 0) {
|
|
34
|
+
errors.push({
|
|
35
|
+
code: "fullstack-required-implementation-refs-missing",
|
|
36
|
+
message: `required acceptance ${item.id} must declare implementation_task_refs`,
|
|
37
|
+
path: `acceptance.${item.id}.verification.implementation_task_refs`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (item.priority === "must" && verRefs.length === 0) {
|
|
41
|
+
errors.push({
|
|
42
|
+
code: "fullstack-required-verification-refs-missing",
|
|
43
|
+
message: `required acceptance ${item.id} must declare verification_task_refs`,
|
|
44
|
+
path: `acceptance.${item.id}.verification.verification_task_refs`,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (item.priority === "must" && (item.verification.required_evidence?.length ?? 0) === 0) {
|
|
48
|
+
errors.push({
|
|
49
|
+
code: "fullstack-required-evidence-missing",
|
|
50
|
+
message: `required acceptance ${item.id} must declare required_evidence`,
|
|
51
|
+
path: `acceptance.${item.id}.verification.required_evidence`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Refs existence + non-overlap between implementation and verification refs.
|
|
56
|
+
for (const item of acceptance.acceptance) {
|
|
57
|
+
const implRefs = item.verification.implementation_task_refs ?? [];
|
|
58
|
+
const verRefs = item.verification.verification_task_refs ?? [];
|
|
59
|
+
for (const ref of implRefs) {
|
|
60
|
+
if (!nodeIds.has(ref)) {
|
|
61
|
+
errors.push({
|
|
62
|
+
code: "acceptance-implementation-ref-missing",
|
|
63
|
+
message: `acceptance ${item.id} implementation_task_refs references missing task ${ref}`,
|
|
64
|
+
path: `acceptance.${item.id}.verification.implementation_task_refs`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
for (const ref of verRefs) {
|
|
69
|
+
if (!nodeIds.has(ref)) {
|
|
70
|
+
errors.push({
|
|
71
|
+
code: "acceptance-verification-ref-missing",
|
|
72
|
+
message: `acceptance ${item.id} verification_task_refs references missing task ${ref}`,
|
|
73
|
+
path: `acceptance.${item.id}.verification.verification_task_refs`,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const overlap = implRefs.filter((ref) => verRefs.includes(ref));
|
|
78
|
+
if (overlap.length > 0) {
|
|
79
|
+
errors.push({
|
|
80
|
+
code: "fullstack-impl-verification-refs-overlap",
|
|
81
|
+
message: `acceptance ${item.id} implementation and verification task refs must not overlap: ${overlap.join(", ")}`,
|
|
82
|
+
path: `acceptance.${item.id}.verification`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const scope = profile.scope ?? {};
|
|
87
|
+
if (!Object.values(scope).some((value) => value === true)) {
|
|
88
|
+
errors.push({
|
|
89
|
+
code: "fullstack-scope-required",
|
|
90
|
+
message: "fullstack-v1 requires at least one enabled scope flag",
|
|
91
|
+
path: "feature.yaml:scope",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const hasWorkflow = (target) => [...workflows.values()].some((entry) => entry.workflow === target);
|
|
95
|
+
// Gate 3: scope.backend → at least one backend implementation agent-dag that
|
|
96
|
+
// is a development-type task (not QA, not architecture/review/final-verify).
|
|
97
|
+
if (scope.backend && !hasBackendImplementation(workflows)) {
|
|
98
|
+
errors.push({
|
|
99
|
+
code: "fullstack-required-workflow-missing",
|
|
100
|
+
message: "scope.backend is enabled but no backend implementation agent-dag task is present",
|
|
101
|
+
path: "feature.yaml:scope.backend",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// Gate 4: scope.frontend → at least one frontend-implementation.
|
|
105
|
+
if (scope.frontend && !hasWorkflow("frontend-implementation")) {
|
|
106
|
+
errors.push({
|
|
107
|
+
code: "fullstack-required-workflow-missing",
|
|
108
|
+
message: "scope.frontend is enabled but no frontend-implementation task is present",
|
|
109
|
+
path: "feature.yaml:scope.frontend",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
// Gate 5: scope.backendVerification → at least one backend-test.
|
|
113
|
+
if (scope.backendVerification && !hasWorkflow("backend-test")) {
|
|
114
|
+
errors.push({
|
|
115
|
+
code: "fullstack-required-workflow-missing",
|
|
116
|
+
message: "scope.backendVerification is enabled but no backend-test task is present",
|
|
117
|
+
path: "feature.yaml:scope.backendVerification",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// Gate 6: scope.frontendVerification → at least one frontend-test.
|
|
121
|
+
if (scope.frontendVerification && !hasWorkflow("frontend-test")) {
|
|
122
|
+
errors.push({
|
|
123
|
+
code: "fullstack-required-workflow-missing",
|
|
124
|
+
message: "scope.frontendVerification is enabled but no frontend-test task is present",
|
|
125
|
+
path: "feature.yaml:scope.frontendVerification",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// Gate 7: frontend-test depends_on at least one frontend-implementation node.
|
|
129
|
+
const frontendImplIds = new Set(graph.nodes.filter((node) => workflows.get(node.id)?.workflow === "frontend-implementation").map((node) => node.id));
|
|
130
|
+
for (const node of graph.nodes) {
|
|
131
|
+
if (workflows.get(node.id)?.workflow !== "frontend-test")
|
|
132
|
+
continue;
|
|
133
|
+
const dependsOnImpl = node.depends_on.some((dep) => frontendImplIds.has(dep));
|
|
134
|
+
if (!dependsOnImpl) {
|
|
135
|
+
errors.push({
|
|
136
|
+
code: "fullstack-frontend-test-missing-impl-dep",
|
|
137
|
+
message: `frontend-test node ${node.id} must depend_on at least one frontend-implementation node`,
|
|
138
|
+
path: `nodes.${node.id}.depends_on`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (scope.backendVerification) {
|
|
142
|
+
const dependsOnBackendVerification = node.depends_on.some((dep) => workflows.get(dep)?.workflow === "backend-test");
|
|
143
|
+
if (!dependsOnBackendVerification) {
|
|
144
|
+
errors.push({
|
|
145
|
+
code: "fullstack-frontend-test-missing-backend-verification-dep",
|
|
146
|
+
message: `frontend-test node ${node.id} must depend_on a backend-test when scope.backendVerification is enabled`,
|
|
147
|
+
path: `nodes.${node.id}.depends_on`,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Gate 8: if a final-verify / closeout-style node exists, it depends_on all
|
|
153
|
+
// required verification workflows (backend-test / frontend-test declared by scope).
|
|
154
|
+
const requiredVerificationKinds = [];
|
|
155
|
+
if (scope.backendVerification)
|
|
156
|
+
requiredVerificationKinds.push("backend-test");
|
|
157
|
+
if (scope.frontendVerification)
|
|
158
|
+
requiredVerificationKinds.push("frontend-test");
|
|
159
|
+
const finalVerifyNode = findFinalVerifyNode(graph, workflows);
|
|
160
|
+
if (finalVerifyNode && requiredVerificationKinds.length > 0) {
|
|
161
|
+
const coveredKinds = new Set();
|
|
162
|
+
for (const dep of finalVerifyNode.depends_on) {
|
|
163
|
+
const depEntry = workflows.get(dep);
|
|
164
|
+
if (depEntry && requiredVerificationKinds.includes(depEntry.workflow))
|
|
165
|
+
coveredKinds.add(depEntry.workflow);
|
|
166
|
+
}
|
|
167
|
+
const missing = requiredVerificationKinds.filter((kind) => !coveredKinds.has(kind));
|
|
168
|
+
if (missing.length > 0) {
|
|
169
|
+
errors.push({
|
|
170
|
+
code: "fullstack-final-verify-missing-verification-dep",
|
|
171
|
+
message: `final verification node ${finalVerifyNode.id} must depend_on required verification workflows: ${missing.join(", ")}`,
|
|
172
|
+
path: `nodes.${finalVerifyNode.id}.depends_on`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// Gate 9: test workflows' allowed_paths must not include product write paths.
|
|
177
|
+
// Conservative heuristic (v1): test tasks should not own `src/**` non-test paths.
|
|
178
|
+
for (const node of graph.nodes) {
|
|
179
|
+
const entry = workflows.get(node.id);
|
|
180
|
+
if (!entry)
|
|
181
|
+
continue;
|
|
182
|
+
if (entry.workflow !== "backend-test" && entry.workflow !== "frontend-test")
|
|
183
|
+
continue;
|
|
184
|
+
const spec = taskSpecs.get(node.id);
|
|
185
|
+
if (!spec)
|
|
186
|
+
continue;
|
|
187
|
+
const writePaths = spec.constraints.allowed_paths ?? [];
|
|
188
|
+
const offending = writePaths.filter((p) => isProductWritePath(p));
|
|
189
|
+
if (offending.length > 0) {
|
|
190
|
+
errors.push({
|
|
191
|
+
code: "fullstack-test-workflow-allowed-paths-write-path",
|
|
192
|
+
message: `test workflow ${node.id} allowed_paths must not include product write paths: ${offending.join(", ")}`,
|
|
193
|
+
path: `${node.task}:constraints.allowed_paths`,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Gate 10: any two writer nodes with overlapping writeSet must have a serial
|
|
198
|
+
// depends_on relationship (transitive).
|
|
199
|
+
const writerNodes = graph.nodes.filter((node) => {
|
|
200
|
+
const entry = workflows.get(node.id);
|
|
201
|
+
return entry?.workflow === "agent-dag" || entry?.workflow === "frontend-implementation";
|
|
202
|
+
});
|
|
203
|
+
for (let i = 0; i < writerNodes.length; i++) {
|
|
204
|
+
for (let j = i + 1; j < writerNodes.length; j++) {
|
|
205
|
+
const a = writerNodes[i];
|
|
206
|
+
const b = writerNodes[j];
|
|
207
|
+
const aPaths = collectWritePaths(taskSpecs.get(a.id));
|
|
208
|
+
const bPaths = collectWritePaths(taskSpecs.get(b.id));
|
|
209
|
+
if (!writeSetsOverlap(aPaths, bPaths))
|
|
210
|
+
continue;
|
|
211
|
+
if (!hasSerialDependency(nodeById, a.id, b.id)) {
|
|
212
|
+
errors.push({
|
|
213
|
+
code: "fullstack-writeset-overlap",
|
|
214
|
+
message: `writer nodes ${a.id} and ${b.id} have overlapping allowed_paths but no serial depends_on relationship`,
|
|
215
|
+
path: `nodes.${b.id}.allowed_paths`,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return errors;
|
|
221
|
+
}
|
|
222
|
+
function hasBackendImplementation(workflows) {
|
|
223
|
+
for (const entry of workflows.values()) {
|
|
224
|
+
if (entry.workflow === "agent-dag" && isDevelopmentType(entry.type)) {
|
|
225
|
+
// backend implementation = an agent-dag development-type task
|
|
226
|
+
// (not QA, not architecture/review, not a final-verify agent-dag).
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
/** Mirrors review.ts isDevelopmentTask: excludes QA, architecture, review. */
|
|
233
|
+
function isDevelopmentType(type) {
|
|
234
|
+
return !type.startsWith("qa-") && !["architecture", "review"].includes(type);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Identify a final-verification / closeout node. In the fullstack topology it
|
|
238
|
+
* is an `agent-dag` node whose id hints at final-verify/closeout, or any node
|
|
239
|
+
* that depends on all test workflows. We use a conservative id heuristic so the
|
|
240
|
+
* gate only fires for explicit final-verify nodes; absence is not an error.
|
|
241
|
+
*/
|
|
242
|
+
function findFinalVerifyNode(graph, workflows) {
|
|
243
|
+
const hint = /(final[-_]?(verify|verification))|closeout/i;
|
|
244
|
+
for (const node of graph.nodes) {
|
|
245
|
+
if (hint.test(node.id))
|
|
246
|
+
return node;
|
|
247
|
+
}
|
|
248
|
+
// Fallback: a non-test node that depends on both a backend-test and frontend-test.
|
|
249
|
+
for (const node of graph.nodes) {
|
|
250
|
+
const kinds = node.depends_on
|
|
251
|
+
.map((dep) => workflows.get(dep)?.workflow)
|
|
252
|
+
.filter((value) => Boolean(value));
|
|
253
|
+
if (kinds.includes("backend-test") && kinds.includes("frontend-test")) {
|
|
254
|
+
return node;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Conservative v1 heuristic: a path is a product write path if it targets the
|
|
261
|
+
* `src/**` tree and is not a test-only subtree. Test fixtures living under
|
|
262
|
+
* `test/**` or `tests/**` are not flagged. This deliberately fails closed: an
|
|
263
|
+
* ambiguous path is reported so the packet declares its intent explicitly.
|
|
264
|
+
*/
|
|
265
|
+
function isProductWritePath(p) {
|
|
266
|
+
if (!p)
|
|
267
|
+
return false;
|
|
268
|
+
// Normalize to forward slashes for glob comparison.
|
|
269
|
+
const normalized = p.replace(/\\/g, "/");
|
|
270
|
+
// Test-only subtrees are allowed for test tasks.
|
|
271
|
+
if (normalized.startsWith("test/") || normalized.startsWith("tests/"))
|
|
272
|
+
return false;
|
|
273
|
+
// `src/**` is treated as product code; everything else is advisory.
|
|
274
|
+
return normalized.startsWith("src/") || normalized === "src" || normalized.startsWith("src/**");
|
|
275
|
+
}
|
|
276
|
+
function collectWritePaths(spec) {
|
|
277
|
+
if (!spec)
|
|
278
|
+
return [];
|
|
279
|
+
return spec.constraints.allowed_paths ?? [];
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Conservative writeSet overlap: treat globs minimally. Two paths overlap when
|
|
283
|
+
* one is a prefix of the other (after stripping trailing globstars) or they are
|
|
284
|
+
* equal. This avoids a glob-engine dependency while still catching the common
|
|
285
|
+
* case of two writers claiming `src/**`.
|
|
286
|
+
*/
|
|
287
|
+
function writeSetsOverlap(a, b) {
|
|
288
|
+
for (const left of a) {
|
|
289
|
+
const leftBase = globBase(left);
|
|
290
|
+
for (const right of b) {
|
|
291
|
+
const rightBase = globBase(right);
|
|
292
|
+
if (leftBase === rightBase)
|
|
293
|
+
return true;
|
|
294
|
+
if (leftBase.startsWith(`${rightBase}/`) || rightBase.startsWith(`${leftBase}/`))
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
function globBase(glob) {
|
|
301
|
+
const normalized = glob.replace(/\\/g, "/");
|
|
302
|
+
// Drop trailing /** and * suffixes to get the directory base.
|
|
303
|
+
return normalized
|
|
304
|
+
.replace(/\/\*\*$/, "")
|
|
305
|
+
.replace(/\/\*$/, "")
|
|
306
|
+
.replace(/\*$/, "");
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Two writer nodes are serially ordered if one is reachable from the other via
|
|
310
|
+
* `depends_on` (in either direction). Parallel writers with overlapping
|
|
311
|
+
* writeSet and no ordering violate gate 10.
|
|
312
|
+
*/
|
|
313
|
+
function hasSerialDependency(nodeById, a, b) {
|
|
314
|
+
const forward = reaches(nodeById, a, b);
|
|
315
|
+
const backward = reaches(nodeById, b, a);
|
|
316
|
+
return forward || backward;
|
|
317
|
+
}
|
|
318
|
+
function reaches(nodeById, from, to) {
|
|
319
|
+
const visited = new Set();
|
|
320
|
+
const stack = [from];
|
|
321
|
+
while (stack.length > 0) {
|
|
322
|
+
const current = stack.pop();
|
|
323
|
+
if (current === to)
|
|
324
|
+
return true;
|
|
325
|
+
if (visited.has(current))
|
|
326
|
+
continue;
|
|
327
|
+
visited.add(current);
|
|
328
|
+
const node = nodeById.get(current);
|
|
329
|
+
if (!node)
|
|
330
|
+
continue;
|
|
331
|
+
for (const dep of node.depends_on)
|
|
332
|
+
stack.push(dep);
|
|
333
|
+
}
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
/** Re-exported for read-model coverage projection to reuse workflow resolution. */
|
|
337
|
+
export { resolveWorkflow };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Feature-level profile for a Feature Packet (M4 `fullstack-v1`).
|
|
4
|
+
*
|
|
5
|
+
* `fullstack-v1` is NOT a new DAG or taskKind. It is `agent-worker`'s
|
|
6
|
+
* deterministic structural validation + read-model projection policy over a
|
|
7
|
+
* Feature Packet. See
|
|
8
|
+
* `docs/design/agent-worker-fullstack-workflow-integration.md` §9.
|
|
9
|
+
*
|
|
10
|
+
* A Feature Packet declares its profile via an optional `feature.yaml` next to
|
|
11
|
+
* `acceptance.yaml`. A packet without `feature.yaml` (or one that does not set
|
|
12
|
+
* `profile`) defaults to `generic`, which keeps all existing legacy/generic
|
|
13
|
+
* validation behavior unchanged (no fullstack gates fire).
|
|
14
|
+
*
|
|
15
|
+
* The schema is `.strict()`: new fields must be declared explicitly, never
|
|
16
|
+
* passthrough, so validators and read models can read declared values safely.
|
|
17
|
+
*/
|
|
18
|
+
export const FEATURE_PROFILE_FILENAME = "feature.yaml";
|
|
19
|
+
export const FULLSTACK_V1 = "fullstack-v1";
|
|
20
|
+
export const GENERIC = "generic";
|
|
21
|
+
export const featureProfileSchema = z
|
|
22
|
+
.object({
|
|
23
|
+
schema_version: z.literal(1),
|
|
24
|
+
feature_id: z.string().min(1),
|
|
25
|
+
profile: z.enum([GENERIC, FULLSTACK_V1]).optional().default(GENERIC),
|
|
26
|
+
/**
|
|
27
|
+
* Declared fullstack scope. Only consulted when `profile === "fullstack-v1"`.
|
|
28
|
+
* Each boolean enables a structural gate (see fullstack-validate.ts).
|
|
29
|
+
*/
|
|
30
|
+
scope: z
|
|
31
|
+
.object({
|
|
32
|
+
backend: z.boolean().optional(),
|
|
33
|
+
frontend: z.boolean().optional(),
|
|
34
|
+
backendVerification: z.boolean().optional(),
|
|
35
|
+
frontendVerification: z.boolean().optional(),
|
|
36
|
+
})
|
|
37
|
+
.strict()
|
|
38
|
+
.optional(),
|
|
39
|
+
})
|
|
40
|
+
.strict();
|
|
41
|
+
/** Whether a parsed profile activates the fullstack-v1 gate set. */
|
|
42
|
+
export function isFullstackProfile(profile) {
|
|
43
|
+
return profile.profile === FULLSTACK_V1;
|
|
44
|
+
}
|
|
@@ -76,6 +76,8 @@ export function reduceFeature(input) {
|
|
|
76
76
|
tasks: input.tasks.map((task) => ({
|
|
77
77
|
taskId: task.taskId,
|
|
78
78
|
type: task.type,
|
|
79
|
+
...(task.workflow ? { workflow: task.workflow } : {}),
|
|
80
|
+
...(task.outcome ? { outcome: task.outcome } : {}),
|
|
79
81
|
status: task.status ?? (isReady(task.taskId, input) ? "Ready" : "Draft"),
|
|
80
82
|
})),
|
|
81
83
|
acceptanceCoverage: input.acceptance,
|
|
@@ -4,6 +4,8 @@ import path from "node:path";
|
|
|
4
4
|
import YAML from "yaml";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { getRunsJsonlPath, getTaskPoolRoot, readFeatureTaskPoolStates, readJsonlFile } from "../pool/run-store.js";
|
|
7
|
+
import { artifactMatchesEvidenceToken, isShellVerificationToken, } from "../outcomes/evidence-tokens.js";
|
|
8
|
+
import { readVerifiedOutcome } from "../outcomes/store.js";
|
|
7
9
|
import { acceptanceSpecSchema } from "../task-graph/acceptance-schema.js";
|
|
8
10
|
import { taskGraphSpecSchema } from "../task-graph/task-graph-schema.js";
|
|
9
11
|
import { taskSpecSchema } from "../task-spec/schema.js";
|
|
@@ -21,11 +23,16 @@ export function renderFeatureReview(model) {
|
|
|
21
23
|
`${model.summary.tasksSucceeded}/${model.summary.tasksTotal} tasks completed; ${model.summary.requiredAcCovered}/${model.summary.requiredAcTotal} required AC covered.`;
|
|
22
24
|
const evidence = [model.evidence.morningReport, model.evidence.observeSnapshot, model.evidence.delivery, model.evidence.closeout, ...model.blockingItems.flatMap((item) => item.evidence)]
|
|
23
25
|
.filter((item) => Boolean(item));
|
|
26
|
+
const workflows = model.tasks.map((task) => task.workflow).filter((value) => Boolean(value));
|
|
27
|
+
const distinctWorkflows = [...new Set(workflows)];
|
|
28
|
+
const outcomes = model.tasks.filter((task) => task.outcome).length;
|
|
24
29
|
return [
|
|
25
30
|
`Feature: ${model.featureId}`,
|
|
26
31
|
`结果: ${model.statusLabel}`,
|
|
27
32
|
`原因: ${reason}`,
|
|
28
33
|
`任务: ${model.summary.tasksSucceeded}/${model.summary.tasksTotal} 完成,${model.summary.tasksReady} Ready,${model.summary.tasksFailed} 失败,${model.summary.tasksBlocked} 阻塞`,
|
|
34
|
+
`工作流: ${distinctWorkflows.length > 0 ? distinctWorkflows.join(", ") : "-"}`,
|
|
35
|
+
`Outcome: ${outcomes}/${model.tasks.length} 已投影`,
|
|
29
36
|
`required AC: ${model.summary.requiredAcCovered}/${model.summary.requiredAcTotal} 已覆盖;缺口 ${model.acceptanceCoverage.filter((item) => item.required && item.status !== "covered" && item.status !== "waived").map((item) => item.acId).join(", ") || "无"}`,
|
|
30
37
|
`风险: high=${model.riskSummary.high}, medium=${model.riskSummary.medium}, low=${model.riskSummary.low}`,
|
|
31
38
|
`交付/Closeout: ${model.status === "deliverable" || model.status === "closed" ? "交付证据齐备" : "尚未就绪"}`,
|
|
@@ -87,6 +94,17 @@ async function loadFeatureProjection(input) {
|
|
|
87
94
|
projectionWarnings.push("Task Pool run record is semantically invalid");
|
|
88
95
|
return valid;
|
|
89
96
|
});
|
|
97
|
+
const outcomes = new Map();
|
|
98
|
+
for (const run of runs) {
|
|
99
|
+
const outcome = await readVerifiedOutcome({
|
|
100
|
+
repoRoot,
|
|
101
|
+
workerRunId: run.workerRunId,
|
|
102
|
+
outcomePath: run.outcomePath,
|
|
103
|
+
outcomeSha256: run.outcomeSha256,
|
|
104
|
+
});
|
|
105
|
+
if (outcome)
|
|
106
|
+
outcomes.set(run.workerRunId, outcome);
|
|
107
|
+
}
|
|
90
108
|
const graphNodes = graph.success ? graph.data.nodes : [];
|
|
91
109
|
let planning;
|
|
92
110
|
if (graph.success) {
|
|
@@ -96,7 +114,7 @@ async function loadFeatureProjection(input) {
|
|
|
96
114
|
const value = YAML.parse(await readFile(path.join(featureDir, "tasks", node.task), "utf-8"));
|
|
97
115
|
taskSpecs.set(node.id, taskSpecSchema.parse(value));
|
|
98
116
|
}
|
|
99
|
-
planning = projectReadyPlan({ featureId: validation.featureId, graph: graph.data, taskSpecs, states, selectionLimit: Math.max(graph.data.nodes.length, 1) });
|
|
117
|
+
planning = projectReadyPlan({ featureId: validation.featureId, graph: graph.data, taskSpecs, states, outcomes, selectionLimit: Math.max(graph.data.nodes.length, 1) });
|
|
100
118
|
}
|
|
101
119
|
catch (error) {
|
|
102
120
|
projectionWarnings.push(`Ready Planner projection is unavailable: ${message(error)}`);
|
|
@@ -189,6 +207,8 @@ async function loadFeatureProjection(input) {
|
|
|
189
207
|
...(state?.status ? { status: state.status } : {}),
|
|
190
208
|
...(state?.workerRunId ? { workerRunId: state.workerRunId } : {}),
|
|
191
209
|
...(state?.failure?.category ? { failureCategory: state.failure.category } : {}),
|
|
210
|
+
...(latest?.workflow ? { workflow: latest.workflow } : {}),
|
|
211
|
+
...(latest?.outcomePath && latest.outcomeSha256 ? { outcome: { path: latest.outcomePath, sha256: latest.outcomeSha256 } } : {}),
|
|
192
212
|
failureEvidence: Object.values(latest?.failureArtifacts ?? {}).filter((value) => typeof value === "string"),
|
|
193
213
|
};
|
|
194
214
|
});
|
|
@@ -206,15 +226,16 @@ async function loadFeatureProjection(input) {
|
|
|
206
226
|
acId: item.id,
|
|
207
227
|
required: item.priority === "must",
|
|
208
228
|
expectedTaskRefs: item.verification.expected_task_refs,
|
|
209
|
-
status:
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
229
|
+
status: resolveAcceptanceCoverageStatus({
|
|
230
|
+
item,
|
|
231
|
+
states,
|
|
232
|
+
outcomes,
|
|
233
|
+
failed,
|
|
234
|
+
done,
|
|
235
|
+
referencedCount: referenced.length,
|
|
236
|
+
explicit,
|
|
237
|
+
expectedFeatureId: validation.featureId,
|
|
238
|
+
}),
|
|
218
239
|
evidence: explicit?.evidence ?? [],
|
|
219
240
|
blockedBy: failed,
|
|
220
241
|
};
|
|
@@ -370,7 +391,7 @@ const explicitCoverageSchema = z.object({
|
|
|
370
391
|
featureId: z.string().min(1),
|
|
371
392
|
items: z.array(z.object({
|
|
372
393
|
acId: z.string().min(1),
|
|
373
|
-
status: z.enum(["covered", "partial", "blocked", "waived", "missing"]),
|
|
394
|
+
status: z.enum(["covered", "partial", "blocked", "waived", "missing", "awaiting-verification"]),
|
|
374
395
|
evidence: z.array(z.object({
|
|
375
396
|
path: z.string().min(1),
|
|
376
397
|
sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
@@ -517,3 +538,77 @@ function normalizeRepoRef(ref, repoRoot) {
|
|
|
517
538
|
function isDevelopmentTask(type) {
|
|
518
539
|
return !type.startsWith("qa-") && !["architecture", "review"].includes(type);
|
|
519
540
|
}
|
|
541
|
+
export function resolveAcceptanceCoverageStatus(input) {
|
|
542
|
+
const dualCoverage = projectDualCoverage(input.item, input.states, input.outcomes, input.failed, input.done, input.expectedFeatureId);
|
|
543
|
+
if (input.failed.length > 0)
|
|
544
|
+
return "blocked";
|
|
545
|
+
if (input.explicit?.status === "covered" &&
|
|
546
|
+
input.explicit.evidence.length > 0 &&
|
|
547
|
+
(dualCoverage === undefined || dualCoverage === "covered")) {
|
|
548
|
+
return "covered";
|
|
549
|
+
}
|
|
550
|
+
if (input.explicit?.status === "waived")
|
|
551
|
+
return "waived";
|
|
552
|
+
if (dualCoverage === "covered")
|
|
553
|
+
return "awaiting-verification";
|
|
554
|
+
return dualCoverage ??
|
|
555
|
+
(input.done.length === input.referencedCount && input.done.length > 0
|
|
556
|
+
? "partial"
|
|
557
|
+
: "missing");
|
|
558
|
+
}
|
|
559
|
+
export function projectDualCoverage(item, states, outcomes, failed, done, expectedFeatureId) {
|
|
560
|
+
const implRefs = item.verification.implementation_task_refs ?? [];
|
|
561
|
+
const verRefs = item.verification.verification_task_refs ?? [];
|
|
562
|
+
const integration = item.verification.integration;
|
|
563
|
+
const dualMode = implRefs.length > 0 || verRefs.length > 0 || integration !== undefined;
|
|
564
|
+
if (!dualMode)
|
|
565
|
+
return undefined;
|
|
566
|
+
const envByWorkerRunId = (workerRunId) => workerRunId ? outcomes.get(workerRunId) : undefined;
|
|
567
|
+
// 1. implementation refs must all be Done.
|
|
568
|
+
const implDone = implRefs.length > 0
|
|
569
|
+
? implRefs.every((id) => states[id]?.status === "Done")
|
|
570
|
+
: done.length > 0;
|
|
571
|
+
// 2. verification refs must each have a succeeded outcome envelope.
|
|
572
|
+
let verSucceeded = false;
|
|
573
|
+
let verEnvelopes = [];
|
|
574
|
+
if (verRefs.length > 0) {
|
|
575
|
+
verEnvelopes = verRefs
|
|
576
|
+
.map((taskId) => ({ taskId, envelope: envByWorkerRunId(states[taskId]?.workerRunId) }))
|
|
577
|
+
.filter((value) => Boolean(value.envelope));
|
|
578
|
+
verSucceeded =
|
|
579
|
+
verEnvelopes.length === verRefs.length &&
|
|
580
|
+
verEnvelopes.every(({ envelope }) => envelope.outcomeStatus === "succeeded");
|
|
581
|
+
}
|
|
582
|
+
if (failed.length > 0)
|
|
583
|
+
return "blocked";
|
|
584
|
+
if (!implDone) {
|
|
585
|
+
return done.length > 0 ? "partial" : "missing";
|
|
586
|
+
}
|
|
587
|
+
if (verRefs.length === 0 || !verSucceeded)
|
|
588
|
+
return "awaiting-verification";
|
|
589
|
+
if (expectedFeatureId && verEnvelopes.some(({ taskId, envelope }) => envelope.identity?.featureId !== expectedFeatureId ||
|
|
590
|
+
envelope.identity?.taskId !== taskId ||
|
|
591
|
+
(envelope.identity?.workflow !== "backend-test" && envelope.identity?.workflow !== "frontend-test"))) {
|
|
592
|
+
return "awaiting-verification";
|
|
593
|
+
}
|
|
594
|
+
const requiredEvidence = item.verification.required_evidence ?? [];
|
|
595
|
+
if (requiredEvidence.some((token) => !verEnvelopes.some(({ envelope }) => isShellVerificationToken(token)
|
|
596
|
+
? envelope.shellVerification?.exitZero === true
|
|
597
|
+
: envelope.artifacts?.some((artifact) => artifactMatchesEvidenceToken(artifact, token))))) {
|
|
598
|
+
return "awaiting-verification";
|
|
599
|
+
}
|
|
600
|
+
// 3. integration policy: real-required demands real evidence on every
|
|
601
|
+
// verification envelope. mock-only / locally-validated (real:false) cannot
|
|
602
|
+
// satisfy real-required.
|
|
603
|
+
if (integration === "real-required") {
|
|
604
|
+
if (verRefs.length === 0)
|
|
605
|
+
return "awaiting-verification";
|
|
606
|
+
const realOk = verEnvelopes.every(({ envelope }) => envelope.integrationStatus?.real === true);
|
|
607
|
+
if (!realOk)
|
|
608
|
+
return "awaiting-verification";
|
|
609
|
+
}
|
|
610
|
+
// 4. All dual-coverage prerequisites are satisfied. The caller still requires
|
|
611
|
+
// an explicit, hash-validated coverage artifact before publishing `covered`;
|
|
612
|
+
// without it, the read model remains awaiting-verification.
|
|
613
|
+
return "covered";
|
|
614
|
+
}
|
|
@@ -6,6 +6,7 @@ import { writeTaskConfig, writeTaskArtifactFile } from "../../infrastructure/har
|
|
|
6
6
|
import { getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
|
|
7
7
|
import { resolveLoopAgentProfile } from "../profile-mapping.js";
|
|
8
8
|
import { mapRiskLevelToComplexity } from "../task-spec/complexity-mapping.js";
|
|
9
|
+
import { resolveWorkflow } from "../task-spec/workflow-routing.js";
|
|
9
10
|
import { validateTaskSpec } from "../task-spec/validate.js";
|
|
10
11
|
export async function materializeTaskSpec(options) {
|
|
11
12
|
const now = options.now ?? new Date();
|
|
@@ -17,6 +18,7 @@ export async function materializeTaskSpec(options) {
|
|
|
17
18
|
throw new Error(`TaskSpec validation failed: ${codes}`);
|
|
18
19
|
}
|
|
19
20
|
const harnessTaskId = buildHarnessTaskId(options.taskSpec, now);
|
|
21
|
+
const resolvedWorkflow = resolveWorkflow(options.taskSpec);
|
|
20
22
|
const existingConfig = await loadExistingTaskConfig(options.repoRoot, harnessTaskId);
|
|
21
23
|
if (!existingConfig) {
|
|
22
24
|
const newTaskResult = await options.client.run(["new-task", harnessTaskId, options.taskSpec.title], {
|
|
@@ -36,6 +38,8 @@ export async function materializeTaskSpec(options) {
|
|
|
36
38
|
...baseConfig,
|
|
37
39
|
taskId: harnessTaskId,
|
|
38
40
|
title: options.taskSpec.title,
|
|
41
|
+
taskKind: resolvedWorkflow.taskKind,
|
|
42
|
+
featureId: options.taskSpec.feature_id,
|
|
39
43
|
allowedPaths: options.taskSpec.constraints.allowed_paths,
|
|
40
44
|
forbiddenPaths: options.taskSpec.constraints.forbidden_paths,
|
|
41
45
|
hardConstraints: options.taskSpec.constraints.hard_constraints,
|
|
@@ -87,6 +91,7 @@ export async function materializeTaskSpec(options) {
|
|
|
87
91
|
businessId: options.taskSpec.id,
|
|
88
92
|
harnessTaskId,
|
|
89
93
|
featureId: options.taskSpec.feature_id,
|
|
94
|
+
workflow: resolvedWorkflow.workflow,
|
|
90
95
|
loopAgentProfile: profileMapping.loopAgentProfile,
|
|
91
96
|
taskConfigPath: paths.taskConfigPath,
|
|
92
97
|
source: {
|
|
@@ -159,6 +159,7 @@ export async function listTaskRunHistory(repoRoot, featureId, taskId, options =
|
|
|
159
159
|
workerRunId: run.workerRunId,
|
|
160
160
|
taskId: run.taskId,
|
|
161
161
|
featureId: run.featureId ?? featureId,
|
|
162
|
+
workflow: enriched?.workflow ?? run.workflow,
|
|
162
163
|
status: enriched?.status ?? mapRunRecordStatus(run.status),
|
|
163
164
|
startedAt: enriched?.startedAt,
|
|
164
165
|
finishedAt: enriched?.finishedAt ?? run.recordedAt,
|
|
@@ -335,6 +336,7 @@ function mergeLedgerRun(taskMap, run, warnings) {
|
|
|
335
336
|
runRecordPath: run.runRecordPath,
|
|
336
337
|
dagPath: run.dagPath,
|
|
337
338
|
...run.failureArtifacts,
|
|
339
|
+
...(run.outcomePath ? { outcome: run.outcomePath } : {}),
|
|
338
340
|
};
|
|
339
341
|
taskMap.set(key, {
|
|
340
342
|
...(sameRun
|
|
@@ -347,6 +349,7 @@ function mergeLedgerRun(taskMap, run, warnings) {
|
|
|
347
349
|
}),
|
|
348
350
|
batchRunId: run.batchRunId,
|
|
349
351
|
featureId,
|
|
352
|
+
workflow: run.workflow ?? existing.workflow,
|
|
350
353
|
status: mapRunRecordStatus(run.status),
|
|
351
354
|
workerRunId: run.workerRunId,
|
|
352
355
|
harnessTaskId: run.harnessTaskId ?? existing.harnessTaskId,
|
|
@@ -491,6 +494,7 @@ function buildHistoricalRunMap(ledgerRuns, events, workerRunFeatureIndex = new M
|
|
|
491
494
|
taskId: run.taskId,
|
|
492
495
|
batchRunId: run.batchRunId,
|
|
493
496
|
featureId: run.featureId,
|
|
497
|
+
workflow: run.workflow,
|
|
494
498
|
status: mapRunRecordStatus(run.status),
|
|
495
499
|
workerRunId: run.workerRunId,
|
|
496
500
|
harnessTaskId: run.harnessTaskId,
|
|
@@ -501,6 +505,7 @@ function buildHistoricalRunMap(ledgerRuns, events, workerRunFeatureIndex = new M
|
|
|
501
505
|
runRecordPath: run.runRecordPath,
|
|
502
506
|
dagPath: run.dagPath,
|
|
503
507
|
...run.failureArtifacts,
|
|
508
|
+
...(run.outcomePath ? { outcome: run.outcomePath } : {}),
|
|
504
509
|
}),
|
|
505
510
|
});
|
|
506
511
|
}
|
|
@@ -1060,6 +1065,8 @@ async function loadLedgerRuns(repoRoot) {
|
|
|
1060
1065
|
workerRunId,
|
|
1061
1066
|
taskId,
|
|
1062
1067
|
featureId: readString(parsed, "featureId"),
|
|
1068
|
+
workflow: readString(parsed, "workflow"),
|
|
1069
|
+
outcomePath: readString(parsed, "outcomePath"),
|
|
1063
1070
|
status,
|
|
1064
1071
|
harnessTaskId: readString(parsed, "harnessTaskId"),
|
|
1065
1072
|
runRecordPath: readString(parsed, "runRecordPath"),
|