@fusengine/harness 0.1.89 → 0.1.91
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/dist/adapters/claude/index.d.mts +1 -1
- package/dist/adapters/claude/index.mjs +1 -1
- package/dist/adapters/cline/index.mjs +1 -1
- package/dist/adapters/codex/index.d.mts +2 -2
- package/dist/adapters/codex/index.mjs +5 -4
- package/dist/adapters/cursor/index.d.mts +15 -28
- package/dist/adapters/cursor/index.mjs +35 -46
- package/dist/adapters/gemini/index.mjs +1 -1
- package/dist/adapters/hermes/index.d.mts +1 -1
- package/dist/adapters/hermes/index.mjs +1 -1
- package/dist/adapters/kimi/index.d.mts +1 -1
- package/dist/adapters/kimi/index.mjs +1 -1
- package/dist/{claude-5bC8TDAz.mjs → claude-Ckv2_TgP.mjs} +3 -43
- package/dist/cli/bin.mjs +429 -16
- package/dist/cli/index.mjs +1 -1
- package/dist/command-string-CALMTnwN.mjs +42 -0
- package/dist/{evaluate-C1VSI5pb.mjs → evaluate-CMiIqHeH.mjs} +261 -13
- package/dist/{handle-DHhukRdh.mjs → handle-C43gA-Pr.mjs} +737 -150
- package/dist/{hermes-kP6NUQlG.mjs → hermes-B9-p_3IF.mjs} +3 -2
- package/dist/{index-DBgnIZn9.d.mts → index-B8PG82su.d.mts} +1 -1
- package/dist/{index-D4C2-IIc.d.mts → index-SmKYkk2N.d.mts} +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/init/index.d.mts +1 -1
- package/dist/init/index.mjs +1 -1
- package/dist/{kimi-D6X7b8wF.mjs → kimi-G2wcSh5-.mjs} +3 -2
- package/dist/normalize-BjG6unTj.mjs +317 -0
- package/dist/permission-mode-B-SFyR0X.mjs +226 -0
- package/dist/policy/index.d.mts +1 -1
- package/dist/policy/index.mjs +2 -2
- package/dist/prompt/index.d.mts +1 -1
- package/dist/{run-CdIlUtjv.mjs → run-DkrzC0gb.mjs} +1 -1
- package/dist/{run-TAXNRSpD.mjs → run-GHAMqcOn.mjs} +26 -3
- package/dist/runtime/index.d.mts +27 -16
- package/dist/runtime/index.mjs +2 -2
- package/dist/{types-C0-igRKr.d.mts → types-Bh0jEF3_.d.mts} +2 -0
- package/dist/{validate-DZFtAbLP.mjs → validate-KjZ1X9tH.mjs} +1 -1
- package/package.json +3 -2
- package/src/adapters/claude/index.ts +92 -0
- package/src/adapters/cline/index.ts +36 -0
- package/src/adapters/codex/apply-patch.ts +94 -0
- package/src/adapters/codex/index.ts +84 -0
- package/src/adapters/codex/permission-mode.ts +16 -0
- package/src/adapters/cursor/context.ts +46 -0
- package/src/adapters/cursor/events.ts +38 -0
- package/src/adapters/cursor/index.ts +43 -0
- package/src/adapters/cursor/interfaces/types.ts +88 -0
- package/src/adapters/cursor/native-response.ts +158 -0
- package/src/adapters/cursor/normalize.ts +123 -0
- package/src/adapters/cursor/respond.ts +123 -0
- package/src/adapters/gemini/index.ts +36 -0
- package/src/adapters/hermes/index.ts +55 -0
- package/src/adapters/hermes/interfaces/types.ts +26 -0
- package/src/adapters/kimi/index.ts +96 -0
- package/src/adapters/kimi/interfaces/types.ts +55 -0
- package/src/cache/compact.ts +55 -0
- package/src/cache/index.ts +5 -0
- package/src/cache/io.ts +38 -0
- package/src/cache/mcp-response.ts +39 -0
- package/src/cache/mcp-store.ts +70 -0
- package/src/cache/store.ts +93 -0
- package/src/changelog/fetch.ts +79 -0
- package/src/cli/bin.ts +100 -0
- package/src/cli/cursor-event-scanner.ts +175 -0
- package/src/cli/cursor-stdin-reader.ts +65 -0
- package/src/cli/doctor.ts +96 -0
- package/src/cli/hook-io.ts +130 -0
- package/src/cli/hook-sound.ts +49 -0
- package/src/cli/index.ts +1 -0
- package/src/cli/json-primitive-scanner.ts +83 -0
- package/src/cli/run.ts +36 -0
- package/src/cli/scope.ts +31 -0
- package/src/codex-rules/build.ts +63 -0
- package/src/codex-rules/index.ts +8 -0
- package/src/codex-rules/mapping.ts +42 -0
- package/src/codex-rules/rules/absolute-paths.ts +52 -0
- package/src/codex-rules/rules/ask.ts +44 -0
- package/src/codex-rules/rules/chmod-ask.ts +57 -0
- package/src/codex-rules/rules/dd.ts +23 -0
- package/src/codex-rules/rules/diskutil.ts +33 -0
- package/src/codex-rules/rules/mkfs.ts +42 -0
- package/src/codex-rules/rules/privilege.ts +49 -0
- package/src/codex-rules/rules/remote-fetch.ts +31 -0
- package/src/codex-rules/rules/rm-variants.ts +58 -0
- package/src/codex-rules/rules/single-commands.ts +57 -0
- package/src/codex-rules/skip-list.ts +25 -0
- package/src/codex-rules/starlark.ts +46 -0
- package/src/codex-rules/types.ts +36 -0
- package/src/config/dotenv.ts +55 -0
- package/src/config/env.ts +10 -0
- package/src/config/home-dir.ts +36 -0
- package/src/config/index.ts +5 -0
- package/src/config/layout.ts +52 -0
- package/src/config/limits.ts +56 -0
- package/src/config/ttl.ts +30 -0
- package/src/detect/harness.ts +73 -0
- package/src/detect/index.ts +1 -0
- package/src/detect/interfaces/types.ts +18 -0
- package/src/freshness/agent-evidence-record.ts +95 -0
- package/src/freshness/agent-evidence.ts +95 -0
- package/src/freshness/codex-spawn-evidence.ts +91 -0
- package/src/freshness/doc-helpers.ts +90 -0
- package/src/freshness/evidence-harvest-io.ts +72 -0
- package/src/freshness/evidence-harvest.ts +61 -0
- package/src/freshness/explore-tools.ts +95 -0
- package/src/freshness/index.ts +2 -0
- package/src/freshness/query-framework.ts +73 -0
- package/src/freshness/ref-evidence.ts +49 -0
- package/src/freshness/ref-journal.ts +98 -0
- package/src/freshness/trivial-edit-counter.ts +30 -0
- package/src/index.ts +11 -0
- package/src/init/index.ts +2 -0
- package/src/init/run.ts +32 -0
- package/src/init/templates.ts +75 -0
- package/src/memory/gitignore.ts +14 -0
- package/src/memory/index.ts +3 -0
- package/src/memory/registry.ts +39 -0
- package/src/memory/session-roots.ts +99 -0
- package/src/memory/state.ts +58 -0
- package/src/policy/apex-authorization.ts +100 -0
- package/src/policy/apex-gates.ts +91 -0
- package/src/policy/apex-target-steps.ts +73 -0
- package/src/policy/apex-target.ts +49 -0
- package/src/policy/apex-task-context.ts +81 -0
- package/src/policy/apex.ts +94 -0
- package/src/policy/cartographer/build-tree.ts +69 -0
- package/src/policy/cartographer/describe.ts +53 -0
- package/src/policy/cartographer/entry.ts +38 -0
- package/src/policy/cartographer/frontmatter.ts +48 -0
- package/src/policy/cartographer/indicators.ts +34 -0
- package/src/policy/claude-md-context.ts +66 -0
- package/src/policy/conventions/index.ts +11 -0
- package/src/policy/conventions/interfaces.ts +71 -0
- package/src/policy/conventions/langs.ts +53 -0
- package/src/policy/conventions/query.ts +49 -0
- package/src/policy/conventions/react-hooks.ts +18 -0
- package/src/policy/conventions/store-or-query.ts +21 -0
- package/src/policy/conventions/stores.ts +56 -0
- package/src/policy/conventions/strip.ts +87 -0
- package/src/policy/conventions/verdict.ts +38 -0
- package/src/policy/creation-intent.ts +11 -0
- package/src/policy/deny-loop.ts +88 -0
- package/src/policy/design/allowed-write.ts +28 -0
- package/src/policy/design/content-checks.ts +45 -0
- package/src/policy/design/corpus-fs.ts +21 -0
- package/src/policy/design/corpus-resolve-cache.ts +49 -0
- package/src/policy/design/corpus-resolve.ts +99 -0
- package/src/policy/design/corpus.ts +92 -0
- package/src/policy/design/design-system-rules.ts +45 -0
- package/src/policy/design/flag.ts +30 -0
- package/src/policy/design/gates-pipeline.ts +105 -0
- package/src/policy/design/gates.ts +87 -0
- package/src/policy/design/screenshot-tools.ts +24 -0
- package/src/policy/design/skill-evidence.ts +34 -0
- package/src/policy/design/skill-gate.ts +81 -0
- package/src/policy/design/skill-triggers.ts +51 -0
- package/src/policy/design/state.ts +92 -0
- package/src/policy/design/template-urls.ts +54 -0
- package/src/policy/design/transitions.ts +68 -0
- package/src/policy/detect-claude-md-project-type.ts +32 -0
- package/src/policy/detect-framework.ts +99 -0
- package/src/policy/detect-primitive-checks.ts +100 -0
- package/src/policy/detect-primitive-lib.ts +55 -0
- package/src/policy/detect-project.ts +99 -0
- package/src/policy/edit-outcome.ts +53 -0
- package/src/policy/evaluate.ts +81 -0
- package/src/policy/expert-agents.ts +60 -0
- package/src/policy/file-size-scope.ts +32 -0
- package/src/policy/file-size.ts +99 -0
- package/src/policy/framework-solid-exclude.ts +52 -0
- package/src/policy/framework-solid-extended.ts +93 -0
- package/src/policy/framework-solid-gates-systems.ts +99 -0
- package/src/policy/framework-solid-gates.ts +98 -0
- package/src/policy/framework-solid.ts +88 -0
- package/src/policy/gemini-mcp-gate.ts +83 -0
- package/src/policy/git-gates.ts +40 -0
- package/src/policy/guards/bash-command-anchor.ts +40 -0
- package/src/policy/guards/bash-write-patterns.ts +140 -0
- package/src/policy/guards/bash-write-redirects.ts +201 -0
- package/src/policy/guards/bash-write-safe-paths.ts +62 -0
- package/src/policy/guards/bash-write.ts +76 -0
- package/src/policy/guards/context.ts +12 -0
- package/src/policy/guards/index.ts +61 -0
- package/src/policy/guards/install.ts +22 -0
- package/src/policy/guards/interface-separation-ext.ts +41 -0
- package/src/policy/guards/interface-separation.ts +100 -0
- package/src/policy/guards/protected-path.ts +100 -0
- package/src/policy/guards/security.ts +97 -0
- package/src/policy/index.ts +19 -0
- package/src/policy/interfaces/types.ts +46 -0
- package/src/policy/js-gate-route.ts +51 -0
- package/src/policy/lessons/lesson-gate.ts +0 -0
- package/src/policy/lessons/trigger-index.ts +84 -0
- package/src/policy/lessons/types.ts +31 -0
- package/src/policy/module-layout.ts +31 -0
- package/src/policy/nearest-manifest.ts +75 -0
- package/src/policy/never-approval.ts +95 -0
- package/src/policy/patterns.ts +80 -0
- package/src/policy/seo/validate.ts +51 -0
- package/src/policy/shadcn-project.ts +22 -0
- package/src/policy/shadcn-skill-gate.ts +97 -0
- package/src/policy/shell-read-refs.ts +96 -0
- package/src/policy/skill-path.ts +46 -0
- package/src/policy/skill-patterns/laravel.ts +40 -0
- package/src/policy/skill-patterns/nextjs.ts +29 -0
- package/src/policy/skill-patterns/react.ts +27 -0
- package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
- package/src/policy/skill-patterns/shadcn.ts +66 -0
- package/src/policy/skill-patterns/swift.ts +30 -0
- package/src/policy/skill-patterns/tailwind.ts +40 -0
- package/src/policy/skill-source.ts +52 -0
- package/src/policy/skill-trigger-patterns.ts +33 -0
- package/src/policy/skill-triggers.ts +108 -0
- package/src/policy/tailwind-skill-gate.ts +63 -0
- package/src/policy/trivial-edits.ts +52 -0
- package/src/policy/verbosity.ts +39 -0
- package/src/prompt/index.ts +1 -0
- package/src/prompt/types.ts +36 -0
- package/src/refs/discover.ts +69 -0
- package/src/refs/frontmatter.ts +24 -0
- package/src/refs/index.ts +4 -0
- package/src/refs/loader.ts +59 -0
- package/src/refs/ref-key.ts +41 -0
- package/src/refs/router.ts +63 -0
- package/src/refs/types.ts +25 -0
- package/src/runtime/activity.ts +99 -0
- package/src/runtime/apply-patch-gate.ts +41 -0
- package/src/runtime/burst-window.ts +21 -0
- package/src/runtime/codex-shell-tool.ts +37 -0
- package/src/runtime/command-string.ts +37 -0
- package/src/runtime/confirm/codex-confirm.ts +126 -0
- package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
- package/src/runtime/confirm/confirm-code.ts +33 -0
- package/src/runtime/confirm/confirm-gate.ts +65 -0
- package/src/runtime/confirm/confirm-irreversible.ts +32 -0
- package/src/runtime/confirm/confirm-pending.ts +34 -0
- package/src/runtime/confirm/confirm-state.ts +56 -0
- package/src/runtime/confirm/confirm-subagent.ts +112 -0
- package/src/runtime/confirm/confirm-submit.ts +38 -0
- package/src/runtime/core-guards-active.ts +84 -0
- package/src/runtime/core-guards-wired.ts +47 -0
- package/src/runtime/deny-loop-store.ts +94 -0
- package/src/runtime/deny-notice.ts +52 -0
- package/src/runtime/design-cache-resolve.ts +51 -0
- package/src/runtime/design-content-gate.ts +95 -0
- package/src/runtime/design-files-gate.ts +114 -0
- package/src/runtime/design-helpers.ts +93 -0
- package/src/runtime/design-lifecycle.ts +39 -0
- package/src/runtime/design-read-credit.ts +28 -0
- package/src/runtime/design.ts +116 -0
- package/src/runtime/dev-context.ts +43 -0
- package/src/runtime/dry-patterns.ts +32 -0
- package/src/runtime/dry.ts +96 -0
- package/src/runtime/fragment-registry.ts +33 -0
- package/src/runtime/framework-skill-gate.ts +79 -0
- package/src/runtime/fs-cleanup.ts +71 -0
- package/src/runtime/gate-apex.ts +81 -0
- package/src/runtime/gate-helpers.ts +52 -0
- package/src/runtime/gate-input.ts +26 -0
- package/src/runtime/gate.ts +128 -0
- package/src/runtime/handle-post.ts +104 -0
- package/src/runtime/handle-pre.ts +137 -0
- package/src/runtime/handle-scope-async.ts +29 -0
- package/src/runtime/handle-types.ts +27 -0
- package/src/runtime/handle.ts +116 -0
- package/src/runtime/home-state.ts +55 -0
- package/src/runtime/index.ts +18 -0
- package/src/runtime/inform.ts +28 -0
- package/src/runtime/inject-budget-recap.ts +47 -0
- package/src/runtime/inject-budget.ts +81 -0
- package/src/runtime/inject-context.ts +85 -0
- package/src/runtime/inject-dedup-exclusive.ts +98 -0
- package/src/runtime/inject-dedup.ts +89 -0
- package/src/runtime/is-agent-tool.ts +19 -0
- package/src/runtime/lifecycle/agent-files.ts +49 -0
- package/src/runtime/lifecycle/agent-memory.ts +82 -0
- package/src/runtime/lifecycle/agent-transcript.ts +80 -0
- package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
- package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
- package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
- package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
- package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
- package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
- package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
- package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +100 -0
- package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
- package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
- package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
- package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
- package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
- package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
- package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
- package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
- package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
- package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
- package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
- package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
- package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
- package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
- package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
- package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
- package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
- package/src/runtime/lifecycle/aipilot/types.ts +72 -0
- package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
- package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
- package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
- package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
- package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
- package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
- package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
- package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
- package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
- package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
- package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
- package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
- package/src/runtime/lifecycle/changelog-research.ts +43 -0
- package/src/runtime/lifecycle/check-file-size.ts +61 -0
- package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
- package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
- package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
- package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
- package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
- package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
- package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
- package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
- package/src/runtime/lifecycle/dispatch.ts +109 -0
- package/src/runtime/lifecycle/failure-lesson.ts +51 -0
- package/src/runtime/lifecycle/index.ts +34 -0
- package/src/runtime/lifecycle/inject-rules.ts +50 -0
- package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
- package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
- package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
- package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
- package/src/runtime/lifecycle/lessons/state.ts +23 -0
- package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
- package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
- package/src/runtime/lifecycle/memory/client.ts +65 -0
- package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
- package/src/runtime/lifecycle/memory/recall.ts +30 -0
- package/src/runtime/lifecycle/memory/salience.ts +31 -0
- package/src/runtime/lifecycle/memory/state.ts +49 -0
- package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
- package/src/runtime/lifecycle/post-compact.ts +38 -0
- package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
- package/src/runtime/lifecycle/post-tracking.ts +40 -0
- package/src/runtime/lifecycle/pre-compact.ts +38 -0
- package/src/runtime/lifecycle/rules-root.ts +83 -0
- package/src/runtime/lifecycle/security/check-skill.ts +63 -0
- package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
- package/src/runtime/lifecycle/security/scan.ts +76 -0
- package/src/runtime/lifecycle/security/skill-state.ts +47 -0
- package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
- package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
- package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
- package/src/runtime/lifecycle/session-end.ts +17 -0
- package/src/runtime/lifecycle/session-start.ts +57 -0
- package/src/runtime/lifecycle/snapshot/board.ts +27 -0
- package/src/runtime/lifecycle/snapshot/format.ts +60 -0
- package/src/runtime/lifecycle/snapshot/git.ts +71 -0
- package/src/runtime/lifecycle/snapshot/index.ts +52 -0
- package/src/runtime/lifecycle/snapshot/version.ts +34 -0
- package/src/runtime/lifecycle/solid-detect.ts +56 -0
- package/src/runtime/lifecycle/stop-core.ts +35 -0
- package/src/runtime/lifecycle/subagent-cache.ts +61 -0
- package/src/runtime/lifecycle/task-completed.ts +94 -0
- package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
- package/src/runtime/lifecycle/teammate-idle.ts +28 -0
- package/src/runtime/lifecycle/tool-failure.ts +24 -0
- package/src/runtime/lifecycle/track-changes.ts +61 -0
- package/src/runtime/lifecycle/validate-solid.ts +70 -0
- package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
- package/src/runtime/lifecycle-bridge.ts +74 -0
- package/src/runtime/mcp-key.ts +44 -0
- package/src/runtime/mcp-tool-name.ts +92 -0
- package/src/runtime/mcp.ts +91 -0
- package/src/runtime/modular.ts +77 -0
- package/src/runtime/normalize.ts +107 -0
- package/src/runtime/notices.ts +91 -0
- package/src/runtime/notification-sound.ts +49 -0
- package/src/runtime/notifications.ts +77 -0
- package/src/runtime/paths.ts +59 -0
- package/src/runtime/post-fanout.ts +41 -0
- package/src/runtime/post-outcome.ts +61 -0
- package/src/runtime/pre-allow.ts +97 -0
- package/src/runtime/precommit.ts +60 -0
- package/src/runtime/prompt-text.ts +38 -0
- package/src/runtime/receipt-capture.ts +34 -0
- package/src/runtime/record.ts +26 -0
- package/src/runtime/respond.ts +92 -0
- package/src/runtime/solid-file-size-gate.ts +58 -0
- package/src/runtime/solid-pre.ts +53 -0
- package/src/runtime/storage.ts +9 -0
- package/src/statusline/colors.ts +31 -0
- package/src/statusline/constants.ts +17 -0
- package/src/statusline/formatters.ts +54 -0
- package/src/statusline/index.ts +4 -0
- package/src/statusline/progress-bar.ts +46 -0
- package/src/tracking/codex-post-failure.ts +73 -0
- package/src/tracking/index.ts +2 -0
- package/src/tracking/integrity.ts +99 -0
- package/src/tracking/one-shot-dedup.ts +34 -0
- package/src/tracking/one-shot-failure.ts +42 -0
- package/src/tracking/one-shot-store.ts +95 -0
- package/src/tracking/one-shot.ts +93 -0
- package/src/tracking/receipts.ts +95 -0
- package/src/tracking/session-state.ts +94 -0
- package/src/tracking/store.ts +96 -0
- package/src/tracking/track-compact.ts +100 -0
- package/src/tracking/track-diff.ts +50 -0
- package/src/tracking/track-journal.ts +97 -0
- package/src/tracking/track-lock-sync.ts +83 -0
- package/src/tracking/track-lock.ts +67 -0
- package/src/util/compact-json.ts +23 -0
- package/src/util/index.ts +2 -0
- package/src/util/json-io.ts +47 -0
- package/src/util/project-root.ts +41 -0
- package/src/util/runtime-io.ts +85 -0
- package/src/util/semver.ts +30 -0
- package/dist/permission-mode-B0MUl7FT.mjs +0 -136
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TanStack Query definition detection over MASKED content (React and Vue
|
|
3
|
+
* variants share the call names; the family scoping is the gate's job, via
|
|
4
|
+
* project deps). Canonical home: `src/query/` — one file per domain. Only
|
|
5
|
+
* DEFINITIONS are gated; inline consumption of a hook defined in `query/`
|
|
6
|
+
* stays allowed.
|
|
7
|
+
*/
|
|
8
|
+
import { maskCommentsAndStrings } from "./strip";
|
|
9
|
+
import { nearestManifestDir, projectCaps } from "../nearest-manifest";
|
|
10
|
+
import { dirname, resolve } from "node:path";
|
|
11
|
+
|
|
12
|
+
/** Import line of a TanStack Query package (raw scan: masking blanks the module string). */
|
|
13
|
+
const QUERY_IMPORT_RE = /^\s*import\s+[^;]*@tanstack\/[\w-]*query[\w-]*/m;
|
|
14
|
+
|
|
15
|
+
/** A TanStack Query definition call. */
|
|
16
|
+
const QUERY_CALL_RE = /\b(?:useQuery|useMutation|useInfiniteQuery|queryOptions)\s*(?:<[^()]*>)?\s*\(/;
|
|
17
|
+
|
|
18
|
+
/** Exported declaration (`export const|function`) — a defined query hook. */
|
|
19
|
+
const EXPORT_DECL_RE = /^\s*export\s+(?:const|function)\s+\w+/m;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* True when the file imports a TanStack Query package (raw scan, anchored to
|
|
23
|
+
* real import lines so `// import …` comments cannot match).
|
|
24
|
+
* @param content - Raw file content.
|
|
25
|
+
*/
|
|
26
|
+
export function importsTanstackQuery(content: string): boolean {
|
|
27
|
+
return QUERY_IMPORT_RE.test(content);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* True when the file DEFINES an exported query/mutation hook (export +
|
|
32
|
+
* query call, masked scan) — as opposed to merely consuming one inline.
|
|
33
|
+
* @param content - Raw file content.
|
|
34
|
+
*/
|
|
35
|
+
export function declaresQueryHook(content: string): boolean {
|
|
36
|
+
const masked = maskCommentsAndStrings(content, "c");
|
|
37
|
+
return QUERY_CALL_RE.test(masked) && EXPORT_DECL_RE.test(masked);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* True when the nearest manifest of `filePath` declares a TanStack Query
|
|
42
|
+
* dependency (`@tanstack/react-query` or `@tanstack/vue-query`) — the
|
|
43
|
+
* cap gate of every query convention (owner: no cap, no query rule).
|
|
44
|
+
* @param filePath - The file being judged (its nearest manifest wins).
|
|
45
|
+
*/
|
|
46
|
+
export function queryCapActive(filePath: string): boolean {
|
|
47
|
+
const caps = projectCaps(nearestManifestDir(dirname(resolve(filePath))));
|
|
48
|
+
return caps.has("tanstack-query") || caps.has("vue-query");
|
|
49
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom React hook declaration detection over MASKED content. Historical
|
|
3
|
+
* rule (`framework-solid-gates.ts`): an exported custom hook belongs in
|
|
4
|
+
* `hooks/` — the regex was naive (`^export (function|const) use[A-Z]`,
|
|
5
|
+
* missing `export default`, `async`, and matching inside comments/strings).
|
|
6
|
+
*/
|
|
7
|
+
import { maskCommentsAndStrings } from "./strip";
|
|
8
|
+
|
|
9
|
+
/** Exported custom hook: `export [default] [async] function|const useX`. */
|
|
10
|
+
const HOOK_DECL_RE = /^\s*export\s+(?:default\s+)?(?:async\s+)?(?:function|const)\s+use[A-Z]\w*/m;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* True when the file declares an exported custom `use*` hook (masked scan).
|
|
14
|
+
* @param content - Raw file content.
|
|
15
|
+
*/
|
|
16
|
+
export function declaresCustomHook(content: string): boolean {
|
|
17
|
+
return HOOK_DECL_RE.test(maskCommentsAndStrings(content, "c"));
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared store/query-definition exemption (owner F0.2): a file is governed
|
|
3
|
+
* by the store/query conventions — NOT by the hook-location rule — ONLY when
|
|
4
|
+
* its signature is validated AND its ecosystem is confirmed:
|
|
5
|
+
* - store: the real `zustand`/`pinia` import (or the v5 curried signature);
|
|
6
|
+
* - query: a TanStack Query definition AND the `@tanstack/*-query` cap in
|
|
7
|
+
* the nearest manifest. Without the cap, a fake `useQuery(` caller is just
|
|
8
|
+
* a hook — the legacy hook rule applies, byte-identical.
|
|
9
|
+
*/
|
|
10
|
+
import { declaresStore } from "./stores";
|
|
11
|
+
import { declaresQueryHook, queryCapActive } from "./query";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* True when the file is a store or query definition (exempt from hook rules).
|
|
15
|
+
* @param filePath - The file being judged (manifest resolution for the cap).
|
|
16
|
+
* @param content - Its RAW content (detectors mask internally).
|
|
17
|
+
*/
|
|
18
|
+
export function isStoreOrQueryDefinition(filePath: string, content: string): boolean {
|
|
19
|
+
if (declaresStore(content, "zustand") || declaresStore(content, "pinia")) return true;
|
|
20
|
+
return declaresQueryHook(content) && queryCapActive(filePath);
|
|
21
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State-store declaration detection over MASKED content — Zustand (React
|
|
3
|
+
* family) and Pinia (Vue family), per the owner's framework scoping: a rule
|
|
4
|
+
* only fires inside its own family (the gate checks the project deps, this
|
|
5
|
+
* module only reads content). Canonical home: `src/stores/`, `*.store.ts`,
|
|
6
|
+
* one store per domain, budget = ratio 0.4 of the global line limit.
|
|
7
|
+
*
|
|
8
|
+
* Signature-strict (audit lesson): `Object.create(` and home-made
|
|
9
|
+
* `create(` factories never match — Zustand requires the `zustand` import
|
|
10
|
+
* (or the v5 curried `create<T>()(...)` call), Pinia requires the `pinia`
|
|
11
|
+
* import alongside `defineStore(`.
|
|
12
|
+
*/
|
|
13
|
+
import { maskCommentsAndStrings, maskCommentsOnly } from "./strip";
|
|
14
|
+
|
|
15
|
+
/** Store library whose declarations are being scanned. */
|
|
16
|
+
export type StoreLib = "zustand" | "pinia";
|
|
17
|
+
|
|
18
|
+
/** Import line of the store package — comments masked (a `// import …` line
|
|
19
|
+
* never matches), strings KEPT (masking would blank the module literal). */
|
|
20
|
+
const STORE_IMPORT_RE: Record<StoreLib, RegExp> = {
|
|
21
|
+
zustand: /^\s*import\s+[^;]*from\s+['"]zustand/m,
|
|
22
|
+
pinia: /^\s*import\s+[^;]*from\s+['"]pinia/m,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Zustand v5 curried declaration: `export const useX = create<S>()(`. */
|
|
26
|
+
const ZUSTAND_CURRIED_RE = /^\s*export\s+const\s+\w+\s*=\s*create\s*<[^=]*>\s*\(\s*\)\s*\(/m;
|
|
27
|
+
/** Zustand declaration with import context: `export const useX = create(`. */
|
|
28
|
+
const ZUSTAND_DECL_RE = /^\s*export\s+const\s+\w+\s*=\s*create(?:Store)?\s*(?:<[^(]+>)?\s*\(/m;
|
|
29
|
+
/** Pinia declaration: `export const useX = defineStore(`. */
|
|
30
|
+
const PINIA_DECL_RE = /^\s*export\s+const\s+\w+\s*=\s*defineStore\s*\(/m;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* True when the file declares a store of the given library (masked scan,
|
|
34
|
+
* signature-strict).
|
|
35
|
+
* @param content - Raw file content.
|
|
36
|
+
* @param lib - Store library to detect.
|
|
37
|
+
*/
|
|
38
|
+
export function declaresStore(content: string, lib: StoreLib): boolean {
|
|
39
|
+
const masked = maskCommentsAndStrings(content, "c");
|
|
40
|
+
const importable = maskCommentsOnly(content, "c");
|
|
41
|
+
if (lib === "pinia") return STORE_IMPORT_RE.pinia.test(importable) && PINIA_DECL_RE.test(masked);
|
|
42
|
+
return ZUSTAND_CURRIED_RE.test(masked) || (STORE_IMPORT_RE.zustand.test(importable) && ZUSTAND_DECL_RE.test(masked));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Count store declarations in one file (multi-store advisory: one per domain).
|
|
47
|
+
* @param content - Raw file content.
|
|
48
|
+
* @param lib - Store library to count.
|
|
49
|
+
*/
|
|
50
|
+
export function countStores(content: string, lib: StoreLib): number {
|
|
51
|
+
const masked = maskCommentsAndStrings(content, "c");
|
|
52
|
+
const re = lib === "pinia" ? PINIA_DECL_RE : ZUSTAND_DECL_RE;
|
|
53
|
+
const curried = masked.match(new RegExp(ZUSTAND_CURRIED_RE.source, "gm"))?.length ?? 0;
|
|
54
|
+
const plain = masked.match(new RegExp(re.source, "gm"))?.length ?? 0;
|
|
55
|
+
return lib === "pinia" ? plain : Math.max(curried, plain);
|
|
56
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lexical comment/string masking for convention detectors. Every convention
|
|
3
|
+
* rule (interfaces, types, hooks, stores, query) matches against MASKED
|
|
4
|
+
* content so a declaration-looking line inside a comment, string, template
|
|
5
|
+
* literal, or heredoc can never trigger a verdict. Masking replaces the
|
|
6
|
+
* covered CHARACTERS with spaces but keeps every newline: line offsets and
|
|
7
|
+
* `^…/m` anchors stay valid, and the output has the same line count.
|
|
8
|
+
* Not a parser — a per-family state machine (c-like, python, ruby, php).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Lexical profile of a language family. */
|
|
12
|
+
export type LexProfile = "c" | "py" | "rb" | "php";
|
|
13
|
+
|
|
14
|
+
/** Mask PHP heredoc/nowdoc blocks (`<<<TAG` … terminator line), offsets kept. */
|
|
15
|
+
function maskHeredocs(content: string): string {
|
|
16
|
+
return content.replace(/<<<['"]?(\w+)['"]?\r?\n[\s\S]*?^\1;?$/gm, (block) => block.replace(/[^\n]/g, " "));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Mask comments and strings of `content` according to its lexical profile.
|
|
21
|
+
* @param content - Source text.
|
|
22
|
+
* @param profile - Lexical family (see `langs.ts` `lexProfileOf`).
|
|
23
|
+
* @returns Same-length text with comment/string interiors blanked.
|
|
24
|
+
*/
|
|
25
|
+
export function maskCommentsAndStrings(content: string, profile: LexProfile): string {
|
|
26
|
+
return maskScan(content, profile, false);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Mask COMMENTS only (strings kept) — for detectors whose signal IS a string
|
|
31
|
+
* literal (module imports like `from "zustand"`): a `// import …` comment
|
|
32
|
+
* cannot match, while the real import line stays readable.
|
|
33
|
+
* @param content - Source text.
|
|
34
|
+
* @param profile - Lexical family.
|
|
35
|
+
* @returns Same-length text with comment interiors blanked.
|
|
36
|
+
*/
|
|
37
|
+
export function maskCommentsOnly(content: string, profile: LexProfile): string {
|
|
38
|
+
return maskScan(content, profile, true);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Shared scanner; `keepStrings` skips the string-masking branch. */
|
|
42
|
+
function maskScan(content: string, profile: LexProfile, keepStrings: boolean): string {
|
|
43
|
+
if (profile === "php") content = maskHeredocs(content);
|
|
44
|
+
const out = content.split("");
|
|
45
|
+
const blank = (i: number) => { if (out[i] !== "\n") out[i] = " "; };
|
|
46
|
+
const isPy = profile === "py" || profile === "rb";
|
|
47
|
+
let i = 0;
|
|
48
|
+
while (i < out.length) {
|
|
49
|
+
const two = content.slice(i, i + 2);
|
|
50
|
+
if (!isPy && two === "//") { while (i < out.length && out[i] !== "\n") blank(i++); continue; }
|
|
51
|
+
if (isPy && out[i] === "#") { while (i < out.length && out[i] !== "\n") blank(i++); continue; }
|
|
52
|
+
if (profile === "php" && out[i] === "#") { while (i < out.length && out[i] !== "\n") blank(i++); continue; }
|
|
53
|
+
if (!isPy && two === "/*") { const end = content.indexOf("*/", i + 2); const stop = end === -1 ? out.length : end + 2; while (i < stop) blank(i++); continue; }
|
|
54
|
+
if (isPy && (content.startsWith("'''", i) || content.startsWith('"""', i))) {
|
|
55
|
+
const q = content.slice(i, i + 3);
|
|
56
|
+
const end = content.indexOf(q, i + 3);
|
|
57
|
+
const stop = end === -1 ? out.length : end + 3;
|
|
58
|
+
while (i < stop) blank(i++);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (!keepStrings && (out[i] === "'" || out[i] === '"' || (profile === "c" && out[i] === "`"))) {
|
|
62
|
+
const q = out[i] as string;
|
|
63
|
+
// Unterminated-quote guard: a single/double quote never opens a string
|
|
64
|
+
// past its own line (JS/TS has no multiline plain strings — a JSX
|
|
65
|
+
// apostrophe like `Don't` must not swallow the rest of the file), and
|
|
66
|
+
// an unterminated template literal masks nothing (unpaired backtick).
|
|
67
|
+
let j = i + 1, closed = -1;
|
|
68
|
+
while (j < out.length && out[j] !== "\n") {
|
|
69
|
+
if (out[j] === "\\") { j += 2; continue; }
|
|
70
|
+
if (out[j] === q) { closed = j; break; }
|
|
71
|
+
j++;
|
|
72
|
+
}
|
|
73
|
+
if (q === "`") {
|
|
74
|
+
if (closed !== -1) { while (i <= closed) blank(i++); continue; }
|
|
75
|
+
const end = content.indexOf("`", i + 1);
|
|
76
|
+
if (end !== -1) { while (i <= end) blank(i++); continue; }
|
|
77
|
+
i++;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (closed === -1) { i++; continue; }
|
|
81
|
+
while (i <= closed) blank(i++);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
i++;
|
|
85
|
+
}
|
|
86
|
+
return out.join("");
|
|
87
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rollout control for NEW convention denies (owner decision, inverted
|
|
3
|
+
* 2026-07): new convention rules BLOCK by default — `FUSE_CONVENTIONS_MODE`
|
|
4
|
+
* is an opt-OUT. Set it to `advisory` to observe without blocking (`inform`
|
|
5
|
+
* output); any absent or invalid value means `deny`. Denies that already
|
|
6
|
+
* existed before the conventions module are unaffected (byte-parity); this
|
|
7
|
+
* module only ever MODULATES the new ones.
|
|
8
|
+
*/
|
|
9
|
+
import type { Prompt } from "../../prompt/types";
|
|
10
|
+
|
|
11
|
+
/** Rollout mode for new convention denies. */
|
|
12
|
+
export type ConventionsMode = "advisory" | "deny";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Current rollout mode: `advisory` ONLY on the exact explicit value —
|
|
16
|
+
* anything else (absent, typo, empty) is `deny` (owner decision: deny-first).
|
|
17
|
+
* @param env - Environment (defaults to `process.env`).
|
|
18
|
+
*/
|
|
19
|
+
export function conventionsMode(env: Record<string, string | undefined> = process.env): ConventionsMode {
|
|
20
|
+
return env.FUSE_CONVENTIONS_MODE === "advisory" ? "advisory" : "deny";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a convention verdict under the rollout flag: a NEW rule yields an
|
|
25
|
+
* `inform` prompt in advisory mode (explicit opt-out), the blocking prompt
|
|
26
|
+
* otherwise; an EXISTING rule always yields the blocking prompt unchanged.
|
|
27
|
+
* @param prompt - The blocking prompt the rule would emit.
|
|
28
|
+
* @param isNew - True when the rule is new (post-conventions module).
|
|
29
|
+
* @param env - Environment (defaults to `process.env`).
|
|
30
|
+
*/
|
|
31
|
+
export function rolloutVerdict(
|
|
32
|
+
prompt: Prompt,
|
|
33
|
+
isNew: boolean,
|
|
34
|
+
env: Record<string, string | undefined> = process.env,
|
|
35
|
+
): Prompt {
|
|
36
|
+
if (!isNew || conventionsMode(env) === "deny") return prompt;
|
|
37
|
+
return { kind: "inform", title: prompt.title, reason: prompt.reason, actions: prompt.actions };
|
|
38
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const CREATE_RE = /\b(?:create|implement|add|build|new|feature|component|generate|make|develop|scaffold)\b/i;
|
|
2
|
+
const SKIP_RE = /\b(?:fix|bug|debug|update|refactor|rename|move|delete|remove|commit|push|status|edit|modify|change|adjust|tweak|existing|current)\b/i;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* True when a prompt expresses creation intent (a new feature/component) and is
|
|
6
|
+
* not a fix/refactor — the signal that brainstorming should precede creation.
|
|
7
|
+
* The harness calls this on UserPromptSubmit, then `recordBrainstormRequired`.
|
|
8
|
+
*/
|
|
9
|
+
export function detectCreationIntent(prompt: string): boolean {
|
|
10
|
+
return CREATE_RE.test(prompt) && !SKIP_RE.test(prompt);
|
|
11
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module deny-loop
|
|
3
|
+
* Pure anti-loop logic: hash a tool-call, decide if it repeats a prior deny, and
|
|
4
|
+
* enrich the repeated block's message.
|
|
5
|
+
*
|
|
6
|
+
* The proprietary rule "NEVER propose the same fix twice" is prose a model under
|
|
7
|
+
* pressure ignores. This makes it machine-enforced: when a call whose
|
|
8
|
+
* `(tool + normalized input)` hash was ALREADY denied in-window is retried, the
|
|
9
|
+
* harness keeps the deny but rewrites the message — `[REPEAT]` title, a
|
|
10
|
+
* mid-sentence STOP warning, forced `research-expert` action. State + wiring live in the sidecar
|
|
11
|
+
* store ({@link module:deny-loop-store}); this file is IO-free and pure.
|
|
12
|
+
* @packageDocumentation
|
|
13
|
+
*/
|
|
14
|
+
import { hashText } from "../util/json-io";
|
|
15
|
+
import type { Prompt } from "../prompt/types";
|
|
16
|
+
|
|
17
|
+
/** One recorded deny: identical-in-window count, and when the last landed. */
|
|
18
|
+
export interface DenyEntry { count: number; lastTs: number; }
|
|
19
|
+
|
|
20
|
+
/** Loop-check outcome: does the input repeat an in-window deny, and its running count. */
|
|
21
|
+
export interface DenyLoopResult { isRepeat: boolean; count: number; hash: string; deduped?: boolean; }
|
|
22
|
+
|
|
23
|
+
/** Stable JSON: keys sorted at every depth so `{a,b}` and `{b,a}` hash identically. */
|
|
24
|
+
function stableStringify(v: unknown): string {
|
|
25
|
+
if (v === null || typeof v !== "object") return JSON.stringify(v) ?? "null";
|
|
26
|
+
if (Array.isArray(v)) return `[${v.map(stableStringify).join(",")}]`;
|
|
27
|
+
const o = v as Record<string, unknown>;
|
|
28
|
+
return `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(o[k])}`).join(",")}}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Stable identity hash of a tool-call = tool name + normalized (key-sorted) input,
|
|
33
|
+
* so re-ordered keys never mask a repeat.
|
|
34
|
+
* @param tool - Tool name (e.g. "Write", "Bash").
|
|
35
|
+
* @param input - Identifying tool input (filePath/content/command...).
|
|
36
|
+
* @returns 8-char hex hash.
|
|
37
|
+
*/
|
|
38
|
+
export function denyHash(tool: string, input: Record<string, unknown>): string {
|
|
39
|
+
return hashText(`${tool}\n${stableStringify(input)}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Pure loop check: given the already-pruned in-window map, compute the running
|
|
44
|
+
* count for `hash` and whether it repeats (count > 1). No IO — the caller persists.
|
|
45
|
+
*
|
|
46
|
+
* When `dedupMs` is set (>0) and an identical prior deny landed within that
|
|
47
|
+
* window, the current call is a sibling hook echoing the SAME event (see
|
|
48
|
+
* {@link module:burst-window}): it returns the prior verdict VERBATIM with
|
|
49
|
+
* `deduped:true` and does NOT bump the count, so all N fan-out processes agree
|
|
50
|
+
* on one number instead of counting to N. Absent `dedupMs` (mono-process
|
|
51
|
+
* callers / unit tests) the historical increment-every-time behaviour holds.
|
|
52
|
+
* @param hash - {@link denyHash}-derived map key of the current call.
|
|
53
|
+
* @param priorDenies - The `{ hash -> DenyEntry }` map, pruned to `now`/`windowMs`.
|
|
54
|
+
* @param opts - Clock + window, plus an optional burst-dedup window.
|
|
55
|
+
* @returns `{ isRepeat, count, hash, deduped? }`.
|
|
56
|
+
*/
|
|
57
|
+
export function denyLoopCheck(hash: string, priorDenies: Record<string, DenyEntry>, opts: { now: number; windowMs: number; dedupMs?: number }): DenyLoopResult {
|
|
58
|
+
const prev = priorDenies[hash];
|
|
59
|
+
const fresh = prev && typeof prev.lastTs === "number" && opts.now - prev.lastTs < opts.windowMs;
|
|
60
|
+
if (!fresh) return { isRepeat: false, count: 1, hash };
|
|
61
|
+
if ((opts.dedupMs ?? 0) > 0 && opts.now - prev.lastTs < (opts.dedupMs ?? 0)) {
|
|
62
|
+
return { isRepeat: prev.count > 1, count: prev.count, hash, deduped: true };
|
|
63
|
+
}
|
|
64
|
+
const count = prev.count + 1;
|
|
65
|
+
return { isRepeat: count > 1, count, hash };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Enrich a REPEATED block prompt — a NEW object, never a mutation (the input may
|
|
70
|
+
* be a shared const like FAIL_CLOSED). The decision stays `block`; only the
|
|
71
|
+
* message changes, so every harness renders it through the same adapter.
|
|
72
|
+
* @param prompt - The original block prompt.
|
|
73
|
+
* @param count - The running identical-deny count (n).
|
|
74
|
+
* @returns A block prompt with `[REPEAT]` title, STOP-prefixed reason, forced research action.
|
|
75
|
+
*/
|
|
76
|
+
export function enrichRepeatDeny(prompt: Prompt, count: number): Prompt {
|
|
77
|
+
// H2b: the counter cannot know whether new reads/actions happened since the
|
|
78
|
+
// last deny, so the hard STOP is reserved for the UNCHANGED retry — a retry
|
|
79
|
+
// after completing the required reads/actions is legitimate and expected.
|
|
80
|
+
const stop = `Identical attempt #${count} already denied for the same reason. Do not retry this same call UNCHANGED — if you have since completed the required reads/actions, retry now; otherwise STOP and pick a DIFFERENT approach. `;
|
|
81
|
+
const action = "Launch fuse-ai-pilot:research-expert to find a DIFFERENT approach";
|
|
82
|
+
return {
|
|
83
|
+
...prompt,
|
|
84
|
+
title: prompt.title.startsWith("[REPEAT]") ? prompt.title : `[REPEAT] ${prompt.title}`,
|
|
85
|
+
reason: stop + prompt.reason,
|
|
86
|
+
actions: [action, ...(prompt.actions ?? [])],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module allowed-write
|
|
3
|
+
* The design-agent write allowlist, split out of `gates.ts` to keep that file
|
|
4
|
+
* within the SOLID size budget (SRP).
|
|
5
|
+
* @packageDocumentation
|
|
6
|
+
*/
|
|
7
|
+
import { basename } from "node:path";
|
|
8
|
+
|
|
9
|
+
/** Extensions the design agent may always write: .html/.css/.md/.json. */
|
|
10
|
+
export const ALLOWED_WRITE: RegExp = /\.(html|css|md|json)$/;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* C4: `motion*.js` ONLY, matched on the BASENAME — every one of the 11
|
|
14
|
+
* corpus references ships its animation as a separate `motion.js` /
|
|
15
|
+
* `motion-nav.js` / `motion-scroll.js` file, so banning every `.js` forced
|
|
16
|
+
* the agent to inline scripts the corpus explicitly teaches out-of-band.
|
|
17
|
+
* Deliberately NOT `\.js$` in general (that would open arbitrary app code,
|
|
18
|
+
* the exact thing this gate exists to block) — after the `motion` prefix,
|
|
19
|
+
* either nothing (`motion.js`) or `.`/`-` then more (`motion-nav.js`) must
|
|
20
|
+
* lead into `.js`; `app.js` and `motionless-app.js` — a bare `^motion`
|
|
21
|
+
* prefix would wrongly pass this — do not match.
|
|
22
|
+
*/
|
|
23
|
+
const MOTION_JS: RegExp = /^motion([.-].*)?\.js$/;
|
|
24
|
+
|
|
25
|
+
/** True when `filePath` is a write the design agent may make: .html/.css/.md/.json, or a `motion*.js` file. */
|
|
26
|
+
export function isAllowedWrite(filePath: string): boolean {
|
|
27
|
+
return ALLOWED_WRITE.test(filePath) || MOTION_JS.test(basename(filePath));
|
|
28
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Accessibility warnings: icon buttons need aria-label, images need alt. */
|
|
2
|
+
function checkAccessibility(content: string): string[] {
|
|
3
|
+
const w: string[] = [];
|
|
4
|
+
if (!/<(button|a|input|img)/.test(content)) return w;
|
|
5
|
+
if (/<button[^>]*>/.test(content) && !/aria-label|aria-labelledby/.test(content) && /<button[^>]*>[^<]*<[^>]*Icon/.test(content)) {
|
|
6
|
+
w.push("Accessibility: icon buttons need an aria-label.");
|
|
7
|
+
}
|
|
8
|
+
for (const m of content.matchAll(/<img[^>]*?>/g)) {
|
|
9
|
+
if (!m[0].includes("alt=")) {
|
|
10
|
+
w.push("Accessibility: images need an alt attribute.");
|
|
11
|
+
break;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return w;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Anti-pattern warnings: colored left borders, AI-slop gradients, emoji-as-icons. */
|
|
18
|
+
function checkPatterns(content: string): string[] {
|
|
19
|
+
const w: string[] = [];
|
|
20
|
+
if (/border-l-[0-9]+ border-l-(blue|green|red|purple)/.test(content)) w.push("Design: avoid colored left borders — use shadow/gradient.");
|
|
21
|
+
if (/from-purple|to-purple|via-purple|from-pink.*to-purple/.test(content)) w.push("Design: avoid purple/pink gradients (AI slop) — use brand colors.");
|
|
22
|
+
if (/>[^\x00-\x7F]+</.test(content)) w.push("Design: avoid emojis as icons — use a real icon set.");
|
|
23
|
+
return w;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Forbidden-font warnings (CSS font-family + Google Fonts import). */
|
|
27
|
+
function checkFonts(content: string): string[] {
|
|
28
|
+
const w: string[] = [];
|
|
29
|
+
if (/font-family:\s*['"]?(Roboto|Inter|Arial|Open Sans|Lato)\b/i.test(content)) w.push("Font: forbidden family (Roboto/Inter/Arial/Open Sans/Lato) — use identity fonts.");
|
|
30
|
+
if (/@import.*fonts\.googleapis.*family=(Roboto|Inter)\b/.test(content)) w.push("Font: Google Fonts import for a forbidden family.");
|
|
31
|
+
return w;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Hard-coded-color warnings (hex in className or inline style). */
|
|
35
|
+
function checkColors(content: string): string[] {
|
|
36
|
+
const w: string[] = [];
|
|
37
|
+
if (/className="[^"]*#[0-9a-fA-F]{3,8}[^"]*"/.test(content)) w.push("Color: hard-coded hex in className — use CSS variables.");
|
|
38
|
+
if (/(?:color|background(?:-color)?|fill|stroke):\s*['"]?#[0-9a-fA-F]{3,8}/.test(content)) w.push("Color: hard-coded hex in style — use var(--color-*).");
|
|
39
|
+
return w;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Run all design content checks → non-blocking warnings (empty = clean). */
|
|
43
|
+
export function runDesignChecks(content: string): string[] {
|
|
44
|
+
return [...checkAccessibility(content), ...checkPatterns(content), ...checkFonts(content), ...checkColors(content)];
|
|
45
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module corpus-fs
|
|
3
|
+
* Shared fs primitives for corpus/plugins root resolution, split out of
|
|
4
|
+
* `corpus-resolve.ts` so `corpus-resolve-cache.ts` can reuse them without a
|
|
5
|
+
* circular import (both modules feed `corpus-resolve.ts`'s `probeClaude`).
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
import { readdirSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
/** The refs-design/ path suffix common to every design-corpus-bearing plugin tree. */
|
|
12
|
+
export const CORPUS_SUFFIX: string = join("skills", "design-web", "references", "refs-design");
|
|
13
|
+
|
|
14
|
+
/** Immediate child dir names of `dir`, alphabetically sorted, or [] when unreadable. */
|
|
15
|
+
export function children(dir: string): string[] {
|
|
16
|
+
try {
|
|
17
|
+
return readdirSync(dir).sort();
|
|
18
|
+
} catch {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module corpus-resolve-cache
|
|
3
|
+
* Claude Code's per-plugin VERSIONED CACHE tree, split out of
|
|
4
|
+
* `corpus-resolve.ts` to keep that file within the SOLID size budget (SRP).
|
|
5
|
+
*
|
|
6
|
+
* Claude Code caches every installed marketplace plugin at
|
|
7
|
+
* `~/.claude/plugins/cache/<mkt>/<plugin>/<version>/` — a tree SEPARATE from
|
|
8
|
+
* `~/.claude/plugins/marketplaces/<mkt>/plugins/<plugin>/` (the marketplace's
|
|
9
|
+
* own checkout), one version dir per `plugin.json` version bump, orphaned
|
|
10
|
+
* versions pruned ~14 days after being superseded (docs: "Plugin caching and
|
|
11
|
+
* file resolution"). The design corpus can ship under EITHER tree, and under
|
|
12
|
+
* a DIFFERENT plugin id in each: a machine observed 2026-07-29 has the corpus
|
|
13
|
+
* at `cache/fusengine-plugins/fuse-design/2.2.3/...` while the marketplace
|
|
14
|
+
* checkout is `marketplaces/fusengine-plugins/plugins/design-expert/...`.
|
|
15
|
+
* Identification here is therefore STRUCTURAL, never by plugin name: a
|
|
16
|
+
* candidate `<mkt>/<plugin>/<version>/` dir qualifies iff it itself contains
|
|
17
|
+
* `CORPUS_SUFFIX` — matching a hardcoded name would silently miss a
|
|
18
|
+
* renamed/relocated corpus (the exact defect this module fixes).
|
|
19
|
+
* @packageDocumentation
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync } from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { maxSemver } from "../../util/semver";
|
|
24
|
+
import { CORPUS_SUFFIX, children } from "./corpus-fs";
|
|
25
|
+
|
|
26
|
+
/** Real (non-prerelease) semver dirs, same filter `probeCodex` uses. */
|
|
27
|
+
const SEMVER_RE = /^\d+\.\d+(\.\d+)?$/;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Claude: `<home>/.claude/plugins/cache/<mkt>/<plugin>/<highest STABLE semver
|
|
31
|
+
* containing refs-design>` ("" when no plugin under any marketplace cache
|
|
32
|
+
* ships the corpus). Marketplaces and plugin ids are both walked — neither is
|
|
33
|
+
* assumed — so a corpus shipped under any plugin name is found.
|
|
34
|
+
*/
|
|
35
|
+
export function probeClaudeCache(home: string): string {
|
|
36
|
+
const cacheRoot = join(home, ".claude", "plugins", "cache");
|
|
37
|
+
for (const mkt of children(cacheRoot)) {
|
|
38
|
+
const mktDir = join(cacheRoot, mkt);
|
|
39
|
+
for (const plugin of children(mktDir)) {
|
|
40
|
+
const pluginDir = join(mktDir, plugin);
|
|
41
|
+
const versions = children(pluginDir).filter(
|
|
42
|
+
(v) => SEMVER_RE.test(v) && existsSync(join(pluginDir, v, CORPUS_SUFFIX)),
|
|
43
|
+
);
|
|
44
|
+
const latest = maxSemver(versions);
|
|
45
|
+
if (latest) return join(pluginDir, latest);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Corpus/plugins root resolution (fs) and the write guard over it. Resolution
|
|
3
|
+
* is PER RUNTIME (Claude marketplaces, Codex versioned plugin cache — same
|
|
4
|
+
* pattern as rules-root.ts); runtimes without a plugin structure (kimi today)
|
|
5
|
+
* resolve to "" and the corpus gates stay dormant. There is NO cwd fallback
|
|
6
|
+
* anywhere: an agent-controlled directory can never become its own taste
|
|
7
|
+
* reference (self-attested proof — the failure this gate exists to prevent).
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { isAbsolute, join, normalize } from "node:path";
|
|
12
|
+
import type { Prompt } from "../../prompt/types";
|
|
13
|
+
import { detectHarness, type HarnessId } from "../../detect/harness";
|
|
14
|
+
import { maxSemver } from "../../util/semver";
|
|
15
|
+
import { CORPUS_SUFFIX, children } from "./corpus-fs";
|
|
16
|
+
import { probeClaudeCache } from "./corpus-resolve-cache";
|
|
17
|
+
|
|
18
|
+
/** Claude marketplace checkout: `<home>/.claude/plugins/marketplaces/<mkt>/plugins/design-expert` ("" when absent). */
|
|
19
|
+
function probeClaudeMarketplace(home: string): string {
|
|
20
|
+
const markets = join(home, ".claude", "plugins", "marketplaces");
|
|
21
|
+
for (const m of children(markets)) {
|
|
22
|
+
const de = join(markets, m, "plugins", "design-expert");
|
|
23
|
+
if (existsSync(de)) return de;
|
|
24
|
+
}
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Claude: the marketplace checkout WINS when present (a single unambiguous
|
|
30
|
+
* live git checkout); the versioned plugin CACHE tree is the fallback, since
|
|
31
|
+
* Claude Code keeps orphaned prior-version cache dirs ~14 days after a bump.
|
|
32
|
+
*/
|
|
33
|
+
function probeClaude(home: string): string {
|
|
34
|
+
return probeClaudeMarketplace(home) || probeClaudeCache(home);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Codex: `<CODEX_HOME|~/.codex>/plugins/cache/<mkt>/design-expert/<highest STABLE semver>` ("" when absent). */
|
|
38
|
+
function probeCodex(env: Record<string, string | undefined>, home: string): string {
|
|
39
|
+
const cache = join(env.CODEX_HOME ?? join(home, ".codex"), "plugins", "cache");
|
|
40
|
+
for (const m of children(cache)) {
|
|
41
|
+
const de = join(cache, m, "design-expert");
|
|
42
|
+
// Only real version dirs compete — no pre-release, no stray backup/ dir.
|
|
43
|
+
const latest = maxSemver(children(de).filter((v) => /^\d+\.\d+(\.\d+)?$/.test(v) && existsSync(join(de, v, "skills"))));
|
|
44
|
+
if (latest) return join(de, latest);
|
|
45
|
+
}
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The design-expert plugin root for the ACTIVE runtime ("" = not installed).
|
|
51
|
+
* `pluginsDirOverride` short-circuits resolution (tests): a PLUGINS dir whose
|
|
52
|
+
* `design-expert` child is used when present, else itself.
|
|
53
|
+
*/
|
|
54
|
+
export function resolvePluginsRoot(
|
|
55
|
+
pluginsDirOverride?: string,
|
|
56
|
+
home: string = homedir(),
|
|
57
|
+
id: HarnessId = detectHarness().id,
|
|
58
|
+
env: Record<string, string | undefined> = process.env,
|
|
59
|
+
): string {
|
|
60
|
+
if (pluginsDirOverride) {
|
|
61
|
+
const de = join(pluginsDirOverride, "design-expert");
|
|
62
|
+
return existsSync(de) ? de : pluginsDirOverride;
|
|
63
|
+
}
|
|
64
|
+
if (id === "claude-code") return probeClaude(home);
|
|
65
|
+
if (id === "codex") return probeCodex(env, home);
|
|
66
|
+
return "";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Resolve the delivered refs-design/ root, or "" when absent/unusable (the fail-open signal). */
|
|
70
|
+
export function resolveCorpusRoot(
|
|
71
|
+
pluginsDirOverride?: string,
|
|
72
|
+
home: string = homedir(),
|
|
73
|
+
id: HarnessId = detectHarness().id,
|
|
74
|
+
env: Record<string, string | undefined> = process.env,
|
|
75
|
+
): string {
|
|
76
|
+
const root = resolvePluginsRoot(pluginsDirOverride, home, id, env);
|
|
77
|
+
if (!root) return "";
|
|
78
|
+
const corpus = join(root, CORPUS_SUFFIX);
|
|
79
|
+
return existsSync(corpus) ? corpus : "";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Deny Write/Edit/apply_patch-file under the resolved plugins root: the corpus
|
|
84
|
+
* is the artefact the pipeline checks reads against, so it must stay
|
|
85
|
+
* agent-proof — including while refs-design/ is still absent. Relative paths
|
|
86
|
+
* (the normal form of Codex patches) are resolved against `cwd` first. SCOPE:
|
|
87
|
+
* no Bash, no symlinks, no sibling marketplace — the gate protects against
|
|
88
|
+
* oversight, not an adversary (the mandatory screenshot stays the unforgeable
|
|
89
|
+
* proof; forging the corpus only lowers a quota).
|
|
90
|
+
*/
|
|
91
|
+
export function pluginsWriteGuard(filePath: string, pluginsRoot: string, cwd = ""): Prompt | null {
|
|
92
|
+
const abs = isAbsolute(filePath) ? filePath : cwd ? normalize(join(cwd, filePath)) : filePath;
|
|
93
|
+
if (!pluginsRoot || !abs.startsWith(`${pluginsRoot}/`)) return null;
|
|
94
|
+
return {
|
|
95
|
+
kind: "block", title: "Design pipeline",
|
|
96
|
+
reason: "BLOCKED: the design-expert plugin dir (refs-design corpus) is read-only — it is the taste reference your reads are checked against. Never create or modify files there.",
|
|
97
|
+
actions: ["Read the corpus with the Read tool; write your own files in the project"],
|
|
98
|
+
};
|
|
99
|
+
}
|