@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
@@ -2,13 +2,25 @@ import path from 'node:path';
2
2
  import { resolveTaskContextFromDir, writeTaskArtifactFile, } from '../infrastructure/harness/task-store.js';
3
3
  export const ARTIFACTS_DIR = 'artifacts';
4
4
  const SPEC_ARTIFACT_STEPS = ['analyze', 'plan'];
5
- const DEFAULT_ARTIFACTS = {
6
- '分析报告.md': '# 分析报告\n\n## 1. 任务目标\n\n## 2. 当前现状\n\n## 3. 受影响模块与文件\n\n## 4. 关键约束与不变式\n\n## 5. 硬约束摘要\n\n## 6. 允许修改范围 / 禁止修改范围\n\n## 7. 风险点\n\n## 8. 待确认问题\n',
7
- '实现计划.md': '# 实现计划\n\n## 1. 修改目标\n\n## 2. 修改范围\n\n## 3. 允许修改文件 / 目录\n\n## 4. 明确不做的内容\n\n## 5. 关键决策\n\n## 6. 不变式\n\n## 7. 风险与回滚点\n\n## 8. 测试与验证方案\n\n## 9. 不在本次范围内的事项\n\n## 10. 待确认事项\n',
5
+ /** Promote-run / closeout bridge templates seeded by new-task. */
6
+ const SEEDED_ARTIFACTS = {
8
7
  '修改记录.md': '# 修改记录\n\n## 1. 修改文件清单\n\n## 2. 关键改动说明\n',
9
8
  '验证结果.md': '# 验证结果\n\n## 1. 执行的命令\n\n## 2. 通过项\n\n## 3. 失败项\n',
9
+ };
10
+ /**
11
+ * Legacy L1 analyze/plan/retrospective templates.
12
+ * Kept only so validateArtifactContent can recognize unchanged historical files.
13
+ * new-task must not seed these.
14
+ */
15
+ const LEGACY_ARTIFACT_TEMPLATES = {
16
+ '分析报告.md': '# 分析报告\n\n## 1. 任务目标\n\n## 2. 当前现状\n\n## 3. 受影响模块与文件\n\n## 4. 关键约束与不变式\n\n## 5. 硬约束摘要\n\n## 6. 允许修改范围 / 禁止修改范围\n\n## 7. 风险点\n\n## 8. 待确认问题\n',
17
+ '实现计划.md': '# 实现计划\n\n## 1. 修改目标\n\n## 2. 修改范围\n\n## 3. 允许修改文件 / 目录\n\n## 4. 明确不做的内容\n\n## 5. 关键决策\n\n## 6. 不变式\n\n## 7. 风险与回滚点\n\n## 8. 测试与验证方案\n\n## 9. 不在本次范围内的事项\n\n## 10. 待确认事项\n',
10
18
  '复盘报告.md': '# 复盘报告\n\n## 1. 质量复盘\n\n## 2. 流程问题\n\n## 3. 下次建议\n',
11
19
  };
20
+ const ARTIFACT_TEMPLATES = {
21
+ ...LEGACY_ARTIFACT_TEMPLATES,
22
+ ...SEEDED_ARTIFACTS,
23
+ };
12
24
  const ARTIFACT_NAME_BY_STEP = {
13
25
  analyze: '分析报告.md',
14
26
  plan: '实现计划.md',
@@ -16,6 +28,8 @@ const ARTIFACT_NAME_BY_STEP = {
16
28
  verify: '验证结果.md',
17
29
  retrospective: '复盘报告.md',
18
30
  };
31
+ /** Steps whose artifact files are seeded by new-task (promote/closeout bridge). */
32
+ export const SEEDED_ARTIFACT_STEPS = ['implement', 'verify'];
19
33
  export function getArtifactFileName(step) {
20
34
  return ARTIFACT_NAME_BY_STEP[step];
21
35
  }
@@ -35,6 +49,9 @@ export function formatSpecArtifactRelativePaths() {
35
49
  export function formatSpecArtifactDisplayNames() {
36
50
  return SPEC_ARTIFACT_STEPS.map((step) => getArtifactDisplayName(step)).join(' + ');
37
51
  }
52
+ export function listSeededArtifactFileNames() {
53
+ return Object.keys(SEEDED_ARTIFACTS);
54
+ }
38
55
  const REQUIRED_HEADINGS = {
39
56
  analyze: ['# 分析报告', '## 1. 任务目标'],
40
57
  plan: ['# 实现计划', '## 1. 修改目标'],
@@ -43,7 +60,7 @@ const REQUIRED_HEADINGS = {
43
60
  };
44
61
  export async function initializeArtifacts(taskDir) {
45
62
  const { repoRoot, taskId } = resolveTaskContextFromDir(taskDir);
46
- await Promise.all(Object.entries(DEFAULT_ARTIFACTS).map(([name, content]) => writeTaskArtifactFile(repoRoot, taskId, name, content)));
63
+ await Promise.all(Object.entries(SEEDED_ARTIFACTS).map(([name, content]) => writeTaskArtifactFile(repoRoot, taskId, name, content)));
47
64
  }
48
65
  export function getArtifactPath(taskDir, step) {
49
66
  return path.join(taskDir, ARTIFACTS_DIR, getArtifactFileName(step));
@@ -57,7 +74,9 @@ function normalizeArtifactContent(content) {
57
74
  }
58
75
  function isUnchangedNewTaskTemplate(step, content) {
59
76
  const artifactName = ARTIFACT_NAME_BY_STEP[step];
60
- const template = DEFAULT_ARTIFACTS[artifactName];
77
+ const template = ARTIFACT_TEMPLATES[artifactName];
78
+ if (!template)
79
+ return false;
61
80
  return normalizeArtifactContent(content) === normalizeArtifactContent(template);
62
81
  }
63
82
  function firstRequiredSectionIsEmpty(step, content) {
@@ -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,488 @@
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 { fileURLToPath } from "node:url";
6
+ import { platform } from "node:os";
7
+ export const LOOP_AGENT_PACKAGE_NAME = "@tea-agent/loop-agent";
8
+ // ---------------------------------------------------------------------------
9
+ // Public helpers
10
+ // ---------------------------------------------------------------------------
11
+ /**
12
+ * Read the `version` field from a package.json file.
13
+ *
14
+ * Returns `undefined` when the file is missing, unreadable, or lacks a
15
+ * string `version` field — callers must treat `undefined` as a hard failure
16
+ * for identity construction.
17
+ */
18
+ export function readPackageVersion(packageDir) {
19
+ try {
20
+ const raw = readFileSync(path.join(packageDir, "package.json"), "utf-8");
21
+ const parsed = JSON.parse(raw);
22
+ if (typeof parsed.version === "string")
23
+ return parsed.version;
24
+ }
25
+ catch {
26
+ // file missing or unreadable
27
+ }
28
+ return undefined;
29
+ }
30
+ /**
31
+ * Read the `name` field from a package.json file.
32
+ *
33
+ * Returns `undefined` when the file is missing, unreadable, or lacks a
34
+ * string `name` field.
35
+ */
36
+ export function readPackageName(packageDir) {
37
+ try {
38
+ const raw = readFileSync(path.join(packageDir, "package.json"), "utf-8");
39
+ const parsed = JSON.parse(raw);
40
+ if (typeof parsed.name === "string")
41
+ return parsed.name;
42
+ }
43
+ catch {
44
+ // file missing or unreadable
45
+ }
46
+ return undefined;
47
+ }
48
+ /**
49
+ * Compute the SHA-256 hash of a file on disk.
50
+ *
51
+ * Returns `undefined` when the file is unreadable — callers must treat this
52
+ * as a hard failure for identity construction.
53
+ */
54
+ export function computeBinarySha256(filePath) {
55
+ try {
56
+ const buffer = readFileSync(filePath);
57
+ return createHash("sha256").update(buffer).digest("hex");
58
+ }
59
+ catch {
60
+ return undefined;
61
+ }
62
+ }
63
+ /**
64
+ * Compute a portable, deterministic content fingerprint for the installed
65
+ * package rooted at `packageRoot`.
66
+ *
67
+ * The fingerprint covers four scopes (package.json, bin/**, dist/**,
68
+ * skills/**) and is independent of the absolute installation path.
69
+ *
70
+ * Sorting uses code-point order (UTF-8 byte comparison), NOT localeCompare,
71
+ * to ensure deterministic behaviour across locales.
72
+ */
73
+ export function computePackageFingerprint(packageRoot) {
74
+ const scope = [
75
+ "package.json",
76
+ "bin/**",
77
+ "dist/**",
78
+ "skills/**",
79
+ ];
80
+ // Collecting entries ---------------------------------------------------
81
+ const entries = [];
82
+ // 1) package.json (required single file)
83
+ addRequiredFile(entries, path.join(packageRoot, "package.json"), "");
84
+ // 2) bin/**, dist/**, skills/** via recursive walk
85
+ for (const dir of ["bin", "dist", "skills"]) {
86
+ const absoluteDir = path.join(packageRoot, dir);
87
+ collectTree(entries, absoluteDir, dir);
88
+ }
89
+ // 3) Deterministic sort by relative path (code-point / byte order)
90
+ entries.sort((a, b) => {
91
+ const aBuf = Buffer.from(a.relativePath, "utf-8");
92
+ const bBuf = Buffer.from(b.relativePath, "utf-8");
93
+ const minLen = Math.min(aBuf.length, bBuf.length);
94
+ for (let i = 0; i < minLen; i++) {
95
+ if (aBuf[i] !== bBuf[i])
96
+ return aBuf[i] - bBuf[i];
97
+ }
98
+ return aBuf.length - bBuf.length;
99
+ });
100
+ // 4) Cumulative hash: hash of (<relativePath> + "\t" + <contentHash>) lines
101
+ const cumulative = createHash("sha256");
102
+ for (const entry of entries) {
103
+ cumulative.update(`${entry.relativePath}\t${entry.contentHash}\n`);
104
+ }
105
+ return {
106
+ algorithm: "sha256",
107
+ scopeVersion: 1,
108
+ scope,
109
+ value: `sha256:${cumulative.digest("hex")}`,
110
+ fileCount: entries.length,
111
+ };
112
+ }
113
+ /**
114
+ * Resolve a requested bin name or path to the absolute executable.
115
+ *
116
+ * Resolution strategy:
117
+ * 1. If `requested` is already an absolute path, use it directly.
118
+ * 2. If `requested` contains a path separator (/, \\), resolve against cwd.
119
+ * 3. Otherwise, search PATH directories (with optional PATHEXT on Windows).
120
+ * PATH misses fail closed; a bare command name is never invented relative to
121
+ * cwd when it was not actually found.
122
+ *
123
+ * After this call, the result must be treated as frozen — subsequent PATH
124
+ * mutations must not trigger a new resolution.
125
+ */
126
+ export function resolveBinPaths(requested, resolvedAt, inputs) {
127
+ const cwd = inputs?.cwd ?? process.cwd();
128
+ const env = inputs?.env ?? process.env;
129
+ const osPlatform = inputs?.platform ?? platform();
130
+ if (path.isAbsolute(requested)) {
131
+ const entry = path.resolve(requested);
132
+ assertRegularFile(entry, requested);
133
+ return { requested, entry, realEntry: realpathSync(entry), resolvedAt };
134
+ }
135
+ // Path-like relative (contains / or \)
136
+ if (requested.includes("/") || requested.includes("\\")) {
137
+ const resolved = path.resolve(cwd, requested);
138
+ assertRegularFile(resolved, requested);
139
+ return { requested, entry: resolved, realEntry: realpathSync(resolved), resolvedAt };
140
+ }
141
+ // Search PATH
142
+ const pathEnv = env.PATH ?? env.Path ?? env.path ?? "";
143
+ const candidates = resolveFromPath(requested, pathEnv, osPlatform, env.PATHEXT ?? env.Pathext ?? env.pathext, cwd);
144
+ if (candidates.length > 0) {
145
+ const entry = candidates[0];
146
+ assertRegularFile(entry, requested);
147
+ return { requested, entry, realEntry: realpathSync(entry), resolvedAt };
148
+ }
149
+ throw new Error(`controller identity: cannot resolve executable ${JSON.stringify(requested)}`);
150
+ }
151
+ /**
152
+ * Resolve an npm-generated Windows `.cmd` shim to the referenced JavaScript
153
+ * bin entry within the same package.
154
+ *
155
+ * On Windows, npm creates `.cmd` wrappers like:
156
+ * ```
157
+ * @IF EXIST "%~dp0\node.exe" (
158
+ * "%~dp0\node.exe" "%~dp0\..\package\bin\cli.js" %*
159
+ * ) ELSE (
160
+ * ...
161
+ * )
162
+ * ```
163
+ *
164
+ * This function parses the `.cmd` shim, extracts the JS script reference, and
165
+ * returns the resolved absolute path. Returns `undefined` if the shim cannot
166
+ * be parsed or the referenced script does not exist.
167
+ *
168
+ * Pure function: does not require a Windows host to execute.
169
+ */
170
+ export function resolveCmdShim(shimPath) {
171
+ try {
172
+ const content = readFileSync(shimPath, "utf-8");
173
+ const matches = content.matchAll(/"(?:%dp0%|%~dp0)([^"]+\.(?:js|mjs|cjs))"/gi);
174
+ for (const match of matches) {
175
+ if (!match[1])
176
+ continue;
177
+ const windowsRelative = match[1].replace(/^[/\\]+/, "");
178
+ const scriptPath = path.resolve(path.dirname(shimPath), ...windowsRelative.split(/[\\/]+/));
179
+ try {
180
+ if (statSync(scriptPath).isFile())
181
+ return realpathSync(scriptPath);
182
+ }
183
+ catch {
184
+ // Try the next JavaScript reference in the shim.
185
+ }
186
+ }
187
+ return undefined;
188
+ }
189
+ catch {
190
+ return undefined;
191
+ }
192
+ }
193
+ /** Check whether a file is a JavaScript script (node shebang or JS extension). */
194
+ export function isJavaScriptScript(filePath) {
195
+ try {
196
+ const header = readFileSync(filePath).subarray(0, 256).toString("utf-8").trimStart();
197
+ if (header.startsWith("#!/usr/bin/env node") || header.startsWith("#!/usr/bin/node") || header.startsWith("#!node")) {
198
+ return true;
199
+ }
200
+ }
201
+ catch {
202
+ // can't read — check by extension only
203
+ }
204
+ const ext = path.extname(filePath).toLowerCase();
205
+ return ext === ".js" || ext === ".mjs" || ext === ".cjs" || ext === ".ts";
206
+ }
207
+ // ---------------------------------------------------------------------------
208
+ // Package root resolution
209
+ // ---------------------------------------------------------------------------
210
+ /**
211
+ * Find the nearest package root by walking upward from `startDir` until a
212
+ * `package.json` is found.
213
+ *
214
+ * Returns `undefined` if no package.json was found at any ancestor (up to 100
215
+ * levels). Callers must treat `undefined` as a hard failure for identity
216
+ * construction.
217
+ */
218
+ export function findPackageRoot(startDir) {
219
+ let current = path.resolve(startDir);
220
+ for (let i = 0; i < 100; i++) {
221
+ try {
222
+ statSync(path.join(current, "package.json"));
223
+ return current;
224
+ }
225
+ catch {
226
+ const parent = path.dirname(current);
227
+ if (parent === current)
228
+ break;
229
+ current = parent;
230
+ }
231
+ }
232
+ return undefined;
233
+ }
234
+ // ---------------------------------------------------------------------------
235
+ // PATHEXT resolution
236
+ // ---------------------------------------------------------------------------
237
+ /**
238
+ * Expand a Windows executable name into candidate entries using PATHEXT.
239
+ *
240
+ * Example: on a system with `PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.JS`,
241
+ * resolving "loop-agent" produces:
242
+ * ["loop-agent.COM", "loop-agent.EXE", "loop-agent.BAT", "loop-agent.CMD", ...]
243
+ *
244
+ * If the name already has an extension matching a PATHEXT entry, no expansion
245
+ * is performed and the name is used as-is.
246
+ *
247
+ * Pure function for testability.
248
+ */
249
+ export function expandPathext(bin, pathext) {
250
+ if (!pathext)
251
+ return [bin];
252
+ const extensions = pathext
253
+ .split(";")
254
+ .map((ext) => ext.trim())
255
+ .filter(Boolean);
256
+ if (extensions.length === 0)
257
+ return [bin];
258
+ const binLower = bin.toLowerCase();
259
+ const alreadyHasExt = extensions.some((ext) => binLower.endsWith(ext.toLowerCase()));
260
+ if (alreadyHasExt)
261
+ return [bin];
262
+ return extensions.map((ext) => bin + ext);
263
+ }
264
+ // ---------------------------------------------------------------------------
265
+ // Internal helpers
266
+ // ---------------------------------------------------------------------------
267
+ function addRequiredFile(entries, absolutePath, relativeDir) {
268
+ const buffer = readFileSync(absolutePath);
269
+ const contentHash = createHash("sha256").update(buffer).digest("hex");
270
+ const relativePath = relativeDir
271
+ ? path.posix.join(relativeDir, path.basename(absolutePath))
272
+ : path.basename(absolutePath);
273
+ entries.push({ relativePath, contentHash });
274
+ }
275
+ function collectTree(entries, absoluteDir, relativeDir) {
276
+ let stats;
277
+ try {
278
+ stats = statSync(absoluteDir);
279
+ }
280
+ catch (error) {
281
+ if (isNotFound(error))
282
+ return; // optional scope directory does not exist
283
+ throw error;
284
+ }
285
+ if (!stats.isDirectory())
286
+ return;
287
+ const dirents = readdirSync(absoluteDir, { withFileTypes: true });
288
+ for (const dirent of dirents) {
289
+ const childAbs = path.join(absoluteDir, dirent.name);
290
+ const childRel = path.posix.join(relativeDir, dirent.name);
291
+ if (dirent.isFile()) {
292
+ addRequiredFile(entries, childAbs, relativeDir);
293
+ }
294
+ else if (dirent.isDirectory()) {
295
+ collectTree(entries, childAbs, childRel);
296
+ }
297
+ // Ignore symlinks, sockets, etc.
298
+ }
299
+ }
300
+ function isNotFound(error) {
301
+ return Boolean(error
302
+ && typeof error === "object"
303
+ && "code" in error
304
+ && error.code === "ENOENT");
305
+ }
306
+ // ---------------------------------------------------------------------------
307
+ // Internal: PATH resolution (synchronous, no external dependencies)
308
+ // ---------------------------------------------------------------------------
309
+ function resolveFromPath(bin, pathEnv, osPlatform, pathext, cwd) {
310
+ const results = [];
311
+ const dirs = pathEnv.split(osPlatform === "win32" ? ";" : path.delimiter);
312
+ // On Windows, expand with PATHEXT
313
+ const isWindows = osPlatform === "win32";
314
+ for (const rawDir of dirs) {
315
+ const dir = rawDir || cwd;
316
+ if (isWindows) {
317
+ // Get PATHEXT from a well-known env var — in tests, passed via the
318
+ // merged env through ResolutionInputs. Here we fall back to process.env.
319
+ const candidates = expandPathext(bin, pathext ?? ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC");
320
+ for (const candidate of candidates) {
321
+ const filePath = path.join(dir, candidate);
322
+ try {
323
+ if (!statSync(filePath).isFile())
324
+ continue;
325
+ results.push(filePath);
326
+ }
327
+ catch {
328
+ // not found in this PATH entry
329
+ }
330
+ }
331
+ }
332
+ else {
333
+ const candidate = path.join(dir, bin);
334
+ try {
335
+ if (!statSync(candidate).isFile())
336
+ continue;
337
+ results.push(candidate);
338
+ }
339
+ catch {
340
+ // not found in this PATH entry
341
+ }
342
+ }
343
+ }
344
+ return results;
345
+ }
346
+ function assertRegularFile(filePath, requested) {
347
+ try {
348
+ if (statSync(filePath).isFile())
349
+ return;
350
+ }
351
+ catch {
352
+ // Fall through to the stable error below.
353
+ }
354
+ throw new Error(`controller identity: resolved executable for ${JSON.stringify(requested)} is not a readable file: ${filePath}`);
355
+ }
356
+ // ---------------------------------------------------------------------------
357
+ // Running-controller identity (in-process capture)
358
+ // ---------------------------------------------------------------------------
359
+ function safeRealpath(filePath) {
360
+ try {
361
+ return realpathSync(filePath);
362
+ }
363
+ catch {
364
+ return filePath;
365
+ }
366
+ }
367
+ /**
368
+ * Build a ControllerIdentityV1 from a readable entry file inside a Node package
369
+ * root. Returns `undefined` when any required identity component cannot be
370
+ * resolved. This reuses the same ControllerIdentityV1 protocol the Worker uses;
371
+ * it never invents a second identity shape.
372
+ */
373
+ export function buildControllerIdentityFromEntry(input) {
374
+ const resolvedAt = (input.now ?? new Date()).toISOString();
375
+ const realEntry = safeRealpath(path.resolve(input.entry));
376
+ try {
377
+ if (!statSync(realEntry).isFile())
378
+ return undefined;
379
+ }
380
+ catch {
381
+ return undefined;
382
+ }
383
+ const packageRoot = findPackageRoot(path.dirname(realEntry));
384
+ if (!packageRoot)
385
+ return undefined;
386
+ const packageName = readPackageName(packageRoot);
387
+ if (!packageName)
388
+ return undefined;
389
+ const packageVersion = readPackageVersion(packageRoot);
390
+ if (!packageVersion)
391
+ return undefined;
392
+ const binarySha256 = computeBinarySha256(realEntry);
393
+ if (!binarySha256)
394
+ return undefined;
395
+ let packageFingerprint;
396
+ try {
397
+ packageFingerprint = computePackageFingerprint(packageRoot);
398
+ }
399
+ catch {
400
+ return undefined;
401
+ }
402
+ const isScript = isJavaScriptScript(realEntry);
403
+ const launch = isScript
404
+ ? { command: safeRealpath(process.execPath), argsPrefix: [realEntry] }
405
+ : { command: realEntry, argsPrefix: [] };
406
+ return {
407
+ schemaVersion: 1,
408
+ packageName,
409
+ binName: path.basename(realEntry),
410
+ requested: input.requested,
411
+ entry: path.resolve(input.entry),
412
+ realEntry,
413
+ launch,
414
+ binarySha256,
415
+ packageRoot,
416
+ packageVersion,
417
+ packageFingerprint,
418
+ resolvedAt,
419
+ };
420
+ }
421
+ /**
422
+ * Stable content anchor for a controller identity. Two identities are the same
423
+ * controller when their anchors are equal. Kept in sync with the Worker anchor
424
+ * so DAG-owned and Worker-owned identity facts are comparable.
425
+ */
426
+ export function controllerIdentityAnchor(identity) {
427
+ return JSON.stringify({
428
+ schemaVersion: identity.schemaVersion,
429
+ packageName: identity.packageName,
430
+ binName: identity.binName,
431
+ requested: identity.requested,
432
+ entry: identity.entry,
433
+ realEntry: identity.realEntry,
434
+ launch: identity.launch,
435
+ binarySha256: identity.binarySha256,
436
+ packageRoot: identity.packageRoot,
437
+ packageVersion: identity.packageVersion,
438
+ packageFingerprint: identity.packageFingerprint,
439
+ });
440
+ }
441
+ export function controllerIdentitiesMatch(left, right) {
442
+ return Boolean(left && right && controllerIdentityAnchor(left) === controllerIdentityAnchor(right));
443
+ }
444
+ /**
445
+ * Resolve the identity of the controller that is executing this process.
446
+ *
447
+ * Preference order:
448
+ * 1. The launched CLI entry (argv[1]) when it resolves inside the loop-agent
449
+ * package — this is the real controller binary in production.
450
+ * 2. The package that physically contains this module — always the loop-agent
451
+ * package, which keeps identity resolvable under tests and source runs.
452
+ *
453
+ * Returns `undefined` when no loop-agent-rooted identity can be built. Callers
454
+ * treat `undefined` as legacy-unpinned rather than fabricating an identity.
455
+ */
456
+ export function resolveRunningControllerIdentity(options) {
457
+ const argv = options?.argv ?? process.argv;
458
+ const entryArg = argv[1];
459
+ if (entryArg) {
460
+ try {
461
+ const abs = path.resolve(entryArg);
462
+ const root = findPackageRoot(path.dirname(abs));
463
+ if (root && readPackageName(root) === LOOP_AGENT_PACKAGE_NAME) {
464
+ const identity = buildControllerIdentityFromEntry({
465
+ requested: entryArg,
466
+ entry: abs,
467
+ ...(options?.now ? { now: options.now } : {}),
468
+ });
469
+ if (identity)
470
+ return identity;
471
+ }
472
+ }
473
+ catch {
474
+ // Fall back to the module location below.
475
+ }
476
+ }
477
+ try {
478
+ const moduleFile = fileURLToPath(import.meta.url);
479
+ return buildControllerIdentityFromEntry({
480
+ requested: entryArg ?? "loop-agent",
481
+ entry: moduleFile,
482
+ ...(options?.now ? { now: options.now } : {}),
483
+ });
484
+ }
485
+ catch {
486
+ return undefined;
487
+ }
488
+ }
@@ -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";