@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.
Files changed (79) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +33 -0
  3. package/README.md +13 -23
  4. package/dist/application/dag/args.js +7 -7
  5. package/dist/application/dag/generate-task-dag.js +2 -2
  6. package/dist/application/dag/run-dag.js +3 -4
  7. package/dist/application/dag/validate-dag.js +1 -1
  8. package/dist/application/task-lifecycle/advance.js +845 -0
  9. package/dist/application/task-lifecycle/gates.js +80 -0
  10. package/dist/application/task-lifecycle/index.js +7 -0
  11. package/dist/application/task-lifecycle/observe.js +395 -0
  12. package/dist/application/task-lifecycle/plan-transitions.js +224 -0
  13. package/dist/application/task-lifecycle/recommendations.js +180 -0
  14. package/dist/application/task-lifecycle/record.js +73 -0
  15. package/dist/application/task-lifecycle/types.js +1 -0
  16. package/dist/cli/command-definitions.js +14 -111
  17. package/dist/cli/help.js +6 -7
  18. package/dist/cli/program.js +26 -90
  19. package/dist/cli/update/policy.js +0 -1
  20. package/dist/commands/dag-final-verification.js +1 -1
  21. package/dist/commands/dag-init-hybrid.js +1 -1
  22. package/dist/commands/delegate.js +2 -2
  23. package/dist/commands/init.js +21 -19
  24. package/dist/commands/run-dag-progress.js +1 -1
  25. package/dist/commands/status.js +18 -17
  26. package/dist/commands/study-init.js +2 -2
  27. package/dist/commands/task-advance.js +335 -0
  28. package/dist/commands/task-contract.js +3 -5
  29. package/dist/commands/task-source-prepare.js +9 -4
  30. package/dist/commands/task-status.js +133 -0
  31. package/dist/executors/dag-pi-executor.js +18 -22
  32. package/dist/executors/shell-executor.js +41 -25
  33. package/dist/governance/manifest-types.js +2 -2
  34. package/dist/shared/operator/capabilities.js +1358 -243
  35. package/dist/task/contract/adopt.js +1 -1
  36. package/dist/task/contract/apply.js +1 -1
  37. package/dist/task/contract/import-revision.js +1 -1
  38. package/dist/task/contract/recover.js +2 -2
  39. package/dist/task/read-model.js +18 -23
  40. package/dist/task/runtime.js +1 -1
  41. package/dist/task/source-prepare/completeness.js +1 -1
  42. package/dist/task/source-prepare/parse-intent.js +6 -1
  43. package/dist/task/source-prepare/prepare.js +23 -20
  44. package/dist/worker/console/operator-actions.js +288 -95
  45. package/dist/worker/console/recovery-cta.js +4 -4
  46. package/dist/worker/console/static/assets/{index-CSRIhuzh.js → index-CNO7n6qB.js} +1 -1
  47. package/dist/worker/console/static/index.html +1 -1
  48. package/dist/worker/materialize/harness-task-materializer.js +6 -5
  49. package/dist/worker/run-task/run-task.js +204 -112
  50. package/dist/worker/runner/run-ready.js +1 -1
  51. package/dist/workflows/dag/backend-test-markdown-workflow.js +49 -23
  52. package/dist/workflows/dag/backend-test-pytest-collection.js +57 -9
  53. package/dist/workflows/dag/frontend-implementation-contract.js +3 -3
  54. package/dist/workflows/dag/frontend-prewrite-gate.js +18 -1
  55. package/dist/workflows/dag/frontend-test-l5-report.js +46 -7
  56. package/dist/workflows/dag/init-hybrid.js +6 -35
  57. package/dist/workflows/dag/output-protocol.js +23 -0
  58. package/docs/templates/backend-test-dag.json +2 -2
  59. package/docs/templates/evaluation/agents-map-slim-v1.md +1 -1
  60. package/docs/templates/evaluation/agents-map-verbose-v0.md +3 -3
  61. package/docs/templates/frontend-implementation-contract.schema.json +2 -2
  62. package/docs/templates/harness.schema.json +2 -2
  63. package/docs/templates/init-managed-agents.md +13 -16
  64. package/docs/templates/production-readiness-checklist.md +3 -3
  65. package/harness.json +1 -1
  66. package/package.json +1 -1
  67. package/scripts/kb-bootstrap-init-skeleton.sh +2 -1
  68. package/scripts/kb-graph-incremental-prepare.mjs +2 -2
  69. package/skills/loop-agent/SKILL.md +18 -13
  70. package/skills/loop-agent/references/README.md +1 -1
  71. package/skills/loop-agent/references/command-reference.md +55 -84
  72. package/skills/loop-agent/references/harness-policy.md +19 -23
  73. package/skills/loop-agent/references/hybrid-dag.md +32 -34
  74. package/skills/loop-agent/references/long-running-loop.md +2 -2
  75. package/skills/loop-agent/references/one-shot-runs.md +4 -5
  76. package/skills/loop-agent/references/orchestrator-and-interventions.md +3 -3
  77. package/skills/loop-agent/references/post-implementation-and-patterns.md +4 -4
  78. package/skills/loop-agent/references/source-and-plan-practice.md +45 -51
  79. 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" + (options.preflight && typeof options.preflight === "object" && options.preflight.runCheckRepo ? " + check-repo" : ""));
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 ? { expectation: 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 dagGenStep = "dag-run-task";
69
- const dagGenStartedAt = Date.now();
70
- await runObservedStep(eventCtx, dagGenStep, async () => {
71
- progress.step("dag run-task: generate DAG");
72
- await runRequiredCommand(options.repoRoot, client, "dag-run-task", [
73
- "dag",
74
- "run-task",
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
- "--strict-models",
79
- "--output",
87
+ "--dag-output",
80
88
  dagPath,
81
- "--cwd",
82
- options.repoRoot,
89
+ "--json",
83
90
  ], true, undefined, eventCtx);
91
+ return extractWriteSetGateToken(result, materializeManifest.harnessTaskId);
84
92
  });
85
- progress.step(`dag generated in ${formatDuration(Date.now() - dagGenStartedAt)}`);
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
- const validateStep = "dag-validate";
94
- const validateStartedAt = Date.now();
95
- await runObservedStep(eventCtx, validateStep, async () => {
96
- progress.step("dag validate");
97
- await runRequiredCommand(options.repoRoot, client, "dag-validate", [
98
- "dag",
99
- "validate",
100
- "--dag",
101
- dagPath,
102
- ...strictModelsArgs(options.piModel),
103
- "--strict-governance",
104
- "--spine-task",
105
- materializeManifest.harnessTaskId,
106
- ], true, undefined, eventCtx);
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 runDagStep = "run-dag";
117
- const runDagStartedAt = Date.now();
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
- await runObservedStep(eventCtx, runDagStep, async () => {
121
- progress.step("run-dag: executing DAG nodes (this is the long step)");
122
- await runDagWithEventsFallback(options.repoRoot, client, [
123
- "run-dag",
124
- "--dag",
125
- dagPath,
126
- "--cwd",
127
- options.repoRoot,
128
- "--run-id",
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
- "--worker-association",
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
- ...maxConcurrentArgs(options.taskSpec),
140
- ], resolveRunDagTimeoutMs(options.taskSpec), eventCtx);
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
- }, { statusForResult: (decision) => decision.succeeded ? "succeeded" : "failed" });
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 ? "succeeded" : "failed";
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 = { category: projection.category, reason: projection.reason };
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, "promote-run", async () => {
275
- progress.step("promote run");
276
- await runRequiredCommand(options.repoRoot, client, "promote-run", ["promote-run", materializeManifest.harnessTaskId, "--run-id", workerRunId], true, undefined, eventCtx);
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 do
356
- * not know the flag; their parser rejects before any DAG node can execute, so
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 assertRunDagCommandOk(result, args) {
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
- async function runDagWithEventsFallback(repoRoot, client, args, timeoutMs, eventCtx) {
365
- const result = await client.run(args, {
366
- cwd: repoRoot,
367
- artifactName: "run-dag",
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) || !args.includes("--events-jsonl")) {
374
- assertRunDagCommandOk(result, args);
469
+ if (!rejectsEventsJsonl(result)) {
470
+ assertAdvanceExecuteOk(result, withEvents);
375
471
  }
376
- const fallbackArgs = withoutFlagAndValue(args, "--events-jsonl");
377
- eventCtx.progress.note("run-dag controller does not support --events-jsonl; retrying without DAG event stream");
378
- const fallback = await client.run(fallbackArgs, {
379
- cwd: repoRoot,
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
- assertRunDagCommandOk(fallback, fallbackArgs);
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 !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(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") ? { status: 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
- ? { structuredArtifactSha256: readString(node, "structuredArtifactSha256") }
664
+ ? {
665
+ structuredArtifactSha256: readString(node, "structuredArtifactSha256"),
666
+ }
571
667
  : {}),
572
668
  ...(readString(node, "structuredArtifactSchemaId")
573
- ? { structuredArtifactSchemaId: readString(node, "structuredArtifactSchemaId") }
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 → dag-run-task validate run-dag → report`);
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 collectBackendTestMappedPytestScripts(workspaceRoot) {
699
+ export async function resolveBackendTestMappedPytestScripts(workspaceRoot) {
689
700
  const files = await markdownFiles(workspaceRoot);
690
- const scripts = new Set();
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 exists(path.resolve(workspaceRoot, expectedScript))) {
709
- scripts.add(expectedScript);
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 exists(path.resolve(workspaceRoot, script))) {
718
- scripts.add(script);
744
+ if (await mappedScriptExists(path.resolve(workspaceRoot, script))) {
745
+ recordScript(script, relativeFile, true);
719
746
  continue;
720
747
  }
721
- if (isSafeBackendPytestScript(expectedScript, workspaceRoot) &&
722
- (await exists(path.resolve(workspaceRoot, expectedScript)))) {
723
- scripts.add(expectedScript);
724
- continue;
748
+ if (!isSafeBackendPytestScript(expectedScript, workspaceRoot)) {
749
+ throw new Error(`unsafe mapped pytest script: ${expectedScript}`);
725
750
  }
726
- throw new Error(`mapped pytest script is missing: ${script} (module one-to-one path ${expectedScript} also missing)`);
751
+ recordScript(expectedScript, relativeFile, await mappedScriptExists(path.resolve(workspaceRoot, expectedScript)));
727
752
  }
728
753
  }
729
- if (scripts.size === 0) {
754
+ if (expectedScripts.size === 0) {
730
755
  throw new Error("no pytest scripts are mapped by final Markdown cases");
731
756
  }
732
- const safeScripts = [];
733
- for (const script of unique([...scripts]).sort()) {
734
- if (!isSafeBackendPytestScript(script, workspaceRoot)) {
735
- throw new Error(`unsafe mapped pytest script: ${script}`);
736
- }
737
- if (!(await exists(path.resolve(workspaceRoot, script)))) {
738
- throw new Error(`mapped pytest script is missing: ${script}`);
739
- }
740
- safeScripts.push(script);
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 safeScripts;
768
+ return resolution.existingScripts;
743
769
  }
744
770
  function cleanCaseTitle(rawHeading, id) {
745
771
  return rawHeading