@tea-agent/loop-agent 0.25.6 → 0.26.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 +2 -1
- package/CHANGELOG.md +27 -1
- package/README.md +8 -3
- package/dist/cli/command-definitions.js +25 -10
- package/dist/cli/help.js +4 -3
- package/dist/cli/program.js +43 -17
- package/dist/commands/import-prd.js +7 -2
- package/dist/commands/init.js +7 -5
- package/dist/commands/task-source-prepare.js +468 -0
- package/dist/executors/dag-pi-executor.js +66 -25
- package/dist/executors/model-routing.js +34 -18
- package/dist/executors/shell-write-guard.js +161 -25
- package/dist/governance/manifest-types.js +33 -5
- package/dist/task/source-prepare/build-draft.js +215 -0
- package/dist/task/source-prepare/completeness.js +195 -0
- package/dist/task/source-prepare/index.js +7 -0
- package/dist/task/source-prepare/parse-intent.js +373 -0
- package/dist/task/source-prepare/path-policy.js +197 -0
- package/dist/task/source-prepare/prepare.js +506 -0
- package/dist/task/source-prepare/reference-integrity.js +274 -0
- package/dist/task/source-prepare/types.js +7 -0
- package/dist/task/task-demand-routing.js +3 -1
- package/dist/worker/console/chat/model-resolver.js +15 -3
- package/dist/worker/observe/static/constants.js +3 -2
- package/dist/worker/observe/static/dag-model.js +1 -0
- package/dist/worker/observe/static/styles.css +182 -42
- package/dist/workflows/dag/lifecycle.js +40 -30
- package/dist/workflows/dag/node-execution.js +13 -0
- package/dist/workflows/dag/types.js +59 -19
- package/docs/templates/harness.schema.json +29 -7
- package/docs/templates/init-managed-agents.md +10 -5
- package/harness.json +1 -2
- package/package.json +1 -1
- package/skills/loop-agent/SKILL.md +5 -2
- package/skills/loop-agent/references/command-reference.md +17 -15
- package/skills/loop-agent/references/harness-policy.md +3 -4
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/skills/loop-agent/references/model-routing.md +2 -0
- package/skills/loop-agent/references/post-implementation-and-patterns.md +1 -1
- package/skills/loop-agent/references/source-and-plan-practice.md +3 -2
- package/skills/loop-agent/references/task-workflow.md +7 -5
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { stdin as stdinStream } from "node:process";
|
|
4
|
+
import { parseAcceptanceFlag, parseVerifyFlag, parseVerifyTimeoutFlag, prepareTaskSource, PREPARE_MAX_FILE_BYTES, } from "../task/source-prepare/index.js";
|
|
5
|
+
import { parseTaskContractDraft } from "../task/contract/schema.js";
|
|
6
|
+
import { buildOperatorResult, operatorFailed, processExitCodeForOutcome, writeOperatorJson, } from "../shared/operator/index.js";
|
|
7
|
+
const COMMAND = "task source prepare";
|
|
8
|
+
const USAGE = `usage:
|
|
9
|
+
task source prepare <task-id>
|
|
10
|
+
[--from-text <string>]
|
|
11
|
+
[--from-file <path>]
|
|
12
|
+
[--from-stdin]
|
|
13
|
+
[--from-draft <path-to-TaskContractDraftV1.json>]
|
|
14
|
+
[--use-imported-prd]
|
|
15
|
+
[--title <string>]
|
|
16
|
+
[--objective <text>]
|
|
17
|
+
[--scope <text>]...
|
|
18
|
+
[--task-kind <TaskKind>]
|
|
19
|
+
[--feature-id <string>]
|
|
20
|
+
[--allowed-path <glob>]...
|
|
21
|
+
[--forbidden-path <glob>]...
|
|
22
|
+
[--no-default-forbidden-paths]
|
|
23
|
+
[--invariant <text>]...
|
|
24
|
+
[--ac <id:text> | --acceptance <id:text>]...
|
|
25
|
+
[--non-goal <text>]...
|
|
26
|
+
[--assumption <text>]...
|
|
27
|
+
[--open-question <text>]...
|
|
28
|
+
[--verify <label:command>]...
|
|
29
|
+
[--verify-timeout <label:timeoutMs>]...
|
|
30
|
+
[--request-id <id>]
|
|
31
|
+
[--dry-run | --apply]
|
|
32
|
+
[--force-overwrite-source]
|
|
33
|
+
[--json]`;
|
|
34
|
+
function pushList(target, value) {
|
|
35
|
+
const list = target ?? [];
|
|
36
|
+
list.push(value);
|
|
37
|
+
return list;
|
|
38
|
+
}
|
|
39
|
+
export function parseTaskSourcePrepareArgs(args) {
|
|
40
|
+
const options = {};
|
|
41
|
+
const positional = [];
|
|
42
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
43
|
+
const token = args[i];
|
|
44
|
+
const next = () => {
|
|
45
|
+
const value = args[i + 1];
|
|
46
|
+
if (value === undefined || value.startsWith("-")) {
|
|
47
|
+
throw new Error(`missing value for ${token}\n${USAGE}`);
|
|
48
|
+
}
|
|
49
|
+
i += 1;
|
|
50
|
+
return value;
|
|
51
|
+
};
|
|
52
|
+
if (token === "--json") {
|
|
53
|
+
options.json = true;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (token === "--dry-run") {
|
|
57
|
+
options.dryRun = true;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (token === "--apply") {
|
|
61
|
+
options.apply = true;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (token === "--force-overwrite-source") {
|
|
65
|
+
options.forceOverwriteSource = true;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (token === "--use-imported-prd") {
|
|
69
|
+
options.useImportedPrd = true;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (token === "--from-stdin") {
|
|
73
|
+
options.fromStdin = true;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (token === "--no-default-forbidden-paths") {
|
|
77
|
+
options.noDefaultForbiddenPaths = true;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (token === "--from-text") {
|
|
81
|
+
options.fromText = next();
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (token === "--from-file") {
|
|
85
|
+
options.fromFile = next();
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (token === "--from-draft") {
|
|
89
|
+
options.fromDraft = next();
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (token === "--title") {
|
|
93
|
+
options.title = next();
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (token === "--objective") {
|
|
97
|
+
options.objective = next();
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (token === "--scope") {
|
|
101
|
+
options.scope = pushList(options.scope, next());
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (token === "--task-kind") {
|
|
105
|
+
options.taskKind = next();
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (token === "--feature-id") {
|
|
109
|
+
options.featureId = next();
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (token === "--allowed-path") {
|
|
113
|
+
options.allowedPaths = pushList(options.allowedPaths, next());
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (token === "--forbidden-path") {
|
|
117
|
+
options.forbiddenPaths = pushList(options.forbiddenPaths, next());
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (token === "--invariant") {
|
|
121
|
+
options.invariants = pushList(options.invariants, next());
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (token === "--ac" || token === "--acceptance") {
|
|
125
|
+
const parsed = parseAcceptanceFlag(next());
|
|
126
|
+
options.acceptanceCriteria = [
|
|
127
|
+
...(options.acceptanceCriteria ?? []),
|
|
128
|
+
parsed,
|
|
129
|
+
];
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (token === "--non-goal") {
|
|
133
|
+
options.nonGoals = pushList(options.nonGoals, next());
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (token === "--assumption") {
|
|
137
|
+
options.assumptions = pushList(options.assumptions, next());
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (token === "--open-question") {
|
|
141
|
+
options.openQuestions = pushList(options.openQuestions, next());
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (token === "--verify") {
|
|
145
|
+
options.verify = pushList(options.verify, next());
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (token === "--verify-timeout") {
|
|
149
|
+
options.verifyTimeout = pushList(options.verifyTimeout, next());
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (token === "--request-id") {
|
|
153
|
+
options.requestId = next();
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (token.startsWith("-")) {
|
|
157
|
+
throw new Error(`unknown flag ${token}\n${USAGE}`);
|
|
158
|
+
}
|
|
159
|
+
positional.push(token);
|
|
160
|
+
}
|
|
161
|
+
const taskId = positional[0];
|
|
162
|
+
if (!taskId) {
|
|
163
|
+
throw new Error(USAGE);
|
|
164
|
+
}
|
|
165
|
+
if (positional.length > 1) {
|
|
166
|
+
throw new Error(`unexpected arguments: ${positional.slice(1).join(" ")}\n${USAGE}`);
|
|
167
|
+
}
|
|
168
|
+
return { taskId, options };
|
|
169
|
+
}
|
|
170
|
+
function validateOptionCombinations(options) {
|
|
171
|
+
if (options.dryRun && options.apply) {
|
|
172
|
+
throw new Error(`--dry-run and --apply are mutually exclusive\n${USAGE}`);
|
|
173
|
+
}
|
|
174
|
+
const textSources = [
|
|
175
|
+
options.fromText !== undefined,
|
|
176
|
+
options.fromFile !== undefined,
|
|
177
|
+
Boolean(options.fromStdin),
|
|
178
|
+
].filter(Boolean).length;
|
|
179
|
+
if (textSources > 1) {
|
|
180
|
+
throw new Error(`--from-text / --from-file / --from-stdin are mutually exclusive\n${USAGE}`);
|
|
181
|
+
}
|
|
182
|
+
if (options.fromDraft) {
|
|
183
|
+
if (options.useImportedPrd ||
|
|
184
|
+
options.fromText !== undefined ||
|
|
185
|
+
options.fromFile !== undefined ||
|
|
186
|
+
options.fromStdin) {
|
|
187
|
+
throw new Error(`--from-draft is mutually exclusive with --use-imported-prd / --from-text / --from-file / --from-stdin\n${USAGE}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const hasIntentSource = Boolean(options.fromDraft) ||
|
|
191
|
+
Boolean(options.useImportedPrd) ||
|
|
192
|
+
options.fromText !== undefined ||
|
|
193
|
+
options.fromFile !== undefined ||
|
|
194
|
+
Boolean(options.fromStdin) ||
|
|
195
|
+
Boolean(options.title) ||
|
|
196
|
+
Boolean(options.objective) ||
|
|
197
|
+
Boolean(options.acceptanceCriteria?.length) ||
|
|
198
|
+
Boolean(options.allowedPaths?.length);
|
|
199
|
+
if (!hasIntentSource) {
|
|
200
|
+
throw new Error(`need at least one intent source: --use-imported-prd, --from-text/--from-file/--from-stdin, --from-draft, or sufficient flags (title/objective + --ac + --allowed-path)\n${USAGE}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function readStdinText() {
|
|
204
|
+
if (stdinStream.isTTY) {
|
|
205
|
+
throw new Error("--from-stdin refused on TTY; pipe input or use --from-file/--from-text");
|
|
206
|
+
}
|
|
207
|
+
const chunks = [];
|
|
208
|
+
let total = 0;
|
|
209
|
+
for await (const chunk of stdinStream) {
|
|
210
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
211
|
+
total += buf.byteLength;
|
|
212
|
+
if (total > PREPARE_MAX_FILE_BYTES) {
|
|
213
|
+
throw new Error(`stdin input exceeds ${PREPARE_MAX_FILE_BYTES} bytes (INPUT_TOO_LARGE)`);
|
|
214
|
+
}
|
|
215
|
+
chunks.push(buf);
|
|
216
|
+
}
|
|
217
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
218
|
+
}
|
|
219
|
+
async function loadTextInput(repoRoot, options) {
|
|
220
|
+
if (options.fromText !== undefined) {
|
|
221
|
+
if (Buffer.byteLength(options.fromText, "utf-8") > PREPARE_MAX_FILE_BYTES) {
|
|
222
|
+
throw new Error(`--from-text exceeds ${PREPARE_MAX_FILE_BYTES} bytes (INPUT_TOO_LARGE)`);
|
|
223
|
+
}
|
|
224
|
+
return { text: options.fromText, label: "from-text" };
|
|
225
|
+
}
|
|
226
|
+
if (options.fromFile) {
|
|
227
|
+
const absolute = path.isAbsolute(options.fromFile)
|
|
228
|
+
? options.fromFile
|
|
229
|
+
: path.resolve(repoRoot, options.fromFile);
|
|
230
|
+
const raw = await readFile(absolute);
|
|
231
|
+
if (raw.byteLength > PREPARE_MAX_FILE_BYTES) {
|
|
232
|
+
throw new Error(`--from-file exceeds ${PREPARE_MAX_FILE_BYTES} bytes (INPUT_TOO_LARGE)`);
|
|
233
|
+
}
|
|
234
|
+
return { text: raw.toString("utf-8"), label: options.fromFile };
|
|
235
|
+
}
|
|
236
|
+
if (options.fromStdin) {
|
|
237
|
+
const text = await readStdinText();
|
|
238
|
+
return { text, label: "stdin" };
|
|
239
|
+
}
|
|
240
|
+
return {};
|
|
241
|
+
}
|
|
242
|
+
async function loadDraftInput(repoRoot, draftPath) {
|
|
243
|
+
const absolute = path.isAbsolute(draftPath)
|
|
244
|
+
? draftPath
|
|
245
|
+
: path.resolve(repoRoot, draftPath);
|
|
246
|
+
const raw = await readFile(absolute);
|
|
247
|
+
if (raw.byteLength > PREPARE_MAX_FILE_BYTES) {
|
|
248
|
+
throw new Error(`--from-draft exceeds ${PREPARE_MAX_FILE_BYTES} bytes (INPUT_TOO_LARGE)`);
|
|
249
|
+
}
|
|
250
|
+
let json;
|
|
251
|
+
try {
|
|
252
|
+
json = JSON.parse(raw.toString("utf-8"));
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
throw new Error(`--from-draft is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
256
|
+
}
|
|
257
|
+
return parseTaskContractDraft(json);
|
|
258
|
+
}
|
|
259
|
+
function buildFlags(options) {
|
|
260
|
+
const flags = {
|
|
261
|
+
title: options.title,
|
|
262
|
+
objective: options.objective,
|
|
263
|
+
scope: options.scope,
|
|
264
|
+
taskKind: options.taskKind,
|
|
265
|
+
featureId: options.featureId,
|
|
266
|
+
allowedPaths: options.allowedPaths,
|
|
267
|
+
forbiddenPaths: options.forbiddenPaths,
|
|
268
|
+
noDefaultForbiddenPaths: options.noDefaultForbiddenPaths,
|
|
269
|
+
invariants: options.invariants,
|
|
270
|
+
acceptanceCriteria: options.acceptanceCriteria,
|
|
271
|
+
nonGoals: options.nonGoals,
|
|
272
|
+
assumptions: options.assumptions,
|
|
273
|
+
openQuestions: options.openQuestions,
|
|
274
|
+
};
|
|
275
|
+
if (options.verify || options.verifyTimeout) {
|
|
276
|
+
const timeouts = new Map();
|
|
277
|
+
for (const raw of options.verifyTimeout ?? []) {
|
|
278
|
+
const parsed = parseVerifyTimeoutFlag(raw);
|
|
279
|
+
timeouts.set(parsed.label, parsed.timeoutMs);
|
|
280
|
+
}
|
|
281
|
+
const commands = (options.verify ?? []).map((raw) => {
|
|
282
|
+
const parsed = parseVerifyFlag(raw);
|
|
283
|
+
const timeoutMs = timeouts.get(parsed.label);
|
|
284
|
+
return {
|
|
285
|
+
label: parsed.label,
|
|
286
|
+
command: parsed.command,
|
|
287
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
288
|
+
};
|
|
289
|
+
});
|
|
290
|
+
// If only timeouts provided without verify, treat as empty explicit list? No — only set when --verify appears.
|
|
291
|
+
if (options.verify) {
|
|
292
|
+
flags.verifyCommands = commands;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return flags;
|
|
296
|
+
}
|
|
297
|
+
function toOperatorEnvelope(result) {
|
|
298
|
+
const payload = {
|
|
299
|
+
taskId: result.taskId,
|
|
300
|
+
mode: result.mode,
|
|
301
|
+
taskKind: result.taskKind,
|
|
302
|
+
title: result.title,
|
|
303
|
+
draft: result.draft,
|
|
304
|
+
gaps: result.gaps,
|
|
305
|
+
risks: result.risks,
|
|
306
|
+
projected: result.projected,
|
|
307
|
+
apply: result.apply ?? null,
|
|
308
|
+
next: result.next,
|
|
309
|
+
contractStatus: result.contractStatus,
|
|
310
|
+
};
|
|
311
|
+
if (result.ok) {
|
|
312
|
+
return buildOperatorResult({
|
|
313
|
+
command: COMMAND,
|
|
314
|
+
ok: true,
|
|
315
|
+
outcome: result.outcome,
|
|
316
|
+
result: payload,
|
|
317
|
+
warnings: result.warnings ?? [],
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
return buildOperatorResult({
|
|
321
|
+
command: COMMAND,
|
|
322
|
+
ok: false,
|
|
323
|
+
outcome: result.outcome,
|
|
324
|
+
result: payload,
|
|
325
|
+
warnings: result.warnings ?? [],
|
|
326
|
+
error: result.error ?? {
|
|
327
|
+
code: "INVALID_INPUT",
|
|
328
|
+
message: "task source prepare failed",
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
function printHuman(result) {
|
|
333
|
+
const lines = [
|
|
334
|
+
`task source prepare: ${result.taskId}`,
|
|
335
|
+
` mode: ${result.mode}`,
|
|
336
|
+
` outcome: ${result.outcome}`,
|
|
337
|
+
` ok: ${result.ok}`,
|
|
338
|
+
];
|
|
339
|
+
if (result.title)
|
|
340
|
+
lines.push(` title: ${result.title}`);
|
|
341
|
+
if (result.taskKind)
|
|
342
|
+
lines.push(` taskKind: ${result.taskKind}`);
|
|
343
|
+
if (result.contractStatus) {
|
|
344
|
+
lines.push(` contractStatus: ${result.contractStatus}`);
|
|
345
|
+
}
|
|
346
|
+
if (result.gaps.length > 0) {
|
|
347
|
+
lines.push(" gaps:");
|
|
348
|
+
for (const gap of result.gaps) {
|
|
349
|
+
lines.push(` - [${gap.level}] ${gap.code}: ${gap.message}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (result.risks.length > 0) {
|
|
353
|
+
lines.push(" risks:");
|
|
354
|
+
for (const risk of result.risks) {
|
|
355
|
+
lines.push(` - [${risk.level}] ${risk.code}: ${risk.message}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (result.projected) {
|
|
359
|
+
lines.push(` projected: ${result.projected.requirementPath}, ${result.projected.constraintsPath}`);
|
|
360
|
+
if (result.draft) {
|
|
361
|
+
lines.push(` allowedPaths: ${result.draft.constraints.allowedPaths.join(", ") || "(none)"}`);
|
|
362
|
+
lines.push(` forbiddenPaths: ${result.draft.constraints.forbiddenPaths.join(", ") || "(none)"}`);
|
|
363
|
+
lines.push(` verifyCommands: ${result.draft.verification.commands
|
|
364
|
+
.map((c) => c.label)
|
|
365
|
+
.join(", ") || "(none)"}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (result.apply) {
|
|
369
|
+
lines.push(` apply: revision=${result.apply.ref.revision} txId=${result.apply.txId}`);
|
|
370
|
+
}
|
|
371
|
+
if (result.error) {
|
|
372
|
+
lines.push(` error: ${result.error.code}: ${result.error.message}`);
|
|
373
|
+
}
|
|
374
|
+
if (result.next.length > 0) {
|
|
375
|
+
lines.push(" next:");
|
|
376
|
+
for (const step of result.next) {
|
|
377
|
+
lines.push(` - ${step}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
console.log(lines.join("\n"));
|
|
381
|
+
}
|
|
382
|
+
export async function runTaskSourcePrepare(repoRoot, args) {
|
|
383
|
+
let taskId;
|
|
384
|
+
let options = {};
|
|
385
|
+
try {
|
|
386
|
+
({ taskId, options } = parseTaskSourcePrepareArgs(args));
|
|
387
|
+
validateOptionCombinations(options);
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
391
|
+
const envelope = operatorFailed({
|
|
392
|
+
command: COMMAND,
|
|
393
|
+
outcome: "invalid",
|
|
394
|
+
code: "INVALID_INPUT",
|
|
395
|
+
message,
|
|
396
|
+
});
|
|
397
|
+
if (options.json || args.includes("--json")) {
|
|
398
|
+
writeOperatorJson(envelope);
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
console.error(message);
|
|
402
|
+
}
|
|
403
|
+
process.exitCode = processExitCodeForOutcome(envelope.outcome);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
const flags = buildFlags(options);
|
|
408
|
+
const mode = options.apply ? "apply" : "dry-run";
|
|
409
|
+
let prepareInput;
|
|
410
|
+
if (options.fromDraft) {
|
|
411
|
+
const draft = await loadDraftInput(repoRoot, options.fromDraft);
|
|
412
|
+
prepareInput = {
|
|
413
|
+
repoRoot,
|
|
414
|
+
taskId,
|
|
415
|
+
mode,
|
|
416
|
+
intent: { kind: "draft", draft, sourcePath: options.fromDraft },
|
|
417
|
+
flags,
|
|
418
|
+
requestId: options.requestId,
|
|
419
|
+
forceOverwriteSource: options.forceOverwriteSource,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
const textInput = await loadTextInput(repoRoot, options);
|
|
424
|
+
prepareInput = {
|
|
425
|
+
repoRoot,
|
|
426
|
+
taskId,
|
|
427
|
+
mode,
|
|
428
|
+
intent: {
|
|
429
|
+
kind: "facts",
|
|
430
|
+
useImportedPrd: Boolean(options.useImportedPrd),
|
|
431
|
+
text: textInput.text,
|
|
432
|
+
textLabel: textInput.label,
|
|
433
|
+
},
|
|
434
|
+
flags,
|
|
435
|
+
requestId: options.requestId,
|
|
436
|
+
forceOverwriteSource: options.forceOverwriteSource,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
const result = await prepareTaskSource(prepareInput);
|
|
440
|
+
const envelope = toOperatorEnvelope(result);
|
|
441
|
+
if (options.json) {
|
|
442
|
+
writeOperatorJson(envelope);
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
printHuman(result);
|
|
446
|
+
}
|
|
447
|
+
process.exitCode = processExitCodeForOutcome(result.outcome);
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
451
|
+
const code = message.includes("INPUT_TOO_LARGE")
|
|
452
|
+
? "INPUT_TOO_LARGE"
|
|
453
|
+
: "INVALID_INPUT";
|
|
454
|
+
const envelope = operatorFailed({
|
|
455
|
+
command: COMMAND,
|
|
456
|
+
outcome: "invalid",
|
|
457
|
+
code,
|
|
458
|
+
message,
|
|
459
|
+
});
|
|
460
|
+
if (options.json) {
|
|
461
|
+
writeOperatorJson(envelope);
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
console.error(message);
|
|
465
|
+
}
|
|
466
|
+
process.exitCode = processExitCodeForOutcome(envelope.outcome);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { writeDagNodeJsonArtifact, writeTextArtifactFile, } from "../infrastructure/harness/artifact-store.js";
|
|
4
4
|
import { executePiStep, } from "./pi-executor.js";
|
|
5
5
|
import { redactPromptForLog, truncateOutput, } from "../shared/output-truncation.js";
|
|
6
|
-
import { pathsChangedDuringRun, readGitStatusPorcelain, snapshotGitStatusPathFingerprints, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
|
|
6
|
+
import { GitStatusUnavailableError, pathsChangedDuringRun, readGitStatusPorcelain, snapshotGitStatusPathFingerprints, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
|
|
7
7
|
import { redactSecrets, truncateUtf8Preview } from "../shared/preview.js";
|
|
8
8
|
export const DAG_PI_READONLY_TOOLS = ["read", "grep", "find", "ls"];
|
|
9
9
|
export const DAG_PI_WRITE_TOOLS = [
|
|
@@ -169,14 +169,25 @@ export function buildDagPiUserMessage(task, persona, step) {
|
|
|
169
169
|
"Do not wrap the output in code fences and do not add conversational preamble.",
|
|
170
170
|
].join(" ");
|
|
171
171
|
}
|
|
172
|
-
function resolveDagPiModelConfig(
|
|
173
|
-
const
|
|
172
|
+
export function resolveDagPiModelConfig(modelReference, options) {
|
|
173
|
+
const separatorIndex = modelReference.indexOf("/");
|
|
174
|
+
const qualified = separatorIndex >= 0;
|
|
175
|
+
const provider = qualified
|
|
176
|
+
? modelReference.slice(0, separatorIndex)
|
|
177
|
+
: (DAG_PI_MODEL_PROVIDERS[modelReference] ?? DEFAULT_DAG_PI_PROVIDER);
|
|
178
|
+
const model = qualified
|
|
179
|
+
? modelReference.slice(separatorIndex + 1)
|
|
180
|
+
: modelReference;
|
|
181
|
+
if (!provider || !model) {
|
|
182
|
+
throw new Error(`invalid DAG Pi model reference "${modelReference}": expected non-empty provider/model`);
|
|
183
|
+
}
|
|
184
|
+
const explicitThinking = options?.thinking?.trim();
|
|
185
|
+
const thinking = explicitThinking ??
|
|
186
|
+
(provider === "wizard-local" && model === "gpt-5.5" ? "low" : undefined);
|
|
174
187
|
return {
|
|
175
188
|
provider,
|
|
176
189
|
model,
|
|
177
|
-
...(
|
|
178
|
-
? { thinking: "low" }
|
|
179
|
-
: {}),
|
|
190
|
+
...(thinking ? { thinking } : {}),
|
|
180
191
|
};
|
|
181
192
|
}
|
|
182
193
|
const SUMMARY_STDOUT_MAX = 4_000;
|
|
@@ -246,7 +257,10 @@ export async function writePiExecutorArtifacts(artifactsDir, input) {
|
|
|
246
257
|
await writeTextArtifactFile(summaryPath, buildPiResultSummaryMarkdown(input));
|
|
247
258
|
return { promptPath, summaryPath };
|
|
248
259
|
}
|
|
249
|
-
|
|
260
|
+
const DEFAULT_DAG_PI_WRITE_GUARD_DEPENDENCIES = {
|
|
261
|
+
readGitStatusPorcelain,
|
|
262
|
+
};
|
|
263
|
+
export async function executeDagPiNode(input, meta, piStepFn = executePiStep, writeGuardDependencies = DEFAULT_DAG_PI_WRITE_GUARD_DEPENDENCIES) {
|
|
250
264
|
const started = Date.now();
|
|
251
265
|
const persona = resolveDagPiPersona(input.task);
|
|
252
266
|
const step = resolveDagPiStepName(input.task);
|
|
@@ -265,10 +279,19 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep) {
|
|
|
265
279
|
let beforePathFingerprints;
|
|
266
280
|
if (isWriteTask) {
|
|
267
281
|
try {
|
|
268
|
-
beforeStatus = await readGitStatusPorcelain(input.cwd);
|
|
269
|
-
|
|
282
|
+
beforeStatus = await writeGuardDependencies.readGitStatusPorcelain(input.cwd, { phase: "pi-writer-before" });
|
|
283
|
+
const beforeSnapshot = snapshotGitStatusPorcelain(beforeStatus);
|
|
284
|
+
beforePathFingerprints = await snapshotGitStatusPathFingerprints(input.cwd, beforeSnapshot);
|
|
285
|
+
await persistWriterGitBaseline({
|
|
286
|
+
runDir: meta.runDir,
|
|
287
|
+
nodeId: input.task.id,
|
|
288
|
+
beforeStatus,
|
|
289
|
+
beforeSnapshot,
|
|
290
|
+
beforePathFingerprints,
|
|
291
|
+
});
|
|
270
292
|
}
|
|
271
293
|
catch (error) {
|
|
294
|
+
await persistGitWriteGuardDiagnostics(meta.runDir, input.task.id, error);
|
|
272
295
|
return {
|
|
273
296
|
ok: false,
|
|
274
297
|
stdout: "",
|
|
@@ -291,7 +314,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep) {
|
|
|
291
314
|
: undefined;
|
|
292
315
|
const result = await piStepFn({
|
|
293
316
|
attachedFiles: [],
|
|
294
|
-
modelConfig: resolveDagPiModelConfig(input.model),
|
|
317
|
+
modelConfig: resolveDagPiModelConfig(input.model, input.thinking ? { thinking: input.thinking } : undefined),
|
|
295
318
|
prompt: input.prompt,
|
|
296
319
|
repoRoot: input.cwd,
|
|
297
320
|
step,
|
|
@@ -304,8 +327,8 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep) {
|
|
|
304
327
|
abortGraceMs: input.abortGraceMs,
|
|
305
328
|
onActivity: bridgeActivity
|
|
306
329
|
? (activity) => {
|
|
307
|
-
if (activity.kind === "lease"
|
|
308
|
-
|
|
330
|
+
if (activity.kind === "lease" ||
|
|
331
|
+
activity.kind === "synthetic-heartbeat") {
|
|
309
332
|
return;
|
|
310
333
|
}
|
|
311
334
|
bridgeActivity(activity.kind, activity.at);
|
|
@@ -338,7 +361,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep) {
|
|
|
338
361
|
let changeManifestChangedFiles;
|
|
339
362
|
if (beforeStatus !== undefined) {
|
|
340
363
|
try {
|
|
341
|
-
const afterStatus = await readGitStatusPorcelain(input.cwd);
|
|
364
|
+
const afterStatus = await writeGuardDependencies.readGitStatusPorcelain(input.cwd, { phase: "pi-writer-after" });
|
|
342
365
|
const afterSnapshot = snapshotGitStatusPorcelain(afterStatus);
|
|
343
366
|
const afterPathFingerprints = await snapshotGitStatusPathFingerprints(input.cwd, afterSnapshot);
|
|
344
367
|
const changedFiles = pathsChangedDuringRun(snapshotGitStatusPorcelain(beforeStatus), afterSnapshot, beforePathFingerprints, afterPathFingerprints);
|
|
@@ -353,6 +376,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep) {
|
|
|
353
376
|
writeGuardViolations = guard.violations;
|
|
354
377
|
}
|
|
355
378
|
catch (error) {
|
|
379
|
+
await persistGitWriteGuardDiagnostics(meta.runDir, input.task.id, error);
|
|
356
380
|
writeGuardOk = false;
|
|
357
381
|
writeGuardViolations = [
|
|
358
382
|
`git status unavailable: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -372,7 +396,9 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep) {
|
|
|
372
396
|
}
|
|
373
397
|
}
|
|
374
398
|
}
|
|
375
|
-
if (writeGuardOk &&
|
|
399
|
+
if (writeGuardOk &&
|
|
400
|
+
beforeStatus !== undefined &&
|
|
401
|
+
changeManifestChangedFiles !== undefined) {
|
|
376
402
|
await persistWriterChangeManifest({
|
|
377
403
|
runDir: meta.runDir,
|
|
378
404
|
nodeId: input.task.id,
|
|
@@ -445,12 +471,8 @@ function parseWriterImplementationOutcome(text) {
|
|
|
445
471
|
const normalized = normalizeProtocolLine(line, WRITER_OUTCOME_PROTOCOL_LINE, nextLine);
|
|
446
472
|
if (normalized === undefined)
|
|
447
473
|
continue;
|
|
448
|
-
const value = normalized
|
|
449
|
-
|
|
450
|
-
.trim();
|
|
451
|
-
const outcome = isWriterImplementationOutcome(value)
|
|
452
|
-
? value
|
|
453
|
-
: undefined;
|
|
474
|
+
const value = normalized.slice(WRITER_OUTCOME_PROTOCOL_LINE.length).trim();
|
|
475
|
+
const outcome = isWriterImplementationOutcome(value) ? value : undefined;
|
|
454
476
|
candidates.push({
|
|
455
477
|
lineIndex,
|
|
456
478
|
value,
|
|
@@ -474,9 +496,7 @@ function parseWriterImplementationOutcome(text) {
|
|
|
474
496
|
};
|
|
475
497
|
}
|
|
476
498
|
function isWriterImplementationOutcome(value) {
|
|
477
|
-
return (value === "changed" ||
|
|
478
|
-
value === "already-satisfied" ||
|
|
479
|
-
value === "blocked");
|
|
499
|
+
return (value === "changed" || value === "already-satisfied" || value === "blocked");
|
|
480
500
|
}
|
|
481
501
|
function writerOutcomeDiagnostics(text, parsed, changedFiles) {
|
|
482
502
|
const firstNonEmpty = text
|
|
@@ -543,8 +563,7 @@ function canonicalizeProtocolFirstLine(assistantText, firstProtocolLine) {
|
|
|
543
563
|
if (protocolNextLineIndex === protocolIndex + 1) {
|
|
544
564
|
after.shift();
|
|
545
565
|
}
|
|
546
|
-
while (before.at(-1)?.trim() === "" &&
|
|
547
|
-
after.at(0)?.trim() === "") {
|
|
566
|
+
while (before.at(-1)?.trim() === "" && after.at(0)?.trim() === "") {
|
|
548
567
|
after.shift();
|
|
549
568
|
}
|
|
550
569
|
const bodyLines = [...before, ...after];
|
|
@@ -598,6 +617,28 @@ function stripMarkdownLineDecorations(line) {
|
|
|
598
617
|
function escapeRegExp(value) {
|
|
599
618
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
600
619
|
}
|
|
620
|
+
async function persistWriterGitBaseline(input) {
|
|
621
|
+
const fingerprintEntries = [...input.beforePathFingerprints.entries()].sort(([left], [right]) => left.localeCompare(right));
|
|
622
|
+
const baseline = {
|
|
623
|
+
schemaVersion: 1,
|
|
624
|
+
nodeId: input.nodeId,
|
|
625
|
+
phase: "pi-writer-before",
|
|
626
|
+
capturedAt: new Date().toISOString(),
|
|
627
|
+
statusSha256: createHash("sha256")
|
|
628
|
+
.update(input.beforeStatus)
|
|
629
|
+
.digest("hex"),
|
|
630
|
+
dirtyPaths: [...input.beforeSnapshot.keys()].sort(),
|
|
631
|
+
pathFingerprintsSha256: createHash("sha256")
|
|
632
|
+
.update(JSON.stringify(fingerprintEntries))
|
|
633
|
+
.digest("hex"),
|
|
634
|
+
};
|
|
635
|
+
await writeDagNodeJsonArtifact(input.runDir, input.nodeId, "write-guard-baseline.json", baseline);
|
|
636
|
+
}
|
|
637
|
+
async function persistGitWriteGuardDiagnostics(runDir, nodeId, error) {
|
|
638
|
+
if (!(error instanceof GitStatusUnavailableError))
|
|
639
|
+
return;
|
|
640
|
+
await writeDagNodeJsonArtifact(runDir, nodeId, "git-write-guard-diagnostics.json", error.diagnostics);
|
|
641
|
+
}
|
|
601
642
|
/**
|
|
602
643
|
* Validate the writer's observed diff against its declared write boundary.
|
|
603
644
|
* Inlined mirror of runPostRunWriteGuard that reuses the already-computed diff
|