@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
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
import { access, mkdir, readFile, readdir, rm, stat, writeFile, } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { loadHarnessManifest } from "./harness.js";
|
|
5
|
+
const README_BASENAME = "README.md";
|
|
6
|
+
/**
|
|
7
|
+
* Plan ids must be safe file stems. We reject anything that could escape the
|
|
8
|
+
* exec-plans directory, masquerade as the README, or carry path separators.
|
|
9
|
+
*/
|
|
10
|
+
export function assertSafePlanId(planId) {
|
|
11
|
+
if (typeof planId !== "string" ||
|
|
12
|
+
planId.length === 0 ||
|
|
13
|
+
planId === README_BASENAME ||
|
|
14
|
+
planId === "README" ||
|
|
15
|
+
path.isAbsolute(planId) ||
|
|
16
|
+
planId.includes("/") ||
|
|
17
|
+
planId.includes("\\") ||
|
|
18
|
+
planId.includes("..") ||
|
|
19
|
+
planId.includes(path.sep) ||
|
|
20
|
+
planId.includes("\0")) {
|
|
21
|
+
throw new Error(`invalid plan id: ${JSON.stringify(planId)}; must be a safe file stem without path separators, '..', or 'README'`);
|
|
22
|
+
}
|
|
23
|
+
// Also block Windows-reserved/odd characters that would break filesystems.
|
|
24
|
+
if (/[<>:"|?*]/.test(planId)) {
|
|
25
|
+
throw new Error(`invalid plan id: ${JSON.stringify(planId)}; contains forbidden filesystem characters`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export async function resolveExecPlanPaths(repoRoot) {
|
|
29
|
+
const manifest = await loadHarnessManifest(repoRoot);
|
|
30
|
+
const execPlansDir = path.join(repoRoot, manifest.artifacts.execPlansDir ??
|
|
31
|
+
path.join(manifest.governanceRoot, "exec-plans"));
|
|
32
|
+
return {
|
|
33
|
+
execPlansDir,
|
|
34
|
+
activeDir: path.join(execPlansDir, "active"),
|
|
35
|
+
completedDir: path.join(execPlansDir, "completed"),
|
|
36
|
+
activeIndex: path.join(execPlansDir, "active", README_BASENAME),
|
|
37
|
+
completedIndex: path.join(execPlansDir, "completed", README_BASENAME),
|
|
38
|
+
localTemplatePath: path.join(repoRoot, manifest.governanceRoot, "templates", "exec-plan.md"),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async function exists(targetPath) {
|
|
42
|
+
try {
|
|
43
|
+
await access(targetPath);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function ensureDir(targetPath) {
|
|
51
|
+
await mkdir(targetPath, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Resolve a plan file path, guaranteeing it lands inside the given directory.
|
|
55
|
+
* This is defense-in-depth on top of {@link assertSafePlanId}: even if a caller
|
|
56
|
+
* bypasses validation, the resolved path is confined to the exec-plans dir.
|
|
57
|
+
*/
|
|
58
|
+
function resolveConfinedPlanPath(dir, planId) {
|
|
59
|
+
assertSafePlanId(planId);
|
|
60
|
+
const base = path.basename(planId);
|
|
61
|
+
const resolved = path.resolve(dir, `${base}.md`);
|
|
62
|
+
const relative = path.relative(dir, resolved);
|
|
63
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
64
|
+
throw new Error(`invalid plan id: ${JSON.stringify(planId)}; escapes directory ${dir}`);
|
|
65
|
+
}
|
|
66
|
+
return resolved;
|
|
67
|
+
}
|
|
68
|
+
function extractTitle(content) {
|
|
69
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
70
|
+
return match?.[1]?.trim() ?? null;
|
|
71
|
+
}
|
|
72
|
+
function extractStatusLine(content) {
|
|
73
|
+
const match = content.match(/^##\s*状态\s*\n\s*\n([^\n]+)/m);
|
|
74
|
+
return match?.[1]?.trim() ?? null;
|
|
75
|
+
}
|
|
76
|
+
function normalizeStatus(statusLine) {
|
|
77
|
+
if (!statusLine)
|
|
78
|
+
return "unknown";
|
|
79
|
+
const value = statusLine.replace(/^-\s*/, "").trim().toLowerCase();
|
|
80
|
+
if (value.startsWith("active"))
|
|
81
|
+
return "active";
|
|
82
|
+
if (value.startsWith("completed"))
|
|
83
|
+
return "completed";
|
|
84
|
+
if (value.startsWith("blocked"))
|
|
85
|
+
return "blocked";
|
|
86
|
+
if (value.startsWith("draft"))
|
|
87
|
+
return "draft";
|
|
88
|
+
return "unknown";
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Build the active exec-plan body. The status line uses the `- active` form so
|
|
92
|
+
* `check` and the shell checker can both parse it deterministically.
|
|
93
|
+
*/
|
|
94
|
+
export function renderActiveExecPlan(title, templateContent) {
|
|
95
|
+
const safeTitle = title.trim();
|
|
96
|
+
if (!safeTitle) {
|
|
97
|
+
throw new Error("plan create requires a non-empty title");
|
|
98
|
+
}
|
|
99
|
+
let rendered = templateContent.replace(/^#\s+.*$/m, `# ${safeTitle}`);
|
|
100
|
+
rendered = rendered.replace(/^## 标题[^\n]*\r?\n(?:\r?\n)?/m, "");
|
|
101
|
+
if (/^##\s*状态\s*\n\s*\n[^\n]+/m.test(rendered)) {
|
|
102
|
+
rendered = rendered.replace(/^##\s*状态\s*\n\s*\n[^\n]+/m, "## 状态\n\n- active");
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
rendered = `${rendered.trimEnd()}\n\n## 状态\n\n- active\n`;
|
|
106
|
+
}
|
|
107
|
+
return `${rendered.trimEnd()}\n`;
|
|
108
|
+
}
|
|
109
|
+
async function loadExecPlanTemplate(paths) {
|
|
110
|
+
if (await exists(paths.localTemplatePath)) {
|
|
111
|
+
return readFile(paths.localTemplatePath, "utf-8");
|
|
112
|
+
}
|
|
113
|
+
const packageRoot = await findPackageRoot();
|
|
114
|
+
const bundledTemplatePath = path.join(packageRoot, "docs", "templates", "exec-plan.md");
|
|
115
|
+
if (!(await exists(bundledTemplatePath))) {
|
|
116
|
+
throw new Error(`exec-plan template not found: ${paths.localTemplatePath} or ${bundledTemplatePath}`);
|
|
117
|
+
}
|
|
118
|
+
return readFile(bundledTemplatePath, "utf-8");
|
|
119
|
+
}
|
|
120
|
+
async function findPackageRoot() {
|
|
121
|
+
let current = path.dirname(fileURLToPath(import.meta.url));
|
|
122
|
+
while (true) {
|
|
123
|
+
if (await exists(path.join(current, "package.json")))
|
|
124
|
+
return current;
|
|
125
|
+
const parent = path.dirname(current);
|
|
126
|
+
if (parent === current) {
|
|
127
|
+
throw new Error("cannot locate loop-agent package root for exec-plan template");
|
|
128
|
+
}
|
|
129
|
+
current = parent;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function applyCompletedStatus(content) {
|
|
133
|
+
const replaced = content.replace(/^##\s*状态\s*\n\s*\n([^\n]+)/m, (_whole, line) => {
|
|
134
|
+
const next = line
|
|
135
|
+
.replace(/^-\s*active\b/i, "- completed")
|
|
136
|
+
.replace(/^active\b/i, "completed");
|
|
137
|
+
return `## 状态\n\n${next}`;
|
|
138
|
+
});
|
|
139
|
+
if (/^##\s*状态\s*\n\s*\n[^\n]+/m.test(replaced)) {
|
|
140
|
+
return replaced;
|
|
141
|
+
}
|
|
142
|
+
return `${replaced.trimEnd()}\n\n## 状态\n\n- completed\n`;
|
|
143
|
+
}
|
|
144
|
+
function appendCompletionSummary(content, summary) {
|
|
145
|
+
const trimmed = summary.trim();
|
|
146
|
+
const block = [
|
|
147
|
+
"## 完成摘要",
|
|
148
|
+
"",
|
|
149
|
+
`- ${trimmed}`,
|
|
150
|
+
"",
|
|
151
|
+
].join("\n");
|
|
152
|
+
if (/^##\s*完成摘要\b/m.test(content)) {
|
|
153
|
+
return content.replace(/^##\s*完成摘要\b.*(?=\n##\s|\n*$)/s, block.trimEnd());
|
|
154
|
+
}
|
|
155
|
+
return `${content.trimEnd()}\n\n${block}`;
|
|
156
|
+
}
|
|
157
|
+
async function listPlanFiles(dir) {
|
|
158
|
+
if (!(await exists(dir)))
|
|
159
|
+
return [];
|
|
160
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
161
|
+
return entries
|
|
162
|
+
.filter((entry) => entry.isFile() &&
|
|
163
|
+
entry.name.endsWith(".md") &&
|
|
164
|
+
entry.name !== README_BASENAME)
|
|
165
|
+
.map((entry) => entry.name)
|
|
166
|
+
.sort((a, b) => a.localeCompare(b));
|
|
167
|
+
}
|
|
168
|
+
async function readPlanStatus(filePath) {
|
|
169
|
+
const content = await readFile(filePath, "utf-8");
|
|
170
|
+
return normalizeStatus(extractStatusLine(content));
|
|
171
|
+
}
|
|
172
|
+
function localIndexReference(rawReference) {
|
|
173
|
+
const normalized = rawReference
|
|
174
|
+
.trim()
|
|
175
|
+
.replace(/^<|>$/g, "")
|
|
176
|
+
.split(/[?#]/, 1)[0]
|
|
177
|
+
.replaceAll("\\", "/");
|
|
178
|
+
if (!normalized.endsWith(".md") || normalized.includes("/"))
|
|
179
|
+
return null;
|
|
180
|
+
return normalized;
|
|
181
|
+
}
|
|
182
|
+
function extractIndexReferences(indexContent) {
|
|
183
|
+
const references = new Set();
|
|
184
|
+
for (const line of indexContent.split(/\r?\n/)) {
|
|
185
|
+
// Match markdown links [text](file.md) and backtick-quoted `file.md`.
|
|
186
|
+
// This mirrors scripts/check-exec-plan-index-sync.sh, which keys on
|
|
187
|
+
// `(${base})` and `` `base` ``. Bare mentions are intentionally NOT
|
|
188
|
+
// treated as references to avoid false positives from partial matches.
|
|
189
|
+
const linkMatches = [...line.matchAll(/\[[^\]]*\]\(([^)]+\.md)\)/g)];
|
|
190
|
+
for (const match of linkMatches) {
|
|
191
|
+
const reference = match[1] ? localIndexReference(match[1]) : null;
|
|
192
|
+
if (reference)
|
|
193
|
+
references.add(reference);
|
|
194
|
+
}
|
|
195
|
+
const backtickMatches = [...line.matchAll(/`([^`]+\.md)`/g)];
|
|
196
|
+
for (const match of backtickMatches) {
|
|
197
|
+
const reference = match[1] ? localIndexReference(match[1]) : null;
|
|
198
|
+
if (reference)
|
|
199
|
+
references.add(reference);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return references;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Check that exec-plan directories and indexes are mutually consistent.
|
|
206
|
+
*
|
|
207
|
+
* Empty repos (no plan files, consistent indexes) are always OK; this only
|
|
208
|
+
* fails closed on real drift so default DAG flow remains unblocked.
|
|
209
|
+
*/
|
|
210
|
+
export async function checkExecPlanIndex(repoRoot, options = {}) {
|
|
211
|
+
const paths = await resolveExecPlanPaths(repoRoot);
|
|
212
|
+
const activeFiles = await listPlanFiles(paths.activeDir);
|
|
213
|
+
const completedFiles = await listPlanFiles(paths.completedDir);
|
|
214
|
+
const issues = [];
|
|
215
|
+
const activeIndexContent = (await exists(paths.activeIndex))
|
|
216
|
+
? await readFile(paths.activeIndex, "utf-8")
|
|
217
|
+
: "";
|
|
218
|
+
const completedIndexContent = (await exists(paths.completedIndex))
|
|
219
|
+
? await readFile(paths.completedIndex, "utf-8")
|
|
220
|
+
: "";
|
|
221
|
+
const activeIndexRefs = extractIndexReferences(activeIndexContent);
|
|
222
|
+
const completedIndexRefs = extractIndexReferences(completedIndexContent);
|
|
223
|
+
for (const file of activeFiles) {
|
|
224
|
+
const status = await readPlanStatus(path.join(paths.activeDir, file));
|
|
225
|
+
if (!activeIndexRefs.has(file)) {
|
|
226
|
+
issues.push({
|
|
227
|
+
kind: "missing-from-index",
|
|
228
|
+
status: "active",
|
|
229
|
+
file,
|
|
230
|
+
message: `active exec-plan 文件 ${file} 未在 active/README.md 索引中登记`,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
if (status === "completed") {
|
|
234
|
+
issues.push({
|
|
235
|
+
kind: "status-directory-mismatch",
|
|
236
|
+
status,
|
|
237
|
+
file,
|
|
238
|
+
message: `exec-plan ${file} 的状态为 completed,但仍位于 active 目录`,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
for (const file of completedFiles) {
|
|
243
|
+
const status = await readPlanStatus(path.join(paths.completedDir, file));
|
|
244
|
+
if (!completedIndexRefs.has(file)) {
|
|
245
|
+
issues.push({
|
|
246
|
+
kind: "missing-from-index",
|
|
247
|
+
status: "completed",
|
|
248
|
+
file,
|
|
249
|
+
message: `completed exec-plan 文件 ${file} 未在 completed/README.md 索引中登记`,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
if (status === "active") {
|
|
253
|
+
issues.push({
|
|
254
|
+
kind: "status-directory-mismatch",
|
|
255
|
+
status,
|
|
256
|
+
file,
|
|
257
|
+
message: `exec-plan ${file} 的状态为 active,但仍位于 completed 目录`,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
for (const ref of activeIndexRefs) {
|
|
262
|
+
if (ref === README_BASENAME)
|
|
263
|
+
continue;
|
|
264
|
+
if (!activeFiles.includes(ref)) {
|
|
265
|
+
issues.push({
|
|
266
|
+
kind: "stale-index-entry",
|
|
267
|
+
status: "active",
|
|
268
|
+
file: ref,
|
|
269
|
+
message: `active/README.md 引用了不存在的文件 ${ref}`,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
for (const ref of completedIndexRefs) {
|
|
274
|
+
if (ref === README_BASENAME)
|
|
275
|
+
continue;
|
|
276
|
+
if (!completedFiles.includes(ref)) {
|
|
277
|
+
issues.push({
|
|
278
|
+
kind: "stale-index-entry",
|
|
279
|
+
status: "completed",
|
|
280
|
+
file: ref,
|
|
281
|
+
message: `completed/README.md 引用了不存在的文件 ${ref}`,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const duplicates = activeFiles.filter((file) => completedFiles.includes(file));
|
|
286
|
+
for (const file of duplicates) {
|
|
287
|
+
issues.push({
|
|
288
|
+
kind: "duplicate-plan-id",
|
|
289
|
+
file,
|
|
290
|
+
message: `exec-plan ${file} 同时出现在 active 和 completed 目录`,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
const ok = issues.length === 0;
|
|
294
|
+
if (options.strict && !ok) {
|
|
295
|
+
throw new Error(`exec-plan index check failed (${issues.length} issue(s)): ${issues
|
|
296
|
+
.map((issue) => issue.message)
|
|
297
|
+
.join("; ")}`);
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
ok,
|
|
301
|
+
issues,
|
|
302
|
+
activeFiles,
|
|
303
|
+
completedFiles,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Assert that the exec-plan index is consistent. Used by `dag run-task`
|
|
308
|
+
* preflight so drift fails fast before expensive DAG nodes run.
|
|
309
|
+
*/
|
|
310
|
+
export async function assertExecPlanIndexConsistent(repoRoot) {
|
|
311
|
+
const result = await checkExecPlanIndex(repoRoot, { strict: true });
|
|
312
|
+
void result;
|
|
313
|
+
}
|
|
314
|
+
function ensureActiveIndexEntry(indexContent, planId, title) {
|
|
315
|
+
const file = `${planId}.md`;
|
|
316
|
+
const safeTitle = title.trim() || planId;
|
|
317
|
+
const marker = `- [\`${file}\`](${file}) — ${safeTitle}`;
|
|
318
|
+
if (indexContent.includes(`(${file})`)) {
|
|
319
|
+
return indexContent;
|
|
320
|
+
}
|
|
321
|
+
const lines = indexContent.split(/\r?\n/);
|
|
322
|
+
const insertIndex = findInsertionPointForActiveEntry(lines);
|
|
323
|
+
const next = [...lines.slice(0, insertIndex), marker, ...lines.slice(insertIndex)];
|
|
324
|
+
return collapseBlankRuns(next).join("\n").replace(/\s+$/g, "") + "\n";
|
|
325
|
+
}
|
|
326
|
+
function findInsertionPointForActiveEntry(lines) {
|
|
327
|
+
// Insert before the first line that looks like a narrative / archive
|
|
328
|
+
// trailing section (blank-line separated context), or at the end.
|
|
329
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
330
|
+
const line = lines[i];
|
|
331
|
+
if (/^[-*]\s+已归档/.test(line))
|
|
332
|
+
return i;
|
|
333
|
+
if (/^[-*]\s+`[^`]+\.md`/.test(line)) {
|
|
334
|
+
// skip existing plan entries
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (/^[-*]\s+/.test(line) && i > 0) {
|
|
338
|
+
// A non-plan bullet (e.g. "已归档:...") starts the trailing context.
|
|
339
|
+
return i;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return lines.length;
|
|
343
|
+
}
|
|
344
|
+
function ensureCompletedIndexEntry(indexContent, planId) {
|
|
345
|
+
const file = `${planId}.md`;
|
|
346
|
+
const marker = `- [\`${file}\`](${file})`;
|
|
347
|
+
if (indexContent.includes(`(${file})`)) {
|
|
348
|
+
return indexContent;
|
|
349
|
+
}
|
|
350
|
+
return `${indexContent.trimEnd()}\n\n${marker}\n`;
|
|
351
|
+
}
|
|
352
|
+
function removeIndexEntries(indexContent, file) {
|
|
353
|
+
const lines = indexContent.split(/\r?\n/);
|
|
354
|
+
const filtered = lines.filter((line) => {
|
|
355
|
+
const linkMatch = line.match(/\[[^\]]*\]\(([^)]+)\)/);
|
|
356
|
+
if (linkMatch && path.basename(linkMatch[1]) === file)
|
|
357
|
+
return false;
|
|
358
|
+
if (line.includes(`\`${file}\``))
|
|
359
|
+
return false;
|
|
360
|
+
return true;
|
|
361
|
+
});
|
|
362
|
+
return collapseBlankRuns(filtered).join("\n").replace(/\s+$/g, "") + "\n";
|
|
363
|
+
}
|
|
364
|
+
function collapseBlankRuns(lines) {
|
|
365
|
+
const result = [];
|
|
366
|
+
let prevBlank = false;
|
|
367
|
+
for (const line of lines) {
|
|
368
|
+
const isBlank = line.trim() === "";
|
|
369
|
+
if (isBlank && prevBlank)
|
|
370
|
+
continue;
|
|
371
|
+
result.push(line);
|
|
372
|
+
prevBlank = isBlank;
|
|
373
|
+
}
|
|
374
|
+
while (result.length > 0 && result[result.length - 1].trim() === "") {
|
|
375
|
+
result.pop();
|
|
376
|
+
}
|
|
377
|
+
return result;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Create an active exec-plan file and register it in the active index.
|
|
381
|
+
* Fails if the plan id already exists in active or completed.
|
|
382
|
+
*/
|
|
383
|
+
export async function createExecPlan(repoRoot, planId, title) {
|
|
384
|
+
assertSafePlanId(planId);
|
|
385
|
+
const paths = await resolveExecPlanPaths(repoRoot);
|
|
386
|
+
const activePlanPath = resolveConfinedPlanPath(paths.activeDir, planId);
|
|
387
|
+
const completedPlanPath = resolveConfinedPlanPath(paths.completedDir, planId);
|
|
388
|
+
if (await exists(activePlanPath)) {
|
|
389
|
+
throw new Error(`exec-plan ${planId} already exists in active directory`);
|
|
390
|
+
}
|
|
391
|
+
if (await exists(completedPlanPath)) {
|
|
392
|
+
throw new Error(`exec-plan ${planId} already exists in completed directory`);
|
|
393
|
+
}
|
|
394
|
+
const activeIndexExisted = await exists(paths.activeIndex);
|
|
395
|
+
const activeIndexWasFile = activeIndexExisted
|
|
396
|
+
? (await stat(paths.activeIndex)).isFile()
|
|
397
|
+
: false;
|
|
398
|
+
const activeIndexOriginal = activeIndexWasFile
|
|
399
|
+
? await readFile(paths.activeIndex, "utf-8")
|
|
400
|
+
: null;
|
|
401
|
+
const body = renderActiveExecPlan(title, await loadExecPlanTemplate(paths));
|
|
402
|
+
try {
|
|
403
|
+
await ensureDir(paths.activeDir);
|
|
404
|
+
await writeFile(activePlanPath, body, "utf-8");
|
|
405
|
+
await ensureIndexFile(paths.activeIndex, "# 进行中的执行计划\n\n");
|
|
406
|
+
const activeIndexContent = await readFile(paths.activeIndex, "utf-8");
|
|
407
|
+
const nextIndex = ensureActiveIndexEntry(activeIndexContent, planId, extractTitle(body) ?? planId);
|
|
408
|
+
if (nextIndex !== activeIndexContent) {
|
|
409
|
+
await writeFile(paths.activeIndex, nextIndex, "utf-8");
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
await rollbackCreation({
|
|
414
|
+
activePlanPath,
|
|
415
|
+
activeIndexPath: paths.activeIndex,
|
|
416
|
+
activeIndexExisted,
|
|
417
|
+
activeIndexOriginal,
|
|
418
|
+
});
|
|
419
|
+
throw error;
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
planId,
|
|
423
|
+
status: "active",
|
|
424
|
+
planPath: activePlanPath,
|
|
425
|
+
indexPath: paths.activeIndex,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
async function rollbackCreation(context) {
|
|
429
|
+
if (await exists(context.activePlanPath)) {
|
|
430
|
+
await rm(context.activePlanPath, { force: true }).catch(() => undefined);
|
|
431
|
+
}
|
|
432
|
+
if (context.activeIndexExisted) {
|
|
433
|
+
if (context.activeIndexOriginal !== null) {
|
|
434
|
+
await writeFile(context.activeIndexPath, context.activeIndexOriginal, "utf-8").catch(() => undefined);
|
|
435
|
+
}
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (await exists(context.activeIndexPath)) {
|
|
439
|
+
await rm(context.activeIndexPath, { force: true }).catch(() => undefined);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
async function ensureIndexFile(indexPath, bootstrapContent) {
|
|
443
|
+
if (await exists(indexPath))
|
|
444
|
+
return;
|
|
445
|
+
await mkdir(path.dirname(indexPath), { recursive: true });
|
|
446
|
+
await writeFile(indexPath, bootstrapContent, "utf-8");
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Move an active exec-plan to completed, set status to completed, append the
|
|
450
|
+
* summary, and sync both indexes. Rollback restores touched files on failure.
|
|
451
|
+
*/
|
|
452
|
+
export async function completeExecPlan(repoRoot, planId, options) {
|
|
453
|
+
assertSafePlanId(planId);
|
|
454
|
+
const summary = options.summary?.trim();
|
|
455
|
+
if (!summary) {
|
|
456
|
+
throw new Error("plan complete requires a non-empty --summary describing what was delivered");
|
|
457
|
+
}
|
|
458
|
+
const paths = await resolveExecPlanPaths(repoRoot);
|
|
459
|
+
const activePlanPath = resolveConfinedPlanPath(paths.activeDir, planId);
|
|
460
|
+
const completedPlanPath = resolveConfinedPlanPath(paths.completedDir, planId);
|
|
461
|
+
if (!(await exists(activePlanPath))) {
|
|
462
|
+
throw new Error(`exec-plan ${planId} not found in active directory`);
|
|
463
|
+
}
|
|
464
|
+
if (await exists(completedPlanPath)) {
|
|
465
|
+
throw new Error(`exec-plan ${planId} already exists in completed directory`);
|
|
466
|
+
}
|
|
467
|
+
// Snapshot everything we will mutate so a mid-flight failure can roll back.
|
|
468
|
+
const activePlanOriginal = await readFile(activePlanPath, "utf-8");
|
|
469
|
+
const activeIndexExisted = await exists(paths.activeIndex);
|
|
470
|
+
const activeIndexOriginal = activeIndexExisted
|
|
471
|
+
? await readFile(paths.activeIndex, "utf-8")
|
|
472
|
+
: null;
|
|
473
|
+
const completedIndexExisted = await exists(paths.completedIndex);
|
|
474
|
+
const completedIndexOriginal = completedIndexExisted
|
|
475
|
+
? await readFile(paths.completedIndex, "utf-8")
|
|
476
|
+
: null;
|
|
477
|
+
const file = `${planId}.md`;
|
|
478
|
+
const completedBody = appendCompletionSummary(applyCompletedStatus(activePlanOriginal), summary);
|
|
479
|
+
try {
|
|
480
|
+
await ensureDir(paths.completedDir);
|
|
481
|
+
await writeFile(completedPlanPath, completedBody, "utf-8");
|
|
482
|
+
await ensureIndexFile(paths.completedIndex, "# 已完成的执行计划\n\n");
|
|
483
|
+
const completedIndexContent = await readFile(paths.completedIndex, "utf-8");
|
|
484
|
+
const nextCompletedIndex = ensureCompletedIndexEntry(completedIndexContent, planId);
|
|
485
|
+
if (nextCompletedIndex !== completedIndexContent) {
|
|
486
|
+
await writeFile(paths.completedIndex, nextCompletedIndex, "utf-8");
|
|
487
|
+
}
|
|
488
|
+
await rm(activePlanPath, { force: true });
|
|
489
|
+
if (activeIndexExisted) {
|
|
490
|
+
const nextActiveIndex = removeIndexEntries(activeIndexOriginal ?? "", file);
|
|
491
|
+
if (nextActiveIndex !== activeIndexOriginal) {
|
|
492
|
+
await writeFile(paths.activeIndex, nextActiveIndex, "utf-8");
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
catch (error) {
|
|
497
|
+
await rollbackCompletion({
|
|
498
|
+
activePlanPath,
|
|
499
|
+
activePlanOriginal,
|
|
500
|
+
completedPlanPath,
|
|
501
|
+
activeIndexPath: paths.activeIndex,
|
|
502
|
+
activeIndexExisted,
|
|
503
|
+
activeIndexOriginal,
|
|
504
|
+
completedIndexPath: paths.completedIndex,
|
|
505
|
+
completedIndexExisted,
|
|
506
|
+
completedIndexOriginal,
|
|
507
|
+
});
|
|
508
|
+
throw error;
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
planId,
|
|
512
|
+
status: "completed",
|
|
513
|
+
fromPath: activePlanPath,
|
|
514
|
+
toPath: completedPlanPath,
|
|
515
|
+
activeIndex: paths.activeIndex,
|
|
516
|
+
completedIndex: paths.completedIndex,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
async function rollbackCompletion(context) {
|
|
520
|
+
try {
|
|
521
|
+
await writeFile(context.activePlanPath, context.activePlanOriginal, "utf-8");
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
// Best-effort restore; original error must still propagate.
|
|
525
|
+
}
|
|
526
|
+
if (await exists(context.completedPlanPath)) {
|
|
527
|
+
await rm(context.completedPlanPath, { force: true });
|
|
528
|
+
}
|
|
529
|
+
await restoreOrRemoveIndex(context.activeIndexPath, context.activeIndexExisted, context.activeIndexOriginal);
|
|
530
|
+
await restoreOrRemoveIndex(context.completedIndexPath, context.completedIndexExisted, context.completedIndexOriginal);
|
|
531
|
+
}
|
|
532
|
+
async function restoreOrRemoveIndex(indexPath, existed, original) {
|
|
533
|
+
if (existed && original !== null) {
|
|
534
|
+
try {
|
|
535
|
+
await writeFile(indexPath, original, "utf-8");
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
// best-effort
|
|
539
|
+
}
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (await exists(indexPath)) {
|
|
543
|
+
await rm(indexPath, { force: true });
|
|
544
|
+
}
|
|
545
|
+
}
|
|
@@ -16,7 +16,8 @@ export const complexityRouteSchema = z.object({
|
|
|
16
16
|
export const worktreeManifestConfigSchema = z.object({
|
|
17
17
|
rootRelativePath: z.string().optional().default(".worktrees"),
|
|
18
18
|
});
|
|
19
|
-
export const taskExecutorSchema = z.enum(["pi"
|
|
19
|
+
export const taskExecutorSchema = z.enum(["pi"]);
|
|
20
|
+
export const CURSOR_TASK_EXECUTOR_REMOVED_ERROR = 'task executor "cursor" is no longer supported; governed runtime is Pi-only';
|
|
20
21
|
export const executorManifestSchema = z.object({
|
|
21
22
|
description: z.string().optional(),
|
|
22
23
|
enabled: z.boolean().optional(),
|
|
@@ -41,6 +42,7 @@ export const workflowPolicyDagTemplateSchema = z.enum([
|
|
|
41
42
|
"standard-dag",
|
|
42
43
|
"review-gated-dag",
|
|
43
44
|
"supervised-implementation",
|
|
45
|
+
"frontend-implementation",
|
|
44
46
|
]);
|
|
45
47
|
export const workflowPolicyOutputLanguageSchema = z.enum(["zh-CN", "en"]);
|
|
46
48
|
export const workflowPolicySchema = z
|
|
@@ -92,7 +94,9 @@ export const workflowPolicySchema = z
|
|
|
92
94
|
})
|
|
93
95
|
.optional()
|
|
94
96
|
.default({});
|
|
95
|
-
export const
|
|
97
|
+
export const CURSOR_HARNESS_EXECUTOR_REMOVED_ERROR = 'harness executors.cursor is no longer supported; remove it and use executors.pi only (Cursor is available only via cursor-prompt sidecar)';
|
|
98
|
+
export const harnessManifestSchema = z
|
|
99
|
+
.object({
|
|
96
100
|
version: z.number(),
|
|
97
101
|
project: z.string(),
|
|
98
102
|
adapter: z.string().optional(),
|
|
@@ -138,4 +142,22 @@ export const harnessManifestSchema = z.object({
|
|
|
138
142
|
.record(z.string(), executorManifestSchema)
|
|
139
143
|
.optional()
|
|
140
144
|
.default({}),
|
|
145
|
+
})
|
|
146
|
+
.superRefine((manifest, ctx) => {
|
|
147
|
+
const executors = manifest.executors ?? {};
|
|
148
|
+
if ("cursor" in executors) {
|
|
149
|
+
ctx.addIssue({
|
|
150
|
+
code: z.ZodIssueCode.custom,
|
|
151
|
+
message: CURSOR_HARNESS_EXECUTOR_REMOVED_ERROR,
|
|
152
|
+
path: ["executors", "cursor"],
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
const entrypoints = manifest.entrypoints ?? {};
|
|
156
|
+
if ("cursorExecutorUsage" in entrypoints) {
|
|
157
|
+
ctx.addIssue({
|
|
158
|
+
code: z.ZodIssueCode.custom,
|
|
159
|
+
message: 'entrypoints.cursorExecutorUsage is no longer supported; remove it (Cursor is cursor-prompt sidecar only)',
|
|
160
|
+
path: ["entrypoints", "cursorExecutorUsage"],
|
|
161
|
+
});
|
|
162
|
+
}
|
|
141
163
|
});
|
|
@@ -9,9 +9,6 @@ function loopActionWriteOptions(repoRoot) {
|
|
|
9
9
|
export async function writeLoopShellVerifyRecord(repoRoot, taskId, round, record) {
|
|
10
10
|
await writeJsonAtomic(loopActionFile(repoRoot, taskId, "verification", `round-${round}.json`), record, loopActionWriteOptions(repoRoot));
|
|
11
11
|
}
|
|
12
|
-
export async function writeLoopCursorFixRecord(repoRoot, taskId, round, record) {
|
|
13
|
-
await writeJsonAtomic(loopActionFile(repoRoot, taskId, "reviews", `round-${round}-cursor-fix.json`), record, loopActionWriteOptions(repoRoot));
|
|
14
|
-
}
|
|
15
12
|
export async function writeLoopDagRecord(repoRoot, taskId, round, record) {
|
|
16
13
|
await writeJsonAtomic(loopActionFile(repoRoot, taskId, "dag", `round-${round}.json`), record, loopActionWriteOptions(repoRoot));
|
|
17
14
|
}
|
package/dist/records/harvest.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { loadHarnessManifest } from '../governance/harness.js';
|
|
5
5
|
import { resolveWorktreePath } from '../task/worktree.js';
|
|
6
6
|
import { getTaskPaths, getTaskStatus, loadTaskConfig } from '../task/runtime.js';
|
|
7
7
|
import { validateDiffPathGuard, listDiffPaths } from '../governance/path-guard.js';
|
|
8
|
-
import { validateCursorArtifacts } from '../executors/cursor-artifacts.js';
|
|
9
|
-
import { checkTaskHandoff } from '../governance/checks.js';
|
|
10
8
|
import { archiveTaskDirFromWorktree } from '../task/worktree-cleanup.js';
|
|
11
9
|
export { copyDir } from '../shared/copy-dir.js';
|
|
12
10
|
/**
|
|
@@ -154,8 +152,7 @@ async function runHarvestGates(repoRoot, taskId, taskRoot, baseBranch, branch) {
|
|
|
154
152
|
catch {
|
|
155
153
|
return;
|
|
156
154
|
}
|
|
157
|
-
const shouldRunPathGuard = taskConfig.
|
|
158
|
-
|| (taskConfig.allowedPaths ?? []).length > 0
|
|
155
|
+
const shouldRunPathGuard = (taskConfig.allowedPaths ?? []).length > 0
|
|
159
156
|
|| (taskConfig.forbiddenPaths ?? []).length > 0;
|
|
160
157
|
if (shouldRunPathGuard) {
|
|
161
158
|
const changedPaths = await listDiffPaths(repoRoot, baseBranch, branch);
|
|
@@ -165,24 +162,6 @@ async function runHarvestGates(repoRoot, taskId, taskRoot, baseBranch, branch) {
|
|
|
165
162
|
'Worktree preserved for manual review.');
|
|
166
163
|
}
|
|
167
164
|
}
|
|
168
|
-
if (taskConfig.executor !== 'cursor') {
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
const paths = getTaskPaths(taskRoot, taskId);
|
|
172
|
-
const modifyLog = await readFile(path.join(paths.taskDir, 'artifacts', '修改记录.md'), 'utf-8');
|
|
173
|
-
const verifyResult = await readFile(path.join(paths.taskDir, 'artifacts', '验证结果.md'), 'utf-8');
|
|
174
|
-
const artifactIssues = validateCursorArtifacts(modifyLog, verifyResult);
|
|
175
|
-
if (artifactIssues.length > 0) {
|
|
176
|
-
throw new Error(`harvest artifact validation failed for cursor task "${taskId}": ${artifactIssues.join('; ')}. ` +
|
|
177
|
-
'Worktree preserved for manual review.');
|
|
178
|
-
}
|
|
179
|
-
const handoff = await checkTaskHandoff(taskRoot, taskId);
|
|
180
|
-
const taskHandoff = handoff.tasks.find((task) => task.taskId === taskId);
|
|
181
|
-
const blockingIssues = (taskHandoff?.issues ?? []).filter((issue) => issue.severity === 'error');
|
|
182
|
-
if (blockingIssues.length > 0) {
|
|
183
|
-
throw new Error(`harvest handoff check failed for task "${taskId}": ${blockingIssues.map((issue) => issue.message).join('; ')}. ` +
|
|
184
|
-
'Worktree preserved for manual review.');
|
|
185
|
-
}
|
|
186
165
|
}
|
|
187
166
|
// ---------------------------------------------------------------------------
|
|
188
167
|
// Helpers
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { access, readdir, readFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { truncateOutput } from '../
|
|
3
|
+
import { truncateOutput } from '../shared/output-truncation.js';
|
|
4
4
|
import { createOneShotActiveRunDir, ensureOneShotRunDirs, finalizeLegacyRunLogToCompleted, transferOneShotRunToCompleted, transferOneShotRunToFailed, writeOneShotActiveLogAtPath, writeOneShotRunMarkdown, writeOneShotRunMeta, } from '../infrastructure/harness/one-shot-run-store.js';
|
|
5
5
|
/** Maximum chars for result summary in the run log. */
|
|
6
6
|
export const RUN_LOG_RESULT_MAX = 4_000;
|