@tea-agent/loop-agent 0.9.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 (183) hide show
  1. package/AGENTS.md +8 -0
  2. package/CHANGELOG.md +92 -11
  3. package/README.md +69 -5
  4. package/dist/application/dag/args.js +4 -14
  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 +1 -1
  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/{executors/cursor-executor.js → sidecars/cursor-prompt/executor.js} +2 -42
  34. package/dist/sidecars/cursor-prompt/index.js +3 -0
  35. package/dist/sidecars/cursor-prompt/stream.js +121 -0
  36. package/dist/task/config-types.js +29 -12
  37. package/dist/task/delegate.js +9 -21
  38. package/dist/task/runtime.js +1 -2
  39. package/dist/worker/cli.js +243 -0
  40. package/dist/worker/closeout/apply.js +73 -0
  41. package/dist/worker/closeout/preview.js +30 -0
  42. package/dist/worker/delivery/final-verification.js +194 -0
  43. package/dist/worker/delivery/git-transaction.js +354 -0
  44. package/dist/worker/delivery/package.js +502 -0
  45. package/dist/worker/feature/decision-loader.js +68 -0
  46. package/dist/worker/feature/discover.js +14 -0
  47. package/dist/worker/feature/next-action.js +74 -0
  48. package/dist/worker/feature/reducer.js +133 -0
  49. package/dist/worker/feature/review.js +502 -0
  50. package/dist/worker/feature/run.js +365 -0
  51. package/dist/worker/feature/types.js +1 -0
  52. package/dist/worker/follow-up/approve.js +270 -0
  53. package/dist/worker/follow-up/factory.js +234 -0
  54. package/dist/worker/follow-up/paths.js +25 -0
  55. package/dist/worker/follow-up/policy.js +26 -0
  56. package/dist/worker/follow-up/schema.js +93 -0
  57. package/dist/worker/follow-up/store.js +96 -0
  58. package/dist/worker/loop-agent/loop-agent-client.js +329 -126
  59. package/dist/worker/metrics/projector.js +139 -0
  60. package/dist/worker/observability/read-model.js +282 -15
  61. package/dist/worker/observe/paths.js +17 -5
  62. package/dist/worker/observe/static/app.js +443 -61
  63. package/dist/worker/observe/static/index.html +3 -1
  64. package/dist/worker/observe/static/styles.css +85 -18
  65. package/dist/worker/pool/run-store.js +14 -2
  66. package/dist/worker/pool/validation.js +59 -0
  67. package/dist/worker/preflight.js +49 -1
  68. package/dist/worker/report/morning-report.js +41 -6
  69. package/dist/worker/run-task/run-task.js +23 -13
  70. package/dist/worker/runner/run-ready.js +89 -11
  71. package/dist/worker/task-spec/schema.js +0 -1
  72. package/dist/workflows/dag/convergence/controller.js +1 -1
  73. package/dist/workflows/dag/executor-registry.js +0 -2
  74. package/dist/workflows/dag/governance-profile.js +10 -0
  75. package/dist/workflows/dag/init-hybrid.js +601 -26
  76. package/dist/workflows/dag/lifecycle.js +146 -0
  77. package/dist/workflows/dag/node-execution.js +64 -7
  78. package/dist/workflows/dag/prompt.js +16 -0
  79. package/dist/workflows/dag/report.js +2 -0
  80. package/dist/workflows/dag/runner.js +176 -119
  81. package/dist/workflows/dag/scheduler.js +7 -2
  82. package/dist/workflows/dag/skill-snapshot.js +527 -0
  83. package/dist/workflows/dag/types.js +45 -9
  84. package/dist/workflows/dag/validate.js +5 -8
  85. package/dist/workflows/loop/actions/dag-action.js +0 -2
  86. package/dist/workflows/loop/actions/shared.js +1 -1
  87. package/dist/workflows/loop/actions.js +14 -31
  88. package/dist/workflows/loop/benchmark.js +1 -1
  89. package/dist/workflows/loop/index.js +1 -1
  90. package/dist/workflows/loop/policy/auto-policy.js +22 -14
  91. package/dist/workflows/loop/policy/path-patterns.js +13 -0
  92. package/docs/README.md +35 -16
  93. package/docs/agent-dag-recovery-playbook.md +1 -1
  94. package/docs/architecture/README.md +26 -0
  95. package/docs/architecture/dag-execution.md +134 -0
  96. package/docs/architecture/evolution.md +52 -0
  97. package/docs/architecture/facts-and-state.md +58 -0
  98. package/docs/architecture/runtime-boundaries.md +41 -15
  99. package/docs/architecture/system-overview.md +93 -0
  100. package/docs/architecture/worker-and-feature.md +81 -0
  101. package/docs/cursor-prompt-sidecar.md +36 -0
  102. package/docs/decisions/README.md +13 -1
  103. package/docs/design/README.md +39 -13
  104. package/docs/development-principles.md +1 -1
  105. package/docs/exec-plans/active/README.md +2 -2
  106. package/docs/exec-plans/completed/README.md +14 -1
  107. package/docs/feature-workflow.md +44 -4
  108. package/docs/init-surface.manifest.json +63 -1
  109. package/docs/loop-agent-harness.md +65 -3
  110. package/docs/progress/README.md +27 -0
  111. package/docs/reports/README.md +74 -8
  112. package/docs/skills/README.md +2 -1
  113. package/docs/skills/vetted-skill-registry.md +2 -1
  114. package/docs/templates/agent-dag-report.schema.json +4 -2
  115. package/docs/templates/agent-dag.base.json +0 -5
  116. package/docs/templates/agent-dag.final-verification.json +0 -5
  117. package/docs/templates/agent-dag.schema.json +1 -2
  118. package/docs/templates/agent-dag.supervised-implementation.json +1 -6
  119. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +131 -0
  120. package/docs/templates/backend-test-dag.json +213 -0
  121. package/docs/templates/backend-test-dag.retrospect.prompt.md +128 -0
  122. package/docs/templates/backend-test-dag.review-cases.prompt.md +85 -0
  123. package/docs/templates/frontend-design-contract.md +33 -0
  124. package/docs/templates/frontend-task-constraints.md +25 -0
  125. package/docs/templates/frontend-task-requirement.md +61 -0
  126. package/docs/templates/harness.schema.json +8 -5
  127. package/docs/templates/hybrid-dag.json +1 -6
  128. package/docs/templates/init-evolution-review.md +4 -2
  129. package/docs/templates/interactive-ui-round2-experiment.md +1 -1
  130. package/docs/templates/product-line/task.yaml +0 -1
  131. package/docs/templates/worker-dogfood-evidence.md +28 -0
  132. package/docs/templates/worker-dogfood-setup.md +20 -0
  133. package/docs/verification-matrix.md +17 -0
  134. package/examples/decision-gate-agent-dag.json +87 -33
  135. package/examples/example-dag.json +0 -5
  136. package/examples/hybrid-loop-agent-dag.json +0 -5
  137. package/harness.json +6 -11
  138. package/package.json +22 -44
  139. package/scripts/check-product-line-docs.sh +10 -3
  140. package/skills/agent-worker/SKILL.md +37 -0
  141. package/skills/agent-worker/references/agent-worker-operator.md +43 -0
  142. package/skills/frontend-design-review/SKILL.md +59 -0
  143. package/skills/frontend-design-review/references/review-checklist.md +37 -0
  144. package/skills/frontend-implementation/SKILL.md +48 -0
  145. package/skills/frontend-implementation/references/code-standards.md +34 -0
  146. package/skills/frontend-implementation/references/design-spec.md +46 -0
  147. package/skills/frontend-implementation/references/node-contracts.md +32 -0
  148. package/skills/frontend-review/SKILL.md +53 -0
  149. package/skills/frontend-review/references/review-findings.md +42 -0
  150. package/skills/frontend-verification/SKILL.md +40 -0
  151. package/skills/frontend-verification/references/verification-checklist.md +56 -0
  152. package/skills/grill-me/SKILL.md +10 -0
  153. package/skills/grill-with-docs/SKILL.md +88 -0
  154. package/skills/grill-with-docs/adr-format.md +47 -0
  155. package/skills/grill-with-docs/context-format.md +60 -0
  156. package/skills/init-capability-evolution/SKILL.md +1 -0
  157. package/skills/loop-agent/SKILL.md +11 -9
  158. package/skills/loop-agent/references/command-reference.md +28 -15
  159. package/skills/loop-agent/references/docs-converge.md +126 -0
  160. package/skills/loop-agent/references/harness-policy.md +7 -7
  161. package/skills/loop-agent/references/hybrid-dag.md +13 -15
  162. package/skills/loop-agent/references/long-running-loop.md +4 -6
  163. package/skills/loop-agent/references/multi-worktree.md +6 -6
  164. package/skills/loop-agent/references/orchestrator-and-interventions.md +3 -3
  165. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +14 -11
  166. package/skills/using-git-worktrees/SKILL.md +215 -0
  167. package/dist/commands/cursor-worker.js +0 -43
  168. package/dist/cursor-worker-entry.js +0 -8
  169. package/dist/executors/cursor-artifacts.js +0 -33
  170. package/dist/executors/cursor-execution-log.js +0 -81
  171. package/dist/executors/cursor-executor-artifacts.js +0 -134
  172. package/dist/executors/cursor-run.js +0 -115
  173. package/dist/executors/cursor-tool.js +0 -94
  174. package/dist/executors/cursor-worker-client.js +0 -223
  175. package/dist/executors/cursor-worker-protocol.js +0 -18
  176. package/dist/executors/cursor-worker-server.js +0 -54
  177. package/dist/executors/cursor-worker.js +0 -3
  178. package/dist/executors/cursor.js +0 -6
  179. package/dist/executors/dag-cursor-executor.js +0 -87
  180. package/dist/workflows/loop/actions/cursor-fix.js +0 -191
  181. package/dist/workflows/loop/policy/cursor-fix-policy.js +0 -31
  182. package/docs/cursor-executor-usage.md +0 -25
  183. package/docs/dynamic-workflow-dag-engine-roadmap.md +0 -1749
@@ -0,0 +1,37 @@
1
+ import { createHash } from "node:crypto";
2
+ export function truncateOutput(text, maxBytes) {
3
+ if (!text)
4
+ return { text, truncated: false };
5
+ if (maxBytes <= 0) {
6
+ return { text: "\n...[truncated]", truncated: true };
7
+ }
8
+ if (Buffer.byteLength(text, "utf-8") <= maxBytes) {
9
+ return { text, truncated: false };
10
+ }
11
+ let low = 0;
12
+ let high = text.length;
13
+ while (low < high) {
14
+ const mid = Math.ceil((low + high) / 2);
15
+ if (Buffer.byteLength(text.slice(0, mid), "utf-8") <= maxBytes) {
16
+ low = mid;
17
+ }
18
+ else {
19
+ high = mid - 1;
20
+ }
21
+ }
22
+ let end = low;
23
+ // Avoid ending the prefix on a dangling UTF-16 high surrogate.
24
+ if (end > 0) {
25
+ const code = text.charCodeAt(end - 1);
26
+ if (code >= 0xd800 && code <= 0xdbff)
27
+ end -= 1;
28
+ }
29
+ return {
30
+ text: `${text.slice(0, end)}\n...[truncated]`,
31
+ truncated: true,
32
+ };
33
+ }
34
+ export function redactPromptForLog(prompt) {
35
+ const hash = createHash("sha256").update(prompt).digest("hex").slice(0, 12);
36
+ return `<prompt sha256=${hash} len=${prompt.length}>`;
37
+ }
@@ -0,0 +1,353 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync, readdirSync, statSync } from "node:fs";
3
+ import { realpathSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { platform } from "node:os";
6
+ // ---------------------------------------------------------------------------
7
+ // Public helpers
8
+ // ---------------------------------------------------------------------------
9
+ /**
10
+ * Read the `version` field from a package.json file.
11
+ *
12
+ * Returns `undefined` when the file is missing, unreadable, or lacks a
13
+ * string `version` field — callers must treat `undefined` as a hard failure
14
+ * for identity construction.
15
+ */
16
+ export function readPackageVersion(packageDir) {
17
+ try {
18
+ const raw = readFileSync(path.join(packageDir, "package.json"), "utf-8");
19
+ const parsed = JSON.parse(raw);
20
+ if (typeof parsed.version === "string")
21
+ return parsed.version;
22
+ }
23
+ catch {
24
+ // file missing or unreadable
25
+ }
26
+ return undefined;
27
+ }
28
+ /**
29
+ * Read the `name` field from a package.json file.
30
+ *
31
+ * Returns `undefined` when the file is missing, unreadable, or lacks a
32
+ * string `name` field.
33
+ */
34
+ export function readPackageName(packageDir) {
35
+ try {
36
+ const raw = readFileSync(path.join(packageDir, "package.json"), "utf-8");
37
+ const parsed = JSON.parse(raw);
38
+ if (typeof parsed.name === "string")
39
+ return parsed.name;
40
+ }
41
+ catch {
42
+ // file missing or unreadable
43
+ }
44
+ return undefined;
45
+ }
46
+ /**
47
+ * Compute the SHA-256 hash of a file on disk.
48
+ *
49
+ * Returns `undefined` when the file is unreadable — callers must treat this
50
+ * as a hard failure for identity construction.
51
+ */
52
+ export function computeBinarySha256(filePath) {
53
+ try {
54
+ const buffer = readFileSync(filePath);
55
+ return createHash("sha256").update(buffer).digest("hex");
56
+ }
57
+ catch {
58
+ return undefined;
59
+ }
60
+ }
61
+ /**
62
+ * Compute a portable, deterministic content fingerprint for the installed
63
+ * package rooted at `packageRoot`.
64
+ *
65
+ * The fingerprint covers four scopes (package.json, bin/**, dist/**,
66
+ * skills/**) and is independent of the absolute installation path.
67
+ *
68
+ * Sorting uses code-point order (UTF-8 byte comparison), NOT localeCompare,
69
+ * to ensure deterministic behaviour across locales.
70
+ */
71
+ export function computePackageFingerprint(packageRoot) {
72
+ const scope = [
73
+ "package.json",
74
+ "bin/**",
75
+ "dist/**",
76
+ "skills/**",
77
+ ];
78
+ // Collecting entries ---------------------------------------------------
79
+ const entries = [];
80
+ // 1) package.json (required single file)
81
+ addRequiredFile(entries, path.join(packageRoot, "package.json"), "");
82
+ // 2) bin/**, dist/**, skills/** via recursive walk
83
+ for (const dir of ["bin", "dist", "skills"]) {
84
+ const absoluteDir = path.join(packageRoot, dir);
85
+ collectTree(entries, absoluteDir, dir);
86
+ }
87
+ // 3) Deterministic sort by relative path (code-point / byte order)
88
+ entries.sort((a, b) => {
89
+ const aBuf = Buffer.from(a.relativePath, "utf-8");
90
+ const bBuf = Buffer.from(b.relativePath, "utf-8");
91
+ const minLen = Math.min(aBuf.length, bBuf.length);
92
+ for (let i = 0; i < minLen; i++) {
93
+ if (aBuf[i] !== bBuf[i])
94
+ return aBuf[i] - bBuf[i];
95
+ }
96
+ return aBuf.length - bBuf.length;
97
+ });
98
+ // 4) Cumulative hash: hash of (<relativePath> + "\t" + <contentHash>) lines
99
+ const cumulative = createHash("sha256");
100
+ for (const entry of entries) {
101
+ cumulative.update(`${entry.relativePath}\t${entry.contentHash}\n`);
102
+ }
103
+ return {
104
+ algorithm: "sha256",
105
+ scopeVersion: 1,
106
+ scope,
107
+ value: `sha256:${cumulative.digest("hex")}`,
108
+ fileCount: entries.length,
109
+ };
110
+ }
111
+ /**
112
+ * Resolve a requested bin name or path to the absolute executable.
113
+ *
114
+ * Resolution strategy:
115
+ * 1. If `requested` is already an absolute path, use it directly.
116
+ * 2. If `requested` contains a path separator (/, \\), resolve against cwd.
117
+ * 3. Otherwise, search PATH directories (with optional PATHEXT on Windows).
118
+ * PATH misses fail closed; a bare command name is never invented relative to
119
+ * cwd when it was not actually found.
120
+ *
121
+ * After this call, the result must be treated as frozen — subsequent PATH
122
+ * mutations must not trigger a new resolution.
123
+ */
124
+ export function resolveBinPaths(requested, resolvedAt, inputs) {
125
+ const cwd = inputs?.cwd ?? process.cwd();
126
+ const env = inputs?.env ?? process.env;
127
+ const osPlatform = inputs?.platform ?? platform();
128
+ if (path.isAbsolute(requested)) {
129
+ const entry = path.resolve(requested);
130
+ assertRegularFile(entry, requested);
131
+ return { requested, entry, realEntry: realpathSync(entry), resolvedAt };
132
+ }
133
+ // Path-like relative (contains / or \)
134
+ if (requested.includes("/") || requested.includes("\\")) {
135
+ const resolved = path.resolve(cwd, requested);
136
+ assertRegularFile(resolved, requested);
137
+ return { requested, entry: resolved, realEntry: realpathSync(resolved), resolvedAt };
138
+ }
139
+ // Search PATH
140
+ const pathEnv = env.PATH ?? env.Path ?? env.path ?? "";
141
+ const candidates = resolveFromPath(requested, pathEnv, osPlatform, env.PATHEXT ?? env.Pathext ?? env.pathext, cwd);
142
+ if (candidates.length > 0) {
143
+ const entry = candidates[0];
144
+ assertRegularFile(entry, requested);
145
+ return { requested, entry, realEntry: realpathSync(entry), resolvedAt };
146
+ }
147
+ throw new Error(`controller identity: cannot resolve executable ${JSON.stringify(requested)}`);
148
+ }
149
+ /**
150
+ * Resolve an npm-generated Windows `.cmd` shim to the referenced JavaScript
151
+ * bin entry within the same package.
152
+ *
153
+ * On Windows, npm creates `.cmd` wrappers like:
154
+ * ```
155
+ * @IF EXIST "%~dp0\node.exe" (
156
+ * "%~dp0\node.exe" "%~dp0\..\package\bin\cli.js" %*
157
+ * ) ELSE (
158
+ * ...
159
+ * )
160
+ * ```
161
+ *
162
+ * This function parses the `.cmd` shim, extracts the JS script reference, and
163
+ * returns the resolved absolute path. Returns `undefined` if the shim cannot
164
+ * be parsed or the referenced script does not exist.
165
+ *
166
+ * Pure function: does not require a Windows host to execute.
167
+ */
168
+ export function resolveCmdShim(shimPath) {
169
+ try {
170
+ const content = readFileSync(shimPath, "utf-8");
171
+ const matches = content.matchAll(/"(?:%dp0%|%~dp0)([^"]+\.(?:js|mjs|cjs))"/gi);
172
+ for (const match of matches) {
173
+ if (!match[1])
174
+ continue;
175
+ const windowsRelative = match[1].replace(/^[/\\]+/, "");
176
+ const scriptPath = path.resolve(path.dirname(shimPath), ...windowsRelative.split(/[\\/]+/));
177
+ try {
178
+ if (statSync(scriptPath).isFile())
179
+ return realpathSync(scriptPath);
180
+ }
181
+ catch {
182
+ // Try the next JavaScript reference in the shim.
183
+ }
184
+ }
185
+ return undefined;
186
+ }
187
+ catch {
188
+ return undefined;
189
+ }
190
+ }
191
+ /** Check whether a file is a JavaScript script (node shebang or JS extension). */
192
+ export function isJavaScriptScript(filePath) {
193
+ try {
194
+ const header = readFileSync(filePath).subarray(0, 256).toString("utf-8").trimStart();
195
+ if (header.startsWith("#!/usr/bin/env node") || header.startsWith("#!/usr/bin/node") || header.startsWith("#!node")) {
196
+ return true;
197
+ }
198
+ }
199
+ catch {
200
+ // can't read — check by extension only
201
+ }
202
+ const ext = path.extname(filePath).toLowerCase();
203
+ return ext === ".js" || ext === ".mjs" || ext === ".cjs" || ext === ".ts";
204
+ }
205
+ // ---------------------------------------------------------------------------
206
+ // Package root resolution
207
+ // ---------------------------------------------------------------------------
208
+ /**
209
+ * Find the nearest package root by walking upward from `startDir` until a
210
+ * `package.json` is found.
211
+ *
212
+ * Returns `undefined` if no package.json was found at any ancestor (up to 100
213
+ * levels). Callers must treat `undefined` as a hard failure for identity
214
+ * construction.
215
+ */
216
+ export function findPackageRoot(startDir) {
217
+ let current = path.resolve(startDir);
218
+ for (let i = 0; i < 100; i++) {
219
+ try {
220
+ statSync(path.join(current, "package.json"));
221
+ return current;
222
+ }
223
+ catch {
224
+ const parent = path.dirname(current);
225
+ if (parent === current)
226
+ break;
227
+ current = parent;
228
+ }
229
+ }
230
+ return undefined;
231
+ }
232
+ // ---------------------------------------------------------------------------
233
+ // PATHEXT resolution
234
+ // ---------------------------------------------------------------------------
235
+ /**
236
+ * Expand a Windows executable name into candidate entries using PATHEXT.
237
+ *
238
+ * Example: on a system with `PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.JS`,
239
+ * resolving "loop-agent" produces:
240
+ * ["loop-agent.COM", "loop-agent.EXE", "loop-agent.BAT", "loop-agent.CMD", ...]
241
+ *
242
+ * If the name already has an extension matching a PATHEXT entry, no expansion
243
+ * is performed and the name is used as-is.
244
+ *
245
+ * Pure function for testability.
246
+ */
247
+ export function expandPathext(bin, pathext) {
248
+ if (!pathext)
249
+ return [bin];
250
+ const extensions = pathext
251
+ .split(";")
252
+ .map((ext) => ext.trim())
253
+ .filter(Boolean);
254
+ if (extensions.length === 0)
255
+ return [bin];
256
+ const binLower = bin.toLowerCase();
257
+ const alreadyHasExt = extensions.some((ext) => binLower.endsWith(ext.toLowerCase()));
258
+ if (alreadyHasExt)
259
+ return [bin];
260
+ return extensions.map((ext) => bin + ext);
261
+ }
262
+ // ---------------------------------------------------------------------------
263
+ // Internal helpers
264
+ // ---------------------------------------------------------------------------
265
+ function addRequiredFile(entries, absolutePath, relativeDir) {
266
+ const buffer = readFileSync(absolutePath);
267
+ const contentHash = createHash("sha256").update(buffer).digest("hex");
268
+ const relativePath = relativeDir
269
+ ? path.posix.join(relativeDir, path.basename(absolutePath))
270
+ : path.basename(absolutePath);
271
+ entries.push({ relativePath, contentHash });
272
+ }
273
+ function collectTree(entries, absoluteDir, relativeDir) {
274
+ let stats;
275
+ try {
276
+ stats = statSync(absoluteDir);
277
+ }
278
+ catch (error) {
279
+ if (isNotFound(error))
280
+ return; // optional scope directory does not exist
281
+ throw error;
282
+ }
283
+ if (!stats.isDirectory())
284
+ return;
285
+ const dirents = readdirSync(absoluteDir, { withFileTypes: true });
286
+ for (const dirent of dirents) {
287
+ const childAbs = path.join(absoluteDir, dirent.name);
288
+ const childRel = path.posix.join(relativeDir, dirent.name);
289
+ if (dirent.isFile()) {
290
+ addRequiredFile(entries, childAbs, relativeDir);
291
+ }
292
+ else if (dirent.isDirectory()) {
293
+ collectTree(entries, childAbs, childRel);
294
+ }
295
+ // Ignore symlinks, sockets, etc.
296
+ }
297
+ }
298
+ function isNotFound(error) {
299
+ return Boolean(error
300
+ && typeof error === "object"
301
+ && "code" in error
302
+ && error.code === "ENOENT");
303
+ }
304
+ // ---------------------------------------------------------------------------
305
+ // Internal: PATH resolution (synchronous, no external dependencies)
306
+ // ---------------------------------------------------------------------------
307
+ function resolveFromPath(bin, pathEnv, osPlatform, pathext, cwd) {
308
+ const results = [];
309
+ const dirs = pathEnv.split(osPlatform === "win32" ? ";" : path.delimiter);
310
+ // On Windows, expand with PATHEXT
311
+ const isWindows = osPlatform === "win32";
312
+ for (const rawDir of dirs) {
313
+ const dir = rawDir || cwd;
314
+ if (isWindows) {
315
+ // Get PATHEXT from a well-known env var — in tests, passed via the
316
+ // merged env through ResolutionInputs. Here we fall back to process.env.
317
+ const candidates = expandPathext(bin, pathext ?? ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC");
318
+ for (const candidate of candidates) {
319
+ const filePath = path.join(dir, candidate);
320
+ try {
321
+ if (!statSync(filePath).isFile())
322
+ continue;
323
+ results.push(filePath);
324
+ }
325
+ catch {
326
+ // not found in this PATH entry
327
+ }
328
+ }
329
+ }
330
+ else {
331
+ const candidate = path.join(dir, bin);
332
+ try {
333
+ if (!statSync(candidate).isFile())
334
+ continue;
335
+ results.push(candidate);
336
+ }
337
+ catch {
338
+ // not found in this PATH entry
339
+ }
340
+ }
341
+ }
342
+ return results;
343
+ }
344
+ function assertRegularFile(filePath, requested) {
345
+ try {
346
+ if (statSync(filePath).isFile())
347
+ return;
348
+ }
349
+ catch {
350
+ // Fall through to the stable error below.
351
+ }
352
+ throw new Error(`controller identity: resolved executable for ${JSON.stringify(requested)} is not a readable file: ${filePath}`);
353
+ }
@@ -1,5 +1,5 @@
1
- import { createHash } from "node:crypto";
2
- import { referencePathJoin } from "../shared/path-refs.js";
1
+ import { referencePathJoin } from "../../shared/path-refs.js";
2
+ import { truncateOutput, redactPromptForLog } from "../../shared/output-truncation.js";
3
3
  /** Cursor SDK account default (user → team → system). Override per call when needed. */
4
4
  export const DEFAULT_CURSOR_MODEL = "default";
5
5
  export const DEFAULT_CURSOR_TIMEOUT_MS = 1_800_000;
@@ -48,47 +48,11 @@ export function buildCursorPrompt(userTask, artifactOptions) {
48
48
  userTask,
49
49
  ].join("\n");
50
50
  }
51
- export function truncateOutput(text, maxBytes) {
52
- if (!text)
53
- return { text, truncated: false };
54
- if (maxBytes <= 0) {
55
- return { text: "\n...[truncated]", truncated: true };
56
- }
57
- if (Buffer.byteLength(text, "utf-8") <= maxBytes) {
58
- return { text, truncated: false };
59
- }
60
- let low = 0;
61
- let high = text.length;
62
- while (low < high) {
63
- const mid = Math.ceil((low + high) / 2);
64
- if (Buffer.byteLength(text.slice(0, mid), "utf-8") <= maxBytes) {
65
- low = mid;
66
- }
67
- else {
68
- high = mid - 1;
69
- }
70
- }
71
- let end = low;
72
- // Avoid ending the prefix on a dangling UTF-16 high surrogate.
73
- if (end > 0) {
74
- const code = text.charCodeAt(end - 1);
75
- if (code >= 0xd800 && code <= 0xdbff)
76
- end -= 1;
77
- }
78
- return {
79
- text: `${text.slice(0, end)}\n...[truncated]`,
80
- truncated: true,
81
- };
82
- }
83
51
  export function redactApiKey(text, apiKey) {
84
52
  if (!apiKey)
85
53
  return text;
86
54
  return text.split(apiKey).join("<redacted-api-key>");
87
55
  }
88
- export function redactPromptForLog(prompt) {
89
- const hash = createHash("sha256").update(prompt).digest("hex").slice(0, 12);
90
- return `<prompt sha256=${hash} len=${prompt.length}>`;
91
- }
92
56
  export function buildRedactedExecutorRecord(input) {
93
57
  const apiKey = input.apiKey ?? process.env.CURSOR_API_KEY;
94
58
  const stdout = truncateOutput(redactApiKey(input.result.stdout, apiKey), 2_000);
@@ -239,10 +203,6 @@ function applyTokenUsageToResult(result, usage) {
239
203
  }
240
204
  }
241
205
  export async function executeCursorTask(options) {
242
- const { shouldUseCursorWorker, executeCursorTaskViaWorker } = await import("./cursor-worker-client.js");
243
- if (shouldUseCursorWorker(options)) {
244
- return executeCursorTaskViaWorker(options);
245
- }
246
206
  const model = options.model ?? DEFAULT_CURSOR_MODEL;
247
207
  const timeoutMs = options.timeoutMs ?? DEFAULT_CURSOR_TIMEOUT_MS;
248
208
  const apiKey = options.apiKey ?? process.env.CURSOR_API_KEY;
@@ -0,0 +1,3 @@
1
+ export { DEFAULT_CURSOR_MODEL, DEFAULT_CURSOR_TIMEOUT_MS, DEFAULT_CURSOR_STREAM_IDLE_TIMEOUT_MS, WAIT_AFTER_STREAM_GRACE_MS, MAX_CURSOR_STDOUT_BYTES, MAX_CURSOR_STDERR_BYTES, buildCursorPrompt, buildArtifactPathPrompt, resolveArtifactWriteDir, buildRedactedExecutorRecord, buildCursorExecutorEnv, classifyCursorFailure, mapRunStatusToExecutionStatus, CursorTimeoutError, isCursorTimeoutError, withCursorTimeout, buildStreamIdleTimeoutMessage, bestEffortCancelRun, extractCursorTokenUsage, executeCursorTask, redactApiKey, } from "./executor.js";
2
+ export { truncateOutput, redactPromptForLog, } from "../../shared/output-truncation.js";
3
+ export { executeCursorPromptStream, } from "./stream.js";
@@ -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);