@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,421 @@
|
|
|
1
|
+
import { access, cp, mkdir, readdir, readFile, rename, rm, unlink, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { truncateOutput } from '../executors/cursor-executor.js';
|
|
4
|
+
/** Maximum chars for result summary in the run log. */
|
|
5
|
+
export const RUN_LOG_RESULT_MAX = 4_000;
|
|
6
|
+
/** Placeholder written to active logs before execution finishes. */
|
|
7
|
+
export const RUN_LOG_RUNNING_PLACEHOLDER = '_(执行中…)_';
|
|
8
|
+
/**
|
|
9
|
+
* Compute the active run directory under .harness/runs/active/<slug>/.
|
|
10
|
+
* Creates the directory and returns the path.
|
|
11
|
+
*/
|
|
12
|
+
export async function computeRunDir(cwd, task) {
|
|
13
|
+
const harnessDir = path.join(cwd, '.harness');
|
|
14
|
+
try {
|
|
15
|
+
await access(harnessDir);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const datePrefix = new Date().toISOString().slice(0, 10);
|
|
21
|
+
const slug = deriveRunSlug(task);
|
|
22
|
+
const activeDir = path.join(harnessDir, 'runs', 'active');
|
|
23
|
+
await mkdir(activeDir, { recursive: true });
|
|
24
|
+
const runDir = await resolveUniqueRunPath(activeDir, `${datePrefix}-${slug}`);
|
|
25
|
+
await mkdir(runDir, { recursive: true });
|
|
26
|
+
return runDir;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Persist a human-readable run log under `.harness/runs/`.
|
|
30
|
+
*
|
|
31
|
+
* Lifecycle:
|
|
32
|
+
* 1. Write to `.harness/runs/active/<date>-<slug>/run.md`
|
|
33
|
+
* 2. If execution succeeded, move the run directory to `.harness/runs/completed/`
|
|
34
|
+
* 3. If execution failed, move the run directory to `.harness/runs/failed/`
|
|
35
|
+
*
|
|
36
|
+
* No-op when `cwd` does not contain a `.harness/` directory.
|
|
37
|
+
*/
|
|
38
|
+
export async function persistCursorRunLog(cwd, input) {
|
|
39
|
+
const harnessDir = path.join(cwd, '.harness');
|
|
40
|
+
try {
|
|
41
|
+
await access(harnessDir);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null; // not a harness repo — skip silently
|
|
45
|
+
}
|
|
46
|
+
const datePrefix = new Date().toISOString().slice(0, 10);
|
|
47
|
+
const slug = deriveRunSlug(input.task);
|
|
48
|
+
const baseName = `${datePrefix}-${slug}`;
|
|
49
|
+
const activeDir = path.join(harnessDir, 'runs', 'active');
|
|
50
|
+
const completedDir = path.join(harnessDir, 'runs', 'completed');
|
|
51
|
+
const failedDir = path.join(harnessDir, 'runs', 'failed');
|
|
52
|
+
await mkdir(activeDir, { recursive: true });
|
|
53
|
+
await mkdir(completedDir, { recursive: true });
|
|
54
|
+
await mkdir(failedDir, { recursive: true });
|
|
55
|
+
const activeRunDir = input.runDir ?? await resolveUniqueRunPath(activeDir, baseName);
|
|
56
|
+
await mkdir(activeRunDir, { recursive: true });
|
|
57
|
+
const content = buildRunLogMarkdown(input);
|
|
58
|
+
const activePath = path.join(activeRunDir, 'run.md');
|
|
59
|
+
await writeFile(activePath, content, 'utf-8');
|
|
60
|
+
await writeRunMeta(activeRunDir, input);
|
|
61
|
+
if (input.result.ok) {
|
|
62
|
+
const completedPath = await resolveUniqueRunPath(completedDir, path.basename(activeRunDir));
|
|
63
|
+
await moveRunDirectory(activeRunDir, completedPath);
|
|
64
|
+
return { logPath: path.join(completedPath, 'run.md'), completed: true };
|
|
65
|
+
}
|
|
66
|
+
const failedPath = await resolveUniqueRunPath(failedDir, path.basename(activeRunDir));
|
|
67
|
+
if (input.result.status === 'failed' || input.result.status === 'timeout' || input.result.status === 'cancelled') {
|
|
68
|
+
await moveRunDirectory(activeRunDir, failedPath);
|
|
69
|
+
return { logPath: path.join(failedPath, 'run.md'), completed: false };
|
|
70
|
+
}
|
|
71
|
+
return { logPath: activePath, completed: false };
|
|
72
|
+
}
|
|
73
|
+
async function moveRunDirectory(fromDir, toDir) {
|
|
74
|
+
await mkdir(path.dirname(toDir), { recursive: true });
|
|
75
|
+
try {
|
|
76
|
+
await rename(fromDir, toDir);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
await cp(fromDir, toDir, { recursive: true });
|
|
80
|
+
await rm(fromDir, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function writeRunMeta(runDir, input, statusOverride) {
|
|
84
|
+
const runId = path.basename(runDir);
|
|
85
|
+
const status = statusOverride ?? (input.result.ok ? 'completed' : input.result.status);
|
|
86
|
+
await writeFile(path.join(runDir, 'meta.json'), `${JSON.stringify({
|
|
87
|
+
runId,
|
|
88
|
+
taskId: input.taskId,
|
|
89
|
+
status,
|
|
90
|
+
model: input.model,
|
|
91
|
+
channel: input.channel,
|
|
92
|
+
cwd: input.cwd,
|
|
93
|
+
startedDate: new Date().toISOString().slice(0, 10),
|
|
94
|
+
durationMs: input.result.durationMs,
|
|
95
|
+
failureCategory: input.result.failureCategory,
|
|
96
|
+
artifactsDir: 'artifacts',
|
|
97
|
+
logPath: 'run.md',
|
|
98
|
+
}, null, 2)}\n`, 'utf-8');
|
|
99
|
+
}
|
|
100
|
+
function isRunMarkdownPath(logPath) {
|
|
101
|
+
return path.basename(logPath) === 'run.md';
|
|
102
|
+
}
|
|
103
|
+
function runDirForLogPath(logPath) {
|
|
104
|
+
return isRunMarkdownPath(logPath) ? path.dirname(logPath) : '';
|
|
105
|
+
}
|
|
106
|
+
function parseTaskIdFallback(logPath) {
|
|
107
|
+
if (isRunMarkdownPath(logPath))
|
|
108
|
+
return path.basename(path.dirname(logPath));
|
|
109
|
+
return path.basename(logPath, '.md');
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Write an in-progress run log under `.harness/runs/active/`.
|
|
113
|
+
* Optional helper for two-phase logging; Pi `cursor` tool uses sync `persistCursorRunLog` instead.
|
|
114
|
+
*/
|
|
115
|
+
export async function createActiveCursorRun(cwd, input) {
|
|
116
|
+
const harnessDir = await resolveHarnessDir(cwd);
|
|
117
|
+
if (!harnessDir)
|
|
118
|
+
return null;
|
|
119
|
+
const datePrefix = new Date().toISOString().slice(0, 10);
|
|
120
|
+
const slug = deriveRunSlug(input.task);
|
|
121
|
+
const baseName = `${datePrefix}-${slug}`;
|
|
122
|
+
const activeDir = path.join(harnessDir, 'runs', 'active');
|
|
123
|
+
await mkdir(activeDir, { recursive: true });
|
|
124
|
+
await mkdir(path.join(harnessDir, 'runs', 'completed'), { recursive: true });
|
|
125
|
+
await mkdir(path.join(harnessDir, 'runs', 'failed'), { recursive: true });
|
|
126
|
+
const activeRunDir = await resolveUniqueRunPath(activeDir, baseName);
|
|
127
|
+
await mkdir(activeRunDir, { recursive: true });
|
|
128
|
+
const activePath = path.join(activeRunDir, 'run.md');
|
|
129
|
+
const content = buildActiveRunLogMarkdown(input);
|
|
130
|
+
await writeFile(activePath, content, 'utf-8');
|
|
131
|
+
await writeRunMeta(activeRunDir, {
|
|
132
|
+
taskId: input.taskId,
|
|
133
|
+
task: input.task,
|
|
134
|
+
model: input.model,
|
|
135
|
+
channel: input.channel,
|
|
136
|
+
cwd: input.cwd,
|
|
137
|
+
result: {
|
|
138
|
+
ok: false,
|
|
139
|
+
status: 'failed',
|
|
140
|
+
failureCategory: 'unknown',
|
|
141
|
+
model: input.model,
|
|
142
|
+
durationMs: 0,
|
|
143
|
+
stdout: '',
|
|
144
|
+
stderr: '',
|
|
145
|
+
},
|
|
146
|
+
}, 'running');
|
|
147
|
+
return { logPath: activePath, taskId: input.taskId };
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Finalize an active run log with execution results.
|
|
151
|
+
* Moves successful runs to `.harness/runs/completed/`.
|
|
152
|
+
*/
|
|
153
|
+
export async function finalizeCursorRunLog(logPath, input) {
|
|
154
|
+
const harnessDir = await resolveHarnessDir(input.cwd);
|
|
155
|
+
if (!harnessDir)
|
|
156
|
+
return null;
|
|
157
|
+
const existing = await readFile(logPath, 'utf-8').catch(() => '');
|
|
158
|
+
const taskId = existing ? parseCursorRunLog(existing, logPath).taskId : undefined;
|
|
159
|
+
const content = buildRunLogMarkdown({ ...input, taskId });
|
|
160
|
+
await writeFile(logPath, content, 'utf-8');
|
|
161
|
+
const activeRunDir = runDirForLogPath(logPath);
|
|
162
|
+
if (activeRunDir) {
|
|
163
|
+
await writeRunMeta(activeRunDir, { ...input, taskId });
|
|
164
|
+
}
|
|
165
|
+
const targetLifecycle = input.result.ok ? 'completed' : 'failed';
|
|
166
|
+
const targetDir = path.join(harnessDir, 'runs', targetLifecycle);
|
|
167
|
+
await mkdir(targetDir, { recursive: true });
|
|
168
|
+
if (isRunMarkdownPath(logPath)) {
|
|
169
|
+
const completedPath = await resolveUniqueRunPath(targetDir, path.basename(path.dirname(logPath)));
|
|
170
|
+
await moveRunDirectory(path.dirname(logPath), completedPath);
|
|
171
|
+
return { logPath: path.join(completedPath, 'run.md'), completed: input.result.ok };
|
|
172
|
+
}
|
|
173
|
+
if (!input.result.ok) {
|
|
174
|
+
return { logPath, completed: false };
|
|
175
|
+
}
|
|
176
|
+
const completedPath = path.join(targetDir, path.basename(logPath));
|
|
177
|
+
try {
|
|
178
|
+
await rename(logPath, completedPath);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
await writeFile(completedPath, content, 'utf-8');
|
|
182
|
+
await unlink(logPath).catch(() => { });
|
|
183
|
+
}
|
|
184
|
+
return { logPath: completedPath, completed: true };
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Query Cursor run records from `.harness/runs/` (for `cursor-task` history / stale detection).
|
|
188
|
+
*/
|
|
189
|
+
export async function queryCursorRuns(cwd, options = {}) {
|
|
190
|
+
const harnessDir = await resolveHarnessDir(cwd);
|
|
191
|
+
if (!harnessDir)
|
|
192
|
+
return [];
|
|
193
|
+
const runsRoot = path.join(harnessDir, 'runs');
|
|
194
|
+
const activeDir = path.join(runsRoot, 'active');
|
|
195
|
+
const completedDir = path.join(runsRoot, 'completed');
|
|
196
|
+
const failedDir = path.join(runsRoot, 'failed');
|
|
197
|
+
const records = [];
|
|
198
|
+
for (const dir of [activeDir, failedDir, completedDir]) {
|
|
199
|
+
let entries;
|
|
200
|
+
try {
|
|
201
|
+
entries = (await readdir(dir)).sort();
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
for (const entry of entries) {
|
|
207
|
+
const entryPath = path.join(dir, entry);
|
|
208
|
+
const logPath = entry.endsWith('.md') ? entryPath : path.join(entryPath, 'run.md');
|
|
209
|
+
const content = await readFile(logPath, 'utf-8').catch(() => '');
|
|
210
|
+
if (!content)
|
|
211
|
+
continue;
|
|
212
|
+
const parsed = parseCursorRunLog(content, logPath);
|
|
213
|
+
parsed.status = resolveRunStatus(parsed, dir === completedDir, options.inMemoryTaskId, dir === failedDir);
|
|
214
|
+
records.push(parsed);
|
|
215
|
+
if (options.taskId && parsed.taskId === options.taskId) {
|
|
216
|
+
return [parsed];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (options.taskId)
|
|
221
|
+
return [];
|
|
222
|
+
const activeRecords = records
|
|
223
|
+
.filter((record) => record.status === 'running' || record.status === 'stale' || record.status === 'failed')
|
|
224
|
+
.sort((a, b) => (b.startTime ?? '').localeCompare(a.startTime ?? ''));
|
|
225
|
+
const completedRecords = records
|
|
226
|
+
.filter((record) => record.status === 'completed')
|
|
227
|
+
.sort((a, b) => (b.startTime ?? '').localeCompare(a.startTime ?? ''))
|
|
228
|
+
.slice(0, options.maxCompleted ?? 10);
|
|
229
|
+
return [...activeRecords, ...completedRecords];
|
|
230
|
+
}
|
|
231
|
+
export function parseCursorRunLog(content, logPath) {
|
|
232
|
+
const readField = (field) => {
|
|
233
|
+
const match = content.match(new RegExp(`\\| ${field} \\| ([^|]+) \\|`));
|
|
234
|
+
return match?.[1]?.trim();
|
|
235
|
+
};
|
|
236
|
+
const resultMatch = content.match(/## 结果摘要\s*\n\s*\n([\s\S]*?)(?:\n\n_\(|$)/);
|
|
237
|
+
const rawSummary = resultMatch?.[1]?.trim() ?? '';
|
|
238
|
+
const truncated = content.includes('_(输出已截断)_');
|
|
239
|
+
return {
|
|
240
|
+
taskId: readField('taskId') ?? parseTaskIdFallback(logPath),
|
|
241
|
+
status: 'running',
|
|
242
|
+
logPath,
|
|
243
|
+
task: extractTaskSection(content),
|
|
244
|
+
model: readField('模型') ?? 'default',
|
|
245
|
+
cwd: readField('cwd') ?? '',
|
|
246
|
+
channel: (readField('通道') ?? 'tool'),
|
|
247
|
+
startTime: readField('日期'),
|
|
248
|
+
durationSec: readField('耗时')?.replace(/s$/, ''),
|
|
249
|
+
tableStatus: readField('状态'),
|
|
250
|
+
failureCategory: readField('失败分类'),
|
|
251
|
+
resultSummary: rawSummary === RUN_LOG_RUNNING_PLACEHOLDER ? undefined : rawSummary,
|
|
252
|
+
resultTruncated: truncated,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
export function resolveRunStatus(record, inCompletedDir, inMemoryTaskId, inFailedDir = false) {
|
|
256
|
+
if (inCompletedDir)
|
|
257
|
+
return 'completed';
|
|
258
|
+
if (inFailedDir)
|
|
259
|
+
return 'failed';
|
|
260
|
+
if (record.resultSummary) {
|
|
261
|
+
if (record.tableStatus === 'completed')
|
|
262
|
+
return 'completed';
|
|
263
|
+
return 'failed';
|
|
264
|
+
}
|
|
265
|
+
if (record.tableStatus && record.tableStatus !== 'running') {
|
|
266
|
+
return 'failed';
|
|
267
|
+
}
|
|
268
|
+
if (inMemoryTaskId && record.taskId === inMemoryTaskId) {
|
|
269
|
+
return 'running';
|
|
270
|
+
}
|
|
271
|
+
return 'stale';
|
|
272
|
+
}
|
|
273
|
+
function extractTaskSection(content) {
|
|
274
|
+
const match = content.match(/## 任务\s*\n\s*\n([\s\S]*?)\n\s*\n## /);
|
|
275
|
+
return match?.[1]?.trim() ?? '';
|
|
276
|
+
}
|
|
277
|
+
async function resolveHarnessDir(cwd) {
|
|
278
|
+
const harnessDir = path.join(cwd, '.harness');
|
|
279
|
+
try {
|
|
280
|
+
await access(harnessDir);
|
|
281
|
+
return harnessDir;
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Derive a filesystem-safe slug from a task description.
|
|
289
|
+
*
|
|
290
|
+
* Rules:
|
|
291
|
+
* - Take first 40 characters
|
|
292
|
+
* - Replace non-alphanumeric (except CJK) with `-`
|
|
293
|
+
* - Collapse consecutive `-`
|
|
294
|
+
* - Trim leading/trailing `-`
|
|
295
|
+
* - Empty task → `untitled`
|
|
296
|
+
*/
|
|
297
|
+
export function deriveRunSlug(task) {
|
|
298
|
+
const raw = task.trim().slice(0, 40);
|
|
299
|
+
if (!raw)
|
|
300
|
+
return 'untitled';
|
|
301
|
+
// Allow alphanumeric, CJK unified ideographs (U+4E00–U+9FFF), hiragana, katakana
|
|
302
|
+
// Replace everything else with `-`
|
|
303
|
+
const slug = raw
|
|
304
|
+
.replace(/[^\p{L}\p{N}]/gu, '-')
|
|
305
|
+
.replace(/-+/g, '-')
|
|
306
|
+
.replace(/^-|-$/g, '');
|
|
307
|
+
return slug || 'untitled';
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Canonical one-shot run id format.
|
|
311
|
+
*
|
|
312
|
+
* One-shot Cursor/Pi run ids are directory basenames under
|
|
313
|
+
* `.harness/runs/<state>/`. New runs use the form produced by
|
|
314
|
+
* `computeRunDir` / `persistCursorRunLog`:
|
|
315
|
+
*
|
|
316
|
+
* YYYY-MM-DD-<kebab-slug>[-N]
|
|
317
|
+
*
|
|
318
|
+
* where `-N` is the collision suffix appended by `resolveUniqueRunPath`
|
|
319
|
+
* (e.g. `-2`, `-3`). Legacy `.md`-only run logs (read-only compatible) may
|
|
320
|
+
* use the bare `YYYY-MM-DD-<slug>.md` form.
|
|
321
|
+
*/
|
|
322
|
+
export const CANONICAL_ONE_SHOT_RUN_ID_PATTERN = /^\d{4}-\d{2}-\d{2}-[a-z0-9]+(-[a-z0-9]+)*(?:-(?!r\d)\d+)?$/;
|
|
323
|
+
/** Bare `.md` legacy log name: `YYYY-MM-DD-<slug>.md` (read-only compatibility). */
|
|
324
|
+
const LEGACY_ONE_SHOT_MD_PATTERN = /^\d{4}-\d{2}-\d{2}-[a-z0-9]+(-[a-z0-9]+)*\.md$/;
|
|
325
|
+
/**
|
|
326
|
+
* Validate a one-shot run id (or legacy `.md` log basename) against the
|
|
327
|
+
* canonical format. Legacy ids remain readable; this only emits warnings.
|
|
328
|
+
*
|
|
329
|
+
* Accepts either a directory-style id (`YYYY-MM-DD-<slug>[-N]`) or a legacy
|
|
330
|
+
* markdown log name (`YYYY-MM-DD-<slug>.md`).
|
|
331
|
+
*/
|
|
332
|
+
export function validateOneShotRunId(runId) {
|
|
333
|
+
const warnings = [];
|
|
334
|
+
const trimmed = runId.trim();
|
|
335
|
+
if (!trimmed) {
|
|
336
|
+
warnings.push("one-shot runId cannot be empty");
|
|
337
|
+
return { warnings };
|
|
338
|
+
}
|
|
339
|
+
const stripped = trimmed.endsWith(".md")
|
|
340
|
+
? trimmed.slice(0, -3)
|
|
341
|
+
: trimmed;
|
|
342
|
+
if (CANONICAL_ONE_SHOT_RUN_ID_PATTERN.test(stripped) ||
|
|
343
|
+
LEGACY_ONE_SHOT_MD_PATTERN.test(trimmed)) {
|
|
344
|
+
return { warnings };
|
|
345
|
+
}
|
|
346
|
+
warnings.push(`one-shot runId "${trimmed}" does not match canonical YYYY-MM-DD-<slug>; legacy run evidence remains readable`);
|
|
347
|
+
return { warnings };
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* If `baseName` already exists in `dir`, append `-2`, `-3`, etc.
|
|
351
|
+
*/
|
|
352
|
+
export async function resolveUniqueRunPath(dir, baseName) {
|
|
353
|
+
let candidate = path.join(dir, baseName);
|
|
354
|
+
let counter = 1;
|
|
355
|
+
while (true) {
|
|
356
|
+
try {
|
|
357
|
+
await access(candidate);
|
|
358
|
+
// File exists — try next suffix
|
|
359
|
+
counter += 1;
|
|
360
|
+
const ext = path.extname(baseName);
|
|
361
|
+
const stem = path.basename(baseName, ext);
|
|
362
|
+
candidate = path.join(dir, `${stem}-${counter}${ext}`);
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
// File does not exist — use this path
|
|
366
|
+
return candidate;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
// Markdown builder
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
function buildRunLogMarkdown(input) {
|
|
374
|
+
const durationSec = (input.result.durationMs / 1000).toFixed(1);
|
|
375
|
+
const outputText = input.result.stdout || input.result.stderr || '(no output)';
|
|
376
|
+
const truncated = truncateOutput(outputText, RUN_LOG_RESULT_MAX);
|
|
377
|
+
const lines = [
|
|
378
|
+
'# Cursor 执行记录',
|
|
379
|
+
'',
|
|
380
|
+
'| 字段 | 值 |',
|
|
381
|
+
'|------|----|',
|
|
382
|
+
];
|
|
383
|
+
if (input.taskId) {
|
|
384
|
+
lines.push(`| taskId | ${input.taskId} |`);
|
|
385
|
+
}
|
|
386
|
+
lines.push(`| 日期 | ${new Date().toISOString().slice(0, 10)} |`, `| 模型 | ${input.model} |`, `| 耗时 | ${durationSec}s |`, `| 状态 | ${input.result.ok ? 'completed' : input.result.status} |`, `| 通道 | ${input.channel} |`, `| cwd | ${input.cwd} |`);
|
|
387
|
+
if (!input.result.ok && input.result.failureCategory !== 'success') {
|
|
388
|
+
lines.push(`| 失败分类 | ${input.result.failureCategory} |`);
|
|
389
|
+
}
|
|
390
|
+
lines.push('', '## 任务', '', input.task, '', '## 结果摘要', '', truncated.text);
|
|
391
|
+
if (truncated.truncated) {
|
|
392
|
+
lines.push('', '_(输出已截断)_');
|
|
393
|
+
}
|
|
394
|
+
lines.push('');
|
|
395
|
+
return lines.join('\n');
|
|
396
|
+
}
|
|
397
|
+
function buildActiveRunLogMarkdown(input) {
|
|
398
|
+
const lines = [
|
|
399
|
+
'# Cursor 执行记录',
|
|
400
|
+
'',
|
|
401
|
+
'| 字段 | 值 |',
|
|
402
|
+
'|------|----|',
|
|
403
|
+
`| taskId | ${input.taskId} |`,
|
|
404
|
+
`| 日期 | ${new Date().toISOString().slice(0, 10)} |`,
|
|
405
|
+
`| 模型 | ${input.model} |`,
|
|
406
|
+
`| 耗时 | - |`,
|
|
407
|
+
`| 状态 | running |`,
|
|
408
|
+
`| 通道 | ${input.channel} |`,
|
|
409
|
+
`| cwd | ${input.cwd} |`,
|
|
410
|
+
'',
|
|
411
|
+
'## 任务',
|
|
412
|
+
'',
|
|
413
|
+
input.task,
|
|
414
|
+
'',
|
|
415
|
+
'## 结果摘要',
|
|
416
|
+
'',
|
|
417
|
+
RUN_LOG_RUNNING_PLACEHOLDER,
|
|
418
|
+
'',
|
|
419
|
+
];
|
|
420
|
+
return lines.join('\n');
|
|
421
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getArtifactPath } from '../shared/artifacts-core.js';
|
|
4
|
+
import { getDagRunDir, readDagRunState } from '../workflows/dag/lifecycle.js';
|
|
5
|
+
import { parseCursorRunLog, resolveRunStatus } from './one-shot-runs.js';
|
|
6
|
+
import { getTaskDir } from '../task/runtime.js';
|
|
7
|
+
export async function promoteRunEvidence(repoRoot, taskId, runId) {
|
|
8
|
+
const dag = await loadCompletedDagEvidence(repoRoot, runId).catch(() => null);
|
|
9
|
+
if (dag) {
|
|
10
|
+
return writePromotion(repoRoot, taskId, runId, 'dag', buildDagModifyLog(taskId, runId, dag), buildDagVerifyResult(taskId, runId, dag));
|
|
11
|
+
}
|
|
12
|
+
const oneShot = await loadCompletedOneShotEvidence(repoRoot, runId);
|
|
13
|
+
if (oneShot) {
|
|
14
|
+
return writePromotion(repoRoot, taskId, runId, 'one-shot', buildOneShotModifyLog(taskId, runId, oneShot), buildOneShotVerifyResult(taskId, runId, oneShot));
|
|
15
|
+
}
|
|
16
|
+
throw new Error(`completed run evidence not found for run-id: ${runId}`);
|
|
17
|
+
}
|
|
18
|
+
export async function closeoutTask(repoRoot, taskId) {
|
|
19
|
+
const taskDir = getTaskDir(repoRoot, taskId);
|
|
20
|
+
const modifyPath = getArtifactPath(taskDir, 'implement');
|
|
21
|
+
const verifyPath = getArtifactPath(taskDir, 'verify');
|
|
22
|
+
const modify = await readFile(modifyPath, 'utf-8').catch(() => '');
|
|
23
|
+
const verify = await readFile(verifyPath, 'utf-8').catch(() => '');
|
|
24
|
+
const slug = taskId.replace(/^\d{4}-\d{2}-\d{2}-/, '');
|
|
25
|
+
const datePrefix = taskId.match(/^\d{4}-\d{2}-\d{2}/)?.[0] ?? new Date().toISOString().slice(0, 10);
|
|
26
|
+
const progressDir = path.join(repoRoot, 'docs', 'progress');
|
|
27
|
+
await mkdir(progressDir, { recursive: true });
|
|
28
|
+
const progressPath = path.join(progressDir, `${datePrefix}-${slug}.md`);
|
|
29
|
+
const verificationSummary = summarizeVerification(verify);
|
|
30
|
+
const content = [
|
|
31
|
+
`# ${taskId}`,
|
|
32
|
+
'',
|
|
33
|
+
'## Summary',
|
|
34
|
+
'',
|
|
35
|
+
summarizeSection(modify) || '- No promoted modification summary found.',
|
|
36
|
+
'',
|
|
37
|
+
'## Verification',
|
|
38
|
+
'',
|
|
39
|
+
verificationSummary,
|
|
40
|
+
'',
|
|
41
|
+
'## Remaining Risk',
|
|
42
|
+
'',
|
|
43
|
+
verificationSummary.includes('No concrete verification evidence')
|
|
44
|
+
? '- Verification evidence is incomplete; do not treat this closeout as proof of passing tests.'
|
|
45
|
+
: '- Review remaining risks in task artifacts and related run facts before archiving.',
|
|
46
|
+
'',
|
|
47
|
+
].join('\n');
|
|
48
|
+
await writeFile(progressPath, content, 'utf-8');
|
|
49
|
+
return { taskId, progressPath };
|
|
50
|
+
}
|
|
51
|
+
async function writePromotion(repoRoot, taskId, runId, sourceKind, modifyLog, verifyResult) {
|
|
52
|
+
const taskDir = getTaskDir(repoRoot, taskId);
|
|
53
|
+
const modifyLogPath = getArtifactPath(taskDir, 'implement');
|
|
54
|
+
const verifyResultPath = getArtifactPath(taskDir, 'verify');
|
|
55
|
+
await mkdir(path.dirname(modifyLogPath), { recursive: true });
|
|
56
|
+
await writeFile(modifyLogPath, modifyLog, 'utf-8');
|
|
57
|
+
await writeFile(verifyResultPath, verifyResult, 'utf-8');
|
|
58
|
+
return { taskId, runId, sourceKind, modifyLogPath, verifyResultPath };
|
|
59
|
+
}
|
|
60
|
+
async function loadCompletedDagEvidence(repoRoot, runId) {
|
|
61
|
+
const runDir = getDagRunDir(repoRoot, 'completed', runId);
|
|
62
|
+
const state = await readDagRunState(runDir);
|
|
63
|
+
if (state.status !== 'finished') {
|
|
64
|
+
throw new Error(`completed DAG run ${runId} is not finished (status=${state.status})`);
|
|
65
|
+
}
|
|
66
|
+
return state;
|
|
67
|
+
}
|
|
68
|
+
async function loadCompletedOneShotEvidence(repoRoot, runId) {
|
|
69
|
+
const completedDir = path.join(repoRoot, '.harness', 'runs', 'completed');
|
|
70
|
+
const direct = await readOneShotRecord(path.join(completedDir, runId, 'run.md'), true);
|
|
71
|
+
if (direct)
|
|
72
|
+
return direct;
|
|
73
|
+
const legacy = await readOneShotRecord(path.join(completedDir, `${runId}.md`), true);
|
|
74
|
+
if (legacy)
|
|
75
|
+
return legacy;
|
|
76
|
+
const entries = await readdir(completedDir).catch(() => []);
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
const logPath = entry.endsWith('.md')
|
|
79
|
+
? path.join(completedDir, entry)
|
|
80
|
+
: path.join(completedDir, entry, 'run.md');
|
|
81
|
+
const record = await readOneShotRecord(logPath, true);
|
|
82
|
+
if (record && oneShotRunIdFromLogPath(logPath) === runId)
|
|
83
|
+
return record;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
async function readOneShotRecord(logPath, completed) {
|
|
88
|
+
const content = await readFile(logPath, 'utf-8').catch(() => '');
|
|
89
|
+
if (!content)
|
|
90
|
+
return null;
|
|
91
|
+
const record = parseCursorRunLog(content, logPath);
|
|
92
|
+
record.status = resolveRunStatus(record, completed);
|
|
93
|
+
return record.status === 'completed' ? record : null;
|
|
94
|
+
}
|
|
95
|
+
function oneShotRunIdFromLogPath(logPath) {
|
|
96
|
+
if (path.basename(logPath) === 'run.md')
|
|
97
|
+
return path.basename(path.dirname(logPath));
|
|
98
|
+
return path.basename(logPath, '.md');
|
|
99
|
+
}
|
|
100
|
+
function buildDagModifyLog(taskId, runId, state) {
|
|
101
|
+
const finished = Object.values(state.nodes).filter((node) => node.status === 'FINISHED');
|
|
102
|
+
return [
|
|
103
|
+
'# 修改记录',
|
|
104
|
+
'',
|
|
105
|
+
'## 1. 修改文件清单',
|
|
106
|
+
'',
|
|
107
|
+
`- Promoted from completed DAG run \`${runId}\` for task \`${taskId}\`.`,
|
|
108
|
+
'',
|
|
109
|
+
'## 2. 关键改动说明',
|
|
110
|
+
'',
|
|
111
|
+
...finished.map((node) => `- ${node.id}: ${excerpt(node.assistantText ?? node.stdout ?? 'finished')}`),
|
|
112
|
+
'',
|
|
113
|
+
].join('\n');
|
|
114
|
+
}
|
|
115
|
+
function buildDagVerifyResult(taskId, runId, state) {
|
|
116
|
+
const nodes = Object.values(state.nodes);
|
|
117
|
+
const verifyNodes = nodes.filter((node) => /verify|ci|check/i.test(node.id));
|
|
118
|
+
const failed = nodes.filter((node) => node.status === 'ERROR');
|
|
119
|
+
const lines = [
|
|
120
|
+
'# 验证结果',
|
|
121
|
+
'',
|
|
122
|
+
'## 1. 执行的命令',
|
|
123
|
+
'',
|
|
124
|
+
verifyNodes.length > 0
|
|
125
|
+
? verifyNodes.map((node) => `- DAG node \`${node.id}\`: ${node.failureCategory ?? node.status}`).join('\n')
|
|
126
|
+
: '- No dedicated verify/ci/check DAG node was found in completed run facts.',
|
|
127
|
+
'',
|
|
128
|
+
'## 2. 通过项',
|
|
129
|
+
'',
|
|
130
|
+
`- Promoted deterministic evidence from completed DAG run \`${runId}\` for task \`${taskId}\`.`,
|
|
131
|
+
...verifyNodes.filter((node) => node.status === 'FINISHED').map((node) => `- ${node.id}: ${node.failureCategory ?? 'success'}`),
|
|
132
|
+
'',
|
|
133
|
+
'## 3. 失败项',
|
|
134
|
+
'',
|
|
135
|
+
failed.length > 0
|
|
136
|
+
? failed.map((node) => `- ${node.id}: ${node.failureCategory ?? 'error'}`).join('\n')
|
|
137
|
+
: '- No failed DAG nodes recorded.',
|
|
138
|
+
'',
|
|
139
|
+
];
|
|
140
|
+
return lines.join('\n');
|
|
141
|
+
}
|
|
142
|
+
function buildOneShotModifyLog(taskId, runId, record) {
|
|
143
|
+
return [
|
|
144
|
+
'# 修改记录',
|
|
145
|
+
'',
|
|
146
|
+
'## 1. 修改文件清单',
|
|
147
|
+
'',
|
|
148
|
+
`- Promoted from completed one-shot run \`${runId}\` for task \`${taskId}\`.`,
|
|
149
|
+
'',
|
|
150
|
+
'## 2. 关键改动说明',
|
|
151
|
+
'',
|
|
152
|
+
`- Task: ${record.task || '(not recorded)'}`,
|
|
153
|
+
`- Result: ${excerpt(record.resultSummary ?? 'completed')}`,
|
|
154
|
+
'',
|
|
155
|
+
].join('\n');
|
|
156
|
+
}
|
|
157
|
+
function buildOneShotVerifyResult(taskId, runId, record) {
|
|
158
|
+
return [
|
|
159
|
+
'# 验证结果',
|
|
160
|
+
'',
|
|
161
|
+
'## 1. 执行的命令',
|
|
162
|
+
'',
|
|
163
|
+
'- One-shot run evidence does not expose deterministic shell commands; inspect the run log for details.',
|
|
164
|
+
'',
|
|
165
|
+
'## 2. 通过项',
|
|
166
|
+
'',
|
|
167
|
+
`- One-shot run \`${runId}\` for task \`${taskId}\` completed with status ${record.tableStatus ?? record.status}.`,
|
|
168
|
+
'',
|
|
169
|
+
'## 3. 失败项',
|
|
170
|
+
'',
|
|
171
|
+
record.failureCategory && record.failureCategory !== 'success'
|
|
172
|
+
? `- Failure category recorded: ${record.failureCategory}`
|
|
173
|
+
: '- No failure category recorded.',
|
|
174
|
+
'',
|
|
175
|
+
].join('\n');
|
|
176
|
+
}
|
|
177
|
+
function summarizeSection(content) {
|
|
178
|
+
return content
|
|
179
|
+
.split(/\r?\n/)
|
|
180
|
+
.filter((line) => line.trim().startsWith('- '))
|
|
181
|
+
.slice(0, 8)
|
|
182
|
+
.join('\n');
|
|
183
|
+
}
|
|
184
|
+
function summarizeVerification(content) {
|
|
185
|
+
if (!/npm|vitest|check-repo|typecheck|lint|CI|exit code|passed|failed/i.test(content)) {
|
|
186
|
+
return '- No concrete verification evidence found in task artifacts.';
|
|
187
|
+
}
|
|
188
|
+
const bullets = summarizeSection(content);
|
|
189
|
+
return bullets || '- Verification artifact exists; inspect task artifacts for details.';
|
|
190
|
+
}
|
|
191
|
+
function excerpt(value, max = 220) {
|
|
192
|
+
const normalized = value.replace(/\s+/g, ' ').trim();
|
|
193
|
+
if (normalized.length <= max)
|
|
194
|
+
return normalized;
|
|
195
|
+
return `${normalized.slice(0, max)}...`;
|
|
196
|
+
}
|
|
197
|
+
export async function pathExists(filePath) {
|
|
198
|
+
return Boolean(await stat(filePath).catch(() => null));
|
|
199
|
+
}
|