@tea-agent/loop-agent 0.24.10 → 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 +35 -0
- 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 +239 -47
- 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 +177 -7
- package/dist/worker/delivery/final-verification.js +12 -0
- 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/workflows/dag/backend-test-markdown-workflow.js +127 -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/init-hybrid.js +208 -19
- package/dist/workflows/dag/reconcile-run.js +24 -0
- package/dist/workflows/dag/types.js +44 -0
- package/dist/workflows/dag/validate.js +16 -0
- package/docs/architecture/dag-execution.md +5 -1
- package/docs/architecture/runtime-boundaries.md +11 -1
- package/docs/architecture/worker-and-feature.md +13 -0
- package/docs/init-surface.manifest.json +3 -0
- package/docs/templates/agent-dag.schema.json +9 -0
- package/docs/templates/backend-test-dag.json +3 -3
- package/docs/templates/frontend-implementation-contract.schema.json +2 -2
- 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-bounded-implement/SKILL.md +53 -0
- package/skills/frontend-implementation/SKILL.md +3 -7
- package/skills/frontend-implementation/references/node-contracts.md +3 -3
- 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
package/dist/worker/cli.js
CHANGED
|
@@ -31,8 +31,11 @@ import { approveFollowUp } from "./follow-up/approve.js";
|
|
|
31
31
|
import { prepareFeatureDelivery } from "./delivery/package.js";
|
|
32
32
|
import { previewFeatureCloseout, renderCloseoutPreview, } from "./closeout/preview.js";
|
|
33
33
|
import { applyFeatureCloseout } from "./closeout/apply.js";
|
|
34
|
+
import { advanceFeature, renderFeatureAdvance, } from "./feature/advance.js";
|
|
35
|
+
import { diagnoseFeature, renderFeatureDoctor, } from "./feature/doctor.js";
|
|
36
|
+
import { FEATURE_SCAFFOLD_TEMPLATES, ScaffoldError, scaffoldFeaturePacket, } from "./feature/scaffold.js";
|
|
34
37
|
import { projectMonthlyMetrics } from "./metrics/projector.js";
|
|
35
|
-
import { runFeatureFinalVerification } from "./delivery/final-verification.js";
|
|
38
|
+
import { defaultFeatureEvidencePaths, runFeatureFinalVerification, } from "./delivery/final-verification.js";
|
|
36
39
|
import { advanceGitCheckpoint } from "./delivery/git-transaction.js";
|
|
37
40
|
export function buildAgentWorkerProgram() {
|
|
38
41
|
const program = new Command();
|
|
@@ -245,6 +248,125 @@ export function buildAgentWorkerProgram() {
|
|
|
245
248
|
if (result.status === "failed" || result.status === "needs-action")
|
|
246
249
|
process.exitCode = 1;
|
|
247
250
|
});
|
|
251
|
+
feature
|
|
252
|
+
.command("advance")
|
|
253
|
+
.requiredOption("--feature-dir <dir>", "Feature directory containing acceptance.yaml and tasks/")
|
|
254
|
+
.requiredOption("--repo <repo-root>", "Target repo root")
|
|
255
|
+
.option("--task-id <id>", "Final verification TaskSpec id (default: resolve FINAL-VERIFY* from Feature review)")
|
|
256
|
+
.option("--loop-agent-bin <bin>", "loop-agent binary", "loop-agent")
|
|
257
|
+
.option("--evidence-mode <mode>", "Evidence aggregate for verify-final: auto, qa-execute, or typed", "auto")
|
|
258
|
+
.option("--qa-evidence <path>", "Override QA pass evidence path")
|
|
259
|
+
.option("--final-verification <path>", "Override final verification evidence path")
|
|
260
|
+
.option("--waivers <path>", "Repo-local acceptance waiver decisions JSON")
|
|
261
|
+
.option("--dry-run", "Run verify-final + delivery dry-run + closeout preview without Delivery writes or closeout apply")
|
|
262
|
+
.option("--apply", "After ready closeout preview, apply Feature Closeout (requires --owner)")
|
|
263
|
+
.option("--owner <owner>", "Closeout owner when using --apply")
|
|
264
|
+
.option("--expected-controller-version <version>", "Exact expected controller version")
|
|
265
|
+
.option("--expected-controller-fingerprint <value>", "Exact expected controller fingerprint sha256:<hex>")
|
|
266
|
+
.option("--json", "Emit stable JSON")
|
|
267
|
+
.description("Orchestrate ADR 0007 delivery path: verify-final → delivery → closeout preview[/apply]")
|
|
268
|
+
.action(async (options) => {
|
|
269
|
+
if (options.evidenceMode !== "auto" &&
|
|
270
|
+
options.evidenceMode !== "qa-execute" &&
|
|
271
|
+
options.evidenceMode !== "typed") {
|
|
272
|
+
throw new Error("feature advance --evidence-mode must be auto, qa-execute, or typed");
|
|
273
|
+
}
|
|
274
|
+
const repoRoot = path.resolve(options.repo);
|
|
275
|
+
const client = new LoopAgentClient({
|
|
276
|
+
loopAgentBin: options.loopAgentBin,
|
|
277
|
+
artifactRoot: path.join(getTaskPoolRoot(repoRoot), "artifacts", `feature-advance-${Date.now()}`),
|
|
278
|
+
resolveIdentity: true,
|
|
279
|
+
});
|
|
280
|
+
const result = await advanceFeature({
|
|
281
|
+
featureDir: path.resolve(options.featureDir),
|
|
282
|
+
repoRoot,
|
|
283
|
+
...(options.taskId ? { taskId: options.taskId } : {}),
|
|
284
|
+
client,
|
|
285
|
+
controllerIdentity: client.getIdentity(),
|
|
286
|
+
controllerExpectation: buildIdentityExpectation(options),
|
|
287
|
+
evidenceMode: options.evidenceMode,
|
|
288
|
+
...(options.qaEvidence
|
|
289
|
+
? { qaEvidencePath: options.qaEvidence }
|
|
290
|
+
: {}),
|
|
291
|
+
...(options.finalVerification
|
|
292
|
+
? { finalVerificationPath: options.finalVerification }
|
|
293
|
+
: {}),
|
|
294
|
+
...(options.waivers ? { waiverPath: options.waivers } : {}),
|
|
295
|
+
dryRun: options.dryRun ?? false,
|
|
296
|
+
apply: options.apply ?? false,
|
|
297
|
+
...(options.owner ? { owner: options.owner } : {}),
|
|
298
|
+
});
|
|
299
|
+
process.stdout.write(options.json
|
|
300
|
+
? `${JSON.stringify(result, null, 2)}\n`
|
|
301
|
+
: renderFeatureAdvance(result));
|
|
302
|
+
if (result.status === "failed" ||
|
|
303
|
+
result.status === "blocked") {
|
|
304
|
+
process.exitCode = 1;
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
feature
|
|
308
|
+
.command("doctor")
|
|
309
|
+
.requiredOption("--feature-dir <dir>", "Feature directory containing acceptance.yaml and tasks/")
|
|
310
|
+
.requiredOption("--repo <repo-root>", "Target repo root")
|
|
311
|
+
.option("--json", "Emit stable JSON")
|
|
312
|
+
.description("Read-only Feature delivery readiness diagnosis (evidence, pool states, dirty worktree)")
|
|
313
|
+
.action(async (options) => {
|
|
314
|
+
const result = await diagnoseFeature({
|
|
315
|
+
featureDir: path.resolve(options.featureDir),
|
|
316
|
+
repoRoot: path.resolve(options.repo),
|
|
317
|
+
});
|
|
318
|
+
process.stdout.write(options.json
|
|
319
|
+
? `${JSON.stringify(result, null, 2)}\n`
|
|
320
|
+
: renderFeatureDoctor(result));
|
|
321
|
+
if (result.status === "blocked")
|
|
322
|
+
process.exitCode = 1;
|
|
323
|
+
});
|
|
324
|
+
feature
|
|
325
|
+
.command("scaffold")
|
|
326
|
+
.requiredOption("--repo <repo-root>", "Target repo root")
|
|
327
|
+
.option("--template <id>", `Locked template: ${FEATURE_SCAFFOLD_TEMPLATES.join(" | ")}`)
|
|
328
|
+
.option("--feature-id <id>", "Feature id (F-YYYY-NNN)")
|
|
329
|
+
.option("--title <text>", "Human title")
|
|
330
|
+
.option("--description <text>", "Optional longer description")
|
|
331
|
+
.option("--be-path <pattern>", "Repo-relative backend allowed path (repeatable)", (value, previous) => [...previous, value], [])
|
|
332
|
+
.option("--fe-path <pattern>", "Repo-relative frontend allowed path (repeatable)", (value, previous) => [...previous, value], [])
|
|
333
|
+
.option("--contract-path <path>", "fe-with-api contract path (default docs/contracts/<feature-id>/api.md)")
|
|
334
|
+
.option("--verify-command <command>", "Explicit verify command for implementation tasks (repeatable)", (value, previous) => [...previous, value], [])
|
|
335
|
+
.option("--batch <file>", "YAML/JSON batch list (mutually exclusive with single-feature flags)")
|
|
336
|
+
.option("--dry-run", "Parse/render/validate without writing features/")
|
|
337
|
+
.option("--json", "Emit stable JSON only")
|
|
338
|
+
.description("Deterministically scaffold a Feature Packet (no model, no Task Pool, no fullstack-v1 default)")
|
|
339
|
+
.action(async (options) => {
|
|
340
|
+
try {
|
|
341
|
+
const result = options.batch
|
|
342
|
+
? await scaffoldFeaturePacket({
|
|
343
|
+
repoRoot: path.resolve(options.repo),
|
|
344
|
+
batchFile: path.resolve(options.batch),
|
|
345
|
+
dryRun: options.dryRun ?? false,
|
|
346
|
+
})
|
|
347
|
+
: await scaffoldFeaturePacket({
|
|
348
|
+
repoRoot: path.resolve(options.repo),
|
|
349
|
+
dryRun: options.dryRun ?? false,
|
|
350
|
+
feature: {
|
|
351
|
+
featureId: options.featureId ?? "",
|
|
352
|
+
title: options.title ?? "",
|
|
353
|
+
template: (options.template ??
|
|
354
|
+
""),
|
|
355
|
+
description: options.description,
|
|
356
|
+
bePaths: options.bePath,
|
|
357
|
+
fePaths: options.fePath,
|
|
358
|
+
contractPath: options.contractPath,
|
|
359
|
+
verifyCommands: options.verifyCommand,
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
process.stdout.write(options.json
|
|
363
|
+
? `${JSON.stringify(result, null, 2)}\n`
|
|
364
|
+
: renderFeatureScaffold(result));
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
handleScaffoldCliError(error, options.json ?? false);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
248
370
|
feature
|
|
249
371
|
.command("verify-final")
|
|
250
372
|
.requiredOption("--feature-dir <dir>", "Feature directory containing acceptance.yaml and tasks/")
|
|
@@ -284,18 +406,30 @@ export function buildAgentWorkerProgram() {
|
|
|
284
406
|
.command("delivery")
|
|
285
407
|
.requiredOption("--feature-dir <dir>", "Feature directory containing acceptance.yaml and tasks/")
|
|
286
408
|
.requiredOption("--repo <repo-root>", "Target repo root")
|
|
287
|
-
.
|
|
288
|
-
.
|
|
409
|
+
.option("--qa-evidence <path>", "Repo-local QA pass evidence (default: .harness/task-pool/evidence/<featureId>/qa-pass.json from verify-final)")
|
|
410
|
+
.option("--final-verification <path>", "Repo-local final verification evidence (default: .../final-verification.json from verify-final)")
|
|
289
411
|
.option("--waivers <path>", "Repo-local acceptance waiver decisions JSON")
|
|
290
412
|
.option("--dry-run", "Validate and show the Delivery Package plan without writes")
|
|
291
413
|
.option("--json", "Emit stable JSON")
|
|
292
414
|
.description("Validate checkpointed Feature evidence and prepare a local Delivery Package")
|
|
293
415
|
.action(async (options) => {
|
|
416
|
+
const featureDir = path.resolve(options.featureDir);
|
|
417
|
+
const repoRoot = path.resolve(options.repo);
|
|
418
|
+
// ADR 0007: default to verify-final evidence locations when omitted.
|
|
419
|
+
let qaEvidencePath = options.qaEvidence;
|
|
420
|
+
let finalVerificationPath = options.finalVerification;
|
|
421
|
+
if (!qaEvidencePath || !finalVerificationPath) {
|
|
422
|
+
const validation = await validateFeatureTaskGraph(featureDir);
|
|
423
|
+
const defaults = defaultFeatureEvidencePaths(validation.featureId);
|
|
424
|
+
qaEvidencePath = qaEvidencePath ?? defaults.qaEvidencePath;
|
|
425
|
+
finalVerificationPath =
|
|
426
|
+
finalVerificationPath ?? defaults.finalVerificationPath;
|
|
427
|
+
}
|
|
294
428
|
const result = await prepareFeatureDelivery({
|
|
295
|
-
featureDir
|
|
296
|
-
repoRoot
|
|
297
|
-
qaEvidencePath
|
|
298
|
-
finalVerificationPath
|
|
429
|
+
featureDir,
|
|
430
|
+
repoRoot,
|
|
431
|
+
qaEvidencePath,
|
|
432
|
+
finalVerificationPath,
|
|
299
433
|
...(options.waivers ? { waiverPath: options.waivers } : {}),
|
|
300
434
|
dryRun: options.dryRun ?? false,
|
|
301
435
|
});
|
|
@@ -743,6 +877,42 @@ function handlePoolRecoveryCliError(error) {
|
|
|
743
877
|
process.stderr.write(`${detail}\n`);
|
|
744
878
|
process.exitCode = 1;
|
|
745
879
|
}
|
|
880
|
+
function handleScaffoldCliError(error, json) {
|
|
881
|
+
const code = error instanceof ScaffoldError ? error.code : "write-failed";
|
|
882
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
883
|
+
const details = error instanceof ScaffoldError ? error.details : undefined;
|
|
884
|
+
if (json) {
|
|
885
|
+
process.stdout.write(`${JSON.stringify({
|
|
886
|
+
schemaVersion: 1,
|
|
887
|
+
ok: false,
|
|
888
|
+
code,
|
|
889
|
+
message,
|
|
890
|
+
...(details !== undefined ? { details } : {}),
|
|
891
|
+
}, null, 2)}\n`);
|
|
892
|
+
}
|
|
893
|
+
else {
|
|
894
|
+
process.stderr.write(`${message}\n`);
|
|
895
|
+
}
|
|
896
|
+
process.exitCode = 1;
|
|
897
|
+
}
|
|
898
|
+
function renderFeatureScaffold(result) {
|
|
899
|
+
const lines = [
|
|
900
|
+
result.dryRun
|
|
901
|
+
? `Feature scaffold dry-run: ${result.features.length} packet(s) validated`
|
|
902
|
+
: `Feature scaffold wrote ${result.features.length} packet(s)`,
|
|
903
|
+
"",
|
|
904
|
+
];
|
|
905
|
+
for (const feature of result.features) {
|
|
906
|
+
lines.push(`- ${feature.featureId} [${feature.template}] → ${feature.featureDir}`);
|
|
907
|
+
}
|
|
908
|
+
if (!result.dryRun && result.writtenPaths.length > 0) {
|
|
909
|
+
lines.push("", "Written:");
|
|
910
|
+
for (const p of result.writtenPaths)
|
|
911
|
+
lines.push(` ${p}`);
|
|
912
|
+
}
|
|
913
|
+
lines.push("");
|
|
914
|
+
return `${lines.join("\n")}\n`;
|
|
915
|
+
}
|
|
746
916
|
function handleFollowUpCliError(error, json, action) {
|
|
747
917
|
const detail = error instanceof Error ? error.message : String(error);
|
|
748
918
|
if (!json)
|
|
@@ -16,6 +16,18 @@ import { preflightTargetRepo } from "../preflight.js";
|
|
|
16
16
|
import { gitTransactionRecordSchema, transactionRecordPath } from "./git-transaction.js";
|
|
17
17
|
import { buildFeatureVerificationBundle, writeFeatureVerificationBundle, } from "./verification-bundle.js";
|
|
18
18
|
const execFileAsync = promisify(execFile);
|
|
19
|
+
/**
|
|
20
|
+
* Canonical Delivery evidence locations written by `feature verify-final`
|
|
21
|
+
* (ADR 0007). Paths are repo-relative with forward slashes.
|
|
22
|
+
*/
|
|
23
|
+
export function defaultFeatureEvidencePaths(featureId) {
|
|
24
|
+
const base = `.harness/task-pool/evidence/${featureId}`;
|
|
25
|
+
return {
|
|
26
|
+
qaEvidencePath: `${base}/qa-pass.json`,
|
|
27
|
+
finalVerificationPath: `${base}/final-verification.json`,
|
|
28
|
+
bundleEvidencePath: `${base}/feature-verification-bundle.json`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
19
31
|
export async function runFeatureFinalVerification(input) {
|
|
20
32
|
const dependencies = {
|
|
21
33
|
preflight: preflightTargetRepo,
|
|
@@ -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
|
+
}
|