@tea-agent/loop-agent 0.21.0 → 0.23.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 (108) hide show
  1. package/AGENTS.md +42 -108
  2. package/CHANGELOG.md +114 -0
  3. package/README.md +7 -4
  4. package/bin/agent-worker.js +0 -0
  5. package/dist/adapters/loop-agent.js +52 -0
  6. package/dist/application/context-usage/skill-resolution-stats.js +263 -0
  7. package/dist/application/dag/generate-task-dag.js +17 -3
  8. package/dist/cli/command-definitions.js +8 -7
  9. package/dist/cli/program.js +17 -15
  10. package/dist/commands/doctor.js +269 -18
  11. package/dist/commands/init.js +198 -86
  12. package/dist/commands/stats.js +40 -11
  13. package/dist/executors/dag-pi-executor.js +2 -0
  14. package/dist/executors/shell-executor.js +162 -19
  15. package/dist/shared/openspec-spec.js +49 -0
  16. package/dist/shared/operator/capabilities.js +11 -1
  17. package/dist/worker/console/app-data.js +4 -0
  18. package/dist/worker/console/chat/instruction-skills.js +217 -0
  19. package/dist/worker/console/chat/model-resolver.js +106 -0
  20. package/dist/worker/console/chat/pi-runtime.js +605 -0
  21. package/dist/worker/console/chat/resource-loader.js +66 -0
  22. package/dist/worker/console/chat/routes.js +357 -0
  23. package/dist/worker/console/chat/session-store.js +238 -0
  24. package/dist/worker/console/chat/tool-adapter.js +238 -0
  25. package/dist/worker/console/chat/tools.js +171 -0
  26. package/dist/worker/console/server.js +55 -0
  27. package/dist/worker/console/static/assets/index-DVl7Jxt5.js +25 -0
  28. package/dist/worker/console/static/assets/index-lVcIr9Ju.css +1 -0
  29. package/dist/worker/console/static/index.html +2 -2
  30. package/dist/worker/feature/profile-schema.js +1 -1
  31. package/dist/worker/observability/read-model.js +21 -1
  32. package/dist/worker/observe/spec-evidence.js +12 -15
  33. package/dist/worker/observe/static/dag-helpers.js +22 -0
  34. package/dist/worker/observe/static/views/dag.js +5 -0
  35. package/dist/workflows/dag/backend-test-markdown-workflow.js +37 -0
  36. package/dist/workflows/dag/frontend-implementation-contract.js +141 -32
  37. package/dist/workflows/dag/frontend-lint-baseline.js +471 -0
  38. package/dist/workflows/dag/frontend-prewrite-gate.js +79 -16
  39. package/dist/workflows/dag/frontend-project-capability.js +11 -8
  40. package/dist/workflows/dag/frontend-repair.js +6 -4
  41. package/dist/workflows/dag/frontend-review-context.js +67 -0
  42. package/dist/workflows/dag/frontend-test-case-quality.js +105 -0
  43. package/dist/workflows/dag/frontend-test-result-contract.js +71 -66
  44. package/dist/workflows/dag/frontend-verification-trace.js +31 -1
  45. package/dist/workflows/dag/frontend-worktree-diff.js +81 -6
  46. package/dist/workflows/dag/init-hybrid.js +412 -84
  47. package/dist/workflows/dag/node-execution.js +38 -1
  48. package/dist/workflows/dag/output-protocol.js +89 -0
  49. package/dist/workflows/dag/prompt.js +35 -1
  50. package/dist/workflows/dag/recovery-recommendation.js +45 -0
  51. package/dist/workflows/dag/report.js +28 -1
  52. package/dist/workflows/dag/rerun-task.js +1 -1
  53. package/dist/workflows/dag/scheduler.js +9 -0
  54. package/dist/workflows/dag/types.js +74 -1
  55. package/dist/workflows/dag/validate.js +55 -0
  56. package/docs/README.md +73 -156
  57. package/docs/architecture/README.md +3 -2
  58. package/docs/architecture/dag-execution.md +2 -2
  59. package/docs/architecture/evolution.md +14 -12
  60. package/docs/architecture/system-overview.md +1 -1
  61. package/docs/architecture/worker-and-feature.md +3 -3
  62. package/docs/governance/README.md +15 -0
  63. package/docs/{harness-methodology-debugging.md → governance/harness-methodology-debugging.md} +27 -3
  64. package/docs/init-surface.manifest.json +22 -4
  65. package/docs/operations/README.md +12 -0
  66. package/docs/{local-development-environment.md → operations/local-development-environment.md} +1 -1
  67. package/docs/skills/vetted-skill-registry.md +23 -3
  68. package/docs/templates/README.md +55 -0
  69. package/docs/templates/agent-dag.schema.json +15 -5
  70. package/docs/templates/backend-test-dag.json +1 -1
  71. package/docs/templates/evaluation/agents-map-slim-v1.candidate.json +9 -0
  72. package/docs/templates/evaluation/agents-map-slim-v1.md +87 -0
  73. package/docs/templates/evaluation/agents-map-verbose-v0.candidate.json +9 -0
  74. package/docs/templates/evaluation/agents-map-verbose-v0.md +153 -0
  75. package/docs/templates/frontend-implementation-contract.schema.json +4 -3
  76. package/docs/templates/frontend-test-case-checklist.md +6 -2
  77. package/docs/templates/frontend-test-dag.json +2 -2
  78. package/docs/templates/hybrid-dag.json +1 -1
  79. package/docs/templates/progress-log.md +9 -2
  80. package/harness.json +5 -5
  81. package/package.json +5 -5
  82. package/scripts/kb-bootstrap-init-skeleton.sh +2 -2
  83. package/skills/agent-worker/SKILL.md +1 -1
  84. package/skills/frontend-design-review/SKILL.md +12 -10
  85. package/skills/frontend-design-review/references/review-checklist.md +4 -4
  86. package/skills/frontend-implementation/SKILL.md +2 -2
  87. package/skills/frontend-implementation/references/code-standards.md +4 -3
  88. package/skills/frontend-implementation/references/design-spec.md +19 -14
  89. package/skills/frontend-implementation/references/node-contracts.md +2 -2
  90. package/skills/frontend-review/SKILL.md +15 -28
  91. package/skills/frontend-review/references/review-findings.md +16 -18
  92. package/skills/frontend-verification/SKILL.md +16 -13
  93. package/skills/frontend-verification/references/verification-checklist.md +18 -30
  94. package/skills/grill-with-docs/SKILL.md +44 -52
  95. package/skills/grill-with-docs/adr-format.md +37 -26
  96. package/skills/grill-with-docs/context-format.md +18 -26
  97. package/skills/loop-agent/SKILL.md +28 -112
  98. package/skills/loop-agent/references/command-reference.md +9 -3
  99. package/skills/loop-agent/references/harness-policy.md +3 -3
  100. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  101. package/skills/loop-agent/references/orchestrator-and-interventions.md +1 -1
  102. package/skills/loop-agent/references/task-workflow.md +2 -0
  103. package/skills/systematic-debugging/SKILL.md +20 -4
  104. package/skills/test-driven-development/SKILL.md +10 -3
  105. package/dist/worker/console/static/assets/index-CUDke82y.js +0 -18
  106. package/dist/worker/console/static/assets/index-wSEksVSO.css +0 -1
  107. /package/docs/{harness-methodology-tdd.md → governance/harness-methodology-tdd.md} +0 -0
  108. /package/docs/{harness-methodology-verification.md → governance/harness-methodology-verification.md} +0 -0
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { access, readdir, readFile, realpath, writeFile, } from "node:fs/promises";
3
+ import { existsSync, readFileSync } from "node:fs";
3
4
  import os from "node:os";
4
5
  import path from "node:path";
5
6
  import { assertValidDagSpec } from "./validate.js";
@@ -17,7 +18,8 @@ import { discoverProjectGovernancePresence } from "./project-governance-context.
17
18
  import { getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
18
19
  import { materializeTaskReferenceDocs } from "../../task/source-references.js";
19
20
  import { observeTaskContract } from "../../task/contract/observe.js";
20
- import { resolveVerifyPreset } from "../../executors/shell-verification.js";
21
+ import { dagHasWriterExecution } from "./task-contract-binding.js";
22
+ import { DEFAULT_VERIFY_TIMEOUT_MS, resolveVerifyPreset, } from "../../executors/shell-verification.js";
21
23
  import { resolveExecutorModelMatrices } from "../../executors/model-routing.js";
22
24
  import { normalizeTaskRequirementText, resolveTaskDagTemplateSelection, } from "./task-demand-routing.js";
23
25
  import { BACKEND_TEST_EXECUTION_DEFAULT_TEST_ROOT, buildBackendTestExecutionPreflightShellSnippet, } from "./backend-test-execution-contract.js";
@@ -555,7 +557,7 @@ export function hasApiDependency(sources) {
555
557
  /**
556
558
  * Resolve frontend Mock mode from capability seed, task config, and interface dependency analysis.
557
559
  *
558
- * Decision matrix (from docs/design/frontend-mock-data-workflow.md):
560
+ * Decision matrix (from docs/design/active/frontend-mock-data-workflow.md):
559
561
  *
560
562
  * | 接口/异步数据依赖 | 既有 Mock 服务 | policy | 结果 |
561
563
  * |---|---|---|---|
@@ -829,6 +831,9 @@ function extractFrontendMockVerifyCommandsFromMarkdown(input) {
829
831
  return commands.filter((command, index, all) => all.findIndex((candidate) => candidate.args.join("\0") === command.args.join("\0")) === index);
830
832
  }
831
833
  function chooseFrontendVerifyCommands(input) {
834
+ if (input.explicitCommands && input.explicitCommands.length > 0) {
835
+ return { commands: input.explicitCommands, commandSource: "inline" };
836
+ }
832
837
  if (input.parsedCommands.length > 0) {
833
838
  return { commands: input.parsedCommands, commandSource: "inline" };
834
839
  }
@@ -837,33 +842,132 @@ function chooseFrontendVerifyCommands(input) {
837
842
  }
838
843
  return { commandSource: "inline" };
839
844
  }
840
- function resolveDagVerifyStrategy(taskConfig) {
845
+ function classifyFrontendVerifyCommand(command) {
846
+ return /\b(typecheck|check-types|lint|eslint|tsc|build|check)\b/i.test(command) ||
847
+ /\bscripts[\\/]+ci(?:-tests)?\.sh\b/i.test(command)
848
+ ? "static"
849
+ : "behavior";
850
+ }
851
+ function buildExplicitFrontendVerifyCommands(taskConfig, repoRoot) {
852
+ const staticCommands = [];
853
+ const behaviorCommands = [];
854
+ if (!repoRoot)
855
+ return { staticCommands, behaviorCommands };
856
+ for (const command of taskConfig.verifyCommands) {
857
+ const verifyCommand = {
858
+ args: ["bash", "-lc", command.command],
859
+ cwd: repoRoot,
860
+ label: command.label,
861
+ timeoutMs: command.timeoutMs,
862
+ };
863
+ if (classifyFrontendVerifyCommand(command.command) === "static") {
864
+ staticCommands.push(verifyCommand);
865
+ }
866
+ else {
867
+ behaviorCommands.push(verifyCommand);
868
+ }
869
+ }
870
+ return { staticCommands, behaviorCommands };
871
+ }
872
+ function verifyCommandKey(command) {
873
+ return `${command.cwd}\0${command.args.join("\0")}`;
874
+ }
875
+ function resolveDagVerifyStrategy(taskConfig, defaultIntermediateQuotaWhenFull = "full") {
876
+ const explicitIntermediateQuota = taskConfig.dagVerifyStrategy?.intermediateQuota;
841
877
  return {
842
- intermediateQuota: taskConfig.dagVerifyStrategy?.intermediateQuota ?? taskConfig.verifyQuota,
878
+ intermediateQuota: explicitIntermediateQuota ??
879
+ (taskConfig.verifyQuota === "full"
880
+ ? defaultIntermediateQuotaWhenFull
881
+ : taskConfig.verifyQuota),
843
882
  finalQuota: "full",
844
883
  focusedCommandSource: taskConfig.dagVerifyStrategy?.focusedCommandSource ?? "adapter",
845
884
  };
846
885
  }
847
886
  function buildVerifyEvidence(input) {
887
+ const selectedCommands = input.commands && input.commands.length > 0 ? input.commands : undefined;
888
+ const commandCount = selectedCommands?.length ?? input.fallbackCommands.length;
848
889
  return {
849
890
  phase: input.phase,
850
891
  quota: input.quota,
851
892
  commandSource: input.commandSource,
852
- commandCount: input.commands?.length ?? input.fallbackCommands.length,
853
- commandLabels: input.commands?.map((command) => command.label) ?? input.fallbackCommands,
893
+ commandCount,
894
+ commandLabels: selectedCommands?.map((command) => command.label) ?? input.fallbackCommands,
895
+ commandTexts: input.commandTexts ?? [],
896
+ commandTimeoutMs: input.commandTimeoutMs,
897
+ totalTimeoutBudgetMs: commandCount * input.commandTimeoutMs,
854
898
  finalFullRequired: input.finalFullRequired,
855
899
  };
856
900
  }
901
+ function isFrontendLintVerifyCommand(command) {
902
+ const text = `${command.label}\n${command.args.join(" ")}`;
903
+ return /\b(?:lint|eslint)\b/i.test(text);
904
+ }
905
+ function isManagedCiWrapperVerifyCommand(command) {
906
+ const text = `${command.label}\n${command.args.join(" ")}`.replaceAll("\\", "/");
907
+ return /\bscripts\/ci(?:-tests)?\.sh\b/.test(text);
908
+ }
909
+ function repoHasNpmScript(repoRoot, scriptName) {
910
+ if (!repoRoot)
911
+ return false;
912
+ const packagePath = path.join(repoRoot, "package.json");
913
+ if (!existsSync(packagePath))
914
+ return false;
915
+ try {
916
+ const decoded = JSON.parse(readFileSync(packagePath, "utf8"));
917
+ return typeof decoded.scripts?.[scriptName] === "string";
918
+ }
919
+ catch {
920
+ return false;
921
+ }
922
+ }
923
+ function partitionFrontendStaticVerifyCommands(input) {
924
+ const commands = input.commands ?? [];
925
+ const lintCommands = commands.filter(isFrontendLintVerifyCommand);
926
+ const staticCommands = commands.filter((command) => !isFrontendLintVerifyCommand(command));
927
+ if (lintCommands.length === 0 &&
928
+ commands.some(isManagedCiWrapperVerifyCommand) &&
929
+ repoHasNpmScript(input.repoRoot, "lint")) {
930
+ lintCommands.push({
931
+ args: ["npm", "run", "lint"],
932
+ cwd: input.repoRoot,
933
+ label: "npm run lint",
934
+ });
935
+ }
936
+ return {
937
+ lint: {
938
+ ...(lintCommands.length > 0 ? { commands: lintCommands } : {}),
939
+ commandSource: input.commandSource,
940
+ },
941
+ static: {
942
+ ...(staticCommands.length > 0 ? { commands: staticCommands } : {}),
943
+ commandSource: input.commandSource,
944
+ },
945
+ };
946
+ }
947
+ function isFrontendTestPathPattern(entry) {
948
+ const segments = entry.replaceAll("\\", "/").split("/");
949
+ return segments.some((segment) => /^(?:test|tests|spec|specs|e2e|__tests__|__specs__)$/i.test(segment));
950
+ }
951
+ function replaceSourceSegmentWithTestSegment(entry) {
952
+ const normalized = entry.replaceAll("\\", "/");
953
+ const segments = normalized.split("/");
954
+ const sourceIndex = segments.findIndex((segment) => /^(?:src|source|app|lib)$/i.test(segment));
955
+ if (sourceIndex < 0)
956
+ return null;
957
+ const testSegments = [...segments];
958
+ testSegments[sourceIndex] = "test";
959
+ return testSegments.join("/");
960
+ }
857
961
  function deriveParallelScoutPaths(taskConfig) {
858
962
  const allowed = taskConfig.allowedPaths;
859
963
  if (allowed.length === 0) {
860
964
  return { srcPaths: ["**"], testPaths: ["**"] };
861
965
  }
862
- const srcPaths = allowed.filter((entry) => !entry.includes("/test/"));
863
- const explicitTestPaths = allowed.filter((entry) => entry.includes("/test/"));
966
+ const srcPaths = allowed.filter((entry) => !isFrontendTestPathPattern(entry));
967
+ const explicitTestPaths = allowed.filter(isFrontendTestPathPattern);
864
968
  const derivedTestPaths = allowed
865
- .filter((entry) => entry.includes("/src/"))
866
- .map((entry) => entry.replace("/src/", "/test/"));
969
+ .map(replaceSourceSegmentWithTestSegment)
970
+ .filter((entry) => Boolean(entry));
867
971
  return {
868
972
  srcPaths: srcPaths.length > 0 ? srcPaths : allowed,
869
973
  testPaths: explicitTestPaths.length > 0
@@ -873,6 +977,80 @@ function deriveParallelScoutPaths(taskConfig) {
873
977
  : allowed,
874
978
  };
875
979
  }
980
+ /**
981
+ * Resolve frontend verification fallbacks from the target project's own
982
+ * package scripts. The DAG builder is also used by unit fixtures without a
983
+ * package.json, so those fixtures retain the historical generic fallback.
984
+ * Real projects never inherit loop-agent's commands when package.json exists.
985
+ */
986
+ async function discoverFrontendFallbackVerifyCommands(repoRoot) {
987
+ const genericFallback = {
988
+ staticCommands: ["npm run typecheck", "npm run build"],
989
+ behaviorCommands: ["npm test"],
990
+ };
991
+ if (!repoRoot)
992
+ return genericFallback;
993
+ let scripts;
994
+ try {
995
+ const packageJson = JSON.parse(await readFile(path.join(repoRoot, "package.json"), "utf8"));
996
+ if (packageJson.scripts && typeof packageJson.scripts === "object") {
997
+ scripts = packageJson.scripts;
998
+ }
999
+ }
1000
+ catch {
1001
+ return genericFallback;
1002
+ }
1003
+ if (!scripts)
1004
+ return genericFallback;
1005
+ let packageManager = "npm";
1006
+ for (const [lockfile, manager] of [
1007
+ ["pnpm-lock.yaml", "pnpm"],
1008
+ ["yarn.lock", "yarn"],
1009
+ ["bun.lockb", "bun"],
1010
+ ["bun.lock", "bun"],
1011
+ ]) {
1012
+ try {
1013
+ await access(path.join(repoRoot, lockfile));
1014
+ packageManager = manager;
1015
+ break;
1016
+ }
1017
+ catch {
1018
+ // Try the next package-manager marker.
1019
+ }
1020
+ }
1021
+ const commandForScript = (script) => script === "test"
1022
+ ? `${packageManager} test`
1023
+ : `${packageManager} run ${script}`;
1024
+ const hasScript = (script) => typeof scripts?.[script] === "string" &&
1025
+ String(scripts[script]).trim().length > 0;
1026
+ const firstExisting = (names) => {
1027
+ const selected = [];
1028
+ for (const name of names) {
1029
+ if (hasScript(name) && !selected.includes(name))
1030
+ selected.push(name);
1031
+ }
1032
+ return selected.map(commandForScript);
1033
+ };
1034
+ const staticCommands = firstExisting([
1035
+ "typecheck",
1036
+ "check-types",
1037
+ "lint",
1038
+ "check",
1039
+ "build",
1040
+ ]);
1041
+ const behaviorCommands = firstExisting([
1042
+ "test:unit",
1043
+ "test:frontend",
1044
+ "test:component",
1045
+ "test",
1046
+ "test:e2e",
1047
+ "e2e",
1048
+ ]);
1049
+ return {
1050
+ staticCommands: staticCommands.length > 0 ? staticCommands : behaviorCommands,
1051
+ behaviorCommands: behaviorCommands.length > 0 ? behaviorCommands : staticCommands,
1052
+ };
1053
+ }
876
1054
  function deriveFrontendBehaviorPaths(taskConfig) {
877
1055
  if (taskConfig.allowedPaths.length === 0)
878
1056
  return ["**"];
@@ -1200,9 +1378,10 @@ export function buildStandardHybridDagFromTask(sources) {
1200
1378
  commands: finalVerifyCommands,
1201
1379
  fallbackCommands: [],
1202
1380
  finalFullRequired: true,
1381
+ commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
1203
1382
  }),
1204
1383
  cwd: ".",
1205
- timeoutMs: 300000,
1384
+ timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
1206
1385
  },
1207
1386
  },
1208
1387
  ]
@@ -1507,9 +1686,11 @@ function buildFrontendMockVerifyNode(sources, implementId, readOnlyPaths, forbid
1507
1686
  commandSource: commands.length > 0 ? "inline" : "adapter",
1508
1687
  commands: verifyCommands.length > 0 ? verifyCommands : undefined,
1509
1688
  fallbackCommands: [],
1689
+ commandTexts: commands,
1690
+ commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
1510
1691
  }),
1511
1692
  cwd: ".",
1512
- timeoutMs: 300000,
1693
+ timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
1513
1694
  },
1514
1695
  };
1515
1696
  }
@@ -1688,7 +1869,7 @@ function pruneFrontendTasksForRisk(tasks, risk) {
1688
1869
  return { ...task, depends_on };
1689
1870
  });
1690
1871
  }
1691
- function buildFrontendHybridDagFromTask(sources) {
1872
+ async function buildFrontendHybridDagFromTask(sources) {
1692
1873
  const { taskConfig } = sources;
1693
1874
  const mockCapability = sources.frontendMockCapability ?? {
1694
1875
  status: "absent",
@@ -1756,6 +1937,8 @@ function buildFrontendHybridDagFromTask(sources) {
1756
1937
  "- verificationTargets is a TOP-LEVEL required array",
1757
1938
  "- uiStates items use name/applicable/expectedBehavior/implementationTargets/verificationTargetIds/notApplicableReason",
1758
1939
  "- mockApi.productionDefaultOff must always be true (including strategy: not-needed)",
1940
+ "- All implementation files, verification files, symbols, and commands must be discovered from the current target workspace and current task. Never copy paths, symbols, or commands from the loop-agent repository, an example task, or prior run output.",
1941
+ "- Use relative POSIX paths rooted at the target workspace. Do not assume a particular src/test directory layout; preserve the target project's actual app/, packages/, spec/, __tests__, or other layout.",
1759
1942
  ].join("\n");
1760
1943
  })();
1761
1944
  const sourceContext = [
@@ -1797,26 +1980,42 @@ function buildFrontendHybridDagFromTask(sources) {
1797
1980
  if (mockMode === "blocked") {
1798
1981
  return buildBlockedFrontendMockDag(frontendSources, readOnlyPaths, forbiddenPaths, globalConstraints);
1799
1982
  }
1800
- const staticFallbackCommands = ["npm run typecheck", "npm run build"];
1801
- const behaviorFallbackCommands = ["npm test"];
1983
+ const fallbackVerifyCommands = await discoverFrontendFallbackVerifyCommands(sources.repoRoot);
1984
+ const staticFallbackCommands = fallbackVerifyCommands.staticCommands;
1985
+ const behaviorFallbackCommands = fallbackVerifyCommands.behaviorCommands;
1802
1986
  const parsedFrontendVerifyCommands = extractFrontendVerifyCommandsFromMarkdown({
1803
1987
  repoRoot: sources.repoRoot,
1804
1988
  requirementMarkdown: sources.requirementMarkdown,
1805
1989
  constraintMarkdown: sources.constraintMarkdown,
1806
1990
  });
1991
+ const explicitFrontendVerifyCommands = buildExplicitFrontendVerifyCommands(taskConfig, sources.repoRoot);
1992
+ const explicitCommandKeys = new Set([...explicitFrontendVerifyCommands.staticCommands, ...explicitFrontendVerifyCommands.behaviorCommands].map(verifyCommandKey));
1993
+ const adapterVerifyCommands = (sources.verifyCommands?.final ?? []).filter((command) => !explicitCommandKeys.has(verifyCommandKey(command)));
1807
1994
  const staticVerifyCommands = chooseFrontendVerifyCommands({
1995
+ explicitCommands: explicitFrontendVerifyCommands.staticCommands,
1808
1996
  parsedCommands: parsedFrontendVerifyCommands.staticCommands,
1809
- adapterCommands: sources.verifyCommands?.intermediate,
1997
+ adapterCommands: adapterVerifyCommands,
1998
+ });
1999
+ const partitionedStaticVerifyCommands = partitionFrontendStaticVerifyCommands({
2000
+ repoRoot: sources.repoRoot,
2001
+ commands: staticVerifyCommands.commands,
2002
+ commandSource: staticVerifyCommands.commandSource,
1810
2003
  });
1811
2004
  const behaviorVerifyCommands = chooseFrontendVerifyCommands({
2005
+ explicitCommands: explicitFrontendVerifyCommands.behaviorCommands,
1812
2006
  parsedCommands: parsedFrontendVerifyCommands.behaviorCommands,
1813
- adapterCommands: sources.verifyCommands?.final,
2007
+ adapterCommands: adapterVerifyCommands,
1814
2008
  });
1815
2009
  const staticShellCommands = buildVerifyShellCommands({
1816
2010
  repoRoot: sources.repoRoot,
1817
- commands: staticVerifyCommands.commands,
2011
+ commands: partitionedStaticVerifyCommands.static.commands,
1818
2012
  fallbackCommands: staticFallbackCommands,
1819
2013
  });
2014
+ const lintShellCommands = buildVerifyShellCommands({
2015
+ repoRoot: sources.repoRoot,
2016
+ commands: partitionedStaticVerifyCommands.lint.commands,
2017
+ fallbackCommands: [],
2018
+ });
1820
2019
  const behaviorShellCommands = buildVerifyShellCommands({
1821
2020
  repoRoot: sources.repoRoot,
1822
2021
  commands: behaviorVerifyCommands.commands,
@@ -1825,17 +2024,32 @@ function buildFrontendHybridDagFromTask(sources) {
1825
2024
  const staticVerifyEvidence = buildVerifyEvidence({
1826
2025
  phase: "intermediate",
1827
2026
  quota: strategy.intermediateQuota ?? "full",
1828
- commandSource: staticVerifyCommands.commandSource,
1829
- commands: staticVerifyCommands.commands,
2027
+ commandSource: partitionedStaticVerifyCommands.static.commandSource,
2028
+ commands: partitionedStaticVerifyCommands.static.commands,
1830
2029
  fallbackCommands: staticFallbackCommands,
2030
+ commandTexts: staticShellCommands,
2031
+ commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
1831
2032
  });
2033
+ const lintVerifyEvidence = lintShellCommands.length > 0
2034
+ ? buildVerifyEvidence({
2035
+ phase: "intermediate",
2036
+ quota: strategy.intermediateQuota ?? "full",
2037
+ commandSource: partitionedStaticVerifyCommands.lint.commandSource,
2038
+ commands: partitionedStaticVerifyCommands.lint.commands,
2039
+ fallbackCommands: [],
2040
+ commandTexts: lintShellCommands,
2041
+ commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
2042
+ })
2043
+ : undefined;
1832
2044
  const behaviorVerifyEvidence = buildVerifyEvidence({
1833
2045
  phase: "final",
1834
2046
  quota: "full",
1835
2047
  commandSource: behaviorVerifyCommands.commandSource,
1836
2048
  commands: behaviorVerifyCommands.commands,
1837
2049
  fallbackCommands: behaviorFallbackCommands,
2050
+ commandTexts: behaviorShellCommands,
1838
2051
  finalFullRequired: true,
2052
+ commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
1839
2053
  });
1840
2054
  const mockVerifyTemplate = mockMode === "required" && hasMockVerifyCommands
1841
2055
  ? buildFrontendMockVerifyNode(frontendSources, implementId, readOnlyPaths, forbiddenPaths)
@@ -1897,6 +2111,7 @@ function buildFrontendHybridDagFromTask(sources) {
1897
2111
  subtask_prompt: [
1898
2112
  "Inspect frontend code, routing, components, styles, package scripts, and tests.",
1899
2113
  "Return code and design observations, existing reuse opportunities, and verification entry points.",
2114
+ "Derive all file paths from this target workspace. Do not assume the project uses src/, test/, React, or the loop-agent repository layout.",
1900
2115
  "Read-only: do not modify repository files.",
1901
2116
  sourceContext,
1902
2117
  ].join("\n\n"),
@@ -1921,6 +2136,7 @@ function buildFrontendHybridDagFromTask(sources) {
1921
2136
  "Based on frontend-contract-pi, frontend-scout-pi, task sources, and the generation-time Mock capability evidence, return a minimal frontend implementation plan.",
1922
2137
  "Select the Mock / API strategy inside the plan and structured contract. Carry endpoint/fixture mapping, explicit activation, production-default-off rule, verification commands, and Real Integration Gap into both outputs.",
1923
2138
  "Include ordered steps, target files, UI state handling, styling/component strategy, interaction notes, Mock/API strategy, dependency policy, deterministic verification entrypoints, and residual risks. Use only the fixed entrypoints below; implementation may add tests behind them but cannot replace them.",
2139
+ "Every target file and verification target must be selected from the current target workspace and task scope. Do not reuse paths or symbols from examples, prior tasks, or loop-agent itself; if the project uses app/, packages/, spec/, __tests__, or another layout, preserve that layout.",
1924
2140
  "End with exactly one fenced json object conforming to frontend-implementation-contract-v1 so small topology can materialize the contract without plan-revision.",
1925
2141
  requirementCoverageInstruction,
1926
2142
  "Read-only: do not modify code, docs, artifacts, or repository files.",
@@ -2042,16 +2258,47 @@ function buildFrontendHybridDagFromTask(sources) {
2042
2258
  : ["native", "browser-intercept", "request-adapter", "not-needed"],
2043
2259
  artifactName: "frontend-implementation-contract.json",
2044
2260
  outputDir: "contracts",
2261
+ requireSourceFreshness: true,
2262
+ implementationWriteSet: implementPaths.writeSet,
2045
2263
  openspecCandidatePaths: sources.frontendProjectCapability?.designEvidence.normativePaths ?? [],
2046
2264
  },
2047
2265
  cwd: ".",
2048
2266
  timeoutMs: 60000,
2049
2267
  },
2050
2268
  },
2269
+ ...(lintShellCommands.length > 0 && lintVerifyEvidence
2270
+ ? [
2271
+ {
2272
+ id: "frontend-lint-baseline-shell",
2273
+ depends_on: ["frontend-prewrite-gate-shell"],
2274
+ role: "verifier",
2275
+ executor: "shell",
2276
+ complexity: "LOW",
2277
+ writePolicy: "read-only",
2278
+ allowedPaths: readOnlyPaths,
2279
+ forbiddenPaths,
2280
+ outputContract: "Capture writer-preceding lint output as frontend-lint-baseline-v1 without treating existing lint diagnostics as writer failure.",
2281
+ subtask_prompt: "Run the frozen lint commands read-only. Preserve raw output and mark the baseline unavailable on timeout, execution failure, unparseable output, or worktree mutation.",
2282
+ shell: {
2283
+ commands: lintShellCommands,
2284
+ frontendLintBaseline: {
2285
+ schemaVersion: 1,
2286
+ lintCommands: lintShellCommands,
2287
+ lintEvidence: lintVerifyEvidence,
2288
+ },
2289
+ cwd: ".",
2290
+ timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
2291
+ },
2292
+ },
2293
+ ]
2294
+ : []),
2051
2295
  {
2052
2296
  id: implementId,
2053
2297
  depends_on: [
2054
2298
  "frontend-prewrite-gate-shell",
2299
+ ...(lintShellCommands.length > 0
2300
+ ? ["frontend-lint-baseline-shell"]
2301
+ : []),
2055
2302
  "frontend-plan-revision-pi",
2056
2303
  "frontend-final-design-review-pi",
2057
2304
  "frontend-plan-pi",
@@ -2098,15 +2345,21 @@ function buildFrontendHybridDagFromTask(sources) {
2098
2345
  frontendVerificationBundle: {
2099
2346
  schemaVersion: 1,
2100
2347
  mockCommands: mockShellCommands,
2348
+ lintCommands: lintShellCommands,
2101
2349
  staticCommands: staticShellCommands,
2102
2350
  behaviorCommands: behaviorShellCommands,
2103
2351
  mockEvidence: mockVerifyEvidence,
2352
+ lintEvidence: lintVerifyEvidence,
2104
2353
  staticEvidence: staticVerifyEvidence,
2105
2354
  behaviorEvidence: behaviorVerifyEvidence,
2355
+ lintBaselineNodeId: lintShellCommands.length > 0
2356
+ ? "frontend-lint-baseline-shell"
2357
+ : undefined,
2358
+ writerNodeIds: lintShellCommands.length > 0 ? [implementId] : [],
2106
2359
  mode: "initial",
2107
2360
  },
2108
2361
  cwd: ".",
2109
- timeoutMs: 300000,
2362
+ timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
2110
2363
  },
2111
2364
  },
2112
2365
  {
@@ -2151,15 +2404,23 @@ function buildFrontendHybridDagFromTask(sources) {
2151
2404
  frontendVerificationBundle: {
2152
2405
  schemaVersion: 1,
2153
2406
  mockCommands: mockShellCommands,
2407
+ lintCommands: lintShellCommands,
2154
2408
  staticCommands: staticShellCommands,
2155
2409
  behaviorCommands: behaviorShellCommands,
2156
2410
  mockEvidence: mockVerifyEvidence,
2411
+ lintEvidence: lintVerifyEvidence,
2157
2412
  staticEvidence: staticVerifyEvidence,
2158
2413
  behaviorEvidence: behaviorVerifyEvidence,
2414
+ lintBaselineNodeId: lintShellCommands.length > 0
2415
+ ? "frontend-lint-baseline-shell"
2416
+ : undefined,
2417
+ writerNodeIds: lintShellCommands.length > 0
2418
+ ? [implementId, "frontend-repair-pi"]
2419
+ : [],
2159
2420
  mode: "repair",
2160
2421
  },
2161
2422
  cwd: ".",
2162
- timeoutMs: 300000,
2423
+ timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
2163
2424
  },
2164
2425
  },
2165
2426
  {
@@ -2177,11 +2438,11 @@ function buildFrontendHybridDagFromTask(sources) {
2177
2438
  writePolicy: "read-only",
2178
2439
  allowedPaths: readOnlyPaths,
2179
2440
  forbiddenPaths,
2180
- outputContract: "Canonical frontend review context containing validated contract, effective verification trace, repair assessment, and actual worktree diff.",
2441
+ outputContract: "Canonical frontend review context containing validated contract, lint assessment when configured, effective verification trace, repair assessment, and actual worktree diff.",
2181
2442
  subtask_prompt: "Capture the actual diff and bind it to the effective initial-or-post-repair verification evidence for final review.",
2182
2443
  shell: {
2183
2444
  commands: [],
2184
- frontendReviewContext: { schemaVersion: 1 },
2445
+ frontendReviewContext: { schemaVersion: 1, requireBaseline: true },
2185
2446
  cwd: ".",
2186
2447
  timeoutMs: 120000,
2187
2448
  },
@@ -2213,7 +2474,8 @@ function buildFrontendHybridDagFromTask(sources) {
2213
2474
  "Review the frontend implementation and verification evidence.",
2214
2475
  "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
2215
2476
  "Any Critical or Important finding must force VERDICT: request-revision.",
2216
- "Read contracts/frontend-review-context.json from frontend-review-context-shell. It binds the validated implementation contract, effective initial-or-post-repair verification trace, repair assessment, and the run-owned actual diff (contracts/frontend-worktree-diff.json + artifacts/diff_patch.patch). Do not claim actual diff is missing when those artifacts exist; do not invent a diff from the implementation summary alone. Trace proves command/file/symbol binding only—not semantic correctness.",
2477
+ "Read contracts/frontend-review-context.json from frontend-review-context-shell. It binds the validated implementation contract, frontend lint assessment when lint is configured, effective initial-or-post-repair verification trace, repair assessment, and the run-owned actual diff (contracts/frontend-worktree-diff.json + artifacts/diff_patch.patch). Do not claim actual diff is missing when those artifacts exist; do not invent a diff from the implementation summary alone. Trace proves command/file/symbol binding only—not semantic correctness.",
2478
+ "Treat lint status exactly as passed | baseline-debt | failed | unavailable. baseline-debt may continue only with intact evidence and zero diagnostics on writer-changed files; report the tolerated debt count and never rewrite it as lint passed. Typecheck, build, and test still require successful final exits.",
2217
2479
  "Flag .skip/.only, deleted or weakened tests, unauthorized config changes, Mock-only evidence claimed as real integration, and Browser/visual claims (always not-run in this workflow).",
2218
2480
  "Use the direct contract and the effective plan/design branch: original plan plus initial pass when revision was skipped, or revised plan plus final design review when revision ran. Do not infer them from the implementation summary.",
2219
2481
  "Treat a commented-out real request, default-enabled Mock, production entrypoint importing test mocks, API/fixture contract drift, unauthorized Mock dependency/path, or missing behavior evidence for the selected strategy as at least Important. Mock strategies require Mock-backed evidence. not-needed requires applicable real/no-remote behavior evidence unless auto mode explicitly skipped Mock because no project Mock capability exists; in that case verify that the real request remains the default and the Real Integration Gap is preserved.",
@@ -2271,7 +2533,7 @@ function buildFrontendHybridDagFromTask(sources) {
2271
2533
  outputContract: "Markdown closeout summary with Changes, Mock Decision / Strategy / Files / Verification / Production Boundary, Verification Evidence, Review Result, Frontend Status, Real Integration Status, Known Risks, and Follow-up. No file writes.",
2272
2534
  subtask_prompt: [
2273
2535
  "Return a frontend closeout summary covering Mock decision/strategy/files/verification/production boundary, changes, verification evidence, review result, known risks, and follow-up.",
2274
- "Include a coverage matrix for each requirement id, applicable UI state, and verification target/check with status passed|failed|not-run|blocked|unavailable. Always state Browser accessibility verification: not-run and Visual regression: not-run. Use contracts/frontend-review-context.json and the effective frontend-verify-assess-shell or frontend-reverify-shell facts; do not invent Browser evidence from component tests.",
2536
+ "Include a coverage matrix for each requirement id, applicable UI state, and verification target/check with status passed|failed|not-run|blocked|unavailable. Report lint separately as passed|baseline-debt|failed|unavailable; baseline-debt is explicit debt, not passed. Always state Browser accessibility verification: not-run and Visual regression: not-run. Use contracts/frontend-review-context.json and the effective frontend-verify-assess-shell or frontend-reverify-shell facts; do not invent Browser evidence from component tests.",
2275
2537
  `When only Mock-backed evidence passed, state exactly Frontend status: mock-validated and Real integration: pending, summarize the Real Integration Gap, and name ${taskConfig.taskId}-real-api-integration-verify as the explicit follow-up task to create/run after backend readiness. This follow-up is not auto-created or auto-executed. Never describe Mock evidence as real API integration.`,
2276
2538
  `When Mock was skipped in auto mode and no real API evidence passed, state exactly Frontend status: locally-validated and Real integration: pending, summarize the Real Integration Gap, and name ${taskConfig.taskId}-real-api-integration-verify as the explicit follow-up task when backend readiness matters.`,
2277
2539
  "Read-only: do not modify code, docs, artifacts, or .harness/dag-runs/.",
@@ -2874,9 +3136,10 @@ function buildExecuteBackendPytestNode(sources, options = {}) {
2874
3136
  commandSource: "inline",
2875
3137
  fallbackCommands: [pytestCommand],
2876
3138
  finalFullRequired: true,
3139
+ commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
2877
3140
  }),
2878
3141
  cwd: ".",
2879
- timeoutMs: 300000,
3142
+ timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
2880
3143
  },
2881
3144
  };
2882
3145
  }
@@ -2993,6 +3256,7 @@ function buildBackendTestOutcomeGateNode(sources) {
2993
3256
  commandSource: "inline",
2994
3257
  fallbackCommands: [gateCommand],
2995
3258
  finalFullRequired: true,
3259
+ commandTimeoutMs: 60_000,
2996
3260
  }),
2997
3261
  cwd: ".",
2998
3262
  timeoutMs: 60000,
@@ -3100,7 +3364,7 @@ async function buildBackendTestHybridDag(sources) {
3100
3364
  'mkdir -p "${HARNESS_DAG_RUN_DIR}/reports"',
3101
3365
  'echo "pytest targets are resolved at runtime from final Markdown 自动化映射"',
3102
3366
  ].join("; ");
3103
- const execute = shellNode("execute-backend-pytest-and-html-report-shell", [traceability.id], "markdown-execute-html", "Resolve the final Markdown Automation Notes/自动化映射 to a unique, safe set of testcase/**/test_*.py targets and execute only those scripts exactly once. Validate JUnit, then render the primary self-contained Chinese HTML report from the same JUnit plus final Markdown case metadata without rerun. Keep 测试结论, quality status, failure overview, and a polished per-case result card with concise scenario, automation test name, result, duration, and redacted bounded HTTP request parameters/response results for both passed and failed cases. Do not render a technical/execution evidence section in HTML; retain auditable paths and hashes in facts.", "One scoped pytest execution over Markdown-mapped scripts producing valid JUnit with per-case captured output, self-contained HTML and reports/backend-test-facts.md; exit 0/1 with valid evidence continues.", [pytestCommand], 300000);
3367
+ const execute = shellNode("execute-backend-pytest-and-html-report-shell", [traceability.id], "markdown-execute-html", "Resolve the final Markdown Automation Notes/自动化映射 to a unique, safe set of testcase/**/test_*.py targets and execute only those scripts exactly once. Validate JUnit, then render the primary self-contained Chinese HTML report from the same JUnit plus final Markdown case metadata without rerun. Keep 测试结论, quality status, failure overview, and a polished per-case result card with concise scenario, automation test name, result, duration, and redacted bounded HTTP request parameters/response results for both passed and failed cases. Do not render a technical/execution evidence section in HTML; retain auditable paths and hashes in facts.", "One scoped pytest execution over Markdown-mapped scripts producing valid JUnit with per-case captured output, self-contained HTML, reports/backend-test.md and reports/backend-test-facts.md; exit 0/1 with valid evidence continues.", [pytestCommand], 300000);
3104
3368
  if (execute.shell) {
3105
3369
  execute.shell.envAllowlist = collectBackendTestShellEnvAllowlist(sources);
3106
3370
  }
@@ -3175,51 +3439,52 @@ function buildFrontendTestHybridDag(sources) {
3175
3439
  const evidenceRoot = "testcase/frontend/evidence";
3176
3440
  const declaredAcIdsLiteral = JSON.stringify(declaredAcIds);
3177
3441
  const maxCasesPerBatchLiteral = String(config.maxCasesPerBatch);
3442
+ const checklistScript = [
3443
+ "const fs=require('fs'),path=require('path');",
3444
+ "const root='testcase/frontend/cases';",
3445
+ "const draft=path.join(root,'manifest.draft.json');",
3446
+ "const final=path.join(root,'manifest.json');",
3447
+ "const manifestPath=fs.existsSync(draft)?draft:(fs.existsSync(final)?final:null);",
3448
+ "if(!manifestPath)throw new Error('checklist: missing manifest.draft.json or manifest.json');",
3449
+ "const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));",
3450
+ "if(!Array.isArray(manifest.cases)||manifest.cases.length===0)throw new Error('checklist: empty cases');",
3451
+ `const declaredAc=new Set(${declaredAcIdsLiteral});`,
3452
+ "const issues=[];",
3453
+ "const openRe=/playwright-cli\\s+open\\s+--browser=chrome\\s+--headed\\s+https?:\\/\\/\\S+/i;",
3454
+ "const prodRe=/(?:^|\\/\\/)(?:www\\.)?[^\\s\\/]*(?:prod|production)/i;",
3455
+ "const caseIdRe=/^FE-[A-Za-z0-9][A-Za-z0-9-]*$/;", ,
3456
+ "const acIdRe=/^AC(?:-[A-Z0-9]+)+$/i;",
3457
+ "for(const c of manifest.cases){",
3458
+ " const id=c&&c.caseId||'?';",
3459
+ " if(typeof c.caseId!=='string'||!caseIdRe.test(c.caseId))issues.push({ruleId:'case-id-shape',caseId:id,detail:'caseId must be FE-<FEATURE>-<NNN>-<dimension>, never AC-FE-*'});",
3460
+ " if(typeof c.caseId==='string'&&/^AC-/i.test(c.caseId))issues.push({ruleId:'case-id-is-ac',caseId:id,detail:'do not use acceptance id as caseId; put AC-FE-* only in acIds'});",
3461
+ " const casePath=typeof c.casePath==='string'?c.casePath:null;",
3462
+ " if(!casePath||!fs.existsSync(casePath)){issues.push({ruleId:'case-file-missing',caseId:id,detail:String(casePath)});continue;}",
3463
+ " if(typeof c.caseId==='string'&&casePath!=='testcase/frontend/cases/'+c.caseId+'.md')issues.push({ruleId:'case-path-mismatch',caseId:id,detail:casePath+' must equal testcase/frontend/cases/'+c.caseId+'.md'});",
3464
+ " const body=fs.readFileSync(casePath,'utf8');",
3465
+ " if(!openRe.test(body))issues.push({ruleId:'open-prefix',caseId:id,detail:'missing playwright-cli open --browser=chrome --headed <absolute-url>; playwright-cli is strongly recommended for browser execution'});", ,
3466
+ " const m=body.match(/playwright-cli\\s+open\\s+--browser=chrome\\s+--headed\\s+(https?:\\/\\/\\S+)/i);",
3467
+ " if(m){const url=m[1].replace(/[)\\]},.\"']+$/,''); if(prodRe.test(url))issues.push({ruleId:'production-url',caseId:id,detail:url});}",
3468
+ " if(!Array.isArray(c.acIds)||c.acIds.length===0)issues.push({ruleId:'ac-mapping',caseId:id,detail:'acIds required (AC-FE-* acceptance ids, not caseId)'});",
3469
+ " else {",
3470
+ " for(const ac of c.acIds){",
3471
+ " if(typeof ac!=='string'){issues.push({ruleId:'ac-id-shape',caseId:id,detail:String(ac)+' must look like AC-FE-001'});continue;}",
3472
+ " if(/^FE-/i.test(ac)){issues.push({ruleId:'ac-id-is-case',caseId:id,detail:ac+' looks like caseId; acIds must be AC-*'});continue;}",
3473
+ " if(!acIdRe.test(ac)){issues.push({ruleId:'ac-id-shape',caseId:id,detail:ac+' must look like AC-FE-001'});continue;}",
3474
+ " if(declaredAc.size>0&&!declaredAc.has(ac))issues.push({ruleId:'unknown-ac',caseId:id,detail:ac+' not in task sourceBinding.requirementIds'});",
3475
+ " }",
3476
+ " }",
3477
+ "}",
3478
+ "if(issues.length){console.error('frontend-test checklist blocked: '+JSON.stringify(issues)); process.exit(1);}",
3479
+ "console.log('frontend-test checklist ok cases='+manifest.cases.length+' source='+path.basename(manifestPath));",
3480
+ ].join("");
3481
+ // Pass generated JavaScript as base64 rather than embedding it inside a
3482
+ // shell-quoted `node -e` argument. Git Bash otherwise consumes backslashes
3483
+ // such as \\s/\\d and interprets Markdown backticks before Node sees them.
3484
+ const checklistScriptBase64 = Buffer.from(checklistScript, "utf8").toString("base64");
3178
3485
  const checklistValidation = [
3179
- "node -e",
3180
- JSON.stringify([
3181
- "const fs=require('fs'),path=require('path');",
3182
- "const root='testcase/frontend/cases';",
3183
- "const draft=path.join(root,'manifest.draft.json');",
3184
- "const final=path.join(root,'manifest.json');",
3185
- "const manifestPath=fs.existsSync(draft)?draft:(fs.existsSync(final)?final:null);",
3186
- "if(!manifestPath)throw new Error('checklist: missing manifest.draft.json or manifest.json');",
3187
- "const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));",
3188
- "if(!Array.isArray(manifest.cases)||manifest.cases.length===0)throw new Error('checklist: empty cases');",
3189
- `const declaredAc=new Set(${declaredAcIdsLiteral});`,
3190
- "const issues=[];",
3191
- "const openRe=/playwright-cli\\s+open\\s+--browser=chrome\\s+--headed\\s+https?:\\/\\/\\S+/i;",
3192
- "const prodRe=/(?:^|\\/\\/)(?:www\\.)?[^\\s\\/]*(?:prod|production)/i;",
3193
- "const codeRe=/\\b(pytest|playwright\\.test|@playwright\\/test)\\b/i;",
3194
- "const barePwRe=/(?:^|[\\s\"'(])(?:npx\\s+playwright\\b|playwright\\s+test\\b|from\\s+['\"]@playwright\\/|require\\(['\"]@playwright\\/|import\\s+.*@playwright\\/|(?<![\\w-])playwright(?!-cli)\\b)/i;",
3195
- "const caseIdRe=/^FE-[A-Za-z0-9][A-Za-z0-9-]*$/;",
3196
- "const acIdRe=/^AC(?:-[A-Z0-9]+)+$/i;",
3197
- "for(const c of manifest.cases){",
3198
- " const id=c&&c.caseId||'?';",
3199
- " if(typeof c.caseId!=='string'||!caseIdRe.test(c.caseId))issues.push({ruleId:'case-id-shape',caseId:id,detail:'caseId must be FE-<FEATURE>-<NNN>-<dimension>, never AC-FE-*'});",
3200
- " if(typeof c.caseId==='string'&&/^AC-/i.test(c.caseId))issues.push({ruleId:'case-id-is-ac',caseId:id,detail:'do not use acceptance id as caseId; put AC-FE-* only in acIds'});",
3201
- " const casePath=typeof c.casePath==='string'?c.casePath:null;",
3202
- " if(!casePath||!fs.existsSync(casePath)){issues.push({ruleId:'case-file-missing',caseId:id,detail:String(casePath)});continue;}",
3203
- " if(typeof c.caseId==='string'&&casePath!=='testcase/frontend/cases/'+c.caseId+'.md')issues.push({ruleId:'case-path-mismatch',caseId:id,detail:casePath+' must equal testcase/frontend/cases/'+c.caseId+'.md'});",
3204
- " const body=fs.readFileSync(casePath,'utf8');",
3205
- " if(!openRe.test(body))issues.push({ruleId:'open-prefix',caseId:id,detail:'missing playwright-cli open --browser=chrome --headed <absolute-url>'});",
3206
- " const m=body.match(/playwright-cli\\s+open\\s+--browser=chrome\\s+--headed\\s+(https?:\\/\\/\\S+)/i);",
3207
- " if(m){const url=m[1].replace(/[)\\]},.\"']+$/,''); if(prodRe.test(url))issues.push({ruleId:'production-url',caseId:id,detail:url});}",
3208
- " if(codeRe.test(body))issues.push({ruleId:'no-test-source',caseId:id,detail:'pytest/playwright test source forbidden'});",
3209
- " if(barePwRe.test(body))issues.push({ruleId:'playwright-cli-only',caseId:id,detail:'only playwright-cli skill commands allowed; bare Playwright CLI/API/test runner forbidden'});",
3210
- " if(!Array.isArray(c.acIds)||c.acIds.length===0)issues.push({ruleId:'ac-mapping',caseId:id,detail:'acIds required (AC-FE-* acceptance ids, not caseId)'});",
3211
- " else {",
3212
- " for(const ac of c.acIds){",
3213
- " if(typeof ac!=='string'){issues.push({ruleId:'ac-id-shape',caseId:id,detail:String(ac)+' must look like AC-FE-001'});continue;}",
3214
- " if(/^FE-/i.test(ac)){issues.push({ruleId:'ac-id-is-case',caseId:id,detail:ac+' looks like caseId; acIds must be AC-*'});continue;}",
3215
- " if(!acIdRe.test(ac)){issues.push({ruleId:'ac-id-shape',caseId:id,detail:ac+' must look like AC-FE-001'});continue;}",
3216
- " if(declaredAc.size>0&&!declaredAc.has(ac))issues.push({ruleId:'unknown-ac',caseId:id,detail:ac+' not in task sourceBinding.requirementIds'});",
3217
- " }",
3218
- " }",
3219
- "}",
3220
- "if(issues.length){console.error('frontend-test checklist blocked: '+JSON.stringify(issues)); process.exit(1);}",
3221
- "console.log('frontend-test checklist ok cases='+manifest.cases.length+' source='+path.basename(manifestPath));",
3222
- ].join("")),
3486
+ "node -e \"eval(Buffer.from(process.argv[1],'base64').toString('utf8'))\"",
3487
+ checklistScriptBase64,
3223
3488
  ].join(" ");
3224
3489
  const manifestValidation = [
3225
3490
  "node -e",
@@ -3260,6 +3525,23 @@ function buildFrontendTestHybridDag(sources) {
3260
3525
  // Heal malformed/missing case evidence to blocked; only unsafe evidenceDir hard-fails.
3261
3526
  const evidenceValidation = buildFrontendCaseEvidenceValidateShellSnippet();
3262
3527
  const frontendTestOutcomeGate = buildFrontendTestOutcomeGateShellSnippet();
3528
+ const frontendCaseQualityAdvisory = [
3529
+ "node -e",
3530
+ JSON.stringify([
3531
+ "const fs=require('fs'),path=require('path');",
3532
+ "const runDir=process.env.HARNESS_DAG_RUN_DIR; if(!runDir)throw new Error('missing HARNESS_DAG_RUN_DIR');",
3533
+ "const resultPath=path.join(runDir,'contracts','frontend-test-result.json'); if(!fs.existsSync(resultPath))throw new Error('missing '+resultPath);",
3534
+ "const r=JSON.parse(fs.readFileSync(resultPath,'utf8')); const findings=Array.isArray(r.advisoryFindings)?r.advisoryFindings:[];",
3535
+ "const reports='testcase/frontend/reports'; fs.mkdirSync(reports,{recursive:true});",
3536
+ "const esc=v=>String(v??'').replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;').replaceAll('\\\"','&quot;');",
3537
+ "const labels={cases:'用例总数',passed:'通过',failed:'失败',blocked:'阻塞'}; const totals=r.totals||{}; const missing=r.acceptanceCoverage&&Array.isArray(r.acceptanceCoverage.missing)?r.acceptanceCoverage.missing:[];",
3538
+ "const md=['# 前端测试质量建议报告','','> Advisory:以下 finding 用于改进测试资产和证据质量,不阻塞后续流程。','','## 执行摘要','',...Object.entries(labels).map(([k,v])=>'- '+v+': '+Number(totals[k]||0)),'- 执行结果: '+String(r.outcome||'unknown'),'- 缺失 AC: '+(missing.join(', ')||'无'),'','## 建议项','',...(findings.length?findings.map(f=>'- ['+f.ruleId+']'+(f.caseId?' '+f.caseId:'')+': '+f.detail):['- 未发现建议项。']),''];",
3539
+ "fs.writeFileSync(path.join(reports,'frontend-test-case-quality-advisory.md'),md.join('\\n'));",
3540
+ "const rows=findings.length?findings.map(f=>'<tr><td><code>'+esc(f.ruleId)+'</code></td><td>'+esc(f.caseId||'-')+'</td><td>'+esc(f.detail)+'</td></tr>').join(''):'<tr><td colspan=3>未发现建议项。</td></tr>';",
3541
+ "const html='<!doctype html><html lang=\\\"zh-CN\\\"><head><meta charset=\\\"utf-8\\\"><meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1\\\"><title>前端测试质量建议报告</title><style>body{font:16px system-ui,Microsoft YaHei,sans-serif;background:#f5f7fb;color:#172033;margin:0;padding:32px}main{max-width:1100px;margin:auto;background:#fff;padding:32px;border-radius:16px}.badge{display:inline-block;padding:6px 10px;border-radius:999px;background:#fff3cd;color:#946200}.grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.card{padding:16px;background:#f8fafc;border-radius:10px}table{width:100%;border-collapse:collapse}th,td{text-align:left;padding:10px;border-bottom:1px solid #e5e7eb}code{color:#475467}@media(max-width:700px){.grid{grid-template-columns:1fr 1fr}}</style></head><body><main><h1>前端测试质量建议报告</h1><p><span class=\\\"badge\\\">Advisory,不阻塞后续流程</span></p><h2>执行摘要</h2><div class=\\\"grid\\\">'+Object.entries(labels).map(([k,v])=>'<div class=\\\"card\\\"><strong>'+v+'</strong><div>'+Number(totals[k]||0)+'</div></div>').join('')+'</div><p>执行结果:'+esc(r.outcome||'unknown')+';缺失 AC:'+esc(missing.join(', ')||'无')+'</p><h2>建议项</h2><table><thead><tr><th>规则</th><th>Case ID</th><th>说明与建议</th></tr></thead><tbody>'+rows+'</tbody></table></main></body></html>';",
3542
+ "fs.writeFileSync(path.join(reports,'frontend-test-case-quality-advisory.html'),html); console.log('frontend-test advisory report findings='+findings.length);",
3543
+ ].join("")),
3544
+ ].join(" ");
3263
3545
  const tasks = [
3264
3546
  {
3265
3547
  id: "retrieve-frontend-test-context-pi",
@@ -3413,8 +3695,8 @@ function buildFrontendTestHybridDag(sources) {
3413
3695
  writePolicy: "read-only",
3414
3696
  allowedPaths: [...ragWriteSet, ...casesWriteSet],
3415
3697
  forbiddenPaths: forbidden,
3416
- outputContract: "Mechanical checklist: open-prefix, non-prod absolute URL, acIds, playwright-cli-only, no pytest/playwright test source; emit structured ruleId issues on failure.",
3417
- subtask_prompt: "Scan generated cases/manifest against the shared blocking checklist. Do not use free-form LLM verdicts.",
3698
+ outputContract: "Mechanical checklist: manifest/case paths, Case ID, non-production playwright-cli open prefix, and AC mapping; alternative executable tool commands are not inspected or blocked.",
3699
+ subtask_prompt: "Scan generated cases/manifest for structural and safety rules only. Strongly recommend playwright-cli for browser execution, but do not inspect or reject alternative executable tool commands and do not use free-form LLM verdicts.",
3418
3700
  shell: { commands: [checklistValidation], cwd: ".", timeoutMs: 120000 },
3419
3701
  }, {
3420
3702
  id: "materialize-frontend-case-manifest-shell",
@@ -3548,6 +3830,20 @@ function buildFrontendTestHybridDag(sources) {
3548
3830
  outputContract: "Write frontend-test retrospective under testcase/frontend/reports/frontend-test-retrospect-<date>.md with coverage, pass/fail/blocked, execution evidence review, risks, findings, and A/B/C/D rating — even when outcome is failed/incomplete. Pipeline acceptance = this report exists (not case 100% pass).",
3549
3831
  subtask_prompt: "Write the frontend test retrospective under testcase/frontend/reports/ after result materialization (do not wait for outcome=pass). Combine AC→case→browser-evidence review with the closeout report: coverage, passed/failed/blocked (including token-budget-exhausted / executor-auth-unavailable), evidence gaps, browser anomalies, residual risks, and A/B/C/D rating. Passed cases need assertion plus screenshot or equivalent evidence when available; failed/blocked need explicit reasons. Blocked cases never count as passed. Do not replace browser evidence with model conclusions. Do not write docs/**. Pipeline success is report production, not case full green.",
3550
3832
  });
3833
+ tasks.push({
3834
+ id: "frontend-test-quality-report-html-shell",
3835
+ depends_on: ["frontend-test-retrospect-pi"],
3836
+ role: "verifier",
3837
+ executor: "shell",
3838
+ complexity: "LOW",
3839
+ writePolicy: "exclusive",
3840
+ writeSet: ["testcase/frontend/reports/**"],
3841
+ allowedPaths: ["testcase/frontend/**"],
3842
+ forbiddenPaths: forbidden,
3843
+ outputContract: "Write a user-readable HTML advisory report after the final Markdown retrospective; findings never block the workflow.",
3844
+ subtask_prompt: "Generate the frontend case quality advisory Markdown and HTML report after the final test report. Record content findings only and never fail because findings exist.",
3845
+ shell: { commands: [frontendCaseQualityAdvisory], cwd: ".", timeoutMs: 120000 },
3846
+ });
3551
3847
  const globalConstraints = [
3552
3848
  ...sources.taskConfig.hardConstraints,
3553
3849
  ...STANDARD_GLOBAL_CONSTRAINTS,
@@ -3701,6 +3997,7 @@ function buildMultiPerspectiveReviewNodes(input) {
3701
3997
  commandSource: "inline",
3702
3998
  fallbackCommands: [aggregateScript],
3703
3999
  finalFullRequired: true,
4000
+ commandTimeoutMs: 60_000,
3704
4001
  }),
3705
4002
  cwd: ".",
3706
4003
  timeoutMs: 60000,
@@ -3885,6 +4182,7 @@ function buildKnowledgeSyncValidateNode(sources, featureId) {
3885
4182
  commandSource: "inline",
3886
4183
  fallbackCommands: [validateScript],
3887
4184
  finalFullRequired: true,
4185
+ commandTimeoutMs: 120_000,
3888
4186
  }),
3889
4187
  cwd: ".",
3890
4188
  timeoutMs: 120000,
@@ -4105,6 +4403,7 @@ function buildKgBootstrapPreflightNode(sources) {
4105
4403
  quota: "full",
4106
4404
  commandSource: "inline",
4107
4405
  fallbackCommands: [script],
4406
+ commandTimeoutMs: 60_000,
4108
4407
  }),
4109
4408
  cwd: ".",
4110
4409
  timeoutMs: 60000,
@@ -4152,6 +4451,7 @@ function buildKgBootstrapInventoryNode(sources) {
4152
4451
  quota: "full",
4153
4452
  commandSource: "inline",
4154
4453
  fallbackCommands: [script],
4454
+ commandTimeoutMs: 120_000,
4155
4455
  }),
4156
4456
  cwd: ".",
4157
4457
  timeoutMs: 120000,
@@ -4246,6 +4546,7 @@ function buildKgBootstrapValidateNode(sources) {
4246
4546
  commandSource: "inline",
4247
4547
  fallbackCommands: [script],
4248
4548
  finalFullRequired: true,
4549
+ commandTimeoutMs: 120_000,
4249
4550
  }),
4250
4551
  cwd: ".",
4251
4552
  timeoutMs: 120000,
@@ -4373,6 +4674,7 @@ function buildKgBootstrapPromoteNode(sources) {
4373
4674
  commandSource: "inline",
4374
4675
  fallbackCommands: [script],
4375
4676
  finalFullRequired: true,
4677
+ commandTimeoutMs: 120_000,
4376
4678
  }),
4377
4679
  cwd: ".",
4378
4680
  timeoutMs: 120000,
@@ -4438,6 +4740,7 @@ function buildKgBootstrapMaterializeNode(sources) {
4438
4740
  commandSource: "inline",
4439
4741
  fallbackCommands: [script],
4440
4742
  finalFullRequired: true,
4743
+ commandTimeoutMs: 120_000,
4441
4744
  }),
4442
4745
  cwd: ".",
4443
4746
  timeoutMs: 120000,
@@ -4494,7 +4797,22 @@ export async function requireManagedTaskContractBinding(input) {
4494
4797
  taskId: input.taskId,
4495
4798
  });
4496
4799
  if (state.effectiveStatus !== "managed" || !state.ref) {
4497
- const err = new Error(`dag generate requires managed Task Contract for task ${input.taskId} (effectiveStatus=${state.effectiveStatus}); run: loop-agent task contract adopt|apply --task ${input.taskId} ...`);
4800
+ const expectedRevision = state.ref?.revision ?? 0;
4801
+ const observedCanonicalHash = state.observedCanonicalHash ?? "<observedCanonicalHash-from-show>";
4802
+ const recovery = state.effectiveStatus === "transaction-incomplete"
4803
+ ? `Recover the unfinished transaction first: loop-agent task contract recover --task ${input.taskId} --json`
4804
+ : [
4805
+ "After reviewing the show JSON, either adopt the current on-disk facts:",
4806
+ `loop-agent task contract adopt --task ${input.taskId} --expected-revision ${expectedRevision} --expected-observed-hash ${observedCanonicalHash} --request-id <unique-request-id> --request-payload-sha256 <sha256-of-this-adopt-request> --json`,
4807
+ "or apply a reviewed draft:",
4808
+ `loop-agent task contract apply --task ${input.taskId} --input <reviewed-draft.json> --expected-revision ${expectedRevision} --expected-observed-hash ${observedCanonicalHash} --request-id <unique-request-id> --request-payload-sha256 <sha256-of-input-file> --json`,
4809
+ ].join(" ");
4810
+ const err = new Error([
4811
+ `dag generate requires managed Task Contract for task ${input.taskId} (effectiveStatus=${state.effectiveStatus})`,
4812
+ `Inspect current state: loop-agent task contract show ${input.taskId} --json`,
4813
+ recovery,
4814
+ "Do not auto-adopt or auto-apply before confirming the Task Contract facts and concurrency fields.",
4815
+ ].join("; "));
4498
4816
  err.code =
4499
4817
  state.effectiveStatus === "externally-modified"
4500
4818
  ? "EXTERNALLY_MODIFIED"
@@ -4548,10 +4866,6 @@ function synthesizeInMemoryTaskContractBinding(sources) {
4548
4866
  };
4549
4867
  }
4550
4868
  async function resolveTaskContractBindingForGenerate(sources) {
4551
- // Real CLI generation freezes the managed ref. Direct builder unit tests
4552
- // may provide repoRoot-shaped fixture paths without a persisted task; keep
4553
- // their deterministic synthetic binding path so topology/prompt tests remain
4554
- // isolated from contract storage.
4555
4869
  if (sources.repoRoot) {
4556
4870
  const state = await observeTaskContract({
4557
4871
  repoRoot: sources.repoRoot,
@@ -4570,7 +4884,7 @@ async function buildHybridDagForTemplate(sources, template) {
4570
4884
  const taskContractBinding = await resolveTaskContractBindingForGenerate(sources);
4571
4885
  let spec;
4572
4886
  if (template === "frontend-implementation") {
4573
- spec = buildFrontendHybridDagFromTask(sources);
4887
+ spec = await buildFrontendHybridDagFromTask(sources);
4574
4888
  }
4575
4889
  else if (template === "frontend-test-dag")
4576
4890
  spec = buildFrontendTestHybridDag(sources);
@@ -4706,7 +5020,12 @@ function buildReviewVerdictRecoveryNode(sources) {
4706
5020
  return {
4707
5021
  id: "review-verdict-recovery-pi",
4708
5022
  depends_on: ["review-pi"],
5023
+ // AC3: recovery is a read-only failure-aware node. It runs after review-pi
5024
+ // settles, consuming either normal FINISHED output or an explicitly tolerated
5025
+ // ERROR (for example protocol-invalid after retries). It must not bypass the
5026
+ // writer, write guard, or final shell gate, which remain fail-closed.
4709
5027
  role: "reviewer",
5028
+ failureAwareDependsOn: ["review-pi"],
4710
5029
  executor: "pi",
4711
5030
  complexity: "LOW",
4712
5031
  writePolicy: "read-only",
@@ -4991,7 +5310,7 @@ function buildWriteSetGateNode(sources) {
4991
5310
  }
4992
5311
  function buildSoftVerifyNode(sources) {
4993
5312
  const implementId = implementationNodeId();
4994
- const strategy = resolveDagVerifyStrategy(sources.taskConfig);
5313
+ const strategy = resolveDagVerifyStrategy(sources.taskConfig, "1");
4995
5314
  const fallbackCommands = ["npm run typecheck"];
4996
5315
  const commands = buildVerifyShellCommands({
4997
5316
  repoRoot: sources.repoRoot,
@@ -5017,9 +5336,10 @@ function buildSoftVerifyNode(sources) {
5017
5336
  commandSource: sources.verifyCommands ? "adapter" : "inline",
5018
5337
  commands: sources.verifyCommands?.intermediate,
5019
5338
  fallbackCommands,
5339
+ commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
5020
5340
  }),
5021
5341
  cwd: ".",
5022
- timeoutMs: 300000,
5342
+ timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
5023
5343
  },
5024
5344
  };
5025
5345
  }
@@ -5029,6 +5349,7 @@ function buildProcessSupervisorNode(sources) {
5029
5349
  return {
5030
5350
  id: "process-supervisor-pi",
5031
5351
  depends_on: ["soft-verify-shell", implementId],
5352
+ failureAwareDependsOn: ["soft-verify-shell"],
5032
5353
  role: "supervisor",
5033
5354
  executor: "pi",
5034
5355
  complexity: "HIGH",
@@ -5135,9 +5456,10 @@ function buildHardVerifyNode(sources) {
5135
5456
  commands: sources.verifyCommands?.final,
5136
5457
  fallbackCommands,
5137
5458
  finalFullRequired: true,
5459
+ commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
5138
5460
  }),
5139
5461
  cwd: ".",
5140
- timeoutMs: 300000,
5462
+ timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
5141
5463
  },
5142
5464
  };
5143
5465
  }
@@ -5186,7 +5508,7 @@ function buildSupervisedHybridDag(standard, sources) {
5186
5508
  objective: `${standard.objective ?? ""}\n\nRoute: supervised implementation DAG selected by workflowPolicy/governanceProfile or explicit CLI profile.`.trim(),
5187
5509
  globalConstraints: supervisedConstraints,
5188
5510
  convergence: sources.taskConfig.convergence,
5189
- verifyStrategy: resolveDagVerifyStrategy(sources.taskConfig),
5511
+ verifyStrategy: resolveDagVerifyStrategy(sources.taskConfig, "1"),
5190
5512
  tasks: [
5191
5513
  cloneTask(contract),
5192
5514
  cloneTask(scoutSrc),
@@ -5256,6 +5578,12 @@ export async function writeHybridDagDraft(sources, outputPath, options = {}) {
5256
5578
  ? await prepareFrontendMockSources(sources)
5257
5579
  : sources;
5258
5580
  const spec = await buildHybridDagForTemplate(preparedSources, template);
5581
+ if (preparedSources.repoRoot && dagHasWriterExecution(spec)) {
5582
+ await requireManagedTaskContractBinding({
5583
+ repoRoot: preparedSources.repoRoot,
5584
+ taskId: preparedSources.taskId,
5585
+ });
5586
+ }
5259
5587
  await writeFile(outputPath, `${JSON.stringify(spec, null, 2)}\n`, "utf-8");
5260
5588
  return {
5261
5589
  taskId: sources.taskId,