@tea-agent/loop-agent 0.1.0 → 0.2.1
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 +62 -45
- package/CHANGELOG.md +60 -28
- package/README.md +160 -124
- package/bin/loop-agent.js +21 -21
- package/dist/adapters/index.js +3 -2
- package/dist/adapters/loop-agent.js +44 -2
- package/dist/application/dag/args.js +420 -0
- package/dist/application/dag/generate-task-dag.js +280 -0
- package/dist/application/dag/report-dag.js +14 -0
- package/dist/application/dag/run-dag.js +106 -0
- package/dist/application/dag/validate-dag.js +102 -0
- package/dist/application/loop/run-action.js +23 -0
- package/dist/cli/catalog.js +2 -237
- package/dist/cli/command-definitions.js +571 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/program.js +65 -1
- package/dist/cli/router.js +13 -0
- package/dist/cli-governance/active-residue-check.js +38 -0
- package/dist/commands/dag-report.js +6 -107
- package/dist/commands/dag-run-task.js +8 -466
- package/dist/commands/dag-validate.js +7 -179
- package/dist/commands/examples.js +90 -0
- package/dist/commands/init.js +1518 -0
- package/dist/commands/loop.js +57 -31
- package/dist/commands/pi-prompt.js +2 -9
- package/dist/commands/run-dag.js +7 -180
- package/dist/executors/cursor-executor-artifacts.js +3 -4
- package/dist/executors/cursor-worker-client.js +13 -3
- package/dist/executors/dag-cursor-executor.js +2 -3
- package/dist/executors/dag-pi-executor.js +3 -4
- package/dist/executors/dag-static-executor.js +2 -5
- package/dist/executors/pi-defaults.js +9 -0
- package/dist/executors/shell-executor.js +12 -20
- package/dist/governance/manifest-types.js +1 -0
- package/dist/infrastructure/harness/active-residue-policy.js +73 -0
- package/dist/infrastructure/harness/artifact-store.js +72 -0
- package/dist/infrastructure/harness/atomic-write.js +49 -0
- package/dist/infrastructure/harness/completed-facts-guard.js +40 -0
- package/dist/infrastructure/harness/loop-action-store.js +23 -0
- package/dist/infrastructure/harness/loop-store.js +41 -0
- package/dist/infrastructure/harness/one-shot-run-store.js +94 -0
- package/dist/infrastructure/harness/task-store.js +77 -0
- package/dist/records/one-shot-runs.js +26 -61
- package/dist/records/promotion.js +3 -4
- package/dist/shared/artifacts-core.js +5 -5
- package/dist/shared/logger.js +9 -15
- package/dist/task/delegate.js +4 -4
- package/dist/task/runtime.js +5 -7
- package/dist/task/state.js +6 -20
- package/dist/workflows/dag/convergence/controller.js +277 -0
- package/dist/workflows/dag/dynamic-runtime/condition.js +48 -0
- package/dist/workflows/dag/dynamic-runtime/loop-until.js +156 -0
- package/dist/workflows/dag/dynamic-runtime/map.js +185 -0
- package/dist/workflows/dag/dynamic-runtime/reduction.js +72 -0
- package/dist/workflows/dag/dynamic-runtime/shared.js +133 -0
- package/dist/workflows/dag/failure-routing.js +82 -0
- package/dist/workflows/dag/lifecycle.js +101 -8
- package/dist/workflows/dag/node-execution.js +262 -0
- package/dist/workflows/dag/report.js +73 -1
- package/dist/workflows/dag/run-store.js +36 -0
- package/dist/workflows/dag/runner.js +82 -1341
- package/dist/workflows/dag/scheduler.js +84 -0
- package/dist/workflows/dag/upstream-artifacts.js +20 -18
- package/dist/workflows/loop/actions/cursor-fix.js +191 -0
- package/dist/workflows/loop/actions/dag-action.js +130 -0
- package/dist/workflows/loop/actions/pi-review.js +267 -0
- package/dist/workflows/loop/actions/shared.js +157 -0
- package/dist/workflows/loop/actions/shell-verify.js +82 -0
- package/dist/workflows/loop/actions/types.js +1 -0
- package/dist/workflows/loop/actions/workflow-action.js +255 -0
- package/dist/workflows/loop/actions.js +55 -1212
- package/dist/workflows/loop/closeout.js +5 -4
- package/dist/workflows/loop/context.js +2 -3
- package/dist/workflows/loop/events.js +3 -2
- package/dist/workflows/loop/policy/auto-policy.js +104 -0
- package/dist/workflows/loop/policy/cursor-fix-policy.js +31 -0
- package/dist/workflows/loop/rounds.js +3 -3
- package/dist/workflows/loop/signals.js +4 -7
- package/dist/workflows/loop/state.js +11 -11
- package/docs/README.md +47 -44
- package/docs/agent-dag-recovery-playbook.md +32 -6
- package/docs/agent-dag-runner.md +17 -17
- package/docs/architecture/runtime-boundaries.md +147 -0
- package/docs/cursor-executor-usage.md +5 -5
- package/docs/decisions/README.md +2 -2
- package/docs/design/README.md +24 -24
- package/docs/development-principles.md +50 -50
- package/docs/dynamic-workflow-dag-engine-roadmap.md +6 -6
- package/docs/exec-plans/README.md +4 -4
- package/docs/exec-plans/active/README.md +10 -5
- package/docs/exec-plans/completed/README.md +9 -5
- package/docs/feature-workflow.md +111 -109
- package/docs/harness-methodology-verification.md +18 -18
- package/docs/loop-agent-harness.md +36 -36
- package/docs/production-readiness.md +96 -0
- package/docs/progress/README.md +2 -2
- package/docs/reports/README.md +4 -2
- package/docs/templates/agent-dag-decision-gate-dogfood-report.md +1 -1
- package/docs/templates/agent-dag-process-supervisor.prompt.md +2 -2
- package/docs/templates/agent-dag-report.schema.json +33 -2
- package/docs/templates/agent-dag-review-verdict.prompt.md +1 -1
- package/docs/templates/agent-dag.base.json +195 -195
- package/docs/templates/agent-dag.final-verification.json +190 -190
- package/docs/templates/agent-dag.schema.json +17 -17
- package/docs/templates/agent-dag.supervised-implementation.json +500 -500
- package/docs/templates/hybrid-dag.json +193 -193
- package/docs/templates/production-readiness-checklist.md +57 -0
- package/docs/templates/progress-log.md +7 -7
- package/docs/templates/project-start-checklist.md +8 -8
- package/docs/templates/qa-report.md +17 -11
- package/docs/templates/sprint-contract.md +19 -19
- package/docs/verification-matrix.md +37 -26
- package/examples/example-dag.json +51 -51
- package/examples/hybrid-loop-agent-dag.json +194 -194
- package/harness.json +5 -5
- package/package.json +62 -61
- package/skills/ai-engineering-context/SKILL.md +21 -21
- package/skills/loop-agent/SKILL.md +56 -171
- package/skills/loop-agent/references/README.md +6 -2
- package/skills/loop-agent/references/command-reference.md +107 -65
- package/skills/loop-agent/references/harness-policy.md +115 -115
- package/skills/loop-agent/references/hybrid-dag.md +30 -30
- package/skills/loop-agent/references/learned/README.md +13 -13
- package/skills/loop-agent/references/long-running-loop.md +59 -0
- package/skills/loop-agent/references/model-routing.md +1 -1
- package/skills/loop-agent/references/orchestrator-and-interventions.md +1 -1
- package/skills/loop-agent/references/pi-prompt.md +9 -9
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +0 -2
- package/skills/loop-agent/references/post-implementation-and-patterns.md +7 -7
- package/skills/loop-agent/references/task-workflow.md +19 -19
- package/skills/loop-agent/references/verification-and-failure-handling.md +54 -0
- package/skills/requesting-code-review/SKILL.md +40 -40
- package/skills/requesting-code-review/code-reviewer.md +4 -4
- package/skills/systematic-debugging/CREATION-LOG.md +43 -43
- package/skills/systematic-debugging/SKILL.md +113 -113
- package/skills/systematic-debugging/condition-based-waiting.md +20 -20
- package/skills/systematic-debugging/defense-in-depth.md +27 -27
- package/skills/systematic-debugging/root-cause-tracing.md +38 -38
- package/skills/systematic-debugging/test-academic.md +6 -6
- package/skills/systematic-debugging/test-pressure-1.md +6 -6
- package/skills/systematic-debugging/test-pressure-2.md +2 -2
- package/skills/systematic-debugging/test-pressure-3.md +6 -6
- package/skills/verification-before-completion/SKILL.md +37 -37
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { DEFAULT_PI_MODEL, DEFAULT_PI_PROVIDER, } from "../../../executors/pi-defaults.js";
|
|
4
|
+
import { executeSingleSdkAttempt } from "../../../executors/pi-sdk-executor.js";
|
|
5
|
+
import { writeLoopPiReviewPrompt, writeLoopPiReviewRecord, } from "../../../infrastructure/harness/loop-action-store.js";
|
|
6
|
+
import { createOneShotActiveRunDir, transferOneShotRunToCompleted, transferOneShotRunToFailed, writeOneShotRunMarkdown, writeOneShotRunMeta, } from "../../../infrastructure/harness/one-shot-run-store.js";
|
|
7
|
+
import { getLoopPaths } from "../paths.js";
|
|
8
|
+
import { appendLoopRound } from "../rounds.js";
|
|
9
|
+
import { rewriteLoopContext } from "../context.js";
|
|
10
|
+
import { readLoopRounds } from "../rounds.js";
|
|
11
|
+
import { latestVerificationSummary, relativeRef, truncateSummary } from "./shared.js";
|
|
12
|
+
const PI_REVIEW_TOOLS = ["read", "grep", "find", "ls"];
|
|
13
|
+
export async function buildLoopPiReviewPrompt(repoRoot, taskId) {
|
|
14
|
+
const paths = getLoopPaths(repoRoot, taskId);
|
|
15
|
+
const [objective, context, rounds] = await Promise.all([
|
|
16
|
+
readFile(paths.objectivePath, "utf-8"),
|
|
17
|
+
readFile(paths.contextPath, "utf-8"),
|
|
18
|
+
readLoopRounds(repoRoot, taskId),
|
|
19
|
+
]);
|
|
20
|
+
return [
|
|
21
|
+
"You are running as a read-only loop-agent loop Pi review action.",
|
|
22
|
+
"Do not edit files. Do not run shell commands. Use only read, grep, find, and ls tools if tools are needed.",
|
|
23
|
+
"Review the current loop state and return exactly one JSON object with these fields:",
|
|
24
|
+
'{"findingSummary":"","failureCategory":"","nextHypothesis":"","recommendedAction":"implement_fix|replan|pause|done","fixScope":["path/or/component"],"rootCause":"","invariant":""}',
|
|
25
|
+
"recommendedAction must be exactly one of: implement_fix, replan, pause, done.",
|
|
26
|
+
"",
|
|
27
|
+
"<objective>",
|
|
28
|
+
objective.trim(),
|
|
29
|
+
"</objective>",
|
|
30
|
+
"",
|
|
31
|
+
"<context>",
|
|
32
|
+
context.trim(),
|
|
33
|
+
"</context>",
|
|
34
|
+
"",
|
|
35
|
+
"<latest_verification>",
|
|
36
|
+
latestVerificationSummary(rounds),
|
|
37
|
+
"</latest_verification>",
|
|
38
|
+
].join("\n");
|
|
39
|
+
}
|
|
40
|
+
function parsePiReviewStructuredOutput(text) {
|
|
41
|
+
const trimmed = text.trim();
|
|
42
|
+
const jsonMatch = trimmed.match(/\{[\s\S]*\}/);
|
|
43
|
+
if (!jsonMatch) {
|
|
44
|
+
throw new Error("pi-review output did not contain a JSON object");
|
|
45
|
+
}
|
|
46
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
47
|
+
for (const key of [
|
|
48
|
+
"findingSummary",
|
|
49
|
+
"failureCategory",
|
|
50
|
+
"nextHypothesis",
|
|
51
|
+
"recommendedAction",
|
|
52
|
+
"rootCause",
|
|
53
|
+
]) {
|
|
54
|
+
if (typeof parsed[key] !== "string" || parsed[key].trim().length === 0) {
|
|
55
|
+
throw new Error(`pi-review output missing structured field: ${key}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!Array.isArray(parsed.fixScope) || parsed.fixScope.length === 0) {
|
|
59
|
+
throw new Error("pi-review output missing structured field: fixScope");
|
|
60
|
+
}
|
|
61
|
+
const findingSummary = parsed.findingSummary;
|
|
62
|
+
const failureCategory = parsed.failureCategory;
|
|
63
|
+
const nextHypothesis = parsed.nextHypothesis;
|
|
64
|
+
const recommendedAction = parsed.recommendedAction;
|
|
65
|
+
const rootCause = parsed.rootCause;
|
|
66
|
+
if (typeof findingSummary !== "string" ||
|
|
67
|
+
typeof failureCategory !== "string" ||
|
|
68
|
+
typeof nextHypothesis !== "string" ||
|
|
69
|
+
typeof recommendedAction !== "string" ||
|
|
70
|
+
typeof rootCause !== "string") {
|
|
71
|
+
throw new Error("pi-review output failed structured field validation");
|
|
72
|
+
}
|
|
73
|
+
if (!isLoopPiReviewRecommendedAction(recommendedAction)) {
|
|
74
|
+
throw new Error(`pi-review output recommendedAction must be one of implement_fix|replan|pause|done, got: ${recommendedAction}`);
|
|
75
|
+
}
|
|
76
|
+
const fixScope = parsed.fixScope
|
|
77
|
+
.filter((value) => typeof value === "string")
|
|
78
|
+
.map((value) => value.trim())
|
|
79
|
+
.filter(Boolean);
|
|
80
|
+
if (fixScope.length === 0) {
|
|
81
|
+
throw new Error("pi-review output fixScope must contain at least one non-empty string");
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
findingSummary: findingSummary.trim(),
|
|
85
|
+
failureCategory: failureCategory.trim(),
|
|
86
|
+
nextHypothesis: nextHypothesis.trim(),
|
|
87
|
+
recommendedAction,
|
|
88
|
+
fixScope,
|
|
89
|
+
rootCause: rootCause.trim(),
|
|
90
|
+
invariant: typeof parsed.invariant === "string" ? parsed.invariant.trim() : undefined,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function isLoopPiReviewRecommendedAction(value) {
|
|
94
|
+
return (value === "implement_fix" ||
|
|
95
|
+
value === "replan" ||
|
|
96
|
+
value === "pause" ||
|
|
97
|
+
value === "done");
|
|
98
|
+
}
|
|
99
|
+
async function defaultPiReviewExecutor(input) {
|
|
100
|
+
const startedAt = Date.now();
|
|
101
|
+
const result = await executeSingleSdkAttempt({
|
|
102
|
+
repoRoot: input.cwd,
|
|
103
|
+
prompt: "You are a read-only reviewer for loop-agent loop. Return only structured JSON.",
|
|
104
|
+
userMessage: input.prompt,
|
|
105
|
+
attachedFiles: [],
|
|
106
|
+
toolNames: [...input.toolNames],
|
|
107
|
+
timeoutMs: input.timeoutMs,
|
|
108
|
+
step: "analyze",
|
|
109
|
+
modelConfig: {
|
|
110
|
+
provider: input.provider,
|
|
111
|
+
model: input.model,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
return {
|
|
115
|
+
ok: result.ok,
|
|
116
|
+
assistantText: result.assistantText,
|
|
117
|
+
durationMs: Date.now() - startedAt,
|
|
118
|
+
failureCategory: result.failureCategory,
|
|
119
|
+
stderr: result.stderr,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
export async function readLatestPiReviewRecord(repoRoot, taskId) {
|
|
123
|
+
const paths = getLoopPaths(repoRoot, taskId);
|
|
124
|
+
let entries;
|
|
125
|
+
try {
|
|
126
|
+
entries = await readdir(paths.reviewDir);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
const roundFiles = entries
|
|
132
|
+
.map((entry) => {
|
|
133
|
+
const match = entry.match(/^round-(\d+)\.json$/);
|
|
134
|
+
return match ? { entry, round: Number.parseInt(match[1], 10) } : undefined;
|
|
135
|
+
})
|
|
136
|
+
.filter((entry) => Boolean(entry))
|
|
137
|
+
.sort((a, b) => b.round - a.round);
|
|
138
|
+
for (const { entry } of roundFiles) {
|
|
139
|
+
try {
|
|
140
|
+
const parsed = JSON.parse(await readFile(path.join(paths.reviewDir, entry), "utf-8"));
|
|
141
|
+
if (parsed.action === "pi-review" && parsed.structured) {
|
|
142
|
+
return parsed;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
export async function runLoopPiReview(repoRoot, taskId, options = {}) {
|
|
152
|
+
const provider = options.provider ?? DEFAULT_PI_PROVIDER;
|
|
153
|
+
const model = options.model ?? DEFAULT_PI_MODEL;
|
|
154
|
+
const timeoutMs = options.timeoutMs ?? 300_000;
|
|
155
|
+
const cwd = options.cwd ? path.resolve(repoRoot, options.cwd) : repoRoot;
|
|
156
|
+
const executor = options.executor ?? defaultPiReviewExecutor;
|
|
157
|
+
const rounds = await readLoopRounds(repoRoot, taskId);
|
|
158
|
+
const round = rounds.length + 1;
|
|
159
|
+
const paths = getLoopPaths(repoRoot, taskId);
|
|
160
|
+
const prompt = await buildLoopPiReviewPrompt(repoRoot, taskId);
|
|
161
|
+
const promptPath = path.join(paths.reviewDir, `round-${round}-prompt.md`);
|
|
162
|
+
await writeLoopPiReviewPrompt(repoRoot, taskId, round, prompt);
|
|
163
|
+
const result = await executor({
|
|
164
|
+
prompt,
|
|
165
|
+
cwd,
|
|
166
|
+
provider,
|
|
167
|
+
model,
|
|
168
|
+
timeoutMs,
|
|
169
|
+
toolNames: PI_REVIEW_TOOLS,
|
|
170
|
+
});
|
|
171
|
+
let structured;
|
|
172
|
+
let ok = result.ok;
|
|
173
|
+
let failureCategory = result.failureCategory ?? "";
|
|
174
|
+
try {
|
|
175
|
+
structured = parsePiReviewStructuredOutput(result.assistantText);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
ok = false;
|
|
179
|
+
failureCategory = "invalid-structured-output";
|
|
180
|
+
structured = {
|
|
181
|
+
findingSummary: error instanceof Error ? error.message : "pi-review output parse failed",
|
|
182
|
+
failureCategory,
|
|
183
|
+
nextHypothesis: "rerun pi-review with a stricter structured-output prompt",
|
|
184
|
+
recommendedAction: "pause",
|
|
185
|
+
fixScope: ["unknown"],
|
|
186
|
+
rootCause: "invalid pi-review structured output",
|
|
187
|
+
invariant: "do not write until review output is valid",
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const targetLifecycle = ok ? "completed" : "failed";
|
|
191
|
+
const runBaseName = `${new Date().toISOString().slice(0, 10)}-${taskId}-pi-review-r${round}`;
|
|
192
|
+
const activeRunDir = await createOneShotActiveRunDir(repoRoot, runBaseName);
|
|
193
|
+
const runId = path.basename(activeRunDir);
|
|
194
|
+
const runMarkdown = [
|
|
195
|
+
"# Pi Review Run",
|
|
196
|
+
"",
|
|
197
|
+
"| Field | Value |",
|
|
198
|
+
"|---|---|",
|
|
199
|
+
`| taskId | ${taskId} |`,
|
|
200
|
+
`| status | ${ok ? "completed" : "failed"} |`,
|
|
201
|
+
`| provider | ${provider} |`,
|
|
202
|
+
`| model | ${model} |`,
|
|
203
|
+
`| tools | ${PI_REVIEW_TOOLS.join(",")} |`,
|
|
204
|
+
`| prompt | ${relativeRef(repoRoot, promptPath)} |`,
|
|
205
|
+
"",
|
|
206
|
+
"## Structured Output",
|
|
207
|
+
"",
|
|
208
|
+
"```json",
|
|
209
|
+
JSON.stringify(structured, null, 2),
|
|
210
|
+
"```",
|
|
211
|
+
"",
|
|
212
|
+
"## Assistant Text Summary",
|
|
213
|
+
"",
|
|
214
|
+
truncateSummary(result.assistantText || result.stderr || ""),
|
|
215
|
+
"",
|
|
216
|
+
].join("\n");
|
|
217
|
+
await writeOneShotRunMarkdown(repoRoot, activeRunDir, runMarkdown);
|
|
218
|
+
await writeOneShotRunMeta(repoRoot, activeRunDir, {
|
|
219
|
+
runId,
|
|
220
|
+
taskId,
|
|
221
|
+
status: ok ? "completed" : "failed",
|
|
222
|
+
model,
|
|
223
|
+
provider,
|
|
224
|
+
channel: "pi-review",
|
|
225
|
+
cwd,
|
|
226
|
+
durationMs: result.durationMs,
|
|
227
|
+
failureCategory,
|
|
228
|
+
toolNames: PI_REVIEW_TOOLS,
|
|
229
|
+
logPath: "run.md",
|
|
230
|
+
});
|
|
231
|
+
const targetRunDir = targetLifecycle === "completed"
|
|
232
|
+
? await transferOneShotRunToCompleted(repoRoot, activeRunDir, runId)
|
|
233
|
+
: await transferOneShotRunToFailed(repoRoot, activeRunDir, runId);
|
|
234
|
+
const runRef = relativeRef(repoRoot, path.join(targetRunDir, "run.md"));
|
|
235
|
+
const record = {
|
|
236
|
+
schemaVersion: 1,
|
|
237
|
+
taskId,
|
|
238
|
+
round,
|
|
239
|
+
action: "pi-review",
|
|
240
|
+
ok,
|
|
241
|
+
provider,
|
|
242
|
+
model,
|
|
243
|
+
toolNames: PI_REVIEW_TOOLS,
|
|
244
|
+
promptPath: relativeRef(repoRoot, promptPath),
|
|
245
|
+
runRef,
|
|
246
|
+
structured,
|
|
247
|
+
assistantTextSummary: truncateSummary(result.assistantText || result.stderr || ""),
|
|
248
|
+
failureCategory,
|
|
249
|
+
recordedAt: new Date().toISOString(),
|
|
250
|
+
};
|
|
251
|
+
await writeLoopPiReviewRecord(repoRoot, taskId, round, record);
|
|
252
|
+
await appendLoopRound(repoRoot, taskId, {
|
|
253
|
+
action: "pi-review",
|
|
254
|
+
refs: [runRef, relativeRef(repoRoot, path.join(paths.reviewDir, `round-${round}.json`))],
|
|
255
|
+
result: ok ? "pi review completed" : "pi review failed",
|
|
256
|
+
verification: ok
|
|
257
|
+
? `pi-review structured output: ${structured.findingSummary}`
|
|
258
|
+
: `pi-review failed: ${structured.findingSummary}`,
|
|
259
|
+
lesson: structured.findingSummary,
|
|
260
|
+
next: structured.recommendedAction,
|
|
261
|
+
decision: "continue",
|
|
262
|
+
failureCategory: structured.failureCategory,
|
|
263
|
+
completedCriteria: ok ? ["pi read-only review action executed"] : [],
|
|
264
|
+
});
|
|
265
|
+
await rewriteLoopContext(repoRoot, taskId);
|
|
266
|
+
return record;
|
|
267
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { repoRelativePath, toPosixPath } from "../../../shared/path-refs.js";
|
|
3
|
+
import { workflowReportSchema } from "../../dynamic/artifacts.js";
|
|
4
|
+
import { normalizePattern } from "../policy/cursor-fix-policy.js";
|
|
5
|
+
export const SUMMARY_LIMIT = 1200;
|
|
6
|
+
export function truncateSummary(text) {
|
|
7
|
+
const normalized = text.trim();
|
|
8
|
+
if (normalized.length <= SUMMARY_LIMIT)
|
|
9
|
+
return normalized;
|
|
10
|
+
return `${normalized.slice(0, SUMMARY_LIMIT)}\n[truncated]`;
|
|
11
|
+
}
|
|
12
|
+
export function extractCommandsFromObjective(objective) {
|
|
13
|
+
const lines = objective.split("\n");
|
|
14
|
+
const commands = [];
|
|
15
|
+
let inVerification = false;
|
|
16
|
+
for (const line of lines) {
|
|
17
|
+
if (/^##\s+/.test(line)) {
|
|
18
|
+
inVerification = /verification|验证/i.test(line);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (!inVerification)
|
|
22
|
+
continue;
|
|
23
|
+
const backtick = line.match(/`([^`]+)`/);
|
|
24
|
+
if (backtick?.[1]) {
|
|
25
|
+
commands.push(backtick[1]);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const bullet = line.match(/^[-*]\s+(.+)$/);
|
|
29
|
+
if (bullet?.[1] && !bullet[1].includes(":")) {
|
|
30
|
+
commands.push(bullet[1].trim());
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return commands;
|
|
34
|
+
}
|
|
35
|
+
export function hasDagRound(rounds) {
|
|
36
|
+
return rounds.some((round) => round.action === "dag");
|
|
37
|
+
}
|
|
38
|
+
export function latestVerificationSummary(rounds) {
|
|
39
|
+
const latest = [...rounds].reverse().find((round) => round.verification.trim());
|
|
40
|
+
return latest
|
|
41
|
+
? `${latest.verification}\nrefs=${latest.refs.join(", ")}`
|
|
42
|
+
: "No verification summary recorded yet.";
|
|
43
|
+
}
|
|
44
|
+
export function asRecord(value, label) {
|
|
45
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`${label} did not return a JSON object`);
|
|
49
|
+
}
|
|
50
|
+
export function stringField(value, key, label) {
|
|
51
|
+
const raw = value[key];
|
|
52
|
+
if (typeof raw === "string" && raw.length > 0)
|
|
53
|
+
return raw;
|
|
54
|
+
throw new Error(`${label} missing string field: ${key}`);
|
|
55
|
+
}
|
|
56
|
+
export function optionalStringField(value, key) {
|
|
57
|
+
const raw = value[key];
|
|
58
|
+
return typeof raw === "string" ? raw : "";
|
|
59
|
+
}
|
|
60
|
+
export function relativeRef(repoRoot, value) {
|
|
61
|
+
if (!value)
|
|
62
|
+
return "";
|
|
63
|
+
return path.isAbsolute(value) ? repoRelativePath(repoRoot, value) : toPosixPath(value);
|
|
64
|
+
}
|
|
65
|
+
export function firstReportRun(report) {
|
|
66
|
+
const root = asRecord(report, "dag report");
|
|
67
|
+
const runs = root.runs;
|
|
68
|
+
if (!Array.isArray(runs) || runs.length === 0) {
|
|
69
|
+
throw new Error("dag report JSON did not contain any runs");
|
|
70
|
+
}
|
|
71
|
+
return asRecord(runs[0], "dag report run");
|
|
72
|
+
}
|
|
73
|
+
export function loopDecisionForDagRun(run) {
|
|
74
|
+
const lifecycle = optionalStringField(run, "lifecycle");
|
|
75
|
+
const status = optionalStringField(run, "status");
|
|
76
|
+
return lifecycle === "paused" || status === "paused" ? "pause" : "continue";
|
|
77
|
+
}
|
|
78
|
+
export function loopResultForDagRun(run) {
|
|
79
|
+
const lifecycle = optionalStringField(run, "lifecycle");
|
|
80
|
+
const status = optionalStringField(run, "status");
|
|
81
|
+
if (lifecycle === "paused" || status === "paused") {
|
|
82
|
+
return "dag run paused";
|
|
83
|
+
}
|
|
84
|
+
if (status === "finished")
|
|
85
|
+
return "dag run finished";
|
|
86
|
+
if (status === "partial_failed" || status === "failed") {
|
|
87
|
+
return `dag run ${status}`;
|
|
88
|
+
}
|
|
89
|
+
return `dag run status=${status || "unknown"}`;
|
|
90
|
+
}
|
|
91
|
+
export function dagFailureCategory(run) {
|
|
92
|
+
const raw = optionalStringField(run, "failureCategory");
|
|
93
|
+
if (raw)
|
|
94
|
+
return raw;
|
|
95
|
+
const status = optionalStringField(run, "status");
|
|
96
|
+
if (status === "partial_failed" || status === "failed")
|
|
97
|
+
return status;
|
|
98
|
+
return "";
|
|
99
|
+
}
|
|
100
|
+
export function summarizeWorkflowReport(raw) {
|
|
101
|
+
if (!raw?.trim()) {
|
|
102
|
+
return {
|
|
103
|
+
markdown: "# Workflow Report\n\nNo final workflow report output was emitted.\n",
|
|
104
|
+
verification: "workflow produced no final report output",
|
|
105
|
+
result: "workflow run finished without final report output",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const parsed = workflowReportSchema.parse(JSON.parse(raw));
|
|
110
|
+
return {
|
|
111
|
+
markdown: [
|
|
112
|
+
"# Workflow Report",
|
|
113
|
+
"",
|
|
114
|
+
parsed.summary,
|
|
115
|
+
"",
|
|
116
|
+
`- Verified findings: ${parsed.verifiedFindings.length}`,
|
|
117
|
+
`- Uncertain findings: ${parsed.uncertainFindings.length}`,
|
|
118
|
+
`- Refuted findings ref: ${parsed.refutedFindingsRef ?? "none"}`,
|
|
119
|
+
"",
|
|
120
|
+
].join("\n"),
|
|
121
|
+
verification: `${parsed.verifiedFindings.length} verified, ${parsed.uncertainFindings.length} uncertain`,
|
|
122
|
+
result: parsed.summary,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return {
|
|
127
|
+
markdown: `# Workflow Report\n\n${raw.trim()}\n`,
|
|
128
|
+
verification: "workflow final output was not a standard WorkflowReport",
|
|
129
|
+
result: "workflow run produced a non-standard final report",
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export function workflowFailureCategory(nodes) {
|
|
134
|
+
for (const node of Object.values(nodes)) {
|
|
135
|
+
if (node.status === "ERROR")
|
|
136
|
+
return node.failureCategory ?? "workflow-node-error";
|
|
137
|
+
}
|
|
138
|
+
return "";
|
|
139
|
+
}
|
|
140
|
+
export function dagRunIdFromRef(ref) {
|
|
141
|
+
const normalized = ref.split(path.sep).join("/");
|
|
142
|
+
const match = normalized.match(/\.harness\/dag-runs\/paused\/([^/]+)/);
|
|
143
|
+
return match?.[1];
|
|
144
|
+
}
|
|
145
|
+
export function dagRunIdFromApprovalMessage(message) {
|
|
146
|
+
const match = message.match(/\brun[-_ ]?id\s*[:=]\s*([a-zA-Z0-9][a-zA-Z0-9_-]*)/i);
|
|
147
|
+
return match?.[1];
|
|
148
|
+
}
|
|
149
|
+
export function filterLoopOwnedChangedPaths(changedPaths, repoRoot, runDir) {
|
|
150
|
+
if (!runDir)
|
|
151
|
+
return changedPaths;
|
|
152
|
+
const runDirRelative = normalizePattern(relativeRef(repoRoot, runDir));
|
|
153
|
+
return changedPaths.filter((changedPath) => {
|
|
154
|
+
const normalized = normalizePattern(changedPath);
|
|
155
|
+
return normalized !== runDirRelative && !normalized.startsWith(`${runDirRelative}/`);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { executeShellCommand } from "../../../executors/shell-executor.js";
|
|
4
|
+
import { writeLoopShellVerifyRecord } from "../../../infrastructure/harness/loop-action-store.js";
|
|
5
|
+
import { getTaskPaths } from "../../../task/runtime.js";
|
|
6
|
+
import { getLoopPaths } from "../paths.js";
|
|
7
|
+
import { appendLoopRound } from "../rounds.js";
|
|
8
|
+
import { rewriteLoopContext } from "../context.js";
|
|
9
|
+
import { readLoopRounds } from "../rounds.js";
|
|
10
|
+
import { extractCommandsFromObjective, relativeRef, truncateSummary } from "./shared.js";
|
|
11
|
+
export async function resolveLoopShellVerifyCommands(repoRoot, taskId, explicitCommands = []) {
|
|
12
|
+
const commands = explicitCommands
|
|
13
|
+
.map((command) => command.trim())
|
|
14
|
+
.filter(Boolean);
|
|
15
|
+
if (commands.length > 0)
|
|
16
|
+
return commands;
|
|
17
|
+
const objective = await readFile(getLoopPaths(repoRoot, taskId).objectivePath, "utf-8");
|
|
18
|
+
const objectiveCommands = extractCommandsFromObjective(objective);
|
|
19
|
+
if (objectiveCommands.length > 0)
|
|
20
|
+
return objectiveCommands;
|
|
21
|
+
const constraints = await readFile(path.join(getTaskPaths(repoRoot, taskId).sourceDir, "执行约束.md"), "utf-8").catch(() => "");
|
|
22
|
+
const constraintCommands = extractCommandsFromObjective(constraints);
|
|
23
|
+
if (constraintCommands.length > 0)
|
|
24
|
+
return constraintCommands;
|
|
25
|
+
throw new Error("loop shell-verify requires at least one --command or verification commands in objective.md");
|
|
26
|
+
}
|
|
27
|
+
export async function runLoopShellVerification(repoRoot, taskId, options = {}) {
|
|
28
|
+
const commands = await resolveLoopShellVerifyCommands(repoRoot, taskId, options.commands);
|
|
29
|
+
const rounds = await readLoopRounds(repoRoot, taskId);
|
|
30
|
+
const round = rounds.length + 1;
|
|
31
|
+
const cwd = options.cwd ? path.resolve(repoRoot, options.cwd) : repoRoot;
|
|
32
|
+
const timeoutMs = options.timeoutMs ?? 300_000;
|
|
33
|
+
const results = [];
|
|
34
|
+
for (const command of commands) {
|
|
35
|
+
results.push(await executeShellCommand({ command, cwd, timeoutMs }));
|
|
36
|
+
}
|
|
37
|
+
const ok = results.every((result) => result.ok);
|
|
38
|
+
const paths = getLoopPaths(repoRoot, taskId);
|
|
39
|
+
const verificationPath = path.join(paths.verificationDir, `round-${round}.json`);
|
|
40
|
+
const record = {
|
|
41
|
+
schemaVersion: 1,
|
|
42
|
+
taskId,
|
|
43
|
+
round,
|
|
44
|
+
action: "shell-verify",
|
|
45
|
+
ok,
|
|
46
|
+
commandCount: commands.length,
|
|
47
|
+
results: results.map((result) => ({
|
|
48
|
+
command: result.command,
|
|
49
|
+
cwd: relativeRef(repoRoot, result.cwd) || ".",
|
|
50
|
+
durationMs: result.durationMs,
|
|
51
|
+
exitCode: result.exitCode,
|
|
52
|
+
failureCategory: result.failureCategory,
|
|
53
|
+
ok: result.ok,
|
|
54
|
+
timedOut: result.timedOut,
|
|
55
|
+
stdoutSummary: truncateSummary(result.stdout),
|
|
56
|
+
stderrSummary: truncateSummary(result.stderr),
|
|
57
|
+
})),
|
|
58
|
+
recordedAt: new Date().toISOString(),
|
|
59
|
+
};
|
|
60
|
+
await writeLoopShellVerifyRecord(repoRoot, taskId, round, record);
|
|
61
|
+
const failed = results.find((result) => !result.ok);
|
|
62
|
+
const verification = ok
|
|
63
|
+
? `passed: ${commands.join(" && ")}`
|
|
64
|
+
: `failed: ${failed?.command ?? commands[0]} (${failed?.failureCategory ?? "unknown"})`;
|
|
65
|
+
await appendLoopRound(repoRoot, taskId, {
|
|
66
|
+
action: "shell-verify",
|
|
67
|
+
refs: [relativeRef(repoRoot, verificationPath)],
|
|
68
|
+
result: ok ? "verification passed" : "verification failed",
|
|
69
|
+
verification,
|
|
70
|
+
lesson: ok
|
|
71
|
+
? "deterministic shell verification passed"
|
|
72
|
+
: "deterministic shell verification failed; inspect verification ref before another implementation round",
|
|
73
|
+
next: ok
|
|
74
|
+
? "evaluate success criteria coverage"
|
|
75
|
+
: "fix the failing verification command",
|
|
76
|
+
decision: ok ? "continue" : "continue",
|
|
77
|
+
failureCategory: ok ? undefined : failed?.failureCategory,
|
|
78
|
+
completedCriteria: ok ? ["shell verification action executed"] : [],
|
|
79
|
+
});
|
|
80
|
+
await rewriteLoopContext(repoRoot, taskId);
|
|
81
|
+
return record;
|
|
82
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|