@tea-agent/loop-agent 0.2.1 → 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 (135) hide show
  1. package/AGENTS.md +91 -87
  2. package/CHANGELOG.md +89 -52
  3. package/README.md +195 -180
  4. package/bin/agent-worker.js +22 -0
  5. package/bin/loop-agent.js +21 -21
  6. package/dist/application/dag/args.js +6 -0
  7. package/dist/application/dag/generate-task-dag.js +2 -0
  8. package/dist/application/dag/run-dag.js +3 -0
  9. package/dist/application/dag/validate-dag.js +40 -0
  10. package/dist/cli/command-definitions.js +2 -2
  11. package/dist/cli/program.js +24 -4
  12. package/dist/commands/init.js +1011 -459
  13. package/dist/commands/loop-benchmark.js +11 -11
  14. package/dist/commands/pi-reuse-benchmark.js +16 -16
  15. package/dist/executors/cursor-executor.js +1 -1
  16. package/dist/executors/dag-pi-executor.js +8 -1
  17. package/dist/task/runtime.js +27 -27
  18. package/dist/worker/cli.js +119 -0
  19. package/dist/worker/loop-agent/command-result.js +1 -0
  20. package/dist/worker/loop-agent/loop-agent-client.js +105 -0
  21. package/dist/worker/loop-agent/parse-json.js +14 -0
  22. package/dist/worker/materialize/harness-task-materializer.js +157 -0
  23. package/dist/worker/pool/failure-routing.js +98 -0
  24. package/dist/worker/pool/run-store.js +117 -0
  25. package/dist/worker/pool/types.js +1 -0
  26. package/dist/worker/preflight.js +108 -0
  27. package/dist/worker/profile-mapping.js +76 -0
  28. package/dist/worker/progress-reporter.js +81 -0
  29. package/dist/worker/report/morning-report.js +69 -0
  30. package/dist/worker/repos/repo-resolver.js +23 -0
  31. package/dist/worker/run-task/run-task.js +359 -0
  32. package/dist/worker/runner/run-ready.js +216 -0
  33. package/dist/worker/task-graph/acceptance-schema.js +25 -0
  34. package/dist/worker/task-graph/ready-queue.js +23 -0
  35. package/dist/worker/task-graph/task-graph-schema.js +28 -0
  36. package/dist/worker/task-graph/types.js +1 -0
  37. package/dist/worker/task-graph/validate.js +188 -0
  38. package/dist/worker/task-spec/complexity-mapping.js +8 -0
  39. package/dist/worker/task-spec/schema.js +116 -0
  40. package/dist/worker/task-spec/types.js +1 -0
  41. package/dist/worker/task-spec/validate.js +352 -0
  42. package/dist/workflows/dag/canvas-observer.js +275 -275
  43. package/dist/workflows/dag/dynamic-runtime/loop-until.js +2 -1
  44. package/dist/workflows/dag/dynamic-runtime/map.js +1 -0
  45. package/dist/workflows/dag/init-hybrid.js +3 -3
  46. package/dist/workflows/dag/skills.js +3 -3
  47. package/dist/workflows/dag/types.js +2 -0
  48. package/dist/workflows/dynamic/compile.js +11 -0
  49. package/dist/workflows/dynamic/spec.js +1 -0
  50. package/docs/README.md +72 -65
  51. package/docs/agent-dag-recovery-playbook.md +184 -184
  52. package/docs/agent-dag-runner.md +42 -40
  53. package/docs/architecture/runtime-boundaries.md +147 -147
  54. package/docs/cursor-executor-usage.md +25 -25
  55. package/docs/decisions/README.md +3 -3
  56. package/docs/design/README.md +36 -36
  57. package/docs/development-principles.md +73 -71
  58. package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -1749
  59. package/docs/exec-plans/README.md +6 -6
  60. package/docs/exec-plans/active/README.md +7 -10
  61. package/docs/exec-plans/completed/README.md +19 -9
  62. package/docs/feature-workflow.md +186 -186
  63. package/docs/harness-methodology-debugging.md +153 -153
  64. package/docs/harness-methodology-tdd.md +130 -130
  65. package/docs/harness-methodology-verification.md +27 -27
  66. package/docs/init-surface.manifest.json +175 -0
  67. package/docs/loop-agent-harness.md +42 -42
  68. package/docs/production-readiness.md +96 -96
  69. package/docs/progress/README.md +3 -3
  70. package/docs/reports/README.md +5 -5
  71. package/docs/skills/README.md +6 -0
  72. package/docs/skills/vetted-skill-registry.md +26 -0
  73. package/docs/templates/adr.md +60 -60
  74. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  75. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  76. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
  77. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  78. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  79. package/docs/templates/agent-dag-report.schema.json +454 -454
  80. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  81. package/docs/templates/agent-dag.base.json +195 -195
  82. package/docs/templates/agent-dag.final-verification.json +190 -190
  83. package/docs/templates/agent-dag.schema.json +316 -316
  84. package/docs/templates/agent-dag.supervised-implementation.json +500 -500
  85. package/docs/templates/exec-plan.md +64 -64
  86. package/docs/templates/feature-spec.md +53 -53
  87. package/docs/templates/hybrid-dag.json +193 -193
  88. package/docs/templates/init-evolution-review.md +33 -0
  89. package/docs/templates/production-readiness-checklist.md +57 -57
  90. package/docs/templates/progress-log.md +17 -17
  91. package/docs/templates/project-start-checklist.md +9 -9
  92. package/docs/templates/qa-report.md +48 -48
  93. package/docs/templates/sprint-contract.md +29 -29
  94. package/docs/verification-matrix.md +41 -41
  95. package/examples/decision-gate-agent-dag.json +123 -123
  96. package/examples/example-dag.json +51 -51
  97. package/examples/hybrid-loop-agent-dag.json +194 -194
  98. package/harness.json +94 -92
  99. package/package.json +66 -62
  100. package/skills/ai-engineering-context/SKILL.md +48 -48
  101. package/skills/code-review-core/SKILL.md +20 -0
  102. package/skills/codebase-scout/SKILL.md +19 -0
  103. package/skills/init-capability-evolution/SKILL.md +69 -0
  104. package/skills/loop-agent/SKILL.md +147 -145
  105. package/skills/loop-agent/references/README.md +67 -67
  106. package/skills/loop-agent/references/command-reference.md +403 -357
  107. package/skills/loop-agent/references/harness-policy.md +259 -258
  108. package/skills/loop-agent/references/hybrid-dag.md +216 -216
  109. package/skills/loop-agent/references/learned/README.md +21 -21
  110. package/skills/loop-agent/references/long-running-loop.md +59 -59
  111. package/skills/loop-agent/references/model-routing.md +36 -36
  112. package/skills/loop-agent/references/multi-worktree.md +54 -54
  113. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  114. package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
  115. package/skills/loop-agent/references/pi-prompt.md +23 -23
  116. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +81 -81
  117. package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
  118. package/skills/loop-agent/references/task-workflow.md +84 -84
  119. package/skills/loop-agent/references/verification-and-failure-handling.md +128 -128
  120. package/skills/requesting-code-review/SKILL.md +101 -101
  121. package/skills/requesting-code-review/code-reviewer.md +168 -168
  122. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  123. package/skills/systematic-debugging/SKILL.md +296 -296
  124. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  125. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  126. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  127. package/skills/systematic-debugging/find-polluter.sh +63 -63
  128. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  129. package/skills/systematic-debugging/test-academic.md +14 -14
  130. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  131. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  132. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  133. package/skills/test-driven-development/SKILL.md +20 -0
  134. package/skills/verification-before-completion/SKILL.md +154 -154
  135. package/skills/webapp-testing/SKILL.md +19 -0
@@ -0,0 +1,359 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { formatDuration, noopProgressReporter } from "../progress-reporter.js";
5
+ import { materializeTaskSpec, } from "../materialize/harness-task-materializer.js";
6
+ import { preflightTargetRepo } from "../preflight.js";
7
+ export const DEFAULT_RUN_DAG_TIMEOUT_MS = 1_800_000;
8
+ export const MAX_WORKER_TIMEOUT_MS = 7_200_000;
9
+ export async function runTaskSpec(options) {
10
+ const now = options.now ?? new Date();
11
+ const workerRunId = options.workerRunId ?? buildWorkerRunId(options.taskSpec.id, now);
12
+ const commands = [];
13
+ const client = new RecordingRunTaskClient(options.client, commands);
14
+ const progress = options.progress ?? noopProgressReporter;
15
+ if (options.preflight !== false) {
16
+ progress.step("preflight: version + inspect + docs-audit + git-status" + (options.preflight && typeof options.preflight === "object" && options.preflight.runCheckRepo ? " + check-repo" : ""));
17
+ const preflightStartedAt = Date.now();
18
+ const preflight = await preflightTargetRepo({
19
+ repoRoot: options.repoRoot,
20
+ client,
21
+ ...(typeof options.preflight === "object" ? options.preflight : {}),
22
+ });
23
+ if (!preflight.ok) {
24
+ progress.note(`preflight FAILED: ${preflight.code}`);
25
+ throw new Error(`target repo preflight failed: ${preflight.code}: ${preflight.message}`);
26
+ }
27
+ progress.step(`preflight ok in ${formatDuration(Date.now() - preflightStartedAt)}`);
28
+ }
29
+ progress.step("materialize harness task");
30
+ const materializeStartedAt = Date.now();
31
+ const materializeManifest = await materializeTaskSpec({
32
+ repoRoot: options.repoRoot,
33
+ taskSpec: options.taskSpec,
34
+ taskSpecPath: options.taskSpecPath,
35
+ client,
36
+ now,
37
+ });
38
+ progress.step(`materialized ${materializeManifest.harnessTaskId} in ${formatDuration(Date.now() - materializeStartedAt)}`);
39
+ const taskArtifactsDir = path.join(options.repoRoot, ".harness", "tasks", materializeManifest.harnessTaskId, "artifacts");
40
+ await mkdir(taskArtifactsDir, { recursive: true });
41
+ const dagPath = path.join(taskArtifactsDir, `${workerRunId}-dag.json`);
42
+ const runRecordPath = path.join(taskArtifactsDir, "worker-run-record.json");
43
+ progress.step("dag run-task: generate DAG");
44
+ const dagGenStartedAt = Date.now();
45
+ await runRequiredCommand(options.repoRoot, client, "dag-run-task", [
46
+ "dag",
47
+ "run-task",
48
+ materializeManifest.harnessTaskId,
49
+ "--profile",
50
+ materializeManifest.loopAgentProfile,
51
+ "--strict-models",
52
+ "--output",
53
+ dagPath,
54
+ "--cwd",
55
+ options.repoRoot,
56
+ ...noCursorArgs(options.taskSpec),
57
+ ]);
58
+ progress.step(`dag generated in ${formatDuration(Date.now() - dagGenStartedAt)}`);
59
+ if (options.piModel) {
60
+ progress.step(`pi-model override: rewriting executorModels.pi → ${options.piModel}`);
61
+ await applyPiModelOverride(dagPath, options.piModel);
62
+ }
63
+ progress.step("dag validate");
64
+ const validateStartedAt = Date.now();
65
+ await runRequiredCommand(options.repoRoot, client, "dag-validate", [
66
+ "dag",
67
+ "validate",
68
+ "--dag",
69
+ dagPath,
70
+ ...strictModelsArgs(options.piModel),
71
+ "--strict-governance",
72
+ "--spine-task",
73
+ materializeManifest.harnessTaskId,
74
+ ...forbidCursorArgs(options.taskSpec),
75
+ ]);
76
+ progress.step(`dag validated in ${formatDuration(Date.now() - validateStartedAt)}`);
77
+ progress.step("run-dag: executing DAG nodes (this is the long step)");
78
+ const runDagStartedAt = Date.now();
79
+ await runRequiredCommand(options.repoRoot, client, "run-dag", [
80
+ "run-dag",
81
+ "--dag",
82
+ dagPath,
83
+ "--cwd",
84
+ options.repoRoot,
85
+ "--run-id",
86
+ workerRunId,
87
+ ...maxConcurrentArgs(options.taskSpec),
88
+ ...noCursorArgs(options.taskSpec),
89
+ ], true, resolveRunDagTimeoutMs(options.taskSpec));
90
+ progress.step(`run-dag finished in ${formatDuration(Date.now() - runDagStartedAt)}`);
91
+ const reportJson = await client.run([
92
+ "dag",
93
+ "report",
94
+ "--run-id",
95
+ workerRunId,
96
+ "--lifecycle",
97
+ "all",
98
+ "--json",
99
+ ], {
100
+ cwd: options.repoRoot,
101
+ artifactName: "dag-report-json",
102
+ expectJson: true,
103
+ });
104
+ const reportDecision = decideFromReport(workerRunId, reportJson);
105
+ await client.run([
106
+ "dag",
107
+ "report",
108
+ "--run-id",
109
+ workerRunId,
110
+ "--lifecycle",
111
+ "all",
112
+ "--markdown",
113
+ ], {
114
+ cwd: options.repoRoot,
115
+ artifactName: "dag-report-markdown",
116
+ });
117
+ const status = reportDecision.succeeded ? "succeeded" : "failed";
118
+ progress.step(`report decision: ${status} (${reportDecision.reason}${reportDecision.runStatus ? `, status=${reportDecision.runStatus}` : ""})`);
119
+ const failureArtifacts = reportDecision.succeeded
120
+ ? undefined
121
+ : await collectFailureArtifacts({
122
+ client,
123
+ repoRoot: options.repoRoot,
124
+ workerRunId,
125
+ taskArtifactsDir,
126
+ });
127
+ if (reportDecision.succeeded) {
128
+ progress.step("promote + closeout");
129
+ await runRequiredCommand(options.repoRoot, client, "promote-run", ["promote-run", materializeManifest.harnessTaskId, "--run-id", workerRunId], true);
130
+ await runRequiredCommand(options.repoRoot, client, "closeout-task", ["closeout", "task", materializeManifest.harnessTaskId], true);
131
+ }
132
+ const record = {
133
+ schemaVersion: 1,
134
+ status,
135
+ workerRunId,
136
+ businessId: options.taskSpec.id,
137
+ harnessTaskId: materializeManifest.harnessTaskId,
138
+ featureId: options.taskSpec.feature_id,
139
+ loopAgentProfile: materializeManifest.loopAgentProfile,
140
+ dagPath,
141
+ runRecordPath,
142
+ materializeManifest,
143
+ reportDecision,
144
+ commands,
145
+ ...(failureArtifacts ? { failureArtifacts } : {}),
146
+ };
147
+ await writeFile(runRecordPath, `${JSON.stringify(record, null, 2)}\n`, "utf-8");
148
+ return {
149
+ status,
150
+ workerRunId,
151
+ businessId: options.taskSpec.id,
152
+ harnessTaskId: materializeManifest.harnessTaskId,
153
+ runRecordPath,
154
+ dagPath,
155
+ reportDecision,
156
+ ...(failureArtifacts ? { failureArtifacts } : {}),
157
+ };
158
+ }
159
+ export function buildWorkerRunId(businessId, now) {
160
+ const date = now.toISOString().slice(0, 10).replace(/-/g, "");
161
+ const hash = createHash("sha256")
162
+ .update(`${businessId}:${now.toISOString()}`)
163
+ .digest("hex")
164
+ .slice(0, 6);
165
+ return `wr-${date}-${businessId}-${hash}`;
166
+ }
167
+ async function runRequiredCommand(repoRoot, client, artifactName, args, expectJson = true, timeoutMs) {
168
+ const result = await client.run(args, {
169
+ cwd: repoRoot,
170
+ artifactName,
171
+ expectJson,
172
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
173
+ });
174
+ if (!result.ok) {
175
+ throw new Error(`loop-agent command failed: ${args.join(" ")}`);
176
+ }
177
+ return result;
178
+ }
179
+ function resolveRunDagTimeoutMs(taskSpec) {
180
+ return Math.min(taskSpec.worker.timeout_ms ?? DEFAULT_RUN_DAG_TIMEOUT_MS, MAX_WORKER_TIMEOUT_MS);
181
+ }
182
+ async function collectFailureArtifacts(input) {
183
+ const doctor = await input.client.run(["dag", "doctor", "--run-id", input.workerRunId, "--markdown"], {
184
+ cwd: input.repoRoot,
185
+ artifactName: "dag-doctor-markdown",
186
+ });
187
+ const closeoutDraftPath = path.join(input.repoRoot, ".task-pool", "failure-handoffs", `${input.workerRunId}-failure-closeout-draft.md`);
188
+ await input.client.run([
189
+ "dag",
190
+ "closeout-draft",
191
+ "--run-id",
192
+ input.workerRunId,
193
+ "--output",
194
+ closeoutDraftPath,
195
+ ], {
196
+ cwd: input.repoRoot,
197
+ artifactName: "dag-closeout-draft",
198
+ });
199
+ const reportMarkdown = input.client instanceof RecordingRunTaskClient
200
+ ? input.client.findArtifact("dag-report-markdown")
201
+ : undefined;
202
+ return {
203
+ reportMarkdownArtifactPath: reportMarkdown?.result.artifacts.stdoutPath ?? "",
204
+ doctorMarkdownArtifactPath: doctor.artifacts.stdoutPath,
205
+ closeoutDraftPath,
206
+ };
207
+ }
208
+ function decideFromReport(workerRunId, result) {
209
+ if (!result.ok || !result.json) {
210
+ return {
211
+ succeeded: false,
212
+ reason: "report-json-unavailable",
213
+ };
214
+ }
215
+ const runs = readObjectArray(result.json, "runs");
216
+ const run = runs.find((candidate) => readString(candidate, "runId") === workerRunId) ??
217
+ runs[0];
218
+ if (!run) {
219
+ return {
220
+ succeeded: false,
221
+ reason: "report-run-missing",
222
+ };
223
+ }
224
+ const runStatus = readString(run, "status");
225
+ const nodes = readObjectArray(run, "nodes");
226
+ const failedNode = nodes.find((node) => {
227
+ const status = readString(node, "status");
228
+ const failureCategory = readString(node, "failureCategory");
229
+ return (status === "ERROR" ||
230
+ status === "SKIPPED" ||
231
+ (Boolean(failureCategory) && failureCategory !== "success"));
232
+ });
233
+ if (runStatus !== "completed" && runStatus !== "finished") {
234
+ return {
235
+ succeeded: false,
236
+ reason: "run-not-completed",
237
+ runStatus,
238
+ primaryFailure: readProperty(run, "primaryFailure"),
239
+ };
240
+ }
241
+ if (failedNode) {
242
+ return {
243
+ succeeded: false,
244
+ reason: "node-failure",
245
+ runStatus,
246
+ primaryFailure: readProperty(run, "primaryFailure") ?? failedNode,
247
+ };
248
+ }
249
+ return {
250
+ succeeded: true,
251
+ reason: "report-completed",
252
+ runStatus,
253
+ primaryFailure: readProperty(run, "primaryFailure"),
254
+ };
255
+ }
256
+ class RecordingRunTaskClient {
257
+ delegate;
258
+ records;
259
+ constructor(delegate, records) {
260
+ this.delegate = delegate;
261
+ this.records = records;
262
+ }
263
+ async run(args, options) {
264
+ const result = await this.delegate.run(args, options);
265
+ this.records.push({
266
+ name: commandRecordName(args, options.artifactName),
267
+ artifactName: options.artifactName,
268
+ args,
269
+ result: {
270
+ ok: result.ok,
271
+ exitCode: result.exitCode,
272
+ durationMs: result.durationMs,
273
+ timedOut: result.timedOut,
274
+ artifacts: result.artifacts,
275
+ ...(result.parseFailure ? { parseFailure: result.parseFailure } : {}),
276
+ },
277
+ });
278
+ return result;
279
+ }
280
+ async runExternal(command, args, options) {
281
+ const result = await this.delegate.runExternal(command, args, options);
282
+ this.records.push({
283
+ name: options.artifactName,
284
+ artifactName: options.artifactName,
285
+ args: [command, ...args],
286
+ result: {
287
+ ok: result.ok,
288
+ exitCode: result.exitCode,
289
+ durationMs: result.durationMs,
290
+ timedOut: result.timedOut,
291
+ artifacts: result.artifacts,
292
+ ...(result.parseFailure ? { parseFailure: result.parseFailure } : {}),
293
+ },
294
+ });
295
+ return result;
296
+ }
297
+ findArtifact(artifactName) {
298
+ return this.records.find((record) => record.artifactName === artifactName);
299
+ }
300
+ }
301
+ function commandRecordName(args, artifactName) {
302
+ if (args[0] === "new-task")
303
+ return "new-task";
304
+ return artifactName;
305
+ }
306
+ function noCursorArgs(taskSpec) {
307
+ return taskSpec.loop_agent.no_cursor ? ["--no-cursor"] : [];
308
+ }
309
+ function forbidCursorArgs(taskSpec) {
310
+ return taskSpec.loop_agent.no_cursor ? ["--forbid-executor", "cursor"] : [];
311
+ }
312
+ function maxConcurrentArgs(taskSpec) {
313
+ return ["--max-concurrent", String(taskSpec.loop_agent.max_concurrent)];
314
+ }
315
+ /**
316
+ * When a smoke `piModel` override is active, drop `--strict-models` so the
317
+ * non-canonical `executorModels.pi` rewrite passes `dag validate`. Governance,
318
+ * forbidden-executor and spine audits are still enforced by their own flags.
319
+ */
320
+ function strictModelsArgs(piModel) {
321
+ return piModel ? [] : ["--strict-models"];
322
+ }
323
+ /**
324
+ * Rewrite `executorModels.pi.{LOW,MED,HIGH}` in an already-generated DAG JSON
325
+ * so every pi executor node resolves to the smoke override model. Cursor / shell
326
+ * slots are left untouched.
327
+ */
328
+ export async function applyPiModelOverride(dagPath, piModel) {
329
+ const raw = await readFile(dagPath, "utf-8");
330
+ const spec = JSON.parse(raw);
331
+ spec.executorModels = {
332
+ ...(spec.executorModels?.cursor ? { cursor: spec.executorModels.cursor } : {}),
333
+ pi: {
334
+ LOW: piModel,
335
+ MED: piModel,
336
+ HIGH: piModel,
337
+ },
338
+ };
339
+ await writeFile(dagPath, `${JSON.stringify(spec, null, 2)}\n`, "utf-8");
340
+ }
341
+ function readObjectArray(value, key) {
342
+ if (!value || typeof value !== "object")
343
+ return [];
344
+ const child = value[key];
345
+ if (!Array.isArray(child))
346
+ return [];
347
+ return child.filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item));
348
+ }
349
+ function readString(value, key) {
350
+ if (!value || typeof value !== "object")
351
+ return undefined;
352
+ const child = value[key];
353
+ return typeof child === "string" ? child : undefined;
354
+ }
355
+ function readProperty(value, key) {
356
+ if (!value || typeof value !== "object")
357
+ return undefined;
358
+ return value[key];
359
+ }
@@ -0,0 +1,216 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import YAML from "yaml";
5
+ import { deriveFailureRoute } from "../pool/failure-routing.js";
6
+ import { findRunByWorkerRunId, getTaskPoolRoot, readAllTaskPoolStates, recordTaskPoolRun, writeTaskPoolState, } from "../pool/run-store.js";
7
+ import { runTaskSpec, } from "../run-task/run-task.js";
8
+ import { formatDuration, noopProgressReporter } from "../progress-reporter.js";
9
+ import { computeReadyQueue } from "../task-graph/ready-queue.js";
10
+ import { taskGraphSpecSchema } from "../task-graph/task-graph-schema.js";
11
+ import { taskSpecSchema } from "../task-spec/schema.js";
12
+ export async function runReadyTasks(options) {
13
+ const now = options.now ?? new Date();
14
+ const startedAt = now.toISOString();
15
+ const batchRunId = options.batchRunId ?? buildBatchRunId(now);
16
+ const graph = await loadTaskGraph(options.featureDir);
17
+ const states = await readAllTaskPoolStates(options.repoRoot);
18
+ const readyTaskIds = computeReadyQueue(graph, toGraphState(states)).filter((taskId) => isPoolReady(states[taskId]));
19
+ const limitedTaskIds = readyTaskIds.slice(0, options.limit ?? readyTaskIds.length);
20
+ const tasks = [];
21
+ const runner = options.runTask ?? runTaskSpec;
22
+ const progress = options.progress ?? noopProgressReporter;
23
+ const total = limitedTaskIds.length;
24
+ let index = 0;
25
+ const batchStartedAt = Date.now();
26
+ progress.batch(`batch ${batchRunId}: feature=${graph.feature_id}, ${total} task${total === 1 ? "" : "s"} ready → starting`);
27
+ for (const taskId of limitedTaskIds) {
28
+ index += 1;
29
+ const node = graph.nodes.find((candidate) => candidate.id === taskId);
30
+ const taskSpecPath = path.join(options.featureDir, "tasks", node?.task ?? `${taskId}.yaml`);
31
+ const taskSpec = await loadTaskSpec(taskSpecPath);
32
+ const workerRunId = options.workerRunIdForTask?.(taskId, taskSpec) ??
33
+ buildStableWorkerRunId(taskId, taskSpec, now);
34
+ if (workerRunId) {
35
+ const existing = await findRunByWorkerRunId(options.repoRoot, workerRunId);
36
+ if (existing) {
37
+ progress.task(`task ${index}/${total} ${taskId}: reuse existing run ${workerRunId}`);
38
+ tasks.push({
39
+ taskId,
40
+ workerRunId,
41
+ status: "reused",
42
+ runRecordPath: existing.runRecordPath,
43
+ });
44
+ continue;
45
+ }
46
+ }
47
+ const taskStartedAt = Date.now();
48
+ progress.task(`task ${index}/${total} ${taskId} "${taskSpec.title}" (${workerRunId})`);
49
+ progress.step(`preflight → materialize → dag-run-task → validate → run-dag → report`);
50
+ let result;
51
+ try {
52
+ await writeTaskPoolState(options.repoRoot, {
53
+ taskId,
54
+ status: "Running",
55
+ updatedAt: new Date().toISOString(),
56
+ workerRunId,
57
+ });
58
+ result = await runner({
59
+ repoRoot: options.repoRoot,
60
+ taskSpec,
61
+ taskSpecPath,
62
+ client: options.client,
63
+ now,
64
+ workerRunId,
65
+ preflight: {
66
+ runCheckRepo: options.runCheckRepo,
67
+ ...(options.checkRepoCommand ? { checkRepoCommand: options.checkRepoCommand } : {}),
68
+ },
69
+ progress,
70
+ ...(options.piModel ? { piModel: options.piModel } : {}),
71
+ });
72
+ }
73
+ catch (error) {
74
+ const message = errorMessage(error);
75
+ progress.note(`task ${taskId}: ERROR ${message}`);
76
+ try {
77
+ await recordTaskPoolRun({
78
+ repoRoot: options.repoRoot,
79
+ run: {
80
+ schemaVersion: 1,
81
+ batchRunId,
82
+ workerRunId,
83
+ taskId,
84
+ featureId: taskSpec.feature_id,
85
+ status: "run-error",
86
+ recordedAt: new Date().toISOString(),
87
+ error: message,
88
+ },
89
+ });
90
+ }
91
+ catch (recordError) {
92
+ tasks.push({
93
+ taskId,
94
+ workerRunId,
95
+ status: "record-error",
96
+ error: errorMessage(recordError),
97
+ });
98
+ continue;
99
+ }
100
+ tasks.push({
101
+ taskId,
102
+ workerRunId,
103
+ status: "run-error",
104
+ error: message,
105
+ });
106
+ continue;
107
+ }
108
+ const failure = deriveFailureRoute(result);
109
+ if (result.status === "succeeded") {
110
+ progress.step(`✓ ${taskId} succeeded in ${formatDuration(Date.now() - taskStartedAt)} (promoted + closed out)`);
111
+ }
112
+ else {
113
+ progress.step(`✗ ${taskId} failed in ${formatDuration(Date.now() - taskStartedAt)} — see failure artifacts`);
114
+ }
115
+ const run = {
116
+ schemaVersion: 1,
117
+ batchRunId,
118
+ workerRunId: result.workerRunId,
119
+ taskId: result.businessId,
120
+ featureId: taskSpec.feature_id,
121
+ status: result.status,
122
+ harnessTaskId: result.harnessTaskId,
123
+ runRecordPath: result.runRecordPath,
124
+ dagPath: result.dagPath,
125
+ recordedAt: new Date().toISOString(),
126
+ ...(failure ? { failure } : {}),
127
+ ...(result.failureArtifacts ? { failureArtifacts: result.failureArtifacts } : {}),
128
+ };
129
+ try {
130
+ await recordTaskPoolRun({ repoRoot: options.repoRoot, run });
131
+ tasks.push({
132
+ taskId,
133
+ workerRunId: result.workerRunId,
134
+ status: result.status,
135
+ runRecordPath: result.runRecordPath,
136
+ });
137
+ }
138
+ catch (error) {
139
+ tasks.push({
140
+ taskId,
141
+ workerRunId: result.workerRunId,
142
+ status: "record-error",
143
+ error: errorMessage(error),
144
+ runRecordPath: result.runRecordPath,
145
+ });
146
+ }
147
+ }
148
+ const summary = summarize(tasks);
149
+ const batchRunPath = path.join(getTaskPoolRoot(options.repoRoot), "runs", `${batchRunId}.json`);
150
+ const output = {
151
+ schemaVersion: 1,
152
+ status: summary.recordErrors > 0 || summary.runErrors > 0 ? "failed" : "completed",
153
+ batchRunId,
154
+ featureId: graph.feature_id,
155
+ startedAt,
156
+ summary,
157
+ tasks,
158
+ batchRunPath,
159
+ };
160
+ await mkdir(path.dirname(batchRunPath), { recursive: true });
161
+ await writeFile(batchRunPath, `${JSON.stringify(output, null, 2)}\n`, "utf-8");
162
+ progress.batch(`batch ${batchRunId} ${output.status}: ${summary.succeeded} succeeded, ${summary.failed} failed, ${summary.reused} reused in ${formatDuration(Date.now() - batchStartedAt)} → ${batchRunPath}`);
163
+ return output;
164
+ }
165
+ export function buildBatchRunId(now) {
166
+ return `batch-${now.toISOString().replace(/[-:.]/g, "").slice(0, 15)}`;
167
+ }
168
+ export function buildStableWorkerRunId(taskId, taskSpec, now) {
169
+ const date = now.toISOString().slice(0, 10).replace(/-/g, "");
170
+ const slug = taskId
171
+ .toLowerCase()
172
+ .replace(/[^a-z0-9]+/g, "-")
173
+ .replace(/^-+|-+$/g, "");
174
+ const hash = createHash("sha256")
175
+ .update(`${taskSpec.feature_id}:${taskSpec.id}:${taskSpec.title}`)
176
+ .digest("hex")
177
+ .slice(0, 10);
178
+ return `wr-${date}-${slug}-${hash}`;
179
+ }
180
+ async function loadTaskGraph(featureDir) {
181
+ const graphPath = path.join(featureDir, "tasks", "task-graph.yaml");
182
+ return taskGraphSpecSchema.parse(YAML.parse(await readFile(graphPath, "utf-8")));
183
+ }
184
+ async function loadTaskSpec(taskSpecPath) {
185
+ return taskSpecSchema.parse(YAML.parse(await readFile(taskSpecPath, "utf-8")));
186
+ }
187
+ function toGraphState(states) {
188
+ const graphState = {};
189
+ for (const [taskId, state] of Object.entries(states)) {
190
+ if (state.status === "Done")
191
+ graphState[taskId] = "completed";
192
+ if (state.status === "Running" || state.status === "Queued")
193
+ graphState[taskId] = "running";
194
+ if (state.status === "Failed")
195
+ graphState[taskId] = "failed";
196
+ if (state.status === "Blocked")
197
+ graphState[taskId] = "blocked";
198
+ }
199
+ return graphState;
200
+ }
201
+ function isPoolReady(state) {
202
+ return !state || state.status === "Ready";
203
+ }
204
+ function summarize(tasks) {
205
+ return {
206
+ total: tasks.length,
207
+ succeeded: tasks.filter((task) => task.status === "succeeded").length,
208
+ failed: tasks.filter((task) => task.status === "failed").length,
209
+ reused: tasks.filter((task) => task.status === "reused").length,
210
+ recordErrors: tasks.filter((task) => task.status === "record-error").length,
211
+ runErrors: tasks.filter((task) => task.status === "run-error").length,
212
+ };
213
+ }
214
+ function errorMessage(error) {
215
+ return error instanceof Error ? error.message : String(error);
216
+ }
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ export const acceptanceItemSchema = z
3
+ .object({
4
+ id: z.string().min(1),
5
+ title: z.string().min(1),
6
+ priority: z.enum(["must", "should", "could", "wont"]).optional().default("must"),
7
+ type: z.string().min(1),
8
+ given: z.string().min(1),
9
+ when: z.string().min(1),
10
+ then: z.string().min(1),
11
+ verification: z
12
+ .object({
13
+ expected_task_refs: z.array(z.string().min(1)).min(1),
14
+ suggested_tests: z.array(z.string().min(1)).optional().default([]),
15
+ })
16
+ .strict(),
17
+ })
18
+ .strict();
19
+ export const acceptanceSpecSchema = z
20
+ .object({
21
+ schema_version: z.literal(1),
22
+ feature_id: z.string().min(1),
23
+ acceptance: z.array(acceptanceItemSchema).min(1),
24
+ })
25
+ .strict();
@@ -0,0 +1,23 @@
1
+ const TERMINAL_OR_ACTIVE = new Set([
2
+ "running",
3
+ "completed",
4
+ "failed",
5
+ "blocked",
6
+ ]);
7
+ export function computeReadyQueue(graph, state) {
8
+ return graph.nodes
9
+ .filter((node) => {
10
+ const ownStatus = getStatus(state[node.id]);
11
+ if (ownStatus && TERMINAL_OR_ACTIVE.has(ownStatus))
12
+ return false;
13
+ return node.depends_on.every((dependencyId) => getStatus(state[dependencyId]) === "completed");
14
+ })
15
+ .map((node) => node.id);
16
+ }
17
+ function getStatus(value) {
18
+ if (!value)
19
+ return undefined;
20
+ if (typeof value === "string")
21
+ return value;
22
+ return value.status;
23
+ }
@@ -0,0 +1,28 @@
1
+ import { z } from "zod";
2
+ import { taskSpecTypeSchema } from "../task-spec/schema.js";
3
+ export const taskGraphNodeSchema = z
4
+ .object({
5
+ id: z.string().min(1),
6
+ task: z.string().min(1),
7
+ type: taskSpecTypeSchema,
8
+ depends_on: z.array(z.string().min(1)).optional().default([]),
9
+ })
10
+ .strict();
11
+ export const taskGraphSpecSchema = z
12
+ .object({
13
+ schema_version: z.literal(1),
14
+ feature_id: z.string().min(1),
15
+ nodes: z.array(taskGraphNodeSchema).min(1),
16
+ parallel_policy: z
17
+ .object({
18
+ max_parallel_tasks: z.number().int().positive().optional().default(1),
19
+ disallow_same_file_parallel_writes: z.boolean().optional().default(true),
20
+ })
21
+ .strict()
22
+ .optional()
23
+ .default({
24
+ max_parallel_tasks: 1,
25
+ disallow_same_file_parallel_writes: true,
26
+ }),
27
+ })
28
+ .strict();
@@ -0,0 +1 @@
1
+ export {};