@tea-agent/loop-agent 0.10.0 → 0.11.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.
- package/AGENTS.md +10 -2
- package/CHANGELOG.md +67 -25
- package/README.md +82 -11
- package/dist/application/dag/args.js +1 -12
- package/dist/application/dag/generate-task-dag.js +23 -2
- package/dist/application/dag/run-dag.js +1 -27
- package/dist/application/dag/validate-dag.js +2 -2
- package/dist/application/loop/run-action.js +0 -4
- package/dist/cli/command-definitions.js +44 -16
- package/dist/cli/program.js +40 -23
- package/dist/cli/update/notifier.js +117 -0
- package/dist/cli/update/npm-client.js +151 -0
- package/dist/cli/update/policy.js +58 -0
- package/dist/cli/update/state.js +68 -0
- package/dist/cli.js +33 -0
- package/dist/commands/cursor-prompt.js +42 -82
- package/dist/commands/dag-approve.js +36 -0
- package/dist/commands/delegate.js +75 -77
- package/dist/commands/doctor.js +0 -18
- package/dist/commands/init.js +476 -91
- package/dist/commands/instructions.js +7 -10
- package/dist/commands/loop.js +4 -20
- package/dist/commands/plan.js +50 -0
- package/dist/executors/config-core.js +0 -51
- package/dist/executors/dag-pi-executor.js +1 -1
- package/dist/executors/dag.js +0 -1
- package/dist/executors/index.js +0 -2
- package/dist/executors/model-routing.js +9 -9
- package/dist/executors/shell-executor.js +1 -1
- package/dist/governance/checks.js +6 -3
- package/dist/governance/exec-plans.js +545 -0
- package/dist/governance/manifest-types.js +24 -2
- package/dist/infrastructure/harness/loop-action-store.js +0 -3
- package/dist/records/harvest.js +2 -23
- package/dist/records/one-shot-runs.js +1 -1
- package/dist/shared/artifacts-core.js +24 -5
- package/dist/shared/output-truncation.js +37 -0
- package/dist/shared/package-metadata.js +353 -0
- package/dist/{executors/cursor-executor.js → sidecars/cursor-prompt/executor.js} +2 -42
- package/dist/sidecars/cursor-prompt/index.js +3 -0
- package/dist/sidecars/cursor-prompt/stream.js +121 -0
- package/dist/task/config-types.js +28 -12
- package/dist/task/delegate.js +9 -21
- package/dist/task/runtime.js +1 -2
- package/dist/worker/cli.js +29 -2
- package/dist/worker/delivery/final-verification.js +47 -11
- package/dist/worker/delivery/package.js +63 -10
- package/dist/worker/feature/run.js +60 -8
- package/dist/worker/loop-agent/loop-agent-client.js +329 -126
- package/dist/worker/observability/read-model.js +27 -1
- package/dist/worker/observe/static/app.js +326 -45
- package/dist/worker/observe/static/index.html +1 -1
- package/dist/worker/observe/static/styles.css +5 -4
- package/dist/worker/preflight.js +49 -1
- package/dist/worker/run-task/run-task.js +22 -12
- package/dist/worker/runner/run-ready.js +76 -12
- package/dist/worker/task-spec/schema.js +0 -1
- package/dist/workflows/dag/convergence/controller.js +1 -1
- package/dist/workflows/dag/executor-registry.js +0 -2
- package/dist/workflows/dag/init-hybrid.js +402 -25
- package/dist/workflows/dag/node-execution.js +61 -7
- package/dist/workflows/dag/runner.js +45 -17
- package/dist/workflows/dag/scheduler.js +7 -2
- package/dist/workflows/dag/sdd-embedded.js +128 -0
- package/dist/workflows/dag/skill-instructions.js +5 -4
- package/dist/workflows/dag/skill-snapshot.js +527 -0
- package/dist/workflows/dag/types.js +42 -9
- package/dist/workflows/dag/validate.js +5 -8
- package/dist/workflows/loop/actions/dag-action.js +0 -2
- package/dist/workflows/loop/actions/shared.js +1 -1
- package/dist/workflows/loop/actions.js +14 -31
- package/dist/workflows/loop/benchmark.js +1 -1
- package/dist/workflows/loop/index.js +1 -1
- package/dist/workflows/loop/policy/auto-policy.js +22 -14
- package/dist/workflows/loop/policy/path-patterns.js +13 -0
- package/docs/README.md +36 -33
- package/docs/agent-dag-recovery-playbook.md +1 -1
- package/docs/agent-dag-runner.md +2 -2
- package/docs/architecture/README.md +26 -0
- package/docs/architecture/dag-execution.md +134 -0
- package/docs/architecture/evolution.md +52 -0
- package/docs/architecture/facts-and-state.md +58 -0
- package/docs/architecture/runtime-boundaries.md +45 -17
- package/docs/architecture/system-overview.md +93 -0
- package/docs/architecture/worker-and-feature.md +81 -0
- package/docs/cursor-prompt-sidecar.md +36 -0
- package/docs/decisions/README.md +13 -1
- package/docs/design/README.md +42 -21
- package/docs/development-principles.md +2 -2
- package/docs/exec-plans/active/README.md +2 -2
- package/docs/exec-plans/completed/README.md +12 -0
- package/docs/feature-workflow.md +50 -4
- package/docs/harness-methodology-debugging.md +1 -1
- package/docs/harness-methodology-tdd.md +3 -3
- package/docs/init-surface.manifest.json +60 -25
- package/docs/loop-agent-harness.md +28 -4
- package/docs/progress/README.md +32 -1
- package/docs/reports/README.md +84 -18
- package/docs/skills/README.md +2 -1
- package/docs/skills/vetted-skill-registry.md +2 -1
- package/docs/templates/agent-dag-report.schema.json +6 -6
- package/docs/templates/agent-dag.base.json +0 -5
- package/docs/templates/agent-dag.final-verification.json +0 -5
- package/docs/templates/agent-dag.schema.json +1 -2
- package/docs/templates/agent-dag.supervised-implementation.json +1 -6
- package/docs/templates/frontend-design-contract.md +33 -0
- package/docs/templates/frontend-task-constraints.md +25 -0
- package/docs/templates/frontend-task-requirement.md +61 -0
- package/docs/templates/harness.schema.json +10 -12
- package/docs/templates/hybrid-dag.json +1 -6
- package/docs/templates/interactive-ui-round2-experiment.md +1 -1
- package/docs/templates/product-line/task.yaml +0 -1
- package/docs/templates/project-start-checklist.md +2 -2
- package/docs/templates/worker-dogfood-evidence.md +28 -0
- package/docs/templates/worker-dogfood-setup.md +20 -0
- package/docs/verification-matrix.md +10 -0
- package/examples/decision-gate-agent-dag.json +87 -33
- package/examples/example-dag.json +0 -5
- package/examples/hybrid-loop-agent-dag.json +0 -5
- package/harness.json +7 -15
- package/package.json +22 -46
- package/scripts/check-product-line-docs.sh +10 -7
- package/skills/agent-worker/SKILL.md +37 -0
- package/skills/agent-worker/references/agent-worker-operator.md +43 -0
- package/skills/frontend-design-review/SKILL.md +59 -0
- package/skills/frontend-design-review/references/review-checklist.md +37 -0
- package/skills/frontend-implementation/SKILL.md +51 -0
- package/skills/frontend-implementation/references/code-standards.md +34 -0
- package/skills/frontend-implementation/references/design-spec.md +46 -0
- package/skills/frontend-implementation/references/node-contracts.md +32 -0
- package/skills/frontend-review/SKILL.md +53 -0
- package/skills/frontend-review/references/review-findings.md +42 -0
- package/skills/frontend-verification/SKILL.md +40 -0
- package/skills/frontend-verification/references/verification-checklist.md +56 -0
- package/skills/grill-me/SKILL.md +10 -0
- package/skills/grill-with-docs/SKILL.md +88 -0
- package/skills/grill-with-docs/adr-format.md +47 -0
- package/skills/grill-with-docs/context-format.md +60 -0
- package/skills/loop-agent/SKILL.md +11 -9
- package/skills/loop-agent/references/command-reference.md +13 -15
- package/skills/loop-agent/references/docs-converge.md +126 -0
- package/skills/loop-agent/references/harness-policy.md +7 -7
- package/skills/loop-agent/references/hybrid-dag.md +15 -18
- package/skills/loop-agent/references/long-running-loop.md +4 -6
- package/skills/loop-agent/references/multi-worktree.md +6 -6
- package/skills/loop-agent/references/orchestrator-and-interventions.md +3 -3
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +14 -11
- package/skills/loop-agent/references/task-workflow.md +1 -1
- package/skills/using-git-worktrees/SKILL.md +215 -0
- package/dist/commands/cursor-worker.js +0 -43
- package/dist/cursor-worker-entry.js +0 -8
- package/dist/executors/cursor-artifacts.js +0 -33
- package/dist/executors/cursor-execution-log.js +0 -81
- package/dist/executors/cursor-executor-artifacts.js +0 -134
- package/dist/executors/cursor-run.js +0 -115
- package/dist/executors/cursor-tool.js +0 -94
- package/dist/executors/cursor-worker-client.js +0 -223
- package/dist/executors/cursor-worker-protocol.js +0 -18
- package/dist/executors/cursor-worker-server.js +0 -54
- package/dist/executors/cursor-worker.js +0 -3
- package/dist/executors/cursor.js +0 -6
- package/dist/executors/dag-cursor-executor.js +0 -87
- package/dist/workflows/loop/actions/cursor-fix.js +0 -191
- package/dist/workflows/loop/policy/cursor-fix-policy.js +0 -31
- package/docs/cursor-executor-usage.md +0 -25
- 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
|
-
|
|
6
|
-
|
|
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(
|
|
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 =
|
|
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,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 {
|
|
2
|
-
import {
|
|
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
|
+
}
|