@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,96 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { extname, resolve, sep } from "node:path";
|
|
3
|
+
import type { Prompt } from "../prompt/types";
|
|
4
|
+
import { DRY_KEYWORDS as KEYWORDS, TS_EXT, TS_PATTERNS, PHP_PATTERNS, EXCLUDE_DIRS } from "./dry-patterns";
|
|
5
|
+
|
|
6
|
+
/** Extract long (>12 char) declared symbol names from new file content. */
|
|
7
|
+
export function extractSymbols(content: string, ext: string): string[] {
|
|
8
|
+
const pats = TS_EXT.has(ext) ? TS_PATTERNS : ext === ".php" ? PHP_PATTERNS : [];
|
|
9
|
+
const names = new Set<string>();
|
|
10
|
+
for (const re of pats) {
|
|
11
|
+
for (const m of content.matchAll(re)) {
|
|
12
|
+
const n = m[1];
|
|
13
|
+
if (n && !KEYWORDS.has(n) && n.length > 12) names.add(n);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return [...names];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** `modules/X/...` -> `"X"`, else `""` (module-boundary key). */
|
|
20
|
+
function moduleOf(path: string): string {
|
|
21
|
+
const parts = path.split(sep);
|
|
22
|
+
const i = parts.indexOf("modules");
|
|
23
|
+
return i >= 0 && i + 1 < parts.length ? (parts[i + 1] ?? "") : "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Verdict from {@link detectDuplication}. */
|
|
27
|
+
export interface DuplicationVerdict {
|
|
28
|
+
names: string[];
|
|
29
|
+
duplicates: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Grep the codebase for existing declarations of the symbols a write introduces,
|
|
34
|
+
* honoring module boundaries (cross-`modules/` matches are ignored). Effectful:
|
|
35
|
+
* shells out to `grep`. Fails open (returns no duplicates) on any grep error,
|
|
36
|
+
* timeout, or no-match — matching the original Python hook.
|
|
37
|
+
*/
|
|
38
|
+
export function detectDuplication(filePath: string, content: string, cwd: string): DuplicationVerdict {
|
|
39
|
+
const ext = extname(filePath).toLowerCase();
|
|
40
|
+
if (!TS_EXT.has(ext) && ext !== ".php") return { names: [], duplicates: [] };
|
|
41
|
+
const names = extractSymbols(content, ext);
|
|
42
|
+
if (!names.length) return { names, duplicates: [] };
|
|
43
|
+
const include = TS_EXT.has(ext)
|
|
44
|
+
? ["--include=*.ts", "--include=*.tsx", "--include=*.js", "--include=*.jsx"]
|
|
45
|
+
: ["--include=*.php"];
|
|
46
|
+
const decl = TS_EXT.has(ext) ? "(function|const|let|class|interface)\\s+" : "(function|class|interface|trait)\\s+";
|
|
47
|
+
const escaped = names.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
48
|
+
const pattern = `${decl}(${escaped})\\b`;
|
|
49
|
+
let out = "";
|
|
50
|
+
try {
|
|
51
|
+
const args = ["-rEl", ...EXCLUDE_DIRS.map((d) => `--exclude-dir=${d}`), ...include, "--", pattern, cwd];
|
|
52
|
+
out = execFileSync("grep", args, { encoding: "utf8", timeout: 1500 });
|
|
53
|
+
} catch {
|
|
54
|
+
return { names, duplicates: [] };
|
|
55
|
+
}
|
|
56
|
+
const self = resolve(filePath);
|
|
57
|
+
const targetMod = moduleOf(filePath);
|
|
58
|
+
const duplicates: string[] = [];
|
|
59
|
+
for (const line of out.split("\n")) {
|
|
60
|
+
const f = line.trim();
|
|
61
|
+
if (!f || resolve(f) === self) continue;
|
|
62
|
+
const dupMod = moduleOf(f);
|
|
63
|
+
if (targetMod && dupMod && dupMod !== targetMod) continue;
|
|
64
|
+
duplicates.push(f);
|
|
65
|
+
}
|
|
66
|
+
return { names, duplicates };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Prompt when a Write/Edit re-declares an existing symbol: a single match is a
|
|
71
|
+
* softer non-blocking "inform" (could be a false positive / same-name coincidence),
|
|
72
|
+
* escalating to a hard "block" once 2+ existing declarations are found. Returns
|
|
73
|
+
* `null` when no duplicate symbol is found at all.
|
|
74
|
+
*/
|
|
75
|
+
export function dryGate(tool: string, filePath: string, content: string | undefined, cwd: string | undefined): Prompt | null {
|
|
76
|
+
if (!cwd || (tool !== "Write" && tool !== "Edit") || !content) return null;
|
|
77
|
+
const dup = detectDuplication(filePath, content, cwd);
|
|
78
|
+
if (dup.duplicates.length === 0) return null;
|
|
79
|
+
const names = dup.names.slice(0, 5).join(", ");
|
|
80
|
+
let files = dup.duplicates.slice(0, 3).join(", ");
|
|
81
|
+
if (dup.duplicates.length > 3) files += ` (+${dup.duplicates.length - 3} more)`;
|
|
82
|
+
if (dup.duplicates.length === 1) {
|
|
83
|
+
return {
|
|
84
|
+
kind: "inform",
|
|
85
|
+
title: "Possible duplicate code (DRY)",
|
|
86
|
+
reason: `[${names}] already declared in: ${files}. Consider importing and reusing instead of re-declaring.`,
|
|
87
|
+
actions: ["Import the existing symbol instead of re-declaring it", "Extend the existing module"],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
kind: "block",
|
|
92
|
+
title: "Duplicate code (DRY)",
|
|
93
|
+
reason: `[${names}] already declared in: ${files}. Import and reuse instead of re-declaring.`,
|
|
94
|
+
actions: ["Import the existing symbol instead of re-declaring it", "Extend the existing module"],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module fragment-registry
|
|
3
|
+
* Per-process registry of injected fragment sizes, populated by
|
|
4
|
+
* {@link module:inject-budget.capFragment} as a side effect and consumed by
|
|
5
|
+
* {@link module:inject-budget-recap} to build ONE event-level recap instead
|
|
6
|
+
* of the scattered per-injection-point reports (each caller of `capFragment`
|
|
7
|
+
* only ever saw its OWN fragment, never the total for the whole event).
|
|
8
|
+
*
|
|
9
|
+
* Module-level state is safe here: each hook invocation is a fresh short-lived
|
|
10
|
+
* CLI process (one per Claude Code hook event), so there is no cross-event
|
|
11
|
+
* leakage in production. {@link resetFragmentRegistry} exists so a single
|
|
12
|
+
* process handling several events in sequence (unit tests; any future
|
|
13
|
+
* long-lived host) can still isolate one event's fragments from the next.
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
import type { FragmentSize } from "./inject-budget";
|
|
17
|
+
|
|
18
|
+
let registry: FragmentSize[] = [];
|
|
19
|
+
|
|
20
|
+
/** Clear the registry. Call once before dispatching a hook event. */
|
|
21
|
+
export function resetFragmentRegistry(): void {
|
|
22
|
+
registry = [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Record one fragment's post-cap size. Called by {@link module:inject-budget.capFragment}. */
|
|
26
|
+
export function recordFragment(label: string, chars: number): void {
|
|
27
|
+
registry.push({ label, chars });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Snapshot of every fragment recorded since the last {@link resetFragmentRegistry}. */
|
|
31
|
+
export function fragmentRegistry(): FragmentSize[] {
|
|
32
|
+
return [...registry];
|
|
33
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { frameworkSolidGate } from "../policy/framework-solid";
|
|
2
|
+
import { skillTriggerGate } from "../policy/skill-triggers";
|
|
3
|
+
import { requiredArchSkill } from "../policy/detect-project";
|
|
4
|
+
import { countFrameworkCodeLines } from "../policy/file-size";
|
|
5
|
+
import { isExcludedJsPath, isExcludedSwiftPath } from "../policy/framework-solid-exclude";
|
|
6
|
+
import type { GateInput } from "./gate-input";
|
|
7
|
+
import type { Prompt } from "../prompt/types";
|
|
8
|
+
|
|
9
|
+
/** PHP paths the Laravel skill check skips — broader than the SOLID vendor-only exclude (parity check-laravel-skill.py `/(vendor|storage|bootstrap/cache)/`). */
|
|
10
|
+
const PHP_SKILL_EXCLUDE_RE: RegExp = /\/(vendor|storage|bootstrap\/cache)\//;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Whether the sub-skill gate should skip this path — parity with the early return
|
|
14
|
+
* in check-*-skill.py (build/vendor artifacts never require a sub-skill).
|
|
15
|
+
* @param framework - the detected framework ("laravel" | "swift" | JS otherwise).
|
|
16
|
+
* @param filePath - the file being written.
|
|
17
|
+
*/
|
|
18
|
+
function skillGateExcluded(framework: string, filePath: string): boolean {
|
|
19
|
+
if (framework === "laravel") return PHP_SKILL_EXCLUDE_RE.test(filePath);
|
|
20
|
+
if (framework === "swift") return isExcludedSwiftPath(filePath);
|
|
21
|
+
return isExcludedJsPath(filePath);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Effective line count for the SOLID size check. On an Edit, `content` is only
|
|
26
|
+
* the `new_string` snippet, so for Next.js — mirroring the base file-size guard
|
|
27
|
+
* and the Python `get_full_file_content` — judge the larger of the snippet and
|
|
28
|
+
* the full on-disk file (`existingCodeLines`). Other frameworks (react/laravel/
|
|
29
|
+
* swift) judge the edited snippet alone, never the on-disk full-file max — their
|
|
30
|
+
* Python source never imports `get_full_file_content`, only nextjs does. On
|
|
31
|
+
* Write, `content` IS the full file, so the snippet count stands regardless of
|
|
32
|
+
* framework (undefined → the gate falls back to `countFrameworkCodeLines`).
|
|
33
|
+
* @param tool - the tool name ("Edit" | "Write" | ...).
|
|
34
|
+
* @param framework - the detected framework; only "nextjs" applies the full-file max.
|
|
35
|
+
* @param content - the written content (snippet on Edit, full file on Write).
|
|
36
|
+
* @param existingCodeLines - full on-disk code-only line count, when known.
|
|
37
|
+
*/
|
|
38
|
+
function effectiveLines(
|
|
39
|
+
tool: string,
|
|
40
|
+
framework: string,
|
|
41
|
+
content: string,
|
|
42
|
+
existingCodeLines?: number,
|
|
43
|
+
): number | undefined {
|
|
44
|
+
if (tool !== "Edit" || existingCodeLines === undefined) return undefined;
|
|
45
|
+
if (framework !== "nextjs") return countFrameworkCodeLines(content);
|
|
46
|
+
return Math.max(countFrameworkCodeLines(content), existingCodeLines);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Framework-aware SOLID + sub-skill gate, run on the Write/Edit path once a
|
|
51
|
+
* `filePath` is present. Combines:
|
|
52
|
+
* - {@link frameworkSolidGate}: framework-specific SOLID rules (line limits,
|
|
53
|
+
* interface/protocol separation, `'use client'`, @MainActor...).
|
|
54
|
+
* - {@link skillTriggerGate}: blocks when written APIs need a sub-skill that
|
|
55
|
+
* was not read this session, also forcing the modular-architecture skill
|
|
56
|
+
* resolved from disk via {@link requiredArchSkill}.
|
|
57
|
+
*
|
|
58
|
+
* @param input - the gated tool-use (filePath + content + framework + cwd).
|
|
59
|
+
* @param refsRead - in-session read reference paths (from the loaded track).
|
|
60
|
+
* @param existingCodeLines - full on-disk code-only line count (so an Edit on
|
|
61
|
+
* an oversized file still fires the framework SOLID size rule). Omit on Write.
|
|
62
|
+
* @returns the first blocking {@link Prompt}, or `null` to allow.
|
|
63
|
+
*/
|
|
64
|
+
export function frameworkSkillGate(
|
|
65
|
+
input: GateInput,
|
|
66
|
+
refsRead: string[],
|
|
67
|
+
existingCodeLines?: number,
|
|
68
|
+
): Prompt | null {
|
|
69
|
+
if (!input.filePath) return null;
|
|
70
|
+
const content = input.content ?? "";
|
|
71
|
+
const solid = frameworkSolidGate(input.filePath, content, effectiveLines(input.tool, input.framework, content, existingCodeLines));
|
|
72
|
+
if (solid) return solid;
|
|
73
|
+
// Skill-gate path exclusions (parity check-*-skill.py early return): a build/
|
|
74
|
+
// vendor artifact never triggers the sub-skill requirement. Applied only to the
|
|
75
|
+
// skill check, not frameworkSolidGate (whose PHP exclude is vendor-only).
|
|
76
|
+
if (skillGateExcluded(input.framework, input.filePath)) return null;
|
|
77
|
+
const forced = input.cwd ? requiredArchSkill(input.cwd) : null;
|
|
78
|
+
return skillTriggerGate(input.framework, content, refsRead, forced, input.cwd, input.filePath);
|
|
79
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, rmdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, relative, sep } from "node:path";
|
|
3
|
+
|
|
4
|
+
/** Age of a file in seconds (now - mtime). Infinity when unstat-able. */
|
|
5
|
+
function ageSec(path: string, now: number): number {
|
|
6
|
+
try {
|
|
7
|
+
return (now - statSync(path).mtimeMs) / 1000;
|
|
8
|
+
} catch {
|
|
9
|
+
return Infinity;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Remove files directly under `dir` matching `test` older than `maxAgeSec`. */
|
|
14
|
+
export function removeOldFiles(dir: string, test: (name: string) => boolean, maxAgeSec: number, now: number = Date.now()): void {
|
|
15
|
+
if (!existsSync(dir)) return;
|
|
16
|
+
for (const name of readdirSync(dir)) {
|
|
17
|
+
const path = join(dir, name);
|
|
18
|
+
if (test(name) && ageSec(path, now) > maxAgeSec) {
|
|
19
|
+
try { rmSync(path, { force: true }); } catch { /* best effort */ }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Trim `file` to its last `keepLines` lines when it exceeds `maxBytes`. */
|
|
25
|
+
export function trimLogFile(file: string, maxBytes: number, keepLines: number): void {
|
|
26
|
+
try {
|
|
27
|
+
if (!existsSync(file) || statSync(file).size <= maxBytes) return;
|
|
28
|
+
const lines = readFileSync(file, "utf-8").split("\n");
|
|
29
|
+
writeFileSync(file, lines.slice(-keepLines).join("\n"), "utf-8");
|
|
30
|
+
} catch { /* best effort */ }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Recursively purge files under `root/<top>` older than `ttls[top]` seconds. */
|
|
34
|
+
export function purgeTtlTree(root: string, ttls: Record<string, number>, now: number = Date.now()): void {
|
|
35
|
+
if (!existsSync(root)) return;
|
|
36
|
+
for (const [top, ttlSec] of Object.entries(ttls)) {
|
|
37
|
+
walkPurge(join(root, top), ttlSec, now);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function walkPurge(dir: string, ttlSec: number, now: number): void {
|
|
42
|
+
if (!existsSync(dir)) return;
|
|
43
|
+
for (const name of readdirSync(dir)) {
|
|
44
|
+
const path = join(dir, name);
|
|
45
|
+
let isDir = false;
|
|
46
|
+
try { isDir = statSync(path).isDirectory(); } catch { continue; }
|
|
47
|
+
if (isDir) { walkPurge(path, ttlSec, now); continue; }
|
|
48
|
+
if (ageSec(path, now) > ttlSec) {
|
|
49
|
+
try { rmSync(path, { force: true }); } catch { /* best effort */ }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Bottom-up removal of empty subdirs under each `root/<top>` (best effort). */
|
|
55
|
+
export function pruneEmptyDirs(root: string, tops: string[]): void {
|
|
56
|
+
for (const top of tops) {
|
|
57
|
+
const sub = join(root, top);
|
|
58
|
+
if (!existsSync(sub)) continue;
|
|
59
|
+
pruneEmpty(sub, sub);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function pruneEmpty(dir: string, stopAt: string): void {
|
|
64
|
+
for (const name of existsSync(dir) ? readdirSync(dir) : []) {
|
|
65
|
+
const path = join(dir, name);
|
|
66
|
+
try { if (statSync(path).isDirectory()) pruneEmpty(path, stopAt); } catch { /* skip */ }
|
|
67
|
+
}
|
|
68
|
+
if (dir !== stopAt && relative(stopAt, dir).split(sep)[0] !== "..") {
|
|
69
|
+
try { rmdirSync(dir); } catch { /* non-empty or gone */ }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { evaluateApex, PRE_AUTH_GATES, POST_AUTH_GATES, type ApexContext } from "../policy/apex";
|
|
2
|
+
import { apexAuthorizationGate } from "../policy/apex-authorization";
|
|
3
|
+
import { isTrivialEdit, protectedPathGate, SKIP_DIRS } from "../policy/trivial-edits";
|
|
4
|
+
import { FAIL_CLOSED } from "../policy/guards";
|
|
5
|
+
import { agentsFresh, recordTarget, recordTrivialEdit, trivialCount, type SessionTrack } from "../tracking/session-state";
|
|
6
|
+
import { agentsRanFromTranscript } from "../freshness/agent-evidence";
|
|
7
|
+
import { agentsFreshInTrack } from "../freshness/agent-evidence-record";
|
|
8
|
+
import { withTrack } from "../tracking/store";
|
|
9
|
+
import { REQUIRED_AGENTS, TRIVIAL_BUDGET } from "./gate";
|
|
10
|
+
import type { GateInput } from "./gate-input";
|
|
11
|
+
import type { Prompt } from "../prompt/types";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The APEX-scoped portion of {@link gate}, in the Python enforce-apex-phases.ts
|
|
15
|
+
* order: protected-paths guard, skip-dirs allow, trivial-edit fast path, then
|
|
16
|
+
* brainstorm/freshness, Check 1 (per-framework doc authorization — a deny
|
|
17
|
+
* persists `track.target` so recordDoc can cross-credit the framework) and
|
|
18
|
+
* Check 2 + SOLID refs. Freshness scans the SESSION track first (parity
|
|
19
|
+
* _scan_agents: sub-agent hooks share the lead's session_id, so sidechain/
|
|
20
|
+
* Workflow research is visible); the platform-authored transcript scan and the
|
|
21
|
+
* legacy exact-name track check are fallbacks.
|
|
22
|
+
* @param input - The tool-use to gate.
|
|
23
|
+
* @param track - The loaded session track.
|
|
24
|
+
* @param window - Freshness window in ms.
|
|
25
|
+
* @returns The first blocking prompt, or null to fall through to the DRY check.
|
|
26
|
+
*/
|
|
27
|
+
export async function apexScopedGate(input: GateInput, track: SessionTrack, window: number): Promise<Prompt | null> {
|
|
28
|
+
// (1) Hook-managed paths: absolute deny BEFORE any other filter (parity :49-52).
|
|
29
|
+
const protectedDeny = protectedPathGate(input.tool, input.filePath);
|
|
30
|
+
if (protectedDeny) return protectedDeny;
|
|
31
|
+
// (2) CODE_EXT runs upstream (gate.ts isApexScoped); dependency/build dirs skip (parity :53-54).
|
|
32
|
+
if (SKIP_DIRS.test(input.filePath ?? "")) return null;
|
|
33
|
+
|
|
34
|
+
// (3) Trivial-edit fast path: a few tiny, non-replace Edits skip the APEX gates
|
|
35
|
+
// (parity :58-65 — only Edit ever qualifies as "trivial"; a Write always
|
|
36
|
+
// creates/replaces a file wholesale and must go through the full gate).
|
|
37
|
+
if (isTrivialEdit(input.tool, input.content, input.isReplaceAll) && trivialCount(track, window, input.now) < TRIVIAL_BUDGET) {
|
|
38
|
+
await withTrack(input.trackFile, (fresh) => recordTrivialEdit(fresh, input.now, window, input.now));
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const freshnessFor = (names: string[], windowMs: number = window): boolean =>
|
|
43
|
+
agentsFreshInTrack(track, names, windowMs, input.now) ||
|
|
44
|
+
(input.transcriptPath ? agentsRanFromTranscript(input.transcriptPath, names, windowMs, input.now) : agentsFresh(track, names, windowMs, input.now));
|
|
45
|
+
|
|
46
|
+
const ctx: ApexContext = {
|
|
47
|
+
sessionId: input.sessionId,
|
|
48
|
+
framework: input.framework,
|
|
49
|
+
filePath: input.filePath ?? "",
|
|
50
|
+
content: input.content ?? "",
|
|
51
|
+
authorizations: track.authorizations,
|
|
52
|
+
refs: input.refs,
|
|
53
|
+
refsRead: track.refsRead,
|
|
54
|
+
refsReadAt: track.refsReadAt, // SOLID-read TTL stamps (absent → no expiry)
|
|
55
|
+
now: input.now,
|
|
56
|
+
agentsFresh: freshnessFor([...REQUIRED_AGENTS]),
|
|
57
|
+
missingAgents: REQUIRED_AGENTS.filter((name) => !freshnessFor([name])),
|
|
58
|
+
windowMs: window,
|
|
59
|
+
// Parity require-apex-agents.py:41 — Edit never creates a file (skip brainstorm),
|
|
60
|
+
// and a subagent (agent_id present) inherits the lead's brainstorm decision, so
|
|
61
|
+
// only a lead Write is ever brainstorm-gated.
|
|
62
|
+
brainstormRequired: input.tool === "Edit" || input.agentId ? false : track.brainstormRequired,
|
|
63
|
+
brainstormFresh: freshnessFor(["brainstorming"], Number.MAX_SAFE_INTEGER),
|
|
64
|
+
};
|
|
65
|
+
try {
|
|
66
|
+
// (4) Brainstorm + explore/research freshness (pre-existing harness gates).
|
|
67
|
+
const pre = evaluateApex(ctx, PRE_AUTH_GATES);
|
|
68
|
+
if (pre) return pre;
|
|
69
|
+
// (5) Check 1 — a deny persists `target` (parity :80-81) for recordDoc's cross-credit.
|
|
70
|
+
const authDeny = apexAuthorizationGate(ctx);
|
|
71
|
+
if (authDeny) {
|
|
72
|
+
const set_at = new Date(input.now).toISOString();
|
|
73
|
+
await withTrack(input.trackFile, (fresh) => recordTarget(fresh, { project: input.cwd ?? "", framework: input.framework, set_by: "apex-authorization", set_at }));
|
|
74
|
+
return authDeny;
|
|
75
|
+
}
|
|
76
|
+
// (6) Check 2 (doc consulted once per session) + SOLID refs.
|
|
77
|
+
return evaluateApex(ctx, POST_AUTH_GATES);
|
|
78
|
+
} catch {
|
|
79
|
+
return FAIL_CLOSED;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { countFrameworkCodeLines, countLines } from "../policy/file-size";
|
|
3
|
+
|
|
4
|
+
/** Source extensions the APEX gates apply to (parity: require-apex-agents.py CODE_EXT). */
|
|
5
|
+
const CODE_EXT = /\.(ts|tsx|js|jsx|py|go|rs|java|php|cpp|c|rb|swift|kt|dart|vue|svelte|astro)$/;
|
|
6
|
+
|
|
7
|
+
/** Paths exempt from the APEX gates even when they are code (parity: require-apex-agents.py EXEMPT_PATTERNS). */
|
|
8
|
+
const EXEMPT_PATTERNS: readonly RegExp[] = [
|
|
9
|
+
/\.claude-plugin\//,
|
|
10
|
+
/CHANGELOG\.md$/,
|
|
11
|
+
/marketplace\.json$/,
|
|
12
|
+
/\/\.(claude|codex)\/(apex|memory|logs|fusengine-cache)\//,
|
|
13
|
+
/\/\.fuse-harness\//,
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* True when `filePath` is a code file the APEX freshness/doc/SOLID gates should
|
|
18
|
+
* police — i.e. it matches a tracked source extension AND no exempt pattern.
|
|
19
|
+
* Mirrors require-apex-agents.py so non-code (and exempt) paths skip those gates.
|
|
20
|
+
* @param filePath - Absolute path of the edit target (or undefined).
|
|
21
|
+
* @returns Whether the APEX gates apply to this path.
|
|
22
|
+
*/
|
|
23
|
+
export function isApexScoped(filePath: string | undefined): boolean {
|
|
24
|
+
if (!filePath || !CODE_EXT.test(filePath)) return false;
|
|
25
|
+
return !EXEMPT_PATTERNS.some((p) => p.test(filePath));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Raw + code-only line counts of the existing on-disk file. */
|
|
29
|
+
export interface ExistingLineCounts {
|
|
30
|
+
/** Raw physical count — parity with core-guards `enforce-file-size.py` (`sum(1 for _ in f)`), fed to the generic `evaluate()` ceiling. */
|
|
31
|
+
raw?: number;
|
|
32
|
+
/** Code-only count (blank/comment lines excluded) — parity with the framework validators' `count_code_lines`, fed to `frameworkSkillGate`. */
|
|
33
|
+
code?: number;
|
|
34
|
+
/** Full on-disk content, read once alongside the counts — fed to the Edit-outcome file-size gate (policy/edit-outcome.ts via evaluate.ts) so it can compute the exact post-edit line count without a second read. */
|
|
35
|
+
content?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Read `path` once and derive both line-count metrics, so the generic
|
|
40
|
+
* core-guards ceiling (`raw`) and the framework SOLID gates (`code`) each get
|
|
41
|
+
* the metric their own Python origin measures, without reading the file twice.
|
|
42
|
+
* @param path - Absolute path of the file being edited (or undefined).
|
|
43
|
+
*/
|
|
44
|
+
export function existingLineCounts(path: string | undefined): ExistingLineCounts {
|
|
45
|
+
if (!path || !existsSync(path)) return {};
|
|
46
|
+
try {
|
|
47
|
+
const content = readFileSync(path, "utf8");
|
|
48
|
+
return { raw: countLines(content), code: countFrameworkCodeLines(content), content };
|
|
49
|
+
} catch {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { RefMeta } from "../refs/types";
|
|
2
|
+
|
|
3
|
+
/** A tool-use to gate, plus the session pointers needed for the stateful gates. */
|
|
4
|
+
export interface GateInput {
|
|
5
|
+
sessionId: string;
|
|
6
|
+
framework: string;
|
|
7
|
+
tool: string;
|
|
8
|
+
filePath?: string;
|
|
9
|
+
content?: string;
|
|
10
|
+
command?: string;
|
|
11
|
+
cwd?: string;
|
|
12
|
+
refs?: RefMeta[];
|
|
13
|
+
now: number;
|
|
14
|
+
trackFile: string;
|
|
15
|
+
windowMs?: number;
|
|
16
|
+
isReplaceAll?: boolean;
|
|
17
|
+
/** Edit only: the tool_input.old_string being replaced (runtime/normalize.ts) — threaded to evaluate()'s file-size gate so it can compute the post-edit outcome (policy/edit-outcome.ts) instead of judging the stale on-disk count alone. */
|
|
18
|
+
oldString?: string;
|
|
19
|
+
agentType?: string;
|
|
20
|
+
/** Claude `agent_id` when the tool-use comes from a subagent (parity require-apex-agents.py:41 — subagents inherit the lead's brainstorm decision). */
|
|
21
|
+
agentId?: string;
|
|
22
|
+
/** Absolute path to the session transcript (Claude `transcript_path`) for evidence-based freshness. */
|
|
23
|
+
transcriptPath?: string;
|
|
24
|
+
/** See PolicyContext.neverApproval — populated only by handle-pre.ts for id==="codex" (approval_policy=never has no interactive ask channel). */
|
|
25
|
+
neverApproval?: boolean;
|
|
26
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { evaluate, type PolicyResult } from "../policy/evaluate";
|
|
2
|
+
import { protectedPathGate } from "../policy/trivial-edits";
|
|
3
|
+
import { existingLineCounts, isApexScoped } from "./gate-helpers";
|
|
4
|
+
import { FAIL_CLOSED } from "../policy/guards";
|
|
5
|
+
import { loadTrack } from "../tracking/store";
|
|
6
|
+
import { dryGate } from "./dry";
|
|
7
|
+
import { preCommitGate } from "./precommit";
|
|
8
|
+
import { modularGate } from "./modular";
|
|
9
|
+
import { frameworkSkillGate } from "./framework-skill-gate";
|
|
10
|
+
import { reconcileRefReadsFromTranscript } from "../freshness/ref-evidence";
|
|
11
|
+
import { reconcileRefReadsFromJournal } from "../freshness/ref-journal";
|
|
12
|
+
import { isShadcnWrite, shadcnSkillGate } from "../policy/shadcn-skill-gate";
|
|
13
|
+
import { isTailwindWrite, tailwindSkillGate } from "../policy/tailwind-skill-gate";
|
|
14
|
+
import { geminiMcpGate } from "../policy/gemini-mcp-gate";
|
|
15
|
+
import { apexScopedGate } from "./gate-apex";
|
|
16
|
+
import { withDenyLoop } from "./deny-loop-store";
|
|
17
|
+
import { recordOneShot } from "../tracking/one-shot";
|
|
18
|
+
import { dirname } from "node:path";
|
|
19
|
+
import type { GateInput } from "./gate-input";
|
|
20
|
+
import type { Prompt } from "../prompt/types";
|
|
21
|
+
|
|
22
|
+
export type { GateInput } from "./gate-input";
|
|
23
|
+
|
|
24
|
+
/** Prior agents the freshness gate requires before a code edit. */
|
|
25
|
+
export const REQUIRED_AGENTS: ReadonlyArray<string> = ["explore-codebase", "research-expert"];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Default freshness window (ms). Matches the plugin's `FUSE_ENFORCE_TTL_SEC`
|
|
29
|
+
* default (120s); only a fallback for callers that omit `windowMs` (e.g. tests) —
|
|
30
|
+
* the real CLI path always supplies it from `resolveTtlSec()` (`src/config/ttl.ts`).
|
|
31
|
+
*/
|
|
32
|
+
export const DEFAULT_WINDOW_MS = 120_000;
|
|
33
|
+
|
|
34
|
+
/** Trivial edits allowed within the window before the full APEX gates apply. */
|
|
35
|
+
export const TRIVIAL_BUDGET = 4;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Full gate: {@link runGates} yields the first blocking prompt (or null); the tail
|
|
39
|
+
* records the one-shot metric ({@link recordOneShot}, observation-only) then lets
|
|
40
|
+
* {@link withDenyLoop} rewrite an identical retried deny (decision unchanged).
|
|
41
|
+
*/
|
|
42
|
+
export async function gate(input: GateInput): Promise<Prompt | null> {
|
|
43
|
+
const prompt = await runGates(input);
|
|
44
|
+
return finalizeGate(input, prompt, input.command);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Gate independent Cursor command candidates without merging their shell syntax.
|
|
49
|
+
* Stateful outcome bookkeeping runs once, against the command that decided the
|
|
50
|
+
* aggregate result. A single distinct candidate follows {@link gate} verbatim.
|
|
51
|
+
*/
|
|
52
|
+
export async function gateCommandCandidates(input: GateInput, candidates: readonly string[]): Promise<Prompt | null> {
|
|
53
|
+
const distinct = [...new Set(candidates)];
|
|
54
|
+
if (distinct.length <= 1) return gate(input);
|
|
55
|
+
|
|
56
|
+
let decisive: { prompt: Prompt; command: string } | undefined;
|
|
57
|
+
for (const command of distinct) {
|
|
58
|
+
const prompt = await runGates({ ...input, command });
|
|
59
|
+
if (!prompt) continue;
|
|
60
|
+
if (!decisive || promptRank(prompt) > promptRank(decisive.prompt)) decisive = { prompt, command };
|
|
61
|
+
if (prompt.kind === "block") break;
|
|
62
|
+
}
|
|
63
|
+
return finalizeGate(input, decisive?.prompt ?? null, decisive?.command ?? input.command);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Persist one final gate outcome and apply deny-loop enrichment exactly once. */
|
|
67
|
+
function finalizeGate(input: GateInput, prompt: Prompt | null, command: string | undefined): Prompt | null {
|
|
68
|
+
const op = { filePath: input.filePath, content: input.content, command };
|
|
69
|
+
const dir = dirname(input.trackFile);
|
|
70
|
+
recordOneShot(prompt, op, { now: input.now, dir, sessionId: input.sessionId });
|
|
71
|
+
return withDenyLoop(prompt, input.tool, op, { now: input.now, dir, windowMs: input.windowMs ?? DEFAULT_WINDOW_MS, sessionId: input.sessionId });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Higher numeric values dominate while equal values preserve wire order. */
|
|
75
|
+
function promptRank(prompt: Prompt): number {
|
|
76
|
+
return prompt.kind === "block" ? 3 : prompt.kind === "ask" ? 2 : 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Stateless guards, then the trivial fast path, then the stateful APEX gates. */
|
|
80
|
+
async function runGates(input: GateInput): Promise<Prompt | null> {
|
|
81
|
+
// Pre-commit lint hard-block runs FIRST: evaluate()'s GIT_ASK branch would
|
|
82
|
+
// otherwise short-circuit a `git commit` before the linters get to veto it.
|
|
83
|
+
const precommit = preCommitGate(input.tool, input.command, input.cwd);
|
|
84
|
+
if (precommit) return precommit;
|
|
85
|
+
// Hook-managed paths: absolute deny on ALL extensions, BEFORE the code-ext/exempt filters (parity enforce-apex-phases.ts:48-52 — isApexScoped never routes a non-code/exempt path to the guard).
|
|
86
|
+
const protectedDeny = protectedPathGate(input.tool, input.filePath);
|
|
87
|
+
if (protectedDeny) return protectedDeny;
|
|
88
|
+
const { raw: existingLines, code: existingCodeLines, content: existingContent } = existingLineCounts(input.filePath);
|
|
89
|
+
let quick: PolicyResult;
|
|
90
|
+
try {
|
|
91
|
+
quick = evaluate({ tool: input.tool, filePath: input.filePath, content: input.content, command: input.command, agentType: input.agentType, existingLines, existingContent, oldString: input.oldString, isReplaceAll: input.isReplaceAll, neverApproval: input.neverApproval });
|
|
92
|
+
} catch {
|
|
93
|
+
return FAIL_CLOSED;
|
|
94
|
+
}
|
|
95
|
+
if (quick.decision !== "allow" && quick.prompt) return quick.prompt;
|
|
96
|
+
const modular = modularGate(input.tool, input.filePath, input.content, input.cwd);
|
|
97
|
+
if (modular) return modular;
|
|
98
|
+
if (!input.filePath) return null;
|
|
99
|
+
const filePath = input.filePath;
|
|
100
|
+
// Reconcile refsRead BEFORE any consumer from two durable, race-immune sources (racy load→save loses lone
|
|
101
|
+
// writes under the fan-out): the transcript restores the LEAD's flushed read, the append-only journal (ref-journal.ts) the TEAMMATE's un-flushed one (lag > TTL).
|
|
102
|
+
const track = reconcileRefReadsFromJournal(
|
|
103
|
+
reconcileRefReadsFromTranscript(await loadTrack(input.trackFile), input.transcriptPath, input.now),
|
|
104
|
+
dirname(input.trackFile), input.now,
|
|
105
|
+
);
|
|
106
|
+
const solidOrSkill = frameworkSkillGate(input, track.refsRead, existingCodeLines);
|
|
107
|
+
if (solidOrSkill) return solidOrSkill;
|
|
108
|
+
// Standalone shadcn/ui gate (ports check-skill-loaded.py): runs independently of `framework` since a components.json / .css write may detect as "generic", mirroring the real plugin's own PreToolUse hook alongside react's.
|
|
109
|
+
if (isShadcnWrite(input.tool, filePath)) {
|
|
110
|
+
const shadcnBlock = shadcnSkillGate(input.tool, filePath, input.content ?? "", { refsRead: track.refsRead, authorizations: track.authorizations, sessionId: input.sessionId });
|
|
111
|
+
if (shadcnBlock) return shadcnBlock;
|
|
112
|
+
}
|
|
113
|
+
// Standalone Tailwind base-skill gate (ports check-tailwind-skill.py Phase 1): a .tsx/.jsx write with Tailwind classes needs a base Tailwind skill read, independent of framework.
|
|
114
|
+
if (isTailwindWrite(input.tool, filePath, input.content ?? "")) {
|
|
115
|
+
const twBlock = tailwindSkillGate(input.tool, filePath, input.content ?? "", track.refsRead);
|
|
116
|
+
if (twBlock) return twBlock;
|
|
117
|
+
}
|
|
118
|
+
// OPT-IN Gemini Design MCP gate (ports enforce-gemini-mcp.py) — a no-op unless FUSE_ENFORCE_GEMINI_MCP is set, then blocks hand-written Tailwind UI until a mcp__gemini-design__* call is made this session.
|
|
119
|
+
const geminiBlock = geminiMcpGate(input.tool, filePath, input.content ?? "", { authorizations: track.authorizations, sessionId: input.sessionId });
|
|
120
|
+
if (geminiBlock) return geminiBlock;
|
|
121
|
+
// The freshness/doc/SOLID APEX gates only police code files (require-apex-agents.py parity): non-code and exempt paths skip straight to the DRY check below.
|
|
122
|
+
if (isApexScoped(input.filePath)) {
|
|
123
|
+
const apex = await apexScopedGate(input, track, input.windowMs ?? DEFAULT_WINDOW_MS);
|
|
124
|
+
if (apex) return apex;
|
|
125
|
+
}
|
|
126
|
+
// DRY duplication (effectful: greps the codebase) — runs once the APEX gates pass.
|
|
127
|
+
return dryGate(input.tool, input.filePath, input.content, input.cwd);
|
|
128
|
+
}
|