@fusengine/harness 0.1.90 → 0.1.92
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 +3 -2
- 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-CBaOcFZq.mjs → handle-BF1dZFjY.mjs} +1194 -144
- 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-Dy8g9Ybl.mjs +397 -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 +37 -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-budget.ts +144 -0
- package/src/adapters/cursor/context-limit.ts +115 -0
- package/src/adapters/cursor/context.ts +65 -0
- package/src/adapters/cursor/events.ts +38 -0
- package/src/adapters/cursor/index.ts +43 -0
- package/src/adapters/cursor/interfaces/context-budget.ts +25 -0
- package/src/adapters/cursor/interfaces/types.ts +88 -0
- package/src/adapters/cursor/native-response.ts +39 -0
- package/src/adapters/cursor/native-schemas.ts +161 -0
- package/src/adapters/cursor/normalize.ts +188 -0
- package/src/adapters/cursor/plugin-root.ts +103 -0
- package/src/adapters/cursor/respond.ts +170 -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 +197 -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 +106 -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 +55 -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 +99 -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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Corpus anchoring for the design pipeline: refs-design/ read classification,
|
|
3
|
+
* per-mode readiness, citation form, citation↔read jointure. Nothing about
|
|
4
|
+
* the corpus content (slugs, section titles, count) is hardcoded — everything
|
|
5
|
+
* is matched by shape or discovered on disk, so a new reference or a renamed
|
|
6
|
+
* section never requires reopening the harness. Pure string logic only; root
|
|
7
|
+
* resolution and the write guard live in `corpus-resolve.ts`.
|
|
8
|
+
*/
|
|
9
|
+
import { basename, join } from "node:path";
|
|
10
|
+
import type { DesignMode } from "./state";
|
|
11
|
+
|
|
12
|
+
export { resolveCorpusRoot, resolvePluginsRoot, pluginsWriteGuard } from "./corpus-resolve";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* What a read under the corpus root is: the index, a tokens-* procedure
|
|
16
|
+
* file, or a per-reference `design-system.md` DIRECTION sheet (C6) — the
|
|
17
|
+
* fiche a reference ships alongside its `tokens-*.md`: register, tone,
|
|
18
|
+
* macrostructure, signature element (the PROCEDURES live in tokens-*;
|
|
19
|
+
* design-system.md is what you read to CHOOSE a reference in the first
|
|
20
|
+
* place). "sheet" is a new variant, not folded into "index" — the two are
|
|
21
|
+
* read for opposite reasons (corpus-wide index vs one reference's direction).
|
|
22
|
+
*/
|
|
23
|
+
export type CorpusKind = "index" | "tokens" | "sheet";
|
|
24
|
+
|
|
25
|
+
const TOKENS_RE = /^tokens-.+\.md$/;
|
|
26
|
+
const CORPUS_LINE_RE = /^[-*]\s*Corpus:\s*(.+)$/gim;
|
|
27
|
+
|
|
28
|
+
/** Classify a read path against the DELIVERED corpus root (null = not a corpus read). */
|
|
29
|
+
export function classifyCorpusRead(filePath: string, corpusRoot: string): CorpusKind | null {
|
|
30
|
+
if (!corpusRoot) return null;
|
|
31
|
+
if (filePath === join(corpusRoot, "README.md")) return "index";
|
|
32
|
+
if (!filePath.startsWith(`${corpusRoot}/`)) return null;
|
|
33
|
+
if (TOKENS_RE.test(basename(filePath))) return "tokens";
|
|
34
|
+
if (basename(filePath) === "design-system.md") return "sheet";
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Per-mode corpus-read threshold (component >= 1 file, page >= 2 files,
|
|
40
|
+
* full = index + 3 tokens — C5: raised from 2, since 2 references still let
|
|
41
|
+
* an agent pattern-match by tonal affinity without ever opening the one
|
|
42
|
+
* reference that actually answered the brief). `"sheet"` reads (C6,
|
|
43
|
+
* `design-system.md`) never count toward this — only `TOKENS_RE` matches do.
|
|
44
|
+
*/
|
|
45
|
+
export function corpusReady(reads: readonly string[], mode: DesignMode): boolean {
|
|
46
|
+
if (mode === "component") return reads.length >= 1;
|
|
47
|
+
if (mode === "page") return reads.length >= 2;
|
|
48
|
+
return reads.includes("README.md") && reads.filter((r) => TOKENS_RE.test(basename(r))).length >= 3;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** True when the content carries a `- Corpus: ref/section` citation line (form only). */
|
|
52
|
+
export function hasCorpusCitation(content: string): boolean {
|
|
53
|
+
return citedCorpusRefs(content).length > 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Extract the reference slugs cited on EVERY `- Corpus:` line (token before
|
|
58
|
+
* `/`, or a bare slug). Anchored at line start with NO indentation
|
|
59
|
+
* (fail-closed: a citation inside a code block does not count — an indented
|
|
60
|
+
* match would also recognize the examples inside the doctrine doc itself).
|
|
61
|
+
* The regex is fence-blind, documented: a `- Corpus:` line at column 0 inside
|
|
62
|
+
* a fenced block WILL be matched — its refs must join like any other.
|
|
63
|
+
*/
|
|
64
|
+
export function citedCorpusRefs(content: string): string[] {
|
|
65
|
+
const refs: string[] = [];
|
|
66
|
+
for (const m of content.matchAll(CORPUS_LINE_RE)) {
|
|
67
|
+
const line = m[1] ?? "";
|
|
68
|
+
refs.push(...line.split(",").map((item) => item.trim().match(/^([\w][\w-]*)(?:\/|\s|$)/)?.[1] ?? "").filter(Boolean));
|
|
69
|
+
}
|
|
70
|
+
return refs;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* True when the cited ref names a REFERENCE in a recorded corpus read, in
|
|
75
|
+
* either naming convention (`reve` README-style ↔ `reve-recode/` on disk —
|
|
76
|
+
* the `-recode` suffix is normalized away on BOTH sides) and either layout
|
|
77
|
+
* (per-reference directories, or flat `tokens-<ref>.md` files). Comparison is
|
|
78
|
+
* exact after normalization: no prefix matching, so `acme` never joins
|
|
79
|
+
* `acme-corp`, `tokens` never joins every tokens file, and `README` names no
|
|
80
|
+
* reference.
|
|
81
|
+
*/
|
|
82
|
+
export function citationJoinsReads(ref: string, reads: readonly string[]): boolean {
|
|
83
|
+
const norm = (s: string): string => s.replace(/\.md$/, "").replace(/-recode$/, "");
|
|
84
|
+
const target = norm(ref);
|
|
85
|
+
return reads.some((r) => {
|
|
86
|
+
const segs = r.split("/");
|
|
87
|
+
const file = segs[segs.length - 1] ?? "";
|
|
88
|
+
const names = segs.slice(0, -1).map(norm);
|
|
89
|
+
if (file.startsWith("tokens-")) names.push(norm(file.slice("tokens-".length)));
|
|
90
|
+
return names.includes(target);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content rules for design-system.md — the four hard requirements the
|
|
3
|
+
* pipeline validates on the nominal write path (and the opt-in Gemini one).
|
|
4
|
+
* Split out of `gates.ts` to keep both files within the SOLID size budget.
|
|
5
|
+
*/
|
|
6
|
+
import { hasCorpusCitation } from "./corpus";
|
|
7
|
+
|
|
8
|
+
const OKLCH_RE = /oklch\(\s*[\d.]+%?\s+0\.0*[1-9]/;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Forbidden fonts, matched in USAGE contexts — never in free prose: a design
|
|
12
|
+
* system may SAY "Inter" ("## Interaction states", "we never use Inter"), it
|
|
13
|
+
* may not USE it as a font. Covered forms: CSS declarations (`font:`,
|
|
14
|
+
* `font-family:` in any case, multiline values), JS config (`fontFamily:`),
|
|
15
|
+
* custom properties named in English (`--*font*`/`--*ff*`/`--*type*`, value
|
|
16
|
+
* unquoted or quoted), ANY custom property whose value quotes a forbidden
|
|
17
|
+
* name (`--police-texte: "Inter", …` — a non-English property name has no
|
|
18
|
+
* keyword to key off, so the quote itself is the usage signal: a quoted
|
|
19
|
+
* token in a `--*` value is a font-family entry, not prose), `@import`
|
|
20
|
+
* URLs, and Markdown table cells (`| Body | Inter |`). Explicitly OUT of
|
|
21
|
+
* scope (chosen, not overlooked): unquoted prose in bullet lines —
|
|
22
|
+
* distinguishing "usage" from "mention" there is prose parsing, not a
|
|
23
|
+
* regex's job. Accepted trade-off of the quote signal: `--comment: "we
|
|
24
|
+
* never use Inter"` is flagged too — a quoted mention reads identically to
|
|
25
|
+
* a quoted usage once the property name carries no keyword to disambiguate.
|
|
26
|
+
*/
|
|
27
|
+
const FONT_DECL_RE = /(?:font(?:-family)?|fontFamily|--[\w-]*(?:font|ff|type)[\w-]*)\s*:[^;\n]*(?:\n\s*)?[^;\n]*\b(?:Inter|Roboto|Arial|Open Sans)\b/i;
|
|
28
|
+
const FONT_IMPORT_RE = /family=[^&"')]*\b(?:Inter|Roboto|Arial|Open Sans)\b/i;
|
|
29
|
+
const FONT_TABLE_RE = /\|\s*\*{0,2}(?:Inter|Roboto|Arial|Open Sans)\*{0,2}\s*\|/;
|
|
30
|
+
const FONT_QUOTED_PROP_RE = /--[\w-]+\s*:[^;\n]*["'][^;\n]*(?:\n\s*)?[^;\n]*\b(?:Inter|Roboto|Arial|Open Sans)\b/i;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Return the requirements missing from a design-system.md (empty = valid).
|
|
34
|
+
* The corpus citation satisfies the source requirement ONLY when the corpus
|
|
35
|
+
* is actually delivered (`corpusCitationOk`) — absent corpus, the URL is
|
|
36
|
+
* mandatory again, exactly the pre-doctrine behavior (fallback never weaker).
|
|
37
|
+
*/
|
|
38
|
+
export function validateDesignSystem(content: string, corpusCitationOk = false): string[] {
|
|
39
|
+
const missing: string[] = [];
|
|
40
|
+
if (!content.includes("## Design Reference")) missing.push("## Design Reference section");
|
|
41
|
+
if (!/https?:\/\//.test(content) && !(corpusCitationOk && hasCorpusCitation(content))) missing.push("reference URL (https://…) or Corpus citation");
|
|
42
|
+
if (!OKLCH_RE.test(content)) missing.push("oklch() color with chroma > 0");
|
|
43
|
+
if (FONT_DECL_RE.test(content) || FONT_IMPORT_RE.test(content) || FONT_TABLE_RE.test(content) || FONT_QUOTED_PROP_RE.test(content)) missing.push("forbidden font (Inter/Roboto/Arial/Open Sans)");
|
|
44
|
+
return missing;
|
|
45
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
const flagPath = (cacheDir: string): string => join(cacheDir, "design-agent-active");
|
|
5
|
+
|
|
6
|
+
/** The active design agent id (the flag), or "" when no design agent is running. */
|
|
7
|
+
export function activeDesignAgent(cacheDir: string): string {
|
|
8
|
+
const path = flagPath(cacheDir);
|
|
9
|
+
if (!existsSync(path)) return "";
|
|
10
|
+
try {
|
|
11
|
+
return readFileSync(path, "utf8").trim();
|
|
12
|
+
} catch {
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Mark a design agent active (writes its id to the flag file). */
|
|
18
|
+
export function setActiveDesignAgent(cacheDir: string, agentId: string): void {
|
|
19
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
20
|
+
writeFileSync(flagPath(cacheDir), agentId);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Clear the active-design-agent flag. */
|
|
24
|
+
export function clearActiveDesignAgent(cacheDir: string): void {
|
|
25
|
+
try {
|
|
26
|
+
rmSync(flagPath(cacheDir));
|
|
27
|
+
} catch {
|
|
28
|
+
/* already absent */
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Prompt } from "../../prompt/types";
|
|
2
|
+
import { type DesignState, quotaFor } from "./state";
|
|
3
|
+
import { PLUGINS_DIR } from "../file-size";
|
|
4
|
+
import { deny } from "./gates";
|
|
5
|
+
import { isTemplateUrl } from "./template-urls";
|
|
6
|
+
|
|
7
|
+
const SKILLS = `${PLUGINS_DIR}/design-expert/skills`;
|
|
8
|
+
|
|
9
|
+
/** Gate writing design-system.md: requires phase >= 2 and the per-mode screenshot quota. */
|
|
10
|
+
export function designSystemWriteGate(filePath: string, state: DesignState, corpusRequired = false): Prompt | null {
|
|
11
|
+
if (!filePath.endsWith("design-system.md")) return null;
|
|
12
|
+
if (state.currentPhase < 2) {
|
|
13
|
+
return deny(
|
|
14
|
+
`BLOCKED: Cannot write design-system.md at phase ${state.currentPhase}. ` +
|
|
15
|
+
"RECOVERY: 1) Read identity templates from skills/design-system/ " +
|
|
16
|
+
"2) Read design-inspiration.md 3) Read the refs-design corpus (README.md + relevant tokens-*.md) with the Read tool " +
|
|
17
|
+
"4) Screenshot real sector sites with mcp__fuse-browser__browser_screenshot on a LIVE session " +
|
|
18
|
+
"(browser_shots_batch/browser_site_shots also count, 1 credit per call) " +
|
|
19
|
+
"5) Then write design-system.md",
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
const needed = quotaFor(state.mode, corpusRequired);
|
|
23
|
+
if (state.screenshotsCount < needed) {
|
|
24
|
+
return deny(
|
|
25
|
+
`BLOCKED: ${state.screenshotsCount}/${needed} screenshots for mode '${state.mode}'. ` +
|
|
26
|
+
`RECOVERY: 1) Read the refs-design corpus (README.md + tokens-*.md) if not done ` +
|
|
27
|
+
`2) Take ${needed - state.screenshotsCount} more screenshots of REAL sector sites with ` +
|
|
28
|
+
"mcp__fuse-browser__browser_screenshot (browser_shots_batch/browser_site_shots also count, 1 credit per call) " +
|
|
29
|
+
"3) Use browser_open + browser_navigate + browser_screenshot fullPage:true 4) Then write design-system.md",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Gate writing .html/.css: PIPELINE gate (never `uiDesignSkillGate`/`UI_FILE_RE`
|
|
37
|
+
* — owner-scoped out of the skill gate's remit). Requires phase >= 3 AND
|
|
38
|
+
* designSystemValid (same two-condition defense as {@link geminiCreateGate})
|
|
39
|
+
* AND `designSystemFileExists` (caller-computed via `findDesignSystem` — a
|
|
40
|
+
* state can say phase 3 after the file was later deleted, or the file can
|
|
41
|
+
* exist while a fresh agent's state is still phase 0).
|
|
42
|
+
*/
|
|
43
|
+
export function htmlCssPipelineGate(filePath: string, state: DesignState, designSystemFileExists: boolean): Prompt | null {
|
|
44
|
+
if (!/\.(html|css)$/.test(filePath)) return null;
|
|
45
|
+
if (state.currentPhase >= 3 && state.designSystemValid && designSystemFileExists) return null;
|
|
46
|
+
const gaps: string[] = [];
|
|
47
|
+
if (state.currentPhase < 3) gaps.push(`phase ${state.currentPhase}/3`);
|
|
48
|
+
if (!state.designSystemValid) gaps.push("design-system.md not validated");
|
|
49
|
+
if (!designSystemFileExists) gaps.push("design-system.md not found on disk");
|
|
50
|
+
return deny(
|
|
51
|
+
`BLOCKED: cannot write '${filePath}' before the design-system pipeline is complete (${gaps.join(", ")}). ` +
|
|
52
|
+
"RECOVERY: 1) Read identity templates from skills/design-system/ 2) Read design-inspiration.md " +
|
|
53
|
+
"3) Read the refs-design corpus with the Read tool 4) Screenshot real sector sites with " +
|
|
54
|
+
"mcp__fuse-browser__browser_screenshot on a LIVE session 5) Write a valid design-system.md " +
|
|
55
|
+
"6) Then write .html/.css",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Gate Gemini create_frontend: requires phase >= 3 and a validated design system. */
|
|
60
|
+
export function geminiCreateGate(state: DesignState): Prompt | null {
|
|
61
|
+
if (state.currentPhase < 3) {
|
|
62
|
+
return deny(
|
|
63
|
+
"BLOCKED: Cannot call Gemini create_frontend before phase 3. " +
|
|
64
|
+
"RECOVERY: 1) Complete the inspiration phase (refs-design corpus + sector screenshots) 2) Write a valid design-system.md " +
|
|
65
|
+
"3) Then call mcp__gemini-design__create_frontend",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (!state.designSystemValid) {
|
|
69
|
+
return deny(
|
|
70
|
+
"BLOCKED: design-system.md not validated. " +
|
|
71
|
+
"RECOVERY: 1) Ensure design-system.md has ## Design Reference, OKLCH tokens, typography pair, reference URL or Corpus citation " +
|
|
72
|
+
"2) Then retry mcp__gemini-design__create_frontend",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Gate fuse-browser navigate: phase >= 1, inspiration read, no template source in phase 1.
|
|
80
|
+
* The allowlist→denylist inversion is the DELIBERATE doctrine change (not a
|
|
81
|
+
* weakening): the gate was never the proof — the mandatory screenshot is,
|
|
82
|
+
* unchanged and unforgeable in both configurations. Corpus-absent has its
|
|
83
|
+
* counterpart already (pre-doctrine quotas + visible warning); what the
|
|
84
|
+
* denylist cannot cover (sector-browse quality) belongs to the doc, per the
|
|
85
|
+
* rule "the gate requires what is verifiable, the doc prescribes zeal".
|
|
86
|
+
*/
|
|
87
|
+
export function browserNavigateGate(state: DesignState, url: string): Prompt | null {
|
|
88
|
+
if (state.currentPhase < 1) {
|
|
89
|
+
return deny(`BLOCKED: Phase 0 not done. READ: ${SKILLS}/design-system/SKILL.md first.`);
|
|
90
|
+
}
|
|
91
|
+
if (!state.inspirationRead) {
|
|
92
|
+
return deny(
|
|
93
|
+
`BLOCKED: Read the inspiration doctrine first. READ: ${SKILLS}/design-web/references/design-inspiration.md, then the refs-design corpus (README.md + relevant tokens-*.md) with the Read tool`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (state.currentPhase === 1 && url && isTemplateUrl(url)) {
|
|
97
|
+
return deny(
|
|
98
|
+
`BLOCKED: '${url}' is a template source — templates are BUILT to be interchangeable, ` +
|
|
99
|
+
"so extracting taste from them converges every page to the same body. " +
|
|
100
|
+
"RECOVERY: 1) taste comes from the refs-design corpus (README.md + relevant tokens-*.md) " +
|
|
101
|
+
"2) browse 1-2 REAL sites in the client's sector, for register only (award galleries are fine as outbound-link finders)",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|