@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,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fuse-memory-neural scope state: per-line logs under
|
|
3
|
+
* `~/.claude/logs/00-memory` + project-type detection. Ports the shared
|
|
4
|
+
* filesystem helpers of the four memory-neural scripts.
|
|
5
|
+
*/
|
|
6
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { claudeHome } from "../../home-state";
|
|
10
|
+
import { atomicWrite } from "../../../util/json-io";
|
|
11
|
+
|
|
12
|
+
/** `~/.claude/logs/00-memory` log directory. */
|
|
13
|
+
export function memoryLogDir(home: string = homedir()): string {
|
|
14
|
+
return join(claudeHome(home), "logs", "00-memory");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Append a line to a memory log file, creating the dir. When `rotateAt > 0` and
|
|
19
|
+
* the file exceeds it, keep only the newest `keep` lines. Best-effort (errors
|
|
20
|
+
* swallowed), so a hook never fails on a logging issue.
|
|
21
|
+
* @param name - Log file name (e.g. `operations.log`).
|
|
22
|
+
* @param line - Line to append (newline added).
|
|
23
|
+
* @param rotateAt - Rotate when line count exceeds this (0 disables).
|
|
24
|
+
* @param keep - Lines to keep on rotation.
|
|
25
|
+
* @param home - Home dir.
|
|
26
|
+
*/
|
|
27
|
+
export function appendMemoryLog(name: string, line: string, rotateAt = 0, keep = 0, home: string = homedir()): void {
|
|
28
|
+
const dir = memoryLogDir(home);
|
|
29
|
+
try {
|
|
30
|
+
mkdirSync(dir, { recursive: true });
|
|
31
|
+
const file = join(dir, name);
|
|
32
|
+
appendFileSync(file, `${line}\n`, "utf-8");
|
|
33
|
+
if (rotateAt > 0) {
|
|
34
|
+
const lines = readFileSync(file, "utf-8").split("\n").filter((l) => l.length > 0);
|
|
35
|
+
if (lines.length > rotateAt) atomicWrite(file, `${lines.slice(-keep).join("\n")}\n`);
|
|
36
|
+
}
|
|
37
|
+
} catch { /* best effort */ }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Detect the project type from cwd markers (mirrors recall-on-session.py). */
|
|
41
|
+
export function detectProjectType(cwd: string): string {
|
|
42
|
+
const markers: Array<[string, string]> = [
|
|
43
|
+
["package.json", "node"], ["composer.json", "php"],
|
|
44
|
+
["Package.swift", "swift"], ["Cargo.toml", "rust"], ["go.mod", "go"],
|
|
45
|
+
];
|
|
46
|
+
for (const [f, t] of markers) if (existsSync(join(cwd, f))) return t;
|
|
47
|
+
if (existsSync(join(cwd, "requirements.txt")) || existsSync(join(cwd, "pyproject.toml"))) return "python";
|
|
48
|
+
return "unknown";
|
|
49
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostToolUse (mcp__graphiti|mcp__qdrant) memory handler. Ports
|
|
3
|
+
* `track-memory-ops.py`: append `[ts] <tool> | ok|error` to
|
|
4
|
+
* `operations.log`, rotating at 1000 lines (keeping the newest 500).
|
|
5
|
+
*/
|
|
6
|
+
import { isoUtc } from "../security/skill-state";
|
|
7
|
+
import { appendMemoryLog } from "./state";
|
|
8
|
+
|
|
9
|
+
/** Append a memory-operation log line for a graphiti/qdrant tool call. */
|
|
10
|
+
export function trackMemoryOp(payload: Record<string, unknown>, now: number): void {
|
|
11
|
+
const tool = typeof payload.tool_name === "string" ? payload.tool_name : "unknown";
|
|
12
|
+
const result = (payload.tool_result ?? payload.tool_response) as { error?: unknown } | undefined;
|
|
13
|
+
const status = result?.error ? "error" : "ok";
|
|
14
|
+
appendMemoryLog("operations.log", `[${isoUtc(now)}] ${tool} | ${status}`, 1000, 500);
|
|
15
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module post-compact
|
|
3
|
+
* PostCompact handler (core scope): re-inject the reconciliation snapshot plus a
|
|
4
|
+
* one-line reminder that read-state may have been reset by compaction, so the
|
|
5
|
+
* agent re-reads files before editing. Deduped per session/window (compaction can
|
|
6
|
+
* fan out too). Fully fail-open — any error yields "".
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
import { contextResponse } from "../../adapters/claude";
|
|
10
|
+
import { oncePerWindow } from "../inject-dedup";
|
|
11
|
+
import { defaultStateDir } from "../paths";
|
|
12
|
+
import { renderSnapshot } from "./snapshot";
|
|
13
|
+
|
|
14
|
+
/** Re-inject at most once per 30s per session (compaction fan-out + retries). */
|
|
15
|
+
const COMPACT_DEDUP_MS = 30_000;
|
|
16
|
+
|
|
17
|
+
/** One-line reminder prepended to the re-injected snapshot. */
|
|
18
|
+
const REMINDER = "Context was compacted — reread files before editing (read-state may be reset).";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Handle PostCompact: emit the reminder + reconciliation snapshot as
|
|
22
|
+
* `additionalContext`, deduped per session. "" on dedup-suppress or any error.
|
|
23
|
+
* @param data - The raw PostCompact payload (`session_id`, `trigger`).
|
|
24
|
+
* @param cwd - Project root.
|
|
25
|
+
* @param moduleUrl - `import.meta.url` of the caller (locates the running package for the version line).
|
|
26
|
+
* @param now - Clock (defaults to `Date.now()`).
|
|
27
|
+
* @returns The native hook stdout, or "".
|
|
28
|
+
*/
|
|
29
|
+
export function postCompactContext(data: Record<string, unknown>, cwd: string, moduleUrl: string, now: number = Date.now()): string {
|
|
30
|
+
try {
|
|
31
|
+
const sessionId = typeof data.session_id === "string" ? data.session_id : "unknown";
|
|
32
|
+
if (!oncePerWindow(`postcompact:${sessionId}`, COMPACT_DEDUP_MS, { now, dir: defaultStateDir(cwd) })) return "";
|
|
33
|
+
const snapshot = renderSnapshot(cwd, moduleUrl);
|
|
34
|
+
return contextResponse("PostCompact", snapshot ? `${REMINDER}\n\n${snapshot}` : REMINDER);
|
|
35
|
+
} catch {
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { basename } from "node:path";
|
|
4
|
+
import { contextResponse } from "../../adapters/claude";
|
|
5
|
+
|
|
6
|
+
const TS_EXT = /\.(ts|tsx)$/;
|
|
7
|
+
const TIMEOUT_MS = 10000;
|
|
8
|
+
|
|
9
|
+
/** True when `bin` is resolvable on PATH (mirrors shutil.which). */
|
|
10
|
+
function hasBin(bin: string): boolean {
|
|
11
|
+
try {
|
|
12
|
+
execFileSync(process.platform === "win32" ? "where" : "which", [bin], { stdio: "pipe", timeout: 2000 });
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Run `bin args`, returning `{ code, out }` (code 1 on spawn error). */
|
|
20
|
+
function run(bin: string, args: string[]): { code: number; out: string } {
|
|
21
|
+
try {
|
|
22
|
+
const out = execFileSync(bin, args, { encoding: "utf-8", stdio: "pipe", timeout: TIMEOUT_MS });
|
|
23
|
+
return { code: 0, out };
|
|
24
|
+
} catch (err) {
|
|
25
|
+
const e = err as { status?: number; stdout?: string | Buffer };
|
|
26
|
+
return { code: e.status ?? 1, out: typeof e.stdout === "string" ? e.stdout : e.stdout?.toString() ?? "" };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Handle PostToolUse for TS/TSX: report eslint/prettier issues (never fixes) as
|
|
32
|
+
* additionalContext. Ports `post-tool-use/post-edit-typescript.py`.
|
|
33
|
+
* @param filePath - The edited file path.
|
|
34
|
+
* @returns The native hook stdout (possibly empty).
|
|
35
|
+
*/
|
|
36
|
+
export function postEditTypescript(filePath: string): string {
|
|
37
|
+
if (!filePath || !TS_EXT.test(filePath) || !existsSync(filePath)) return "";
|
|
38
|
+
const issues: string[] = [];
|
|
39
|
+
if (hasBin("eslint")) {
|
|
40
|
+
const r = run("eslint", ["--no-fix", "--format", "compact", filePath]);
|
|
41
|
+
if (r.code !== 0 && r.out.trim()) issues.push(`ESLint:\n${r.out.trim()}`);
|
|
42
|
+
}
|
|
43
|
+
if (hasBin("prettier")) {
|
|
44
|
+
const r = run("prettier", ["--check", filePath]);
|
|
45
|
+
if (r.code !== 0) issues.push(`Prettier: ${basename(filePath)} needs formatting`);
|
|
46
|
+
}
|
|
47
|
+
if (issues.length === 0) return "";
|
|
48
|
+
return contextResponse("PostToolUse", `Lint issues in ${basename(filePath)}: ${issues.join(" | ")}`);
|
|
49
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostToolUse side-effect trackers for non-core scopes (carto/security/
|
|
3
|
+
* changelog). Pure side-effects — never produces stdout.
|
|
4
|
+
*/
|
|
5
|
+
import type { NormalizedEvent } from "../normalize";
|
|
6
|
+
import type { PluginScope } from "./dispatch";
|
|
7
|
+
import { trackEnrichment } from "./cartographer/track-enrichment";
|
|
8
|
+
import { trackSkillRead } from "./security/track-skill-read";
|
|
9
|
+
import { trackMcpResearch } from "./security/track-mcp";
|
|
10
|
+
import { trackWatchResearch } from "./changelog-research";
|
|
11
|
+
import { dispatchLessons } from "./lessons/dispatch";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Dispatch the appropriate PostToolUse tracker for the invoking scope. Carto
|
|
15
|
+
* persists manual enrichments; security records skill reads + MCP research;
|
|
16
|
+
* changelog records watch research; lessons arms the per-project throttle.
|
|
17
|
+
* Side-effect only.
|
|
18
|
+
* @param scope - The invoking plugin scope.
|
|
19
|
+
* @param event - The normalized event.
|
|
20
|
+
* @param input - The raw tool input.
|
|
21
|
+
* @param now - Clock.
|
|
22
|
+
* @param payload - The raw hook payload (for the lessons mark).
|
|
23
|
+
* @param cwd - The project root (for the lessons mark).
|
|
24
|
+
*/
|
|
25
|
+
export function postTrackingSideEffects(scope: PluginScope, event: NormalizedEvent, input: Record<string, unknown>, now: number, payload: Record<string, unknown> = {}, cwd: string = process.cwd()): void {
|
|
26
|
+
if (scope === "carto" && (event.tool === "Edit" || event.tool === "Write") && event.filePath) {
|
|
27
|
+
trackEnrichment(event.filePath);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (scope === "security") {
|
|
31
|
+
trackSkillRead(event.tool, event.filePath ?? "", now);
|
|
32
|
+
trackMcpResearch(event.tool, input, now);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (scope === "changelog") {
|
|
36
|
+
trackWatchResearch(event.tool, input, now);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (scope === "lessons") dispatchLessons("PostToolUse", payload, cwd, now);
|
|
40
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { harnessHomeSegment } from "../../policy/apex-target";
|
|
4
|
+
|
|
5
|
+
/** Two-digit zero-pad. */
|
|
6
|
+
function pad(n: number): string {
|
|
7
|
+
return String(n).padStart(2, "0");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Compact local timestamp `YYYYMMDD-HHMMSS` (mirrors Python strftime). */
|
|
11
|
+
function stamp(now: number): string {
|
|
12
|
+
const d = new Date(now);
|
|
13
|
+
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Handle PreCompact: back up the target apex `task.json` (`.claude/apex/`,
|
|
18
|
+
* `.codex/apex/`, ...) to `backups/`, keep only the 5 newest, and emit a
|
|
19
|
+
* confirmation. Ports `pre-compact/save-apex-state.py`.
|
|
20
|
+
* @param cwd - Project root.
|
|
21
|
+
* @param now - Clock (defaults to `Date.now()`).
|
|
22
|
+
* @param id - Harness target id (defaults to "claude-code" — zero-regression default).
|
|
23
|
+
* @returns The native hook stdout (possibly empty when no task.json).
|
|
24
|
+
*/
|
|
25
|
+
export function saveApexState(cwd: string, now: number = Date.now(), id: string = "claude-code"): string {
|
|
26
|
+
const seg = harnessHomeSegment(id);
|
|
27
|
+
const apexDir = join(cwd, seg, "apex");
|
|
28
|
+
const stateFile = join(apexDir, "task.json");
|
|
29
|
+
if (!existsSync(stateFile)) return "";
|
|
30
|
+
const backupDir = join(apexDir, "backups");
|
|
31
|
+
mkdirSync(backupDir, { recursive: true });
|
|
32
|
+
copyFileSync(stateFile, join(backupDir, `task-${stamp(now)}.json`));
|
|
33
|
+
const backups = readdirSync(backupDir).filter((n) => n.startsWith("task-") && n.endsWith(".json")).sort().reverse();
|
|
34
|
+
for (const old of backups.slice(5)) {
|
|
35
|
+
try { rmSync(join(backupDir, old), { force: true }); } catch { /* best effort */ }
|
|
36
|
+
}
|
|
37
|
+
return JSON.stringify({ additionalContext: `APEX state saved before compaction. Previous task state preserved in ${seg}/apex/backups/` });
|
|
38
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic rules-plugin root resolution. The historical `CLAUDE_PLUGIN_ROOT ??
|
|
3
|
+
* cwd` chain only worked when the harness exported the plugin root — Kimi
|
|
4
|
+
* injects `KIMI_PLUGIN_ROOT` instead, and a bare cwd fallback never held a
|
|
5
|
+
* `rules/` dir. Resolution order (first hit wins):
|
|
6
|
+
* 1. `CLAUDE_PLUGIN_ROOT` (claude-code/codex plugin-declared hooks);
|
|
7
|
+
* 2. `KIMI_PLUGIN_ROOT` (kimi plugin-declared hooks);
|
|
8
|
+
* 3. Per-harness install probe (claude marketplace, codex versioned cache,
|
|
9
|
+
* kimi managed plugins) — first `<plugin>/rules` dir whose plugin folder
|
|
10
|
+
* name contains "rules";
|
|
11
|
+
* 4. `cwd` (historical fallback, unchanged behavior).
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { maxSemver } from "../../util/semver";
|
|
17
|
+
|
|
18
|
+
/** Immediate child dir names of `dir`, or [] when unreadable. */
|
|
19
|
+
function children(dir: string): string[] {
|
|
20
|
+
try { return readdirSync(dir); } catch { return []; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** First existing `<base>/<child>/rules` where `child` names a rules plugin. */
|
|
24
|
+
function probe(base: string, childFilter: (name: string) => boolean): string | null {
|
|
25
|
+
for (const child of children(base).sort()) {
|
|
26
|
+
if (!childFilter(child)) continue;
|
|
27
|
+
const dir = join(base, child, "rules");
|
|
28
|
+
if (existsSync(dir)) return join(base, child);
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Claude: `~/.claude/plugins/marketplaces/<marketplace>/plugins/<rules-plugin>/`. */
|
|
34
|
+
function probeClaude(home: string): string | null {
|
|
35
|
+
const markets = join(home, ".claude", "plugins", "marketplaces");
|
|
36
|
+
for (const m of children(markets).sort()) {
|
|
37
|
+
const hit = probe(join(markets, m, "plugins"), (n) => n.includes("rules"));
|
|
38
|
+
if (hit) return hit;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Codex: `~/.codex/plugins/cache/<marketplace>/<rules-plugin>/<version>/` (highest semver). */
|
|
44
|
+
function probeCodex(home: string): string | null {
|
|
45
|
+
const cache = join(home, ".codex", "plugins", "cache");
|
|
46
|
+
for (const m of children(cache).sort()) {
|
|
47
|
+
for (const p of children(join(cache, m)).sort()) {
|
|
48
|
+
if (!p.includes("rules")) continue;
|
|
49
|
+
const versions = children(join(cache, m, p)).filter((v) => existsSync(join(cache, m, p, v, "rules")));
|
|
50
|
+
const latest = maxSemver(versions);
|
|
51
|
+
if (latest) return join(cache, m, p, latest);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Kimi: `<KIMI_CODE_HOME>/plugins/managed/<rules-plugin>/` (managed install copy). */
|
|
58
|
+
function probeKimi(env: Record<string, string | undefined>, home: string): string | null {
|
|
59
|
+
const root = env.KIMI_CODE_HOME ?? join(home, ".kimi-code");
|
|
60
|
+
return probe(join(root, "plugins", "managed"), (n) => n.includes("rules"));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve the directory whose `rules/` subtree holds the `*.md` rule files.
|
|
65
|
+
* @param id - Harness id (selects the install-layout probe).
|
|
66
|
+
* @param cwd - Historical fallback root.
|
|
67
|
+
* @param env - Environment (defaults to `process.env`).
|
|
68
|
+
* @returns The plugin root to read `rules/` from.
|
|
69
|
+
*/
|
|
70
|
+
export function resolveRulesRoot(
|
|
71
|
+
id: string,
|
|
72
|
+
cwd: string,
|
|
73
|
+
env: Record<string, string | undefined> = process.env,
|
|
74
|
+
): string {
|
|
75
|
+
if (env.CLAUDE_PLUGIN_ROOT) return env.CLAUDE_PLUGIN_ROOT;
|
|
76
|
+
if (env.KIMI_PLUGIN_ROOT) return env.KIMI_PLUGIN_ROOT;
|
|
77
|
+
const home = env.HOME ?? homedir();
|
|
78
|
+
const hit = id === "codex" ? probeCodex(home)
|
|
79
|
+
: id === "kimi" ? probeKimi(env, home)
|
|
80
|
+
: id === "claude-code" ? probeClaude(home)
|
|
81
|
+
: null;
|
|
82
|
+
return hit ?? cwd;
|
|
83
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security advisory (PreToolUse Write/Edit on code files) — NON-BLOCKING.
|
|
3
|
+
* Ports `check-security-skill.py`: nudge the agent to read the security skill,
|
|
4
|
+
* but always allow the edit.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { securityStatePath } from "./skill-state";
|
|
9
|
+
import { contextResponse } from "../../../adapters/claude";
|
|
10
|
+
import type { NormalizedFile } from "../../normalize";
|
|
11
|
+
|
|
12
|
+
const CODE_RE = /\.(ts|tsx|js|jsx|py|php|swift|go|rs|rb|java)$/;
|
|
13
|
+
const ADVISORY = "SECURITY: Read security skill references before modifying code. Use: Read skills/security-scan/references/scan-patterns.md";
|
|
14
|
+
|
|
15
|
+
/** True once today's security skill is marked read. Fail-open: missing/corrupt state reads as unread. */
|
|
16
|
+
function skillAlreadyRead(now: number, home: string): boolean {
|
|
17
|
+
const path = securityStatePath(now, home);
|
|
18
|
+
if (!existsSync(path)) return false;
|
|
19
|
+
try {
|
|
20
|
+
const state = JSON.parse(readFileSync(path, "utf-8")) as { skill_read?: boolean };
|
|
21
|
+
return state.skill_read === true;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build a non-blocking PreToolUse advisory when editing a code file before the
|
|
29
|
+
* security skill has been read, "" otherwise. Renders through the shared
|
|
30
|
+
* {@link contextResponse} builder (`additionalContext` only) — NEVER a naked
|
|
31
|
+
* `permissionDecision: "allow"`, which the Codex adapter's own hook shape
|
|
32
|
+
* (`src/adapters/claude/index.ts`) never emits and which Codex rejects.
|
|
33
|
+
* @param tool - The tool name (`Write`/`Edit`).
|
|
34
|
+
* @param filePath - The target file path.
|
|
35
|
+
* @param now - Clock.
|
|
36
|
+
* @param home - Home dir.
|
|
37
|
+
* @returns The advisory response JSON, or "".
|
|
38
|
+
*/
|
|
39
|
+
export function securityAdvisory(tool: string, filePath: string, now: number = Date.now(), home: string = homedir()): string {
|
|
40
|
+
if (tool !== "Write" && tool !== "Edit") return "";
|
|
41
|
+
if (!CODE_RE.test(filePath)) return "";
|
|
42
|
+
if (skillAlreadyRead(now, home)) return "";
|
|
43
|
+
return contextResponse("PreToolUse", ADVISORY);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Multi-file counterpart for a Codex `apply_patch` envelope: evaluate EACH
|
|
48
|
+
* add/update file (delete ignored outright; non-code filtered by the same
|
|
49
|
+
* `CODE_RE` inside {@link securityAdvisory}) and return the advisory
|
|
50
|
+
* triggered by the FIRST qualifying file, or "" once the skill has been read
|
|
51
|
+
* or no file qualifies.
|
|
52
|
+
* @param files - The patch's per-file changes ({@link NormalizedFile}).
|
|
53
|
+
* @param now - Clock.
|
|
54
|
+
* @param home - Home dir.
|
|
55
|
+
*/
|
|
56
|
+
export function securityAdvisoryForPatch(files: readonly NormalizedFile[], now: number = Date.now(), home: string = homedir()): string {
|
|
57
|
+
for (const f of files) {
|
|
58
|
+
if (f.op === "delete") continue;
|
|
59
|
+
const advisory = securityAdvisory(f.op === "add" ? "Write" : "Edit", f.filePath, now, home);
|
|
60
|
+
if (advisory) return advisory;
|
|
61
|
+
}
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module security/scan-patterns
|
|
3
|
+
* OWASP scan pattern table by language. Ports the Python
|
|
4
|
+
* `security_scan_patterns.py`: each entry is (severity, category, regex, glob).
|
|
5
|
+
* Go/Rust/unknown intentionally have no patterns (parity: empty result).
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Vulnerability severity level (drives the summary counters). */
|
|
10
|
+
export type Severity = "CRITICAL" | "HIGH" | "MEDIUM" | "LOW";
|
|
11
|
+
|
|
12
|
+
/** A single scan rule: a regex applied to files matching `glob`. */
|
|
13
|
+
export interface ScanPattern {
|
|
14
|
+
severity: Severity;
|
|
15
|
+
category: string;
|
|
16
|
+
regex: RegExp;
|
|
17
|
+
glob: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** OWASP scan patterns keyed by detected language (ported 1:1 from Python). */
|
|
21
|
+
const PATTERNS: Record<string, ScanPattern[]> = {
|
|
22
|
+
javascript: [
|
|
23
|
+
{ severity: "HIGH", category: "XSS", regex: /innerHTML\s*=/, glob: "*.js" },
|
|
24
|
+
{ severity: "HIGH", category: "XSS", regex: /dangerouslySetInnerHTML/, glob: "*.js" },
|
|
25
|
+
{ severity: "CRITICAL", category: "CODE_EXEC", regex: /eval\(/, glob: "*.js" },
|
|
26
|
+
{ severity: "CRITICAL", category: "CODE_EXEC", regex: /new Function\(/, glob: "*.js" },
|
|
27
|
+
{ severity: "CRITICAL", category: "CMD_INJECTION", regex: /child_process/, glob: "*.js" },
|
|
28
|
+
{ severity: "HIGH", category: "CMD_INJECTION", regex: /shell:\s*true/, glob: "*.js" },
|
|
29
|
+
{ severity: "MEDIUM", category: "WEAK_CRYPTO", regex: /Math\.random\(\)/, glob: "*.js" },
|
|
30
|
+
{ severity: "CRITICAL", category: "SECRETS", regex: /AKIA[0-9A-Z]{16}/, glob: "*.js" },
|
|
31
|
+
],
|
|
32
|
+
php: [
|
|
33
|
+
{ severity: "CRITICAL", category: "RCE", regex: /shell_exec|system\(|passthru\(/, glob: "*.php" },
|
|
34
|
+
{ severity: "CRITICAL", category: "CODE_EXEC", regex: /eval\(|assert\(/, glob: "*.php" },
|
|
35
|
+
{ severity: "HIGH", category: "SQL_INJECTION", regex: /mysql_query\(/, glob: "*.php" },
|
|
36
|
+
],
|
|
37
|
+
python: [
|
|
38
|
+
{ severity: "CRITICAL", category: "CODE_EXEC", regex: /eval\(|exec\(/, glob: "*.py" },
|
|
39
|
+
{ severity: "CRITICAL", category: "CMD_INJECTION", regex: /os\.system\(|subprocess.*shell=True/, glob: "*.py" },
|
|
40
|
+
{ severity: "HIGH", category: "DESERIALIZATION", regex: /pickle\.loads\(/, glob: "*.py" },
|
|
41
|
+
{ severity: "HIGH", category: "TLS", regex: /verify=False|ssl\.CERT_NONE/, glob: "*.py" },
|
|
42
|
+
],
|
|
43
|
+
swift: [
|
|
44
|
+
{ severity: "HIGH", category: "INSECURE_STORAGE", regex: /UserDefaults.*password|token|secret/, glob: "*.swift" },
|
|
45
|
+
{ severity: "MEDIUM", category: "INSECURE_HTTP", regex: /"http:\/\//, glob: "*.swift" },
|
|
46
|
+
{ severity: "HIGH", category: "WEAK_KEYCHAIN", regex: /kSecAttrAccessibleAlways/, glob: "*.swift" },
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Return the scan patterns for a language ("" / go / rust / unknown → none). */
|
|
51
|
+
export function getPatterns(lang: string): ScanPattern[] {
|
|
52
|
+
return PATTERNS[lang] ?? [];
|
|
53
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module security/scan
|
|
3
|
+
* OWASP vulnerability scanner. Ports the Python `security-scan.py`: detect the
|
|
4
|
+
* project language, walk source files (excluding vendored/build dirs), match the
|
|
5
|
+
* per-language {@link getPatterns} rules, and aggregate a JSON report. Invoked by
|
|
6
|
+
* the `harness scan` CLI command (skill `/fuse-security:scan`), NOT a hook.
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, readdirSync, readFileSync, type Dirent } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { getPatterns, type ScanPattern, type Severity } from "./scan-patterns";
|
|
12
|
+
|
|
13
|
+
const EXCLUDE_RE = /(^|\/)(node_modules|vendor|\.git|dist|build)(\/|$)/;
|
|
14
|
+
const LANG_FILES: readonly (readonly [string, string])[] = [
|
|
15
|
+
["package.json", "javascript"], ["composer.json", "php"], ["requirements.txt", "python"],
|
|
16
|
+
["pyproject.toml", "python"], ["Package.swift", "swift"], ["go.mod", "go"], ["Cargo.toml", "rust"],
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
/** A single vulnerability match in the scanned tree. */
|
|
20
|
+
export interface Finding {
|
|
21
|
+
severity: Severity; category: string; pattern: string; file: string; line: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The full scan report (mirrors the Python JSON shape). */
|
|
25
|
+
export interface ScanReport {
|
|
26
|
+
language: string; directory: string;
|
|
27
|
+
summary: Record<string, number>; findings: Finding[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Detect the project language from its config files (first match wins). */
|
|
31
|
+
export function detectLanguage(dir: string): string {
|
|
32
|
+
for (const [file, lang] of LANG_FILES) if (existsSync(join(dir, file))) return lang;
|
|
33
|
+
return "unknown";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Recursively collect files whose name ends with `ext`, skipping vendored dirs. */
|
|
37
|
+
function collectFiles(dir: string, ext: string, out: string[] = []): string[] {
|
|
38
|
+
let entries: Dirent[];
|
|
39
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return out; }
|
|
40
|
+
for (const e of entries) {
|
|
41
|
+
const full = join(dir, e.name);
|
|
42
|
+
if (EXCLUDE_RE.test(full)) continue;
|
|
43
|
+
if (e.isDirectory()) collectFiles(full, ext, out);
|
|
44
|
+
else if (e.name.endsWith(ext)) out.push(full);
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Apply one pattern to every matching file and collect line-level findings. */
|
|
50
|
+
function scanPattern(dir: string, p: ScanPattern): Finding[] {
|
|
51
|
+
const ext = p.glob.replace(/^\*/, "");
|
|
52
|
+
const findings: Finding[] = [];
|
|
53
|
+
for (const file of collectFiles(dir, ext)) {
|
|
54
|
+
let text: string;
|
|
55
|
+
try { text = readFileSync(file, "utf8"); } catch { continue; }
|
|
56
|
+
text.split("\n").forEach((line, i) => {
|
|
57
|
+
if (p.regex.test(line)) findings.push({ severity: p.severity, category: p.category, pattern: p.regex.source, file, line: i + 1 });
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return findings;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Run the OWASP scan over `dir` and return the aggregated {@link ScanReport}. */
|
|
64
|
+
export function runSecurityScan(dir: string): ScanReport {
|
|
65
|
+
const language = detectLanguage(dir);
|
|
66
|
+
const counts: Record<string, number> = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
67
|
+
const findings: Finding[] = [];
|
|
68
|
+
for (const p of getPatterns(language)) {
|
|
69
|
+
const hits = scanPattern(dir, p);
|
|
70
|
+
findings.push(...hits);
|
|
71
|
+
const key = p.severity.toLowerCase();
|
|
72
|
+
counts[key] = (counts[key] ?? 0) + hits.length;
|
|
73
|
+
}
|
|
74
|
+
const total = Object.values(counts).reduce((a, b) => a + b, 0);
|
|
75
|
+
return { language, directory: dir, summary: { ...counts, total }, findings };
|
|
76
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared security-tracker state: per-UTC-day JSON under
|
|
3
|
+
* `~/.claude/logs/00-security`. Ports the state helpers of
|
|
4
|
+
* `check-security-skill.py` / `track-skill-read.py` / `track-mcp-research.py`.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { claudeHome } from "../../home-state";
|
|
10
|
+
import { atomicWrite } from "../../../util/json-io";
|
|
11
|
+
|
|
12
|
+
/** `~/.claude/logs/00-security` state directory. */
|
|
13
|
+
export function securityStateDir(home: string = homedir()): string {
|
|
14
|
+
return join(claudeHome(home), "logs", "00-security");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Current UTC date as `YYYY-MM-DD`. */
|
|
18
|
+
export function todayUtc(now: number = Date.now()): string {
|
|
19
|
+
return new Date(now).toISOString().slice(0, 10);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Current UTC instant as `YYYY-MM-DDTHH:MM:SSZ` (seconds, no millis). */
|
|
23
|
+
export function isoUtc(now: number = Date.now()): string {
|
|
24
|
+
return new Date(now).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Today's security-state file path. */
|
|
28
|
+
export function securityStatePath(now: number = Date.now(), home: string = homedir()): string {
|
|
29
|
+
return join(securityStateDir(home), `${todayUtc(now)}-state.json`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Load today's security state, or `{}` when missing/corrupt. */
|
|
33
|
+
export function loadSecurityState(now: number = Date.now(), home: string = homedir()): Record<string, unknown> {
|
|
34
|
+
const path = securityStatePath(now, home);
|
|
35
|
+
try {
|
|
36
|
+
if (!existsSync(path)) return {};
|
|
37
|
+
const data: unknown = JSON.parse(readFileSync(path, "utf-8"));
|
|
38
|
+
return typeof data === "object" && data !== null && !Array.isArray(data) ? (data as Record<string, unknown>) : {};
|
|
39
|
+
} catch {
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Persist today's security state (indent 2, no trailing newline). */
|
|
45
|
+
export function saveSecurityState(state: Record<string, unknown>, now: number = Date.now(), home: string = homedir()): void {
|
|
46
|
+
atomicWrite(securityStatePath(now, home), JSON.stringify(state, null, 2));
|
|
47
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security MCP-research tracker (PostToolUse context7/exa). Ports
|
|
3
|
+
* `track-mcp-research.py`: logs documentation queries to today's state.
|
|
4
|
+
*/
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { isoUtc, loadSecurityState, saveSecurityState } from "./skill-state";
|
|
7
|
+
|
|
8
|
+
/** A recorded MCP research call. */
|
|
9
|
+
interface ResearchEntry {
|
|
10
|
+
timestamp: string;
|
|
11
|
+
tool: string;
|
|
12
|
+
query: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Append a context7/exa research call to today's security state. No-op for other
|
|
17
|
+
* tools. No stdout.
|
|
18
|
+
* @param tool - The tool name.
|
|
19
|
+
* @param input - The tool input (query/libraryId/libraryName).
|
|
20
|
+
* @param now - Clock.
|
|
21
|
+
* @param home - Home dir.
|
|
22
|
+
*/
|
|
23
|
+
export function trackMcpResearch(tool: string, input: Record<string, unknown>, now: number = Date.now(), home: string = homedir()): void {
|
|
24
|
+
if (!tool.includes("context7") && !tool.includes("exa")) return;
|
|
25
|
+
const query = String(input.query ?? input.libraryId ?? input.libraryName ?? "");
|
|
26
|
+
const state = loadSecurityState(now, home) as { skill_read?: boolean; reads?: unknown[]; research?: ResearchEntry[] };
|
|
27
|
+
(state.research ??= []).push({ timestamp: isoUtc(now), tool, query });
|
|
28
|
+
saveSecurityState(state, now, home);
|
|
29
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security skill-read tracker (PostToolUse Read). Ports `track-skill-read.py`:
|
|
3
|
+
* flips `skill_read` once a security skill reference is read.
|
|
4
|
+
*/
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { isoUtc, loadSecurityState, saveSecurityState } from "./skill-state";
|
|
7
|
+
|
|
8
|
+
const SKILL_RE = /skills\/(security-scan|cve-research|dependency-audit|security-headers|auth-audit)\//;
|
|
9
|
+
|
|
10
|
+
/** A recorded skill-reference read. */
|
|
11
|
+
interface SkillReadEntry {
|
|
12
|
+
timestamp: string;
|
|
13
|
+
file: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Mark the security skill as read when a Read hits a security skill reference.
|
|
18
|
+
* No-op for other tools/paths. No stdout.
|
|
19
|
+
* @param tool - The tool name.
|
|
20
|
+
* @param filePath - The read file path.
|
|
21
|
+
* @param now - Clock.
|
|
22
|
+
* @param home - Home dir.
|
|
23
|
+
*/
|
|
24
|
+
export function trackSkillRead(tool: string, filePath: string, now: number = Date.now(), home: string = homedir()): void {
|
|
25
|
+
if (tool !== "Read") return;
|
|
26
|
+
if (!SKILL_RE.test(filePath)) return;
|
|
27
|
+
const state = loadSecurityState(now, home) as { skill_read?: boolean; reads?: SkillReadEntry[] };
|
|
28
|
+
state.skill_read = true;
|
|
29
|
+
(state.reads ??= []).push({ timestamp: isoUtc(now), file: filePath });
|
|
30
|
+
saveSecurityState(state, now, home);
|
|
31
|
+
}
|