@tea-agent/loop-agent 0.26.5-beta.2 → 0.27.1-beta.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 +2 -2
- package/CHANGELOG.md +33 -0
- package/README.md +13 -23
- package/dist/application/dag/args.js +7 -7
- package/dist/application/dag/generate-task-dag.js +2 -2
- package/dist/application/dag/run-dag.js +3 -4
- package/dist/application/dag/validate-dag.js +1 -1
- package/dist/application/task-lifecycle/advance.js +845 -0
- package/dist/application/task-lifecycle/gates.js +80 -0
- package/dist/application/task-lifecycle/index.js +7 -0
- package/dist/application/task-lifecycle/observe.js +395 -0
- package/dist/application/task-lifecycle/plan-transitions.js +224 -0
- package/dist/application/task-lifecycle/recommendations.js +180 -0
- package/dist/application/task-lifecycle/record.js +73 -0
- package/dist/application/task-lifecycle/types.js +1 -0
- package/dist/cli/command-definitions.js +14 -111
- package/dist/cli/help.js +6 -7
- package/dist/cli/program.js +26 -90
- package/dist/cli/update/policy.js +0 -1
- package/dist/commands/dag-final-verification.js +1 -1
- package/dist/commands/dag-init-hybrid.js +1 -1
- package/dist/commands/delegate.js +2 -2
- package/dist/commands/init.js +21 -19
- package/dist/commands/run-dag-progress.js +1 -1
- package/dist/commands/status.js +18 -17
- package/dist/commands/study-init.js +2 -2
- package/dist/commands/task-advance.js +335 -0
- package/dist/commands/task-contract.js +3 -5
- package/dist/commands/task-source-prepare.js +9 -4
- package/dist/commands/task-status.js +133 -0
- package/dist/executors/dag-pi-executor.js +18 -22
- package/dist/executors/shell-executor.js +41 -25
- package/dist/governance/manifest-types.js +2 -2
- package/dist/shared/operator/capabilities.js +1358 -243
- package/dist/task/contract/adopt.js +1 -1
- package/dist/task/contract/apply.js +1 -1
- package/dist/task/contract/import-revision.js +1 -1
- package/dist/task/contract/recover.js +2 -2
- package/dist/task/read-model.js +18 -23
- package/dist/task/runtime.js +1 -1
- package/dist/task/source-prepare/completeness.js +1 -1
- package/dist/task/source-prepare/parse-intent.js +6 -1
- package/dist/task/source-prepare/prepare.js +23 -20
- package/dist/worker/console/operator-actions.js +288 -95
- package/dist/worker/console/recovery-cta.js +4 -4
- package/dist/worker/console/static/assets/{index-CSRIhuzh.js → index-CNO7n6qB.js} +1 -1
- package/dist/worker/console/static/index.html +1 -1
- package/dist/worker/materialize/harness-task-materializer.js +6 -5
- package/dist/worker/run-task/run-task.js +204 -112
- package/dist/worker/runner/run-ready.js +1 -1
- package/dist/workflows/dag/backend-test-markdown-workflow.js +49 -23
- package/dist/workflows/dag/backend-test-pytest-collection.js +57 -9
- package/dist/workflows/dag/frontend-implementation-contract.js +3 -3
- package/dist/workflows/dag/frontend-prewrite-gate.js +18 -1
- package/dist/workflows/dag/frontend-test-l5-report.js +46 -7
- package/dist/workflows/dag/init-hybrid.js +6 -35
- package/dist/workflows/dag/output-protocol.js +23 -0
- package/docs/templates/backend-test-dag.json +2 -2
- package/docs/templates/evaluation/agents-map-slim-v1.md +1 -1
- package/docs/templates/evaluation/agents-map-verbose-v0.md +3 -3
- package/docs/templates/frontend-implementation-contract.schema.json +2 -2
- package/docs/templates/harness.schema.json +2 -2
- package/docs/templates/init-managed-agents.md +13 -16
- package/docs/templates/production-readiness-checklist.md +3 -3
- package/harness.json +1 -1
- package/package.json +1 -1
- package/scripts/kb-bootstrap-init-skeleton.sh +2 -1
- package/scripts/kb-graph-incremental-prepare.mjs +2 -2
- package/skills/loop-agent/SKILL.md +18 -13
- package/skills/loop-agent/references/README.md +1 -1
- package/skills/loop-agent/references/command-reference.md +55 -84
- package/skills/loop-agent/references/harness-policy.md +19 -23
- package/skills/loop-agent/references/hybrid-dag.md +32 -34
- package/skills/loop-agent/references/long-running-loop.md +2 -2
- package/skills/loop-agent/references/one-shot-runs.md +4 -5
- package/skills/loop-agent/references/orchestrator-and-interventions.md +3 -3
- package/skills/loop-agent/references/post-implementation-and-patterns.md +4 -4
- package/skills/loop-agent/references/source-and-plan-practice.md +45 -51
- package/skills/loop-agent/references/task-workflow.md +14 -15
|
@@ -3,7 +3,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { writeTextAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
5
5
|
import { getTaskPaths } from "../../task/runtime.js";
|
|
6
|
-
import { formatDuration, noopProgressReporter } from "../progress-reporter.js";
|
|
6
|
+
import { formatDuration, noopProgressReporter, } from "../progress-reporter.js";
|
|
7
7
|
import { redactForPreview, truncatePreview } from "../observability/events.js";
|
|
8
8
|
import { controllerIdentityExpectationFailure, resolveControllerIdentity, } from "../loop-agent/loop-agent-client.js";
|
|
9
9
|
import { materializeTaskSpec, } from "../materialize/harness-task-materializer.js";
|
|
@@ -33,12 +33,19 @@ export async function runTaskSpec(options) {
|
|
|
33
33
|
const preflightStep = "preflight";
|
|
34
34
|
const preflightStartedAt = Date.now();
|
|
35
35
|
await runObservedStep(eventCtx, preflightStep, async () => {
|
|
36
|
-
progress.step("preflight: version + inspect + docs-audit + git-status" +
|
|
36
|
+
progress.step("preflight: version + inspect + docs-audit + git-status" +
|
|
37
|
+
(options.preflight &&
|
|
38
|
+
typeof options.preflight === "object" &&
|
|
39
|
+
options.preflight.runCheckRepo
|
|
40
|
+
? " + check-repo"
|
|
41
|
+
: ""));
|
|
37
42
|
const preflight = await preflightTargetRepo({
|
|
38
43
|
repoRoot: options.repoRoot,
|
|
39
44
|
client,
|
|
40
45
|
...(typeof options.preflight === "object" ? options.preflight : {}),
|
|
41
|
-
...(options.controllerExpectation
|
|
46
|
+
...(options.controllerExpectation
|
|
47
|
+
? { expectation: options.controllerExpectation }
|
|
48
|
+
: {}),
|
|
42
49
|
});
|
|
43
50
|
if (!preflight.ok) {
|
|
44
51
|
progress.note(`preflight FAILED: ${preflight.code}`);
|
|
@@ -65,81 +72,95 @@ export async function runTaskSpec(options) {
|
|
|
65
72
|
await mkdir(taskArtifactsDir, { recursive: true });
|
|
66
73
|
const dagPath = path.join(taskArtifactsDir, `${workerRunId}-dag.json`);
|
|
67
74
|
const runRecordPath = path.join(taskArtifactsDir, "worker-run-record.json");
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
const canonicalDraftPath = getTaskPaths(options.repoRoot, materializeManifest.harnessTaskId).dagDraftPath;
|
|
76
|
+
// Standard lifecycle: task advance to writeSet gate (generate + strict validate).
|
|
77
|
+
const prepareStep = "task-advance-prepare";
|
|
78
|
+
const prepareStartedAt = Date.now();
|
|
79
|
+
let gateToken = await runObservedStep(eventCtx, prepareStep, async () => {
|
|
80
|
+
progress.step("task advance: generate/validate DAG and open writeSet gate");
|
|
81
|
+
const result = await runRequiredCommand(options.repoRoot, client, "task-advance-prepare", [
|
|
82
|
+
"task",
|
|
83
|
+
"advance",
|
|
75
84
|
materializeManifest.harnessTaskId,
|
|
76
85
|
"--profile",
|
|
77
86
|
materializeManifest.loopAgentProfile,
|
|
78
|
-
"--
|
|
79
|
-
"--output",
|
|
87
|
+
"--dag-output",
|
|
80
88
|
dagPath,
|
|
81
|
-
"--
|
|
82
|
-
options.repoRoot,
|
|
89
|
+
"--json",
|
|
83
90
|
], true, undefined, eventCtx);
|
|
91
|
+
return extractWriteSetGateToken(result, materializeManifest.harnessTaskId);
|
|
84
92
|
});
|
|
85
|
-
progress.step(`
|
|
93
|
+
progress.step(`task advance prepare finished in ${formatDuration(Date.now() - prepareStartedAt)}`);
|
|
86
94
|
if (options.piModel) {
|
|
87
95
|
const piModelStep = "pi-model-override";
|
|
88
96
|
await runObservedStep(eventCtx, piModelStep, async () => {
|
|
89
97
|
progress.step(`pi-model override: rewriting executorModels.pi → ${options.piModel}`);
|
|
98
|
+
// Rewrite both per-run copy and canonical draft so re-validate sees override.
|
|
99
|
+
await applyPiModelOverride(canonicalDraftPath, options.piModel);
|
|
90
100
|
await applyPiModelOverride(dagPath, options.piModel);
|
|
91
101
|
});
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
});
|
|
108
|
-
progress.step(`dag validated in ${formatDuration(Date.now() - validateStartedAt)}`);
|
|
109
|
-
const canonicalDraftPath = getTaskPaths(options.repoRoot, materializeManifest.harnessTaskId).dagDraftPath;
|
|
110
|
-
await runObservedStep(eventCtx, "publish-canonical-dag-draft", async () => {
|
|
111
|
-
const dagBytes = await readFile(dagPath, "utf-8");
|
|
112
|
-
await writeTextAtomic(canonicalDraftPath, dagBytes, {
|
|
113
|
-
repoRoot: options.repoRoot,
|
|
102
|
+
// Re-open gate after draft rewrite (digest bound to DAG hash).
|
|
103
|
+
gateToken = await runObservedStep(eventCtx, "task-advance-revalidate", async () => {
|
|
104
|
+
progress.step("task advance: re-validate after piModel override (no strict-models)");
|
|
105
|
+
const result = await runRequiredCommand(options.repoRoot, client, "task-advance-revalidate", [
|
|
106
|
+
"task",
|
|
107
|
+
"advance",
|
|
108
|
+
materializeManifest.harnessTaskId,
|
|
109
|
+
"--profile",
|
|
110
|
+
materializeManifest.loopAgentProfile,
|
|
111
|
+
"--no-strict-models",
|
|
112
|
+
"--dag-output",
|
|
113
|
+
dagPath,
|
|
114
|
+
"--json",
|
|
115
|
+
], true, undefined, eventCtx);
|
|
116
|
+
return extractWriteSetGateToken(result, materializeManifest.harnessTaskId);
|
|
114
117
|
});
|
|
115
|
-
}
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
+
}
|
|
119
|
+
const runAdvanceStep = "task-advance-execute";
|
|
120
|
+
const runAdvanceStartedAt = Date.now();
|
|
118
121
|
const dagEventsPath = path.join(getTaskPoolRoot(options.repoRoot), "observability", "runs", workerRunId, "dag-events.jsonl");
|
|
119
122
|
await mkdir(path.dirname(dagEventsPath), { recursive: true }).catch(() => { });
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
123
|
+
const workerAssociation = JSON.stringify({
|
|
124
|
+
schemaVersion: 1,
|
|
125
|
+
workerRunId,
|
|
126
|
+
featureId: options.taskSpec.feature_id,
|
|
127
|
+
taskId: options.taskSpec.id,
|
|
128
|
+
});
|
|
129
|
+
await runObservedStep(eventCtx, runAdvanceStep, async () => {
|
|
130
|
+
progress.step("task advance: approve writeSet gate and execute DAG (long step)");
|
|
131
|
+
const advanceResult = await runTaskAdvanceExecuteWithEventsFallback({
|
|
132
|
+
repoRoot: options.repoRoot,
|
|
133
|
+
client,
|
|
134
|
+
taskId: materializeManifest.harnessTaskId,
|
|
135
|
+
gateToken,
|
|
129
136
|
workerRunId,
|
|
130
|
-
|
|
131
|
-
JSON.stringify({
|
|
132
|
-
schemaVersion: 1,
|
|
133
|
-
workerRunId,
|
|
134
|
-
featureId: options.taskSpec.feature_id,
|
|
135
|
-
taskId: options.taskSpec.id,
|
|
136
|
-
}),
|
|
137
|
-
"--events-jsonl",
|
|
137
|
+
workerAssociation,
|
|
138
138
|
dagEventsPath,
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
maxConcurrent: options.taskSpec.loop_agent.max_concurrent,
|
|
140
|
+
timeoutMs: resolveRunDagTimeoutMs(options.taskSpec),
|
|
141
|
+
eventCtx,
|
|
142
|
+
});
|
|
143
|
+
// Terminal DAG failure is still a completed execute step for Worker: report
|
|
144
|
+
// decision + failure artifacts remain authoritative downstream.
|
|
145
|
+
if (!advanceResult.ok && !isTerminalTaskAdvanceFailure(advanceResult)) {
|
|
146
|
+
assertAdvanceExecuteOk(advanceResult, [
|
|
147
|
+
"task",
|
|
148
|
+
"advance",
|
|
149
|
+
materializeManifest.harnessTaskId,
|
|
150
|
+
"--approve-gate",
|
|
151
|
+
gateToken,
|
|
152
|
+
]);
|
|
153
|
+
}
|
|
154
|
+
return advanceResult;
|
|
155
|
+
});
|
|
156
|
+
progress.step(`task advance execute finished in ${formatDuration(Date.now() - runAdvanceStartedAt)}`);
|
|
157
|
+
// Keep per-run DAG artifact aligned with canonical draft after execution prep.
|
|
158
|
+
await runObservedStep(eventCtx, "publish-per-run-dag-copy", async () => {
|
|
159
|
+
const dagBytes = await readFile(canonicalDraftPath, "utf-8");
|
|
160
|
+
await writeTextAtomic(dagPath, dagBytes, {
|
|
161
|
+
repoRoot: options.repoRoot,
|
|
162
|
+
});
|
|
141
163
|
});
|
|
142
|
-
progress.step(`run-dag finished in ${formatDuration(Date.now() - runDagStartedAt)}`);
|
|
143
164
|
const reportStep = "report-decision";
|
|
144
165
|
let capturedReportJson;
|
|
145
166
|
const reportDecision = await runObservedStep(eventCtx, reportStep, async () => {
|
|
@@ -171,7 +192,9 @@ export async function runTaskSpec(options) {
|
|
|
171
192
|
artifactName: "dag-report-markdown",
|
|
172
193
|
});
|
|
173
194
|
return decision;
|
|
174
|
-
}, {
|
|
195
|
+
}, {
|
|
196
|
+
statusForResult: (decision) => decision.succeeded ? "succeeded" : "failed",
|
|
197
|
+
});
|
|
175
198
|
const dagReportRun = extractDagReportRun(workerRunId, capturedReportJson);
|
|
176
199
|
const declaredArtifacts = options.declaredArtifacts
|
|
177
200
|
? options.declaredArtifacts.map((entry) => ({
|
|
@@ -180,7 +203,9 @@ export async function runTaskSpec(options) {
|
|
|
180
203
|
...(entry.path ? { path: entry.path } : {}),
|
|
181
204
|
}))
|
|
182
205
|
: undefined;
|
|
183
|
-
let status = reportDecision.succeeded
|
|
206
|
+
let status = reportDecision.succeeded
|
|
207
|
+
? "succeeded"
|
|
208
|
+
: "failed";
|
|
184
209
|
progress.step(`report decision: ${status} (${reportDecision.reason}${reportDecision.runStatus ? `, status=${reportDecision.runStatus}` : ""})`);
|
|
185
210
|
const failureArtifacts = reportDecision.succeeded
|
|
186
211
|
? undefined
|
|
@@ -237,7 +262,10 @@ export async function runTaskSpec(options) {
|
|
|
237
262
|
});
|
|
238
263
|
if (!projection.ok) {
|
|
239
264
|
status = "failed";
|
|
240
|
-
outcomeFailure = {
|
|
265
|
+
outcomeFailure = {
|
|
266
|
+
category: projection.category,
|
|
267
|
+
reason: projection.reason,
|
|
268
|
+
};
|
|
241
269
|
progress.step(`outcome projection failed: ${projection.category} (${projection.reason})`);
|
|
242
270
|
}
|
|
243
271
|
else {
|
|
@@ -271,13 +299,10 @@ export async function runTaskSpec(options) {
|
|
|
271
299
|
outcome = await writeOutcome(options.repoRoot, envelope);
|
|
272
300
|
}
|
|
273
301
|
if (status === "succeeded" && !options.skipSuccessFinalization) {
|
|
274
|
-
await runObservedStep(eventCtx, "
|
|
275
|
-
progress.step("promote
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
await runObservedStep(eventCtx, "closeout-task", async () => {
|
|
279
|
-
progress.step("closeout task");
|
|
280
|
-
await runRequiredCommand(options.repoRoot, client, "closeout-task", ["closeout", "task", materializeManifest.harnessTaskId], true, undefined, eventCtx);
|
|
302
|
+
await runObservedStep(eventCtx, "task-advance-closeout", async () => {
|
|
303
|
+
progress.step("task advance: promote + closeout");
|
|
304
|
+
// Lifecycle closeout path; advance is idempotent when run already associated.
|
|
305
|
+
await runRequiredCommand(options.repoRoot, client, "task-advance-closeout", ["task", "advance", materializeManifest.harnessTaskId, "--json"], true, undefined, eventCtx);
|
|
281
306
|
});
|
|
282
307
|
}
|
|
283
308
|
const record = {
|
|
@@ -351,47 +376,111 @@ async function runRequiredCommand(repoRoot, client, artifactName, args, expectJs
|
|
|
351
376
|
}
|
|
352
377
|
return result;
|
|
353
378
|
}
|
|
379
|
+
function extractWriteSetGateToken(result, taskId) {
|
|
380
|
+
const root = result.json;
|
|
381
|
+
if (!root || typeof root !== "object") {
|
|
382
|
+
throw new Error(`task advance prepare did not return JSON for ${taskId}; cannot approve writeSet gate`);
|
|
383
|
+
}
|
|
384
|
+
const envelope = root;
|
|
385
|
+
const payload = envelope.result && typeof envelope.result === "object"
|
|
386
|
+
? envelope.result
|
|
387
|
+
: envelope;
|
|
388
|
+
const gate = payload.gate && typeof payload.gate === "object"
|
|
389
|
+
? payload.gate
|
|
390
|
+
: undefined;
|
|
391
|
+
const id = typeof gate?.id === "string" ? gate.id : undefined;
|
|
392
|
+
const digest = typeof gate?.digest === "string" ? gate.digest : undefined;
|
|
393
|
+
if (!id || !digest) {
|
|
394
|
+
const lifecycleState = typeof payload.lifecycleState === "string"
|
|
395
|
+
? payload.lifecycleState
|
|
396
|
+
: "unknown";
|
|
397
|
+
throw new Error(`task advance prepare for ${taskId} did not open a writeSet gate (lifecycleState=${lifecycleState})`);
|
|
398
|
+
}
|
|
399
|
+
return `${id}:${digest}`;
|
|
400
|
+
}
|
|
354
401
|
/**
|
|
355
|
-
* `--events-jsonl` is additive observability. Older published controllers
|
|
356
|
-
*
|
|
357
|
-
* retrying the exact command without it is safe and preserves the same run id.
|
|
402
|
+
* `--events-jsonl` is additive observability. Older published controllers may
|
|
403
|
+
* reject the flag; retry without it, keeping the same run id / gate digest.
|
|
358
404
|
*/
|
|
359
|
-
function
|
|
360
|
-
if (!result.ok) {
|
|
405
|
+
function assertAdvanceExecuteOk(result, args) {
|
|
406
|
+
if (!result.ok && !isTerminalTaskAdvanceFailure(result)) {
|
|
361
407
|
throw new Error(`loop-agent command failed: ${args.join(" ")}`);
|
|
362
408
|
}
|
|
363
409
|
}
|
|
364
|
-
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
410
|
+
function readTaskAdvancePayload(result) {
|
|
411
|
+
const root = result.json;
|
|
412
|
+
if (!root || typeof root !== "object")
|
|
413
|
+
return null;
|
|
414
|
+
const envelope = root;
|
|
415
|
+
if (envelope.result && typeof envelope.result === "object") {
|
|
416
|
+
return envelope.result;
|
|
417
|
+
}
|
|
418
|
+
return envelope;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Terminal DAG failure from `task advance --approve-gate` is an expected
|
|
422
|
+
* worker path: CLI may return non-zero / outcome=blocked while still writing
|
|
423
|
+
* a completed run that `dag report` can diagnose.
|
|
424
|
+
*/
|
|
425
|
+
function isTerminalTaskAdvanceFailure(result) {
|
|
426
|
+
const payload = readTaskAdvancePayload(result);
|
|
427
|
+
if (!payload)
|
|
428
|
+
return false;
|
|
429
|
+
const lifecycleState = typeof payload.lifecycleState === "string" ? payload.lifecycleState : "";
|
|
430
|
+
if (lifecycleState === "run-failed" ||
|
|
431
|
+
lifecycleState === "needs-attention" ||
|
|
432
|
+
lifecycleState === "awaiting-decision") {
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
const run = payload.run && typeof payload.run === "object"
|
|
436
|
+
? payload.run
|
|
437
|
+
: null;
|
|
438
|
+
const runStatus = typeof run?.status === "string" ? run.status : "";
|
|
439
|
+
const runLifecycle = typeof run?.lifecycle === "string" ? run.lifecycle : "";
|
|
440
|
+
return (runStatus === "failed" ||
|
|
441
|
+
runStatus === "partial_failed" ||
|
|
442
|
+
(runLifecycle === "completed" && runStatus !== "finished"));
|
|
443
|
+
}
|
|
444
|
+
async function runTaskAdvanceExecuteWithEventsFallback(input) {
|
|
445
|
+
const baseArgs = [
|
|
446
|
+
"task",
|
|
447
|
+
"advance",
|
|
448
|
+
input.taskId,
|
|
449
|
+
"--approve-gate",
|
|
450
|
+
input.gateToken,
|
|
451
|
+
"--run-id",
|
|
452
|
+
input.workerRunId,
|
|
453
|
+
"--worker-association",
|
|
454
|
+
input.workerAssociation,
|
|
455
|
+
"--max-concurrent",
|
|
456
|
+
String(input.maxConcurrent),
|
|
457
|
+
"--skip-finalize",
|
|
458
|
+
"--json",
|
|
459
|
+
];
|
|
460
|
+
const withEvents = [...baseArgs, "--events-jsonl", input.dagEventsPath];
|
|
461
|
+
const result = await input.client.run(withEvents, {
|
|
462
|
+
cwd: input.repoRoot,
|
|
463
|
+
artifactName: "task-advance-execute",
|
|
368
464
|
expectJson: true,
|
|
369
|
-
timeoutMs,
|
|
465
|
+
timeoutMs: input.timeoutMs,
|
|
370
466
|
});
|
|
371
|
-
if (result.ok)
|
|
467
|
+
if (result.ok || isTerminalTaskAdvanceFailure(result))
|
|
372
468
|
return result;
|
|
373
|
-
if (!rejectsEventsJsonl(result)
|
|
374
|
-
|
|
469
|
+
if (!rejectsEventsJsonl(result)) {
|
|
470
|
+
assertAdvanceExecuteOk(result, withEvents);
|
|
375
471
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
artifactName: "run-dag-events-fallback",
|
|
472
|
+
input.eventCtx.progress.note("task advance controller does not support --events-jsonl; retrying without DAG event stream");
|
|
473
|
+
const fallback = await input.client.run(baseArgs, {
|
|
474
|
+
cwd: input.repoRoot,
|
|
475
|
+
artifactName: "task-advance-execute-events-fallback",
|
|
381
476
|
expectJson: true,
|
|
382
|
-
timeoutMs,
|
|
477
|
+
timeoutMs: input.timeoutMs,
|
|
383
478
|
});
|
|
384
|
-
|
|
479
|
+
assertAdvanceExecuteOk(fallback, baseArgs);
|
|
385
480
|
return fallback;
|
|
386
481
|
}
|
|
387
482
|
function rejectsEventsJsonl(result) {
|
|
388
|
-
return /unknown run-dag flag:\s*--events-jsonl/i.test(`${result.stderr}\n${result.stdout}`);
|
|
389
|
-
}
|
|
390
|
-
function withoutFlagAndValue(args, flag) {
|
|
391
|
-
const index = args.indexOf(flag);
|
|
392
|
-
if (index < 0)
|
|
393
|
-
return args;
|
|
394
|
-
return [...args.slice(0, index), ...args.slice(index + 2)];
|
|
483
|
+
return /unknown (?:run-dag|dag execute|task advance) flag:\s*--events-jsonl|unknown flag --events-jsonl/i.test(`${result.stderr}\n${result.stdout}`);
|
|
395
484
|
}
|
|
396
485
|
function emit(progress, input) {
|
|
397
486
|
const structured = progress;
|
|
@@ -479,7 +568,10 @@ async function collectFailureArtifacts(input) {
|
|
|
479
568
|
}
|
|
480
569
|
function isPathInside(root, target) {
|
|
481
570
|
const relative = path.relative(root, target);
|
|
482
|
-
return relative !== "" &&
|
|
571
|
+
return (relative !== "" &&
|
|
572
|
+
relative !== ".." &&
|
|
573
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
574
|
+
!path.isAbsolute(relative));
|
|
483
575
|
}
|
|
484
576
|
/** Exported for unit tests: map DAG report JSON into Worker report decision. */
|
|
485
577
|
export function decideFromReport(workerRunId, result) {
|
|
@@ -553,7 +645,9 @@ function extractDagReportRun(workerRunId, result) {
|
|
|
553
645
|
...(readString(node, "nodeId") || readString(node, "id")
|
|
554
646
|
? { id: readString(node, "nodeId") ?? readString(node, "id") }
|
|
555
647
|
: {}),
|
|
556
|
-
...(readString(node, "status")
|
|
648
|
+
...(readString(node, "status")
|
|
649
|
+
? { status: readString(node, "status") }
|
|
650
|
+
: {}),
|
|
557
651
|
...(readString(node, "failureCategory")
|
|
558
652
|
? { failureCategory: readString(node, "failureCategory") }
|
|
559
653
|
: {}),
|
|
@@ -567,10 +661,14 @@ function extractDagReportRun(workerRunId, result) {
|
|
|
567
661
|
? { structuredArtifactPath: readString(node, "structuredArtifactPath") }
|
|
568
662
|
: {}),
|
|
569
663
|
...(readString(node, "structuredArtifactSha256")
|
|
570
|
-
? {
|
|
664
|
+
? {
|
|
665
|
+
structuredArtifactSha256: readString(node, "structuredArtifactSha256"),
|
|
666
|
+
}
|
|
571
667
|
: {}),
|
|
572
668
|
...(readString(node, "structuredArtifactSchemaId")
|
|
573
|
-
? {
|
|
669
|
+
? {
|
|
670
|
+
structuredArtifactSchemaId: readString(node, "structuredArtifactSchemaId"),
|
|
671
|
+
}
|
|
574
672
|
: {}),
|
|
575
673
|
}));
|
|
576
674
|
return {
|
|
@@ -733,19 +831,13 @@ class ObservedRunTaskClient {
|
|
|
733
831
|
function commandRecordName(args, artifactName) {
|
|
734
832
|
if (args[0] === "new-task")
|
|
735
833
|
return "new-task";
|
|
834
|
+
if (args[0] === "task" && args[1] === "advance") {
|
|
835
|
+
return artifactName.startsWith("task-advance-")
|
|
836
|
+
? artifactName
|
|
837
|
+
: "task-advance";
|
|
838
|
+
}
|
|
736
839
|
return artifactName;
|
|
737
840
|
}
|
|
738
|
-
function maxConcurrentArgs(taskSpec) {
|
|
739
|
-
return ["--max-concurrent", String(taskSpec.loop_agent.max_concurrent)];
|
|
740
|
-
}
|
|
741
|
-
/**
|
|
742
|
-
* When a smoke `piModel` override is active, drop `--strict-models` so the
|
|
743
|
-
* non-canonical `executorModels.pi` rewrite passes `dag validate`. Governance,
|
|
744
|
-
* forbidden-executor and spine audits are still enforced by their own flags.
|
|
745
|
-
*/
|
|
746
|
-
function strictModelsArgs(piModel) {
|
|
747
|
-
return piModel ? [] : ["--strict-models"];
|
|
748
|
-
}
|
|
749
841
|
/**
|
|
750
842
|
* Rewrite `executorModels.pi.{LOW,MED,HIGH}` in an already-generated DAG JSON
|
|
751
843
|
* so every pi executor node resolves to the smoke override model.
|
|
@@ -125,7 +125,7 @@ export async function runReadyTasks(options) {
|
|
|
125
125
|
workerRunId,
|
|
126
126
|
status: "running",
|
|
127
127
|
});
|
|
128
|
-
progress.step(`materialize →
|
|
128
|
+
progress.step(`materialize → task advance → report`);
|
|
129
129
|
let result;
|
|
130
130
|
try {
|
|
131
131
|
await writeTaskPoolState(options.repoRoot, {
|
|
@@ -43,6 +43,17 @@ async function exists(filePath) {
|
|
|
43
43
|
return false;
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
+
async function mappedScriptExists(filePath) {
|
|
47
|
+
try {
|
|
48
|
+
await access(filePath);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error.code === "ENOENT")
|
|
53
|
+
return false;
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
46
57
|
async function existingPaths(root, candidates) {
|
|
47
58
|
const found = [];
|
|
48
59
|
for (const candidate of candidates) {
|
|
@@ -685,9 +696,24 @@ export async function validateBackendMarkdownTraceability(workspaceRoot) {
|
|
|
685
696
|
...(findings.length > 0 ? findings.map((finding) => `- ${finding}`) : ["- None"]),
|
|
686
697
|
].join("\n") + "\n");
|
|
687
698
|
}
|
|
688
|
-
export async function
|
|
699
|
+
export async function resolveBackendTestMappedPytestScripts(workspaceRoot) {
|
|
689
700
|
const files = await markdownFiles(workspaceRoot);
|
|
690
|
-
const
|
|
701
|
+
const expectedScripts = new Set();
|
|
702
|
+
const existingScripts = new Set();
|
|
703
|
+
const missingScripts = new Set();
|
|
704
|
+
const scriptOwners = new Map();
|
|
705
|
+
const recordScript = (script, owner, present) => {
|
|
706
|
+
const previousOwner = scriptOwners.get(script);
|
|
707
|
+
if (previousOwner && previousOwner !== owner) {
|
|
708
|
+
throw new Error(`conflicting Markdown pytest mapping: ${previousOwner} and ${owner} both map ${script}`);
|
|
709
|
+
}
|
|
710
|
+
scriptOwners.set(script, owner);
|
|
711
|
+
expectedScripts.add(script);
|
|
712
|
+
if (present)
|
|
713
|
+
existingScripts.add(script);
|
|
714
|
+
else
|
|
715
|
+
missingScripts.add(script);
|
|
716
|
+
};
|
|
691
717
|
for (const file of files) {
|
|
692
718
|
if (path.basename(file).toLowerCase() === "readme.md")
|
|
693
719
|
continue;
|
|
@@ -703,10 +729,11 @@ export async function collectBackendTestMappedPytestScripts(workspaceRoot) {
|
|
|
703
729
|
// Prefer explicit mappings, but recover to the deterministic module stem path
|
|
704
730
|
// when the model wrote the correct one-to-one file while Markdown still names
|
|
705
731
|
// a drifted script (common: health.md maps test_health.py but writer emitted
|
|
706
|
-
// test_be_health.py for BE-HEALTH.md, or the reverse).
|
|
732
|
+
// test_be_health.py for BE-HEALTH.md, or the reverse). If neither exists, the
|
|
733
|
+
// deterministic module path is the only path a bounded repair may create.
|
|
707
734
|
if (mappedFromModule.size === 0) {
|
|
708
|
-
if (await
|
|
709
|
-
|
|
735
|
+
if (await mappedScriptExists(path.resolve(workspaceRoot, expectedScript))) {
|
|
736
|
+
recordScript(expectedScript, relativeFile, true);
|
|
710
737
|
}
|
|
711
738
|
continue;
|
|
712
739
|
}
|
|
@@ -714,32 +741,31 @@ export async function collectBackendTestMappedPytestScripts(workspaceRoot) {
|
|
|
714
741
|
if (!isSafeBackendPytestScript(script, workspaceRoot)) {
|
|
715
742
|
throw new Error(`unsafe mapped pytest script: ${script}`);
|
|
716
743
|
}
|
|
717
|
-
if (await
|
|
718
|
-
|
|
744
|
+
if (await mappedScriptExists(path.resolve(workspaceRoot, script))) {
|
|
745
|
+
recordScript(script, relativeFile, true);
|
|
719
746
|
continue;
|
|
720
747
|
}
|
|
721
|
-
if (isSafeBackendPytestScript(expectedScript, workspaceRoot)
|
|
722
|
-
|
|
723
|
-
scripts.add(expectedScript);
|
|
724
|
-
continue;
|
|
748
|
+
if (!isSafeBackendPytestScript(expectedScript, workspaceRoot)) {
|
|
749
|
+
throw new Error(`unsafe mapped pytest script: ${expectedScript}`);
|
|
725
750
|
}
|
|
726
|
-
|
|
751
|
+
recordScript(expectedScript, relativeFile, await mappedScriptExists(path.resolve(workspaceRoot, expectedScript)));
|
|
727
752
|
}
|
|
728
753
|
}
|
|
729
|
-
if (
|
|
754
|
+
if (expectedScripts.size === 0) {
|
|
730
755
|
throw new Error("no pytest scripts are mapped by final Markdown cases");
|
|
731
756
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
757
|
+
return {
|
|
758
|
+
expectedScripts: unique(expectedScripts),
|
|
759
|
+
existingScripts: unique(existingScripts),
|
|
760
|
+
missingScripts: unique(missingScripts),
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
export async function collectBackendTestMappedPytestScripts(workspaceRoot) {
|
|
764
|
+
const resolution = await resolveBackendTestMappedPytestScripts(workspaceRoot);
|
|
765
|
+
if (resolution.missingScripts.length > 0) {
|
|
766
|
+
throw new Error(`mapped pytest script is missing: ${resolution.missingScripts[0]}`);
|
|
741
767
|
}
|
|
742
|
-
return
|
|
768
|
+
return resolution.existingScripts;
|
|
743
769
|
}
|
|
744
770
|
function cleanCaseTitle(rawHeading, id) {
|
|
745
771
|
return rawHeading
|