@fusengine/harness 0.1.89 → 0.1.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude/index.d.mts +1 -1
- package/dist/adapters/claude/index.mjs +1 -1
- package/dist/adapters/cline/index.mjs +1 -1
- package/dist/adapters/codex/index.d.mts +2 -2
- package/dist/adapters/codex/index.mjs +5 -4
- package/dist/adapters/cursor/index.d.mts +15 -28
- package/dist/adapters/cursor/index.mjs +35 -46
- package/dist/adapters/gemini/index.mjs +1 -1
- package/dist/adapters/hermes/index.d.mts +1 -1
- package/dist/adapters/hermes/index.mjs +1 -1
- package/dist/adapters/kimi/index.d.mts +1 -1
- package/dist/adapters/kimi/index.mjs +1 -1
- package/dist/{claude-5bC8TDAz.mjs → claude-Ckv2_TgP.mjs} +3 -43
- package/dist/cli/bin.mjs +429 -16
- package/dist/cli/index.mjs +1 -1
- package/dist/command-string-CALMTnwN.mjs +42 -0
- package/dist/{evaluate-C1VSI5pb.mjs → evaluate-CMiIqHeH.mjs} +261 -13
- package/dist/{handle-DHhukRdh.mjs → handle-C43gA-Pr.mjs} +737 -150
- package/dist/{hermes-kP6NUQlG.mjs → hermes-B9-p_3IF.mjs} +3 -2
- package/dist/{index-DBgnIZn9.d.mts → index-B8PG82su.d.mts} +1 -1
- package/dist/{index-D4C2-IIc.d.mts → index-SmKYkk2N.d.mts} +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/init/index.d.mts +1 -1
- package/dist/init/index.mjs +1 -1
- package/dist/{kimi-D6X7b8wF.mjs → kimi-G2wcSh5-.mjs} +3 -2
- package/dist/normalize-BjG6unTj.mjs +317 -0
- package/dist/permission-mode-B-SFyR0X.mjs +226 -0
- package/dist/policy/index.d.mts +1 -1
- package/dist/policy/index.mjs +2 -2
- package/dist/prompt/index.d.mts +1 -1
- package/dist/{run-CdIlUtjv.mjs → run-DkrzC0gb.mjs} +1 -1
- package/dist/{run-TAXNRSpD.mjs → run-GHAMqcOn.mjs} +26 -3
- package/dist/runtime/index.d.mts +27 -16
- package/dist/runtime/index.mjs +2 -2
- package/dist/{types-C0-igRKr.d.mts → types-Bh0jEF3_.d.mts} +2 -0
- package/dist/{validate-DZFtAbLP.mjs → validate-KjZ1X9tH.mjs} +1 -1
- package/package.json +3 -2
- package/src/adapters/claude/index.ts +92 -0
- package/src/adapters/cline/index.ts +36 -0
- package/src/adapters/codex/apply-patch.ts +94 -0
- package/src/adapters/codex/index.ts +84 -0
- package/src/adapters/codex/permission-mode.ts +16 -0
- package/src/adapters/cursor/context.ts +46 -0
- package/src/adapters/cursor/events.ts +38 -0
- package/src/adapters/cursor/index.ts +43 -0
- package/src/adapters/cursor/interfaces/types.ts +88 -0
- package/src/adapters/cursor/native-response.ts +158 -0
- package/src/adapters/cursor/normalize.ts +123 -0
- package/src/adapters/cursor/respond.ts +123 -0
- package/src/adapters/gemini/index.ts +36 -0
- package/src/adapters/hermes/index.ts +55 -0
- package/src/adapters/hermes/interfaces/types.ts +26 -0
- package/src/adapters/kimi/index.ts +96 -0
- package/src/adapters/kimi/interfaces/types.ts +55 -0
- package/src/cache/compact.ts +55 -0
- package/src/cache/index.ts +5 -0
- package/src/cache/io.ts +38 -0
- package/src/cache/mcp-response.ts +39 -0
- package/src/cache/mcp-store.ts +70 -0
- package/src/cache/store.ts +93 -0
- package/src/changelog/fetch.ts +79 -0
- package/src/cli/bin.ts +100 -0
- package/src/cli/cursor-event-scanner.ts +175 -0
- package/src/cli/cursor-stdin-reader.ts +65 -0
- package/src/cli/doctor.ts +96 -0
- package/src/cli/hook-io.ts +130 -0
- package/src/cli/hook-sound.ts +49 -0
- package/src/cli/index.ts +1 -0
- package/src/cli/json-primitive-scanner.ts +83 -0
- package/src/cli/run.ts +36 -0
- package/src/cli/scope.ts +31 -0
- package/src/codex-rules/build.ts +63 -0
- package/src/codex-rules/index.ts +8 -0
- package/src/codex-rules/mapping.ts +42 -0
- package/src/codex-rules/rules/absolute-paths.ts +52 -0
- package/src/codex-rules/rules/ask.ts +44 -0
- package/src/codex-rules/rules/chmod-ask.ts +57 -0
- package/src/codex-rules/rules/dd.ts +23 -0
- package/src/codex-rules/rules/diskutil.ts +33 -0
- package/src/codex-rules/rules/mkfs.ts +42 -0
- package/src/codex-rules/rules/privilege.ts +49 -0
- package/src/codex-rules/rules/remote-fetch.ts +31 -0
- package/src/codex-rules/rules/rm-variants.ts +58 -0
- package/src/codex-rules/rules/single-commands.ts +57 -0
- package/src/codex-rules/skip-list.ts +25 -0
- package/src/codex-rules/starlark.ts +46 -0
- package/src/codex-rules/types.ts +36 -0
- package/src/config/dotenv.ts +55 -0
- package/src/config/env.ts +10 -0
- package/src/config/home-dir.ts +36 -0
- package/src/config/index.ts +5 -0
- package/src/config/layout.ts +52 -0
- package/src/config/limits.ts +56 -0
- package/src/config/ttl.ts +30 -0
- package/src/detect/harness.ts +73 -0
- package/src/detect/index.ts +1 -0
- package/src/detect/interfaces/types.ts +18 -0
- package/src/freshness/agent-evidence-record.ts +95 -0
- package/src/freshness/agent-evidence.ts +95 -0
- package/src/freshness/codex-spawn-evidence.ts +91 -0
- package/src/freshness/doc-helpers.ts +90 -0
- package/src/freshness/evidence-harvest-io.ts +72 -0
- package/src/freshness/evidence-harvest.ts +61 -0
- package/src/freshness/explore-tools.ts +95 -0
- package/src/freshness/index.ts +2 -0
- package/src/freshness/query-framework.ts +73 -0
- package/src/freshness/ref-evidence.ts +49 -0
- package/src/freshness/ref-journal.ts +98 -0
- package/src/freshness/trivial-edit-counter.ts +30 -0
- package/src/index.ts +11 -0
- package/src/init/index.ts +2 -0
- package/src/init/run.ts +32 -0
- package/src/init/templates.ts +75 -0
- package/src/memory/gitignore.ts +14 -0
- package/src/memory/index.ts +3 -0
- package/src/memory/registry.ts +39 -0
- package/src/memory/session-roots.ts +99 -0
- package/src/memory/state.ts +58 -0
- package/src/policy/apex-authorization.ts +100 -0
- package/src/policy/apex-gates.ts +91 -0
- package/src/policy/apex-target-steps.ts +73 -0
- package/src/policy/apex-target.ts +49 -0
- package/src/policy/apex-task-context.ts +81 -0
- package/src/policy/apex.ts +94 -0
- package/src/policy/cartographer/build-tree.ts +69 -0
- package/src/policy/cartographer/describe.ts +53 -0
- package/src/policy/cartographer/entry.ts +38 -0
- package/src/policy/cartographer/frontmatter.ts +48 -0
- package/src/policy/cartographer/indicators.ts +34 -0
- package/src/policy/claude-md-context.ts +66 -0
- package/src/policy/conventions/index.ts +11 -0
- package/src/policy/conventions/interfaces.ts +71 -0
- package/src/policy/conventions/langs.ts +53 -0
- package/src/policy/conventions/query.ts +49 -0
- package/src/policy/conventions/react-hooks.ts +18 -0
- package/src/policy/conventions/store-or-query.ts +21 -0
- package/src/policy/conventions/stores.ts +56 -0
- package/src/policy/conventions/strip.ts +87 -0
- package/src/policy/conventions/verdict.ts +38 -0
- package/src/policy/creation-intent.ts +11 -0
- package/src/policy/deny-loop.ts +88 -0
- package/src/policy/design/allowed-write.ts +28 -0
- package/src/policy/design/content-checks.ts +45 -0
- package/src/policy/design/corpus-fs.ts +21 -0
- package/src/policy/design/corpus-resolve-cache.ts +49 -0
- package/src/policy/design/corpus-resolve.ts +99 -0
- package/src/policy/design/corpus.ts +92 -0
- package/src/policy/design/design-system-rules.ts +45 -0
- package/src/policy/design/flag.ts +30 -0
- package/src/policy/design/gates-pipeline.ts +105 -0
- package/src/policy/design/gates.ts +87 -0
- package/src/policy/design/screenshot-tools.ts +24 -0
- package/src/policy/design/skill-evidence.ts +34 -0
- package/src/policy/design/skill-gate.ts +81 -0
- package/src/policy/design/skill-triggers.ts +51 -0
- package/src/policy/design/state.ts +92 -0
- package/src/policy/design/template-urls.ts +54 -0
- package/src/policy/design/transitions.ts +68 -0
- package/src/policy/detect-claude-md-project-type.ts +32 -0
- package/src/policy/detect-framework.ts +99 -0
- package/src/policy/detect-primitive-checks.ts +100 -0
- package/src/policy/detect-primitive-lib.ts +55 -0
- package/src/policy/detect-project.ts +99 -0
- package/src/policy/edit-outcome.ts +53 -0
- package/src/policy/evaluate.ts +81 -0
- package/src/policy/expert-agents.ts +60 -0
- package/src/policy/file-size-scope.ts +32 -0
- package/src/policy/file-size.ts +99 -0
- package/src/policy/framework-solid-exclude.ts +52 -0
- package/src/policy/framework-solid-extended.ts +93 -0
- package/src/policy/framework-solid-gates-systems.ts +99 -0
- package/src/policy/framework-solid-gates.ts +98 -0
- package/src/policy/framework-solid.ts +88 -0
- package/src/policy/gemini-mcp-gate.ts +83 -0
- package/src/policy/git-gates.ts +40 -0
- package/src/policy/guards/bash-command-anchor.ts +40 -0
- package/src/policy/guards/bash-write-patterns.ts +140 -0
- package/src/policy/guards/bash-write-redirects.ts +201 -0
- package/src/policy/guards/bash-write-safe-paths.ts +62 -0
- package/src/policy/guards/bash-write.ts +76 -0
- package/src/policy/guards/context.ts +12 -0
- package/src/policy/guards/index.ts +61 -0
- package/src/policy/guards/install.ts +22 -0
- package/src/policy/guards/interface-separation-ext.ts +41 -0
- package/src/policy/guards/interface-separation.ts +100 -0
- package/src/policy/guards/protected-path.ts +100 -0
- package/src/policy/guards/security.ts +97 -0
- package/src/policy/index.ts +19 -0
- package/src/policy/interfaces/types.ts +46 -0
- package/src/policy/js-gate-route.ts +51 -0
- package/src/policy/lessons/lesson-gate.ts +0 -0
- package/src/policy/lessons/trigger-index.ts +84 -0
- package/src/policy/lessons/types.ts +31 -0
- package/src/policy/module-layout.ts +31 -0
- package/src/policy/nearest-manifest.ts +75 -0
- package/src/policy/never-approval.ts +95 -0
- package/src/policy/patterns.ts +80 -0
- package/src/policy/seo/validate.ts +51 -0
- package/src/policy/shadcn-project.ts +22 -0
- package/src/policy/shadcn-skill-gate.ts +97 -0
- package/src/policy/shell-read-refs.ts +96 -0
- package/src/policy/skill-path.ts +46 -0
- package/src/policy/skill-patterns/laravel.ts +40 -0
- package/src/policy/skill-patterns/nextjs.ts +29 -0
- package/src/policy/skill-patterns/react.ts +27 -0
- package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
- package/src/policy/skill-patterns/shadcn.ts +66 -0
- package/src/policy/skill-patterns/swift.ts +30 -0
- package/src/policy/skill-patterns/tailwind.ts +40 -0
- package/src/policy/skill-source.ts +52 -0
- package/src/policy/skill-trigger-patterns.ts +33 -0
- package/src/policy/skill-triggers.ts +108 -0
- package/src/policy/tailwind-skill-gate.ts +63 -0
- package/src/policy/trivial-edits.ts +52 -0
- package/src/policy/verbosity.ts +39 -0
- package/src/prompt/index.ts +1 -0
- package/src/prompt/types.ts +36 -0
- package/src/refs/discover.ts +69 -0
- package/src/refs/frontmatter.ts +24 -0
- package/src/refs/index.ts +4 -0
- package/src/refs/loader.ts +59 -0
- package/src/refs/ref-key.ts +41 -0
- package/src/refs/router.ts +63 -0
- package/src/refs/types.ts +25 -0
- package/src/runtime/activity.ts +99 -0
- package/src/runtime/apply-patch-gate.ts +41 -0
- package/src/runtime/burst-window.ts +21 -0
- package/src/runtime/codex-shell-tool.ts +37 -0
- package/src/runtime/command-string.ts +37 -0
- package/src/runtime/confirm/codex-confirm.ts +126 -0
- package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
- package/src/runtime/confirm/confirm-code.ts +33 -0
- package/src/runtime/confirm/confirm-gate.ts +65 -0
- package/src/runtime/confirm/confirm-irreversible.ts +32 -0
- package/src/runtime/confirm/confirm-pending.ts +34 -0
- package/src/runtime/confirm/confirm-state.ts +56 -0
- package/src/runtime/confirm/confirm-subagent.ts +112 -0
- package/src/runtime/confirm/confirm-submit.ts +38 -0
- package/src/runtime/core-guards-active.ts +84 -0
- package/src/runtime/core-guards-wired.ts +47 -0
- package/src/runtime/deny-loop-store.ts +94 -0
- package/src/runtime/deny-notice.ts +52 -0
- package/src/runtime/design-cache-resolve.ts +51 -0
- package/src/runtime/design-content-gate.ts +95 -0
- package/src/runtime/design-files-gate.ts +114 -0
- package/src/runtime/design-helpers.ts +93 -0
- package/src/runtime/design-lifecycle.ts +39 -0
- package/src/runtime/design-read-credit.ts +28 -0
- package/src/runtime/design.ts +116 -0
- package/src/runtime/dev-context.ts +43 -0
- package/src/runtime/dry-patterns.ts +32 -0
- package/src/runtime/dry.ts +96 -0
- package/src/runtime/fragment-registry.ts +33 -0
- package/src/runtime/framework-skill-gate.ts +79 -0
- package/src/runtime/fs-cleanup.ts +71 -0
- package/src/runtime/gate-apex.ts +81 -0
- package/src/runtime/gate-helpers.ts +52 -0
- package/src/runtime/gate-input.ts +26 -0
- package/src/runtime/gate.ts +128 -0
- package/src/runtime/handle-post.ts +104 -0
- package/src/runtime/handle-pre.ts +137 -0
- package/src/runtime/handle-scope-async.ts +29 -0
- package/src/runtime/handle-types.ts +27 -0
- package/src/runtime/handle.ts +116 -0
- package/src/runtime/home-state.ts +55 -0
- package/src/runtime/index.ts +18 -0
- package/src/runtime/inform.ts +28 -0
- package/src/runtime/inject-budget-recap.ts +47 -0
- package/src/runtime/inject-budget.ts +81 -0
- package/src/runtime/inject-context.ts +85 -0
- package/src/runtime/inject-dedup-exclusive.ts +98 -0
- package/src/runtime/inject-dedup.ts +89 -0
- package/src/runtime/is-agent-tool.ts +19 -0
- package/src/runtime/lifecycle/agent-files.ts +49 -0
- package/src/runtime/lifecycle/agent-memory.ts +82 -0
- package/src/runtime/lifecycle/agent-transcript.ts +80 -0
- package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
- package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
- package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
- package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
- package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
- package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
- package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
- package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +100 -0
- package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
- package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
- package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
- package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
- package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
- package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
- package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
- package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
- package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
- package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
- package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
- package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
- package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
- package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
- package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
- package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
- package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
- package/src/runtime/lifecycle/aipilot/types.ts +72 -0
- package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
- package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
- package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
- package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
- package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
- package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
- package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
- package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
- package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
- package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
- package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
- package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
- package/src/runtime/lifecycle/changelog-research.ts +43 -0
- package/src/runtime/lifecycle/check-file-size.ts +61 -0
- package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
- package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
- package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
- package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
- package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
- package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
- package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
- package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
- package/src/runtime/lifecycle/dispatch.ts +109 -0
- package/src/runtime/lifecycle/failure-lesson.ts +51 -0
- package/src/runtime/lifecycle/index.ts +34 -0
- package/src/runtime/lifecycle/inject-rules.ts +50 -0
- package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
- package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
- package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
- package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
- package/src/runtime/lifecycle/lessons/state.ts +23 -0
- package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
- package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
- package/src/runtime/lifecycle/memory/client.ts +65 -0
- package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
- package/src/runtime/lifecycle/memory/recall.ts +30 -0
- package/src/runtime/lifecycle/memory/salience.ts +31 -0
- package/src/runtime/lifecycle/memory/state.ts +49 -0
- package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
- package/src/runtime/lifecycle/post-compact.ts +38 -0
- package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
- package/src/runtime/lifecycle/post-tracking.ts +40 -0
- package/src/runtime/lifecycle/pre-compact.ts +38 -0
- package/src/runtime/lifecycle/rules-root.ts +83 -0
- package/src/runtime/lifecycle/security/check-skill.ts +63 -0
- package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
- package/src/runtime/lifecycle/security/scan.ts +76 -0
- package/src/runtime/lifecycle/security/skill-state.ts +47 -0
- package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
- package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
- package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
- package/src/runtime/lifecycle/session-end.ts +17 -0
- package/src/runtime/lifecycle/session-start.ts +57 -0
- package/src/runtime/lifecycle/snapshot/board.ts +27 -0
- package/src/runtime/lifecycle/snapshot/format.ts +60 -0
- package/src/runtime/lifecycle/snapshot/git.ts +71 -0
- package/src/runtime/lifecycle/snapshot/index.ts +52 -0
- package/src/runtime/lifecycle/snapshot/version.ts +34 -0
- package/src/runtime/lifecycle/solid-detect.ts +56 -0
- package/src/runtime/lifecycle/stop-core.ts +35 -0
- package/src/runtime/lifecycle/subagent-cache.ts +61 -0
- package/src/runtime/lifecycle/task-completed.ts +94 -0
- package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
- package/src/runtime/lifecycle/teammate-idle.ts +28 -0
- package/src/runtime/lifecycle/tool-failure.ts +24 -0
- package/src/runtime/lifecycle/track-changes.ts +61 -0
- package/src/runtime/lifecycle/validate-solid.ts +70 -0
- package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
- package/src/runtime/lifecycle-bridge.ts +74 -0
- package/src/runtime/mcp-key.ts +44 -0
- package/src/runtime/mcp-tool-name.ts +92 -0
- package/src/runtime/mcp.ts +91 -0
- package/src/runtime/modular.ts +77 -0
- package/src/runtime/normalize.ts +107 -0
- package/src/runtime/notices.ts +91 -0
- package/src/runtime/notification-sound.ts +49 -0
- package/src/runtime/notifications.ts +77 -0
- package/src/runtime/paths.ts +59 -0
- package/src/runtime/post-fanout.ts +41 -0
- package/src/runtime/post-outcome.ts +61 -0
- package/src/runtime/pre-allow.ts +97 -0
- package/src/runtime/precommit.ts +60 -0
- package/src/runtime/prompt-text.ts +38 -0
- package/src/runtime/receipt-capture.ts +34 -0
- package/src/runtime/record.ts +26 -0
- package/src/runtime/respond.ts +92 -0
- package/src/runtime/solid-file-size-gate.ts +58 -0
- package/src/runtime/solid-pre.ts +53 -0
- package/src/runtime/storage.ts +9 -0
- package/src/statusline/colors.ts +31 -0
- package/src/statusline/constants.ts +17 -0
- package/src/statusline/formatters.ts +54 -0
- package/src/statusline/index.ts +4 -0
- package/src/statusline/progress-bar.ts +46 -0
- package/src/tracking/codex-post-failure.ts +73 -0
- package/src/tracking/index.ts +2 -0
- package/src/tracking/integrity.ts +99 -0
- package/src/tracking/one-shot-dedup.ts +34 -0
- package/src/tracking/one-shot-failure.ts +42 -0
- package/src/tracking/one-shot-store.ts +95 -0
- package/src/tracking/one-shot.ts +93 -0
- package/src/tracking/receipts.ts +95 -0
- package/src/tracking/session-state.ts +94 -0
- package/src/tracking/store.ts +96 -0
- package/src/tracking/track-compact.ts +100 -0
- package/src/tracking/track-diff.ts +50 -0
- package/src/tracking/track-journal.ts +97 -0
- package/src/tracking/track-lock-sync.ts +83 -0
- package/src/tracking/track-lock.ts +67 -0
- package/src/util/compact-json.ts +23 -0
- package/src/util/index.ts +2 -0
- package/src/util/json-io.ts +47 -0
- package/src/util/project-root.ts +41 -0
- package/src/util/runtime-io.ts +85 -0
- package/src/util/semver.ts +30 -0
- package/dist/permission-mode-B0MUl7FT.mjs +0 -136
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { evaluate, type PolicyResult } from "../policy/evaluate";
|
|
2
|
+
import { protectedPathGate } from "../policy/trivial-edits";
|
|
3
|
+
import { existingLineCounts, isApexScoped } from "./gate-helpers";
|
|
4
|
+
import { FAIL_CLOSED } from "../policy/guards";
|
|
5
|
+
import { loadTrack } from "../tracking/store";
|
|
6
|
+
import { dryGate } from "./dry";
|
|
7
|
+
import { preCommitGate } from "./precommit";
|
|
8
|
+
import { modularGate } from "./modular";
|
|
9
|
+
import { frameworkSkillGate } from "./framework-skill-gate";
|
|
10
|
+
import { reconcileRefReadsFromTranscript } from "../freshness/ref-evidence";
|
|
11
|
+
import { reconcileRefReadsFromJournal } from "../freshness/ref-journal";
|
|
12
|
+
import { isShadcnWrite, shadcnSkillGate } from "../policy/shadcn-skill-gate";
|
|
13
|
+
import { isTailwindWrite, tailwindSkillGate } from "../policy/tailwind-skill-gate";
|
|
14
|
+
import { geminiMcpGate } from "../policy/gemini-mcp-gate";
|
|
15
|
+
import { apexScopedGate } from "./gate-apex";
|
|
16
|
+
import { withDenyLoop } from "./deny-loop-store";
|
|
17
|
+
import { recordOneShot } from "../tracking/one-shot";
|
|
18
|
+
import { dirname } from "node:path";
|
|
19
|
+
import type { GateInput } from "./gate-input";
|
|
20
|
+
import type { Prompt } from "../prompt/types";
|
|
21
|
+
|
|
22
|
+
export type { GateInput } from "./gate-input";
|
|
23
|
+
|
|
24
|
+
/** Prior agents the freshness gate requires before a code edit. */
|
|
25
|
+
export const REQUIRED_AGENTS: ReadonlyArray<string> = ["explore-codebase", "research-expert"];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Default freshness window (ms). Matches the plugin's `FUSE_ENFORCE_TTL_SEC`
|
|
29
|
+
* default (120s); only a fallback for callers that omit `windowMs` (e.g. tests) —
|
|
30
|
+
* the real CLI path always supplies it from `resolveTtlSec()` (`src/config/ttl.ts`).
|
|
31
|
+
*/
|
|
32
|
+
export const DEFAULT_WINDOW_MS = 120_000;
|
|
33
|
+
|
|
34
|
+
/** Trivial edits allowed within the window before the full APEX gates apply. */
|
|
35
|
+
export const TRIVIAL_BUDGET = 4;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Full gate: {@link runGates} yields the first blocking prompt (or null); the tail
|
|
39
|
+
* records the one-shot metric ({@link recordOneShot}, observation-only) then lets
|
|
40
|
+
* {@link withDenyLoop} rewrite an identical retried deny (decision unchanged).
|
|
41
|
+
*/
|
|
42
|
+
export async function gate(input: GateInput): Promise<Prompt | null> {
|
|
43
|
+
const prompt = await runGates(input);
|
|
44
|
+
return finalizeGate(input, prompt, input.command);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Gate independent Cursor command candidates without merging their shell syntax.
|
|
49
|
+
* Stateful outcome bookkeeping runs once, against the command that decided the
|
|
50
|
+
* aggregate result. A single distinct candidate follows {@link gate} verbatim.
|
|
51
|
+
*/
|
|
52
|
+
export async function gateCommandCandidates(input: GateInput, candidates: readonly string[]): Promise<Prompt | null> {
|
|
53
|
+
const distinct = [...new Set(candidates)];
|
|
54
|
+
if (distinct.length <= 1) return gate(input);
|
|
55
|
+
|
|
56
|
+
let decisive: { prompt: Prompt; command: string } | undefined;
|
|
57
|
+
for (const command of distinct) {
|
|
58
|
+
const prompt = await runGates({ ...input, command });
|
|
59
|
+
if (!prompt) continue;
|
|
60
|
+
if (!decisive || promptRank(prompt) > promptRank(decisive.prompt)) decisive = { prompt, command };
|
|
61
|
+
if (prompt.kind === "block") break;
|
|
62
|
+
}
|
|
63
|
+
return finalizeGate(input, decisive?.prompt ?? null, decisive?.command ?? input.command);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Persist one final gate outcome and apply deny-loop enrichment exactly once. */
|
|
67
|
+
function finalizeGate(input: GateInput, prompt: Prompt | null, command: string | undefined): Prompt | null {
|
|
68
|
+
const op = { filePath: input.filePath, content: input.content, command };
|
|
69
|
+
const dir = dirname(input.trackFile);
|
|
70
|
+
recordOneShot(prompt, op, { now: input.now, dir, sessionId: input.sessionId });
|
|
71
|
+
return withDenyLoop(prompt, input.tool, op, { now: input.now, dir, windowMs: input.windowMs ?? DEFAULT_WINDOW_MS, sessionId: input.sessionId });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Higher numeric values dominate while equal values preserve wire order. */
|
|
75
|
+
function promptRank(prompt: Prompt): number {
|
|
76
|
+
return prompt.kind === "block" ? 3 : prompt.kind === "ask" ? 2 : 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Stateless guards, then the trivial fast path, then the stateful APEX gates. */
|
|
80
|
+
async function runGates(input: GateInput): Promise<Prompt | null> {
|
|
81
|
+
// Pre-commit lint hard-block runs FIRST: evaluate()'s GIT_ASK branch would
|
|
82
|
+
// otherwise short-circuit a `git commit` before the linters get to veto it.
|
|
83
|
+
const precommit = preCommitGate(input.tool, input.command, input.cwd);
|
|
84
|
+
if (precommit) return precommit;
|
|
85
|
+
// Hook-managed paths: absolute deny on ALL extensions, BEFORE the code-ext/exempt filters (parity enforce-apex-phases.ts:48-52 — isApexScoped never routes a non-code/exempt path to the guard).
|
|
86
|
+
const protectedDeny = protectedPathGate(input.tool, input.filePath);
|
|
87
|
+
if (protectedDeny) return protectedDeny;
|
|
88
|
+
const { raw: existingLines, code: existingCodeLines, content: existingContent } = existingLineCounts(input.filePath);
|
|
89
|
+
let quick: PolicyResult;
|
|
90
|
+
try {
|
|
91
|
+
quick = evaluate({ tool: input.tool, filePath: input.filePath, content: input.content, command: input.command, agentType: input.agentType, existingLines, existingContent, oldString: input.oldString, isReplaceAll: input.isReplaceAll, neverApproval: input.neverApproval });
|
|
92
|
+
} catch {
|
|
93
|
+
return FAIL_CLOSED;
|
|
94
|
+
}
|
|
95
|
+
if (quick.decision !== "allow" && quick.prompt) return quick.prompt;
|
|
96
|
+
const modular = modularGate(input.tool, input.filePath, input.content, input.cwd);
|
|
97
|
+
if (modular) return modular;
|
|
98
|
+
if (!input.filePath) return null;
|
|
99
|
+
const filePath = input.filePath;
|
|
100
|
+
// Reconcile refsRead BEFORE any consumer from two durable, race-immune sources (racy load→save loses lone
|
|
101
|
+
// writes under the fan-out): the transcript restores the LEAD's flushed read, the append-only journal (ref-journal.ts) the TEAMMATE's un-flushed one (lag > TTL).
|
|
102
|
+
const track = reconcileRefReadsFromJournal(
|
|
103
|
+
reconcileRefReadsFromTranscript(await loadTrack(input.trackFile), input.transcriptPath, input.now),
|
|
104
|
+
dirname(input.trackFile), input.now,
|
|
105
|
+
);
|
|
106
|
+
const solidOrSkill = frameworkSkillGate(input, track.refsRead, existingCodeLines);
|
|
107
|
+
if (solidOrSkill) return solidOrSkill;
|
|
108
|
+
// Standalone shadcn/ui gate (ports check-skill-loaded.py): runs independently of `framework` since a components.json / .css write may detect as "generic", mirroring the real plugin's own PreToolUse hook alongside react's.
|
|
109
|
+
if (isShadcnWrite(input.tool, filePath)) {
|
|
110
|
+
const shadcnBlock = shadcnSkillGate(input.tool, filePath, input.content ?? "", { refsRead: track.refsRead, authorizations: track.authorizations, sessionId: input.sessionId });
|
|
111
|
+
if (shadcnBlock) return shadcnBlock;
|
|
112
|
+
}
|
|
113
|
+
// Standalone Tailwind base-skill gate (ports check-tailwind-skill.py Phase 1): a .tsx/.jsx write with Tailwind classes needs a base Tailwind skill read, independent of framework.
|
|
114
|
+
if (isTailwindWrite(input.tool, filePath, input.content ?? "")) {
|
|
115
|
+
const twBlock = tailwindSkillGate(input.tool, filePath, input.content ?? "", track.refsRead);
|
|
116
|
+
if (twBlock) return twBlock;
|
|
117
|
+
}
|
|
118
|
+
// OPT-IN Gemini Design MCP gate (ports enforce-gemini-mcp.py) — a no-op unless FUSE_ENFORCE_GEMINI_MCP is set, then blocks hand-written Tailwind UI until a mcp__gemini-design__* call is made this session.
|
|
119
|
+
const geminiBlock = geminiMcpGate(input.tool, filePath, input.content ?? "", { authorizations: track.authorizations, sessionId: input.sessionId });
|
|
120
|
+
if (geminiBlock) return geminiBlock;
|
|
121
|
+
// The freshness/doc/SOLID APEX gates only police code files (require-apex-agents.py parity): non-code and exempt paths skip straight to the DRY check below.
|
|
122
|
+
if (isApexScoped(input.filePath)) {
|
|
123
|
+
const apex = await apexScopedGate(input, track, input.windowMs ?? DEFAULT_WINDOW_MS);
|
|
124
|
+
if (apex) return apex;
|
|
125
|
+
}
|
|
126
|
+
// DRY duplication (effectful: greps the codebase) — runs once the APEX gates pass.
|
|
127
|
+
return dryGate(input.tool, input.filePath, input.content, input.cwd);
|
|
128
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { extractText } from "../cache/mcp-response";
|
|
2
|
+
import { activityFor } from "./activity";
|
|
3
|
+
import { mcpPostStore } from "./mcp";
|
|
4
|
+
import { recordActivity } from "./record";
|
|
5
|
+
import { designGate } from "./design";
|
|
6
|
+
import { postEditContext } from "./lifecycle-bridge";
|
|
7
|
+
import { postTrackingSideEffects } from "./lifecycle/post-tracking";
|
|
8
|
+
import { aipilotPostToolUse, checkFileSize, validateTailwind } from "./lifecycle";
|
|
9
|
+
import { seoPostToolUseResponse } from "./lifecycle/seo/post-tool-use";
|
|
10
|
+
import { classifyAgentEvidence, recordAgentEvidence } from "../freshness/agent-evidence-record";
|
|
11
|
+
import { recordCodexSpawnEvidence } from "../freshness/codex-spawn-evidence";
|
|
12
|
+
import { captureBashReceipt } from "./receipt-capture";
|
|
13
|
+
import { recordCodexPostFailure } from "../tracking/codex-post-failure";
|
|
14
|
+
import { defaultStateDir } from "./paths";
|
|
15
|
+
import { fanOutFiles, firstFileMatch } from "./post-fanout";
|
|
16
|
+
import { postOutcome } from "./post-outcome";
|
|
17
|
+
import type { PreContext } from "./handle-pre";
|
|
18
|
+
import type { HandleOutcome } from "./handle";
|
|
19
|
+
|
|
20
|
+
function isCursorAfterFileEdit(id: string, payload: Record<string, unknown>): boolean {
|
|
21
|
+
if (id !== "cursor") return false;
|
|
22
|
+
const hookEvent = typeof payload.hook_event_name === "string" ? payload.hook_event_name : "";
|
|
23
|
+
if (hookEvent) return /^afterFileEdit$/i.test(hookEvent);
|
|
24
|
+
return typeof payload.file_path === "string" && Array.isArray(payload.edits);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Run the PostToolUse pipeline: store the MCP response, emit a design warning,
|
|
29
|
+
* record the activity into the session track, apply per-scope side-effects (SEO
|
|
30
|
+
* deny, aipilot task cache), then inject the post-edit context.
|
|
31
|
+
*
|
|
32
|
+
* POST is advisory-only for the design pipeline: it can never undo a tool
|
|
33
|
+
* that already ran (the hard block lives in the PreToolUse `designFilesGate`).
|
|
34
|
+
* `designGate` therefore stays on the RAW, un-fanned event — its
|
|
35
|
+
* `recordPost` apply_patch branch is promote-only and resolves relative
|
|
36
|
+
* `design-system.md` paths via `join(cwd, …)` (design-helpers.ts); fanning
|
|
37
|
+
* that call would instead route the file through the Write/Edit branch,
|
|
38
|
+
* which reads `event.filePath` UNRESOLVED (breaking cwd-relative promotion)
|
|
39
|
+
* and can DEGRADE the state — both forbidden by the apply_patch promote-only
|
|
40
|
+
* doctrine (see `design-files-gate.ts` module doc). Only `designPassNotice`
|
|
41
|
+
* (pure formatting, no disk access, no state write) is fanned via
|
|
42
|
+
* {@link fanOutFiles}, so `apply_patch` gets one notice line per real file
|
|
43
|
+
* instead of none.
|
|
44
|
+
* @param ctx - The resolved context (same shape as the pre pipeline).
|
|
45
|
+
* @returns The native hook outcome.
|
|
46
|
+
*/
|
|
47
|
+
export async function handlePost(ctx: PreContext): Promise<HandleOutcome> {
|
|
48
|
+
const { id, payload, event, framework, mcpDir, file, opts } = ctx;
|
|
49
|
+
const cursorAfterFileEdit = isCursorAfterFileEdit(id, payload);
|
|
50
|
+
const designCacheDir = ctx.designCacheDir ?? mcpDir;
|
|
51
|
+
let response = payload.tool_response ?? payload.tool_output;
|
|
52
|
+
if (id === "cursor" && /^afterMCPExecution$/i.test(event.eventName ?? "")) {
|
|
53
|
+
try {
|
|
54
|
+
response = typeof payload.result_json === "string" ? JSON.parse(payload.result_json) : payload.result_json;
|
|
55
|
+
} catch {
|
|
56
|
+
response = undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
mcpPostStore(event.tool, event.input, response, mcpDir);
|
|
60
|
+
const designWarn = designGate(payload, event, designCacheDir, opts.cwd, opts.corpusRoot);
|
|
61
|
+
const activities = activityFor({ tool: event.tool, input: event.input, sessionId: event.sessionId, framework, now: opts.now, responseLength: extractText(response).length });
|
|
62
|
+
for (const activity of activities) await recordActivity(file, activity);
|
|
63
|
+
// Session-scoped evidence (parity track-subagent-research.py): sub-agent hooks
|
|
64
|
+
// carry the LEAD's session_id — Task/Agent launches excluded (credited above).
|
|
65
|
+
const evidence = classifyAgentEvidence(event.tool, event.input, response);
|
|
66
|
+
if (evidence) await recordAgentEvidence(file, evidence, opts.now, typeof payload.agent_id === "string" ? payload.agent_id : undefined);
|
|
67
|
+
// Codex multi_agent_v2 `spawn_agent` -> same session track (no-op for every
|
|
68
|
+
// other harness / non-spawn tool / missing `agent_type`; see module doc).
|
|
69
|
+
await recordCodexSpawnEvidence(file, id, event.tool, event.input, opts.now);
|
|
70
|
+
// Verification receipts (tsc/bun test runs) — structured responses only
|
|
71
|
+
// (Kimi's string `tool_output` would forge a success receipt; see module).
|
|
72
|
+
await captureBashReceipt(file, event.tool, event.command, payload.tool_result, response, opts.now);
|
|
73
|
+
if (id === "codex") recordCodexPostFailure(event.tool, payload.tool_result ?? response, { now: opts.now, dir: defaultStateDir(opts.cwd), sessionId: event.sessionId });
|
|
74
|
+
// Codex `apply_patch` and Cursor `afterFileEdit` fan into per-file events for
|
|
75
|
+
// tracking, validation, post-edit context, and notices.
|
|
76
|
+
const files = fanOutFiles(event);
|
|
77
|
+
for (const f of files) postTrackingSideEffects(opts.scope ?? "core", f, f.input, opts.now, payload, opts.cwd);
|
|
78
|
+
const seoDeny = opts.scope === "seo" ? seoPostToolUseResponse(payload) : null;
|
|
79
|
+
if (seoDeny && !cursorAfterFileEdit) return { stdout: seoDeny, exit: 0 };
|
|
80
|
+
if (opts.scope === "solid") {
|
|
81
|
+
const solidWarn = firstFileMatch(files, checkFileSize);
|
|
82
|
+
if (solidWarn && !cursorAfterFileEdit) return { stdout: solidWarn, exit: 0 };
|
|
83
|
+
}
|
|
84
|
+
if (opts.scope === "tailwindcss") {
|
|
85
|
+
const tailwindWarn = firstFileMatch(files, validateTailwind);
|
|
86
|
+
if (tailwindWarn && !cursorAfterFileEdit) return { stdout: tailwindWarn, exit: 0 };
|
|
87
|
+
}
|
|
88
|
+
if (opts.scope === "aipilot" && (event.tool === "TaskCreate" || event.tool === "TaskUpdate" || event.tool === "Write" || event.tool === "Edit")) {
|
|
89
|
+
const out = await aipilotPostToolUse(payload, opts.cwd, id);
|
|
90
|
+
if (out && !cursorAfterFileEdit) return { stdout: out, exit: 0 };
|
|
91
|
+
}
|
|
92
|
+
let extra = "";
|
|
93
|
+
for (const f of files) {
|
|
94
|
+
extra = await postEditContext(opts.scope ?? "core", f, opts.now, id);
|
|
95
|
+
if (extra) break;
|
|
96
|
+
}
|
|
97
|
+
return postOutcome({
|
|
98
|
+
id, agentId: typeof payload.agent_id === "string" ? payload.agent_id : "", sessionId: event.sessionId,
|
|
99
|
+
now: opts.now, cwd: opts.cwd, activities, files,
|
|
100
|
+
designCacheDir, designWarn, extra,
|
|
101
|
+
cursorAfterFileEdit,
|
|
102
|
+
cursorEventName: typeof payload.hook_event_name === "string" ? payload.hook_event_name : "",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { dirname } from "node:path";
|
|
2
|
+
import { loadRefs } from "../refs/loader";
|
|
3
|
+
import { gate, gateCommandCandidates } from "./gate";
|
|
4
|
+
import { MCP_TTL_MS, mcpPreIntercept } from "./mcp";
|
|
5
|
+
import type { NormalizedEvent } from "./normalize";
|
|
6
|
+
import { recordActivity } from "./record";
|
|
7
|
+
import { respond } from "./respond";
|
|
8
|
+
import { withDenyNotice } from "./deny-notice";
|
|
9
|
+
import { designGate } from "./design";
|
|
10
|
+
import { taskContext } from "./inject-context";
|
|
11
|
+
import { securityAdvisory, securityAdvisoryForPatch } from "./lifecycle/security/check-skill";
|
|
12
|
+
import { solidScopeOutcome } from "./solid-pre";
|
|
13
|
+
import { isAgentTool } from "./is-agent-tool";
|
|
14
|
+
import { allowOutcome } from "./pre-allow";
|
|
15
|
+
import { applyPatchGate } from "./apply-patch-gate";
|
|
16
|
+
import { isBypassPermissions } from "../adapters/codex/permission-mode";
|
|
17
|
+
import { evaluate } from "../policy/evaluate";
|
|
18
|
+
import { confirmGate } from "./confirm/confirm-gate";
|
|
19
|
+
import type { HandleOptions, HandleOutcome } from "./handle";
|
|
20
|
+
|
|
21
|
+
/** Context the PreToolUse pipeline needs (resolved once by {@link handleHook}). */
|
|
22
|
+
export interface PreContext {
|
|
23
|
+
id: string;
|
|
24
|
+
payload: Record<string, unknown>;
|
|
25
|
+
event: NormalizedEvent;
|
|
26
|
+
framework: string;
|
|
27
|
+
mcpDir: string;
|
|
28
|
+
/**
|
|
29
|
+
* Session-anchored design-pipeline cache dir (see design-cache-resolve.ts) —
|
|
30
|
+
* distinct from `mcpDir`. Optional: pre-existing context literals (tests
|
|
31
|
+
* built before this field existed) omit it and fall back to `mcpDir`
|
|
32
|
+
* unchanged, so they keep their old, already-passing behavior verbatim.
|
|
33
|
+
*/
|
|
34
|
+
designCacheDir?: string;
|
|
35
|
+
file: string;
|
|
36
|
+
opts: HandleOptions;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Run the PreToolUse pipeline: MCP/WebFetch cache intercept, design gate, APEX Task context injection, then the stateless+APEX gate chain, returning the native hook outcome (deny/ask/inject or allow).
|
|
41
|
+
* @param ctx - The resolved pre-context.
|
|
42
|
+
* @returns The hook outcome.
|
|
43
|
+
*/
|
|
44
|
+
export async function handlePre(ctx: PreContext): Promise<HandleOutcome> {
|
|
45
|
+
const { id, payload, event, framework, mcpDir, file, opts } = ctx;
|
|
46
|
+
const designCacheDir = ctx.designCacheDir ?? mcpDir;
|
|
47
|
+
const intercept = mcpPreIntercept(id, event.tool, event.input, mcpDir, MCP_TTL_MS, opts.now);
|
|
48
|
+
if (intercept !== null) {
|
|
49
|
+
if (intercept.docSource) await recordActivity(file, { kind: "doc", framework, sessionId: event.sessionId, source: intercept.docSource });
|
|
50
|
+
return { stdout: intercept.stdout, exit: 0 };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const designBlock = designGate(payload, event, designCacheDir, opts.cwd, opts.corpusRoot);
|
|
54
|
+
if (designBlock) return { stdout: withDenyNotice(id, respond(id, designBlock, event.eventName ?? "PreToolUse"), designBlock, event.sessionId, dirname(file), opts.now), exit: 0 };
|
|
55
|
+
|
|
56
|
+
if (id === "cursor" && (event.eventName === "beforeReadFile" || event.eventName === "beforeTabFileRead")) {
|
|
57
|
+
const readPolicy = evaluate({ tool: event.tool, filePath: event.filePath });
|
|
58
|
+
return {
|
|
59
|
+
stdout: readPolicy.prompt
|
|
60
|
+
? respond(id, readPolicy.prompt, event.eventName)
|
|
61
|
+
: JSON.stringify({ permission: "allow" }),
|
|
62
|
+
exit: 0,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Security scope is advisory-only (ports check-security-skill.py): emit the
|
|
67
|
+
// non-blocking advisory when the skill is unread, else allow — NEVER run the
|
|
68
|
+
// core APEX/SOLID/file-size gate chain (the security plugin never did).
|
|
69
|
+
if (opts.scope === "security") {
|
|
70
|
+
return { stdout: event.files?.length ? securityAdvisoryForPatch(event.files, opts.now) : securityAdvisory(event.tool, event.filePath ?? "", opts.now), exit: 0 };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Solid scope mirrors security above: run ONLY the solid-scope gates
|
|
74
|
+
// (file-size deny, then Go/Python interface-location) and ALWAYS return —
|
|
75
|
+
// NEVER the core gate chain: core-guards owns it, so falling through would
|
|
76
|
+
// run gate() twice per edit (duplicate denies + added latency).
|
|
77
|
+
if (opts.scope === "solid") {
|
|
78
|
+
return solidScopeOutcome(id, event, file, opts.now);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// PreToolUse sub-agent dispatch (Task on claude/codex, Agent/AgentSwarm on
|
|
82
|
+
// kimi): inject APEX sub-agent context when the target apex dir exists.
|
|
83
|
+
if (isAgentTool(event.tool)) {
|
|
84
|
+
const taskCtx = taskContext(opts.cwd, id);
|
|
85
|
+
if (taskCtx) return { stdout: taskCtx, exit: 0 };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Codex `apply_patch`: normalize.ts fanned the freeform patch into per-file
|
|
89
|
+
// changes. OR their static verdicts — one violating hunk blocks the whole
|
|
90
|
+
// envelope. `event.files` is undefined for every other tool/harness.
|
|
91
|
+
if (event.files && event.files.length > 0) {
|
|
92
|
+
const patchPrompt = applyPatchGate(event.files, opts.cwd);
|
|
93
|
+
if (patchPrompt) return { stdout: withDenyNotice(id, respond(id, patchPrompt, event.eventName ?? "PreToolUse"), patchPrompt, event.sessionId, dirname(file), opts.now), exit: 0 };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const refs = opts.refsDir ? await loadRefs(opts.refsDir) : undefined;
|
|
97
|
+
const gateInput = {
|
|
98
|
+
sessionId: event.sessionId,
|
|
99
|
+
framework,
|
|
100
|
+
tool: event.tool,
|
|
101
|
+
filePath: event.filePath,
|
|
102
|
+
content: event.content,
|
|
103
|
+
command: event.command,
|
|
104
|
+
cwd: opts.cwd,
|
|
105
|
+
refs,
|
|
106
|
+
isReplaceAll: event.input.replace_all === true,
|
|
107
|
+
oldString: event.oldString,
|
|
108
|
+
agentType: event.agentType,
|
|
109
|
+
agentId: typeof payload.agent_id === "string" ? payload.agent_id : undefined,
|
|
110
|
+
windowMs: opts.windowMs,
|
|
111
|
+
now: opts.now,
|
|
112
|
+
trackFile: file,
|
|
113
|
+
transcriptPath: typeof payload.transcript_path === "string" ? payload.transcript_path : undefined,
|
|
114
|
+
neverApproval: id === "codex" && isBypassPermissions(event.permissionMode),
|
|
115
|
+
};
|
|
116
|
+
const prompt = id === "cursor" && (event.commandCandidates?.length ?? 0) > 1
|
|
117
|
+
? await gateCommandCandidates(gateInput, event.commandCandidates!)
|
|
118
|
+
: await gate(gateInput);
|
|
119
|
+
if (prompt) {
|
|
120
|
+
// CONFIRM <code> flow: ONLY changes anything when Codex/Kimi are about to
|
|
121
|
+
// downgrade THIS `ask` to a hard deny (confirmGate returns null in every
|
|
122
|
+
// other case — including every claude-code call, unconditionally, and
|
|
123
|
+
// every non-`ask` prompt kind — so the line below is byte-identical to
|
|
124
|
+
// the pre-CONFIRM behavior whenever it applies).
|
|
125
|
+
const confirm = confirmGate(id, prompt, event.command, event.sessionId, opts.now, opts.home,
|
|
126
|
+
id === "codex" ? { tool: event.tool, cwd: event.cwd ?? opts.cwd, toolUseId: event.toolUseId } : undefined);
|
|
127
|
+
if (confirm?.allow) {
|
|
128
|
+
return allowOutcome(id, event, payload, designCacheDir, opts.cwd, { trackFile: file, windowMs: opts.windowMs, now: opts.now }, opts.corpusRoot);
|
|
129
|
+
}
|
|
130
|
+
const finalPrompt = confirm ? confirm.prompt : prompt;
|
|
131
|
+
return { stdout: withDenyNotice(id, respond(id, finalPrompt, event.eventName ?? "PreToolUse"), finalPrompt, event.sessionId, dirname(file), opts.now), exit: 0 };
|
|
132
|
+
}
|
|
133
|
+
// Every gate allowed: hand off to the ALLOW-path assembly (pass notice +
|
|
134
|
+
// decision-time lesson + evidence-fresh notice). A deny/ask already returned
|
|
135
|
+
// above, so nothing it emits can block nor override a decision.
|
|
136
|
+
return allowOutcome(id, event, payload, designCacheDir, opts.cwd, { trackFile: file, windowMs: opts.windowMs, now: opts.now }, opts.corpusRoot);
|
|
137
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-pipeline async scope interception for {@link handleHook}. The aipilot and
|
|
3
|
+
* memory scopes reach external resources (cache files / Graphiti HTTP) and may
|
|
4
|
+
* emit stdout for lifecycle events, so they run before the sync gate pipeline.
|
|
5
|
+
*/
|
|
6
|
+
import { dispatchAipilot } from "./lifecycle";
|
|
7
|
+
import { dispatchMemory } from "./lifecycle/memory/dispatch";
|
|
8
|
+
import { cursorEventContract } from "../adapters/cursor/events";
|
|
9
|
+
import { toCursorLifecycleResponse } from "../adapters/cursor/respond";
|
|
10
|
+
import type { PluginScope } from "./lifecycle";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Run the async per-scope dispatcher for the invoking scope, if any.
|
|
14
|
+
* @param scope - The invoking plugin scope.
|
|
15
|
+
* @param event - The raw hook event name.
|
|
16
|
+
* @param payload - The raw hook payload.
|
|
17
|
+
* @param cwd - Project root.
|
|
18
|
+
* @param now - Clock.
|
|
19
|
+
* @param id - Harness target id (defaults to "claude-code" — zero-regression default).
|
|
20
|
+
* @returns The native stdout when intercepted, or `null` to fall through.
|
|
21
|
+
*/
|
|
22
|
+
export async function asyncScopeStdout(scope: PluginScope | undefined, event: string, payload: Record<string, unknown>, cwd: string, now: number, id: string = "claude-code"): Promise<string | null> {
|
|
23
|
+
const dispatchedEvent = id === "cursor" ? cursorEventContract(event).lifecycle : event;
|
|
24
|
+
if (dispatchedEvent === null) return null;
|
|
25
|
+
let stdout: string | null = null;
|
|
26
|
+
if (scope === "aipilot") stdout = await dispatchAipilot(dispatchedEvent, payload, cwd, now, undefined, id);
|
|
27
|
+
if (scope === "memory") stdout = await dispatchMemory(dispatchedEvent, payload, cwd, now);
|
|
28
|
+
return id === "cursor" && stdout !== null ? toCursorLifecycleResponse(stdout, event) : stdout;
|
|
29
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { PluginScope } from "./lifecycle";
|
|
2
|
+
|
|
3
|
+
/** Options for {@link handleHook} (caller supplies the clock + project root). */
|
|
4
|
+
export interface HandleOptions {
|
|
5
|
+
now: number;
|
|
6
|
+
cwd: string;
|
|
7
|
+
/** Directory of SOLID reference `.md` files for `solidReadGate` (else inert). */
|
|
8
|
+
refsDir?: string;
|
|
9
|
+
/** APEX freshness window in ms (from `FUSE_ENFORCE_TTL_SEC`). */
|
|
10
|
+
windowMs?: number;
|
|
11
|
+
/** Which plugin's hooks.json invoked the harness (selects lifecycle behavior). */
|
|
12
|
+
scope?: PluginScope;
|
|
13
|
+
/**
|
|
14
|
+
* Test/integration seam: the resolved refs-design corpus root ("" = absent).
|
|
15
|
+
* Undefined = resolve from disk (the production behavior); supplied = used
|
|
16
|
+
* as-is, so tests can drive the corpus-present branch deterministically.
|
|
17
|
+
*/
|
|
18
|
+
corpusRoot?: string;
|
|
19
|
+
/** Test-only OS home override for the session-anchored design cache dir (see design-cache-resolve.ts). Undefined = real `os.homedir()`. */
|
|
20
|
+
home?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** What the hook bin should print + exit with. */
|
|
24
|
+
export interface HandleOutcome {
|
|
25
|
+
stdout: string;
|
|
26
|
+
exit: number;
|
|
27
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { projectLayout } from "../config/layout";
|
|
2
|
+
import { detectFramework } from "../policy/detect-framework";
|
|
3
|
+
import { detectCreationIntent } from "../policy/creation-intent";
|
|
4
|
+
import { recordBrainstormRequired } from "../tracking/session-state";
|
|
5
|
+
import { withTrack } from "../tracking/store";
|
|
6
|
+
import { normalizeEvent } from "./normalize";
|
|
7
|
+
import { defaultStateDir, trackFile } from "./paths";
|
|
8
|
+
import { designLifecycle } from "./design-lifecycle";
|
|
9
|
+
import { promptSubmitContext } from "./inject-context";
|
|
10
|
+
import { lifecycleStdout } from "./lifecycle-bridge";
|
|
11
|
+
import { handlePre } from "./handle-pre";
|
|
12
|
+
import { handlePost } from "./handle-post";
|
|
13
|
+
import { asyncScopeStdout } from "./handle-scope-async";
|
|
14
|
+
import { resolveDesignCacheDir } from "./design-cache-resolve";
|
|
15
|
+
import { resyncCodexAgents } from "./lifecycle/codex-resync/resync";
|
|
16
|
+
import { resetFragmentRegistry } from "./fragment-registry";
|
|
17
|
+
import { attachBudgetRecap } from "./inject-budget-recap";
|
|
18
|
+
import { promptText } from "./prompt-text";
|
|
19
|
+
import { handleConfirmSubmit } from "./confirm/confirm-submit";
|
|
20
|
+
import { submitCodexConfirmation } from "./confirm/codex-confirm";
|
|
21
|
+
import { codexPromptOrigin } from "./confirm/codex-prompt-origin";
|
|
22
|
+
import { cursorProjectCwd } from "../adapters/cursor/context";
|
|
23
|
+
import { toCursorLifecycleResponse } from "../adapters/cursor/respond";
|
|
24
|
+
import type { HandleOptions, HandleOutcome } from "./handle-types";
|
|
25
|
+
export type { HandleOptions, HandleOutcome } from "./handle-types";
|
|
26
|
+
|
|
27
|
+
/** Raw Claude hook event name from a payload (empty when absent). */
|
|
28
|
+
function rawEventName(payload: Record<string, unknown>): string {
|
|
29
|
+
return typeof payload.hook_event_name === "string" ? payload.hook_event_name : "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The full hook handler: on a PRE event it gates the tool-use (stateless guards
|
|
34
|
+
* then APEX gates from the session track) and returns the native response; on a
|
|
35
|
+
* POST event it records the activity into the track. The loop that makes the
|
|
36
|
+
* package behave like the Claude plugin, on any harness.
|
|
37
|
+
*/
|
|
38
|
+
async function handleHookCore(id: string, payload: Record<string, unknown>, opts: HandleOptions): Promise<HandleOutcome> {
|
|
39
|
+
const event = normalizeEvent(id, payload);
|
|
40
|
+
if (id === "cursor") {
|
|
41
|
+
const cursorCwd = cursorProjectCwd(event.cwd, event.workspaceRoots ?? [], event.filePath, opts.cwd);
|
|
42
|
+
if (cursorCwd !== opts.cwd) opts = { ...opts, cwd: cursorCwd };
|
|
43
|
+
}
|
|
44
|
+
const rawPrompt = payload.prompt;
|
|
45
|
+
const userPrompt = typeof rawPrompt === "string" || Array.isArray(rawPrompt) ? promptText(rawPrompt) : undefined;
|
|
46
|
+
if (id === "codex" && rawEventName(payload) === "UserPromptSubmit" && userPrompt !== undefined) {
|
|
47
|
+
submitCodexConfirmation(event.sessionId, userPrompt, opts.now, opts.home, codexPromptOrigin(payload));
|
|
48
|
+
}
|
|
49
|
+
// Fresh slate for this invocation's capFragment tally — one hook event is
|
|
50
|
+
// exactly one lifecycle branch below (see dispatchLifecycle), so a single
|
|
51
|
+
// reset here can never mix fragments across unrelated events.
|
|
52
|
+
resetFragmentRegistry();
|
|
53
|
+
const layout = projectLayout(opts.cwd);
|
|
54
|
+
const file = trackFile(event.sessionId, defaultStateDir(opts.cwd));
|
|
55
|
+
const mcpDir = layout.cacheDir;
|
|
56
|
+
// Design pipeline (flag + `.design-state-*.json`) is anchored on the session
|
|
57
|
+
// id, not `mcpDir`/cwd — see design-cache-resolve.ts. `mcpDir` itself stays
|
|
58
|
+
// cwd-derived for the unrelated MCP/WebFetch cache.
|
|
59
|
+
const designCacheDir = resolveDesignCacheDir(event.sessionId, mcpDir, opts.home);
|
|
60
|
+
const framework = detectFramework(event.filePath ?? "", event.content ?? "", opts.cwd);
|
|
61
|
+
|
|
62
|
+
// Design-agent lifecycle (SubagentStart/Stop): init/cleanup the pipeline state machine.
|
|
63
|
+
// claude-code + codex only: verified against openai/codex's OWN generated hook schemas
|
|
64
|
+
// (codex-rs/hooks/schema/generated/subagent-{start,stop}.command.input.schema.json @ 385c0a93,
|
|
65
|
+
// superseding the stale 44918ea1 Cargo.toml-only bump) — `agent_id`/`agent_type` are REQUIRED
|
|
66
|
+
// on both events, unconditional on `hide_spawn_agent_metadata` (that flag only reshapes the
|
|
67
|
+
// spawn_agent tool RESULT, not the hook payload). `payload.prompt` (design-mode "component"
|
|
68
|
+
// detection below) is absent from Codex's schema — degrades to detectMode's default, not a
|
|
69
|
+
// break. Cursor/Gemini/Cline/Hermes remain unverified — NOT added without the same proof.
|
|
70
|
+
if ((id === "claude-code" || id === "codex") && designLifecycle(payload, designCacheDir, opts.cwd, String(opts.now), opts.now)) {
|
|
71
|
+
return { stdout: "", exit: 0 };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Codex-only, fail-open: refresh the plugin agents/commands cache on SessionStart when its fingerprint changed (strict no-op on every other harness).
|
|
75
|
+
if (id === "codex" && rawEventName(payload) === "SessionStart") resyncCodexAgents();
|
|
76
|
+
|
|
77
|
+
// Async per-scope lifecycle (aipilot cache handlers + memory-neural Graphiti).
|
|
78
|
+
const asyncOut = await asyncScopeStdout(opts.scope, rawEventName(payload), payload, opts.cwd, opts.now, id);
|
|
79
|
+
if (asyncOut !== null) return { stdout: asyncOut, exit: 0 };
|
|
80
|
+
|
|
81
|
+
// Ported lifecycle/session/context hooks (SessionStart, SubagentStart/Stop, etc.).
|
|
82
|
+
const life = lifecycleStdout(payload, opts.cwd, opts.scope ?? "core", opts.now, id);
|
|
83
|
+
if (life !== null) {
|
|
84
|
+
// Claude-Code-only: attachBudgetRecap's systemMessage envelope assumes the
|
|
85
|
+
// Claude adapter's stdout shape (mirrors the designLifecycle gate above).
|
|
86
|
+
const stdout = id === "claude-code" ? attachBudgetRecap(life, rawEventName(payload), event.sessionId, opts.cwd, opts.now) : life;
|
|
87
|
+
return { stdout, exit: 0 };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// UserPromptSubmit (core scope): brainstorm flag + CLAUDE.md injection.
|
|
91
|
+
// `payload.prompt` is a string on Claude Code/Codex, an array of content
|
|
92
|
+
// blocks on Kimi (see promptText) — either shape is normalized to text;
|
|
93
|
+
// anything else (field absent, or an unrecognized type) stays `undefined`
|
|
94
|
+
// so the block below is skipped exactly as before promptText existed.
|
|
95
|
+
if (userPrompt !== undefined) {
|
|
96
|
+
if (id !== "codex") handleConfirmSubmit(event.sessionId, userPrompt, opts.now, opts.home);
|
|
97
|
+
await withTrack(file, (track) => recordBrainstormRequired(track, detectCreationIntent(userPrompt)));
|
|
98
|
+
return { stdout: promptSubmitContext(userPrompt, opts.cwd, id), exit: 0 };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (event.phase === "post") {
|
|
102
|
+
return handlePost({ id, payload, event, framework, mcpDir, designCacheDir, file, opts });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return handlePre({ id, payload, event, framework, mcpDir, designCacheDir, file, opts });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Run one hook and adapt every Cursor scope outcome at the common runtime exit.
|
|
110
|
+
* Other harnesses retain the core handler's stdout and exit status unchanged.
|
|
111
|
+
*/
|
|
112
|
+
export async function handleHook(id: string, payload: Record<string, unknown>, opts: HandleOptions): Promise<HandleOutcome> {
|
|
113
|
+
const outcome = await handleHookCore(id, payload, opts);
|
|
114
|
+
if (id !== "cursor") return outcome;
|
|
115
|
+
return { ...outcome, stdout: toCursorLifecycleResponse(outcome.stdout, rawEventName(payload)) };
|
|
116
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { atomicWrite } from "../util/json-io";
|
|
5
|
+
|
|
6
|
+
/** Home `~/.claude` dir — per-harness config (CLAUDE.md, logs, plugins). */
|
|
7
|
+
export function claudeHome(home: string = homedir()): string {
|
|
8
|
+
return join(home, ".claude");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Neutral, harness-agnostic home for fuse-harness's OWN cache/state: `~/.fuse-harness`. */
|
|
12
|
+
export function fuseHarnessHome(home: string = homedir()): string {
|
|
13
|
+
return join(home, ".fuse-harness");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** `~/.fuse-harness/cache` base dir for session/cache state (shared across harnesses). */
|
|
17
|
+
export function fusengineCache(home: string = homedir()): string {
|
|
18
|
+
return join(fuseHarnessHome(home), "cache");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** `~/.fuse-harness/cache/sessions` — per-session JSON state dir. */
|
|
22
|
+
export function sessionsDir(home: string = homedir()): string {
|
|
23
|
+
return join(fusengineCache(home), "sessions");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const SID_RE = /^[a-zA-Z0-9_-]{1,128}$/;
|
|
27
|
+
|
|
28
|
+
/** Validate a session id (1-128 url-safe chars); null when invalid. */
|
|
29
|
+
export function sanitizeSessionId(sid: unknown): string | null {
|
|
30
|
+
const s = String(sid ?? "").trim();
|
|
31
|
+
return SID_RE.test(s) ? s : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Unified per-session state file path: `sessions/session-<sid>.json`. */
|
|
35
|
+
export function sessionStatePath(sid: string, home: string = homedir()): string {
|
|
36
|
+
return join(sessionsDir(home), `session-${sid}.json`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Load a session-state dict, or `{}` when missing/corrupt (mirrors Python). */
|
|
40
|
+
export function loadSessionState(sid: string, home: string = homedir()): Record<string, unknown> {
|
|
41
|
+
const path = sessionStatePath(sid, home);
|
|
42
|
+
try {
|
|
43
|
+
if (!existsSync(path)) return {};
|
|
44
|
+
const data: unknown = JSON.parse(readFileSync(path, "utf-8"));
|
|
45
|
+
return typeof data === "object" && data !== null && !Array.isArray(data) ? (data as Record<string, unknown>) : {};
|
|
46
|
+
} catch {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Atomically persist a session-state dict (0o600 via atomicWrite, indent 2). */
|
|
52
|
+
export function saveSessionState(sid: string, state: Record<string, unknown>, home: string = homedir()): void {
|
|
53
|
+
mkdirSync(sessionsDir(home), { recursive: true, mode: 0o700 });
|
|
54
|
+
atomicWrite(sessionStatePath(sid, home), JSON.stringify(state, null, 2));
|
|
55
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export * from "./paths";
|
|
2
|
+
export * from "./storage";
|
|
3
|
+
export * from "./record";
|
|
4
|
+
export * from "./activity";
|
|
5
|
+
export * from "./gate";
|
|
6
|
+
export * from "./dry";
|
|
7
|
+
export * from "./precommit";
|
|
8
|
+
export * from "./normalize";
|
|
9
|
+
export * from "./respond";
|
|
10
|
+
export * from "./mcp";
|
|
11
|
+
export * from "./inject-context";
|
|
12
|
+
export * from "./home-state";
|
|
13
|
+
export * from "./fs-cleanup";
|
|
14
|
+
export * from "./dev-context";
|
|
15
|
+
export * from "./lifecycle";
|
|
16
|
+
export * from "./lifecycle-bridge";
|
|
17
|
+
export * from "./handle-pre";
|
|
18
|
+
export * from "./handle";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lifecycle context-injection renderer. Every emitter that injects text into
|
|
3
|
+
* the model's context (rules, lessons, CLAUDE.md preamble, ...) goes through
|
|
4
|
+
* here instead of calling the Claude builders directly:
|
|
5
|
+
* - kimi: raw text on stdout — Kimi Code CLI appends stdout to the context on
|
|
6
|
+
* exit 0 (UserPromptSubmit is blockable; the Claude-shaped
|
|
7
|
+
* `additionalContext` envelope would be ignored or leak as raw JSON text).
|
|
8
|
+
* - every other harness: the historical Claude-shaped envelope, byte-identical
|
|
9
|
+
* to what the call sites produced before this module existed (zero-regression
|
|
10
|
+
* default — `attachSystemMessage` concatenates notices with "\n", so a single
|
|
11
|
+
* combined notice equals the previous chained calls).
|
|
12
|
+
*/
|
|
13
|
+
import { attachSystemMessage, contextResponse } from "../adapters/claude";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Render a context injection for the firing harness.
|
|
17
|
+
* @param id - Harness id.
|
|
18
|
+
* @param event - The firing hook event name (envelope tag for Claude shapes).
|
|
19
|
+
* @param text - The context body to inject.
|
|
20
|
+
* @param notice - Optional human-facing notice (systemMessage on Claude
|
|
21
|
+
* shapes; appended as plain text for kimi).
|
|
22
|
+
* @returns The native hook stdout.
|
|
23
|
+
*/
|
|
24
|
+
export function renderInform(id: string, event: string, text: string, notice?: string): string {
|
|
25
|
+
if (id === "kimi") return notice ? `${text}\n\n${notice}` : text;
|
|
26
|
+
const base = contextResponse(event, text);
|
|
27
|
+
return notice ? attachSystemMessage(base, notice) : base;
|
|
28
|
+
}
|