@tea-agent/loop-agent 0.44.0-next.11 → 0.44.0-next.13

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 (132) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/commands/init.js +1 -0
  4. package/dist/executors/dag-pi-executor.js +11 -6
  5. package/dist/executors/pi-writer-tool-policy.js +60 -4
  6. package/dist/executors/playwright-cli-launcher.js +31 -45
  7. package/dist/executors/shell-executor.js +39 -11
  8. package/dist/executors/shell-write-guard.js +24 -67
  9. package/dist/governance/exec-plans.js +10 -1
  10. package/dist/task/config-types.js +4 -2
  11. package/dist/task/source-prepare/build-draft.js +1 -0
  12. package/dist/task/source-prepare/path-policy.js +17 -7
  13. package/dist/worker/console/chat/browser-automation.js +343 -38
  14. package/dist/worker/console/chat/browser-content.js +74 -0
  15. package/dist/worker/console/chat/browser-diagnostics.js +250 -0
  16. package/dist/worker/console/chat/browser-evaluation.js +58 -0
  17. package/dist/worker/console/chat/browser-operation-queue.js +19 -0
  18. package/dist/worker/console/chat/browser-pick.js +113 -0
  19. package/dist/worker/console/chat/browser-routes.js +33 -5
  20. package/dist/worker/console/chat/browser-screenshot-result.js +47 -0
  21. package/dist/worker/console/chat/browser-storage.js +89 -0
  22. package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +125 -174
  23. package/dist/worker/console/chat/pi-runtime.js +1 -1
  24. package/dist/worker/console/chat/repo-browser.js +80 -2
  25. package/dist/worker/console/chat/repo-preview-kind.js +155 -0
  26. package/dist/worker/console/chat/session-mode.js +1 -1
  27. package/dist/worker/console/chat/session-store.js +4 -2
  28. package/dist/worker/console/chat/tools.js +12 -0
  29. package/dist/worker/console/routes.js +5 -0
  30. package/dist/worker/console/static/app-icon.svg +39 -0
  31. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-r1NAPU_5.js → abnfDiagram-N423BO3Z-BA7qCL_r.js} +1 -1
  32. package/dist/worker/console/static/assets/{arc-dT5T4sVL.js → arc-Df09ajwL.js} +1 -1
  33. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CP_cOq46.js → architectureDiagram-T3A2C74G-CpF9sw5R.js} +1 -1
  34. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-sToEZxEf.js → blockDiagram-VBNYF7ZC-BtPys8ja.js} +1 -1
  35. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DSxwwi2m.js → c4Diagram-5PPSVZJV-DZPnTWqT.js} +1 -1
  36. package/dist/worker/console/static/assets/channel-DiTNLZUm.js +1 -0
  37. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B9fh0IkF.js → chunk-2GRJ4B5K-BD9D4h1p.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CAUNl-A6.js → chunk-2Q5K7J3B-Svn8yBmn.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BomeqbNX.js → chunk-5RXB4S5H-B8EbQj-S.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BNAuTOLE.js → chunk-5VM5RSS4-tENZpcpF.js} +1 -1
  41. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DfzjRD5y.js → chunk-6Q2QTUOP-DBx71Lco.js} +1 -1
  42. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C3uE-yc2.js → chunk-GF5L2VYU-1v_QamMz.js} +1 -1
  43. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-C3sQMMYr.js → chunk-JWPE2WC7-BAMCNNBG.js} +1 -1
  44. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D_AtEYQL.js → chunk-KBJHAD2P-BvuEury-.js} +1 -1
  45. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DW4TwhKv.js → chunk-RYQCIY6F-YqFuHp8s.js} +1 -1
  46. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CTQDmJcp.js → chunk-XXDRQBXY-ouI5T7Mr.js} +1 -1
  47. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DgL8aiyZ.js +1 -0
  48. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DgL8aiyZ.js +1 -0
  49. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-iMqPuBsL.js → cose-bilkent-JH36ORCC-Bri0_Izk.js} +1 -1
  50. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DktIuDWs.js → cynefin-VYW2F7L2-e8Vhhi4X.js} +1 -1
  51. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BgVnjyEs.js → cynefinDiagram-MW4NZA55-tSIXGQiJ.js} +1 -1
  52. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CR25E4bu.js → dagre-VZM6K2ZE-Rl-sF5pt.js} +1 -1
  53. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CPM2h5HY.js → diagram-7IWD3JNH-Db6ddPz8.js} +1 -1
  54. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DqF0x2g1.js → diagram-B4RE2ZJO-Jwd_Xw5S.js} +1 -1
  55. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-G8BYp2eb.js → diagram-LBJQPF4R-CeC_8InC.js} +1 -1
  56. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BbC4kT_D.js → diagram-Q27KOJAE-LKPRaNq8.js} +1 -1
  57. package/dist/worker/console/static/assets/{diagram-UB23O5K3-CKZwZ-G5.js → diagram-UB23O5K3-DlVQytMW.js} +1 -1
  58. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-kJDmheia.js → ebnfDiagram-BXEA7PRR-DnfJZwO-.js} +1 -1
  59. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DFTVHyxB.js → erDiagram-JOGREHBK-3axVSXca.js} +1 -1
  60. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BqdN5_os.js → flowDiagram-UKHOOZJN-xe04-_L-.js} +1 -1
  61. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-SYDjVakh.js → ganttDiagram-PKOTCBZU-hpmRINiD.js} +1 -1
  62. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js → gitGraphDiagram-DS77QQ5N-6HSNz9Ko.js} +1 -1
  63. package/dist/worker/console/static/assets/index-C_8_gWal.css +1 -0
  64. package/dist/worker/console/static/assets/{index-CTMZsDmh.js → index-DybcVXCu.js} +102 -100
  65. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DIeAmJ45.js → infoDiagram-6WML65LV-BF6ksWTX.js} +1 -1
  66. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-oikqUAlo.js → ishikawaDiagram-WSZJBQD7-DXisdeD7.js} +1 -1
  67. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-mKEF_xYT.js → journeyDiagram-NVQOT4AX-BWu65wmu.js} +1 -1
  68. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BdWJHPnN.js → kanban-definition-27J2QSJJ-hfNhHqAI.js} +1 -1
  69. package/dist/worker/console/static/assets/{linear-DGRlieMH.js → linear-D6HTfQcp.js} +1 -1
  70. package/dist/worker/console/static/assets/{mermaid.core-4P9yPC9c.js → mermaid.core-Iv91R1WP.js} +5 -5
  71. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-5PTSkos6.js → mindmap-definition-FAOFIHXS-CfPChwjR.js} +1 -1
  72. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DONIFpG5.js → pegDiagram-VL7TDLO6-B7TcoqHz.js} +1 -1
  73. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-D_Z4S70M.js → pieDiagram-7S7Q4E2Y-ouyPKMtQ.js} +1 -1
  74. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CAsjh8iY.js → quadrantDiagram-CIZ2JOQS-B9tuyLbh.js} +1 -1
  75. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Cx4Dd6ac.js → railroadDiagram-AXF67PYL-Ck4q9nJs.js} +1 -1
  76. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-cQb7mwyH.js → requirementDiagram-LRYGKXZP-wXLX5Dnp.js} +1 -1
  77. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cd-DhUdf.js → sankeyDiagram-W5VNT64P-D2XWw6_m.js} +1 -1
  78. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-AlKYRgIk.js → sequenceDiagram-SI44F4Z6-DzxKiug4.js} +1 -1
  79. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CUy_ET4e.js → sizeCapture-X5ZJPWSS-DWUqK5Us.js} +1 -1
  80. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CTuVe6Sa.js → stateDiagram-OKZ733FA-D5Pa4O5g.js} +1 -1
  81. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-C0QIbqO4.js +1 -0
  82. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-ChFLJb-v.js → swimlanes-SLNWSIFB-VH7cRnsJ.js} +2 -2
  83. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BB9CzRHB.js +8 -0
  84. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DZZeiAPr.js → timeline-definition-Z64GVDOM-C4HM5NoZ.js} +1 -1
  85. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-t4O91cZU.js → vennDiagram-T6HMQDX7-Bu-4hOlA.js} +1 -1
  86. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CV5qGHZu.js → wardleyDiagram-T6FBY63Y-CasFEvvl.js} +1 -1
  87. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DyXbytUI.js → xychartDiagram-ELKLHX3M-D-gpwPSA.js} +1 -1
  88. package/dist/worker/console/static/index.html +5 -2
  89. package/dist/worker/console/static/manifest.webmanifest +19 -0
  90. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +57 -9
  91. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +6 -0
  92. package/dist/worker/console/workspace-context.js +1 -0
  93. package/dist/worker/materialize/harness-task-lineage.js +4 -2
  94. package/dist/worker/observe/static/console-theme.css +33 -29
  95. package/dist/worker/observe/static/console-theme.js +38 -0
  96. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +3 -1
  97. package/dist/workflows/dag/backend-test-layout.js +27 -7
  98. package/dist/workflows/dag/backend-test-markdown-workflow.js +3 -1
  99. package/dist/workflows/dag/frontend-repair.js +24 -0
  100. package/dist/workflows/dag/frontend-review-scopes.js +10 -2
  101. package/dist/workflows/dag/frontend-test-case-checklist.js +12 -3
  102. package/dist/workflows/dag/frontend-test-environment-probe.js +0 -2
  103. package/dist/workflows/dag/frontend-test-execution-evidence.js +163 -67
  104. package/dist/workflows/dag/frontend-test-framework-adapters.js +309 -0
  105. package/dist/workflows/dag/frontend-test-layout.js +41 -12
  106. package/dist/workflows/dag/hybrid/sources.js +21 -2
  107. package/dist/workflows/dag/hybrid/templates/backend-test.js +65 -50
  108. package/dist/workflows/dag/hybrid/templates/frontend-test.js +58 -37
  109. package/dist/workflows/dag/test-artifact-namespace.js +73 -0
  110. package/dist/workflows/dag/validate.js +22 -3
  111. package/docs/architecture/dag-execution.md +3 -3
  112. package/docs/templates/README.md +4 -4
  113. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +3 -3
  114. package/docs/templates/backend-test-dag.json +708 -708
  115. package/docs/templates/backend-test-dag.review-cases.prompt.md +3 -3
  116. package/docs/templates/frontend-test-case-checklist.md +2 -2
  117. package/docs/templates/frontend-test-dag.generate-cases.prompt.md +7 -7
  118. package/docs/templates/frontend-test-dag.json +46 -37
  119. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +2 -2
  120. package/docs/templates/frontend-test-dag.retrospect.prompt.md +1 -1
  121. package/docs/templates/frontend-test-dag.review-cases.prompt.md +1 -1
  122. package/package.json +7 -2
  123. package/skills/fe-test-ui-scout/SKILL.md +4 -4
  124. package/skills/fe-test-ui-scout/references/ledger-schema.md +1 -1
  125. package/skills/playwright-cli/SKILL.md +1 -1
  126. package/skills/playwright-cli-case-generator/SKILL.md +4 -4
  127. package/dist/worker/console/static/assets/channel-CyBY_yGk.js +0 -1
  128. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +0 -1
  129. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +0 -1
  130. package/dist/worker/console/static/assets/index-B28Onmfy.css +0 -1
  131. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +0 -1
  132. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +0 -8
@@ -0,0 +1,309 @@
1
+ import { realpathSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { z } from "zod";
4
+ /**
5
+ * Framework adapters decouple the frontend behavior-verification contract from
6
+ * any single test runner.
7
+ *
8
+ * The contract, writer admission and execution all need three things from a
9
+ * frozen verify command:
10
+ * 1. whether the runner can emit a machine-readable execution report at all
11
+ * (capability), so an unsupported runner fails closed *before* implement;
12
+ * 2. how to ask that runner for a report without clobbering the project's
13
+ * own reporter configuration;
14
+ * 3. how to read that report back as uniform file/title/status triples.
15
+ *
16
+ * Historically all three were hardcoded to Vitest (`vitest-json-v1`), while the
17
+ * command classifier was already framework-agnostic. Jest and Playwright
18
+ * commands were therefore classified into the behavior lane correctly and then
19
+ * rejected as "unsupported", surfacing a capability gap to the user as if they
20
+ * had misconfigured their project. Adapters make the two sources of truth agree.
21
+ */
22
+ /** Report protocols a frozen behavior command may be observed through. */
23
+ export const frontendTestReportSchemeSchema = z.enum([
24
+ "vitest-json-v1",
25
+ "jest-json-v1",
26
+ "playwright-json-v1",
27
+ ]);
28
+ /** Runtime guard for a persisted scheme, e.g. read back from a trace artifact. */
29
+ export const isFrontendTestReportScheme = (value) => typeof value === "string" &&
30
+ frontendTestReportSchemeSchema.options.includes(value);
31
+ const asRecord = (value) => typeof value === "object" && value !== null
32
+ ? value
33
+ : undefined;
34
+ /**
35
+ * Resolve a runner-relative path into contract workspace coordinates.
36
+ *
37
+ * A runner may report an absolute path (`config.rootDir` is absolute in a
38
+ * Playwright report) or one relative to its own root. Either way the result is
39
+ * expressed against `workspaceRoot`, which is resolved physically first: a
40
+ * workspace reached through a symlink (macOS `/var` -> `/private/var`, i.e. the
41
+ * default `os.tmpdir()`) is never a lexical prefix of the runner's canonical
42
+ * path, so comparing raw strings would yield a `../..` escape instead of a
43
+ * contract-relative file.
44
+ */
45
+ const toWorkspacePath = (raw, cwd, workspaceRoot) => {
46
+ const absolute = path.resolve(cwd, raw);
47
+ const physical = (target) => {
48
+ try {
49
+ return realpathSync(target);
50
+ }
51
+ catch {
52
+ return target;
53
+ }
54
+ };
55
+ const root = physical(path.resolve(workspaceRoot));
56
+ // The path may itself sit under the unresolved root; re-anchor it physically
57
+ // by resolving its root-relative tail against the physical root.
58
+ const anchored = absolute.startsWith(path.resolve(workspaceRoot) + path.sep)
59
+ ? path.join(root, path.relative(path.resolve(workspaceRoot), absolute))
60
+ : absolute;
61
+ return path.relative(root, physical(anchored)).replace(/\\/g, "/");
62
+ };
63
+ /**
64
+ * Strip package-manager indirection so `npx vitest run`, `pnpm exec vitest
65
+ * run`, `yarn exec vitest run` and a bare `vitest run` all reduce to the same
66
+ * runner invocation. Returns the remaining words.
67
+ */
68
+ export function resolveRunnerArgv(argv) {
69
+ const words = [...argv];
70
+ // Skip leading env assignments (`CI=1 vitest run`).
71
+ while (words.length > 0 && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0])) {
72
+ words.shift();
73
+ }
74
+ const head = words[0];
75
+ if (head === "npx" || head === "pnpx" || head === "bunx") {
76
+ words.shift();
77
+ // `npx --no-install vitest run` / `npx -y vitest run`
78
+ while (words[0]?.startsWith("-"))
79
+ words.shift();
80
+ return words;
81
+ }
82
+ if ((head === "pnpm" || head === "yarn" || head === "npm") && words[1] === "exec") {
83
+ words.splice(0, 2);
84
+ while (words[0]?.startsWith("-"))
85
+ words.shift();
86
+ return words;
87
+ }
88
+ return words;
89
+ }
90
+ /** Runner binaries each adapter claims, keyed by adapter scheme. */
91
+ const VITEST_BINARIES = new Set(["vitest"]);
92
+ const JEST_BINARIES = new Set(["jest"]);
93
+ const PLAYWRIGHT_BINARIES = new Set(["playwright"]);
94
+ const basenameOf = (value) => value.split(/[\\/]/).pop() ?? "";
95
+ /** True when `runner` invokes `binary` directly (allowing an explicit path). */
96
+ const invokesBinary = (runner, binaries) => binaries.has(basenameOf(runner[0] ?? ""));
97
+ const vitestAdapter = {
98
+ scheme: "vitest-json-v1",
99
+ runner: "vitest",
100
+ matches: ({ runner }) => invokesBinary(runner, VITEST_BINARIES),
101
+ // Vitest's json reporter writes to stdout unless an output file is given,
102
+ // and the executor reads the report from disk, so both flags are required.
103
+ reportSuffix: () => `--reporter=json --outputFile=`,
104
+ parse: (raw, cwd, workspaceRoot) => {
105
+ const schema = z.object({
106
+ testResults: z.array(z.object({
107
+ name: z.string().min(1),
108
+ assertionResults: z.array(z.object({
109
+ title: z.string(),
110
+ fullName: z.string().optional(),
111
+ ancestorTitles: z.array(z.string()).optional(),
112
+ status: z.string(),
113
+ })),
114
+ })),
115
+ });
116
+ return schema
117
+ .parse(raw)
118
+ .testResults.flatMap((suite) => suite.assertionResults.map((test) => ({
119
+ file: toWorkspacePath(suite.name, cwd, workspaceRoot),
120
+ title: test.fullName ??
121
+ [...(test.ancestorTitles ?? []), test.title].join(" "),
122
+ status: test.status,
123
+ })));
124
+ },
125
+ };
126
+ const jestAdapter = {
127
+ scheme: "jest-json-v1",
128
+ runner: "jest",
129
+ matches: ({ runner }) => invokesBinary(runner, JEST_BINARIES),
130
+ reportSuffix: () => `--json --outputFile`,
131
+ parse: (raw, cwd, workspaceRoot) => {
132
+ // Jest's `--json` shape: testResults[] carry a per-file `name` plus
133
+ // assertionResults[] that mirror Vitest's field names.
134
+ const schema = z.object({
135
+ testResults: z.array(z.object({
136
+ name: z.string().min(1),
137
+ assertionResults: z.array(z.object({
138
+ title: z.string(),
139
+ fullName: z.string().optional(),
140
+ ancestorTitles: z.array(z.string()).optional(),
141
+ status: z.string(),
142
+ })),
143
+ })),
144
+ });
145
+ return schema
146
+ .parse(raw)
147
+ .testResults.flatMap((suite) => suite.assertionResults.map((test) => ({
148
+ file: toWorkspacePath(suite.name, cwd, workspaceRoot),
149
+ title: test.fullName ??
150
+ [...(test.ancestorTitles ?? []), test.title].join(" "),
151
+ status: test.status,
152
+ })));
153
+ },
154
+ };
155
+ /**
156
+ * Playwright's JSON reporter emits a nested `suites` tree whose leaf `specs`
157
+ * carry `tests[].results[].status`. File paths live on the enclosing `file`
158
+ * field of each suite node, so the tree is walked while tracking it.
159
+ */
160
+ const playwrightAdapter = {
161
+ scheme: "playwright-json-v1",
162
+ runner: "playwright",
163
+ matches: ({ runner }) => {
164
+ if (!invokesBinary(runner, PLAYWRIGHT_BINARIES))
165
+ return false;
166
+ // `playwright test` runs tests; `playwright install`/`codegen` do not.
167
+ return runner[1] === "test" || runner[1] === undefined;
168
+ },
169
+ reportSuffix: () => `--reporter=json`,
170
+ // Playwright ignores an output filename on the CLI (`--output` is its
171
+ // artifacts directory); its JSON reporter goes to stdout unless this
172
+ // variable names a file.
173
+ reportPathEnv: "PLAYWRIGHT_JSON_OUTPUT_NAME",
174
+ parse: (raw, cwd, workspaceRoot) => {
175
+ const suiteSchema = z.lazy(() => z.object({
176
+ file: z.string().optional(),
177
+ title: z.string().optional(),
178
+ specs: z
179
+ .array(z.object({
180
+ title: z.string(),
181
+ ok: z.boolean(),
182
+ tests: z.array(z.object({
183
+ status: z.string().optional(),
184
+ results: z
185
+ .array(z.object({
186
+ status: z.string(),
187
+ }))
188
+ .optional(),
189
+ })),
190
+ }))
191
+ .optional(),
192
+ suites: z.array(suiteSchema).optional(),
193
+ }));
194
+ const root = z
195
+ .object({
196
+ suites: z.array(suiteSchema).optional(),
197
+ // Playwright reports each suite's `file` relative to the resolved
198
+ // `testDir` (`config.rootDir`), not to the process cwd. A project
199
+ // with `testDir: "./tests"` reports `orders.spec.js` for
200
+ // `tests/orders.spec.js`, so resolving against cwd would produce a
201
+ // path that can never match a contract target.
202
+ config: z
203
+ .object({ rootDir: z.string().optional() })
204
+ .optional(),
205
+ })
206
+ .parse(raw);
207
+ // Prefer the runner's own reported root; fall back to cwd only when the
208
+ // report omitted it (older report shapes).
209
+ const reportedRoot = root.config?.rootDir;
210
+ const baseCwd = typeof reportedRoot === "string" && reportedRoot.length > 0
211
+ ? reportedRoot
212
+ : cwd;
213
+ const out = [];
214
+ const walk = (node, inheritedFile) => {
215
+ const record = asRecord(node);
216
+ if (!record)
217
+ return;
218
+ const file = typeof record.file === "string" ? record.file : inheritedFile;
219
+ for (const spec of record.specs ?? []) {
220
+ const specRecord = asRecord(spec);
221
+ if (!specRecord)
222
+ continue;
223
+ const title = String(specRecord.title ?? "");
224
+ const tests = specRecord.tests ?? [];
225
+ // A spec with several projects/retries yields several tests; the
226
+ // spec only passes when every attempt passed.
227
+ const statuses = tests.flatMap((test) => {
228
+ const testRecord = asRecord(test);
229
+ const results = testRecord?.results ?? [];
230
+ const attempts = results
231
+ .map((result) => asRecord(result)?.status)
232
+ .filter((value) => typeof value === "string");
233
+ return attempts.length > 0
234
+ ? attempts
235
+ : [String(testRecord?.status ?? "unknown")];
236
+ });
237
+ const status = statuses.every((value) => value === "passed")
238
+ ? "passed"
239
+ : statuses.includes("passed")
240
+ ? "failed"
241
+ : (statuses[0] ?? "unknown");
242
+ out.push({
243
+ file: toWorkspacePath(file ?? "", baseCwd, workspaceRoot),
244
+ title,
245
+ status,
246
+ });
247
+ }
248
+ for (const child of record.suites ?? []) {
249
+ walk(child, file);
250
+ }
251
+ };
252
+ for (const suite of root.suites ?? [])
253
+ walk(suite, undefined);
254
+ return out;
255
+ },
256
+ };
257
+ export const FRONTEND_TEST_ADAPTERS = [
258
+ vitestAdapter,
259
+ jestAdapter,
260
+ playwrightAdapter,
261
+ ];
262
+ /**
263
+ * Runners the container classifier recognizes as behavior verification but
264
+ * that have no adapter yet. Naming them lets the capability gap read as
265
+ * "framework not adapted yet" instead of a user configuration error.
266
+ */
267
+ const KNOWN_UNADAPTED_RUNNERS = new Set([
268
+ "mocha",
269
+ "ava",
270
+ "tap",
271
+ "cypress",
272
+ "testcafe",
273
+ "nightwatch",
274
+ "wdio",
275
+ "webdriverio",
276
+ ]);
277
+ /** Resolve the adapter for a command, or `undefined` when none matches. */
278
+ export function resolveFrontendTestAdapter(context) {
279
+ // A package script owns the detail: `npm test` must resolve through the
280
+ // script body, not the wrapper, or `npm test` and `vitest run` would
281
+ // disagree about capability.
282
+ if (context.scriptArgv) {
283
+ const scriptRunner = resolveRunnerArgv(context.scriptArgv);
284
+ const fromScript = FRONTEND_TEST_ADAPTERS.find((adapter) => adapter.matches({ ...context, runner: scriptRunner }));
285
+ if (fromScript)
286
+ return fromScript;
287
+ }
288
+ return FRONTEND_TEST_ADAPTERS.find((adapter) => adapter.matches(context));
289
+ }
290
+ /**
291
+ * Detect a recognized-but-unadapted runner so callers can report a capability
292
+ * gap. Returns the runner name when the command is a test runner we know of
293
+ * but cannot observe.
294
+ */
295
+ export function detectUnadaptedRunner(runner) {
296
+ const binary = basenameOf(runner[0] ?? "").replace(/\.(?:js|cjs|mjs|cmd|exe)$/u, "");
297
+ if (KNOWN_UNADAPTED_RUNNERS.has(binary))
298
+ return binary;
299
+ // `node --test`, `node --test-only`, plus `deno test`/`bun test`.
300
+ if (binary === "node" && runner.includes("--test"))
301
+ return "node --test";
302
+ if (binary === "deno" && runner[1] === "test")
303
+ return "deno test";
304
+ if (binary === "bun" && runner[1] === "test")
305
+ return "bun test";
306
+ return undefined;
307
+ }
308
+ /** Empty `file` means the runner reported a spec outside the workspace. */
309
+ export const isOutsideWorkspacePath = (file) => file === ".." || file.startsWith("../") || path.isAbsolute(file);
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { canonicalTestArtifactRoot, } from "./test-artifact-namespace.js";
2
3
  /**
3
4
  * Frontend-test artifact layout (configurable testRoot).
4
5
  *
@@ -6,14 +7,16 @@ import path from "node:path";
6
7
  * frozen on the spec for the whole run. Writers, gates, Playwright fences
7
8
  * and TaskSpec allowedPaths all derive from this single object.
8
9
  *
9
- * Default layout keeps the historical frozen contract:
10
- * testcase/frontend/rag/**
11
- * testcase/frontend/cases/**
12
- * testcase/frontend/evidence/**
13
- * testcase/frontend/reports/**
14
- * testcase/frontend/fixtures/**
10
+ * Default layout (since 2026-09-17) lives under the shared runtime namespace:
11
+ * .harness/testcase/frontend/rag/**
12
+ * .harness/testcase/frontend/cases/**
13
+ * .harness/testcase/frontend/evidence/**
14
+ * .harness/testcase/frontend/reports/**
15
+ * .harness/testcase/frontend/fixtures/**
15
16
  */
16
- export const FRONTEND_TEST_DEFAULT_TEST_ROOT = "testcase/frontend";
17
+ export const FRONTEND_TEST_DEFAULT_TEST_ROOT = canonicalTestArtifactRoot("frontend-test");
18
+ /** Historical pre-2026-09-17 default root, kept for text rewrites only. */
19
+ export const FRONTEND_TEST_LEGACY_DEFAULT_TEST_ROOT = "testcase/frontend";
17
20
  const FORBIDDEN_TEST_ROOT_PREFIXES = [
18
21
  "src",
19
22
  "app",
@@ -59,6 +62,10 @@ function normalizeRelPosix(value, label) {
59
62
  return segments.join("/");
60
63
  }
61
64
  function assertTestRootAllowed(testRoot) {
65
+ // D3: the canonical harness test-artifact root is the ONLY hidden-root
66
+ // exception. Every other hidden or forbidden top segment stays rejected.
67
+ if (testRoot === FRONTEND_TEST_DEFAULT_TEST_ROOT)
68
+ return;
62
69
  const topSegment = testRoot.split("/")[0];
63
70
  if (FORBIDDEN_TEST_ROOT_PREFIXES.some((forbidden) => topSegment === forbidden || topSegment.startsWith(`${forbidden}.`))) {
64
71
  throw new Error(`frontendTest.testRoot must not target production/governance trees: ${testRoot}`);
@@ -67,14 +74,25 @@ function assertTestRootAllowed(testRoot) {
67
74
  throw new Error(`frontendTest.testRoot must not target hidden directories: ${testRoot}`);
68
75
  }
69
76
  }
77
+ function resolveTestRootInput(raw, canonical) {
78
+ // D3: only the EXACT canonical harness test-artifact root may be a hidden
79
+ // path. Compare after trimming + stripping trailing slashes so
80
+ // `.harness/testcase/frontend/` matches, but every other hidden spelling
81
+ // falls through to the fail-closed normalizer below.
82
+ const trimmed = raw.trim().replace(/\/+$/, "");
83
+ if (trimmed === canonical)
84
+ return canonical;
85
+ return normalizeRelPosix(raw, "testRoot");
86
+ }
70
87
  /**
71
88
  * Resolve the frozen frontend-test artifact layout from task config.
72
- * Throws on unsafe or ambiguous configuration (fail-closed).
89
+ * Roots of the form `.harness/testcase/frontend` are accepted only as the
90
+ * exact canonical default; all other hidden roots fail closed (D3).
73
91
  */
74
92
  export function resolveFrontendTestLayout(config) {
75
93
  const testRoot = config?.testRoot === undefined || config?.testRoot === null
76
94
  ? FRONTEND_TEST_DEFAULT_TEST_ROOT
77
- : normalizeRelPosix(config.testRoot, "testRoot");
95
+ : resolveTestRootInput(config.testRoot, FRONTEND_TEST_DEFAULT_TEST_ROOT);
78
96
  assertTestRootAllowed(testRoot);
79
97
  const isDefault = testRoot === FRONTEND_TEST_DEFAULT_TEST_ROOT;
80
98
  return {
@@ -98,10 +116,16 @@ export function frontendTestLayoutFromSpec(spec) {
98
116
  return resolveFrontendTestLayout({ testRoot: frozen.testRoot });
99
117
  }
100
118
  const DEFAULT_PREFIX = `${FRONTEND_TEST_DEFAULT_TEST_ROOT}/`;
119
+ const LEGACY_DEFAULT_PREFIX = `${FRONTEND_TEST_LEGACY_DEFAULT_TEST_ROOT}/`;
101
120
  /**
102
- * Longest-prefix rewrite of historical `testcase/frontend/` paths.
103
- * No-op when layout.isDefault so default DAGs stay byte-identical.
104
- * Never replaces the short token `testcase/` (would collide with backend-test).
121
+ * Longest-prefix rewrite of frontend test-root paths in template text.
122
+ *
123
+ * No-op when layout.isDefault (default DAGs stay byte-identical to their
124
+ * templates, which already use the canonical `.harness/testcase/frontend`
125
+ * root). Custom roots rewrite BOTH the canonical default prefix and the
126
+ * historical `testcase/frontend/` prefix, so legacy templates and frozen
127
+ * specs converge onto the configured root. Never replaces the short token
128
+ * `testcase/` alone (would collide with backend-test).
105
129
  */
106
130
  export function applyFrontendTestLayoutToText(text, layout) {
107
131
  if (layout.isDefault)
@@ -119,6 +143,11 @@ export function applyFrontendTestLayoutToText(text, layout) {
119
143
  i += DEFAULT_PREFIX.length;
120
144
  continue;
121
145
  }
146
+ if (text.startsWith(LEGACY_DEFAULT_PREFIX, i)) {
147
+ output += replacement;
148
+ i += LEGACY_DEFAULT_PREFIX.length;
149
+ continue;
150
+ }
122
151
  output += text[i];
123
152
  i += 1;
124
153
  }
@@ -7,6 +7,7 @@ import { DAG_AGENT_RUNTIME_PI_ONLY, DAG_REPAIR_WRITER_PROTOCOL_EXPLICIT_NODE_V1,
7
7
  import { planMavenVerification } from "../../../verification/maven/index.js";
8
8
  import { pathMatchesPattern } from "../../../shared/git-progress.js";
9
9
  import { BASELINE_FORBIDDEN_PATHS } from "../governance-constants.js";
10
+ import { isAnyTestArtifactPath } from "../test-artifact-namespace.js";
10
11
  import { resolveAdapter } from "../../../adapters/index.js";
11
12
  import { loadHarnessManifest } from "../../../governance/harness.js";
12
13
  import { mergeDocumentIndexCompanions } from "../../../governance/document-index-closure.js";
@@ -1534,20 +1535,38 @@ export function patternStaticPrefix(pattern) {
1534
1535
  const wildcard = normalized.search(/[?*]/);
1535
1536
  return (wildcard >= 0 ? normalized.slice(0, wildcard) : normalized).replace(/\/+$/, "");
1536
1537
  }
1537
- export function allowedPathHitsBaselineForbidden(allowedPath) {
1538
+ export function allowedPathHitsBaselineForbidden(allowedPath, options) {
1538
1539
  const probes = probePathsForWriteSetEntry(allowedPath);
1539
1540
  for (const forbidden of BASELINE_FORBIDDEN_PATHS) {
1540
1541
  if (probes.some((probe) => pathMatchesPattern(probe, forbidden)) ||
1541
1542
  pathMatchesPattern(allowedPath, forbidden) ||
1542
1543
  pathMatchesPattern(forbidden, allowedPath)) {
1544
+ // D3 test-artifact exception: dedicated test tasks may own exactly the
1545
+ // canonical `.harness/testcase` roots in task allowedPaths; every other
1546
+ // `.harness` overlap stays a baseline violation.
1547
+ if (options?.testArtifactNamespace &&
1548
+ isHarnessNamespacePattern(forbidden) &&
1549
+ probes.every((probe) => isAnyTestArtifactPath(probe))) {
1550
+ continue;
1551
+ }
1543
1552
  return forbidden;
1544
1553
  }
1545
1554
  }
1546
1555
  return undefined;
1547
1556
  }
1557
+ function isHarnessNamespacePattern(pattern) {
1558
+ const normalized = pattern.replace(/\\/g, "/").replace(/^\.\//, "");
1559
+ return (normalized === ".harness" ||
1560
+ normalized === ".harness/**" ||
1561
+ normalized.startsWith(".harness/"));
1562
+ }
1548
1563
  export function assertTaskAllowedPathsPreflight(taskConfig) {
1564
+ const testArtifactNamespace = taskConfig.taskKind === "backend-test" ||
1565
+ taskConfig.taskKind === "frontend-test";
1549
1566
  for (const allowedPath of taskConfig.allowedPaths) {
1550
- const forbidden = allowedPathHitsBaselineForbidden(allowedPath);
1567
+ const forbidden = allowedPathHitsBaselineForbidden(allowedPath, {
1568
+ testArtifactNamespace,
1569
+ });
1551
1570
  if (forbidden) {
1552
1571
  throw new Error(`task allowedPaths entry "${allowedPath}" overlaps baseline forbidden path "${forbidden}"; task source paths are main-session owned and must not be executor writeSet`);
1553
1572
  }