@tea-agent/loop-agent 0.16.1-beta.2 → 0.16.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/AGENTS.md +4 -8
- package/CHANGELOG.md +76 -18
- package/README.md +76 -299
- 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/dag-reconcile-run.js +3 -116
- package/dist/commands/eval.js +1176 -13
- package/dist/commands/init.js +7 -1
- package/dist/executors/dag-pi-executor.js +8 -46
- package/dist/executors/pi-sdk-executor.js +66 -3
- package/dist/executors/shell-executor.js +213 -30
- package/dist/executors/shell-presets.js +12 -2
- package/dist/executors/shell-write-guard.js +20 -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/records/promotion.js +3 -1
- package/dist/shared/git-progress.js +9 -2
- package/dist/worker/cli.js +83 -0
- package/dist/worker/delivery/git-transaction.js +75 -0
- package/dist/worker/delivery/verification-bundle.js +13 -2
- package/dist/worker/feature/review.js +3 -2
- package/dist/worker/observability/read-model.js +56 -0
- package/dist/worker/observe/server.js +6 -3
- package/dist/worker/observe/static/dag-helpers.js +0 -62
- package/dist/worker/observe/static/styles.css +18 -55
- package/dist/worker/observe/static/views/dag.js +13 -5
- package/dist/worker/outcomes/adapters.js +4 -1
- package/dist/worker/outcomes/declared-artifacts.js +103 -0
- package/dist/worker/outcomes/evidence-tokens.js +29 -0
- package/dist/worker/outcomes/gate.js +10 -11
- package/dist/worker/outcomes/projector.js +30 -4
- package/dist/worker/outcomes/types.js +3 -0
- package/dist/worker/pool/reconcile.js +285 -0
- package/dist/worker/run-task/run-task.js +81 -4
- package/dist/worker/runner/run-ready.js +25 -2
- package/dist/worker/task-graph/ready-planner.js +14 -8
- package/dist/worker/task-graph/task-graph-schema.js +5 -3
- package/dist/workflows/dag/backend-test-analysis-contract.js +87 -30
- package/dist/workflows/dag/backend-test-case-manifest.js +71 -8
- package/dist/workflows/dag/backend-test-execution-contract.js +63 -11
- package/dist/workflows/dag/backend-test-repair-contract.js +94 -0
- package/dist/workflows/dag/backend-test-result-contract.js +6 -4
- package/dist/workflows/dag/backend-test-semantic-review-contract.js +36 -0
- package/dist/workflows/dag/budget-enforcement.js +67 -0
- package/dist/workflows/dag/context-policy.js +137 -0
- package/dist/workflows/dag/dynamic-runtime/condition.js +1 -1
- package/dist/workflows/dag/dynamic-runtime/shared.js +42 -0
- package/dist/workflows/dag/failure-routing.js +8 -1
- package/dist/workflows/dag/frontend-implementation-contract.js +32 -93
- package/dist/workflows/dag/init-hybrid.js +624 -172
- package/dist/workflows/dag/knowledge-curator.js +3 -0
- package/dist/workflows/dag/lifecycle.js +33 -2
- package/dist/workflows/dag/node-execution.js +11 -4
- package/dist/workflows/dag/prompt.js +1 -1
- package/dist/workflows/dag/reconcile-run.js +121 -0
- package/dist/workflows/dag/report.js +12 -0
- package/dist/workflows/dag/runner.js +43 -16
- package/dist/workflows/dag/scheduler.js +87 -17
- package/dist/workflows/dag/skill-snapshot.js +11 -7
- package/dist/workflows/dag/types.js +49 -1
- package/dist/workflows/dag/validate.js +35 -15
- package/docs/README.md +3 -1
- package/docs/architecture/runtime-boundaries.md +3 -2
- package/docs/init-surface.manifest.json +4 -0
- package/docs/local-development-environment.md +52 -0
- package/docs/templates/agent-dag.schema.json +25 -7
- package/docs/templates/agent-dag.supervised-implementation.json +23 -4
- package/docs/templates/backend-test-analysis.schema.json +9 -16
- package/docs/templates/backend-test-dag.json +493 -197
- package/docs/templates/backend-test-dag.review-cases.prompt.md +10 -4
- package/docs/templates/backend-test-execution.schema.json +6 -1
- package/docs/templates/branch-merge-report.md +14 -0
- 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 +2 -1
- package/skills/frontend-implementation/references/node-contracts.md +3 -3
- package/skills/loop-agent/references/command-reference.md +5 -0
- package/skills/loop-agent/references/hybrid-dag.md +7 -4
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { executeDagReconcileRun } from "../../workflows/dag/reconcile-run.js";
|
|
4
|
+
import { assessDagRunLiveness, assessDagRunRecoveryEligibility, locateDagRun, readDagRunState, } from "../../workflows/dag/lifecycle.js";
|
|
5
|
+
import { appendTaskPoolEvent, findRunByWorkerRunId, getTaskPoolRoot, readTaskPoolState, writeTaskPoolState, } from "./run-store.js";
|
|
6
|
+
const RECOVERABLE_LIVENESS = new Set([
|
|
7
|
+
"active",
|
|
8
|
+
"node-quiet",
|
|
9
|
+
"stale",
|
|
10
|
+
]);
|
|
11
|
+
export async function reconcileTaskPoolAbandon(input) {
|
|
12
|
+
const reason = input.reason.trim();
|
|
13
|
+
if (!reason)
|
|
14
|
+
throw new Error("task reconcile abandon requires --reason <text>");
|
|
15
|
+
const ref = { featureId: input.featureId, taskId: input.taskId };
|
|
16
|
+
const current = await readTaskPoolState(input.repoRoot, ref);
|
|
17
|
+
if (!current) {
|
|
18
|
+
throw new Error(`cannot reconcile ${input.taskId}: Task Pool state does not exist for ${input.featureId}/${input.taskId}`);
|
|
19
|
+
}
|
|
20
|
+
if (current.status === "Done") {
|
|
21
|
+
throw new Error(`cannot reconcile ${input.taskId}: Task Pool status is Done`);
|
|
22
|
+
}
|
|
23
|
+
const { run, dagLocated } = await validateWorkerDagAssociation({
|
|
24
|
+
repoRoot: input.repoRoot,
|
|
25
|
+
featureId: input.featureId,
|
|
26
|
+
taskId: input.taskId,
|
|
27
|
+
workerRunId: input.workerRunId,
|
|
28
|
+
dagRunId: input.dagRunId,
|
|
29
|
+
});
|
|
30
|
+
const dagState = await readDagRunState(dagLocated.runDir);
|
|
31
|
+
const liveness = assessDagRunLiveness({ state: dagState });
|
|
32
|
+
const eligibility = assessDagRunRecoveryEligibility({
|
|
33
|
+
lifecycle: dagLocated.lifecycle,
|
|
34
|
+
state: dagState,
|
|
35
|
+
liveness: liveness.status,
|
|
36
|
+
});
|
|
37
|
+
const alreadyAbandoned = dagLocated.lifecycle === "completed" &&
|
|
38
|
+
(dagState.status === "abandoned" || dagState.reconciliation?.action === "abandon");
|
|
39
|
+
const poolAlreadyFailed = current.status === "Failed" &&
|
|
40
|
+
current.workerRunId === input.workerRunId &&
|
|
41
|
+
current.operatorRecovery?.action === "task-reconcile-abandon";
|
|
42
|
+
if (poolAlreadyFailed && alreadyAbandoned) {
|
|
43
|
+
const evidencePath = current.operatorRecovery?.evidencePath ??
|
|
44
|
+
recoveryEvidencePath(input.repoRoot, input.featureId, input.taskId, input.workerRunId);
|
|
45
|
+
return {
|
|
46
|
+
schemaVersion: 1,
|
|
47
|
+
status: "Failed",
|
|
48
|
+
taskId: input.taskId,
|
|
49
|
+
featureId: input.featureId,
|
|
50
|
+
workerRunId: input.workerRunId,
|
|
51
|
+
dagRunId: input.dagRunId,
|
|
52
|
+
idempotent: true,
|
|
53
|
+
evidencePath,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
assertAbandonAllowed({
|
|
57
|
+
eligibility,
|
|
58
|
+
liveness: liveness.status,
|
|
59
|
+
forceAbandonRecoverable: input.forceAbandonRecoverable === true,
|
|
60
|
+
dagRunId: input.dagRunId,
|
|
61
|
+
});
|
|
62
|
+
let dagReconcile;
|
|
63
|
+
if (!alreadyAbandoned) {
|
|
64
|
+
dagReconcile = await executeDagReconcileRun(input.repoRoot, [
|
|
65
|
+
"--run-id",
|
|
66
|
+
input.dagRunId,
|
|
67
|
+
"--action",
|
|
68
|
+
"abandon",
|
|
69
|
+
"--reason",
|
|
70
|
+
reason,
|
|
71
|
+
]);
|
|
72
|
+
}
|
|
73
|
+
const at = (input.now ?? new Date()).toISOString();
|
|
74
|
+
const evidencePath = await writeRecoveryEvidence(input.repoRoot, {
|
|
75
|
+
kind: "task-reconcile-abandon",
|
|
76
|
+
featureId: input.featureId,
|
|
77
|
+
taskId: input.taskId,
|
|
78
|
+
workerRunId: input.workerRunId,
|
|
79
|
+
dagRunId: input.dagRunId,
|
|
80
|
+
reason,
|
|
81
|
+
at,
|
|
82
|
+
runRecordPath: run.runRecordPath,
|
|
83
|
+
dagReconcile,
|
|
84
|
+
});
|
|
85
|
+
const operatorRecovery = {
|
|
86
|
+
action: "task-reconcile-abandon",
|
|
87
|
+
reason,
|
|
88
|
+
workerRunId: input.workerRunId,
|
|
89
|
+
dagRunId: input.dagRunId,
|
|
90
|
+
abandonedWorkerRunId: input.workerRunId,
|
|
91
|
+
at,
|
|
92
|
+
evidencePath: path.relative(input.repoRoot, evidencePath).split(path.sep).join("/"),
|
|
93
|
+
};
|
|
94
|
+
await projectTaskPoolFailed(input.repoRoot, current, {
|
|
95
|
+
featureId: input.featureId,
|
|
96
|
+
taskId: input.taskId,
|
|
97
|
+
workerRunId: input.workerRunId,
|
|
98
|
+
updatedAt: at,
|
|
99
|
+
operatorRecovery,
|
|
100
|
+
});
|
|
101
|
+
await appendTaskPoolEvent(input.repoRoot, {
|
|
102
|
+
schemaVersion: 1,
|
|
103
|
+
at,
|
|
104
|
+
type: "task-reconcile-abandon",
|
|
105
|
+
featureId: input.featureId,
|
|
106
|
+
taskId: input.taskId,
|
|
107
|
+
workerRunId: input.workerRunId,
|
|
108
|
+
dagRunId: input.dagRunId,
|
|
109
|
+
reason,
|
|
110
|
+
idempotent: false,
|
|
111
|
+
evidencePath: operatorRecovery.evidencePath,
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
schemaVersion: 1,
|
|
115
|
+
status: "Failed",
|
|
116
|
+
taskId: input.taskId,
|
|
117
|
+
featureId: input.featureId,
|
|
118
|
+
workerRunId: input.workerRunId,
|
|
119
|
+
dagRunId: input.dagRunId,
|
|
120
|
+
idempotent: false,
|
|
121
|
+
...(dagReconcile ? { dagReconcile } : {}),
|
|
122
|
+
evidencePath: operatorRecovery.evidencePath,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
export async function markTaskPoolFailed(input) {
|
|
126
|
+
const reason = input.reason.trim();
|
|
127
|
+
if (!reason)
|
|
128
|
+
throw new Error("pool mark-failed requires --reason <text>");
|
|
129
|
+
await validateWorkerRunOwnership({
|
|
130
|
+
repoRoot: input.repoRoot,
|
|
131
|
+
featureId: input.featureId,
|
|
132
|
+
taskId: input.taskId,
|
|
133
|
+
workerRunId: input.workerRunId,
|
|
134
|
+
});
|
|
135
|
+
const ref = { featureId: input.featureId, taskId: input.taskId };
|
|
136
|
+
const current = await readTaskPoolState(input.repoRoot, ref);
|
|
137
|
+
if (!current) {
|
|
138
|
+
throw new Error(`cannot mark-failed ${input.taskId}: Task Pool state does not exist for ${input.featureId}/${input.taskId}`);
|
|
139
|
+
}
|
|
140
|
+
if (current.status === "Done") {
|
|
141
|
+
throw new Error(`cannot mark-failed ${input.taskId}: Task Pool status is Done`);
|
|
142
|
+
}
|
|
143
|
+
const at = (input.now ?? new Date()).toISOString();
|
|
144
|
+
const evidencePath = recoveryEvidencePath(input.repoRoot, input.featureId, input.taskId, input.workerRunId);
|
|
145
|
+
const relativeEvidencePath = path.relative(input.repoRoot, evidencePath).split(path.sep).join("/");
|
|
146
|
+
if (current.status === "Failed" &&
|
|
147
|
+
current.workerRunId === input.workerRunId &&
|
|
148
|
+
current.operatorRecovery?.action === "pool-mark-failed") {
|
|
149
|
+
return {
|
|
150
|
+
schemaVersion: 1,
|
|
151
|
+
status: "Failed",
|
|
152
|
+
taskId: input.taskId,
|
|
153
|
+
featureId: input.featureId,
|
|
154
|
+
workerRunId: input.workerRunId,
|
|
155
|
+
idempotent: true,
|
|
156
|
+
evidencePath: current.operatorRecovery.evidencePath ?? relativeEvidencePath,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
await writeRecoveryEvidence(input.repoRoot, {
|
|
160
|
+
kind: "pool-mark-failed",
|
|
161
|
+
featureId: input.featureId,
|
|
162
|
+
taskId: input.taskId,
|
|
163
|
+
workerRunId: input.workerRunId,
|
|
164
|
+
reason,
|
|
165
|
+
at,
|
|
166
|
+
});
|
|
167
|
+
const operatorRecovery = {
|
|
168
|
+
action: "pool-mark-failed",
|
|
169
|
+
reason,
|
|
170
|
+
workerRunId: input.workerRunId,
|
|
171
|
+
at,
|
|
172
|
+
evidencePath: relativeEvidencePath,
|
|
173
|
+
note: "emergency Task Pool projection; does not mutate DAG lifecycle",
|
|
174
|
+
};
|
|
175
|
+
await projectTaskPoolFailed(input.repoRoot, current, {
|
|
176
|
+
featureId: input.featureId,
|
|
177
|
+
taskId: input.taskId,
|
|
178
|
+
workerRunId: input.workerRunId,
|
|
179
|
+
updatedAt: at,
|
|
180
|
+
operatorRecovery,
|
|
181
|
+
});
|
|
182
|
+
await appendTaskPoolEvent(input.repoRoot, {
|
|
183
|
+
schemaVersion: 1,
|
|
184
|
+
at,
|
|
185
|
+
type: "task-pool-mark-failed",
|
|
186
|
+
featureId: input.featureId,
|
|
187
|
+
taskId: input.taskId,
|
|
188
|
+
workerRunId: input.workerRunId,
|
|
189
|
+
reason,
|
|
190
|
+
idempotent: false,
|
|
191
|
+
evidencePath: relativeEvidencePath,
|
|
192
|
+
});
|
|
193
|
+
return {
|
|
194
|
+
schemaVersion: 1,
|
|
195
|
+
status: "Failed",
|
|
196
|
+
taskId: input.taskId,
|
|
197
|
+
featureId: input.featureId,
|
|
198
|
+
workerRunId: input.workerRunId,
|
|
199
|
+
idempotent: false,
|
|
200
|
+
evidencePath: relativeEvidencePath,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
async function validateWorkerDagAssociation(input) {
|
|
204
|
+
const run = await validateWorkerRunOwnership(input);
|
|
205
|
+
const dagLocated = await locateDagRun(input.repoRoot, input.dagRunId);
|
|
206
|
+
if (!dagLocated) {
|
|
207
|
+
throw new Error(`dag run not found: ${input.dagRunId}`);
|
|
208
|
+
}
|
|
209
|
+
const dagState = await readDagRunState(dagLocated.runDir);
|
|
210
|
+
if (dagState.runId !== input.dagRunId) {
|
|
211
|
+
throw new Error(`dag run identity mismatch: expected ${input.dagRunId}, found ${dagState.runId}`);
|
|
212
|
+
}
|
|
213
|
+
const linked = await proveWorkerDagLink(input.repoRoot, run, input.workerRunId, input.dagRunId);
|
|
214
|
+
if (!linked) {
|
|
215
|
+
throw new Error(`worker run ${input.workerRunId} is not associated with dag run ${input.dagRunId}`);
|
|
216
|
+
}
|
|
217
|
+
return { run, dagLocated: { ...dagLocated, runId: input.dagRunId } };
|
|
218
|
+
}
|
|
219
|
+
async function validateWorkerRunOwnership(input) {
|
|
220
|
+
const run = await findRunByWorkerRunId(input.repoRoot, input.workerRunId);
|
|
221
|
+
if (!run) {
|
|
222
|
+
throw new Error(`worker run not found: ${input.workerRunId}`);
|
|
223
|
+
}
|
|
224
|
+
if (run.featureId !== input.featureId || run.taskId !== input.taskId) {
|
|
225
|
+
throw new Error(`worker run ownership mismatch: expected ${input.featureId}/${input.taskId}, found ${run.featureId}/${run.taskId}`);
|
|
226
|
+
}
|
|
227
|
+
return run;
|
|
228
|
+
}
|
|
229
|
+
async function proveWorkerDagLink(repoRoot, run, workerRunId, dagRunId) {
|
|
230
|
+
if (dagRunId === workerRunId)
|
|
231
|
+
return true;
|
|
232
|
+
if (!run.runRecordPath)
|
|
233
|
+
return false;
|
|
234
|
+
try {
|
|
235
|
+
const absolutePath = path.isAbsolute(run.runRecordPath)
|
|
236
|
+
? run.runRecordPath
|
|
237
|
+
: path.join(repoRoot, run.runRecordPath);
|
|
238
|
+
const record = JSON.parse(await readFile(absolutePath, "utf-8"));
|
|
239
|
+
for (const command of record.commands ?? []) {
|
|
240
|
+
const args = command.args ?? [];
|
|
241
|
+
const runIdIndex = args.indexOf("--run-id");
|
|
242
|
+
if (runIdIndex >= 0 && args[runIdIndex + 1] === dagRunId)
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
function assertAbandonAllowed(input) {
|
|
252
|
+
if (input.forceAbandonRecoverable)
|
|
253
|
+
return;
|
|
254
|
+
if (input.eligibility.canResume) {
|
|
255
|
+
throw new Error(`dag run ${input.dagRunId} is recoverable via resume; refuse abandon or pass --force-abandon-recoverable with documented operator intent`);
|
|
256
|
+
}
|
|
257
|
+
if (RECOVERABLE_LIVENESS.has(input.liveness)) {
|
|
258
|
+
throw new Error(`dag run ${input.dagRunId} liveness is ${input.liveness}; refuse abandon or pass --force-abandon-recoverable with documented operator intent`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async function projectTaskPoolFailed(repoRoot, current, next) {
|
|
262
|
+
await writeTaskPoolState(repoRoot, {
|
|
263
|
+
schemaVersion: 2,
|
|
264
|
+
featureId: next.featureId,
|
|
265
|
+
taskId: next.taskId,
|
|
266
|
+
status: "Failed",
|
|
267
|
+
updatedAt: next.updatedAt,
|
|
268
|
+
workerRunId: next.workerRunId,
|
|
269
|
+
...(current.lastRunRecordPath ? { lastRunRecordPath: current.lastRunRecordPath } : {}),
|
|
270
|
+
...(current.failure ? { failure: current.failure } : {}),
|
|
271
|
+
...(current.retryOfWorkerRunId ? { retryOfWorkerRunId: current.retryOfWorkerRunId } : {}),
|
|
272
|
+
...(current.retryRequestedAt ? { retryRequestedAt: current.retryRequestedAt } : {}),
|
|
273
|
+
...(current.retryReason ? { retryReason: current.retryReason } : {}),
|
|
274
|
+
operatorRecovery: next.operatorRecovery,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
function recoveryEvidencePath(repoRoot, featureId, taskId, workerRunId) {
|
|
278
|
+
return path.join(getTaskPoolRoot(repoRoot), "artifacts", "recovery", featureId, taskId, `${workerRunId}.json`);
|
|
279
|
+
}
|
|
280
|
+
async function writeRecoveryEvidence(repoRoot, payload) {
|
|
281
|
+
const evidencePath = recoveryEvidencePath(repoRoot, payload.featureId, payload.taskId, payload.workerRunId);
|
|
282
|
+
await mkdir(path.dirname(evidencePath), { recursive: true });
|
|
283
|
+
await writeFile(evidencePath, `${JSON.stringify({ schemaVersion: 1, ...payload }, null, 2)}\n`, "utf-8");
|
|
284
|
+
return evidencePath;
|
|
285
|
+
}
|
|
@@ -124,6 +124,7 @@ export async function runTaskSpec(options) {
|
|
|
124
124
|
});
|
|
125
125
|
progress.step(`run-dag finished in ${formatDuration(Date.now() - runDagStartedAt)}`);
|
|
126
126
|
const reportStep = "report-decision";
|
|
127
|
+
let capturedReportJson;
|
|
127
128
|
const reportDecision = await runObservedStep(eventCtx, reportStep, async () => {
|
|
128
129
|
const reportJson = await client.run([
|
|
129
130
|
"dag",
|
|
@@ -138,6 +139,7 @@ export async function runTaskSpec(options) {
|
|
|
138
139
|
artifactName: "dag-report-json",
|
|
139
140
|
expectJson: true,
|
|
140
141
|
});
|
|
142
|
+
capturedReportJson = reportJson;
|
|
141
143
|
const decision = decideFromReport(workerRunId, reportJson);
|
|
142
144
|
await client.run([
|
|
143
145
|
"dag",
|
|
@@ -153,6 +155,14 @@ export async function runTaskSpec(options) {
|
|
|
153
155
|
});
|
|
154
156
|
return decision;
|
|
155
157
|
}, { statusForResult: (decision) => decision.succeeded ? "succeeded" : "failed" });
|
|
158
|
+
const dagReportRun = extractDagReportRun(workerRunId, capturedReportJson);
|
|
159
|
+
const declaredArtifacts = options.declaredArtifacts
|
|
160
|
+
? options.declaredArtifacts.map((entry) => ({
|
|
161
|
+
kind: entry.kind,
|
|
162
|
+
...(entry.schemaId ? { schemaId: entry.schemaId } : {}),
|
|
163
|
+
...(entry.path ? { path: entry.path } : {}),
|
|
164
|
+
}))
|
|
165
|
+
: undefined;
|
|
156
166
|
let status = reportDecision.succeeded ? "succeeded" : "failed";
|
|
157
167
|
progress.step(`report decision: ${status} (${reportDecision.reason}${reportDecision.runStatus ? `, status=${reportDecision.runStatus}` : ""})`);
|
|
158
168
|
const failureArtifacts = reportDecision.succeeded
|
|
@@ -180,6 +190,7 @@ export async function runTaskSpec(options) {
|
|
|
180
190
|
materializeManifest,
|
|
181
191
|
reportDecision,
|
|
182
192
|
commands,
|
|
193
|
+
...(declaredArtifacts ? { declaredArtifacts } : {}),
|
|
183
194
|
...(failureArtifacts ? { failureArtifacts } : {}),
|
|
184
195
|
...(controllerIdentity ? { controllerIdentity } : {}),
|
|
185
196
|
};
|
|
@@ -202,6 +213,8 @@ export async function runTaskSpec(options) {
|
|
|
202
213
|
reportDecision,
|
|
203
214
|
runRecordPath,
|
|
204
215
|
dagPath,
|
|
216
|
+
...(dagReportRun ? { dagReportRun } : {}),
|
|
217
|
+
...(declaredArtifacts ? { declaredArtifacts } : {}),
|
|
205
218
|
acceptanceRefs: options.taskSpec.acceptance_refs,
|
|
206
219
|
now,
|
|
207
220
|
});
|
|
@@ -211,15 +224,34 @@ export async function runTaskSpec(options) {
|
|
|
211
224
|
progress.step(`outcome projection failed: ${projection.category} (${projection.reason})`);
|
|
212
225
|
}
|
|
213
226
|
else {
|
|
214
|
-
|
|
227
|
+
let envelope = projection.envelope;
|
|
228
|
+
const gate = checkRequiredOutputs(envelope, options.taskSpec.outputs.required);
|
|
215
229
|
if (reportDecision.succeeded && !gate.passed) {
|
|
216
230
|
status = "failed";
|
|
217
|
-
outcomeFailure = {
|
|
231
|
+
outcomeFailure = {
|
|
232
|
+
category: "ContractMismatch",
|
|
233
|
+
reason: `missing required outputs: ${gate.missing.join(", ")}`,
|
|
234
|
+
};
|
|
235
|
+
envelope = {
|
|
236
|
+
...envelope,
|
|
237
|
+
outcomeStatus: "failed",
|
|
238
|
+
outcomeFailure: {
|
|
239
|
+
category: "ContractMismatch",
|
|
240
|
+
reason: `missing required outputs: ${gate.missing.join(", ")}`,
|
|
241
|
+
missingOutputs: [...gate.missing],
|
|
242
|
+
},
|
|
243
|
+
};
|
|
218
244
|
progress.step(`required output gate failed: ${gate.missing.join(", ")}`);
|
|
219
245
|
}
|
|
220
|
-
else {
|
|
221
|
-
|
|
246
|
+
else if (envelope.outcomeFailure) {
|
|
247
|
+
outcomeFailure = {
|
|
248
|
+
category: envelope.outcomeFailure.category,
|
|
249
|
+
reason: envelope.outcomeFailure.reason,
|
|
250
|
+
};
|
|
222
251
|
}
|
|
252
|
+
// Persist succeeded and failed envelopes so read models can inspect
|
|
253
|
+
// outcomeFailure without re-parsing run-record / report decision.
|
|
254
|
+
outcome = await writeOutcome(options.repoRoot, envelope);
|
|
223
255
|
}
|
|
224
256
|
if (status === "succeeded" && !options.skipSuccessFinalization) {
|
|
225
257
|
await runObservedStep(eventCtx, "promote-run", async () => {
|
|
@@ -245,6 +277,7 @@ export async function runTaskSpec(options) {
|
|
|
245
277
|
materializeManifest,
|
|
246
278
|
reportDecision,
|
|
247
279
|
commands,
|
|
280
|
+
...(declaredArtifacts ? { declaredArtifacts } : {}),
|
|
248
281
|
...(failureArtifacts ? { failureArtifacts } : {}),
|
|
249
282
|
...(controllerIdentity ? { controllerIdentity } : {}),
|
|
250
283
|
};
|
|
@@ -470,6 +503,50 @@ function decideFromReport(workerRunId, result) {
|
|
|
470
503
|
primaryFailure: readProperty(run, "primaryFailure"),
|
|
471
504
|
};
|
|
472
505
|
}
|
|
506
|
+
/**
|
|
507
|
+
* Extract the canonical DAG report run fact used by Outcome adapters for
|
|
508
|
+
* structured artifact projection. Returns undefined when the report is missing
|
|
509
|
+
* or does not contain the worker run — adapters then project only canonical
|
|
510
|
+
* runtime artifacts (fail-closed for domain structured kinds).
|
|
511
|
+
*/
|
|
512
|
+
function extractDagReportRun(workerRunId, result) {
|
|
513
|
+
if (!result?.ok || !result.json)
|
|
514
|
+
return undefined;
|
|
515
|
+
const runs = readObjectArray(result.json, "runs");
|
|
516
|
+
const run = runs.find((candidate) => readString(candidate, "runId") === workerRunId) ??
|
|
517
|
+
runs[0];
|
|
518
|
+
if (!run)
|
|
519
|
+
return undefined;
|
|
520
|
+
const nodes = readObjectArray(run, "nodes").map((node) => ({
|
|
521
|
+
...(readString(node, "nodeId") || readString(node, "id")
|
|
522
|
+
? { id: readString(node, "nodeId") ?? readString(node, "id") }
|
|
523
|
+
: {}),
|
|
524
|
+
...(readString(node, "status") ? { status: readString(node, "status") } : {}),
|
|
525
|
+
...(readString(node, "failureCategory")
|
|
526
|
+
? { failureCategory: readString(node, "failureCategory") }
|
|
527
|
+
: {}),
|
|
528
|
+
...(readString(node, "stdoutArtifactPath")
|
|
529
|
+
? { stdoutArtifactPath: readString(node, "stdoutArtifactPath") }
|
|
530
|
+
: {}),
|
|
531
|
+
...(readString(node, "assistantArtifactPath")
|
|
532
|
+
? { assistantArtifactPath: readString(node, "assistantArtifactPath") }
|
|
533
|
+
: {}),
|
|
534
|
+
...(readString(node, "structuredArtifactPath")
|
|
535
|
+
? { structuredArtifactPath: readString(node, "structuredArtifactPath") }
|
|
536
|
+
: {}),
|
|
537
|
+
...(readString(node, "structuredArtifactSha256")
|
|
538
|
+
? { structuredArtifactSha256: readString(node, "structuredArtifactSha256") }
|
|
539
|
+
: {}),
|
|
540
|
+
...(readString(node, "structuredArtifactSchemaId")
|
|
541
|
+
? { structuredArtifactSchemaId: readString(node, "structuredArtifactSchemaId") }
|
|
542
|
+
: {}),
|
|
543
|
+
}));
|
|
544
|
+
return {
|
|
545
|
+
...(readString(run, "runId") ? { runId: readString(run, "runId") } : {}),
|
|
546
|
+
...(readString(run, "status") ? { status: readString(run, "status") } : {}),
|
|
547
|
+
nodes,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
473
550
|
class RecordingRunTaskClient {
|
|
474
551
|
delegate;
|
|
475
552
|
records;
|
|
@@ -158,6 +158,15 @@ export async function runReadyTasks(options) {
|
|
|
158
158
|
...(options.piModel ? { piModel: options.piModel } : {}),
|
|
159
159
|
...(controllerIdentity ? { controllerIdentity } : {}),
|
|
160
160
|
...(options.controllerExpectation ? { controllerExpectation: options.controllerExpectation } : {}),
|
|
161
|
+
...(node?.produces && node.produces.length > 0
|
|
162
|
+
? {
|
|
163
|
+
declaredArtifacts: node.produces.map((entry) => ({
|
|
164
|
+
kind: entry.kind,
|
|
165
|
+
...(entry.schemaId ? { schemaId: entry.schemaId } : {}),
|
|
166
|
+
...(entry.path ? { path: entry.path } : {}),
|
|
167
|
+
})),
|
|
168
|
+
}
|
|
169
|
+
: {}),
|
|
161
170
|
});
|
|
162
171
|
}
|
|
163
172
|
catch (error) {
|
|
@@ -271,6 +280,17 @@ export async function runReadyTasks(options) {
|
|
|
271
280
|
};
|
|
272
281
|
let recorded = false;
|
|
273
282
|
try {
|
|
283
|
+
// Checkpoint (onTaskFinalized) must succeed before Task Pool Done for
|
|
284
|
+
// successful runs. Recording Done first left Pool Done with no Git
|
|
285
|
+
// checkpoint when promote/closeout wrote outside allowed_paths.
|
|
286
|
+
if (result.status === "succeeded" && options.onTaskFinalized) {
|
|
287
|
+
await options.onTaskFinalized({
|
|
288
|
+
status: result.status,
|
|
289
|
+
taskSpec,
|
|
290
|
+
workerRunId: result.workerRunId,
|
|
291
|
+
runRecordPath: result.runRecordPath,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
274
294
|
await recordTaskPoolRun({ repoRoot: options.repoRoot, run });
|
|
275
295
|
recorded = true;
|
|
276
296
|
tasks.push({
|
|
@@ -281,7 +301,9 @@ export async function runReadyTasks(options) {
|
|
|
281
301
|
});
|
|
282
302
|
}
|
|
283
303
|
catch (error) {
|
|
284
|
-
|
|
304
|
+
if (result.status !== "succeeded") {
|
|
305
|
+
await options.onTaskFinalized?.({ status: result.status, taskSpec, workerRunId: result.workerRunId, runRecordPath: result.runRecordPath });
|
|
306
|
+
}
|
|
285
307
|
tasks.push({
|
|
286
308
|
taskId,
|
|
287
309
|
workerRunId: result.workerRunId,
|
|
@@ -291,8 +313,9 @@ export async function runReadyTasks(options) {
|
|
|
291
313
|
});
|
|
292
314
|
break;
|
|
293
315
|
}
|
|
294
|
-
if (recorded)
|
|
316
|
+
if (recorded && result.status !== "succeeded") {
|
|
295
317
|
await options.onTaskFinalized?.({ status: result.status, taskSpec, workerRunId: result.workerRunId, runRecordPath: result.runRecordPath });
|
|
318
|
+
}
|
|
296
319
|
}
|
|
297
320
|
const summary = summarize(tasks);
|
|
298
321
|
const batchRunPath = path.join(getTaskPoolRoot(options.repoRoot), "artifacts", batchRunId, "batch-run.json");
|
|
@@ -155,12 +155,15 @@ export function planReadyTasks(input) {
|
|
|
155
155
|
* Evaluate artifact-gate eligibility for a node whose dependencies are all
|
|
156
156
|
* `Done`. Returns the first blocking projection, or `undefined` when eligible.
|
|
157
157
|
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
158
|
+
* Envelope-level binding only: Feature / producer task / kind / optional
|
|
159
|
+
* schemaId / optional sourceBinding must match the injected Outcome. The
|
|
160
|
+
* planner is pure and synchronous — it does **not** re-read artifact files or
|
|
161
|
+
* recompute sha256. Callers must inject envelopes via `readVerifiedOutcome`
|
|
162
|
+
* (path/hash of the outcome JSON). File-level artifact sha256 is enforced at
|
|
163
|
+
* `projectOutcome` and again at Feature Verification Bundle / verify-final /
|
|
164
|
+
* Delivery / Closeout (`verifyArtifactRefs`).
|
|
165
|
+
*
|
|
166
|
+
* When `outcomes` is omitted, this gate is skipped (legacy callers unchanged).
|
|
164
167
|
*/
|
|
165
168
|
function evaluateArtifactGate(consumes, featureId, states, outcomes, producerKinds) {
|
|
166
169
|
if (!outcomes || !consumes || consumes.length === 0)
|
|
@@ -230,8 +233,11 @@ function evaluateArtifactRef(ref, featureId, states, outcomes) {
|
|
|
230
233
|
if (!artifact) {
|
|
231
234
|
return projection("artifact-kind-absent", `no artifact with kind ${ref.kind}`);
|
|
232
235
|
}
|
|
233
|
-
if (ref.schemaId !== undefined
|
|
234
|
-
|
|
236
|
+
if (ref.schemaId !== undefined) {
|
|
237
|
+
const artifactSchemaId = artifact.schemaId ?? artifact.kind;
|
|
238
|
+
if (artifactSchemaId !== ref.schemaId) {
|
|
239
|
+
return projection("schema-mismatch", `artifact schemaId ${String(artifactSchemaId)} != ${ref.schemaId}`);
|
|
240
|
+
}
|
|
235
241
|
}
|
|
236
242
|
if (ref.sourceBinding) {
|
|
237
243
|
const envBinding = envelope.sourceBinding;
|
|
@@ -9,9 +9,11 @@ export const artifactDeclSchema = z
|
|
|
9
9
|
})
|
|
10
10
|
.strict();
|
|
11
11
|
/**
|
|
12
|
-
* Consumer-side artifact reference.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* Consumer-side artifact reference. Ready Planner validates envelope-level
|
|
13
|
+
* Feature / producer / kind / optional schemaId / optional sourceBinding.
|
|
14
|
+
* File-level path+sha256 of artifact bytes is enforced at Outcome projection
|
|
15
|
+
* and Feature Verification Bundle / Delivery / Closeout — not inside the
|
|
16
|
+
* synchronous Ready Planner.
|
|
15
17
|
*/
|
|
16
18
|
export const artifactRefSchema = z
|
|
17
19
|
.object({
|