@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,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module context-budget
|
|
3
|
+
* Cursor-only shared `additional_context` budget registry. Cursor 3.18.25
|
|
4
|
+
* runs every hook plugin configured on an event in its OWN process, then
|
|
5
|
+
* merges their `additional_context` outputs with a 9-char `"\n\n---\n\n"`
|
|
6
|
+
* separator and drops the WHOLE merge past 10,000 UTF-16 units — so no
|
|
7
|
+
* single process can know the total by itself. This module gives every
|
|
8
|
+
* plugin's process a shared, best-effort view of that total via a small
|
|
9
|
+
* JSON registry file under the project's state dir (see `../../runtime/paths.ts`),
|
|
10
|
+
* keyed by `${sessionId}|${event}|${generationId ?? ""}|${toolUseId ?? ""}`
|
|
11
|
+
* (one key per merge group — Cursor merges preToolUse/postToolUse/
|
|
12
|
+
* postToolUseFailure PER TOOL CALL, so `toolUseId` joins the key on those
|
|
13
|
+
* three events), with entries older than 10s ignored (concurrent hooks on one
|
|
14
|
+
* event fire within the same second). Best-effort, fail-open throughout: any
|
|
15
|
+
* I/O or JSON error degrades to "no shared budget", i.e. the flat
|
|
16
|
+
* per-response cap in `./context-limit.ts` alone — never a thrown error, and
|
|
17
|
+
* never a Cursor-side regression.
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { atomicWrite } from "../../util/json-io";
|
|
22
|
+
import {
|
|
23
|
+
ADDITIONAL_CONTEXT_LIMIT, TRUNCATION_MARKER, additionalContextLength, capAdditionalContext, omitAdditionalContext,
|
|
24
|
+
} from "./context-limit";
|
|
25
|
+
import type { CursorBudgetContext } from "./interfaces/context-budget";
|
|
26
|
+
|
|
27
|
+
const REGISTRY_FILE = "cursor-context-budget.json";
|
|
28
|
+
/** Matches Cursor 3.18.25's observed `"\n\n---\n\n"` merge separator length. */
|
|
29
|
+
const SEPARATOR_LENGTH = 9;
|
|
30
|
+
const ENTRY_WINDOW_MS = 10_000;
|
|
31
|
+
/** Below this, a truncated value would carry more marker than budget — omit the field instead. */
|
|
32
|
+
const OMIT_THRESHOLD = TRUNCATION_MARKER.length + 100;
|
|
33
|
+
|
|
34
|
+
interface BudgetEntry {
|
|
35
|
+
at: number;
|
|
36
|
+
length: number;
|
|
37
|
+
}
|
|
38
|
+
type BudgetRegistry = Record<string, BudgetEntry[]>;
|
|
39
|
+
|
|
40
|
+
function isRegistry(value: unknown): value is BudgetRegistry {
|
|
41
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function registryPath(stateDir: string): string {
|
|
45
|
+
return join(stateDir, REGISTRY_FILE);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function budgetKey(ctx: Pick<CursorBudgetContext, "sessionId" | "event" | "generationId" | "toolUseId">): string {
|
|
49
|
+
return `${ctx.sessionId}|${ctx.event}|${ctx.generationId ?? ""}|${ctx.toolUseId ?? ""}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function loadRegistry(path: string): BudgetRegistry {
|
|
53
|
+
try {
|
|
54
|
+
if (!existsSync(path)) return {};
|
|
55
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
56
|
+
return isRegistry(parsed) ? parsed : {};
|
|
57
|
+
} catch {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function freshEntries(entries: BudgetEntry[] | undefined, now: number): BudgetEntry[] {
|
|
63
|
+
return (entries ?? []).filter((entry) => now - entry.at <= ENTRY_WINDOW_MS);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Sum of a key's fresh entry lengths plus the separators already joining them. */
|
|
67
|
+
function consumed(entries: BudgetEntry[]): number {
|
|
68
|
+
return entries.reduce((total, entry) => total + entry.length, 0) + SEPARATOR_LENGTH * Math.max(0, entries.length - 1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** {@link reserveAdditionalContext} input: budget context plus the length the caller wants to emit. */
|
|
72
|
+
export type ReserveInput = CursorBudgetContext & { wanted: number };
|
|
73
|
+
/** {@link recordAdditionalContext} input: budget context plus the length actually emitted. */
|
|
74
|
+
export type RecordInput = CursorBudgetContext & { emitted: number };
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Reserve room in the shared budget for one hook's `additional_context`
|
|
78
|
+
* contribution to one (session, event, generation) merge group. `wanted` is
|
|
79
|
+
* accepted for a symmetric call shape with {@link recordAdditionalContext}
|
|
80
|
+
* but does not shrink `allowed` itself — the ceiling only depends on what
|
|
81
|
+
* OTHER entries already hold; a smaller `wanted` simply means the caller
|
|
82
|
+
* won't need all of it. Best-effort, fail-open: any I/O/JSON error returns
|
|
83
|
+
* the full flat ceiling, as if no other plugin had run.
|
|
84
|
+
* @param input - Registry location, reservation key, and the wanted length.
|
|
85
|
+
*/
|
|
86
|
+
export function reserveAdditionalContext(input: ReserveInput): { allowed: number } {
|
|
87
|
+
try {
|
|
88
|
+
const now = input.now ?? Date.now();
|
|
89
|
+
const registry = loadRegistry(registryPath(input.stateDir));
|
|
90
|
+
const fresh = freshEntries(registry[budgetKey(input)], now);
|
|
91
|
+
const separator = fresh.length > 0 ? SEPARATOR_LENGTH : 0;
|
|
92
|
+
return { allowed: Math.max(0, ADDITIONAL_CONTEXT_LIMIT - consumed(fresh) - separator) };
|
|
93
|
+
} catch {
|
|
94
|
+
return { allowed: ADDITIONAL_CONTEXT_LIMIT };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Record the length actually emitted for one reservation, best-effort.
|
|
100
|
+
* Prunes every key's stale entries while it holds the write so the registry
|
|
101
|
+
* file stays bounded. Silently no-ops on any I/O error (fail-open).
|
|
102
|
+
* @param input - Registry location, reservation key, and the emitted length.
|
|
103
|
+
*/
|
|
104
|
+
export function recordAdditionalContext(input: RecordInput): void {
|
|
105
|
+
try {
|
|
106
|
+
const now = input.now ?? Date.now();
|
|
107
|
+
const path = registryPath(input.stateDir);
|
|
108
|
+
const registry = loadRegistry(path);
|
|
109
|
+
const pruned: BudgetRegistry = {};
|
|
110
|
+
for (const [key, entries] of Object.entries(registry)) {
|
|
111
|
+
const fresh = freshEntries(entries, now);
|
|
112
|
+
if (fresh.length > 0) pruned[key] = fresh;
|
|
113
|
+
}
|
|
114
|
+
const key = budgetKey(input);
|
|
115
|
+
pruned[key] = [...(pruned[key] ?? []), { at: now, length: input.emitted }];
|
|
116
|
+
atomicWrite(path, JSON.stringify(pruned));
|
|
117
|
+
} catch {
|
|
118
|
+
// Best-effort: a lost entry only makes the NEXT reservation over-generous
|
|
119
|
+
// (never under), which is the safe direction to fail in.
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Cap a Cursor stdout JSON's `additional_context` against the shared budget
|
|
125
|
+
* instead of the flat per-response ceiling alone. Falls back to the plain
|
|
126
|
+
* cap (`./context-limit.ts`), unbudgeted, when `budget` is `undefined` or
|
|
127
|
+
* the stdout carries no `additional_context` at all.
|
|
128
|
+
* @param stdout - A native Cursor JSON stdout candidate.
|
|
129
|
+
* @param budget - Shared budget context, or `undefined` to skip it.
|
|
130
|
+
*/
|
|
131
|
+
export function capAdditionalContextWithBudget(stdout: string, budget: CursorBudgetContext | undefined): string {
|
|
132
|
+
if (!budget) return capAdditionalContext(stdout);
|
|
133
|
+
const wanted = additionalContextLength(stdout);
|
|
134
|
+
if (wanted === 0) return stdout;
|
|
135
|
+
const { allowed } = reserveAdditionalContext({ ...budget, wanted });
|
|
136
|
+
if (allowed < OMIT_THRESHOLD) {
|
|
137
|
+
process.stderr.write(`[fuse-harness] cursor: additional_context budget exhausted for ${budget.event} (allowed=${allowed})\n`);
|
|
138
|
+
return omitAdditionalContext(stdout);
|
|
139
|
+
}
|
|
140
|
+
const limit = Math.min(ADDITIONAL_CONTEXT_LIMIT, allowed);
|
|
141
|
+
const capped = capAdditionalContext(stdout, limit);
|
|
142
|
+
recordAdditionalContext({ ...budget, emitted: additionalContextLength(capped) });
|
|
143
|
+
return capped;
|
|
144
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module context-limit
|
|
3
|
+
* Cursor 3.18.25's `hooks-carriers` drops an `additional_context` carrier
|
|
4
|
+
* once `o.length>1e4` — but `o` is the MERGED text of every hook's
|
|
5
|
+
* `additional_context` for that event (concatenated with `"\n\n---\n\n"`
|
|
6
|
+
* before the 10,000-char check), not this harness's response in isolation.
|
|
7
|
+
* Capping our own contribution at {@link ADDITIONAL_CONTEXT_LIMIT} is
|
|
8
|
+
* therefore the LAST-RESORT guard, not the real protection: on its own it
|
|
9
|
+
* only proves OUR piece stays under 10,000, while the total across every
|
|
10
|
+
* hook plugin configured on the same event can still exceed it and get
|
|
11
|
+
* dropped wholesale — measured at ~8,400 chars on `sessionStart` from core
|
|
12
|
+
* plugins alone, close enough to the ceiling that one more plugin tips it
|
|
13
|
+
* over. The actual protection is the cross-process shared budget registry
|
|
14
|
+
* in `./context-budget.ts` (Cursor id only), which reserves a slice of the
|
|
15
|
+
* 10,000 ceiling per (session, event, generation) key BEFORE calling
|
|
16
|
+
* {@link truncateAdditionalContext} here with the reserved amount instead of
|
|
17
|
+
* the flat {@link ADDITIONAL_CONTEXT_LIMIT} — this module stays a pure,
|
|
18
|
+
* budget-agnostic primitive so it keeps working unbudgeted (its historical,
|
|
19
|
+
* still-correct behavior) wherever no budget context is available. The
|
|
20
|
+
* limit unit is UTF-16 code units (`String.prototype.length`), matching
|
|
21
|
+
* `value.length` here exactly. Only 5 events carry `additional_context`
|
|
22
|
+
* through this carrier — sessionStart, beforeSubmitPrompt, preToolUse,
|
|
23
|
+
* postToolUse, postToolUseFailure — subagentStart/subagentStop use a
|
|
24
|
+
* different, unlimited channel. "Drops silently" also only holds when no
|
|
25
|
+
* `failClosed: true` hook is declared on that step/tool: with one declared,
|
|
26
|
+
* an oversized carrier REJECTS the tool call instead of being dropped quiet.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** Cursor's hard `additional_context` character ceiling. */
|
|
30
|
+
export const ADDITIONAL_CONTEXT_LIMIT = 10_000;
|
|
31
|
+
|
|
32
|
+
/** Suffix appended by {@link truncateAdditionalContext} once a value is cut. */
|
|
33
|
+
export const TRUNCATION_MARKER = "\n[fuse-harness] additional_context truncated to Cursor's 10000-char limit";
|
|
34
|
+
|
|
35
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
36
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Truncate a string to end with {@link TRUNCATION_MARKER} once its length
|
|
41
|
+
* exceeds `limit`. Leaves shorter values untouched. Idempotent under a
|
|
42
|
+
* SHRINKING `limit` across repeated calls (e.g. an unbudgeted flat-cap pass
|
|
43
|
+
* followed by a budgeted re-cap of the same stdout — see `./respond.ts`'s
|
|
44
|
+
* `toCursorLifecycleResponse` doc): when `value` already ends with
|
|
45
|
+
* {@link TRUNCATION_MARKER}, that marker is stripped BEFORE re-slicing so the
|
|
46
|
+
* result carries exactly one marker instead of risking a duplicated/cut one.
|
|
47
|
+
* @param value - Candidate `additional_context` body.
|
|
48
|
+
* @param limit - Effective ceiling for this call (defaults to the flat
|
|
49
|
+
* {@link ADDITIONAL_CONTEXT_LIMIT}; a shared-budget caller passes a smaller,
|
|
50
|
+
* per-reservation value instead).
|
|
51
|
+
*/
|
|
52
|
+
export function truncateAdditionalContext(value: string, limit: number = ADDITIONAL_CONTEXT_LIMIT): string {
|
|
53
|
+
const alreadyMarked = value.endsWith(TRUNCATION_MARKER);
|
|
54
|
+
if (!alreadyMarked && value.length <= limit) return value;
|
|
55
|
+
if (limit <= TRUNCATION_MARKER.length) return TRUNCATION_MARKER.slice(0, Math.max(0, limit));
|
|
56
|
+
const base = alreadyMarked ? value.slice(0, value.length - TRUNCATION_MARKER.length) : value;
|
|
57
|
+
return base.slice(0, limit - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Length of a Cursor stdout JSON's `additional_context` string field, or 0
|
|
62
|
+
* when the stdout is not JSON, has no such field, or that field isn't a
|
|
63
|
+
* string.
|
|
64
|
+
* @param stdout - A native Cursor JSON stdout candidate.
|
|
65
|
+
*/
|
|
66
|
+
export function additionalContextLength(stdout: string): number {
|
|
67
|
+
let parsed: unknown;
|
|
68
|
+
try {
|
|
69
|
+
parsed = JSON.parse(stdout);
|
|
70
|
+
} catch {
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
return isPlainObject(parsed) && typeof parsed.additional_context === "string" ? parsed.additional_context.length : 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Re-serialize a Cursor stdout string with its `additional_context` field
|
|
78
|
+
* dropped entirely — used once the shared budget has no room left even for
|
|
79
|
+
* a truncated marker. Returns the input byte-for-byte unchanged when it is
|
|
80
|
+
* not JSON or has no string `additional_context` field.
|
|
81
|
+
* @param stdout - A native Cursor JSON stdout candidate.
|
|
82
|
+
*/
|
|
83
|
+
export function omitAdditionalContext(stdout: string): string {
|
|
84
|
+
let parsed: unknown;
|
|
85
|
+
try {
|
|
86
|
+
parsed = JSON.parse(stdout);
|
|
87
|
+
} catch {
|
|
88
|
+
return stdout;
|
|
89
|
+
}
|
|
90
|
+
if (!isPlainObject(parsed) || typeof parsed.additional_context !== "string") return stdout;
|
|
91
|
+
const { additional_context: _omitted, ...rest } = parsed;
|
|
92
|
+
return JSON.stringify(rest);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Re-serialize a Cursor stdout string with its `additional_context` field
|
|
97
|
+
* capped at `limit` characters. Returns the input byte-for-byte unchanged
|
|
98
|
+
* when it is not JSON, has no string `additional_context` field, or that
|
|
99
|
+
* field is already within the limit — so callers can wrap every return path
|
|
100
|
+
* unconditionally.
|
|
101
|
+
* @param stdout - A native Cursor JSON stdout candidate.
|
|
102
|
+
* @param limit - Effective ceiling for this call (see {@link truncateAdditionalContext}).
|
|
103
|
+
*/
|
|
104
|
+
export function capAdditionalContext(stdout: string, limit: number = ADDITIONAL_CONTEXT_LIMIT): string {
|
|
105
|
+
let parsed: unknown;
|
|
106
|
+
try {
|
|
107
|
+
parsed = JSON.parse(stdout);
|
|
108
|
+
} catch {
|
|
109
|
+
return stdout;
|
|
110
|
+
}
|
|
111
|
+
if (!isPlainObject(parsed) || typeof parsed.additional_context !== "string") return stdout;
|
|
112
|
+
const truncated = truncateAdditionalContext(parsed.additional_context, limit);
|
|
113
|
+
if (truncated === parsed.additional_context) return stdout;
|
|
114
|
+
return JSON.stringify({ ...parsed, additional_context: truncated });
|
|
115
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { isAbsolute, normalize, relative } from "node:path";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
/** Preserve a Cursor path value only when it is non-empty and NUL-free. */
|
|
5
|
+
export function cursorPath(value: unknown): string | undefined {
|
|
6
|
+
return typeof value === "string" && value.length > 0 && !value.includes("\0") ? value : undefined;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Validate and normalize an absolute path supplied by Cursor. */
|
|
10
|
+
export function cursorAbsolutePath(value: unknown): string | undefined {
|
|
11
|
+
const candidate = cursorPath(value);
|
|
12
|
+
if (!candidate || !isAbsolute(candidate)) return undefined;
|
|
13
|
+
const path = normalize(candidate);
|
|
14
|
+
try {
|
|
15
|
+
return realpathSync.native(path);
|
|
16
|
+
} catch {
|
|
17
|
+
return path;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Preserve distinct, validated Cursor workspace roots in wire order. */
|
|
22
|
+
export function cursorWorkspaceRoots(value: unknown): string[] {
|
|
23
|
+
if (!Array.isArray(value)) return [];
|
|
24
|
+
const roots = value.map(cursorAbsolutePath).filter((root): root is string => root !== undefined);
|
|
25
|
+
return [...new Set(roots)];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function contains(root: string, filePath: string): boolean {
|
|
29
|
+
const rel = relative(root, filePath);
|
|
30
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Select Cursor's project scope without replacing a valid payload cwd. Order:
|
|
35
|
+
* payload `cwd` -> longest workspace root containing `filePath` ->
|
|
36
|
+
* `workspaceRoots[0]` -> `CURSOR_PROJECT_DIR` env -> `CLAUDE_PROJECT_DIR` env
|
|
37
|
+
* -> `fallback`. Both env vars are validated the same way as any other Cursor
|
|
38
|
+
* path (`cursorAbsolutePath`: absolute, NUL-free, realpath-resolved), so an
|
|
39
|
+
* unset or malformed value is silently skipped rather than trusted.
|
|
40
|
+
* @param cwd - Cursor payload `cwd`, when present.
|
|
41
|
+
* @param workspaceRoots - Validated, deduped Cursor `workspace_roots`.
|
|
42
|
+
* @param filePath - The file the current event targets, when present.
|
|
43
|
+
* @param fallback - Caller-supplied last resort (never `process.cwd()`).
|
|
44
|
+
* @param env - Environment (defaults to `process.env`).
|
|
45
|
+
* @returns The resolved project root.
|
|
46
|
+
*/
|
|
47
|
+
export function cursorProjectCwd(
|
|
48
|
+
cwd: string | undefined,
|
|
49
|
+
workspaceRoots: readonly string[],
|
|
50
|
+
filePath: string | undefined,
|
|
51
|
+
fallback: string,
|
|
52
|
+
env: Record<string, string | undefined> = process.env,
|
|
53
|
+
): string {
|
|
54
|
+
if (cwd) return cwd;
|
|
55
|
+
if (filePath) {
|
|
56
|
+
const matches = workspaceRoots.filter((root) => contains(root, filePath));
|
|
57
|
+
if (matches.length > 0) return matches.sort((a, b) => b.length - a.length)[0]!;
|
|
58
|
+
}
|
|
59
|
+
if (workspaceRoots[0]) return workspaceRoots[0];
|
|
60
|
+
const fromCursorEnv = cursorAbsolutePath(env.CURSOR_PROJECT_DIR);
|
|
61
|
+
if (fromCursorEnv) return fromCursorEnv;
|
|
62
|
+
const fromClaudeEnv = cursorAbsolutePath(env.CLAUDE_PROJECT_DIR);
|
|
63
|
+
if (fromClaudeEnv) return fromClaudeEnv;
|
|
64
|
+
return fallback;
|
|
65
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { CursorEventContract } from "./interfaces/types";
|
|
2
|
+
|
|
3
|
+
const EVENT_CONTRACTS = {
|
|
4
|
+
sessionStart: { phase: "pre", lifecycle: "SessionStart", response: "session-context", blockable: false, known: true },
|
|
5
|
+
sessionEnd: { phase: "post", lifecycle: "SessionEnd", response: "neutral", blockable: false, known: true },
|
|
6
|
+
beforeSubmitPrompt: { phase: "pre", lifecycle: "UserPromptSubmit", response: "submit-control", blockable: true, known: true },
|
|
7
|
+
preCompact: { phase: "pre", lifecycle: "PreCompact", response: "compact-notice", blockable: false, known: true },
|
|
8
|
+
subagentStart: { phase: "pre", lifecycle: "SubagentStart", response: "permission", blockable: true, known: true },
|
|
9
|
+
subagentStop: { phase: "post", lifecycle: "SubagentStop", response: "followup", blockable: false, known: true },
|
|
10
|
+
preToolUse: { phase: "pre", lifecycle: "PreToolUse", response: "permission", blockable: true, known: true },
|
|
11
|
+
postToolUse: { phase: "post", lifecycle: "PostToolUse", response: "post-context", blockable: false, known: true },
|
|
12
|
+
postToolUseFailure: { phase: "post", lifecycle: "PostToolUseFailure", response: "neutral", blockable: false, known: true },
|
|
13
|
+
beforeShellExecution: { phase: "pre", lifecycle: "BeforeShellExecution", response: "permission", blockable: true, known: true },
|
|
14
|
+
afterShellExecution: { phase: "post", lifecycle: "AfterShellExecution", response: "neutral", blockable: false, known: true },
|
|
15
|
+
beforeMCPExecution: { phase: "pre", lifecycle: "BeforeMCPExecution", response: "permission", blockable: true, known: true },
|
|
16
|
+
afterMCPExecution: { phase: "post", lifecycle: "AfterMCPExecution", response: "neutral", blockable: false, known: true },
|
|
17
|
+
beforeReadFile: { phase: "pre", lifecycle: "BeforeReadFile", response: "permission", blockable: true, known: true },
|
|
18
|
+
afterFileEdit: { phase: "post", lifecycle: "AfterFileEdit", response: "neutral", blockable: false, known: true },
|
|
19
|
+
beforeTabFileRead: { phase: "pre", lifecycle: "BeforeTabFileRead", response: "permission", blockable: true, known: true },
|
|
20
|
+
afterTabFileEdit: { phase: "post", lifecycle: "AfterTabFileEdit", response: "neutral", blockable: false, known: true },
|
|
21
|
+
afterAgentResponse: { phase: "post", lifecycle: "AfterAgentResponse", response: "neutral", blockable: false, known: true },
|
|
22
|
+
afterAgentThought: { phase: "post", lifecycle: "AfterAgentThought", response: "neutral", blockable: false, known: true },
|
|
23
|
+
stop: { phase: "post", lifecycle: "Stop", response: "followup", blockable: false, known: true },
|
|
24
|
+
workspaceOpen: { phase: "pre", lifecycle: "WorkspaceOpen", response: "plugin-paths", blockable: false, known: true },
|
|
25
|
+
} as const satisfies Record<string, CursorEventContract>;
|
|
26
|
+
|
|
27
|
+
const UNKNOWN_EVENT: CursorEventContract = {
|
|
28
|
+
phase: "post",
|
|
29
|
+
lifecycle: null,
|
|
30
|
+
response: "neutral",
|
|
31
|
+
blockable: false,
|
|
32
|
+
known: false,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Return explicit routing and response metadata for a Cursor event name. */
|
|
36
|
+
export function cursorEventContract(eventName: string): CursorEventContract {
|
|
37
|
+
return EVENT_CONTRACTS[eventName as keyof typeof EVENT_CONTRACTS] ?? UNKNOWN_EVENT;
|
|
38
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Cursor hook adapter; post-edit handling remains observe-only. */
|
|
2
|
+
import { evaluate } from "../../policy/evaluate";
|
|
3
|
+
import { formatPrompt } from "../../prompt/types";
|
|
4
|
+
import { extractCursorEvent } from "./normalize";
|
|
5
|
+
import type { CursorShellPayload, CursorToolPayload, CursorEditPayload, CursorResponse, CursorEditResponse } from "./interfaces/types";
|
|
6
|
+
|
|
7
|
+
export type { CursorShellPayload, CursorToolPayload, CursorEditPayload, CursorResponse, CursorEditResponse } from "./interfaces/types";
|
|
8
|
+
|
|
9
|
+
function guardCursor(payload: object): CursorResponse {
|
|
10
|
+
const event = extractCursorEvent(payload);
|
|
11
|
+
const result = evaluate({ tool: event.tool, filePath: event.filePath, content: event.content, oldString: event.oldString, command: event.command });
|
|
12
|
+
if (result.decision === "allow" || !result.prompt) return { permission: "allow" };
|
|
13
|
+
const message = formatPrompt(result.prompt);
|
|
14
|
+
const userMessage = result.prompt.kind === "ask"
|
|
15
|
+
? `[downgraded from ask — Cursor preToolUse does not reliably enforce ask]\n${message}`
|
|
16
|
+
: message;
|
|
17
|
+
return { permission: "deny", user_message: userMessage, agent_message: message };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function namedPayload(payload: object, eventName: string): object {
|
|
21
|
+
return Object.hasOwn(payload, "hook_event_name") ? payload : { ...payload, hook_event_name: eventName };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Guard a shell command (git/install policies). */
|
|
25
|
+
export function beforeShellExecution(payload: CursorShellPayload): CursorResponse {
|
|
26
|
+
return guardCursor(namedPayload(payload, "beforeShellExecution"));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Guard a generic Cursor tool call using the same extraction as the runtime. */
|
|
30
|
+
export function preToolUse(payload: CursorToolPayload): CursorResponse {
|
|
31
|
+
return guardCursor(namedPayload(payload, "preToolUse"));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Complete an observed file edit without emitting pre-execution permission
|
|
36
|
+
* fields. Cursor does not define a callback schema for this post hook.
|
|
37
|
+
* @param payload - The `afterFileEdit` stdin payload.
|
|
38
|
+
* @returns An empty successful response.
|
|
39
|
+
*/
|
|
40
|
+
export function afterFileEdit(payload: CursorEditPayload): CursorEditResponse {
|
|
41
|
+
void payload;
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reservation key + registry location for one hook invocation's slice of
|
|
3
|
+
* Cursor's shared, cross-process `additional_context` budget (see
|
|
4
|
+
* `../context-budget.ts`). `undefined` at a call site means "no shared
|
|
5
|
+
* budget available" — callers then fall back to the flat per-response cap.
|
|
6
|
+
*/
|
|
7
|
+
export interface CursorBudgetContext {
|
|
8
|
+
/** Project state directory the registry file lives under (see `defaultStateDir`). */
|
|
9
|
+
stateDir: string;
|
|
10
|
+
/** Cursor `session_id` (its `conversation_id`). */
|
|
11
|
+
sessionId: string;
|
|
12
|
+
/** Raw Cursor `hook_event_name` (e.g. `"sessionStart"`). */
|
|
13
|
+
event: string;
|
|
14
|
+
/** Cursor `generation_id`; absent on `sessionStart`/`workspaceOpen`. */
|
|
15
|
+
generationId?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Cursor `tool_use_id`; present on preToolUse/postToolUse/postToolUseFailure
|
|
18
|
+
* — Cursor merges `additional_context` PER TOOL CALL for these events, not
|
|
19
|
+
* once per (session, event, generation), so this must join the key or
|
|
20
|
+
* concurrent tool calls in the same generation would wrongly share one slice.
|
|
21
|
+
*/
|
|
22
|
+
toolUseId?: string;
|
|
23
|
+
/** Test seam: injectable clock (defaults to `Date.now()`). */
|
|
24
|
+
now?: number;
|
|
25
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** `beforeShellExecution` stdin payload (subset). */
|
|
2
|
+
export interface CursorShellPayload {
|
|
3
|
+
command?: string;
|
|
4
|
+
cwd?: string;
|
|
5
|
+
sandbox?: boolean;
|
|
6
|
+
workspace_roots?: string[];
|
|
7
|
+
hook_event_name?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** `preToolUse` stdin payload subset consumed by the adapter. */
|
|
11
|
+
export interface CursorToolPayload {
|
|
12
|
+
tool_name?: string;
|
|
13
|
+
tool_input?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** `afterFileEdit` stdin payload (subset). */
|
|
17
|
+
export interface CursorEditPayload {
|
|
18
|
+
file_path?: string;
|
|
19
|
+
edits?: { old_string: string; new_string: string }[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** One extracted Cursor file edit used by runtime post fan-out. */
|
|
23
|
+
export interface CursorExtractedFile {
|
|
24
|
+
filePath: string;
|
|
25
|
+
oldString?: string;
|
|
26
|
+
content: string;
|
|
27
|
+
op: "update";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Shared Cursor extraction result consumed by runtime and public adapter. */
|
|
31
|
+
export interface CursorExtractedEvent {
|
|
32
|
+
eventName: string;
|
|
33
|
+
lifecycleEvent: string | null;
|
|
34
|
+
responseKind: CursorResponseKind;
|
|
35
|
+
blockable: boolean;
|
|
36
|
+
cwd?: string;
|
|
37
|
+
workspaceRoots?: string[];
|
|
38
|
+
phase: "pre" | "post";
|
|
39
|
+
tool: string;
|
|
40
|
+
input: Record<string, unknown>;
|
|
41
|
+
filePath?: string;
|
|
42
|
+
content?: string;
|
|
43
|
+
oldString?: string;
|
|
44
|
+
command?: string;
|
|
45
|
+
/** Distinct beforeMCPExecution root and nested commands, in wire order. */
|
|
46
|
+
commandCandidates?: string[];
|
|
47
|
+
files?: CursorExtractedFile[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Native Cursor stdout contract selected for one hook event. */
|
|
51
|
+
export type CursorResponseKind =
|
|
52
|
+
| "permission"
|
|
53
|
+
| "post-context"
|
|
54
|
+
| "session-context"
|
|
55
|
+
| "submit-control"
|
|
56
|
+
| "followup"
|
|
57
|
+
| "compact-notice"
|
|
58
|
+
| "plugin-paths"
|
|
59
|
+
| "neutral";
|
|
60
|
+
|
|
61
|
+
/** Routing metadata for a documented Cursor lifecycle event. */
|
|
62
|
+
export interface CursorEventContract {
|
|
63
|
+
phase: "pre" | "post";
|
|
64
|
+
lifecycle: string | null;
|
|
65
|
+
response: CursorResponseKind;
|
|
66
|
+
blockable: boolean;
|
|
67
|
+
known: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Empty afterFileEdit callback. Cursor documents no output fields for this
|
|
72
|
+
* post hook, so pre-execution permission fields are intentionally impossible.
|
|
73
|
+
*/
|
|
74
|
+
export type CursorEditResponse = Record<string, never>;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* `beforeShellExecution` stdout response. Message keys are snake_case:
|
|
78
|
+
* Cursor silently ignores camelCase `userMessage`/`agentMessage` (#141516,
|
|
79
|
+
* regression persists through v2.0.77+ — forum #142589), matching the
|
|
80
|
+
* schema respond.ts emits for the cursor harness.
|
|
81
|
+
*/
|
|
82
|
+
export interface CursorResponse {
|
|
83
|
+
permission: "allow" | "deny" | "ask";
|
|
84
|
+
/** User-visible message — snake_case required (#141516, #142589). */
|
|
85
|
+
user_message?: string;
|
|
86
|
+
/** Agent-visible message — snake_case required (#141516, #142589). */
|
|
87
|
+
agent_message?: string;
|
|
88
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { NATIVE_SCHEMAS, recordValue, type NativeSchema } from "./native-schemas";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Check that every enumerable own key of `value` is a documented field for
|
|
5
|
+
* `eventName` and passes its validator, and that every required field is
|
|
6
|
+
* present. Rejects prototype-polluted or exotic-shaped candidates via
|
|
7
|
+
* {@link recordValue}.
|
|
8
|
+
* @param value - Parsed JSON candidate.
|
|
9
|
+
* @param eventName - The Cursor hook event the candidate would answer.
|
|
10
|
+
*/
|
|
11
|
+
function isNativeCursorResponse(value: unknown, eventName: string): boolean {
|
|
12
|
+
try {
|
|
13
|
+
if (!recordValue(value)) return false;
|
|
14
|
+
const schema = NATIVE_SCHEMAS[eventName as keyof typeof NATIVE_SCHEMAS] as NativeSchema | undefined;
|
|
15
|
+
if (!schema) return false;
|
|
16
|
+
const descriptors = Object.getOwnPropertyDescriptors(value as object);
|
|
17
|
+
if (schema.required?.some((field) => !Object.hasOwn(descriptors, field))) return false;
|
|
18
|
+
return Reflect.ownKeys(descriptors).every((field) => {
|
|
19
|
+
if (typeof field !== "string") return false;
|
|
20
|
+
const descriptor = descriptors[field];
|
|
21
|
+
if (!Object.hasOwn(schema.fields, field)) return false;
|
|
22
|
+
const validate = schema.fields[field];
|
|
23
|
+
return descriptor?.enumerable === true && "value" in descriptor
|
|
24
|
+
&& validate !== undefined && validate(descriptor.value);
|
|
25
|
+
});
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Preserve raw stdout only when JSON.parse proves a documented native Cursor response. */
|
|
32
|
+
export function parseNativeCursorStdout(stdout: string, eventName: string): string | null {
|
|
33
|
+
if (typeof stdout !== "string") return null;
|
|
34
|
+
try {
|
|
35
|
+
return isNativeCursorResponse(JSON.parse(stdout), eventName) ? stdout : null;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|