@tea-agent/loop-agent 0.1.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 +121 -0
- package/CHANGELOG.md +37 -0
- package/README.md +144 -0
- package/bin/loop-agent.js +21 -0
- package/dist/adapters/aimax.js +91 -0
- package/dist/adapters/context.js +32 -0
- package/dist/adapters/index.js +28 -0
- package/dist/adapters/loop-agent.js +98 -0
- package/dist/adapters/types.js +1 -0
- package/dist/cli/catalog.js +259 -0
- package/dist/cli/help.js +55 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/program.js +505 -0
- package/dist/cli.js +12 -0
- package/dist/commands/closeout.js +13 -0
- package/dist/commands/coverage-audit.js +14 -0
- package/dist/commands/cursor-prompt.js +222 -0
- package/dist/commands/cursor-worker.js +43 -0
- package/dist/commands/dag-approve.js +102 -0
- package/dist/commands/dag-final-verification.js +76 -0
- package/dist/commands/dag-init-hybrid.js +56 -0
- package/dist/commands/dag-reconcile-tasks.js +51 -0
- package/dist/commands/dag-reject.js +91 -0
- package/dist/commands/dag-report.js +177 -0
- package/dist/commands/dag-resume.js +34 -0
- package/dist/commands/dag-run-task.js +470 -0
- package/dist/commands/dag-validate.js +186 -0
- package/dist/commands/dag-workflow-compile.js +91 -0
- package/dist/commands/dag-workflow-plan.js +130 -0
- package/dist/commands/dag-workflow-validate.js +66 -0
- package/dist/commands/delegate.js +132 -0
- package/dist/commands/docs-archive.js +5 -0
- package/dist/commands/docs-audit.js +5 -0
- package/dist/commands/doctor.js +50 -0
- package/dist/commands/goal.js +92 -0
- package/dist/commands/handoff-check.js +5 -0
- package/dist/commands/harvest.js +44 -0
- package/dist/commands/inspect.js +11 -0
- package/dist/commands/instructions.js +195 -0
- package/dist/commands/knowledge.js +64 -0
- package/dist/commands/loop-benchmark.js +72 -0
- package/dist/commands/loop.js +241 -0
- package/dist/commands/new-task.js +5 -0
- package/dist/commands/pi-prompt.js +181 -0
- package/dist/commands/pi-reuse-benchmark.js +153 -0
- package/dist/commands/plan-list.js +5 -0
- package/dist/commands/promote-run.js +29 -0
- package/dist/commands/reference-index.js +16 -0
- package/dist/commands/run-dag.js +184 -0
- package/dist/commands/spine.js +38 -0
- package/dist/commands/stats.js +84 -0
- package/dist/commands/status.js +56 -0
- package/dist/commands/study-init.js +192 -0
- package/dist/commands/workflow.js +259 -0
- package/dist/commands/worktree-create.js +31 -0
- package/dist/commands/worktree-list.js +5 -0
- package/dist/commands/worktree-remove.js +26 -0
- package/dist/cursor-worker-entry.js +8 -0
- package/dist/executors/config-core.js +55 -0
- package/dist/executors/config.js +2 -0
- package/dist/executors/cursor-artifacts.js +33 -0
- package/dist/executors/cursor-execution-log.js +81 -0
- package/dist/executors/cursor-executor-artifacts.js +135 -0
- package/dist/executors/cursor-executor.js +468 -0
- package/dist/executors/cursor-run.js +115 -0
- package/dist/executors/cursor-tool.js +94 -0
- package/dist/executors/cursor-worker-client.js +213 -0
- package/dist/executors/cursor-worker-protocol.js +18 -0
- package/dist/executors/cursor-worker-server.js +54 -0
- package/dist/executors/cursor-worker.js +3 -0
- package/dist/executors/cursor.js +6 -0
- package/dist/executors/dag-cursor-executor.js +88 -0
- package/dist/executors/dag-pi-executor.js +322 -0
- package/dist/executors/dag-static-executor.js +45 -0
- package/dist/executors/dag.js +4 -0
- package/dist/executors/index.js +8 -0
- package/dist/executors/model-routing.js +60 -0
- package/dist/executors/pi-event-serializer.js +43 -0
- package/dist/executors/pi-executor.js +606 -0
- package/dist/executors/pi-reuse-benchmark.js +316 -0
- package/dist/executors/pi-runtime-reuse.js +29 -0
- package/dist/executors/pi-sdk-executor.js +255 -0
- package/dist/executors/pi-sdk.js +1 -0
- package/dist/executors/pi.js +3 -0
- package/dist/executors/shell-executor.js +300 -0
- package/dist/executors/shell-presets.js +47 -0
- package/dist/executors/shell-verification.js +251 -0
- package/dist/executors/shell-write-guard.js +126 -0
- package/dist/executors/shell.js +3 -0
- package/dist/executors/static.js +1 -0
- package/dist/governance/checks.js +434 -0
- package/dist/governance/harness.js +9 -0
- package/dist/governance/index.js +3 -0
- package/dist/governance/manifest-types.js +128 -0
- package/dist/governance/manifest.js +2 -0
- package/dist/governance/path-guard.js +69 -0
- package/dist/governance/path-guards.js +2 -0
- package/dist/governance/profiles.js +3 -0
- package/dist/governance/requirement-coverage.js +425 -0
- package/dist/governance/skill-safety.js +135 -0
- package/dist/governance/spine-audit.js +152 -0
- package/dist/records/closeout.js +2 -0
- package/dist/records/harvest.js +236 -0
- package/dist/records/index.js +3 -0
- package/dist/records/one-shot-runs.js +421 -0
- package/dist/records/promotion.js +199 -0
- package/dist/shared/artifacts-core.js +88 -0
- package/dist/shared/artifacts.js +2 -0
- package/dist/shared/context-files.js +32 -0
- package/dist/shared/context.js +2 -0
- package/dist/shared/copy-dir.js +17 -0
- package/dist/shared/git-progress.js +165 -0
- package/dist/shared/index.js +5 -0
- package/dist/shared/logger.js +23 -0
- package/dist/shared/one-shot-prompt-args.js +98 -0
- package/dist/shared/path-refs.js +31 -0
- package/dist/shared/prompts.js +26 -0
- package/dist/shared/reference-context.js +238 -0
- package/dist/shared/timeout-policy.js +19 -0
- package/dist/shared/timeout.js +1 -0
- package/dist/shared/types.js +5 -0
- package/dist/task/config-types.js +97 -0
- package/dist/task/config.js +2 -0
- package/dist/task/delegate.js +220 -0
- package/dist/task/goal-audit.js +51 -0
- package/dist/task/goal-policy.js +8 -0
- package/dist/task/goal.js +3 -0
- package/dist/task/ids.js +1 -0
- package/dist/task/index.js +9 -0
- package/dist/task/lifecycle.js +1 -0
- package/dist/task/paths.js +1 -0
- package/dist/task/read-model.js +149 -0
- package/dist/task/runtime.js +699 -0
- package/dist/task/source-state.js +1 -0
- package/dist/task/state.js +55 -0
- package/dist/task/subagent-guidance.js +1 -0
- package/dist/task/workflow-state-types.js +92 -0
- package/dist/task/worktree-cleanup.js +140 -0
- package/dist/task/worktree.js +171 -0
- package/dist/workflows/dag/authoring.js +8 -0
- package/dist/workflows/dag/authority-surface.js +138 -0
- package/dist/workflows/dag/canvas-observer.js +474 -0
- package/dist/workflows/dag/decision-envelope.js +502 -0
- package/dist/workflows/dag/decision-evidence.js +153 -0
- package/dist/workflows/dag/decision-gates.js +1 -0
- package/dist/workflows/dag/executor-registry.js +25 -0
- package/dist/workflows/dag/facts.js +4 -0
- package/dist/workflows/dag/failure-category.js +111 -0
- package/dist/workflows/dag/final-verification.js +180 -0
- package/dist/workflows/dag/governance-constants.js +5 -0
- package/dist/workflows/dag/governance-profile.js +405 -0
- package/dist/workflows/dag/index.js +6 -0
- package/dist/workflows/dag/init-hybrid.js +855 -0
- package/dist/workflows/dag/knowledge-curator.js +162 -0
- package/dist/workflows/dag/lifecycle.js +484 -0
- package/dist/workflows/dag/prompt-source.js +88 -0
- package/dist/workflows/dag/prompt.js +130 -0
- package/dist/workflows/dag/reconcile-tasks.js +404 -0
- package/dist/workflows/dag/recovery-recommendation.js +226 -0
- package/dist/workflows/dag/repair-artifact.js +136 -0
- package/dist/workflows/dag/report.js +1019 -0
- package/dist/workflows/dag/runner.js +1677 -0
- package/dist/workflows/dag/runtime.js +5 -0
- package/dist/workflows/dag/skill-instructions.js +471 -0
- package/dist/workflows/dag/skills.js +41 -0
- package/dist/workflows/dag/spec.js +3 -0
- package/dist/workflows/dag/topo.js +30 -0
- package/dist/workflows/dag/types.js +275 -0
- package/dist/workflows/dag/upstream-artifacts.js +95 -0
- package/dist/workflows/dag/validate.js +527 -0
- package/dist/workflows/dynamic/artifacts.js +65 -0
- package/dist/workflows/dynamic/compile.js +360 -0
- package/dist/workflows/dynamic/compileTypes.js +1 -0
- package/dist/workflows/dynamic/errors.js +5 -0
- package/dist/workflows/dynamic/index.js +7 -0
- package/dist/workflows/dynamic/profiles.js +156 -0
- package/dist/workflows/dynamic/spec.js +114 -0
- package/dist/workflows/dynamic/validate.js +275 -0
- package/dist/workflows/loop/actions.js +1334 -0
- package/dist/workflows/loop/benchmark.js +510 -0
- package/dist/workflows/loop/closeout.js +134 -0
- package/dist/workflows/loop/context.js +48 -0
- package/dist/workflows/loop/events.js +25 -0
- package/dist/workflows/loop/hash.js +32 -0
- package/dist/workflows/loop/index.js +8 -0
- package/dist/workflows/loop/paths.js +17 -0
- package/dist/workflows/loop/rounds.js +81 -0
- package/dist/workflows/loop/signals.js +55 -0
- package/dist/workflows/loop/state.js +116 -0
- package/dist/workflows/loop/templates.js +54 -0
- package/dist/workflows/loop/types.js +28 -0
- package/docs/README.md +62 -0
- package/docs/agent-dag-recovery-playbook.md +158 -0
- package/docs/agent-dag-runner.md +40 -0
- package/docs/cursor-executor-usage.md +25 -0
- package/docs/decisions/README.md +3 -0
- package/docs/design/README.md +36 -0
- package/docs/development-principles.md +71 -0
- package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -0
- package/docs/exec-plans/README.md +6 -0
- package/docs/exec-plans/active/README.md +5 -0
- package/docs/exec-plans/completed/README.md +5 -0
- package/docs/feature-workflow.md +184 -0
- package/docs/harness-methodology-debugging.md +153 -0
- package/docs/harness-methodology-tdd.md +130 -0
- package/docs/harness-methodology-verification.md +27 -0
- package/docs/loop-agent-harness.md +42 -0
- package/docs/progress/README.md +3 -0
- package/docs/reports/README.md +3 -0
- package/docs/templates/adr.md +60 -0
- package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -0
- package/docs/templates/agent-dag-decision-envelope.schema.json +213 -0
- package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -0
- package/docs/templates/agent-dag-decision-gate.prompt.md +246 -0
- package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -0
- package/docs/templates/agent-dag-report.schema.json +423 -0
- package/docs/templates/agent-dag-review-verdict.prompt.md +68 -0
- package/docs/templates/agent-dag.base.json +195 -0
- package/docs/templates/agent-dag.final-verification.json +190 -0
- package/docs/templates/agent-dag.schema.json +316 -0
- package/docs/templates/agent-dag.supervised-implementation.json +500 -0
- package/docs/templates/exec-plan.md +64 -0
- package/docs/templates/feature-spec.md +53 -0
- package/docs/templates/hybrid-dag.json +193 -0
- package/docs/templates/progress-log.md +17 -0
- package/docs/templates/project-start-checklist.md +9 -0
- package/docs/templates/qa-report.md +42 -0
- package/docs/templates/sprint-contract.md +29 -0
- package/docs/verification-matrix.md +30 -0
- package/examples/decision-gate-agent-dag.json +123 -0
- package/examples/example-dag.json +51 -0
- package/examples/hybrid-loop-agent-dag.json +194 -0
- package/harness.json +92 -0
- package/package.json +61 -0
- package/skills/ai-engineering-context/SKILL.md +48 -0
- package/skills/loop-agent/SKILL.md +260 -0
- package/skills/loop-agent/references/README.md +63 -0
- package/skills/loop-agent/references/command-reference.md +315 -0
- package/skills/loop-agent/references/harness-policy.md +258 -0
- package/skills/loop-agent/references/hybrid-dag.md +216 -0
- package/skills/loop-agent/references/learned/README.md +21 -0
- package/skills/loop-agent/references/model-routing.md +36 -0
- package/skills/loop-agent/references/multi-worktree.md +54 -0
- package/skills/loop-agent/references/one-shot-runs.md +85 -0
- package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -0
- package/skills/loop-agent/references/pi-prompt.md +23 -0
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +83 -0
- package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -0
- package/skills/loop-agent/references/task-workflow.md +84 -0
- package/skills/loop-agent/references/verification-and-failure-handling.md +74 -0
- package/skills/requesting-code-review/SKILL.md +101 -0
- package/skills/requesting-code-review/code-reviewer.md +168 -0
- package/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/skills/systematic-debugging/SKILL.md +296 -0
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/skills/systematic-debugging/find-polluter.sh +63 -0
- package/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/skills/systematic-debugging/test-academic.md +14 -0
- package/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/skills/verification-before-completion/SKILL.md +154 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { referencePathJoin } from "../shared/path-refs.js";
|
|
3
|
+
/** Cursor SDK account default (user → team → system). Override per call when needed. */
|
|
4
|
+
export const DEFAULT_CURSOR_MODEL = "default";
|
|
5
|
+
export const DEFAULT_CURSOR_TIMEOUT_MS = 1_800_000;
|
|
6
|
+
/** Detect stalled stream consumption before the full task deadline. */
|
|
7
|
+
export const DEFAULT_CURSOR_STREAM_IDLE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
8
|
+
/** Avoid hanging indefinitely in wait() when the stream is already done. */
|
|
9
|
+
export const WAIT_AFTER_STREAM_GRACE_MS = 15_000;
|
|
10
|
+
export const MAX_CURSOR_STDOUT_BYTES = 64_000;
|
|
11
|
+
export const MAX_CURSOR_STDERR_BYTES = 16_000;
|
|
12
|
+
const ARTIFACT_INSTRUCTIONS = [
|
|
13
|
+
"Write clear deliverables: 修改记录.md (what changed + risk) and 验证结果.md (verification commands + pass/fail).",
|
|
14
|
+
].join("\n");
|
|
15
|
+
const CURSOR_INSTRUCTIONS = [
|
|
16
|
+
"You are a coding agent delegated by a parent agent (Pi).",
|
|
17
|
+
"Work in the configured workspace directory.",
|
|
18
|
+
"Help the user inspect, edit, and validate code with small focused changes.",
|
|
19
|
+
"Before changing files, understand the surrounding code and preserve unrelated user work.",
|
|
20
|
+
"Keep progress updates concise and summarize the result clearly.",
|
|
21
|
+
"Do NOT initiate conversation or ask clarifying questions — deliver the result directly.",
|
|
22
|
+
].join("\n");
|
|
23
|
+
export function resolveArtifactWriteDir(options) {
|
|
24
|
+
if (options.artifactDir)
|
|
25
|
+
return options.artifactDir;
|
|
26
|
+
if (options.runDir)
|
|
27
|
+
return referencePathJoin(options.runDir, "artifacts");
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
export function buildArtifactPathPrompt(writeDir) {
|
|
31
|
+
if (!writeDir)
|
|
32
|
+
return `After changes, write artifacts/修改记录.md and artifacts/验证结果.md with verification evidence.
|
|
33
|
+
${ARTIFACT_INSTRUCTIONS}`;
|
|
34
|
+
return [
|
|
35
|
+
`After changes, write the following files:`,
|
|
36
|
+
` - ${referencePathJoin(writeDir, "修改记录.md")}`,
|
|
37
|
+
` - ${referencePathJoin(writeDir, "验证结果.md")}`,
|
|
38
|
+
ARTIFACT_INSTRUCTIONS,
|
|
39
|
+
].join("\n");
|
|
40
|
+
}
|
|
41
|
+
export function buildCursorPrompt(userTask, artifactOptions) {
|
|
42
|
+
const artifactPrompt = buildArtifactPathPrompt(resolveArtifactWriteDir(artifactOptions ?? {}));
|
|
43
|
+
return [
|
|
44
|
+
CURSOR_INSTRUCTIONS,
|
|
45
|
+
artifactPrompt,
|
|
46
|
+
"",
|
|
47
|
+
"## User Task",
|
|
48
|
+
userTask,
|
|
49
|
+
].join("\n");
|
|
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
|
+
export function redactApiKey(text, apiKey) {
|
|
84
|
+
if (!apiKey)
|
|
85
|
+
return text;
|
|
86
|
+
return text.split(apiKey).join("<redacted-api-key>");
|
|
87
|
+
}
|
|
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
|
+
export function buildRedactedExecutorRecord(input) {
|
|
93
|
+
const apiKey = input.apiKey ?? process.env.CURSOR_API_KEY;
|
|
94
|
+
const stdout = truncateOutput(redactApiKey(input.result.stdout, apiKey), 2_000);
|
|
95
|
+
const stderr = truncateOutput(redactApiKey(input.result.stderr, apiKey), 2_000);
|
|
96
|
+
const record = {
|
|
97
|
+
executor: input.executor,
|
|
98
|
+
model: input.model,
|
|
99
|
+
durationMs: input.durationMs,
|
|
100
|
+
failureCategory: input.failureCategory,
|
|
101
|
+
ok: input.ok,
|
|
102
|
+
status: input.status,
|
|
103
|
+
redactedCommand: redactPromptForLog(input.prompt),
|
|
104
|
+
stdout: stdout.text,
|
|
105
|
+
stderr: stderr.text,
|
|
106
|
+
artifactPath: input.result.artifactPath,
|
|
107
|
+
details: {
|
|
108
|
+
...(input.result.details ?? {}),
|
|
109
|
+
stdoutTruncated: stdout.truncated,
|
|
110
|
+
stderrTruncated: stderr.truncated,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
if (input.result.tokensUsed !== undefined) {
|
|
114
|
+
record.tokensUsed = input.result.tokensUsed;
|
|
115
|
+
}
|
|
116
|
+
return record;
|
|
117
|
+
}
|
|
118
|
+
export function buildCursorExecutorEnv(apiKey) {
|
|
119
|
+
const env = {
|
|
120
|
+
HOME: process.env.HOME,
|
|
121
|
+
PATH: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin",
|
|
122
|
+
NODE_OPTIONS: "",
|
|
123
|
+
};
|
|
124
|
+
if (apiKey) {
|
|
125
|
+
env.CURSOR_API_KEY = apiKey;
|
|
126
|
+
}
|
|
127
|
+
else if (process.env.CURSOR_API_KEY) {
|
|
128
|
+
env.CURSOR_API_KEY = process.env.CURSOR_API_KEY;
|
|
129
|
+
}
|
|
130
|
+
return env;
|
|
131
|
+
}
|
|
132
|
+
export function classifyCursorFailure(input) {
|
|
133
|
+
if (input.timedOut)
|
|
134
|
+
return "timeout";
|
|
135
|
+
const combined = `${input.startupError ?? ""}\n${input.stderr ?? ""}\n${input.stdout ?? ""}`.toLowerCase();
|
|
136
|
+
if (/missing-api-key|missing api key|set cursor_api_key|cursor_api_key environment variable is not set/.test(combined)) {
|
|
137
|
+
return "missing-api-key";
|
|
138
|
+
}
|
|
139
|
+
if (/unauthorized|forbidden|invalid api key|authentication|auth failed/.test(combined)) {
|
|
140
|
+
return "auth";
|
|
141
|
+
}
|
|
142
|
+
if (/network|econnreset|etimedout|socket hang up|connection reset|dns|fetch failed/.test(combined)) {
|
|
143
|
+
return "network";
|
|
144
|
+
}
|
|
145
|
+
if (input.runStatus === "cancelled")
|
|
146
|
+
return "unknown";
|
|
147
|
+
if (input.exitCode !== undefined &&
|
|
148
|
+
input.exitCode !== null &&
|
|
149
|
+
input.exitCode !== 0) {
|
|
150
|
+
return "nonzero-exit";
|
|
151
|
+
}
|
|
152
|
+
if (input.runStatus && !["finished", "completed"].includes(input.runStatus)) {
|
|
153
|
+
return "nonzero-exit";
|
|
154
|
+
}
|
|
155
|
+
return "success";
|
|
156
|
+
}
|
|
157
|
+
export function mapRunStatusToExecutionStatus(runStatus, timedOut) {
|
|
158
|
+
if (timedOut)
|
|
159
|
+
return "timeout";
|
|
160
|
+
if (runStatus === "cancelled")
|
|
161
|
+
return "cancelled";
|
|
162
|
+
if (runStatus === "finished" || runStatus === "completed")
|
|
163
|
+
return "completed";
|
|
164
|
+
return "failed";
|
|
165
|
+
}
|
|
166
|
+
export class CursorTimeoutError extends Error {
|
|
167
|
+
timeoutKind;
|
|
168
|
+
constructor(message, timeoutKind) {
|
|
169
|
+
super(message);
|
|
170
|
+
this.name = "CursorTimeoutError";
|
|
171
|
+
this.timeoutKind = timeoutKind;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
export function isCursorTimeoutError(error) {
|
|
175
|
+
return error instanceof CursorTimeoutError;
|
|
176
|
+
}
|
|
177
|
+
export async function withCursorTimeout(promise, timeoutMs, timeoutMessage, timeoutKind) {
|
|
178
|
+
let timer;
|
|
179
|
+
try {
|
|
180
|
+
return await Promise.race([
|
|
181
|
+
promise,
|
|
182
|
+
new Promise((_, reject) => {
|
|
183
|
+
timer = setTimeout(() => reject(new CursorTimeoutError(timeoutMessage, timeoutKind)), timeoutMs);
|
|
184
|
+
}),
|
|
185
|
+
]);
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
if (timer)
|
|
189
|
+
clearTimeout(timer);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
export function buildStreamIdleTimeoutMessage(input) {
|
|
193
|
+
const effective = `${input.timeoutMs}ms`;
|
|
194
|
+
if (input.timeoutMs < input.streamIdleTimeoutMs) {
|
|
195
|
+
return `Cursor stream produced no events within ${effective} before the task deadline (configured stream idle timeout: ${input.streamIdleTimeoutMs}ms)`;
|
|
196
|
+
}
|
|
197
|
+
return `Cursor stream produced no events within ${effective}`;
|
|
198
|
+
}
|
|
199
|
+
export async function bestEffortCancelRun(run) {
|
|
200
|
+
if (!run)
|
|
201
|
+
return false;
|
|
202
|
+
const canCancel = typeof run.cancel === "function" &&
|
|
203
|
+
(run.supports?.("cancel") ?? true);
|
|
204
|
+
if (!canCancel)
|
|
205
|
+
return false;
|
|
206
|
+
try {
|
|
207
|
+
await run.cancel();
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
export function extractCursorTokenUsage(runResult) {
|
|
215
|
+
if (!runResult || typeof runResult !== "object")
|
|
216
|
+
return undefined;
|
|
217
|
+
const usage = runResult.usage;
|
|
218
|
+
if (!usage || typeof usage !== "object")
|
|
219
|
+
return undefined;
|
|
220
|
+
const raw = usage;
|
|
221
|
+
const inputTokens = typeof raw.inputTokens === "number" ? raw.inputTokens : undefined;
|
|
222
|
+
const outputTokens = typeof raw.outputTokens === "number" ? raw.outputTokens : undefined;
|
|
223
|
+
if (inputTokens === undefined && outputTokens === undefined) {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
const totalTokens = (inputTokens ?? 0) + (outputTokens ?? 0);
|
|
227
|
+
return {
|
|
228
|
+
inputTokens,
|
|
229
|
+
outputTokens,
|
|
230
|
+
totalTokens: totalTokens > 0 ? totalTokens : undefined,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function applyTokenUsageToResult(result, usage) {
|
|
234
|
+
if (!usage)
|
|
235
|
+
return;
|
|
236
|
+
result.details = { ...(result.details ?? {}), usage };
|
|
237
|
+
if (usage.totalTokens !== undefined) {
|
|
238
|
+
result.tokensUsed = usage.totalTokens;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
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
|
+
const model = options.model ?? DEFAULT_CURSOR_MODEL;
|
|
247
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_CURSOR_TIMEOUT_MS;
|
|
248
|
+
const apiKey = options.apiKey ?? process.env.CURSOR_API_KEY;
|
|
249
|
+
if (!apiKey?.trim() && !options.runHook) {
|
|
250
|
+
return {
|
|
251
|
+
ok: false,
|
|
252
|
+
status: "failed",
|
|
253
|
+
failureCategory: "missing-api-key",
|
|
254
|
+
model,
|
|
255
|
+
durationMs: 0,
|
|
256
|
+
stdout: "",
|
|
257
|
+
stderr: "CURSOR_API_KEY environment variable is not set.",
|
|
258
|
+
details: { startupError: true },
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const prompt = buildCursorPrompt(options.task, {
|
|
262
|
+
artifactDir: options.artifactDir,
|
|
263
|
+
runDir: options.runDir,
|
|
264
|
+
});
|
|
265
|
+
const startedAt = Date.now();
|
|
266
|
+
if (options.runHook) {
|
|
267
|
+
try {
|
|
268
|
+
const hookResult = await options.runHook({
|
|
269
|
+
apiKey: apiKey ?? "",
|
|
270
|
+
cwd: options.cwd,
|
|
271
|
+
model,
|
|
272
|
+
task: prompt,
|
|
273
|
+
timeoutMs,
|
|
274
|
+
});
|
|
275
|
+
const stdoutResult = truncateOutput(hookResult.stdout ?? "", MAX_CURSOR_STDOUT_BYTES);
|
|
276
|
+
const stderrResult = truncateOutput(hookResult.stderr ?? "", MAX_CURSOR_STDERR_BYTES);
|
|
277
|
+
const failureCategory = hookResult.ok
|
|
278
|
+
? "success"
|
|
279
|
+
: classifyCursorFailure({
|
|
280
|
+
runStatus: hookResult.status,
|
|
281
|
+
stderr: hookResult.stderr,
|
|
282
|
+
stdout: hookResult.stdout,
|
|
283
|
+
});
|
|
284
|
+
return {
|
|
285
|
+
ok: hookResult.ok,
|
|
286
|
+
status: hookResult.status ?? (hookResult.ok ? "completed" : "failed"),
|
|
287
|
+
failureCategory,
|
|
288
|
+
model,
|
|
289
|
+
durationMs: Date.now() - startedAt,
|
|
290
|
+
stdout: stdoutResult.text,
|
|
291
|
+
stderr: stderrResult.text,
|
|
292
|
+
details: {
|
|
293
|
+
...(hookResult.details ?? {}),
|
|
294
|
+
truncated: stdoutResult.truncated || stderrResult.truncated,
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
300
|
+
return {
|
|
301
|
+
ok: false,
|
|
302
|
+
status: "failed",
|
|
303
|
+
failureCategory: classifyCursorFailure({
|
|
304
|
+
startupError: message,
|
|
305
|
+
stderr: message,
|
|
306
|
+
}),
|
|
307
|
+
model,
|
|
308
|
+
durationMs: Date.now() - startedAt,
|
|
309
|
+
stdout: "",
|
|
310
|
+
stderr: message,
|
|
311
|
+
details: { startupError: true },
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
let agent;
|
|
316
|
+
let run;
|
|
317
|
+
let disposeError;
|
|
318
|
+
let result;
|
|
319
|
+
const streamIdleTimeoutMs = options.streamIdleTimeoutMs ?? DEFAULT_CURSOR_STREAM_IDLE_TIMEOUT_MS;
|
|
320
|
+
const disposeAgent = async () => {
|
|
321
|
+
if (!agent)
|
|
322
|
+
return;
|
|
323
|
+
const toDispose = agent;
|
|
324
|
+
agent = undefined;
|
|
325
|
+
try {
|
|
326
|
+
await toDispose[Symbol.asyncDispose]();
|
|
327
|
+
}
|
|
328
|
+
catch (disposeErr) {
|
|
329
|
+
disposeError =
|
|
330
|
+
disposeErr instanceof Error ? disposeErr.message : String(disposeErr);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
const attachDisposeError = (details = {}) => disposeError ? { ...details, disposeError } : details;
|
|
334
|
+
const buildTimeoutResult = (message, timeoutKind, outputText = "", cancelAttempted = false) => {
|
|
335
|
+
const stdoutResult = truncateOutput(outputText, MAX_CURSOR_STDOUT_BYTES);
|
|
336
|
+
return {
|
|
337
|
+
ok: false,
|
|
338
|
+
status: "timeout",
|
|
339
|
+
failureCategory: "timeout",
|
|
340
|
+
model,
|
|
341
|
+
durationMs: Date.now() - startedAt,
|
|
342
|
+
stdout: stdoutResult.text,
|
|
343
|
+
stderr: message,
|
|
344
|
+
details: {
|
|
345
|
+
timedOut: true,
|
|
346
|
+
timeoutKind,
|
|
347
|
+
cancelAttempted,
|
|
348
|
+
truncated: stdoutResult.truncated,
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
};
|
|
352
|
+
let outputText = "";
|
|
353
|
+
try {
|
|
354
|
+
const { Agent } = await import("@cursor/sdk");
|
|
355
|
+
agent = (await Agent.create({
|
|
356
|
+
apiKey: apiKey ?? "",
|
|
357
|
+
name: "pi-harness-cursor",
|
|
358
|
+
model: { id: model },
|
|
359
|
+
local: { cwd: options.cwd },
|
|
360
|
+
}));
|
|
361
|
+
run = await agent.send(prompt);
|
|
362
|
+
const iterator = run.stream()[Symbol.asyncIterator]();
|
|
363
|
+
const deadline = startedAt + timeoutMs;
|
|
364
|
+
while (true) {
|
|
365
|
+
const remaining = deadline - Date.now();
|
|
366
|
+
if (remaining <= 0) {
|
|
367
|
+
throw new CursorTimeoutError(`Cursor execution timed out after ${timeoutMs}ms`, "total");
|
|
368
|
+
}
|
|
369
|
+
const timeoutForNext = Math.min(remaining, streamIdleTimeoutMs);
|
|
370
|
+
const timeoutKind = timeoutForNext < streamIdleTimeoutMs ? "total" : "stream-idle";
|
|
371
|
+
const next = await withCursorTimeout(iterator.next(), timeoutForNext, buildStreamIdleTimeoutMessage({
|
|
372
|
+
timeoutMs: timeoutForNext,
|
|
373
|
+
streamIdleTimeoutMs,
|
|
374
|
+
}), timeoutKind);
|
|
375
|
+
if (next.done)
|
|
376
|
+
break;
|
|
377
|
+
const event = next.value;
|
|
378
|
+
if (event.type === "assistant") {
|
|
379
|
+
for (const block of event.message?.content ?? []) {
|
|
380
|
+
if (block.type === "text") {
|
|
381
|
+
outputText += block.text ?? "";
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const waitGraceMs = Math.min(deadline - Date.now(), WAIT_AFTER_STREAM_GRACE_MS);
|
|
387
|
+
if (waitGraceMs <= 0) {
|
|
388
|
+
throw new CursorTimeoutError(`Cursor execution timed out after ${timeoutMs}ms`, "total");
|
|
389
|
+
}
|
|
390
|
+
let runResult;
|
|
391
|
+
try {
|
|
392
|
+
runResult = await withCursorTimeout(run.wait(), waitGraceMs, `Cursor run did not finalize within ${waitGraceMs}ms after stream completion`, "wait-grace");
|
|
393
|
+
}
|
|
394
|
+
catch (waitErr) {
|
|
395
|
+
if (isCursorTimeoutError(waitErr) &&
|
|
396
|
+
run.status !== "running" &&
|
|
397
|
+
run.status !== undefined) {
|
|
398
|
+
runResult = {
|
|
399
|
+
status: run.status,
|
|
400
|
+
durationMs: run.durationMs,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
throw waitErr;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const stdoutResult = truncateOutput(outputText, MAX_CURSOR_STDOUT_BYTES);
|
|
408
|
+
const runStatus = String(runResult?.status ?? "");
|
|
409
|
+
const status = mapRunStatusToExecutionStatus(runStatus, false);
|
|
410
|
+
const ok = status === "completed" && outputText.trim().length > 0;
|
|
411
|
+
const failureCategory = ok
|
|
412
|
+
? "success"
|
|
413
|
+
: classifyCursorFailure({
|
|
414
|
+
runStatus,
|
|
415
|
+
stderr: "",
|
|
416
|
+
stdout: outputText,
|
|
417
|
+
});
|
|
418
|
+
const usage = extractCursorTokenUsage(runResult);
|
|
419
|
+
result = {
|
|
420
|
+
ok,
|
|
421
|
+
status,
|
|
422
|
+
failureCategory,
|
|
423
|
+
model,
|
|
424
|
+
durationMs: runResult?.durationMs ??
|
|
425
|
+
Date.now() - startedAt,
|
|
426
|
+
stdout: stdoutResult.text,
|
|
427
|
+
stderr: "",
|
|
428
|
+
details: {
|
|
429
|
+
truncated: stdoutResult.truncated,
|
|
430
|
+
runStatus: runResult?.status,
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
applyTokenUsageToResult(result, usage);
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
if (isCursorTimeoutError(error)) {
|
|
437
|
+
const cancelAttempted = await bestEffortCancelRun(run);
|
|
438
|
+
result = buildTimeoutResult(error.message, error.timeoutKind, outputText, cancelAttempted);
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
442
|
+
const stderrResult = truncateOutput(redactApiKey(message, apiKey), MAX_CURSOR_STDERR_BYTES);
|
|
443
|
+
result = {
|
|
444
|
+
ok: false,
|
|
445
|
+
status: "failed",
|
|
446
|
+
failureCategory: classifyCursorFailure({
|
|
447
|
+
startupError: message,
|
|
448
|
+
stderr: message,
|
|
449
|
+
}),
|
|
450
|
+
model,
|
|
451
|
+
durationMs: Date.now() - startedAt,
|
|
452
|
+
stdout: "",
|
|
453
|
+
stderr: stderrResult.text,
|
|
454
|
+
details: { startupError: true, truncated: stderrResult.truncated },
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
finally {
|
|
459
|
+
if (run?.status === "running") {
|
|
460
|
+
await bestEffortCancelRun(run);
|
|
461
|
+
}
|
|
462
|
+
await disposeAgent();
|
|
463
|
+
if (result) {
|
|
464
|
+
result.details = attachDisposeError(result.details ?? {});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return result;
|
|
468
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { validateCursorArtifacts } from './cursor-artifacts.js';
|
|
4
|
+
import { buildCursorPrompt, executeCursorTask, } from './cursor-executor.js';
|
|
5
|
+
import { writeCursorExecutorArtifacts } from './cursor-executor-artifacts.js';
|
|
6
|
+
import { appendCursorExecutionLog, buildCursorExecutionLogRecord, } from './cursor-execution-log.js';
|
|
7
|
+
import { assertCursorExecutorAvailable, resolveCursorExecutorConfig, resolveCursorModelForTaskConfig, } from './config-core.js';
|
|
8
|
+
import { appendWorkflowLog } from '../shared/logger.js';
|
|
9
|
+
import { getTaskDir, getTaskPaths, loadTaskConfig } from '../task/runtime.js';
|
|
10
|
+
export async function runCursorDelegatedTask(repoRoot, taskId, manifest, options = {}) {
|
|
11
|
+
const taskConfig = await loadTaskConfig(repoRoot, taskId);
|
|
12
|
+
if (taskConfig.executor !== 'cursor') {
|
|
13
|
+
throw new Error(`runCursorDelegatedTask requires executor=cursor, got "${taskConfig.executor ?? 'pi'}"`);
|
|
14
|
+
}
|
|
15
|
+
assertCursorExecutorAvailable(manifest);
|
|
16
|
+
const cursorConfig = resolveCursorExecutorConfig(manifest);
|
|
17
|
+
const taskDir = getTaskDir(repoRoot, taskId);
|
|
18
|
+
const userTask = await buildCursorTaskDescription(repoRoot, taskId);
|
|
19
|
+
const model = resolveCursorModelForTaskConfig(taskConfig, cursorConfig);
|
|
20
|
+
await appendWorkflowLog(taskDir, `cursor executor start model=${model}`);
|
|
21
|
+
const result = await executeCursorTask({
|
|
22
|
+
task: userTask,
|
|
23
|
+
cwd: repoRoot,
|
|
24
|
+
model,
|
|
25
|
+
timeoutMs: taskConfig.timeoutMs,
|
|
26
|
+
runHook: options.runHook,
|
|
27
|
+
});
|
|
28
|
+
const prompt = buildCursorPrompt(userTask);
|
|
29
|
+
const executionContext = {
|
|
30
|
+
channel: 'delegate',
|
|
31
|
+
cwd: repoRoot,
|
|
32
|
+
executionMode: options.executionMode ?? 'direct',
|
|
33
|
+
taskId,
|
|
34
|
+
};
|
|
35
|
+
await appendCursorExecutionLog(taskDir, buildCursorExecutionLogRecord({
|
|
36
|
+
context: executionContext,
|
|
37
|
+
model,
|
|
38
|
+
prompt,
|
|
39
|
+
result,
|
|
40
|
+
}));
|
|
41
|
+
await writeCursorExecutorArtifacts(path.join(taskDir, 'logs'), {
|
|
42
|
+
prompt,
|
|
43
|
+
model,
|
|
44
|
+
context: executionContext,
|
|
45
|
+
result,
|
|
46
|
+
});
|
|
47
|
+
const artifactIssues = await validateCursorTaskArtifacts(repoRoot, taskId);
|
|
48
|
+
if (!result.ok) {
|
|
49
|
+
await appendWorkflowLog(taskDir, `cursor executor failed category=${result.failureCategory}`);
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
if (artifactIssues.length > 0) {
|
|
53
|
+
await appendWorkflowLog(taskDir, `cursor artifact validation failed: ${artifactIssues.join('; ')}`);
|
|
54
|
+
return {
|
|
55
|
+
...result,
|
|
56
|
+
ok: false,
|
|
57
|
+
status: 'failed',
|
|
58
|
+
failureCategory: 'invalid-output',
|
|
59
|
+
stderr: artifactIssues.join('\n'),
|
|
60
|
+
details: {
|
|
61
|
+
...(result.details ?? {}),
|
|
62
|
+
artifactIssues,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const postRun = options.postRun ?? 'none';
|
|
67
|
+
if (postRun === 'verify') {
|
|
68
|
+
const message = 'Cursor direct run no longer runs legacy sequential verify; use DAG verification, promote-run, or closeout.';
|
|
69
|
+
await appendWorkflowLog(taskDir, `cursor post-run verify rejected: ${message}`);
|
|
70
|
+
return {
|
|
71
|
+
...result,
|
|
72
|
+
ok: false,
|
|
73
|
+
status: 'failed',
|
|
74
|
+
failureCategory: 'nonzero-exit',
|
|
75
|
+
stderr: message,
|
|
76
|
+
details: {
|
|
77
|
+
...(result.details ?? {}),
|
|
78
|
+
postRunFailed: true,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
await appendWorkflowLog(taskDir, 'cursor executor completed; one-shot evidence only');
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
async function buildCursorTaskDescription(repoRoot, taskId) {
|
|
86
|
+
const paths = getTaskPaths(repoRoot, taskId);
|
|
87
|
+
const taskConfig = await loadTaskConfig(repoRoot, taskId);
|
|
88
|
+
const requirement = await readFile(path.join(paths.sourceDir, '需求.md'), 'utf-8');
|
|
89
|
+
const constraints = await readFile(path.join(paths.sourceDir, '执行约束.md'), 'utf-8');
|
|
90
|
+
return [
|
|
91
|
+
`# Task ${taskId}`,
|
|
92
|
+
'',
|
|
93
|
+
'## 需求.md',
|
|
94
|
+
requirement.trim(),
|
|
95
|
+
'',
|
|
96
|
+
'## 执行约束.md',
|
|
97
|
+
constraints.trim(),
|
|
98
|
+
'',
|
|
99
|
+
'## Path boundaries',
|
|
100
|
+
`- allowedPaths: ${(taskConfig.allowedPaths ?? []).join(', ') || '(read-only; no code writes outside .harness/tasks/)'}`,
|
|
101
|
+
`- forbiddenPaths: ${(taskConfig.forbiddenPaths ?? []).join(', ') || '(none)'}`,
|
|
102
|
+
'',
|
|
103
|
+
'## Required artifacts',
|
|
104
|
+
'- artifacts/修改记录.md must include 修改摘要 / 修改文件 / 验证 / 风险',
|
|
105
|
+
'- artifacts/验证结果.md must include commands, exit code, pass/fail',
|
|
106
|
+
].join('\n');
|
|
107
|
+
}
|
|
108
|
+
async function validateCursorTaskArtifacts(repoRoot, taskId) {
|
|
109
|
+
const paths = getTaskPaths(repoRoot, taskId);
|
|
110
|
+
const modifyLogPath = path.join(paths.taskDir, 'artifacts', '修改记录.md');
|
|
111
|
+
const verifyResultPath = path.join(paths.taskDir, 'artifacts', '验证结果.md');
|
|
112
|
+
const modifyLog = await readFile(modifyLogPath, 'utf-8');
|
|
113
|
+
const verifyResult = await readFile(verifyResultPath, 'utf-8');
|
|
114
|
+
return validateCursorArtifacts(modifyLog, verifyResult);
|
|
115
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { access } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { executeCursorTask, truncateOutput, DEFAULT_CURSOR_MODEL, } from './cursor-executor.js';
|
|
4
|
+
import { formatCursorFailureMessage, persistCursorToolExecution, } from './cursor-execution-log.js';
|
|
5
|
+
import { persistCursorRunLog, computeRunDir } from '../records/one-shot-runs.js';
|
|
6
|
+
export const DEFAULT_CURSOR_TOOL_MODEL = DEFAULT_CURSOR_MODEL;
|
|
7
|
+
export const MAX_CURSOR_TOOL_OUTPUT_CHARS = 4_000;
|
|
8
|
+
export async function validateCursorToolCwd(cwd) {
|
|
9
|
+
const resolvedCwd = resolve(cwd);
|
|
10
|
+
try {
|
|
11
|
+
await access(resolvedCwd);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return { ok: false, error: `cwd does not exist: ${resolvedCwd}` };
|
|
15
|
+
}
|
|
16
|
+
return { ok: true, resolvedCwd };
|
|
17
|
+
}
|
|
18
|
+
export async function runCursorTool(params, options = {}) {
|
|
19
|
+
const apiKey = options.apiKey ?? process.env.CURSOR_API_KEY;
|
|
20
|
+
if (!apiKey?.trim()) {
|
|
21
|
+
return {
|
|
22
|
+
content: [{ type: 'text', text: `Error: ${formatCursorFailureMessage('missing-api-key')}` }],
|
|
23
|
+
details: { error: 'missing-api-key', failureCategory: 'missing-api-key' },
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const cwdCheck = await validateCursorToolCwd(params.cwd ?? process.cwd());
|
|
27
|
+
if (!cwdCheck.ok) {
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: 'text', text: `Error: ${cwdCheck.error}` }],
|
|
30
|
+
details: { error: 'invalid_cwd' },
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const model = params.model ?? DEFAULT_CURSOR_TOOL_MODEL;
|
|
34
|
+
const runDir = options.skipRunLog ? undefined : (await computeRunDir(cwdCheck.resolvedCwd, params.task) ?? undefined);
|
|
35
|
+
const execution = await executeCursorTask({
|
|
36
|
+
task: params.task,
|
|
37
|
+
cwd: cwdCheck.resolvedCwd,
|
|
38
|
+
model,
|
|
39
|
+
apiKey,
|
|
40
|
+
runDir: runDir ?? undefined,
|
|
41
|
+
runHook: options.runHook,
|
|
42
|
+
});
|
|
43
|
+
await persistCursorToolExecution(cwdCheck.resolvedCwd, {
|
|
44
|
+
task: params.task,
|
|
45
|
+
model,
|
|
46
|
+
result: execution,
|
|
47
|
+
apiKey,
|
|
48
|
+
});
|
|
49
|
+
// Persist human-readable run log under .harness/runs/
|
|
50
|
+
if (!options.skipRunLog) {
|
|
51
|
+
await persistCursorRunLog(cwdCheck.resolvedCwd, {
|
|
52
|
+
task: params.task,
|
|
53
|
+
model,
|
|
54
|
+
channel: 'tool',
|
|
55
|
+
cwd: cwdCheck.resolvedCwd,
|
|
56
|
+
result: execution,
|
|
57
|
+
runDir,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
const truncated = truncateOutput(execution.stdout || execution.stderr, MAX_CURSOR_TOOL_OUTPUT_CHARS);
|
|
61
|
+
const summary = truncated.text || `(No text output. Status: ${execution.status})`;
|
|
62
|
+
if (!execution.ok) {
|
|
63
|
+
return {
|
|
64
|
+
content: [{
|
|
65
|
+
type: 'text',
|
|
66
|
+
text: summary || `Error: ${formatCursorFailureMessage(execution.failureCategory)}`,
|
|
67
|
+
}],
|
|
68
|
+
details: {
|
|
69
|
+
status: execution.status,
|
|
70
|
+
durationMs: execution.durationMs,
|
|
71
|
+
model,
|
|
72
|
+
truncated: truncated.truncated,
|
|
73
|
+
error: execution.failureCategory,
|
|
74
|
+
failureCategory: execution.failureCategory,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
content: [{ type: 'text', text: summary }],
|
|
80
|
+
details: {
|
|
81
|
+
status: execution.status,
|
|
82
|
+
durationMs: execution.durationMs,
|
|
83
|
+
model,
|
|
84
|
+
truncated: truncated.truncated,
|
|
85
|
+
failureCategory: execution.failureCategory,
|
|
86
|
+
inputTokens: readTokenField(execution.details, 'inputTokens'),
|
|
87
|
+
outputTokens: readTokenField(execution.details, 'outputTokens'),
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function readTokenField(details, key) {
|
|
92
|
+
const value = details?.[key];
|
|
93
|
+
return typeof value === 'number' ? value : undefined;
|
|
94
|
+
}
|