@tea-agent/loop-agent 0.3.0 → 0.4.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 (119) hide show
  1. package/AGENTS.md +135 -133
  2. package/CHANGELOG.md +88 -63
  3. package/README.md +171 -168
  4. package/bin/agent-worker.js +22 -0
  5. package/bin/loop-agent.js +21 -21
  6. package/dist/commands/init.js +457 -457
  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/executors/dag-pi-executor.js +8 -1
  11. package/dist/task/runtime.js +27 -27
  12. package/dist/worker/cli.js +119 -0
  13. package/dist/worker/loop-agent/command-result.js +1 -0
  14. package/dist/worker/loop-agent/loop-agent-client.js +105 -0
  15. package/dist/worker/loop-agent/parse-json.js +14 -0
  16. package/dist/worker/materialize/harness-task-materializer.js +157 -0
  17. package/dist/worker/pool/failure-routing.js +98 -0
  18. package/dist/worker/pool/run-store.js +117 -0
  19. package/dist/worker/pool/types.js +1 -0
  20. package/dist/worker/preflight.js +108 -0
  21. package/dist/worker/profile-mapping.js +76 -0
  22. package/dist/worker/progress-reporter.js +81 -0
  23. package/dist/worker/report/morning-report.js +69 -0
  24. package/dist/worker/repos/repo-resolver.js +23 -0
  25. package/dist/worker/run-task/run-task.js +359 -0
  26. package/dist/worker/runner/run-ready.js +216 -0
  27. package/dist/worker/task-graph/acceptance-schema.js +25 -0
  28. package/dist/worker/task-graph/ready-queue.js +23 -0
  29. package/dist/worker/task-graph/task-graph-schema.js +28 -0
  30. package/dist/worker/task-graph/types.js +1 -0
  31. package/dist/worker/task-graph/validate.js +188 -0
  32. package/dist/worker/task-spec/complexity-mapping.js +8 -0
  33. package/dist/worker/task-spec/schema.js +116 -0
  34. package/dist/worker/task-spec/types.js +1 -0
  35. package/dist/worker/task-spec/validate.js +352 -0
  36. package/dist/workflows/dag/canvas-observer.js +275 -275
  37. package/docs/README.md +65 -61
  38. package/docs/agent-dag-recovery-playbook.md +184 -184
  39. package/docs/agent-dag-runner.md +42 -42
  40. package/docs/architecture/runtime-boundaries.md +147 -147
  41. package/docs/cursor-executor-usage.md +25 -25
  42. package/docs/decisions/README.md +3 -3
  43. package/docs/design/README.md +36 -36
  44. package/docs/development-principles.md +73 -71
  45. package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -1749
  46. package/docs/exec-plans/README.md +6 -6
  47. package/docs/exec-plans/active/README.md +7 -7
  48. package/docs/exec-plans/completed/README.md +19 -11
  49. package/docs/feature-workflow.md +186 -186
  50. package/docs/harness-methodology-debugging.md +153 -153
  51. package/docs/harness-methodology-tdd.md +130 -130
  52. package/docs/harness-methodology-verification.md +27 -27
  53. package/docs/loop-agent-harness.md +42 -42
  54. package/docs/production-readiness.md +96 -96
  55. package/docs/progress/README.md +3 -3
  56. package/docs/reports/README.md +5 -5
  57. package/docs/skills/README.md +6 -6
  58. package/docs/skills/vetted-skill-registry.md +22 -22
  59. package/docs/templates/adr.md +60 -60
  60. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  61. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  62. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
  63. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  64. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  65. package/docs/templates/agent-dag-report.schema.json +454 -454
  66. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  67. package/docs/templates/agent-dag.base.json +195 -195
  68. package/docs/templates/agent-dag.final-verification.json +190 -190
  69. package/docs/templates/agent-dag.schema.json +316 -316
  70. package/docs/templates/agent-dag.supervised-implementation.json +500 -500
  71. package/docs/templates/exec-plan.md +64 -64
  72. package/docs/templates/feature-spec.md +53 -53
  73. package/docs/templates/hybrid-dag.json +193 -193
  74. package/docs/templates/production-readiness-checklist.md +57 -57
  75. package/docs/templates/progress-log.md +17 -17
  76. package/docs/templates/project-start-checklist.md +9 -9
  77. package/docs/templates/qa-report.md +48 -48
  78. package/docs/templates/sprint-contract.md +29 -29
  79. package/docs/verification-matrix.md +41 -41
  80. package/examples/decision-gate-agent-dag.json +123 -123
  81. package/examples/example-dag.json +51 -51
  82. package/examples/hybrid-loop-agent-dag.json +194 -194
  83. package/harness.json +89 -89
  84. package/package.json +60 -58
  85. package/skills/ai-engineering-context/SKILL.md +48 -48
  86. package/skills/code-review-core/SKILL.md +20 -20
  87. package/skills/codebase-scout/SKILL.md +19 -19
  88. package/skills/loop-agent/SKILL.md +147 -145
  89. package/skills/loop-agent/references/README.md +67 -67
  90. package/skills/loop-agent/references/command-reference.md +368 -340
  91. package/skills/loop-agent/references/harness-policy.md +259 -258
  92. package/skills/loop-agent/references/hybrid-dag.md +216 -216
  93. package/skills/loop-agent/references/learned/README.md +21 -21
  94. package/skills/loop-agent/references/long-running-loop.md +59 -59
  95. package/skills/loop-agent/references/model-routing.md +36 -36
  96. package/skills/loop-agent/references/multi-worktree.md +54 -54
  97. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  98. package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
  99. package/skills/loop-agent/references/pi-prompt.md +23 -23
  100. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +81 -81
  101. package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
  102. package/skills/loop-agent/references/task-workflow.md +84 -84
  103. package/skills/loop-agent/references/verification-and-failure-handling.md +128 -128
  104. package/skills/requesting-code-review/SKILL.md +101 -101
  105. package/skills/requesting-code-review/code-reviewer.md +168 -168
  106. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  107. package/skills/systematic-debugging/SKILL.md +296 -296
  108. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  109. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  110. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  111. package/skills/systematic-debugging/find-polluter.sh +63 -63
  112. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  113. package/skills/systematic-debugging/test-academic.md +14 -14
  114. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  115. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  116. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  117. package/skills/test-driven-development/SKILL.md +20 -20
  118. package/skills/verification-before-completion/SKILL.md +154 -154
  119. package/skills/webapp-testing/SKILL.md +19 -19
@@ -0,0 +1,117 @@
1
+ import { appendFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ export function getTaskPoolRoot(repoRoot) {
4
+ return path.join(repoRoot, ".task-pool");
5
+ }
6
+ export function getRunsJsonlPath(repoRoot) {
7
+ return path.join(getTaskPoolRoot(repoRoot), "runs", "runs.jsonl");
8
+ }
9
+ export function getEventsJsonlPath(repoRoot) {
10
+ return path.join(getTaskPoolRoot(repoRoot), "events", "events.jsonl");
11
+ }
12
+ export function getTaskStatePath(repoRoot, taskId) {
13
+ return path.join(getTaskPoolRoot(repoRoot), "state", `${taskId}.json`);
14
+ }
15
+ export async function recordTaskPoolRun(input) {
16
+ await ensurePoolDirs(input.repoRoot);
17
+ await appendJsonlFile(getRunsJsonlPath(input.repoRoot), input.run);
18
+ await writeTaskPoolState(input.repoRoot, stateFromRun(input.run));
19
+ await appendJsonlFile(getEventsJsonlPath(input.repoRoot), eventFromRun(input.run));
20
+ }
21
+ export async function findRunByWorkerRunId(repoRoot, workerRunId) {
22
+ const runs = await readJsonlFile(getRunsJsonlPath(repoRoot));
23
+ return runs.find((run) => run.workerRunId === workerRunId);
24
+ }
25
+ export async function readTaskPoolState(repoRoot, taskId) {
26
+ const statePath = getTaskStatePath(repoRoot, taskId);
27
+ try {
28
+ return JSON.parse(await readFile(statePath, "utf-8"));
29
+ }
30
+ catch (error) {
31
+ if (isNotFound(error))
32
+ return undefined;
33
+ throw error;
34
+ }
35
+ }
36
+ export async function readAllTaskPoolStates(repoRoot) {
37
+ const stateDir = path.join(getTaskPoolRoot(repoRoot), "state");
38
+ try {
39
+ const entries = await readdir(stateDir);
40
+ const states = {};
41
+ for (const entry of entries) {
42
+ if (!entry.endsWith(".json"))
43
+ continue;
44
+ const raw = await readFile(path.join(stateDir, entry), "utf-8");
45
+ const state = JSON.parse(raw);
46
+ states[state.taskId] = state;
47
+ }
48
+ return states;
49
+ }
50
+ catch (error) {
51
+ if (isNotFound(error))
52
+ return {};
53
+ throw error;
54
+ }
55
+ }
56
+ export async function writeTaskPoolState(repoRoot, state) {
57
+ const statePath = getTaskStatePath(repoRoot, state.taskId);
58
+ await mkdir(path.dirname(statePath), { recursive: true });
59
+ await writeFile(statePath, `${JSON.stringify({ schemaVersion: 1, ...state }, null, 2)}\n`, "utf-8");
60
+ }
61
+ export async function readJsonlFile(filePath) {
62
+ try {
63
+ const raw = await readFile(filePath, "utf-8");
64
+ return raw
65
+ .split(/\r?\n/)
66
+ .filter((line) => line.trim().length > 0)
67
+ .map((line) => JSON.parse(line));
68
+ }
69
+ catch (error) {
70
+ if (isNotFound(error))
71
+ return [];
72
+ throw error;
73
+ }
74
+ }
75
+ async function appendJsonlFile(filePath, value) {
76
+ await mkdir(path.dirname(filePath), { recursive: true });
77
+ await appendFile(filePath, `${JSON.stringify(value)}\n`, "utf-8");
78
+ }
79
+ async function ensurePoolDirs(repoRoot) {
80
+ await mkdir(path.join(getTaskPoolRoot(repoRoot), "runs"), { recursive: true });
81
+ await mkdir(path.join(getTaskPoolRoot(repoRoot), "events"), { recursive: true });
82
+ await mkdir(path.join(getTaskPoolRoot(repoRoot), "state"), { recursive: true });
83
+ }
84
+ function stateFromRun(run) {
85
+ return {
86
+ taskId: run.taskId,
87
+ status: stateStatusFromRun(run),
88
+ updatedAt: run.recordedAt,
89
+ workerRunId: run.workerRunId,
90
+ ...(run.runRecordPath ? { lastRunRecordPath: run.runRecordPath } : {}),
91
+ ...(run.failure ? { failure: run.failure } : {}),
92
+ };
93
+ }
94
+ function stateStatusFromRun(run) {
95
+ if (run.status === "succeeded")
96
+ return "Done";
97
+ if (run.status === "run-error")
98
+ return "Blocked";
99
+ return "Failed";
100
+ }
101
+ function eventFromRun(run) {
102
+ return {
103
+ schemaVersion: 1,
104
+ at: run.recordedAt,
105
+ type: "task-run-recorded",
106
+ batchRunId: run.batchRunId,
107
+ workerRunId: run.workerRunId,
108
+ taskId: run.taskId,
109
+ status: run.status,
110
+ };
111
+ }
112
+ function isNotFound(error) {
113
+ return Boolean(error &&
114
+ typeof error === "object" &&
115
+ "code" in error &&
116
+ error.code === "ENOENT");
117
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,108 @@
1
+ import { access } from "node:fs/promises";
2
+ import path from "node:path";
3
+ export const DEFAULT_CHECK_REPO_TIMEOUT_MS = 300_000;
4
+ export async function preflightTargetRepo(input) {
5
+ const commands = [];
6
+ if (!(await exists(path.join(input.repoRoot, "harness.json")))) {
7
+ return {
8
+ ok: false,
9
+ code: "target-not-initialized",
10
+ message: "target repo is missing harness.json",
11
+ commands,
12
+ };
13
+ }
14
+ if (!(await exists(path.join(input.repoRoot, ".harness")))) {
15
+ return {
16
+ ok: false,
17
+ code: "target-not-initialized",
18
+ message: "target repo is missing .harness",
19
+ commands,
20
+ };
21
+ }
22
+ const version = await input.client.run(["--version"], {
23
+ cwd: input.repoRoot,
24
+ artifactName: "loop-agent-version",
25
+ });
26
+ commands.push({ artifactName: "loop-agent-version", result: version });
27
+ if (!version.ok) {
28
+ return {
29
+ ok: false,
30
+ code: "version-failed",
31
+ message: "loop-agent --version failed",
32
+ commands,
33
+ };
34
+ }
35
+ const inspect = await input.client.run(["inspect"], {
36
+ cwd: input.repoRoot,
37
+ artifactName: "inspect",
38
+ expectJson: true,
39
+ });
40
+ commands.push({ artifactName: "inspect", result: inspect });
41
+ if (!inspect.ok || !inspect.json || typeof inspect.json !== "object") {
42
+ return {
43
+ ok: false,
44
+ code: "inspect-failed",
45
+ message: "loop-agent inspect failed or did not return JSON",
46
+ commands,
47
+ };
48
+ }
49
+ const docsAudit = await input.client.run(["docs", "audit"], {
50
+ cwd: input.repoRoot,
51
+ artifactName: "docs-audit",
52
+ });
53
+ commands.push({ artifactName: "docs-audit", result: docsAudit });
54
+ if (!docsAudit.ok) {
55
+ return {
56
+ ok: false,
57
+ code: "docs-audit-failed",
58
+ message: "loop-agent docs audit failed",
59
+ commands,
60
+ };
61
+ }
62
+ const gitStatus = await input.client.runExternal("git", ["status", "--short", "--branch"], {
63
+ cwd: input.repoRoot,
64
+ artifactName: "git-status",
65
+ });
66
+ commands.push({ artifactName: "git-status", result: gitStatus });
67
+ if (!gitStatus.ok) {
68
+ return {
69
+ ok: false,
70
+ code: "git-status-failed",
71
+ message: "git status --short --branch failed",
72
+ commands,
73
+ };
74
+ }
75
+ if (input.runCheckRepo) {
76
+ const [command, ...args] = input.checkRepoCommand ?? ["bash", "scripts/check-repo.sh"];
77
+ const checkRepo = await input.client.runExternal(command, args, {
78
+ cwd: input.repoRoot,
79
+ artifactName: "check-repo",
80
+ timeoutMs: DEFAULT_CHECK_REPO_TIMEOUT_MS,
81
+ });
82
+ commands.push({ artifactName: "check-repo", result: checkRepo });
83
+ if (!checkRepo.ok) {
84
+ return {
85
+ ok: false,
86
+ code: "check-repo-failed",
87
+ message: `${[command, ...args].join(" ")} failed`,
88
+ commands,
89
+ };
90
+ }
91
+ }
92
+ return {
93
+ ok: true,
94
+ version: version.stdout.trim(),
95
+ inspect: inspect.json,
96
+ gitStatus: gitStatus.stdout,
97
+ commands,
98
+ };
99
+ }
100
+ async function exists(filePath) {
101
+ try {
102
+ await access(filePath);
103
+ return true;
104
+ }
105
+ catch {
106
+ return false;
107
+ }
108
+ }
@@ -0,0 +1,76 @@
1
+ const MINIMAL_TYPES = new Set([
2
+ "doc-update",
3
+ "qa-analysis",
4
+ "qa-casegen",
5
+ "qa-execute",
6
+ ]);
7
+ const SENSITIVE_PATH_MARKERS = [
8
+ "auth",
9
+ "payment",
10
+ "database",
11
+ "migration",
12
+ "migrations",
13
+ "infra",
14
+ "secret",
15
+ "public-contract",
16
+ "public_contract",
17
+ ];
18
+ export function resolveLoopAgentProfile(taskSpec) {
19
+ if (taskSpec.risk_level === "high") {
20
+ return mapping(taskSpec, "supervised", "risk-high", [
21
+ "risk_level=high requires supervised governance",
22
+ ]);
23
+ }
24
+ if (MINIMAL_TYPES.has(taskSpec.type)) {
25
+ return mapping(taskSpec, "minimal", `${taskSpec.type}-minimal`, [
26
+ `type=${taskSpec.type}`,
27
+ ]);
28
+ }
29
+ const sensitiveMarkers = collectSensitiveMarkers(taskSpec);
30
+ if (sensitiveMarkers.length > 0) {
31
+ return mapping(taskSpec, "reviewed", "sensitive-path", [
32
+ `sensitive path markers: ${sensitiveMarkers.join(", ")}`,
33
+ ]);
34
+ }
35
+ if (taskSpec.type === "backend-feature" ||
36
+ taskSpec.type === "frontend-feature") {
37
+ if (taskSpec.risk_level === "medium") {
38
+ return mapping(taskSpec, "reviewed", `${taskSpec.type}-medium`, [
39
+ `type=${taskSpec.type}`,
40
+ "risk_level=medium",
41
+ ]);
42
+ }
43
+ return mapping(taskSpec, "standard", `${taskSpec.type}-low`, [
44
+ `type=${taskSpec.type}`,
45
+ "risk_level=low",
46
+ ]);
47
+ }
48
+ if (taskSpec.type === "qa-testcode") {
49
+ return mapping(taskSpec, "standard", "qa-testcode-standard", [
50
+ "type=qa-testcode",
51
+ ]);
52
+ }
53
+ if (taskSpec.type === "ci-fix" || taskSpec.type === "reviewer-gate") {
54
+ return mapping(taskSpec, "reviewed", `${taskSpec.type}-reviewed`, [
55
+ `type=${taskSpec.type}`,
56
+ ]);
57
+ }
58
+ return mapping(taskSpec, "reviewed", "default-reviewed", [
59
+ `type=${taskSpec.type}`,
60
+ "default governance profile",
61
+ ]);
62
+ }
63
+ function mapping(taskSpec, loopAgentProfile, ruleId, reasons) {
64
+ return {
65
+ taskId: taskSpec.id,
66
+ businessProfile: taskSpec.type,
67
+ riskLevel: taskSpec.risk_level,
68
+ loopAgentProfile,
69
+ ruleId,
70
+ reasons,
71
+ };
72
+ }
73
+ function collectSensitiveMarkers(taskSpec) {
74
+ const haystack = taskSpec.constraints.allowed_paths.map((value) => value.toLowerCase());
75
+ return SENSITIVE_PATH_MARKERS.filter((marker) => haystack.some((value) => value.includes(marker)));
76
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Human-readable progress reporting for long-running worker commands.
3
+ *
4
+ * Why this exists: agent-worker batch runs can take many minutes per task with
5
+ * zero stdout/stderr feedback, which looks indistinguishable from a hang. The
6
+ * system writes rich state to disk (state.json, run.json, .task-pool artifacts)
7
+ * but never echoes it to the triggering terminal, so users assume it is dead.
8
+ *
9
+ * Design:
10
+ * - Progress goes to STDERR only. STDOUT stays reserved for the final
11
+ * machine-readable JSON payload, so `... | jq` and pipes keep working.
12
+ * - Default ON. `--quiet` disables it. Non-interactive use that only wants JSON
13
+ * can pass `--quiet` (or redirect stderr).
14
+ * - TTY-aware for ANSI only: if stderr is not a TTY, we still print the lines
15
+ * (they are line-buffered text and flush fine) but skip ANSI decoration.
16
+ * - A `noop` reporter keeps library callers (and tests that don't care) quiet
17
+ * with zero changes to their call sites beyond accepting the option.
18
+ */
19
+ const ANSI = {
20
+ bold: "\x1b[1m",
21
+ green: "\x1b[32m",
22
+ red: "\x1b[31m",
23
+ yellow: "\x1b[33m",
24
+ cyan: "\x1b[36m",
25
+ dim: "\x1b[2m",
26
+ reset: "\x1b[0m",
27
+ };
28
+ function stamp(now) {
29
+ const iso = now.toISOString();
30
+ // YYYY-MM-DDTHH:MM:SS — drop millis and trailing Z noise for compact lines.
31
+ return `[${iso.slice(0, 19)}]`;
32
+ }
33
+ export function createProgressReporter(options = {}) {
34
+ if (options.quiet)
35
+ return noopProgressReporter;
36
+ const sink = options.sink ?? ((chunk) => process.stderr.write(chunk));
37
+ const isTty = options.isTty ?? process.stderr.isTTY === true;
38
+ const now = options.now ?? (() => new Date());
39
+ function decorate(prefix, message, color) {
40
+ return isTty ? `${color}${prefix}${ANSI.reset} ${message}` : `${prefix} ${message}`;
41
+ }
42
+ return {
43
+ batch(message) {
44
+ sink(`${stamp(now())} ${decorate("==", message, ANSI.bold + ANSI.cyan)}\n`);
45
+ },
46
+ task(message) {
47
+ sink(`${stamp(now())} ${decorate("▶", message, ANSI.bold)}\n`);
48
+ },
49
+ step(message) {
50
+ const body = isTty ? `${ANSI.dim}${message}${ANSI.reset}` : message;
51
+ sink(`${stamp(now())} ${body}\n`);
52
+ },
53
+ note(message) {
54
+ sink(`${stamp(now())} ${decorate("•", message, ANSI.dim)}\n`);
55
+ },
56
+ };
57
+ }
58
+ export const noopProgressReporter = {
59
+ batch() { },
60
+ task() { },
61
+ step() { },
62
+ note() { },
63
+ };
64
+ /** Format a millisecond duration as a compact human string: "6m 35s", "1h 2m 3s", "450ms". */
65
+ export function formatDuration(ms) {
66
+ if (!Number.isFinite(ms) || ms < 0)
67
+ return "?";
68
+ if (ms < 1000)
69
+ return `${Math.round(ms)}ms`;
70
+ const totalSeconds = Math.round(ms / 1000);
71
+ const hours = Math.floor(totalSeconds / 3600);
72
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
73
+ const seconds = totalSeconds % 60;
74
+ const parts = [];
75
+ if (hours > 0)
76
+ parts.push(`${hours}h`);
77
+ if (minutes > 0 || hours > 0)
78
+ parts.push(`${minutes}m`);
79
+ parts.push(`${seconds}s`);
80
+ return parts.join(" ");
81
+ }
@@ -0,0 +1,69 @@
1
+ import { access, writeFile, mkdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { getRunsJsonlPath, readAllTaskPoolStates, readJsonlFile, } from "../pool/run-store.js";
4
+ export async function renderMorningReport(options) {
5
+ const runs = (await readJsonlFile(getRunsJsonlPath(options.repoRoot))).filter((run) => !options.batchRunId || run.batchRunId === options.batchRunId);
6
+ const total = runs.length;
7
+ const succeeded = runs.filter((run) => run.status === "succeeded").length;
8
+ const failed = runs.filter((run) => run.status === "failed").length;
9
+ const followUps = runs.filter((run) => run.failure).length;
10
+ const states = await readAllTaskPoolStates(options.repoRoot);
11
+ const blocked = Object.values(states).filter((state) => state.status === "Blocked").length;
12
+ const lines = [
13
+ "# Nightly Worker Report",
14
+ "",
15
+ "## Summary",
16
+ "",
17
+ `- Total: ${total}`,
18
+ `- Succeeded: ${succeeded}`,
19
+ `- Failed: ${failed}`,
20
+ `- Blocked: ${blocked}`,
21
+ `- Follow-ups: ${followUps}`,
22
+ "",
23
+ "## Results",
24
+ "",
25
+ "| Task | Status | Run | Failure | Next | Artifacts |",
26
+ "|---|---|---|---|---|---|",
27
+ ];
28
+ for (const run of runs) {
29
+ lines.push(`| ${run.taskId} | ${run.status} | ${run.workerRunId} | ${run.failure?.category ?? "-"} | ${run.failure?.derivedFollowUpTaskId ?? "-"} | ${await artifactSummary(run)} |`);
30
+ }
31
+ if (followUps > 0) {
32
+ lines.push("", "## Human Actions", "");
33
+ for (const run of runs.filter((candidate) => candidate.failure)) {
34
+ lines.push(`- ${run.failure?.derivedFollowUpTaskId}: ${run.failure?.recommendedFollowUpKind} for ${run.taskId}`);
35
+ }
36
+ }
37
+ return `${lines.join("\n")}\n`;
38
+ }
39
+ export async function writeMorningReport(input) {
40
+ const markdown = await renderMorningReport(input);
41
+ await mkdir(path.dirname(input.outputPath), { recursive: true });
42
+ await writeFile(input.outputPath, markdown, "utf-8");
43
+ return input.outputPath;
44
+ }
45
+ async function artifactSummary(run) {
46
+ const artifacts = [
47
+ ["dag", run.dagPath],
48
+ ["record", run.runRecordPath],
49
+ ["report", run.failureArtifacts?.reportMarkdownArtifactPath],
50
+ ["doctor", run.failureArtifacts?.doctorMarkdownArtifactPath],
51
+ ["closeout", run.failureArtifacts?.closeoutDraftPath],
52
+ ].filter((entry) => Boolean(entry[1]));
53
+ if (artifacts.length === 0)
54
+ return "-";
55
+ const parts = [];
56
+ for (const [label, artifactPath] of artifacts) {
57
+ parts.push(`${label}:${(await exists(artifactPath)) ? artifactPath : `missing artifact ${artifactPath}`}`);
58
+ }
59
+ return parts.join("<br>");
60
+ }
61
+ async function exists(filePath) {
62
+ try {
63
+ await access(filePath);
64
+ return true;
65
+ }
66
+ catch {
67
+ return false;
68
+ }
69
+ }
@@ -0,0 +1,23 @@
1
+ import path from "node:path";
2
+ export function resolveRepoPath(config, pathRef) {
3
+ const entry = config.repos?.[pathRef];
4
+ if (!entry) {
5
+ return {
6
+ ok: false,
7
+ code: "repo-path-ref-missing",
8
+ message: `repo path_ref is not configured: ${pathRef}`,
9
+ };
10
+ }
11
+ if (typeof entry.path !== "string" || entry.path.trim().length === 0) {
12
+ return {
13
+ ok: false,
14
+ code: "repo-path-invalid",
15
+ message: `repo path_ref has invalid path: ${pathRef}`,
16
+ };
17
+ }
18
+ return {
19
+ ok: true,
20
+ repoRoot: path.resolve(entry.path),
21
+ defaultBranch: typeof entry.default_branch === "string" ? entry.default_branch : undefined,
22
+ };
23
+ }