@tea-agent/loop-agent 0.24.11-beta.0 → 0.25.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/CHANGELOG.md +28 -5
- package/dist/commands/init.js +16 -2
- package/dist/executors/dag-pi-executor.js +74 -13
- package/dist/executors/pi-executor.js +25 -7
- package/dist/executors/pi-prompt-transport.js +198 -0
- package/dist/executors/pi-sdk-executor.js +1 -1
- package/dist/executors/process-tree.js +33 -0
- package/dist/executors/shell-executor.js +332 -24
- package/dist/executors/shell-verification.js +4 -2
- package/dist/infrastructure/harness/task-store.js +31 -0
- package/dist/shared/operator/capabilities.js +6 -0
- package/dist/verification/maven/cache.js +142 -0
- package/dist/verification/maven/index.js +120 -0
- package/dist/verification/maven/plan-commands.js +421 -0
- package/dist/verification/maven/pom-static.js +136 -0
- package/dist/verification/maven/scope-resolve.js +153 -0
- package/dist/verification/maven/stale.js +130 -0
- package/dist/verification/maven/types.js +23 -0
- package/dist/verification/maven/workspace-graph.js +322 -0
- package/dist/worker/cli.js +236 -21
- package/dist/worker/delivery/final-verification.js +12 -0
- package/dist/worker/delivery/git-transaction.js +32 -7
- package/dist/worker/delivery/package.js +9 -5
- package/dist/worker/delivery/verification-bundle.js +26 -5
- package/dist/worker/feature/advance.js +301 -0
- package/dist/worker/feature/doctor.js +223 -0
- package/dist/worker/feature/next-action.js +11 -3
- package/dist/worker/feature/scaffold.js +798 -0
- package/dist/worker/observe/dag-run-artifacts.js +90 -0
- package/dist/worker/observe/node-input.js +444 -0
- package/dist/worker/observe/routes.js +17 -0
- package/dist/worker/observe/static/api.js +9 -0
- package/dist/worker/observe/static/constants.js +9 -0
- package/dist/worker/observe/static/state.js +14 -0
- package/dist/worker/observe/static/styles.css +74 -0
- package/dist/worker/observe/static/views/dag-inspector.js +371 -15
- package/dist/worker/outcomes/projector.js +5 -1
- package/dist/worker/runner/run-ready.js +41 -1
- package/dist/workflows/dag/backend-test-markdown-workflow.js +202 -9
- package/dist/workflows/dag/backend-test-result-contract.js +103 -0
- package/dist/workflows/dag/failure-category.js +5 -1
- package/dist/workflows/dag/frontend-implementation-contract.js +4 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +0 -17
- package/dist/workflows/dag/frontend-test-result-contract.js +106 -41
- package/dist/workflows/dag/init-hybrid.js +220 -32
- package/dist/workflows/dag/node-execution.js +3 -2
- package/dist/workflows/dag/reconcile-run.js +24 -0
- package/dist/workflows/dag/types.js +46 -0
- package/dist/workflows/dag/validate.js +19 -2
- package/docs/architecture/dag-execution.md +5 -1
- package/docs/architecture/runtime-boundaries.md +11 -1
- package/docs/architecture/worker-and-feature.md +2 -0
- package/docs/templates/backend-test-dag.json +3 -3
- package/docs/templates/product-line/README.md +17 -1
- package/docs/templates/product-line/feature-scaffold-batch.example.yaml +31 -0
- package/docs/templates/product-line/scaffold-samples/README.md +36 -0
- package/docs/templates/product-line/scaffold-samples/backend-only/acceptance.yaml +13 -0
- package/docs/templates/product-line/scaffold-samples/backend-only/design.md +14 -0
- package/docs/templates/product-line/scaffold-samples/backend-only/feature.yaml +3 -0
- package/docs/templates/product-line/scaffold-samples/backend-only/requirement.md +6 -0
- package/docs/templates/product-line/scaffold-samples/backend-only/tasks/BE-IMPL-001.yaml +83 -0
- package/docs/templates/product-line/scaffold-samples/backend-only/tasks/task-graph.yaml +14 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/acceptance.yaml +15 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/design.md +18 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/feature.yaml +3 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/requirement.md +6 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/BE-IMPL-001.yaml +84 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/CONTRACT-001.yaml +84 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/FE-IMPL-001.yaml +86 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/task-graph.yaml +40 -0
- package/docs/templates/product-line/scaffold-samples/frontend-only/acceptance.yaml +13 -0
- package/docs/templates/product-line/scaffold-samples/frontend-only/design.md +14 -0
- package/docs/templates/product-line/scaffold-samples/frontend-only/feature.yaml +3 -0
- package/docs/templates/product-line/scaffold-samples/frontend-only/requirement.md +6 -0
- package/docs/templates/product-line/scaffold-samples/frontend-only/tasks/FE-IMPL-001.yaml +85 -0
- package/docs/templates/product-line/scaffold-samples/frontend-only/tasks/task-graph.yaml +14 -0
- package/examples/l5-report-coms-process-definition.html +322 -0
- package/package.json +1 -1
- package/skills/agent-worker/references/agent-worker-operator.md +84 -0
- package/skills/frontend-implementation/references/node-contracts.md +2 -2
- package/skills/loop-agent/SKILL.md +8 -8
- package/skills/loop-agent/references/command-reference.md +14 -2
- package/skills/loop-agent/references/harness-policy.md +22 -0
- package/skills/loop-agent/references/task-workflow.md +5 -0
|
@@ -101,11 +101,12 @@ export async function finalizeGitTask(transaction, outcome) {
|
|
|
101
101
|
await assertTransactionPosition(transaction.repoRoot, current);
|
|
102
102
|
const currentIgnored = await readIgnoredBaseline(transaction.repoRoot);
|
|
103
103
|
const ignoredBaselineByPath = new Map(current.ignoredBaseline.map((entry) => [entry.path, entry.sha256]));
|
|
104
|
-
const changedBaselineIgnored = currentIgnored
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
104
|
+
const changedBaselineIgnored = currentIgnored
|
|
105
|
+
.filter((entry) => ignoredBaselineByPath.has(entry.path) && ignoredBaselineByPath.get(entry.path) !== entry.sha256)
|
|
106
|
+
.map((entry) => entry.path);
|
|
107
|
+
const missingBaselineIgnored = current.ignoredBaseline
|
|
108
|
+
.filter((entry) => !currentIgnored.some((candidate) => candidate.path === entry.path))
|
|
109
|
+
.map((entry) => entry.path);
|
|
109
110
|
const existing = current.checkpoints.find((entry) => entry.workerRunId === outcome.workerRunId);
|
|
110
111
|
if (outcome.status === "reused" || existing) {
|
|
111
112
|
if (!existing)
|
|
@@ -123,12 +124,28 @@ export async function finalizeGitTask(transaction, outcome) {
|
|
|
123
124
|
const newIgnored = currentIgnored.map((entry) => entry.path).filter((entry) => !ignoredBaselineByPath.has(entry));
|
|
124
125
|
if (outcome.status === "succeeded") {
|
|
125
126
|
const allowedPaths = expandAllowedPathsForWorkflow(resolveWorkflow(outcome.taskSpec).workflow, outcome.taskSpec.constraints.allowed_paths);
|
|
127
|
+
const isAllowedEvidencePath = (entry) => !isSensitivePath(entry) &&
|
|
128
|
+
!isEphemeralToolCachePath(entry) &&
|
|
129
|
+
allowedPaths.some((glob) => matchesGlob(entry, glob));
|
|
130
|
+
// Pre-existing ignored evidence under allowed_paths may be rewritten by
|
|
131
|
+
// FINAL-VERIFY smoke / verify-final and must force-add into the checkpoint.
|
|
132
|
+
// Other baseline ignored mutations remain fail-closed (e.g. dist/, secrets).
|
|
133
|
+
const blockedBaselineChanged = changedBaselineIgnored.filter((entry) => !isAllowedEvidencePath(entry));
|
|
134
|
+
const blockedBaselineMissing = missingBaselineIgnored.filter((entry) => !isEphemeralToolCachePath(entry) && !isAllowedEvidencePath(entry));
|
|
135
|
+
if (blockedBaselineChanged.length > 0 || blockedBaselineMissing.length > 0) {
|
|
136
|
+
throw new Error(`pre-existing ignored file changed during Git transaction: ${[...blockedBaselineChanged, ...blockedBaselineMissing].join(", ")}`);
|
|
137
|
+
}
|
|
126
138
|
// Evidence under allowed_paths may be gitignored for local convenience (e.g.
|
|
127
139
|
// reports/welcome/** smoke JSON) but must still checkpoint for Task Pool Done.
|
|
128
140
|
// Sensitive / out-of-scope ignored files remain fail-closed; ephemeral tool
|
|
129
141
|
// caches neither block nor force-add.
|
|
130
142
|
const forceAddIgnored = [];
|
|
131
143
|
const blockedIgnored = [];
|
|
144
|
+
const rewrittenBaselineEvidence = changedBaselineIgnored.filter((entry) => isAllowedEvidencePath(entry));
|
|
145
|
+
for (const entry of rewrittenBaselineEvidence) {
|
|
146
|
+
if (!forceAddIgnored.includes(entry))
|
|
147
|
+
forceAddIgnored.push(entry);
|
|
148
|
+
}
|
|
132
149
|
for (const entry of newIgnored) {
|
|
133
150
|
if (isEphemeralToolCachePath(entry))
|
|
134
151
|
continue;
|
|
@@ -137,7 +154,8 @@ export async function finalizeGitTask(transaction, outcome) {
|
|
|
137
154
|
continue;
|
|
138
155
|
}
|
|
139
156
|
if (allowedPaths.some((glob) => matchesGlob(entry, glob))) {
|
|
140
|
-
forceAddIgnored.
|
|
157
|
+
if (!forceAddIgnored.includes(entry))
|
|
158
|
+
forceAddIgnored.push(entry);
|
|
141
159
|
}
|
|
142
160
|
else {
|
|
143
161
|
blockedIgnored.push(entry);
|
|
@@ -166,7 +184,10 @@ export async function finalizeGitTask(transaction, outcome) {
|
|
|
166
184
|
await assertTransactionPosition(transaction.repoRoot, { ...current, lastCheckpoint: commit });
|
|
167
185
|
await assertCheckpointMetadata(transaction.repoRoot, commit, outcome.taskSpec, outcome.workerRunId);
|
|
168
186
|
const afterCommitIgnored = await readIgnoredBaseline(transaction.repoRoot);
|
|
169
|
-
|
|
187
|
+
// force-add may promote allowed evidence from ignored → tracked; drop those
|
|
188
|
+
// paths from the post-commit baseline equality check.
|
|
189
|
+
const forceAdded = new Set(forceAddIgnored);
|
|
190
|
+
assertIgnoredBaselineUnchanged(current.ignoredBaseline.filter((entry) => !isEphemeralToolCachePath(entry.path) && !forceAdded.has(entry.path)), afterCommitIgnored.filter((entry) => !isEphemeralToolCachePath(entry.path) && !forceAdded.has(entry.path)));
|
|
170
191
|
}
|
|
171
192
|
catch (error) {
|
|
172
193
|
const branch = await git(transaction.repoRoot, ["branch", "--show-current"]).catch(() => "");
|
|
@@ -199,6 +220,10 @@ export async function finalizeGitTask(transaction, outcome) {
|
|
|
199
220
|
changedFiles: checkpointFiles,
|
|
200
221
|
};
|
|
201
222
|
}
|
|
223
|
+
// Failed / keep-diff paths still fail closed on unexpected baseline ignored mutations.
|
|
224
|
+
if (changedBaselineIgnored.length > 0 || missingBaselineIgnored.length > 0) {
|
|
225
|
+
throw new Error(`pre-existing ignored file changed during Git transaction: ${[...changedBaselineIgnored, ...missingBaselineIgnored].join(", ")}`);
|
|
226
|
+
}
|
|
202
227
|
const artifactDir = path.join(path.dirname(transaction.recordPath), "failures", outcome.workerRunId);
|
|
203
228
|
await captureFailureArtifacts(transaction.repoRoot, artifactDir, changes, newIgnored, outcome);
|
|
204
229
|
if (outcome.keepFailedDiff) {
|
|
@@ -284,11 +284,15 @@ export async function validateDeliveryPackage(input) {
|
|
|
284
284
|
blockers.push(`Acceptance coverage status is not derivable: ${ac.id}`);
|
|
285
285
|
if (JSON.stringify([...item.blockedBy].sort()) !== JSON.stringify([...blockedBy].sort()))
|
|
286
286
|
blockers.push(`Acceptance blockers changed: ${ac.id}`);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
287
|
+
// partial / missing / blocked should-ACs intentionally omit full evidence lists
|
|
288
|
+
// (only must ACs covered by QA require run+QA hash binding).
|
|
289
|
+
if (expectedStatus === "covered") {
|
|
290
|
+
for (const ref of expectedRefs)
|
|
291
|
+
if (!item.evidence.some((candidate) => candidate.path === ref.path && candidate.sha256 === ref.sha256))
|
|
292
|
+
blockers.push(`Acceptance run evidence is missing: ${ac.id}`);
|
|
293
|
+
if (qa?.data.acIds.includes(ac.id) && !item.evidence.some((candidate) => candidate.path === qa.ref.path && candidate.sha256 === qa.ref.sha256))
|
|
294
|
+
blockers.push(`Acceptance QA evidence is missing: ${ac.id}`);
|
|
295
|
+
}
|
|
292
296
|
for (const ref of item.evidence)
|
|
293
297
|
if (!await hashedRefValid(repoRoot, ref))
|
|
294
298
|
blockers.push(`Acceptance evidence changed: ${item.acId}`);
|
|
@@ -404,7 +404,23 @@ export async function verifyBundleOutcomes(repoRoot, bundle) {
|
|
|
404
404
|
return false;
|
|
405
405
|
if (JSON.stringify(verified.artifacts) !== JSON.stringify(outcome.artifacts))
|
|
406
406
|
return false;
|
|
407
|
-
|
|
407
|
+
// Rehash only durable typed evidence. run_record/dag_json may be rewritten
|
|
408
|
+
// after outcome projection (worker-run-record appends report commands).
|
|
409
|
+
const evidenceArtifacts = outcome.artifacts.filter((artifact) => {
|
|
410
|
+
const kind = artifact.kind ?? "";
|
|
411
|
+
if (kind === "run_record" || kind === "dag_json")
|
|
412
|
+
return false;
|
|
413
|
+
if (kind === "backend-test-result" ||
|
|
414
|
+
kind === "frontend-test-result" ||
|
|
415
|
+
kind.includes("backend-test-result") ||
|
|
416
|
+
kind.includes("frontend-test-result")) {
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
// Unknown kinds: still rehash unless clearly infrastructure paths.
|
|
420
|
+
return (!artifact.path.includes("worker-run-record") &&
|
|
421
|
+
!artifact.path.includes("-dag.json"));
|
|
422
|
+
});
|
|
423
|
+
if (!await verifyArtifactRefs(repoRoot, evidenceArtifacts))
|
|
408
424
|
return false;
|
|
409
425
|
}
|
|
410
426
|
return true;
|
|
@@ -417,9 +433,12 @@ export function verifyBundleTaskSpecBindings(bundle, specs) {
|
|
|
417
433
|
return false;
|
|
418
434
|
taskIds.add(outcome.taskId);
|
|
419
435
|
const spec = specs.get(outcome.taskId);
|
|
436
|
+
// Typed verification tasks may be qa-execute (frontend-test) or qa-testcode
|
|
437
|
+
// (backend-test Markdown-first). Delivery binds on feature_id + workflow.
|
|
438
|
+
const typedVerificationTypes = new Set(["qa-execute", "qa-testcode"]);
|
|
420
439
|
if (!spec ||
|
|
421
440
|
spec.feature_id !== bundle.featureId ||
|
|
422
|
-
spec.type
|
|
441
|
+
!typedVerificationTypes.has(spec.type) ||
|
|
423
442
|
spec.execution?.workflow !== outcome.workflow)
|
|
424
443
|
return false;
|
|
425
444
|
}
|
|
@@ -460,9 +479,11 @@ async function verifyArtifactRefs(repoRoot, artifacts) {
|
|
|
460
479
|
return false;
|
|
461
480
|
}
|
|
462
481
|
for (const artifact of artifacts) {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
const lexical = path.
|
|
482
|
+
// Accept absolute paths only when they resolve inside the repo (legacy
|
|
483
|
+
// envelopes may store abs paths). Prefer relative for new projections.
|
|
484
|
+
const lexical = path.isAbsolute(artifact.path)
|
|
485
|
+
? path.resolve(artifact.path)
|
|
486
|
+
: path.resolve(canonicalRepoRoot, artifact.path);
|
|
466
487
|
let resolved;
|
|
467
488
|
let content;
|
|
468
489
|
try {
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { defaultFeatureEvidencePaths, runFeatureFinalVerification, } from "../delivery/final-verification.js";
|
|
3
|
+
import { prepareFeatureDelivery, } from "../delivery/package.js";
|
|
4
|
+
import { applyFeatureCloseout, } from "../closeout/apply.js";
|
|
5
|
+
import { previewFeatureCloseout, } from "../closeout/preview.js";
|
|
6
|
+
import { validateFeatureTaskGraph } from "../task-graph/validate.js";
|
|
7
|
+
import { reviewFeature } from "./review.js";
|
|
8
|
+
const defaultDeps = {
|
|
9
|
+
validateFeature: validateFeatureTaskGraph,
|
|
10
|
+
review: reviewFeature,
|
|
11
|
+
verifyFinal: runFeatureFinalVerification,
|
|
12
|
+
delivery: prepareFeatureDelivery,
|
|
13
|
+
previewCloseout: previewFeatureCloseout,
|
|
14
|
+
applyCloseout: applyFeatureCloseout,
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* ADR 0007 orchestration: verify-final → delivery → closeout preview[/apply].
|
|
18
|
+
* Does not run implementation tasks (use `feature run` for Ready writers).
|
|
19
|
+
*/
|
|
20
|
+
export async function advanceFeature(input, deps = defaultDeps) {
|
|
21
|
+
const featureDir = path.resolve(input.featureDir);
|
|
22
|
+
const repoRoot = path.resolve(input.repoRoot);
|
|
23
|
+
const dryRun = input.dryRun ?? false;
|
|
24
|
+
const apply = Boolean(input.apply) && !dryRun;
|
|
25
|
+
const steps = [];
|
|
26
|
+
const blockers = [];
|
|
27
|
+
const started = Date.now();
|
|
28
|
+
const push = (id, status, message, from = started) => {
|
|
29
|
+
steps.push({
|
|
30
|
+
id,
|
|
31
|
+
status,
|
|
32
|
+
...(message ? { message } : {}),
|
|
33
|
+
durationMs: Math.max(0, Date.now() - from),
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
const validateStarted = Date.now();
|
|
37
|
+
const validation = await deps.validateFeature(featureDir);
|
|
38
|
+
if (!validation.ok) {
|
|
39
|
+
push("validate", "failed", validation.errors.map((e) => e.code).join(", "), validateStarted);
|
|
40
|
+
return {
|
|
41
|
+
schemaVersion: 1,
|
|
42
|
+
featureId: validation.featureId || "unknown",
|
|
43
|
+
mode: dryRun ? "dry-run" : "executed",
|
|
44
|
+
status: "failed",
|
|
45
|
+
finalTaskId: null,
|
|
46
|
+
steps,
|
|
47
|
+
blockers: validation.errors.map((e) => e.message),
|
|
48
|
+
nextCommand: `agent-worker task validate-feature ${JSON.stringify(featureDir)}`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
push("validate", "completed", `featureId=${validation.featureId}`, validateStarted);
|
|
52
|
+
const featureId = validation.featureId;
|
|
53
|
+
const resolveStarted = Date.now();
|
|
54
|
+
let finalTaskId = input.taskId?.trim() || "";
|
|
55
|
+
if (!finalTaskId) {
|
|
56
|
+
const model = await deps.review({ featureDir, repoRoot });
|
|
57
|
+
finalTaskId = resolveFinalVerifyTaskId(model) ?? "";
|
|
58
|
+
}
|
|
59
|
+
if (!finalTaskId) {
|
|
60
|
+
push("resolve-final-task", "failed", "could not resolve FINAL-VERIFY task id", resolveStarted);
|
|
61
|
+
return {
|
|
62
|
+
schemaVersion: 1,
|
|
63
|
+
featureId,
|
|
64
|
+
mode: dryRun ? "dry-run" : "executed",
|
|
65
|
+
status: "failed",
|
|
66
|
+
finalTaskId: null,
|
|
67
|
+
steps,
|
|
68
|
+
blockers: [
|
|
69
|
+
"missing --task-id and no FINAL-VERIFY* task found in Feature review",
|
|
70
|
+
],
|
|
71
|
+
nextCommand: `agent-worker feature review --feature-dir ${JSON.stringify(featureDir)} --repo ${JSON.stringify(repoRoot)}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
push("resolve-final-task", "completed", finalTaskId, resolveStarted);
|
|
75
|
+
if (!input.client) {
|
|
76
|
+
push("verify-final", "failed", "loop-agent client required for verify-final");
|
|
77
|
+
return {
|
|
78
|
+
schemaVersion: 1,
|
|
79
|
+
featureId,
|
|
80
|
+
mode: dryRun ? "dry-run" : "executed",
|
|
81
|
+
status: "failed",
|
|
82
|
+
finalTaskId,
|
|
83
|
+
steps,
|
|
84
|
+
blockers: [
|
|
85
|
+
"feature advance requires --loop-agent-bin / client for verify-final",
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const verifyStarted = Date.now();
|
|
90
|
+
let verifyFinal;
|
|
91
|
+
try {
|
|
92
|
+
verifyFinal = await deps.verifyFinal({
|
|
93
|
+
featureDir,
|
|
94
|
+
repoRoot,
|
|
95
|
+
taskId: finalTaskId,
|
|
96
|
+
client: input.client,
|
|
97
|
+
controllerIdentity: input.controllerIdentity ?? input.client.getIdentity?.(),
|
|
98
|
+
...(input.controllerExpectation
|
|
99
|
+
? { controllerExpectation: input.controllerExpectation }
|
|
100
|
+
: {}),
|
|
101
|
+
evidenceMode: input.evidenceMode ?? "auto",
|
|
102
|
+
...(input.now ? { now: input.now } : {}),
|
|
103
|
+
});
|
|
104
|
+
push("verify-final", "completed", `mode=${verifyFinal.evidenceMode ?? "qa-execute"}`, verifyStarted);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
108
|
+
push("verify-final", "failed", message, verifyStarted);
|
|
109
|
+
return {
|
|
110
|
+
schemaVersion: 1,
|
|
111
|
+
featureId,
|
|
112
|
+
mode: dryRun ? "dry-run" : "executed",
|
|
113
|
+
status: "failed",
|
|
114
|
+
finalTaskId,
|
|
115
|
+
steps,
|
|
116
|
+
blockers: [message],
|
|
117
|
+
nextCommand: `agent-worker feature verify-final --feature-dir ${JSON.stringify(featureDir)} --repo ${JSON.stringify(repoRoot)} --task-id ${JSON.stringify(finalTaskId)} --json`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const defaults = defaultFeatureEvidencePaths(featureId);
|
|
121
|
+
const qaEvidencePath = input.qaEvidencePath ??
|
|
122
|
+
verifyFinal.qaEvidencePath ??
|
|
123
|
+
defaults.qaEvidencePath;
|
|
124
|
+
const finalVerificationPath = input.finalVerificationPath ??
|
|
125
|
+
verifyFinal.finalVerificationPath ??
|
|
126
|
+
defaults.finalVerificationPath;
|
|
127
|
+
const deliveryStarted = Date.now();
|
|
128
|
+
const delivery = await deps.delivery({
|
|
129
|
+
featureDir,
|
|
130
|
+
repoRoot,
|
|
131
|
+
qaEvidencePath,
|
|
132
|
+
finalVerificationPath,
|
|
133
|
+
...(input.waiverPath ? { waiverPath: input.waiverPath } : {}),
|
|
134
|
+
dryRun,
|
|
135
|
+
...(input.now ? { now: input.now } : {}),
|
|
136
|
+
});
|
|
137
|
+
if (delivery.status === "blocked") {
|
|
138
|
+
push("delivery", "failed", delivery.blockers.join("; ") || "blocked", deliveryStarted);
|
|
139
|
+
return {
|
|
140
|
+
schemaVersion: 1,
|
|
141
|
+
featureId,
|
|
142
|
+
mode: dryRun ? "dry-run" : "executed",
|
|
143
|
+
status: "blocked",
|
|
144
|
+
finalTaskId,
|
|
145
|
+
steps,
|
|
146
|
+
verifyFinal,
|
|
147
|
+
delivery,
|
|
148
|
+
blockers: delivery.blockers,
|
|
149
|
+
nextCommand: `agent-worker feature delivery --feature-dir ${JSON.stringify(featureDir)} --repo ${JSON.stringify(repoRoot)} --json`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
push("delivery", "completed", delivery.status, deliveryStarted);
|
|
153
|
+
const previewStarted = Date.now();
|
|
154
|
+
const closeoutPreview = await deps.previewCloseout({
|
|
155
|
+
featureDir,
|
|
156
|
+
repoRoot,
|
|
157
|
+
...(input.now ? { now: input.now } : {}),
|
|
158
|
+
});
|
|
159
|
+
if (closeoutPreview.status !== "ready") {
|
|
160
|
+
const failedGates = closeoutPreview.gates
|
|
161
|
+
.filter((g) => !g.passed)
|
|
162
|
+
.map((g) => `${g.id}: ${g.message}`);
|
|
163
|
+
push("closeout-preview", "failed", failedGates.join("; ") || "blocked", previewStarted);
|
|
164
|
+
blockers.push(...failedGates);
|
|
165
|
+
return {
|
|
166
|
+
schemaVersion: 1,
|
|
167
|
+
featureId,
|
|
168
|
+
mode: dryRun ? "dry-run" : "executed",
|
|
169
|
+
status: "blocked",
|
|
170
|
+
finalTaskId,
|
|
171
|
+
steps,
|
|
172
|
+
verifyFinal,
|
|
173
|
+
delivery,
|
|
174
|
+
closeoutPreview,
|
|
175
|
+
blockers,
|
|
176
|
+
nextCommand: `agent-worker feature closeout --feature-dir ${JSON.stringify(featureDir)} --repo ${JSON.stringify(repoRoot)} --json`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
push("closeout-preview", "completed", "ready", previewStarted);
|
|
180
|
+
if (!apply) {
|
|
181
|
+
push("closeout-apply", "skipped", dryRun
|
|
182
|
+
? "dry-run: closeout apply skipped"
|
|
183
|
+
: "pass --apply --owner to apply closeout");
|
|
184
|
+
const reviewAfter = await deps.review({ featureDir, repoRoot });
|
|
185
|
+
return {
|
|
186
|
+
schemaVersion: 1,
|
|
187
|
+
featureId,
|
|
188
|
+
mode: dryRun ? "dry-run" : "executed",
|
|
189
|
+
status: "ready",
|
|
190
|
+
finalTaskId,
|
|
191
|
+
steps,
|
|
192
|
+
verifyFinal,
|
|
193
|
+
delivery,
|
|
194
|
+
closeoutPreview,
|
|
195
|
+
reviewAfter: {
|
|
196
|
+
status: reviewAfter.status,
|
|
197
|
+
statusLabel: reviewAfter.statusLabel,
|
|
198
|
+
nextAction: reviewAfter.nextAction,
|
|
199
|
+
},
|
|
200
|
+
blockers: [],
|
|
201
|
+
nextCommand: `agent-worker feature advance --feature-dir ${JSON.stringify(featureDir)} --repo ${JSON.stringify(repoRoot)} --task-id ${JSON.stringify(finalTaskId)} --apply --owner <owner>`,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (!input.owner?.trim()) {
|
|
205
|
+
push("closeout-apply", "failed", "owner required for --apply");
|
|
206
|
+
return {
|
|
207
|
+
schemaVersion: 1,
|
|
208
|
+
featureId,
|
|
209
|
+
mode: "executed",
|
|
210
|
+
status: "failed",
|
|
211
|
+
finalTaskId,
|
|
212
|
+
steps,
|
|
213
|
+
verifyFinal,
|
|
214
|
+
delivery,
|
|
215
|
+
closeoutPreview,
|
|
216
|
+
blockers: ["feature advance --apply requires non-empty --owner"],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
const applyStarted = Date.now();
|
|
220
|
+
try {
|
|
221
|
+
const closeoutApply = await deps.applyCloseout({
|
|
222
|
+
featureDir,
|
|
223
|
+
repoRoot,
|
|
224
|
+
owner: input.owner.trim(),
|
|
225
|
+
...(input.now ? { now: input.now } : {}),
|
|
226
|
+
});
|
|
227
|
+
push("closeout-apply", "completed", closeoutApply.status, applyStarted);
|
|
228
|
+
const reviewAfter = await deps.review({ featureDir, repoRoot });
|
|
229
|
+
return {
|
|
230
|
+
schemaVersion: 1,
|
|
231
|
+
featureId,
|
|
232
|
+
mode: "executed",
|
|
233
|
+
status: "applied",
|
|
234
|
+
finalTaskId,
|
|
235
|
+
steps,
|
|
236
|
+
verifyFinal,
|
|
237
|
+
delivery,
|
|
238
|
+
closeoutPreview,
|
|
239
|
+
closeoutApply,
|
|
240
|
+
reviewAfter: {
|
|
241
|
+
status: reviewAfter.status,
|
|
242
|
+
statusLabel: reviewAfter.statusLabel,
|
|
243
|
+
nextAction: reviewAfter.nextAction,
|
|
244
|
+
},
|
|
245
|
+
blockers: [],
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
250
|
+
push("closeout-apply", "failed", message, applyStarted);
|
|
251
|
+
return {
|
|
252
|
+
schemaVersion: 1,
|
|
253
|
+
featureId,
|
|
254
|
+
mode: "executed",
|
|
255
|
+
status: "failed",
|
|
256
|
+
finalTaskId,
|
|
257
|
+
steps,
|
|
258
|
+
verifyFinal,
|
|
259
|
+
delivery,
|
|
260
|
+
closeoutPreview,
|
|
261
|
+
blockers: [message],
|
|
262
|
+
nextCommand: `agent-worker feature closeout --feature-dir ${JSON.stringify(featureDir)} --repo ${JSON.stringify(repoRoot)} --apply --owner ${JSON.stringify(input.owner)} --json`,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
export function resolveFinalVerifyTaskId(model) {
|
|
267
|
+
const hint = /(final[-_]?(verify|verification))|closeout/i;
|
|
268
|
+
const byId = model.tasks.find((t) => hint.test(t.taskId));
|
|
269
|
+
if (byId)
|
|
270
|
+
return byId.taskId;
|
|
271
|
+
// Fallback: last Done-or-any task that depends on both test-ish types is not available here;
|
|
272
|
+
// review model lacks depends_on depth — require explicit task when id heuristic fails.
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
export function renderFeatureAdvance(result) {
|
|
276
|
+
const lines = [
|
|
277
|
+
`Feature: ${result.featureId}`,
|
|
278
|
+
`Advance: ${result.status} (${result.mode})`,
|
|
279
|
+
`Final task: ${result.finalTaskId ?? "none"}`,
|
|
280
|
+
...result.steps.map((s) => `${statusMark(s.status)} ${s.id}${s.message ? `: ${s.message}` : ""}`),
|
|
281
|
+
];
|
|
282
|
+
if (result.blockers.length) {
|
|
283
|
+
lines.push(`Blockers: ${result.blockers.join("; ")}`);
|
|
284
|
+
}
|
|
285
|
+
if (result.nextCommand) {
|
|
286
|
+
lines.push(`Next: ${result.nextCommand}`);
|
|
287
|
+
}
|
|
288
|
+
return `${lines.join("\n")}\n`;
|
|
289
|
+
}
|
|
290
|
+
function statusMark(status) {
|
|
291
|
+
switch (status) {
|
|
292
|
+
case "completed":
|
|
293
|
+
return "✓";
|
|
294
|
+
case "failed":
|
|
295
|
+
return "✗";
|
|
296
|
+
case "skipped":
|
|
297
|
+
return "·";
|
|
298
|
+
default:
|
|
299
|
+
return "○";
|
|
300
|
+
}
|
|
301
|
+
}
|