@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,36 @@
|
|
|
1
|
+
/** How a policy outcome should surface in a harness. */
|
|
2
|
+
export type PromptKind = "ask" | "block" | "inform";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Portable, harness-agnostic representation of a policy outcome as a prompt.
|
|
6
|
+
* Adapters render this into their harness's native shape (a Claude
|
|
7
|
+
* `permissionDecision`, a Cursor block, a CLI exit + message, ...).
|
|
8
|
+
*/
|
|
9
|
+
export interface Prompt {
|
|
10
|
+
kind: PromptKind;
|
|
11
|
+
/** Stable policy identifier for confirmation diagnostics. */
|
|
12
|
+
ruleId?: string;
|
|
13
|
+
/** Short title, e.g. "SOLID file-size limit". */
|
|
14
|
+
title: string;
|
|
15
|
+
/** Why this fired. */
|
|
16
|
+
reason: string;
|
|
17
|
+
/** Concrete next actions to proceed. */
|
|
18
|
+
actions?: string[];
|
|
19
|
+
/**
|
|
20
|
+
* User-facing notice (Python `hook_output.allow_pass`/`post_pass` parity):
|
|
21
|
+
* shown to the HUMAN — Claude Code's `systemMessage` — never to the agent,
|
|
22
|
+
* unlike `reason`/additionalContext. Excluded from {@link formatPrompt}.
|
|
23
|
+
*/
|
|
24
|
+
userMessage?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Render a {@link Prompt} as a consistent, agent-readable memo block. */
|
|
28
|
+
export function formatPrompt(p: Prompt): string {
|
|
29
|
+
const tag = p.kind === "block" ? "BLOCKED" : p.kind === "ask" ? "CONFIRM" : "NOTE";
|
|
30
|
+
const lines = [`[${tag}] ${p.title}`, p.reason];
|
|
31
|
+
if (p.actions?.length) {
|
|
32
|
+
lines.push("Next:");
|
|
33
|
+
p.actions.forEach((a, i) => lines.push(` ${i + 1}. ${a}`));
|
|
34
|
+
}
|
|
35
|
+
return lines.join("\n");
|
|
36
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { readdirSync } from "node:fs";
|
|
2
|
+
import { join, delimiter } from "node:path";
|
|
3
|
+
|
|
4
|
+
/** Immediate subdirectory names of `p`, or `[]` when it is missing/inaccessible. */
|
|
5
|
+
function subdirs(p: string): string[] {
|
|
6
|
+
try {
|
|
7
|
+
return readdirSync(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
8
|
+
} catch {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Every `skills/` parent dir across the Open-Agent-Skills layouts
|
|
15
|
+
* (agentskills.io) of Claude Code, Codex and Cursor: the standalone skill roots
|
|
16
|
+
* (`<base>/{.claude,.codex,.cursor,.agents}/skills` + `/etc/codex/skills`) plus
|
|
17
|
+
* installed-plugin roots (`<plugin>/skills` under the Claude marketplace and the
|
|
18
|
+
* Claude/Codex version caches). Shallow `readdir` only — no deep walk — so it is
|
|
19
|
+
* cheap enough to run on every hook.
|
|
20
|
+
*/
|
|
21
|
+
function skillParents(home: string, cwd: string, marketplaces: string[]): string[] {
|
|
22
|
+
const out: string[] = [];
|
|
23
|
+
for (const base of [cwd, home]) {
|
|
24
|
+
for (const tool of [".claude", ".codex", ".cursor", ".agents"]) out.push(join(base, tool, "skills"));
|
|
25
|
+
}
|
|
26
|
+
out.push(join("/etc", "codex", "skills"));
|
|
27
|
+
// Claude marketplace: <mkt>/plugins/<plugin>/skills.
|
|
28
|
+
const mktRoot = join(home, ".claude", "plugins", "marketplaces");
|
|
29
|
+
for (const mkt of subdirs(mktRoot)) {
|
|
30
|
+
if (!marketplaces.includes(mkt)) continue;
|
|
31
|
+
const pluginsDir = join(mktRoot, mkt, "plugins");
|
|
32
|
+
for (const plugin of subdirs(pluginsDir)) out.push(join(pluginsDir, plugin, "skills"));
|
|
33
|
+
}
|
|
34
|
+
// Version caches: <cache>/<mkt>/<plugin>/<version>/skills.
|
|
35
|
+
for (const cacheRoot of [join(home, ".claude", "plugins", "cache"), join(home, ".codex", "plugins", "cache")]) {
|
|
36
|
+
for (const mkt of subdirs(cacheRoot)) {
|
|
37
|
+
if (!marketplaces.includes(mkt)) continue;
|
|
38
|
+
for (const plugin of subdirs(join(cacheRoot, mkt))) {
|
|
39
|
+
for (const ver of subdirs(join(cacheRoot, mkt, plugin))) out.push(join(cacheRoot, mkt, plugin, ver, "skills"));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Auto-discover SOLID reference dirs (`<skill>/references` for every `solid-*`
|
|
48
|
+
* skill) across the Claude/Codex/Cursor skill layouts — the fallback used when
|
|
49
|
+
* `FUSE_HARNESS_REFS` is unset. Deduped by skill name (first source wins, so a
|
|
50
|
+
* marketplace skill shadows its version-cache copy). Returns a path-delimiter
|
|
51
|
+
* list, or `""` when none is found — with no refs the SOLID-read gate stays off.
|
|
52
|
+
* @param home - User home dir (`os.homedir()`).
|
|
53
|
+
* @param cwd - Current project dir (`process.cwd()`).
|
|
54
|
+
* @param marketplaces - Allowlist of marketplace names to scan (env
|
|
55
|
+
* `FUSE_HARNESS_MARKETPLACES`, default `["fusengine-plugins"]`); a marketplace
|
|
56
|
+
* absent from disk simply contributes nothing. Standalone
|
|
57
|
+
* `.claude`/`.codex`/`.cursor`/`.agents` skill roots are always scanned.
|
|
58
|
+
* @returns A `path.delimiter`-joined list of `references` dirs, or `""`.
|
|
59
|
+
*/
|
|
60
|
+
export function discoverRefs(home: string, cwd: string, marketplaces: string[]): string {
|
|
61
|
+
const bySkill = new Map<string, string>();
|
|
62
|
+
for (const parent of skillParents(home, cwd, marketplaces)) {
|
|
63
|
+
for (const skill of subdirs(parent)) {
|
|
64
|
+
if (!skill.startsWith("solid-") || bySkill.has(skill)) continue;
|
|
65
|
+
if (subdirs(join(parent, skill)).includes("references")) bySkill.set(skill, join(parent, skill, "references"));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return [...bySkill.values()].join(delimiter);
|
|
69
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Extract frontmatter key/value pairs from markdown content (quotes stripped). */
|
|
2
|
+
export function parseFrontmatter(content: string): Record<string, string> {
|
|
3
|
+
const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
4
|
+
if (!m?.[1]) return {};
|
|
5
|
+
const result: Record<string, string> = {};
|
|
6
|
+
for (const line of m[1].split("\n")) {
|
|
7
|
+
const idx = line.indexOf(":");
|
|
8
|
+
if (idx > 0) {
|
|
9
|
+
result[line.slice(0, idx).trim()] = line.slice(idx + 1).trim().replace(/^["']|["']$/g, "");
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Convert a simple glob (`**`, `*`) to an anchored RegExp. */
|
|
16
|
+
export function globToRe(g: string): RegExp {
|
|
17
|
+
const escaped = g
|
|
18
|
+
.trim()
|
|
19
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
20
|
+
.replace(/\*\*/g, "\0")
|
|
21
|
+
.replace(/\*/g, "[^/]*")
|
|
22
|
+
.replace(/\0/g, ".*");
|
|
23
|
+
return new RegExp(`^${escaped}$`);
|
|
24
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join, delimiter } from "node:path";
|
|
3
|
+
import { parseFrontmatter } from "./frontmatter";
|
|
4
|
+
import type { RefMeta } from "./types";
|
|
5
|
+
|
|
6
|
+
function pick(fm: Record<string, string>, ...keys: string[]): string {
|
|
7
|
+
for (const k of keys) {
|
|
8
|
+
const v = fm[k];
|
|
9
|
+
if (v) return v;
|
|
10
|
+
}
|
|
11
|
+
return "";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const SOLID_SLUGS = new Set(["single-responsibility", "open-closed", "liskov-substitution", "interface-segregation", "dependency-inversion", "solid-principles"]);
|
|
15
|
+
|
|
16
|
+
/** Infer a ref's level from its path when the frontmatter omits it (templates/→template, SOLID slug→principle). */
|
|
17
|
+
function inferLevel(filePath: string): string {
|
|
18
|
+
if (filePath.includes("/templates/")) return "template";
|
|
19
|
+
const stem = (filePath.split("/").pop() ?? "").replace(/\.md$/, "");
|
|
20
|
+
return SOLID_SLUGS.has(stem) ? "principle" : "architecture";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Build a {@link RefMeta} from parsed frontmatter, tolerant of kebab/camel keys. */
|
|
24
|
+
export function toRefMeta(fm: Record<string, string>, filePath: string): RefMeta {
|
|
25
|
+
return {
|
|
26
|
+
name: pick(fm, "name") || filePath,
|
|
27
|
+
description: pick(fm, "description"),
|
|
28
|
+
keywords: pick(fm, "keywords"),
|
|
29
|
+
priority: pick(fm, "priority"),
|
|
30
|
+
related: pick(fm, "related"),
|
|
31
|
+
appliesTo: pick(fm, "appliesTo", "applies-to", "applies_to"),
|
|
32
|
+
triggerOnEdit: pick(fm, "triggerOnEdit", "trigger-on-edit", "trigger_on_edit"),
|
|
33
|
+
level: pick(fm, "level") || inferLevel(filePath),
|
|
34
|
+
filePath,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Scan a directory recursively for `.md` reference files and parse each into a
|
|
40
|
+
* {@link RefMeta}. The content is entirely the consumer's — point this at any
|
|
41
|
+
* refs dir (`FUSE_HARNESS_REFS`). Returns an empty list when the dir is absent.
|
|
42
|
+
*/
|
|
43
|
+
export async function loadRefs(dirs: string): Promise<RefMeta[]> {
|
|
44
|
+
const refs: RefMeta[] = [];
|
|
45
|
+
for (const dir of dirs.split(delimiter).filter(Boolean)) {
|
|
46
|
+
let entries: string[];
|
|
47
|
+
try {
|
|
48
|
+
entries = (await readdir(dir, { recursive: true })) as string[];
|
|
49
|
+
} catch {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
for (const rel of entries) {
|
|
53
|
+
if (!rel.endsWith(".md")) continue;
|
|
54
|
+
const filePath = join(dir, rel);
|
|
55
|
+
refs.push(toRefMeta(parseFrontmatter(await readFile(filePath, "utf8")), filePath));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return refs;
|
|
59
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path shapes {@link discoverRefs}'s `skillParents` treats as legitimate skill
|
|
3
|
+
* roots: standalone `.claude`/`.codex`/`.cursor`/`.agents` skill dirs, marketplace
|
|
4
|
+
* and version-cache plugins installed UNDER one of those same config dirs, and
|
|
5
|
+
* the system-rooted `/etc/codex/skills`. Kept as a pure path-shape regex (no
|
|
6
|
+
* disk access, no home/cwd) so {@link skillRefKey} stays a cheap string test
|
|
7
|
+
* callable from the synchronous `solidReadGate`. Every alternative uses only
|
|
8
|
+
* bounded `[^/]+` groups — no nested quantifiers, so no ReDoS exposure.
|
|
9
|
+
*
|
|
10
|
+
* ANCHORING: the marketplace/cache branches require the `.claude`/`.codex`/
|
|
11
|
+
* `.cursor`/`.agents` segment to IMMEDIATELY precede `plugins/…/skills/` —
|
|
12
|
+
* a bare `/plugins/cache/<a>/<b>/<c>/skills/` floating anywhere (e.g. under an
|
|
13
|
+
* attacker-writable `/tmp/plugins/cache/...`) is NOT enough. `/etc/codex/skills/`
|
|
14
|
+
* is anchored at the string start (`^`), since that shape is a real absolute
|
|
15
|
+
* system path, not a directory name that can legitimately appear mid-path.
|
|
16
|
+
*/
|
|
17
|
+
const RECOGNIZED_SKILL_ROOT_RE =
|
|
18
|
+
/(^|\/)\.(claude|codex|cursor|agents)\/(skills\/|plugins\/(marketplaces\/[^/]+\/plugins\/[^/]+|cache\/[^/]+\/[^/]+\/[^/]+)\/skills\/)|^\/etc\/codex\/skills\//;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Normalize a reference path to its `skills/<skill>/...` suffix — the part
|
|
22
|
+
* that stays IDENTICAL between a marketplace-first path (what `discoverRefs`
|
|
23
|
+
* returns, carried by `ctx.refs`/`routed.*.filePath`) and the same skill read
|
|
24
|
+
* by a sub-agent/teammate through a different root: a version-cache path
|
|
25
|
+
* (extra version segment, plugin dir name that can diverge from the
|
|
26
|
+
* marketplace one, e.g. `solid` vs `fuse-solid`) or a standalone
|
|
27
|
+
* `.codex`/`.cursor`/`.agents` skills dir.
|
|
28
|
+
*
|
|
29
|
+
* ANTI-FORGERY: returns `null` — never a usable key — unless `path` also
|
|
30
|
+
* matches one of the recognized skill-root SHAPES above. A path like
|
|
31
|
+
* `/tmp/skills/solid-generic/references/solid-principles.md` has the right
|
|
32
|
+
* suffix but no recognized root, so it cannot credit a SOLID read by suffix
|
|
33
|
+
* alone; only an exact byte-for-byte match (same as any unrelated file) would.
|
|
34
|
+
* @param path - Absolute path to normalize (a `ctx.refs` entry or a `refsRead` entry).
|
|
35
|
+
* @returns The `skills/...` suffix, or `null` when `path` isn't a recognized skill path.
|
|
36
|
+
*/
|
|
37
|
+
export function skillRefKey(path: string): string | null {
|
|
38
|
+
if (!RECOGNIZED_SKILL_ROOT_RE.test(path)) return null;
|
|
39
|
+
const i = path.lastIndexOf("skills/");
|
|
40
|
+
return i === -1 ? null : path.slice(i);
|
|
41
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { globToRe } from "./frontmatter";
|
|
2
|
+
import type { RefMeta, ScoredRef, RouteResult } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Score references against a file edit (pure) — flat scoring, parity with the
|
|
6
|
+
* Python `ref_router._score_ref` (comma-split, single award per category):
|
|
7
|
+
* +10 once if ANY `applies-to` glob matches, +5 once if ANY `trigger-on-edit`
|
|
8
|
+
* fragment (trailing `/` stripped) is a substring of the path, +1 per keyword.
|
|
9
|
+
*/
|
|
10
|
+
export function scoreReferences(refs: RefMeta[], filePath: string, content: string): ScoredRef[] {
|
|
11
|
+
const scored: ScoredRef[] = [];
|
|
12
|
+
const hay = `${filePath} ${content}`.toLowerCase();
|
|
13
|
+
for (const meta of refs) {
|
|
14
|
+
let score = 0;
|
|
15
|
+
if (meta.appliesTo?.split(",").some((g) => g.trim() && globToRe(g.trim()).test(filePath))) score += 10;
|
|
16
|
+
if (meta.triggerOnEdit?.split(",").some((f) => { const t = f.trim().replace(/\/+$/, ""); return t && filePath.includes(t); })) score += 5;
|
|
17
|
+
if (meta.keywords) {
|
|
18
|
+
for (const kw of meta.keywords.split(",")) {
|
|
19
|
+
const k = kw.trim().toLowerCase();
|
|
20
|
+
if (k && hay.includes(k)) score += 1;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (score > 0) scored.push({ meta, score });
|
|
24
|
+
}
|
|
25
|
+
return scored;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Route references for a file edit: top-2 required + next-2 optional, ensuring a
|
|
30
|
+
* `principle` and a `template` appear in the top 4. Returns null when nothing scores.
|
|
31
|
+
*/
|
|
32
|
+
export function routeReferences(
|
|
33
|
+
refs: RefMeta[],
|
|
34
|
+
filePath: string,
|
|
35
|
+
content: string,
|
|
36
|
+
skillPath = "",
|
|
37
|
+
): RouteResult | null {
|
|
38
|
+
const scored = scoreReferences(refs, filePath, content);
|
|
39
|
+
if (!scored.length) return null;
|
|
40
|
+
scored.sort((a, b) => b.score - a.score);
|
|
41
|
+
const hoist = (level: string): void => {
|
|
42
|
+
if (scored.slice(0, 4).some((r) => r.meta.level === level)) return;
|
|
43
|
+
const found = scored.find((r) => r.meta.level === level);
|
|
44
|
+
if (found) {
|
|
45
|
+
scored.splice(scored.indexOf(found), 1);
|
|
46
|
+
scored.splice(3, 0, found);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
hoist("principle");
|
|
50
|
+
hoist("template");
|
|
51
|
+
return { required: scored.slice(0, 2), optional: scored.slice(2, 4), skillPath: skillPath || skillPathFromRef(scored[0]?.meta.filePath ?? "") };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Derive a skill's `SKILL.md` path from one of its `references/` file paths
|
|
56
|
+
* (`<skill>/references/...` → `<skill>/SKILL.md`), matching the on-disk skill
|
|
57
|
+
* layout {@link discoverRefs} walks. Returns "" when the ref's path doesn't
|
|
58
|
+
* follow that layout (e.g. ad-hoc refs dirs used in tests).
|
|
59
|
+
*/
|
|
60
|
+
function skillPathFromRef(refFilePath: string): string {
|
|
61
|
+
const i = refFilePath.indexOf("/references/");
|
|
62
|
+
return i === -1 ? "" : `${refFilePath.slice(0, i)}/SKILL.md`;
|
|
63
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Parsed frontmatter metadata from a reference .md file. */
|
|
2
|
+
export interface RefMeta {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
keywords: string;
|
|
6
|
+
priority: string;
|
|
7
|
+
related: string;
|
|
8
|
+
appliesTo: string;
|
|
9
|
+
triggerOnEdit: string;
|
|
10
|
+
level: string;
|
|
11
|
+
filePath: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** A reference with its computed routing score. */
|
|
15
|
+
export interface ScoredRef {
|
|
16
|
+
meta: RefMeta;
|
|
17
|
+
score: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Routing result with categorized references. */
|
|
21
|
+
export interface RouteResult {
|
|
22
|
+
required: ScoredRef[];
|
|
23
|
+
optional: ScoredRef[];
|
|
24
|
+
skillPath: string;
|
|
25
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Activity } from "./record";
|
|
2
|
+
import type { AgentQuality } from "../tracking/session-state";
|
|
3
|
+
import { classifyExplore } from "../freshness/explore-tools";
|
|
4
|
+
import { docFramework } from "../freshness/query-framework";
|
|
5
|
+
import { shellReadRefPaths } from "../policy/shell-read-refs";
|
|
6
|
+
import { isAgentTool } from "./is-agent-tool";
|
|
7
|
+
|
|
8
|
+
/** A live tool-use, already normalized to `tool` + `input` by the adapter. */
|
|
9
|
+
export interface ToolEvent {
|
|
10
|
+
/** The harness's `tool_name` (Cline: `preToolUse.toolName`), as a plain string. */
|
|
11
|
+
tool: string;
|
|
12
|
+
input?: Record<string, unknown>;
|
|
13
|
+
sessionId: string;
|
|
14
|
+
framework: string;
|
|
15
|
+
now: number;
|
|
16
|
+
/** Length of the tool response (POST events) — drives agent `quality`. */
|
|
17
|
+
responseLength?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Quality thresholds (chars): lead agent calls vs direct explore/research (parity Python `> 50`). */
|
|
21
|
+
const AGENT_QUALITY_MIN = 500;
|
|
22
|
+
const EXPLORE_QUALITY_MIN = 50;
|
|
23
|
+
|
|
24
|
+
/** Read tools across harnesses (Claude `Read`, Gemini/Cline `read_file`, …). */
|
|
25
|
+
const READ_TOOLS = new Set(["Read", "read_file", "read_many_files"]);
|
|
26
|
+
|
|
27
|
+
/** Grade a response length against a threshold; `undefined` when length is unknown. */
|
|
28
|
+
function qualityFor(len: number | undefined, min: number): AgentQuality | undefined {
|
|
29
|
+
return len === undefined ? undefined : len > min ? "sufficient" : "insufficient";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Build an `agent` activity, attaching `quality` only when graded. */
|
|
33
|
+
function agentActivity(name: string, ts: number, quality: AgentQuality | undefined): Activity {
|
|
34
|
+
return quality ? { kind: "agent", name, ts, quality } : { kind: "agent", name, ts };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The documentation source a tool satisfies for the doc-consultation gate, or
|
|
39
|
+
* undefined. Context7/Exa are the primary sources; fuse-browser is the fallback
|
|
40
|
+
* when Exa is down (`browser_fetch`/`browser_crawl`/`browser_serp_batch`); the
|
|
41
|
+
* built-in WebSearch/WebFetch also count.
|
|
42
|
+
* @param tool - The harness tool name.
|
|
43
|
+
*/
|
|
44
|
+
function docSourceOf(tool: string): string | undefined {
|
|
45
|
+
if (/context7/i.test(tool)) return "context7";
|
|
46
|
+
if (/exa/i.test(tool)) return "exa";
|
|
47
|
+
if (/browser_(fetch|crawl|serp)/i.test(tool)) return "fuse-browser";
|
|
48
|
+
if (tool === "WebSearch") return "websearch";
|
|
49
|
+
if (tool === "WebFetch") return "webfetch";
|
|
50
|
+
if (/^mcp__shadcn__/i.test(tool)) return "shadcn-mcp";
|
|
51
|
+
if (/^mcp__gemini-design__/i.test(tool)) return "gemini-mcp";
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Map a live tool-use to the activity to record, or null when nothing is
|
|
57
|
+
* tracked. Works across harnesses — tool names are globally distinct:
|
|
58
|
+
* - MCP doc calls (`context7` / `exa`, any separator) → `doc`
|
|
59
|
+
* - `Task`/`Agent`/`AgentSwarm` + `subagent_type` (Claude/Cursor/Kimi) → `agent` (bare agent name)
|
|
60
|
+
* - direct exploration/research (Glob/Grep, explore Bash, web, MCP cache reads)
|
|
61
|
+
* → `agent` credited to the matching REQUIRED_AGENTS phase
|
|
62
|
+
* - a read tool opening a `.md` reference → `ref`
|
|
63
|
+
*/
|
|
64
|
+
export function activityFor(event: ToolEvent): Activity[] {
|
|
65
|
+
const out: Activity[] = [];
|
|
66
|
+
// Doc consultation is recorded ALONGSIDE the research-phase credit (Python runs
|
|
67
|
+
// track-doc-consultation + track-subagent-research as two independent hooks):
|
|
68
|
+
// context7/exa/WebSearch/WebFetch AND fuse-browser (browser_fetch/crawl/serp_batch,
|
|
69
|
+
// in RESEARCH_TOOLS since explore-tools.ts:38) all credit BOTH doc and research-expert.
|
|
70
|
+
const docSource = docSourceOf(event.tool);
|
|
71
|
+
if (docSource) out.push({ kind: "doc", framework: docFramework(event.input, event.framework), sessionId: event.sessionId, source: docSource, ts: event.now });
|
|
72
|
+
|
|
73
|
+
// AgentSwarm is Kimi Code's batch launcher — same `subagent_type` identity field.
|
|
74
|
+
if (isAgentTool(event.tool)) {
|
|
75
|
+
const name = String(event.input?.subagent_type ?? event.input?.name ?? "").split(":").pop() ?? "";
|
|
76
|
+
if (name) out.push(agentActivity(name, event.now, qualityFor(event.responseLength, AGENT_QUALITY_MIN)));
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
const hit = classifyExplore(event.tool, event.input);
|
|
80
|
+
if (hit) {
|
|
81
|
+
const quality = hit.cacheHit ? "sufficient" : qualityFor(event.responseLength, EXPLORE_QUALITY_MIN);
|
|
82
|
+
out.push(agentActivity(hit.phase, event.now, quality));
|
|
83
|
+
} else if (READ_TOOLS.has(event.tool)) {
|
|
84
|
+
const path = String(event.input?.file_path ?? event.input?.path ?? "");
|
|
85
|
+
// `event.now` is the event timestamp `recordRefRead` stamps into
|
|
86
|
+
// `refsReadAt` (SOLID read TTL, parity track-solid-reads.py); `record.ts`
|
|
87
|
+
// forwards it as `recordRefRead(track, path, ts)`.
|
|
88
|
+
if (path.endsWith(".md")) out.push({ kind: "ref", path, ts: event.now });
|
|
89
|
+
}
|
|
90
|
+
// Codex teammates often read a skill/SOLID `.md` reference via a shell
|
|
91
|
+
// command (`cat`, `head`, …) instead of a native Read — independent of the
|
|
92
|
+
// classification above (a Bash call can ALSO be classified as `explore`
|
|
93
|
+
// evidence by classifyExplore), so every detected path is credited as its
|
|
94
|
+
// own `ref` activity (see shell-read-refs.ts for the read-only whitelist).
|
|
95
|
+
if (event.tool === "Bash") {
|
|
96
|
+
for (const path of shellReadRefPaths(event.input?.command)) out.push({ kind: "ref", path, ts: event.now });
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { protectedPathGate } from "../policy/trivial-edits";
|
|
2
|
+
import { evaluate } from "../policy/evaluate";
|
|
3
|
+
import { dryGate } from "./dry";
|
|
4
|
+
import { existingLineCounts } from "./gate-helpers";
|
|
5
|
+
import type { NormalizedFile } from "./normalize";
|
|
6
|
+
import type { Prompt } from "../prompt/types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* OR the static per-file verdict for a Codex `apply_patch` envelope: run the
|
|
10
|
+
* file-level gates (protected-path, SOLID file-size, DRY) that key off
|
|
11
|
+
* `filePath`/`content` over EACH touched file and return the first blocking
|
|
12
|
+
* {@link Prompt}. One violating hunk blocks the whole patch — the parity the
|
|
13
|
+
* single-file `Write`/`Edit` path already has, extended to the multi-file
|
|
14
|
+
* primitive.
|
|
15
|
+
*
|
|
16
|
+
* Only the read-only/pure gates run here (no session-state writes), so the
|
|
17
|
+
* `~11×` hook fan-out stays idempotent — the stateful APEX freshness/skill
|
|
18
|
+
* gates are tool-level and never policed `apply_patch` (its `filePath` was
|
|
19
|
+
* always undefined), so they are intentionally out of scope.
|
|
20
|
+
*
|
|
21
|
+
* File-size tool mapping mirrors Claude: an `add` is judged like a `Write`
|
|
22
|
+
* (full new content), an `update` like an `Edit` (partial content, compared
|
|
23
|
+
* against the on-disk count so an already-oversized file still blocks).
|
|
24
|
+
* @param files - Per-file changes from {@link NormalizedFile}.
|
|
25
|
+
* @param cwd - Project root for the DRY codebase grep.
|
|
26
|
+
* @returns The first blocking prompt, or null when every file passes.
|
|
27
|
+
*/
|
|
28
|
+
export function applyPatchGate(files: readonly NormalizedFile[], cwd: string): Prompt | null {
|
|
29
|
+
for (const f of files) {
|
|
30
|
+
const tool = f.op === "add" ? "Write" : "Edit";
|
|
31
|
+
const protectedDeny = protectedPathGate(tool, f.filePath);
|
|
32
|
+
if (protectedDeny) return protectedDeny;
|
|
33
|
+
if (f.op === "delete") continue;
|
|
34
|
+
const { raw: existingLines } = existingLineCounts(f.filePath);
|
|
35
|
+
const quick = evaluate({ tool, filePath: f.filePath, content: f.content, existingLines });
|
|
36
|
+
if (quick.decision !== "allow" && quick.prompt) return quick.prompt;
|
|
37
|
+
const dry = dryGate(tool, f.filePath, f.content, cwd);
|
|
38
|
+
if (dry) return dry;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module burst-window
|
|
3
|
+
* Single source of truth for the multi-plugin hook fan-out window.
|
|
4
|
+
*
|
|
5
|
+
* Every DEPLOYED plugin registers its OWN PreToolUse/PostToolUse hook, so ONE
|
|
6
|
+
* Claude tool event spawns ~11 sibling harness processes that each record the
|
|
7
|
+
* same deny / one-shot / sniper reminder within milliseconds. Left unchecked
|
|
8
|
+
* the deny-loop counter jumped by ~11 per real attempt ([REPEAT] "#9" on the
|
|
9
|
+
* FIRST try), the one-shot metric inflated ~11×, and the sniper reminder was
|
|
10
|
+
* injected ~11× (token noise).
|
|
11
|
+
*
|
|
12
|
+
* A record landing within this window after an identical prior one (same
|
|
13
|
+
* operation hash + same `session_id`) is treated as the SAME event and folded
|
|
14
|
+
* into it instead of re-counted. Two REAL agent retries are always spaced
|
|
15
|
+
* further apart than the burst, so this never masks a genuine loop. No env var:
|
|
16
|
+
* the fan-out is a physical property of the installed plugin set, not policy.
|
|
17
|
+
* @packageDocumentation
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Fan-out dedup window (ms). The ~11 sibling hooks for one event land in <2s. */
|
|
21
|
+
export const BURST_DEDUP_MS = 2000;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module codex-shell-tool
|
|
3
|
+
* Canonicalizes a Codex-only shell `tool_name` alias to `"Bash"`, so every
|
|
4
|
+
* existing `tool === "Bash"` consumer (activity credit, explore-tools
|
|
5
|
+
* classification, the protected-path guard, the design corpus read gate, …)
|
|
6
|
+
* treats a Code Mode exec dispatch exactly like a native Bash call — no
|
|
7
|
+
* per-consumer change needed.
|
|
8
|
+
*
|
|
9
|
+
* A Code Mode-wrapped call (`functions.exec` -> `tools.exec_command(...)`)
|
|
10
|
+
* surfaces via the generic `CoreToolRuntime` hook default (openai/codex#23757,
|
|
11
|
+
* commit 5c20513), which keeps the raw function-tool name `"exec_command"`
|
|
12
|
+
* verbatim — unlike a DIRECT `exec_command` dispatch, which
|
|
13
|
+
* `ExecCommandHandler`'s own `pre_tool_use_payload` override re-labels
|
|
14
|
+
* `"Bash"` (`tool_name: HookToolName::bash()`). This is the exact call chain
|
|
15
|
+
* a real Codex CLI 0.146.0 session was observed emitting for a `sed -n` skill
|
|
16
|
+
* read. Only ONE alias is confirmed here — `ToolName::plain("exec_command")`
|
|
17
|
+
* in `core/src/tools/handlers/unified_exec/exec_command.rs`; `shell`,
|
|
18
|
+
* `unified_exec`, `local_shell`, and `shell_command` were audited but not
|
|
19
|
+
* evidenced as a distinct hook `tool_name` value, so they are deliberately
|
|
20
|
+
* left out rather than guessed.
|
|
21
|
+
* @packageDocumentation
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** Codex `tool_name` values that are a shell execution but not literally `"Bash"`. */
|
|
25
|
+
const CODEX_SHELL_TOOL_ALIASES = new Set(["exec_command"]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Canonicalize a Codex-only shell tool alias to `"Bash"`. Scoped to
|
|
29
|
+
* `id === "codex"` — every other harness id, and every non-aliased tool
|
|
30
|
+
* name, passes through unchanged (see `test/codex-shell-read-credit.test.ts`
|
|
31
|
+
* for the non-regression proof).
|
|
32
|
+
* @param id - Harness adapter id (e.g. "codex", "claude-code", "kimi").
|
|
33
|
+
* @param tool - Raw `tool_name` from the hook payload.
|
|
34
|
+
*/
|
|
35
|
+
export function canonicalizeCodexShellTool(id: string, tool: string): string {
|
|
36
|
+
return id === "codex" && CODEX_SHELL_TOOL_ALIASES.has(tool) ? "Bash" : tool;
|
|
37
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Shell basenames whose `-c`-style argv carries the real script in argv[2]. */
|
|
2
|
+
const SHELLS = new Set(["bash", "sh", "zsh", "dash"]);
|
|
3
|
+
|
|
4
|
+
/** True when `arg` is (a path to) a POSIX shell we know wraps a `-c` script. */
|
|
5
|
+
function isShell(arg: string | undefined): boolean {
|
|
6
|
+
if (arg === undefined) return false;
|
|
7
|
+
const base = arg.slice(arg.lastIndexOf("/") + 1);
|
|
8
|
+
return SHELLS.has(base);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** True for a shell command flag (`-c`, `-lc`, `-ic`, …) — anything ending in `c`. */
|
|
12
|
+
function isCommandFlag(arg: string | undefined): boolean {
|
|
13
|
+
return arg !== undefined && /^-[a-z]*c$/.test(arg);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Coerce a hook payload's `command` field to the shell string the guard chain
|
|
18
|
+
* expects. Harnesses disagree on the wire shape: Claude Code, Cursor, and Hermes
|
|
19
|
+
* send a plain string, but Codex CLI sends an argv ARRAY
|
|
20
|
+
* (`["bash", "-lc", "git commit -m x"]`). A naive `typeof === "string"` check
|
|
21
|
+
* dropped the array to `undefined`, blinding every `ctx.command` guard
|
|
22
|
+
* (destructive-git, mutators, install…) into a silent fail-open. This restores
|
|
23
|
+
* the script so the array and string payloads reach an identical verdict.
|
|
24
|
+
*
|
|
25
|
+
* @param v - Raw `command` value off the payload (string, argv array, or other).
|
|
26
|
+
* @returns A string as-is; a `[shell, -c|-lc, script]` argv reduced to its
|
|
27
|
+
* script; any other all-string array joined with spaces; and `undefined` for
|
|
28
|
+
* empty arrays, arrays holding non-strings, or any other type.
|
|
29
|
+
*/
|
|
30
|
+
export function commandToString(v: unknown): string | undefined {
|
|
31
|
+
if (typeof v === "string") return v;
|
|
32
|
+
if (!Array.isArray(v)) return undefined;
|
|
33
|
+
if (v.length === 0 || !v.every((e) => typeof e === "string")) return undefined;
|
|
34
|
+
const argv = v as string[];
|
|
35
|
+
if (argv.length >= 3 && isShell(argv[0]) && isCommandFlag(argv[1])) return argv[2];
|
|
36
|
+
return argv.join(" ");
|
|
37
|
+
}
|