@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,87 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import type { Prompt } from "../../prompt/types";
|
|
3
|
+
import { loadDesignState, type DesignState } from "./state";
|
|
4
|
+
import { activeDesignAgent } from "./flag";
|
|
5
|
+
import { isUiWrite } from "./skill-gate";
|
|
6
|
+
import { SKILL_TRIGGERS } from "./skill-triggers";
|
|
7
|
+
import { ALLOWED_WRITE, isAllowedWrite } from "./allowed-write";
|
|
8
|
+
|
|
9
|
+
export { validateDesignSystem } from "./design-system-rules";
|
|
10
|
+
export { ALLOWED_WRITE, isAllowedWrite } from "./allowed-write";
|
|
11
|
+
|
|
12
|
+
export const EXEMPT_DIRS: readonly string[] = ["node_modules/", "dist/", "build/", ".claude/"];
|
|
13
|
+
const NAV = "mcp__fuse-browser__browser_navigate";
|
|
14
|
+
const SHOT = "mcp__fuse-browser__browser_screenshot";
|
|
15
|
+
const GEMINI = "mcp__gemini-design__create_frontend";
|
|
16
|
+
|
|
17
|
+
export const deny = (reason: string): Prompt => ({
|
|
18
|
+
kind: "block", title: "Design pipeline", reason,
|
|
19
|
+
actions: ["Follow the design pipeline phases (0→identity, 1→inspiration, 2→screenshots, 3→design-system, 4→generate) in order"],
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/** Block the design agent from writing anything but .html/.css/.md/.json (+ motion*.js). */
|
|
23
|
+
export function htmlCssOnlyGate(filePath: string): Prompt | null {
|
|
24
|
+
if (EXEMPT_DIRS.some((d) => filePath.includes(d)) || isAllowedWrite(filePath)) return null;
|
|
25
|
+
return deny(
|
|
26
|
+
"BLOCKED: design-expert can only write .html, .css, .json, .md, and motion*.js files. " +
|
|
27
|
+
"Framework files (.tsx, .astro, .vue, .swift, .php) must be written by the domain expert " +
|
|
28
|
+
"(astro-expert, react-expert, etc.) AFTER design validation.",
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Block edits to the harness-managed `.design-state-*` files (read-only to the agent). */
|
|
33
|
+
export function stateFileGate(filePath: string): Prompt | null {
|
|
34
|
+
return filePath.includes(".design-state-")
|
|
35
|
+
? deny(
|
|
36
|
+
"BLOCKED: .design-state files are READ-ONLY for you. Hooks update them automatically as you progress. " +
|
|
37
|
+
"Do NOT try to modify them — it will not unblock you. Follow the pipeline: Phase 0→1→2→3→4→5→6 in order.",
|
|
38
|
+
)
|
|
39
|
+
: null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Gate a screenshot: require a scroll since the last navigate (lazy-load content). */
|
|
43
|
+
export function screenshotScrollGate(state: DesignState): Prompt | null {
|
|
44
|
+
return state.scrolledSinceNav
|
|
45
|
+
? null
|
|
46
|
+
: deny("BLOCKED: scroll the page before a screenshot — browser_scroll to:'end', wait, scroll back, then fullPage screenshot.");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The Gemini design gates are OPT-IN: off unless `FUSE_DESIGN_GEMINI` is `1`/`true`. */
|
|
50
|
+
export function geminiEnabled(): boolean {
|
|
51
|
+
const v = process.env.FUSE_DESIGN_GEMINI;
|
|
52
|
+
return v === "1" || v === "true";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Inputs for {@link designPassNotice} — primitives only, so policy stays runtime-free. */
|
|
56
|
+
export interface PassNoticeInput { agentId: string; tool: string; filePath: string; content: string; url: string; phase: "pre" | "post" }
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Python-parity pass notices (`hook_output.allow_pass`/`post_pass`): the
|
|
60
|
+
* user-visible `script: detail` lines the old plugins printed on EVERY passing
|
|
61
|
+
* design hook. Call AFTER the gates allowed the event; returns an `inform`
|
|
62
|
+
* Prompt carrying only `userMessage` (for the human, never agent context), or
|
|
63
|
+
* null when nothing applies. The check-design-skill line fires for ANY agent
|
|
64
|
+
* on a UI write; the rest only inside the active design-agent context.
|
|
65
|
+
*/
|
|
66
|
+
export function designPassNotice(i: PassNoticeInput & { corpusMissing?: boolean }, cacheDir: string): Prompt | null {
|
|
67
|
+
const isWrite = i.tool === "Write" || i.tool === "Edit";
|
|
68
|
+
const fp = i.filePath;
|
|
69
|
+
const lines: string[] = [];
|
|
70
|
+
if (i.phase === "pre" && isUiWrite(i.tool, fp, i.content)) {
|
|
71
|
+
const domains = SKILL_TRIGGERS.filter((t) => t.patterns.some((re) => re.test(i.content))).map((t) => t.skill).join(", ");
|
|
72
|
+
lines.push(`check-design-skill: pass (domain: ${domains || "base"})`);
|
|
73
|
+
}
|
|
74
|
+
const state = i.agentId && activeDesignAgent(cacheDir) === i.agentId ? loadDesignState(cacheDir, i.agentId) : null;
|
|
75
|
+
if (state && i.phase === "pre") {
|
|
76
|
+
if (i.corpusMissing) lines.push("design-corpus: refs-design corpus not found — corpus requirement waived (screenshot-only fallback, install defect)");
|
|
77
|
+
if (isWrite && fp && !EXEMPT_DIRS.some((d) => fp.includes(d)) && ALLOWED_WRITE.test(fp)) lines.push(`enforce-html-css-only: allowed: ${basename(fp)}`);
|
|
78
|
+
if (i.tool === NAV) lines.push(`check-inspiration-read: pass (${i.url})`);
|
|
79
|
+
if (i.tool === GEMINI && geminiEnabled()) lines.push("validate-design-system: design-system.md ok");
|
|
80
|
+
if (isWrite || i.tool === NAV || i.tool === SHOT || i.tool === GEMINI) lines.push(`pipeline-gate: phase ${state.currentPhase} ok`);
|
|
81
|
+
}
|
|
82
|
+
if (state && i.phase === "post" && isWrite) {
|
|
83
|
+
if (basename(fp) === "design-system.md") lines.push("validate-design: design-system.md → phase 3");
|
|
84
|
+
else if (/\.(tsx|jsx|css)$/.test(fp)) lines.push("validate-design: design ok");
|
|
85
|
+
}
|
|
86
|
+
return lines.length ? { kind: "inform", title: "Design pipeline", reason: "", userMessage: lines.join("\n") } : null;
|
|
87
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module screenshot-tools
|
|
3
|
+
* Screenshot-credit tool set (C1), split out of `runtime/design-helpers.ts` to
|
|
4
|
+
* keep that file within the SOLID size budget (SRP).
|
|
5
|
+
* @packageDocumentation
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Screenshot-credit tools: the single-shot capture PLUS the two fuse-browser
|
|
10
|
+
* batch capture primitives (`browser_shots_batch`, `browser_site_shots`) — a
|
|
11
|
+
* batch call that captures N urls/viewports must credit the quota too, or
|
|
12
|
+
* capturing efficiently would make the quota infranchissable. LIMITATION
|
|
13
|
+
* (deliberate, documented): this repo's cross-harness `NormalizedEvent` (see
|
|
14
|
+
* `runtime/normalize.ts`) never surfaces `tool_response` for any tool — by
|
|
15
|
+
* design, since its shape is undocumented for MCP tools and harness-specific
|
|
16
|
+
* for built-ins — so no per-call item count reaches this gate. A batch call
|
|
17
|
+
* therefore credits EXACTLY 1, same as a unit screenshot, never the N it may
|
|
18
|
+
* have captured.
|
|
19
|
+
*/
|
|
20
|
+
export const SHOT_TOOLS: ReadonlySet<string> = new Set([
|
|
21
|
+
"mcp__fuse-browser__browser_screenshot",
|
|
22
|
+
"mcp__fuse-browser__browser_shots_batch",
|
|
23
|
+
"mcp__fuse-browser__browser_site_shots",
|
|
24
|
+
]);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module design/skill-evidence
|
|
3
|
+
* IO side of the UI design-skill gate: read the verified session track and
|
|
4
|
+
* derive {@link DesignEvidence} (read references + doc-consulted) for
|
|
5
|
+
* {@link uiDesignSkillGate}. Kept separate from the pure gate logic (SRP).
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
import type { SessionTrack } from "../../tracking/session-state";
|
|
9
|
+
import { trackJournalEnabled } from "../../tracking/store";
|
|
10
|
+
import { readTrackSync } from "../../tracking/track-compact";
|
|
11
|
+
import { defaultStateDir, trackFile } from "../../runtime/paths";
|
|
12
|
+
import { isDocConsulted } from "../../freshness/doc-helpers";
|
|
13
|
+
import type { DesignEvidence } from "./skill-gate";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Read the verified session track (snapshot ⊕ journal) and derive
|
|
17
|
+
* {@link DesignEvidence}. Fail-closed on a missing/corrupt/tampered track (no
|
|
18
|
+
* evidence → the gate blocks).
|
|
19
|
+
* @param sessionId - the Claude session id.
|
|
20
|
+
* @param cwd - the project root (selects the per-project state dir).
|
|
21
|
+
* @param baseDir - override the track base dir (tests); defaults to the project state dir.
|
|
22
|
+
*/
|
|
23
|
+
export function collectDesignEvidence(sessionId: string, cwd: string, baseDir: string = defaultStateDir(cwd)): DesignEvidence {
|
|
24
|
+
let track: SessionTrack | null = null;
|
|
25
|
+
try {
|
|
26
|
+
track = readTrackSync(trackFile(sessionId, baseDir), trackJournalEnabled());
|
|
27
|
+
} catch {
|
|
28
|
+
track = null;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
refsRead: track?.refsRead ?? [],
|
|
32
|
+
docConsulted: track ? isDocConsulted(track.authorizations, sessionId) : false,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module design/skill-gate
|
|
3
|
+
* UI design-skill gate — ports `check-design-skill.py` WITHOUT requiring Gemini.
|
|
4
|
+
*
|
|
5
|
+
* Fires for ANY agent writing a UI file (.tsx/.jsx/.scss/.vue/.svelte) in a
|
|
6
|
+
* UI-ish path (or with Tailwind classes). Blocks unless BOTH: (1) a design skill
|
|
7
|
+
* reference was read this session (reuses the `refsRead` track), and (2) both
|
|
8
|
+
* Context7 AND Exa were consulted via {@link isDocConsulted}.
|
|
9
|
+
* Gemini is NEVER a condition — doc research alone satisfies the gate.
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
import type { Prompt } from "../../prompt/types";
|
|
13
|
+
import { missingDomainSkills } from "./skill-triggers";
|
|
14
|
+
|
|
15
|
+
/** UI source files the gate polices (.html/.css are handled by the pipeline gate, not here). */
|
|
16
|
+
const UI_FILE_RE = /\.(tsx|jsx|scss|vue|svelte)$/;
|
|
17
|
+
/** Generated/vendored paths that never need a design skill (match at start or mid-path). */
|
|
18
|
+
const EXEMPT_RE = /(^|\/)(node_modules|dist|build)\/|\.claude\//;
|
|
19
|
+
/** UI-intent path fragments (ports the Python `UI_PATH_PATTERNS`). */
|
|
20
|
+
const UI_PATH_RE = /(components|ui|styles|page|layout|content|view|feature|section|hero|footer|header|sidebar|nav|modal|dialog)/;
|
|
21
|
+
/** Tailwind utility usage in JSX `className` (ports the Python `has_jsx_tailwind`). */
|
|
22
|
+
const TAILWIND_RE = /className\s*=.*(?:flex|grid|p-|m-|bg-|text-|rounded|shadow|border|gap-|w-|h-)/;
|
|
23
|
+
/** A read reference path that counts as "a design skill was consulted". */
|
|
24
|
+
const DESIGN_SKILL_RE = /skills\/(design-method|design-system|design-web|design-webapp|design-motion|design-review|ux-copy|design-ios|design-android)/i;
|
|
25
|
+
|
|
26
|
+
/** Session evidence the gate consumes: read references + whether docs were consulted. */
|
|
27
|
+
export interface DesignEvidence {
|
|
28
|
+
refsRead: readonly string[];
|
|
29
|
+
docConsulted: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** True when this Write/Edit targets a UI file in a UI-ish path or with Tailwind classes. */
|
|
33
|
+
export function isUiWrite(tool: string, filePath: string, content: string): boolean {
|
|
34
|
+
if (tool !== "Write" && tool !== "Edit") return false;
|
|
35
|
+
if (!UI_FILE_RE.test(filePath) || EXEMPT_RE.test(filePath)) return false;
|
|
36
|
+
return UI_PATH_RE.test(filePath) || TAILWIND_RE.test(content);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** True when a design skill reference was read this session (scans the `refsRead` track). */
|
|
40
|
+
export function designSkillRead(refsRead: readonly string[]): boolean {
|
|
41
|
+
return refsRead.some((p) => DESIGN_SKILL_RE.test(p));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const block = (reason: string, action: string): Prompt => ({
|
|
45
|
+
kind: "block", title: "Design skill", reason, actions: [action],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Gate a UI write: require a design-skill read AND any doc source. Returns a
|
|
50
|
+
* blocking {@link Prompt}, or `null` to allow. NEVER requires Gemini.
|
|
51
|
+
* @param tool - the tool name ("Write" | "Edit" | ...).
|
|
52
|
+
* @param filePath - the written file path.
|
|
53
|
+
* @param content - the written content (snippet on Edit, full file on Write).
|
|
54
|
+
* @param ev - the session evidence (refsRead + docConsulted).
|
|
55
|
+
*/
|
|
56
|
+
export function uiDesignSkillGate(tool: string, filePath: string, content: string, ev: DesignEvidence): Prompt | null {
|
|
57
|
+
if (!isUiWrite(tool, filePath, content)) return null;
|
|
58
|
+
if (!designSkillRead(ev.refsRead)) {
|
|
59
|
+
return block(
|
|
60
|
+
"BLOCKED: design skill not consulted before writing UI. Read a design SKILL.md "
|
|
61
|
+
+ "(design-system, design-web, or design-motion), then retry.",
|
|
62
|
+
"Read a design skill reference, then retry",
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
const missing = missingDomainSkills(content, ev.refsRead);
|
|
66
|
+
if (missing.length) {
|
|
67
|
+
return block(
|
|
68
|
+
`BLOCKED: code uses ${missing.join(", ")} but the matching design skill(s) were not consulted. `
|
|
69
|
+
+ `Read the SKILL.md FILE (with the Read tool) for: ${missing.join(", ")}, then retry.`,
|
|
70
|
+
`Read design skill(s): ${missing.join(", ")}, then retry`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (!ev.docConsulted) {
|
|
74
|
+
return block(
|
|
75
|
+
"BLOCKED: no documentation consulted. Use BOTH mcp__context7__query-docs AND "
|
|
76
|
+
+ "mcp__exa__web_search_exa, or a web fallback alone (WebSearch/WebFetch). Gemini is NOT required.",
|
|
77
|
+
"Consult Context7+Exa, or web docs alone (Gemini not required), then retry",
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module design/skill-triggers
|
|
3
|
+
* Domain-specific design-skill detection: maps written code patterns to the
|
|
4
|
+
* precise design SKILL.md that must have been consulted. Ports the Python
|
|
5
|
+
* `design_skill_triggers.py` `SKILL_TRIGGERS` table + `detect_required_skills`.
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** A domain skill + the code patterns that require it + its `refsRead` path fragment. */
|
|
10
|
+
export interface SkillTrigger {
|
|
11
|
+
/** Skill id for the block message, e.g. "design-motion". */
|
|
12
|
+
skill: string;
|
|
13
|
+
/** Path fragment proving the skill was read (matched against `refsRead`). */
|
|
14
|
+
readFragment: string;
|
|
15
|
+
/** Code patterns that trigger the requirement (case-insensitive). */
|
|
16
|
+
patterns: readonly RegExp[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Domain skill triggers (ported 1:1 from `design_skill_triggers.py`). */
|
|
20
|
+
export const SKILL_TRIGGERS: readonly SkillTrigger[] = [
|
|
21
|
+
{ skill: "design-web", readFragment: "design-web", patterns: [
|
|
22
|
+
/className\s*=\s*["{]/i, /<(div|section|main|header|footer|nav|aside)\s/i,
|
|
23
|
+
/flex|grid|gap-|p-|m-|bg-|text-|rounded|shadow|border/i,
|
|
24
|
+
/(Button|Card|Dialog|Sheet|Input|Select|Table)\b/i,
|
|
25
|
+
/cva|class-variance-authority|variants\b/i, /VariantProps|variant.*:\s*\{/i,
|
|
26
|
+
/children|slots|asChild|Slot|render.?prop/i, /forwardRef|React\.cloneElement|compound/i,
|
|
27
|
+
] },
|
|
28
|
+
{ skill: "design-system", readFragment: "design-system", patterns: [
|
|
29
|
+
/--(\w+-)+color:|:root\s*\{|@theme\b/i, /design.?system|token|palette|typography.?scale/i,
|
|
30
|
+
/--(\w+)-(foreground|background|primary|muted|accent):/i, /cssVariables|themeConfig|colorScheme/i,
|
|
31
|
+
/dark:|prefers-color-scheme|next-themes|useTheme/i, /ThemeProvider|data-theme|color-scheme/i,
|
|
32
|
+
/sm:|md:|lg:|xl:|2xl:/i, /@container|container-type|@media/i, /clamp\(|fluid|min-width:/i,
|
|
33
|
+
] },
|
|
34
|
+
{ skill: "design-motion", readFragment: "design-motion", patterns: [
|
|
35
|
+
/motion\.|framer-motion|animate|variants/i, /whileHover|whileTap|AnimatePresence|transition/i,
|
|
36
|
+
/@keyframes\b|animation:\s/i, /backdrop-blur|bg-.*\/([\d]+)|glass/i, /backdrop-filter:\s*blur/i,
|
|
37
|
+
/hover:|focus:|active:|disabled:|focus-visible:/i, /data-\[state=|data-\[disabled\]/i,
|
|
38
|
+
/bg-gradient|from-|via-|to-/i, /radial-gradient|conic-gradient|bg-\[url/i, /blur-.*xl|opacity-|mix-blend/i,
|
|
39
|
+
] },
|
|
40
|
+
{ skill: "design-review", readFragment: "design-review", patterns: [
|
|
41
|
+
/aria-|role=|sr-only|tabIndex|alt=/i, /WCAG|a11y|contrast|screen.?reader/i,
|
|
42
|
+
] },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/** Return the domain skills required by `content` but NOT present in `refsRead`. */
|
|
46
|
+
export function missingDomainSkills(content: string, refsRead: readonly string[]): string[] {
|
|
47
|
+
return SKILL_TRIGGERS
|
|
48
|
+
.filter((t) => t.patterns.some((re) => re.test(content)))
|
|
49
|
+
.filter((t) => !refsRead.some((p) => p.includes(t.readFragment)))
|
|
50
|
+
.map((t) => t.skill);
|
|
51
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { existsSync, readFileSync, renameSync, readdirSync, statSync, rmSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { atomicWrite } from "../../util/json-io";
|
|
4
|
+
|
|
5
|
+
/** Pipeline mode — drives the screenshot quota and phase expectations. */
|
|
6
|
+
export type DesignMode = "full" | "page" | "component";
|
|
7
|
+
|
|
8
|
+
/** The design-agent pipeline state-machine snapshot (phases 0→4). */
|
|
9
|
+
export interface DesignState {
|
|
10
|
+
agentId: string;
|
|
11
|
+
mode: DesignMode;
|
|
12
|
+
currentPhase: number;
|
|
13
|
+
phasesCompleted: string[];
|
|
14
|
+
inspirationRead: boolean;
|
|
15
|
+
scrolledSinceNav: boolean;
|
|
16
|
+
screenshotsCount: number;
|
|
17
|
+
/** Distinct refs-design files read (paths relative to the corpus root), deduped. */
|
|
18
|
+
corpusReads: string[];
|
|
19
|
+
designSystemExists: boolean;
|
|
20
|
+
designSystemValid: boolean;
|
|
21
|
+
geminiCalls: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Minimum fuse-browser screenshots required before writing design-system.md, per mode (corpus present). */
|
|
25
|
+
export const MIN_SCREENSHOTS: Readonly<Record<DesignMode, number>> = { full: 2, page: 1, component: 1 };
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Screenshot quotas when the refs-design corpus is ABSENT (install defect →
|
|
29
|
+
* fail-open): exactly today's effective behavior, so the fallback can never be
|
|
30
|
+
* weaker than the pre-doctrine gate. Component is 1 explicitly — the effective
|
|
31
|
+
* quota today (the first screenshot trips `1 >= 0`), no longer a hidden quirk.
|
|
32
|
+
*/
|
|
33
|
+
export const MIN_SCREENSHOTS_NO_CORPUS: Readonly<Record<DesignMode, number>> = { full: 4, page: 2, component: 1 };
|
|
34
|
+
|
|
35
|
+
/** Screenshot quota for `mode` — the SINGLE choice point between the two tables, driven by corpus availability. */
|
|
36
|
+
export function quotaFor(mode: DesignMode, corpusRequired: boolean): number {
|
|
37
|
+
return (corpusRequired ? MIN_SCREENSHOTS : MIN_SCREENSHOTS_NO_CORPUS)[mode];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const stateFile = (cacheDir: string, agentId: string): string => join(cacheDir, `.design-state-${agentId}.json`);
|
|
41
|
+
|
|
42
|
+
/** Load the design state for `agentId`, or null when absent/corrupt (fail-open). */
|
|
43
|
+
export function loadDesignState(cacheDir: string, agentId: string): DesignState | null {
|
|
44
|
+
const path = stateFile(cacheDir, agentId);
|
|
45
|
+
if (!existsSync(path)) return null;
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as DesignState;
|
|
48
|
+
// Retrocompat: states persisted before corpusReads existed load with an
|
|
49
|
+
// empty list (never undefined — an undefined count would silently
|
|
50
|
+
// fail-open or fail-closed the corpus conjunction downstream).
|
|
51
|
+
return { ...parsed, corpusReads: parsed.corpusReads ?? [] };
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Persist the design state under its agent id. */
|
|
58
|
+
export function saveDesignState(cacheDir: string, state: DesignState): void {
|
|
59
|
+
atomicWrite(stateFile(cacheDir, state.agentId), JSON.stringify(state, null, 2));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Build the initial state for a design agent starting a run. */
|
|
63
|
+
export function initDesignState(agentId: string, mode: DesignMode, designSystemExists: boolean): DesignState {
|
|
64
|
+
return {
|
|
65
|
+
agentId, mode, currentPhase: 0, phasesCompleted: [], inspirationRead: false,
|
|
66
|
+
scrolledSinceNav: false, screenshotsCount: 0, corpusReads: [], designSystemExists, designSystemValid: false, geminiCalls: 0,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Archive the active state file (timestamp suffix) and drop archives older than 7 days. */
|
|
71
|
+
export function cleanupDesignStates(cacheDir: string, agentId: string, stamp: string, now: number): void {
|
|
72
|
+
if (agentId) {
|
|
73
|
+
const src = stateFile(cacheDir, agentId);
|
|
74
|
+
if (existsSync(src)) renameSync(src, join(cacheDir, `.design-state-${agentId}-${stamp}.json`));
|
|
75
|
+
}
|
|
76
|
+
let entries: string[];
|
|
77
|
+
try {
|
|
78
|
+
entries = readdirSync(cacheDir);
|
|
79
|
+
} catch {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const cutoff = now - 7 * 86_400_000;
|
|
83
|
+
for (const name of entries) {
|
|
84
|
+
if (!name.startsWith(".design-state-")) continue;
|
|
85
|
+
const path = join(cacheDir, name);
|
|
86
|
+
try {
|
|
87
|
+
if (statSync(path).mtimeMs < cutoff) rmSync(path);
|
|
88
|
+
} catch {
|
|
89
|
+
/* ignore unlink races */
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template-platform detection for the phase-1 navigate gate
|
|
3
|
+
* (design-inspiration.md §FORBIDDEN Navigation Targets). Two rules, because
|
|
4
|
+
* neither alone matches the doctrine:
|
|
5
|
+
* - template SHOPS are banned outright, on the host (subdomains included);
|
|
6
|
+
* - the path rule (/templates, /themes) applies ONLY to real product
|
|
7
|
+
* platforms that also host a template catalog — never to the whole web,
|
|
8
|
+
* or sothebys.com/en/marketplace (register research) would be denied.
|
|
9
|
+
* The host is normalized (lowercase, port, userinfo, trailing dot stripped).
|
|
10
|
+
* HONEST SCOPE: this is a LEXICAL filter — it follows no redirect, is not and
|
|
11
|
+
* will never be exhaustive, and it is NOT the taste gate: the source of taste
|
|
12
|
+
* is the refs-design corpus. This list exists to catch the reflex, not to map
|
|
13
|
+
* the template economy. Frozen by owner decision (C5): no host or pattern may
|
|
14
|
+
* be added or removed without re-opening that decision.
|
|
15
|
+
* Award galleries are deliberately absent: they pass, as outbound-link finders.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Template shops: banned outright, any path (subdomains included). */
|
|
19
|
+
const TEMPLATE_HOSTS: readonly string[] = [
|
|
20
|
+
"themeforest.net", "themes.shopify.com", "marketplace.atlassian.com",
|
|
21
|
+
"templatemonster.com", "creativemarket.com", "ui8.net", "html5up.net",
|
|
22
|
+
"colorlib.com", "bootstrapmade.com", "startbootstrap.com", "cruip.com",
|
|
23
|
+
"uideck.com", "envato.com", "tailwindui.com",
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/** Real-product platforms whose template CATALOGS are banned via the path rule. */
|
|
27
|
+
const PLATFORM_HOSTS: readonly string[] = [
|
|
28
|
+
"framer.com", "webflow.com", "vercel.com", "astro.build", "squarespace.com", "wix.com",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Template-catalog path segments — evaluated ONLY on PLATFORM_HOSTS, and
|
|
33
|
+
* deliberately WITHOUT `/marketplace`: those platforms run real, non-template
|
|
34
|
+
* marketplaces (vercel.com/marketplace is infra). Atlassian/Shopify template
|
|
35
|
+
* stores are already covered by the full host ban above.
|
|
36
|
+
*/
|
|
37
|
+
const TEMPLATE_PATH_RE = /^\/(templates|themes)(\/|$)/i;
|
|
38
|
+
|
|
39
|
+
/** Host part of `url`, normalized: lowercase, no port, no userinfo, no trailing dot. */
|
|
40
|
+
function hostOf(url: string): string {
|
|
41
|
+
return (/^https?:\/\/(?:[^@/?#]*@)?([^/:?#]+)/i.exec(url)?.[1] ?? "").replace(/\.$/, "").toLowerCase();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const under = (host: string, domain: string): boolean => host === domain || host.endsWith(`.${domain}`);
|
|
45
|
+
|
|
46
|
+
/** True when `url` points at a banned template source (host rule, then platform path rule). */
|
|
47
|
+
export function isTemplateUrl(url: string): boolean {
|
|
48
|
+
const host = hostOf(url);
|
|
49
|
+
if (!host) return false;
|
|
50
|
+
if (TEMPLATE_HOSTS.some((d) => under(host, d))) return true;
|
|
51
|
+
if (!PLATFORM_HOSTS.some((d) => under(host, d))) return false;
|
|
52
|
+
const path = /^https?:\/\/(?:[^@/?#]*@)?[^/?#]*(\/[^?#]*)/i.exec(url)?.[1] ?? "/";
|
|
53
|
+
return TEMPLATE_PATH_RE.test(path);
|
|
54
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type DesignState, type DesignMode, quotaFor } from "./state";
|
|
2
|
+
import { corpusReady } from "./corpus";
|
|
3
|
+
|
|
4
|
+
/** Infer the pipeline mode from the launch prompt + whether a design-system.md already exists. */
|
|
5
|
+
export function detectMode(prompt: string, designSystemExists: boolean): DesignMode {
|
|
6
|
+
const p = prompt.toLowerCase();
|
|
7
|
+
if (["component", "composant", "snippet"].some((k) => p.includes(k))) return "component";
|
|
8
|
+
return designSystemExists ? "page" : "full";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Re-evaluate the phase-2 conjunction MONOTONICALLY: corpus reads (when the
|
|
13
|
+
* corpus is delivered) AND the screenshot quota must both hold, and the phase
|
|
14
|
+
* is never written downwards (a Read after phase 3 changes nothing).
|
|
15
|
+
*/
|
|
16
|
+
function maybePhase2(state: DesignState, corpusRequired: boolean): DesignState {
|
|
17
|
+
if (state.currentPhase >= 2) return state;
|
|
18
|
+
const corpusOk = !corpusRequired || corpusReady(state.corpusReads, state.mode);
|
|
19
|
+
if (!corpusOk || state.screenshotsCount < quotaFor(state.mode, corpusRequired)) return state;
|
|
20
|
+
return { ...state, currentPhase: 2, phasesCompleted: [...new Set([...state.phasesCompleted, "identity", "research"])] };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Record a screenshot: bump the count, then re-evaluate the phase-2 conjunction. */
|
|
24
|
+
export function recordScreenshot(state: DesignState, corpusRequired: boolean): DesignState {
|
|
25
|
+
return maybePhase2({ ...state, screenshotsCount: state.screenshotsCount + 1 }, corpusRequired);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Record a fuse-browser navigate (resets the scroll-before-screenshot guard). */
|
|
29
|
+
export function recordNavigate(state: DesignState): DesignState {
|
|
30
|
+
return { ...state, scrolledSinceNav: false };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Record a fuse-browser scroll (satisfies the scroll-before-screenshot guard). */
|
|
34
|
+
export function recordScroll(state: DesignState): DesignState {
|
|
35
|
+
return { ...state, scrolledSinceNav: true };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Mark the design system validated and advance to phase 3 (after a passing create_frontend check). */
|
|
39
|
+
export function recordValidDesignSystem(state: DesignState): DesignState {
|
|
40
|
+
return {
|
|
41
|
+
...state, designSystemExists: true, designSystemValid: true,
|
|
42
|
+
currentPhase: Math.max(state.currentPhase, 3),
|
|
43
|
+
phasesCompleted: [...new Set([...state.phasesCompleted, "design-system"])],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Record a corpus read: distinct files only (a re-read never inflates the
|
|
49
|
+
* list), then re-evaluate the phase-2 conjunction. `relPath` is relative to
|
|
50
|
+
* the corpus root, keeping the persisted state small and prefix-independent.
|
|
51
|
+
*/
|
|
52
|
+
export function recordCorpusRead(state: DesignState, relPath: string, corpusRequired: boolean): DesignState {
|
|
53
|
+
return maybePhase2({ ...state, corpusReads: [...new Set([...state.corpusReads, relPath])] }, corpusRequired);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Record a skill-file Read: reading the identity templates enters phase 1 (browsing
|
|
58
|
+
* allowed); reading the inspiration catalog satisfies the browse prerequisite.
|
|
59
|
+
*/
|
|
60
|
+
export function recordRead(state: DesignState, filePath: string, corpusRequired = false): DesignState {
|
|
61
|
+
const next: DesignState = { ...state };
|
|
62
|
+
if (filePath.includes("design-system/SKILL.md")) {
|
|
63
|
+
next.currentPhase = Math.max(state.currentPhase, 1);
|
|
64
|
+
next.phasesCompleted = [...new Set([...state.phasesCompleted, "identity"])];
|
|
65
|
+
}
|
|
66
|
+
if (filePath.includes("design-inspiration")) next.inspirationRead = true;
|
|
67
|
+
return maybePhase2(next, corpusRequired);
|
|
68
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { ProjectType } from "./detect-project";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detect the project type from the cwd, reproducing the legacy Python logic:
|
|
7
|
+
* package.json containing "next" → nextjs, else "react" → react; else
|
|
8
|
+
* composer.json+artisan → laravel; else Package.swift / *.xcodeproj → swift;
|
|
9
|
+
* else generic.
|
|
10
|
+
* @param cwd - Project root to scan.
|
|
11
|
+
* @returns The detected project type label.
|
|
12
|
+
*/
|
|
13
|
+
export function detectClaudeMdProjectType(cwd: string): ProjectType {
|
|
14
|
+
const pkg = join(cwd, "package.json");
|
|
15
|
+
if (existsSync(pkg)) {
|
|
16
|
+
try {
|
|
17
|
+
const content = readFileSync(pkg, "utf-8");
|
|
18
|
+
if (content.includes("next")) return "nextjs";
|
|
19
|
+
if (content.includes("react")) return "react";
|
|
20
|
+
} catch {
|
|
21
|
+
/* unreadable package.json → fall through */
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (existsSync(join(cwd, "composer.json")) && existsSync(join(cwd, "artisan"))) return "laravel";
|
|
25
|
+
if (existsSync(join(cwd, "Package.swift"))) return "swift";
|
|
26
|
+
try {
|
|
27
|
+
if (readdirSync(cwd).some((f) => f.endsWith(".xcodeproj"))) return "swift";
|
|
28
|
+
} catch {
|
|
29
|
+
/* unreadable dir → ignore */
|
|
30
|
+
}
|
|
31
|
+
return "generic";
|
|
32
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { dirname, resolve } from "node:path";
|
|
2
|
+
import { maskCommentsAndStrings } from "./conventions/strip";
|
|
3
|
+
import { nearestManifestDir, projectCaps, type Cap } from "./nearest-manifest";
|
|
4
|
+
|
|
5
|
+
/** File-derived signal: a definitive non-JS framework, or a JS-family hint. */
|
|
6
|
+
interface FileSignal {
|
|
7
|
+
/** Definitive framework from extension/content (non-JS) — wins outright. */
|
|
8
|
+
definitive?: string;
|
|
9
|
+
/** JS-family hint needing project caps to confirm (react/nextjs/tanstack-start). */
|
|
10
|
+
jsHint?: "react" | "nextjs" | "tanstack-start";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** react markers in a plain .ts/.js file (avoids TS-generic false positives). */
|
|
14
|
+
const REACT_CONTENT = /from ['"]react['"]|\buse(State|Effect|Context|Ref|Memo|Callback|Reducer|LayoutEffect)\b|className=/;
|
|
15
|
+
/** next.js markers in content. */
|
|
16
|
+
const NEXT_CONTENT = /use client|use server|NextRequest|NextResponse|from ['"]next|getServerSideProps|getStaticProps/;
|
|
17
|
+
/** next.js route-file conventions. */
|
|
18
|
+
const NEXT_ROUTE = /(page|layout|loading|error|route|middleware)\.(ts|tsx|js|jsx)$/;
|
|
19
|
+
/** TanStack Start markers (file routes, server fns, start/router imports). */
|
|
20
|
+
const TANSTACK_CONTENT = /createFileRoute|createServerFn|from ['"]@tanstack\/react-(start|router)/;
|
|
21
|
+
/**
|
|
22
|
+
* C7: Tailwind CSS-file signals, verified against official docs (v3 + v4) —
|
|
23
|
+
* `@tailwind` (v3 layer directive, removed in v4), `@apply` (both versions),
|
|
24
|
+
* `@config` (v3 native / v4 compat, JS config path), `@source` (v4 content
|
|
25
|
+
* globbing), `@theme` (v4 design-token block), `theme(` (the function, both
|
|
26
|
+
* versions' syntax). Deliberately NOT a bare utility-class-in-selector scan
|
|
27
|
+
* (e.g. `.flex{}`) — an ordinary hand-written class named `.flex` is exactly
|
|
28
|
+
* the false-positive shape this fix removes; only genuine Tailwind-specific
|
|
29
|
+
* at-rules/functions count.
|
|
30
|
+
*/
|
|
31
|
+
const TAILWIND_CSS_CONTENT = /@tailwind\b|@apply\b|@config\b|@source\b|@theme\b|\btheme\(/;
|
|
32
|
+
|
|
33
|
+
/** Derive the raw signal an extension + content carry (no filesystem access). */
|
|
34
|
+
function fileSignal(filePath: string, content: string): FileSignal {
|
|
35
|
+
if (/\.swift$/.test(filePath)) return { definitive: "swift" };
|
|
36
|
+
if (/\.php$/.test(filePath)) return { definitive: "laravel" };
|
|
37
|
+
if (/\.java$/.test(filePath)) return { definitive: "java" };
|
|
38
|
+
if (/\.go$/.test(filePath)) return { definitive: "go" };
|
|
39
|
+
if (/\.rb$/.test(filePath)) return { definitive: "ruby" };
|
|
40
|
+
if (/\.rs$/.test(filePath)) return { definitive: "rust" };
|
|
41
|
+
// C7: a .css file is "tailwind" only when its CONTENT shows it — bare
|
|
42
|
+
// extension used to classify EVERY .css as tailwind, forcing 6 unrelated
|
|
43
|
+
// skill reads on hand-written CSS. Empty content (file-size-scope.ts's
|
|
44
|
+
// caller) can never prove Tailwind usage, so it falls through to the
|
|
45
|
+
// JS-family checks below (all false for .css) and returns "generic" — the
|
|
46
|
+
// function's existing fail-open contract, not a special case. That caller
|
|
47
|
+
// never reaches here in practice: .css is already outside its
|
|
48
|
+
// FILE_SIZE_CODE_EXT scope, so this is inert there either way.
|
|
49
|
+
if (/\.css$/.test(filePath) && TAILWIND_CSS_CONTENT.test(content)) return { definitive: "tailwind" };
|
|
50
|
+
// JS branch: content signals ride MASKED content (a `// migrated from
|
|
51
|
+
// createServerFn pattern` comment must not flip nextjs → tanstack-start).
|
|
52
|
+
const masked = maskCommentsAndStrings(content, "c");
|
|
53
|
+
const isTs = TANSTACK_CONTENT.test(masked);
|
|
54
|
+
const isNext = !isTs && (NEXT_ROUTE.test(filePath) || NEXT_CONTENT.test(masked));
|
|
55
|
+
if (/\.(tsx|jsx)$/.test(filePath)) return { jsHint: isTs ? "tanstack-start" : isNext ? "nextjs" : "react" };
|
|
56
|
+
if (/\.(ts|js)$/.test(filePath)) {
|
|
57
|
+
if (isTs) return { jsHint: "tanstack-start" };
|
|
58
|
+
if (isNext) return { jsHint: "nextjs" };
|
|
59
|
+
if (REACT_CONTENT.test(masked)) return { jsHint: "react" };
|
|
60
|
+
}
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Reconcile the JS signal against the project's REAL capabilities. */
|
|
65
|
+
function reconcile(caps: Set<Cap>, hint: "react" | "nextjs" | "tanstack-start"): string {
|
|
66
|
+
const nextCap = caps.has("nextjs");
|
|
67
|
+
const reactCap = caps.has("react") || nextCap; // next always ships react at runtime
|
|
68
|
+
if (hint === "tanstack-start") {
|
|
69
|
+
return caps.has("tanstack-start") || caps.has("tanstack-router") ? "tanstack-start" : reactCap ? "react" : "generic";
|
|
70
|
+
}
|
|
71
|
+
if (hint === "nextjs") return nextCap ? "nextjs" : reactCap ? "react" : "generic";
|
|
72
|
+
return reactCap ? "react" : "generic";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Detect a file's framework as the intersection of the REAL project (its nearest
|
|
77
|
+
* manifest's capabilities) and the file's own extension/content signal. A
|
|
78
|
+
* backend `.ts` in a react project is `generic` (no react signal) — fixing the
|
|
79
|
+
* old extension-only default that flagged every `.ts` as react. Fail-open: any
|
|
80
|
+
* error yields `"generic"`, never throws. Return values stay within the label
|
|
81
|
+
* union ("react" | "nextjs" | "tanstack-start" | "laravel" | "swift" |
|
|
82
|
+
* "tailwind" | "java" | "go" | "ruby" | "rust" | "generic") — `tanstack-start`
|
|
83
|
+
* added 2026-07 (owner spec: first-class framework, Next.js-equivalent).
|
|
84
|
+
* @param filePath - Path of the file being written/edited.
|
|
85
|
+
* @param content - Its (incoming) content, for JS content signals.
|
|
86
|
+
* @param cwd - Root to resolve a relative `filePath` against (default cwd).
|
|
87
|
+
* @returns A framework label from the union.
|
|
88
|
+
*/
|
|
89
|
+
export function detectFramework(filePath: string, content: string, cwd: string = process.cwd()): string {
|
|
90
|
+
try {
|
|
91
|
+
const sig = fileSignal(filePath, content);
|
|
92
|
+
if (sig.definitive) return sig.definitive;
|
|
93
|
+
if (!sig.jsHint) return "generic";
|
|
94
|
+
const caps = projectCaps(nearestManifestDir(dirname(resolve(cwd, filePath))));
|
|
95
|
+
return reconcile(caps, sig.jsHint);
|
|
96
|
+
} catch {
|
|
97
|
+
return "generic";
|
|
98
|
+
}
|
|
99
|
+
}
|