@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
|
@@ -3,11 +3,11 @@ import { a as harnessHome, c as resolveTtlSec } from "./dotenv-BLBkBTww.mjs";
|
|
|
3
3
|
import { r as projectLayout } from "./layout-KWoE_Mqn.mjs";
|
|
4
4
|
import { i as walkUpFor, n as projectRoot, r as projectRootOrNull, t as isCodeFile } from "./project-root-3kk7gCOp.mjs";
|
|
5
5
|
import { t as detectHarness } from "./harness-BMuLJ9lm.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { D as isVendorPath, G as computeEditResultLines, K as isFileSizeScoped, V as shellOutputRedirects, Y as GIT_BLOCKED, b as interfaceDeclLevel, ct as detectProjectType$1, et as matchPatterns, g as conventionsMode, it as maskCommentsAndStrings, n as FAIL_CLOSED, q as resolveSolidRefFramework, st as detectModularArchitecture, t as evaluate, tt as detectFramework, ut as requiredArchSkill } from "./evaluate-CMiIqHeH.mjs";
|
|
7
7
|
import { a as sanitizeSessionId, c as sessionsDir, d as countFrameworkCodeLines, f as countLines, i as loadSessionState, l as PLUGINS_DIR, n as fuseHarnessHome, o as saveSessionState, p as evaluateFileSize, r as fusengineCache, t as claudeHome } from "./home-state-oUGFB4ds.mjs";
|
|
8
8
|
import { a as writeJsonFile, i as readJsonFile, r as hashText, t as atomicWrite } from "./json-io-DisYd2fb.mjs";
|
|
9
9
|
import { r as isDocConsulted } from "./doc-helpers-CWZegVdR.mjs";
|
|
10
|
-
import { D as frameworkSolidGate, F as isExcludedSwiftPath, G as evaluateApex, H as PRE_AUTH_GATES, P as isExcludedJsPath, R as capVerbosity, S as parseField, T as usesTailwindUtilities, V as POST_AUTH_GATES, W as docConsultedGate, b as scanPlugin, c as EXCLUDE_DIRS$1, d as buildApexTaskInjection, g as buildClaudeMdContext, l as PROJECT_INDICATORS, n as missingSeoElements, o as parseEnrichment, p as apexAgentSteps, r as descFromText, s as parseEntry, t as isHtmlLike, v as apexDocName, w as skillTriggerGate, y as harnessHomeSegment, z as detectCreationIntent } from "./validate-
|
|
10
|
+
import { D as frameworkSolidGate, F as isExcludedSwiftPath, G as evaluateApex, H as PRE_AUTH_GATES, P as isExcludedJsPath, R as capVerbosity, S as parseField, T as usesTailwindUtilities, V as POST_AUTH_GATES, W as docConsultedGate, b as scanPlugin, c as EXCLUDE_DIRS$1, d as buildApexTaskInjection, g as buildClaudeMdContext, l as PROJECT_INDICATORS, n as missingSeoElements, o as parseEnrichment, p as apexAgentSteps, r as descFromText, s as parseEntry, t as isHtmlLike, v as apexDocName, w as skillTriggerGate, y as harnessHomeSegment, z as detectCreationIntent } from "./validate-KjZ1X9tH.mjs";
|
|
11
11
|
import { n as findMarketplacePlugins, r as readPluginMeta, t as resolveSkillPath } from "./skill-path-DVML3zfp.mjs";
|
|
12
12
|
import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
|
|
13
13
|
import { a as nowStamp, l as throttleMs, n as readRoots, o as readState, s as setStateField, t as addRoot } from "./registry-IhHk2KlT.mjs";
|
|
@@ -15,11 +15,13 @@ import { d as loadIndex, i as cacheLookupMeta, n as webfetchCacheWrite, o as cac
|
|
|
15
15
|
import { t as loadRefs } from "./loader-AGz4nK7d.mjs";
|
|
16
16
|
import { C as apexAuthorizationGate, S as trivialCount, _ as recordBrainstormRequired, a as journalLogPath, b as recordTarget, c as appendEvent, d as signTrack, f as verifyTrack, g as recordAgent, h as emptyTrack, i as withTrack, l as withTrackLockSync, m as agentsFresh, o as readTrackSync, p as writeLastNonce, r as trackJournalEnabled, s as diffTrackEvents, t as loadTrack, u as LOCK_FAILED, v as recordDoc, x as recordTrivialEdit, y as recordRefRead } from "./store-5-ZPKb0u.mjs";
|
|
17
17
|
import { i as parseApplyPatch, n as canonicalizeMcpToolName, r as canonicalizeCodexShellTool, t as isBypassPermissions } from "./permission-mode-B-SFyR0X.mjs";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import { r as
|
|
18
|
+
import { t as commandToString } from "./command-string-CALMTnwN.mjs";
|
|
19
|
+
import { n as cursorProjectCwd, r as cursorEventContract, t as extractCursorEvent } from "./normalize-Dy8g9Ybl.mjs";
|
|
20
|
+
import { d as collectFiles, f as pathExists, g as writeText, h as spawnCapture, i as denyResponse, l as systemMessage, m as sleep, n as blockResponse, p as readText$1, r as contextResponse, s as informResponse, t as attachSystemMessage } from "./claude-Ckv2_TgP.mjs";
|
|
21
|
+
import { i as toKimiResponse, n as kimiDenyResponse } from "./kimi-G2wcSh5-.mjs";
|
|
22
|
+
import { r as toHermesResponse } from "./hermes-B9-p_3IF.mjs";
|
|
21
23
|
import { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
22
|
-
import { appendFileSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { appendFileSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
23
25
|
import { homedir } from "node:os";
|
|
24
26
|
import { mkdir, rmdir } from "node:fs/promises";
|
|
25
27
|
import { createHash } from "node:crypto";
|
|
@@ -69,9 +71,7 @@ function str(v) {
|
|
|
69
71
|
return typeof v === "string" ? v : void 0;
|
|
70
72
|
}
|
|
71
73
|
/**
|
|
72
|
-
* Normalize a
|
|
73
|
-
* `preToolUse`/`postToolUse` shape and the top-level `tool_name`/`tool_input`
|
|
74
|
-
* shape used by Claude, Codex, Gemini, and Cursor.
|
|
74
|
+
* Normalize a hook payload, including Cline nesting and native Cursor events.
|
|
75
75
|
*/
|
|
76
76
|
function normalizeEvent(id, payload) {
|
|
77
77
|
if (id === "cline") {
|
|
@@ -88,6 +88,12 @@ function normalizeEvent(id, payload) {
|
|
|
88
88
|
command: str(params.command)
|
|
89
89
|
};
|
|
90
90
|
}
|
|
91
|
+
if (id === "cursor") return {
|
|
92
|
+
...extractCursorEvent(payload),
|
|
93
|
+
sessionId: str(payload.session_id) ?? str(payload.conversation_id) ?? "",
|
|
94
|
+
agentType: str(payload.agent_type),
|
|
95
|
+
permissionMode: str(payload.permission_mode)
|
|
96
|
+
};
|
|
91
97
|
const event = str(payload.hook_event_name) ?? "";
|
|
92
98
|
const input = payload.tool_input ?? payload;
|
|
93
99
|
const tool = canonicalizeCodexShellTool(id, canonicalizeMcpToolName(id, str(payload.tool_name) ?? ""));
|
|
@@ -97,7 +103,9 @@ function normalizeEvent(id, payload) {
|
|
|
97
103
|
input,
|
|
98
104
|
sessionId: str(payload.session_id) ?? str(payload.conversation_id) ?? "",
|
|
99
105
|
agentType: str(payload.agent_type) ?? str(input.subagent_type),
|
|
100
|
-
permissionMode: str(payload.permission_mode)
|
|
106
|
+
permissionMode: str(payload.permission_mode),
|
|
107
|
+
toolUseId: str(payload.tool_use_id),
|
|
108
|
+
cwd: str(payload.cwd)
|
|
101
109
|
};
|
|
102
110
|
if (tool === "apply_patch") {
|
|
103
111
|
const files = parseApplyPatch(str(input.command) ?? str(payload.command) ?? "").map((f) => ({
|
|
@@ -2136,13 +2144,114 @@ function postEditTypescript(filePath) {
|
|
|
2136
2144
|
return contextResponse("PostToolUse", `Lint issues in ${basename(filePath)}: ${issues.join(" | ")}`);
|
|
2137
2145
|
}
|
|
2138
2146
|
//#endregion
|
|
2147
|
+
//#region src/adapters/cursor/plugin-root.ts
|
|
2148
|
+
/**
|
|
2149
|
+
* Cursor plugin-root resolution — independent from the rules-plugin probing
|
|
2150
|
+
* in `../../runtime/lifecycle/rules-root.ts`. Ground truth (Cursor 3.18.25
|
|
2151
|
+
* binary + cursor.com/docs/hooks): `CURSOR_PLUGIN_ROOT` / `CLAUDE_PLUGIN_ROOT`
|
|
2152
|
+
* (both equal to the plugin install dir) are injected ONLY into
|
|
2153
|
+
* plugin-declared hook processes — never user (`~/.cursor/hooks.json`),
|
|
2154
|
+
* project (`.cursor/hooks.json`), or enterprise hooks. A plugin hook's cwd is
|
|
2155
|
+
* the plugin install dir, EXCEPT for `stop`/`subagentStop`, where it is the
|
|
2156
|
+
* workspace root — callers must pass the right `cwd` for the event they are
|
|
2157
|
+
* handling. Precedence: (1) `CURSOR_PLUGIN_ROOT` env, (2) `CLAUDE_PLUGIN_ROOT`
|
|
2158
|
+
* env, (3) `cwd` when it carries a Cursor plugin marker
|
|
2159
|
+
* (`.cursor-plugin/plugin.json`, `plugin.json` + `hooks/hooks.json`, or a
|
|
2160
|
+
* bare `hooks/hooks.json` — matches installed-plugin layouts under
|
|
2161
|
+
* `~/.cursor/plugins/cache/**` and `~/.cursor/plugins/local/<name>/`), (4)
|
|
2162
|
+
* `none`. Cursor refuses symlinked config paths itself; we do not share that
|
|
2163
|
+
* constraint, so every resolved candidate is realpath-followed instead.
|
|
2164
|
+
*/
|
|
2165
|
+
/** Validate an env candidate: non-empty, NUL-free, absolute, existing dir. */
|
|
2166
|
+
function validateEnvCandidate(label, value, checked) {
|
|
2167
|
+
if (value === void 0 || value === "") {
|
|
2168
|
+
checked.push(`${label}: unset`);
|
|
2169
|
+
return null;
|
|
2170
|
+
}
|
|
2171
|
+
if (value.includes("\0")) {
|
|
2172
|
+
checked.push(`${label}: invalid (contains NUL): "${value}"`);
|
|
2173
|
+
return null;
|
|
2174
|
+
}
|
|
2175
|
+
if (!isAbsolute(value)) {
|
|
2176
|
+
checked.push(`${label}: invalid (not absolute): "${value}"`);
|
|
2177
|
+
return null;
|
|
2178
|
+
}
|
|
2179
|
+
try {
|
|
2180
|
+
if (!statSync(value).isDirectory()) {
|
|
2181
|
+
checked.push(`${label}: invalid (not a directory): "${value}"`);
|
|
2182
|
+
return null;
|
|
2183
|
+
}
|
|
2184
|
+
} catch {
|
|
2185
|
+
checked.push(`${label}: invalid (no such directory): "${value}"`);
|
|
2186
|
+
return null;
|
|
2187
|
+
}
|
|
2188
|
+
try {
|
|
2189
|
+
return realpathSync.native(value);
|
|
2190
|
+
} catch {
|
|
2191
|
+
checked.push(`${label}: invalid (realpath failed): "${value}"`);
|
|
2192
|
+
return null;
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
/** True when `dir` carries a recognized Cursor plugin install marker. */
|
|
2196
|
+
function hasPluginMarker(dir) {
|
|
2197
|
+
if (existsSync(join(dir, ".cursor-plugin", "plugin.json"))) return true;
|
|
2198
|
+
if (existsSync(join(dir, "plugin.json")) && existsSync(join(dir, "hooks", "hooks.json"))) return true;
|
|
2199
|
+
return existsSync(join(dir, "hooks", "hooks.json"));
|
|
2200
|
+
}
|
|
2201
|
+
/**
|
|
2202
|
+
* Resolve the Cursor plugin install root a plugin-declared hook runs from.
|
|
2203
|
+
* @param env - Environment (defaults to `process.env`).
|
|
2204
|
+
* @param cwd - The hook process's cwd for the current event (plugin root for
|
|
2205
|
+
* most events, workspace root for `stop`/`subagentStop` — caller's choice).
|
|
2206
|
+
* @returns The resolved root, its source, and every rejected candidate.
|
|
2207
|
+
*/
|
|
2208
|
+
function resolveCursorPluginRoot(env, cwd) {
|
|
2209
|
+
const checked = [];
|
|
2210
|
+
const fromCursor = validateEnvCandidate("env:CURSOR_PLUGIN_ROOT", env.CURSOR_PLUGIN_ROOT, checked);
|
|
2211
|
+
if (fromCursor) return {
|
|
2212
|
+
root: fromCursor,
|
|
2213
|
+
source: "env:CURSOR_PLUGIN_ROOT",
|
|
2214
|
+
checked
|
|
2215
|
+
};
|
|
2216
|
+
const fromClaude = validateEnvCandidate("env:CLAUDE_PLUGIN_ROOT", env.CLAUDE_PLUGIN_ROOT, checked);
|
|
2217
|
+
if (fromClaude) return {
|
|
2218
|
+
root: fromClaude,
|
|
2219
|
+
source: "env:CLAUDE_PLUGIN_ROOT",
|
|
2220
|
+
checked
|
|
2221
|
+
};
|
|
2222
|
+
if (hasPluginMarker(cwd)) {
|
|
2223
|
+
let resolved = cwd;
|
|
2224
|
+
try {
|
|
2225
|
+
resolved = realpathSync.native(cwd);
|
|
2226
|
+
} catch {}
|
|
2227
|
+
return {
|
|
2228
|
+
root: resolved,
|
|
2229
|
+
source: "cwd:plugin-marker",
|
|
2230
|
+
checked
|
|
2231
|
+
};
|
|
2232
|
+
}
|
|
2233
|
+
checked.push(`cwd:"${cwd}": no plugin marker found`);
|
|
2234
|
+
return {
|
|
2235
|
+
root: null,
|
|
2236
|
+
source: "none",
|
|
2237
|
+
checked
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
//#endregion
|
|
2139
2241
|
//#region src/runtime/lifecycle/rules-root.ts
|
|
2140
2242
|
/**
|
|
2141
2243
|
* Dynamic rules-plugin root resolution. The historical `CLAUDE_PLUGIN_ROOT ??
|
|
2142
2244
|
* cwd` chain only worked when the harness exported the plugin root — Kimi
|
|
2143
2245
|
* injects `KIMI_PLUGIN_ROOT` instead, and a bare cwd fallback never held a
|
|
2144
|
-
* `rules/` dir.
|
|
2145
|
-
*
|
|
2246
|
+
* `rules/` dir.
|
|
2247
|
+
*
|
|
2248
|
+
* `id === "cursor"` is resolved by a SEPARATE branch (`resolveCursorPluginRoot`)
|
|
2249
|
+
* before any of the below, because Cursor's env contract differs from the
|
|
2250
|
+
* other harnesses (see `../../adapters/cursor/plugin-root.ts`) — it is never
|
|
2251
|
+
* folded into the shared switch. For every other id, resolution order (first
|
|
2252
|
+
* hit wins, unchanged):
|
|
2253
|
+
* 1. `CLAUDE_PLUGIN_ROOT` (read for ALL non-cursor ids, historical quirk —
|
|
2254
|
+
* frozen by non-regression tests, do not "fix" without an explicit ask);
|
|
2146
2255
|
* 2. `KIMI_PLUGIN_ROOT` (kimi plugin-declared hooks);
|
|
2147
2256
|
* 3. Per-harness install probe (claude marketplace, codex versioned cache,
|
|
2148
2257
|
* kimi managed plugins) — first `<plugin>/rules` dir whose plugin folder
|
|
@@ -2196,6 +2305,12 @@ function probeKimi(env, home) {
|
|
|
2196
2305
|
* @returns The plugin root to read `rules/` from.
|
|
2197
2306
|
*/
|
|
2198
2307
|
function resolveRulesRoot(id, cwd, env = process.env) {
|
|
2308
|
+
if (id === "cursor") {
|
|
2309
|
+
const result = resolveCursorPluginRoot(env, cwd);
|
|
2310
|
+
if (result.root) return result.root;
|
|
2311
|
+
process.stderr.write(`[fuse-harness] cursor: no plugin root proven (checked: ${result.checked.join("; ")}); rules root falls back to ${cwd}\n`);
|
|
2312
|
+
return cwd;
|
|
2313
|
+
}
|
|
2199
2314
|
if (env.CLAUDE_PLUGIN_ROOT) return env.CLAUDE_PLUGIN_ROOT;
|
|
2200
2315
|
if (env.KIMI_PLUGIN_ROOT) return env.KIMI_PLUGIN_ROOT;
|
|
2201
2316
|
const home = env.HOME ?? homedir();
|
|
@@ -3064,7 +3179,7 @@ function lessonsStateFileFor(root) {
|
|
|
3064
3179
|
//#endregion
|
|
3065
3180
|
//#region src/tracking/one-shot-store.ts
|
|
3066
3181
|
/** A fresh, empty state — always spread (`{ ...EMPTY }`) so the const is never shared. */
|
|
3067
|
-
const EMPTY = {
|
|
3182
|
+
const EMPTY$1 = {
|
|
3068
3183
|
gates: {},
|
|
3069
3184
|
firstTry: 0,
|
|
3070
3185
|
corrected: 0,
|
|
@@ -3076,7 +3191,7 @@ const EMPTY = {
|
|
|
3076
3191
|
* gates/pending older than `windowMs`. Keeps the "7d" window honest, bounds size.
|
|
3077
3192
|
*/
|
|
3078
3193
|
function pruneState(s, now, windowMs) {
|
|
3079
|
-
if (now - s.updatedAt >= windowMs) return { ...EMPTY };
|
|
3194
|
+
if (now - s.updatedAt >= windowMs) return { ...EMPTY$1 };
|
|
3080
3195
|
const gates = {};
|
|
3081
3196
|
for (const [k, g] of Object.entries(s.gates)) if (now - g.lastTs < windowMs) gates[k] = g;
|
|
3082
3197
|
const pending = {};
|
|
@@ -3309,16 +3424,16 @@ const SIDECAR$1 = "one-shot.json";
|
|
|
3309
3424
|
/** Retention window: 7 days. Aggregates and pending denies older than this are pruned. */
|
|
3310
3425
|
const WINDOW_MS = 10080 * 60 * 1e3;
|
|
3311
3426
|
/** Load the state, or a fresh copy when missing/corrupt. */
|
|
3312
|
-
function loadState(path) {
|
|
3427
|
+
function loadState$1(path) {
|
|
3313
3428
|
try {
|
|
3314
|
-
if (!existsSync(path)) return { ...EMPTY };
|
|
3429
|
+
if (!existsSync(path)) return { ...EMPTY$1 };
|
|
3315
3430
|
const d = JSON.parse(readFileSync(path, "utf8"));
|
|
3316
3431
|
return d && typeof d === "object" && !Array.isArray(d) ? {
|
|
3317
|
-
...EMPTY,
|
|
3432
|
+
...EMPTY$1,
|
|
3318
3433
|
...d
|
|
3319
|
-
} : { ...EMPTY };
|
|
3434
|
+
} : { ...EMPTY$1 };
|
|
3320
3435
|
} catch {
|
|
3321
|
-
return { ...EMPTY };
|
|
3436
|
+
return { ...EMPTY$1 };
|
|
3322
3437
|
}
|
|
3323
3438
|
}
|
|
3324
3439
|
/**
|
|
@@ -3341,7 +3456,7 @@ function recordOneShot(prompt, input, opts) {
|
|
|
3341
3456
|
});
|
|
3342
3457
|
if (!burstFirst(op, prompt ? `deny:${prompt.title}` : "allow", opts)) return;
|
|
3343
3458
|
const path = join(opts.dir, SIDECAR$1);
|
|
3344
|
-
let s = pruneState(loadState(path), opts.now, WINDOW_MS);
|
|
3459
|
+
let s = pruneState(loadState$1(path), opts.now, WINDOW_MS);
|
|
3345
3460
|
s = prompt ? applyDeny(s, prompt.title, op, opts.now) : applyAllow(s, op, opts.now, input.content != null || input.command != null);
|
|
3346
3461
|
atomicWrite(path, JSON.stringify(s));
|
|
3347
3462
|
} catch {}
|
|
@@ -3356,7 +3471,7 @@ function recordOneShot(prompt, input, opts) {
|
|
|
3356
3471
|
*/
|
|
3357
3472
|
function oneShotSummary(cwd) {
|
|
3358
3473
|
try {
|
|
3359
|
-
return formatSummary(pruneState(loadState(join(defaultStateDir(cwd), SIDECAR$1)), Date.now(), WINDOW_MS));
|
|
3474
|
+
return formatSummary(pruneState(loadState$1(join(defaultStateDir(cwd), SIDECAR$1)), Date.now(), WINDOW_MS));
|
|
3360
3475
|
} catch {
|
|
3361
3476
|
return "";
|
|
3362
3477
|
}
|
|
@@ -3401,7 +3516,7 @@ function recordFailure(tool, opts) {
|
|
|
3401
3516
|
try {
|
|
3402
3517
|
if (!burstFirst(`fail:${tool}`, "failure", opts)) return;
|
|
3403
3518
|
const path = join(opts.dir, SIDECAR$1);
|
|
3404
|
-
const next = applyFailure(pruneState(loadState(path), opts.now, WINDOW_MS), tool, opts.now);
|
|
3519
|
+
const next = applyFailure(pruneState(loadState$1(path), opts.now, WINDOW_MS), tool, opts.now);
|
|
3405
3520
|
atomicWrite(path, JSON.stringify(next));
|
|
3406
3521
|
} catch {}
|
|
3407
3522
|
}
|
|
@@ -3414,8 +3529,12 @@ function recordFailure(tool, opts) {
|
|
|
3414
3529
|
* matches the failure message — reusing the PreToolUse {@link lessonFor} index and
|
|
3415
3530
|
* its cooldown (idempotent under the ~11-process fan-out). Fail-open throughout.
|
|
3416
3531
|
*
|
|
3417
|
-
* Claude-Code-only
|
|
3418
|
-
*
|
|
3532
|
+
* Claude-Code-only in the sense that no equivalent `PostToolUseFailure` hook
|
|
3533
|
+
* exists on Codex or Hermes — but Cursor's own `postToolUseFailure` DOES
|
|
3534
|
+
* arrive here too, via `lifecycle-bridge.ts`'s `lifecycleStdout` translating
|
|
3535
|
+
* the wire event name and forwarding to `dispatchLifecycle`'s
|
|
3536
|
+
* `"PostToolUseFailure"` case (see `handle.ts`'s `cursorRawPayloadProjection`
|
|
3537
|
+
* for how `data.tool_name` below arrives already canonicalized on Cursor).
|
|
3419
3538
|
* @packageDocumentation
|
|
3420
3539
|
*/
|
|
3421
3540
|
/** The failure message across the documented `error` field and defensive fallbacks; "" when none. */
|
|
@@ -6024,7 +6143,7 @@ async function dispatchAipilot(event, payload, cwd, now, home = homedir(), id =
|
|
|
6024
6143
|
await cacheAnalyticsSave(home, now);
|
|
6025
6144
|
return "";
|
|
6026
6145
|
}
|
|
6027
|
-
if (event === "PreToolUse") return docCacheGate(payload, cwd, now, home, id);
|
|
6146
|
+
if (event === "PreToolUse" || id === "cursor" && event === "BeforeMCPExecution") return docCacheGate(payload, cwd, now, home, id);
|
|
6028
6147
|
return null;
|
|
6029
6148
|
}
|
|
6030
6149
|
/** PostToolUse (Write/Edit SOLID check, else TaskCreate/TaskUpdate sync) for the ai-pilot scope; `id` selects the harness target (defaults to "claude-code"). */
|
|
@@ -6646,13 +6765,571 @@ async function autoDocumentRead(filePath, now = Date.now(), id = "claude-code")
|
|
|
6646
6765
|
return systemMessage(`📖 [${docType}] ${fname} logged`);
|
|
6647
6766
|
}
|
|
6648
6767
|
//#endregion
|
|
6768
|
+
//#region src/adapters/cursor/native-schemas.ts
|
|
6769
|
+
const stringValue = (value) => typeof value === "string";
|
|
6770
|
+
const booleanValue = (value) => typeof value === "boolean";
|
|
6771
|
+
/** A plain `{}`-literal or `Object.create(null)` object — never a class instance or array. */
|
|
6772
|
+
const plainRecord = (value) => {
|
|
6773
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
6774
|
+
try {
|
|
6775
|
+
const prototype = Object.getPrototypeOf(value);
|
|
6776
|
+
return prototype === Object.prototype || prototype === null;
|
|
6777
|
+
} catch {
|
|
6778
|
+
return false;
|
|
6779
|
+
}
|
|
6780
|
+
};
|
|
6781
|
+
function jsonChildren(value) {
|
|
6782
|
+
const keys = Reflect.ownKeys(value);
|
|
6783
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
6784
|
+
if (Array.isArray(value)) {
|
|
6785
|
+
if (Object.getPrototypeOf(value) !== Array.prototype) return null;
|
|
6786
|
+
const length = descriptors.length;
|
|
6787
|
+
if (!length || !("value" in length) || !Number.isSafeInteger(length.value) || length.value < 0) return null;
|
|
6788
|
+
if (keys.length !== length.value + 1 || keys.some((key) => typeof key === "symbol")) return null;
|
|
6789
|
+
const children = [];
|
|
6790
|
+
for (let index = 0; index < length.value; index += 1) {
|
|
6791
|
+
const descriptor = descriptors[String(index)];
|
|
6792
|
+
if (!descriptor?.enumerable || !("value" in descriptor)) return null;
|
|
6793
|
+
children.push(descriptor.value);
|
|
6794
|
+
}
|
|
6795
|
+
return children;
|
|
6796
|
+
}
|
|
6797
|
+
if (!plainRecord(value) || keys.some((key) => typeof key === "symbol")) return null;
|
|
6798
|
+
const children = [];
|
|
6799
|
+
for (const key of keys) {
|
|
6800
|
+
const descriptor = descriptors[key];
|
|
6801
|
+
if (!descriptor?.enumerable || !("value" in descriptor)) return null;
|
|
6802
|
+
children.push(descriptor.value);
|
|
6803
|
+
}
|
|
6804
|
+
return children;
|
|
6805
|
+
}
|
|
6806
|
+
function jsonValue(root) {
|
|
6807
|
+
const active = /* @__PURE__ */ new WeakSet();
|
|
6808
|
+
const stack = [{ value: root }];
|
|
6809
|
+
while (stack.length > 0) {
|
|
6810
|
+
const frame = stack.pop();
|
|
6811
|
+
if (frame.leave) {
|
|
6812
|
+
active.delete(frame.value);
|
|
6813
|
+
continue;
|
|
6814
|
+
}
|
|
6815
|
+
const { value } = frame;
|
|
6816
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") continue;
|
|
6817
|
+
if (typeof value === "number") {
|
|
6818
|
+
if (!Number.isFinite(value)) return false;
|
|
6819
|
+
continue;
|
|
6820
|
+
}
|
|
6821
|
+
if (typeof value !== "object" || active.has(value)) return false;
|
|
6822
|
+
let children;
|
|
6823
|
+
try {
|
|
6824
|
+
children = jsonChildren(value);
|
|
6825
|
+
} catch {
|
|
6826
|
+
return false;
|
|
6827
|
+
}
|
|
6828
|
+
if (!children) return false;
|
|
6829
|
+
active.add(value);
|
|
6830
|
+
stack.push({
|
|
6831
|
+
value,
|
|
6832
|
+
leave: true
|
|
6833
|
+
});
|
|
6834
|
+
for (let index = children.length - 1; index >= 0; index -= 1) stack.push({ value: children[index] });
|
|
6835
|
+
}
|
|
6836
|
+
return true;
|
|
6837
|
+
}
|
|
6838
|
+
/** A JSON-safe plain object (no cycles, no non-finite numbers, no exotic prototypes). */
|
|
6839
|
+
const recordValue = (value) => plainRecord(value) && jsonValue(value);
|
|
6840
|
+
const stringRecord = (value) => {
|
|
6841
|
+
if (!recordValue(value)) return false;
|
|
6842
|
+
try {
|
|
6843
|
+
return Object.values(Object.getOwnPropertyDescriptors(value)).every((descriptor) => "value" in descriptor && typeof descriptor.value === "string");
|
|
6844
|
+
} catch {
|
|
6845
|
+
return false;
|
|
6846
|
+
}
|
|
6847
|
+
};
|
|
6848
|
+
const stringArray = (value) => Array.isArray(value) && value.every(stringValue);
|
|
6849
|
+
const permission = (...values) => (value) => typeof value === "string" && values.includes(value);
|
|
6850
|
+
const EMPTY = { fields: {} };
|
|
6851
|
+
const FOLLOWUP = { fields: { followup_message: stringValue } };
|
|
6852
|
+
const PERMISSION_ASK = {
|
|
6853
|
+
fields: {
|
|
6854
|
+
permission: permission("allow", "deny", "ask"),
|
|
6855
|
+
user_message: stringValue,
|
|
6856
|
+
agent_message: stringValue
|
|
6857
|
+
},
|
|
6858
|
+
required: ["permission"]
|
|
6859
|
+
};
|
|
6860
|
+
const PRE_TOOL_USE = {
|
|
6861
|
+
fields: {
|
|
6862
|
+
permission: permission("allow", "deny", "ask"),
|
|
6863
|
+
user_message: stringValue,
|
|
6864
|
+
agent_message: stringValue,
|
|
6865
|
+
updated_input: recordValue,
|
|
6866
|
+
additional_context: stringValue
|
|
6867
|
+
},
|
|
6868
|
+
required: ["permission"]
|
|
6869
|
+
};
|
|
6870
|
+
/**
|
|
6871
|
+
* Exact native stdout field set Cursor 3.18.25 reads per hook event.
|
|
6872
|
+
* Nothing beyond this list is invented: any additional key on a candidate
|
|
6873
|
+
* value fails {@link isNativeCursorResponse} in native-response.ts.
|
|
6874
|
+
*/
|
|
6875
|
+
const NATIVE_SCHEMAS = {
|
|
6876
|
+
sessionStart: { fields: {
|
|
6877
|
+
env: stringRecord,
|
|
6878
|
+
additional_context: stringValue,
|
|
6879
|
+
continue: booleanValue,
|
|
6880
|
+
user_message: stringValue
|
|
6881
|
+
} },
|
|
6882
|
+
sessionEnd: EMPTY,
|
|
6883
|
+
beforeSubmitPrompt: {
|
|
6884
|
+
fields: {
|
|
6885
|
+
continue: booleanValue,
|
|
6886
|
+
user_message: stringValue,
|
|
6887
|
+
additional_context: stringValue
|
|
6888
|
+
},
|
|
6889
|
+
required: ["continue"]
|
|
6890
|
+
},
|
|
6891
|
+
preCompact: { fields: { user_message: stringValue } },
|
|
6892
|
+
subagentStart: {
|
|
6893
|
+
fields: {
|
|
6894
|
+
permission: permission("allow", "deny"),
|
|
6895
|
+
user_message: stringValue
|
|
6896
|
+
},
|
|
6897
|
+
required: ["permission"]
|
|
6898
|
+
},
|
|
6899
|
+
subagentStop: FOLLOWUP,
|
|
6900
|
+
preToolUse: PRE_TOOL_USE,
|
|
6901
|
+
postToolUse: { fields: {
|
|
6902
|
+
updated_mcp_tool_output: recordValue,
|
|
6903
|
+
additional_context: stringValue
|
|
6904
|
+
} },
|
|
6905
|
+
postToolUseFailure: { fields: { additional_context: stringValue } },
|
|
6906
|
+
beforeShellExecution: PERMISSION_ASK,
|
|
6907
|
+
afterShellExecution: EMPTY,
|
|
6908
|
+
beforeMCPExecution: PERMISSION_ASK,
|
|
6909
|
+
afterMCPExecution: EMPTY,
|
|
6910
|
+
beforeReadFile: {
|
|
6911
|
+
fields: {
|
|
6912
|
+
permission: permission("allow", "deny"),
|
|
6913
|
+
user_message: stringValue
|
|
6914
|
+
},
|
|
6915
|
+
required: ["permission"]
|
|
6916
|
+
},
|
|
6917
|
+
afterFileEdit: EMPTY,
|
|
6918
|
+
beforeTabFileRead: {
|
|
6919
|
+
fields: { permission: permission("allow", "deny") },
|
|
6920
|
+
required: ["permission"]
|
|
6921
|
+
},
|
|
6922
|
+
afterTabFileEdit: EMPTY,
|
|
6923
|
+
afterAgentResponse: EMPTY,
|
|
6924
|
+
afterAgentThought: EMPTY,
|
|
6925
|
+
stop: FOLLOWUP,
|
|
6926
|
+
workspaceOpen: { fields: { pluginPaths: stringArray } }
|
|
6927
|
+
};
|
|
6928
|
+
//#endregion
|
|
6929
|
+
//#region src/adapters/cursor/native-response.ts
|
|
6930
|
+
/**
|
|
6931
|
+
* Check that every enumerable own key of `value` is a documented field for
|
|
6932
|
+
* `eventName` and passes its validator, and that every required field is
|
|
6933
|
+
* present. Rejects prototype-polluted or exotic-shaped candidates via
|
|
6934
|
+
* {@link recordValue}.
|
|
6935
|
+
* @param value - Parsed JSON candidate.
|
|
6936
|
+
* @param eventName - The Cursor hook event the candidate would answer.
|
|
6937
|
+
*/
|
|
6938
|
+
function isNativeCursorResponse(value, eventName) {
|
|
6939
|
+
try {
|
|
6940
|
+
if (!recordValue(value)) return false;
|
|
6941
|
+
const schema = NATIVE_SCHEMAS[eventName];
|
|
6942
|
+
if (!schema) return false;
|
|
6943
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
6944
|
+
if (schema.required?.some((field) => !Object.hasOwn(descriptors, field))) return false;
|
|
6945
|
+
return Reflect.ownKeys(descriptors).every((field) => {
|
|
6946
|
+
if (typeof field !== "string") return false;
|
|
6947
|
+
const descriptor = descriptors[field];
|
|
6948
|
+
if (!Object.hasOwn(schema.fields, field)) return false;
|
|
6949
|
+
const validate = schema.fields[field];
|
|
6950
|
+
return descriptor?.enumerable === true && "value" in descriptor && validate !== void 0 && validate(descriptor.value);
|
|
6951
|
+
});
|
|
6952
|
+
} catch {
|
|
6953
|
+
return false;
|
|
6954
|
+
}
|
|
6955
|
+
}
|
|
6956
|
+
/** Preserve raw stdout only when JSON.parse proves a documented native Cursor response. */
|
|
6957
|
+
function parseNativeCursorStdout(stdout, eventName) {
|
|
6958
|
+
if (typeof stdout !== "string") return null;
|
|
6959
|
+
try {
|
|
6960
|
+
return isNativeCursorResponse(JSON.parse(stdout), eventName) ? stdout : null;
|
|
6961
|
+
} catch {
|
|
6962
|
+
return null;
|
|
6963
|
+
}
|
|
6964
|
+
}
|
|
6965
|
+
//#endregion
|
|
6966
|
+
//#region src/adapters/cursor/context-limit.ts
|
|
6967
|
+
/**
|
|
6968
|
+
* @module context-limit
|
|
6969
|
+
* Cursor 3.18.25's `hooks-carriers` drops an `additional_context` carrier
|
|
6970
|
+
* once `o.length>1e4` — but `o` is the MERGED text of every hook's
|
|
6971
|
+
* `additional_context` for that event (concatenated with `"\n\n---\n\n"`
|
|
6972
|
+
* before the 10,000-char check), not this harness's response in isolation.
|
|
6973
|
+
* Capping our own contribution at {@link ADDITIONAL_CONTEXT_LIMIT} is
|
|
6974
|
+
* therefore the LAST-RESORT guard, not the real protection: on its own it
|
|
6975
|
+
* only proves OUR piece stays under 10,000, while the total across every
|
|
6976
|
+
* hook plugin configured on the same event can still exceed it and get
|
|
6977
|
+
* dropped wholesale — measured at ~8,400 chars on `sessionStart` from core
|
|
6978
|
+
* plugins alone, close enough to the ceiling that one more plugin tips it
|
|
6979
|
+
* over. The actual protection is the cross-process shared budget registry
|
|
6980
|
+
* in `./context-budget.ts` (Cursor id only), which reserves a slice of the
|
|
6981
|
+
* 10,000 ceiling per (session, event, generation) key BEFORE calling
|
|
6982
|
+
* {@link truncateAdditionalContext} here with the reserved amount instead of
|
|
6983
|
+
* the flat {@link ADDITIONAL_CONTEXT_LIMIT} — this module stays a pure,
|
|
6984
|
+
* budget-agnostic primitive so it keeps working unbudgeted (its historical,
|
|
6985
|
+
* still-correct behavior) wherever no budget context is available. The
|
|
6986
|
+
* limit unit is UTF-16 code units (`String.prototype.length`), matching
|
|
6987
|
+
* `value.length` here exactly. Only 5 events carry `additional_context`
|
|
6988
|
+
* through this carrier — sessionStart, beforeSubmitPrompt, preToolUse,
|
|
6989
|
+
* postToolUse, postToolUseFailure — subagentStart/subagentStop use a
|
|
6990
|
+
* different, unlimited channel. "Drops silently" also only holds when no
|
|
6991
|
+
* `failClosed: true` hook is declared on that step/tool: with one declared,
|
|
6992
|
+
* an oversized carrier REJECTS the tool call instead of being dropped quiet.
|
|
6993
|
+
*/
|
|
6994
|
+
/** Cursor's hard `additional_context` character ceiling. */
|
|
6995
|
+
const ADDITIONAL_CONTEXT_LIMIT = 1e4;
|
|
6996
|
+
/** Suffix appended by {@link truncateAdditionalContext} once a value is cut. */
|
|
6997
|
+
const TRUNCATION_MARKER = "\n[fuse-harness] additional_context truncated to Cursor's 10000-char limit";
|
|
6998
|
+
function isPlainObject(value) {
|
|
6999
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7000
|
+
}
|
|
7001
|
+
/**
|
|
7002
|
+
* Truncate a string to end with {@link TRUNCATION_MARKER} once its length
|
|
7003
|
+
* exceeds `limit`. Leaves shorter values untouched. Idempotent under a
|
|
7004
|
+
* SHRINKING `limit` across repeated calls (e.g. an unbudgeted flat-cap pass
|
|
7005
|
+
* followed by a budgeted re-cap of the same stdout — see `./respond.ts`'s
|
|
7006
|
+
* `toCursorLifecycleResponse` doc): when `value` already ends with
|
|
7007
|
+
* {@link TRUNCATION_MARKER}, that marker is stripped BEFORE re-slicing so the
|
|
7008
|
+
* result carries exactly one marker instead of risking a duplicated/cut one.
|
|
7009
|
+
* @param value - Candidate `additional_context` body.
|
|
7010
|
+
* @param limit - Effective ceiling for this call (defaults to the flat
|
|
7011
|
+
* {@link ADDITIONAL_CONTEXT_LIMIT}; a shared-budget caller passes a smaller,
|
|
7012
|
+
* per-reservation value instead).
|
|
7013
|
+
*/
|
|
7014
|
+
function truncateAdditionalContext(value, limit = ADDITIONAL_CONTEXT_LIMIT) {
|
|
7015
|
+
const alreadyMarked = value.endsWith(TRUNCATION_MARKER);
|
|
7016
|
+
if (!alreadyMarked && value.length <= limit) return value;
|
|
7017
|
+
if (limit <= 73) return TRUNCATION_MARKER.slice(0, Math.max(0, limit));
|
|
7018
|
+
return (alreadyMarked ? value.slice(0, value.length - 73) : value).slice(0, limit - 73) + TRUNCATION_MARKER;
|
|
7019
|
+
}
|
|
7020
|
+
/**
|
|
7021
|
+
* Length of a Cursor stdout JSON's `additional_context` string field, or 0
|
|
7022
|
+
* when the stdout is not JSON, has no such field, or that field isn't a
|
|
7023
|
+
* string.
|
|
7024
|
+
* @param stdout - A native Cursor JSON stdout candidate.
|
|
7025
|
+
*/
|
|
7026
|
+
function additionalContextLength(stdout) {
|
|
7027
|
+
let parsed;
|
|
7028
|
+
try {
|
|
7029
|
+
parsed = JSON.parse(stdout);
|
|
7030
|
+
} catch {
|
|
7031
|
+
return 0;
|
|
7032
|
+
}
|
|
7033
|
+
return isPlainObject(parsed) && typeof parsed.additional_context === "string" ? parsed.additional_context.length : 0;
|
|
7034
|
+
}
|
|
7035
|
+
/**
|
|
7036
|
+
* Re-serialize a Cursor stdout string with its `additional_context` field
|
|
7037
|
+
* dropped entirely — used once the shared budget has no room left even for
|
|
7038
|
+
* a truncated marker. Returns the input byte-for-byte unchanged when it is
|
|
7039
|
+
* not JSON or has no string `additional_context` field.
|
|
7040
|
+
* @param stdout - A native Cursor JSON stdout candidate.
|
|
7041
|
+
*/
|
|
7042
|
+
function omitAdditionalContext(stdout) {
|
|
7043
|
+
let parsed;
|
|
7044
|
+
try {
|
|
7045
|
+
parsed = JSON.parse(stdout);
|
|
7046
|
+
} catch {
|
|
7047
|
+
return stdout;
|
|
7048
|
+
}
|
|
7049
|
+
if (!isPlainObject(parsed) || typeof parsed.additional_context !== "string") return stdout;
|
|
7050
|
+
const { additional_context: _omitted, ...rest } = parsed;
|
|
7051
|
+
return JSON.stringify(rest);
|
|
7052
|
+
}
|
|
7053
|
+
/**
|
|
7054
|
+
* Re-serialize a Cursor stdout string with its `additional_context` field
|
|
7055
|
+
* capped at `limit` characters. Returns the input byte-for-byte unchanged
|
|
7056
|
+
* when it is not JSON, has no string `additional_context` field, or that
|
|
7057
|
+
* field is already within the limit — so callers can wrap every return path
|
|
7058
|
+
* unconditionally.
|
|
7059
|
+
* @param stdout - A native Cursor JSON stdout candidate.
|
|
7060
|
+
* @param limit - Effective ceiling for this call (see {@link truncateAdditionalContext}).
|
|
7061
|
+
*/
|
|
7062
|
+
function capAdditionalContext(stdout, limit = ADDITIONAL_CONTEXT_LIMIT) {
|
|
7063
|
+
let parsed;
|
|
7064
|
+
try {
|
|
7065
|
+
parsed = JSON.parse(stdout);
|
|
7066
|
+
} catch {
|
|
7067
|
+
return stdout;
|
|
7068
|
+
}
|
|
7069
|
+
if (!isPlainObject(parsed) || typeof parsed.additional_context !== "string") return stdout;
|
|
7070
|
+
const truncated = truncateAdditionalContext(parsed.additional_context, limit);
|
|
7071
|
+
if (truncated === parsed.additional_context) return stdout;
|
|
7072
|
+
return JSON.stringify({
|
|
7073
|
+
...parsed,
|
|
7074
|
+
additional_context: truncated
|
|
7075
|
+
});
|
|
7076
|
+
}
|
|
7077
|
+
//#endregion
|
|
7078
|
+
//#region src/adapters/cursor/context-budget.ts
|
|
7079
|
+
/**
|
|
7080
|
+
* @module context-budget
|
|
7081
|
+
* Cursor-only shared `additional_context` budget registry. Cursor 3.18.25
|
|
7082
|
+
* runs every hook plugin configured on an event in its OWN process, then
|
|
7083
|
+
* merges their `additional_context` outputs with a 9-char `"\n\n---\n\n"`
|
|
7084
|
+
* separator and drops the WHOLE merge past 10,000 UTF-16 units — so no
|
|
7085
|
+
* single process can know the total by itself. This module gives every
|
|
7086
|
+
* plugin's process a shared, best-effort view of that total via a small
|
|
7087
|
+
* JSON registry file under the project's state dir (see `../../runtime/paths.ts`),
|
|
7088
|
+
* keyed by `${sessionId}|${event}|${generationId ?? ""}|${toolUseId ?? ""}`
|
|
7089
|
+
* (one key per merge group — Cursor merges preToolUse/postToolUse/
|
|
7090
|
+
* postToolUseFailure PER TOOL CALL, so `toolUseId` joins the key on those
|
|
7091
|
+
* three events), with entries older than 10s ignored (concurrent hooks on one
|
|
7092
|
+
* event fire within the same second). Best-effort, fail-open throughout: any
|
|
7093
|
+
* I/O or JSON error degrades to "no shared budget", i.e. the flat
|
|
7094
|
+
* per-response cap in `./context-limit.ts` alone — never a thrown error, and
|
|
7095
|
+
* never a Cursor-side regression.
|
|
7096
|
+
*/
|
|
7097
|
+
const REGISTRY_FILE = "cursor-context-budget.json";
|
|
7098
|
+
/** Matches Cursor 3.18.25's observed `"\n\n---\n\n"` merge separator length. */
|
|
7099
|
+
const SEPARATOR_LENGTH = 9;
|
|
7100
|
+
const ENTRY_WINDOW_MS = 1e4;
|
|
7101
|
+
/** Below this, a truncated value would carry more marker than budget — omit the field instead. */
|
|
7102
|
+
const OMIT_THRESHOLD = 173;
|
|
7103
|
+
function isRegistry(value) {
|
|
7104
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7105
|
+
}
|
|
7106
|
+
function registryPath(stateDir) {
|
|
7107
|
+
return join(stateDir, REGISTRY_FILE);
|
|
7108
|
+
}
|
|
7109
|
+
function budgetKey(ctx) {
|
|
7110
|
+
return `${ctx.sessionId}|${ctx.event}|${ctx.generationId ?? ""}|${ctx.toolUseId ?? ""}`;
|
|
7111
|
+
}
|
|
7112
|
+
function loadRegistry(path) {
|
|
7113
|
+
try {
|
|
7114
|
+
if (!existsSync(path)) return {};
|
|
7115
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
7116
|
+
return isRegistry(parsed) ? parsed : {};
|
|
7117
|
+
} catch {
|
|
7118
|
+
return {};
|
|
7119
|
+
}
|
|
7120
|
+
}
|
|
7121
|
+
function freshEntries(entries, now) {
|
|
7122
|
+
return (entries ?? []).filter((entry) => now - entry.at <= ENTRY_WINDOW_MS);
|
|
7123
|
+
}
|
|
7124
|
+
/** Sum of a key's fresh entry lengths plus the separators already joining them. */
|
|
7125
|
+
function consumed(entries) {
|
|
7126
|
+
return entries.reduce((total, entry) => total + entry.length, 0) + SEPARATOR_LENGTH * Math.max(0, entries.length - 1);
|
|
7127
|
+
}
|
|
7128
|
+
/**
|
|
7129
|
+
* Reserve room in the shared budget for one hook's `additional_context`
|
|
7130
|
+
* contribution to one (session, event, generation) merge group. `wanted` is
|
|
7131
|
+
* accepted for a symmetric call shape with {@link recordAdditionalContext}
|
|
7132
|
+
* but does not shrink `allowed` itself — the ceiling only depends on what
|
|
7133
|
+
* OTHER entries already hold; a smaller `wanted` simply means the caller
|
|
7134
|
+
* won't need all of it. Best-effort, fail-open: any I/O/JSON error returns
|
|
7135
|
+
* the full flat ceiling, as if no other plugin had run.
|
|
7136
|
+
* @param input - Registry location, reservation key, and the wanted length.
|
|
7137
|
+
*/
|
|
7138
|
+
function reserveAdditionalContext(input) {
|
|
7139
|
+
try {
|
|
7140
|
+
const now = input.now ?? Date.now();
|
|
7141
|
+
const fresh = freshEntries(loadRegistry(registryPath(input.stateDir))[budgetKey(input)], now);
|
|
7142
|
+
const separator = fresh.length > 0 ? SEPARATOR_LENGTH : 0;
|
|
7143
|
+
return { allowed: Math.max(0, ADDITIONAL_CONTEXT_LIMIT - consumed(fresh) - separator) };
|
|
7144
|
+
} catch {
|
|
7145
|
+
return { allowed: ADDITIONAL_CONTEXT_LIMIT };
|
|
7146
|
+
}
|
|
7147
|
+
}
|
|
7148
|
+
/**
|
|
7149
|
+
* Record the length actually emitted for one reservation, best-effort.
|
|
7150
|
+
* Prunes every key's stale entries while it holds the write so the registry
|
|
7151
|
+
* file stays bounded. Silently no-ops on any I/O error (fail-open).
|
|
7152
|
+
* @param input - Registry location, reservation key, and the emitted length.
|
|
7153
|
+
*/
|
|
7154
|
+
function recordAdditionalContext(input) {
|
|
7155
|
+
try {
|
|
7156
|
+
const now = input.now ?? Date.now();
|
|
7157
|
+
const path = registryPath(input.stateDir);
|
|
7158
|
+
const registry = loadRegistry(path);
|
|
7159
|
+
const pruned = {};
|
|
7160
|
+
for (const [key, entries] of Object.entries(registry)) {
|
|
7161
|
+
const fresh = freshEntries(entries, now);
|
|
7162
|
+
if (fresh.length > 0) pruned[key] = fresh;
|
|
7163
|
+
}
|
|
7164
|
+
const key = budgetKey(input);
|
|
7165
|
+
pruned[key] = [...pruned[key] ?? [], {
|
|
7166
|
+
at: now,
|
|
7167
|
+
length: input.emitted
|
|
7168
|
+
}];
|
|
7169
|
+
atomicWrite(path, JSON.stringify(pruned));
|
|
7170
|
+
} catch {}
|
|
7171
|
+
}
|
|
7172
|
+
/**
|
|
7173
|
+
* Cap a Cursor stdout JSON's `additional_context` against the shared budget
|
|
7174
|
+
* instead of the flat per-response ceiling alone. Falls back to the plain
|
|
7175
|
+
* cap (`./context-limit.ts`), unbudgeted, when `budget` is `undefined` or
|
|
7176
|
+
* the stdout carries no `additional_context` at all.
|
|
7177
|
+
* @param stdout - A native Cursor JSON stdout candidate.
|
|
7178
|
+
* @param budget - Shared budget context, or `undefined` to skip it.
|
|
7179
|
+
*/
|
|
7180
|
+
function capAdditionalContextWithBudget(stdout, budget) {
|
|
7181
|
+
if (!budget) return capAdditionalContext(stdout);
|
|
7182
|
+
const wanted = additionalContextLength(stdout);
|
|
7183
|
+
if (wanted === 0) return stdout;
|
|
7184
|
+
const { allowed } = reserveAdditionalContext({
|
|
7185
|
+
...budget,
|
|
7186
|
+
wanted
|
|
7187
|
+
});
|
|
7188
|
+
if (allowed < OMIT_THRESHOLD) {
|
|
7189
|
+
process.stderr.write(`[fuse-harness] cursor: additional_context budget exhausted for ${budget.event} (allowed=${allowed})\n`);
|
|
7190
|
+
return omitAdditionalContext(stdout);
|
|
7191
|
+
}
|
|
7192
|
+
const capped = capAdditionalContext(stdout, Math.min(ADDITIONAL_CONTEXT_LIMIT, allowed));
|
|
7193
|
+
recordAdditionalContext({
|
|
7194
|
+
...budget,
|
|
7195
|
+
emitted: additionalContextLength(capped)
|
|
7196
|
+
});
|
|
7197
|
+
return capped;
|
|
7198
|
+
}
|
|
7199
|
+
//#endregion
|
|
7200
|
+
//#region src/adapters/cursor/respond.ts
|
|
7201
|
+
const AGENT_MESSAGE_EVENTS = /* @__PURE__ */ new Set([
|
|
7202
|
+
"preToolUse",
|
|
7203
|
+
"beforeShellExecution",
|
|
7204
|
+
"beforeMCPExecution"
|
|
7205
|
+
]);
|
|
7206
|
+
const USER_MESSAGE_EVENTS = /* @__PURE__ */ new Set([
|
|
7207
|
+
"preToolUse",
|
|
7208
|
+
"beforeShellExecution",
|
|
7209
|
+
"beforeMCPExecution",
|
|
7210
|
+
"beforeReadFile",
|
|
7211
|
+
"subagentStart"
|
|
7212
|
+
]);
|
|
7213
|
+
function permissionMessages(eventName, userMessage, agentMessage) {
|
|
7214
|
+
return {
|
|
7215
|
+
...userMessage && USER_MESSAGE_EVENTS.has(eventName) ? { user_message: userMessage } : {},
|
|
7216
|
+
...agentMessage && AGENT_MESSAGE_EVENTS.has(eventName) ? { agent_message: agentMessage } : {}
|
|
7217
|
+
};
|
|
7218
|
+
}
|
|
7219
|
+
function joinMessages(...values) {
|
|
7220
|
+
return values.filter((value) => typeof value === "string" && value.length > 0).join("\n");
|
|
7221
|
+
}
|
|
7222
|
+
function isRecord(value) {
|
|
7223
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7224
|
+
}
|
|
7225
|
+
/**
|
|
7226
|
+
* Render a portable policy prompt using the native Cursor event contract.
|
|
7227
|
+
* Switches exhaustively on {@link CursorResponseKind} — the `never` default
|
|
7228
|
+
* fails to compile if a new kind is ever added without a matching case.
|
|
7229
|
+
* `contract.known === false` is not tested separately: the single
|
|
7230
|
+
* `UNKNOWN_EVENT` fallback in events.ts always pairs `known: false` with
|
|
7231
|
+
* `response: "neutral"`, so both collapse to the same `"{}"` branch.
|
|
7232
|
+
*/
|
|
7233
|
+
function toCursorResponse(prompt, eventName) {
|
|
7234
|
+
const contract = cursorEventContract(eventName);
|
|
7235
|
+
const message = formatPrompt(prompt);
|
|
7236
|
+
switch (contract.response) {
|
|
7237
|
+
case "neutral":
|
|
7238
|
+
case "plugin-paths": return "{}";
|
|
7239
|
+
case "post-context":
|
|
7240
|
+
case "session-context": return capAdditionalContext(JSON.stringify({ additional_context: message }));
|
|
7241
|
+
case "followup": return JSON.stringify({ followup_message: message });
|
|
7242
|
+
case "compact-notice": return JSON.stringify({ user_message: prompt.userMessage ?? message });
|
|
7243
|
+
case "submit-control": return JSON.stringify({
|
|
7244
|
+
continue: prompt.kind !== "block",
|
|
7245
|
+
user_message: prompt.userMessage ?? message
|
|
7246
|
+
});
|
|
7247
|
+
case "permission": {
|
|
7248
|
+
if (prompt.kind === "inform") return JSON.stringify({
|
|
7249
|
+
permission: "allow",
|
|
7250
|
+
...permissionMessages(eventName, prompt.userMessage, prompt.reason ? message : void 0)
|
|
7251
|
+
});
|
|
7252
|
+
const userMessage = prompt.kind === "ask" ? `[downgraded from ask — Cursor does not enforce approval for this event]\n${message}` : message;
|
|
7253
|
+
return JSON.stringify({
|
|
7254
|
+
permission: "deny",
|
|
7255
|
+
...permissionMessages(eventName, userMessage, userMessage)
|
|
7256
|
+
});
|
|
7257
|
+
}
|
|
7258
|
+
default: return contract.response;
|
|
7259
|
+
}
|
|
7260
|
+
}
|
|
7261
|
+
/**
|
|
7262
|
+
* Convert a shared lifecycle handler's output to the native Cursor envelope.
|
|
7263
|
+
* The `neutral` and empty-`text` short circuits run before the switch (they
|
|
7264
|
+
* apply identically across several {@link CursorResponseKind} values), so
|
|
7265
|
+
* only the remaining 7 kinds need a case — `never` below still catches a
|
|
7266
|
+
* future kind added without updating this function. This is the single
|
|
7267
|
+
* point every Cursor stdout passes through exactly once (see `handle.ts`'s
|
|
7268
|
+
* `handleHook`), so `budget` — when supplied — is reserved from and
|
|
7269
|
+
* recorded into here, never at the inner `toCursorResponse` pre-cap (that
|
|
7270
|
+
* one's output is re-capped here again on the native-passthrough branch
|
|
7271
|
+
* below, so budgeting it too would double-count the same contribution).
|
|
7272
|
+
* @param stdout - The shared handler's raw stdout for this hook invocation.
|
|
7273
|
+
* @param eventName - Cursor's raw `hook_event_name`.
|
|
7274
|
+
* @param budget - Shared `additional_context` budget context (see
|
|
7275
|
+
* {@link CursorBudgetContext}); `undefined` falls back to the flat
|
|
7276
|
+
* per-response 10,000-char cap, unbudgeted.
|
|
7277
|
+
*/
|
|
7278
|
+
function toCursorLifecycleResponse(stdout, eventName, budget) {
|
|
7279
|
+
const contract = cursorEventContract(eventName);
|
|
7280
|
+
const native = parseNativeCursorStdout(stdout, eventName);
|
|
7281
|
+
if (native !== null) return capAdditionalContextWithBudget(native, budget);
|
|
7282
|
+
let text = stdout;
|
|
7283
|
+
let decision;
|
|
7284
|
+
let userMessage = "";
|
|
7285
|
+
let agentMessage = "";
|
|
7286
|
+
let decisionMessage = "";
|
|
7287
|
+
let structured = false;
|
|
7288
|
+
try {
|
|
7289
|
+
const parsed = JSON.parse(stdout);
|
|
7290
|
+
const subagentAsk = eventName === "subagentStart" && isRecord(parsed) && Object.hasOwn(parsed, "permission") && parsed.permission === "ask";
|
|
7291
|
+
const shared = isRecord(parsed) ? parsed : {};
|
|
7292
|
+
structured = true;
|
|
7293
|
+
decision = shared.hookSpecificOutput?.permissionDecision ?? (subagentAsk ? "ask" : void 0);
|
|
7294
|
+
userMessage = shared.systemMessage ?? (subagentAsk && Object.hasOwn(shared, "user_message") && typeof shared.user_message === "string" ? shared.user_message : "");
|
|
7295
|
+
decisionMessage = joinMessages(shared.hookSpecificOutput?.permissionDecisionReason, shared.reason);
|
|
7296
|
+
agentMessage = joinMessages(shared.hookSpecificOutput?.additionalContext, decisionMessage);
|
|
7297
|
+
text = joinMessages(agentMessage, userMessage, shared.followup_message);
|
|
7298
|
+
} catch {
|
|
7299
|
+
text = stdout;
|
|
7300
|
+
}
|
|
7301
|
+
if (contract.response === "neutral") return "{}";
|
|
7302
|
+
if (!text) return contract.response === "permission" ? "{\"permission\":\"allow\"}" : "{}";
|
|
7303
|
+
switch (contract.response) {
|
|
7304
|
+
case "session-context":
|
|
7305
|
+
case "post-context": return capAdditionalContextWithBudget(JSON.stringify({ additional_context: text }), budget);
|
|
7306
|
+
case "permission": {
|
|
7307
|
+
const permission = decision === "deny" || decision === "ask" ? "deny" : "allow";
|
|
7308
|
+
const denied = permission === "deny";
|
|
7309
|
+
if (eventName === "subagentStart" && !denied) return "{\"permission\":\"allow\"}";
|
|
7310
|
+
return JSON.stringify({
|
|
7311
|
+
permission,
|
|
7312
|
+
...permissionMessages(eventName, userMessage || (denied ? decisionMessage || agentMessage : ""), agentMessage || (denied ? decisionMessage || userMessage : structured ? "" : text))
|
|
7313
|
+
});
|
|
7314
|
+
}
|
|
7315
|
+
case "followup": return JSON.stringify({ followup_message: text });
|
|
7316
|
+
case "compact-notice": return JSON.stringify({ user_message: text });
|
|
7317
|
+
case "submit-control": return JSON.stringify({
|
|
7318
|
+
continue: true,
|
|
7319
|
+
user_message: text
|
|
7320
|
+
});
|
|
7321
|
+
case "plugin-paths": return "{}";
|
|
7322
|
+
default: return contract.response;
|
|
7323
|
+
}
|
|
7324
|
+
}
|
|
7325
|
+
//#endregion
|
|
6649
7326
|
//#region src/runtime/lifecycle-bridge.ts
|
|
6650
7327
|
/** Raw event name from a payload (Cline lacks one; lifecycle is Claude-only). */
|
|
6651
7328
|
function rawEvent(payload) {
|
|
6652
7329
|
return typeof payload.hook_event_name === "string" ? payload.hook_event_name : "";
|
|
6653
7330
|
}
|
|
6654
7331
|
/** Extract the `additionalContext` body from a `contextResponse(...)` stdout string ("" when empty/unparseable). */
|
|
6655
|
-
function additionalContextOf(stdout) {
|
|
7332
|
+
function additionalContextOf$1(stdout) {
|
|
6656
7333
|
if (!stdout) return "";
|
|
6657
7334
|
try {
|
|
6658
7335
|
return JSON.parse(stdout).hookSpecificOutput?.additionalContext ?? "";
|
|
@@ -6673,14 +7350,30 @@ function additionalContextOf(stdout) {
|
|
|
6673
7350
|
* @returns The native stdout, or `null` when unhandled.
|
|
6674
7351
|
*/
|
|
6675
7352
|
function lifecycleStdout(payload, cwd, scope, now, id = "claude-code") {
|
|
6676
|
-
|
|
6677
|
-
|
|
7353
|
+
const wireEvent = rawEvent(payload);
|
|
7354
|
+
if (id !== "cursor") return dispatchLifecycle({
|
|
7355
|
+
event: wireEvent,
|
|
6678
7356
|
payload,
|
|
6679
7357
|
cwd,
|
|
6680
7358
|
scope,
|
|
6681
7359
|
now,
|
|
6682
7360
|
id
|
|
6683
7361
|
});
|
|
7362
|
+
const lifecycleEvent = cursorEventContract(wireEvent).lifecycle;
|
|
7363
|
+
if (lifecycleEvent === null) return null;
|
|
7364
|
+
const sessionId = typeof payload.session_id === "string" ? payload.session_id : payload.conversation_id;
|
|
7365
|
+
const stdout = dispatchLifecycle({
|
|
7366
|
+
event: lifecycleEvent,
|
|
7367
|
+
payload: {
|
|
7368
|
+
...payload,
|
|
7369
|
+
session_id: sessionId
|
|
7370
|
+
},
|
|
7371
|
+
cwd,
|
|
7372
|
+
scope,
|
|
7373
|
+
now,
|
|
7374
|
+
id
|
|
7375
|
+
});
|
|
7376
|
+
return stdout === null ? null : toCursorLifecycleResponse(stdout, wireEvent);
|
|
6684
7377
|
}
|
|
6685
7378
|
/**
|
|
6686
7379
|
* Post-edit additions for core-scope PostToolUse: auto-document a Read of a
|
|
@@ -6702,7 +7395,7 @@ async function postEditContext(scope, event, now, id = "claude-code") {
|
|
|
6702
7395
|
const sniper = trackSessionChanges(event.sessionId, event.filePath, void 0, now);
|
|
6703
7396
|
const lint = postEditTypescript(event.filePath);
|
|
6704
7397
|
if (!sniper || !lint) return sniper || lint;
|
|
6705
|
-
return contextResponse("PostToolUse", `${additionalContextOf(sniper)}\n\n${additionalContextOf(lint)}`);
|
|
7398
|
+
return contextResponse("PostToolUse", `${additionalContextOf$1(sniper)}\n\n${additionalContextOf$1(lint)}`);
|
|
6706
7399
|
}
|
|
6707
7400
|
//#endregion
|
|
6708
7401
|
//#region src/policy/trivial-edits.ts
|
|
@@ -7694,11 +8387,37 @@ const TRIVIAL_BUDGET = 4;
|
|
|
7694
8387
|
* {@link withDenyLoop} rewrite an identical retried deny (decision unchanged).
|
|
7695
8388
|
*/
|
|
7696
8389
|
async function gate(input) {
|
|
7697
|
-
|
|
8390
|
+
return finalizeGate(input, await runGates(input), input.command);
|
|
8391
|
+
}
|
|
8392
|
+
/**
|
|
8393
|
+
* Gate independent Cursor command candidates without merging their shell syntax.
|
|
8394
|
+
* Stateful outcome bookkeeping runs once, against the command that decided the
|
|
8395
|
+
* aggregate result. A single distinct candidate follows {@link gate} verbatim.
|
|
8396
|
+
*/
|
|
8397
|
+
async function gateCommandCandidates(input, candidates) {
|
|
8398
|
+
const distinct = [...new Set(candidates)];
|
|
8399
|
+
if (distinct.length <= 1) return gate(input);
|
|
8400
|
+
let decisive;
|
|
8401
|
+
for (const command of distinct) {
|
|
8402
|
+
const prompt = await runGates({
|
|
8403
|
+
...input,
|
|
8404
|
+
command
|
|
8405
|
+
});
|
|
8406
|
+
if (!prompt) continue;
|
|
8407
|
+
if (!decisive || promptRank(prompt) > promptRank(decisive.prompt)) decisive = {
|
|
8408
|
+
prompt,
|
|
8409
|
+
command
|
|
8410
|
+
};
|
|
8411
|
+
if (prompt.kind === "block") break;
|
|
8412
|
+
}
|
|
8413
|
+
return finalizeGate(input, decisive?.prompt ?? null, decisive?.command ?? input.command);
|
|
8414
|
+
}
|
|
8415
|
+
/** Persist one final gate outcome and apply deny-loop enrichment exactly once. */
|
|
8416
|
+
function finalizeGate(input, prompt, command) {
|
|
7698
8417
|
const op = {
|
|
7699
8418
|
filePath: input.filePath,
|
|
7700
8419
|
content: input.content,
|
|
7701
|
-
command
|
|
8420
|
+
command
|
|
7702
8421
|
};
|
|
7703
8422
|
const dir = dirname(input.trackFile);
|
|
7704
8423
|
recordOneShot(prompt, op, {
|
|
@@ -7713,6 +8432,10 @@ async function gate(input) {
|
|
|
7713
8432
|
sessionId: input.sessionId
|
|
7714
8433
|
});
|
|
7715
8434
|
}
|
|
8435
|
+
/** Higher numeric values dominate while equal values preserve wire order. */
|
|
8436
|
+
function promptRank(prompt) {
|
|
8437
|
+
return prompt.kind === "block" ? 3 : prompt.kind === "ask" ? 2 : 1;
|
|
8438
|
+
}
|
|
7716
8439
|
/** Stateless guards, then the trivial fast path, then the stateful APEX gates. */
|
|
7717
8440
|
async function runGates(input) {
|
|
7718
8441
|
const precommit = preCommitGate(input.tool, input.command, input.cwd);
|
|
@@ -7900,8 +8623,8 @@ async function recordActivity(file, activity) {
|
|
|
7900
8623
|
* - gemini-cli/cline: their real hook schemas have no interactive "ask"
|
|
7901
8624
|
* state (deny is the only blocking outcome), so `ask` and `inform` already
|
|
7902
8625
|
* both resolve to non-blocking context injection — unchanged.
|
|
7903
|
-
* - cursor: `ask`
|
|
7904
|
-
*
|
|
8626
|
+
* - cursor: `ask` degrades to a native `permission:"deny"` because Cursor does
|
|
8627
|
+
* not reliably apply `ask` on `preToolUse`; `inform` stays non-blocking.
|
|
7905
8628
|
* - hermes: delegated to the adapter's `toHermesResponse` — `block` ->
|
|
7906
8629
|
* `{decision:"block",reason}`; `ask`/`inform` degrade to non-blocking
|
|
7907
8630
|
* `{context}` (Hermes has no interactive "ask" state).
|
|
@@ -7955,25 +8678,7 @@ function respond(id, prompt, event = "PreToolUse") {
|
|
|
7955
8678
|
systemMessage: userMessage
|
|
7956
8679
|
});
|
|
7957
8680
|
return JSON.stringify({ hookSpecificOutput: { additionalContext: message } });
|
|
7958
|
-
case "cursor":
|
|
7959
|
-
if (kind === "inform") {
|
|
7960
|
-
if (userMessage) return JSON.stringify({
|
|
7961
|
-
permission: "allow",
|
|
7962
|
-
user_message: userMessage,
|
|
7963
|
-
...reason ? { agent_message: message } : {}
|
|
7964
|
-
});
|
|
7965
|
-
return JSON.stringify({
|
|
7966
|
-
permission: "allow",
|
|
7967
|
-
user_message: message,
|
|
7968
|
-
agent_message: message
|
|
7969
|
-
});
|
|
7970
|
-
}
|
|
7971
|
-
return JSON.stringify({
|
|
7972
|
-
permission: kind === "block" ? "deny" : "ask",
|
|
7973
|
-
continue: false,
|
|
7974
|
-
user_message: message,
|
|
7975
|
-
agent_message: message
|
|
7976
|
-
});
|
|
8681
|
+
case "cursor": return toCursorResponse(prompt, event === "PreToolUse" ? "preToolUse" : event);
|
|
7977
8682
|
case "hermes": return toHermesResponse(prompt);
|
|
7978
8683
|
case "kimi": return toKimiResponse(prompt);
|
|
7979
8684
|
case "cline":
|
|
@@ -8658,8 +9363,8 @@ function segments(command) {
|
|
|
8658
9363
|
}
|
|
8659
9364
|
/** Strip a redirection (`>`, `>>`, `<`, `2>`, `&>`, …) and everything after — its target is WRITTEN, not read. */
|
|
8660
9365
|
function beforeRedirect(segment) {
|
|
8661
|
-
const
|
|
8662
|
-
return
|
|
9366
|
+
const first = shellOutputRedirects(segment)[0];
|
|
9367
|
+
return first ? segment.slice(0, first.start) : segment;
|
|
8663
9368
|
}
|
|
8664
9369
|
/** Naive shell tokenizer: whitespace-split, stripping one matching layer of quotes per token. */
|
|
8665
9370
|
function tokenize(segment) {
|
|
@@ -9155,7 +9860,7 @@ async function allowOutcome(id, event, payload, mcpDir, cwd, evidence, corpusRoo
|
|
|
9155
9860
|
stdout: respond(id, userMessage ? {
|
|
9156
9861
|
...lesson,
|
|
9157
9862
|
userMessage
|
|
9158
|
-
} : lesson),
|
|
9863
|
+
} : lesson, event.eventName ?? "PreToolUse"),
|
|
9159
9864
|
exit: 0
|
|
9160
9865
|
};
|
|
9161
9866
|
}
|
|
@@ -9170,12 +9875,12 @@ async function allowOutcome(id, event, payload, mcpDir, cwd, evidence, corpusRoo
|
|
|
9170
9875
|
title: "APEX freshness",
|
|
9171
9876
|
reason: "",
|
|
9172
9877
|
userMessage
|
|
9173
|
-
}),
|
|
9878
|
+
}, event.eventName ?? "PreToolUse"),
|
|
9174
9879
|
exit: 0
|
|
9175
9880
|
};
|
|
9176
9881
|
}
|
|
9177
9882
|
return {
|
|
9178
|
-
stdout: notice ? respond(id, notice) : "",
|
|
9883
|
+
stdout: notice ? respond(id, notice, event.eventName ?? "PreToolUse") : "",
|
|
9179
9884
|
exit: 0
|
|
9180
9885
|
};
|
|
9181
9886
|
}
|
|
@@ -9316,7 +10021,7 @@ function getPendingDeny(sessionIdRaw, home = homedir()) {
|
|
|
9316
10021
|
//#endregion
|
|
9317
10022
|
//#region src/runtime/confirm/confirm-state.ts
|
|
9318
10023
|
/** Confirmation freshness window (G2) — 5 minutes, matches the validated prototype. */
|
|
9319
|
-
const TTL_MS = 300 * 1e3;
|
|
10024
|
+
const TTL_MS$1 = 300 * 1e3;
|
|
9320
10025
|
/**
|
|
9321
10026
|
* Pose a confirmation token for this session's pending action. No-op while
|
|
9322
10027
|
* {@link isSubagentActive} (G0, confirm-subagent.ts) — the sole gate that
|
|
@@ -9356,7 +10061,7 @@ function consumeConfirmToken(sessionIdRaw, hash, now, home = homedir()) {
|
|
|
9356
10061
|
if (!sid) return false;
|
|
9357
10062
|
const tok = loadSessionState(sid, home).confirmToken;
|
|
9358
10063
|
if (!tok || tok.hash !== hash) return false;
|
|
9359
|
-
if (now - tok.ts > TTL_MS) {
|
|
10064
|
+
if (now - tok.ts > TTL_MS$1) {
|
|
9360
10065
|
dropConfirmToken(sid, home);
|
|
9361
10066
|
return false;
|
|
9362
10067
|
}
|
|
@@ -9364,6 +10069,141 @@ function consumeConfirmToken(sessionIdRaw, hash, now, home = homedir()) {
|
|
|
9364
10069
|
return true;
|
|
9365
10070
|
}
|
|
9366
10071
|
//#endregion
|
|
10072
|
+
//#region src/runtime/confirm/codex-confirm.ts
|
|
10073
|
+
const TTL_MS = 300 * 1e3;
|
|
10074
|
+
const STATE_VERSION = 1;
|
|
10075
|
+
function statePath(sid, home) {
|
|
10076
|
+
return join(sessionsDir(home), `codex-confirm-${sid}.json`);
|
|
10077
|
+
}
|
|
10078
|
+
function lockDir(sid, home) {
|
|
10079
|
+
return join(sessionsDir(home), ".confirm-locks", sid);
|
|
10080
|
+
}
|
|
10081
|
+
function loadState(sid, home) {
|
|
10082
|
+
const path = statePath(sid, home);
|
|
10083
|
+
if (!existsSync(path)) return {};
|
|
10084
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
10085
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : {};
|
|
10086
|
+
}
|
|
10087
|
+
function saveState(sid, state, home) {
|
|
10088
|
+
mkdirSync(sessionsDir(home), {
|
|
10089
|
+
recursive: true,
|
|
10090
|
+
mode: 448
|
|
10091
|
+
});
|
|
10092
|
+
atomicWrite(statePath(sid, home), JSON.stringify(state, null, 2));
|
|
10093
|
+
}
|
|
10094
|
+
/** Canonical identity of one Codex shell action and its display token. */
|
|
10095
|
+
function codexAction(tool, cwd, command, now) {
|
|
10096
|
+
const canonicalCommand = commandToString(command);
|
|
10097
|
+
if (!canonicalCommand) return null;
|
|
10098
|
+
const identity = JSON.stringify({
|
|
10099
|
+
version: STATE_VERSION,
|
|
10100
|
+
harness: "codex",
|
|
10101
|
+
tool,
|
|
10102
|
+
cwd: resolve(cwd),
|
|
10103
|
+
command: canonicalCommand
|
|
10104
|
+
});
|
|
10105
|
+
return {
|
|
10106
|
+
hash: hashForAction(identity),
|
|
10107
|
+
code: displayCodeForAction(identity),
|
|
10108
|
+
command: canonicalCommand,
|
|
10109
|
+
ts: now
|
|
10110
|
+
};
|
|
10111
|
+
}
|
|
10112
|
+
/** Atomically authorize or reject a Codex action, recording the current denial when needed. */
|
|
10113
|
+
function authorizeCodexAction(sessionIdRaw, action, toolUseId, now, home = homedir()) {
|
|
10114
|
+
const sid = sanitizeSessionId(sessionIdRaw);
|
|
10115
|
+
if (!sid) return {
|
|
10116
|
+
allow: false,
|
|
10117
|
+
reason: "state-io"
|
|
10118
|
+
};
|
|
10119
|
+
try {
|
|
10120
|
+
const result = withTrackLockSync(lockDir(sid, home), () => {
|
|
10121
|
+
const state = loadState(sid, home);
|
|
10122
|
+
const token = state.codexConfirmToken;
|
|
10123
|
+
const receipt = state.codexConfirmReceipt;
|
|
10124
|
+
const receiptExpired = receipt !== void 0 && now - receipt.ts > TTL_MS;
|
|
10125
|
+
if (!receiptExpired && receipt?.hash === action.hash && receipt.toolUseId === toolUseId && toolUseId) return { allow: true };
|
|
10126
|
+
let reason = "no-token";
|
|
10127
|
+
if (token) if (token.hash !== action.hash) reason = "mismatch";
|
|
10128
|
+
else if (now - token.ts > TTL_MS) reason = "expired";
|
|
10129
|
+
else if (!toolUseId) reason = "missing-tool-use-id";
|
|
10130
|
+
else {
|
|
10131
|
+
const { codexConfirmToken: _token, codexConfirmPending: _pending, ...rest } = state;
|
|
10132
|
+
saveState(sid, {
|
|
10133
|
+
...rest,
|
|
10134
|
+
codexConfirmReceipt: {
|
|
10135
|
+
...action,
|
|
10136
|
+
toolUseId
|
|
10137
|
+
}
|
|
10138
|
+
}, home);
|
|
10139
|
+
return { allow: true };
|
|
10140
|
+
}
|
|
10141
|
+
else if (receipt?.hash === action.hash) reason = receiptExpired ? "expired" : "already-consumed";
|
|
10142
|
+
if (reason === "missing-tool-use-id") return {
|
|
10143
|
+
allow: false,
|
|
10144
|
+
reason
|
|
10145
|
+
};
|
|
10146
|
+
const pending = state.codexConfirmPending;
|
|
10147
|
+
const stablePending = pending?.hash === action.hash ? pending : action;
|
|
10148
|
+
const { codexConfirmToken: _token, codexConfirmReceipt: _receipt, ...withoutReceipt } = state;
|
|
10149
|
+
const rest = receiptExpired ? withoutReceipt : {
|
|
10150
|
+
...withoutReceipt,
|
|
10151
|
+
codexConfirmReceipt: receipt
|
|
10152
|
+
};
|
|
10153
|
+
saveState(sid, {
|
|
10154
|
+
...rest,
|
|
10155
|
+
codexConfirmPending: stablePending
|
|
10156
|
+
}, home);
|
|
10157
|
+
return {
|
|
10158
|
+
allow: false,
|
|
10159
|
+
reason
|
|
10160
|
+
};
|
|
10161
|
+
});
|
|
10162
|
+
return result === LOCK_FAILED ? {
|
|
10163
|
+
allow: false,
|
|
10164
|
+
reason: "state-io"
|
|
10165
|
+
} : result;
|
|
10166
|
+
} catch {
|
|
10167
|
+
return {
|
|
10168
|
+
allow: false,
|
|
10169
|
+
reason: "state-io"
|
|
10170
|
+
};
|
|
10171
|
+
}
|
|
10172
|
+
}
|
|
10173
|
+
/**
|
|
10174
|
+
* Atomically arm the last Codex denial, consuming its pending record exactly once.
|
|
10175
|
+
* A classified root prompt may bypass G0; classified subagent/unknown prompts fail closed.
|
|
10176
|
+
*/
|
|
10177
|
+
function submitCodexConfirmation(sessionIdRaw, text, now, home = homedir(), origin) {
|
|
10178
|
+
const sid = sanitizeSessionId(sessionIdRaw);
|
|
10179
|
+
if (!sid) return;
|
|
10180
|
+
const refusal = /\b(non|no|stop|annule|cancel|abort|nope|laisse tomber|pas maintenant)\b/i.test(text);
|
|
10181
|
+
const typedCode = text.trim().match(/^confirm[ _-]*([0-9a-f]{4})$/i)?.[1];
|
|
10182
|
+
try {
|
|
10183
|
+
withTrackLockSync(lockDir(sid, home), () => {
|
|
10184
|
+
const state = loadState(sid, home);
|
|
10185
|
+
if (origin !== void 0 && origin !== "root") return;
|
|
10186
|
+
if (refusal) {
|
|
10187
|
+
const { codexConfirmPending: _pending, codexConfirmToken: _token, ...rest } = state;
|
|
10188
|
+
saveState(sid, rest, home);
|
|
10189
|
+
return;
|
|
10190
|
+
}
|
|
10191
|
+
if (!typedCode) return;
|
|
10192
|
+
if (origin === void 0 && isSubagentActive(sid, now, home)) return;
|
|
10193
|
+
const pending = state.codexConfirmPending;
|
|
10194
|
+
if (!pending || pending.code.toLowerCase() !== typedCode.toLowerCase()) return;
|
|
10195
|
+
const { codexConfirmPending: _pending, ...rest } = state;
|
|
10196
|
+
saveState(sid, {
|
|
10197
|
+
...rest,
|
|
10198
|
+
codexConfirmToken: {
|
|
10199
|
+
...pending,
|
|
10200
|
+
ts: now
|
|
10201
|
+
}
|
|
10202
|
+
}, home);
|
|
10203
|
+
});
|
|
10204
|
+
} catch {}
|
|
10205
|
+
}
|
|
10206
|
+
//#endregion
|
|
9367
10207
|
//#region src/runtime/confirm/confirm-gate.ts
|
|
9368
10208
|
/**
|
|
9369
10209
|
* Harnesses where `respond.ts` silently downgrades `kind: "ask"` to a hard
|
|
@@ -9387,9 +10227,23 @@ const DEGRADES_ASK_TO_DENY = /* @__PURE__ */ new Set(["codex", "kimi"]);
|
|
|
9387
10227
|
* @param now - Epoch ms.
|
|
9388
10228
|
* @param home - Test-only OS home override.
|
|
9389
10229
|
*/
|
|
9390
|
-
function confirmGate(id, prompt, command, sessionId, now, home) {
|
|
10230
|
+
function confirmGate(id, prompt, command, sessionId, now, home, codex) {
|
|
9391
10231
|
if (prompt.kind !== "ask" || !DEGRADES_ASK_TO_DENY.has(id) || !command || isIrreversible(command)) return null;
|
|
9392
10232
|
try {
|
|
10233
|
+
if (id === "codex" && codex) {
|
|
10234
|
+
const action = codexAction(codex.tool, codex.cwd, command, now);
|
|
10235
|
+
if (!action) return null;
|
|
10236
|
+
const verdict = authorizeCodexAction(sessionId, action, codex.toolUseId, now, home);
|
|
10237
|
+
if (verdict.allow) return verdict;
|
|
10238
|
+
const diagnostic = `rule ID: ${prompt.ruleId ?? "policy:ask"}\ncanonical command: ${action.command}\nexpected token: CONFIRM ${action.code}\nrejection: ${verdict.reason}`;
|
|
10239
|
+
return {
|
|
10240
|
+
allow: false,
|
|
10241
|
+
prompt: {
|
|
10242
|
+
...prompt,
|
|
10243
|
+
reason: `${prompt.reason}\nPour autoriser, réponds : CONFIRM ${action.code}\n${diagnostic}`
|
|
10244
|
+
}
|
|
10245
|
+
};
|
|
10246
|
+
}
|
|
9393
10247
|
const hash = hashForAction(command);
|
|
9394
10248
|
if (consumeConfirmToken(sessionId, hash, now, home)) return { allow: true };
|
|
9395
10249
|
const code = displayCodeForAction(command);
|
|
@@ -9430,9 +10284,19 @@ async function handlePre(ctx) {
|
|
|
9430
10284
|
}
|
|
9431
10285
|
const designBlock = designGate(payload, event, designCacheDir, opts.cwd, opts.corpusRoot);
|
|
9432
10286
|
if (designBlock) return {
|
|
9433
|
-
stdout: withDenyNotice(id, respond(id, designBlock), designBlock, event.sessionId, dirname(file), opts.now),
|
|
10287
|
+
stdout: withDenyNotice(id, respond(id, designBlock, event.eventName ?? "PreToolUse"), designBlock, event.sessionId, dirname(file), opts.now),
|
|
9434
10288
|
exit: 0
|
|
9435
10289
|
};
|
|
10290
|
+
if (id === "cursor" && (event.eventName === "beforeReadFile" || event.eventName === "beforeTabFileRead")) {
|
|
10291
|
+
const readPolicy = evaluate({
|
|
10292
|
+
tool: event.tool,
|
|
10293
|
+
filePath: event.filePath
|
|
10294
|
+
});
|
|
10295
|
+
return {
|
|
10296
|
+
stdout: readPolicy.prompt ? respond(id, readPolicy.prompt, event.eventName) : JSON.stringify({ permission: "allow" }),
|
|
10297
|
+
exit: 0
|
|
10298
|
+
};
|
|
10299
|
+
}
|
|
9436
10300
|
if (opts.scope === "security") return {
|
|
9437
10301
|
stdout: event.files?.length ? securityAdvisoryForPatch(event.files, opts.now) : securityAdvisory(event.tool, event.filePath ?? "", opts.now),
|
|
9438
10302
|
exit: 0
|
|
@@ -9448,11 +10312,12 @@ async function handlePre(ctx) {
|
|
|
9448
10312
|
if (event.files && event.files.length > 0) {
|
|
9449
10313
|
const patchPrompt = applyPatchGate(event.files, opts.cwd);
|
|
9450
10314
|
if (patchPrompt) return {
|
|
9451
|
-
stdout: withDenyNotice(id, respond(id, patchPrompt), patchPrompt, event.sessionId, dirname(file), opts.now),
|
|
10315
|
+
stdout: withDenyNotice(id, respond(id, patchPrompt, event.eventName ?? "PreToolUse"), patchPrompt, event.sessionId, dirname(file), opts.now),
|
|
9452
10316
|
exit: 0
|
|
9453
10317
|
};
|
|
9454
10318
|
}
|
|
9455
|
-
const
|
|
10319
|
+
const refs = opts.refsDir ? await loadRefs(opts.refsDir) : void 0;
|
|
10320
|
+
const gateInput = {
|
|
9456
10321
|
sessionId: event.sessionId,
|
|
9457
10322
|
framework,
|
|
9458
10323
|
tool: event.tool,
|
|
@@ -9460,7 +10325,7 @@ async function handlePre(ctx) {
|
|
|
9460
10325
|
content: event.content,
|
|
9461
10326
|
command: event.command,
|
|
9462
10327
|
cwd: opts.cwd,
|
|
9463
|
-
refs
|
|
10328
|
+
refs,
|
|
9464
10329
|
isReplaceAll: event.input.replace_all === true,
|
|
9465
10330
|
oldString: event.oldString,
|
|
9466
10331
|
agentType: event.agentType,
|
|
@@ -9470,9 +10335,14 @@ async function handlePre(ctx) {
|
|
|
9470
10335
|
trackFile: file,
|
|
9471
10336
|
transcriptPath: typeof payload.transcript_path === "string" ? payload.transcript_path : void 0,
|
|
9472
10337
|
neverApproval: id === "codex" && isBypassPermissions(event.permissionMode)
|
|
9473
|
-
}
|
|
10338
|
+
};
|
|
10339
|
+
const prompt = id === "cursor" && (event.commandCandidates?.length ?? 0) > 1 ? await gateCommandCandidates(gateInput, event.commandCandidates) : await gate(gateInput);
|
|
9474
10340
|
if (prompt) {
|
|
9475
|
-
const confirm = confirmGate(id, prompt, event.command, event.sessionId, opts.now, opts.home
|
|
10341
|
+
const confirm = confirmGate(id, prompt, event.command, event.sessionId, opts.now, opts.home, id === "codex" ? {
|
|
10342
|
+
tool: event.tool,
|
|
10343
|
+
cwd: event.cwd ?? opts.cwd,
|
|
10344
|
+
toolUseId: event.toolUseId
|
|
10345
|
+
} : void 0);
|
|
9476
10346
|
if (confirm?.allow) return allowOutcome(id, event, payload, designCacheDir, opts.cwd, {
|
|
9477
10347
|
trackFile: file,
|
|
9478
10348
|
windowMs: opts.windowMs,
|
|
@@ -9480,7 +10350,7 @@ async function handlePre(ctx) {
|
|
|
9480
10350
|
}, opts.corpusRoot);
|
|
9481
10351
|
const finalPrompt = confirm ? confirm.prompt : prompt;
|
|
9482
10352
|
return {
|
|
9483
|
-
stdout: withDenyNotice(id, respond(id, finalPrompt), finalPrompt, event.sessionId, dirname(file), opts.now),
|
|
10353
|
+
stdout: withDenyNotice(id, respond(id, finalPrompt, event.eventName ?? "PreToolUse"), finalPrompt, event.sessionId, dirname(file), opts.now),
|
|
9484
10354
|
exit: 0
|
|
9485
10355
|
};
|
|
9486
10356
|
}
|
|
@@ -9847,14 +10717,14 @@ function recordCodexPostFailure(tool, result, opts) {
|
|
|
9847
10717
|
//#endregion
|
|
9848
10718
|
//#region src/runtime/post-fanout.ts
|
|
9849
10719
|
/**
|
|
9850
|
-
* Fan
|
|
10720
|
+
* Fan Codex `apply_patch` or Cursor `afterFileEdit` `event.files` into one
|
|
9851
10721
|
* per-file event per touched file, so the per-file PostToolUse gates (SOLID
|
|
9852
10722
|
* size, Tailwind, tracking, post-edit context) see EACH file instead of the
|
|
9853
10723
|
* whole patch (whose own `filePath`/`content` are always undefined). `add`
|
|
9854
10724
|
* maps to `Write`, `update` to `Edit` (mirrors the Pre-phase `applyPatchGate`'s
|
|
9855
10725
|
* tool mapping); `delete`/`move` map to a tool none of those Write|Edit-gated
|
|
9856
10726
|
* checks recognize, so they no-op on it without special-casing each gate.
|
|
9857
|
-
* Returns `[event]` unchanged
|
|
10727
|
+
* Returns `[event]` unchanged when no per-file changes are present.
|
|
9858
10728
|
* @param event - The normalized PostToolUse event.
|
|
9859
10729
|
* @returns One event per touched file, or the original event.
|
|
9860
10730
|
*/
|
|
@@ -9864,7 +10734,8 @@ function fanOutFiles(event) {
|
|
|
9864
10734
|
...event,
|
|
9865
10735
|
tool: f.op === "add" ? "Write" : f.op === "update" ? "Edit" : "apply_patch:delete",
|
|
9866
10736
|
filePath: f.filePath,
|
|
9867
|
-
content: f.op === "delete" ? void 0 : f.content
|
|
10737
|
+
content: f.op === "delete" ? void 0 : f.content,
|
|
10738
|
+
oldString: f.oldString
|
|
9868
10739
|
}));
|
|
9869
10740
|
}
|
|
9870
10741
|
/**
|
|
@@ -9884,7 +10755,106 @@ function firstFileMatch(files, check) {
|
|
|
9884
10755
|
return "";
|
|
9885
10756
|
}
|
|
9886
10757
|
//#endregion
|
|
10758
|
+
//#region src/runtime/post-outcome.ts
|
|
10759
|
+
/** Extract the additional-context body from a Claude-shaped post response. */
|
|
10760
|
+
function additionalContextOf(stdout) {
|
|
10761
|
+
if (!stdout) return "";
|
|
10762
|
+
try {
|
|
10763
|
+
return JSON.parse(stdout).hookSpecificOutput?.additionalContext ?? "";
|
|
10764
|
+
} catch {
|
|
10765
|
+
return "";
|
|
10766
|
+
}
|
|
10767
|
+
}
|
|
10768
|
+
function cursorPostToolOutput(eventName, designWarn, userMessage, extra) {
|
|
10769
|
+
if (!/^postToolUse$/i.test(eventName)) return "";
|
|
10770
|
+
const context = [
|
|
10771
|
+
designWarn ? formatPrompt(designWarn) : "",
|
|
10772
|
+
designWarn?.userMessage,
|
|
10773
|
+
userMessage,
|
|
10774
|
+
additionalContextOf(extra)
|
|
10775
|
+
].filter(Boolean).join("\n");
|
|
10776
|
+
return context ? JSON.stringify({ additional_context: context }) : "";
|
|
10777
|
+
}
|
|
10778
|
+
/**
|
|
10779
|
+
* Assemble the final PostToolUse response after all side effects and deny paths.
|
|
10780
|
+
* @param input - Final assembly values captured at their original evaluation times.
|
|
10781
|
+
* @returns The native hook outcome.
|
|
10782
|
+
*/
|
|
10783
|
+
function postOutcome(input) {
|
|
10784
|
+
const { id, agentId, sessionId, now, cwd, activities, files, designCacheDir, designWarn, extra, cursorAfterFileEdit, cursorEventName } = input;
|
|
10785
|
+
const noticeLines = [];
|
|
10786
|
+
for (const f of files) {
|
|
10787
|
+
const notice = designPassNotice({
|
|
10788
|
+
agentId,
|
|
10789
|
+
tool: f.tool,
|
|
10790
|
+
filePath: f.filePath ?? "",
|
|
10791
|
+
content: f.content ?? "",
|
|
10792
|
+
url: "",
|
|
10793
|
+
phase: "post"
|
|
10794
|
+
}, designCacheDir);
|
|
10795
|
+
if (notice?.userMessage) noticeLines.push(notice.userMessage);
|
|
10796
|
+
}
|
|
10797
|
+
const notice = noticeLines.length ? {
|
|
10798
|
+
kind: "inform",
|
|
10799
|
+
title: "Design pipeline",
|
|
10800
|
+
reason: "",
|
|
10801
|
+
userMessage: noticeLines.join("\n")
|
|
10802
|
+
} : null;
|
|
10803
|
+
const refNotice = refCreditNoticeFor(activities, sessionId, now, defaultStateDir(cwd));
|
|
10804
|
+
const userMessage = [notice?.userMessage, refNotice].filter(Boolean).join("\n") || void 0;
|
|
10805
|
+
if (cursorAfterFileEdit) return {
|
|
10806
|
+
stdout: "{}",
|
|
10807
|
+
exit: 0
|
|
10808
|
+
};
|
|
10809
|
+
if (id === "cursor") return {
|
|
10810
|
+
stdout: cursorPostToolOutput(cursorEventName, designWarn, userMessage, extra),
|
|
10811
|
+
exit: 0
|
|
10812
|
+
};
|
|
10813
|
+
if (designWarn) return {
|
|
10814
|
+
stdout: respond(id, userMessage ? {
|
|
10815
|
+
...designWarn,
|
|
10816
|
+
userMessage
|
|
10817
|
+
} : designWarn, "PostToolUse"),
|
|
10818
|
+
exit: 0
|
|
10819
|
+
};
|
|
10820
|
+
if (!userMessage) return {
|
|
10821
|
+
stdout: extra,
|
|
10822
|
+
exit: 0
|
|
10823
|
+
};
|
|
10824
|
+
const withUserMessage = notice ? {
|
|
10825
|
+
...notice,
|
|
10826
|
+
userMessage
|
|
10827
|
+
} : {
|
|
10828
|
+
kind: "inform",
|
|
10829
|
+
title: "Compliance",
|
|
10830
|
+
reason: "",
|
|
10831
|
+
userMessage
|
|
10832
|
+
};
|
|
10833
|
+
if (!extra) return {
|
|
10834
|
+
stdout: respond(id, withUserMessage, "PostToolUse"),
|
|
10835
|
+
exit: 0
|
|
10836
|
+
};
|
|
10837
|
+
if (id === "claude-code" || id === "codex") return {
|
|
10838
|
+
stdout: attachSystemMessage(extra, userMessage),
|
|
10839
|
+
exit: 0
|
|
10840
|
+
};
|
|
10841
|
+
if (id === "kimi") return {
|
|
10842
|
+
stdout: respond(id, withUserMessage, "PostToolUse"),
|
|
10843
|
+
exit: 0
|
|
10844
|
+
};
|
|
10845
|
+
return {
|
|
10846
|
+
stdout: respond(id, withUserMessage, "PostToolUse") || extra,
|
|
10847
|
+
exit: 0
|
|
10848
|
+
};
|
|
10849
|
+
}
|
|
10850
|
+
//#endregion
|
|
9887
10851
|
//#region src/runtime/handle-post.ts
|
|
10852
|
+
function isCursorAfterFileEdit(id, payload) {
|
|
10853
|
+
if (id !== "cursor") return false;
|
|
10854
|
+
const hookEvent = typeof payload.hook_event_name === "string" ? payload.hook_event_name : "";
|
|
10855
|
+
if (hookEvent) return /^afterFileEdit$/i.test(hookEvent);
|
|
10856
|
+
return typeof payload.file_path === "string" && Array.isArray(payload.edits);
|
|
10857
|
+
}
|
|
9888
10858
|
/**
|
|
9889
10859
|
* Run the PostToolUse pipeline: store the MCP response, emit a design warning,
|
|
9890
10860
|
* record the activity into the session track, apply per-scope side-effects (SEO
|
|
@@ -9907,8 +10877,14 @@ function firstFileMatch(files, check) {
|
|
|
9907
10877
|
*/
|
|
9908
10878
|
async function handlePost(ctx) {
|
|
9909
10879
|
const { id, payload, event, framework, mcpDir, file, opts } = ctx;
|
|
10880
|
+
const cursorAfterFileEdit = isCursorAfterFileEdit(id, payload);
|
|
9910
10881
|
const designCacheDir = ctx.designCacheDir ?? mcpDir;
|
|
9911
|
-
|
|
10882
|
+
let response = payload.tool_response ?? payload.tool_output;
|
|
10883
|
+
if (id === "cursor" && /^afterMCPExecution$/i.test(event.eventName ?? "")) try {
|
|
10884
|
+
response = typeof payload.result_json === "string" ? JSON.parse(payload.result_json) : payload.result_json;
|
|
10885
|
+
} catch {
|
|
10886
|
+
response = void 0;
|
|
10887
|
+
}
|
|
9912
10888
|
mcpPostStore(event.tool, event.input, response, mcpDir);
|
|
9913
10889
|
const designWarn = designGate(payload, event, designCacheDir, opts.cwd, opts.corpusRoot);
|
|
9914
10890
|
const activities = activityFor({
|
|
@@ -9932,27 +10908,27 @@ async function handlePost(ctx) {
|
|
|
9932
10908
|
const files = fanOutFiles(event);
|
|
9933
10909
|
for (const f of files) postTrackingSideEffects(opts.scope ?? "core", f, f.input, opts.now, payload, opts.cwd);
|
|
9934
10910
|
const seoDeny = opts.scope === "seo" ? seoPostToolUseResponse(payload) : null;
|
|
9935
|
-
if (seoDeny) return {
|
|
10911
|
+
if (seoDeny && !cursorAfterFileEdit) return {
|
|
9936
10912
|
stdout: seoDeny,
|
|
9937
10913
|
exit: 0
|
|
9938
10914
|
};
|
|
9939
10915
|
if (opts.scope === "solid") {
|
|
9940
10916
|
const solidWarn = firstFileMatch(files, checkFileSize);
|
|
9941
|
-
if (solidWarn) return {
|
|
10917
|
+
if (solidWarn && !cursorAfterFileEdit) return {
|
|
9942
10918
|
stdout: solidWarn,
|
|
9943
10919
|
exit: 0
|
|
9944
10920
|
};
|
|
9945
10921
|
}
|
|
9946
10922
|
if (opts.scope === "tailwindcss") {
|
|
9947
10923
|
const tailwindWarn = firstFileMatch(files, validateTailwind);
|
|
9948
|
-
if (tailwindWarn) return {
|
|
10924
|
+
if (tailwindWarn && !cursorAfterFileEdit) return {
|
|
9949
10925
|
stdout: tailwindWarn,
|
|
9950
10926
|
exit: 0
|
|
9951
10927
|
};
|
|
9952
10928
|
}
|
|
9953
10929
|
if (opts.scope === "aipilot" && (event.tool === "TaskCreate" || event.tool === "TaskUpdate" || event.tool === "Write" || event.tool === "Edit")) {
|
|
9954
10930
|
const out = await aipilotPostToolUse(payload, opts.cwd, id);
|
|
9955
|
-
if (out) return {
|
|
10931
|
+
if (out && !cursorAfterFileEdit) return {
|
|
9956
10932
|
stdout: out,
|
|
9957
10933
|
exit: 0
|
|
9958
10934
|
};
|
|
@@ -9962,63 +10938,20 @@ async function handlePost(ctx) {
|
|
|
9962
10938
|
extra = await postEditContext(opts.scope ?? "core", f, opts.now, id);
|
|
9963
10939
|
if (extra) break;
|
|
9964
10940
|
}
|
|
9965
|
-
|
|
9966
|
-
|
|
9967
|
-
|
|
9968
|
-
|
|
9969
|
-
|
|
9970
|
-
|
|
9971
|
-
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
kind: "inform",
|
|
9980
|
-
title: "Design pipeline",
|
|
9981
|
-
reason: "",
|
|
9982
|
-
userMessage: noticeLines.join("\n")
|
|
9983
|
-
} : null;
|
|
9984
|
-
const refNotice = refCreditNoticeFor(activities, event.sessionId, opts.now, defaultStateDir(opts.cwd));
|
|
9985
|
-
const userMessage = [notice?.userMessage, refNotice].filter(Boolean).join("\n") || void 0;
|
|
9986
|
-
if (designWarn) return {
|
|
9987
|
-
stdout: respond(id, userMessage ? {
|
|
9988
|
-
...designWarn,
|
|
9989
|
-
userMessage
|
|
9990
|
-
} : designWarn, "PostToolUse"),
|
|
9991
|
-
exit: 0
|
|
9992
|
-
};
|
|
9993
|
-
if (!userMessage) return {
|
|
9994
|
-
stdout: extra,
|
|
9995
|
-
exit: 0
|
|
9996
|
-
};
|
|
9997
|
-
const withUserMessage = notice ? {
|
|
9998
|
-
...notice,
|
|
9999
|
-
userMessage
|
|
10000
|
-
} : {
|
|
10001
|
-
kind: "inform",
|
|
10002
|
-
title: "Compliance",
|
|
10003
|
-
reason: "",
|
|
10004
|
-
userMessage
|
|
10005
|
-
};
|
|
10006
|
-
if (!extra) return {
|
|
10007
|
-
stdout: respond(id, withUserMessage, "PostToolUse"),
|
|
10008
|
-
exit: 0
|
|
10009
|
-
};
|
|
10010
|
-
if (id === "claude-code" || id === "codex") return {
|
|
10011
|
-
stdout: attachSystemMessage(extra, userMessage),
|
|
10012
|
-
exit: 0
|
|
10013
|
-
};
|
|
10014
|
-
if (id === "kimi") return {
|
|
10015
|
-
stdout: respond(id, withUserMessage, "PostToolUse"),
|
|
10016
|
-
exit: 0
|
|
10017
|
-
};
|
|
10018
|
-
return {
|
|
10019
|
-
stdout: respond(id, withUserMessage, "PostToolUse") || extra,
|
|
10020
|
-
exit: 0
|
|
10021
|
-
};
|
|
10941
|
+
return postOutcome({
|
|
10942
|
+
id,
|
|
10943
|
+
agentId: typeof payload.agent_id === "string" ? payload.agent_id : "",
|
|
10944
|
+
sessionId: event.sessionId,
|
|
10945
|
+
now: opts.now,
|
|
10946
|
+
cwd: opts.cwd,
|
|
10947
|
+
activities,
|
|
10948
|
+
files,
|
|
10949
|
+
designCacheDir,
|
|
10950
|
+
designWarn,
|
|
10951
|
+
extra,
|
|
10952
|
+
cursorAfterFileEdit,
|
|
10953
|
+
cursorEventName: typeof payload.hook_event_name === "string" ? payload.hook_event_name : ""
|
|
10954
|
+
});
|
|
10022
10955
|
}
|
|
10023
10956
|
//#endregion
|
|
10024
10957
|
//#region src/runtime/handle-scope-async.ts
|
|
@@ -10038,9 +10971,12 @@ async function handlePost(ctx) {
|
|
|
10038
10971
|
* @returns The native stdout when intercepted, or `null` to fall through.
|
|
10039
10972
|
*/
|
|
10040
10973
|
async function asyncScopeStdout(scope, event, payload, cwd, now, id = "claude-code") {
|
|
10041
|
-
|
|
10042
|
-
if (
|
|
10043
|
-
|
|
10974
|
+
const dispatchedEvent = id === "cursor" ? cursorEventContract(event).lifecycle : event;
|
|
10975
|
+
if (dispatchedEvent === null) return null;
|
|
10976
|
+
let stdout = null;
|
|
10977
|
+
if (scope === "aipilot") stdout = await dispatchAipilot(dispatchedEvent, payload, cwd, now, void 0, id);
|
|
10978
|
+
if (scope === "memory") stdout = await dispatchMemory(dispatchedEvent, payload, cwd, now);
|
|
10979
|
+
return id === "cursor" && stdout !== null ? toCursorLifecycleResponse(stdout, event) : stdout;
|
|
10044
10980
|
}
|
|
10045
10981
|
//#endregion
|
|
10046
10982
|
//#region src/runtime/design-cache-resolve.ts
|
|
@@ -10509,19 +11445,99 @@ function handleConfirmSubmit(sessionId, text, now, home) {
|
|
|
10509
11445
|
} catch {}
|
|
10510
11446
|
}
|
|
10511
11447
|
//#endregion
|
|
11448
|
+
//#region src/runtime/confirm/codex-prompt-origin.ts
|
|
11449
|
+
const AGENT_FIELDS = ["agent_id", "agent_type"];
|
|
11450
|
+
/**
|
|
11451
|
+
* Classify Codex prompt provenance from its optional agent metadata.
|
|
11452
|
+
* Missing metadata is the observed root shape; malformed metadata fails closed.
|
|
11453
|
+
*/
|
|
11454
|
+
function codexPromptOrigin(payload) {
|
|
11455
|
+
const present = AGENT_FIELDS.filter((field) => Object.hasOwn(payload, field));
|
|
11456
|
+
if (present.length === 0) return "root";
|
|
11457
|
+
return present.every((field) => typeof payload[field] === "string" && payload[field].trim().length > 0) ? "subagent" : "unknown";
|
|
11458
|
+
}
|
|
11459
|
+
//#endregion
|
|
10512
11460
|
//#region src/runtime/handle.ts
|
|
10513
11461
|
/** Raw Claude hook event name from a payload (empty when absent). */
|
|
10514
11462
|
function rawEventName(payload) {
|
|
10515
11463
|
return typeof payload.hook_event_name === "string" ? payload.hook_event_name : "";
|
|
10516
11464
|
}
|
|
10517
11465
|
/**
|
|
11466
|
+
* `payload.tool_input` parsed into an object when it's a JSON STRING —
|
|
11467
|
+
* Cursor's real wire format for `beforeMCPExecution`/`afterMCPExecution`
|
|
11468
|
+
* (ground truth), unlike every other harness (and Cursor's own
|
|
11469
|
+
* `preToolUse`/`postToolUse`), which always sends it as an object already.
|
|
11470
|
+
* `undefined` when `tool_input` is already an object, absent, or fails to
|
|
11471
|
+
* parse into one (fail-open — the caller then keeps the original value).
|
|
11472
|
+
* @param payload - The raw hook payload.
|
|
11473
|
+
*/
|
|
11474
|
+
function cursorParsedToolInput(payload) {
|
|
11475
|
+
const raw = payload.tool_input;
|
|
11476
|
+
if (typeof raw !== "string") return void 0;
|
|
11477
|
+
try {
|
|
11478
|
+
const parsed = JSON.parse(raw);
|
|
11479
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : void 0;
|
|
11480
|
+
} catch {
|
|
11481
|
+
return;
|
|
11482
|
+
}
|
|
11483
|
+
}
|
|
11484
|
+
/**
|
|
11485
|
+
* `id === "cursor"` only: project the already-resolved canonical `tool_name`
|
|
11486
|
+
* (`event.tool`, normalized by {@link normalizeEvent}) and `cwd` (the project
|
|
11487
|
+
* root resolved via `cursorProjectCwd`, already applied to `opts.cwd`) onto a
|
|
11488
|
+
* shallow payload copy — the single passage point for every downstream
|
|
11489
|
+
* consumer that reads `payload.tool_name`/`payload.cwd`/`payload.tool_input`
|
|
11490
|
+
* RAW instead of `event.tool`/`opts.cwd`/`event.input` (lifecycle-bridge's
|
|
11491
|
+
* `failure-lesson.ts`/`agent-memory.ts`, handle-scope-async's aipilot/memory
|
|
11492
|
+
* dispatchers — including `doc-cache-gate.ts`'s `libraryOf`, which never
|
|
11493
|
+
* `JSON.parse`s a string `tool_input` itself — and the seo scope's
|
|
11494
|
+
* `post-tool-use.ts`). `tool_input` is additionally replaced by its parsed
|
|
11495
|
+
* object form via {@link cursorParsedToolInput} when Cursor sent it as a
|
|
11496
|
+
* JSON string (`beforeMCPExecution`/`afterMCPExecution`). Cursor's own wire
|
|
11497
|
+
* values ("Shell", `MCP:<tool>`, a bare `workspace_roots` array with no
|
|
11498
|
+
* `cwd` field, a stringified `tool_input`, …) are preserved under
|
|
11499
|
+
* `cursor_tool_name`/`cursor_cwd`/`cursor_tool_input` so nothing is lost.
|
|
11500
|
+
* Every other harness id is untouched (returns the SAME object,
|
|
11501
|
+
* byte-identical).
|
|
11502
|
+
* @param payload - The raw hook payload.
|
|
11503
|
+
* @param event - The already-normalized event (`event.tool` is canonical).
|
|
11504
|
+
* @param cwd - The resolved project root for this invocation.
|
|
11505
|
+
* @param id - Harness adapter id.
|
|
11506
|
+
*/
|
|
11507
|
+
function cursorRawPayloadProjection(payload, event, cwd, id) {
|
|
11508
|
+
if (id !== "cursor") return payload;
|
|
11509
|
+
const parsedToolInput = cursorParsedToolInput(payload);
|
|
11510
|
+
return {
|
|
11511
|
+
...payload,
|
|
11512
|
+
cursor_tool_name: payload.tool_name,
|
|
11513
|
+
cursor_cwd: payload.cwd,
|
|
11514
|
+
tool_name: event.tool,
|
|
11515
|
+
cwd,
|
|
11516
|
+
...parsedToolInput ? {
|
|
11517
|
+
cursor_tool_input: payload.tool_input,
|
|
11518
|
+
tool_input: parsedToolInput
|
|
11519
|
+
} : {}
|
|
11520
|
+
};
|
|
11521
|
+
}
|
|
11522
|
+
/**
|
|
10518
11523
|
* The full hook handler: on a PRE event it gates the tool-use (stateless guards
|
|
10519
11524
|
* then APEX gates from the session track) and returns the native response; on a
|
|
10520
11525
|
* POST event it records the activity into the track. The loop that makes the
|
|
10521
11526
|
* package behave like the Claude plugin, on any harness.
|
|
10522
11527
|
*/
|
|
10523
|
-
async function
|
|
11528
|
+
async function handleHookCore(id, payload, opts) {
|
|
10524
11529
|
const event = normalizeEvent(id, payload);
|
|
11530
|
+
if (id === "cursor") {
|
|
11531
|
+
const cursorCwd = cursorProjectCwd(event.cwd, event.workspaceRoots ?? [], event.filePath, opts.cwd);
|
|
11532
|
+
if (cursorCwd !== opts.cwd) opts = {
|
|
11533
|
+
...opts,
|
|
11534
|
+
cwd: cursorCwd
|
|
11535
|
+
};
|
|
11536
|
+
}
|
|
11537
|
+
const hookPayload = cursorRawPayloadProjection(payload, event, opts.cwd, id);
|
|
11538
|
+
const rawPrompt = payload.prompt;
|
|
11539
|
+
const userPrompt = typeof rawPrompt === "string" || Array.isArray(rawPrompt) ? promptText(rawPrompt) : void 0;
|
|
11540
|
+
if (id === "codex" && rawEventName(payload) === "UserPromptSubmit" && userPrompt !== void 0) submitCodexConfirmation(event.sessionId, userPrompt, opts.now, opts.home, codexPromptOrigin(payload));
|
|
10525
11541
|
resetFragmentRegistry();
|
|
10526
11542
|
const layout = projectLayout(opts.cwd);
|
|
10527
11543
|
const file = trackFile(event.sessionId, defaultStateDir(opts.cwd));
|
|
@@ -10533,20 +11549,18 @@ async function handleHook(id, payload, opts) {
|
|
|
10533
11549
|
exit: 0
|
|
10534
11550
|
};
|
|
10535
11551
|
if (id === "codex" && rawEventName(payload) === "SessionStart") resyncCodexAgents();
|
|
10536
|
-
const asyncOut = await asyncScopeStdout(opts.scope, rawEventName(payload),
|
|
11552
|
+
const asyncOut = await asyncScopeStdout(opts.scope, rawEventName(payload), hookPayload, opts.cwd, opts.now, id);
|
|
10537
11553
|
if (asyncOut !== null) return {
|
|
10538
11554
|
stdout: asyncOut,
|
|
10539
11555
|
exit: 0
|
|
10540
11556
|
};
|
|
10541
|
-
const life = lifecycleStdout(
|
|
11557
|
+
const life = lifecycleStdout(hookPayload, opts.cwd, opts.scope ?? "core", opts.now, id);
|
|
10542
11558
|
if (life !== null) return {
|
|
10543
11559
|
stdout: id === "claude-code" ? attachBudgetRecap(life, rawEventName(payload), event.sessionId, opts.cwd, opts.now) : life,
|
|
10544
11560
|
exit: 0
|
|
10545
11561
|
};
|
|
10546
|
-
const rawPrompt = payload.prompt;
|
|
10547
|
-
const userPrompt = typeof rawPrompt === "string" || Array.isArray(rawPrompt) ? promptText(rawPrompt) : void 0;
|
|
10548
11562
|
if (userPrompt !== void 0) {
|
|
10549
|
-
handleConfirmSubmit(event.sessionId, userPrompt, opts.now, opts.home);
|
|
11563
|
+
if (id !== "codex") handleConfirmSubmit(event.sessionId, userPrompt, opts.now, opts.home);
|
|
10550
11564
|
await withTrack(file, (track) => recordBrainstormRequired(track, detectCreationIntent(userPrompt)));
|
|
10551
11565
|
return {
|
|
10552
11566
|
stdout: promptSubmitContext(userPrompt, opts.cwd, id),
|
|
@@ -10555,7 +11569,7 @@ async function handleHook(id, payload, opts) {
|
|
|
10555
11569
|
}
|
|
10556
11570
|
if (event.phase === "post") return handlePost({
|
|
10557
11571
|
id,
|
|
10558
|
-
payload,
|
|
11572
|
+
payload: hookPayload,
|
|
10559
11573
|
event,
|
|
10560
11574
|
framework,
|
|
10561
11575
|
mcpDir,
|
|
@@ -10565,7 +11579,7 @@ async function handleHook(id, payload, opts) {
|
|
|
10565
11579
|
});
|
|
10566
11580
|
return handlePre({
|
|
10567
11581
|
id,
|
|
10568
|
-
payload,
|
|
11582
|
+
payload: hookPayload,
|
|
10569
11583
|
event,
|
|
10570
11584
|
framework,
|
|
10571
11585
|
mcpDir,
|
|
@@ -10574,5 +11588,41 @@ async function handleHook(id, payload, opts) {
|
|
|
10574
11588
|
opts
|
|
10575
11589
|
});
|
|
10576
11590
|
}
|
|
11591
|
+
/**
|
|
11592
|
+
* Run one hook and adapt every Cursor scope outcome at the common runtime exit.
|
|
11593
|
+
* Other harnesses retain the core handler's stdout and exit status unchanged.
|
|
11594
|
+
* Cursor's shared `additional_context` budget context (see
|
|
11595
|
+
* `../adapters/cursor/context-budget.ts`) is assembled here too — this is
|
|
11596
|
+
* the single point every Cursor stdout passes through exactly once, so it's
|
|
11597
|
+
* also the single point that reserves from and records into the registry.
|
|
11598
|
+
* With no `session_id`/`conversation_id` at all, `sessionId` is `""` — the
|
|
11599
|
+
* registry key would degenerate to one bucket shared by every session-less
|
|
11600
|
+
* call on the same (cwd, event) pair, so `budget` stays `undefined` instead
|
|
11601
|
+
* (falls back to the flat per-response cap in `toCursorLifecycleResponse`,
|
|
11602
|
+
* with zero registry I/O). `stateDir` honors `opts.home` (test-only OS home
|
|
11603
|
+
* override, see `HandleOptions`) so tests never need the real `os.homedir()`.
|
|
11604
|
+
*/
|
|
11605
|
+
async function handleHook(id, payload, opts) {
|
|
11606
|
+
const outcome = await handleHookCore(id, payload, opts);
|
|
11607
|
+
if (id !== "cursor") return outcome;
|
|
11608
|
+
const eventName = rawEventName(payload);
|
|
11609
|
+
const cursorEvent = normalizeEvent(id, payload);
|
|
11610
|
+
const cwd = cursorProjectCwd(cursorEvent.cwd, cursorEvent.workspaceRoots ?? [], cursorEvent.filePath, opts.cwd);
|
|
11611
|
+
const sessionId = cursorEvent.sessionId;
|
|
11612
|
+
const generationId = typeof payload.generation_id === "string" && payload.generation_id ? payload.generation_id : void 0;
|
|
11613
|
+
const toolUseId = typeof payload.tool_use_id === "string" && payload.tool_use_id ? payload.tool_use_id : void 0;
|
|
11614
|
+
const stateDir = join(fuseHarnessHome(opts.home), "state", projectHash$1(cwd));
|
|
11615
|
+
const budget = sessionId ? {
|
|
11616
|
+
stateDir,
|
|
11617
|
+
sessionId,
|
|
11618
|
+
event: eventName,
|
|
11619
|
+
generationId,
|
|
11620
|
+
toolUseId
|
|
11621
|
+
} : void 0;
|
|
11622
|
+
return {
|
|
11623
|
+
...outcome,
|
|
11624
|
+
stdout: toCursorLifecycleResponse(outcome.stdout, eventName, budget)
|
|
11625
|
+
};
|
|
11626
|
+
}
|
|
10577
11627
|
//#endregion
|
|
10578
|
-
export {
|
|
11628
|
+
export { runDoctor as $, trackWatchResearch as A, taskContext as At, writePluginMap as B, todayUtc as Bt, postEditContext as C, removeOldFiles as Ct, securityAdvisory as D, projectContext as Dt, dispatchMemory as E, gitContext as Et, aipilotPostToolUse as F, isoUtc as Ft, mergeLines as G, isProject as H, dispatchAipilot as I, loadSecurityState as It, listChildren as J, countFiles as K, dispatchLessons as L, saveSecurityState as Lt, trackSkillRead as M, projectHash$1 as Mt, trackEnrichment as N, trackFile as Nt, securityAdvisoryForPatch as O, claudeMdKey as Ot, dispatchLifecycle as P, normalizeEvent as Pt, notify as Q, cartoSessionStart as R, securityStateDir as Rt, lifecycleStdout as S, purgeTtlTree as St, seoPostToolUseResponse as T, devContext as Tt, writeTree as U, generateProjectMap as V, loadEnriched as W, lessonsFileFor as X, lessonsArchiveFileFor as Y, lessonsStateFileFor as Z, gateCommandCandidates as _, injectRules as _t, recordActivity as a, cleanupSession as at, dryGate as b, sessionStartCore as bt, MCP_TTL_MS as c, validateTeammateOutput as ct, isMcpTool as d, validateTailwind as dt, runningVersion as et, queryOf as f, validateSolidGate as ft, gate as g, solidDetectStart as gt, TRIVIAL_BUDGET as h, detectSolidProfile as ht, respond as i, validateRulesLoaded as it, trackMcpResearch as j, defaultStateDir as jt, postTrackingSideEffects as k, promptSubmitContext as kt, WEBFETCH_TTL_MS as l, trackAgentMemory as lt, REQUIRED_AGENTS as m, countLoc as mt, activityFor as n, postEditTypescript as nt, mcpPostStore as o, saveApexState as ot, DEFAULT_WINDOW_MS as p, checkFileSize as pt, getFileDesc as q, handlePre as r, trackSessionChanges as rt, mcpPreIntercept as s, logToolFailure as st, handleHook as t, versionBanner as tt, cacheQueryOf as u, subagentCacheContext as ut, preCommitGate as v, readRules as vt, seoPostToolUse as w, trimLogFile as wt, extractSymbols as x, pruneEmptyDirs as xt, detectDuplication as y, runSessionStartCleanups as yt, generateEcosystemMap as z, securityStatePath as zt };
|