@bastani/atomic 0.9.11-alpha.6 → 0.9.11-alpha.8

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 (452) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +3 -5
  3. package/dist/builtin/intercom/package.json +1 -1
  4. package/dist/builtin/mcp/package.json +1 -1
  5. package/dist/builtin/mcp/sampling-handler.ts +1 -5
  6. package/dist/builtin/subagents/package.json +1 -1
  7. package/dist/builtin/web-access/package.json +1 -1
  8. package/dist/builtin/workflows/CHANGELOG.md +1 -0
  9. package/dist/builtin/workflows/README.md +1 -1
  10. package/dist/builtin/workflows/package.json +1 -1
  11. package/dist/builtin/workflows/skills/impeccable/SKILL.md +33 -129
  12. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_asset_producer.toml +11 -3
  13. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_documenter.toml +26 -0
  14. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_finish_reviewer.toml +35 -0
  15. package/dist/builtin/workflows/skills/impeccable/reference/android.md +1 -1
  16. package/dist/builtin/workflows/skills/impeccable/reference/animate.md +72 -189
  17. package/dist/builtin/workflows/skills/impeccable/reference/audit.md +10 -9
  18. package/dist/builtin/workflows/skills/impeccable/reference/audit.native.md +2 -2
  19. package/dist/builtin/workflows/skills/impeccable/reference/bolder.md +19 -108
  20. package/dist/builtin/workflows/skills/impeccable/reference/clarify.md +59 -253
  21. package/dist/builtin/workflows/skills/impeccable/reference/colorize.md +51 -222
  22. package/dist/builtin/workflows/skills/impeccable/reference/craft-floor.md +45 -0
  23. package/dist/builtin/workflows/skills/impeccable/reference/craft.md +3 -121
  24. package/dist/builtin/workflows/skills/impeccable/reference/critique.md +28 -20
  25. package/dist/builtin/workflows/skills/impeccable/reference/degraded/asset-producer.md +97 -0
  26. package/dist/builtin/workflows/skills/impeccable/reference/degraded/documenter.md +23 -0
  27. package/dist/builtin/workflows/skills/impeccable/reference/degraded/finish-reviewer.md +32 -0
  28. package/dist/builtin/workflows/skills/impeccable/reference/degraded/manual-edit-applier.md +92 -0
  29. package/dist/builtin/workflows/skills/impeccable/reference/delight.md +47 -279
  30. package/dist/builtin/workflows/skills/impeccable/reference/distill.md +2 -2
  31. package/dist/builtin/workflows/skills/impeccable/reference/doctor.md +53 -0
  32. package/dist/builtin/workflows/skills/impeccable/reference/document.md +60 -73
  33. package/dist/builtin/workflows/skills/impeccable/reference/harden.md +1 -12
  34. package/dist/builtin/workflows/skills/impeccable/reference/hooks.md +17 -4
  35. package/dist/builtin/workflows/skills/impeccable/reference/init.md +64 -163
  36. package/dist/builtin/workflows/skills/impeccable/reference/ios.md +1 -1
  37. package/dist/builtin/workflows/skills/impeccable/reference/layout.md +52 -153
  38. package/dist/builtin/workflows/skills/impeccable/reference/live.md +44 -32
  39. package/dist/builtin/workflows/skills/impeccable/reference/new-work.md +105 -0
  40. package/dist/builtin/workflows/skills/impeccable/reference/{product.md → operate.md} +6 -5
  41. package/dist/builtin/workflows/skills/impeccable/reference/optimize.md +4 -4
  42. package/dist/builtin/workflows/skills/impeccable/reference/overdrive.md +1 -4
  43. package/dist/builtin/workflows/skills/impeccable/reference/polish.md +68 -212
  44. package/dist/builtin/workflows/skills/impeccable/reference/quieter.md +3 -3
  45. package/dist/builtin/workflows/skills/impeccable/reference/routing.md +18 -0
  46. package/dist/builtin/workflows/skills/impeccable/reference/shape.md +38 -144
  47. package/dist/builtin/workflows/skills/impeccable/reference/typeset.md +48 -269
  48. package/dist/builtin/workflows/skills/impeccable/reference/visualize.md +38 -0
  49. package/dist/builtin/workflows/skills/impeccable/scripts/command-metadata.json +1 -1
  50. package/dist/builtin/workflows/skills/impeccable/scripts/concept-seed.mjs +584 -0
  51. package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +117 -9
  52. package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +486 -59
  53. package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +16 -45
  54. package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +96 -10
  55. package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +143 -26
  56. package/dist/builtin/workflows/skills/impeccable/scripts/detector/design-system.mjs +181 -12
  57. package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +3187 -182
  58. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +102 -7
  59. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +282 -70
  60. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +183 -15
  61. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +22 -7
  62. package/dist/builtin/workflows/skills/impeccable/scripts/detector/findings.mjs +7 -1
  63. package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +16 -2
  64. package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +155 -42
  65. package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +2988 -155
  66. package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/constants.mjs +11 -0
  67. package/dist/builtin/workflows/skills/impeccable/scripts/doctor.mjs +336 -0
  68. package/dist/builtin/workflows/skills/impeccable/scripts/generate-image.mjs +232 -0
  69. package/dist/builtin/workflows/skills/impeccable/scripts/hook-admin.mjs +90 -10
  70. package/dist/builtin/workflows/skills/impeccable/scripts/hook-lib.mjs +410 -78
  71. package/dist/builtin/workflows/skills/impeccable/scripts/hook.mjs +25 -8
  72. package/dist/builtin/workflows/skills/impeccable/scripts/lib/artifact-schema.mjs +93 -0
  73. package/dist/builtin/workflows/skills/impeccable/scripts/lib/composition-catalog.mjs +165 -0
  74. package/dist/builtin/workflows/skills/impeccable/scripts/lib/concept-catalog.mjs +329 -0
  75. package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-config.mjs +20 -5
  76. package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-paths.mjs +16 -8
  77. package/dist/builtin/workflows/skills/impeccable/scripts/lib/provider.mjs +1 -0
  78. package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness-deep.mjs +455 -0
  79. package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness-notice.mjs +169 -0
  80. package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness.mjs +457 -0
  81. package/dist/builtin/workflows/skills/impeccable/scripts/lib/surface-briefs.mjs +151 -0
  82. package/dist/builtin/workflows/skills/impeccable/scripts/lib/target-slug.mjs +33 -0
  83. package/dist/builtin/workflows/skills/impeccable/scripts/lib/template-extensions.mjs +146 -0
  84. package/dist/builtin/workflows/skills/impeccable/scripts/live/completion.mjs +10 -1
  85. package/dist/builtin/workflows/skills/impeccable/scripts/live/event-validation.mjs +15 -0
  86. package/dist/builtin/workflows/skills/impeccable/scripts/live/generation-preflight.mjs +149 -0
  87. package/dist/builtin/workflows/skills/impeccable/scripts/live/poll-lanes.mjs +14 -0
  88. package/dist/builtin/workflows/skills/impeccable/scripts/live/session-store.mjs +109 -32
  89. package/dist/builtin/workflows/skills/impeccable/scripts/live/source-lock.mjs +105 -0
  90. package/dist/builtin/workflows/skills/impeccable/scripts/live/source-search.mjs +105 -0
  91. package/dist/builtin/workflows/skills/impeccable/scripts/live/sveltekit-adapter.mjs +8 -6
  92. package/dist/builtin/workflows/skills/impeccable/scripts/live/tanstack-adapter.mjs +280 -0
  93. package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +208 -68
  94. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +563 -112
  95. package/dist/builtin/workflows/skills/impeccable/scripts/live-commit-manual-edits.mjs +3 -0
  96. package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +198 -13
  97. package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +24 -6
  98. package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edit-evidence.mjs +6 -1
  99. package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +44 -14
  100. package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +398 -33
  101. package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +9 -5
  102. package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +81 -67
  103. package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +7 -2
  104. package/dist/builtin/workflows/skills/impeccable/scripts/palette.mjs +76 -81
  105. package/dist/builtin/workflows/skills/impeccable/scripts/pin.mjs +2 -2
  106. package/dist/builtin/workflows/skills/impeccable/scripts/serve-question.mjs +890 -0
  107. package/dist/builtin/workflows/skills/impeccable/scripts/surface-brief.mjs +74 -0
  108. package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +1 -24
  109. package/dist/builtin/workflows/src/extension/index.bundle.mjs +232 -50
  110. package/dist/builtin/workflows/src/extension/workflow-model-catalog.ts +26 -0
  111. package/dist/builtin/workflows/src/extension/workflow-prompts.ts +3 -2
  112. package/dist/builtin/workflows/src/runs/foreground/executor-stage-control.ts +23 -9
  113. package/dist/builtin/workflows/src/runs/foreground/executor-stage-replay.ts +1 -0
  114. package/dist/builtin/workflows/src/runs/foreground/postmortem-stage-chat.ts +20 -5
  115. package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +10 -0
  116. package/dist/builtin/workflows/src/runs/foreground/stage-delivery-activity.ts +86 -0
  117. package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +6 -2
  118. package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +24 -18
  119. package/dist/builtin/workflows/src/runs/foreground/stage-runner-send-user-message.ts +8 -0
  120. package/dist/builtin/workflows/src/runs/foreground/stage-runner-session-options.ts +3 -3
  121. package/dist/builtin/workflows/src/runs/foreground/stage-runner-session.ts +2 -2
  122. package/dist/builtin/workflows/src/runs/foreground/stage-runner-types.ts +16 -0
  123. package/dist/builtin/workflows/src/tui/stage-chat-view-delivery-activity.ts +121 -0
  124. package/dist/builtin/workflows/src/tui/stage-chat-view-live-events.ts +42 -0
  125. package/dist/builtin/workflows/src/tui/stage-chat-view-state.ts +16 -0
  126. package/dist/builtin/workflows/src/tui/stage-chat-view-types.ts +4 -0
  127. package/dist/cli/list-models.d.ts +2 -2
  128. package/dist/cli/list-models.d.ts.map +1 -1
  129. package/dist/cli/list-models.js +5 -5
  130. package/dist/cli/list-models.js.map +1 -1
  131. package/dist/core/agent-session-accessors.js +1 -1
  132. package/dist/core/agent-session-accessors.js.map +1 -1
  133. package/dist/core/agent-session-auto-compaction.d.ts.map +1 -1
  134. package/dist/core/agent-session-auto-compaction.js +2 -5
  135. package/dist/core/agent-session-auto-compaction.js.map +1 -1
  136. package/dist/core/agent-session-compaction.js +1 -1
  137. package/dist/core/agent-session-compaction.js.map +1 -1
  138. package/dist/core/agent-session-extension-bindings.js +5 -5
  139. package/dist/core/agent-session-extension-bindings.js.map +1 -1
  140. package/dist/core/agent-session-methods.d.ts +4 -4
  141. package/dist/core/agent-session-methods.d.ts.map +1 -1
  142. package/dist/core/agent-session-methods.js.map +1 -1
  143. package/dist/core/agent-session-models.d.ts +1 -1
  144. package/dist/core/agent-session-models.d.ts.map +1 -1
  145. package/dist/core/agent-session-models.js +18 -14
  146. package/dist/core/agent-session-models.js.map +1 -1
  147. package/dist/core/agent-session-post-tool-compaction.d.ts.map +1 -1
  148. package/dist/core/agent-session-post-tool-compaction.js +2 -4
  149. package/dist/core/agent-session-post-tool-compaction.js.map +1 -1
  150. package/dist/core/agent-session-prompt.d.ts.map +1 -1
  151. package/dist/core/agent-session-prompt.js +3 -7
  152. package/dist/core/agent-session-prompt.js.map +1 -1
  153. package/dist/core/agent-session-retry.js +1 -1
  154. package/dist/core/agent-session-retry.js.map +1 -1
  155. package/dist/core/agent-session-runtime-auth.d.ts +3 -3
  156. package/dist/core/agent-session-runtime-auth.d.ts.map +1 -1
  157. package/dist/core/agent-session-runtime-auth.js +4 -14
  158. package/dist/core/agent-session-runtime-auth.js.map +1 -1
  159. package/dist/core/agent-session-runtime.d.ts +1 -1
  160. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  161. package/dist/core/agent-session-runtime.js +3 -3
  162. package/dist/core/agent-session-runtime.js.map +1 -1
  163. package/dist/core/agent-session-services.d.ts +3 -8
  164. package/dist/core/agent-session-services.d.ts.map +1 -1
  165. package/dist/core/agent-session-services.js +16 -23
  166. package/dist/core/agent-session-services.js.map +1 -1
  167. package/dist/core/agent-session-tool-registry.d.ts.map +1 -1
  168. package/dist/core/agent-session-tool-registry.js +2 -1
  169. package/dist/core/agent-session-tool-registry.js.map +1 -1
  170. package/dist/core/agent-session-types.d.ts +2 -2
  171. package/dist/core/agent-session-types.d.ts.map +1 -1
  172. package/dist/core/agent-session-types.js.map +1 -1
  173. package/dist/core/agent-session.d.ts +2 -2
  174. package/dist/core/agent-session.d.ts.map +1 -1
  175. package/dist/core/agent-session.js +1 -1
  176. package/dist/core/agent-session.js.map +1 -1
  177. package/dist/core/auth-storage-backends.d.ts +1 -0
  178. package/dist/core/auth-storage-backends.d.ts.map +1 -1
  179. package/dist/core/auth-storage-backends.js +18 -4
  180. package/dist/core/auth-storage-backends.js.map +1 -1
  181. package/dist/core/auth-storage.d.ts +12 -139
  182. package/dist/core/auth-storage.d.ts.map +1 -1
  183. package/dist/core/auth-storage.js +68 -374
  184. package/dist/core/auth-storage.js.map +1 -1
  185. package/dist/core/copilot-env-routing.d.ts +36 -0
  186. package/dist/core/copilot-env-routing.d.ts.map +1 -0
  187. package/dist/core/copilot-env-routing.js +94 -0
  188. package/dist/core/copilot-env-routing.js.map +1 -0
  189. package/dist/core/extensions/loader-virtual-modules.d.ts.map +1 -1
  190. package/dist/core/extensions/loader-virtual-modules.js +10 -2
  191. package/dist/core/extensions/loader-virtual-modules.js.map +1 -1
  192. package/dist/core/extensions/provider-types.d.ts +0 -40
  193. package/dist/core/extensions/provider-types.d.ts.map +1 -1
  194. package/dist/core/extensions/provider-types.js.map +1 -1
  195. package/dist/core/fallback-models.d.ts +4 -4
  196. package/dist/core/fallback-models.d.ts.map +1 -1
  197. package/dist/core/fallback-models.js +3 -3
  198. package/dist/core/fallback-models.js.map +1 -1
  199. package/dist/core/model-config.d.ts +542 -0
  200. package/dist/core/model-config.d.ts.map +1 -0
  201. package/dist/core/{model-registry-schemas.js → model-config.js} +85 -34
  202. package/dist/core/model-config.js.map +1 -0
  203. package/dist/core/model-registry.d.ts +25 -85
  204. package/dist/core/model-registry.d.ts.map +1 -1
  205. package/dist/core/model-registry.js +65 -388
  206. package/dist/core/model-registry.js.map +1 -1
  207. package/dist/core/model-resolver-cli.d.ts +2 -2
  208. package/dist/core/model-resolver-cli.d.ts.map +1 -1
  209. package/dist/core/model-resolver-cli.js +2 -2
  210. package/dist/core/model-resolver-cli.js.map +1 -1
  211. package/dist/core/model-resolver-initial.d.ts +4 -4
  212. package/dist/core/model-resolver-initial.d.ts.map +1 -1
  213. package/dist/core/model-resolver-initial.js +17 -17
  214. package/dist/core/model-resolver-initial.js.map +1 -1
  215. package/dist/core/model-resolver-scope.d.ts +3 -3
  216. package/dist/core/model-resolver-scope.d.ts.map +1 -1
  217. package/dist/core/model-resolver-scope.js +4 -4
  218. package/dist/core/model-resolver-scope.js.map +1 -1
  219. package/dist/core/model-runtime-auth.d.ts +7 -0
  220. package/dist/core/model-runtime-auth.d.ts.map +1 -0
  221. package/dist/core/model-runtime-auth.js +14 -0
  222. package/dist/core/model-runtime-auth.js.map +1 -0
  223. package/dist/core/model-runtime-providers.d.ts +6 -0
  224. package/dist/core/model-runtime-providers.d.ts.map +1 -0
  225. package/dist/core/model-runtime-providers.js +22 -0
  226. package/dist/core/model-runtime-providers.js.map +1 -0
  227. package/dist/core/model-runtime-restoration.d.ts +6 -0
  228. package/dist/core/model-runtime-restoration.d.ts.map +1 -0
  229. package/dist/core/model-runtime-restoration.js +21 -0
  230. package/dist/core/model-runtime-restoration.js.map +1 -0
  231. package/dist/core/model-runtime-snapshot.d.ts +16 -0
  232. package/dist/core/model-runtime-snapshot.d.ts.map +1 -0
  233. package/dist/core/model-runtime-snapshot.js +52 -0
  234. package/dist/core/model-runtime-snapshot.js.map +1 -0
  235. package/dist/core/model-runtime-streaming.d.ts +17 -0
  236. package/dist/core/model-runtime-streaming.d.ts.map +1 -0
  237. package/dist/core/model-runtime-streaming.js +66 -0
  238. package/dist/core/model-runtime-streaming.js.map +1 -0
  239. package/dist/core/model-runtime-types.d.ts +21 -0
  240. package/dist/core/model-runtime-types.d.ts.map +1 -0
  241. package/dist/core/model-runtime-types.js +2 -0
  242. package/dist/core/model-runtime-types.js.map +1 -0
  243. package/dist/core/model-runtime.d.ts +50 -29
  244. package/dist/core/model-runtime.d.ts.map +1 -1
  245. package/dist/core/model-runtime.js +330 -90
  246. package/dist/core/model-runtime.js.map +1 -1
  247. package/dist/core/models-store.d.ts +2 -10
  248. package/dist/core/models-store.d.ts.map +1 -1
  249. package/dist/core/models-store.js +2 -29
  250. package/dist/core/models-store.js.map +1 -1
  251. package/dist/core/oauth-login.d.ts +24 -0
  252. package/dist/core/oauth-login.d.ts.map +1 -0
  253. package/dist/core/oauth-login.js +87 -0
  254. package/dist/core/oauth-login.js.map +1 -0
  255. package/dist/core/oauth-provider-metadata.d.ts +5 -0
  256. package/dist/core/oauth-provider-metadata.d.ts.map +1 -0
  257. package/dist/core/oauth-provider-metadata.js +19 -0
  258. package/dist/core/oauth-provider-metadata.js.map +1 -0
  259. package/dist/core/provider-composer-internal.d.ts +53 -0
  260. package/dist/core/provider-composer-internal.d.ts.map +1 -0
  261. package/dist/core/provider-composer-internal.js +273 -0
  262. package/dist/core/provider-composer-internal.js.map +1 -0
  263. package/dist/core/provider-composer.d.ts +16 -0
  264. package/dist/core/provider-composer.d.ts.map +1 -0
  265. package/dist/core/provider-composer.js +108 -0
  266. package/dist/core/provider-composer.js.map +1 -0
  267. package/dist/core/remote-catalog-provider.d.ts.map +1 -1
  268. package/dist/core/remote-catalog-provider.js +74 -122
  269. package/dist/core/remote-catalog-provider.js.map +1 -1
  270. package/dist/core/resolve-config-value.d.ts +8 -7
  271. package/dist/core/resolve-config-value.d.ts.map +1 -1
  272. package/dist/core/resolve-config-value.js +26 -23
  273. package/dist/core/resolve-config-value.js.map +1 -1
  274. package/dist/core/runtime-credentials.d.ts +16 -0
  275. package/dist/core/runtime-credentials.d.ts.map +1 -0
  276. package/dist/core/runtime-credentials.js +42 -0
  277. package/dist/core/runtime-credentials.js.map +1 -0
  278. package/dist/core/sdk-types.d.ts +1 -7
  279. package/dist/core/sdk-types.d.ts.map +1 -1
  280. package/dist/core/sdk-types.js.map +1 -1
  281. package/dist/core/sdk.d.ts.map +1 -1
  282. package/dist/core/sdk.js +27 -28
  283. package/dist/core/sdk.js.map +1 -1
  284. package/dist/extensions/llama/index.js +1 -1
  285. package/dist/extensions/llama/index.js.map +1 -1
  286. package/dist/extensions/llama/provider.d.ts +2 -4
  287. package/dist/extensions/llama/provider.d.ts.map +1 -1
  288. package/dist/extensions/llama/provider.js +25 -17
  289. package/dist/extensions/llama/provider.js.map +1 -1
  290. package/dist/index.d.ts +2 -3
  291. package/dist/index.d.ts.map +1 -1
  292. package/dist/index.js +3 -4
  293. package/dist/index.js.map +1 -1
  294. package/dist/main-runtime-api-key.d.ts +6 -0
  295. package/dist/main-runtime-api-key.d.ts.map +1 -0
  296. package/dist/main-runtime-api-key.js +6 -0
  297. package/dist/main-runtime-api-key.js.map +1 -0
  298. package/dist/main-session-options.d.ts +2 -2
  299. package/dist/main-session-options.d.ts.map +1 -1
  300. package/dist/main-session-options.js +3 -3
  301. package/dist/main-session-options.js.map +1 -1
  302. package/dist/main.d.ts.map +1 -1
  303. package/dist/main.js +9 -11
  304. package/dist/main.js.map +1 -1
  305. package/dist/modes/interactive/components/chat-session-host-actions.d.ts.map +1 -1
  306. package/dist/modes/interactive/components/chat-session-host-actions.js +2 -14
  307. package/dist/modes/interactive/components/chat-session-host-actions.js.map +1 -1
  308. package/dist/modes/interactive/components/chat-session-host-events.d.ts.map +1 -1
  309. package/dist/modes/interactive/components/chat-session-host-events.js +9 -0
  310. package/dist/modes/interactive/components/chat-session-host-events.js.map +1 -1
  311. package/dist/modes/interactive/components/chat-session-host-runtime.d.ts +25 -0
  312. package/dist/modes/interactive/components/chat-session-host-runtime.d.ts.map +1 -1
  313. package/dist/modes/interactive/components/chat-session-host-runtime.js +53 -0
  314. package/dist/modes/interactive/components/chat-session-host-runtime.js.map +1 -1
  315. package/dist/modes/interactive/components/chat-session-host.d.ts +13 -0
  316. package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
  317. package/dist/modes/interactive/components/chat-session-host.js +22 -1
  318. package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
  319. package/dist/modes/interactive/components/footer.js +1 -1
  320. package/dist/modes/interactive/components/footer.js.map +1 -1
  321. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  322. package/dist/modes/interactive/components/login-dialog.js +1 -3
  323. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  324. package/dist/modes/interactive/components/model-selector.d.ts +4 -3
  325. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  326. package/dist/modes/interactive/components/model-selector.js +56 -73
  327. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  328. package/dist/modes/interactive/components/oauth-selector.d.ts +4 -3
  329. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  330. package/dist/modes/interactive/components/oauth-selector.js +15 -24
  331. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  332. package/dist/modes/interactive/interactive-agent-events.js +10 -4
  333. package/dist/modes/interactive/interactive-agent-events.js.map +1 -1
  334. package/dist/modes/interactive/interactive-auth-login.js +20 -31
  335. package/dist/modes/interactive/interactive-auth-login.js.map +1 -1
  336. package/dist/modes/interactive/interactive-auth-routing.d.ts +1 -1
  337. package/dist/modes/interactive/interactive-auth-routing.d.ts.map +1 -1
  338. package/dist/modes/interactive/interactive-auth-routing.js +21 -47
  339. package/dist/modes/interactive/interactive-auth-routing.js.map +1 -1
  340. package/dist/modes/interactive/interactive-autocomplete.js +3 -3
  341. package/dist/modes/interactive/interactive-autocomplete.js.map +1 -1
  342. package/dist/modes/interactive/interactive-deferred-startup.js +8 -8
  343. package/dist/modes/interactive/interactive-deferred-startup.js.map +1 -1
  344. package/dist/modes/interactive/interactive-extension-runtime.js +2 -1
  345. package/dist/modes/interactive/interactive-extension-runtime.js.map +1 -1
  346. package/dist/modes/interactive/interactive-model-catalog-startup.js +2 -2
  347. package/dist/modes/interactive/interactive-model-catalog-startup.js.map +1 -1
  348. package/dist/modes/interactive/interactive-model-routing.js +11 -11
  349. package/dist/modes/interactive/interactive-model-routing.js.map +1 -1
  350. package/dist/modes/interactive/interactive-render-chat.js +1 -1
  351. package/dist/modes/interactive/interactive-render-chat.js.map +1 -1
  352. package/dist/modes/interactive/interactive-slash-commands.js +2 -2
  353. package/dist/modes/interactive/interactive-slash-commands.js.map +1 -1
  354. package/dist/modes/interactive/interactive-startup.js +1 -1
  355. package/dist/modes/interactive/interactive-startup.js.map +1 -1
  356. package/dist/modes/interactive-engine/isolated-auth.d.ts +2 -2
  357. package/dist/modes/interactive-engine/isolated-auth.d.ts.map +1 -1
  358. package/dist/modes/interactive-engine/isolated-auth.js +4 -3
  359. package/dist/modes/interactive-engine/isolated-auth.js.map +1 -1
  360. package/dist/modes/interactive-engine/isolated-runtime.d.ts.map +1 -1
  361. package/dist/modes/interactive-engine/isolated-runtime.js +4 -3
  362. package/dist/modes/interactive-engine/isolated-runtime.js.map +1 -1
  363. package/dist/modes/interactive-engine/remote-model-catalog.d.ts +0 -1
  364. package/dist/modes/interactive-engine/remote-model-catalog.d.ts.map +1 -1
  365. package/dist/modes/interactive-engine/remote-model-catalog.js +11 -50
  366. package/dist/modes/interactive-engine/remote-model-catalog.js.map +1 -1
  367. package/dist/modes/rpc/rpc-client-api.d.ts +2 -2
  368. package/dist/modes/rpc/rpc-client-api.d.ts.map +1 -1
  369. package/dist/modes/rpc/rpc-client-api.js.map +1 -1
  370. package/dist/modes/rpc/rpc-command-handler.d.ts.map +1 -1
  371. package/dist/modes/rpc/rpc-command-handler.js +26 -19
  372. package/dist/modes/rpc/rpc-command-handler.js.map +1 -1
  373. package/dist/modes/rpc/rpc-oauth-client.d.ts +1 -1
  374. package/dist/modes/rpc/rpc-oauth-client.d.ts.map +1 -1
  375. package/dist/modes/rpc/rpc-oauth-client.js +1 -1
  376. package/dist/modes/rpc/rpc-oauth-client.js.map +1 -1
  377. package/dist/modes/rpc/rpc-oauth-interaction.d.ts +1 -1
  378. package/dist/modes/rpc/rpc-oauth-interaction.d.ts.map +1 -1
  379. package/dist/modes/rpc/rpc-oauth-interaction.js +1 -1
  380. package/dist/modes/rpc/rpc-oauth-interaction.js.map +1 -1
  381. package/dist/modes/rpc/rpc-provider-auth.d.ts +4 -5
  382. package/dist/modes/rpc/rpc-provider-auth.d.ts.map +1 -1
  383. package/dist/modes/rpc/rpc-provider-auth.js +19 -66
  384. package/dist/modes/rpc/rpc-provider-auth.js.map +1 -1
  385. package/dist/modes/rpc/rpc-session-binding.js +1 -1
  386. package/dist/modes/rpc/rpc-session-binding.js.map +1 -1
  387. package/dist/modes/rpc/rpc-types.d.ts +7 -4
  388. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  389. package/dist/modes/rpc/rpc-types.js.map +1 -1
  390. package/dist/package-manager-cli.d.ts.map +1 -1
  391. package/dist/package-manager-cli.js +5 -7
  392. package/dist/package-manager-cli.js.map +1 -1
  393. package/docs/changelog.mdx +1 -1
  394. package/docs/custom-provider.md +1 -1
  395. package/docs/models.md +4 -6
  396. package/docs/providers.md +13 -0
  397. package/docs/quickstart.md +2 -1
  398. package/docs/sdk.md +32 -49
  399. package/docs/tui.md +7 -2
  400. package/docs/workflows.md +4 -4
  401. package/examples/sdk/02-custom-model.ts +8 -10
  402. package/examples/sdk/09-api-keys-and-oauth.ts +17 -23
  403. package/examples/sdk/12-full-control.ts +9 -11
  404. package/examples/sdk/README.md +17 -21
  405. package/npm-shrinkwrap.json +23 -23
  406. package/package.json +2 -2
  407. package/dist/builtin/workflows/skills/impeccable/reference/brand.md +0 -108
  408. package/dist/builtin/workflows/skills/impeccable/reference/codex.md +0 -105
  409. package/dist/builtin/workflows/skills/impeccable/reference/interaction-design.md +0 -189
  410. package/dist/core/model-registry-auth.d.ts +0 -10
  411. package/dist/core/model-registry-auth.d.ts.map +0 -1
  412. package/dist/core/model-registry-auth.js +0 -98
  413. package/dist/core/model-registry-auth.js.map +0 -1
  414. package/dist/core/model-registry-builtins.d.ts +0 -8
  415. package/dist/core/model-registry-builtins.d.ts.map +0 -1
  416. package/dist/core/model-registry-builtins.js +0 -104
  417. package/dist/core/model-registry-builtins.js.map +0 -1
  418. package/dist/core/model-registry-custom-loader.d.ts +0 -3
  419. package/dist/core/model-registry-custom-loader.d.ts.map +0 -1
  420. package/dist/core/model-registry-custom-loader.js +0 -255
  421. package/dist/core/model-registry-custom-loader.js.map +0 -1
  422. package/dist/core/model-registry-dynamic.d.ts +0 -7
  423. package/dist/core/model-registry-dynamic.d.ts.map +0 -1
  424. package/dist/core/model-registry-dynamic.js +0 -189
  425. package/dist/core/model-registry-dynamic.js.map +0 -1
  426. package/dist/core/model-registry-extension-refresh.d.ts +0 -23
  427. package/dist/core/model-registry-extension-refresh.d.ts.map +0 -1
  428. package/dist/core/model-registry-extension-refresh.js +0 -32
  429. package/dist/core/model-registry-extension-refresh.js.map +0 -1
  430. package/dist/core/model-registry-loader.d.ts +0 -5
  431. package/dist/core/model-registry-loader.d.ts.map +0 -1
  432. package/dist/core/model-registry-loader.js +0 -29
  433. package/dist/core/model-registry-loader.js.map +0 -1
  434. package/dist/core/model-registry-schemas.d.ts +0 -1399
  435. package/dist/core/model-registry-schemas.d.ts.map +0 -1
  436. package/dist/core/model-registry-schemas.js.map +0 -1
  437. package/dist/core/model-registry-types.d.ts +0 -91
  438. package/dist/core/model-registry-types.d.ts.map +0 -1
  439. package/dist/core/model-registry-types.js +0 -2
  440. package/dist/core/model-registry-types.js.map +0 -1
  441. package/dist/core/model-registry-validation.d.ts +0 -7
  442. package/dist/core/model-registry-validation.d.ts.map +0 -1
  443. package/dist/core/model-registry-validation.js +0 -12
  444. package/dist/core/model-registry-validation.js.map +0 -1
  445. package/dist/core/oauth-compat.d.ts +0 -17
  446. package/dist/core/oauth-compat.d.ts.map +0 -1
  447. package/dist/core/oauth-compat.js +0 -2
  448. package/dist/core/oauth-compat.js.map +0 -1
  449. package/dist/core/oauth-provider-bridge.d.ts +0 -63
  450. package/dist/core/oauth-provider-bridge.d.ts.map +0 -1
  451. package/dist/core/oauth-provider-bridge.js +0 -232
  452. package/dist/core/oauth-provider-bridge.js.map +0 -1
@@ -13,8 +13,11 @@
13
13
  * normalizeIgnoreValue(value)
14
14
  * readCache(cwd) / persistCache(cwd, cache) / resolveCacheCwd(primaryFile, sessionCwd)
15
15
  * bumpEditCount(cache, sessionId, filePath) -> number
16
+ * touchFile(cache, sessionId, filePath)
16
17
  * suppressionNotice(filePath)
17
18
  * filterFindings(findings, content, ext, config)
19
+ * ADVISORY_RULES / isAdvisoryFinding(finding)
20
+ * IMMEDIATE_TIER_RULES / splitFindingsByTier(findings) / perEditTieringActive(config, harness)
18
21
  * matchConfiguredExtension(filePath, extensions)
19
22
  * dedupeAgainstCache(findings, cache, sessionId, filePath)
20
23
  * renderTemplate(findings, filePath, config, opts)
@@ -25,6 +28,7 @@
25
28
  * matchesAnyGlob(filePath, globs)
26
29
  * normalizeScanTargets(primaryTargets, projectCwd)
27
30
  * runHook(deps) -> { exitCode, stdout, audit, reason? }
31
+ * runStopHook(deps) -> { exitCode, stdout, audit, emission? }
28
32
  *
29
33
  * Design notes:
30
34
  * - All errors are swallowed at the runHook seam. The detector throwing must
@@ -42,6 +46,16 @@ import path from 'node:path';
42
46
  import { pathToFileURL, fileURLToPath } from 'node:url';
43
47
  import { extractPlatform, loadContext } from './context.mjs';
44
48
  import { IMPECCABLE_COMMAND } from './lib/provider.mjs';
49
+ // `detector.extensions` (issue #316) is shared with Live's source search, which
50
+ // needs the same answer for `.heex` / `.blade.php` when it hunts for session
51
+ // markers. lib/template-extensions.mjs owns the shape; re-exported here because
52
+ // hook-lib has been the import site for matchConfiguredExtension since #347.
53
+ import {
54
+ matchConfiguredExtension,
55
+ mergeExtensions,
56
+ } from './lib/template-extensions.mjs';
57
+
58
+ export { matchConfiguredExtension };
45
59
 
46
60
  const __filename = fileURLToPath(import.meta.url);
47
61
  const __dirname = path.dirname(__filename);
@@ -70,10 +84,69 @@ export const SENSITIVE_PATH = new RegExp([
70
84
  ].join('|'), 'i');
71
85
 
72
86
  // Hard-skip regex for generated, lock, minified, and build-output paths.
73
- export const GENERATED_PATH = /(?:\.generated\.[a-z]+$|\.d\.ts$|\.min\.[a-z]+$|[/\\]node_modules[/\\]|[/\\](?:dist|build|out|\.next|\.cache|coverage)[/\\]|[/\\]?[^/\\]+\.lock(?:\.json)?$)/i;
87
+ // `generated` is matched as a whole path segment so authored names such as
88
+ // `generated-utils.ts` or `CodeGenerator.tsx` still get scanned.
89
+ export const GENERATED_PATH = /(?:\.generated\.[a-z]+$|\.d\.ts$|\.min\.[a-z]+$|[/\\]node_modules[/\\]|[/\\]generated[/\\]|[/\\](?:dist|build|out|\.next|\.cache|coverage)[/\\]|[/\\]?[^/\\]+\.lock(?:\.json)?$)/i;
74
90
 
75
91
  export const TRUTHY = /^(1|true|yes|on)$/i;
76
92
 
93
+ // ── Two-tier rule surfacing ──────────────────────────────────────────────
94
+ // The per-edit PostToolUse pass surfaces only this "immediate" tier: rules
95
+ // that are mechanical, unambiguous, and worth interrupting an edit for —
96
+ // broken output the user would see (broken images, overflow, clipped
97
+ // popovers, text on the viewport edge), objective contrast/legibility
98
+ // failures, single-property slop that is trivial to fix in place (gradient
99
+ // text, glow shadows), and design-system drift (which compounds with every
100
+ // further edit if left uncorrected). Everything else — copy-cadence rules,
101
+ // palette/typography taste, layout rhythm — is deferred to the Stop-event
102
+ // deep pass (`runStopHook`), which runs the FULL rule set over every file
103
+ // touched this session and surfaces the remainder once.
104
+ //
105
+ // Rationale (measured in the eval harness): the per-edit stream fires
106
+ // overwhelmingly on copy-level rules, and that steady nag stream makes
107
+ // models more conservative, while a single full pass at completion fixes
108
+ // contrast/padding/glow just as reliably. Restore the old full per-edit
109
+ // behavior with `.impeccable/config.json` → `hook: { "perEditRules": "all" }`.
110
+ export const IMMEDIATE_TIER_RULES = new Set([
111
+ // Broken output.
112
+ 'broken-image',
113
+ 'text-overflow',
114
+ 'clipped-overflow-container',
115
+ 'body-text-viewport-edge',
116
+ // Objective contrast / legibility failures.
117
+ 'low-contrast',
118
+ 'gray-on-color',
119
+ 'tiny-text',
120
+ // Single-property mechanical slop, trivial to fix at the edit site.
121
+ 'gradient-text',
122
+ 'dark-glow',
123
+ // Design-system drift compounds if not corrected at edit time.
124
+ 'design-system-font',
125
+ 'design-system-color',
126
+ 'design-system-radius',
127
+ 'design-system-font-size',
128
+ ]);
129
+
130
+ // ── Advisory rules ────────────────────────────────────────────────────────
131
+ // Advisory rules are opt-in noise: the CLI reports them in a separate section
132
+ // and they never count as failures. The design hook skips them entirely by
133
+ // default — in both the per-edit PostToolUse pass and the Stop deep pass — so
134
+ // the agent is never nagged about a taste call a human might make on purpose.
135
+ // A project opts back in with `.impeccable/config.json`:
136
+ // { "detector": { "advisoryRules": "include" } }
137
+ // This set is the hook's own copy of the registry's `advisory: true` rules,
138
+ // mirroring how IMMEDIATE_TIER_RULES lists rule ids inline so the hook stays
139
+ // self-contained and testable without loading the detector. Keep it in sync
140
+ // with the registry (cli/engine/registry/antipatterns.mjs).
141
+ export const ADVISORY_RULES = new Set([
142
+ 'em-dash-overuse',
143
+ ]);
144
+
145
+ export function isAdvisoryFinding(finding) {
146
+ const id = finding && normalizeIgnoreRule(finding.antipattern);
147
+ return Boolean(id && (ADVISORY_RULES.has(id) || finding.advisory === true));
148
+ }
149
+
77
150
  export const DEFAULT_CONFIG = Object.freeze({
78
151
  enabled: true,
79
152
  quiet: false,
@@ -83,7 +156,16 @@ export const DEFAULT_CONFIG = Object.freeze({
83
156
  ignoreFiles: [],
84
157
  ignoreValues: [],
85
158
  extensions: [],
86
- limits: { maxFindings: 5, maxChars: 8000 },
159
+ perEditRules: 'immediate',
160
+ // Advisory rules are skipped unless a project sets detector.advisoryRules to
161
+ // "include". See ADVISORY_RULES above.
162
+ advisoryRules: 'exclude',
163
+ // maxFileBytes: not every generated artifact lives under a path we can
164
+ // recognize. Committed browser bundles and vendored detector copies sit
165
+ // next to source and run 200KB+, while genuinely authored stylesheets in
166
+ // this codebase top out under 90KB. A single file past the ceiling is a
167
+ // bundle, and findings against a bundle are never actionable.
168
+ limits: { maxFindings: 5, maxChars: 8000, maxFileBytes: 131072 },
87
169
  });
88
170
 
89
171
  export const HOOK_LOCAL_IGNORE_PATTERNS = Object.freeze([
@@ -228,6 +310,11 @@ function cloneDefaultConfig() {
228
310
 
229
311
  function applyDetectorConfigSource(config, raw) {
230
312
  if (!raw || typeof raw !== 'object') return config;
313
+ // `detector.advisoryRules: "include"` opts the hook into advisory rules
314
+ // (em-dash overuse, etc.). Any other value keeps the default "exclude".
315
+ if (raw.advisoryRules === 'include' || raw.advisoryRules === 'exclude') {
316
+ config.advisoryRules = raw.advisoryRules;
317
+ }
231
318
  if (raw.designSystem && typeof raw.designSystem === 'object' && !Array.isArray(raw.designSystem)) {
232
319
  config.designSystem = {
233
320
  ...config.designSystem,
@@ -249,49 +336,6 @@ function applyDetectorConfigSource(config, raw) {
249
336
  return config;
250
337
  }
251
338
 
252
- // Extra scanned extensions from `detector.extensions` config. Entries are
253
- // `{ ext, engine }` (engine 'html' | 'text', default 'html' — the common case
254
- // for server-side templates) or bare strings as shorthand. Extensions are
255
- // matched against the end of the filename, not path.extname, so double
256
- // extensions like `.blade.php` and `.html.erb` work (issue #316).
257
- function normalizeExtensionEntries(entries) {
258
- if (!Array.isArray(entries)) return [];
259
- const out = [];
260
- for (const entry of entries) {
261
- const raw = typeof entry === 'string' ? entry : entry?.ext;
262
- if (typeof raw !== 'string') continue;
263
- let ext = raw.trim().toLowerCase();
264
- if (!ext) continue;
265
- if (!ext.startsWith('.')) ext = `.${ext}`;
266
- const engine = (!(typeof entry === 'string') && entry?.engine === 'text') ? 'text' : 'html';
267
- out.push({ ext, engine });
268
- }
269
- return out;
270
- }
271
-
272
- function mergeExtensions(existing, incoming) {
273
- const map = new Map();
274
- for (const entry of normalizeExtensionEntries(existing)) map.set(entry.ext, entry);
275
- for (const entry of normalizeExtensionEntries(incoming)) map.set(entry.ext, entry);
276
- return Array.from(map.values());
277
- }
278
-
279
- export function matchConfiguredExtension(filePath, extensions) {
280
- if (!Array.isArray(extensions) || extensions.length === 0) return null;
281
- const name = path.basename(String(filePath || '')).toLowerCase();
282
- if (!name) return null;
283
- // The longest matching suffix wins, so `.blade.php` beats a broader `.php`
284
- // entry regardless of config order.
285
- let best = null;
286
- for (const entry of normalizeExtensionEntries(extensions)) {
287
- if (name.length > entry.ext.length && name.endsWith(entry.ext)
288
- && (!best || entry.ext.length > best.ext.length)) {
289
- best = entry;
290
- }
291
- }
292
- return best;
293
- }
294
-
295
339
  function applyConfigSource(config, raw) {
296
340
  if (!raw || typeof raw !== 'object') return config;
297
341
  if (Object.prototype.hasOwnProperty.call(raw, 'enabled')) {
@@ -300,6 +344,9 @@ function applyConfigSource(config, raw) {
300
344
  if (Object.prototype.hasOwnProperty.call(raw, 'quiet')) {
301
345
  config.quiet = raw.quiet === true;
302
346
  }
347
+ if (raw.perEditRules === 'all' || raw.perEditRules === 'immediate') {
348
+ config.perEditRules = raw.perEditRules;
349
+ }
303
350
  if (typeof raw.auditLog === 'string' && raw.auditLog.trim()) {
304
351
  config.auditLog = raw.auditLog.trim();
305
352
  }
@@ -308,6 +355,7 @@ function applyConfigSource(config, raw) {
308
355
  config.limits = {
309
356
  maxFindings: numberOr(raw.limits.maxFindings, config.limits.maxFindings),
310
357
  maxChars: numberOr(raw.limits.maxChars, config.limits.maxChars),
358
+ maxFileBytes: numberOr(raw.limits.maxFileBytes, config.limits.maxFileBytes),
311
359
  };
312
360
  }
313
361
  return config;
@@ -495,12 +543,15 @@ export function normalizeIgnoreValueEntries(entries) {
495
543
  ...(Array.isArray(entry.files) ? entry.files.filter(v => typeof v === 'string' && v.trim()).map(v => v.trim()) : []),
496
544
  ]);
497
545
  if (files.length > 0) normalized.files = files;
498
- if (typeof entry.reason === 'string' && entry.reason.trim()) {
499
- normalized.reason = entry.reason.trim();
500
- }
546
+ // Key order is rule, value, files, createdAt, reason and must stay that way:
547
+ // normalizing runs on every write, so emitting a different order than the one
548
+ // already on disk rewrites every untouched entry and churns the diff.
501
549
  if (typeof entry.createdAt === 'string' && entry.createdAt.trim()) {
502
550
  normalized.createdAt = entry.createdAt.trim();
503
551
  }
552
+ if (typeof entry.reason === 'string' && entry.reason.trim()) {
553
+ normalized.reason = entry.reason.trim();
554
+ }
504
555
  out.push(normalized);
505
556
  }
506
557
  return out;
@@ -518,7 +569,9 @@ function mergeIgnoreValues(existing, incoming) {
518
569
  }
519
570
 
520
571
  function ignoreValueFilesKey(files) {
521
- return Array.isArray(files) && files.length > 0 ? files.join('\x1f') : '';
572
+ // Sort before joining: a scope is a set, so an entry already on disk in another
573
+ // order must compare equal rather than dedup as two distinct entries.
574
+ return Array.isArray(files) && files.length > 0 ? [...files].sort().join('\x1f') : '';
522
575
  }
523
576
 
524
577
  export function readCache(cwd) {
@@ -654,6 +707,14 @@ export function bumpEditCount(cache, sessionId, filePath) {
654
707
  return fileEntry.editCount;
655
708
  }
656
709
 
710
+ // Record that a file was scanned this session without bumping its edit count.
711
+ // The Stop deep pass reads the session's file list to know what to re-scan,
712
+ // so a file whose per-edit findings were all deferred still needs an entry.
713
+ export function touchFile(cache, sessionId, filePath) {
714
+ ensureFile(cache, sessionId, filePath);
715
+ ensureSession(cache, sessionId).updatedAt = Date.now();
716
+ }
717
+
657
718
  export function suppressionNotice(filePath) {
658
719
  return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run ${IMPECCABLE_COMMAND} audit to revisit.`;
659
720
  }
@@ -716,14 +777,43 @@ export function filterFindings(findings, _content, _ext, config) {
716
777
  if (!Array.isArray(findings) || findings.length === 0) return [];
717
778
  const ignoreRules = new Set((config.ignoreRules || []).map((rule) => normalizeIgnoreRule(rule)));
718
779
  const ignoreValues = normalizeIgnoreValueEntries(config.ignoreValues || []);
780
+ // Advisory rules are skipped by default so the hook never nags about them;
781
+ // a project opts in with detector.advisoryRules: "include".
782
+ const includeAdvisory = (config?.advisoryRules || DEFAULT_CONFIG.advisoryRules) === 'include';
719
783
  return findings.filter((f) => {
720
784
  if (!f || typeof f !== 'object') return false;
785
+ if (!includeAdvisory && isAdvisoryFinding(f)) return false;
721
786
  if (ignoreRules.has(normalizeIgnoreRule(f.antipattern))) return false;
722
787
  if (isIgnoredFindingValue(f, ignoreValues)) return false;
723
788
  return true;
724
789
  });
725
790
  }
726
791
 
792
+ // Split filtered findings into the per-edit "immediate" tier and the tier
793
+ // deferred to the Stop deep pass. See IMMEDIATE_TIER_RULES for the tiering
794
+ // rationale.
795
+ export function splitFindingsByTier(findings) {
796
+ const immediate = [];
797
+ const deferred = [];
798
+ for (const f of Array.isArray(findings) ? findings : []) {
799
+ if (f && IMMEDIATE_TIER_RULES.has(normalizeIgnoreRule(f.antipattern))) {
800
+ immediate.push(f);
801
+ } else {
802
+ deferred.push(f);
803
+ }
804
+ }
805
+ return { immediate, deferred };
806
+ }
807
+
808
+ // Whether the per-edit pass for this harness should defer non-immediate
809
+ // findings to a Stop deep pass. Only Claude Code and Codex dispatch our Stop
810
+ // hook; GitHub Copilot has no deep pass wired, so deferring for it would
811
+ // silently drop the non-immediate rules entirely.
812
+ export function perEditTieringActive(config, harness) {
813
+ if (harness === 'github') return false;
814
+ return (config?.perEditRules || DEFAULT_CONFIG.perEditRules) !== 'all';
815
+ }
816
+
727
817
  function isIgnoredFindingValue(finding, ignoreValues) {
728
818
  if (!Array.isArray(ignoreValues) || ignoreValues.length === 0) return false;
729
819
  const rule = normalizeIgnoreRule(finding.antipattern);
@@ -762,6 +852,7 @@ export function extractFindingIgnoreValue(finding) {
762
852
  'design-system-font',
763
853
  'design-system-color',
764
854
  'design-system-radius',
855
+ 'design-system-font-size',
765
856
  ]);
766
857
  if (!directValueRules.has(rule)) return '';
767
858
  return normalizeIgnoreValue(extractFindingIgnoreValueRaw(finding, rule));
@@ -838,11 +929,20 @@ export function dedupeAgainstCache(findings, cache, sessionId, filePath) {
838
929
  return fresh;
839
930
  }
840
931
 
932
+ // Sync the remembered set to the findings present in the scan just performed.
933
+ //
934
+ // This replaces rather than accumulates, and that is the whole point. An
935
+ // append-only set made the hook lie twice over: the pending ack counted
936
+ // history instead of the live scan, so it kept naming findings the agent had
937
+ // already fixed, and a finding that was fixed and later reintroduced was
938
+ // deduped against a stale memory and never re-reported. Forgetting what is no
939
+ // longer there is what lets the count shrink and a regression fire again.
940
+ //
941
+ // Callers must pass the complete current finding set, not just the fresh ones.
841
942
  export function rememberFindings(cache, sessionId, filePath, findings) {
842
943
  const fileEntry = ensureFile(cache, sessionId, filePath);
843
- const known = new Set(fileEntry.findings || []);
844
- for (const f of findings) known.add(findingCacheKey(f));
845
- fileEntry.findings = Array.from(known);
944
+ const keys = new Set((findings || []).map(f => findingCacheKey(f)));
945
+ fileEntry.findings = Array.from(keys);
846
946
  ensureSession(cache, sessionId).updatedAt = Date.now();
847
947
  }
848
948
 
@@ -1430,16 +1530,17 @@ export function appendDesignSystemNote(text, scanOptions) {
1430
1530
  // raw envelope. Asking the model to surface the resolution in its
1431
1531
  // reply is the cheapest way to make the feedback loop visible.
1432
1532
  function directiveFooter(display, opts = {}) {
1433
- const ignoreFileCommand = `${IMPECCABLE_COMMAND} hooks ignore-file ${quoteCommandArg(display)}`;
1434
- const fileIgnoreGuidance = opts.grouped
1435
- ? `run \`${IMPECCABLE_COMMAND} hooks ignore-file <path>\` for the specific file`
1436
- : `run \`${ignoreFileCommand}\``;
1533
+ // Offer the rule-scoped-to-file form first. `ignore-file` silences every rule
1534
+ // for the path forever, which is far more than one noisy rule on a real UI
1535
+ // surface justifies, and it was previously the only option named here.
1536
+ const target = opts.grouped ? '<path>' : quoteCommandArg(display);
1537
+ const fileIgnoreGuidance = `run \`${IMPECCABLE_COMMAND} hooks ignore-value <id> "*" --file ${target}\` to scope just that rule to the file, or \`${IMPECCABLE_COMMAND} hooks ignore-file ${target}\` only when the whole file is out of scope for design review (a fixture, a generated artifact, a deliberate demo)`;
1437
1538
  return [
1438
1539
  'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.',
1439
1540
  '',
1440
1541
  'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.',
1441
1542
  '',
1442
- `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable <rule>\` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`${IMPECCABLE_COMMAND} hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`${IMPECCABLE_COMMAND} hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run ${IMPECCABLE_COMMAND} audit for the full pass.`,
1543
+ `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable <rule>\` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`${IMPECCABLE_COMMAND} hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For a finding whose line shows no exact ignore-value command, such as \`side-tab\`, ${fileIgnoreGuidance}; use \`${IMPECCABLE_COMMAND} hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run ${IMPECCABLE_COMMAND} audit for the full pass.`,
1443
1544
  ].join('\n');
1444
1545
  }
1445
1546
 
@@ -1510,15 +1611,20 @@ export async function runHook({ stdinJson, env = {}, cwd = process.cwd(), now =
1510
1611
  return result({ skipped: 'detector-missing', durationMs: Date.now() - started });
1511
1612
  }
1512
1613
  const scanOptions = designSystemOptions(config, det, projectCwd);
1614
+ const tiered = perEditTieringActive(config, harness);
1513
1615
 
1514
1616
  let pendingWinner = null;
1515
1617
  let cleanWinner = null;
1516
1618
  const freshGroups = [];
1517
1619
  let suppressionWinner = null;
1620
+ let cleanAckDeduped = false;
1621
+ let skippedBytes = 0;
1622
+ const quietMode = truthy(env.IMPECCABLE_HOOK_QUIET) || config.quiet === true;
1518
1623
  let detectorThrewAny = false;
1519
1624
  let lastSkip = 'no-scannable-file';
1520
1625
  let suppressedHit = false;
1521
1626
  let cacheDirty = false;
1627
+ let deferredTotal = 0;
1522
1628
 
1523
1629
  for (const filePath of targetFiles) {
1524
1630
  audit.file = filePath;
@@ -1550,6 +1656,17 @@ export async function runHook({ stdinJson, env = {}, cwd = process.cwd(), now =
1550
1656
  continue;
1551
1657
  }
1552
1658
 
1659
+ const maxFileBytes = config.limits?.maxFileBytes ?? DEFAULT_CONFIG.limits.maxFileBytes;
1660
+ if (maxFileBytes > 0) {
1661
+ let size = 0;
1662
+ try { size = fs.statSync(filePath).size; } catch { size = 0; }
1663
+ if (size > maxFileBytes) {
1664
+ skippedBytes = size;
1665
+ lastSkip = 'too-large';
1666
+ continue;
1667
+ }
1668
+ }
1669
+
1553
1670
  if (primaryFileSet.has(filePath)) {
1554
1671
  const editCount = bumpEditCount(cache, sessionId, filePath);
1555
1672
  cacheDirty = true;
@@ -1579,36 +1696,75 @@ export async function runHook({ stdinJson, env = {}, cwd = process.cwd(), now =
1579
1696
  }
1580
1697
 
1581
1698
  const filtered = filterFindings(findings || [], content, ext, config);
1582
- const fresh = dedupeAgainstCache(filtered, cache, sessionId, filePath);
1699
+ // Per-edit only surfaces the immediate tier; the rest waits for the
1700
+ // Stop deep pass. The file is still marked touched so the deep pass
1701
+ // knows to re-scan it.
1702
+ const { immediate, deferred } = tiered
1703
+ ? splitFindingsByTier(filtered)
1704
+ : { immediate: filtered, deferred: [] };
1705
+ if (deferred.length > 0) {
1706
+ touchFile(cache, sessionId, filePath);
1707
+ cacheDirty = true;
1708
+ deferredTotal += deferred.length;
1709
+ }
1710
+ const fresh = dedupeAgainstCache(immediate, cache, sessionId, filePath);
1583
1711
  audit.findings = (findings || []).length;
1584
1712
  audit.freshFindings = fresh.length;
1713
+ if (deferredTotal > 0) audit.deferred = deferredTotal;
1585
1714
 
1586
- if (fresh.length > 0) {
1587
- rememberFindings(cache, sessionId, filePath, fresh);
1588
- cacheDirty = true;
1589
- freshGroups.push({ filePath, findings: fresh });
1715
+ // A detector failure tells us nothing about the file, so leave whatever
1716
+ // was remembered alone rather than recording an empty scan as truth.
1717
+ if (detectorThrew) {
1718
+ detectorThrewAny = true;
1590
1719
  continue;
1591
1720
  }
1592
1721
 
1593
- if (detectorThrew) {
1594
- detectorThrewAny = true;
1722
+ // Sync the cache to this scan before deciding what to emit, so fixed
1723
+ // findings stop being remembered and a reintroduced one reads as fresh.
1724
+ // Only the immediate tier is remembered: a deferred finding the per-edit
1725
+ // pass never reported must still read as fresh to the Stop deep pass.
1726
+ rememberFindings(cache, sessionId, filePath, immediate);
1727
+ cacheDirty = true;
1728
+
1729
+ if (fresh.length > 0) {
1730
+ freshGroups.push({ filePath, findings: fresh });
1595
1731
  continue;
1596
1732
  }
1597
1733
 
1598
- if (filtered.length > 0 && !pendingWinner) {
1599
- const known = (ensureFile(cache, sessionId, filePath).findings || []).slice();
1600
- pendingWinner = { filePath, known };
1601
- } else if (filtered.length === 0 && !cleanWinner) {
1602
- cleanWinner = { filePath };
1734
+ if (immediate.length > 0 && !pendingWinner) {
1735
+ // Count the live scan, not the session's history.
1736
+ pendingWinner = { filePath, known: immediate.map(f => findingCacheKey(f)) };
1737
+ } else if (immediate.length === 0 && !cleanWinner) {
1738
+ // The clean ack carries no finding, only the standing steer that a
1739
+ // silent hook is not a verdict on the design. Repeating it on every
1740
+ // clean edit spends context to say nothing, so it fires once per file
1741
+ // per session. The pending ack, which names real unresolved work, is
1742
+ // deliberately left to repeat.
1743
+ //
1744
+ // Quiet mode emits nothing, so it must not consume the ack and leave a
1745
+ // later non-quiet run in this session silent.
1746
+ if (quietMode || !shouldEmitAckForFile(filePath, config)) {
1747
+ cleanWinner = { filePath };
1748
+ } else if (ensureFile(cache, sessionId, filePath).cleanAcked) {
1749
+ // Spent for this file. Remember it for the audit trail, but keep
1750
+ // scanning: another target in this same event may still be owed an
1751
+ // ack, and dropping out here would lose it.
1752
+ cleanAckDeduped = true;
1753
+ } else {
1754
+ ensureFile(cache, sessionId, filePath).cleanAcked = true;
1755
+ cleanWinner = { filePath };
1756
+ cleanAckDeduped = false;
1757
+ }
1603
1758
  }
1604
1759
  }
1605
1760
 
1606
1761
  // Persist only when the write is earned: fresh findings justify creating
1607
- // `.impeccable/` (dedup and suppression need it), and an already-present
1608
- // `.impeccable/` dir marks a project that opted in. A non-UI edit, or a
1609
- // clean UI edit in a project with no Impeccable footprint, must be a
1610
- // no-op on disk (issues #344, #305).
1611
- if (freshGroups.length > 0
1762
+ // `.impeccable/` (dedup and suppression need it), deferred findings do
1763
+ // too (the Stop deep pass needs the touched-file list to surface them),
1764
+ // and an already-present `.impeccable/` dir marks a project that opted
1765
+ // in. A non-UI edit, or a clean UI edit in a project with no Impeccable
1766
+ // footprint, must be a no-op on disk (issues #344, #305).
1767
+ if (freshGroups.length > 0 || deferredTotal > 0
1612
1768
  || (cacheDirty && fs.existsSync(path.join(projectCwd, '.impeccable')))) {
1613
1769
  persistCache(projectCwd, cache);
1614
1770
  }
@@ -1642,7 +1798,7 @@ export async function runHook({ stdinJson, env = {}, cwd = process.cwd(), now =
1642
1798
  return result({ emitted: false, error: 'detector-threw', durationMs: Date.now() - started });
1643
1799
  }
1644
1800
 
1645
- if (truthy(env.IMPECCABLE_HOOK_QUIET) || config.quiet === true) {
1801
+ if (quietMode) {
1646
1802
  return result({ emitted: false, quiet: true, durationMs: Date.now() - started });
1647
1803
  }
1648
1804
 
@@ -1680,7 +1836,7 @@ export async function runHook({ stdinJson, env = {}, cwd = process.cwd(), now =
1680
1836
  };
1681
1837
  }
1682
1838
 
1683
- if (cleanWinner && shouldEmitAckForFile(cleanWinner.filePath, config)) {
1839
+ if (cleanWinner && !cleanAckDeduped && shouldEmitAckForFile(cleanWinner.filePath, config)) {
1684
1840
  const text = appendDesignSystemNote(renderCleanAck(cleanWinner.filePath, { cwd: projectCwd }), scanOptions);
1685
1841
  return {
1686
1842
  exitCode: 0,
@@ -1697,15 +1853,191 @@ export async function runHook({ stdinJson, env = {}, cwd = process.cwd(), now =
1697
1853
  };
1698
1854
  }
1699
1855
 
1700
- if (pendingWinner || cleanWinner) {
1856
+ if (pendingWinner) {
1857
+ return result({ emitted: false, skipped: 'non-ui-ack', durationMs: Date.now() - started });
1858
+ }
1859
+
1860
+ // Distinct from non-ui-ack so the audit log shows noise being suppressed on
1861
+ // purpose rather than a file the hook could not classify.
1862
+ if (cleanWinner) {
1701
1863
  return result({ emitted: false, skipped: 'non-ui-ack', durationMs: Date.now() - started });
1702
1864
  }
1703
1865
 
1866
+ if (cleanAckDeduped) {
1867
+ return result({ emitted: false, skipped: 'clean-ack-deduped', durationMs: Date.now() - started });
1868
+ }
1869
+
1704
1870
  if (suppressedHit) {
1705
1871
  return result({ suppressed: true, emitted: false, durationMs: Date.now() - started });
1706
1872
  }
1707
1873
 
1708
- return result({ skipped: lastSkip, durationMs: Date.now() - started });
1874
+ return result({
1875
+ skipped: lastSkip,
1876
+ ...(lastSkip === 'too-large' ? { bytes: skippedBytes } : {}),
1877
+ durationMs: Date.now() - started,
1878
+ });
1879
+ } catch (err) {
1880
+ return {
1881
+ exitCode: 0,
1882
+ stdout: '',
1883
+ audit: { ...audit, error: String(err && err.message ? err.message : err) },
1884
+ };
1885
+ }
1886
+ }
1887
+
1888
+ // Cap on files the Stop deep pass will scan. The touched-file list is
1889
+ // session-scoped and already capped per edit, but a very long session could
1890
+ // accumulate more than the 30s hook timeout comfortably covers.
1891
+ export const STOP_MAX_FILES = 20;
1892
+
1893
+ /**
1894
+ * Run the Stop-event deep pass: the FULL detector rule set over every UI
1895
+ * file touched this session, surfaced once, deduped against everything the
1896
+ * per-edit hook already reported. Same result contract as runHook():
1897
+ * { exitCode, stdout, audit, emission? }
1898
+ *
1899
+ * Never throws; exits silent (and fast) when the session touched no UI
1900
+ * files. Output uses the Stop hookSpecificOutput channel: additionalContext
1901
+ * is delivered to the model and the conversation continues so it can act.
1902
+ */
1903
+ export async function runStopHook({ stdinJson, env = {}, cwd = process.cwd(), now = Date.now, detector } = {}) {
1904
+ const audit = { ts: new Date(now()).toISOString(), event: 'Stop' };
1905
+ const result = (extra) => ({ exitCode: 0, stdout: '', audit: { ...audit, ...extra } });
1906
+
1907
+ try {
1908
+ // Re-entrancy guard, same as the per-edit pass.
1909
+ if (depthIsSet(env.IMPECCABLE_HOOK_DEPTH) || depthIsSet(env.CLAUDE_HOOK_DEPTH)) {
1910
+ return result({ reentrant: true, durationMs: 0 });
1911
+ }
1912
+ if (truthy(env.IMPECCABLE_HOOK_DISABLED)) {
1913
+ return result({ skipped: 'env-disabled', durationMs: 0 });
1914
+ }
1915
+
1916
+ const started = Date.now();
1917
+
1918
+ let event;
1919
+ try {
1920
+ event = typeof stdinJson === 'string' ? JSON.parse(stdinJson) : stdinJson;
1921
+ } catch {
1922
+ return result({ skipped: 'stdin-malformed', durationMs: Date.now() - started });
1923
+ }
1924
+ if (!event || typeof event !== 'object') {
1925
+ return result({ skipped: 'stdin-empty', durationMs: Date.now() - started });
1926
+ }
1927
+
1928
+ // Claude Code's Stop-hook contract: `stop_hook_active` is true when this
1929
+ // hook is being re-invoked only because a prior invocation kept the turn
1930
+ // alive (here, via hookSpecificOutput.additionalContext). Re-scanning and
1931
+ // re-blocking now would loop until Claude Code's consecutive-block cap
1932
+ // force-ends the turn (issue #400). The prior fire already surfaced the
1933
+ // findings; whether to act on them is the agent's call. Exit fast with no
1934
+ // output before any scan. Only Claude Code sends this field; other
1935
+ // harnesses omit it, so the strict `=== true` is a no-op for them. This
1936
+ // guard makes the loop impossible regardless of the finding cache key's
1937
+ // line-number sensitivity (out of scope here; see findingCacheKey).
1938
+ if (event.stop_hook_active === true) {
1939
+ return result({ skipped: 'stop-hook-active', durationMs: Date.now() - started });
1940
+ }
1941
+
1942
+ const harness = resolveHarness(env, event);
1943
+ audit.harness = harness;
1944
+
1945
+ // A Stop event carries no file, so the session cwd is the project.
1946
+ // Umbrella-dir launches keyed their per-edit cache to the edited file's
1947
+ // project root (resolveCacheCwd); those sessions no-op here rather than
1948
+ // guessing which child project the session was about.
1949
+ const projectCwd = path.resolve(event.cwd || cwd);
1950
+ audit.cwd = projectCwd;
1951
+ const sessionId = event.session_id || 'unknown';
1952
+ audit.session = sessionId;
1953
+
1954
+ const config = readConfig(projectCwd);
1955
+ if (config.enabled === false) {
1956
+ return result({ skipped: 'config-disabled', durationMs: Date.now() - started });
1957
+ }
1958
+
1959
+ const cache = readCache(projectCwd);
1960
+ const touched = Object.keys(cache.sessions?.[sessionId]?.files || {});
1961
+ if (touched.length === 0) {
1962
+ return result({ skipped: 'no-touched-files', durationMs: Date.now() - started });
1963
+ }
1964
+
1965
+ const platform = resolveProjectPlatform(projectCwd);
1966
+ if (isNativePlatform(platform)) {
1967
+ return result({ skipped: 'native-platform', platform, durationMs: Date.now() - started });
1968
+ }
1969
+
1970
+ const det = detector || await loadDetector();
1971
+ if (!det || typeof det.detectText !== 'function') {
1972
+ return result({ skipped: 'detector-missing', durationMs: Date.now() - started });
1973
+ }
1974
+ const scanOptions = designSystemOptions(config, det, projectCwd);
1975
+
1976
+ const freshGroups = [];
1977
+ let scanned = 0;
1978
+ for (const filePath of touched) {
1979
+ if (scanned >= STOP_MAX_FILES) break;
1980
+ if (hasPathTraversal(filePath) || SENSITIVE_PATH.test(filePath)) continue;
1981
+ if (GENERATED_PATH.test(filePath)) continue;
1982
+ const ext = path.extname(filePath).toLowerCase();
1983
+ const configuredExt = matchConfiguredExtension(filePath, config.extensions);
1984
+ if (!ALLOWED_EXTS.has(ext) && !configuredExt) continue;
1985
+ const relForMatch = relativize(filePath, projectCwd);
1986
+ if (matchesAnyGlob(relForMatch, config.ignoreFiles) || matchesAnyGlob(filePath, config.ignoreFiles)) continue;
1987
+ if (!fs.existsSync(filePath)) continue;
1988
+
1989
+ scanned += 1;
1990
+ let content = '';
1991
+ try { content = fs.readFileSync(filePath, 'utf-8'); } catch { continue; }
1992
+
1993
+ let findings;
1994
+ const useHtmlEngine = configuredExt
1995
+ ? configuredExt.engine === 'html'
1996
+ : (ext === '.html' || ext === '.htm');
1997
+
1998
+ if (useHtmlEngine && typeof det.detectHtml === 'function') {
1999
+ try { findings = await det.detectHtml(filePath, scanOptions); } catch { findings = []; }
2000
+ } else {
2001
+ try { findings = await det.detectText(content, filePath, scanOptions); } catch { findings = []; }
2002
+ }
2003
+
2004
+ // Full rule set: no tier split here. Config/inline ignores still apply,
2005
+ // and the session dedupe drops everything the per-edit pass (or an
2006
+ // earlier Stop pass) already surfaced.
2007
+ const filtered = filterFindings(findings || [], content, ext, config);
2008
+ const fresh = dedupeAgainstCache(filtered, cache, sessionId, filePath);
2009
+ if (fresh.length > 0) {
2010
+ rememberFindings(cache, sessionId, filePath, fresh);
2011
+ freshGroups.push({ filePath, findings: fresh });
2012
+ }
2013
+ }
2014
+ audit.scannedFiles = scanned;
2015
+
2016
+ if (freshGroups.length === 0) {
2017
+ return result({ emitted: false, skipped: 'stop-clean', durationMs: Date.now() - started });
2018
+ }
2019
+
2020
+ // Fresh findings earn the cache write so the next Stop fire is silent
2021
+ // unless new issues appear.
2022
+ persistCache(projectCwd, cache);
2023
+
2024
+ const text = appendDesignSystemNote(renderGroupedTemplate(freshGroups, config, { cwd: projectCwd }), scanOptions);
2025
+ return {
2026
+ exitCode: 0,
2027
+ stdout: payload(text, 'Stop', harness),
2028
+ emission: {
2029
+ kind: 'stop-deep-pass',
2030
+ groups: freshGroups,
2031
+ },
2032
+ audit: {
2033
+ ...audit,
2034
+ emitted: true,
2035
+ freshFiles: freshGroups.length,
2036
+ freshFindings: freshGroups.reduce((sum, group) => sum + group.findings.length, 0),
2037
+ chars: text.length,
2038
+ durationMs: Date.now() - started,
2039
+ },
2040
+ };
1709
2041
  } catch (err) {
1710
2042
  return {
1711
2043
  exitCode: 0,