@tea-agent/loop-agent 0.10.0 → 0.12.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 (207) hide show
  1. package/AGENTS.md +10 -2
  2. package/CHANGELOG.md +91 -24
  3. package/README.md +84 -12
  4. package/dist/application/dag/args.js +1 -12
  5. package/dist/application/dag/generate-task-dag.js +38 -2
  6. package/dist/application/dag/run-dag.js +11 -27
  7. package/dist/application/dag/validate-dag.js +13 -2
  8. package/dist/application/loop/run-action.js +0 -4
  9. package/dist/cli/command-definitions.js +44 -16
  10. package/dist/cli/program.js +40 -23
  11. package/dist/cli/update/notifier.js +117 -0
  12. package/dist/cli/update/npm-client.js +151 -0
  13. package/dist/cli/update/policy.js +58 -0
  14. package/dist/cli/update/state.js +68 -0
  15. package/dist/cli.js +33 -0
  16. package/dist/commands/cursor-prompt.js +42 -82
  17. package/dist/commands/dag-approve.js +36 -0
  18. package/dist/commands/delegate.js +75 -77
  19. package/dist/commands/doctor.js +0 -18
  20. package/dist/commands/init.js +547 -95
  21. package/dist/commands/instructions.js +7 -10
  22. package/dist/commands/loop.js +4 -20
  23. package/dist/commands/plan.js +50 -0
  24. package/dist/executors/config-core.js +0 -51
  25. package/dist/executors/dag-pi-executor.js +1 -1
  26. package/dist/executors/dag.js +0 -1
  27. package/dist/executors/index.js +0 -2
  28. package/dist/executors/model-routing.js +9 -9
  29. package/dist/executors/shell-executor.js +1 -1
  30. package/dist/governance/checks.js +6 -3
  31. package/dist/governance/exec-plans.js +545 -0
  32. package/dist/governance/manifest-types.js +24 -2
  33. package/dist/infrastructure/harness/loop-action-store.js +0 -3
  34. package/dist/records/harvest.js +2 -23
  35. package/dist/records/one-shot-runs.js +1 -1
  36. package/dist/shared/artifacts-core.js +24 -5
  37. package/dist/shared/output-truncation.js +37 -0
  38. package/dist/shared/package-metadata.js +488 -0
  39. package/dist/{executors/cursor-executor.js → sidecars/cursor-prompt/executor.js} +2 -42
  40. package/dist/sidecars/cursor-prompt/index.js +3 -0
  41. package/dist/sidecars/cursor-prompt/stream.js +121 -0
  42. package/dist/task/config-types.js +29 -12
  43. package/dist/task/delegate.js +9 -21
  44. package/dist/task/runtime.js +1 -2
  45. package/dist/worker/cli.js +32 -3
  46. package/dist/worker/delivery/final-verification.js +47 -11
  47. package/dist/worker/delivery/package.js +63 -10
  48. package/dist/worker/feature/run.js +60 -8
  49. package/dist/worker/loop-agent/loop-agent-client.js +329 -126
  50. package/dist/worker/observability/event-history.js +216 -0
  51. package/dist/worker/observability/read-model.js +338 -83
  52. package/dist/worker/observe/paths.js +17 -0
  53. package/dist/worker/observe/routes.js +165 -21
  54. package/dist/worker/observe/server.js +59 -1
  55. package/dist/worker/observe/static/api.js +27 -0
  56. package/dist/worker/observe/static/app.js +120 -2317
  57. package/dist/worker/observe/static/constants.js +148 -0
  58. package/dist/worker/observe/static/copy.js +67 -0
  59. package/dist/worker/observe/static/dag-helpers.js +172 -0
  60. package/dist/worker/observe/static/dag-model.js +72 -0
  61. package/dist/worker/observe/static/dom.js +61 -0
  62. package/dist/worker/observe/static/format-pool.js +67 -0
  63. package/dist/worker/observe/static/format.js +292 -0
  64. package/dist/worker/observe/static/index.html +300 -82
  65. package/dist/worker/observe/static/kpi.js +94 -0
  66. package/dist/worker/observe/static/relations.js +128 -0
  67. package/dist/worker/observe/static/router.js +85 -0
  68. package/dist/worker/observe/static/run-processing.js +148 -0
  69. package/dist/worker/observe/static/shell-chrome.js +68 -0
  70. package/dist/worker/observe/static/state.js +253 -0
  71. package/dist/worker/observe/static/styles.css +1720 -495
  72. package/dist/worker/observe/static/views/batch.js +226 -0
  73. package/dist/worker/observe/static/views/dag-graph.js +172 -0
  74. package/dist/worker/observe/static/views/dag-inspector.js +477 -0
  75. package/dist/worker/observe/static/views/dag.js +362 -0
  76. package/dist/worker/observe/static/views/dashboard.js +442 -0
  77. package/dist/worker/observe/static/views/failures.js +143 -0
  78. package/dist/worker/observe/static/views/feature.js +453 -0
  79. package/dist/worker/observe/static/views/pool.js +347 -0
  80. package/dist/worker/observe/static/views/run.js +453 -0
  81. package/dist/worker/observe/static/views/session-timeline.js +205 -0
  82. package/dist/worker/observe/static/views/shell.js +7 -0
  83. package/dist/worker/observe/static/views/task.js +260 -0
  84. package/dist/worker/observe/static/views/timeline.js +163 -0
  85. package/dist/worker/preflight.js +49 -1
  86. package/dist/worker/run-task/run-task.js +22 -12
  87. package/dist/worker/runner/run-ready.js +76 -12
  88. package/dist/worker/task-spec/schema.js +0 -1
  89. package/dist/workflows/dag/controller-identity.js +104 -0
  90. package/dist/workflows/dag/convergence/controller.js +1 -1
  91. package/dist/workflows/dag/executor-registry.js +0 -2
  92. package/dist/workflows/dag/init-hybrid.js +797 -27
  93. package/dist/workflows/dag/node-execution.js +183 -35
  94. package/dist/workflows/dag/repair-artifact.js +91 -0
  95. package/dist/workflows/dag/report.js +50 -0
  96. package/dist/workflows/dag/retry-policy.js +138 -0
  97. package/dist/workflows/dag/runner.js +77 -17
  98. package/dist/workflows/dag/runtime-contract.js +87 -0
  99. package/dist/workflows/dag/scheduler.js +7 -2
  100. package/dist/workflows/dag/sdd-embedded.js +128 -0
  101. package/dist/workflows/dag/skill-instructions.js +5 -4
  102. package/dist/workflows/dag/skill-snapshot.js +529 -0
  103. package/dist/workflows/dag/types.js +86 -10
  104. package/dist/workflows/dag/validate.js +73 -12
  105. package/dist/workflows/loop/actions/dag-action.js +0 -2
  106. package/dist/workflows/loop/actions/shared.js +1 -1
  107. package/dist/workflows/loop/actions.js +14 -31
  108. package/dist/workflows/loop/benchmark.js +1 -1
  109. package/dist/workflows/loop/index.js +1 -1
  110. package/dist/workflows/loop/policy/auto-policy.js +22 -14
  111. package/dist/workflows/loop/policy/path-patterns.js +13 -0
  112. package/docs/README.md +36 -33
  113. package/docs/agent-dag-recovery-playbook.md +1 -1
  114. package/docs/agent-dag-runner.md +28 -3
  115. package/docs/architecture/README.md +26 -0
  116. package/docs/architecture/dag-execution.md +140 -0
  117. package/docs/architecture/evolution.md +53 -0
  118. package/docs/architecture/facts-and-state.md +58 -0
  119. package/docs/architecture/runtime-boundaries.md +45 -17
  120. package/docs/architecture/system-overview.md +93 -0
  121. package/docs/architecture/worker-and-feature.md +81 -0
  122. package/docs/cursor-prompt-sidecar.md +36 -0
  123. package/docs/decisions/README.md +13 -1
  124. package/docs/design/README.md +43 -21
  125. package/docs/development-principles.md +2 -2
  126. package/docs/exec-plans/active/README.md +1 -3
  127. package/docs/exec-plans/completed/README.md +23 -0
  128. package/docs/feature-workflow.md +78 -4
  129. package/docs/harness-methodology-debugging.md +1 -1
  130. package/docs/harness-methodology-tdd.md +3 -3
  131. package/docs/init-surface.manifest.json +60 -25
  132. package/docs/loop-agent-harness.md +28 -4
  133. package/docs/progress/README.md +50 -1
  134. package/docs/reports/README.md +90 -18
  135. package/docs/skills/README.md +2 -1
  136. package/docs/skills/vetted-skill-registry.md +2 -1
  137. package/docs/templates/agent-dag-report.schema.json +23 -6
  138. package/docs/templates/agent-dag.base.json +0 -5
  139. package/docs/templates/agent-dag.final-verification.json +0 -5
  140. package/docs/templates/agent-dag.schema.json +70 -3
  141. package/docs/templates/agent-dag.supervised-implementation.json +9 -8
  142. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +139 -0
  143. package/docs/templates/backend-test-dag.json +276 -0
  144. package/docs/templates/backend-test-dag.retrospect.prompt.md +125 -0
  145. package/docs/templates/backend-test-dag.review-cases.prompt.md +81 -0
  146. package/docs/templates/frontend-design-contract.md +33 -0
  147. package/docs/templates/frontend-task-constraints.md +25 -0
  148. package/docs/templates/frontend-task-requirement.md +61 -0
  149. package/docs/templates/harness.schema.json +10 -12
  150. package/docs/templates/hybrid-dag.json +1 -6
  151. package/docs/templates/interactive-ui-round2-experiment.md +1 -1
  152. package/docs/templates/product-line/task.yaml +0 -1
  153. package/docs/templates/project-start-checklist.md +2 -2
  154. package/docs/templates/worker-dogfood-evidence.md +28 -0
  155. package/docs/templates/worker-dogfood-setup.md +20 -0
  156. package/docs/verification-matrix.md +10 -0
  157. package/examples/decision-gate-agent-dag.json +87 -33
  158. package/examples/example-dag.json +0 -5
  159. package/examples/hybrid-loop-agent-dag.json +0 -5
  160. package/harness.json +7 -15
  161. package/package.json +22 -46
  162. package/scripts/check-product-line-docs.sh +10 -7
  163. package/skills/agent-worker/SKILL.md +37 -0
  164. package/skills/agent-worker/references/agent-worker-operator.md +43 -0
  165. package/skills/frontend-design-review/SKILL.md +59 -0
  166. package/skills/frontend-design-review/references/review-checklist.md +37 -0
  167. package/skills/frontend-implementation/SKILL.md +51 -0
  168. package/skills/frontend-implementation/references/code-standards.md +34 -0
  169. package/skills/frontend-implementation/references/design-spec.md +46 -0
  170. package/skills/frontend-implementation/references/node-contracts.md +32 -0
  171. package/skills/frontend-review/SKILL.md +53 -0
  172. package/skills/frontend-review/references/review-findings.md +42 -0
  173. package/skills/frontend-verification/SKILL.md +40 -0
  174. package/skills/frontend-verification/references/verification-checklist.md +56 -0
  175. package/skills/grill-me/SKILL.md +10 -0
  176. package/skills/grill-with-docs/SKILL.md +88 -0
  177. package/skills/grill-with-docs/adr-format.md +47 -0
  178. package/skills/grill-with-docs/context-format.md +60 -0
  179. package/skills/loop-agent/SKILL.md +11 -9
  180. package/skills/loop-agent/references/command-reference.md +14 -15
  181. package/skills/loop-agent/references/docs-converge.md +126 -0
  182. package/skills/loop-agent/references/harness-policy.md +7 -7
  183. package/skills/loop-agent/references/hybrid-dag.md +36 -20
  184. package/skills/loop-agent/references/long-running-loop.md +4 -6
  185. package/skills/loop-agent/references/multi-worktree.md +6 -6
  186. package/skills/loop-agent/references/orchestrator-and-interventions.md +3 -3
  187. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +14 -11
  188. package/skills/loop-agent/references/task-workflow.md +1 -1
  189. package/skills/loop-agent/references/verification-and-failure-handling.md +6 -0
  190. package/skills/using-git-worktrees/SKILL.md +215 -0
  191. package/dist/commands/cursor-worker.js +0 -43
  192. package/dist/cursor-worker-entry.js +0 -8
  193. package/dist/executors/cursor-artifacts.js +0 -33
  194. package/dist/executors/cursor-execution-log.js +0 -81
  195. package/dist/executors/cursor-executor-artifacts.js +0 -134
  196. package/dist/executors/cursor-run.js +0 -115
  197. package/dist/executors/cursor-tool.js +0 -94
  198. package/dist/executors/cursor-worker-client.js +0 -223
  199. package/dist/executors/cursor-worker-protocol.js +0 -18
  200. package/dist/executors/cursor-worker-server.js +0 -54
  201. package/dist/executors/cursor-worker.js +0 -3
  202. package/dist/executors/cursor.js +0 -6
  203. package/dist/executors/dag-cursor-executor.js +0 -87
  204. package/dist/workflows/loop/actions/cursor-fix.js +0 -191
  205. package/dist/workflows/loop/policy/cursor-fix-policy.js +0 -31
  206. package/docs/cursor-executor-usage.md +0 -25
  207. package/docs/dynamic-workflow-dag-engine-roadmap.md +0 -1749
@@ -0,0 +1,121 @@
1
+ import { bestEffortCancelRun, buildCursorPrompt, classifyCursorFailure, mapRunStatusToExecutionStatus, } from "./executor.js";
2
+ async function createCursorStreamAgent(input) {
3
+ const { Agent } = await import("@cursor/sdk");
4
+ return Agent.create({
5
+ apiKey: input.apiKey,
6
+ name: "pi-cursor-prompt",
7
+ model: { id: input.model },
8
+ local: { cwd: input.cwd },
9
+ });
10
+ }
11
+ /**
12
+ * One-shot streaming path for cursor-prompt CLI. Loads @cursor/sdk only here.
13
+ */
14
+ export async function executeCursorPromptStream(input) {
15
+ const apiKey = input.apiKey ?? process.env.CURSOR_API_KEY;
16
+ if (!apiKey?.trim()) {
17
+ return {
18
+ ok: false,
19
+ status: "failed",
20
+ failureCategory: "missing-api-key",
21
+ model: input.model,
22
+ durationMs: 0,
23
+ stdout: "",
24
+ stderr: "CURSOR_API_KEY environment variable is not set.",
25
+ details: { startupError: true },
26
+ };
27
+ }
28
+ const prompt = buildCursorPrompt(input.task, { runDir: input.runDir });
29
+ const startedAt = Date.now();
30
+ let timeoutHandle;
31
+ let agent;
32
+ let run;
33
+ let timedOut = false;
34
+ try {
35
+ agent = await (input.agentFactory ?? createCursorStreamAgent)({
36
+ apiKey,
37
+ cwd: input.cwd,
38
+ model: input.model,
39
+ });
40
+ const runPromise = (async () => {
41
+ run = await agent.send(prompt);
42
+ for await (const event of run.stream()) {
43
+ if (event.type === "assistant") {
44
+ for (const block of event.message.content) {
45
+ if (block.type === "text" && block.text) {
46
+ input.callbacks?.onAssistantText?.(block.text);
47
+ }
48
+ }
49
+ }
50
+ }
51
+ return run.wait();
52
+ })();
53
+ const timeoutRace = new Promise((resolve) => {
54
+ timeoutHandle = setTimeout(() => resolve("timeout"), input.timeoutMs);
55
+ });
56
+ const raced = await Promise.race([
57
+ runPromise.then((value) => ({ kind: "done", value })),
58
+ timeoutRace.then(() => ({ kind: "timeout" })),
59
+ ]);
60
+ if (raced.kind === "timeout") {
61
+ timedOut = true;
62
+ await bestEffortCancelRun(run);
63
+ return {
64
+ ok: false,
65
+ status: "timeout",
66
+ failureCategory: "timeout",
67
+ model: input.model,
68
+ durationMs: Date.now() - startedAt,
69
+ stdout: "",
70
+ stderr: `Cursor execution timed out after ${input.timeoutMs}ms`,
71
+ details: { timedOut: true, timeoutKind: "total" },
72
+ };
73
+ }
74
+ const sdkResult = raced.value;
75
+ const runStatus = String(sdkResult.status ?? "");
76
+ const status = mapRunStatusToExecutionStatus(runStatus, false);
77
+ const ok = status === "completed";
78
+ return {
79
+ ok,
80
+ status,
81
+ failureCategory: ok
82
+ ? "success"
83
+ : classifyCursorFailure({ runStatus, stderr: "", stdout: "" }),
84
+ model: input.model,
85
+ durationMs: sdkResult.durationMs ?? Date.now() - startedAt,
86
+ stdout: "",
87
+ stderr: "",
88
+ };
89
+ }
90
+ catch (error) {
91
+ const message = error instanceof Error ? error.message : String(error);
92
+ return {
93
+ ok: false,
94
+ status: "failed",
95
+ failureCategory: classifyCursorFailure({
96
+ startupError: message,
97
+ stderr: message,
98
+ }),
99
+ model: input.model,
100
+ durationMs: Date.now() - startedAt,
101
+ stdout: "",
102
+ stderr: message,
103
+ details: { startupError: true },
104
+ };
105
+ }
106
+ finally {
107
+ if (timeoutHandle)
108
+ clearTimeout(timeoutHandle);
109
+ if (!timedOut && run?.status === "running") {
110
+ await bestEffortCancelRun(run);
111
+ }
112
+ if (agent) {
113
+ try {
114
+ await agent[Symbol.asyncDispose]();
115
+ }
116
+ catch {
117
+ // The one-shot result remains authoritative; disposal is best effort.
118
+ }
119
+ }
120
+ }
121
+ }
@@ -1,5 +1,4 @@
1
1
  import { z } from "zod";
2
- import { taskExecutorSchema } from "../governance/manifest-types.js";
3
2
  export const taskFlowSchema = z.enum([
4
3
  "auto",
5
4
  "micro",
@@ -8,7 +7,12 @@ export const taskFlowSchema = z.enum([
8
7
  "loop",
9
8
  "feature-study",
10
9
  ]);
11
- export const taskKindSchema = z.enum(["standard", "feature-study"]);
10
+ export const taskKindSchema = z.enum([
11
+ "standard",
12
+ "feature-study",
13
+ "frontend-implementation",
14
+ "backend-test",
15
+ ]);
12
16
  export const referenceRepoConfigSchema = z.object({
13
17
  name: z.string().min(1),
14
18
  path: z.string().min(1),
@@ -40,11 +44,13 @@ export const dagVerifyStrategySchema = z
40
44
  .default("adapter"),
41
45
  })
42
46
  .optional();
43
- export const loopAutoWritePolicySchema = z.enum([
47
+ export const loopAutoExecutionPolicySchema = z.enum([
44
48
  "off",
45
49
  "approval-required",
46
50
  "enabled",
47
51
  ]);
52
+ export const LOOP_AUTO_WRITE_POLICY_REMOVED_ERROR = "task field loopAutoWritePolicy is no longer supported; use loopAutoExecutionPolicy (off | approval-required | enabled)";
53
+ export const CURSOR_TASK_FIELD_REMOVED_ERROR = 'task fields "executor" and "cursorModel" are no longer supported; governed runtime is Pi-only';
48
54
  export const convergenceConfigSchema = z.object({
49
55
  enabled: z.boolean().optional().default(false),
50
56
  maxPasses: z.number().int().positive().optional().default(3),
@@ -52,11 +58,11 @@ export const convergenceConfigSchema = z.object({
52
58
  stopOnHardVerifyPass: z.boolean().optional().default(true),
53
59
  pauseOnRegression: z.boolean().optional().default(true),
54
60
  });
55
- export const taskConfigSchema = z.object({
61
+ const taskConfigObjectSchema = z.object({
56
62
  taskId: z.string(),
57
63
  title: z.string(),
58
64
  sourceFiles: z.array(z.string()),
59
- /** standard: 本仓库需求实现;feature-study: 参考外部代码特性并在目标仓库落地 */
65
+ /** standard: 本仓库需求实现;feature-study: 参考外部代码特性并在目标仓库落地;frontend-implementation: 使用前端实现 DAG 模板 */
60
66
  taskKind: taskKindSchema.optional().default("standard"),
61
67
  referenceRepos: z.array(referenceRepoConfigSchema).optional().default([]),
62
68
  referenceDocs: z.array(referenceDocConfigSchema).optional().default([]),
@@ -67,7 +73,7 @@ export const taskConfigSchema = z.object({
67
73
  hardConstraints: z.array(z.string()).optional().default([]),
68
74
  autoCommitAfterVerify: z.boolean().optional().default(true),
69
75
  autoCommitMessage: z.string().optional().default(""),
70
- /** Explicit reason that a medium/large task could not use DAG before loop cursor-fix. */
76
+ /** Explicit reason that a medium/large task could not use DAG before loop auto execution. */
71
77
  dagFallbackReason: z.string().optional(),
72
78
  /** full: attach governance bundle; slim: analyze/plan 只带最少仓库上下文(source + harness 等) */
73
79
  contextProfile: contextProfileSchema.optional().default("full"),
@@ -90,8 +96,10 @@ export const taskConfigSchema = z.object({
90
96
  maxFixLoops: z.number().int().min(0).optional().default(2),
91
97
  /** Supervised DAG convergence is opt-in until runtime smoke evidence is stronger. */
92
98
  convergence: convergenceConfigSchema.optional().default({ enabled: false }),
93
- /** Outer loop auto mode never writes by default; cursor-fix requires this policy plus bounded path guards. */
94
- loopAutoWritePolicy: loopAutoWritePolicySchema.optional().default("off"),
99
+ /** Outer loop auto mode never writes by default; DAG execute requires this policy plus path/writeSet gates. */
100
+ loopAutoExecutionPolicy: loopAutoExecutionPolicySchema
101
+ .optional()
102
+ .default("off"),
95
103
  requireRetrospective: z.boolean().optional().default(false),
96
104
  /** inherit: use host shell env; clean: sanitized env for deterministic verify */
97
105
  verifyEnv: z.enum(["inherit", "clean"]).optional().default("clean"),
@@ -99,9 +107,18 @@ export const taskConfigSchema = z.object({
99
107
  maxGoalContinuationsPerRun: z.number().int().positive().optional().default(5),
100
108
  /** Pi subagent assisted mode: 'off' (default), 'analyze-plan', or 'full' */
101
109
  piSubagentMode: piSubagentModeSchema.optional().default("off"),
102
- /** Leaf executor: default pi; cursor requires delegate + worktree isolation */
103
- executor: taskExecutorSchema.optional().default("pi"),
104
- /** Cursor model override (e.g. gpt-5.5); falls back to harness executors.cursor.defaultModel */
105
- cursorModel: z.string().optional(),
106
110
  notes: z.string().optional().default(""),
107
111
  });
112
+ export const taskConfigSchema = z.preprocess((raw) => {
113
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
114
+ return raw;
115
+ }
116
+ const obj = raw;
117
+ if ("loopAutoWritePolicy" in obj) {
118
+ throw new Error(LOOP_AUTO_WRITE_POLICY_REMOVED_ERROR);
119
+ }
120
+ if ("executor" in obj || "cursorModel" in obj) {
121
+ throw new Error(CURSOR_TASK_FIELD_REMOVED_ERROR);
122
+ }
123
+ return raw;
124
+ }, taskConfigObjectSchema);
@@ -9,31 +9,24 @@ import { getTaskPaths, loadTaskConfig } from './runtime.js';
9
9
  import { copyDir } from '../records/harvest.js';
10
10
  import { saveWorkflowState } from './state.js';
11
11
  import { workflowStateSchema } from '../shared/types.js';
12
- import { assertCursorExecutorAvailable, resolveTaskExecutor, validateCursorTaskPreflight } from '../executors/config-core.js';
13
- import { resolveDelegateExecutionMode } from '../executors/cursor-execution-log.js';
14
12
  /**
15
13
  * Atomic delegation: validate source → check conflicts → create worktree →
16
14
  * sync source → optionally symlink node_modules.
17
15
  *
18
- * Execution runs in-process for Cursor SDK; Pi worktrees are prepared for manual DAG follow-up.
16
+ * Default is isolation only; callers may run Pi DAG via application use cases.
19
17
  */
20
18
  export async function delegateTask(repoRoot, manifest, taskId, opts = {}) {
21
19
  const symlinkEnabled = opts.symlinkNodeModules !== false;
22
20
  await validateSourceMaterials(repoRoot, taskId);
23
- const taskConfig = await loadTaskConfig(repoRoot, taskId);
21
+ await loadTaskConfig(repoRoot, taskId);
24
22
  const effectiveManifest = manifest ?? (await loadHarnessManifest(repoRoot));
25
- const executor = resolveTaskExecutor(taskConfig, opts.executor);
26
- if (executor === 'cursor') {
27
- assertCursorExecutorAvailable(effectiveManifest);
28
- validateCursorTaskPreflight(taskConfig);
29
- }
30
23
  await validateNoConflicts(repoRoot, taskId, opts.branch);
31
24
  const worktree = await createWorktree(repoRoot, effectiveManifest, {
32
25
  taskId,
33
26
  branch: opts.branch,
34
27
  base: opts.base,
35
28
  });
36
- await syncSourceToWorktree(repoRoot, taskId, worktree.path, executor);
29
+ await syncSourceToWorktree(repoRoot, taskId, worktree.path);
37
30
  let symlinkedNodeModules = false;
38
31
  if (symlinkEnabled) {
39
32
  symlinkedNodeModules = await setupNodeModulesSymlink(repoRoot, worktree.path);
@@ -46,13 +39,9 @@ export async function delegateTask(repoRoot, manifest, taskId, opts = {}) {
46
39
  branch: worktree.branch,
47
40
  baseBranch: worktree.base,
48
41
  symlinkedNodeModules,
49
- executor,
50
- executionMode: resolveDelegateExecutionMode(executor),
42
+ executionMode: 'pi-worktree',
51
43
  };
52
44
  }
53
- // ---------------------------------------------------------------------------
54
- // Internal helpers
55
- // ---------------------------------------------------------------------------
56
45
  async function validateSourceMaterials(repoRoot, taskId) {
57
46
  const { taskConfigPath, sourceDir } = getTaskPaths(repoRoot, taskId);
58
47
  try {
@@ -96,7 +85,7 @@ async function validateNoConflicts(repoRoot, taskId, branch) {
96
85
  console.error(`[delegate] warning: branch "${targetBranch}" already exists; will reuse it for the worktree.`);
97
86
  }
98
87
  }
99
- async function syncSourceToWorktree(repoRoot, taskId, worktreePath, executor) {
88
+ async function syncSourceToWorktree(repoRoot, taskId, worktreePath) {
100
89
  const srcPaths = getTaskPaths(repoRoot, taskId);
101
90
  const destPaths = getTaskPaths(worktreePath, taskId);
102
91
  const destTaskDir = destPaths.taskDir;
@@ -106,16 +95,15 @@ async function syncSourceToWorktree(repoRoot, taskId, worktreePath, executor) {
106
95
  await copyDir(srcPaths.sourceDir, destSourceDir);
107
96
  const rawJson = await readFile(srcPaths.taskConfigPath, 'utf-8');
108
97
  const parsedTaskConfig = JSON.parse(rawJson);
109
- parsedTaskConfig.executor = executor;
110
98
  await mkdir(path.dirname(destTaskConfigPath), { recursive: true });
111
99
  await writeTaskConfig(worktreePath, taskId, parsedTaskConfig);
112
100
  await mkdir(destPaths.logsDir, { recursive: true });
113
101
  await initializeArtifacts(destTaskDir);
114
102
  await initializeTaskLogStubs(worktreePath, taskId);
115
- const state = await loadOrCreateWorkflowState(srcPaths.statePath, taskId, executor);
103
+ const state = await loadOrCreateWorkflowState(srcPaths.statePath, taskId);
116
104
  await saveWorkflowState(destPaths.statePath, state);
117
105
  }
118
- async function loadOrCreateWorkflowState(statePath, taskId, executor) {
106
+ async function loadOrCreateWorkflowState(statePath, taskId) {
119
107
  try {
120
108
  const raw = await readFile(statePath, 'utf-8');
121
109
  return workflowStateSchema.parse(JSON.parse(raw));
@@ -129,8 +117,8 @@ async function loadOrCreateWorkflowState(statePath, taskId, executor) {
129
117
  completedSteps: [],
130
118
  lastUpdated: now,
131
119
  executor: {
132
- name: executor,
133
- mode: executor === 'cursor' ? 'sdk' : 'json',
120
+ name: 'pi',
121
+ mode: 'json',
134
122
  },
135
123
  artifacts: {
136
124
  analysis: false,
@@ -117,11 +117,10 @@ export async function createTask(repoRoot, taskId, title) {
117
117
  stopOnHardVerifyPass: true,
118
118
  pauseOnRegression: true,
119
119
  },
120
- loopAutoWritePolicy: 'off',
120
+ loopAutoExecutionPolicy: 'off',
121
121
  verifyEnv: 'clean',
122
122
  maxGoalContinuationsPerRun: 5,
123
123
  piSubagentMode: 'off',
124
- executor: 'pi',
125
124
  notes: '',
126
125
  };
127
126
  const state = {
@@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { Command } from "commander";
6
+ import { readPackageVersion } from "../shared/package-metadata.js";
6
7
  import YAML from "yaml";
7
8
  import { LoopAgentClient } from "./loop-agent/loop-agent-client.js";
8
9
  import { resolveLoopAgentProfile } from "./profile-mapping.js";
@@ -31,6 +32,7 @@ export function buildAgentWorkerProgram() {
31
32
  program
32
33
  .name("agent-worker")
33
34
  .description("Local product-line worker utilities for TaskSpec validation")
35
+ .version(readPackageVersion(path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))))) ?? "0.0.0", "-V, --version", "display version")
34
36
  .showHelpAfterError()
35
37
  .showSuggestionAfterError();
36
38
  const task = program.command("task").description("TaskSpec utilities");
@@ -66,6 +68,8 @@ export function buildAgentWorkerProgram() {
66
68
  .option("--check-repo-command <command...>", "Override the check-repo preflight command")
67
69
  .option("--quiet", "Suppress human-readable progress on stderr")
68
70
  .option("--pi-model <model>", "Smoke override for pi executor nodes")
71
+ .option("--expected-controller-version <version>", "Exact expected controller version (fail before writes if mismatched)")
72
+ .option("--expected-controller-fingerprint <value>", "Exact expected controller fingerprint sha256:<hex> (fail before writes if mismatched)")
69
73
  .description("Validate and advance one Ready Feature task through the existing Worker pipeline")
70
74
  .action(async (options) => {
71
75
  if (options.gitMode !== "none" && options.gitMode !== "checkpoint")
@@ -74,9 +78,11 @@ export function buildAgentWorkerProgram() {
74
78
  throw new Error("--keep-failed-diff requires --git-mode checkpoint");
75
79
  const repoRoot = path.resolve(options.repo);
76
80
  const batchRunId = options.batchRunId ?? buildBatchRunId(new Date());
81
+ const expectedIdentity = buildIdentityExpectation(options);
77
82
  const client = new LoopAgentClient({
78
83
  loopAgentBin: options.loopAgentBin,
79
84
  artifactRoot: path.join(getTaskPoolRoot(repoRoot), "artifacts", batchRunId),
85
+ resolveIdentity: true,
80
86
  });
81
87
  let progress;
82
88
  try {
@@ -102,6 +108,8 @@ export function buildAgentWorkerProgram() {
102
108
  ...(options.piModel ? { piModel: options.piModel } : {}),
103
109
  gitMode: options.gitMode === "checkpoint" ? "checkpoint" : "none",
104
110
  keepFailedDiff: options.keepFailedDiff ?? false,
111
+ controllerIdentity: client.getIdentity(),
112
+ controllerExpectation: expectedIdentity,
105
113
  });
106
114
  process.stdout.write(options.json ? `${JSON.stringify(result, null, 2)}\n` : renderFeatureRun(result));
107
115
  if (result.status === "failed" || result.status === "needs-action")
@@ -114,11 +122,13 @@ export function buildAgentWorkerProgram() {
114
122
  .requiredOption("--task-id <id>", "Completed qa-execute TaskSpec to rerun as dedicated final verification")
115
123
  .option("--loop-agent-bin <bin>", "loop-agent binary", "loop-agent")
116
124
  .option("--json", "Emit stable JSON")
125
+ .option("--expected-controller-version <version>", "Exact expected controller version (fail before writes if mismatched)")
126
+ .option("--expected-controller-fingerprint <value>", "Exact expected controller fingerprint sha256:<hex> (fail before writes if mismatched)")
117
127
  .description("Run an independent HEAD-bound final QA verification and project Delivery evidence")
118
128
  .action(async (options) => {
119
129
  const repoRoot = path.resolve(options.repo);
120
- const client = new LoopAgentClient({ loopAgentBin: options.loopAgentBin, artifactRoot: path.join(getTaskPoolRoot(repoRoot), "artifacts", `final-verification-${Date.now()}`) });
121
- const result = await runFeatureFinalVerification({ featureDir: path.resolve(options.featureDir), repoRoot, taskId: options.taskId, client });
130
+ const client = new LoopAgentClient({ loopAgentBin: options.loopAgentBin, artifactRoot: path.join(getTaskPoolRoot(repoRoot), "artifacts", `final-verification-${Date.now()}`), resolveIdentity: true });
131
+ const result = await runFeatureFinalVerification({ featureDir: path.resolve(options.featureDir), repoRoot, taskId: options.taskId, client, controllerIdentity: client.getIdentity(), controllerExpectation: buildIdentityExpectation(options) });
122
132
  process.stdout.write(options.json ? `${JSON.stringify(result, null, 2)}\n` : `Feature: ${result.featureId}\nFinal verification: ${result.workerRunId}\nQA evidence: ${result.qaEvidencePath}\nFinal evidence: ${result.finalVerificationPath}\n`);
123
133
  });
124
134
  feature
@@ -269,13 +279,17 @@ export function buildAgentWorkerProgram() {
269
279
  .option("--check-repo-command <command...>", "Override the check-repo preflight command")
270
280
  .option("--quiet", "Suppress human-readable progress on stderr (JSON still goes to stdout)")
271
281
  .option("--pi-model <model>", "Smoke override: force every pi executor node to this model (drops --strict-models)")
282
+ .option("--expected-controller-version <version>", "Exact expected controller version (fail before writes if mismatched)")
283
+ .option("--expected-controller-fingerprint <value>", "Exact expected controller fingerprint sha256:<hex> (fail before writes if mismatched)")
272
284
  .description("Run ready TaskSpec tasks serially")
273
285
  .action(async (options) => {
274
286
  const repoRoot = path.resolve(options.repo);
275
287
  const batchRunId = options.batchRunId ?? buildBatchRunId(new Date());
288
+ const expectedIdentity = buildIdentityExpectation(options);
276
289
  const client = new LoopAgentClient({
277
290
  loopAgentBin: options.loopAgentBin,
278
291
  artifactRoot: path.join(getTaskPoolRoot(repoRoot), "artifacts", batchRunId),
292
+ resolveIdentity: true,
279
293
  });
280
294
  let progress;
281
295
  try {
@@ -301,6 +315,8 @@ export function buildAgentWorkerProgram() {
301
315
  : {}),
302
316
  progress,
303
317
  ...(options.piModel ? { piModel: options.piModel } : {}),
318
+ controllerIdentity: client.getIdentity(),
319
+ controllerExpectation: expectedIdentity,
304
320
  });
305
321
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
306
322
  if (result.status !== "completed")
@@ -310,7 +326,8 @@ export function buildAgentWorkerProgram() {
310
326
  .command("serve")
311
327
  .requiredOption("--repo <repo-root>", "Target repo root")
312
328
  .option("--port <port>", "HTTP port", "8787")
313
- .option("--host <host>", "Bind host", "127.0.0.1")
329
+ .option("--host <host>", "Bind host (default 127.0.0.1; use 0.0.0.0 for LAN, no auth)", "127.0.0.1")
330
+ .option("--debug", "Log request source/method/path/status to stderr")
314
331
  .description("Start read-only observe HTTP server")
315
332
  .action(async (options) => {
316
333
  const repoRoot = path.resolve(options.repo);
@@ -318,6 +335,7 @@ export function buildAgentWorkerProgram() {
318
335
  repoRoot,
319
336
  host: options.host,
320
337
  port: Number.parseInt(options.port, 10),
338
+ debug: options.debug === true,
321
339
  });
322
340
  process.stdout.write(`${server.url}\n`);
323
341
  await new Promise((resolve) => {
@@ -365,6 +383,17 @@ export function buildAgentWorkerProgram() {
365
383
  });
366
384
  return program;
367
385
  }
386
+ export function buildIdentityExpectation(options) {
387
+ if (options.expectedControllerVersion || options.expectedControllerFingerprint) {
388
+ const expectation = {};
389
+ if (options.expectedControllerVersion)
390
+ expectation.expectedVersion = options.expectedControllerVersion;
391
+ if (options.expectedControllerFingerprint)
392
+ expectation.expectedFingerprint = options.expectedControllerFingerprint;
393
+ return expectation;
394
+ }
395
+ return undefined;
396
+ }
368
397
  export async function main(argv = process.argv) {
369
398
  await buildAgentWorkerProgram().parseAsync(argv);
370
399
  }
@@ -4,15 +4,29 @@ import { mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promis
4
4
  import path from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  import YAML from "yaml";
7
+ import { controllerIdentitiesMatch, controllerIdentityExpectationFailure, resolveControllerIdentity, } from "../loop-agent/loop-agent-client.js";
7
8
  import { acceptanceSpecSchema } from "../task-graph/acceptance-schema.js";
8
9
  import { taskGraphSpecSchema } from "../task-graph/task-graph-schema.js";
9
10
  import { validateFeatureTaskGraph } from "../task-graph/validate.js";
10
11
  import { getRunsJsonlPath, getTaskPoolRoot, readJsonlFile, recordTaskPoolRun } from "../pool/run-store.js";
11
12
  import { buildWorkerRunId, runTaskSpec } from "../run-task/run-task.js";
12
13
  import { taskSpecSchema } from "../task-spec/schema.js";
14
+ import { preflightTargetRepo } from "../preflight.js";
13
15
  import { gitTransactionRecordSchema, transactionRecordPath } from "./git-transaction.js";
14
16
  const execFileAsync = promisify(execFile);
15
17
  export async function runFeatureFinalVerification(input) {
18
+ const dependencies = {
19
+ preflight: preflightTargetRepo,
20
+ runTask: runTaskSpec,
21
+ git,
22
+ gitRaw,
23
+ ...input.dependencies,
24
+ };
25
+ let controllerIdentity = resolveControllerIdentity(input.client, input.controllerIdentity);
26
+ const identityFailure = controllerIdentityExpectationFailure(controllerIdentity, input.controllerExpectation);
27
+ if (identityFailure) {
28
+ throw new Error(`${identityFailure.code}: ${identityFailure.message}`);
29
+ }
16
30
  const repoRoot = await realpath(path.resolve(input.repoRoot));
17
31
  const featureDir = await realpath(path.resolve(input.featureDir));
18
32
  const relativeFeature = path.relative(repoRoot, featureDir);
@@ -22,10 +36,19 @@ export async function runFeatureFinalVerification(input) {
22
36
  if (!validation.ok)
23
37
  throw new Error(`Feature Packet is invalid: ${validation.errors.map((error) => error.code).join(", ")}`);
24
38
  const featureId = validation.featureId;
39
+ const preflight = await dependencies.preflight({
40
+ repoRoot,
41
+ client: input.client,
42
+ ...(input.controllerExpectation ? { expectation: input.controllerExpectation } : {}),
43
+ });
44
+ if (!preflight.ok) {
45
+ throw new Error(`target repo preflight failed: ${preflight.code}: ${preflight.message}`);
46
+ }
47
+ controllerIdentity = preflight.controllerIdentity ?? controllerIdentity;
25
48
  const record = gitTransactionRecordSchema.parse(JSON.parse(await readFile(transactionRecordPath(repoRoot, featureId), "utf-8")));
26
- const branch = await git(repoRoot, ["branch", "--show-current"]);
27
- const headSha = await git(repoRoot, ["rev-parse", "HEAD"]);
28
- const status = await gitRaw(repoRoot, ["status", "--porcelain=v1", "--untracked-files=all"]);
49
+ const branch = await dependencies.git(repoRoot, ["branch", "--show-current"]);
50
+ const headSha = await dependencies.git(repoRoot, ["rev-parse", "HEAD"]);
51
+ const status = await dependencies.gitRaw(repoRoot, ["status", "--porcelain=v1", "--untracked-files=all"]);
29
52
  if (branch !== record.branch || headSha !== record.lastCheckpoint)
30
53
  throw new Error("final verification requires the recorded Feature branch at lastCheckpoint");
31
54
  if (status.trim())
@@ -50,17 +73,28 @@ export async function runFeatureFinalVerification(input) {
50
73
  };
51
74
  const now = input.now ?? new Date();
52
75
  let runs = await readJsonlFile(getRunsJsonlPath(repoRoot));
53
- let finalRun = await reusableFinalRun(repoRoot, runs, taskSpec.id, featureId, record, now);
76
+ let finalRun = await reusableFinalRun(repoRoot, runs, taskSpec.id, featureId, record, now, controllerIdentity);
54
77
  if (!finalRun) {
55
78
  const workerRunId = buildWorkerRunId(`${input.taskId}-final`, now);
56
- const result = await runTaskSpec({ repoRoot, taskSpec: verificationTaskSpec, taskSpecPath, client: input.client, now, workerRunId, skipSuccessFinalization: true });
79
+ const result = await dependencies.runTask({
80
+ repoRoot,
81
+ taskSpec: verificationTaskSpec,
82
+ taskSpecPath,
83
+ client: input.client,
84
+ now,
85
+ workerRunId,
86
+ skipSuccessFinalization: true,
87
+ preflight: false,
88
+ ...(controllerIdentity ? { controllerIdentity } : {}),
89
+ ...(input.controllerExpectation ? { controllerExpectation: input.controllerExpectation } : {}),
90
+ });
57
91
  if (result.status !== "succeeded")
58
92
  throw new Error(`dedicated final verification failed: ${workerRunId}`);
59
- const afterHead = await git(repoRoot, ["rev-parse", "HEAD"]);
60
- const afterStatus = await gitRaw(repoRoot, ["status", "--porcelain=v1", "--untracked-files=all"]);
93
+ const afterHead = await dependencies.git(repoRoot, ["rev-parse", "HEAD"]);
94
+ const afterStatus = await dependencies.gitRaw(repoRoot, ["status", "--porcelain=v1", "--untracked-files=all"]);
61
95
  if (afterHead !== headSha || afterStatus.trim())
62
96
  throw new Error("dedicated final verification changed the Delivery HEAD or worktree");
63
- finalRun = { schemaVersion: 1, batchRunId: `final-verification-${workerRunId}`, workerRunId, taskId: taskSpec.id, featureId, status: "succeeded", harnessTaskId: result.harnessTaskId, runRecordPath: result.runRecordPath, dagPath: result.dagPath, recordedAt: new Date().toISOString() };
97
+ finalRun = { schemaVersion: 1, batchRunId: `final-verification-${workerRunId}`, workerRunId, taskId: taskSpec.id, featureId, status: "succeeded", harnessTaskId: result.harnessTaskId, runRecordPath: result.runRecordPath, dagPath: result.dagPath, recordedAt: now.toISOString(), ...(controllerIdentity ? { controllerIdentity } : {}) };
64
98
  await recordTaskPoolRun({ repoRoot, run: finalRun });
65
99
  runs = [...runs, finalRun];
66
100
  }
@@ -87,7 +121,7 @@ export async function runFeatureFinalVerification(input) {
87
121
  qa: { schemaVersion: 1, featureId, verdict: "passed", acIds: requiredAcIds, runs: qaRuns.map(runRef) },
88
122
  final: { schemaVersion: 1, featureId, kind: "final-verification", status: "passed", headSha, run: runRef(finalRun), shellSummary: { path: summaryRelative, sha256: createHash("sha256").update(summary).digest("hex") } },
89
123
  });
90
- return { schemaVersion: 1, featureId, taskId: taskSpec.id, workerRunId: finalRun.workerRunId, headSha, qaEvidencePath: repoRef(repoRoot, qaEvidencePath), finalVerificationPath: repoRef(repoRoot, finalVerificationPath), qaRunCount: qaRuns.length };
124
+ return { schemaVersion: 1, featureId, taskId: taskSpec.id, workerRunId: finalRun.workerRunId, headSha, qaEvidencePath: repoRef(repoRoot, qaEvidencePath), finalVerificationPath: repoRef(repoRoot, finalVerificationPath), qaRunCount: qaRuns.length, ...(controllerIdentity ? { controllerIdentity } : {}) };
91
125
  }
92
126
  export function latestSuccessfulQaRuns(runs, specs, excludedTaskId, featureId) {
93
127
  const byTask = new Map();
@@ -96,11 +130,13 @@ export function latestSuccessfulQaRuns(runs, specs, excludedTaskId, featureId) {
96
130
  byTask.set(run.taskId, run);
97
131
  return [...byTask.values()].sort((a, b) => a.taskId.localeCompare(b.taskId));
98
132
  }
99
- async function reusableFinalRun(repoRoot, runs, taskId, featureId, record, now) {
133
+ async function reusableFinalRun(repoRoot, runs, taskId, featureId, record, now, controllerIdentity) {
100
134
  const latestCheckpointAt = Math.max(...record.checkpoints.map((entry) => new Date(entry.createdAt).getTime()));
101
135
  for (const run of [...runs].reverse()) {
102
136
  if (run.taskId !== taskId || run.featureId !== featureId || run.status !== "succeeded" || !run.workerRunId.includes("-final-") || !run.runRecordPath)
103
137
  continue;
138
+ if (controllerIdentity && !controllerIdentitiesMatch(run.controllerIdentity, controllerIdentity))
139
+ continue;
104
140
  const recordedAt = new Date(run.recordedAt).getTime();
105
141
  if (recordedAt < latestCheckpointAt || now.getTime() - recordedAt > 24 * 60 * 60 * 1000)
106
142
  continue;
@@ -117,7 +153,7 @@ async function reusableFinalRun(repoRoot, runs, taskId, featureId, record, now)
117
153
  }
118
154
  return undefined;
119
155
  }
120
- function runRef(run) { return { taskId: run.taskId, workerRunId: run.workerRunId, recordedAt: run.recordedAt }; }
156
+ function runRef(run) { return { taskId: run.taskId, workerRunId: run.workerRunId, recordedAt: run.recordedAt, ...(run.controllerIdentity ? { controllerIdentity: run.controllerIdentity } : {}) }; }
121
157
  function repoRef(repoRoot, absolute) { return path.relative(repoRoot, absolute).replace(/\\/g, "/"); }
122
158
  export async function writeEvidencePairAtomic(evidenceDir, value, fs = { rm }) {
123
159
  const parent = path.dirname(evidenceDir);