@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.
- package/AGENTS.md +10 -2
- package/CHANGELOG.md +91 -24
- package/README.md +84 -12
- package/dist/application/dag/args.js +1 -12
- package/dist/application/dag/generate-task-dag.js +38 -2
- package/dist/application/dag/run-dag.js +11 -27
- package/dist/application/dag/validate-dag.js +13 -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 +547 -95
- 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 +488 -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 +29 -12
- package/dist/task/delegate.js +9 -21
- package/dist/task/runtime.js +1 -2
- package/dist/worker/cli.js +32 -3
- 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/event-history.js +216 -0
- package/dist/worker/observability/read-model.js +338 -83
- package/dist/worker/observe/paths.js +17 -0
- package/dist/worker/observe/routes.js +165 -21
- package/dist/worker/observe/server.js +59 -1
- package/dist/worker/observe/static/api.js +27 -0
- package/dist/worker/observe/static/app.js +120 -2317
- package/dist/worker/observe/static/constants.js +148 -0
- package/dist/worker/observe/static/copy.js +67 -0
- package/dist/worker/observe/static/dag-helpers.js +172 -0
- package/dist/worker/observe/static/dag-model.js +72 -0
- package/dist/worker/observe/static/dom.js +61 -0
- package/dist/worker/observe/static/format-pool.js +67 -0
- package/dist/worker/observe/static/format.js +292 -0
- package/dist/worker/observe/static/index.html +300 -82
- package/dist/worker/observe/static/kpi.js +94 -0
- package/dist/worker/observe/static/relations.js +128 -0
- package/dist/worker/observe/static/router.js +85 -0
- package/dist/worker/observe/static/run-processing.js +148 -0
- package/dist/worker/observe/static/shell-chrome.js +68 -0
- package/dist/worker/observe/static/state.js +253 -0
- package/dist/worker/observe/static/styles.css +1720 -495
- package/dist/worker/observe/static/views/batch.js +226 -0
- package/dist/worker/observe/static/views/dag-graph.js +172 -0
- package/dist/worker/observe/static/views/dag-inspector.js +477 -0
- package/dist/worker/observe/static/views/dag.js +362 -0
- package/dist/worker/observe/static/views/dashboard.js +442 -0
- package/dist/worker/observe/static/views/failures.js +143 -0
- package/dist/worker/observe/static/views/feature.js +453 -0
- package/dist/worker/observe/static/views/pool.js +347 -0
- package/dist/worker/observe/static/views/run.js +453 -0
- package/dist/worker/observe/static/views/session-timeline.js +205 -0
- package/dist/worker/observe/static/views/shell.js +7 -0
- package/dist/worker/observe/static/views/task.js +260 -0
- package/dist/worker/observe/static/views/timeline.js +163 -0
- 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/controller-identity.js +104 -0
- 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 +797 -27
- package/dist/workflows/dag/node-execution.js +183 -35
- package/dist/workflows/dag/repair-artifact.js +91 -0
- package/dist/workflows/dag/report.js +50 -0
- package/dist/workflows/dag/retry-policy.js +138 -0
- package/dist/workflows/dag/runner.js +77 -17
- package/dist/workflows/dag/runtime-contract.js +87 -0
- 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 +529 -0
- package/dist/workflows/dag/types.js +86 -10
- package/dist/workflows/dag/validate.js +73 -12
- 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 +28 -3
- package/docs/architecture/README.md +26 -0
- package/docs/architecture/dag-execution.md +140 -0
- package/docs/architecture/evolution.md +53 -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 +43 -21
- package/docs/development-principles.md +2 -2
- package/docs/exec-plans/active/README.md +1 -3
- package/docs/exec-plans/completed/README.md +23 -0
- package/docs/feature-workflow.md +78 -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 +50 -1
- package/docs/reports/README.md +90 -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 +23 -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 +70 -3
- package/docs/templates/agent-dag.supervised-implementation.json +9 -8
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +139 -0
- package/docs/templates/backend-test-dag.json +276 -0
- package/docs/templates/backend-test-dag.retrospect.prompt.md +125 -0
- package/docs/templates/backend-test-dag.review-cases.prompt.md +81 -0
- 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 +14 -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 +36 -20
- 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/loop-agent/references/verification-and-failure-handling.md +6 -0
- 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,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 {
|
|
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";
|