@tea-agent/loop-agent 0.7.4 → 0.8.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 (127) hide show
  1. package/AGENTS.md +143 -142
  2. package/CHANGELOG.md +148 -161
  3. package/README.md +206 -204
  4. package/bin/agent-worker.js +22 -22
  5. package/bin/loop-agent.js +21 -21
  6. package/dist/commands/init.js +518 -488
  7. package/dist/commands/loop-benchmark.js +11 -11
  8. package/dist/commands/pi-reuse-benchmark.js +16 -16
  9. package/dist/executors/cursor-executor.js +1 -1
  10. package/dist/governance/manifest-types.js +1 -1
  11. package/dist/task/runtime.js +27 -27
  12. package/dist/worker/cli.js +3 -3
  13. package/dist/worker/observability/event-store.js +2 -1
  14. package/dist/worker/observability/read-model.js +51 -13
  15. package/dist/worker/observe/paths.js +2 -2
  16. package/dist/worker/observe/routes.js +4 -3
  17. package/dist/worker/observe/static/app.js +1479 -1419
  18. package/dist/worker/observe/static/dag-layout.d.ts +31 -0
  19. package/dist/worker/observe/static/dag-layout.js +83 -0
  20. package/dist/worker/observe/static/index.html +63 -63
  21. package/dist/worker/observe/static/styles.css +722 -613
  22. package/dist/worker/pool/run-store.js +7 -8
  23. package/dist/worker/run-task/run-task.js +11 -2
  24. package/dist/worker/runner/run-ready.js +1 -1
  25. package/dist/workflows/dag/canvas-observer.js +275 -275
  26. package/docs/README.md +80 -76
  27. package/docs/agent-dag-recovery-playbook.md +184 -184
  28. package/docs/agent-dag-runner.md +42 -42
  29. package/docs/architecture/runtime-boundaries.md +162 -162
  30. package/docs/cursor-executor-usage.md +25 -25
  31. package/docs/decisions/README.md +3 -3
  32. package/docs/design/README.md +49 -49
  33. package/docs/development-principles.md +73 -73
  34. package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -1749
  35. package/docs/exec-plans/README.md +6 -6
  36. package/docs/exec-plans/active/README.md +11 -12
  37. package/docs/exec-plans/completed/README.md +35 -32
  38. package/docs/feature-workflow.md +187 -187
  39. package/docs/harness-methodology-debugging.md +153 -153
  40. package/docs/harness-methodology-tdd.md +130 -130
  41. package/docs/harness-methodology-verification.md +27 -27
  42. package/docs/init-surface.manifest.json +245 -241
  43. package/docs/loop-agent-harness.md +63 -55
  44. package/docs/production-readiness.md +96 -96
  45. package/docs/progress/README.md +3 -3
  46. package/docs/reports/README.md +9 -9
  47. package/docs/skills/README.md +6 -6
  48. package/docs/skills/vetted-skill-registry.md +26 -26
  49. package/docs/templates/adr.md +60 -60
  50. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  51. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  52. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
  53. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  54. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  55. package/docs/templates/agent-dag-report.schema.json +454 -454
  56. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  57. package/docs/templates/agent-dag.base.json +195 -195
  58. package/docs/templates/agent-dag.final-verification.json +190 -190
  59. package/docs/templates/agent-dag.schema.json +316 -316
  60. package/docs/templates/agent-dag.supervised-implementation.json +500 -500
  61. package/docs/templates/exec-plan.md +64 -64
  62. package/docs/templates/feature-spec.md +53 -53
  63. package/docs/templates/harness.schema.json +218 -0
  64. package/docs/templates/hybrid-dag.json +193 -193
  65. package/docs/templates/init-evolution-review.md +33 -33
  66. package/docs/templates/interactive-ui-round2-experiment.md +66 -66
  67. package/docs/templates/product-line/AGENTS.md +8 -8
  68. package/docs/templates/product-line/README.md +9 -9
  69. package/docs/templates/product-line/acceptance.yaml +14 -14
  70. package/docs/templates/product-line/closeout.yaml +9 -9
  71. package/docs/templates/product-line/design.md +13 -13
  72. package/docs/templates/product-line/links.md +10 -10
  73. package/docs/templates/product-line/requirement.md +17 -17
  74. package/docs/templates/product-line/task-graph.yaml +15 -15
  75. package/docs/templates/product-line/task.yaml +65 -65
  76. package/docs/templates/product-line/test-plan.md +7 -7
  77. package/docs/templates/production-readiness-checklist.md +57 -57
  78. package/docs/templates/progress-log.md +17 -17
  79. package/docs/templates/project-start-checklist.md +9 -9
  80. package/docs/templates/qa-report.md +48 -48
  81. package/docs/templates/sprint-contract.md +29 -29
  82. package/docs/templates/worker-dogfood-evidence.md +52 -52
  83. package/docs/templates/worker-dogfood-setup.md +48 -48
  84. package/docs/verification-matrix.md +49 -49
  85. package/examples/decision-gate-agent-dag.json +123 -123
  86. package/examples/example-dag.json +51 -51
  87. package/examples/hybrid-loop-agent-dag.json +194 -194
  88. package/harness.json +73 -71
  89. package/package.json +68 -67
  90. package/scripts/check-product-line-docs.sh +22 -22
  91. package/scripts/check-task-pool-root.sh +32 -0
  92. package/skills/ai-engineering-context/SKILL.md +48 -48
  93. package/skills/code-review-core/SKILL.md +20 -20
  94. package/skills/codebase-scout/SKILL.md +19 -19
  95. package/skills/init-capability-evolution/SKILL.md +69 -69
  96. package/skills/loop-agent/SKILL.md +149 -149
  97. package/skills/loop-agent/references/README.md +67 -67
  98. package/skills/loop-agent/references/command-reference.md +432 -412
  99. package/skills/loop-agent/references/harness-policy.md +263 -263
  100. package/skills/loop-agent/references/hybrid-dag.md +216 -216
  101. package/skills/loop-agent/references/learned/README.md +21 -21
  102. package/skills/loop-agent/references/long-running-loop.md +59 -59
  103. package/skills/loop-agent/references/model-routing.md +36 -36
  104. package/skills/loop-agent/references/multi-worktree.md +54 -54
  105. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  106. package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
  107. package/skills/loop-agent/references/pi-prompt.md +23 -23
  108. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +81 -81
  109. package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
  110. package/skills/loop-agent/references/task-workflow.md +89 -89
  111. package/skills/loop-agent/references/verification-and-failure-handling.md +128 -128
  112. package/skills/requesting-code-review/SKILL.md +101 -101
  113. package/skills/requesting-code-review/code-reviewer.md +168 -168
  114. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  115. package/skills/systematic-debugging/SKILL.md +296 -296
  116. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  117. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  118. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  119. package/skills/systematic-debugging/find-polluter.sh +63 -63
  120. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  121. package/skills/systematic-debugging/test-academic.md +14 -14
  122. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  123. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  124. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  125. package/skills/test-driven-development/SKILL.md +20 -20
  126. package/skills/verification-before-completion/SKILL.md +154 -154
  127. package/skills/webapp-testing/SKILL.md +19 -19
@@ -37,17 +37,17 @@ export function parseLoopBenchmarkArgs(args) {
37
37
  return { json, markdown, outputPath };
38
38
  }
39
39
  export function printLoopBenchmarkUsage() {
40
- console.log(`usage: loop-benchmark [options]
41
-
42
- Deterministic loop-agent loop benchmark baseline (no live Pi/Cursor calls).
43
-
44
- Options:
45
- --json Emit JSON (default when no format flag is set)
46
- --markdown Emit Markdown report
47
- --output <path> Write Markdown report to a repo-relative or absolute path
48
- -h, --help Show this help
49
-
50
- Control groups: single-repair, 3-pass-convergence, 3-pass-convergence+quota.
40
+ console.log(`usage: loop-benchmark [options]
41
+
42
+ Deterministic loop-agent loop benchmark baseline (no live Pi/Cursor calls).
43
+
44
+ Options:
45
+ --json Emit JSON (default when no format flag is set)
46
+ --markdown Emit Markdown report
47
+ --output <path> Write Markdown report to a repo-relative or absolute path
48
+ -h, --help Show this help
49
+
50
+ Control groups: single-repair, 3-pass-convergence, 3-pass-convergence+quota.
51
51
  Recommendation never changes convergence.enabled default.`);
52
52
  }
53
53
  export async function runLoopBenchmark(repoRoot, rawArgs) {
@@ -106,22 +106,22 @@ export function parsePiReuseBenchmarkArgs(args) {
106
106
  };
107
107
  }
108
108
  export function printPiReuseBenchmarkUsage() {
109
- console.log(`usage: pi-reuse-benchmark [options]
110
-
111
- Deterministic Pi runtime reuse benchmark/decision summary (no live Pi calls).
112
-
113
- Options:
114
- --report <path> Benchmark report markdown (approval status)
115
- --approval <path> Explicit approval JSON artifact
116
- --off-executor <path> Baseline executor.jsonl (reuse off)
117
- --on-executor <path> Treatment executor.jsonl (reuse on)
118
- --off-task <task-id> Resolve baseline from .harness/tasks/<id>/logs/executor.jsonl
119
- --on-task <task-id> Resolve treatment from .harness/tasks/<id>/logs/executor.jsonl
120
- --json Emit JSON (default when no format flag is set)
121
- --markdown Emit Markdown summary
122
- -h, --help Show this help
123
-
124
- Recommendations: defer | maintain-opt-in | eligible-for-human-review
109
+ console.log(`usage: pi-reuse-benchmark [options]
110
+
111
+ Deterministic Pi runtime reuse benchmark/decision summary (no live Pi calls).
112
+
113
+ Options:
114
+ --report <path> Benchmark report markdown (approval status)
115
+ --approval <path> Explicit approval JSON artifact
116
+ --off-executor <path> Baseline executor.jsonl (reuse off)
117
+ --on-executor <path> Treatment executor.jsonl (reuse on)
118
+ --off-task <task-id> Resolve baseline from .harness/tasks/<id>/logs/executor.jsonl
119
+ --on-task <task-id> Resolve treatment from .harness/tasks/<id>/logs/executor.jsonl
120
+ --json Emit JSON (default when no format flag is set)
121
+ --markdown Emit Markdown summary
122
+ -h, --help Show this help
123
+
124
+ Recommendations: defer | maintain-opt-in | eligible-for-human-review
125
125
  Never changes CODE_AGENT_PI_REUSE_RUNTIME default (off).`);
126
126
  }
127
127
  function resolveRepoRelative(repoRoot, filePath) {
@@ -29,7 +29,7 @@ export function resolveArtifactWriteDir(options) {
29
29
  }
30
30
  export function buildArtifactPathPrompt(writeDir) {
31
31
  if (!writeDir)
32
- return `After changes, write artifacts/修改记录.md and artifacts/验证结果.md with verification evidence.
32
+ return `After changes, write artifacts/修改记录.md and artifacts/验证结果.md with verification evidence.
33
33
  ${ARTIFACT_INSTRUCTIONS}`;
34
34
  return [
35
35
  `After changes, write the following files:`,
@@ -46,7 +46,7 @@ export const workflowPolicySchema = z
46
46
  .object({
47
47
  /** Preferred implementation workflow for recoverable loop-agent work. Declarative policy; callers may still require explicit CLI flags. */
48
48
  defaultImplementationWorkflow: z
49
- .enum(["agent-dag", "level-1"])
49
+ .enum(["agent-dag"])
50
50
  .optional()
51
51
  .default("agent-dag"),
52
52
  dag: z
@@ -237,35 +237,35 @@ export function shouldInjectSubagentGuidance(step, mode) {
237
237
  return false;
238
238
  }
239
239
  /** Advisory guidance for `analyze-plan` mode. */
240
- export const SUBAGENT_GUIDANCE_STANDARD = `<subagent_guidance>
241
- You have access to the \`subagent\` tool for lightweight delegation within this step.
242
- Use it only for read-only tasks:
243
- - Parallel scout: dispatch multiple subagents to search/read different areas simultaneously
244
- - Chain: scout -> planner (one subagent scouts, another plans based on findings)
245
- - Reviewer: have a subagent review your analysis/plan before finalizing
246
- Do NOT use subagent for writing, editing, or executing commands.
247
- Subagent output is advisory only; always verify and incorporate findings into your own output.
248
- Do NOT treat subagent results as authoritative state or artifact sources.
240
+ export const SUBAGENT_GUIDANCE_STANDARD = `<subagent_guidance>
241
+ You have access to the \`subagent\` tool for lightweight delegation within this step.
242
+ Use it only for read-only tasks:
243
+ - Parallel scout: dispatch multiple subagents to search/read different areas simultaneously
244
+ - Chain: scout -> planner (one subagent scouts, another plans based on findings)
245
+ - Reviewer: have a subagent review your analysis/plan before finalizing
246
+ Do NOT use subagent for writing, editing, or executing commands.
247
+ Subagent output is advisory only; always verify and incorporate findings into your own output.
248
+ Do NOT treat subagent results as authoritative state or artifact sources.
249
249
  </subagent_guidance>`;
250
250
  /** Strong guidance for `full` mode — prescriptive when to delegate. */
251
- export const SUBAGENT_GUIDANCE_STRONG = `<subagent_guidance>
252
- You have access to the \`subagent\` tool for lightweight delegation within this step.
253
-
254
- You SHOULD delegate to subagent scouts when:
255
- - The task requires scanning 3+ directories or comparing implementations across modules
256
- - You would otherwise need 5+ sequential read/grep calls to gather context
257
- - A reviewer subagent can independently catch scope drift before you finalize your output
258
-
259
- Delegation saves context tokens and produces better results.
260
-
261
- Allowed patterns:
262
- - Parallel scout: dispatch 2-3 subagents simultaneously to cover different file trees
263
- - Chain: scout -> planner (one subagent scouts, another plans based on findings)
264
- - Reviewer: have a subagent review your analysis/plan before finalizing
265
-
266
- Do NOT use subagent for writing, editing, or executing commands.
267
- Subagent output is advisory only; always verify and incorporate findings into your own output.
268
- Do NOT treat subagent results as authoritative state or artifact sources.
251
+ export const SUBAGENT_GUIDANCE_STRONG = `<subagent_guidance>
252
+ You have access to the \`subagent\` tool for lightweight delegation within this step.
253
+
254
+ You SHOULD delegate to subagent scouts when:
255
+ - The task requires scanning 3+ directories or comparing implementations across modules
256
+ - You would otherwise need 5+ sequential read/grep calls to gather context
257
+ - A reviewer subagent can independently catch scope drift before you finalize your output
258
+
259
+ Delegation saves context tokens and produces better results.
260
+
261
+ Allowed patterns:
262
+ - Parallel scout: dispatch 2-3 subagents simultaneously to cover different file trees
263
+ - Chain: scout -> planner (one subagent scouts, another plans based on findings)
264
+ - Reviewer: have a subagent review your analysis/plan before finalizing
265
+
266
+ Do NOT use subagent for writing, editing, or executing commands.
267
+ Subagent output is advisory only; always verify and incorporate findings into your own output.
268
+ Do NOT treat subagent results as authoritative state or artifact sources.
269
269
  </subagent_guidance>`;
270
270
  /** Preserved for backward compatibility (alias of STANDARD). */
271
271
  export const SUBAGENT_GUIDANCE = SUBAGENT_GUIDANCE_STANDARD;
@@ -13,7 +13,7 @@ import { createCompositeProgressReporter } from "./observability/progress-compos
13
13
  import { createRoutedWorkerEventStore } from "./observability/event-store.js";
14
14
  import { buildGlobalSnapshot } from "./observability/read-model.js";
15
15
  import { createObserveServer } from "./observe/server.js";
16
- import { prepareTaskPoolRetry } from "./pool/run-store.js";
16
+ import { getTaskPoolRoot, prepareTaskPoolRetry } from "./pool/run-store.js";
17
17
  import { taskSpecSchema } from "./task-spec/schema.js";
18
18
  import { validateTaskSpec } from "./task-spec/validate.js";
19
19
  import { validateFeatureTaskGraph } from "./task-graph/validate.js";
@@ -91,7 +91,7 @@ export function buildAgentWorkerProgram() {
91
91
  const batchRunId = options.batchRunId ?? buildBatchRunId(new Date());
92
92
  const client = new LoopAgentClient({
93
93
  loopAgentBin: options.loopAgentBin,
94
- artifactRoot: path.join(repoRoot, ".task-pool", "artifacts", batchRunId),
94
+ artifactRoot: path.join(getTaskPoolRoot(repoRoot), "artifacts", batchRunId),
95
95
  });
96
96
  let progress;
97
97
  try {
@@ -161,7 +161,7 @@ export function buildAgentWorkerProgram() {
161
161
  .action(async (options) => {
162
162
  const repoRoot = path.resolve(options.repo);
163
163
  const outputPath = options.output ??
164
- path.join(repoRoot, ".task-pool", "reports", "morning-report.md");
164
+ path.join(getTaskPoolRoot(repoRoot), "reports", "morning-report.md");
165
165
  await writeMorningReport({
166
166
  repoRoot,
167
167
  ...(options.batchRunId ? { batchRunId: options.batchRunId } : {}),
@@ -1,6 +1,7 @@
1
1
  import { appendFile, mkdir } from "node:fs/promises";
2
2
  import { appendFileSync, mkdirSync } from "node:fs";
3
3
  import path from "node:path";
4
+ import { getTaskPoolRoot } from "../pool/run-store.js";
4
5
  function writeDiagnostic(context, error) {
5
6
  const message = error instanceof Error ? error.message : String(error);
6
7
  process.stderr.write(`[worker-event-store] ${context}: ${message}\n`);
@@ -43,7 +44,7 @@ export function createWorkerEventStore(jsonlPath) {
43
44
  };
44
45
  }
45
46
  export function createRoutedWorkerEventStore(repoRoot) {
46
- const observabilityRoot = path.join(path.resolve(repoRoot), ".task-pool", "observability");
47
+ const observabilityRoot = path.join(getTaskPoolRoot(path.resolve(repoRoot)), "observability");
47
48
  const globalStore = createWorkerEventStore(path.join(observabilityRoot, "events.jsonl"));
48
49
  let queue = Promise.resolve();
49
50
  function storesFor(event) {
@@ -3,6 +3,7 @@ import { readdir, readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { truncateUtf8Preview } from "../../shared/preview.js";
5
5
  import { parseWorkerEventLine } from "./events.js";
6
+ import { getTaskPoolRoot } from "../pool/run-store.js";
6
7
  const ACTIVE_TASK_STATUSES = new Set(["running", "pending"]);
7
8
  const ACTIVE_BATCH_STATUSES = new Set(["running", "pending"]);
8
9
  export async function buildGlobalSnapshot(options) {
@@ -474,7 +475,7 @@ function mapBatchStatus(status) {
474
475
  }
475
476
  async function loadObservabilityEvents(repoRoot) {
476
477
  const events = [];
477
- const obsRoot = path.join(repoRoot, ".task-pool", "observability");
478
+ const obsRoot = path.join(getTaskPoolRoot(repoRoot), "observability");
478
479
  await appendJsonlEvents(path.join(obsRoot, "events.jsonl"), events);
479
480
  const batchesDir = path.join(obsRoot, "batches");
480
481
  await forEachSubdirJsonl(batchesDir, "events.jsonl", events);
@@ -510,7 +511,7 @@ async function appendJsonlEvents(filePath, events) {
510
511
  }
511
512
  async function loadLedgerRuns(repoRoot) {
512
513
  const runs = [];
513
- const raw = await safeReadText(path.join(repoRoot, ".task-pool", "runs", "runs.jsonl"));
514
+ const raw = await safeReadText(path.join(getTaskPoolRoot(repoRoot), "runs.jsonl"));
514
515
  if (!raw)
515
516
  return runs;
516
517
  for (const line of raw.split("\n")) {
@@ -553,14 +554,15 @@ async function loadLedgerRuns(repoRoot) {
553
554
  }
554
555
  async function loadLedgerBatches(repoRoot) {
555
556
  const batches = [];
556
- const runsDir = path.join(repoRoot, ".task-pool", "runs");
557
+ const artifactsDir = path.join(getTaskPoolRoot(repoRoot), "artifacts");
557
558
  try {
558
- const entries = await readdir(runsDir, { withFileTypes: true });
559
+ const entries = await readdir(artifactsDir, { withFileTypes: true });
559
560
  for (const entry of entries) {
560
- if (!entry.isFile() || !entry.name.endsWith(".json") || entry.name === "runs.jsonl")
561
+ if (!entry.isDirectory())
561
562
  continue;
562
- const batchRunId = entry.name.replace(/\.json$/, "");
563
- const parsed = await safeReadJson(path.join(runsDir, entry.name));
563
+ const batchRunId = entry.name;
564
+ const batchRunPath = path.join(artifactsDir, batchRunId, "batch-run.json");
565
+ const parsed = await safeReadJson(batchRunPath);
564
566
  if (!parsed)
565
567
  continue;
566
568
  const summaryObj = readObject(parsed, "summary");
@@ -579,7 +581,7 @@ async function loadLedgerBatches(repoRoot) {
579
581
  }
580
582
  : undefined,
581
583
  tasks: [],
582
- batchRunPath: path.join(runsDir, entry.name),
584
+ batchRunPath,
583
585
  });
584
586
  }
585
587
  }
@@ -590,7 +592,7 @@ async function loadLedgerBatches(repoRoot) {
590
592
  }
591
593
  async function loadStateRecords(repoRoot) {
592
594
  const states = [];
593
- const stateDir = path.join(repoRoot, ".task-pool", "state");
595
+ const stateDir = path.join(getTaskPoolRoot(repoRoot), "states");
594
596
  try {
595
597
  const entries = await readdir(stateDir, { withFileTypes: true });
596
598
  for (const entry of entries) {
@@ -621,7 +623,7 @@ async function loadStateRecords(repoRoot) {
621
623
  }
622
624
  async function loadFailureHandoffs(repoRoot) {
623
625
  const handoffs = [];
624
- const handoffDir = path.join(repoRoot, ".task-pool", "failure-handoffs");
626
+ const handoffDir = path.join(getTaskPoolRoot(repoRoot), "failure-handoffs");
625
627
  try {
626
628
  const entries = await readdir(handoffDir, { withFileTypes: true });
627
629
  for (const entry of entries) {
@@ -645,7 +647,7 @@ async function loadDagRuns(repoRoot) {
645
647
  const byId = new Map();
646
648
  const statePaths = await findStateJsonFiles(path.join(repoRoot, ".harness", "dag-runs"));
647
649
  for (const statePath of statePaths) {
648
- const summary = parseDagStateFile(statePath);
650
+ const summary = await parseDagStateFile(statePath);
649
651
  if (summary)
650
652
  byId.set(summary.dagRunId, mergeDagRun(byId.get(summary.dagRunId), summary));
651
653
  }
@@ -674,6 +676,7 @@ function mergeDagRun(existing, incoming) {
674
676
  ...(durationMs !== undefined ? { durationMs } : {}),
675
677
  ...(ranks && ranks.length > 0 ? { ranks } : {}),
676
678
  nodes: mergeDagNodes(existing.nodes, incoming.nodes),
679
+ edges: incoming.edges.length > 0 ? incoming.edges : existing.edges,
677
680
  ...(incoming.dagPath ?? existing.dagPath ? { dagPath: incoming.dagPath ?? existing.dagPath } : {}),
678
681
  };
679
682
  }
@@ -735,7 +738,7 @@ function nodeErrorPreview(node, status) {
735
738
  return truncateUtf8Preview(stderr);
736
739
  }
737
740
  async function loadDagEventFiles(repoRoot) {
738
- const runsDir = path.join(repoRoot, ".task-pool", "observability", "runs");
741
+ const runsDir = path.join(getTaskPoolRoot(repoRoot), "observability", "runs");
739
742
  let entries;
740
743
  try {
741
744
  entries = await readdir(runsDir, { withFileTypes: true });
@@ -819,6 +822,7 @@ async function loadDagEventFiles(repoRoot) {
819
822
  ...(finishedAt ? { finishedAt } : {}),
820
823
  ...(durationMs !== undefined ? { durationMs } : {}),
821
824
  nodes: [...nodeMap.values()].sort((a, b) => a.nodeId.localeCompare(b.nodeId)),
825
+ edges: [],
822
826
  });
823
827
  }
824
828
  return result;
@@ -845,7 +849,7 @@ async function walkForStateJson(dir, results) {
845
849
  // skip
846
850
  }
847
851
  }
848
- function parseDagStateFile(statePath) {
852
+ async function parseDagStateFile(statePath) {
849
853
  try {
850
854
  if (!existsSync(statePath))
851
855
  return undefined;
@@ -863,6 +867,7 @@ function parseDagStateFile(statePath) {
863
867
  const ranks = readStringMatrix(parsed, "ranks");
864
868
  const rankByNode = buildRankIndex(ranks);
865
869
  const nodes = parseDagNodes(parsed, rankByNode);
870
+ const edges = await parseDagEdges(path.join(runDir, "run.json"), nodes);
866
871
  return {
867
872
  dagRunId,
868
873
  status,
@@ -872,6 +877,7 @@ function parseDagStateFile(statePath) {
872
877
  ...(durationMs !== undefined ? { durationMs } : {}),
873
878
  ...(ranks.length > 0 ? { ranks } : {}),
874
879
  nodes,
880
+ edges,
875
881
  dagPath: runDir,
876
882
  };
877
883
  }
@@ -879,6 +885,30 @@ function parseDagStateFile(statePath) {
879
885
  return undefined;
880
886
  }
881
887
  }
888
+ async function parseDagEdges(runPath, nodes) {
889
+ const parsed = await safeReadJson(runPath);
890
+ if (!parsed)
891
+ return [];
892
+ const tasks = readObjectArray(parsed, "tasks");
893
+ if (tasks.length === 0)
894
+ return [];
895
+ const knownNodeIds = new Set(nodes.map((node) => node.nodeId));
896
+ const edges = new Map();
897
+ for (const task of tasks) {
898
+ const taskId = readString(task, "id") ?? readString(task, "nodeId");
899
+ if (!taskId || !knownNodeIds.has(taskId))
900
+ continue;
901
+ const dependencies = readStringArray(task, "depends_on");
902
+ const compatibleDependencies = dependencies.length > 0 ? dependencies : readStringArray(task, "dependsOn");
903
+ for (const dependencyId of compatibleDependencies) {
904
+ if (dependencyId === taskId || !knownNodeIds.has(dependencyId))
905
+ continue;
906
+ const edge = { from: dependencyId, to: taskId };
907
+ edges.set(`${edge.from}\u0000${edge.to}`, edge);
908
+ }
909
+ }
910
+ return [...edges.values()].sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
911
+ }
882
912
  function buildRankIndex(ranks) {
883
913
  const index = new Map();
884
914
  for (let i = 0; i < ranks.length; i++) {
@@ -995,6 +1025,14 @@ function readString(value, key) {
995
1025
  const child = value[key];
996
1026
  return typeof child === "string" ? child : undefined;
997
1027
  }
1028
+ function readStringArray(value, key) {
1029
+ if (!value || typeof value !== "object")
1030
+ return [];
1031
+ const child = value[key];
1032
+ if (!Array.isArray(child))
1033
+ return [];
1034
+ return child.filter((item) => typeof item === "string");
1035
+ }
998
1036
  function readNumber(value, key) {
999
1037
  if (!value || typeof value !== "object")
1000
1038
  return undefined;
@@ -33,8 +33,8 @@ export function resolveArtifactPath(repoRoot, relative) {
33
33
  function isAllowedArtifactRelativePath(relative) {
34
34
  const normalized = path.normalize(relative);
35
35
  const segments = normalized.split(path.sep).filter(Boolean);
36
- if (segments[0] === ".task-pool") {
37
- return ["artifacts", "failure-handoffs", "reports"].includes(segments[1] ?? "");
36
+ if (segments[0] === ".harness" && segments[1] === "task-pool") {
37
+ return ["artifacts", "failure-handoffs", "reports"].includes(segments[2] ?? "");
38
38
  }
39
39
  if (segments[0] === ".harness" && segments[1] === "tasks") {
40
40
  return segments.includes("artifacts");
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { parseWorkerEventLine } from "../observability/events.js";
5
5
  import { isSafeObservabilityIdentifier } from "../observability/event-store.js";
6
6
  import { buildGlobalSnapshot } from "../observability/read-model.js";
7
+ import { getTaskPoolRoot } from "../pool/run-store.js";
7
8
  import { resolveArtifactPath, toRepoRelativeArtifactPath, } from "./paths.js";
8
9
  const ROUTES = [
9
10
  { method: "GET", pattern: /^\/api\/health$/, handler: handleHealth },
@@ -148,7 +149,7 @@ async function handleRunEvents(_req, res, match, ctx) {
148
149
  return;
149
150
  }
150
151
  const after = parseNonNegativeInt(match.query.get("after"), 0);
151
- const eventsPath = path.join(ctx.repoRoot, ".task-pool", "observability", "runs", workerRunId, "events.jsonl");
152
+ const eventsPath = path.join(getTaskPoolRoot(ctx.repoRoot), "observability", "runs", workerRunId, "events.jsonl");
152
153
  const events = (await readJsonlLines(eventsPath, after)).map((event) => normalizeEventArtifactRefs(ctx.repoRoot, event));
153
154
  sendJson(res, 200, { events });
154
155
  }
@@ -170,7 +171,7 @@ async function handleRunArtifacts(_req, res, match, ctx) {
170
171
  artifacts.push({ path: artifactPath, label });
171
172
  }
172
173
  }
173
- const eventsPath = path.join(ctx.repoRoot, ".task-pool", "observability", "runs", workerRunId, "events.jsonl");
174
+ const eventsPath = path.join(getTaskPoolRoot(ctx.repoRoot), "observability", "runs", workerRunId, "events.jsonl");
174
175
  const runEvents = await readJsonlLines(eventsPath, 0);
175
176
  for (const event of runEvents) {
176
177
  const parsed = event;
@@ -254,7 +255,7 @@ async function handleEventStream(req, res, match, ctx) {
254
255
  "Cache-Control": "no-cache",
255
256
  Connection: "keep-alive",
256
257
  });
257
- const observabilityRoot = path.join(ctx.repoRoot, ".task-pool", "observability");
258
+ const observabilityRoot = path.join(getTaskPoolRoot(ctx.repoRoot), "observability");
258
259
  const eventsPath = path.join(observabilityRoot, "events.jsonl");
259
260
  let offset = 0;
260
261
  const sendMatchingEvents = async (initial) => {