@tangle-network/agent-runtime 0.105.0 → 0.106.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 (218) hide show
  1. package/README.md +2 -2
  2. package/dist/activation-CRjVfOiy.js +203 -0
  3. package/dist/activation-CRjVfOiy.js.map +1 -0
  4. package/dist/activation-DyWB0K6E.d.ts +98 -0
  5. package/dist/agent.d.ts +216 -234
  6. package/dist/agent.js +615 -547
  7. package/dist/agent.js.map +1 -1
  8. package/dist/analyst-loop-DGdtVENg.js +517 -0
  9. package/dist/analyst-loop-DGdtVENg.js.map +1 -0
  10. package/dist/analyst-loop.d.ts +10 -43
  11. package/dist/analyst-loop.js +2 -11
  12. package/dist/backends-CiOCyRHb.js +743 -0
  13. package/dist/backends-CiOCyRHb.js.map +1 -0
  14. package/dist/candidate-execution/index.d.ts +4 -192
  15. package/dist/candidate-execution/index.js +5 -66
  16. package/dist/candidate-execution-DqMbv1rU.js +488 -0
  17. package/dist/candidate-execution-DqMbv1rU.js.map +1 -0
  18. package/dist/coder-yhVWbdWc.js +40 -0
  19. package/dist/coder-yhVWbdWc.js.map +1 -0
  20. package/dist/conversation-BpLQZGPH.js +1265 -0
  21. package/dist/conversation-BpLQZGPH.js.map +1 -0
  22. package/dist/conversation.d.ts +2 -804
  23. package/dist/conversation.js +2 -55
  24. package/dist/environment-provider-CfLUmKmf.js +1168 -0
  25. package/dist/environment-provider-CfLUmKmf.js.map +1 -0
  26. package/dist/environment-provider-IUGU3epE.d.ts +964 -0
  27. package/dist/environment-provider.d.ts +2 -78
  28. package/dist/environment-provider.js +2 -18
  29. package/dist/errors-DEAvWQPy.js +111 -0
  30. package/dist/errors-DEAvWQPy.js.map +1 -0
  31. package/dist/improvement-adapter-Cr5TBSji.d.ts +166 -0
  32. package/dist/improvement-cycle-C095kS_g.js +2624 -0
  33. package/dist/improvement-cycle-C095kS_g.js.map +1 -0
  34. package/dist/index-BGdxAwfO.d.ts +838 -0
  35. package/dist/index-C-FYUuFG.d.ts +193 -0
  36. package/dist/index-CbNM40xG.d.ts +140 -0
  37. package/dist/index-DruSKs3Z.d.ts +7224 -0
  38. package/dist/index-Efjb3nrQ.d.ts +671 -0
  39. package/dist/index.d.ts +14 -1058
  40. package/dist/index.js +2106 -2082
  41. package/dist/index.js.map +1 -1
  42. package/dist/intelligence.d.ts +768 -889
  43. package/dist/intelligence.js +1466 -1367
  44. package/dist/intelligence.js.map +1 -1
  45. package/dist/{kb-gate-CwHO0vz6.d.ts → kb-gate-C8z2juK8.d.ts} +33 -31
  46. package/dist/kb-gate-DpaSwXVx.js +80 -0
  47. package/dist/kb-gate-DpaSwXVx.js.map +1 -0
  48. package/dist/knowledge-Ku-mPP4f.js +433 -0
  49. package/dist/knowledge-Ku-mPP4f.js.map +1 -0
  50. package/dist/knowledge.d.ts +2 -150
  51. package/dist/knowledge.js +2 -40
  52. package/dist/local-harness-Dh8PJ0ot.d.ts +455 -0
  53. package/dist/loop-runner-bin-BXsFEUcf.d.ts +162 -0
  54. package/dist/loop-runner-bin-CRagyuNb.js +268 -0
  55. package/dist/loop-runner-bin-CRagyuNb.js.map +1 -0
  56. package/dist/loop-runner-bin.d.ts +2 -15
  57. package/dist/loop-runner-bin.js +2 -24
  58. package/dist/loops.d.ts +5 -4164
  59. package/dist/loops.js +8 -419
  60. package/dist/mcp/bin.d.ts +1 -1
  61. package/dist/mcp/bin.js +116 -146
  62. package/dist/mcp/bin.js.map +1 -1
  63. package/dist/mcp/index.d.ts +295 -436
  64. package/dist/mcp/index.js +1242 -1177
  65. package/dist/mcp/index.js.map +1 -1
  66. package/dist/mcp/memory-bin.d.ts +1 -1
  67. package/dist/mcp/memory-bin.js +37 -16
  68. package/dist/mcp/memory-bin.js.map +1 -1
  69. package/dist/memory-server-DL6cE2Ag.js +234 -0
  70. package/dist/memory-server-DL6cE2Ag.js.map +1 -0
  71. package/dist/model-resolution-Btd9iIKV.js +98 -0
  72. package/dist/model-resolution-Btd9iIKV.js.map +1 -0
  73. package/dist/openai-tools-B68JaOCx.d.ts +24 -0
  74. package/dist/openai-tools-CKLy1C7M.js +44 -0
  75. package/dist/openai-tools-CKLy1C7M.js.map +1 -0
  76. package/dist/otel-export-D34IQW1C.js +843 -0
  77. package/dist/otel-export-D34IQW1C.js.map +1 -0
  78. package/dist/platform.d.ts +188 -185
  79. package/dist/platform.js +185 -218
  80. package/dist/platform.js.map +1 -1
  81. package/dist/prepare-B53sPV7M.js +4292 -0
  82. package/dist/prepare-B53sPV7M.js.map +1 -0
  83. package/dist/primeintellect/index.d.ts +149 -145
  84. package/dist/primeintellect/index.js +626 -1003
  85. package/dist/primeintellect/index.js.map +1 -1
  86. package/dist/profiles.d.ts +296 -490
  87. package/dist/profiles.js +722 -749
  88. package/dist/profiles.js.map +1 -1
  89. package/dist/protected-model-port-BLPH5VVv.js +722 -0
  90. package/dist/protected-model-port-BLPH5VVv.js.map +1 -0
  91. package/dist/protected-model-port-BP6Z4eau.d.ts +283 -0
  92. package/dist/protected-redaction--F3v1oo8.js +200 -0
  93. package/dist/protected-redaction--F3v1oo8.js.map +1 -0
  94. package/dist/redact-D-u-rrcn.js +82 -0
  95. package/dist/redact-D-u-rrcn.js.map +1 -0
  96. package/dist/redact-kbIOjfjM.d.ts +452 -0
  97. package/dist/researcher-CoVqNhfI.js +433 -0
  98. package/dist/researcher-CoVqNhfI.js.map +1 -0
  99. package/dist/runtime-5zM5XxLr.js +5408 -0
  100. package/dist/runtime-5zM5XxLr.js.map +1 -0
  101. package/dist/sandbox-events-DeI5xX8P.js +316 -0
  102. package/dist/sandbox-events-DeI5xX8P.js.map +1 -0
  103. package/dist/spawn-journal-CwPvKUTa.js +456 -0
  104. package/dist/spawn-journal-CwPvKUTa.js.map +1 -0
  105. package/dist/structural-rollout-IXUEplky.js +1445 -0
  106. package/dist/structural-rollout-IXUEplky.js.map +1 -0
  107. package/dist/substrate-B0TYNrXn.js +27 -0
  108. package/dist/substrate-B0TYNrXn.js.map +1 -0
  109. package/dist/{substrate-Dyiqmd3o.d.ts → substrate-BcnuSHXm.d.ts} +57 -54
  110. package/dist/supervise-BiRutHS9.js +7321 -0
  111. package/dist/supervise-BiRutHS9.js.map +1 -0
  112. package/dist/supervisor-DTKhF-RV.js +3826 -0
  113. package/dist/supervisor-DTKhF-RV.js.map +1 -0
  114. package/dist/testing.d.ts +4 -3
  115. package/dist/testing.js +2478 -2637
  116. package/dist/testing.js.map +1 -1
  117. package/dist/tool-server-RcWgLIsL.js +141 -0
  118. package/dist/tool-server-RcWgLIsL.js.map +1 -0
  119. package/dist/trace-propagation-CJJC7SVB.js +57 -0
  120. package/dist/trace-propagation-CJJC7SVB.js.map +1 -0
  121. package/dist/types-BaIpb0XT.d.ts +492 -0
  122. package/dist/types-BevOjfTY.d.ts +765 -0
  123. package/dist/types-C9j4qg6l.d.ts +500 -0
  124. package/dist/types-Cja5OTGh.d.ts +190 -0
  125. package/dist/util-MVgdwuIS.js +145 -0
  126. package/dist/util-MVgdwuIS.js.map +1 -0
  127. package/dist/workspace-archive-BosQiotA.js +575 -0
  128. package/dist/workspace-archive-BosQiotA.js.map +1 -0
  129. package/package.json +44 -56
  130. package/skills/build-with-agent-runtime/SKILL.md +1 -0
  131. package/dist/activation-Cm-9MMX1.d.ts +0 -69
  132. package/dist/analyst-loop.js.map +0 -1
  133. package/dist/backends-BO4Jqoe2.d.ts +0 -134
  134. package/dist/candidate-execution/index.js.map +0 -1
  135. package/dist/chunk-2KGAN2HM.js +0 -1256
  136. package/dist/chunk-2KGAN2HM.js.map +0 -1
  137. package/dist/chunk-2VZYRB55.js +0 -834
  138. package/dist/chunk-2VZYRB55.js.map +0 -1
  139. package/dist/chunk-3MDZX7YU.js +0 -125
  140. package/dist/chunk-3MDZX7YU.js.map +0 -1
  141. package/dist/chunk-5IBQAPVB.js +0 -29
  142. package/dist/chunk-5IBQAPVB.js.map +0 -1
  143. package/dist/chunk-5QOB7H74.js +0 -387
  144. package/dist/chunk-5QOB7H74.js.map +0 -1
  145. package/dist/chunk-C5RTIBNZ.js +0 -48
  146. package/dist/chunk-C5RTIBNZ.js.map +0 -1
  147. package/dist/chunk-CTRA64LY.js +0 -680
  148. package/dist/chunk-CTRA64LY.js.map +0 -1
  149. package/dist/chunk-DPEUKJRO.js +0 -27
  150. package/dist/chunk-DPEUKJRO.js.map +0 -1
  151. package/dist/chunk-FD2MBMOH.js +0 -494
  152. package/dist/chunk-FD2MBMOH.js.map +0 -1
  153. package/dist/chunk-H5QPIZNX.js +0 -907
  154. package/dist/chunk-H5QPIZNX.js.map +0 -1
  155. package/dist/chunk-HLKC4UYB.js +0 -3514
  156. package/dist/chunk-HLKC4UYB.js.map +0 -1
  157. package/dist/chunk-HNP72PNU.js +0 -192
  158. package/dist/chunk-HNP72PNU.js.map +0 -1
  159. package/dist/chunk-ISPWRSEC.js +0 -408
  160. package/dist/chunk-ISPWRSEC.js.map +0 -1
  161. package/dist/chunk-KRBFHMV6.js +0 -1228
  162. package/dist/chunk-KRBFHMV6.js.map +0 -1
  163. package/dist/chunk-M22PL4GH.js +0 -171
  164. package/dist/chunk-M22PL4GH.js.map +0 -1
  165. package/dist/chunk-NURIQBJQ.js +0 -72
  166. package/dist/chunk-NURIQBJQ.js.map +0 -1
  167. package/dist/chunk-OPVWXJ2H.js +0 -75
  168. package/dist/chunk-OPVWXJ2H.js.map +0 -1
  169. package/dist/chunk-PZZKQVQV.js +0 -52
  170. package/dist/chunk-PZZKQVQV.js.map +0 -1
  171. package/dist/chunk-Q2JSAVQ3.js +0 -671
  172. package/dist/chunk-Q2JSAVQ3.js.map +0 -1
  173. package/dist/chunk-RDOAVVHY.js +0 -699
  174. package/dist/chunk-RDOAVVHY.js.map +0 -1
  175. package/dist/chunk-SGKPNBXE.js +0 -60
  176. package/dist/chunk-SGKPNBXE.js.map +0 -1
  177. package/dist/chunk-SMQXZGLZ.js +0 -351
  178. package/dist/chunk-SMQXZGLZ.js.map +0 -1
  179. package/dist/chunk-UHEZW5BU.js +0 -2918
  180. package/dist/chunk-UHEZW5BU.js.map +0 -1
  181. package/dist/chunk-UPWGXKXB.js +0 -254
  182. package/dist/chunk-UPWGXKXB.js.map +0 -1
  183. package/dist/chunk-VISA6CI3.js +0 -200
  184. package/dist/chunk-VISA6CI3.js.map +0 -1
  185. package/dist/chunk-WMTCUOQL.js +0 -474
  186. package/dist/chunk-WMTCUOQL.js.map +0 -1
  187. package/dist/chunk-WRTOVNN4.js +0 -4728
  188. package/dist/chunk-WRTOVNN4.js.map +0 -1
  189. package/dist/chunk-WSTRQZYQ.js +0 -5576
  190. package/dist/chunk-WSTRQZYQ.js.map +0 -1
  191. package/dist/chunk-XBG2W2VW.js +0 -6434
  192. package/dist/chunk-XBG2W2VW.js.map +0 -1
  193. package/dist/chunk-YEJR7IXO.js +0 -68
  194. package/dist/chunk-YEJR7IXO.js.map +0 -1
  195. package/dist/chunk-YJZA2BIK.js +0 -1338
  196. package/dist/chunk-YJZA2BIK.js.map +0 -1
  197. package/dist/completion-gate-BMy5LGoP.d.ts +0 -256
  198. package/dist/conversation.js.map +0 -1
  199. package/dist/coordination-BZZSVYpZ.d.ts +0 -2570
  200. package/dist/environment-provider.js.map +0 -1
  201. package/dist/improvement-adapter-HAZz-7vK.d.ts +0 -203
  202. package/dist/knowledge.js.map +0 -1
  203. package/dist/local-harness-BDNcl6jI.d.ts +0 -175
  204. package/dist/loop-runner-bin-68aoM9-7.d.ts +0 -199
  205. package/dist/loop-runner-bin.js.map +0 -1
  206. package/dist/loops.js.map +0 -1
  207. package/dist/openai-tools-fnj6SRVg.d.ts +0 -46
  208. package/dist/protected-model-port-B4t-OQjL.d.ts +0 -271
  209. package/dist/redact-BuFjxBUJ.d.ts +0 -481
  210. package/dist/sanitize-DEbPNtyI.d.ts +0 -328
  211. package/dist/structural-rollout-DEh6CEsa.d.ts +0 -446
  212. package/dist/supervise-4_48wwvy.d.ts +0 -379
  213. package/dist/types-BCoemcXU.d.ts +0 -802
  214. package/dist/types-BwoZWq-i.d.ts +0 -510
  215. package/dist/types-Cbp72qyu.d.ts +0 -491
  216. package/dist/types-CmYCMbFT.d.ts +0 -203
  217. package/dist/types-CvNwMzQt.d.ts +0 -957
  218. package/dist/worktree-fanout-DxhAWr5Z.d.ts +0 -999
package/dist/profiles.js CHANGED
@@ -1,586 +1,572 @@
1
- import {
2
- coderTaskToPrompt
3
- } from "./chunk-5IBQAPVB.js";
4
- import {
5
- createResearcherValidator,
6
- multiHarnessResearcherFanout,
7
- researcherProfile
8
- } from "./chunk-5QOB7H74.js";
9
- import {
10
- UI_FINDING_SEVERITIES,
11
- UI_LENSES
12
- } from "./chunk-DPEUKJRO.js";
13
-
14
- // src/audit/issue-writer.ts
15
- import { promises as fs } from "fs";
16
- import path from "path";
17
-
18
- // src/profiles/ui-auditor/slugify.ts
1
+ import { n as multiHarnessResearcherFanout, r as researcherProfile, t as createResearcherValidator } from "./researcher-CoVqNhfI.js";
2
+ import { n as UI_LENSES, t as UI_FINDING_SEVERITIES } from "./substrate-B0TYNrXn.js";
3
+ import { t as coderTaskToPrompt } from "./coder-yhVWbdWc.js";
4
+ import { promises } from "node:fs";
5
+ import path from "node:path";
6
+ //#region src/profiles/ui-auditor/slugify.ts
7
+ /**
8
+ *
9
+ * Shared slug helper used by both the in-process auditor client (for
10
+ * screenshot filenames) and the issue writer (for issue Markdown
11
+ * filenames). Lowercases, normalizes, strips non-alphanumeric, trims
12
+ * dashes, caps at 80 chars. Throws when the result is empty so callers
13
+ * never silently write to a name-less filename.
14
+ *
15
+ * @experimental
16
+ */
17
+ /** @experimental */
19
18
  function slugify(value, fieldName) {
20
- const slug = value.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
21
- if (slug.length === 0) {
22
- throw new Error(
23
- `ui-auditor: ${fieldName} slugified to empty string. Provide a non-empty value (got ${JSON.stringify(value)}).`
24
- );
25
- }
26
- return slug;
27
- }
28
-
29
- // src/audit/issue-writer.ts
30
- var SEVERITY_ORDER = {
31
- critical: 0,
32
- high: 1,
33
- med: 2,
34
- low: 3
19
+ const slug = value.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
20
+ if (slug.length === 0) throw new Error(`ui-auditor: ${fieldName} slugified to empty string. Provide a non-empty value (got ${JSON.stringify(value)}).`);
21
+ return slug;
22
+ }
23
+ //#endregion
24
+ //#region src/audit/issue-writer.ts
25
+ /**
26
+ *
27
+ * UI-audit issue writer — pure I/O. Takes a workspace dir + `UiFinding[]`
28
+ * and emits:
29
+ * - `<workspace>/issues/NNN--<lens>--<slug>.md` one self-contained
30
+ * GitHub-issue-ready Markdown per finding, with embedded screenshot
31
+ * references.
32
+ * - `<workspace>/registry.json` — finding index for dedup and audit
33
+ * resume across iterations.
34
+ * - `<workspace>/index.md` — human-readable rollup (severity / lens /
35
+ * route counts plus a sorted finding list).
36
+ *
37
+ * The writer is deterministic, idempotent for `appendFindings()`, and
38
+ * never invokes an LLM. It assigns the next monotonic id to a finding the
39
+ * caller did not pre-id.
40
+ *
41
+ * @experimental
42
+ */
43
+ const SEVERITY_ORDER = {
44
+ critical: 0,
45
+ high: 1,
46
+ med: 2,
47
+ low: 3
35
48
  };
36
49
  function assertValidWorkspaceDir(dir) {
37
- if (typeof dir !== "string" || dir.length === 0) {
38
- throw new Error(
39
- `audit-writer: workspaceDir must be a non-empty string (got ${JSON.stringify(dir)})`
40
- );
41
- }
42
- if (!path.isAbsolute(dir)) {
43
- throw new Error(
44
- `audit-writer: workspaceDir must be an absolute path (got ${JSON.stringify(dir)})`
45
- );
46
- }
47
- if (dir.split(path.sep).includes("..")) {
48
- throw new Error(
49
- `audit-writer: workspaceDir must not contain '..' segments (got ${JSON.stringify(dir)})`
50
- );
51
- }
50
+ if (typeof dir !== "string" || dir.length === 0) throw new Error(`audit-writer: workspaceDir must be a non-empty string (got ${JSON.stringify(dir)})`);
51
+ if (!path.isAbsolute(dir)) throw new Error(`audit-writer: workspaceDir must be an absolute path (got ${JSON.stringify(dir)})`);
52
+ if (dir.split(path.sep).includes("..")) throw new Error(`audit-writer: workspaceDir must not contain '..' segments (got ${JSON.stringify(dir)})`);
52
53
  }
54
+ /** Create the `issues/`, `screenshots/`, and `registry.json` scaffold in a new audit workspace. @experimental */
53
55
  async function initAuditWorkspace(workspaceDir) {
54
- assertValidWorkspaceDir(workspaceDir);
55
- await fs.mkdir(path.join(workspaceDir, "issues"), { recursive: true });
56
- await fs.mkdir(path.join(workspaceDir, "screenshots"), { recursive: true });
57
- const regPath = path.join(workspaceDir, "registry.json");
58
- try {
59
- await fs.access(regPath);
60
- } catch {
61
- const fresh = { schemaVersion: 1, findings: [], routes: {} };
62
- await fs.writeFile(regPath, JSON.stringify(fresh, null, 2));
63
- }
64
- }
56
+ assertValidWorkspaceDir(workspaceDir);
57
+ await promises.mkdir(path.join(workspaceDir, "issues"), { recursive: true });
58
+ await promises.mkdir(path.join(workspaceDir, "screenshots"), { recursive: true });
59
+ const regPath = path.join(workspaceDir, "registry.json");
60
+ try {
61
+ await promises.access(regPath);
62
+ } catch {
63
+ await promises.writeFile(regPath, JSON.stringify({
64
+ schemaVersion: 1,
65
+ findings: [],
66
+ routes: {}
67
+ }, null, 2));
68
+ }
69
+ }
70
+ /** Read and validate the `registry.json` from an audit workspace. @experimental */
65
71
  async function readAuditRegistry(workspaceDir) {
66
- assertValidWorkspaceDir(workspaceDir);
67
- const regPath = path.join(workspaceDir, "registry.json");
68
- const text = await fs.readFile(regPath, "utf8");
69
- const parsed = JSON.parse(text);
70
- if (!parsed || typeof parsed !== "object") {
71
- throw new Error(`audit-writer: registry.json at ${regPath} is not a JSON object`);
72
- }
73
- const reg = parsed;
74
- if (reg.schemaVersion !== 1 || !Array.isArray(reg.findings) || typeof reg.routes !== "object") {
75
- throw new Error(`audit-writer: registry.json at ${regPath} has an unrecognized shape`);
76
- }
77
- return reg;
72
+ assertValidWorkspaceDir(workspaceDir);
73
+ const regPath = path.join(workspaceDir, "registry.json");
74
+ const text = await promises.readFile(regPath, "utf8");
75
+ const parsed = JSON.parse(text);
76
+ if (!parsed || typeof parsed !== "object") throw new Error(`audit-writer: registry.json at ${regPath} is not a JSON object`);
77
+ const reg = parsed;
78
+ if (reg.schemaVersion !== 1 || !Array.isArray(reg.findings) || typeof reg.routes !== "object") throw new Error(`audit-writer: registry.json at ${regPath} has an unrecognized shape`);
79
+ return reg;
78
80
  }
79
81
  async function writeAuditRegistry(workspaceDir, reg) {
80
- const regPath = path.join(workspaceDir, "registry.json");
81
- const tmpPath = `${regPath}.tmp-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
82
- await fs.writeFile(tmpPath, JSON.stringify(reg, null, 2));
83
- await fs.rename(tmpPath, regPath);
82
+ const regPath = path.join(workspaceDir, "registry.json");
83
+ const tmpPath = `${regPath}.tmp-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
84
+ await promises.writeFile(tmpPath, JSON.stringify(reg, null, 2));
85
+ await promises.rename(tmpPath, regPath);
84
86
  }
85
- var workspaceLocks = /* @__PURE__ */ new Map();
87
+ const workspaceLocks = /* @__PURE__ */ new Map();
86
88
  async function withWorkspaceLock(workspaceDir, fn) {
87
- const key = path.resolve(workspaceDir);
88
- const prev = workspaceLocks.get(key) ?? Promise.resolve();
89
- let signalDone;
90
- const done = new Promise((resolve) => {
91
- signalDone = resolve;
92
- });
93
- workspaceLocks.set(key, done);
94
- try {
95
- await prev;
96
- return await fn();
97
- } finally {
98
- signalDone();
99
- if (workspaceLocks.get(key) === done) workspaceLocks.delete(key);
100
- }
89
+ const key = path.resolve(workspaceDir);
90
+ const prev = workspaceLocks.get(key) ?? Promise.resolve();
91
+ let signalDone;
92
+ const done = new Promise((resolve) => {
93
+ signalDone = resolve;
94
+ });
95
+ workspaceLocks.set(key, done);
96
+ try {
97
+ await prev;
98
+ return await fn();
99
+ } finally {
100
+ signalDone();
101
+ if (workspaceLocks.get(key) === done) workspaceLocks.delete(key);
102
+ }
101
103
  }
102
104
  function assertFindingShape(f, index) {
103
- const where = `audit-writer: findings[${index}]`;
104
- if (!UI_LENSES.includes(f.lens)) {
105
- throw new Error(
106
- `${where}: invalid lens ${JSON.stringify(f.lens)}; one of ${UI_LENSES.join("|")}`
107
- );
108
- }
109
- if (!UI_FINDING_SEVERITIES.includes(f.severity)) {
110
- throw new Error(
111
- `${where}: invalid severity ${JSON.stringify(f.severity)}; one of ${UI_FINDING_SEVERITIES.join("|")}`
112
- );
113
- }
114
- for (const field of ["title", "route", "observation", "impact", "suggestedFix"]) {
115
- const v = f[field];
116
- if (typeof v !== "string" || v.trim().length === 0) {
117
- throw new Error(`${where}: ${field} must be a non-empty string`);
118
- }
119
- }
120
- if (!Array.isArray(f.screenshots) || f.screenshots.length === 0) {
121
- throw new Error(`${where}: screenshots must be a non-empty array`);
122
- }
123
- for (let i = 0; i < f.screenshots.length; i += 1) {
124
- const s = f.screenshots[i];
125
- if (!s || typeof s.path !== "string" || s.path.length === 0) {
126
- throw new Error(`${where}.screenshots[${i}].path must be a non-empty string`);
127
- }
128
- if (s.path.split(/[/\\]/).includes("..")) {
129
- throw new Error(`${where}.screenshots[${i}].path must not contain '..' segments`);
130
- }
131
- }
105
+ const where = `audit-writer: findings[${index}]`;
106
+ if (!UI_LENSES.includes(f.lens)) throw new Error(`${where}: invalid lens ${JSON.stringify(f.lens)}; one of ${UI_LENSES.join("|")}`);
107
+ if (!UI_FINDING_SEVERITIES.includes(f.severity)) throw new Error(`${where}: invalid severity ${JSON.stringify(f.severity)}; one of ${UI_FINDING_SEVERITIES.join("|")}`);
108
+ for (const field of [
109
+ "title",
110
+ "route",
111
+ "observation",
112
+ "impact",
113
+ "suggestedFix"
114
+ ]) {
115
+ const v = f[field];
116
+ if (typeof v !== "string" || v.trim().length === 0) throw new Error(`${where}: ${field} must be a non-empty string`);
117
+ }
118
+ if (!Array.isArray(f.screenshots) || f.screenshots.length === 0) throw new Error(`${where}: screenshots must be a non-empty array`);
119
+ for (let i = 0; i < f.screenshots.length; i += 1) {
120
+ const s = f.screenshots[i];
121
+ if (!s || typeof s.path !== "string" || s.path.length === 0) throw new Error(`${where}.screenshots[${i}].path must be a non-empty string`);
122
+ if (s.path.split(/[/\\]/).includes("..")) throw new Error(`${where}.screenshots[${i}].path must not contain '..' segments`);
123
+ }
132
124
  }
133
125
  function nextFindingId(reg) {
134
- let max = 0;
135
- for (const f of reg.findings) {
136
- if (typeof f.id === "number" && f.id > max) max = f.id;
137
- }
138
- return max + 1;
126
+ let max = 0;
127
+ for (const f of reg.findings) if (typeof f.id === "number" && f.id > max) max = f.id;
128
+ return max + 1;
139
129
  }
140
130
  function slugifyTitle(title) {
141
- return slugify(title, "title");
131
+ return slugify(title, "title");
142
132
  }
143
133
  function renderFinding(finding) {
144
- if (!finding.id) {
145
- throw new Error("audit-writer: cannot render a finding without an assigned id");
146
- }
147
- if (finding.screenshots.length === 0) {
148
- throw new Error(`audit-writer: finding #${finding.id} has no screenshots`);
149
- }
150
- const id = finding.id;
151
- const numStr = String(id).padStart(3, "0");
152
- const metaLines = [
153
- `> **Issue #${numStr}** \xB7 **Severity:** ${finding.severity} \xB7 **Lens:** ${finding.lens}`,
154
- `> **Route:** \`${finding.route}\`${finding.url ? ` \xB7 **URL:** ${finding.url}` : ""}`
155
- ];
156
- if (finding.viewport) metaLines.push(`> **Viewport:** ${finding.viewport}`);
157
- if (finding.selector) metaLines.push(`> **Selector:** \`${finding.selector}\``);
158
- if (finding.tags && finding.tags.length > 0) {
159
- metaLines.push(`> **Tags:** ${finding.tags.map((t) => `\`${t}\``).join(", ")}`);
160
- }
161
- if (finding.similarTo && finding.similarTo.length > 0) {
162
- metaLines.push(
163
- `> **Possible duplicates:** ${finding.similarTo.map((n) => `#${String(n).padStart(3, "0")}`).join(", ")}`
164
- );
165
- }
166
- metaLines.push(`> _Generated: ${finding.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()}_`);
167
- const reproSteps = finding.reproSteps ?? `1. Open \`${finding.route}\`${finding.url ? ` (${finding.url})` : ""}
168
- 2. Observe the area shown in the screenshot${finding.selector ? ` (selector: \`${finding.selector}\`)` : ""}.`;
169
- const evidence = finding.screenshots.map((s) => `![${path.basename(s.path)}](../${s.path})`).join("\n\n");
170
- return [
171
- `# [UI] ${finding.title}`,
172
- "",
173
- metaLines.join("\n"),
174
- "",
175
- "## Observation",
176
- finding.observation.trim(),
177
- "",
178
- "## Why it matters",
179
- finding.impact.trim(),
180
- "",
181
- "## Steps to reproduce",
182
- reproSteps,
183
- "",
184
- "## Suggested fix",
185
- finding.suggestedFix.trim(),
186
- "",
187
- "## Evidence",
188
- evidence,
189
- "",
190
- "---",
191
- `<sub>Generated by agent-runtime/ui-auditor \xB7 lens=\`${finding.lens}\` \xB7 severity=\`${finding.severity}\`</sub>`,
192
- ""
193
- ].join("\n");
194
- }
134
+ if (!finding.id) throw new Error("audit-writer: cannot render a finding without an assigned id");
135
+ if (finding.screenshots.length === 0) throw new Error(`audit-writer: finding #${finding.id} has no screenshots`);
136
+ const id = finding.id;
137
+ const metaLines = [`> **Issue #${String(id).padStart(3, "0")}** · **Severity:** ${finding.severity} · **Lens:** ${finding.lens}`, `> **Route:** \`${finding.route}\`${finding.url ? ` · **URL:** ${finding.url}` : ""}`];
138
+ if (finding.viewport) metaLines.push(`> **Viewport:** ${finding.viewport}`);
139
+ if (finding.selector) metaLines.push(`> **Selector:** \`${finding.selector}\``);
140
+ if (finding.tags && finding.tags.length > 0) metaLines.push(`> **Tags:** ${finding.tags.map((t) => `\`${t}\``).join(", ")}`);
141
+ if (finding.similarTo && finding.similarTo.length > 0) metaLines.push(`> **Possible duplicates:** ${finding.similarTo.map((n) => `#${String(n).padStart(3, "0")}`).join(", ")}`);
142
+ metaLines.push(`> _Generated: ${finding.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()}_`);
143
+ const reproSteps = finding.reproSteps ?? `1. Open \`${finding.route}\`${finding.url ? ` (${finding.url})` : ""}\n2. Observe the area shown in the screenshot${finding.selector ? ` (selector: \`${finding.selector}\`)` : ""}.`;
144
+ const evidence = finding.screenshots.map((s) => `![${path.basename(s.path)}](../${s.path})`).join("\n\n");
145
+ return [
146
+ `# [UI] ${finding.title}`,
147
+ "",
148
+ metaLines.join("\n"),
149
+ "",
150
+ "## Observation",
151
+ finding.observation.trim(),
152
+ "",
153
+ "## Why it matters",
154
+ finding.impact.trim(),
155
+ "",
156
+ "## Steps to reproduce",
157
+ reproSteps,
158
+ "",
159
+ "## Suggested fix",
160
+ finding.suggestedFix.trim(),
161
+ "",
162
+ "## Evidence",
163
+ evidence,
164
+ "",
165
+ "---",
166
+ `<sub>Generated by agent-runtime/ui-auditor · lens=\`${finding.lens}\` · severity=\`${finding.severity}\`</sub>`,
167
+ ""
168
+ ].join("\n");
169
+ }
170
+ /**
171
+ * Append findings to a workspace, writing one Markdown file per finding
172
+ * and updating registry.json. Assigns monotonically increasing ids to
173
+ * findings that arrived without one.
174
+ *
175
+ * Findings already carrying an id that collides with the registry are
176
+ * rejected — callers must either freshly mint findings (id undefined) or
177
+ * use a separate update path. This protects against accidental overwrite.
178
+ *
179
+ * @experimental
180
+ */
195
181
  async function appendFindings(workspaceDir, findings) {
196
- assertValidWorkspaceDir(workspaceDir);
197
- for (let i = 0; i < findings.length; i += 1) {
198
- const f = findings[i];
199
- if (!f) throw new Error(`audit-writer: findings[${i}] is undefined`);
200
- assertFindingShape(f, i);
201
- }
202
- return withWorkspaceLock(workspaceDir, async () => {
203
- await initAuditWorkspace(workspaceDir);
204
- const reg = await readAuditRegistry(workspaceDir);
205
- const usedIds = /* @__PURE__ */ new Set();
206
- for (const f of reg.findings) {
207
- if (typeof f.id === "number") usedIds.add(f.id);
208
- }
209
- const written = [];
210
- const files = [];
211
- let nextId = nextFindingId(reg);
212
- for (const incoming of findings) {
213
- let id = incoming.id;
214
- if (id !== void 0) {
215
- if (usedIds.has(id)) {
216
- throw new Error(
217
- `audit-writer: incoming finding id ${id} (title=${JSON.stringify(incoming.title)}) collides with an existing registry entry`
218
- );
219
- }
220
- } else {
221
- id = nextId;
222
- while (usedIds.has(id)) id += 1;
223
- nextId = id + 1;
224
- }
225
- usedIds.add(id);
226
- const createdAt = incoming.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
227
- const persisted = { ...incoming, id, createdAt };
228
- const slug = slugifyTitle(persisted.title);
229
- const fileName = `${String(id).padStart(3, "0")}--${persisted.lens}--${slug}.md`;
230
- const filePathAbs = path.join(workspaceDir, "issues", fileName);
231
- const filePathRel = `issues/${fileName}`;
232
- await fs.writeFile(filePathAbs, renderFinding(persisted));
233
- reg.findings.push(persisted);
234
- written.push(persisted);
235
- files.push(filePathRel);
236
- }
237
- await writeAuditRegistry(workspaceDir, reg);
238
- return { written, files };
239
- });
240
- }
182
+ assertValidWorkspaceDir(workspaceDir);
183
+ for (let i = 0; i < findings.length; i += 1) {
184
+ const f = findings[i];
185
+ if (!f) throw new Error(`audit-writer: findings[${i}] is undefined`);
186
+ assertFindingShape(f, i);
187
+ }
188
+ return withWorkspaceLock(workspaceDir, async () => {
189
+ await initAuditWorkspace(workspaceDir);
190
+ const reg = await readAuditRegistry(workspaceDir);
191
+ const usedIds = /* @__PURE__ */ new Set();
192
+ for (const f of reg.findings) if (typeof f.id === "number") usedIds.add(f.id);
193
+ const written = [];
194
+ const files = [];
195
+ let nextId = nextFindingId(reg);
196
+ for (const incoming of findings) {
197
+ let id = incoming.id;
198
+ if (id !== void 0) {
199
+ if (usedIds.has(id)) throw new Error(`audit-writer: incoming finding id ${id} (title=${JSON.stringify(incoming.title)}) collides with an existing registry entry`);
200
+ } else {
201
+ id = nextId;
202
+ while (usedIds.has(id)) id += 1;
203
+ nextId = id + 1;
204
+ }
205
+ usedIds.add(id);
206
+ const createdAt = incoming.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
207
+ const persisted = {
208
+ ...incoming,
209
+ id,
210
+ createdAt
211
+ };
212
+ const slug = slugifyTitle(persisted.title);
213
+ const fileName = `${String(id).padStart(3, "0")}--${persisted.lens}--${slug}.md`;
214
+ const filePathAbs = path.join(workspaceDir, "issues", fileName);
215
+ const filePathRel = `issues/${fileName}`;
216
+ await promises.writeFile(filePathAbs, renderFinding(persisted));
217
+ reg.findings.push(persisted);
218
+ written.push(persisted);
219
+ files.push(filePathRel);
220
+ }
221
+ await writeAuditRegistry(workspaceDir, reg);
222
+ return {
223
+ written,
224
+ files
225
+ };
226
+ });
227
+ }
228
+ /**
229
+ * Record screenshots taken for a route in the registry, without filing a
230
+ * finding. Useful when the auditor wants to remember which captures
231
+ * exist for resume / dedup purposes.
232
+ *
233
+ * @experimental
234
+ */
241
235
  async function registerCaptures(workspaceDir, options) {
242
- assertValidWorkspaceDir(workspaceDir);
243
- if (typeof options.route !== "string" || options.route.trim().length === 0) {
244
- throw new Error("audit-writer: registerCaptures: route must be a non-empty string");
245
- }
246
- return withWorkspaceLock(workspaceDir, async () => {
247
- await initAuditWorkspace(workspaceDir);
248
- const reg = await readAuditRegistry(workspaceDir);
249
- const slot = reg.routes[options.route] ?? { captures: [] };
250
- if (options.url) slot.url = options.url;
251
- slot.captures = [...slot.captures, ...options.captures];
252
- reg.routes[options.route] = slot;
253
- await writeAuditRegistry(workspaceDir, reg);
254
- });
255
- }
236
+ assertValidWorkspaceDir(workspaceDir);
237
+ if (typeof options.route !== "string" || options.route.trim().length === 0) throw new Error("audit-writer: registerCaptures: route must be a non-empty string");
238
+ return withWorkspaceLock(workspaceDir, async () => {
239
+ await initAuditWorkspace(workspaceDir);
240
+ const reg = await readAuditRegistry(workspaceDir);
241
+ const slot = reg.routes[options.route] ?? { captures: [] };
242
+ if (options.url) slot.url = options.url;
243
+ slot.captures = [...slot.captures, ...options.captures];
244
+ reg.routes[options.route] = slot;
245
+ await writeAuditRegistry(workspaceDir, reg);
246
+ });
247
+ }
248
+ /** Compute finding counts by severity, lens, and route from an `AuditRegistry`. @experimental */
256
249
  function summarizeRegistry(reg) {
257
- const bySeverity = {
258
- critical: 0,
259
- high: 0,
260
- med: 0,
261
- low: 0
262
- };
263
- const byLens = {};
264
- const byRoute = {};
265
- for (const f of reg.findings) {
266
- bySeverity[f.severity] += 1;
267
- byLens[f.lens] = (byLens[f.lens] ?? 0) + 1;
268
- byRoute[f.route] = (byRoute[f.route] ?? 0) + 1;
269
- }
270
- return { total: reg.findings.length, bySeverity, byLens, byRoute };
271
- }
250
+ const bySeverity = {
251
+ critical: 0,
252
+ high: 0,
253
+ med: 0,
254
+ low: 0
255
+ };
256
+ const byLens = {};
257
+ const byRoute = {};
258
+ for (const f of reg.findings) {
259
+ bySeverity[f.severity] += 1;
260
+ byLens[f.lens] = (byLens[f.lens] ?? 0) + 1;
261
+ byRoute[f.route] = (byRoute[f.route] ?? 0) + 1;
262
+ }
263
+ return {
264
+ total: reg.findings.length,
265
+ bySeverity,
266
+ byLens,
267
+ byRoute
268
+ };
269
+ }
270
+ /**
271
+ * Regenerate `<workspace>/index.md` from registry.json.
272
+ *
273
+ * @experimental
274
+ */
272
275
  async function writeAuditIndex(workspaceDir) {
273
- assertValidWorkspaceDir(workspaceDir);
274
- const reg = await readAuditRegistry(workspaceDir);
275
- const summary = summarizeRegistry(reg);
276
- const sorted = [...reg.findings].sort((a, b) => {
277
- const sa = SEVERITY_ORDER[a.severity];
278
- const sb = SEVERITY_ORDER[b.severity];
279
- if (sa !== sb) return sa - sb;
280
- return (a.id ?? 0) - (b.id ?? 0);
281
- });
282
- const lines = [];
283
- lines.push("# UI audit index", "");
284
- lines.push(`_Generated: ${(/* @__PURE__ */ new Date()).toISOString()}_`, "");
285
- lines.push(`**Total findings:** ${summary.total}`, "");
286
- lines.push("## By severity");
287
- for (const s of ["critical", "high", "med", "low"]) {
288
- const n = summary.bySeverity[s] ?? 0;
289
- if (n > 0) lines.push(`- **${s}** \u2014 ${n}`);
290
- }
291
- lines.push("", "## By lens");
292
- const lensEntries = Object.entries(summary.byLens).map(([k, v]) => [
293
- k,
294
- v ?? 0
295
- ]);
296
- for (const [lens, n] of lensEntries.sort((a, b) => b[1] - a[1])) {
297
- lines.push(`- **${lens}** \u2014 ${n}`);
298
- }
299
- lines.push("", "## By route");
300
- const routeEntries = Object.entries(summary.byRoute).map(([k, v]) => [
301
- k,
302
- v ?? 0
303
- ]);
304
- for (const [route, n] of routeEntries.sort((a, b) => b[1] - a[1])) {
305
- lines.push(`- \`${route}\` \u2014 ${n}`);
306
- }
307
- lines.push("", "## Findings", "");
308
- for (const f of sorted) {
309
- if (typeof f.id !== "number") continue;
310
- const num = String(f.id).padStart(3, "0");
311
- const slug = slugifyTitle(f.title);
312
- const file = `issues/${num}--${f.lens}--${slug}.md`;
313
- lines.push(`- [#${num} \`${f.severity}\` \`${f.lens}\` \`${f.route}\` \u2014 ${f.title}](${file})`);
314
- }
315
- lines.push("");
316
- const out = path.join(workspaceDir, "index.md");
317
- const body = lines.join("\n");
318
- await fs.writeFile(out, body);
319
- return out;
320
- }
321
-
322
- // src/profiles/ui-auditor/prompt.ts
323
- var ENVELOPE_BEGIN = "<<UI_AUDIT_TASK>>";
324
- var ENVELOPE_END = "<<UI_AUDIT_TASK_END>>";
276
+ assertValidWorkspaceDir(workspaceDir);
277
+ const reg = await readAuditRegistry(workspaceDir);
278
+ const summary = summarizeRegistry(reg);
279
+ const sorted = [...reg.findings].sort((a, b) => {
280
+ const sa = SEVERITY_ORDER[a.severity];
281
+ const sb = SEVERITY_ORDER[b.severity];
282
+ if (sa !== sb) return sa - sb;
283
+ return (a.id ?? 0) - (b.id ?? 0);
284
+ });
285
+ const lines = [];
286
+ lines.push("# UI audit index", "");
287
+ lines.push(`_Generated: ${(/* @__PURE__ */ new Date()).toISOString()}_`, "");
288
+ lines.push(`**Total findings:** ${summary.total}`, "");
289
+ lines.push("## By severity");
290
+ for (const s of [
291
+ "critical",
292
+ "high",
293
+ "med",
294
+ "low"
295
+ ]) {
296
+ const n = summary.bySeverity[s] ?? 0;
297
+ if (n > 0) lines.push(`- **${s}** — ${n}`);
298
+ }
299
+ lines.push("", "## By lens");
300
+ const lensEntries = Object.entries(summary.byLens).map(([k, v]) => [k, v ?? 0]);
301
+ for (const [lens, n] of lensEntries.sort((a, b) => b[1] - a[1])) lines.push(`- **${lens}** — ${n}`);
302
+ lines.push("", "## By route");
303
+ const routeEntries = Object.entries(summary.byRoute).map(([k, v]) => [k, v ?? 0]);
304
+ for (const [route, n] of routeEntries.sort((a, b) => b[1] - a[1])) lines.push(`- \`${route}\` — ${n}`);
305
+ lines.push("", "## Findings", "");
306
+ for (const f of sorted) {
307
+ if (typeof f.id !== "number") continue;
308
+ const num = String(f.id).padStart(3, "0");
309
+ const slug = slugifyTitle(f.title);
310
+ const file = `issues/${num}--${f.lens}--${slug}.md`;
311
+ lines.push(`- [#${num} \`${f.severity}\` \`${f.lens}\` \`${f.route}\` — ${f.title}](${file})`);
312
+ }
313
+ lines.push("");
314
+ const out = path.join(workspaceDir, "index.md");
315
+ const body = lines.join("\n");
316
+ await promises.writeFile(out, body);
317
+ return out;
318
+ }
319
+ //#endregion
320
+ //#region src/profiles/ui-auditor/prompt.ts
321
+ const ENVELOPE_BEGIN = "<<UI_AUDIT_TASK>>";
322
+ const ENVELOPE_END = "<<UI_AUDIT_TASK_END>>";
323
+ /** Wrap a `UiAuditTask` in a machine-readable envelope so iterations are self-describing. @experimental */
325
324
  function encodeAuditTaskEnvelope(task) {
326
- return `${ENVELOPE_BEGIN}${JSON.stringify(task)}${ENVELOPE_END}`;
327
- }
325
+ return `${ENVELOPE_BEGIN}${JSON.stringify(task)}${ENVELOPE_END}`;
326
+ }
327
+ /**
328
+ * Parse a task envelope back out of a prompt string. Returns undefined if
329
+ * the prompt does not contain a complete envelope OR if the payload is
330
+ * not valid JSON.
331
+ *
332
+ * @experimental
333
+ */
328
334
  function decodeAuditTaskEnvelope(prompt) {
329
- const start = prompt.indexOf(ENVELOPE_BEGIN);
330
- if (start === -1) return void 0;
331
- const payloadStart = start + ENVELOPE_BEGIN.length;
332
- const end = prompt.indexOf(ENVELOPE_END, payloadStart);
333
- if (end === -1) return void 0;
334
- const payload = prompt.slice(payloadStart, end);
335
- try {
336
- const parsed = JSON.parse(payload);
337
- if (!parsed || typeof parsed !== "object") return void 0;
338
- const t = parsed;
339
- if (typeof t.lens !== "string" || !Array.isArray(t.captures)) return void 0;
340
- return t;
341
- } catch {
342
- return void 0;
343
- }
344
- }
335
+ const start = prompt.indexOf(ENVELOPE_BEGIN);
336
+ if (start === -1) return void 0;
337
+ const payloadStart = start + 17;
338
+ const end = prompt.indexOf(ENVELOPE_END, payloadStart);
339
+ if (end === -1) return void 0;
340
+ const payload = prompt.slice(payloadStart, end);
341
+ try {
342
+ const parsed = JSON.parse(payload);
343
+ if (!parsed || typeof parsed !== "object") return void 0;
344
+ const t = parsed;
345
+ if (typeof t.lens !== "string" || !Array.isArray(t.captures)) return void 0;
346
+ return t;
347
+ } catch {
348
+ return;
349
+ }
350
+ }
351
+ /** Produce the user message for one audit iteration: lens, captures to take, and the task envelope. @experimental */
345
352
  function formatAuditorPrompt(task) {
346
- const lines = [];
347
- lines.push(`# UI audit iteration \u2014 lens: ${task.lens}`);
348
- lines.push("");
349
- if (task.productContext && task.productContext.trim().length > 0) {
350
- lines.push("## Product context");
351
- lines.push(task.productContext.trim());
352
- lines.push("");
353
- }
354
- lines.push("## Captures to take");
355
- task.captures.forEach((cap, i) => {
356
- const vp = cap.viewport ? `${cap.viewport.width}x${cap.viewport.height}` : "1280x800 (default)";
357
- const detail = [
358
- `viewport=${vp}`,
359
- cap.fullPage ? "fullPage=true" : null,
360
- cap.elementSelector ? `selector=\`${cap.elementSelector}\`` : null,
361
- cap.waitFor ? `waitFor=\`${cap.waitFor}\`` : null,
362
- cap.waitMs !== void 0 ? `waitMs=${cap.waitMs}` : null,
363
- cap.label ? `label=${cap.label}` : null
364
- ].filter((s) => s !== null).join(" \xB7 ");
365
- lines.push(`${i + 1}. route=\`${cap.route}\` url=${cap.url} ${detail ? `(${detail})` : ""}`);
366
- });
367
- lines.push("");
368
- if (task.knownFindingIds && task.knownFindingIds.length > 0) {
369
- lines.push("## Known findings (link via similarTo, do not refile)");
370
- lines.push(task.knownFindingIds.map((n) => `#${String(n).padStart(3, "0")}`).join(", "));
371
- lines.push("");
372
- }
373
- lines.push("## Output format");
374
- lines.push(
375
- "Emit a single JSON object with the shape `{ findings: UiFinding[], notes?: string }` where every finding has the fields enumerated in your system prompt. The screenshots field on each finding must reference the captures above by path. Do not emit findings outside the lens."
376
- );
377
- return lines.join("\n");
378
- }
379
-
380
- // src/profiles/ui-auditor/in-process-client.ts
353
+ const lines = [];
354
+ lines.push(`# UI audit iteration lens: ${task.lens}`);
355
+ lines.push("");
356
+ if (task.productContext && task.productContext.trim().length > 0) {
357
+ lines.push("## Product context");
358
+ lines.push(task.productContext.trim());
359
+ lines.push("");
360
+ }
361
+ lines.push("## Captures to take");
362
+ task.captures.forEach((cap, i) => {
363
+ const detail = [
364
+ `viewport=${cap.viewport ? `${cap.viewport.width}x${cap.viewport.height}` : "1280x800 (default)"}`,
365
+ cap.fullPage ? "fullPage=true" : null,
366
+ cap.elementSelector ? `selector=\`${cap.elementSelector}\`` : null,
367
+ cap.waitFor ? `waitFor=\`${cap.waitFor}\`` : null,
368
+ cap.waitMs !== void 0 ? `waitMs=${cap.waitMs}` : null,
369
+ cap.label ? `label=${cap.label}` : null
370
+ ].filter((s) => s !== null).join(" · ");
371
+ lines.push(`${i + 1}. route=\`${cap.route}\` url=${cap.url} ${detail ? `(${detail})` : ""}`);
372
+ });
373
+ lines.push("");
374
+ if (task.knownFindingIds && task.knownFindingIds.length > 0) {
375
+ lines.push("## Known findings (link via similarTo, do not refile)");
376
+ lines.push(task.knownFindingIds.map((n) => `#${String(n).padStart(3, "0")}`).join(", "));
377
+ lines.push("");
378
+ }
379
+ lines.push("## Output format");
380
+ lines.push("Emit a single JSON object with the shape `{ findings: UiFinding[], notes?: string }` where every finding has the fields enumerated in your system prompt. The screenshots field on each finding must reference the captures above by path. Do not emit findings outside the lens.");
381
+ return lines.join("\n");
382
+ }
383
+ //#endregion
384
+ //#region src/profiles/ui-auditor/in-process-client.ts
381
385
  function asSandboxEvent(type, data) {
382
- return { type, data };
383
- }
384
- var DEFAULT_VIEWPORT = { width: 1280, height: 800 };
385
- var NAV_TIMEOUT_MS = 3e4;
386
+ return {
387
+ type,
388
+ data
389
+ };
390
+ }
391
+ const DEFAULT_VIEWPORT = {
392
+ width: 1280,
393
+ height: 800
394
+ };
395
+ const NAV_TIMEOUT_MS = 3e4;
386
396
  async function defaultLaunch() {
387
- const mod = await import("playwright");
388
- if (!mod?.chromium || typeof mod.chromium.launch !== "function") {
389
- throw new Error(
390
- "ui-auditor: playwright is not installed. Install `playwright` (and run `playwright install chromium`) or pass a custom `launchBrowser` to createInProcessUiAuditClient."
391
- );
392
- }
393
- return mod.chromium.launch({ headless: true });
397
+ const mod = await import("playwright");
398
+ if (!mod?.chromium || typeof mod.chromium.launch !== "function") throw new Error("ui-auditor: playwright is not installed. Install `playwright` (and run `playwright install chromium`) or pass a custom `launchBrowser` to createInProcessUiAuditClient.");
399
+ return mod.chromium.launch({ headless: true });
394
400
  }
395
401
  function nowStamp() {
396
- const d = /* @__PURE__ */ new Date();
397
- const pad = (n) => String(n).padStart(2, "0");
398
- return `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}-${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}-${String(d.getUTCMilliseconds()).padStart(3, "0")}`;
402
+ const d = /* @__PURE__ */ new Date();
403
+ const pad = (n) => String(n).padStart(2, "0");
404
+ return `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}-${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}-${String(d.getUTCMilliseconds()).padStart(3, "0")}`;
399
405
  }
400
406
  function viewportOf(req) {
401
- return req.viewport ?? DEFAULT_VIEWPORT;
407
+ return req.viewport ?? DEFAULT_VIEWPORT;
402
408
  }
403
409
  function captureFilename(req) {
404
- const vp = viewportOf(req);
405
- const labelPart = req.label ? `--${slugify(req.label, "label")}` : "";
406
- return `${slugify(req.route, "route")}--${vp.width}x${vp.height}${labelPart}--${nowStamp()}.png`;
410
+ const vp = viewportOf(req);
411
+ const labelPart = req.label ? `--${slugify(req.label, "label")}` : "";
412
+ return `${slugify(req.route, "route")}--${vp.width}x${vp.height}${labelPart}--${nowStamp()}.png`;
407
413
  }
408
414
  function assertHttpUrl(url) {
409
- let parsed;
410
- try {
411
- parsed = new URL(url);
412
- } catch {
413
- throw new Error(`ui-auditor: capture url is not parseable (got ${JSON.stringify(url)})`);
414
- }
415
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
416
- throw new Error(
417
- `ui-auditor: capture url must use http or https (got ${parsed.protocol} in ${JSON.stringify(url)})`
418
- );
419
- }
415
+ let parsed;
416
+ try {
417
+ parsed = new URL(url);
418
+ } catch {
419
+ throw new Error(`ui-auditor: capture url is not parseable (got ${JSON.stringify(url)})`);
420
+ }
421
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error(`ui-auditor: capture url must use http or https (got ${parsed.protocol} in ${JSON.stringify(url)})`);
420
422
  }
421
423
  async function captureOne(page, req, outAbsPath, signal, navPolicy) {
422
- signal.throwIfAborted();
423
- assertHttpUrl(req.url);
424
- await page.setViewportSize(viewportOf(req));
425
- const waitUntil = navPolicy === "spa" ? "domcontentloaded" : "networkidle";
426
- await page.goto(req.url, { waitUntil, timeout: NAV_TIMEOUT_MS });
427
- if (req.waitFor) {
428
- await page.waitForSelector(req.waitFor, { timeout: 15e3 });
429
- }
430
- const extra = req.waitMs ?? 500;
431
- if (extra > 0) await page.waitForTimeout(extra);
432
- signal.throwIfAborted();
433
- if (req.elementSelector) {
434
- await page.locator(req.elementSelector).first().screenshot({ path: outAbsPath });
435
- } else {
436
- await page.screenshot({ path: outAbsPath, fullPage: req.fullPage === true });
437
- }
424
+ signal.throwIfAborted();
425
+ assertHttpUrl(req.url);
426
+ await page.setViewportSize(viewportOf(req));
427
+ const waitUntil = navPolicy === "spa" ? "domcontentloaded" : "networkidle";
428
+ await page.goto(req.url, {
429
+ waitUntil,
430
+ timeout: NAV_TIMEOUT_MS
431
+ });
432
+ if (req.waitFor) await page.waitForSelector(req.waitFor, { timeout: 15e3 });
433
+ const extra = req.waitMs ?? 500;
434
+ if (extra > 0) await page.waitForTimeout(extra);
435
+ signal.throwIfAborted();
436
+ if (req.elementSelector) await page.locator(req.elementSelector).first().screenshot({ path: outAbsPath });
437
+ else await page.screenshot({
438
+ path: outAbsPath,
439
+ fullPage: req.fullPage === true
440
+ });
438
441
  }
439
442
  function makeSandboxId() {
440
- const rand = () => Math.random().toString(16).slice(2, 10);
441
- return `ui-audit-${rand()}${rand()}`;
443
+ const rand = () => Math.random().toString(16).slice(2, 10);
444
+ return `ui-audit-${rand()}${rand()}`;
442
445
  }
446
+ /** Create a `SandboxClient` that drives a local Playwright browser for in-process UI audits. @experimental */
443
447
  function createInProcessUiAuditClient(options) {
444
- const launch = options.launchBrowser ?? defaultLaunch;
445
- const navPolicy = options.navPolicy ?? "strict";
446
- let browserPromise;
447
- let closed = false;
448
- async function getBrowser() {
449
- if (closed) {
450
- throw new Error("ui-auditor: client is closed; create a new client to run another iteration");
451
- }
452
- if (!browserPromise) browserPromise = launch();
453
- return browserPromise;
454
- }
455
- async function* runIteration(promptText, signal) {
456
- const task = decodeAuditTaskEnvelope(promptText);
457
- if (!task) {
458
- throw new Error(
459
- "ui-auditor: prompt is missing a UI_AUDIT_TASK envelope. Use uiAuditorProfile().taskToPrompt to format prompts, or pass an envelope-prefixed prompt manually."
460
- );
461
- }
462
- if (task.captures.length === 0) {
463
- throw new Error("ui-auditor: task has zero captures; nothing to audit.");
464
- }
465
- yield asSandboxEvent("audit.lens", { lens: task.lens });
466
- const browser = await getBrowser();
467
- const context = await browser.newContext({ viewport: DEFAULT_VIEWPORT });
468
- let primaryError;
469
- let closeError;
470
- try {
471
- const page = await context.newPage();
472
- const captures = [];
473
- const fs2 = await import("fs/promises");
474
- const path2 = await import("path");
475
- const shotsDir = path2.join(options.workspaceDir, "screenshots");
476
- await fs2.mkdir(shotsDir, { recursive: true });
477
- for (const req of task.captures) {
478
- signal.throwIfAborted();
479
- const filename = captureFilename(req);
480
- const absPath = path2.join(shotsDir, filename);
481
- const relPath = `screenshots/${filename}`;
482
- await captureOne(page, req, absPath, signal, navPolicy);
483
- const vp = viewportOf(req);
484
- const cap = {
485
- path: relPath,
486
- viewport: `${vp.width}x${vp.height}`,
487
- fullPage: req.fullPage === true,
488
- route: req.route,
489
- url: req.url,
490
- capturedAt: (/* @__PURE__ */ new Date()).toISOString()
491
- };
492
- if (req.elementSelector) cap.elementSelector = req.elementSelector;
493
- if (req.label) cap.label = req.label;
494
- captures.push(cap);
495
- yield asSandboxEvent("audit.capture", cap);
496
- }
497
- const judgeOut = await options.judge({
498
- lens: task.lens,
499
- captures,
500
- productContext: task.productContext,
501
- knownFindingIds: task.knownFindingIds,
502
- promptText,
503
- signal
504
- });
505
- for (const finding of judgeOut.findings) {
506
- yield asSandboxEvent("audit.finding", finding);
507
- }
508
- if (judgeOut.notes && judgeOut.notes.trim().length > 0) {
509
- yield asSandboxEvent("audit.notes", { notes: judgeOut.notes });
510
- }
511
- const usage = judgeOut.tokenUsage ?? { input: 0, output: 0 };
512
- yield asSandboxEvent("done", {
513
- tokenUsage: {
514
- inputTokens: usage.input,
515
- outputTokens: usage.output
516
- },
517
- totalCostUsd: judgeOut.costUsd ?? 0
518
- });
519
- } catch (err) {
520
- primaryError = err;
521
- } finally {
522
- try {
523
- await context.close();
524
- } catch (err) {
525
- closeError = err;
526
- }
527
- }
528
- if (primaryError !== void 0 && closeError !== void 0) {
529
- throw new AggregateError(
530
- [primaryError, closeError],
531
- "ui-auditor: iteration failed AND context.close() failed; both errors attached."
532
- );
533
- }
534
- if (primaryError !== void 0) throw primaryError;
535
- if (closeError !== void 0) throw closeError;
536
- }
537
- function makeSyntheticSandbox() {
538
- const id = makeSandboxId();
539
- const instance = {
540
- id,
541
- streamPrompt(message, opts) {
542
- const signal = opts?.signal ?? new AbortController().signal;
543
- return runIteration(message, signal);
544
- },
545
- async delete() {
546
- }
547
- };
548
- return instance;
549
- }
550
- return {
551
- async create(_options) {
552
- return makeSyntheticSandbox();
553
- },
554
- describePlacement(box) {
555
- const id = box.id;
556
- return { kind: "sibling", sandboxId: typeof id === "string" ? id : void 0 };
557
- },
558
- async close() {
559
- closed = true;
560
- const pending = browserPromise;
561
- browserPromise = void 0;
562
- if (pending) {
563
- const browser = await pending;
564
- await browser.close();
565
- }
566
- }
567
- };
568
- }
569
-
570
- // src/profiles/ui-auditor/lens-prompts.ts
571
- var SHARED_AUDITOR_RULES = `
572
- You are auditing a UI for a specific class of problems. Stay strictly in your assigned lens \u2014 do not file issues that belong to another lens (a separate iteration will catch those).
448
+ const launch = options.launchBrowser ?? defaultLaunch;
449
+ const navPolicy = options.navPolicy ?? "strict";
450
+ let browserPromise;
451
+ let closed = false;
452
+ async function getBrowser() {
453
+ if (closed) throw new Error("ui-auditor: client is closed; create a new client to run another iteration");
454
+ if (!browserPromise) browserPromise = launch();
455
+ return browserPromise;
456
+ }
457
+ async function* runIteration(promptText, signal) {
458
+ const task = decodeAuditTaskEnvelope(promptText);
459
+ if (!task) throw new Error("ui-auditor: prompt is missing a UI_AUDIT_TASK envelope. Use uiAuditorProfile().taskToPrompt to format prompts, or pass an envelope-prefixed prompt manually.");
460
+ if (task.captures.length === 0) throw new Error("ui-auditor: task has zero captures; nothing to audit.");
461
+ yield asSandboxEvent("audit.lens", { lens: task.lens });
462
+ const context = await (await getBrowser()).newContext({ viewport: DEFAULT_VIEWPORT });
463
+ let primaryError;
464
+ let closeError;
465
+ try {
466
+ const page = await context.newPage();
467
+ const captures = [];
468
+ const fs = await import("node:fs/promises");
469
+ const path = await import("node:path");
470
+ const shotsDir = path.join(options.workspaceDir, "screenshots");
471
+ await fs.mkdir(shotsDir, { recursive: true });
472
+ for (const req of task.captures) {
473
+ signal.throwIfAborted();
474
+ const filename = captureFilename(req);
475
+ const absPath = path.join(shotsDir, filename);
476
+ const relPath = `screenshots/${filename}`;
477
+ await captureOne(page, req, absPath, signal, navPolicy);
478
+ const vp = viewportOf(req);
479
+ const cap = {
480
+ path: relPath,
481
+ viewport: `${vp.width}x${vp.height}`,
482
+ fullPage: req.fullPage === true,
483
+ route: req.route,
484
+ url: req.url,
485
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
486
+ };
487
+ if (req.elementSelector) cap.elementSelector = req.elementSelector;
488
+ if (req.label) cap.label = req.label;
489
+ captures.push(cap);
490
+ yield asSandboxEvent("audit.capture", cap);
491
+ }
492
+ const judgeOut = await options.judge({
493
+ lens: task.lens,
494
+ captures,
495
+ productContext: task.productContext,
496
+ knownFindingIds: task.knownFindingIds,
497
+ promptText,
498
+ signal
499
+ });
500
+ for (const finding of judgeOut.findings) yield asSandboxEvent("audit.finding", finding);
501
+ if (judgeOut.notes && judgeOut.notes.trim().length > 0) yield asSandboxEvent("audit.notes", { notes: judgeOut.notes });
502
+ const usage = judgeOut.tokenUsage ?? {
503
+ input: 0,
504
+ output: 0
505
+ };
506
+ yield asSandboxEvent("done", {
507
+ tokenUsage: {
508
+ inputTokens: usage.input,
509
+ outputTokens: usage.output
510
+ },
511
+ totalCostUsd: judgeOut.costUsd ?? 0
512
+ });
513
+ } catch (err) {
514
+ primaryError = err;
515
+ } finally {
516
+ try {
517
+ await context.close();
518
+ } catch (err) {
519
+ closeError = err;
520
+ }
521
+ }
522
+ if (primaryError !== void 0 && closeError !== void 0) throw new AggregateError([primaryError, closeError], "ui-auditor: iteration failed AND context.close() failed; both errors attached.");
523
+ if (primaryError !== void 0) throw primaryError;
524
+ if (closeError !== void 0) throw closeError;
525
+ }
526
+ function makeSyntheticSandbox() {
527
+ return {
528
+ id: makeSandboxId(),
529
+ streamPrompt(message, opts) {
530
+ return runIteration(message, opts?.signal ?? new AbortController().signal);
531
+ },
532
+ async delete() {}
533
+ };
534
+ }
535
+ return {
536
+ async create(_options) {
537
+ return makeSyntheticSandbox();
538
+ },
539
+ describePlacement(box) {
540
+ const id = box.id;
541
+ return {
542
+ kind: "sibling",
543
+ sandboxId: typeof id === "string" ? id : void 0
544
+ };
545
+ },
546
+ async close() {
547
+ closed = true;
548
+ const pending = browserPromise;
549
+ browserPromise = void 0;
550
+ if (pending) await (await pending).close();
551
+ }
552
+ };
553
+ }
554
+ //#endregion
555
+ //#region src/profiles/ui-auditor/lens-prompts.ts
556
+ /** Cross-lens rules injected into every UI audit iteration: finding quality standards and scope limits. @experimental */
557
+ const SHARED_AUDITOR_RULES = `
558
+ You are auditing a UI for a specific class of problems. Stay strictly in your assigned lens — do not file issues that belong to another lens (a separate iteration will catch those).
573
559
 
574
560
  A finding is only valid if a thoughtful product designer would agree the screenshot shows something that should change. Avoid:
575
561
  - Personal taste ("I'd prefer brand blue").
576
562
  - Hallucinated text or controls you cannot actually see in the screenshot.
577
563
  - Suggestions that depend on requirements you don't have access to.
578
- - Pile-on findings about the same root cause \u2014 file ONE finding and use \`similarTo\` to link the rest.
564
+ - Pile-on findings about the same root cause file ONE finding and use \`similarTo\` to link the rest.
579
565
 
580
566
  Required for every finding:
581
567
  - title: concrete, names the offending element AND what's wrong (NOT "improve UX").
582
568
  - severity: critical=blocks a core task or accessibility blocker; high=noticeable friction; med=visible polish issue; low=nitpick.
583
- - observation: 1\u20133 sentences describing exactly what you see that is wrong.
569
+ - observation: 1–3 sentences describing exactly what you see that is wrong.
584
570
  - impact: who is affected and how (concrete).
585
571
  - suggestedFix: a specific change a developer could apply without asking you back.
586
572
  - screenshots: refer to the captures attached to this iteration by path.
@@ -588,286 +574,273 @@ Required for every finding:
588
574
 
589
575
  Most findings are med or low. Reserve high/critical for genuine blockers.
590
576
  `.trim();
591
- var LENS_BRIEFS = {
592
- consistency: `
577
+ /** Per-lens auditor briefs: concrete signals to look for and cross-lens distinctions to respect. @experimental */
578
+ const LENS_BRIEFS = {
579
+ consistency: `
593
580
  LENS: consistency
594
- Look for inconsistencies in the design system \u2014 things that look like they came from different products glued together.
581
+ Look for inconsistencies in the design system things that look like they came from different products glued together.
595
582
  Signals: multiple font families, inconsistent weights/sizes for the same role, two shades of "primary", arbitrary paddings/margins that don't snap to a scale (4/8/12/16/24), same control with different border-radius or shadow on different pages, mixed icon styles (filled vs outlined), inconsistent button heights/padding for the same variant, inconsistent capitalization (Title Case vs sentence case) for the same role.
596
583
  NOT this lens: layout misalignment (use \`layout\`), confusing user flow (use \`ux-flow\`), contrast/keyboard issues (use \`accessibility\`).
597
584
  Title format: \`Inconsistent <thing> between <A> and <B>\`.
598
585
  `.trim(),
599
- hierarchy: `
586
+ hierarchy: `
600
587
  LENS: hierarchy
601
- Look for broken visual hierarchy \u2014 places where the eye does not land on what matters most.
588
+ Look for broken visual hierarchy places where the eye does not land on what matters most.
602
589
  Signals: primary CTA same weight as secondary/tertiary controls, headings (H1/H2/H3) nearly the same size, important data buried (headline number smaller than its label), decoration outshining content, too many emphases competing, wrong scan order, missing or overly heavy section dividers.
603
590
  NOT this lens: same-role styled differently (\`consistency\`), grid/alignment (\`layout\`), contrast-failing text (\`accessibility\`).
604
591
  Title format: \`Weak hierarchy: <element> does not read as the <intended-role>\`.
605
592
  `.trim(),
606
- layout: `
593
+ layout: `
607
594
  LENS: layout
608
- Look for layout and organization problems \u2014 alignment, grouping, whitespace, structural choices that hurt scannability.
595
+ Look for layout and organization problems alignment, grouping, whitespace, structural choices that hurt scannability.
609
596
  Signals: misalignment within rows, inconsistent gutters in grids, orphan whitespace next to crammed regions, poor grouping (related fields separated, unrelated fields adjacent), no visual sections (long wall of content), container overflow (text/content punching out of card boundaries), cramped or oversized hit targets, sidebars/headers sized wrong relative to main content.
610
597
  NOT this lens: same-role styled differently (\`consistency\`), click-distance/friction (\`ux-flow\`), overflow specifically at small viewports (\`responsive\`).
611
598
  Title format: \`<Region> alignment/spacing problem\` or \`<Region> grouping unclear\`.
612
599
  `.trim(),
613
- "ux-flow": `
600
+ "ux-flow": `
614
601
  LENS: ux-flow
615
- Look for interaction-flow friction \u2014 action sequences that are slower, more annoying, or more error-prone than necessary.
602
+ Look for interaction-flow friction action sequences that are slower, more annoying, or more error-prone than necessary.
616
603
  Signals: sequential clicks far apart (e.g. Next top-right while user is bottom-left), destructive action adjacent to primary with same weight, confirmations that don't say what's being confirmed, primary CTA below the fold or hidden in a kebab menu, silent state changes (toggle gives no feedback), form ordering that fights real-world order, dead-end states after submit, lost inputs on back-navigation, hidden pre-selected options.
617
604
  NOT this lens: visual style only (\`consistency\`), component arrangement without a flow problem (\`layout\`), microcopy clarity (\`content\`).
618
- Title format: \`<Action A> \u2192 <Action B> friction: <root cause>\`.
605
+ Title format: \`<Action A> <Action B> friction: <root cause>\`.
619
606
  `.trim(),
620
- duplication: `
607
+ duplication: `
621
608
  LENS: duplication
622
- Look for redundancy \u2014 the same control, link, or piece of content appearing more than once with no good reason.
609
+ Look for redundancy the same control, link, or piece of content appearing more than once with no good reason.
623
610
  Signals: two ways to do the same action on the same screen with no difference, repeated nav (same links in sidebar AND top nav), drifted duplicates (two copies that have diverged), content repeated verbatim, icon + label saying the same thing twice in one row, per-row + bulk actions that overlap confusingly, multiple status indicators conveying the same status.
624
- NOT this lens: inconsistent styling of duplicates (\`consistency\`) \u2014 this lens is about the existence of duplicates.
611
+ NOT this lens: inconsistent styling of duplicates (\`consistency\`) this lens is about the existence of duplicates.
625
612
  Title format: \`Duplicate <thing> in <location A> and <location B>\`.
626
613
  `.trim(),
627
- accessibility: `
614
+ accessibility: `
628
615
  LENS: accessibility
629
- Look for accessibility blockers and degradations. Be conservative \u2014 do not assume violations you cannot see.
630
- Signals: insufficient contrast on body text or controls, missing/invisible focus styles, tiny tap targets (<24px on mobile), color as sole signal (red border with no message), form labels missing or not associated (placeholders standing in for labels), broken heading order (H1 \u2192 H4), modals that don't trap focus, decorative elements that take focus, errors not announced, important text rendered inside images.
616
+ Look for accessibility blockers and degradations. Be conservative do not assume violations you cannot see.
617
+ Signals: insufficient contrast on body text or controls, missing/invisible focus styles, tiny tap targets (<24px on mobile), color as sole signal (red border with no message), form labels missing or not associated (placeholders standing in for labels), broken heading order (H1 H4), modals that don't trap focus, decorative elements that take focus, errors not announced, important text rendered inside images.
631
618
  NOT this lens: generic "looks confusing" (\`hierarchy\` or \`content\`), layout overflow at small viewports (\`responsive\`).
632
619
  Title format: \`Accessibility: <specific blocker> in <element>\`.
633
620
  `.trim(),
634
- responsive: `
621
+ responsive: `
635
622
  LENS: responsive
636
- Look for layout breakage across viewport sizes \u2014 content that works at one width but degrades at another. This iteration's captures should include the same surface at >=2 viewports; compare across them.
623
+ Look for layout breakage across viewport sizes content that works at one width but degrades at another. This iteration's captures should include the same surface at >=2 viewports; compare across them.
637
624
  Signals: horizontal scroll where content should reflow, overlapping elements (header overlaps content, fixed footer covers inputs), desktop nav crammed into mobile without collapsing, table columns that don't truncate, tap targets too close at touch sizes, controls vanishing at certain widths, layout flips that break grouping order, modals exceeding viewport height (confirm button unreachable).
638
625
  NOT this lens: issues present at every viewport (\`consistency\` / \`hierarchy\` instead).
639
626
  Title format: \`<Element/Region> breaks at <viewport>\`.
640
627
  `.trim(),
641
- states: `
628
+ states: `
642
629
  LENS: states
643
- Look for missing or broken UI states \u2014 the not-happy-paths that make a product feel finished or unfinished. The iteration's captures should depict at least one non-default state.
630
+ Look for missing or broken UI states the not-happy-paths that make a product feel finished or unfinished. The iteration's captures should depict at least one non-default state.
644
631
  Signals: empty lists with no guidance, skeletons that don't match final layout (CLS on settle), error states with no message or recovery action, disabled buttons with no explanation, toasts that disappear before being read, success states that don't confirm, missing hover/focus/active/disabled variants on primary controls, no long-content view, no-permission state broken.
645
632
  NOT this lens: generic polish on the happy path (other lenses), missing focus rings specifically (\`accessibility\`).
646
633
  Title format: \`Missing/broken <state> state on <surface>\`.
647
634
  `.trim(),
648
- content: `
635
+ content: `
649
636
  LENS: content
650
- Look for microcopy and content problems \u2014 text that is unclear, inconsistent, condescending, jargon-heavy, or wrong.
637
+ Look for microcopy and content problems text that is unclear, inconsistent, condescending, jargon-heavy, or wrong.
651
638
  Signals: jargon/internal language leaking ("Provisioning a Tenant" instead of "Setting up your account"), inconsistent terminology (workspace vs team), verbose button labels, empty-state copy that's just "No results", error messages blaming the user, tone inconsistency, truncation without affordance, mixed date/number formats on one page, placeholder used as a label, "Saved!" toast appearing before save completes, typos and grammar errors.
652
639
  NOT this lens: visual treatment of text (\`hierarchy\` / \`consistency\`), missing labels for a11y (\`accessibility\`).
653
640
  Title format: \`Copy: "<actual text>" in <location>\` or \`Inconsistent term: "<A>" vs "<B>"\`.
654
641
  `.trim(),
655
- interaction: `
642
+ interaction: `
656
643
  LENS: interaction
657
- Look for interaction quality problems \u2014 affordances, feedback, and micro-interactions.
644
+ Look for interaction quality problems affordances, feedback, and micro-interactions.
658
645
  Signals: no affordance (clickable areas not looking clickable, non-clickable areas looking clickable), missing feedback (>100ms click with no progress), hover surprises (whole row highlights but only title clickable), cursor inconsistency, animations that block input, missing transitions where they're needed (accordion snaps open), drag-and-drop without indicators, scroll-jacking, click-through bugs (card click handler firing alongside button), hover-only revelations on touch.
659
646
  NOT this lens: position of controls (\`layout\` / \`ux-flow\`), missing focus styles (\`accessibility\`).
660
647
  Title format: \`<Action> on <element>: <missing/wrong> feedback\`.
661
648
  `.trim(),
662
- "performance-perceived": `
649
+ "performance-perceived": `
663
650
  LENS: performance-perceived
664
- Look for perceived-performance problems \u2014 visible jank a real user would notice, not benchmark numbers. This iteration's captures should include >=2 frames during load to show shift.
651
+ Look for perceived-performance problems visible jank a real user would notice, not benchmark numbers. This iteration's captures should include >=2 frames during load to show shift.
665
652
  Signals: layout shift (CLS) when late-arriving images/fonts/banners settle, FOUC (flash of unstyled content), font swap jumps, late-loading hero images that shift everything, skeletons that don't match final shape, spinners on instant local actions, loading state reappearing after content paints (refetch on focus), modal open animation longer than the operation it precedes.
666
653
  NOT this lens: slow API calls (file separately), stale data after navigation (\`states\`).
667
654
  Title format: \`Layout shift / late paint on <route>: <root cause>\`.
668
655
  `.trim(),
669
- other: `
656
+ other: `
670
657
  LENS: other
671
- Use ONLY when a finding is clearly a UI quality issue but does not fit any other lens. Strongly prefer a specific lens \u2014 \`other\` should be rare. Title must still be concrete.
658
+ Use ONLY when a finding is clearly a UI quality issue but does not fit any other lens. Strongly prefer a specific lens \`other\` should be rare. Title must still be concrete.
672
659
  `.trim()
673
660
  };
661
+ /**
662
+ * Build a system prompt for a single auditor iteration.
663
+ *
664
+ * @experimental
665
+ */
674
666
  function buildAuditorSystemPrompt(lens) {
675
- const brief = LENS_BRIEFS[lens];
676
- return `${SHARED_AUDITOR_RULES}
677
-
678
- ${brief}`;
667
+ const brief = LENS_BRIEFS[lens];
668
+ return `${SHARED_AUDITOR_RULES}\n\n${brief}`;
679
669
  }
680
-
681
- // src/profiles/ui-auditor/output-adapter.ts
682
- var KNOWN_LENS_VALUES = new Set(UI_LENSES);
670
+ //#endregion
671
+ //#region src/profiles/ui-auditor/output-adapter.ts
672
+ const KNOWN_LENS_VALUES = new Set(UI_LENSES);
683
673
  function isUiLens(v) {
684
- return typeof v === "string" && KNOWN_LENS_VALUES.has(v);
674
+ return typeof v === "string" && KNOWN_LENS_VALUES.has(v);
685
675
  }
676
+ /** Parse raw `SandboxEvent` emissions from an audit iteration into structured `UiAuditOutput`. @experimental */
686
677
  function parseAuditorEvents(events) {
687
- const findings = [];
688
- const captures = [];
689
- let lens;
690
- let notes;
691
- for (const evt of events) {
692
- if (!evt || typeof evt !== "object") continue;
693
- const type = String(evt.type ?? "");
694
- const data = evt.data && typeof evt.data === "object" ? evt.data : void 0;
695
- if (!data) continue;
696
- switch (type) {
697
- case "audit.lens": {
698
- const v = data.lens;
699
- if (isUiLens(v)) lens = v;
700
- break;
701
- }
702
- case "audit.capture": {
703
- const cap = data;
704
- if (typeof cap.path === "string" && typeof cap.viewport === "string" && typeof cap.fullPage === "boolean" && typeof cap.route === "string" && typeof cap.url === "string" && typeof cap.capturedAt === "string") {
705
- const out2 = {
706
- path: cap.path,
707
- viewport: cap.viewport,
708
- fullPage: cap.fullPage,
709
- route: cap.route,
710
- url: cap.url,
711
- capturedAt: cap.capturedAt
712
- };
713
- if (cap.elementSelector) out2.elementSelector = cap.elementSelector;
714
- if (cap.label) out2.label = cap.label;
715
- captures.push(out2);
716
- }
717
- break;
718
- }
719
- case "audit.finding": {
720
- const f = data;
721
- if (typeof f.title === "string" && f.title.trim().length > 0 && isUiLens(f.lens) && typeof f.severity === "string" && ["low", "med", "high", "critical"].includes(f.severity) && typeof f.route === "string" && typeof f.observation === "string" && typeof f.impact === "string" && typeof f.suggestedFix === "string" && Array.isArray(f.screenshots)) {
722
- findings.push(f);
723
- }
724
- break;
725
- }
726
- case "audit.notes": {
727
- const n = data.notes;
728
- if (typeof n === "string" && n.trim().length > 0) notes = n;
729
- break;
730
- }
731
- default:
732
- break;
733
- }
734
- }
735
- const out = { lens: lens ?? "other", findings, captures };
736
- if (notes) out.notes = notes;
737
- return out;
738
- }
739
-
740
- // src/profiles/ui-auditor/validator.ts
741
- var GENERIC_TITLE_PATTERNS = [
742
- /^improve\s/i,
743
- /^fix\s/i,
744
- /^update\s/i,
745
- /^better\s/i,
746
- /^bad\s/i,
747
- /^make\s.+\sbetter/i,
748
- /\bUX\b\s*$/i,
749
- /\bUI\b\s*$/i
678
+ const findings = [];
679
+ const captures = [];
680
+ let lens;
681
+ let notes;
682
+ for (const evt of events) {
683
+ if (!evt || typeof evt !== "object") continue;
684
+ const type = String(evt.type ?? "");
685
+ const data = evt.data && typeof evt.data === "object" ? evt.data : void 0;
686
+ if (!data) continue;
687
+ switch (type) {
688
+ case "audit.lens": {
689
+ const v = data.lens;
690
+ if (isUiLens(v)) lens = v;
691
+ break;
692
+ }
693
+ case "audit.capture": {
694
+ const cap = data;
695
+ if (typeof cap.path === "string" && typeof cap.viewport === "string" && typeof cap.fullPage === "boolean" && typeof cap.route === "string" && typeof cap.url === "string" && typeof cap.capturedAt === "string") {
696
+ const out = {
697
+ path: cap.path,
698
+ viewport: cap.viewport,
699
+ fullPage: cap.fullPage,
700
+ route: cap.route,
701
+ url: cap.url,
702
+ capturedAt: cap.capturedAt
703
+ };
704
+ if (cap.elementSelector) out.elementSelector = cap.elementSelector;
705
+ if (cap.label) out.label = cap.label;
706
+ captures.push(out);
707
+ }
708
+ break;
709
+ }
710
+ case "audit.finding": {
711
+ const f = data;
712
+ if (typeof f.title === "string" && f.title.trim().length > 0 && isUiLens(f.lens) && typeof f.severity === "string" && [
713
+ "low",
714
+ "med",
715
+ "high",
716
+ "critical"
717
+ ].includes(f.severity) && typeof f.route === "string" && typeof f.observation === "string" && typeof f.impact === "string" && typeof f.suggestedFix === "string" && Array.isArray(f.screenshots)) findings.push(f);
718
+ break;
719
+ }
720
+ case "audit.notes": {
721
+ const n = data.notes;
722
+ if (typeof n === "string" && n.trim().length > 0) notes = n;
723
+ break;
724
+ }
725
+ default: break;
726
+ }
727
+ }
728
+ const out = {
729
+ lens: lens ?? "other",
730
+ findings,
731
+ captures
732
+ };
733
+ if (notes) out.notes = notes;
734
+ return out;
735
+ }
736
+ //#endregion
737
+ //#region src/profiles/ui-auditor/validator.ts
738
+ const GENERIC_TITLE_PATTERNS = [
739
+ /^improve\s/i,
740
+ /^fix\s/i,
741
+ /^update\s/i,
742
+ /^better\s/i,
743
+ /^bad\s/i,
744
+ /^make\s.+\sbetter/i,
745
+ /\bUX\b\s*$/i,
746
+ /\bUI\b\s*$/i
750
747
  ];
751
748
  function isGenericTitle(title) {
752
- const t = title.trim();
753
- if (t.length < 16) return true;
754
- return GENERIC_TITLE_PATTERNS.some((re) => re.test(t));
749
+ const t = title.trim();
750
+ if (t.length < 16) return true;
751
+ return GENERIC_TITLE_PATTERNS.some((re) => re.test(t));
755
752
  }
753
+ /** Build a `Validator` that rejects off-lens findings and findings missing screenshot evidence. @experimental */
756
754
  function createUiAuditorValidator(task) {
757
- return {
758
- async validate(output) {
759
- const findings = output.findings;
760
- const captures = output.captures;
761
- const capturePaths = new Set(captures.map((c) => c.path));
762
- const offLens = findings.filter((f) => f.lens !== task.lens);
763
- if (offLens.length > 0) {
764
- const verdict2 = {
765
- valid: false,
766
- score: 0,
767
- notes: `${offLens.length} finding(s) filed under wrong lens (expected ${task.lens}; got ${offLens.map((f) => f.lens).join(", ")})`,
768
- scores: { offLens: 0 }
769
- };
770
- return verdict2;
771
- }
772
- const missingEvidence = findings.filter(
773
- (f) => !Array.isArray(f.screenshots) || f.screenshots.length === 0
774
- );
775
- if (missingEvidence.length > 0) {
776
- const verdict2 = {
777
- valid: false,
778
- score: 0,
779
- notes: `${missingEvidence.length} finding(s) have no screenshot evidence`,
780
- scores: { evidence: 0 }
781
- };
782
- return verdict2;
783
- }
784
- const unresolvedShot = findings.filter(
785
- (f) => f.screenshots.some((s) => !capturePaths.has(s.path))
786
- );
787
- if (unresolvedShot.length > 0) {
788
- const verdict2 = {
789
- valid: false,
790
- score: 0,
791
- notes: `${unresolvedShot.length} finding(s) reference screenshot paths not captured this iteration`,
792
- scores: { evidence: 0 }
793
- };
794
- return verdict2;
795
- }
796
- if (findings.length === 0) {
797
- const verdict2 = {
798
- valid: true,
799
- score: 0.5,
800
- notes: "No findings reported. Neither a confident pass nor a failure.",
801
- scores: { specificity: 0, evidence: 1, titles: 1 }
802
- };
803
- return verdict2;
804
- }
805
- const withSelector = findings.filter((f) => typeof f.selector === "string").length;
806
- const specificity = withSelector / findings.length;
807
- const generic = findings.filter((f) => isGenericTitle(f.title)).length;
808
- const titles = 1 - generic / findings.length;
809
- const withFullEvidence = findings.filter(
810
- (f) => Array.isArray(f.screenshots) && f.screenshots.length > 0 && f.screenshots.every((s) => capturePaths.has(s.path))
811
- ).length;
812
- const evidence = withFullEvidence / findings.length;
813
- const score = Number((0.4 * specificity + 0.4 * evidence + 0.2 * titles).toFixed(4));
814
- const verdict = {
815
- valid: true,
816
- score,
817
- notes: `${findings.length} finding(s) \u2014 specificity=${specificity.toFixed(2)} evidence=${evidence.toFixed(2)} titles=${titles.toFixed(2)}`,
818
- scores: { specificity, evidence, titles }
819
- };
820
- return verdict;
821
- }
822
- };
823
- }
824
-
825
- // src/profiles/ui-auditor/profile.ts
755
+ return { async validate(output) {
756
+ const findings = output.findings;
757
+ const captures = output.captures;
758
+ const capturePaths = new Set(captures.map((c) => c.path));
759
+ const offLens = findings.filter((f) => f.lens !== task.lens);
760
+ if (offLens.length > 0) return {
761
+ valid: false,
762
+ score: 0,
763
+ notes: `${offLens.length} finding(s) filed under wrong lens (expected ${task.lens}; got ${offLens.map((f) => f.lens).join(", ")})`,
764
+ scores: { offLens: 0 }
765
+ };
766
+ const missingEvidence = findings.filter((f) => !Array.isArray(f.screenshots) || f.screenshots.length === 0);
767
+ if (missingEvidence.length > 0) return {
768
+ valid: false,
769
+ score: 0,
770
+ notes: `${missingEvidence.length} finding(s) have no screenshot evidence`,
771
+ scores: { evidence: 0 }
772
+ };
773
+ const unresolvedShot = findings.filter((f) => f.screenshots.some((s) => !capturePaths.has(s.path)));
774
+ if (unresolvedShot.length > 0) return {
775
+ valid: false,
776
+ score: 0,
777
+ notes: `${unresolvedShot.length} finding(s) reference screenshot paths not captured this iteration`,
778
+ scores: { evidence: 0 }
779
+ };
780
+ if (findings.length === 0) return {
781
+ valid: true,
782
+ score: .5,
783
+ notes: "No findings reported. Neither a confident pass nor a failure.",
784
+ scores: {
785
+ specificity: 0,
786
+ evidence: 1,
787
+ titles: 1
788
+ }
789
+ };
790
+ const specificity = findings.filter((f) => typeof f.selector === "string").length / findings.length;
791
+ const titles = 1 - findings.filter((f) => isGenericTitle(f.title)).length / findings.length;
792
+ const evidence = findings.filter((f) => Array.isArray(f.screenshots) && f.screenshots.length > 0 && f.screenshots.every((s) => capturePaths.has(s.path))).length / findings.length;
793
+ return {
794
+ valid: true,
795
+ score: Number((.4 * specificity + .4 * evidence + .2 * titles).toFixed(4)),
796
+ notes: `${findings.length} finding(s) — specificity=${specificity.toFixed(2)} evidence=${evidence.toFixed(2)} titles=${titles.toFixed(2)}`,
797
+ scores: {
798
+ specificity,
799
+ evidence,
800
+ titles
801
+ }
802
+ };
803
+ } };
804
+ }
805
+ //#endregion
806
+ //#region src/profiles/ui-auditor/profile.ts
807
+ /**
808
+ * Preset `runAgentRounds` bundle for vision-driven UI audits: returns the `AgentRunSpec`, output adapter, validator, and prompt formatter the loop kernel needs.
809
+ *
810
+ * @experimental
811
+ */
826
812
  function uiAuditorProfile(options = {}) {
827
- const name = options.name ?? "ui-auditor";
828
- const profile = {
829
- name,
830
- description: "Vision-driven UI auditor. One lens per iteration.",
831
- prompt: { systemPrompt: "" },
832
- model: options.model ? { default: options.model } : void 0,
833
- tools: { browser: true, vision: true },
834
- metadata: { role: "ui-auditor" }
835
- };
836
- const output = { parse: parseAuditorEvents };
837
- const validator = options.task ? createUiAuditorValidator(options.task) : createUiAuditorValidator({ lens: "other", captures: [] });
838
- const taskToPrompt = (task) => `${encodeAuditTaskEnvelope(task)}
839
- ${buildAuditorSystemPrompt(task.lens)}
813
+ const name = options.name ?? "ui-auditor";
814
+ const profile = {
815
+ name,
816
+ description: "Vision-driven UI auditor. One lens per iteration.",
817
+ prompt: { systemPrompt: "" },
818
+ model: options.model ? { default: options.model } : void 0,
819
+ tools: {
820
+ browser: true,
821
+ vision: true
822
+ },
823
+ metadata: { role: "ui-auditor" }
824
+ };
825
+ const output = { parse: parseAuditorEvents };
826
+ const validator = options.task ? createUiAuditorValidator(options.task) : createUiAuditorValidator({
827
+ lens: "other",
828
+ captures: []
829
+ });
830
+ const taskToPrompt = (task) => `${encodeAuditTaskEnvelope(task)}\n${buildAuditorSystemPrompt(task.lens)}\n\n${formatAuditorPrompt(task)}`;
831
+ return {
832
+ profile,
833
+ taskToPrompt,
834
+ output,
835
+ validator,
836
+ agentRunSpec: {
837
+ name,
838
+ profile,
839
+ taskToPrompt
840
+ }
841
+ };
842
+ }
843
+ //#endregion
844
+ export { LENS_BRIEFS, SHARED_AUDITOR_RULES, UI_FINDING_SEVERITIES, UI_LENSES, appendFindings, buildAuditorSystemPrompt, coderTaskToPrompt, createInProcessUiAuditClient, createResearcherValidator, createUiAuditorValidator, decodeAuditTaskEnvelope, encodeAuditTaskEnvelope, formatAuditorPrompt, initAuditWorkspace, multiHarnessResearcherFanout, parseAuditorEvents, readAuditRegistry, registerCaptures, researcherProfile, summarizeRegistry, uiAuditorProfile, writeAuditIndex };
840
845
 
841
- ${formatAuditorPrompt(task)}`;
842
- const agentRunSpec = {
843
- name,
844
- profile,
845
- taskToPrompt
846
- };
847
- return { profile, taskToPrompt, output, validator, agentRunSpec };
848
- }
849
- export {
850
- LENS_BRIEFS,
851
- SHARED_AUDITOR_RULES,
852
- UI_FINDING_SEVERITIES,
853
- UI_LENSES,
854
- appendFindings,
855
- buildAuditorSystemPrompt,
856
- coderTaskToPrompt,
857
- createInProcessUiAuditClient,
858
- createResearcherValidator,
859
- createUiAuditorValidator,
860
- decodeAuditTaskEnvelope,
861
- encodeAuditTaskEnvelope,
862
- formatAuditorPrompt,
863
- initAuditWorkspace,
864
- multiHarnessResearcherFanout,
865
- parseAuditorEvents,
866
- readAuditRegistry,
867
- registerCaptures,
868
- researcherProfile,
869
- summarizeRegistry,
870
- uiAuditorProfile,
871
- writeAuditIndex
872
- };
873
846
  //# sourceMappingURL=profiles.js.map