@tea-agent/loop-agent 0.8.0 → 0.10.0-alpha.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 (188) hide show
  1. package/AGENTS.md +10 -0
  2. package/CHANGELOG.md +101 -1
  3. package/README.md +69 -5
  4. package/dist/application/dag/args.js +13 -16
  5. package/dist/application/dag/generate-task-dag.js +32 -2
  6. package/dist/application/dag/run-dag.js +1 -27
  7. package/dist/application/dag/validate-dag.js +2 -2
  8. package/dist/application/loop/run-action.js +0 -4
  9. package/dist/cli/command-definitions.js +7 -11
  10. package/dist/cli/program.js +9 -21
  11. package/dist/commands/cursor-prompt.js +42 -82
  12. package/dist/commands/dag-approve.js +36 -0
  13. package/dist/commands/dag-reconcile-run.js +118 -0
  14. package/dist/commands/delegate.js +75 -77
  15. package/dist/commands/doctor.js +0 -18
  16. package/dist/commands/init.js +60 -40
  17. package/dist/commands/instructions.js +7 -10
  18. package/dist/commands/loop.js +4 -20
  19. package/dist/executors/config-core.js +0 -51
  20. package/dist/executors/dag-pi-executor.js +1 -1
  21. package/dist/executors/dag.js +0 -1
  22. package/dist/executors/index.js +0 -2
  23. package/dist/executors/model-routing.js +9 -9
  24. package/dist/executors/shell-executor.js +75 -9
  25. package/dist/governance/checks.js +6 -3
  26. package/dist/governance/manifest-types.js +33 -2
  27. package/dist/infrastructure/harness/loop-action-store.js +0 -3
  28. package/dist/records/harvest.js +2 -23
  29. package/dist/records/one-shot-runs.js +1 -1
  30. package/dist/shared/artifacts-core.js +24 -5
  31. package/dist/shared/output-truncation.js +37 -0
  32. package/dist/shared/package-metadata.js +353 -0
  33. package/dist/shared/reference-context.js +48 -22
  34. package/dist/{executors/cursor-executor.js → sidecars/cursor-prompt/executor.js} +2 -42
  35. package/dist/sidecars/cursor-prompt/index.js +3 -0
  36. package/dist/sidecars/cursor-prompt/stream.js +121 -0
  37. package/dist/task/config-types.js +30 -13
  38. package/dist/task/delegate.js +9 -21
  39. package/dist/task/runtime.js +2 -3
  40. package/dist/worker/cli.js +243 -0
  41. package/dist/worker/closeout/apply.js +73 -0
  42. package/dist/worker/closeout/preview.js +30 -0
  43. package/dist/worker/delivery/final-verification.js +194 -0
  44. package/dist/worker/delivery/git-transaction.js +354 -0
  45. package/dist/worker/delivery/package.js +502 -0
  46. package/dist/worker/feature/decision-loader.js +68 -0
  47. package/dist/worker/feature/discover.js +14 -0
  48. package/dist/worker/feature/next-action.js +74 -0
  49. package/dist/worker/feature/reducer.js +133 -0
  50. package/dist/worker/feature/review.js +502 -0
  51. package/dist/worker/feature/run.js +365 -0
  52. package/dist/worker/feature/types.js +1 -0
  53. package/dist/worker/follow-up/approve.js +270 -0
  54. package/dist/worker/follow-up/factory.js +234 -0
  55. package/dist/worker/follow-up/paths.js +25 -0
  56. package/dist/worker/follow-up/policy.js +26 -0
  57. package/dist/worker/follow-up/schema.js +93 -0
  58. package/dist/worker/follow-up/store.js +96 -0
  59. package/dist/worker/loop-agent/loop-agent-client.js +345 -101
  60. package/dist/worker/metrics/projector.js +139 -0
  61. package/dist/worker/observability/read-model.js +282 -15
  62. package/dist/worker/observe/paths.js +17 -5
  63. package/dist/worker/observe/routes.js +78 -20
  64. package/dist/worker/observe/server.js +8 -6
  65. package/dist/worker/observe/static/app.js +1045 -177
  66. package/dist/worker/observe/static/index.html +70 -43
  67. package/dist/worker/observe/static/styles.css +553 -610
  68. package/dist/worker/pool/run-store.js +14 -2
  69. package/dist/worker/pool/validation.js +59 -0
  70. package/dist/worker/preflight.js +49 -1
  71. package/dist/worker/report/morning-report.js +41 -6
  72. package/dist/worker/run-task/run-task.js +23 -13
  73. package/dist/worker/runner/run-ready.js +89 -11
  74. package/dist/worker/task-spec/schema.js +0 -1
  75. package/dist/workflows/dag/convergence/controller.js +1 -1
  76. package/dist/workflows/dag/executor-registry.js +0 -2
  77. package/dist/workflows/dag/governance-profile.js +10 -0
  78. package/dist/workflows/dag/init-hybrid.js +601 -26
  79. package/dist/workflows/dag/lifecycle.js +146 -0
  80. package/dist/workflows/dag/node-execution.js +64 -7
  81. package/dist/workflows/dag/prompt.js +16 -0
  82. package/dist/workflows/dag/report.js +2 -0
  83. package/dist/workflows/dag/runner.js +176 -119
  84. package/dist/workflows/dag/scheduler.js +7 -2
  85. package/dist/workflows/dag/skill-snapshot.js +527 -0
  86. package/dist/workflows/dag/types.js +45 -9
  87. package/dist/workflows/dag/validate.js +5 -8
  88. package/dist/workflows/loop/actions/dag-action.js +0 -2
  89. package/dist/workflows/loop/actions/shared.js +1 -1
  90. package/dist/workflows/loop/actions.js +14 -31
  91. package/dist/workflows/loop/benchmark.js +1 -1
  92. package/dist/workflows/loop/index.js +1 -1
  93. package/dist/workflows/loop/policy/auto-policy.js +22 -14
  94. package/dist/workflows/loop/policy/path-patterns.js +13 -0
  95. package/docs/README.md +35 -12
  96. package/docs/agent-dag-recovery-playbook.md +1 -1
  97. package/docs/architecture/README.md +26 -0
  98. package/docs/architecture/dag-execution.md +134 -0
  99. package/docs/architecture/evolution.md +52 -0
  100. package/docs/architecture/facts-and-state.md +58 -0
  101. package/docs/architecture/runtime-boundaries.md +41 -15
  102. package/docs/architecture/system-overview.md +93 -0
  103. package/docs/architecture/worker-and-feature.md +81 -0
  104. package/docs/cursor-prompt-sidecar.md +36 -0
  105. package/docs/decisions/README.md +13 -1
  106. package/docs/design/README.md +39 -13
  107. package/docs/development-principles.md +1 -1
  108. package/docs/exec-plans/active/README.md +2 -2
  109. package/docs/exec-plans/completed/README.md +21 -0
  110. package/docs/feature-workflow.md +44 -4
  111. package/docs/init-surface.manifest.json +63 -1
  112. package/docs/loop-agent-harness.md +65 -3
  113. package/docs/progress/README.md +27 -0
  114. package/docs/reports/README.md +74 -5
  115. package/docs/skills/README.md +2 -1
  116. package/docs/skills/vetted-skill-registry.md +2 -1
  117. package/docs/templates/agent-dag-report.schema.json +4 -2
  118. package/docs/templates/agent-dag.base.json +0 -5
  119. package/docs/templates/agent-dag.final-verification.json +0 -5
  120. package/docs/templates/agent-dag.schema.json +1 -2
  121. package/docs/templates/agent-dag.supervised-implementation.json +1 -6
  122. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +131 -0
  123. package/docs/templates/backend-test-dag.json +213 -0
  124. package/docs/templates/backend-test-dag.retrospect.prompt.md +128 -0
  125. package/docs/templates/backend-test-dag.review-cases.prompt.md +85 -0
  126. package/docs/templates/frontend-design-contract.md +33 -0
  127. package/docs/templates/frontend-task-constraints.md +25 -0
  128. package/docs/templates/frontend-task-requirement.md +61 -0
  129. package/docs/templates/harness.schema.json +8 -5
  130. package/docs/templates/hybrid-dag.json +1 -6
  131. package/docs/templates/init-evolution-review.md +4 -2
  132. package/docs/templates/interactive-ui-round2-experiment.md +1 -1
  133. package/docs/templates/product-line/task.yaml +0 -1
  134. package/docs/templates/worker-dogfood-evidence.md +28 -0
  135. package/docs/templates/worker-dogfood-setup.md +20 -0
  136. package/docs/verification-matrix.md +17 -0
  137. package/examples/decision-gate-agent-dag.json +87 -33
  138. package/examples/example-dag.json +0 -5
  139. package/examples/hybrid-loop-agent-dag.json +0 -5
  140. package/harness.json +6 -11
  141. package/package.json +22 -44
  142. package/scripts/check-product-line-docs.sh +10 -3
  143. package/scripts/check-task-pool-root.sh +1 -1
  144. package/skills/agent-worker/SKILL.md +37 -0
  145. package/skills/agent-worker/references/agent-worker-operator.md +43 -0
  146. package/skills/frontend-design-review/SKILL.md +59 -0
  147. package/skills/frontend-design-review/references/review-checklist.md +37 -0
  148. package/skills/frontend-implementation/SKILL.md +48 -0
  149. package/skills/frontend-implementation/references/code-standards.md +34 -0
  150. package/skills/frontend-implementation/references/design-spec.md +46 -0
  151. package/skills/frontend-implementation/references/node-contracts.md +32 -0
  152. package/skills/frontend-review/SKILL.md +53 -0
  153. package/skills/frontend-review/references/review-findings.md +42 -0
  154. package/skills/frontend-verification/SKILL.md +40 -0
  155. package/skills/frontend-verification/references/verification-checklist.md +56 -0
  156. package/skills/grill-me/SKILL.md +10 -0
  157. package/skills/grill-with-docs/SKILL.md +88 -0
  158. package/skills/grill-with-docs/adr-format.md +47 -0
  159. package/skills/grill-with-docs/context-format.md +60 -0
  160. package/skills/init-capability-evolution/SKILL.md +1 -0
  161. package/skills/loop-agent/SKILL.md +11 -9
  162. package/skills/loop-agent/references/command-reference.md +28 -15
  163. package/skills/loop-agent/references/docs-converge.md +126 -0
  164. package/skills/loop-agent/references/harness-policy.md +7 -7
  165. package/skills/loop-agent/references/hybrid-dag.md +13 -15
  166. package/skills/loop-agent/references/long-running-loop.md +4 -6
  167. package/skills/loop-agent/references/multi-worktree.md +6 -6
  168. package/skills/loop-agent/references/orchestrator-and-interventions.md +3 -3
  169. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +14 -11
  170. package/skills/loop-agent/references/verification-and-failure-handling.md +8 -3
  171. package/skills/using-git-worktrees/SKILL.md +215 -0
  172. package/dist/commands/cursor-worker.js +0 -43
  173. package/dist/cursor-worker-entry.js +0 -8
  174. package/dist/executors/cursor-artifacts.js +0 -33
  175. package/dist/executors/cursor-execution-log.js +0 -81
  176. package/dist/executors/cursor-executor-artifacts.js +0 -134
  177. package/dist/executors/cursor-run.js +0 -115
  178. package/dist/executors/cursor-tool.js +0 -94
  179. package/dist/executors/cursor-worker-client.js +0 -223
  180. package/dist/executors/cursor-worker-protocol.js +0 -18
  181. package/dist/executors/cursor-worker-server.js +0 -54
  182. package/dist/executors/cursor-worker.js +0 -3
  183. package/dist/executors/cursor.js +0 -6
  184. package/dist/executors/dag-cursor-executor.js +0 -87
  185. package/dist/workflows/loop/actions/cursor-fix.js +0 -191
  186. package/dist/workflows/loop/policy/cursor-fix-policy.js +0 -31
  187. package/docs/cursor-executor-usage.md +0 -25
  188. package/docs/dynamic-workflow-dag-engine-roadmap.md +0 -1749
@@ -1,4 +1,5 @@
1
- import { appendFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
1
+ import { randomUUID } from "node:crypto";
2
+ import { appendFile, mkdir, readFile, readdir, rename, unlink, writeFile } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  export const TASK_POOL_RELATIVE_ROOT = ".harness/task-pool";
4
5
  export function getTaskPoolRoot(repoRoot) {
@@ -19,6 +20,10 @@ export async function recordTaskPoolRun(input) {
19
20
  await writeTaskPoolState(input.repoRoot, stateFromRun(input.run));
20
21
  await appendJsonlFile(getEventsJsonlPath(input.repoRoot), eventFromRun(input.run));
21
22
  }
23
+ export async function appendTaskPoolEvent(repoRoot, event) {
24
+ await ensurePoolDirs(repoRoot);
25
+ await appendJsonlFile(getEventsJsonlPath(repoRoot), event);
26
+ }
22
27
  export async function findRunByWorkerRunId(repoRoot, workerRunId) {
23
28
  const runs = await readJsonlFile(getRunsJsonlPath(repoRoot));
24
29
  return runs.find((run) => run.workerRunId === workerRunId);
@@ -99,7 +104,14 @@ export async function readAllTaskPoolStates(repoRoot) {
99
104
  export async function writeTaskPoolState(repoRoot, state) {
100
105
  const statePath = getTaskStatePath(repoRoot, state.taskId);
101
106
  await mkdir(path.dirname(statePath), { recursive: true });
102
- await writeFile(statePath, `${JSON.stringify({ schemaVersion: 1, ...state }, null, 2)}\n`, "utf-8");
107
+ const tempPath = path.join(path.dirname(statePath), `.${path.basename(statePath)}.${randomUUID()}.tmp`);
108
+ try {
109
+ await writeFile(tempPath, `${JSON.stringify({ schemaVersion: 1, ...state }, null, 2)}\n`, "utf-8");
110
+ await rename(tempPath, statePath);
111
+ }
112
+ finally {
113
+ await unlink(tempPath).catch(() => { });
114
+ }
103
115
  }
104
116
  export async function readJsonlFile(filePath) {
105
117
  try {
@@ -0,0 +1,59 @@
1
+ import { z } from "zod";
2
+ import { readFile, readdir } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { getRunsJsonlPath, getTaskPoolRoot } from "./run-store.js";
5
+ export const taskPoolRunFactSchema = z.object({
6
+ schemaVersion: z.literal(1), batchRunId: z.string().min(1), workerRunId: z.string().min(1), taskId: z.string().min(1), featureId: z.string().min(1),
7
+ status: z.enum(["succeeded", "failed", "run-error"]), recordedAt: z.string().datetime(),
8
+ }).passthrough();
9
+ export const taskPoolStateFactSchema = z.object({
10
+ taskId: z.string().min(1), status: z.enum(["Draft", "Ready", "Queued", "Running", "AgentCompleted", "VerificationRunning", "HumanReview", "Done", "Failed", "Blocked", "Abandoned"]), updatedAt: z.string().datetime(),
11
+ }).passthrough();
12
+ export async function readValidTaskPoolRuns(repoRoot) {
13
+ const records = [];
14
+ const warnings = [];
15
+ try {
16
+ const raw = await readFile(getRunsJsonlPath(repoRoot), "utf-8");
17
+ for (const [index, line] of raw.split(/\r?\n/).filter(Boolean).entries())
18
+ try {
19
+ const parsed = taskPoolRunFactSchema.safeParse(JSON.parse(line));
20
+ if (parsed.success)
21
+ records.push(parsed.data);
22
+ else
23
+ warnings.push(`Task Pool runs.jsonl line ${index + 1} is semantically invalid`);
24
+ }
25
+ catch {
26
+ warnings.push(`Task Pool runs.jsonl line ${index + 1} is corrupt`);
27
+ }
28
+ }
29
+ catch (error) {
30
+ if (!isNotFound(error))
31
+ warnings.push("Task Pool runs.jsonl is unreadable");
32
+ }
33
+ return { records, warnings };
34
+ }
35
+ export async function readValidTaskPoolStates(repoRoot) {
36
+ const records = {};
37
+ const warnings = [];
38
+ const stateDir = path.join(getTaskPoolRoot(repoRoot), "states");
39
+ try {
40
+ for (const entry of await readdir(stateDir))
41
+ if (entry.endsWith(".json"))
42
+ try {
43
+ const parsed = taskPoolStateFactSchema.safeParse(JSON.parse(await readFile(path.join(stateDir, entry), "utf-8")));
44
+ if (parsed.success && parsed.data.taskId === entry.slice(0, -5))
45
+ records[parsed.data.taskId] = parsed.data;
46
+ else
47
+ warnings.push(`Task Pool state is semantically invalid: ${entry}`);
48
+ }
49
+ catch {
50
+ warnings.push(`Task Pool state is corrupt: ${entry}`);
51
+ }
52
+ }
53
+ catch (error) {
54
+ if (!isNotFound(error))
55
+ warnings.push("Task Pool states are unreadable");
56
+ }
57
+ return { records, warnings };
58
+ }
59
+ function isNotFound(error) { return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT"); }
@@ -1,14 +1,27 @@
1
1
  import { access } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { controllerIdentityExpectationFailure, resolveControllerIdentity, } from "./loop-agent/loop-agent-client.js";
3
4
  export const DEFAULT_CHECK_REPO_TIMEOUT_MS = 300_000;
4
5
  export async function preflightTargetRepo(input) {
5
6
  const commands = [];
7
+ const identity = resolveControllerIdentity(input.client);
8
+ const expectationFailure = controllerIdentityExpectationFailure(identity, input.expectation);
9
+ if (expectationFailure) {
10
+ return {
11
+ ok: false,
12
+ code: expectationFailure.code,
13
+ message: expectationFailure.message,
14
+ commands,
15
+ controllerIdentity: identity,
16
+ };
17
+ }
6
18
  if (!(await exists(path.join(input.repoRoot, "harness.json")))) {
7
19
  return {
8
20
  ok: false,
9
21
  code: "target-not-initialized",
10
22
  message: "target repo is missing harness.json",
11
23
  commands,
24
+ controllerIdentity: identity,
12
25
  };
13
26
  }
14
27
  if (!(await exists(path.join(input.repoRoot, ".harness")))) {
@@ -17,6 +30,7 @@ export async function preflightTargetRepo(input) {
17
30
  code: "target-not-initialized",
18
31
  message: "target repo is missing .harness",
19
32
  commands,
33
+ controllerIdentity: identity,
20
34
  };
21
35
  }
22
36
  const version = await input.client.run(["--version"], {
@@ -30,8 +44,36 @@ export async function preflightTargetRepo(input) {
30
44
  code: "version-failed",
31
45
  message: "loop-agent --version failed",
32
46
  commands,
47
+ controllerIdentity: identity,
33
48
  };
34
49
  }
50
+ const reportedVersion = version.stdout.trim();
51
+ let controllerIdentity = identity
52
+ ? { ...identity, reportedVersion }
53
+ : undefined;
54
+ if (identity && reportedVersion !== identity.packageVersion) {
55
+ return {
56
+ ok: false,
57
+ code: "reported-version-mismatch",
58
+ message: `reported version ${reportedVersion} does not match package version ${identity.packageVersion}`,
59
+ commands,
60
+ controllerIdentity,
61
+ };
62
+ }
63
+ if (identity && input.client.observeReportedVersion) {
64
+ try {
65
+ controllerIdentity = input.client.observeReportedVersion(reportedVersion) ?? controllerIdentity;
66
+ }
67
+ catch (error) {
68
+ return {
69
+ ok: false,
70
+ code: "reported-version-mismatch",
71
+ message: error instanceof Error ? error.message : String(error),
72
+ commands,
73
+ controllerIdentity,
74
+ };
75
+ }
76
+ }
35
77
  const inspect = await input.client.run(["inspect"], {
36
78
  cwd: input.repoRoot,
37
79
  artifactName: "inspect",
@@ -44,6 +86,7 @@ export async function preflightTargetRepo(input) {
44
86
  code: "inspect-failed",
45
87
  message: "loop-agent inspect failed or did not return JSON",
46
88
  commands,
89
+ controllerIdentity,
47
90
  };
48
91
  }
49
92
  const docsAudit = await input.client.run(["docs", "audit"], {
@@ -57,6 +100,7 @@ export async function preflightTargetRepo(input) {
57
100
  code: "docs-audit-failed",
58
101
  message: "loop-agent docs audit failed",
59
102
  commands,
103
+ controllerIdentity,
60
104
  };
61
105
  }
62
106
  const gitStatus = await input.client.runExternal("git", ["status", "--short", "--branch"], {
@@ -70,6 +114,7 @@ export async function preflightTargetRepo(input) {
70
114
  code: "git-status-failed",
71
115
  message: "git status --short --branch failed",
72
116
  commands,
117
+ controllerIdentity,
73
118
  };
74
119
  }
75
120
  if (input.runCheckRepo) {
@@ -86,15 +131,18 @@ export async function preflightTargetRepo(input) {
86
131
  code: "check-repo-failed",
87
132
  message: `${[command, ...args].join(" ")} failed`,
88
133
  commands,
134
+ controllerIdentity,
89
135
  };
90
136
  }
91
137
  }
92
138
  return {
93
139
  ok: true,
94
- version: version.stdout.trim(),
140
+ version: reportedVersion,
141
+ reportedVersion: controllerIdentity?.reportedVersion,
95
142
  inspect: inspect.json,
96
143
  gitStatus: gitStatus.stdout,
97
144
  commands,
145
+ controllerIdentity,
98
146
  };
99
147
  }
100
148
  async function exists(filePath) {
@@ -1,17 +1,30 @@
1
- import { access, writeFile, mkdir } from "node:fs/promises";
1
+ import { access, writeFile, mkdir, readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { getRunsJsonlPath, readAllTaskPoolStates, readJsonlFile, } from "../pool/run-store.js";
3
+ import YAML from "yaml";
4
+ import { followUpActionCardSchema } from "../follow-up/schema.js";
5
+ import { readFollowUpIndex, resolveRepoFile } from "../follow-up/store.js";
6
+ import { loadFeatureDecisionModels } from "../feature/decision-loader.js";
7
+ import { readValidTaskPoolRuns, readValidTaskPoolStates } from "../pool/validation.js";
4
8
  export async function renderMorningReport(options) {
5
- const runs = (await readJsonlFile(getRunsJsonlPath(options.repoRoot))).filter((run) => !options.batchRunId || run.batchRunId === options.batchRunId);
9
+ const { features } = await loadFeatureDecisionModels(options.repoRoot);
10
+ const allRuns = (await readValidTaskPoolRuns(options.repoRoot)).records;
11
+ const runs = allRuns.filter((run) => !options.batchRunId || run.batchRunId === options.batchRunId);
6
12
  const total = runs.length;
7
13
  const succeeded = runs.filter((run) => run.status === "succeeded").length;
8
14
  const failed = runs.filter((run) => run.status === "failed").length;
9
15
  const followUps = runs.filter((run) => run.failure).length;
10
- const states = await readAllTaskPoolStates(options.repoRoot);
16
+ const states = (await readValidTaskPoolStates(options.repoRoot)).records;
11
17
  const blocked = Object.values(states).filter((state) => state.status === "Blocked").length;
12
18
  const lines = [
13
19
  "# Nightly Worker Report",
14
20
  "",
21
+ "## Feature Decision Summary",
22
+ "",
23
+ ...(features.length === 0 ? ["- Status: no Feature Packet discovered", "- Next Action: add or locate a Feature Packet", "- Why: no shared Feature read model is available", "- Evidence: none"] : features.flatMap((feature) => {
24
+ const why = feature.blockingItems[0]?.message ?? `${feature.summary.tasksSucceeded}/${feature.summary.tasksTotal} tasks; ${feature.summary.requiredAcCovered}/${feature.summary.requiredAcTotal} required AC`;
25
+ const evidence = [feature.evidence.delivery, feature.evidence.closeout, feature.evidence.morningReport, feature.evidence.observeSnapshot, ...feature.blockingItems.flatMap((item) => item.evidence)].find(Boolean) ?? "none";
26
+ return [`### ${feature.featureId}`, "", `- Status: ${feature.status}`, `- Next Action: ${feature.nextAction?.command ?? feature.nextAction?.label ?? "none"}`, `- Why: ${why}`, `- Evidence: ${evidence}`, ""];
27
+ })),
15
28
  "## Summary",
16
29
  "",
17
30
  `- Total: ${total}`,
@@ -26,16 +39,38 @@ export async function renderMorningReport(options) {
26
39
  "|---|---|---|---|---|---|",
27
40
  ];
28
41
  for (const run of runs) {
29
- lines.push(`| ${run.taskId} | ${run.status} | ${run.workerRunId} | ${run.failure?.category ?? "-"} | ${run.failure?.derivedFollowUpTaskId ?? "-"} | ${await artifactSummary(run)} |`);
42
+ const followUp = await followUpSummary(run, options.repoRoot);
43
+ lines.push(`| ${run.taskId} | ${run.status} | ${run.workerRunId} | ${run.failure?.category ?? "-"} | ${followUp ?? run.failure?.derivedFollowUpTaskId ?? "-"} | ${await artifactSummary(run)} |`);
30
44
  }
31
45
  if (followUps > 0) {
32
46
  lines.push("", "## Human Actions", "");
33
47
  for (const run of runs.filter((candidate) => candidate.failure)) {
34
- lines.push(`- ${run.failure?.derivedFollowUpTaskId}: ${run.failure?.recommendedFollowUpKind} for ${run.taskId}`);
48
+ const followUp = await followUpSummary(run, options.repoRoot);
49
+ lines.push(`- ${followUp ?? `${run.failure?.derivedFollowUpTaskId}: ${run.failure?.recommendedFollowUpKind} for ${run.taskId}`}`);
35
50
  }
36
51
  }
37
52
  return `${lines.join("\n")}\n`;
38
53
  }
54
+ async function followUpSummary(run, repoRoot) {
55
+ if (!run.failure)
56
+ return undefined;
57
+ try {
58
+ const index = await readFollowUpIndex(repoRoot, run.featureId);
59
+ const entry = index.entries.find((candidate) => candidate.workerRunId === run.workerRunId && candidate.status !== "Superseded" && candidate.status !== "Rejected");
60
+ if (!entry)
61
+ return undefined;
62
+ if (entry.kind === "TaskDraft") {
63
+ return entry.status === "Approved"
64
+ ? `${entry.followUpId}: approved as ${entry.proposedTaskId}`
65
+ : `${entry.followUpId}: approve ${entry.proposedTaskId}`;
66
+ }
67
+ const card = followUpActionCardSchema.parse(YAML.parse(await readFile(await resolveRepoFile(repoRoot, entry.draftPath), "utf-8")));
68
+ return `${entry.followUpId}: ${card.command ?? card.label}`;
69
+ }
70
+ catch {
71
+ return undefined;
72
+ }
73
+ }
39
74
  export async function writeMorningReport(input) {
40
75
  const markdown = await renderMorningReport(input);
41
76
  await mkdir(path.dirname(input.outputPath), { recursive: true });
@@ -3,12 +3,18 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { formatDuration, noopProgressReporter } from "../progress-reporter.js";
5
5
  import { redactForPreview, truncatePreview } from "../observability/events.js";
6
+ import { controllerIdentityExpectationFailure, resolveControllerIdentity, } from "../loop-agent/loop-agent-client.js";
6
7
  import { materializeTaskSpec, } from "../materialize/harness-task-materializer.js";
7
8
  import { preflightTargetRepo } from "../preflight.js";
8
9
  import { getTaskPoolRoot } from "../pool/run-store.js";
9
10
  export const DEFAULT_RUN_DAG_TIMEOUT_MS = 1_800_000;
10
11
  export const MAX_WORKER_TIMEOUT_MS = 7_200_000;
11
12
  export async function runTaskSpec(options) {
13
+ let controllerIdentity = resolveControllerIdentity(options.client, options.controllerIdentity);
14
+ const identityFailure = controllerIdentityExpectationFailure(controllerIdentity, options.controllerExpectation);
15
+ if (identityFailure) {
16
+ throw new Error(`${identityFailure.code}: ${identityFailure.message}`);
17
+ }
12
18
  const now = options.now ?? new Date();
13
19
  const workerRunId = options.workerRunId ?? buildWorkerRunId(options.taskSpec.id, now);
14
20
  const commands = [];
@@ -26,11 +32,13 @@ export async function runTaskSpec(options) {
26
32
  repoRoot: options.repoRoot,
27
33
  client,
28
34
  ...(typeof options.preflight === "object" ? options.preflight : {}),
35
+ ...(options.controllerExpectation ? { expectation: options.controllerExpectation } : {}),
29
36
  });
30
37
  if (!preflight.ok) {
31
38
  progress.note(`preflight FAILED: ${preflight.code}`);
32
39
  throw new Error(`target repo preflight failed: ${preflight.code}: ${preflight.message}`);
33
40
  }
41
+ controllerIdentity = preflight.controllerIdentity ?? controllerIdentity;
34
42
  });
35
43
  progress.step(`preflight ok in ${formatDuration(Date.now() - preflightStartedAt)}`);
36
44
  }
@@ -66,7 +74,6 @@ export async function runTaskSpec(options) {
66
74
  dagPath,
67
75
  "--cwd",
68
76
  options.repoRoot,
69
- ...noCursorArgs(options.taskSpec),
70
77
  ], true, undefined, eventCtx);
71
78
  });
72
79
  progress.step(`dag generated in ${formatDuration(Date.now() - dagGenStartedAt)}`);
@@ -90,7 +97,6 @@ export async function runTaskSpec(options) {
90
97
  "--strict-governance",
91
98
  "--spine-task",
92
99
  materializeManifest.harnessTaskId,
93
- ...forbidCursorArgs(options.taskSpec),
94
100
  ], true, undefined, eventCtx);
95
101
  });
96
102
  progress.step(`dag validated in ${formatDuration(Date.now() - validateStartedAt)}`);
@@ -111,7 +117,6 @@ export async function runTaskSpec(options) {
111
117
  "--events-jsonl",
112
118
  dagEventsPath,
113
119
  ...maxConcurrentArgs(options.taskSpec),
114
- ...noCursorArgs(options.taskSpec),
115
120
  ], resolveRunDagTimeoutMs(options.taskSpec), eventCtx);
116
121
  });
117
122
  progress.step(`run-dag finished in ${formatDuration(Date.now() - runDagStartedAt)}`);
@@ -156,7 +161,7 @@ export async function runTaskSpec(options) {
156
161
  taskArtifactsDir,
157
162
  eventCtx,
158
163
  });
159
- if (reportDecision.succeeded) {
164
+ if (reportDecision.succeeded && !options.skipSuccessFinalization) {
160
165
  await runObservedStep(eventCtx, "promote-run", async () => {
161
166
  progress.step("promote run");
162
167
  await runRequiredCommand(options.repoRoot, client, "promote-run", ["promote-run", materializeManifest.harnessTaskId, "--run-id", workerRunId], true, undefined, eventCtx);
@@ -180,6 +185,7 @@ export async function runTaskSpec(options) {
180
185
  reportDecision,
181
186
  commands,
182
187
  ...(failureArtifacts ? { failureArtifacts } : {}),
188
+ ...(controllerIdentity ? { controllerIdentity } : {}),
183
189
  };
184
190
  await writeFile(runRecordPath, `${JSON.stringify(record, null, 2)}\n`, "utf-8");
185
191
  emit(progress, {
@@ -407,6 +413,12 @@ class RecordingRunTaskClient {
407
413
  this.delegate = delegate;
408
414
  this.records = records;
409
415
  }
416
+ getIdentity() {
417
+ return this.delegate.getIdentity?.();
418
+ }
419
+ observeReportedVersion(reportedVersion) {
420
+ return this.delegate.observeReportedVersion?.(reportedVersion);
421
+ }
410
422
  async run(args, options) {
411
423
  const result = await this.delegate.run(args, options);
412
424
  this.records.push({
@@ -452,6 +464,12 @@ class ObservedRunTaskClient {
452
464
  this.delegate = delegate;
453
465
  this.eventCtx = eventCtx;
454
466
  }
467
+ getIdentity() {
468
+ return this.delegate.getIdentity();
469
+ }
470
+ observeReportedVersion(reportedVersion) {
471
+ return this.delegate.observeReportedVersion(reportedVersion);
472
+ }
455
473
  run(args, options) {
456
474
  return this.runObserved(args, options, (observedOptions) => this.delegate.run(args, observedOptions));
457
475
  }
@@ -544,12 +562,6 @@ function commandRecordName(args, artifactName) {
544
562
  return "new-task";
545
563
  return artifactName;
546
564
  }
547
- function noCursorArgs(taskSpec) {
548
- return taskSpec.loop_agent.no_cursor ? ["--no-cursor"] : [];
549
- }
550
- function forbidCursorArgs(taskSpec) {
551
- return taskSpec.loop_agent.no_cursor ? ["--forbid-executor", "cursor"] : [];
552
- }
553
565
  function maxConcurrentArgs(taskSpec) {
554
566
  return ["--max-concurrent", String(taskSpec.loop_agent.max_concurrent)];
555
567
  }
@@ -563,14 +575,12 @@ function strictModelsArgs(piModel) {
563
575
  }
564
576
  /**
565
577
  * Rewrite `executorModels.pi.{LOW,MED,HIGH}` in an already-generated DAG JSON
566
- * so every pi executor node resolves to the smoke override model. Cursor / shell
567
- * slots are left untouched.
578
+ * so every pi executor node resolves to the smoke override model.
568
579
  */
569
580
  export async function applyPiModelOverride(dagPath, piModel) {
570
581
  const raw = await readFile(dagPath, "utf-8");
571
582
  const spec = JSON.parse(raw);
572
583
  spec.executorModels = {
573
- ...(spec.executorModels?.cursor ? { cursor: spec.executorModels.cursor } : {}),
574
584
  pi: {
575
585
  LOW: piModel,
576
586
  MED: piModel,
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { readFile, writeFile, mkdir } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import YAML from "yaml";
5
+ import { controllerIdentitiesMatch, controllerIdentityExpectationFailure, resolveControllerIdentity, } from "../loop-agent/loop-agent-client.js";
5
6
  import { deriveFailureRoute, deriveFailureRouteFromError, } from "../pool/failure-routing.js";
6
7
  import { findRunByWorkerRunId, getTaskPoolRoot, readAllTaskPoolStates, recordTaskPoolRun, writeTaskPoolState, } from "../pool/run-store.js";
7
8
  import { runTaskSpec, } from "../run-task/run-task.js";
@@ -9,7 +10,26 @@ import { formatDuration, noopProgressReporter, } from "../progress-reporter.js";
9
10
  import { computeReadyQueue } from "../task-graph/ready-queue.js";
10
11
  import { taskGraphSpecSchema } from "../task-graph/task-graph-schema.js";
11
12
  import { taskSpecSchema } from "../task-spec/schema.js";
13
+ import { preflightTargetRepo } from "../preflight.js";
12
14
  export async function runReadyTasks(options) {
15
+ let controllerIdentity = resolveControllerIdentity(options.client, options.controllerIdentity);
16
+ const identityFailure = controllerIdentityExpectationFailure(controllerIdentity, options.controllerExpectation);
17
+ if (identityFailure) {
18
+ throw new Error(`${identityFailure.code}: ${identityFailure.message}`);
19
+ }
20
+ if (!options.skipTaskPreflight && !options.runTask) {
21
+ const preflight = await preflightTargetRepo({
22
+ repoRoot: options.repoRoot,
23
+ client: options.client,
24
+ runCheckRepo: options.runCheckRepo,
25
+ ...(options.checkRepoCommand ? { checkRepoCommand: options.checkRepoCommand } : {}),
26
+ ...(options.controllerExpectation ? { expectation: options.controllerExpectation } : {}),
27
+ });
28
+ if (!preflight.ok) {
29
+ throw new Error(`target repo preflight failed: ${preflight.code}: ${preflight.message}`);
30
+ }
31
+ controllerIdentity = preflight.controllerIdentity ?? controllerIdentity;
32
+ }
13
33
  const now = options.now ?? new Date();
14
34
  const startedAt = now.toISOString();
15
35
  const batchRunId = options.batchRunId ?? buildBatchRunId(now);
@@ -43,12 +63,19 @@ export async function runReadyTasks(options) {
43
63
  const taskSpecPath = path.join(options.featureDir, "tasks", node?.task ?? `${taskId}.yaml`);
44
64
  const taskSpec = await loadTaskSpec(taskSpecPath);
45
65
  const retryOfWorkerRunId = states[taskId]?.retryOfWorkerRunId;
46
- const workerRunId = retryOfWorkerRunId
66
+ let workerRunId = retryOfWorkerRunId
47
67
  ? buildRetryWorkerRunId(taskId, retryOfWorkerRunId, now)
48
68
  : options.workerRunIdForTask?.(taskId, taskSpec) ??
49
- buildStableWorkerRunId(taskId, taskSpec, now);
69
+ buildStableWorkerRunId(taskId, taskSpec, now, controllerIdentity);
50
70
  if (workerRunId) {
51
- const existing = await findRunByWorkerRunId(options.repoRoot, workerRunId);
71
+ let existing = await findRunByWorkerRunId(options.repoRoot, workerRunId);
72
+ if (existing && !controllerRunCanBeReused(existing, controllerIdentity)) {
73
+ workerRunId = buildControllerScopedWorkerRunId(workerRunId, controllerIdentity);
74
+ existing = await findRunByWorkerRunId(options.repoRoot, workerRunId);
75
+ if (existing && !controllerRunCanBeReused(existing, controllerIdentity)) {
76
+ throw new Error(`workerRunId collision has incompatible controller identity: ${workerRunId}`);
77
+ }
78
+ }
52
79
  if (existing) {
53
80
  progress.task(`task ${index}/${total} ${taskId}: reuse existing run ${workerRunId}`);
54
81
  emit(progress, {
@@ -66,6 +93,7 @@ export async function runReadyTasks(options) {
66
93
  status: "reused",
67
94
  runRecordPath: existing.runRecordPath,
68
95
  });
96
+ await options.onTaskFinalized?.({ status: "reused", taskSpec, workerRunId, ...(existing.runRecordPath ? { runRecordPath: existing.runRecordPath } : {}) });
69
97
  continue;
70
98
  }
71
99
  }
@@ -80,7 +108,7 @@ export async function runReadyTasks(options) {
80
108
  workerRunId,
81
109
  status: "running",
82
110
  });
83
- progress.step(`preflight → materialize → dag-run-task → validate → run-dag → report`);
111
+ progress.step(`materialize → dag-run-task → validate → run-dag → report`);
84
112
  let result;
85
113
  try {
86
114
  await writeTaskPoolState(options.repoRoot, {
@@ -106,12 +134,11 @@ export async function runReadyTasks(options) {
106
134
  client: options.client,
107
135
  now,
108
136
  workerRunId,
109
- preflight: {
110
- runCheckRepo: options.runCheckRepo,
111
- ...(options.checkRepoCommand ? { checkRepoCommand: options.checkRepoCommand } : {}),
112
- },
137
+ preflight: false,
113
138
  progress,
114
139
  ...(options.piModel ? { piModel: options.piModel } : {}),
140
+ ...(controllerIdentity ? { controllerIdentity } : {}),
141
+ ...(options.controllerExpectation ? { controllerExpectation: options.controllerExpectation } : {}),
115
142
  });
116
143
  }
117
144
  catch (error) {
@@ -128,6 +155,7 @@ export async function runReadyTasks(options) {
128
155
  status: "failed",
129
156
  message,
130
157
  });
158
+ let recorded = false;
131
159
  try {
132
160
  await recordTaskPoolRun({
133
161
  repoRoot: options.repoRoot,
@@ -142,18 +170,23 @@ export async function runReadyTasks(options) {
142
170
  error: message,
143
171
  failure,
144
172
  ...(retryOfWorkerRunId ? { retryOfWorkerRunId } : {}),
173
+ ...(controllerIdentity ? { controllerIdentity } : {}),
145
174
  },
146
175
  });
176
+ recorded = true;
147
177
  }
148
178
  catch (recordError) {
179
+ await options.onTaskFinalized?.({ status: "run-error", taskSpec, workerRunId });
149
180
  tasks.push({
150
181
  taskId,
151
182
  workerRunId,
152
183
  status: "record-error",
153
184
  error: errorMessage(recordError),
154
185
  });
155
- continue;
186
+ break;
156
187
  }
188
+ if (recorded)
189
+ await options.onTaskFinalized?.({ status: "run-error", taskSpec, workerRunId });
157
190
  tasks.push({
158
191
  taskId,
159
192
  workerRunId,
@@ -206,9 +239,12 @@ export async function runReadyTasks(options) {
206
239
  ...(failure ? { failure } : {}),
207
240
  ...(retryOfWorkerRunId ? { retryOfWorkerRunId } : {}),
208
241
  ...(result.failureArtifacts ? { failureArtifacts: result.failureArtifacts } : {}),
242
+ ...(controllerIdentity ? { controllerIdentity } : {}),
209
243
  };
244
+ let recorded = false;
210
245
  try {
211
246
  await recordTaskPoolRun({ repoRoot: options.repoRoot, run });
247
+ recorded = true;
212
248
  tasks.push({
213
249
  taskId,
214
250
  workerRunId: result.workerRunId,
@@ -217,6 +253,7 @@ export async function runReadyTasks(options) {
217
253
  });
218
254
  }
219
255
  catch (error) {
256
+ await options.onTaskFinalized?.({ status: result.status, taskSpec, workerRunId: result.workerRunId, runRecordPath: result.runRecordPath });
220
257
  tasks.push({
221
258
  taskId,
222
259
  workerRunId: result.workerRunId,
@@ -224,7 +261,10 @@ export async function runReadyTasks(options) {
224
261
  error: errorMessage(error),
225
262
  runRecordPath: result.runRecordPath,
226
263
  });
264
+ break;
227
265
  }
266
+ if (recorded)
267
+ await options.onTaskFinalized?.({ status: result.status, taskSpec, workerRunId: result.workerRunId, runRecordPath: result.runRecordPath });
228
268
  }
229
269
  const summary = summarize(tasks);
230
270
  const batchRunPath = path.join(getTaskPoolRoot(options.repoRoot), "artifacts", batchRunId, "batch-run.json");
@@ -237,6 +277,7 @@ export async function runReadyTasks(options) {
237
277
  summary,
238
278
  tasks,
239
279
  batchRunPath,
280
+ ...(controllerIdentity ? { controllerIdentity } : {}),
240
281
  };
241
282
  await mkdir(path.dirname(batchRunPath), { recursive: true });
242
283
  await writeFile(batchRunPath, `${JSON.stringify(output, null, 2)}\n`, "utf-8");
@@ -260,18 +301,55 @@ function emit(progress, input) {
260
301
  export function buildBatchRunId(now) {
261
302
  return `batch-${now.toISOString().replace(/[-:.]/g, "").slice(0, 15)}`;
262
303
  }
263
- export function buildStableWorkerRunId(taskId, taskSpec, now) {
304
+ export function buildStableWorkerRunId(taskId, taskSpec, now, controllerIdentity) {
264
305
  const date = now.toISOString().slice(0, 10).replace(/-/g, "");
265
306
  const slug = taskId
266
307
  .toLowerCase()
267
308
  .replace(/[^a-z0-9]+/g, "-")
268
309
  .replace(/^-+|-+$/g, "");
269
310
  const hash = createHash("sha256")
270
- .update(`${taskSpec.feature_id}:${taskSpec.id}:${taskSpec.title}`)
311
+ .update([
312
+ taskSpec.feature_id,
313
+ taskSpec.id,
314
+ taskSpec.title,
315
+ ...(controllerIdentity
316
+ ? [
317
+ controllerIdentity.packageName,
318
+ controllerIdentity.packageVersion,
319
+ controllerIdentity.binarySha256,
320
+ controllerIdentity.packageFingerprint.value,
321
+ ]
322
+ : []),
323
+ ].join(":"))
271
324
  .digest("hex")
272
325
  .slice(0, 10);
273
326
  return `wr-${date}-${slug}-${hash}`;
274
327
  }
328
+ function buildControllerScopedWorkerRunId(baseWorkerRunId, controllerIdentity) {
329
+ const hash = createHash("sha256")
330
+ .update(controllerIdentity
331
+ ? JSON.stringify({
332
+ packageName: controllerIdentity.packageName,
333
+ binName: controllerIdentity.binName,
334
+ packageVersion: controllerIdentity.packageVersion,
335
+ binarySha256: controllerIdentity.binarySha256,
336
+ packageFingerprint: controllerIdentity.packageFingerprint.value,
337
+ requested: controllerIdentity.requested,
338
+ entry: controllerIdentity.entry,
339
+ realEntry: controllerIdentity.realEntry,
340
+ launch: controllerIdentity.launch,
341
+ packageRoot: controllerIdentity.packageRoot,
342
+ })
343
+ : "legacy-unpinned-controller")
344
+ .digest("hex")
345
+ .slice(0, 10);
346
+ return `${baseWorkerRunId}-controller-${hash}`;
347
+ }
348
+ function controllerRunCanBeReused(existing, controllerIdentity) {
349
+ if (!existing.controllerIdentity && !controllerIdentity)
350
+ return true;
351
+ return controllerIdentitiesMatch(existing.controllerIdentity, controllerIdentity);
352
+ }
275
353
  /** A retry is a distinct execution attempt and must not collide with failed evidence. */
276
354
  export function buildRetryWorkerRunId(taskId, previousWorkerRunId, now) {
277
355
  const date = now.toISOString().slice(0, 10).replace(/-/g, "");
@@ -97,7 +97,6 @@ export const taskSpecSchema = z
97
97
  profile_policy: z.literal("mapped"),
98
98
  strict_models: z.boolean().optional().default(true),
99
99
  strict_governance: z.boolean().optional().default(true),
100
- no_cursor: z.boolean().optional().default(false),
101
100
  max_concurrent: z.number().int().positive().optional().default(1),
102
101
  })
103
102
  .strict(),
@@ -5,7 +5,7 @@ import { freshNodeRecord } from "../dynamic-runtime/shared.js";
5
5
  const CONVERGENCE_CHAIN_NODE_IDS = [
6
6
  "process-supervisor-pi",
7
7
  "process-gate-shell",
8
- "repair-cursor",
8
+ "repair-pi",
9
9
  "hard-verify-shell",
10
10
  ];
11
11
  const CONVERGENCE_NON_RETRY_FAILURES = new Set([
@@ -1,4 +1,3 @@
1
- import { executeDagCursorNode } from "../../executors/dag-cursor-executor.js";
2
1
  import { executeDagPiNode } from "../../executors/dag-pi-executor.js";
3
2
  import { executeDagShellNode } from "../../executors/shell-executor.js";
4
3
  import { executeDagStaticNode } from "../../executors/dag-static-executor.js";
@@ -9,7 +8,6 @@ function requireModel(input) {
9
8
  return input;
10
9
  }
11
10
  export const DEFAULT_DAG_EXECUTOR_REGISTRY = {
12
- cursor: (input, meta) => executeDagCursorNode(requireModel(input), meta),
13
11
  pi: (input, meta) => executeDagPiNode(requireModel(input), meta),
14
12
  shell: executeDagShellNode,
15
13
  static: executeDagStaticNode,