@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.
Files changed (72) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/executors/dag-pi-executor.js +74 -13
  3. package/dist/executors/pi-executor.js +25 -7
  4. package/dist/executors/pi-prompt-transport.js +198 -0
  5. package/dist/executors/pi-sdk-executor.js +1 -1
  6. package/dist/executors/process-tree.js +33 -0
  7. package/dist/executors/shell-executor.js +239 -47
  8. package/dist/executors/shell-verification.js +4 -2
  9. package/dist/infrastructure/harness/task-store.js +31 -0
  10. package/dist/shared/operator/capabilities.js +6 -0
  11. package/dist/verification/maven/cache.js +142 -0
  12. package/dist/verification/maven/index.js +120 -0
  13. package/dist/verification/maven/plan-commands.js +421 -0
  14. package/dist/verification/maven/pom-static.js +136 -0
  15. package/dist/verification/maven/scope-resolve.js +153 -0
  16. package/dist/verification/maven/stale.js +130 -0
  17. package/dist/verification/maven/types.js +23 -0
  18. package/dist/verification/maven/workspace-graph.js +322 -0
  19. package/dist/worker/cli.js +177 -7
  20. package/dist/worker/delivery/final-verification.js +12 -0
  21. package/dist/worker/feature/advance.js +301 -0
  22. package/dist/worker/feature/doctor.js +223 -0
  23. package/dist/worker/feature/next-action.js +11 -3
  24. package/dist/worker/feature/scaffold.js +798 -0
  25. package/dist/workflows/dag/backend-test-markdown-workflow.js +127 -0
  26. package/dist/workflows/dag/failure-category.js +5 -1
  27. package/dist/workflows/dag/frontend-implementation-contract.js +4 -0
  28. package/dist/workflows/dag/frontend-prewrite-gate.js +0 -17
  29. package/dist/workflows/dag/init-hybrid.js +208 -19
  30. package/dist/workflows/dag/reconcile-run.js +24 -0
  31. package/dist/workflows/dag/types.js +44 -0
  32. package/dist/workflows/dag/validate.js +16 -0
  33. package/docs/architecture/dag-execution.md +5 -1
  34. package/docs/architecture/runtime-boundaries.md +11 -1
  35. package/docs/architecture/worker-and-feature.md +13 -0
  36. package/docs/init-surface.manifest.json +3 -0
  37. package/docs/templates/agent-dag.schema.json +9 -0
  38. package/docs/templates/backend-test-dag.json +3 -3
  39. package/docs/templates/frontend-implementation-contract.schema.json +2 -2
  40. package/docs/templates/product-line/README.md +17 -1
  41. package/docs/templates/product-line/feature-scaffold-batch.example.yaml +31 -0
  42. package/docs/templates/product-line/scaffold-samples/README.md +36 -0
  43. package/docs/templates/product-line/scaffold-samples/backend-only/acceptance.yaml +13 -0
  44. package/docs/templates/product-line/scaffold-samples/backend-only/design.md +14 -0
  45. package/docs/templates/product-line/scaffold-samples/backend-only/feature.yaml +3 -0
  46. package/docs/templates/product-line/scaffold-samples/backend-only/requirement.md +6 -0
  47. package/docs/templates/product-line/scaffold-samples/backend-only/tasks/BE-IMPL-001.yaml +83 -0
  48. package/docs/templates/product-line/scaffold-samples/backend-only/tasks/task-graph.yaml +14 -0
  49. package/docs/templates/product-line/scaffold-samples/fe-with-api/acceptance.yaml +15 -0
  50. package/docs/templates/product-line/scaffold-samples/fe-with-api/design.md +18 -0
  51. package/docs/templates/product-line/scaffold-samples/fe-with-api/feature.yaml +3 -0
  52. package/docs/templates/product-line/scaffold-samples/fe-with-api/requirement.md +6 -0
  53. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/BE-IMPL-001.yaml +84 -0
  54. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/CONTRACT-001.yaml +84 -0
  55. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/FE-IMPL-001.yaml +86 -0
  56. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/task-graph.yaml +40 -0
  57. package/docs/templates/product-line/scaffold-samples/frontend-only/acceptance.yaml +13 -0
  58. package/docs/templates/product-line/scaffold-samples/frontend-only/design.md +14 -0
  59. package/docs/templates/product-line/scaffold-samples/frontend-only/feature.yaml +3 -0
  60. package/docs/templates/product-line/scaffold-samples/frontend-only/requirement.md +6 -0
  61. package/docs/templates/product-line/scaffold-samples/frontend-only/tasks/FE-IMPL-001.yaml +85 -0
  62. package/docs/templates/product-line/scaffold-samples/frontend-only/tasks/task-graph.yaml +14 -0
  63. package/examples/l5-report-coms-process-definition.html +322 -0
  64. package/package.json +1 -1
  65. package/skills/agent-worker/references/agent-worker-operator.md +84 -0
  66. package/skills/frontend-bounded-implement/SKILL.md +53 -0
  67. package/skills/frontend-implementation/SKILL.md +3 -7
  68. package/skills/frontend-implementation/references/node-contracts.md +3 -3
  69. package/skills/loop-agent/SKILL.md +8 -8
  70. package/skills/loop-agent/references/command-reference.md +14 -2
  71. package/skills/loop-agent/references/harness-policy.md +22 -0
  72. package/skills/loop-agent/references/task-workflow.md +5 -0
@@ -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
- .requiredOption("--qa-evidence <path>", "Repo-local QA pass evidence")
288
- .requiredOption("--final-verification <path>", "Repo-local fresh final verification evidence")
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: path.resolve(options.featureDir),
296
- repoRoot: path.resolve(options.repo),
297
- qaEvidencePath: options.qaEvidence,
298
- finalVerificationPath: options.finalVerification,
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
+ }