@fusengine/harness 0.1.89 → 0.1.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude/index.d.mts +1 -1
- package/dist/adapters/claude/index.mjs +1 -1
- package/dist/adapters/cline/index.mjs +1 -1
- package/dist/adapters/codex/index.d.mts +2 -2
- package/dist/adapters/codex/index.mjs +5 -4
- package/dist/adapters/cursor/index.d.mts +15 -28
- package/dist/adapters/cursor/index.mjs +35 -46
- package/dist/adapters/gemini/index.mjs +1 -1
- package/dist/adapters/hermes/index.d.mts +1 -1
- package/dist/adapters/hermes/index.mjs +1 -1
- package/dist/adapters/kimi/index.d.mts +1 -1
- package/dist/adapters/kimi/index.mjs +1 -1
- package/dist/{claude-5bC8TDAz.mjs → claude-Ckv2_TgP.mjs} +3 -43
- package/dist/cli/bin.mjs +429 -16
- package/dist/cli/index.mjs +1 -1
- package/dist/command-string-CALMTnwN.mjs +42 -0
- package/dist/{evaluate-C1VSI5pb.mjs → evaluate-CMiIqHeH.mjs} +261 -13
- package/dist/{handle-DHhukRdh.mjs → handle-C43gA-Pr.mjs} +737 -150
- package/dist/{hermes-kP6NUQlG.mjs → hermes-B9-p_3IF.mjs} +3 -2
- package/dist/{index-DBgnIZn9.d.mts → index-B8PG82su.d.mts} +1 -1
- package/dist/{index-D4C2-IIc.d.mts → index-SmKYkk2N.d.mts} +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/init/index.d.mts +1 -1
- package/dist/init/index.mjs +1 -1
- package/dist/{kimi-D6X7b8wF.mjs → kimi-G2wcSh5-.mjs} +3 -2
- package/dist/normalize-BjG6unTj.mjs +317 -0
- package/dist/permission-mode-B-SFyR0X.mjs +226 -0
- package/dist/policy/index.d.mts +1 -1
- package/dist/policy/index.mjs +2 -2
- package/dist/prompt/index.d.mts +1 -1
- package/dist/{run-CdIlUtjv.mjs → run-DkrzC0gb.mjs} +1 -1
- package/dist/{run-TAXNRSpD.mjs → run-GHAMqcOn.mjs} +26 -3
- package/dist/runtime/index.d.mts +27 -16
- package/dist/runtime/index.mjs +2 -2
- package/dist/{types-C0-igRKr.d.mts → types-Bh0jEF3_.d.mts} +2 -0
- package/dist/{validate-DZFtAbLP.mjs → validate-KjZ1X9tH.mjs} +1 -1
- package/package.json +3 -2
- package/src/adapters/claude/index.ts +92 -0
- package/src/adapters/cline/index.ts +36 -0
- package/src/adapters/codex/apply-patch.ts +94 -0
- package/src/adapters/codex/index.ts +84 -0
- package/src/adapters/codex/permission-mode.ts +16 -0
- package/src/adapters/cursor/context.ts +46 -0
- package/src/adapters/cursor/events.ts +38 -0
- package/src/adapters/cursor/index.ts +43 -0
- package/src/adapters/cursor/interfaces/types.ts +88 -0
- package/src/adapters/cursor/native-response.ts +158 -0
- package/src/adapters/cursor/normalize.ts +123 -0
- package/src/adapters/cursor/respond.ts +123 -0
- package/src/adapters/gemini/index.ts +36 -0
- package/src/adapters/hermes/index.ts +55 -0
- package/src/adapters/hermes/interfaces/types.ts +26 -0
- package/src/adapters/kimi/index.ts +96 -0
- package/src/adapters/kimi/interfaces/types.ts +55 -0
- package/src/cache/compact.ts +55 -0
- package/src/cache/index.ts +5 -0
- package/src/cache/io.ts +38 -0
- package/src/cache/mcp-response.ts +39 -0
- package/src/cache/mcp-store.ts +70 -0
- package/src/cache/store.ts +93 -0
- package/src/changelog/fetch.ts +79 -0
- package/src/cli/bin.ts +100 -0
- package/src/cli/cursor-event-scanner.ts +175 -0
- package/src/cli/cursor-stdin-reader.ts +65 -0
- package/src/cli/doctor.ts +96 -0
- package/src/cli/hook-io.ts +130 -0
- package/src/cli/hook-sound.ts +49 -0
- package/src/cli/index.ts +1 -0
- package/src/cli/json-primitive-scanner.ts +83 -0
- package/src/cli/run.ts +36 -0
- package/src/cli/scope.ts +31 -0
- package/src/codex-rules/build.ts +63 -0
- package/src/codex-rules/index.ts +8 -0
- package/src/codex-rules/mapping.ts +42 -0
- package/src/codex-rules/rules/absolute-paths.ts +52 -0
- package/src/codex-rules/rules/ask.ts +44 -0
- package/src/codex-rules/rules/chmod-ask.ts +57 -0
- package/src/codex-rules/rules/dd.ts +23 -0
- package/src/codex-rules/rules/diskutil.ts +33 -0
- package/src/codex-rules/rules/mkfs.ts +42 -0
- package/src/codex-rules/rules/privilege.ts +49 -0
- package/src/codex-rules/rules/remote-fetch.ts +31 -0
- package/src/codex-rules/rules/rm-variants.ts +58 -0
- package/src/codex-rules/rules/single-commands.ts +57 -0
- package/src/codex-rules/skip-list.ts +25 -0
- package/src/codex-rules/starlark.ts +46 -0
- package/src/codex-rules/types.ts +36 -0
- package/src/config/dotenv.ts +55 -0
- package/src/config/env.ts +10 -0
- package/src/config/home-dir.ts +36 -0
- package/src/config/index.ts +5 -0
- package/src/config/layout.ts +52 -0
- package/src/config/limits.ts +56 -0
- package/src/config/ttl.ts +30 -0
- package/src/detect/harness.ts +73 -0
- package/src/detect/index.ts +1 -0
- package/src/detect/interfaces/types.ts +18 -0
- package/src/freshness/agent-evidence-record.ts +95 -0
- package/src/freshness/agent-evidence.ts +95 -0
- package/src/freshness/codex-spawn-evidence.ts +91 -0
- package/src/freshness/doc-helpers.ts +90 -0
- package/src/freshness/evidence-harvest-io.ts +72 -0
- package/src/freshness/evidence-harvest.ts +61 -0
- package/src/freshness/explore-tools.ts +95 -0
- package/src/freshness/index.ts +2 -0
- package/src/freshness/query-framework.ts +73 -0
- package/src/freshness/ref-evidence.ts +49 -0
- package/src/freshness/ref-journal.ts +98 -0
- package/src/freshness/trivial-edit-counter.ts +30 -0
- package/src/index.ts +11 -0
- package/src/init/index.ts +2 -0
- package/src/init/run.ts +32 -0
- package/src/init/templates.ts +75 -0
- package/src/memory/gitignore.ts +14 -0
- package/src/memory/index.ts +3 -0
- package/src/memory/registry.ts +39 -0
- package/src/memory/session-roots.ts +99 -0
- package/src/memory/state.ts +58 -0
- package/src/policy/apex-authorization.ts +100 -0
- package/src/policy/apex-gates.ts +91 -0
- package/src/policy/apex-target-steps.ts +73 -0
- package/src/policy/apex-target.ts +49 -0
- package/src/policy/apex-task-context.ts +81 -0
- package/src/policy/apex.ts +94 -0
- package/src/policy/cartographer/build-tree.ts +69 -0
- package/src/policy/cartographer/describe.ts +53 -0
- package/src/policy/cartographer/entry.ts +38 -0
- package/src/policy/cartographer/frontmatter.ts +48 -0
- package/src/policy/cartographer/indicators.ts +34 -0
- package/src/policy/claude-md-context.ts +66 -0
- package/src/policy/conventions/index.ts +11 -0
- package/src/policy/conventions/interfaces.ts +71 -0
- package/src/policy/conventions/langs.ts +53 -0
- package/src/policy/conventions/query.ts +49 -0
- package/src/policy/conventions/react-hooks.ts +18 -0
- package/src/policy/conventions/store-or-query.ts +21 -0
- package/src/policy/conventions/stores.ts +56 -0
- package/src/policy/conventions/strip.ts +87 -0
- package/src/policy/conventions/verdict.ts +38 -0
- package/src/policy/creation-intent.ts +11 -0
- package/src/policy/deny-loop.ts +88 -0
- package/src/policy/design/allowed-write.ts +28 -0
- package/src/policy/design/content-checks.ts +45 -0
- package/src/policy/design/corpus-fs.ts +21 -0
- package/src/policy/design/corpus-resolve-cache.ts +49 -0
- package/src/policy/design/corpus-resolve.ts +99 -0
- package/src/policy/design/corpus.ts +92 -0
- package/src/policy/design/design-system-rules.ts +45 -0
- package/src/policy/design/flag.ts +30 -0
- package/src/policy/design/gates-pipeline.ts +105 -0
- package/src/policy/design/gates.ts +87 -0
- package/src/policy/design/screenshot-tools.ts +24 -0
- package/src/policy/design/skill-evidence.ts +34 -0
- package/src/policy/design/skill-gate.ts +81 -0
- package/src/policy/design/skill-triggers.ts +51 -0
- package/src/policy/design/state.ts +92 -0
- package/src/policy/design/template-urls.ts +54 -0
- package/src/policy/design/transitions.ts +68 -0
- package/src/policy/detect-claude-md-project-type.ts +32 -0
- package/src/policy/detect-framework.ts +99 -0
- package/src/policy/detect-primitive-checks.ts +100 -0
- package/src/policy/detect-primitive-lib.ts +55 -0
- package/src/policy/detect-project.ts +99 -0
- package/src/policy/edit-outcome.ts +53 -0
- package/src/policy/evaluate.ts +81 -0
- package/src/policy/expert-agents.ts +60 -0
- package/src/policy/file-size-scope.ts +32 -0
- package/src/policy/file-size.ts +99 -0
- package/src/policy/framework-solid-exclude.ts +52 -0
- package/src/policy/framework-solid-extended.ts +93 -0
- package/src/policy/framework-solid-gates-systems.ts +99 -0
- package/src/policy/framework-solid-gates.ts +98 -0
- package/src/policy/framework-solid.ts +88 -0
- package/src/policy/gemini-mcp-gate.ts +83 -0
- package/src/policy/git-gates.ts +40 -0
- package/src/policy/guards/bash-command-anchor.ts +40 -0
- package/src/policy/guards/bash-write-patterns.ts +140 -0
- package/src/policy/guards/bash-write-redirects.ts +201 -0
- package/src/policy/guards/bash-write-safe-paths.ts +62 -0
- package/src/policy/guards/bash-write.ts +76 -0
- package/src/policy/guards/context.ts +12 -0
- package/src/policy/guards/index.ts +61 -0
- package/src/policy/guards/install.ts +22 -0
- package/src/policy/guards/interface-separation-ext.ts +41 -0
- package/src/policy/guards/interface-separation.ts +100 -0
- package/src/policy/guards/protected-path.ts +100 -0
- package/src/policy/guards/security.ts +97 -0
- package/src/policy/index.ts +19 -0
- package/src/policy/interfaces/types.ts +46 -0
- package/src/policy/js-gate-route.ts +51 -0
- package/src/policy/lessons/lesson-gate.ts +0 -0
- package/src/policy/lessons/trigger-index.ts +84 -0
- package/src/policy/lessons/types.ts +31 -0
- package/src/policy/module-layout.ts +31 -0
- package/src/policy/nearest-manifest.ts +75 -0
- package/src/policy/never-approval.ts +95 -0
- package/src/policy/patterns.ts +80 -0
- package/src/policy/seo/validate.ts +51 -0
- package/src/policy/shadcn-project.ts +22 -0
- package/src/policy/shadcn-skill-gate.ts +97 -0
- package/src/policy/shell-read-refs.ts +96 -0
- package/src/policy/skill-path.ts +46 -0
- package/src/policy/skill-patterns/laravel.ts +40 -0
- package/src/policy/skill-patterns/nextjs.ts +29 -0
- package/src/policy/skill-patterns/react.ts +27 -0
- package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
- package/src/policy/skill-patterns/shadcn.ts +66 -0
- package/src/policy/skill-patterns/swift.ts +30 -0
- package/src/policy/skill-patterns/tailwind.ts +40 -0
- package/src/policy/skill-source.ts +52 -0
- package/src/policy/skill-trigger-patterns.ts +33 -0
- package/src/policy/skill-triggers.ts +108 -0
- package/src/policy/tailwind-skill-gate.ts +63 -0
- package/src/policy/trivial-edits.ts +52 -0
- package/src/policy/verbosity.ts +39 -0
- package/src/prompt/index.ts +1 -0
- package/src/prompt/types.ts +36 -0
- package/src/refs/discover.ts +69 -0
- package/src/refs/frontmatter.ts +24 -0
- package/src/refs/index.ts +4 -0
- package/src/refs/loader.ts +59 -0
- package/src/refs/ref-key.ts +41 -0
- package/src/refs/router.ts +63 -0
- package/src/refs/types.ts +25 -0
- package/src/runtime/activity.ts +99 -0
- package/src/runtime/apply-patch-gate.ts +41 -0
- package/src/runtime/burst-window.ts +21 -0
- package/src/runtime/codex-shell-tool.ts +37 -0
- package/src/runtime/command-string.ts +37 -0
- package/src/runtime/confirm/codex-confirm.ts +126 -0
- package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
- package/src/runtime/confirm/confirm-code.ts +33 -0
- package/src/runtime/confirm/confirm-gate.ts +65 -0
- package/src/runtime/confirm/confirm-irreversible.ts +32 -0
- package/src/runtime/confirm/confirm-pending.ts +34 -0
- package/src/runtime/confirm/confirm-state.ts +56 -0
- package/src/runtime/confirm/confirm-subagent.ts +112 -0
- package/src/runtime/confirm/confirm-submit.ts +38 -0
- package/src/runtime/core-guards-active.ts +84 -0
- package/src/runtime/core-guards-wired.ts +47 -0
- package/src/runtime/deny-loop-store.ts +94 -0
- package/src/runtime/deny-notice.ts +52 -0
- package/src/runtime/design-cache-resolve.ts +51 -0
- package/src/runtime/design-content-gate.ts +95 -0
- package/src/runtime/design-files-gate.ts +114 -0
- package/src/runtime/design-helpers.ts +93 -0
- package/src/runtime/design-lifecycle.ts +39 -0
- package/src/runtime/design-read-credit.ts +28 -0
- package/src/runtime/design.ts +116 -0
- package/src/runtime/dev-context.ts +43 -0
- package/src/runtime/dry-patterns.ts +32 -0
- package/src/runtime/dry.ts +96 -0
- package/src/runtime/fragment-registry.ts +33 -0
- package/src/runtime/framework-skill-gate.ts +79 -0
- package/src/runtime/fs-cleanup.ts +71 -0
- package/src/runtime/gate-apex.ts +81 -0
- package/src/runtime/gate-helpers.ts +52 -0
- package/src/runtime/gate-input.ts +26 -0
- package/src/runtime/gate.ts +128 -0
- package/src/runtime/handle-post.ts +104 -0
- package/src/runtime/handle-pre.ts +137 -0
- package/src/runtime/handle-scope-async.ts +29 -0
- package/src/runtime/handle-types.ts +27 -0
- package/src/runtime/handle.ts +116 -0
- package/src/runtime/home-state.ts +55 -0
- package/src/runtime/index.ts +18 -0
- package/src/runtime/inform.ts +28 -0
- package/src/runtime/inject-budget-recap.ts +47 -0
- package/src/runtime/inject-budget.ts +81 -0
- package/src/runtime/inject-context.ts +85 -0
- package/src/runtime/inject-dedup-exclusive.ts +98 -0
- package/src/runtime/inject-dedup.ts +89 -0
- package/src/runtime/is-agent-tool.ts +19 -0
- package/src/runtime/lifecycle/agent-files.ts +49 -0
- package/src/runtime/lifecycle/agent-memory.ts +82 -0
- package/src/runtime/lifecycle/agent-transcript.ts +80 -0
- package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
- package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
- package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
- package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
- package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
- package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
- package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
- package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +100 -0
- package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
- package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
- package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
- package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
- package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
- package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
- package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
- package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
- package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
- package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
- package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
- package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
- package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
- package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
- package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
- package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
- package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
- package/src/runtime/lifecycle/aipilot/types.ts +72 -0
- package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
- package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
- package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
- package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
- package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
- package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
- package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
- package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
- package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
- package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
- package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
- package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
- package/src/runtime/lifecycle/changelog-research.ts +43 -0
- package/src/runtime/lifecycle/check-file-size.ts +61 -0
- package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
- package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
- package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
- package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
- package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
- package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
- package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
- package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
- package/src/runtime/lifecycle/dispatch.ts +109 -0
- package/src/runtime/lifecycle/failure-lesson.ts +51 -0
- package/src/runtime/lifecycle/index.ts +34 -0
- package/src/runtime/lifecycle/inject-rules.ts +50 -0
- package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
- package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
- package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
- package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
- package/src/runtime/lifecycle/lessons/state.ts +23 -0
- package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
- package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
- package/src/runtime/lifecycle/memory/client.ts +65 -0
- package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
- package/src/runtime/lifecycle/memory/recall.ts +30 -0
- package/src/runtime/lifecycle/memory/salience.ts +31 -0
- package/src/runtime/lifecycle/memory/state.ts +49 -0
- package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
- package/src/runtime/lifecycle/post-compact.ts +38 -0
- package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
- package/src/runtime/lifecycle/post-tracking.ts +40 -0
- package/src/runtime/lifecycle/pre-compact.ts +38 -0
- package/src/runtime/lifecycle/rules-root.ts +83 -0
- package/src/runtime/lifecycle/security/check-skill.ts +63 -0
- package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
- package/src/runtime/lifecycle/security/scan.ts +76 -0
- package/src/runtime/lifecycle/security/skill-state.ts +47 -0
- package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
- package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
- package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
- package/src/runtime/lifecycle/session-end.ts +17 -0
- package/src/runtime/lifecycle/session-start.ts +57 -0
- package/src/runtime/lifecycle/snapshot/board.ts +27 -0
- package/src/runtime/lifecycle/snapshot/format.ts +60 -0
- package/src/runtime/lifecycle/snapshot/git.ts +71 -0
- package/src/runtime/lifecycle/snapshot/index.ts +52 -0
- package/src/runtime/lifecycle/snapshot/version.ts +34 -0
- package/src/runtime/lifecycle/solid-detect.ts +56 -0
- package/src/runtime/lifecycle/stop-core.ts +35 -0
- package/src/runtime/lifecycle/subagent-cache.ts +61 -0
- package/src/runtime/lifecycle/task-completed.ts +94 -0
- package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
- package/src/runtime/lifecycle/teammate-idle.ts +28 -0
- package/src/runtime/lifecycle/tool-failure.ts +24 -0
- package/src/runtime/lifecycle/track-changes.ts +61 -0
- package/src/runtime/lifecycle/validate-solid.ts +70 -0
- package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
- package/src/runtime/lifecycle-bridge.ts +74 -0
- package/src/runtime/mcp-key.ts +44 -0
- package/src/runtime/mcp-tool-name.ts +92 -0
- package/src/runtime/mcp.ts +91 -0
- package/src/runtime/modular.ts +77 -0
- package/src/runtime/normalize.ts +107 -0
- package/src/runtime/notices.ts +91 -0
- package/src/runtime/notification-sound.ts +49 -0
- package/src/runtime/notifications.ts +77 -0
- package/src/runtime/paths.ts +59 -0
- package/src/runtime/post-fanout.ts +41 -0
- package/src/runtime/post-outcome.ts +61 -0
- package/src/runtime/pre-allow.ts +97 -0
- package/src/runtime/precommit.ts +60 -0
- package/src/runtime/prompt-text.ts +38 -0
- package/src/runtime/receipt-capture.ts +34 -0
- package/src/runtime/record.ts +26 -0
- package/src/runtime/respond.ts +92 -0
- package/src/runtime/solid-file-size-gate.ts +58 -0
- package/src/runtime/solid-pre.ts +53 -0
- package/src/runtime/storage.ts +9 -0
- package/src/statusline/colors.ts +31 -0
- package/src/statusline/constants.ts +17 -0
- package/src/statusline/formatters.ts +54 -0
- package/src/statusline/index.ts +4 -0
- package/src/statusline/progress-bar.ts +46 -0
- package/src/tracking/codex-post-failure.ts +73 -0
- package/src/tracking/index.ts +2 -0
- package/src/tracking/integrity.ts +99 -0
- package/src/tracking/one-shot-dedup.ts +34 -0
- package/src/tracking/one-shot-failure.ts +42 -0
- package/src/tracking/one-shot-store.ts +95 -0
- package/src/tracking/one-shot.ts +93 -0
- package/src/tracking/receipts.ts +95 -0
- package/src/tracking/session-state.ts +94 -0
- package/src/tracking/store.ts +96 -0
- package/src/tracking/track-compact.ts +100 -0
- package/src/tracking/track-diff.ts +50 -0
- package/src/tracking/track-journal.ts +97 -0
- package/src/tracking/track-lock-sync.ts +83 -0
- package/src/tracking/track-lock.ts +67 -0
- package/src/util/compact-json.ts +23 -0
- package/src/util/index.ts +2 -0
- package/src/util/json-io.ts +47 -0
- package/src/util/project-root.ts +41 -0
- package/src/util/runtime-io.ts +85 -0
- package/src/util/semver.ts +30 -0
- package/dist/permission-mode-B0MUl7FT.mjs +0 -136
|
@@ -0,0 +1,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
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection checks for Radix UI vs Base UI primitives, ported verbatim from
|
|
3
|
+
* `detect_primitive_checks.py` (the shadcn-expert plugin's standalone
|
|
4
|
+
* detection script — not wired into any hook; called on demand by the agent
|
|
5
|
+
* to decide which shadcn-detection reference to read).
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
/** Package manager inferred from the project's lockfile. */
|
|
12
|
+
export type PackageManager = "bun" | "pnpm" | "yarn" | "npm";
|
|
13
|
+
|
|
14
|
+
/** `grep -rq <pattern> <path>`; true on a match, false on no-match/timeout/error. */
|
|
15
|
+
function grepQuiet(pattern: string, path: string): boolean {
|
|
16
|
+
try {
|
|
17
|
+
execFileSync("grep", ["-rq", pattern, path], { timeout: 10_000, stdio: "ignore" });
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Check `package.json` for UI library deps (weight: 40). Returns `[radix, baseui]`. */
|
|
25
|
+
export function checkPkgJson(root: string, signals: string[]): [number, number] {
|
|
26
|
+
const pkg = join(root, "package.json");
|
|
27
|
+
if (!existsSync(pkg)) return [0, 0];
|
|
28
|
+
let radix = 0;
|
|
29
|
+
let baseui = 0;
|
|
30
|
+
try {
|
|
31
|
+
const content = readFileSync(pkg, "utf8");
|
|
32
|
+
if (content.includes('"@radix-ui/react-')) { radix = 40; signals.push("pkg:radix-ui"); }
|
|
33
|
+
if (content.includes('"@base-ui/react')) { baseui = 40; signals.push("pkg:base-ui"); }
|
|
34
|
+
} catch {
|
|
35
|
+
return [0, 0];
|
|
36
|
+
}
|
|
37
|
+
return [radix, baseui];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Check `components.json` style (weight: 20). Returns `[radix, baseui]`. */
|
|
41
|
+
export function checkComponentsJson(root: string, signals: string[]): [number, number] {
|
|
42
|
+
const cjson = join(root, "components.json");
|
|
43
|
+
if (!existsSync(cjson)) return [0, 0];
|
|
44
|
+
try {
|
|
45
|
+
const data = JSON.parse(readFileSync(cjson, "utf8")) as { style?: string };
|
|
46
|
+
const style = data.style ?? "";
|
|
47
|
+
if (style === "new-york" || style === "default") {
|
|
48
|
+
signals.push(`style:${style}`);
|
|
49
|
+
return [20, 0];
|
|
50
|
+
}
|
|
51
|
+
if (style === "base-vega") {
|
|
52
|
+
signals.push("style:base-vega");
|
|
53
|
+
return [0, 20];
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
return [0, 0];
|
|
57
|
+
}
|
|
58
|
+
return [0, 0];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Dirs scanned for import/data-attribute signals. */
|
|
62
|
+
const SCAN_DIRS: ReadonlyArray<string> = ["src", "components", "app"];
|
|
63
|
+
|
|
64
|
+
function existingScanDirs(root: string): string[] {
|
|
65
|
+
return SCAN_DIRS.map((d) => join(root, d)).filter((d) => {
|
|
66
|
+
try { return statSync(d).isDirectory(); } catch { return false; }
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Scan imports (weight: 25) and data attributes (weight: 15). Returns `[radix, baseui]`. */
|
|
71
|
+
export function scanImportsAndAttrs(root: string, signals: string[]): [number, number] {
|
|
72
|
+
const dirs = existingScanDirs(root);
|
|
73
|
+
let radix = 0;
|
|
74
|
+
let baseui = 0;
|
|
75
|
+
if (dirs.some((d) => grepQuiet("@radix-ui/react-", d))) { radix += 25; signals.push("import:radix"); }
|
|
76
|
+
if (dirs.some((d) => grepQuiet("@base-ui/react", d))) { baseui += 25; signals.push("import:base-ui"); }
|
|
77
|
+
if (dirs.some((d) => grepQuiet("data-state=", d))) { radix += 15; signals.push("attr:data-state"); }
|
|
78
|
+
if (dirs.some((d) => grepQuiet("data-\\[open\\]", d))) { baseui += 15; signals.push("attr:data-[open]"); }
|
|
79
|
+
return [radix, baseui];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Lockfile → package manager + runner, checked in priority order. */
|
|
83
|
+
const LOCKFILES: ReadonlyArray<readonly [string, PackageManager, string]> = [
|
|
84
|
+
["bun.lockb", "bun", "bunx"],
|
|
85
|
+
["bun.lock", "bun", "bunx"],
|
|
86
|
+
["pnpm-lock.yaml", "pnpm", "pnpm dlx"],
|
|
87
|
+
["yarn.lock", "yarn", "yarn dlx"],
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
/** Detect the package manager from lockfiles present at `root` (defaults to npm/npx). */
|
|
91
|
+
export function detectPm(root: string, signals: string[]): [PackageManager, string] {
|
|
92
|
+
for (const [lockfile, pm, runner] of LOCKFILES) {
|
|
93
|
+
if (existsSync(join(root, lockfile))) {
|
|
94
|
+
signals.push(`pm:${pm}`);
|
|
95
|
+
return [pm, runner];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
signals.push("pm:npm");
|
|
99
|
+
return ["npm", "npx"];
|
|
100
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compose the Radix vs Base UI weighted detection, ported verbatim from
|
|
3
|
+
* `detect-primitive-lib.py`'s `main()`. Pure aggregation over
|
|
4
|
+
* {@link checkPkgJson}/{@link checkComponentsJson}/{@link scanImportsAndAttrs}/
|
|
5
|
+
* {@link detectPm} (`./detect-primitive-checks`).
|
|
6
|
+
*/
|
|
7
|
+
import { checkComponentsJson, checkPkgJson, detectPm, scanImportsAndAttrs, type PackageManager } from "./detect-primitive-checks";
|
|
8
|
+
|
|
9
|
+
export type { PackageManager } from "./detect-primitive-checks";
|
|
10
|
+
|
|
11
|
+
/** Which shadcn/ui primitive library the project uses, per the weighted signal score. */
|
|
12
|
+
export type PrimitiveLib = "radix" | "base-ui" | "mixed" | "none";
|
|
13
|
+
|
|
14
|
+
/** Result of {@link detectPrimitiveLib}. */
|
|
15
|
+
export interface PrimitiveDetection {
|
|
16
|
+
primitive: PrimitiveLib;
|
|
17
|
+
confidence: number;
|
|
18
|
+
pm: PackageManager;
|
|
19
|
+
runner: string;
|
|
20
|
+
signals: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Detect whether `root` uses Radix UI or Base UI as its shadcn/ui primitive,
|
|
25
|
+
* from weighted signals (package.json 40, components.json style 20, imports 25,
|
|
26
|
+
* data attributes 15). `"mixed"` when both accumulate signal, `"none"` when
|
|
27
|
+
* neither does.
|
|
28
|
+
* @param root - project root to scan (defaults to `process.cwd()`).
|
|
29
|
+
*/
|
|
30
|
+
export function detectPrimitiveLib(root: string = process.cwd()): PrimitiveDetection {
|
|
31
|
+
const signals: string[] = [];
|
|
32
|
+
const [r1, b1] = checkPkgJson(root, signals);
|
|
33
|
+
const [r2, b2] = checkComponentsJson(root, signals);
|
|
34
|
+
const [r3, b3] = scanImportsAndAttrs(root, signals);
|
|
35
|
+
const [pm, runner] = detectPm(root, signals);
|
|
36
|
+
const radix = r1 + r2 + r3;
|
|
37
|
+
const baseui = b1 + b2 + b3;
|
|
38
|
+
|
|
39
|
+
let primitive: PrimitiveLib;
|
|
40
|
+
let confidence: number;
|
|
41
|
+
if (radix > 0 && baseui > 0) {
|
|
42
|
+
primitive = "mixed";
|
|
43
|
+
confidence = Math.floor((radix + baseui) / 2);
|
|
44
|
+
} else if (radix > baseui) {
|
|
45
|
+
primitive = "radix";
|
|
46
|
+
confidence = radix;
|
|
47
|
+
} else if (baseui > radix) {
|
|
48
|
+
primitive = "base-ui";
|
|
49
|
+
confidence = baseui;
|
|
50
|
+
} else {
|
|
51
|
+
primitive = "none";
|
|
52
|
+
confidence = 0;
|
|
53
|
+
}
|
|
54
|
+
return { primitive, confidence, pm, runner, signals };
|
|
55
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
/** Project types detected from filesystem indicators. */
|
|
5
|
+
export type ProjectType =
|
|
6
|
+
| "nextjs" | "nuxt" | "angular" | "svelte" | "vue" | "react" | "tailwind"
|
|
7
|
+
| "laravel" | "rails" | "django" | "python" | "go" | "rust" | "swift"
|
|
8
|
+
| "java" | "scala" | "elixir" | "ruby" | "generic";
|
|
9
|
+
|
|
10
|
+
/** Keywords that signal a development task (APEX trigger). */
|
|
11
|
+
export const DEV_KEYWORDS: RegExp =
|
|
12
|
+
/\b(implement|create|build|fix|add|refactor|develop|feature|bug|update|modify|change|write|code)\b/i;
|
|
13
|
+
|
|
14
|
+
/** True when the prompt invokes the /apex command. */
|
|
15
|
+
export function isApexCommand(prompt: string): boolean {
|
|
16
|
+
return /(?:^|\s)\/apex|\/fuse-ai-pilot:apex/i.test(prompt);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Modular architecture variants layered on top of the framework. */
|
|
20
|
+
export type ModularArchitecture = "fusecore" | "nextjs-modular" | null;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Detect a project-internal modular architecture (a sub-architecture the
|
|
24
|
+
* framework-level {@link detectProjectType} doesn't capture): Fusengine's
|
|
25
|
+
* FuseCore (Laravel) or a `modules/`-based Next.js layout.
|
|
26
|
+
*/
|
|
27
|
+
export function detectModularArchitecture(dir: string): ModularArchitecture {
|
|
28
|
+
const has = (f: string): boolean => existsSync(join(dir, f));
|
|
29
|
+
if (has("FuseCore") && has("artisan")) return "fusecore";
|
|
30
|
+
if (has("modules") && (has("next.config.js") || has("next.config.ts") || has("next.config.mjs"))) {
|
|
31
|
+
return "nextjs-modular";
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the skill a detected modular architecture forces.
|
|
38
|
+
*
|
|
39
|
+
* Ports the Python `check-nextjs-skill.py` / `check-laravel-skill.py` gates:
|
|
40
|
+
* when the project is detected on disk as a modular architecture, a specific
|
|
41
|
+
* skill is required ('solid-nextjs' for nextjs-modular, 'fusecore' for
|
|
42
|
+
* fusecore). Returns `null` when no modular architecture is detected.
|
|
43
|
+
*
|
|
44
|
+
* @param cwd - Project root directory to scan.
|
|
45
|
+
* @returns The forced skill name, or `null` when none applies.
|
|
46
|
+
*/
|
|
47
|
+
export function requiredArchSkill(cwd: string): string | null {
|
|
48
|
+
switch (detectModularArchitecture(cwd)) {
|
|
49
|
+
case "nextjs-modular":
|
|
50
|
+
return "solid-nextjs";
|
|
51
|
+
case "fusecore":
|
|
52
|
+
return "fusecore";
|
|
53
|
+
default:
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Config file names that mark a project as using Tailwind (v3 JS config or legacy). */
|
|
59
|
+
const TAILWIND_CONFIG_FILES = ["tailwind.config.js", "tailwind.config.ts", "tailwind.config.mjs", "tailwind.config.cjs"];
|
|
60
|
+
|
|
61
|
+
/** True when `package.json` lists `tailwindcss` as a dep (Tailwind v4 CSS-first, no config file). */
|
|
62
|
+
function hasTailwindDependency(dir: string): boolean {
|
|
63
|
+
const pkgPath = join(dir, "package.json");
|
|
64
|
+
if (!existsSync(pkgPath)) return false;
|
|
65
|
+
try {
|
|
66
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as {
|
|
67
|
+
dependencies?: Record<string, string>;
|
|
68
|
+
devDependencies?: Record<string, string>;
|
|
69
|
+
};
|
|
70
|
+
return "tailwindcss" in { ...pkg.dependencies, ...pkg.devDependencies };
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Detect the project type by scanning config files in `dir`. */
|
|
77
|
+
export function detectProjectType(dir: string): ProjectType {
|
|
78
|
+
const has = (f: string): boolean => existsSync(join(dir, f));
|
|
79
|
+
if (has("next.config.js") || has("next.config.ts") || has("next.config.mjs")) return "nextjs";
|
|
80
|
+
if (has("nuxt.config.ts") || has("nuxt.config.js")) return "nuxt";
|
|
81
|
+
if (has("angular.json")) return "angular";
|
|
82
|
+
if (has("svelte.config.js") || has("svelte.config.ts")) return "svelte";
|
|
83
|
+
if (has("vite.config.ts") && has("src/App.vue")) return "vue";
|
|
84
|
+
if (has("vite.config.ts") || has("vite.config.js")) return "react";
|
|
85
|
+
if (TAILWIND_CONFIG_FILES.some(has) || hasTailwindDependency(dir)) return "tailwind";
|
|
86
|
+
if (has("composer.json") && has("artisan")) return "laravel";
|
|
87
|
+
if (has("Gemfile") && has("config/routes.rb")) return "rails";
|
|
88
|
+
if (has("requirements.txt") || has("pyproject.toml") || has("setup.py")) {
|
|
89
|
+
return has("manage.py") ? "django" : "python";
|
|
90
|
+
}
|
|
91
|
+
if (has("go.mod")) return "go";
|
|
92
|
+
if (has("Cargo.toml")) return "rust";
|
|
93
|
+
if (has("Package.swift")) return "swift";
|
|
94
|
+
if (has("pom.xml") || has("build.gradle") || has("build.gradle.kts")) return "java";
|
|
95
|
+
if (has("build.sbt")) return "scala";
|
|
96
|
+
if (has("mix.exs")) return "elixir";
|
|
97
|
+
if (has("Gemfile")) return "ruby";
|
|
98
|
+
return "generic";
|
|
99
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { countLines } from "./file-size";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Count non-overlapping literal occurrences of `needle` in `haystack`. An
|
|
5
|
+
* empty `needle` returns 0 — treated as "not found" by callers, since Edit's
|
|
6
|
+
* `old_string` is never legitimately empty and an empty needle has no
|
|
7
|
+
* well-defined occurrence count.
|
|
8
|
+
* @param haystack - The on-disk file content to search.
|
|
9
|
+
* @param needle - The literal substring to count.
|
|
10
|
+
*/
|
|
11
|
+
function countOccurrences(haystack: string, needle: string): number {
|
|
12
|
+
if (needle === "") return 0;
|
|
13
|
+
let count = 0;
|
|
14
|
+
let from = 0;
|
|
15
|
+
for (;;) {
|
|
16
|
+
const at = haystack.indexOf(needle, from);
|
|
17
|
+
if (at === -1) break;
|
|
18
|
+
count++;
|
|
19
|
+
from = at + needle.length;
|
|
20
|
+
}
|
|
21
|
+
return count;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Compute the line count an Edit would PRODUCE, before it lands on disk —
|
|
26
|
+
* `existingLines − countLines(old_string) + countLines(new_string)`, scaled
|
|
27
|
+
* by how many `old_string` occurrences are actually replaced (1 normally, or
|
|
28
|
+
* every occurrence under `replace_all`). Pure arithmetic on the literal
|
|
29
|
+
* strings — no regex/fuzzy matching, matching the real Edit tool's own
|
|
30
|
+
* exact-substring semantics.
|
|
31
|
+
*
|
|
32
|
+
* Returns `null` (fail-closed — caller must fall back to judging the stale
|
|
33
|
+
* on-disk count alone) when `old_string` is missing/empty, or does not occur
|
|
34
|
+
* in `existingContent` at all: an Edit whose `old_string` doesn't match the
|
|
35
|
+
* real file is stale or adversarial, never a case to special-case an allow for.
|
|
36
|
+
* @param existingContent - Full on-disk content of the file being edited.
|
|
37
|
+
* @param oldString - The Edit's `old_string` (tool_input, unmodified).
|
|
38
|
+
* @param newString - The Edit's `new_string` (tool_input, unmodified).
|
|
39
|
+
* @param replaceAll - The Edit's `replace_all` flag.
|
|
40
|
+
*/
|
|
41
|
+
export function computeEditResultLines(
|
|
42
|
+
existingContent: string,
|
|
43
|
+
oldString: string | undefined,
|
|
44
|
+
newString: string,
|
|
45
|
+
replaceAll: boolean,
|
|
46
|
+
): number | null {
|
|
47
|
+
if (!oldString) return null;
|
|
48
|
+
const occurrences = countOccurrences(existingContent, oldString);
|
|
49
|
+
if (occurrences === 0) return null;
|
|
50
|
+
const times = replaceAll ? occurrences : 1;
|
|
51
|
+
const delta = countLines(newString) - countLines(oldString);
|
|
52
|
+
return countLines(existingContent) + delta * times;
|
|
53
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { isFileSizeScoped, resolveSolidRefFramework } from "./file-size-scope";
|
|
2
|
+
import { countLines, evaluateFileSize } from "./file-size";
|
|
3
|
+
import { computeEditResultLines } from "./edit-outcome";
|
|
4
|
+
import { resolveMaxLines } from "../config/limits";
|
|
5
|
+
import { matchPatterns, GIT_BLOCKED, RALPH_SAFE, isRalphMode } from "./patterns";
|
|
6
|
+
import { runGuards } from "./guards";
|
|
7
|
+
import { isSingleCommand, buildNeverApprovalPrompt, NEVER_SAFE, isSafePushForm } from "./never-approval";
|
|
8
|
+
import { evaluateGitGates } from "./git-gates";
|
|
9
|
+
import type { PolicyContext, PolicyResult } from "./interfaces/types";
|
|
10
|
+
|
|
11
|
+
export type { PolicyContext, PolicyResult } from "./interfaces/types";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Evaluate a single tool-use against the bundled policies, returning a pure
|
|
15
|
+
* decision plus a portable {@link Prompt}. Adapters translate the prompt into
|
|
16
|
+
* their harness's native response (Claude `permissionDecision`, etc.).
|
|
17
|
+
*/
|
|
18
|
+
export function evaluate(ctx: PolicyContext): PolicyResult {
|
|
19
|
+
const guard = runGuards(ctx);
|
|
20
|
+
if (guard) return { decision: "deny", message: guard.reason, prompt: guard };
|
|
21
|
+
// Ralph mode (opt-in via RALPH_MODE) exempts the SAFE git commands from the git
|
|
22
|
+
// block/ask gates for autonomous runs — parity git-guard.py:48-51. Destructive
|
|
23
|
+
// commands are NOT in RALPH_SAFE, so force-push / reset --hard stay blocked.
|
|
24
|
+
const cmd = ctx.command;
|
|
25
|
+
const ralphSafe = !!cmd && isRalphMode() && RALPH_SAFE.some((s) => cmd.startsWith(s));
|
|
26
|
+
// Codex `approval_policy=never` has no ask channel: for the NEVER_SAFE subset
|
|
27
|
+
// (RALPH_SAFE + "git push"), never destructive (GIT_BLOCKED) and never chained
|
|
28
|
+
// (isSingleCommand — `git commit -m x && git push --force` must NOT slip
|
|
29
|
+
// through), auto-approve with a visible `warn` instead of falling through to
|
|
30
|
+
// GIT_ASK's normal `ask`. isSafePushForm additionally narrows the "git push"
|
|
31
|
+
// entry: it excludes --delete/--mirror/--prune and a `:refspec` remote-delete,
|
|
32
|
+
// none of which GIT_BLOCKED catches (that gate only covers --force). RALPH_MODE
|
|
33
|
+
// takes priority (ralphSafe already silent-allows above this) — its own path
|
|
34
|
+
// stays on RALPH_SAFE, unaffected by this push exemption.
|
|
35
|
+
const neverExempt = !ralphSafe && !!ctx.neverApproval && !!cmd && !matchPatterns(cmd, GIT_BLOCKED) && isSingleCommand(cmd) && NEVER_SAFE.some((s) => cmd.startsWith(s)) && isSafePushForm(cmd);
|
|
36
|
+
if (neverExempt && cmd) {
|
|
37
|
+
const prompt = buildNeverApprovalPrompt(cmd);
|
|
38
|
+
return { decision: "warn", message: prompt.reason, prompt };
|
|
39
|
+
}
|
|
40
|
+
const gitGate = evaluateGitGates(ctx.command, ralphSafe);
|
|
41
|
+
if (gitGate) return gitGate;
|
|
42
|
+
if (ctx.filePath && isFileSizeScoped(ctx.filePath) && ctx.agentType !== "Explore" && ctx.agentType !== "Plan") {
|
|
43
|
+
const incoming = ctx.content !== undefined ? countLines(ctx.content) : 0;
|
|
44
|
+
const existing = ctx.existingLines ?? 0;
|
|
45
|
+
const max = ctx.maxLines ?? resolveMaxLines();
|
|
46
|
+
// A computable Edit outcome bypasses the on-disk-only ceiling below: either
|
|
47
|
+
// the result itself is compliant, or the file was already oversized and
|
|
48
|
+
// this Edit strictly shrinks it (see policy/edit-outcome.ts). Fails closed
|
|
49
|
+
// to the ceiling whenever the outcome isn't computable.
|
|
50
|
+
const editResult = ctx.tool === "Edit" && ctx.existingContent !== undefined && ctx.content !== undefined
|
|
51
|
+
? computeEditResultLines(ctx.existingContent, ctx.oldString, ctx.content, ctx.isReplaceAll === true)
|
|
52
|
+
: null;
|
|
53
|
+
if (editResult !== null && (editResult <= max || editResult < existing)) {
|
|
54
|
+
return { decision: "allow", message: null };
|
|
55
|
+
}
|
|
56
|
+
// Write provides the full new content → judge it. Edit judges the computed
|
|
57
|
+
// outcome when known (closes the old grow-via-Edit hole), else the on-disk file.
|
|
58
|
+
const lines = ctx.tool === "Edit" ? (editResult ?? Math.max(incoming, existing)) : incoming || existing;
|
|
59
|
+
const framework = resolveSolidRefFramework(ctx.filePath);
|
|
60
|
+
// Python parity (enforce-file-size.py:44-57): the block message always
|
|
61
|
+
// reports the pre-existing on-disk count, even for a Write whose new
|
|
62
|
+
// content is itself over the limit — the incoming count only ever gates
|
|
63
|
+
// an early "shrunk to compliant" allow, it's never what gets displayed.
|
|
64
|
+
const displayLines = ctx.tool === "Write" ? ctx.existingLines ?? lines : lines;
|
|
65
|
+
const verdict = evaluateFileSize(lines, max, ctx.filePath, framework, displayLines);
|
|
66
|
+
if (lines > 0 && !verdict.ok) {
|
|
67
|
+
return {
|
|
68
|
+
decision: "deny",
|
|
69
|
+
message: verdict.message,
|
|
70
|
+
prompt: {
|
|
71
|
+
kind: "block",
|
|
72
|
+
title: "SOLID file-size limit",
|
|
73
|
+
reason: verdict.message ?? "",
|
|
74
|
+
actions: [`Split into modules under ${verdict.max} lines (Single Responsibility)`, "Then re-run the write"],
|
|
75
|
+
},
|
|
76
|
+
meta: { framework, lines: verdict.lines, max: verdict.max },
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { decision: "allow", message: null };
|
|
81
|
+
}
|