@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.
- package/CHANGELOG.md +38 -0
- package/README.md +3 -5
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/mcp/sampling-handler.ts +1 -5
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +1 -0
- package/dist/builtin/workflows/README.md +1 -1
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/skills/impeccable/SKILL.md +33 -129
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_asset_producer.toml +11 -3
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_documenter.toml +26 -0
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_finish_reviewer.toml +35 -0
- package/dist/builtin/workflows/skills/impeccable/reference/android.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/animate.md +72 -189
- package/dist/builtin/workflows/skills/impeccable/reference/audit.md +10 -9
- package/dist/builtin/workflows/skills/impeccable/reference/audit.native.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/bolder.md +19 -108
- package/dist/builtin/workflows/skills/impeccable/reference/clarify.md +59 -253
- package/dist/builtin/workflows/skills/impeccable/reference/colorize.md +51 -222
- package/dist/builtin/workflows/skills/impeccable/reference/craft-floor.md +45 -0
- package/dist/builtin/workflows/skills/impeccable/reference/craft.md +3 -121
- package/dist/builtin/workflows/skills/impeccable/reference/critique.md +28 -20
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/asset-producer.md +97 -0
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/documenter.md +23 -0
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/finish-reviewer.md +32 -0
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/manual-edit-applier.md +92 -0
- package/dist/builtin/workflows/skills/impeccable/reference/delight.md +47 -279
- package/dist/builtin/workflows/skills/impeccable/reference/distill.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/doctor.md +53 -0
- package/dist/builtin/workflows/skills/impeccable/reference/document.md +60 -73
- package/dist/builtin/workflows/skills/impeccable/reference/harden.md +1 -12
- package/dist/builtin/workflows/skills/impeccable/reference/hooks.md +17 -4
- package/dist/builtin/workflows/skills/impeccable/reference/init.md +64 -163
- package/dist/builtin/workflows/skills/impeccable/reference/ios.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/layout.md +52 -153
- package/dist/builtin/workflows/skills/impeccable/reference/live.md +44 -32
- package/dist/builtin/workflows/skills/impeccable/reference/new-work.md +105 -0
- package/dist/builtin/workflows/skills/impeccable/reference/{product.md → operate.md} +6 -5
- package/dist/builtin/workflows/skills/impeccable/reference/optimize.md +4 -4
- package/dist/builtin/workflows/skills/impeccable/reference/overdrive.md +1 -4
- package/dist/builtin/workflows/skills/impeccable/reference/polish.md +68 -212
- package/dist/builtin/workflows/skills/impeccable/reference/quieter.md +3 -3
- package/dist/builtin/workflows/skills/impeccable/reference/routing.md +18 -0
- package/dist/builtin/workflows/skills/impeccable/reference/shape.md +38 -144
- package/dist/builtin/workflows/skills/impeccable/reference/typeset.md +48 -269
- package/dist/builtin/workflows/skills/impeccable/reference/visualize.md +38 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/command-metadata.json +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/concept-seed.mjs +584 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +117 -9
- package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +486 -59
- package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +16 -45
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +96 -10
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +143 -26
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/design-system.mjs +181 -12
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +3187 -182
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +102 -7
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +282 -70
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +183 -15
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +22 -7
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/findings.mjs +7 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +16 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +155 -42
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +2988 -155
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/constants.mjs +11 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/doctor.mjs +336 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/generate-image.mjs +232 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/hook-admin.mjs +90 -10
- package/dist/builtin/workflows/skills/impeccable/scripts/hook-lib.mjs +410 -78
- package/dist/builtin/workflows/skills/impeccable/scripts/hook.mjs +25 -8
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/artifact-schema.mjs +93 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/composition-catalog.mjs +165 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/concept-catalog.mjs +329 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-config.mjs +20 -5
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-paths.mjs +16 -8
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/provider.mjs +1 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness-deep.mjs +455 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness-notice.mjs +169 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness.mjs +457 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/surface-briefs.mjs +151 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/target-slug.mjs +33 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/template-extensions.mjs +146 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/completion.mjs +10 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/live/event-validation.mjs +15 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/generation-preflight.mjs +149 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/poll-lanes.mjs +14 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/session-store.mjs +109 -32
- package/dist/builtin/workflows/skills/impeccable/scripts/live/source-lock.mjs +105 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/source-search.mjs +105 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/sveltekit-adapter.mjs +8 -6
- package/dist/builtin/workflows/skills/impeccable/scripts/live/tanstack-adapter.mjs +280 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +208 -68
- package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +563 -112
- package/dist/builtin/workflows/skills/impeccable/scripts/live-commit-manual-edits.mjs +3 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +198 -13
- package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +24 -6
- package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edit-evidence.mjs +6 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +44 -14
- package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +398 -33
- package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +9 -5
- package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +81 -67
- package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +7 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/palette.mjs +76 -81
- package/dist/builtin/workflows/skills/impeccable/scripts/pin.mjs +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/serve-question.mjs +890 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/surface-brief.mjs +74 -0
- package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +1 -24
- package/dist/builtin/workflows/src/extension/index.bundle.mjs +232 -50
- package/dist/builtin/workflows/src/extension/workflow-model-catalog.ts +26 -0
- package/dist/builtin/workflows/src/extension/workflow-prompts.ts +3 -2
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-control.ts +23 -9
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-replay.ts +1 -0
- package/dist/builtin/workflows/src/runs/foreground/postmortem-stage-chat.ts +20 -5
- package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +10 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-delivery-activity.ts +86 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +6 -2
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +24 -18
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-send-user-message.ts +8 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-session-options.ts +3 -3
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-session.ts +2 -2
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-types.ts +16 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view-delivery-activity.ts +121 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view-live-events.ts +42 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view-state.ts +16 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view-types.ts +4 -0
- package/dist/cli/list-models.d.ts +2 -2
- package/dist/cli/list-models.d.ts.map +1 -1
- package/dist/cli/list-models.js +5 -5
- package/dist/cli/list-models.js.map +1 -1
- package/dist/core/agent-session-accessors.js +1 -1
- package/dist/core/agent-session-accessors.js.map +1 -1
- package/dist/core/agent-session-auto-compaction.d.ts.map +1 -1
- package/dist/core/agent-session-auto-compaction.js +2 -5
- package/dist/core/agent-session-auto-compaction.js.map +1 -1
- package/dist/core/agent-session-compaction.js +1 -1
- package/dist/core/agent-session-compaction.js.map +1 -1
- package/dist/core/agent-session-extension-bindings.js +5 -5
- package/dist/core/agent-session-extension-bindings.js.map +1 -1
- package/dist/core/agent-session-methods.d.ts +4 -4
- package/dist/core/agent-session-methods.d.ts.map +1 -1
- package/dist/core/agent-session-methods.js.map +1 -1
- package/dist/core/agent-session-models.d.ts +1 -1
- package/dist/core/agent-session-models.d.ts.map +1 -1
- package/dist/core/agent-session-models.js +18 -14
- package/dist/core/agent-session-models.js.map +1 -1
- package/dist/core/agent-session-post-tool-compaction.d.ts.map +1 -1
- package/dist/core/agent-session-post-tool-compaction.js +2 -4
- package/dist/core/agent-session-post-tool-compaction.js.map +1 -1
- package/dist/core/agent-session-prompt.d.ts.map +1 -1
- package/dist/core/agent-session-prompt.js +3 -7
- package/dist/core/agent-session-prompt.js.map +1 -1
- package/dist/core/agent-session-retry.js +1 -1
- package/dist/core/agent-session-retry.js.map +1 -1
- package/dist/core/agent-session-runtime-auth.d.ts +3 -3
- package/dist/core/agent-session-runtime-auth.d.ts.map +1 -1
- package/dist/core/agent-session-runtime-auth.js +4 -14
- package/dist/core/agent-session-runtime-auth.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts +1 -1
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +3 -3
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +3 -8
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +16 -23
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session-tool-registry.d.ts.map +1 -1
- package/dist/core/agent-session-tool-registry.js +2 -1
- package/dist/core/agent-session-tool-registry.js.map +1 -1
- package/dist/core/agent-session-types.d.ts +2 -2
- package/dist/core/agent-session-types.d.ts.map +1 -1
- package/dist/core/agent-session-types.js.map +1 -1
- package/dist/core/agent-session.d.ts +2 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +1 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage-backends.d.ts +1 -0
- package/dist/core/auth-storage-backends.d.ts.map +1 -1
- package/dist/core/auth-storage-backends.js +18 -4
- package/dist/core/auth-storage-backends.js.map +1 -1
- package/dist/core/auth-storage.d.ts +12 -139
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +68 -374
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/copilot-env-routing.d.ts +36 -0
- package/dist/core/copilot-env-routing.d.ts.map +1 -0
- package/dist/core/copilot-env-routing.js +94 -0
- package/dist/core/copilot-env-routing.js.map +1 -0
- package/dist/core/extensions/loader-virtual-modules.d.ts.map +1 -1
- package/dist/core/extensions/loader-virtual-modules.js +10 -2
- package/dist/core/extensions/loader-virtual-modules.js.map +1 -1
- package/dist/core/extensions/provider-types.d.ts +0 -40
- package/dist/core/extensions/provider-types.d.ts.map +1 -1
- package/dist/core/extensions/provider-types.js.map +1 -1
- package/dist/core/fallback-models.d.ts +4 -4
- package/dist/core/fallback-models.d.ts.map +1 -1
- package/dist/core/fallback-models.js +3 -3
- package/dist/core/fallback-models.js.map +1 -1
- package/dist/core/model-config.d.ts +542 -0
- package/dist/core/model-config.d.ts.map +1 -0
- package/dist/core/{model-registry-schemas.js → model-config.js} +85 -34
- package/dist/core/model-config.js.map +1 -0
- package/dist/core/model-registry.d.ts +25 -85
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +65 -388
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver-cli.d.ts +2 -2
- package/dist/core/model-resolver-cli.d.ts.map +1 -1
- package/dist/core/model-resolver-cli.js +2 -2
- package/dist/core/model-resolver-cli.js.map +1 -1
- package/dist/core/model-resolver-initial.d.ts +4 -4
- package/dist/core/model-resolver-initial.d.ts.map +1 -1
- package/dist/core/model-resolver-initial.js +17 -17
- package/dist/core/model-resolver-initial.js.map +1 -1
- package/dist/core/model-resolver-scope.d.ts +3 -3
- package/dist/core/model-resolver-scope.d.ts.map +1 -1
- package/dist/core/model-resolver-scope.js +4 -4
- package/dist/core/model-resolver-scope.js.map +1 -1
- package/dist/core/model-runtime-auth.d.ts +7 -0
- package/dist/core/model-runtime-auth.d.ts.map +1 -0
- package/dist/core/model-runtime-auth.js +14 -0
- package/dist/core/model-runtime-auth.js.map +1 -0
- package/dist/core/model-runtime-providers.d.ts +6 -0
- package/dist/core/model-runtime-providers.d.ts.map +1 -0
- package/dist/core/model-runtime-providers.js +22 -0
- package/dist/core/model-runtime-providers.js.map +1 -0
- package/dist/core/model-runtime-restoration.d.ts +6 -0
- package/dist/core/model-runtime-restoration.d.ts.map +1 -0
- package/dist/core/model-runtime-restoration.js +21 -0
- package/dist/core/model-runtime-restoration.js.map +1 -0
- package/dist/core/model-runtime-snapshot.d.ts +16 -0
- package/dist/core/model-runtime-snapshot.d.ts.map +1 -0
- package/dist/core/model-runtime-snapshot.js +52 -0
- package/dist/core/model-runtime-snapshot.js.map +1 -0
- package/dist/core/model-runtime-streaming.d.ts +17 -0
- package/dist/core/model-runtime-streaming.d.ts.map +1 -0
- package/dist/core/model-runtime-streaming.js +66 -0
- package/dist/core/model-runtime-streaming.js.map +1 -0
- package/dist/core/model-runtime-types.d.ts +21 -0
- package/dist/core/model-runtime-types.d.ts.map +1 -0
- package/dist/core/model-runtime-types.js +2 -0
- package/dist/core/model-runtime-types.js.map +1 -0
- package/dist/core/model-runtime.d.ts +50 -29
- package/dist/core/model-runtime.d.ts.map +1 -1
- package/dist/core/model-runtime.js +330 -90
- package/dist/core/model-runtime.js.map +1 -1
- package/dist/core/models-store.d.ts +2 -10
- package/dist/core/models-store.d.ts.map +1 -1
- package/dist/core/models-store.js +2 -29
- package/dist/core/models-store.js.map +1 -1
- package/dist/core/oauth-login.d.ts +24 -0
- package/dist/core/oauth-login.d.ts.map +1 -0
- package/dist/core/oauth-login.js +87 -0
- package/dist/core/oauth-login.js.map +1 -0
- package/dist/core/oauth-provider-metadata.d.ts +5 -0
- package/dist/core/oauth-provider-metadata.d.ts.map +1 -0
- package/dist/core/oauth-provider-metadata.js +19 -0
- package/dist/core/oauth-provider-metadata.js.map +1 -0
- package/dist/core/provider-composer-internal.d.ts +53 -0
- package/dist/core/provider-composer-internal.d.ts.map +1 -0
- package/dist/core/provider-composer-internal.js +273 -0
- package/dist/core/provider-composer-internal.js.map +1 -0
- package/dist/core/provider-composer.d.ts +16 -0
- package/dist/core/provider-composer.d.ts.map +1 -0
- package/dist/core/provider-composer.js +108 -0
- package/dist/core/provider-composer.js.map +1 -0
- package/dist/core/remote-catalog-provider.d.ts.map +1 -1
- package/dist/core/remote-catalog-provider.js +74 -122
- package/dist/core/remote-catalog-provider.js.map +1 -1
- package/dist/core/resolve-config-value.d.ts +8 -7
- package/dist/core/resolve-config-value.d.ts.map +1 -1
- package/dist/core/resolve-config-value.js +26 -23
- package/dist/core/resolve-config-value.js.map +1 -1
- package/dist/core/runtime-credentials.d.ts +16 -0
- package/dist/core/runtime-credentials.d.ts.map +1 -0
- package/dist/core/runtime-credentials.js +42 -0
- package/dist/core/runtime-credentials.js.map +1 -0
- package/dist/core/sdk-types.d.ts +1 -7
- package/dist/core/sdk-types.d.ts.map +1 -1
- package/dist/core/sdk-types.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +27 -28
- package/dist/core/sdk.js.map +1 -1
- package/dist/extensions/llama/index.js +1 -1
- package/dist/extensions/llama/index.js.map +1 -1
- package/dist/extensions/llama/provider.d.ts +2 -4
- package/dist/extensions/llama/provider.d.ts.map +1 -1
- package/dist/extensions/llama/provider.js +25 -17
- package/dist/extensions/llama/provider.js.map +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -4
- package/dist/index.js.map +1 -1
- package/dist/main-runtime-api-key.d.ts +6 -0
- package/dist/main-runtime-api-key.d.ts.map +1 -0
- package/dist/main-runtime-api-key.js +6 -0
- package/dist/main-runtime-api-key.js.map +1 -0
- package/dist/main-session-options.d.ts +2 -2
- package/dist/main-session-options.d.ts.map +1 -1
- package/dist/main-session-options.js +3 -3
- package/dist/main-session-options.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +9 -11
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-actions.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-actions.js +2 -14
- package/dist/modes/interactive/components/chat-session-host-actions.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-events.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-events.js +9 -0
- package/dist/modes/interactive/components/chat-session-host-events.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-runtime.d.ts +25 -0
- package/dist/modes/interactive/components/chat-session-host-runtime.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-runtime.js +53 -0
- package/dist/modes/interactive/components/chat-session-host-runtime.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host.d.ts +13 -0
- package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host.js +22 -1
- package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
- package/dist/modes/interactive/components/footer.js +1 -1
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +1 -3
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +4 -3
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +56 -73
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.d.ts +4 -3
- package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.js +15 -24
- package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-agent-events.js +10 -4
- package/dist/modes/interactive/interactive-agent-events.js.map +1 -1
- package/dist/modes/interactive/interactive-auth-login.js +20 -31
- package/dist/modes/interactive/interactive-auth-login.js.map +1 -1
- package/dist/modes/interactive/interactive-auth-routing.d.ts +1 -1
- package/dist/modes/interactive/interactive-auth-routing.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-auth-routing.js +21 -47
- package/dist/modes/interactive/interactive-auth-routing.js.map +1 -1
- package/dist/modes/interactive/interactive-autocomplete.js +3 -3
- package/dist/modes/interactive/interactive-autocomplete.js.map +1 -1
- package/dist/modes/interactive/interactive-deferred-startup.js +8 -8
- package/dist/modes/interactive/interactive-deferred-startup.js.map +1 -1
- package/dist/modes/interactive/interactive-extension-runtime.js +2 -1
- package/dist/modes/interactive/interactive-extension-runtime.js.map +1 -1
- package/dist/modes/interactive/interactive-model-catalog-startup.js +2 -2
- package/dist/modes/interactive/interactive-model-catalog-startup.js.map +1 -1
- package/dist/modes/interactive/interactive-model-routing.js +11 -11
- package/dist/modes/interactive/interactive-model-routing.js.map +1 -1
- package/dist/modes/interactive/interactive-render-chat.js +1 -1
- package/dist/modes/interactive/interactive-render-chat.js.map +1 -1
- package/dist/modes/interactive/interactive-slash-commands.js +2 -2
- package/dist/modes/interactive/interactive-slash-commands.js.map +1 -1
- package/dist/modes/interactive/interactive-startup.js +1 -1
- package/dist/modes/interactive/interactive-startup.js.map +1 -1
- package/dist/modes/interactive-engine/isolated-auth.d.ts +2 -2
- package/dist/modes/interactive-engine/isolated-auth.d.ts.map +1 -1
- package/dist/modes/interactive-engine/isolated-auth.js +4 -3
- package/dist/modes/interactive-engine/isolated-auth.js.map +1 -1
- package/dist/modes/interactive-engine/isolated-runtime.d.ts.map +1 -1
- package/dist/modes/interactive-engine/isolated-runtime.js +4 -3
- package/dist/modes/interactive-engine/isolated-runtime.js.map +1 -1
- package/dist/modes/interactive-engine/remote-model-catalog.d.ts +0 -1
- package/dist/modes/interactive-engine/remote-model-catalog.d.ts.map +1 -1
- package/dist/modes/interactive-engine/remote-model-catalog.js +11 -50
- package/dist/modes/interactive-engine/remote-model-catalog.js.map +1 -1
- package/dist/modes/rpc/rpc-client-api.d.ts +2 -2
- package/dist/modes/rpc/rpc-client-api.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client-api.js.map +1 -1
- package/dist/modes/rpc/rpc-command-handler.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-command-handler.js +26 -19
- package/dist/modes/rpc/rpc-command-handler.js.map +1 -1
- package/dist/modes/rpc/rpc-oauth-client.d.ts +1 -1
- package/dist/modes/rpc/rpc-oauth-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-oauth-client.js +1 -1
- package/dist/modes/rpc/rpc-oauth-client.js.map +1 -1
- package/dist/modes/rpc/rpc-oauth-interaction.d.ts +1 -1
- package/dist/modes/rpc/rpc-oauth-interaction.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-oauth-interaction.js +1 -1
- package/dist/modes/rpc/rpc-oauth-interaction.js.map +1 -1
- package/dist/modes/rpc/rpc-provider-auth.d.ts +4 -5
- package/dist/modes/rpc/rpc-provider-auth.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-provider-auth.js +19 -66
- package/dist/modes/rpc/rpc-provider-auth.js.map +1 -1
- package/dist/modes/rpc/rpc-session-binding.js +1 -1
- package/dist/modes/rpc/rpc-session-binding.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +7 -4
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +5 -7
- package/dist/package-manager-cli.js.map +1 -1
- package/docs/changelog.mdx +1 -1
- package/docs/custom-provider.md +1 -1
- package/docs/models.md +4 -6
- package/docs/providers.md +13 -0
- package/docs/quickstart.md +2 -1
- package/docs/sdk.md +32 -49
- package/docs/tui.md +7 -2
- package/docs/workflows.md +4 -4
- package/examples/sdk/02-custom-model.ts +8 -10
- package/examples/sdk/09-api-keys-and-oauth.ts +17 -23
- package/examples/sdk/12-full-control.ts +9 -11
- package/examples/sdk/README.md +17 -21
- package/npm-shrinkwrap.json +23 -23
- package/package.json +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/brand.md +0 -108
- package/dist/builtin/workflows/skills/impeccable/reference/codex.md +0 -105
- package/dist/builtin/workflows/skills/impeccable/reference/interaction-design.md +0 -189
- package/dist/core/model-registry-auth.d.ts +0 -10
- package/dist/core/model-registry-auth.d.ts.map +0 -1
- package/dist/core/model-registry-auth.js +0 -98
- package/dist/core/model-registry-auth.js.map +0 -1
- package/dist/core/model-registry-builtins.d.ts +0 -8
- package/dist/core/model-registry-builtins.d.ts.map +0 -1
- package/dist/core/model-registry-builtins.js +0 -104
- package/dist/core/model-registry-builtins.js.map +0 -1
- package/dist/core/model-registry-custom-loader.d.ts +0 -3
- package/dist/core/model-registry-custom-loader.d.ts.map +0 -1
- package/dist/core/model-registry-custom-loader.js +0 -255
- package/dist/core/model-registry-custom-loader.js.map +0 -1
- package/dist/core/model-registry-dynamic.d.ts +0 -7
- package/dist/core/model-registry-dynamic.d.ts.map +0 -1
- package/dist/core/model-registry-dynamic.js +0 -189
- package/dist/core/model-registry-dynamic.js.map +0 -1
- package/dist/core/model-registry-extension-refresh.d.ts +0 -23
- package/dist/core/model-registry-extension-refresh.d.ts.map +0 -1
- package/dist/core/model-registry-extension-refresh.js +0 -32
- package/dist/core/model-registry-extension-refresh.js.map +0 -1
- package/dist/core/model-registry-loader.d.ts +0 -5
- package/dist/core/model-registry-loader.d.ts.map +0 -1
- package/dist/core/model-registry-loader.js +0 -29
- package/dist/core/model-registry-loader.js.map +0 -1
- package/dist/core/model-registry-schemas.d.ts +0 -1399
- package/dist/core/model-registry-schemas.d.ts.map +0 -1
- package/dist/core/model-registry-schemas.js.map +0 -1
- package/dist/core/model-registry-types.d.ts +0 -91
- package/dist/core/model-registry-types.d.ts.map +0 -1
- package/dist/core/model-registry-types.js +0 -2
- package/dist/core/model-registry-types.js.map +0 -1
- package/dist/core/model-registry-validation.d.ts +0 -7
- package/dist/core/model-registry-validation.d.ts.map +0 -1
- package/dist/core/model-registry-validation.js +0 -12
- package/dist/core/model-registry-validation.js.map +0 -1
- package/dist/core/oauth-compat.d.ts +0 -17
- package/dist/core/oauth-compat.d.ts.map +0 -1
- package/dist/core/oauth-compat.js +0 -2
- package/dist/core/oauth-compat.js.map +0 -1
- package/dist/core/oauth-provider-bridge.d.ts +0 -63
- package/dist/core/oauth-provider-bridge.d.ts.map +0 -1
- package/dist/core/oauth-provider-bridge.js +0 -232
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
499
|
-
|
|
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
|
-
|
|
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
|
|
844
|
-
|
|
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
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
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
|
-
|
|
1594
|
-
|
|
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 (
|
|
1599
|
-
|
|
1600
|
-
pendingWinner = { filePath, known };
|
|
1601
|
-
} else if (
|
|
1602
|
-
|
|
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),
|
|
1608
|
-
//
|
|
1609
|
-
//
|
|
1610
|
-
//
|
|
1611
|
-
|
|
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 (
|
|
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
|
|
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({
|
|
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,
|