@tea-agent/loop-agent 0.26.5-beta.2 → 0.27.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 +29 -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/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/frontend-implementation-contract.js +6 -72
- package/dist/workflows/dag/frontend-prewrite-gate.js +4 -17
- package/dist/workflows/dag/init-hybrid.js +11 -66
- 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/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 +4 -4
- 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 +31 -33
- 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
|
@@ -3,7 +3,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { writeTextAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
5
5
|
import { getTaskPaths } from "../../task/runtime.js";
|
|
6
|
-
import { formatDuration, noopProgressReporter } from "../progress-reporter.js";
|
|
6
|
+
import { formatDuration, noopProgressReporter, } from "../progress-reporter.js";
|
|
7
7
|
import { redactForPreview, truncatePreview } from "../observability/events.js";
|
|
8
8
|
import { controllerIdentityExpectationFailure, resolveControllerIdentity, } from "../loop-agent/loop-agent-client.js";
|
|
9
9
|
import { materializeTaskSpec, } from "../materialize/harness-task-materializer.js";
|
|
@@ -33,12 +33,19 @@ export async function runTaskSpec(options) {
|
|
|
33
33
|
const preflightStep = "preflight";
|
|
34
34
|
const preflightStartedAt = Date.now();
|
|
35
35
|
await runObservedStep(eventCtx, preflightStep, async () => {
|
|
36
|
-
progress.step("preflight: version + inspect + docs-audit + git-status" +
|
|
36
|
+
progress.step("preflight: version + inspect + docs-audit + git-status" +
|
|
37
|
+
(options.preflight &&
|
|
38
|
+
typeof options.preflight === "object" &&
|
|
39
|
+
options.preflight.runCheckRepo
|
|
40
|
+
? " + check-repo"
|
|
41
|
+
: ""));
|
|
37
42
|
const preflight = await preflightTargetRepo({
|
|
38
43
|
repoRoot: options.repoRoot,
|
|
39
44
|
client,
|
|
40
45
|
...(typeof options.preflight === "object" ? options.preflight : {}),
|
|
41
|
-
...(options.controllerExpectation
|
|
46
|
+
...(options.controllerExpectation
|
|
47
|
+
? { expectation: options.controllerExpectation }
|
|
48
|
+
: {}),
|
|
42
49
|
});
|
|
43
50
|
if (!preflight.ok) {
|
|
44
51
|
progress.note(`preflight FAILED: ${preflight.code}`);
|
|
@@ -65,81 +72,95 @@ export async function runTaskSpec(options) {
|
|
|
65
72
|
await mkdir(taskArtifactsDir, { recursive: true });
|
|
66
73
|
const dagPath = path.join(taskArtifactsDir, `${workerRunId}-dag.json`);
|
|
67
74
|
const runRecordPath = path.join(taskArtifactsDir, "worker-run-record.json");
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
const canonicalDraftPath = getTaskPaths(options.repoRoot, materializeManifest.harnessTaskId).dagDraftPath;
|
|
76
|
+
// Standard lifecycle: task advance to writeSet gate (generate + strict validate).
|
|
77
|
+
const prepareStep = "task-advance-prepare";
|
|
78
|
+
const prepareStartedAt = Date.now();
|
|
79
|
+
let gateToken = await runObservedStep(eventCtx, prepareStep, async () => {
|
|
80
|
+
progress.step("task advance: generate/validate DAG and open writeSet gate");
|
|
81
|
+
const result = await runRequiredCommand(options.repoRoot, client, "task-advance-prepare", [
|
|
82
|
+
"task",
|
|
83
|
+
"advance",
|
|
75
84
|
materializeManifest.harnessTaskId,
|
|
76
85
|
"--profile",
|
|
77
86
|
materializeManifest.loopAgentProfile,
|
|
78
|
-
"--
|
|
79
|
-
"--output",
|
|
87
|
+
"--dag-output",
|
|
80
88
|
dagPath,
|
|
81
|
-
"--
|
|
82
|
-
options.repoRoot,
|
|
89
|
+
"--json",
|
|
83
90
|
], true, undefined, eventCtx);
|
|
91
|
+
return extractWriteSetGateToken(result, materializeManifest.harnessTaskId);
|
|
84
92
|
});
|
|
85
|
-
progress.step(`
|
|
93
|
+
progress.step(`task advance prepare finished in ${formatDuration(Date.now() - prepareStartedAt)}`);
|
|
86
94
|
if (options.piModel) {
|
|
87
95
|
const piModelStep = "pi-model-override";
|
|
88
96
|
await runObservedStep(eventCtx, piModelStep, async () => {
|
|
89
97
|
progress.step(`pi-model override: rewriting executorModels.pi → ${options.piModel}`);
|
|
98
|
+
// Rewrite both per-run copy and canonical draft so re-validate sees override.
|
|
99
|
+
await applyPiModelOverride(canonicalDraftPath, options.piModel);
|
|
90
100
|
await applyPiModelOverride(dagPath, options.piModel);
|
|
91
101
|
});
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
});
|
|
108
|
-
progress.step(`dag validated in ${formatDuration(Date.now() - validateStartedAt)}`);
|
|
109
|
-
const canonicalDraftPath = getTaskPaths(options.repoRoot, materializeManifest.harnessTaskId).dagDraftPath;
|
|
110
|
-
await runObservedStep(eventCtx, "publish-canonical-dag-draft", async () => {
|
|
111
|
-
const dagBytes = await readFile(dagPath, "utf-8");
|
|
112
|
-
await writeTextAtomic(canonicalDraftPath, dagBytes, {
|
|
113
|
-
repoRoot: options.repoRoot,
|
|
102
|
+
// Re-open gate after draft rewrite (digest bound to DAG hash).
|
|
103
|
+
gateToken = await runObservedStep(eventCtx, "task-advance-revalidate", async () => {
|
|
104
|
+
progress.step("task advance: re-validate after piModel override (no strict-models)");
|
|
105
|
+
const result = await runRequiredCommand(options.repoRoot, client, "task-advance-revalidate", [
|
|
106
|
+
"task",
|
|
107
|
+
"advance",
|
|
108
|
+
materializeManifest.harnessTaskId,
|
|
109
|
+
"--profile",
|
|
110
|
+
materializeManifest.loopAgentProfile,
|
|
111
|
+
"--no-strict-models",
|
|
112
|
+
"--dag-output",
|
|
113
|
+
dagPath,
|
|
114
|
+
"--json",
|
|
115
|
+
], true, undefined, eventCtx);
|
|
116
|
+
return extractWriteSetGateToken(result, materializeManifest.harnessTaskId);
|
|
114
117
|
});
|
|
115
|
-
}
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
+
}
|
|
119
|
+
const runAdvanceStep = "task-advance-execute";
|
|
120
|
+
const runAdvanceStartedAt = Date.now();
|
|
118
121
|
const dagEventsPath = path.join(getTaskPoolRoot(options.repoRoot), "observability", "runs", workerRunId, "dag-events.jsonl");
|
|
119
122
|
await mkdir(path.dirname(dagEventsPath), { recursive: true }).catch(() => { });
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
123
|
+
const workerAssociation = JSON.stringify({
|
|
124
|
+
schemaVersion: 1,
|
|
125
|
+
workerRunId,
|
|
126
|
+
featureId: options.taskSpec.feature_id,
|
|
127
|
+
taskId: options.taskSpec.id,
|
|
128
|
+
});
|
|
129
|
+
await runObservedStep(eventCtx, runAdvanceStep, async () => {
|
|
130
|
+
progress.step("task advance: approve writeSet gate and execute DAG (long step)");
|
|
131
|
+
const advanceResult = await runTaskAdvanceExecuteWithEventsFallback({
|
|
132
|
+
repoRoot: options.repoRoot,
|
|
133
|
+
client,
|
|
134
|
+
taskId: materializeManifest.harnessTaskId,
|
|
135
|
+
gateToken,
|
|
129
136
|
workerRunId,
|
|
130
|
-
|
|
131
|
-
JSON.stringify({
|
|
132
|
-
schemaVersion: 1,
|
|
133
|
-
workerRunId,
|
|
134
|
-
featureId: options.taskSpec.feature_id,
|
|
135
|
-
taskId: options.taskSpec.id,
|
|
136
|
-
}),
|
|
137
|
-
"--events-jsonl",
|
|
137
|
+
workerAssociation,
|
|
138
138
|
dagEventsPath,
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
maxConcurrent: options.taskSpec.loop_agent.max_concurrent,
|
|
140
|
+
timeoutMs: resolveRunDagTimeoutMs(options.taskSpec),
|
|
141
|
+
eventCtx,
|
|
142
|
+
});
|
|
143
|
+
// Terminal DAG failure is still a completed execute step for Worker: report
|
|
144
|
+
// decision + failure artifacts remain authoritative downstream.
|
|
145
|
+
if (!advanceResult.ok && !isTerminalTaskAdvanceFailure(advanceResult)) {
|
|
146
|
+
assertAdvanceExecuteOk(advanceResult, [
|
|
147
|
+
"task",
|
|
148
|
+
"advance",
|
|
149
|
+
materializeManifest.harnessTaskId,
|
|
150
|
+
"--approve-gate",
|
|
151
|
+
gateToken,
|
|
152
|
+
]);
|
|
153
|
+
}
|
|
154
|
+
return advanceResult;
|
|
155
|
+
});
|
|
156
|
+
progress.step(`task advance execute finished in ${formatDuration(Date.now() - runAdvanceStartedAt)}`);
|
|
157
|
+
// Keep per-run DAG artifact aligned with canonical draft after execution prep.
|
|
158
|
+
await runObservedStep(eventCtx, "publish-per-run-dag-copy", async () => {
|
|
159
|
+
const dagBytes = await readFile(canonicalDraftPath, "utf-8");
|
|
160
|
+
await writeTextAtomic(dagPath, dagBytes, {
|
|
161
|
+
repoRoot: options.repoRoot,
|
|
162
|
+
});
|
|
141
163
|
});
|
|
142
|
-
progress.step(`run-dag finished in ${formatDuration(Date.now() - runDagStartedAt)}`);
|
|
143
164
|
const reportStep = "report-decision";
|
|
144
165
|
let capturedReportJson;
|
|
145
166
|
const reportDecision = await runObservedStep(eventCtx, reportStep, async () => {
|
|
@@ -171,7 +192,9 @@ export async function runTaskSpec(options) {
|
|
|
171
192
|
artifactName: "dag-report-markdown",
|
|
172
193
|
});
|
|
173
194
|
return decision;
|
|
174
|
-
}, {
|
|
195
|
+
}, {
|
|
196
|
+
statusForResult: (decision) => decision.succeeded ? "succeeded" : "failed",
|
|
197
|
+
});
|
|
175
198
|
const dagReportRun = extractDagReportRun(workerRunId, capturedReportJson);
|
|
176
199
|
const declaredArtifacts = options.declaredArtifacts
|
|
177
200
|
? options.declaredArtifacts.map((entry) => ({
|
|
@@ -180,7 +203,9 @@ export async function runTaskSpec(options) {
|
|
|
180
203
|
...(entry.path ? { path: entry.path } : {}),
|
|
181
204
|
}))
|
|
182
205
|
: undefined;
|
|
183
|
-
let status = reportDecision.succeeded
|
|
206
|
+
let status = reportDecision.succeeded
|
|
207
|
+
? "succeeded"
|
|
208
|
+
: "failed";
|
|
184
209
|
progress.step(`report decision: ${status} (${reportDecision.reason}${reportDecision.runStatus ? `, status=${reportDecision.runStatus}` : ""})`);
|
|
185
210
|
const failureArtifacts = reportDecision.succeeded
|
|
186
211
|
? undefined
|
|
@@ -237,7 +262,10 @@ export async function runTaskSpec(options) {
|
|
|
237
262
|
});
|
|
238
263
|
if (!projection.ok) {
|
|
239
264
|
status = "failed";
|
|
240
|
-
outcomeFailure = {
|
|
265
|
+
outcomeFailure = {
|
|
266
|
+
category: projection.category,
|
|
267
|
+
reason: projection.reason,
|
|
268
|
+
};
|
|
241
269
|
progress.step(`outcome projection failed: ${projection.category} (${projection.reason})`);
|
|
242
270
|
}
|
|
243
271
|
else {
|
|
@@ -271,13 +299,10 @@ export async function runTaskSpec(options) {
|
|
|
271
299
|
outcome = await writeOutcome(options.repoRoot, envelope);
|
|
272
300
|
}
|
|
273
301
|
if (status === "succeeded" && !options.skipSuccessFinalization) {
|
|
274
|
-
await runObservedStep(eventCtx, "
|
|
275
|
-
progress.step("promote
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
await runObservedStep(eventCtx, "closeout-task", async () => {
|
|
279
|
-
progress.step("closeout task");
|
|
280
|
-
await runRequiredCommand(options.repoRoot, client, "closeout-task", ["closeout", "task", materializeManifest.harnessTaskId], true, undefined, eventCtx);
|
|
302
|
+
await runObservedStep(eventCtx, "task-advance-closeout", async () => {
|
|
303
|
+
progress.step("task advance: promote + closeout");
|
|
304
|
+
// Lifecycle closeout path; advance is idempotent when run already associated.
|
|
305
|
+
await runRequiredCommand(options.repoRoot, client, "task-advance-closeout", ["task", "advance", materializeManifest.harnessTaskId, "--json"], true, undefined, eventCtx);
|
|
281
306
|
});
|
|
282
307
|
}
|
|
283
308
|
const record = {
|
|
@@ -351,47 +376,111 @@ async function runRequiredCommand(repoRoot, client, artifactName, args, expectJs
|
|
|
351
376
|
}
|
|
352
377
|
return result;
|
|
353
378
|
}
|
|
379
|
+
function extractWriteSetGateToken(result, taskId) {
|
|
380
|
+
const root = result.json;
|
|
381
|
+
if (!root || typeof root !== "object") {
|
|
382
|
+
throw new Error(`task advance prepare did not return JSON for ${taskId}; cannot approve writeSet gate`);
|
|
383
|
+
}
|
|
384
|
+
const envelope = root;
|
|
385
|
+
const payload = envelope.result && typeof envelope.result === "object"
|
|
386
|
+
? envelope.result
|
|
387
|
+
: envelope;
|
|
388
|
+
const gate = payload.gate && typeof payload.gate === "object"
|
|
389
|
+
? payload.gate
|
|
390
|
+
: undefined;
|
|
391
|
+
const id = typeof gate?.id === "string" ? gate.id : undefined;
|
|
392
|
+
const digest = typeof gate?.digest === "string" ? gate.digest : undefined;
|
|
393
|
+
if (!id || !digest) {
|
|
394
|
+
const lifecycleState = typeof payload.lifecycleState === "string"
|
|
395
|
+
? payload.lifecycleState
|
|
396
|
+
: "unknown";
|
|
397
|
+
throw new Error(`task advance prepare for ${taskId} did not open a writeSet gate (lifecycleState=${lifecycleState})`);
|
|
398
|
+
}
|
|
399
|
+
return `${id}:${digest}`;
|
|
400
|
+
}
|
|
354
401
|
/**
|
|
355
|
-
* `--events-jsonl` is additive observability. Older published controllers
|
|
356
|
-
*
|
|
357
|
-
* retrying the exact command without it is safe and preserves the same run id.
|
|
402
|
+
* `--events-jsonl` is additive observability. Older published controllers may
|
|
403
|
+
* reject the flag; retry without it, keeping the same run id / gate digest.
|
|
358
404
|
*/
|
|
359
|
-
function
|
|
360
|
-
if (!result.ok) {
|
|
405
|
+
function assertAdvanceExecuteOk(result, args) {
|
|
406
|
+
if (!result.ok && !isTerminalTaskAdvanceFailure(result)) {
|
|
361
407
|
throw new Error(`loop-agent command failed: ${args.join(" ")}`);
|
|
362
408
|
}
|
|
363
409
|
}
|
|
364
|
-
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
410
|
+
function readTaskAdvancePayload(result) {
|
|
411
|
+
const root = result.json;
|
|
412
|
+
if (!root || typeof root !== "object")
|
|
413
|
+
return null;
|
|
414
|
+
const envelope = root;
|
|
415
|
+
if (envelope.result && typeof envelope.result === "object") {
|
|
416
|
+
return envelope.result;
|
|
417
|
+
}
|
|
418
|
+
return envelope;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Terminal DAG failure from `task advance --approve-gate` is an expected
|
|
422
|
+
* worker path: CLI may return non-zero / outcome=blocked while still writing
|
|
423
|
+
* a completed run that `dag report` can diagnose.
|
|
424
|
+
*/
|
|
425
|
+
function isTerminalTaskAdvanceFailure(result) {
|
|
426
|
+
const payload = readTaskAdvancePayload(result);
|
|
427
|
+
if (!payload)
|
|
428
|
+
return false;
|
|
429
|
+
const lifecycleState = typeof payload.lifecycleState === "string" ? payload.lifecycleState : "";
|
|
430
|
+
if (lifecycleState === "run-failed" ||
|
|
431
|
+
lifecycleState === "needs-attention" ||
|
|
432
|
+
lifecycleState === "awaiting-decision") {
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
const run = payload.run && typeof payload.run === "object"
|
|
436
|
+
? payload.run
|
|
437
|
+
: null;
|
|
438
|
+
const runStatus = typeof run?.status === "string" ? run.status : "";
|
|
439
|
+
const runLifecycle = typeof run?.lifecycle === "string" ? run.lifecycle : "";
|
|
440
|
+
return (runStatus === "failed" ||
|
|
441
|
+
runStatus === "partial_failed" ||
|
|
442
|
+
(runLifecycle === "completed" && runStatus !== "finished"));
|
|
443
|
+
}
|
|
444
|
+
async function runTaskAdvanceExecuteWithEventsFallback(input) {
|
|
445
|
+
const baseArgs = [
|
|
446
|
+
"task",
|
|
447
|
+
"advance",
|
|
448
|
+
input.taskId,
|
|
449
|
+
"--approve-gate",
|
|
450
|
+
input.gateToken,
|
|
451
|
+
"--run-id",
|
|
452
|
+
input.workerRunId,
|
|
453
|
+
"--worker-association",
|
|
454
|
+
input.workerAssociation,
|
|
455
|
+
"--max-concurrent",
|
|
456
|
+
String(input.maxConcurrent),
|
|
457
|
+
"--skip-finalize",
|
|
458
|
+
"--json",
|
|
459
|
+
];
|
|
460
|
+
const withEvents = [...baseArgs, "--events-jsonl", input.dagEventsPath];
|
|
461
|
+
const result = await input.client.run(withEvents, {
|
|
462
|
+
cwd: input.repoRoot,
|
|
463
|
+
artifactName: "task-advance-execute",
|
|
368
464
|
expectJson: true,
|
|
369
|
-
timeoutMs,
|
|
465
|
+
timeoutMs: input.timeoutMs,
|
|
370
466
|
});
|
|
371
|
-
if (result.ok)
|
|
467
|
+
if (result.ok || isTerminalTaskAdvanceFailure(result))
|
|
372
468
|
return result;
|
|
373
|
-
if (!rejectsEventsJsonl(result)
|
|
374
|
-
|
|
469
|
+
if (!rejectsEventsJsonl(result)) {
|
|
470
|
+
assertAdvanceExecuteOk(result, withEvents);
|
|
375
471
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
artifactName: "run-dag-events-fallback",
|
|
472
|
+
input.eventCtx.progress.note("task advance controller does not support --events-jsonl; retrying without DAG event stream");
|
|
473
|
+
const fallback = await input.client.run(baseArgs, {
|
|
474
|
+
cwd: input.repoRoot,
|
|
475
|
+
artifactName: "task-advance-execute-events-fallback",
|
|
381
476
|
expectJson: true,
|
|
382
|
-
timeoutMs,
|
|
477
|
+
timeoutMs: input.timeoutMs,
|
|
383
478
|
});
|
|
384
|
-
|
|
479
|
+
assertAdvanceExecuteOk(fallback, baseArgs);
|
|
385
480
|
return fallback;
|
|
386
481
|
}
|
|
387
482
|
function rejectsEventsJsonl(result) {
|
|
388
|
-
return /unknown run-dag flag:\s*--events-jsonl/i.test(`${result.stderr}\n${result.stdout}`);
|
|
389
|
-
}
|
|
390
|
-
function withoutFlagAndValue(args, flag) {
|
|
391
|
-
const index = args.indexOf(flag);
|
|
392
|
-
if (index < 0)
|
|
393
|
-
return args;
|
|
394
|
-
return [...args.slice(0, index), ...args.slice(index + 2)];
|
|
483
|
+
return /unknown (?:run-dag|dag execute|task advance) flag:\s*--events-jsonl|unknown flag --events-jsonl/i.test(`${result.stderr}\n${result.stdout}`);
|
|
395
484
|
}
|
|
396
485
|
function emit(progress, input) {
|
|
397
486
|
const structured = progress;
|
|
@@ -479,7 +568,10 @@ async function collectFailureArtifacts(input) {
|
|
|
479
568
|
}
|
|
480
569
|
function isPathInside(root, target) {
|
|
481
570
|
const relative = path.relative(root, target);
|
|
482
|
-
return relative !== "" &&
|
|
571
|
+
return (relative !== "" &&
|
|
572
|
+
relative !== ".." &&
|
|
573
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
574
|
+
!path.isAbsolute(relative));
|
|
483
575
|
}
|
|
484
576
|
/** Exported for unit tests: map DAG report JSON into Worker report decision. */
|
|
485
577
|
export function decideFromReport(workerRunId, result) {
|
|
@@ -553,7 +645,9 @@ function extractDagReportRun(workerRunId, result) {
|
|
|
553
645
|
...(readString(node, "nodeId") || readString(node, "id")
|
|
554
646
|
? { id: readString(node, "nodeId") ?? readString(node, "id") }
|
|
555
647
|
: {}),
|
|
556
|
-
...(readString(node, "status")
|
|
648
|
+
...(readString(node, "status")
|
|
649
|
+
? { status: readString(node, "status") }
|
|
650
|
+
: {}),
|
|
557
651
|
...(readString(node, "failureCategory")
|
|
558
652
|
? { failureCategory: readString(node, "failureCategory") }
|
|
559
653
|
: {}),
|
|
@@ -567,10 +661,14 @@ function extractDagReportRun(workerRunId, result) {
|
|
|
567
661
|
? { structuredArtifactPath: readString(node, "structuredArtifactPath") }
|
|
568
662
|
: {}),
|
|
569
663
|
...(readString(node, "structuredArtifactSha256")
|
|
570
|
-
? {
|
|
664
|
+
? {
|
|
665
|
+
structuredArtifactSha256: readString(node, "structuredArtifactSha256"),
|
|
666
|
+
}
|
|
571
667
|
: {}),
|
|
572
668
|
...(readString(node, "structuredArtifactSchemaId")
|
|
573
|
-
? {
|
|
669
|
+
? {
|
|
670
|
+
structuredArtifactSchemaId: readString(node, "structuredArtifactSchemaId"),
|
|
671
|
+
}
|
|
574
672
|
: {}),
|
|
575
673
|
}));
|
|
576
674
|
return {
|
|
@@ -733,19 +831,13 @@ class ObservedRunTaskClient {
|
|
|
733
831
|
function commandRecordName(args, artifactName) {
|
|
734
832
|
if (args[0] === "new-task")
|
|
735
833
|
return "new-task";
|
|
834
|
+
if (args[0] === "task" && args[1] === "advance") {
|
|
835
|
+
return artifactName.startsWith("task-advance-")
|
|
836
|
+
? artifactName
|
|
837
|
+
: "task-advance";
|
|
838
|
+
}
|
|
736
839
|
return artifactName;
|
|
737
840
|
}
|
|
738
|
-
function maxConcurrentArgs(taskSpec) {
|
|
739
|
-
return ["--max-concurrent", String(taskSpec.loop_agent.max_concurrent)];
|
|
740
|
-
}
|
|
741
|
-
/**
|
|
742
|
-
* When a smoke `piModel` override is active, drop `--strict-models` so the
|
|
743
|
-
* non-canonical `executorModels.pi` rewrite passes `dag validate`. Governance,
|
|
744
|
-
* forbidden-executor and spine audits are still enforced by their own flags.
|
|
745
|
-
*/
|
|
746
|
-
function strictModelsArgs(piModel) {
|
|
747
|
-
return piModel ? [] : ["--strict-models"];
|
|
748
|
-
}
|
|
749
841
|
/**
|
|
750
842
|
* Rewrite `executorModels.pi.{LOW,MED,HIGH}` in an already-generated DAG JSON
|
|
751
843
|
* so every pi executor node resolves to the smoke override model.
|
|
@@ -125,7 +125,7 @@ export async function runReadyTasks(options) {
|
|
|
125
125
|
workerRunId,
|
|
126
126
|
status: "running",
|
|
127
127
|
});
|
|
128
|
-
progress.step(`materialize →
|
|
128
|
+
progress.step(`materialize → task advance → report`);
|
|
129
129
|
let result;
|
|
130
130
|
try {
|
|
131
131
|
await writeTaskPoolState(options.repoRoot, {
|
|
@@ -155,7 +155,7 @@ export const frontendImplementationContractSchema = z
|
|
|
155
155
|
"not-needed",
|
|
156
156
|
]),
|
|
157
157
|
productionDefaultOff: z.literal(true),
|
|
158
|
-
activation: z.
|
|
158
|
+
activation: z.string().min(1),
|
|
159
159
|
endpoints: z.array(z
|
|
160
160
|
.object({
|
|
161
161
|
method: z.enum([
|
|
@@ -168,10 +168,7 @@ export const frontendImplementationContractSchema = z
|
|
|
168
168
|
"OPTIONS",
|
|
169
169
|
]),
|
|
170
170
|
path: z.string().startsWith("/"),
|
|
171
|
-
|
|
172
|
-
// intentionally not needed. Treat it like an omitted optional
|
|
173
|
-
// field; active Mock strategies still fail the refinement below.
|
|
174
|
-
fixture: z.preprocess((value) => (value === "" ? undefined : value), safePath.optional()),
|
|
171
|
+
fixture: safePath.optional(),
|
|
175
172
|
consumer: safePath.optional(),
|
|
176
173
|
})
|
|
177
174
|
.strict()),
|
|
@@ -190,7 +187,7 @@ export const frontendImplementationContractSchema = z
|
|
|
190
187
|
type: z.enum(["static", "unit", "component", "integration", "mock"]),
|
|
191
188
|
commandLabel: z.string().min(1),
|
|
192
189
|
file: safePath,
|
|
193
|
-
symbol: z.
|
|
190
|
+
symbol: z.string().min(1).optional(),
|
|
194
191
|
requirementIds: z.array(id),
|
|
195
192
|
uiStates: z.array(z.string().min(1)),
|
|
196
193
|
})
|
|
@@ -346,75 +343,12 @@ function secretIssues(value, at = "$", issues = []) {
|
|
|
346
343
|
}
|
|
347
344
|
export function extractFrontendImplementationJson(text) {
|
|
348
345
|
const trimmed = text.trim();
|
|
349
|
-
const parse = (source) => {
|
|
350
|
-
try {
|
|
351
|
-
return JSON.parse(source);
|
|
352
|
-
}
|
|
353
|
-
catch (error) {
|
|
354
|
-
// Models sometimes put ordinary ASCII quotes inside a JSON string
|
|
355
|
-
// (for example: `reason: "支持..."`). Repair only quotes that are
|
|
356
|
-
// clearly not structural: a closing quote is followed by JSON
|
|
357
|
-
// punctuation, while an embedded quote is followed by content.
|
|
358
|
-
let repaired = "";
|
|
359
|
-
let inString = false;
|
|
360
|
-
let escaped = false;
|
|
361
|
-
for (let index = 0; index < source.length; index += 1) {
|
|
362
|
-
const character = source[index];
|
|
363
|
-
if (character !== '"') {
|
|
364
|
-
repaired += character;
|
|
365
|
-
if (inString && character === "\\" && !escaped)
|
|
366
|
-
escaped = true;
|
|
367
|
-
else
|
|
368
|
-
escaped = false;
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
if (escaped) {
|
|
372
|
-
repaired += character;
|
|
373
|
-
escaped = false;
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
if (!inString) {
|
|
377
|
-
inString = true;
|
|
378
|
-
repaired += character;
|
|
379
|
-
continue;
|
|
380
|
-
}
|
|
381
|
-
const next = source.slice(index + 1).trimStart()[0];
|
|
382
|
-
if ([",", "}", "]", ":"].includes(next ?? "") || source.slice(index + 1).trim() === "") {
|
|
383
|
-
inString = false;
|
|
384
|
-
repaired += character;
|
|
385
|
-
}
|
|
386
|
-
else {
|
|
387
|
-
repaired += "\\\"";
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
try {
|
|
391
|
-
return JSON.parse(repaired);
|
|
392
|
-
}
|
|
393
|
-
catch {
|
|
394
|
-
throw error;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
};
|
|
398
346
|
if (trimmed.startsWith("{") && trimmed.endsWith("}"))
|
|
399
|
-
return parse(trimmed);
|
|
347
|
+
return JSON.parse(trimmed);
|
|
400
348
|
const blocks = [...trimmed.matchAll(/```json\s*\n([\s\S]*?)\n```/gi)];
|
|
401
|
-
if (blocks.length
|
|
349
|
+
if (blocks.length !== 1)
|
|
402
350
|
throw new Error("output must contain exactly one fenced json object");
|
|
403
|
-
|
|
404
|
-
for (const block of blocks) {
|
|
405
|
-
try {
|
|
406
|
-
const candidate = parse(block[1]);
|
|
407
|
-
if (candidate && typeof candidate === "object" && !Array.isArray(candidate))
|
|
408
|
-
candidates.push(candidate);
|
|
409
|
-
}
|
|
410
|
-
catch {
|
|
411
|
-
// Ignore incomplete model scratch blocks. A later complete contract
|
|
412
|
-
// block may still be deterministically recoverable.
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
if (candidates.length !== 1)
|
|
416
|
-
throw new Error(`output must contain exactly one valid fenced json object (found ${candidates.length})`);
|
|
417
|
-
return candidates[0];
|
|
351
|
+
return JSON.parse(blocks[0][1]);
|
|
418
352
|
}
|
|
419
353
|
/**
|
|
420
354
|
* Build the authoritative frontend-implementation-contract sourceBinding from
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { readFile, stat } from "node:fs/promises";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
|
|
5
|
-
import { frontendImplementationContractSchema,
|
|
4
|
+
import { frontendImplementationContractSchema, materializeFrontendImplementationContract, assertFrontendSourceBindingFresh, } from "./frontend-implementation-contract.js";
|
|
6
5
|
import { captureFrontendWorktreeBaseline } from "./frontend-worktree-diff.js";
|
|
7
6
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
8
7
|
async function selectNode(runDir, primary, fallbacks) {
|
|
@@ -47,12 +46,7 @@ function firstNonEmptyVerdictLine(text) {
|
|
|
47
46
|
// The prompt requires VERDICT to be the first non-empty line, but model
|
|
48
47
|
// output can still prepend a summary. Keep the protocol strict in the
|
|
49
48
|
// prompt while making the deterministic gate resilient to that drift.
|
|
50
|
-
|
|
51
|
-
if (/^VERDICT:\s*pass(?:\s*[.!?。!?]|\s+|$)/i.test(verdictLine))
|
|
52
|
-
return "VERDICT: pass";
|
|
53
|
-
if (/^VERDICT:\s*request-revision(?:\s*[.!?。!?]|\s+|$)/i.test(verdictLine))
|
|
54
|
-
return "VERDICT: request-revision";
|
|
55
|
-
return verdictLine;
|
|
49
|
+
return normalizedLines.find((line) => /^VERDICT:/.test(line)) ?? first;
|
|
56
50
|
}
|
|
57
51
|
function eventArgs(event) {
|
|
58
52
|
return event.args ?? event.toolInput ?? event.input ?? {};
|
|
@@ -161,20 +155,13 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
161
155
|
if (missingIds.length > 0) {
|
|
162
156
|
throw new Error(`frontend prewrite gate missing requirement ids: ${missingIds.join(", ")}`);
|
|
163
157
|
}
|
|
164
|
-
const
|
|
165
|
-
const artifact = await stat(artifactPath)
|
|
166
|
-
.then(async () => ({
|
|
167
|
-
path: artifactPath,
|
|
168
|
-
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
169
|
-
sha256: createHash("sha256").update(await readFile(artifactPath)).digest("hex"),
|
|
170
|
-
}))
|
|
171
|
-
.catch(() => materializeFrontendImplementationContract({
|
|
158
|
+
const artifact = await materializeFrontendImplementationContract({
|
|
172
159
|
runDir: input.runDir,
|
|
173
160
|
fromNodeId: planNodeId,
|
|
174
161
|
artifactName: input.config.artifactName,
|
|
175
162
|
outputDir: input.config.outputDir,
|
|
176
163
|
sourceBinding: input.sourceBinding,
|
|
177
|
-
})
|
|
164
|
+
});
|
|
178
165
|
const raw = JSON.parse(await readFile(artifact.path, "utf8"));
|
|
179
166
|
const contract = frontendImplementationContractSchema.parse(raw);
|
|
180
167
|
if (!input.config.allowedMockStrategies.includes(contract.mockApi.strategy)) {
|