@tea-agent/loop-agent 0.26.5-beta.2 → 0.27.1-beta.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 +2 -2
- package/CHANGELOG.md +33 -0
- package/README.md +13 -23
- package/dist/application/dag/args.js +7 -7
- package/dist/application/dag/generate-task-dag.js +2 -2
- package/dist/application/dag/run-dag.js +3 -4
- package/dist/application/dag/validate-dag.js +1 -1
- package/dist/application/task-lifecycle/advance.js +845 -0
- package/dist/application/task-lifecycle/gates.js +80 -0
- package/dist/application/task-lifecycle/index.js +7 -0
- package/dist/application/task-lifecycle/observe.js +395 -0
- package/dist/application/task-lifecycle/plan-transitions.js +224 -0
- package/dist/application/task-lifecycle/recommendations.js +180 -0
- package/dist/application/task-lifecycle/record.js +73 -0
- package/dist/application/task-lifecycle/types.js +1 -0
- package/dist/cli/command-definitions.js +14 -111
- package/dist/cli/help.js +6 -7
- package/dist/cli/program.js +26 -90
- package/dist/cli/update/policy.js +0 -1
- package/dist/commands/dag-final-verification.js +1 -1
- package/dist/commands/dag-init-hybrid.js +1 -1
- package/dist/commands/delegate.js +2 -2
- package/dist/commands/init.js +21 -19
- package/dist/commands/run-dag-progress.js +1 -1
- package/dist/commands/status.js +18 -17
- package/dist/commands/study-init.js +2 -2
- package/dist/commands/task-advance.js +335 -0
- package/dist/commands/task-contract.js +3 -5
- package/dist/commands/task-source-prepare.js +9 -4
- package/dist/commands/task-status.js +133 -0
- package/dist/executors/dag-pi-executor.js +18 -22
- package/dist/executors/shell-executor.js +41 -25
- package/dist/governance/manifest-types.js +2 -2
- package/dist/shared/operator/capabilities.js +1358 -243
- package/dist/task/contract/adopt.js +1 -1
- package/dist/task/contract/apply.js +1 -1
- package/dist/task/contract/import-revision.js +1 -1
- package/dist/task/contract/recover.js +2 -2
- package/dist/task/read-model.js +18 -23
- package/dist/task/runtime.js +1 -1
- package/dist/task/source-prepare/completeness.js +1 -1
- package/dist/task/source-prepare/parse-intent.js +6 -1
- package/dist/task/source-prepare/prepare.js +23 -20
- package/dist/worker/console/operator-actions.js +288 -95
- package/dist/worker/console/recovery-cta.js +4 -4
- package/dist/worker/console/static/assets/{index-CSRIhuzh.js → index-CNO7n6qB.js} +1 -1
- package/dist/worker/console/static/index.html +1 -1
- package/dist/worker/materialize/harness-task-materializer.js +6 -5
- package/dist/worker/run-task/run-task.js +204 -112
- package/dist/worker/runner/run-ready.js +1 -1
- package/dist/workflows/dag/backend-test-markdown-workflow.js +49 -23
- package/dist/workflows/dag/backend-test-pytest-collection.js +57 -9
- package/dist/workflows/dag/frontend-implementation-contract.js +3 -3
- package/dist/workflows/dag/frontend-prewrite-gate.js +18 -1
- package/dist/workflows/dag/frontend-test-l5-report.js +46 -7
- package/dist/workflows/dag/init-hybrid.js +6 -35
- package/dist/workflows/dag/output-protocol.js +23 -0
- package/docs/templates/backend-test-dag.json +2 -2
- package/docs/templates/evaluation/agents-map-slim-v1.md +1 -1
- package/docs/templates/evaluation/agents-map-verbose-v0.md +3 -3
- package/docs/templates/frontend-implementation-contract.schema.json +2 -2
- package/docs/templates/harness.schema.json +2 -2
- package/docs/templates/init-managed-agents.md +13 -16
- package/docs/templates/production-readiness-checklist.md +3 -3
- package/harness.json +1 -1
- package/package.json +1 -1
- package/scripts/kb-bootstrap-init-skeleton.sh +2 -1
- package/scripts/kb-graph-incremental-prepare.mjs +2 -2
- package/skills/loop-agent/SKILL.md +18 -13
- package/skills/loop-agent/references/README.md +1 -1
- package/skills/loop-agent/references/command-reference.md +55 -84
- package/skills/loop-agent/references/harness-policy.md +19 -23
- package/skills/loop-agent/references/hybrid-dag.md +32 -34
- package/skills/loop-agent/references/long-running-loop.md +2 -2
- package/skills/loop-agent/references/one-shot-runs.md +4 -5
- package/skills/loop-agent/references/orchestrator-and-interventions.md +3 -3
- package/skills/loop-agent/references/post-implementation-and-patterns.md +4 -4
- package/skills/loop-agent/references/source-and-plan-practice.md +45 -51
- package/skills/loop-agent/references/task-workflow.md +14 -15
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { advanceTaskLifecycle, } from "../application/task-lifecycle/index.js";
|
|
2
|
+
import { buildOperatorResult, operatorFailed, processExitCodeForOutcome, writeOperatorJson, } from "../shared/operator/index.js";
|
|
3
|
+
const COMMAND = "task advance";
|
|
4
|
+
const USAGE = `usage:
|
|
5
|
+
task advance <task-id> [title]
|
|
6
|
+
[--prd <path>]...
|
|
7
|
+
[--role <role>]...
|
|
8
|
+
[--from-text <string>]
|
|
9
|
+
[--from-draft <TaskContractDraftV1.json>]
|
|
10
|
+
[--task-kind <kind>]
|
|
11
|
+
[--feature-id <id>]
|
|
12
|
+
[--allowed-path <glob>]...
|
|
13
|
+
[--forbidden-path <glob>]...
|
|
14
|
+
[--verify <label:command>]...
|
|
15
|
+
[--verify-timeout <label:timeoutMs>]...
|
|
16
|
+
[--accept-recommendations <digest>]
|
|
17
|
+
[--approve-gate <gate-id:digest>]
|
|
18
|
+
[--reject-gate <gate-id:digest> --reason <text>]
|
|
19
|
+
[--profile auto|minimal|standard|reviewed|supervised]
|
|
20
|
+
[--max-concurrent <n>]
|
|
21
|
+
[--run-id <id>]
|
|
22
|
+
[--events-jsonl <path>]
|
|
23
|
+
[--worker-association <json>]
|
|
24
|
+
[--dag-output <path>]
|
|
25
|
+
[--skip-finalize]
|
|
26
|
+
[--no-strict-models]
|
|
27
|
+
[--dry-run]
|
|
28
|
+
[--json]`;
|
|
29
|
+
function pushList(target, value) {
|
|
30
|
+
const list = target ?? [];
|
|
31
|
+
list.push(value);
|
|
32
|
+
return list;
|
|
33
|
+
}
|
|
34
|
+
function parseVerify(flag) {
|
|
35
|
+
const idx = flag.indexOf(":");
|
|
36
|
+
if (idx <= 0 || idx === flag.length - 1) {
|
|
37
|
+
throw new Error(`invalid --verify ${flag}; expected label:command`);
|
|
38
|
+
}
|
|
39
|
+
return { label: flag.slice(0, idx), command: flag.slice(idx + 1) };
|
|
40
|
+
}
|
|
41
|
+
function parseVerifyTimeout(flag) {
|
|
42
|
+
const idx = flag.indexOf(":");
|
|
43
|
+
if (idx <= 0 || idx === flag.length - 1) {
|
|
44
|
+
throw new Error(`invalid --verify-timeout ${flag}; expected label:timeoutMs`);
|
|
45
|
+
}
|
|
46
|
+
const timeoutMs = Number(flag.slice(idx + 1));
|
|
47
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
48
|
+
throw new Error(`invalid --verify-timeout ${flag}; timeoutMs must be > 0`);
|
|
49
|
+
}
|
|
50
|
+
return { label: flag.slice(0, idx), timeoutMs };
|
|
51
|
+
}
|
|
52
|
+
export function parseTaskAdvanceArgs(args) {
|
|
53
|
+
const options = {};
|
|
54
|
+
const positional = [];
|
|
55
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
56
|
+
const token = args[i];
|
|
57
|
+
const next = () => {
|
|
58
|
+
const value = args[i + 1];
|
|
59
|
+
if (value === undefined || value.startsWith("-")) {
|
|
60
|
+
throw new Error(`missing value for ${token}\n${USAGE}`);
|
|
61
|
+
}
|
|
62
|
+
i += 1;
|
|
63
|
+
return value;
|
|
64
|
+
};
|
|
65
|
+
if (token === "--json") {
|
|
66
|
+
options.json = true;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (token === "--dry-run") {
|
|
70
|
+
options.dryRun = true;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (token === "--prd") {
|
|
74
|
+
options.prdPaths = pushList(options.prdPaths, next());
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (token === "--role") {
|
|
78
|
+
options.roles = pushList(options.roles, next());
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (token === "--from-text") {
|
|
82
|
+
options.fromText = next();
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (token === "--from-draft") {
|
|
86
|
+
options.fromDraft = next();
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (token === "--task-kind") {
|
|
90
|
+
options.taskKind = next();
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (token === "--feature-id") {
|
|
94
|
+
options.featureId = next();
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (token === "--allowed-path") {
|
|
98
|
+
options.allowedPaths = pushList(options.allowedPaths, next());
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (token === "--forbidden-path") {
|
|
102
|
+
options.forbiddenPaths = pushList(options.forbiddenPaths, next());
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (token === "--verify") {
|
|
106
|
+
options.verify = pushList(options.verify, next());
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (token === "--verify-timeout") {
|
|
110
|
+
options.verifyTimeout = pushList(options.verifyTimeout, next());
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (token === "--accept-recommendations") {
|
|
114
|
+
options.acceptRecommendations = next();
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (token === "--approve-gate") {
|
|
118
|
+
options.approveGate = next();
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (token === "--reject-gate") {
|
|
122
|
+
options.rejectGate = next();
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (token === "--reason") {
|
|
126
|
+
options.reason = next();
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (token === "--profile") {
|
|
130
|
+
const value = next();
|
|
131
|
+
if (value !== "auto" &&
|
|
132
|
+
value !== "minimal" &&
|
|
133
|
+
value !== "standard" &&
|
|
134
|
+
value !== "reviewed" &&
|
|
135
|
+
value !== "supervised") {
|
|
136
|
+
throw new Error(`--profile must be auto|minimal|standard|reviewed|supervised\n${USAGE}`);
|
|
137
|
+
}
|
|
138
|
+
options.profile = value;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (token === "--max-concurrent") {
|
|
142
|
+
const value = Number(next());
|
|
143
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
144
|
+
throw new Error(`--max-concurrent must be a positive number\n${USAGE}`);
|
|
145
|
+
}
|
|
146
|
+
options.maxConcurrent = value;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (token === "--run-id") {
|
|
150
|
+
options.runId = next();
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (token === "--events-jsonl") {
|
|
154
|
+
options.eventsJsonlPath = next();
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (token === "--worker-association") {
|
|
158
|
+
options.workerAssociation = next();
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (token === "--dag-output") {
|
|
162
|
+
options.dagOutputPath = next();
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (token === "--skip-finalize") {
|
|
166
|
+
options.skipFinalize = true;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (token === "--no-strict-models") {
|
|
170
|
+
options.strictModels = false;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (token === "--help" || token === "-h") {
|
|
174
|
+
throw new Error(USAGE);
|
|
175
|
+
}
|
|
176
|
+
if (token.startsWith("-")) {
|
|
177
|
+
throw new Error(`unknown flag ${token}\n${USAGE}`);
|
|
178
|
+
}
|
|
179
|
+
positional.push(token);
|
|
180
|
+
}
|
|
181
|
+
const taskId = positional[0];
|
|
182
|
+
if (!taskId)
|
|
183
|
+
throw new Error(USAGE);
|
|
184
|
+
if (positional.length > 1) {
|
|
185
|
+
options.title = positional.slice(1).join(" ");
|
|
186
|
+
}
|
|
187
|
+
return { taskId, options };
|
|
188
|
+
}
|
|
189
|
+
function mapOutcome(result) {
|
|
190
|
+
if (result.blockers.length > 0) {
|
|
191
|
+
const code = result.blockers[0]?.code ?? "";
|
|
192
|
+
if (code === "STALE_GATE" || code.includes("CONFLICT"))
|
|
193
|
+
return "conflict";
|
|
194
|
+
if (code === "INTAKE_REQUIRED" || code === "INVALID_DRAFT")
|
|
195
|
+
return "invalid";
|
|
196
|
+
if (code === "NOT_FOUND" || code === "TASK_NOT_FOUND")
|
|
197
|
+
return "not-found";
|
|
198
|
+
return "blocked";
|
|
199
|
+
}
|
|
200
|
+
if (result.lifecycleState === "awaiting-write-set-approval") {
|
|
201
|
+
// Shared operator outcomes do not include awaiting-approval; use blocked
|
|
202
|
+
// with result.gate so machine clients can branch on lifecycleState/gate.
|
|
203
|
+
return "blocked";
|
|
204
|
+
}
|
|
205
|
+
if (result.lifecycleState === "needs-attention")
|
|
206
|
+
return "needs-reconcile";
|
|
207
|
+
if (result.lifecycleState === "run-failed")
|
|
208
|
+
return "blocked";
|
|
209
|
+
return "succeeded";
|
|
210
|
+
}
|
|
211
|
+
function toUseCaseInput(repoRoot, taskId, options) {
|
|
212
|
+
const timeouts = new Map((options.verifyTimeout ?? []).map((entry) => {
|
|
213
|
+
const parsed = parseVerifyTimeout(entry);
|
|
214
|
+
return [parsed.label, parsed.timeoutMs];
|
|
215
|
+
}));
|
|
216
|
+
const verifyCommands = (options.verify ?? []).map((entry) => {
|
|
217
|
+
const parsed = parseVerify(entry);
|
|
218
|
+
const timeoutMs = timeouts.get(parsed.label);
|
|
219
|
+
return timeoutMs
|
|
220
|
+
? { ...parsed, timeoutMs }
|
|
221
|
+
: { label: parsed.label, command: parsed.command };
|
|
222
|
+
});
|
|
223
|
+
const roles = options.roles ?? [];
|
|
224
|
+
const prdPaths = (options.prdPaths ?? []).map((filePath, index) => ({
|
|
225
|
+
filePath,
|
|
226
|
+
role: roles[index],
|
|
227
|
+
}));
|
|
228
|
+
let workerAssociation;
|
|
229
|
+
if (options.workerAssociation) {
|
|
230
|
+
let parsed;
|
|
231
|
+
try {
|
|
232
|
+
parsed = JSON.parse(options.workerAssociation);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
throw new Error("--worker-association must be valid JSON {schemaVersion:1,workerRunId,featureId,taskId}");
|
|
236
|
+
}
|
|
237
|
+
if (!parsed ||
|
|
238
|
+
typeof parsed !== "object" ||
|
|
239
|
+
parsed.schemaVersion !== 1 ||
|
|
240
|
+
typeof parsed.workerRunId !== "string" ||
|
|
241
|
+
typeof parsed.featureId !== "string" ||
|
|
242
|
+
typeof parsed.taskId !== "string") {
|
|
243
|
+
throw new Error("--worker-association must be JSON {schemaVersion:1,workerRunId,featureId,taskId}");
|
|
244
|
+
}
|
|
245
|
+
workerAssociation = parsed;
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
repoRoot,
|
|
249
|
+
taskId,
|
|
250
|
+
title: options.title,
|
|
251
|
+
prdPaths: prdPaths.length > 0 ? prdPaths : undefined,
|
|
252
|
+
fromText: options.fromText,
|
|
253
|
+
fromDraftPath: options.fromDraft,
|
|
254
|
+
taskKind: options.taskKind,
|
|
255
|
+
featureId: options.featureId,
|
|
256
|
+
allowedPaths: options.allowedPaths,
|
|
257
|
+
forbiddenPaths: options.forbiddenPaths,
|
|
258
|
+
verifyCommands: verifyCommands.length > 0 ? verifyCommands : undefined,
|
|
259
|
+
approveGateToken: options.approveGate,
|
|
260
|
+
rejectGateToken: options.rejectGate,
|
|
261
|
+
rejectReason: options.reason,
|
|
262
|
+
acceptRecommendationsDigest: options.acceptRecommendations,
|
|
263
|
+
dryRun: options.dryRun,
|
|
264
|
+
profile: options.profile,
|
|
265
|
+
profileExplicit: Boolean(options.profile),
|
|
266
|
+
maxConcurrent: options.maxConcurrent,
|
|
267
|
+
runId: options.runId,
|
|
268
|
+
eventsJsonlPath: options.eventsJsonlPath,
|
|
269
|
+
workerAssociation,
|
|
270
|
+
dagOutputPath: options.dagOutputPath,
|
|
271
|
+
skipFinalize: options.skipFinalize,
|
|
272
|
+
strictModels: options.strictModels,
|
|
273
|
+
onProgress: options.json
|
|
274
|
+
? undefined
|
|
275
|
+
: (message) => {
|
|
276
|
+
process.stderr.write(`[task advance] ${message}\n`);
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
export async function runTaskAdvance(repoRoot, args) {
|
|
281
|
+
let taskId;
|
|
282
|
+
let options;
|
|
283
|
+
try {
|
|
284
|
+
({ taskId, options } = parseTaskAdvanceArgs(args));
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
288
|
+
const envelope = operatorFailed({
|
|
289
|
+
command: COMMAND,
|
|
290
|
+
outcome: "invalid",
|
|
291
|
+
code: "INVALID_INPUT",
|
|
292
|
+
message,
|
|
293
|
+
});
|
|
294
|
+
writeOperatorJson(envelope);
|
|
295
|
+
process.exitCode = processExitCodeForOutcome(envelope.outcome);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
const result = await advanceTaskLifecycle(toUseCaseInput(repoRoot, taskId, options));
|
|
300
|
+
const outcome = mapOutcome(result);
|
|
301
|
+
const gateStop = result.lifecycleState === "awaiting-write-set-approval" &&
|
|
302
|
+
result.blockers.length === 0;
|
|
303
|
+
const createOnlyStop = result.lifecycleState === "intake-incomplete" &&
|
|
304
|
+
result.blockers.length === 0 &&
|
|
305
|
+
(result.completedTransitions.includes("task-created") ||
|
|
306
|
+
result.completedTransitions.includes("prd-imported"));
|
|
307
|
+
const ok = outcome === "succeeded" || gateStop || createOnlyStop;
|
|
308
|
+
const envelope = buildOperatorResult({
|
|
309
|
+
command: COMMAND,
|
|
310
|
+
ok,
|
|
311
|
+
// awaiting write-set approval is a successful stop condition for
|
|
312
|
+
// the first advance call; keep outcome blocked for machine branch
|
|
313
|
+
// but ok=true when no blockers.
|
|
314
|
+
outcome: gateStop ? "blocked" : createOnlyStop ? "succeeded" : outcome,
|
|
315
|
+
result,
|
|
316
|
+
warnings: result.warnings,
|
|
317
|
+
});
|
|
318
|
+
writeOperatorJson(envelope);
|
|
319
|
+
process.exitCode =
|
|
320
|
+
envelope.ok && envelope.outcome === "blocked"
|
|
321
|
+
? 0
|
|
322
|
+
: processExitCodeForOutcome(envelope.outcome);
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
326
|
+
const envelope = operatorFailed({
|
|
327
|
+
command: COMMAND,
|
|
328
|
+
outcome: "blocked",
|
|
329
|
+
code: "INTERNAL_ERROR",
|
|
330
|
+
message,
|
|
331
|
+
});
|
|
332
|
+
writeOperatorJson(envelope);
|
|
333
|
+
process.exitCode = processExitCodeForOutcome(envelope.outcome);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { ContractMutationError, adoptTaskContract, applyTaskContract, diffTaskContract, doctorTaskContract, loadDraftFromPath, observeTaskContractState, recoverTaskContract, validateDraftForTask, } from "../task/contract/index.js";
|
|
2
2
|
import { operatorFailed, operatorSucceeded, processExitCodeForOutcome, writeOperatorJson, } from "../shared/operator/index.js";
|
|
3
3
|
const USAGE = `usage:
|
|
4
|
-
task contract show <taskId> [--json]
|
|
5
4
|
task contract validate --task <id> --input <path> [--json]
|
|
6
5
|
task contract diff --task <id> --input <path> [--json]
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
task contract recover --task <id> [--tx-id <id>] [--json]`;
|
|
6
|
+
|
|
7
|
+
Advanced-only public leaves. Managed apply/show/adopt/doctor/recover are internal services
|
|
8
|
+
reached via task advance / task status (or application APIs), not public CLI subcommands.`;
|
|
11
9
|
export function parseTaskContractArgs(args) {
|
|
12
10
|
const options = {};
|
|
13
11
|
const positional = [];
|
|
@@ -205,13 +205,13 @@ function validateOptionCombinations(options) {
|
|
|
205
205
|
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}`);
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
|
-
async function
|
|
209
|
-
if (
|
|
208
|
+
export async function readTaskSourcePrepareStdin(stream = stdinStream) {
|
|
209
|
+
if (stream.isTTY) {
|
|
210
210
|
throw new Error("--from-stdin refused on TTY; pipe input or use --from-file/--from-text");
|
|
211
211
|
}
|
|
212
212
|
const chunks = [];
|
|
213
213
|
let total = 0;
|
|
214
|
-
for await (const chunk of
|
|
214
|
+
for await (const chunk of stream) {
|
|
215
215
|
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
216
216
|
total += buf.byteLength;
|
|
217
217
|
if (total > PREPARE_MAX_FILE_BYTES) {
|
|
@@ -239,7 +239,7 @@ async function loadTextInput(repoRoot, options) {
|
|
|
239
239
|
return { text: raw.toString("utf-8"), label: options.fromFile };
|
|
240
240
|
}
|
|
241
241
|
if (options.fromStdin) {
|
|
242
|
-
const text = await
|
|
242
|
+
const text = await readTaskSourcePrepareStdin();
|
|
243
243
|
return { text, label: "stdin" };
|
|
244
244
|
}
|
|
245
245
|
return {};
|
|
@@ -386,6 +386,11 @@ function printHuman(result) {
|
|
|
386
386
|
console.log(lines.join("\n"));
|
|
387
387
|
}
|
|
388
388
|
export async function runTaskSourcePrepare(repoRoot, args) {
|
|
389
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
390
|
+
console.log(USAGE);
|
|
391
|
+
process.exitCode = 0;
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
389
394
|
let taskId;
|
|
390
395
|
let options = {};
|
|
391
396
|
try {
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { observeTaskLifecycle } from "../application/task-lifecycle/index.js";
|
|
2
|
+
import { buildOperatorResult, operatorFailed, processExitCodeForOutcome, writeOperatorJson, } from "../shared/operator/index.js";
|
|
3
|
+
const COMMAND = "task status";
|
|
4
|
+
const USAGE = `usage:
|
|
5
|
+
task status <task-id> [--json|--markdown] [--details]`;
|
|
6
|
+
export function parseTaskStatusArgs(args) {
|
|
7
|
+
const options = {};
|
|
8
|
+
const positional = [];
|
|
9
|
+
for (const token of args) {
|
|
10
|
+
if (token === "--json") {
|
|
11
|
+
options.json = true;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (token === "--markdown") {
|
|
15
|
+
options.markdown = true;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (token === "--details") {
|
|
19
|
+
options.details = true;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (token === "--help" || token === "-h") {
|
|
23
|
+
throw new Error(USAGE);
|
|
24
|
+
}
|
|
25
|
+
if (token.startsWith("-")) {
|
|
26
|
+
throw new Error(`unknown flag ${token}\n${USAGE}`);
|
|
27
|
+
}
|
|
28
|
+
positional.push(token);
|
|
29
|
+
}
|
|
30
|
+
const taskId = positional[0];
|
|
31
|
+
if (!taskId || positional.length > 1)
|
|
32
|
+
throw new Error(USAGE);
|
|
33
|
+
return { taskId, options };
|
|
34
|
+
}
|
|
35
|
+
function formatMarkdown(snapshot, details) {
|
|
36
|
+
const lines = [
|
|
37
|
+
`# Task ${snapshot.taskId}`,
|
|
38
|
+
"",
|
|
39
|
+
`- lifecycleState: \`${snapshot.lifecycleState}\``,
|
|
40
|
+
`- exists: ${snapshot.exists}`,
|
|
41
|
+
];
|
|
42
|
+
if (snapshot.title)
|
|
43
|
+
lines.push(`- title: ${snapshot.title}`);
|
|
44
|
+
if (snapshot.contract) {
|
|
45
|
+
lines.push(`- contract: ${snapshot.contract.effectiveStatus} rev=${snapshot.contract.revision ?? "n/a"}`);
|
|
46
|
+
}
|
|
47
|
+
if (snapshot.dagDraft) {
|
|
48
|
+
lines.push(`- dagDraft: exists=${snapshot.dagDraft.exists} strictValidated=${snapshot.dagDraft.strictValidated}`);
|
|
49
|
+
if (details && snapshot.dagDraft.writeSet.length > 0) {
|
|
50
|
+
lines.push(`- writeSet: ${snapshot.dagDraft.writeSet.join(", ")}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (snapshot.gate) {
|
|
54
|
+
lines.push(`- gate: ${snapshot.gate.id} digest=${snapshot.gate.digest}`);
|
|
55
|
+
lines.push(`- approve: \`${snapshot.gate.approveCommand}\``);
|
|
56
|
+
}
|
|
57
|
+
if (snapshot.activeRun) {
|
|
58
|
+
lines.push(`- activeRun: ${snapshot.activeRun.runId} (${snapshot.activeRun.lifecycle}/${snapshot.activeRun.status ?? "?"})`);
|
|
59
|
+
}
|
|
60
|
+
if (snapshot.latestRun) {
|
|
61
|
+
lines.push(`- latestRun: ${snapshot.latestRun.runId} (${snapshot.latestRun.lifecycle}/${snapshot.latestRun.status ?? "?"})`);
|
|
62
|
+
}
|
|
63
|
+
if (snapshot.next.kind === "none") {
|
|
64
|
+
lines.push("- next: none");
|
|
65
|
+
}
|
|
66
|
+
else if (snapshot.next.kind === "external-action") {
|
|
67
|
+
lines.push(`- next: ${snapshot.next.description}`);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
lines.push(`- next: \`${snapshot.next.command}\``);
|
|
71
|
+
}
|
|
72
|
+
if (details && snapshot.completedTransitions.length > 0) {
|
|
73
|
+
lines.push("", "## completedTransitions", "");
|
|
74
|
+
for (const transition of snapshot.completedTransitions) {
|
|
75
|
+
lines.push(`- ${transition}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (snapshot.blockers.length > 0) {
|
|
79
|
+
lines.push("", "## blockers", "");
|
|
80
|
+
for (const blocker of snapshot.blockers) {
|
|
81
|
+
lines.push(`- ${blocker.code}: ${blocker.message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return `${lines.join("\n")}\n`;
|
|
85
|
+
}
|
|
86
|
+
export async function runTaskStatus(repoRoot, args) {
|
|
87
|
+
let taskId;
|
|
88
|
+
let options;
|
|
89
|
+
try {
|
|
90
|
+
({ taskId, options } = parseTaskStatusArgs(args));
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
94
|
+
const envelope = operatorFailed({
|
|
95
|
+
command: COMMAND,
|
|
96
|
+
outcome: "invalid",
|
|
97
|
+
code: "INVALID_INPUT",
|
|
98
|
+
message,
|
|
99
|
+
});
|
|
100
|
+
writeOperatorJson(envelope);
|
|
101
|
+
process.exitCode = processExitCodeForOutcome(envelope.outcome);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const snapshot = await observeTaskLifecycle(repoRoot, taskId);
|
|
106
|
+
if (options.markdown && !options.json) {
|
|
107
|
+
process.stdout.write(formatMarkdown(snapshot, Boolean(options.details)));
|
|
108
|
+
process.exitCode = snapshot.exists ? 0 : 2;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const envelope = buildOperatorResult({
|
|
112
|
+
command: COMMAND,
|
|
113
|
+
ok: true,
|
|
114
|
+
outcome: snapshot.exists ? "succeeded" : "not-found",
|
|
115
|
+
result: snapshot,
|
|
116
|
+
});
|
|
117
|
+
// status is read-only; missing task is still a valid observation.
|
|
118
|
+
envelope.ok = true;
|
|
119
|
+
writeOperatorJson(envelope);
|
|
120
|
+
process.exitCode = snapshot.exists ? 0 : 2;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
124
|
+
const envelope = operatorFailed({
|
|
125
|
+
command: COMMAND,
|
|
126
|
+
outcome: "blocked",
|
|
127
|
+
code: "INTERNAL_ERROR",
|
|
128
|
+
message,
|
|
129
|
+
});
|
|
130
|
+
writeOperatorJson(envelope);
|
|
131
|
+
process.exitCode = processExitCodeForOutcome(envelope.outcome);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -576,32 +576,19 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
576
576
|
export function validateWriterImplementationOutcome(text, changedFiles) {
|
|
577
577
|
const parsed = parseWriterImplementationOutcome(text);
|
|
578
578
|
const diagnostics = writerOutcomeDiagnostics(text, parsed, changedFiles.length);
|
|
579
|
-
|
|
579
|
+
// The workspace diff is authoritative. The model outcome is advisory and
|
|
580
|
+
// may be missing, malformed, or inconsistent without blocking a completed
|
|
581
|
+
// writer. An explicit blocked signal remains a hard failure.
|
|
582
|
+
if (parsed.candidates.some((candidate) => candidate.outcome === "blocked")) {
|
|
580
583
|
return {
|
|
581
584
|
ok: false,
|
|
582
|
-
reason: `writer outcome validation failed:
|
|
585
|
+
reason: `writer outcome validation failed: explicit blocked outcome; ${diagnostics}`,
|
|
583
586
|
};
|
|
584
587
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
reason: `writer outcome validation failed: IMPLEMENTATION_OUTCOME: blocked cannot complete successfully; ${diagnostics}`,
|
|
590
|
-
};
|
|
591
|
-
}
|
|
592
|
-
if (outcome === "changed" && changedFiles.length === 0) {
|
|
593
|
-
return {
|
|
594
|
-
ok: false,
|
|
595
|
-
reason: `writer outcome validation failed: changed outcome has an empty diff; ${diagnostics}`,
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
|
-
if (outcome === "already-satisfied" && changedFiles.length > 0) {
|
|
599
|
-
return {
|
|
600
|
-
ok: false,
|
|
601
|
-
reason: `writer outcome validation failed: already-satisfied outcome has a non-empty diff; ${diagnostics}`,
|
|
602
|
-
};
|
|
603
|
-
}
|
|
604
|
-
return { ok: true, outcome };
|
|
588
|
+
return {
|
|
589
|
+
ok: true,
|
|
590
|
+
outcome: changedFiles.length > 0 ? "changed" : "already-satisfied",
|
|
591
|
+
};
|
|
605
592
|
}
|
|
606
593
|
function parseWriterImplementationOutcome(text) {
|
|
607
594
|
const lines = text.split(/\r?\n/);
|
|
@@ -732,6 +719,15 @@ function normalizeProtocolLine(line, firstProtocolLine, nextLine) {
|
|
|
732
719
|
const value = normalizedProtocolValue(direct[1] ?? "");
|
|
733
720
|
return value ? `${firstProtocolLine} ${value}` : undefined;
|
|
734
721
|
}
|
|
722
|
+
// Writers sometimes place a short delivery sentence before the protocol
|
|
723
|
+
// line. Accept the protocol token when it appears inline, while preserving
|
|
724
|
+
// the candidate value so template repetitions still fail as unknown or
|
|
725
|
+
// conflicting outcomes.
|
|
726
|
+
const inline = normalizedLine.match(new RegExp(`${labelPattern}\\s*[::]\\s*(.+)$`, "i"));
|
|
727
|
+
if (inline) {
|
|
728
|
+
const value = normalizedProtocolValue(inline[1] ?? "");
|
|
729
|
+
return value ? `${firstProtocolLine} ${value}` : undefined;
|
|
730
|
+
}
|
|
735
731
|
const splitValue = normalizedLine.match(new RegExp(`^${labelPattern}\\s*[::]?\\s*$`, "i"));
|
|
736
732
|
if (splitValue && nextLine !== undefined) {
|
|
737
733
|
const value = normalizedProtocolValue(nextLine);
|
|
@@ -24,8 +24,8 @@ import { formatTraceabilityGateStdout, materializeBackendTestCaseManifest, runBa
|
|
|
24
24
|
import { materializeBackendTestExecutionContract } from "../workflows/dag/backend-test-execution-contract.js";
|
|
25
25
|
import { analyzeBackendTestCaseCoverage, analyzeBackendTestMarkdownPytestCorrespondence, materializeBackendTestCaseManifestFromFacts, } from "../workflows/dag/backend-test-case-coverage-analysis.js";
|
|
26
26
|
import { materializeBackendTestResultFromPytestHtml, materializeBackendTestResultFromRunDir, parsePytestHtmlReport, } from "../workflows/dag/backend-test-result-contract.js";
|
|
27
|
-
import { collectBackendTestHumanCaseCatalog, collectBackendTestMappedPytestScripts, collectJacocoCoverage, hasBlockingBackendMarkdownSafetyFindings, inspectBackendTestEnvironment, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, renderBackendTestL5Dashboard, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
|
|
28
|
-
import { assessBackendPytestCollection, assertBackendPytestCollectionFresh, buildBackendPytestAssetInventory, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
|
|
27
|
+
import { collectBackendTestHumanCaseCatalog, collectBackendTestMappedPytestScripts, resolveBackendTestMappedPytestScripts, collectJacocoCoverage, hasBlockingBackendMarkdownSafetyFindings, inspectBackendTestEnvironment, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, renderBackendTestL5Dashboard, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
|
|
28
|
+
import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assertBackendPytestCollectionFresh, buildBackendPytestAssetInventory, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
|
|
29
29
|
import { computeL5ReportMetrics } from "../workflows/dag/l5-report-metrics.js";
|
|
30
30
|
import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
|
|
31
31
|
import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
|
|
@@ -515,31 +515,45 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
515
515
|
}
|
|
516
516
|
}
|
|
517
517
|
else if (pipeline === "markdown-collection-assess") {
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
518
|
+
const resolution = await resolveBackendTestMappedPytestScripts(input.cwd);
|
|
519
|
+
let facts;
|
|
520
|
+
if (resolution.missingScripts.length > 0) {
|
|
521
|
+
const inventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false });
|
|
522
|
+
facts = assessMissingBackendPytestScripts({
|
|
523
|
+
mappedScripts: resolution.expectedScripts,
|
|
524
|
+
existingMappedScripts: resolution.existingScripts,
|
|
525
|
+
missingMappedScripts: resolution.missingScripts,
|
|
526
|
+
assetFiles: inventory.assetFiles,
|
|
527
|
+
inputHashes: inventory.inputHashes,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
const mappedScripts = resolution.existingScripts;
|
|
532
|
+
const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts);
|
|
533
|
+
const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
534
|
+
const targets = mappedScripts.map(shellQuote).join(" ");
|
|
535
|
+
const command = `PYTHONDONTWRITEBYTECODE=1 python -m pytest --collect-only -q -p no:cacheprovider ${targets}`;
|
|
536
|
+
const [result] = await executePipelineCommands(input, meta, [command]);
|
|
537
|
+
if (!result)
|
|
538
|
+
throw new Error("backend pytest collection command did not produce a result");
|
|
539
|
+
if (["spawn-error", "timeout", "termination-unconfirmed"].includes(result.failureCategory ?? "")) {
|
|
540
|
+
return {
|
|
541
|
+
ok: false,
|
|
542
|
+
stdout: result.stdout,
|
|
543
|
+
stderr: result.stderr || "backend pytest collection could not start",
|
|
544
|
+
failureCategory: result.failureCategory,
|
|
545
|
+
durationMs: Date.now() - started,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
facts = assessBackendPytestCollection({
|
|
549
|
+
phase: "initial",
|
|
550
|
+
mappedScripts,
|
|
551
|
+
inventory,
|
|
552
|
+
exitCode: result.exitCode ?? 2,
|
|
529
553
|
stdout: result.stdout,
|
|
530
|
-
stderr: result.stderr
|
|
531
|
-
|
|
532
|
-
durationMs: Date.now() - started,
|
|
533
|
-
};
|
|
554
|
+
stderr: result.stderr,
|
|
555
|
+
});
|
|
534
556
|
}
|
|
535
|
-
const facts = assessBackendPytestCollection({
|
|
536
|
-
phase: "initial",
|
|
537
|
-
mappedScripts,
|
|
538
|
-
inventory,
|
|
539
|
-
exitCode: result.exitCode ?? 2,
|
|
540
|
-
stdout: result.stdout,
|
|
541
|
-
stderr: result.stderr,
|
|
542
|
-
});
|
|
543
557
|
const artifacts = await writeBackendPytestCollectionArtifacts({
|
|
544
558
|
runDir: meta.runDir,
|
|
545
559
|
stem: "initial",
|
|
@@ -551,8 +565,10 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
551
565
|
stdout: JSON.stringify({
|
|
552
566
|
status: facts.status,
|
|
553
567
|
repairEligible: facts.repairEligible,
|
|
568
|
+
collectionAttempted: facts.collectionAttempted,
|
|
554
569
|
pytestExitCode: facts.pytestExitCode,
|
|
555
570
|
collectedItemCount: facts.collectedItemCount,
|
|
571
|
+
missingMappedScripts: facts.missingMappedScripts,
|
|
556
572
|
factsPath: "contracts/backend-test-pytest-collection-initial.json",
|
|
557
573
|
}),
|
|
558
574
|
stderr: "",
|