@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
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { writeTextArtifactFile } from "../infrastructure/harness/artifact-store.js";
|
|
3
|
-
import { redactPromptForLog, truncateOutput, } from "./cursor-executor.js";
|
|
4
|
-
import { formatCursorFailureMessage, } from "./cursor-execution-log.js";
|
|
5
|
-
const SUMMARY_STDOUT_MAX = 4_000;
|
|
6
|
-
const SUMMARY_STDERR_MAX = 2_000;
|
|
7
|
-
export function buildPromptRedactedMarkdown(prompt) {
|
|
8
|
-
const redacted = redactPromptForLog(prompt);
|
|
9
|
-
return [
|
|
10
|
-
"# Cursor prompt (redacted)",
|
|
11
|
-
"",
|
|
12
|
-
`- ${redacted}`,
|
|
13
|
-
"",
|
|
14
|
-
"Full prompt text is not stored. See `logs/executor.jsonl` for the same hash reference.",
|
|
15
|
-
"",
|
|
16
|
-
].join("\n");
|
|
17
|
-
}
|
|
18
|
-
export function buildCursorRecoverySteps(input) {
|
|
19
|
-
const taskId = input.taskId ?? "<task-id>";
|
|
20
|
-
const session = input.sessionName ?? `task-${taskId}`;
|
|
21
|
-
const lines = ["## Recovery steps", ""];
|
|
22
|
-
if (input.failureCategory) {
|
|
23
|
-
lines.push(`- **Failure**: ${formatCursorFailureMessage(input.failureCategory)} (\`${input.failureCategory}\`)`);
|
|
24
|
-
lines.push("");
|
|
25
|
-
}
|
|
26
|
-
if (input.channel === "delegate") {
|
|
27
|
-
lines.push("### Retry in-process (direct SDK)");
|
|
28
|
-
lines.push("```bash");
|
|
29
|
-
lines.push("cd .");
|
|
30
|
-
lines.push(`npm run dev -- delegate ${taskId} --executor cursor --auto-run`);
|
|
31
|
-
lines.push("```");
|
|
32
|
-
if (input.worktreePath) {
|
|
33
|
-
lines.push("");
|
|
34
|
-
lines.push(`- Worktree: \`${input.worktreePath}\``);
|
|
35
|
-
}
|
|
36
|
-
lines.push("");
|
|
37
|
-
lines.push("### Promote evidence");
|
|
38
|
-
lines.push("```bash");
|
|
39
|
-
lines.push(`npm run dev -- promote-run ${taskId} --run-id <run-id>`);
|
|
40
|
-
lines.push(`npm run dev -- closeout task ${taskId}`);
|
|
41
|
-
lines.push("```");
|
|
42
|
-
lines.push("");
|
|
43
|
-
lines.push("### After closeout");
|
|
44
|
-
lines.push("```bash");
|
|
45
|
-
lines.push(`npm run dev -- harvest ${taskId}`);
|
|
46
|
-
lines.push("```");
|
|
47
|
-
}
|
|
48
|
-
else if (input.channel === "dag") {
|
|
49
|
-
lines.push("### Re-run DAG node");
|
|
50
|
-
lines.push("- Fix upstream outputs or prompts in the DAG JSON, then re-run `run-dag` with a new `--run-id`.");
|
|
51
|
-
lines.push("- Inspect `executor.jsonl` and per-node `result.summary.md` under `.harness/dag-runs/completed/<run-id>/` (or `active/<run-id>/` while the run is still in progress).");
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
lines.push("### Pi cursor tool");
|
|
55
|
-
lines.push("- Retry with a clearer `task` and correct `cwd`.");
|
|
56
|
-
lines.push("- Check `.harness/cache/cursor-executions.jsonl` when cwd contains `.harness`.");
|
|
57
|
-
}
|
|
58
|
-
if (input.failureCategory === "missing-api-key") {
|
|
59
|
-
lines.push("");
|
|
60
|
-
lines.push("- Export `CURSOR_API_KEY` before retrying.");
|
|
61
|
-
}
|
|
62
|
-
return lines;
|
|
63
|
-
}
|
|
64
|
-
function formatTokenUsage(result) {
|
|
65
|
-
const rows = [];
|
|
66
|
-
if (result.tokensUsed !== undefined) {
|
|
67
|
-
rows.push(`| tokensUsed | ${result.tokensUsed} |`);
|
|
68
|
-
}
|
|
69
|
-
const usage = result.details?.usage;
|
|
70
|
-
if (typeof usage?.inputTokens === "number") {
|
|
71
|
-
rows.push(`| inputTokens | ${usage.inputTokens} |`);
|
|
72
|
-
}
|
|
73
|
-
if (typeof usage?.outputTokens === "number") {
|
|
74
|
-
rows.push(`| outputTokens | ${usage.outputTokens} |`);
|
|
75
|
-
}
|
|
76
|
-
if (typeof usage?.totalTokens === "number" &&
|
|
77
|
-
result.tokensUsed === undefined) {
|
|
78
|
-
rows.push(`| totalTokens | ${usage.totalTokens} |`);
|
|
79
|
-
}
|
|
80
|
-
return rows;
|
|
81
|
-
}
|
|
82
|
-
export function buildResultSummaryMarkdown(input) {
|
|
83
|
-
const stdout = truncateOutput(input.result.stdout, SUMMARY_STDOUT_MAX);
|
|
84
|
-
const stderr = truncateOutput(input.result.stderr || "", SUMMARY_STDERR_MAX);
|
|
85
|
-
const recovery = input.recoveryLines ??
|
|
86
|
-
buildCursorRecoverySteps({
|
|
87
|
-
channel: input.context.channel,
|
|
88
|
-
taskId: input.context.taskId,
|
|
89
|
-
sessionName: input.context.taskId
|
|
90
|
-
? `task-${input.context.taskId}`
|
|
91
|
-
: undefined,
|
|
92
|
-
worktreePath: input.context.cwd,
|
|
93
|
-
failureCategory: input.result.ok
|
|
94
|
-
? undefined
|
|
95
|
-
: input.result.failureCategory,
|
|
96
|
-
});
|
|
97
|
-
const meta = [
|
|
98
|
-
"# Cursor execution summary",
|
|
99
|
-
"",
|
|
100
|
-
"## Run metadata",
|
|
101
|
-
"",
|
|
102
|
-
`| Field | Value |`,
|
|
103
|
-
`|-------|-------|`,
|
|
104
|
-
`| channel | ${input.context.channel} |`,
|
|
105
|
-
];
|
|
106
|
-
if (input.context.executionMode)
|
|
107
|
-
meta.push(`| executionMode | ${input.context.executionMode} |`);
|
|
108
|
-
if (input.context.taskId)
|
|
109
|
-
meta.push(`| taskId | ${input.context.taskId} |`);
|
|
110
|
-
if (input.context.dagRunId)
|
|
111
|
-
meta.push(`| dagRunId | ${input.context.dagRunId} |`);
|
|
112
|
-
if (input.context.dagNodeId)
|
|
113
|
-
meta.push(`| dagNodeId | ${input.context.dagNodeId} |`);
|
|
114
|
-
meta.push(`| cwd | ${input.context.cwd} |`, `| model | ${input.model} |`, `| ok | ${input.result.ok} |`, `| status | ${input.result.status} |`, `| failureCategory | ${input.result.failureCategory} |`, `| durationMs | ${input.result.durationMs} |`, ...formatTokenUsage(input.result), "", "## Prompt reference", "", `- ${redactPromptForLog(input.prompt)}`, "", "## Output", "");
|
|
115
|
-
if (stdout.text.trim()) {
|
|
116
|
-
meta.push("### stdout", "", "```text", stdout.text, "```", "");
|
|
117
|
-
if (stdout.truncated)
|
|
118
|
-
meta.push("_(stdout truncated)_", "");
|
|
119
|
-
}
|
|
120
|
-
if (stderr.text.trim()) {
|
|
121
|
-
meta.push("### stderr", "", "```text", stderr.text, "```", "");
|
|
122
|
-
if (stderr.truncated)
|
|
123
|
-
meta.push("_(stderr truncated)_", "");
|
|
124
|
-
}
|
|
125
|
-
meta.push(...recovery, "");
|
|
126
|
-
return meta.join("\n");
|
|
127
|
-
}
|
|
128
|
-
export async function writeCursorExecutorArtifacts(artifactsDir, input) {
|
|
129
|
-
const promptPath = path.join(artifactsDir, "prompt.redacted.md");
|
|
130
|
-
const summaryPath = path.join(artifactsDir, "result.summary.md");
|
|
131
|
-
await writeTextArtifactFile(promptPath, buildPromptRedactedMarkdown(input.prompt));
|
|
132
|
-
await writeTextArtifactFile(summaryPath, buildResultSummaryMarkdown(input));
|
|
133
|
-
return { promptPath, summaryPath };
|
|
134
|
-
}
|
|
@@ -1,115 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { createInterface } from 'node:readline';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { DEFAULT_CURSOR_TIMEOUT_MS, } from './cursor-executor.js';
|
|
6
|
-
const WORKER_MODULE_PATH = fileURLToPath(import.meta.url);
|
|
7
|
-
const WORKER_MODULE_DIR = path.dirname(WORKER_MODULE_PATH);
|
|
8
|
-
const WORKER_ENTRY = resolveCursorWorkerEntryPath(WORKER_MODULE_PATH);
|
|
9
|
-
let workerProcess = null;
|
|
10
|
-
let workerBoot = null;
|
|
11
|
-
const pending = new Map();
|
|
12
|
-
let nextId = 1;
|
|
13
|
-
const DEFAULT_RPC_RESPONSE_GRACE_MS = 5_000;
|
|
14
|
-
const CURSOR_WORKER_STOP_SIGNAL = 'SIGTERM';
|
|
15
|
-
function getRpcResponseGraceMs() {
|
|
16
|
-
const raw = process.env.CURSOR_WORKER_RPC_GRACE_MS?.trim();
|
|
17
|
-
if (raw) {
|
|
18
|
-
const parsed = Number(raw);
|
|
19
|
-
if (Number.isFinite(parsed) && parsed >= 0)
|
|
20
|
-
return parsed;
|
|
21
|
-
}
|
|
22
|
-
return DEFAULT_RPC_RESPONSE_GRACE_MS;
|
|
23
|
-
}
|
|
24
|
-
export function isCursorWorkerEnabled() {
|
|
25
|
-
const raw = process.env.CURSOR_USE_WORKER?.trim().toLowerCase();
|
|
26
|
-
return raw === '1' || raw === 'true' || raw === 'yes';
|
|
27
|
-
}
|
|
28
|
-
/** True when this process should accept worker RPC (child), not delegate to a worker. */
|
|
29
|
-
export function isCursorWorkerChild() {
|
|
30
|
-
return process.env.CURSOR_WORKER_CHILD === '1';
|
|
31
|
-
}
|
|
32
|
-
export function shouldUseCursorWorker(options) {
|
|
33
|
-
if (options?.runHook)
|
|
34
|
-
return false;
|
|
35
|
-
if (isCursorWorkerChild())
|
|
36
|
-
return false;
|
|
37
|
-
if (options?.useWorker === true)
|
|
38
|
-
return true;
|
|
39
|
-
return isCursorWorkerEnabled();
|
|
40
|
-
}
|
|
41
|
-
export function resolveCursorWorkerEntryPath(modulePath) {
|
|
42
|
-
const extension = path.extname(modulePath) === '.ts' ? '.ts' : '.js';
|
|
43
|
-
return path.join(path.dirname(modulePath), `../cursor-worker-entry${extension}`);
|
|
44
|
-
}
|
|
45
|
-
export function buildCursorWorkerScriptArgs(entryPath) {
|
|
46
|
-
return path.extname(entryPath) === '.ts'
|
|
47
|
-
? ['--import', 'tsx/esm', entryPath]
|
|
48
|
-
: [entryPath];
|
|
49
|
-
}
|
|
50
|
-
function workerScriptArgs() {
|
|
51
|
-
return buildCursorWorkerScriptArgs(WORKER_ENTRY);
|
|
52
|
-
}
|
|
53
|
-
export function getCursorWorkerEntryPath() {
|
|
54
|
-
return WORKER_ENTRY;
|
|
55
|
-
}
|
|
56
|
-
function dispatchResponse(line) {
|
|
57
|
-
let parsed;
|
|
58
|
-
try {
|
|
59
|
-
parsed = JSON.parse(line);
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
const waiter = pending.get(parsed.id);
|
|
65
|
-
if (!waiter)
|
|
66
|
-
return;
|
|
67
|
-
pending.delete(parsed.id);
|
|
68
|
-
waiter.resolve(parsed);
|
|
69
|
-
}
|
|
70
|
-
async function ensureWorkerProcess() {
|
|
71
|
-
if (workerProcess && !workerProcess.killed && workerProcess.exitCode === null) {
|
|
72
|
-
return workerProcess;
|
|
73
|
-
}
|
|
74
|
-
if (!workerBoot) {
|
|
75
|
-
workerBoot = new Promise((resolve, reject) => {
|
|
76
|
-
const child = spawn(process.execPath, workerScriptArgs(), {
|
|
77
|
-
cwd: process.cwd(),
|
|
78
|
-
env: { ...process.env, CURSOR_WORKER_CHILD: '1' },
|
|
79
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
80
|
-
});
|
|
81
|
-
workerProcess = child;
|
|
82
|
-
const rl = createInterface({ input: child.stdout });
|
|
83
|
-
rl.on('line', dispatchResponse);
|
|
84
|
-
child.stderr?.on('data', (chunk) => {
|
|
85
|
-
const text = chunk.toString('utf-8').trim();
|
|
86
|
-
if (text)
|
|
87
|
-
process.stderr.write(`[cursor-worker] ${text}\n`);
|
|
88
|
-
});
|
|
89
|
-
child.on('error', (err) => {
|
|
90
|
-
workerBoot = null;
|
|
91
|
-
reject(err);
|
|
92
|
-
});
|
|
93
|
-
child.on('exit', (code) => {
|
|
94
|
-
if (workerProcess !== child)
|
|
95
|
-
return;
|
|
96
|
-
workerProcess = null;
|
|
97
|
-
workerBoot = null;
|
|
98
|
-
for (const [, waiter] of pending) {
|
|
99
|
-
waiter.reject(new Error(`cursor worker exited with code ${code ?? 'unknown'}`));
|
|
100
|
-
}
|
|
101
|
-
pending.clear();
|
|
102
|
-
});
|
|
103
|
-
// ping to confirm ready
|
|
104
|
-
const pingId = '__boot__';
|
|
105
|
-
const bootWaiter = new Promise((res, rej) => {
|
|
106
|
-
const timeout = setTimeout(() => rej(new Error('cursor worker boot timeout')), 30_000);
|
|
107
|
-
pending.set(pingId, {
|
|
108
|
-
resolve: (response) => {
|
|
109
|
-
clearTimeout(timeout);
|
|
110
|
-
if (!response.ok) {
|
|
111
|
-
rej(new Error('cursor worker ping failed'));
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
res();
|
|
115
|
-
},
|
|
116
|
-
reject: (err) => {
|
|
117
|
-
clearTimeout(timeout);
|
|
118
|
-
rej(err);
|
|
119
|
-
},
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
child.stdin.write(`${JSON.stringify({ id: pingId, type: 'ping' })}\n`);
|
|
123
|
-
bootWaiter.then(() => resolve()).catch(reject);
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
await workerBoot;
|
|
127
|
-
if (!workerProcess)
|
|
128
|
-
throw new Error('cursor worker failed to start');
|
|
129
|
-
return workerProcess;
|
|
130
|
-
}
|
|
131
|
-
export async function stopCursorWorker(signal = CURSOR_WORKER_STOP_SIGNAL) {
|
|
132
|
-
const pid = workerProcess?.pid;
|
|
133
|
-
const hadLiveWorker = Boolean(workerProcess && workerProcess.exitCode === null && !workerProcess.killed);
|
|
134
|
-
if (hadLiveWorker && workerProcess) {
|
|
135
|
-
try {
|
|
136
|
-
workerProcess.kill(signal);
|
|
137
|
-
}
|
|
138
|
-
catch {
|
|
139
|
-
/* ignore ESRCH / race with natural exit */
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
workerProcess = null;
|
|
143
|
-
workerBoot = null;
|
|
144
|
-
for (const [, waiter] of pending) {
|
|
145
|
-
waiter.reject(new Error('cursor worker stopped'));
|
|
146
|
-
}
|
|
147
|
-
pending.clear();
|
|
148
|
-
return {
|
|
149
|
-
workerPid: pid,
|
|
150
|
-
workerTerminationAttempted: hadLiveWorker,
|
|
151
|
-
workerStopSignal: hadLiveWorker ? signal : undefined,
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
/** Test-only: reset module singletons between cases. */
|
|
155
|
-
export async function resetCursorWorkerStateForTests() {
|
|
156
|
-
await stopCursorWorker();
|
|
157
|
-
nextId = 1;
|
|
158
|
-
}
|
|
159
|
-
export function isCursorWorkerRunning() {
|
|
160
|
-
return Boolean(workerProcess && workerProcess.exitCode === null && !workerProcess.killed);
|
|
161
|
-
}
|
|
162
|
-
export async function executeCursorTaskViaWorker(options) {
|
|
163
|
-
const child = await ensureWorkerProcess();
|
|
164
|
-
const id = `req-${nextId++}`;
|
|
165
|
-
const request = {
|
|
166
|
-
id,
|
|
167
|
-
type: 'execute',
|
|
168
|
-
task: options.task,
|
|
169
|
-
cwd: options.cwd,
|
|
170
|
-
model: options.model,
|
|
171
|
-
timeoutMs: options.timeoutMs,
|
|
172
|
-
runDir: options.runDir,
|
|
173
|
-
artifactDir: options.artifactDir,
|
|
174
|
-
};
|
|
175
|
-
const taskTimeoutMs = options.timeoutMs ?? DEFAULT_CURSOR_TIMEOUT_MS;
|
|
176
|
-
const rpcGraceMs = getRpcResponseGraceMs();
|
|
177
|
-
const responsePromise = new Promise((resolve, reject) => {
|
|
178
|
-
const timer = setTimeout(() => {
|
|
179
|
-
pending.delete(id);
|
|
180
|
-
void stopCursorWorker().then((stopResult) => {
|
|
181
|
-
resolve({
|
|
182
|
-
id,
|
|
183
|
-
ok: true,
|
|
184
|
-
result: {
|
|
185
|
-
ok: false,
|
|
186
|
-
status: 'timeout',
|
|
187
|
-
failureCategory: 'timeout',
|
|
188
|
-
model: options.model ?? 'default',
|
|
189
|
-
durationMs: taskTimeoutMs,
|
|
190
|
-
stdout: '',
|
|
191
|
-
stderr: `cursor worker request timeout after ${taskTimeoutMs}ms`,
|
|
192
|
-
details: {
|
|
193
|
-
timedOut: true,
|
|
194
|
-
timeoutKind: 'rpc',
|
|
195
|
-
cancelDeferred: true,
|
|
196
|
-
cancelAttempted: false,
|
|
197
|
-
workerPid: stopResult.workerPid,
|
|
198
|
-
workerTerminationAttempted: stopResult.workerTerminationAttempted,
|
|
199
|
-
workerStopSignal: stopResult.workerStopSignal,
|
|
200
|
-
workerRestartDeferred: true,
|
|
201
|
-
},
|
|
202
|
-
},
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
}, taskTimeoutMs + rpcGraceMs);
|
|
206
|
-
pending.set(id, {
|
|
207
|
-
resolve: (value) => {
|
|
208
|
-
clearTimeout(timer);
|
|
209
|
-
resolve(value);
|
|
210
|
-
},
|
|
211
|
-
reject: (err) => {
|
|
212
|
-
clearTimeout(timer);
|
|
213
|
-
reject(err);
|
|
214
|
-
},
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
child.stdin.write(`${JSON.stringify(request)}\n`);
|
|
218
|
-
const response = await responsePromise;
|
|
219
|
-
if (!response.ok) {
|
|
220
|
-
throw new Error(response.error);
|
|
221
|
-
}
|
|
222
|
-
return response.result;
|
|
223
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export function parseWorkerRequestLine(line) {
|
|
2
|
-
const parsed = JSON.parse(line);
|
|
3
|
-
if (!parsed || typeof parsed !== 'object' || !('id' in parsed) || !('type' in parsed)) {
|
|
4
|
-
throw new Error('invalid worker request');
|
|
5
|
-
}
|
|
6
|
-
if (parsed.type === 'ping')
|
|
7
|
-
return parsed;
|
|
8
|
-
if (parsed.type === 'execute') {
|
|
9
|
-
if (typeof parsed.task !== 'string' || typeof parsed.cwd !== 'string') {
|
|
10
|
-
throw new Error('execute request requires task and cwd');
|
|
11
|
-
}
|
|
12
|
-
return parsed;
|
|
13
|
-
}
|
|
14
|
-
throw new Error(`unknown worker request type: ${parsed.type}`);
|
|
15
|
-
}
|
|
16
|
-
export function serializeWorkerResponse(response) {
|
|
17
|
-
return `${JSON.stringify(response)}\n`;
|
|
18
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { createInterface } from 'node:readline';
|
|
2
|
-
import { executeCursorTask } from './cursor-executor.js';
|
|
3
|
-
import { parseWorkerRequestLine, serializeWorkerResponse, } from './cursor-worker-protocol.js';
|
|
4
|
-
export async function handleCursorWorkerRequest(request) {
|
|
5
|
-
if (request.type === 'ping') {
|
|
6
|
-
return {
|
|
7
|
-
id: request.id,
|
|
8
|
-
ok: true,
|
|
9
|
-
result: {
|
|
10
|
-
ok: true,
|
|
11
|
-
status: 'completed',
|
|
12
|
-
failureCategory: 'success',
|
|
13
|
-
model: 'worker',
|
|
14
|
-
durationMs: 0,
|
|
15
|
-
stdout: 'pong',
|
|
16
|
-
stderr: '',
|
|
17
|
-
details: { worker: true },
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
try {
|
|
22
|
-
const result = await executeCursorTask({
|
|
23
|
-
task: request.task,
|
|
24
|
-
cwd: request.cwd,
|
|
25
|
-
model: request.model,
|
|
26
|
-
timeoutMs: request.timeoutMs,
|
|
27
|
-
runDir: request.runDir,
|
|
28
|
-
artifactDir: request.artifactDir,
|
|
29
|
-
});
|
|
30
|
-
return { id: request.id, ok: true, result };
|
|
31
|
-
}
|
|
32
|
-
catch (error) {
|
|
33
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
34
|
-
return { id: request.id, ok: false, error: message };
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
export async function runCursorWorkerOnStreams(input, output) {
|
|
38
|
-
const rl = createInterface({ input, crlfDelay: Infinity });
|
|
39
|
-
for await (const line of rl) {
|
|
40
|
-
const trimmed = line.trim();
|
|
41
|
-
if (!trimmed)
|
|
42
|
-
continue;
|
|
43
|
-
let response;
|
|
44
|
-
try {
|
|
45
|
-
const request = parseWorkerRequestLine(trimmed);
|
|
46
|
-
response = await handleCursorWorkerRequest(request);
|
|
47
|
-
}
|
|
48
|
-
catch (error) {
|
|
49
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
50
|
-
response = { id: 'unknown', ok: false, error: message };
|
|
51
|
-
}
|
|
52
|
-
output.write(serializeWorkerResponse(response));
|
|
53
|
-
}
|
|
54
|
-
}
|
package/dist/executors/cursor.js
DELETED