@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
@@ -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,
@@ -101,11 +101,12 @@ export async function finalizeGitTask(transaction, outcome) {
101
101
  await assertTransactionPosition(transaction.repoRoot, current);
102
102
  const currentIgnored = await readIgnoredBaseline(transaction.repoRoot);
103
103
  const ignoredBaselineByPath = new Map(current.ignoredBaseline.map((entry) => [entry.path, entry.sha256]));
104
- const changedBaselineIgnored = currentIgnored.filter((entry) => ignoredBaselineByPath.has(entry.path) && ignoredBaselineByPath.get(entry.path) !== entry.sha256).map((entry) => entry.path);
105
- const missingBaselineIgnored = current.ignoredBaseline.filter((entry) => !currentIgnored.some((candidate) => candidate.path === entry.path)).map((entry) => entry.path);
106
- if (changedBaselineIgnored.length > 0 || missingBaselineIgnored.length > 0) {
107
- throw new Error(`pre-existing ignored file changed during Git transaction: ${[...changedBaselineIgnored, ...missingBaselineIgnored].join(", ")}`);
108
- }
104
+ const changedBaselineIgnored = currentIgnored
105
+ .filter((entry) => ignoredBaselineByPath.has(entry.path) && ignoredBaselineByPath.get(entry.path) !== entry.sha256)
106
+ .map((entry) => entry.path);
107
+ const missingBaselineIgnored = current.ignoredBaseline
108
+ .filter((entry) => !currentIgnored.some((candidate) => candidate.path === entry.path))
109
+ .map((entry) => entry.path);
109
110
  const existing = current.checkpoints.find((entry) => entry.workerRunId === outcome.workerRunId);
110
111
  if (outcome.status === "reused" || existing) {
111
112
  if (!existing)
@@ -123,12 +124,28 @@ export async function finalizeGitTask(transaction, outcome) {
123
124
  const newIgnored = currentIgnored.map((entry) => entry.path).filter((entry) => !ignoredBaselineByPath.has(entry));
124
125
  if (outcome.status === "succeeded") {
125
126
  const allowedPaths = expandAllowedPathsForWorkflow(resolveWorkflow(outcome.taskSpec).workflow, outcome.taskSpec.constraints.allowed_paths);
127
+ const isAllowedEvidencePath = (entry) => !isSensitivePath(entry) &&
128
+ !isEphemeralToolCachePath(entry) &&
129
+ allowedPaths.some((glob) => matchesGlob(entry, glob));
130
+ // Pre-existing ignored evidence under allowed_paths may be rewritten by
131
+ // FINAL-VERIFY smoke / verify-final and must force-add into the checkpoint.
132
+ // Other baseline ignored mutations remain fail-closed (e.g. dist/, secrets).
133
+ const blockedBaselineChanged = changedBaselineIgnored.filter((entry) => !isAllowedEvidencePath(entry));
134
+ const blockedBaselineMissing = missingBaselineIgnored.filter((entry) => !isEphemeralToolCachePath(entry) && !isAllowedEvidencePath(entry));
135
+ if (blockedBaselineChanged.length > 0 || blockedBaselineMissing.length > 0) {
136
+ throw new Error(`pre-existing ignored file changed during Git transaction: ${[...blockedBaselineChanged, ...blockedBaselineMissing].join(", ")}`);
137
+ }
126
138
  // Evidence under allowed_paths may be gitignored for local convenience (e.g.
127
139
  // reports/welcome/** smoke JSON) but must still checkpoint for Task Pool Done.
128
140
  // Sensitive / out-of-scope ignored files remain fail-closed; ephemeral tool
129
141
  // caches neither block nor force-add.
130
142
  const forceAddIgnored = [];
131
143
  const blockedIgnored = [];
144
+ const rewrittenBaselineEvidence = changedBaselineIgnored.filter((entry) => isAllowedEvidencePath(entry));
145
+ for (const entry of rewrittenBaselineEvidence) {
146
+ if (!forceAddIgnored.includes(entry))
147
+ forceAddIgnored.push(entry);
148
+ }
132
149
  for (const entry of newIgnored) {
133
150
  if (isEphemeralToolCachePath(entry))
134
151
  continue;
@@ -137,7 +154,8 @@ export async function finalizeGitTask(transaction, outcome) {
137
154
  continue;
138
155
  }
139
156
  if (allowedPaths.some((glob) => matchesGlob(entry, glob))) {
140
- forceAddIgnored.push(entry);
157
+ if (!forceAddIgnored.includes(entry))
158
+ forceAddIgnored.push(entry);
141
159
  }
142
160
  else {
143
161
  blockedIgnored.push(entry);
@@ -166,7 +184,10 @@ export async function finalizeGitTask(transaction, outcome) {
166
184
  await assertTransactionPosition(transaction.repoRoot, { ...current, lastCheckpoint: commit });
167
185
  await assertCheckpointMetadata(transaction.repoRoot, commit, outcome.taskSpec, outcome.workerRunId);
168
186
  const afterCommitIgnored = await readIgnoredBaseline(transaction.repoRoot);
169
- assertIgnoredBaselineUnchanged(current.ignoredBaseline.filter((entry) => !isEphemeralToolCachePath(entry.path)), afterCommitIgnored.filter((entry) => !isEphemeralToolCachePath(entry.path)));
187
+ // force-add may promote allowed evidence from ignored → tracked; drop those
188
+ // paths from the post-commit baseline equality check.
189
+ const forceAdded = new Set(forceAddIgnored);
190
+ assertIgnoredBaselineUnchanged(current.ignoredBaseline.filter((entry) => !isEphemeralToolCachePath(entry.path) && !forceAdded.has(entry.path)), afterCommitIgnored.filter((entry) => !isEphemeralToolCachePath(entry.path) && !forceAdded.has(entry.path)));
170
191
  }
171
192
  catch (error) {
172
193
  const branch = await git(transaction.repoRoot, ["branch", "--show-current"]).catch(() => "");
@@ -199,6 +220,10 @@ export async function finalizeGitTask(transaction, outcome) {
199
220
  changedFiles: checkpointFiles,
200
221
  };
201
222
  }
223
+ // Failed / keep-diff paths still fail closed on unexpected baseline ignored mutations.
224
+ if (changedBaselineIgnored.length > 0 || missingBaselineIgnored.length > 0) {
225
+ throw new Error(`pre-existing ignored file changed during Git transaction: ${[...changedBaselineIgnored, ...missingBaselineIgnored].join(", ")}`);
226
+ }
202
227
  const artifactDir = path.join(path.dirname(transaction.recordPath), "failures", outcome.workerRunId);
203
228
  await captureFailureArtifacts(transaction.repoRoot, artifactDir, changes, newIgnored, outcome);
204
229
  if (outcome.keepFailedDiff) {
@@ -284,11 +284,15 @@ export async function validateDeliveryPackage(input) {
284
284
  blockers.push(`Acceptance coverage status is not derivable: ${ac.id}`);
285
285
  if (JSON.stringify([...item.blockedBy].sort()) !== JSON.stringify([...blockedBy].sort()))
286
286
  blockers.push(`Acceptance blockers changed: ${ac.id}`);
287
- for (const ref of expectedRefs)
288
- if (!item.evidence.some((candidate) => candidate.path === ref.path && candidate.sha256 === ref.sha256))
289
- blockers.push(`Acceptance run evidence is missing: ${ac.id}`);
290
- if (qa?.data.acIds.includes(ac.id) && !item.evidence.some((candidate) => candidate.path === qa.ref.path && candidate.sha256 === qa.ref.sha256))
291
- blockers.push(`Acceptance QA evidence is missing: ${ac.id}`);
287
+ // partial / missing / blocked should-ACs intentionally omit full evidence lists
288
+ // (only must ACs covered by QA require run+QA hash binding).
289
+ if (expectedStatus === "covered") {
290
+ for (const ref of expectedRefs)
291
+ if (!item.evidence.some((candidate) => candidate.path === ref.path && candidate.sha256 === ref.sha256))
292
+ blockers.push(`Acceptance run evidence is missing: ${ac.id}`);
293
+ if (qa?.data.acIds.includes(ac.id) && !item.evidence.some((candidate) => candidate.path === qa.ref.path && candidate.sha256 === qa.ref.sha256))
294
+ blockers.push(`Acceptance QA evidence is missing: ${ac.id}`);
295
+ }
292
296
  for (const ref of item.evidence)
293
297
  if (!await hashedRefValid(repoRoot, ref))
294
298
  blockers.push(`Acceptance evidence changed: ${item.acId}`);
@@ -404,7 +404,23 @@ export async function verifyBundleOutcomes(repoRoot, bundle) {
404
404
  return false;
405
405
  if (JSON.stringify(verified.artifacts) !== JSON.stringify(outcome.artifacts))
406
406
  return false;
407
- if (!await verifyArtifactRefs(repoRoot, outcome.artifacts))
407
+ // Rehash only durable typed evidence. run_record/dag_json may be rewritten
408
+ // after outcome projection (worker-run-record appends report commands).
409
+ const evidenceArtifacts = outcome.artifacts.filter((artifact) => {
410
+ const kind = artifact.kind ?? "";
411
+ if (kind === "run_record" || kind === "dag_json")
412
+ return false;
413
+ if (kind === "backend-test-result" ||
414
+ kind === "frontend-test-result" ||
415
+ kind.includes("backend-test-result") ||
416
+ kind.includes("frontend-test-result")) {
417
+ return true;
418
+ }
419
+ // Unknown kinds: still rehash unless clearly infrastructure paths.
420
+ return (!artifact.path.includes("worker-run-record") &&
421
+ !artifact.path.includes("-dag.json"));
422
+ });
423
+ if (!await verifyArtifactRefs(repoRoot, evidenceArtifacts))
408
424
  return false;
409
425
  }
410
426
  return true;
@@ -417,9 +433,12 @@ export function verifyBundleTaskSpecBindings(bundle, specs) {
417
433
  return false;
418
434
  taskIds.add(outcome.taskId);
419
435
  const spec = specs.get(outcome.taskId);
436
+ // Typed verification tasks may be qa-execute (frontend-test) or qa-testcode
437
+ // (backend-test Markdown-first). Delivery binds on feature_id + workflow.
438
+ const typedVerificationTypes = new Set(["qa-execute", "qa-testcode"]);
420
439
  if (!spec ||
421
440
  spec.feature_id !== bundle.featureId ||
422
- spec.type !== "qa-execute" ||
441
+ !typedVerificationTypes.has(spec.type) ||
423
442
  spec.execution?.workflow !== outcome.workflow)
424
443
  return false;
425
444
  }
@@ -460,9 +479,11 @@ async function verifyArtifactRefs(repoRoot, artifacts) {
460
479
  return false;
461
480
  }
462
481
  for (const artifact of artifacts) {
463
- if (path.isAbsolute(artifact.path))
464
- return false;
465
- const lexical = path.resolve(canonicalRepoRoot, artifact.path);
482
+ // Accept absolute paths only when they resolve inside the repo (legacy
483
+ // envelopes may store abs paths). Prefer relative for new projections.
484
+ const lexical = path.isAbsolute(artifact.path)
485
+ ? path.resolve(artifact.path)
486
+ : path.resolve(canonicalRepoRoot, artifact.path);
466
487
  let resolved;
467
488
  let content;
468
489
  try {