@tea-agent/loop-agent 0.44.0-next.7 → 0.44.0-next.9

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 (147) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/dist/application/evaluation/budget.js +21 -0
  3. package/dist/application/evaluation/corpus-hash.js +10 -15
  4. package/dist/application/evaluation/corpus.js +2 -1
  5. package/dist/application/evaluation/frontend-browser-acceptance.js +77 -0
  6. package/dist/application/evaluation/frontend-corpus-browser-acceptance.js +175 -0
  7. package/dist/application/evaluation/frontend-gateway-evidence.js +226 -0
  8. package/dist/application/evaluation/frontend-pair-registration.js +143 -0
  9. package/dist/application/evaluation/frontend-paired-summary.js +150 -0
  10. package/dist/application/evaluation/frontend-run-observation.js +144 -0
  11. package/dist/application/evaluation/frontend-shared-evidence.js +105 -0
  12. package/dist/application/evaluation/types.js +45 -12
  13. package/dist/application/task-lifecycle/observe.js +26 -1
  14. package/dist/build-stamp.json +3 -3
  15. package/dist/cli/command-definitions.js +11 -0
  16. package/dist/cli/program.js +4 -0
  17. package/dist/commands/task-advance.js +3 -5
  18. package/dist/commands/task-source-prepare.js +13 -1
  19. package/dist/executors/dag-pi/guards.js +4 -0
  20. package/dist/executors/dag-pi/model-config.js +30 -0
  21. package/dist/{workflows/dag/frontend-plan-completeness.js → executors/dag-pi/plan/facts.js} +25 -41
  22. package/dist/executors/dag-pi/plan/ledger-contract.js +55 -0
  23. package/dist/executors/dag-pi/tools/contract-tools.js +515 -0
  24. package/dist/executors/dag-pi/tools/design-terminal-tools.js +166 -0
  25. package/dist/executors/dag-pi/tools/plan-decision-tools.js +519 -0
  26. package/dist/executors/dag-pi/tools/review-terminal-tools.js +166 -0
  27. package/dist/executors/dag-pi/tools/scout-evidence-tools.js +270 -0
  28. package/dist/executors/dag-pi-executor.js +198 -118
  29. package/dist/executors/pi-executor.js +6 -1
  30. package/dist/executors/pi-sdk-executor.js +76 -11
  31. package/dist/executors/shell-executor.js +88 -12
  32. package/dist/infrastructure/console/operation-store.js +2 -0
  33. package/dist/infrastructure/evaluation/corpus-store.js +37 -12
  34. package/dist/shared/operator/capabilities.js +10 -5
  35. package/dist/task/config-types.js +22 -9
  36. package/dist/task/contract/project.js +13 -0
  37. package/dist/task/contract/schema.js +2 -8
  38. package/dist/task/frontend-preflight.js +1 -1
  39. package/dist/task/source-prepare/parse-intent.js +124 -5
  40. package/dist/task/source-prepare/prepare.js +6 -1
  41. package/dist/task/source-prepare/semantic-intake.js +18 -3
  42. package/dist/task/source-prepare/source-execution.js +65 -0
  43. package/dist/task/source-prepare/source-fidelity-pi.js +22 -9
  44. package/dist/task/source-prepare/source-provider-budget.js +290 -0
  45. package/dist/worker/console/operator-actions.js +0 -1
  46. package/dist/worker/console/operator-user-error.js +1 -1
  47. package/dist/worker/console/prd-intake-bridge.js +5 -15
  48. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DTuku_-7.js → abnfDiagram-N423BO3Z-r1NAPU_5.js} +1 -1
  49. package/dist/worker/console/static/assets/{arc-CQbCsSqb.js → arc-dT5T4sVL.js} +1 -1
  50. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-C-xUntTR.js → architectureDiagram-T3A2C74G-CP_cOq46.js} +1 -1
  51. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DhNlwGAl.js → blockDiagram-VBNYF7ZC-sToEZxEf.js} +1 -1
  52. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B7j3nx8o.js → c4Diagram-5PPSVZJV-DSxwwi2m.js} +1 -1
  53. package/dist/worker/console/static/assets/channel-CyBY_yGk.js +1 -0
  54. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BCrcxEh2.js → chunk-2GRJ4B5K-B9fh0IkF.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CsaePDb3.js → chunk-2Q5K7J3B-CAUNl-A6.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-5RXB4S5H--SRQc1rN.js → chunk-5RXB4S5H-BomeqbNX.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C5BFY2Po.js → chunk-5VM5RSS4-BNAuTOLE.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Caso6uN7.js → chunk-6Q2QTUOP-DfzjRD5y.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-AY2ZSAx2.js → chunk-GF5L2VYU-C3uE-yc2.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DiMqZQ_8.js → chunk-JWPE2WC7-C3sQMMYr.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-gpvvriOp.js → chunk-KBJHAD2P-D_AtEYQL.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CUdyoJhI.js → chunk-RYQCIY6F-DW4TwhKv.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bp1xW3T4.js → chunk-XXDRQBXY-CTQDmJcp.js} +1 -1
  64. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +1 -0
  65. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +1 -0
  66. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-D-JIcOIT.js → cose-bilkent-JH36ORCC-iMqPuBsL.js} +1 -1
  67. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-C-NKFflx.js → cynefin-VYW2F7L2-DktIuDWs.js} +1 -1
  68. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-CSOcvOAA.js → cynefinDiagram-MW4NZA55-BgVnjyEs.js} +1 -1
  69. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BQSBltMy.js → dagre-VZM6K2ZE-CR25E4bu.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B0LVC45G.js → diagram-7IWD3JNH-CPM2h5HY.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Cypg3nqy.js → diagram-B4RE2ZJO-DqF0x2g1.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BBDU88XJ.js → diagram-LBJQPF4R-G8BYp2eb.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BZusLqSb.js → diagram-Q27KOJAE-BbC4kT_D.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-UB23O5K3-Cfolotcr.js → diagram-UB23O5K3-CKZwZ-G5.js} +1 -1
  75. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CjfyUy0m.js → ebnfDiagram-BXEA7PRR-kJDmheia.js} +1 -1
  76. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DJdz7txE.js → erDiagram-JOGREHBK-DFTVHyxB.js} +1 -1
  77. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CQqgPO6y.js → flowDiagram-UKHOOZJN-BqdN5_os.js} +1 -1
  78. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BOpGfS2m.js → ganttDiagram-PKOTCBZU-SYDjVakh.js} +1 -1
  79. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-C6UCu0qx.js → gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js} +1 -1
  80. package/dist/worker/console/static/assets/index-B28Onmfy.css +1 -0
  81. package/dist/worker/console/static/assets/{index-DWKejNVJ.js → index-CTMZsDmh.js} +98 -98
  82. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BWeaVEUB.js → infoDiagram-6WML65LV-DIeAmJ45.js} +1 -1
  83. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-C8gmlBbk.js → ishikawaDiagram-WSZJBQD7-oikqUAlo.js} +1 -1
  84. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DF3bm8Bp.js → journeyDiagram-NVQOT4AX-mKEF_xYT.js} +1 -1
  85. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-D2fG2Vxi.js → kanban-definition-27J2QSJJ-BdWJHPnN.js} +1 -1
  86. package/dist/worker/console/static/assets/{linear-BGKgMc2T.js → linear-DGRlieMH.js} +1 -1
  87. package/dist/worker/console/static/assets/{mermaid.core-DF6mMPJ9.js → mermaid.core-4P9yPC9c.js} +5 -5
  88. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-AMhePheC.js → mindmap-definition-FAOFIHXS-5PTSkos6.js} +1 -1
  89. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DN1Cl0W0.js → pegDiagram-VL7TDLO6-DONIFpG5.js} +1 -1
  90. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-Cx9ngUeN.js → pieDiagram-7S7Q4E2Y-D_Z4S70M.js} +1 -1
  91. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-BcYPTQJb.js → quadrantDiagram-CIZ2JOQS-CAsjh8iY.js} +1 -1
  92. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BqHUZWnH.js → railroadDiagram-AXF67PYL-Cx4Dd6ac.js} +1 -1
  93. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CmqWZ0-B.js → requirementDiagram-LRYGKXZP-cQb7mwyH.js} +1 -1
  94. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P--4YetH1y.js → sankeyDiagram-W5VNT64P-Cd-DhUdf.js} +1 -1
  95. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DGD4VMfD.js → sequenceDiagram-SI44F4Z6-AlKYRgIk.js} +1 -1
  96. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-QVw9q7rh.js → sizeCapture-X5ZJPWSS-CUy_ET4e.js} +1 -1
  97. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CuemV9pe.js → stateDiagram-OKZ733FA-CTuVe6Sa.js} +1 -1
  98. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +1 -0
  99. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-C7oKzZKs.js → swimlanes-SLNWSIFB-ChFLJb-v.js} +2 -2
  100. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +8 -0
  101. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-C2Of60nT.js → timeline-definition-Z64GVDOM-DZZeiAPr.js} +1 -1
  102. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DHtpQHyL.js → vennDiagram-T6HMQDX7-t4O91cZU.js} +1 -1
  103. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-COi9OhSy.js → wardleyDiagram-T6FBY63Y-CV5qGHZu.js} +1 -1
  104. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DPHQfRrt.js → xychartDiagram-ELKLHX3M-DyXbytUI.js} +1 -1
  105. package/dist/worker/console/static/index.html +2 -2
  106. package/dist/workflows/dag/budget-enforcement.js +31 -2
  107. package/dist/workflows/dag/frontend-closeout.js +3 -1
  108. package/dist/workflows/dag/frontend-contract-facts.js +11 -8
  109. package/dist/workflows/dag/frontend-design-policy.js +6 -6
  110. package/dist/workflows/dag/frontend-durable-tools.js +90 -70
  111. package/dist/workflows/dag/frontend-implementation-contract.js +146 -59
  112. package/dist/workflows/dag/frontend-plan-canary.js +66 -16
  113. package/dist/workflows/dag/frontend-plan-decision-contract.js +13 -21
  114. package/dist/workflows/dag/frontend-plan-progress.js +92 -0
  115. package/dist/workflows/dag/frontend-recovery-plan.js +11 -10
  116. package/dist/workflows/dag/frontend-recovery-run.js +51 -46
  117. package/dist/workflows/dag/frontend-repair-assertions.js +36 -0
  118. package/dist/workflows/dag/frontend-review-context.js +29 -1
  119. package/dist/workflows/dag/frontend-review-scopes.js +92 -2
  120. package/dist/workflows/dag/frontend-risk.js +8 -3
  121. package/dist/workflows/dag/frontend-root-observation.js +261 -0
  122. package/dist/workflows/dag/frontend-session-budget.js +52 -39
  123. package/dist/workflows/dag/frontend-session-context.js +10 -0
  124. package/dist/workflows/dag/frontend-test-execution-evidence.js +206 -48
  125. package/dist/workflows/dag/frontend-typed-event-store.js +11 -6
  126. package/dist/workflows/dag/frontend-verification-trace.js +6 -0
  127. package/dist/workflows/dag/frontend-writer-admission.js +2 -1
  128. package/dist/workflows/dag/init-hybrid.js +108 -37
  129. package/dist/workflows/dag/node-execution.js +11 -11
  130. package/dist/workflows/dag/rerun-feedback.js +35 -4
  131. package/dist/workflows/dag/rerun-task.js +62 -67
  132. package/dist/workflows/dag/runner.js +58 -12
  133. package/dist/workflows/dag/types.js +15 -8
  134. package/docs/architecture/runtime-boundaries.md +1 -1
  135. package/docs/templates/backend-test-dag.json +4 -4
  136. package/docs/templates/frontend-implementation-contract.schema.json +31 -11
  137. package/package.json +1 -1
  138. package/skills/frontend-design-review/SKILL.md +8 -11
  139. package/skills/frontend-design-review/references/review-checklist.md +3 -4
  140. package/skills/frontend-review/SKILL.md +5 -1
  141. package/skills/loop-agent/references/command-reference.md +8 -0
  142. package/dist/worker/console/static/assets/channel-5_os7t_x.js +0 -1
  143. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-loz0n18W.js +0 -1
  144. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-loz0n18W.js +0 -1
  145. package/dist/worker/console/static/assets/index-CUT440A8.css +0 -1
  146. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BdlGdIJw.js +0 -1
  147. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DjjbHbde.js +0 -8
@@ -1,59 +1,217 @@
1
1
  import path from "node:path";
2
- import { readFile } from "node:fs/promises";
2
+ import { createHash } from "node:crypto";
3
+ import { pathMatchesPattern } from "../../shared/git-progress.js";
4
+ import { readFile, realpath } from "node:fs/promises";
3
5
  import { z } from "zod";
4
- /** Decide whether a bare vitest/npm-test command is observable and, if so,
5
- * return the command with the separator (" --") a reporter would need. */
6
- async function adaptPlainTestCommand(command, cwd) {
7
- const direct = /^(?:(?:npx|pnpm exec|yarn exec)\s+)?vitest\s+run(?:\s|$)/.test(command.trim());
8
- const npm = /^npm\s+(?:run\s+([\w:-]+)|(test))(?=\s|$)(.*)$/.exec(command.trim());
9
- let separator = "";
10
- if (!direct) {
11
- if (!npm)
12
- return undefined;
13
- let manifest;
14
- try {
15
- manifest = JSON.parse(await readFile(path.join(cwd, "package.json"), "utf8"));
6
+ export const frontendTestObservationSchema = z.object({
7
+ scheme: z.literal("vitest-json-v1"),
8
+ commandId: z.string().optional(),
9
+ status: z.enum(["supported", "deferred", "unsupported"]),
10
+ commandSha256: z.string().regex(/^[a-f0-9]{64}$/),
11
+ cwd: z.string(),
12
+ packageScript: z.object({ file: z.string(), name: z.string(), sha256: z.string().nullable() }).optional(),
13
+ reason: z.string().optional(),
14
+ }).strict();
15
+ const hash = (text) => createHash("sha256").update(text).digest("hex");
16
+ const quote = (text) => `'${text.replace(/'/g, `'"'"'`)}'`;
17
+ /** Literal shell words only. Keep spans to replace a wrapper payload as ONE
18
+ * shell argument; never splice nested quotes into an already quoted payload. */
19
+ function words(command) {
20
+ const result = [];
21
+ let i = 0;
22
+ while (i < command.length) {
23
+ if (/[\r\n]/.test(command[i]))
24
+ throw Error("multiple shell lines are unsupported");
25
+ if (/\s/.test(command[i])) {
26
+ i++;
27
+ continue;
16
28
  }
17
- catch {
18
- return undefined;
29
+ const start = i;
30
+ if (command.slice(i, i + 2) === "&&") {
31
+ result.push({ value: "&&", start, end: i += 2, operator: true });
32
+ continue;
19
33
  }
20
- const script = manifest.scripts?.[npm[1] ?? "test"] ?? "";
21
- if (!/^vitest\s+run(?:\s|$)/.test(script) || /[;&|<>`\n\r$]|--(?:reporter|outputFile)/.test(script))
22
- return undefined;
23
- if (npm[3]?.trim() && !/^--(?:\s|$)/.test(npm[3].trim()))
24
- return undefined;
25
- separator = npm[3]?.trim() ? "" : " --";
34
+ let value = "";
35
+ let quoted;
36
+ while (i < command.length) {
37
+ const c = command[i];
38
+ if (quoted === "'") {
39
+ if (c === "'")
40
+ quoted = undefined;
41
+ else
42
+ value += c;
43
+ i++;
44
+ continue;
45
+ }
46
+ if (c === "\\") {
47
+ const next = command[++i];
48
+ if (!next || /[\r\n]/.test(next))
49
+ throw Error("invalid shell escape");
50
+ if (quoted === '"' && !['$', '`', '"', '\\'].includes(next))
51
+ value += "\\";
52
+ value += next;
53
+ i++;
54
+ continue;
55
+ }
56
+ if (quoted === '"') {
57
+ if (c === '"')
58
+ quoted = undefined;
59
+ else if (c === "$" || c === "`")
60
+ throw Error("shell expansion is unsupported");
61
+ else
62
+ value += c;
63
+ i++;
64
+ continue;
65
+ }
66
+ if (/\s/.test(c) || command.slice(i, i + 2) === "&&")
67
+ break;
68
+ if (c === "'" || c === '"') {
69
+ quoted = c;
70
+ i++;
71
+ continue;
72
+ }
73
+ if (/[;&|<>`$(){}*?\[\]#~]/.test(c))
74
+ throw Error("shell operators or expansion are unsupported");
75
+ value += c;
76
+ i++;
77
+ }
78
+ if (quoted)
79
+ throw Error("unterminated shell quote");
80
+ if (i === start)
81
+ throw Error("unsupported shell syntax");
82
+ result.push({ value, start, end: i });
83
+ }
84
+ return result;
85
+ }
86
+ function literalCommand(command, cwd, depth = 0) {
87
+ const tokens = words(command);
88
+ let offset = 0;
89
+ if (tokens[0]?.value === "cd") {
90
+ if (tokens[1]?.operator || tokens[2]?.value !== "&&" || !tokens[2].operator)
91
+ throw Error("only a leading literal cd is supported");
92
+ cwd = path.resolve(cwd, tokens[1].value);
93
+ offset = 3;
94
+ }
95
+ if (tokens.slice(offset).some(t => t.operator))
96
+ throw Error("compound commands are unsupported");
97
+ while (tokens[offset] && /^[A-Za-z_][A-Za-z0-9_]*=/.test(command.slice(tokens[offset].start, tokens[offset].end)))
98
+ offset++;
99
+ const argv = tokens.slice(offset).map(t => t.value);
100
+ if (argv[0] === "bash") {
101
+ // `bash -lc <payload>` is a wrapper: unwrap it and keep resolving the
102
+ // payload. Repeated layers are unwrapped too, matching the
103
+ // normalization the DAG generator already applies
104
+ // (normalizeVerifyCommandArgs in init-hybrid). Depth stays bounded so a
105
+ // pathological self-nesting payload terminates.
106
+ if (argv.length === 3 && argv[1] === "-lc") {
107
+ if (depth >= 8)
108
+ throw Error("unsupported bash wrapper");
109
+ const payload = tokens[offset + 2];
110
+ const inner = literalCommand(argv[2], cwd, depth + 1);
111
+ return { ...inner, append: suffix => command.slice(0, payload.start) + quote(inner.append(suffix)) + command.slice(payload.end) };
112
+ }
113
+ // A plain `bash <script> [args…]` invocation is NOT a wrapper: it is an
114
+ // ordinary command that simply is not a vitest/npm-test adapter. The
115
+ // frozen command for a declared repo script is
116
+ // `… 'bash' '-lc' 'bash scripts/check-repo.sh'`, so the payload lands
117
+ // here; throwing made the caller record
118
+ // FRONTEND_TEST_OBSERVATION_INVALID and blocked writer admission before
119
+ // implement could run. Returning the argv lets the normal capability
120
+ // check classify it as unsupported (non-fatal) instead.
121
+ if (argv.length >= 2)
122
+ return { argv, cwd, append: suffix => command.trimEnd() + suffix };
123
+ throw Error("unsupported bash wrapper");
124
+ }
125
+ return { argv, cwd, append: suffix => command.trimEnd() + suffix };
126
+ }
127
+ function directVitest(argv) {
128
+ const args = [...argv];
129
+ if (["npx"].includes(args[0] ?? ""))
130
+ args.shift();
131
+ else if (["pnpm", "yarn"].includes(args[0] ?? "") && args[1] === "exec")
132
+ args.splice(0, 2);
133
+ return args[0] === "vitest" && args[1] === "run";
134
+ }
135
+ function hasReporter(argv) { return argv.some(arg => /^--(?:reporter|outputFile)(?:[.=]|$)/.test(arg)); }
136
+ /** Shared read-only capability resolver, used at generation, admission and
137
+ * immediately before execution. It never executes or rewrites an npm script. */
138
+ export async function inspectFrontendTestCommand(input) {
139
+ const root = path.resolve(input.workspaceRoot ?? input.cwd);
140
+ let cwd = path.resolve(input.cwd);
141
+ const base = { scheme: "vitest-json-v1", status: "unsupported", commandSha256: hash(input.command), cwd: path.relative(root, cwd).replace(/\\/g, "/") || "." };
142
+ try {
143
+ const parsed = literalCommand(input.command, cwd);
144
+ cwd = parsed.cwd;
145
+ base.cwd = path.relative(root, cwd).replace(/\\/g, "/") || ".";
146
+ if (base.cwd === ".." || base.cwd.startsWith("../") || path.isAbsolute(base.cwd))
147
+ throw Error("test cwd is outside the workspace");
148
+ // Reject symlink escapes too, while a future directory remains governed
149
+ // by the existing cwd/future-delivery preflight.
150
+ const physicalRoot = await realpath(root).catch(() => root);
151
+ const physicalCwd = await realpath(cwd).catch(() => cwd);
152
+ const physicalRelative = path.relative(physicalRoot, physicalCwd);
153
+ if (physicalRelative === ".." || physicalRelative.startsWith(`..${path.sep}`) || path.isAbsolute(physicalRelative))
154
+ throw Error("test cwd resolves outside the workspace");
155
+ if (hasReporter(parsed.argv))
156
+ throw Error("frozen command already owns reporter/outputFile flags");
157
+ let separator = "";
158
+ if (!directVitest(parsed.argv)) {
159
+ const argv = parsed.argv;
160
+ const name = argv[0] === "npm" ? argv[1] === "test" ? "test" : argv[1] === "run" ? argv[2] : undefined : undefined;
161
+ if (!name || !/^[\w:-]+$/.test(name))
162
+ throw Error("no supported test reporter adapter for this command");
163
+ const rest = argv.slice(argv[1] === "test" ? 2 : 3);
164
+ if (rest.length && rest[0] !== "--")
165
+ throw Error("npm script arguments require --");
166
+ const file = path.relative(root, path.join(cwd, "package.json")).replace(/\\/g, "/");
167
+ let manifest = {};
168
+ try {
169
+ manifest = JSON.parse(await readFile(path.join(cwd, "package.json"), "utf8"));
170
+ }
171
+ catch (error) {
172
+ if (error.code !== "ENOENT")
173
+ throw Error("package.json is unreadable or invalid");
174
+ }
175
+ const script = manifest?.scripts?.[name];
176
+ base.packageScript = { file, name, sha256: typeof script === "string" ? hash(JSON.stringify({ script, pre: manifest.scripts?.[`pre${name}`] ?? null, post: manifest.scripts?.[`post${name}`] ?? null })) : null };
177
+ if (script === undefined) {
178
+ const authorized = input.allowedPaths?.some(pattern => pathMatchesPattern(file, pattern)) && !input.forbiddenPaths?.some(pattern => pathMatchesPattern(file, pattern));
179
+ if (!authorized)
180
+ throw Error(`missing npm script ${name}; ${file} is not an authorized delivery`);
181
+ return { observation: { ...base, status: "deferred", reason: `Awaiting authorized ${file} script ${name} with vitest-json-v1 capability` }, effectiveCwd: cwd };
182
+ }
183
+ if (typeof script !== "string")
184
+ throw Error("npm script must be a string");
185
+ const scriptWords = words(script);
186
+ if (scriptWords.some(word => word.operator) || !directVitest(scriptWords.map(w => w.value)) || hasReporter(scriptWords.map(w => w.value)))
187
+ throw Error("npm script has no supported Vitest JSON reporter capability");
188
+ separator = rest.length ? "" : " --";
189
+ }
190
+ return { observation: { ...base, status: "supported" }, effectiveCwd: cwd,
191
+ withReporter: reportPath => parsed.append(`${separator} --reporter=json --outputFile=${quote(reportPath)}`) };
26
192
  }
27
- return `${command}${separator}`;
193
+ catch (error) {
194
+ return { observation: { ...base, status: "unsupported", reason: error instanceof Error ? error.message : String(error) }, effectiveCwd: cwd };
195
+ }
196
+ }
197
+ export function compareFrontendTestObservation(frozen, observed, phase, writeSet = []) {
198
+ if (frozen.status === "unsupported")
199
+ return frozen.reason ?? "unsupported test observation scheme";
200
+ if (observed.status === "unsupported")
201
+ return observed.reason ?? "unsupported test observation scheme";
202
+ if (frozen.scheme !== observed.scheme || frozen.commandSha256 !== observed.commandSha256 || frozen.cwd !== observed.cwd || frozen.packageScript?.file !== observed.packageScript?.file || frozen.packageScript?.name !== observed.packageScript?.name)
203
+ return "frozen test command/cwd/script binding changed";
204
+ if (frozen.status === "supported" && (observed.status !== "supported" || frozen.packageScript?.sha256 !== observed.packageScript?.sha256))
205
+ return "frozen npm test script changed";
206
+ if (frozen.status === "deferred" && phase === "admission" && (!frozen.packageScript || !writeSet.includes(frozen.packageScript.file)))
207
+ return "future test script is outside the admitted concrete writeSet";
208
+ if (phase === "execution" && observed.status !== "supported")
209
+ return "future test reporter capability is still unavailable";
210
+ return undefined;
28
211
  }
29
212
  /** Observe supported frozen Vitest commands without changing their selection. */
30
213
  export async function withFrontendTestReporter(input) {
31
- // Compound commands/wrappers need their own adapter: never append flags to
32
- // a different shell command, change selection, or override a user reporter.
33
- if (/[;&|<>`\n\r$]/.test(input.command) || /--(?:reporter|outputFile)/.test(input.command))
34
- return undefined;
35
- // Frozen verify commands are env-prefixed bash wrappers —
36
- // `HARNESS_ALLOW_ACTIVE_DAG_RUNS=1 'bash' '-lc' 'npm test'` — so the JSON
37
- // reporter must be injected INSIDE the quoted payload; appending it to the
38
- // wrapper would target bash's own CLI and produce no test report at all
39
- // (smoke r54: every behavior verification target then failed the trace
40
- // gate with "test execution report unavailable").
41
- const bashWrapped = /^((?:[A-Za-z_][A-Za-z0-9_]*=(?:'[^']*'|[^\s'"]+)\s+)*)'bash'\s+'-lc'\s+'([^']*)'\s*$/.exec(input.command);
42
- if (bashWrapped) {
43
- const inner = bashWrapped[2];
44
- if (/[;&|<>`\n\r$]/.test(inner) || /--(?:reporter|outputFile)/.test(inner))
45
- return undefined;
46
- const adaptedInner = await adaptPlainTestCommand(inner, input.cwd);
47
- if (!adaptedInner)
48
- return undefined;
49
- const quote = (value) => `'${value.replace(/'/g, `'"'"'`)}'`;
50
- return input.command.replace(`'${inner}'`, `'${adaptedInner} --reporter=json --outputFile=${quote(input.reportPath)}'`);
51
- }
52
- const adapted = await adaptPlainTestCommand(input.command, input.cwd);
53
- if (!adapted)
54
- return undefined;
55
- const quote = (value) => `'${value.replace(/'/g, `'"'"'`)}'`;
56
- return `${adapted} --reporter=json --outputFile=${quote(input.reportPath)}`;
214
+ return (await inspectFrontendTestCommand(input)).withReporter?.(input.reportPath);
57
215
  }
58
216
  const resultSchema = z.object({
59
217
  testResults: z.array(z.object({
@@ -1,3 +1,4 @@
1
+ import { sanitizeRepairAssertions } from "./frontend-repair-assertions.js";
1
2
  import { readFile } from "node:fs/promises";
2
3
  import { z } from "zod";
3
4
  import { sha256OfCanonicalJson } from "../../task/contract/hash.js";
@@ -196,13 +197,17 @@ export const reviewFindingSchema = z
196
197
  line: z.number().int().positive().optional(),
197
198
  issue: z.string().min(1),
198
199
  requiredChange: z.string().min(1).optional(),
199
- repairAssertions: z.array(z.object({
200
- check: z.enum(["file-absent", "state-present", "vt-changed"]),
201
- token: z.string().min(1), source: z.string(),
202
- expectedRequirementIds: z.array(z.string().min(1)).optional(),
203
- }).strict()).optional(),
200
+ repairAssertions: z.unknown().optional(),
201
+ repairAssertionDiagnostics: z.array(z.string()).optional(),
204
202
  })
205
- .strict();
203
+ .strict().transform(finding => {
204
+ const { repairAssertions, ...base } = finding;
205
+ if (repairAssertions === undefined)
206
+ return base;
207
+ const sanitized = sanitizeRepairAssertions(repairAssertions);
208
+ const diagnostics = [...(finding.repairAssertionDiagnostics ?? []), ...sanitized.diagnostics];
209
+ return { ...finding, repairAssertions: sanitized.assertions, ...(diagnostics.length ? { repairAssertionDiagnostics: [...new Set(diagnostics)] } : {}) };
210
+ });
206
211
  /** `approve_review` terminal fact payload. Approval may carry informational
207
212
  * findings (Minor/Info) but never Critical/Important. */
208
213
  export const approveReviewFactSchema = z
@@ -261,6 +261,8 @@ export async function evaluateFrontendWriterTracePreflight(input) {
261
261
  ...(target.symbol ? { symbol: target.symbol } : {}),
262
262
  status: fileTrace.issues.length ? "failed" : "ok",
263
263
  evidence: fileTrace.issues.length ? "unavailable" : "file-exists",
264
+ plannedEvidenceLevel: target.evidenceLevel ?? "unknown",
265
+ ...(target.boundary ? { boundary: target.boundary } : {}),
264
266
  behaviorCoverage: "unconfirmed",
265
267
  matchedNodeIds: [],
266
268
  issues: fileTrace.issues,
@@ -322,6 +324,8 @@ export async function evaluateVerificationTargets(input) {
322
324
  evidence: issues.length
323
325
  ? "unavailable"
324
326
  : target.mode === "static" ? "command-execution" : "test-file-execution",
327
+ plannedEvidenceLevel: target.evidenceLevel ?? "unknown",
328
+ ...(target.boundary ? { boundary: target.boundary } : {}),
325
329
  behaviorCoverage: "unconfirmed",
326
330
  matchedNodeIds: owners ?? [],
327
331
  issues,
@@ -494,6 +498,7 @@ export async function runFrontendVerificationTraceGate(input) {
494
498
  contractSchemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
495
499
  browserStatus: "not-run",
496
500
  visualStatus: "not-run",
501
+ testExecutionEvidence: input.evidence?.behavior.testExecutionEvidence ?? [],
497
502
  targets,
498
503
  mockStrategy: contract.mockApi.strategy,
499
504
  mockCommandLabels,
@@ -515,6 +520,7 @@ export async function runFrontendVerificationTraceGate(input) {
515
520
  mockCommandNodeId: result.mockCommandNodeId,
516
521
  mockCommandTexts: result.mockCommandTexts,
517
522
  targets: result.targets,
523
+ testExecutionEvidence: result.testExecutionEvidence,
518
524
  }, null, 2)}\n`, "utf8");
519
525
  result.artifactPath = artifactPath;
520
526
  }
@@ -163,7 +163,7 @@ export function deriveWriteSet(contract, options) {
163
163
  const candidates = [
164
164
  ...contract.targets.files,
165
165
  ...contract.verificationTargets
166
- .filter((target) => target.mode !== "static")
166
+ .filter((target) => target.mode !== "static" && !options?.existingVerificationPaths?.includes(target.file))
167
167
  .map((target) => target.file),
168
168
  ];
169
169
  // Expand glob entries (e.g. `src/**` inherited from the skeleton) against
@@ -249,6 +249,7 @@ export function deriveFrontendWriterAdmissionShell(input) {
249
249
  }
250
250
  const derived = deriveWriteSet(input.contract, {
251
251
  workspaceFiles: input.workspaceFiles,
252
+ existingVerificationPaths: input.existingVerificationPaths,
252
253
  });
253
254
  findings.push(...derived.findings);
254
255
  const baseline = freezeBaseline(input.baseline);