@tea-agent/loop-agent 0.24.11-beta.0 → 0.25.1

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 (97) hide show
  1. package/CHANGELOG.md +53 -4
  2. package/README.md +4 -4
  3. package/dist/application/dag/generate-task-dag.js +25 -8
  4. package/dist/commands/dag-init-hybrid.js +3 -2
  5. package/dist/commands/delegate.js +11 -11
  6. package/dist/commands/init.js +29 -14
  7. package/dist/commands/instructions.js +1 -2
  8. package/dist/executors/dag-pi-executor.js +74 -13
  9. package/dist/executors/pi-executor.js +25 -7
  10. package/dist/executors/pi-prompt-transport.js +198 -0
  11. package/dist/executors/pi-sdk-executor.js +1 -1
  12. package/dist/executors/process-tree.js +33 -0
  13. package/dist/executors/shell-executor.js +362 -24
  14. package/dist/executors/shell-verification.js +4 -2
  15. package/dist/infrastructure/harness/task-store.js +31 -0
  16. package/dist/shared/operator/capabilities.js +6 -0
  17. package/dist/task/read-model.js +3 -7
  18. package/dist/task/runtime.js +12 -3
  19. package/dist/verification/maven/cache.js +142 -0
  20. package/dist/verification/maven/index.js +120 -0
  21. package/dist/verification/maven/plan-commands.js +421 -0
  22. package/dist/verification/maven/pom-static.js +136 -0
  23. package/dist/verification/maven/scope-resolve.js +153 -0
  24. package/dist/verification/maven/stale.js +130 -0
  25. package/dist/verification/maven/types.js +23 -0
  26. package/dist/verification/maven/workspace-graph.js +322 -0
  27. package/dist/worker/cli.js +236 -21
  28. package/dist/worker/delivery/final-verification.js +12 -0
  29. package/dist/worker/delivery/git-transaction.js +32 -7
  30. package/dist/worker/delivery/package.js +9 -5
  31. package/dist/worker/delivery/verification-bundle.js +26 -5
  32. package/dist/worker/feature/advance.js +301 -0
  33. package/dist/worker/feature/doctor.js +223 -0
  34. package/dist/worker/feature/next-action.js +11 -3
  35. package/dist/worker/feature/scaffold.js +798 -0
  36. package/dist/worker/observe/dag-run-artifacts.js +90 -0
  37. package/dist/worker/observe/node-input.js +444 -0
  38. package/dist/worker/observe/routes.js +17 -0
  39. package/dist/worker/observe/static/api.js +9 -0
  40. package/dist/worker/observe/static/constants.js +9 -0
  41. package/dist/worker/observe/static/state.js +14 -0
  42. package/dist/worker/observe/static/styles.css +74 -0
  43. package/dist/worker/observe/static/views/dag-inspector.js +371 -15
  44. package/dist/worker/outcomes/projector.js +5 -1
  45. package/dist/worker/run-task/run-task.js +18 -2
  46. package/dist/worker/runner/run-ready.js +41 -1
  47. package/dist/workflows/dag/backend-test-markdown-workflow.js +300 -9
  48. package/dist/workflows/dag/backend-test-result-contract.js +103 -0
  49. package/dist/workflows/dag/failure-category.js +5 -1
  50. package/dist/workflows/dag/frontend-implementation-contract.js +4 -0
  51. package/dist/workflows/dag/frontend-prewrite-gate.js +0 -17
  52. package/dist/workflows/dag/frontend-test-result-contract.js +106 -41
  53. package/dist/workflows/dag/init-hybrid.js +224 -39
  54. package/dist/workflows/dag/node-execution.js +3 -2
  55. package/dist/workflows/dag/reconcile-run.js +24 -0
  56. package/dist/workflows/dag/types.js +57 -0
  57. package/dist/workflows/dag/validate.js +19 -2
  58. package/dist/workflows/loop/actions/dag-action.js +2 -1
  59. package/docs/architecture/dag-execution.md +5 -1
  60. package/docs/architecture/runtime-boundaries.md +11 -1
  61. package/docs/architecture/worker-and-feature.md +2 -0
  62. package/docs/init-surface.manifest.json +3 -0
  63. package/docs/templates/backend-test-dag.json +3 -3
  64. package/docs/templates/product-line/README.md +17 -1
  65. package/docs/templates/product-line/feature-scaffold-batch.example.yaml +31 -0
  66. package/docs/templates/product-line/scaffold-samples/README.md +36 -0
  67. package/docs/templates/product-line/scaffold-samples/backend-only/acceptance.yaml +13 -0
  68. package/docs/templates/product-line/scaffold-samples/backend-only/design.md +14 -0
  69. package/docs/templates/product-line/scaffold-samples/backend-only/feature.yaml +3 -0
  70. package/docs/templates/product-line/scaffold-samples/backend-only/requirement.md +6 -0
  71. package/docs/templates/product-line/scaffold-samples/backend-only/tasks/BE-IMPL-001.yaml +83 -0
  72. package/docs/templates/product-line/scaffold-samples/backend-only/tasks/task-graph.yaml +14 -0
  73. package/docs/templates/product-line/scaffold-samples/fe-with-api/acceptance.yaml +15 -0
  74. package/docs/templates/product-line/scaffold-samples/fe-with-api/design.md +18 -0
  75. package/docs/templates/product-line/scaffold-samples/fe-with-api/feature.yaml +3 -0
  76. package/docs/templates/product-line/scaffold-samples/fe-with-api/requirement.md +6 -0
  77. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/BE-IMPL-001.yaml +84 -0
  78. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/CONTRACT-001.yaml +84 -0
  79. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/FE-IMPL-001.yaml +86 -0
  80. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/task-graph.yaml +40 -0
  81. package/docs/templates/product-line/scaffold-samples/frontend-only/acceptance.yaml +13 -0
  82. package/docs/templates/product-line/scaffold-samples/frontend-only/design.md +14 -0
  83. package/docs/templates/product-line/scaffold-samples/frontend-only/feature.yaml +3 -0
  84. package/docs/templates/product-line/scaffold-samples/frontend-only/requirement.md +6 -0
  85. package/docs/templates/product-line/scaffold-samples/frontend-only/tasks/FE-IMPL-001.yaml +85 -0
  86. package/docs/templates/product-line/scaffold-samples/frontend-only/tasks/task-graph.yaml +14 -0
  87. package/examples/l5-report-coms-process-definition.html +322 -0
  88. package/package.json +1 -1
  89. package/skills/agent-worker/references/agent-worker-operator.md +84 -0
  90. package/skills/frontend-implementation/references/node-contracts.md +2 -2
  91. package/skills/loop-agent/SKILL.md +14 -16
  92. package/skills/loop-agent/references/command-reference.md +33 -12
  93. package/skills/loop-agent/references/harness-policy.md +28 -6
  94. package/skills/loop-agent/references/hybrid-dag.md +8 -7
  95. package/skills/loop-agent/references/post-implementation-and-patterns.md +19 -9
  96. package/skills/loop-agent/references/source-and-plan-practice.md +161 -0
  97. package/skills/loop-agent/references/task-workflow.md +13 -5
@@ -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
+ }
@@ -0,0 +1,223 @@
1
+ import { access } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { execFile } from "node:child_process";
4
+ import { promisify } from "node:util";
5
+ import { defaultFeatureEvidencePaths } from "../delivery/final-verification.js";
6
+ import { transactionRecordPath } from "../delivery/git-transaction.js";
7
+ import { readFeatureTaskPoolStates } from "../pool/run-store.js";
8
+ import { validateFeatureTaskGraph } from "../task-graph/validate.js";
9
+ import { reviewFeature } from "./review.js";
10
+ import { resolveFinalVerifyTaskId } from "./advance.js";
11
+ const execFileAsync = promisify(execFile);
12
+ /**
13
+ * Read-only Feature delivery readiness doctor (not pool migrate / not auto-repair).
14
+ */
15
+ export async function diagnoseFeature(input) {
16
+ const featureDir = path.resolve(input.featureDir);
17
+ const repoRoot = path.resolve(input.repoRoot);
18
+ const findings = [];
19
+ const nextCommands = [];
20
+ const validation = await validateFeatureTaskGraph(featureDir);
21
+ const featureId = validation.featureId || "unknown";
22
+ if (!validation.ok) {
23
+ findings.push({
24
+ id: "feature-validation",
25
+ severity: "error",
26
+ message: `Feature Packet invalid: ${validation.errors.map((e) => e.code).join(", ")}`,
27
+ evidence: validation.errors.flatMap((e) => (e.path ? [e.path] : [])),
28
+ suggestedCommand: `agent-worker task validate-feature ${JSON.stringify(featureDir)}`,
29
+ });
30
+ }
31
+ let review;
32
+ try {
33
+ review = await reviewFeature({ featureDir, repoRoot });
34
+ }
35
+ catch (error) {
36
+ findings.push({
37
+ id: "feature-review",
38
+ severity: "error",
39
+ message: error instanceof Error ? error.message : String(error),
40
+ });
41
+ }
42
+ const finalTaskId = review ? resolveFinalVerifyTaskId(review) : null;
43
+ const defaults = defaultFeatureEvidencePaths(featureId);
44
+ const deliveryManifestRel = `.harness/task-pool/artifacts/features/${featureId}/delivery/delivery-manifest.json`;
45
+ const gitTxAbs = transactionRecordPath(repoRoot, featureId);
46
+ const gitTxRel = path.relative(repoRoot, gitTxAbs).replace(/\\/g, "/");
47
+ const evidence = {
48
+ qaPass: {
49
+ path: defaults.qaEvidencePath,
50
+ present: await exists(path.join(repoRoot, defaults.qaEvidencePath)),
51
+ },
52
+ finalVerification: {
53
+ path: defaults.finalVerificationPath,
54
+ present: await exists(path.join(repoRoot, defaults.finalVerificationPath)),
55
+ },
56
+ bundle: {
57
+ path: defaults.bundleEvidencePath,
58
+ present: await exists(path.join(repoRoot, defaults.bundleEvidencePath)),
59
+ },
60
+ deliveryManifest: {
61
+ path: deliveryManifestRel,
62
+ present: await exists(path.join(repoRoot, deliveryManifestRel)),
63
+ },
64
+ gitTransaction: {
65
+ path: gitTxRel,
66
+ present: await exists(gitTxAbs),
67
+ },
68
+ };
69
+ const statesById = validation.ok
70
+ ? await readFeatureTaskPoolStates(repoRoot, featureId)
71
+ : {};
72
+ const states = Object.values(statesById);
73
+ const taskSummary = {
74
+ total: states.length,
75
+ done: states.filter((s) => s.status === "Done").length,
76
+ failed: states.filter((s) => s.status === "Failed").length,
77
+ running: states.filter((s) => s.status === "Running").length,
78
+ ready: states.filter((s) => s.status === "Ready").length,
79
+ other: states.filter((s) => !["Done", "Failed", "Running", "Ready"].includes(s.status)).length,
80
+ };
81
+ if (taskSummary.running > 0) {
82
+ const running = states.filter((s) => s.status === "Running");
83
+ findings.push({
84
+ id: "running-tasks",
85
+ severity: "warn",
86
+ message: `${taskSummary.running} task(s) still Running`,
87
+ evidence: running.map((s) => s.taskId),
88
+ suggestedCommand: `agent-worker task reconcile <task-id> --feature-id ${JSON.stringify(featureId)} --repo ${JSON.stringify(repoRoot)} --action abandon`,
89
+ });
90
+ }
91
+ if (taskSummary.failed > 0) {
92
+ const failed = states.filter((s) => s.status === "Failed");
93
+ findings.push({
94
+ id: "failed-tasks",
95
+ severity: "warn",
96
+ message: `${taskSummary.failed} task(s) Failed`,
97
+ evidence: failed.map((s) => s.taskId),
98
+ suggestedCommand: `agent-worker task retry <task-id> --feature-id ${JSON.stringify(featureId)} --repo ${JSON.stringify(repoRoot)} --reason <reason>`,
99
+ });
100
+ }
101
+ const dirty = await gitDirty(repoRoot);
102
+ if (dirty.dirty) {
103
+ findings.push({
104
+ id: "dirty-worktree",
105
+ severity: "warn",
106
+ message: "Target repo worktree is dirty (may block Delivery/closeout)",
107
+ evidence: dirty.lines.slice(0, 20),
108
+ });
109
+ }
110
+ if (!evidence.gitTransaction.present && taskSummary.done > 0) {
111
+ findings.push({
112
+ id: "missing-git-transaction",
113
+ severity: "info",
114
+ message: "No Feature git transaction record yet (expected before checkpointed Delivery)",
115
+ evidence: [evidence.gitTransaction.path],
116
+ });
117
+ }
118
+ const allTasksDone = review &&
119
+ review.tasks.length > 0 &&
120
+ review.tasks.every((t) => t.status === "Done");
121
+ if (allTasksDone) {
122
+ if (!evidence.qaPass.present || !evidence.finalVerification.present) {
123
+ findings.push({
124
+ id: "missing-final-evidence",
125
+ severity: "info",
126
+ message: "Implementation/QA tasks Done but verify-final evidence missing (ADR 0007)",
127
+ evidence: [
128
+ evidence.qaPass.path,
129
+ evidence.finalVerification.path,
130
+ ].filter((p, i) => i === 0 ? !evidence.qaPass.present : !evidence.finalVerification.present),
131
+ suggestedCommand: finalTaskId
132
+ ? `agent-worker feature advance --feature-dir ${JSON.stringify(featureDir)} --repo ${JSON.stringify(repoRoot)} --task-id ${JSON.stringify(finalTaskId)} --dry-run --json`
133
+ : `agent-worker feature verify-final --feature-dir ${JSON.stringify(featureDir)} --repo ${JSON.stringify(repoRoot)} --task-id <FINAL-VERIFY-id> --json`,
134
+ });
135
+ if (finalTaskId) {
136
+ nextCommands.push(`agent-worker feature advance --feature-dir ${JSON.stringify(featureDir)} --repo ${JSON.stringify(repoRoot)} --task-id ${JSON.stringify(finalTaskId)} --json`);
137
+ }
138
+ }
139
+ else if (!evidence.deliveryManifest.present) {
140
+ findings.push({
141
+ id: "missing-delivery",
142
+ severity: "info",
143
+ message: "Final evidence present but Delivery Package not written",
144
+ suggestedCommand: `agent-worker feature advance --feature-dir ${JSON.stringify(featureDir)} --repo ${JSON.stringify(repoRoot)}${finalTaskId ? ` --task-id ${JSON.stringify(finalTaskId)}` : ""} --json`,
145
+ });
146
+ nextCommands.push(`agent-worker feature advance --feature-dir ${JSON.stringify(featureDir)} --repo ${JSON.stringify(repoRoot)}${finalTaskId ? ` --task-id ${JSON.stringify(finalTaskId)}` : ""} --json`);
147
+ }
148
+ else if (review?.status === "deliverable") {
149
+ findings.push({
150
+ id: "ready-for-closeout",
151
+ severity: "info",
152
+ message: "Feature is deliverable; preview or apply closeout",
153
+ suggestedCommand: `agent-worker feature closeout --feature-dir ${JSON.stringify(featureDir)} --repo ${JSON.stringify(repoRoot)} --json`,
154
+ });
155
+ nextCommands.push(`agent-worker feature advance --feature-dir ${JSON.stringify(featureDir)} --repo ${JSON.stringify(repoRoot)}${finalTaskId ? ` --task-id ${JSON.stringify(finalTaskId)}` : ""} --apply --owner <owner> --json`);
156
+ }
157
+ }
158
+ else if (review?.status === "ready") {
159
+ nextCommands.push(`agent-worker feature run --feature-dir ${JSON.stringify(featureDir)} --repo ${JSON.stringify(repoRoot)} --limit 1 --json`);
160
+ }
161
+ const hasError = findings.some((f) => f.severity === "error");
162
+ const hasWarn = findings.some((f) => f.severity === "warn");
163
+ const status = hasError ? "blocked" : hasWarn ? "attention" : "healthy";
164
+ if (nextCommands.length === 0 && review?.nextAction?.command) {
165
+ nextCommands.push(review.nextAction.command);
166
+ }
167
+ return {
168
+ schemaVersion: 1,
169
+ featureId,
170
+ status,
171
+ ...(review ? { reviewStatus: review.status } : {}),
172
+ finalTaskId,
173
+ evidence,
174
+ taskSummary,
175
+ findings,
176
+ nextCommands,
177
+ };
178
+ }
179
+ export function renderFeatureDoctor(result) {
180
+ const lines = [
181
+ `Feature: ${result.featureId}`,
182
+ `Doctor: ${result.status}${result.reviewStatus ? ` (review=${result.reviewStatus})` : ""}`,
183
+ `Tasks: total=${result.taskSummary.total} done=${result.taskSummary.done} failed=${result.taskSummary.failed} running=${result.taskSummary.running} ready=${result.taskSummary.ready}`,
184
+ `Final task: ${result.finalTaskId ?? "none"}`,
185
+ `Evidence: qa=${result.evidence.qaPass.present ? "yes" : "no"} final=${result.evidence.finalVerification.present ? "yes" : "no"} bundle=${result.evidence.bundle.present ? "yes" : "no"} delivery=${result.evidence.deliveryManifest.present ? "yes" : "no"}`,
186
+ ...result.findings.map((f) => `${severityMark(f.severity)} ${f.id}: ${f.message}${f.suggestedCommand ? `\n → ${f.suggestedCommand}` : ""}`),
187
+ ];
188
+ if (result.nextCommands.length) {
189
+ lines.push("Next commands:");
190
+ for (const cmd of result.nextCommands)
191
+ lines.push(` - ${cmd}`);
192
+ }
193
+ return `${lines.join("\n")}\n`;
194
+ }
195
+ function severityMark(severity) {
196
+ if (severity === "error")
197
+ return "✗";
198
+ if (severity === "warn")
199
+ return "!";
200
+ return "·";
201
+ }
202
+ async function exists(filePath) {
203
+ try {
204
+ await access(filePath);
205
+ return true;
206
+ }
207
+ catch {
208
+ return false;
209
+ }
210
+ }
211
+ async function gitDirty(repoRoot) {
212
+ try {
213
+ const { stdout } = await execFileAsync("git", ["status", "--short", "--branch"], { cwd: repoRoot, encoding: "utf8" });
214
+ const lines = stdout
215
+ .split(/\r?\n/)
216
+ .map((l) => l.trimEnd())
217
+ .filter((l) => l && !l.startsWith("##"));
218
+ return { dirty: lines.length > 0, lines };
219
+ }
220
+ catch {
221
+ return { dirty: false, lines: [] };
222
+ }
223
+ }
@@ -66,13 +66,21 @@ export function projectNextAction(input, status) {
66
66
  };
67
67
  }
68
68
  if (status === "awaiting_qa") {
69
+ const finalish = input.tasks.find((task) => /(final[-_]?(verify|verification))|closeout/i.test(task.taskId));
70
+ if (finalish && (finalish.status === "Done" || finalish.status === "Ready")) {
71
+ return {
72
+ kind: "advance_delivery",
73
+ label: "运行 feature advance(verify-final 权威路径)",
74
+ command: `agent-worker feature advance ${featureArgs} --task-id ${quote(finalish.taskId)}`,
75
+ };
76
+ }
69
77
  return { kind: "complete_qa", label: "补齐 QA verdict 与 required AC 证据" };
70
78
  }
71
79
  if (status === "deliverable") {
72
80
  return {
73
- kind: "prepare_closeout",
74
- label: "预览 Feature Closeout gates",
75
- command: `agent-worker feature closeout ${featureArgs}`,
81
+ kind: "advance_delivery",
82
+ label: "编排 verify-final delivery → closeout(ADR 0007)",
83
+ command: `agent-worker feature advance ${featureArgs}`,
76
84
  };
77
85
  }
78
86
  return null;