@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.
Files changed (84) hide show
  1. package/CHANGELOG.md +28 -5
  2. package/dist/commands/init.js +16 -2
  3. package/dist/executors/dag-pi-executor.js +74 -13
  4. package/dist/executors/pi-executor.js +25 -7
  5. package/dist/executors/pi-prompt-transport.js +198 -0
  6. package/dist/executors/pi-sdk-executor.js +1 -1
  7. package/dist/executors/process-tree.js +33 -0
  8. package/dist/executors/shell-executor.js +332 -24
  9. package/dist/executors/shell-verification.js +4 -2
  10. package/dist/infrastructure/harness/task-store.js +31 -0
  11. package/dist/shared/operator/capabilities.js +6 -0
  12. package/dist/verification/maven/cache.js +142 -0
  13. package/dist/verification/maven/index.js +120 -0
  14. package/dist/verification/maven/plan-commands.js +421 -0
  15. package/dist/verification/maven/pom-static.js +136 -0
  16. package/dist/verification/maven/scope-resolve.js +153 -0
  17. package/dist/verification/maven/stale.js +130 -0
  18. package/dist/verification/maven/types.js +23 -0
  19. package/dist/verification/maven/workspace-graph.js +322 -0
  20. package/dist/worker/cli.js +236 -21
  21. package/dist/worker/delivery/final-verification.js +12 -0
  22. package/dist/worker/delivery/git-transaction.js +32 -7
  23. package/dist/worker/delivery/package.js +9 -5
  24. package/dist/worker/delivery/verification-bundle.js +26 -5
  25. package/dist/worker/feature/advance.js +301 -0
  26. package/dist/worker/feature/doctor.js +223 -0
  27. package/dist/worker/feature/next-action.js +11 -3
  28. package/dist/worker/feature/scaffold.js +798 -0
  29. package/dist/worker/observe/dag-run-artifacts.js +90 -0
  30. package/dist/worker/observe/node-input.js +444 -0
  31. package/dist/worker/observe/routes.js +17 -0
  32. package/dist/worker/observe/static/api.js +9 -0
  33. package/dist/worker/observe/static/constants.js +9 -0
  34. package/dist/worker/observe/static/state.js +14 -0
  35. package/dist/worker/observe/static/styles.css +74 -0
  36. package/dist/worker/observe/static/views/dag-inspector.js +371 -15
  37. package/dist/worker/outcomes/projector.js +5 -1
  38. package/dist/worker/runner/run-ready.js +41 -1
  39. package/dist/workflows/dag/backend-test-markdown-workflow.js +202 -9
  40. package/dist/workflows/dag/backend-test-result-contract.js +103 -0
  41. package/dist/workflows/dag/failure-category.js +5 -1
  42. package/dist/workflows/dag/frontend-implementation-contract.js +4 -0
  43. package/dist/workflows/dag/frontend-prewrite-gate.js +0 -17
  44. package/dist/workflows/dag/frontend-test-result-contract.js +106 -41
  45. package/dist/workflows/dag/init-hybrid.js +220 -32
  46. package/dist/workflows/dag/node-execution.js +3 -2
  47. package/dist/workflows/dag/reconcile-run.js +24 -0
  48. package/dist/workflows/dag/types.js +46 -0
  49. package/dist/workflows/dag/validate.js +19 -2
  50. package/docs/architecture/dag-execution.md +5 -1
  51. package/docs/architecture/runtime-boundaries.md +11 -1
  52. package/docs/architecture/worker-and-feature.md +2 -0
  53. package/docs/templates/backend-test-dag.json +3 -3
  54. package/docs/templates/product-line/README.md +17 -1
  55. package/docs/templates/product-line/feature-scaffold-batch.example.yaml +31 -0
  56. package/docs/templates/product-line/scaffold-samples/README.md +36 -0
  57. package/docs/templates/product-line/scaffold-samples/backend-only/acceptance.yaml +13 -0
  58. package/docs/templates/product-line/scaffold-samples/backend-only/design.md +14 -0
  59. package/docs/templates/product-line/scaffold-samples/backend-only/feature.yaml +3 -0
  60. package/docs/templates/product-line/scaffold-samples/backend-only/requirement.md +6 -0
  61. package/docs/templates/product-line/scaffold-samples/backend-only/tasks/BE-IMPL-001.yaml +83 -0
  62. package/docs/templates/product-line/scaffold-samples/backend-only/tasks/task-graph.yaml +14 -0
  63. package/docs/templates/product-line/scaffold-samples/fe-with-api/acceptance.yaml +15 -0
  64. package/docs/templates/product-line/scaffold-samples/fe-with-api/design.md +18 -0
  65. package/docs/templates/product-line/scaffold-samples/fe-with-api/feature.yaml +3 -0
  66. package/docs/templates/product-line/scaffold-samples/fe-with-api/requirement.md +6 -0
  67. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/BE-IMPL-001.yaml +84 -0
  68. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/CONTRACT-001.yaml +84 -0
  69. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/FE-IMPL-001.yaml +86 -0
  70. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/task-graph.yaml +40 -0
  71. package/docs/templates/product-line/scaffold-samples/frontend-only/acceptance.yaml +13 -0
  72. package/docs/templates/product-line/scaffold-samples/frontend-only/design.md +14 -0
  73. package/docs/templates/product-line/scaffold-samples/frontend-only/feature.yaml +3 -0
  74. package/docs/templates/product-line/scaffold-samples/frontend-only/requirement.md +6 -0
  75. package/docs/templates/product-line/scaffold-samples/frontend-only/tasks/FE-IMPL-001.yaml +85 -0
  76. package/docs/templates/product-line/scaffold-samples/frontend-only/tasks/task-graph.yaml +14 -0
  77. package/examples/l5-report-coms-process-definition.html +322 -0
  78. package/package.json +1 -1
  79. package/skills/agent-worker/references/agent-worker-operator.md +84 -0
  80. package/skills/frontend-implementation/references/node-contracts.md +2 -2
  81. package/skills/loop-agent/SKILL.md +8 -8
  82. package/skills/loop-agent/references/command-reference.md +14 -2
  83. package/skills/loop-agent/references/harness-policy.md +22 -0
  84. package/skills/loop-agent/references/task-workflow.md +5 -0
@@ -0,0 +1,322 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, lstatSync, readdirSync, readFileSync, statSync, } from "node:fs";
3
+ import path from "node:path";
4
+ import { readStaticPomFile, repoRelativePosix } from "./pom-static.js";
5
+ import { MAVEN_CACHE_SCHEMA_VERSION, MAVEN_PLANNER_VERSION, MavenPlanError, } from "./types.js";
6
+ const SKIP_DIR_NAMES = new Set([
7
+ ".git",
8
+ ".harness",
9
+ "node_modules",
10
+ "target",
11
+ "dist",
12
+ "build",
13
+ ".idea",
14
+ ".vscode",
15
+ "coverage",
16
+ ".turbo",
17
+ ".next",
18
+ ]);
19
+ const WRAPPER_BASENAMES = new Set([
20
+ "mvnw",
21
+ "mvnw.cmd",
22
+ "mvnw.bat",
23
+ "maven-wrapper.properties",
24
+ "maven-wrapper.jar",
25
+ ]);
26
+ function toPosix(value) {
27
+ return value.replace(/\\/g, "/");
28
+ }
29
+ /**
30
+ * Walk repository files using Dirent and do not follow directory/file symlinks.
31
+ * Captures every successfully scanned directory in the same pass so cache-hit
32
+ * checks can detect a POM or wrapper added below a previously empty directory.
33
+ */
34
+ function walkFiles(root) {
35
+ const files = [];
36
+ const directories = [];
37
+ const stack = [root];
38
+ while (stack.length > 0) {
39
+ const current = stack.pop();
40
+ let entries;
41
+ try {
42
+ entries = readdirSync(current, { withFileTypes: true });
43
+ }
44
+ catch {
45
+ continue;
46
+ }
47
+ directories.push({
48
+ path: current,
49
+ entryNames: entries
50
+ .filter((entry) => !entry.isSymbolicLink() && !SKIP_DIR_NAMES.has(entry.name))
51
+ .map((entry) => entry.name)
52
+ .sort(),
53
+ });
54
+ for (const entry of entries) {
55
+ if (SKIP_DIR_NAMES.has(entry.name))
56
+ continue;
57
+ // Never follow symlinks (dir or file).
58
+ if (entry.isSymbolicLink())
59
+ continue;
60
+ const absolute = path.join(current, entry.name);
61
+ // Double-check with lstat in case platform Dirent is incomplete.
62
+ try {
63
+ const lst = lstatSync(absolute);
64
+ if (lst.isSymbolicLink())
65
+ continue;
66
+ if (lst.isDirectory()) {
67
+ stack.push(absolute);
68
+ }
69
+ else if (lst.isFile()) {
70
+ files.push(absolute);
71
+ }
72
+ }
73
+ catch {
74
+ continue;
75
+ }
76
+ }
77
+ }
78
+ return { files, directories };
79
+ }
80
+ function isPomFile(filePath) {
81
+ return path.basename(filePath).toLowerCase() === "pom.xml";
82
+ }
83
+ function isWrapperFile(filePath) {
84
+ const base = path.basename(filePath).toLowerCase();
85
+ if (WRAPPER_BASENAMES.has(base))
86
+ return true;
87
+ const posix = toPosix(filePath).toLowerCase();
88
+ return posix.endsWith("/.mvn/wrapper/maven-wrapper.properties");
89
+ }
90
+ function contentSha(absolutePath) {
91
+ const buf = readFileSync(absolutePath);
92
+ return createHash("sha256").update(buf).digest("hex");
93
+ }
94
+ function resolveModulePomPath(repoRoot, parentDir, moduleDecl) {
95
+ const absDir = path.resolve(repoRoot, parentDir, moduleDecl);
96
+ const candidates = [
97
+ path.join(absDir, "pom.xml"),
98
+ // moduleDecl may already point at a pom file
99
+ absDir.endsWith("pom.xml") ? absDir : null,
100
+ ].filter((value) => Boolean(value));
101
+ for (const candidate of candidates) {
102
+ try {
103
+ const lst = lstatSync(candidate);
104
+ if (lst.isSymbolicLink())
105
+ continue;
106
+ if (lst.isFile()) {
107
+ return repoRelativePosix(repoRoot, candidate);
108
+ }
109
+ }
110
+ catch {
111
+ // try next
112
+ }
113
+ }
114
+ return null;
115
+ }
116
+ /**
117
+ * Build reactor graphs with transitive nested aggregator membership.
118
+ * -pl coordinates are module paths relative to the reactor root, not bare artifactIds.
119
+ */
120
+ function buildReactors(repoRoot, nodes) {
121
+ const byPom = new Map(nodes.map((node) => [node.pomPath, node]));
122
+ const reactors = [];
123
+ // Prefer outermost reactor root for each member when nested.
124
+ const reactorRootByPomPath = {};
125
+ const rootDepth = new Map();
126
+ for (const node of nodes) {
127
+ if (node.modules.length === 0)
128
+ continue;
129
+ const modulePomByArtifactId = {};
130
+ const artifactIdByPomPath = {
131
+ [node.pomPath]: node.artifactId,
132
+ };
133
+ const modulePathByPomPath = {};
134
+ const pomPaths = new Set([node.pomPath]);
135
+ const queue = node.modules.map((moduleDecl) => ({
136
+ parentNode: node,
137
+ moduleDecl,
138
+ pathFromRoot: toPosix(moduleDecl).replace(/\/+$/, ""),
139
+ stack: [node.pomPath],
140
+ }));
141
+ const seenEdges = new Set();
142
+ while (queue.length > 0) {
143
+ const item = queue.shift();
144
+ const edgeKey = `${item.parentNode.pomPath}=>${item.moduleDecl}`;
145
+ if (seenEdges.has(edgeKey))
146
+ continue;
147
+ seenEdges.add(edgeKey);
148
+ if (item.stack.includes(item.parentNode.pomPath) && item.stack.length > 1) {
149
+ // stack already contains parent from deeper cycle checks below
150
+ }
151
+ const modulePom = resolveModulePomPath(repoRoot, item.parentNode.dir, item.moduleDecl);
152
+ if (!modulePom) {
153
+ throw new MavenPlanError(`Reactor module path cannot be resolved statically from ${item.parentNode.pomPath}: ${item.moduleDecl}`, {
154
+ code: "maven-reactor-module-missing",
155
+ candidates: [item.parentNode.pomPath, item.moduleDecl],
156
+ });
157
+ }
158
+ if (item.stack.includes(modulePom)) {
159
+ throw new MavenPlanError(`Cyclic reactor module relationship detected involving ${modulePom}`, {
160
+ code: "maven-reactor-cycle",
161
+ candidates: [...item.stack, modulePom],
162
+ });
163
+ }
164
+ const moduleNode = byPom.get(modulePom);
165
+ if (!moduleNode) {
166
+ throw new MavenPlanError(`Reactor module POM was not discovered: ${modulePom}`, { code: "maven-reactor-module-missing", candidates: [modulePom] });
167
+ }
168
+ // module path relative to reactor root (for -pl)
169
+ const modulePath = item.pathFromRoot
170
+ .replace(/\/pom\.xml$/i, "")
171
+ .replace(/^\.\//, "");
172
+ if (modulePomByArtifactId[moduleNode.artifactId] &&
173
+ modulePomByArtifactId[moduleNode.artifactId] !== modulePom) {
174
+ throw new MavenPlanError(`Duplicate artifactId '${moduleNode.artifactId}' in reactor ${node.pomPath}`, {
175
+ code: "maven-reactor-duplicate-artifactId",
176
+ candidates: [
177
+ modulePomByArtifactId[moduleNode.artifactId],
178
+ modulePom,
179
+ ],
180
+ });
181
+ }
182
+ modulePomByArtifactId[moduleNode.artifactId] = modulePom;
183
+ artifactIdByPomPath[modulePom] = moduleNode.artifactId;
184
+ modulePathByPomPath[modulePom] = modulePath;
185
+ pomPaths.add(modulePom);
186
+ // Recurse into nested aggregator modules.
187
+ for (const childDecl of moduleNode.modules) {
188
+ const childPath = toPosix(path.posix.join(modulePath, toPosix(childDecl))).replace(/\\/g, "/");
189
+ queue.push({
190
+ parentNode: moduleNode,
191
+ moduleDecl: childDecl,
192
+ pathFromRoot: childPath,
193
+ stack: [...item.stack, modulePom],
194
+ });
195
+ }
196
+ }
197
+ const reactor = {
198
+ rootPomPath: node.pomPath,
199
+ modulePomByArtifactId,
200
+ artifactIdByPomPath,
201
+ // Store module path coordinates for -pl via artifactIdByPomPath values when
202
+ // consumers need paths: expose via modulePathByPomPath on the side channel.
203
+ pomPaths: [...pomPaths].sort(),
204
+ // Extended field kept optional for schema compatibility via cast below.
205
+ ...(Object.keys(modulePathByPomPath).length > 0
206
+ ? { modulePathByPomPath }
207
+ : {}),
208
+ };
209
+ // Attach modulePathByPomPath for planners (types extended separately).
210
+ reactor
211
+ .modulePathByPomPath = modulePathByPomPath;
212
+ reactors.push(reactor);
213
+ // Register membership: prefer outermost (shallower path depth) reactor root.
214
+ const depth = node.pomPath.split("/").length;
215
+ for (const member of pomPaths) {
216
+ const existing = reactorRootByPomPath[member];
217
+ if (!existing) {
218
+ reactorRootByPomPath[member] = node.pomPath;
219
+ rootDepth.set(member, depth);
220
+ continue;
221
+ }
222
+ const existingDepth = rootDepth.get(member) ?? Number.MAX_SAFE_INTEGER;
223
+ if (depth < existingDepth) {
224
+ reactorRootByPomPath[member] = node.pomPath;
225
+ rootDepth.set(member, depth);
226
+ }
227
+ }
228
+ }
229
+ return { reactors, reactorRootByPomPath };
230
+ }
231
+ /**
232
+ * Statically discover all POMs and wrappers under the repository.
233
+ * Never executes Maven goals. Does not follow symlinks.
234
+ */
235
+ /** Test/probe counter: increments only on full walk+parse builds. */
236
+ let fullBuildCount = 0;
237
+ export function getMavenFullBuildCount() {
238
+ return fullBuildCount;
239
+ }
240
+ export function resetMavenFullBuildCount() {
241
+ fullBuildCount = 0;
242
+ }
243
+ export function buildMavenWorkspaceGraph(repoRoot) {
244
+ if (!existsSync(repoRoot)) {
245
+ throw new MavenPlanError(`Repository root does not exist: ${repoRoot}`, {
246
+ code: "maven-repo-missing",
247
+ });
248
+ }
249
+ fullBuildCount += 1;
250
+ const walk = walkFiles(repoRoot);
251
+ const allFiles = walk.files;
252
+ const pomAbs = allFiles.filter(isPomFile).sort();
253
+ const wrapperAbs = allFiles.filter(isWrapperFile).sort();
254
+ const nodes = [];
255
+ const fingerprintParts = [];
256
+ const fileMeta = [];
257
+ const dirMeta = walk.directories.map((directory) => ({
258
+ path: directory.path === repoRoot
259
+ ? ""
260
+ : repoRelativePosix(repoRoot, directory.path),
261
+ entryNames: directory.entryNames,
262
+ }));
263
+ for (const abs of pomAbs) {
264
+ const pomPath = repoRelativePosix(repoRoot, abs);
265
+ const fields = readStaticPomFile(abs);
266
+ const st = statSync(abs);
267
+ const dir = toPosix(path.posix.dirname(pomPath));
268
+ nodes.push({
269
+ pomPath,
270
+ dir: dir === "." ? "" : dir,
271
+ artifactId: fields.artifactId,
272
+ groupId: fields.groupId,
273
+ packaging: fields.packaging,
274
+ modules: fields.modules,
275
+ parentRelativePath: fields.parentRelativePath,
276
+ });
277
+ const sha = contentSha(abs);
278
+ fingerprintParts.push(`pom:${pomPath}:${sha}`);
279
+ fileMeta.push({
280
+ path: pomPath,
281
+ size: st.size,
282
+ mtimeMs: st.mtimeMs,
283
+ });
284
+ }
285
+ const wrapperPaths = [];
286
+ for (const abs of wrapperAbs) {
287
+ const rel = repoRelativePosix(repoRoot, abs);
288
+ wrapperPaths.push(rel);
289
+ const st = statSync(abs);
290
+ const sha = contentSha(abs);
291
+ fingerprintParts.push(`wrapper:${rel}:${sha}`);
292
+ fileMeta.push({
293
+ path: rel,
294
+ size: st.size,
295
+ mtimeMs: st.mtimeMs,
296
+ });
297
+ }
298
+ fingerprintParts.sort();
299
+ const fingerprint = createHash("sha256")
300
+ .update(fingerprintParts.join("\n"))
301
+ .update(`\nplanner:${MAVEN_PLANNER_VERSION}`)
302
+ .update(`\nschema:${MAVEN_CACHE_SCHEMA_VERSION}`)
303
+ .digest("hex");
304
+ const { reactors, reactorRootByPomPath } = buildReactors(repoRoot, nodes);
305
+ return {
306
+ schemaVersion: MAVEN_CACHE_SCHEMA_VERSION,
307
+ plannerVersion: MAVEN_PLANNER_VERSION,
308
+ fingerprint,
309
+ pomPaths: nodes.map((n) => n.pomPath).sort(),
310
+ wrapperPaths: wrapperPaths.sort(),
311
+ nodes,
312
+ reactors,
313
+ reactorRootByPomPath,
314
+ manifest: {
315
+ files: fileMeta.sort((a, b) => a.path.localeCompare(b.path)),
316
+ directories: dirMeta.sort((a, b) => a.path.localeCompare(b.path)),
317
+ },
318
+ };
319
+ }
320
+ export function computeWorkspaceFingerprint(repoRoot) {
321
+ return buildMavenWorkspaceGraph(repoRoot).fingerprint;
322
+ }
@@ -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();
@@ -41,7 +44,8 @@ export function buildAgentWorkerProgram() {
41
44
  .description("Local product-line worker utilities for TaskSpec validation")
42
45
  .version(readPackageVersion(path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))))) ?? "0.0.0", "-V, --version", "display version")
43
46
  .showHelpAfterError()
44
- .showSuggestionAfterError();
47
+ .showSuggestionAfterError()
48
+ .enablePositionalOptions();
45
49
  const task = program.command("task").description("TaskSpec utilities");
46
50
  const batch = program
47
51
  .command("batch")
@@ -244,6 +248,125 @@ export function buildAgentWorkerProgram() {
244
248
  if (result.status === "failed" || result.status === "needs-action")
245
249
  process.exitCode = 1;
246
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
+ });
247
370
  feature
248
371
  .command("verify-final")
249
372
  .requiredOption("--feature-dir <dir>", "Feature directory containing acceptance.yaml and tasks/")
@@ -283,18 +406,30 @@ export function buildAgentWorkerProgram() {
283
406
  .command("delivery")
284
407
  .requiredOption("--feature-dir <dir>", "Feature directory containing acceptance.yaml and tasks/")
285
408
  .requiredOption("--repo <repo-root>", "Target repo root")
286
- .requiredOption("--qa-evidence <path>", "Repo-local QA pass evidence")
287
- .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)")
288
411
  .option("--waivers <path>", "Repo-local acceptance waiver decisions JSON")
289
412
  .option("--dry-run", "Validate and show the Delivery Package plan without writes")
290
413
  .option("--json", "Emit stable JSON")
291
414
  .description("Validate checkpointed Feature evidence and prepare a local Delivery Package")
292
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
+ }
293
428
  const result = await prepareFeatureDelivery({
294
- featureDir: path.resolve(options.featureDir),
295
- repoRoot: path.resolve(options.repo),
296
- qaEvidencePath: options.qaEvidence,
297
- finalVerificationPath: options.finalVerification,
429
+ featureDir,
430
+ repoRoot,
431
+ qaEvidencePath,
432
+ finalVerificationPath,
298
433
  ...(options.waivers ? { waiverPath: options.waivers } : {}),
299
434
  dryRun: options.dryRun ?? false,
300
435
  });
@@ -593,18 +728,17 @@ export function buildAgentWorkerProgram() {
593
728
  process.stdout.write(`${JSON.stringify(snapshot)}\n`);
594
729
  });
595
730
  consoleCmd
596
- .command("serve")
597
- .requiredOption("--repo <repo-root>", "Target repo root")
731
+ .description("Loop Operator Console (local); bare `agent-worker console` starts it directly (repo defaults to current working directory, port defaults to 8790)")
732
+ .enablePositionalOptions()
733
+ .passThroughOptions()
734
+ .option("--repo <repo-root>", "Target repo root (default: current working directory)", process.cwd())
598
735
  .option("--port <port>", "HTTP port", "8790")
599
736
  .option("--host <host>", "Bind host (default 127.0.0.1; use 0.0.0.0 for LAN, no network auth)", "127.0.0.1")
600
737
  .option("--debug", "Log request source/method/path/status to stderr")
601
738
  .option("--app-data <path>", "Override Console application-data root (Draft/Operation/confirmation)")
602
- .description("Start Loop Operator Console HTTP server (Operate + Inspect; static SPA + Operator API)")
603
739
  .action(async (options) => {
604
- const { createConsoleServer } = await import("./console/server.js");
605
- const repoRoot = path.resolve(options.repo);
606
- const server = await createConsoleServer({
607
- repoRoot,
740
+ await runConsoleServe({
741
+ repoRoot: path.resolve(options.repo),
608
742
  host: options.host,
609
743
  port: Number.parseInt(options.port, 10),
610
744
  debug: options.debug === true,
@@ -612,13 +746,25 @@ export function buildAgentWorkerProgram() {
612
746
  ? path.resolve(options.appData)
613
747
  : undefined,
614
748
  });
615
- process.stdout.write(`${server.url}\n`);
616
- await new Promise((resolve) => {
617
- const shutdown = () => resolve();
618
- process.once("SIGINT", shutdown);
619
- process.once("SIGTERM", shutdown);
749
+ });
750
+ consoleCmd
751
+ .command("serve")
752
+ .option("--repo <repo-root>", "Target repo root (default: current working directory)", process.cwd())
753
+ .option("--port <port>", "HTTP port", "8790")
754
+ .option("--host <host>", "Bind host (default 127.0.0.1; use 0.0.0.0 for LAN, no network auth)", "127.0.0.1")
755
+ .option("--debug", "Log request source/method/path/status to stderr")
756
+ .option("--app-data <path>", "Override Console application-data root (Draft/Operation/confirmation)")
757
+ .description("Start Loop Operator Console HTTP server (compat entry; equivalent to bare `agent-worker console`)")
758
+ .action(async (options) => {
759
+ await runConsoleServe({
760
+ repoRoot: path.resolve(options.repo),
761
+ host: options.host,
762
+ port: Number.parseInt(options.port, 10),
763
+ debug: options.debug === true,
764
+ appDataRoot: options.appData
765
+ ? path.resolve(options.appData)
766
+ : undefined,
620
767
  });
621
- await server.close();
622
768
  });
623
769
  consoleCmd
624
770
  .command("doctor")
@@ -693,11 +839,80 @@ export async function main(argv = process.argv) {
693
839
  async function readTaskYaml(taskSpecPath) {
694
840
  return YAML.parse(await readFile(taskSpecPath, "utf-8"));
695
841
  }
842
+ /** Shared console serve entry used by both bare `console` and `console serve`. */
843
+ async function runConsoleServe(options) {
844
+ const { createConsoleServer } = await import("./console/server.js");
845
+ try {
846
+ const server = await createConsoleServer({
847
+ repoRoot: options.repoRoot,
848
+ host: options.host,
849
+ port: options.port,
850
+ debug: options.debug === true,
851
+ appDataRoot: options.appDataRoot,
852
+ });
853
+ process.stdout.write(`${server.url}\n`);
854
+ await new Promise((resolve) => {
855
+ const shutdown = () => resolve();
856
+ process.once("SIGINT", shutdown);
857
+ process.once("SIGTERM", shutdown);
858
+ });
859
+ await server.close();
860
+ }
861
+ catch (error) {
862
+ const detail = error instanceof Error ? error.message : String(error);
863
+ const code = error?.code;
864
+ const isPortInUse = code === "EADDRINUSE" ||
865
+ /listen EADDRINUSE/i.test(detail) ||
866
+ /address already in use/i.test(detail);
867
+ if (isPortInUse) {
868
+ process.stderr.write(`端口 ${options.port} 已被占用,请使用 --port <port> 指定其它端口后重试。\n`);
869
+ process.exit(1);
870
+ }
871
+ process.stderr.write(`${detail}\n`);
872
+ process.exit(1);
873
+ }
874
+ }
696
875
  function handlePoolRecoveryCliError(error) {
697
876
  const detail = error instanceof Error ? error.message : String(error);
698
877
  process.stderr.write(`${detail}\n`);
699
878
  process.exitCode = 1;
700
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
+ }
701
916
  function handleFollowUpCliError(error, json, action) {
702
917
  const detail = error instanceof Error ? error.message : String(error);
703
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,