@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,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Documentation-consultation freshness: was ANY documentation source consulted
|
|
3
|
+
* this session — Context7, Exa, or WebSearch/WebFetch — via a live call or a
|
|
4
|
+
* Read on a cached MCP result file?
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Authorization entry from APEX state (legacy session + new sessions[]). */
|
|
8
|
+
export interface AuthEntry {
|
|
9
|
+
source?: string;
|
|
10
|
+
sources?: string[];
|
|
11
|
+
sessions?: string[];
|
|
12
|
+
session?: string;
|
|
13
|
+
doc_sessions?: string[];
|
|
14
|
+
read_paths?: string[];
|
|
15
|
+
/** ISO stamp of the latest doc/skill consultation for this framework (Check-1 TTL, parity enforce-apex-phases.ts). */
|
|
16
|
+
doc_consulted?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Resolve the sessions array from an auth entry (legacy fallback). */
|
|
20
|
+
export function resolveSessions(auth: AuthEntry | undefined): string[] {
|
|
21
|
+
if (!auth) return [];
|
|
22
|
+
return auth.sessions ?? (auth.session ? [auth.session] : []);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sessionAuthsFor(
|
|
26
|
+
authorizations: Record<string, AuthEntry> | undefined,
|
|
27
|
+
sessionId: string,
|
|
28
|
+
): AuthEntry[] {
|
|
29
|
+
if (!authorizations) return [];
|
|
30
|
+
return Object.values(authorizations).filter((a) => a.doc_sessions?.includes(sessionId));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Per-source satisfaction status for a session. */
|
|
34
|
+
export interface DocSatisfactionStatus {
|
|
35
|
+
context7: boolean;
|
|
36
|
+
exa: boolean;
|
|
37
|
+
web: boolean;
|
|
38
|
+
viaCache: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function evaluateDoc(auths: AuthEntry[]): DocSatisfactionStatus {
|
|
42
|
+
const sources = auths.flatMap((a) => a.sources ?? [a.source ?? ""]);
|
|
43
|
+
const readPaths = auths.flatMap((a) => a.read_paths ?? []);
|
|
44
|
+
const liveC7 = sources.some((s) => /context7/i.test(s));
|
|
45
|
+
const liveExa = sources.some((s) => /exa/i.test(s));
|
|
46
|
+
const liveWeb = sources.some((s) => /web(search|fetch)|fuse-browser/i.test(s));
|
|
47
|
+
const cacheC7 = readPaths.some((p) => /\/context\/mcp\/context7-/.test(p));
|
|
48
|
+
const cacheExa = readPaths.some((p) => /\/context\/mcp\/(exa-search|exa-code-context)-/.test(p));
|
|
49
|
+
return {
|
|
50
|
+
context7: liveC7 || cacheC7,
|
|
51
|
+
exa: liveExa || cacheExa,
|
|
52
|
+
web: liveWeb,
|
|
53
|
+
viaCache: (!liveC7 && cacheC7) || (!liveExa && cacheExa),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* True when documentation research is satisfied for the session. Primary path
|
|
59
|
+
* is parity with the Python `mcp_research_done`
|
|
60
|
+
* (`_shared/scripts/check_skill_common.py`): `"context7:" in content and
|
|
61
|
+
* "exa:" in content`, a strict AND (the Python source has no web-alone path).
|
|
62
|
+
* `web` (WebSearch/WebFetch/fuse-browser) is a deliberate TS-side addition —
|
|
63
|
+
* it alone satisfies the gate, since fuse-browser is this harness's preferred
|
|
64
|
+
* doc-research fast-path (see CLAUDE.md) and Context7/Exa can be unavailable.
|
|
65
|
+
*/
|
|
66
|
+
export function isDocConsulted(
|
|
67
|
+
authorizations: Record<string, AuthEntry> | undefined,
|
|
68
|
+
sessionId: string,
|
|
69
|
+
): boolean {
|
|
70
|
+
const s = evaluateDoc(sessionAuthsFor(authorizations, sessionId));
|
|
71
|
+
return (s.context7 && s.exa) || s.web;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Report how each doc source was satisfied for a session. */
|
|
75
|
+
export function formatDocSatisfactionStatus(
|
|
76
|
+
authorizations: Record<string, AuthEntry> | undefined,
|
|
77
|
+
sessionId: string,
|
|
78
|
+
): DocSatisfactionStatus {
|
|
79
|
+
return evaluateDoc(sessionAuthsFor(authorizations, sessionId));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Deny message when online documentation has not been consulted. */
|
|
83
|
+
export function formatDocDeny(framework: string): string {
|
|
84
|
+
return [
|
|
85
|
+
`BLOCKED: No MCP research done for ${framework}. Use BOTH:`,
|
|
86
|
+
"1) mcp__context7__query-docs AND 2) mcp__exa__web_search_exa",
|
|
87
|
+
"— OR a web fallback alone: mcp__fuse-browser__browser_fetch / browser_crawl / browser_serp_batch, WebSearch, or WebFetch.",
|
|
88
|
+
"This check is once per session — after consulting the required source(s), Write/Edit will be allowed.",
|
|
89
|
+
].join("\n");
|
|
90
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SubagentStop wiring for {@link harvestAgentEvidence}. The core SubagentStop
|
|
3
|
+
* dispatch (`runtime/lifecycle/dispatch.ts`) is SYNCHRONOUS and runs in a
|
|
4
|
+
* short-lived hook process, so this does SYNC track I/O (load → harvest → save)
|
|
5
|
+
* rather than the async `loadTrack`/`saveTrack`: a floating async write could be
|
|
6
|
+
* dropped before the process exits. Mirrors the sync read precedent in
|
|
7
|
+
* `policy/design/skill-evidence.ts` and the exact save body of `tracking/store.ts`
|
|
8
|
+
* (`signTrack` → atomic write → `writeLastNonce`) — kept sync on purpose here.
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { harvestAgentEvidence } from "./evidence-harvest";
|
|
12
|
+
import { defaultStateDir, trackFile } from "../runtime/paths";
|
|
13
|
+
import { emptyTrack, type SessionTrack } from "../tracking/session-state";
|
|
14
|
+
import { signTrack, verifyTrack, writeLastNonce, type TrackEnvelope } from "../tracking/integrity";
|
|
15
|
+
import { atomicWrite } from "../util/json-io";
|
|
16
|
+
import { LOCK_FAILED, withTrackLockSync } from "../tracking/track-lock-sync";
|
|
17
|
+
import { trackJournalEnabled } from "../tracking/store";
|
|
18
|
+
import { appendEvent } from "../tracking/track-journal";
|
|
19
|
+
import { diffTrackEvents } from "../tracking/track-diff";
|
|
20
|
+
import { journalLogPath, readTrackSync } from "../tracking/track-compact";
|
|
21
|
+
import { dirname } from "node:path";
|
|
22
|
+
|
|
23
|
+
/** Load the current track synchronously; `emptyTrack()` when absent/corrupt (fail-closed read). */
|
|
24
|
+
function loadTrackSync(file: string): SessionTrack {
|
|
25
|
+
try {
|
|
26
|
+
return verifyTrack(JSON.parse(readFileSync(file, "utf8")) as TrackEnvelope) ?? emptyTrack();
|
|
27
|
+
} catch {
|
|
28
|
+
return emptyTrack();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Harvest the finishing sub-agent's transcript into its session track. Runs for
|
|
34
|
+
* EVERY agent type (unlike the sniper reminder, which skips research/explore) —
|
|
35
|
+
* those are exactly the agents whose research/explore evidence we want to credit.
|
|
36
|
+
* Fully fail-open: a missing `agent_transcript_path`, an unreadable transcript, or
|
|
37
|
+
* a write error leaves the track untouched and never throws out of the hook.
|
|
38
|
+
* @param payload - The raw SubagentStop hook payload.
|
|
39
|
+
* @param cwd - Project root (selects the per-project state dir).
|
|
40
|
+
* @param now - Fallback epoch-ms for unstamped transcript tool_uses.
|
|
41
|
+
* @param baseDir - Override the track base dir (tests); defaults to the project state dir.
|
|
42
|
+
*/
|
|
43
|
+
export function harvestSubagentTrack(payload: Record<string, unknown>, cwd: string, now: number, baseDir: string = defaultStateDir(cwd)): void {
|
|
44
|
+
const transcriptPath = typeof payload.agent_transcript_path === "string" ? payload.agent_transcript_path : undefined;
|
|
45
|
+
if (!transcriptPath) return;
|
|
46
|
+
const sessionId = typeof payload.session_id === "string" ? payload.session_id : "unknown";
|
|
47
|
+
const file = trackFile(sessionId, baseDir);
|
|
48
|
+
// Journal mode (default): snapshot ⊕ log read, then lock-free signed event
|
|
49
|
+
// appends — same fail-open contract, never a "write skipped" under fan-out.
|
|
50
|
+
if (trackJournalEnabled()) {
|
|
51
|
+
const track = readTrackSync(file, true);
|
|
52
|
+
const next = harvestAgentEvidence(transcriptPath, track, now);
|
|
53
|
+
if (next === track) return; // nothing harvested → no write
|
|
54
|
+
const log = journalLogPath(file);
|
|
55
|
+
for (const ev of diffTrackEvents(track, next, now)) appendEvent(log, ev.field, ev.op, ev.value, ev.ts);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// Locked RMW (legacy kill-switch path, sync variant — this path cannot float
|
|
59
|
+
// async work): on contention the harvest is skipped like any other write
|
|
60
|
+
// error, fail-open.
|
|
61
|
+
const ran = withTrackLockSync(dirname(file), () => {
|
|
62
|
+
const track = loadTrackSync(file);
|
|
63
|
+
const next = harvestAgentEvidence(transcriptPath, track, now);
|
|
64
|
+
if (next === track) return; // nothing harvested (or unreadable) → no rewrite
|
|
65
|
+
try {
|
|
66
|
+
const envelope = signTrack(next);
|
|
67
|
+
atomicWrite(file, JSON.stringify(envelope, null, 2));
|
|
68
|
+
writeLastNonce(envelope.nonce);
|
|
69
|
+
} catch { /* fail-open: a lifecycle hook must never throw */ }
|
|
70
|
+
});
|
|
71
|
+
if (ran === LOCK_FAILED) return;
|
|
72
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retroactive evidence harvest at SubagentStop. Parses a completed sub-agent's
|
|
3
|
+
* OWN transcript (`agent_transcript_path`) and backfills the session track with
|
|
4
|
+
* the research/explore tool_use it performed (→ `agents`) and the `.md` reference
|
|
5
|
+
* files it read (→ `refsReadAt`). Per LESSON.md + issues #43612/#27655/#34692,
|
|
6
|
+
* sidechain PostToolUse hooks are NOT guaranteed to fire, so live in-session
|
|
7
|
+
* crediting is only opportunistic; SubagentStop is dispatched by the main session
|
|
8
|
+
* (reliable), and its transcript is the durable anchor this harvest reads.
|
|
9
|
+
*
|
|
10
|
+
* QUALITY: harvested agent evidence is credited `sufficient`. The freshness reader
|
|
11
|
+
* (`agentsFreshInTrack`) ignores `insufficient` entries, so anything less would
|
|
12
|
+
* make the harvest inert. This is sound: the platform-authored transcript proves
|
|
13
|
+
* the tool genuinely ran to completion in a finished sub-agent — a STRONGER
|
|
14
|
+
* guarantee than the live path's response-size heuristic (an anti-gaming measure
|
|
15
|
+
* for the self-recorded track, which the transcript's provenance supersedes).
|
|
16
|
+
*/
|
|
17
|
+
import { classifyAgentEvidence } from "./agent-evidence-record";
|
|
18
|
+
import { readAgentToolUses, type TranscriptToolUse } from "../runtime/lifecycle/agent-transcript";
|
|
19
|
+
import { recordAgent, recordRefRead, type SessionTrack } from "../tracking/session-state";
|
|
20
|
+
|
|
21
|
+
/** Dedup tolerance (ms) against evidence a live sidechain hook may already hold. */
|
|
22
|
+
const DEDUP_MS = 2000;
|
|
23
|
+
|
|
24
|
+
/** True when an `agents` entry with `name` already sits within ±{@link DEDUP_MS} of `ts`. */
|
|
25
|
+
function agentAlreadyRecorded(track: SessionTrack, name: string, ts: number): boolean {
|
|
26
|
+
return track.agents.some((a) => a.name === name && Math.abs(a.ts - ts) <= DEDUP_MS);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Fold one transcript tool_use into the track (immutably): credit classified
|
|
31
|
+
* research/explore evidence to `agents` (deduped, forced `sufficient`) and any
|
|
32
|
+
* `.md` Read to `refsReadAt` (never restamping a MORE-recent existing read).
|
|
33
|
+
* Returns the same reference when the call contributes nothing.
|
|
34
|
+
*/
|
|
35
|
+
function applyToolUse(track: SessionTrack, u: TranscriptToolUse, fallbackNow: number): SessionTrack {
|
|
36
|
+
const ts = u.ts ?? fallbackNow;
|
|
37
|
+
let next = track;
|
|
38
|
+
const ev = classifyAgentEvidence(u.name, u.input, undefined);
|
|
39
|
+
if (ev && !agentAlreadyRecorded(next, ev.name, ts)) next = recordAgent(next, ev.name, ts, "sufficient");
|
|
40
|
+
if (u.name === "Read") {
|
|
41
|
+
const path = String(u.input?.file_path ?? u.input?.path ?? "");
|
|
42
|
+
const prev = next.refsReadAt?.[path];
|
|
43
|
+
if (path.endsWith(".md") && (prev === undefined || prev < ts)) next = recordRefRead(next, path, ts);
|
|
44
|
+
}
|
|
45
|
+
return next;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Backfill `track` from the sub-agent transcript at `transcriptPath`. PURE: the
|
|
50
|
+
* caller owns load/save. Fail-open — an absent or unreadable transcript returns
|
|
51
|
+
* `track` UNCHANGED (same reference), never throwing.
|
|
52
|
+
* @param transcriptPath - Sub-agent's own `.jsonl` (SubagentStop `agent_transcript_path`).
|
|
53
|
+
* @param track - The current session track.
|
|
54
|
+
* @param now - Fallback epoch-ms for tool_uses the platform left unstamped.
|
|
55
|
+
* @returns The backfilled track, or `track` itself when nothing was harvested.
|
|
56
|
+
*/
|
|
57
|
+
export function harvestAgentEvidence(transcriptPath: string | undefined, track: SessionTrack, now: number): SessionTrack {
|
|
58
|
+
const uses = readAgentToolUses(transcriptPath);
|
|
59
|
+
if (uses === null) return track; // unreadable → unchanged (no regression)
|
|
60
|
+
return uses.reduce((acc, u) => applyToolUse(acc, u, now), track);
|
|
61
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classifier crediting DIRECT (lead-agent) exploration/research tool use as the
|
|
3
|
+
* matching APEX phase, so the freshness gate's self-recorded fallback accepts
|
|
4
|
+
* genuine direct work — not only `Task`/`Agent` sub-agent spawns. Mirrors the
|
|
5
|
+
* Python core-guards `_shared/apex_constants.py` + `track-subagent-research.py`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** A REQUIRED_AGENTS name credited for a classified direct tool use. */
|
|
9
|
+
export type ExplorePhase = "explore-codebase" | "research-expert";
|
|
10
|
+
|
|
11
|
+
/** Outcome of classifying one direct tool use. */
|
|
12
|
+
export interface ExploreHit {
|
|
13
|
+
/** APEX phase credited (a REQUIRED_AGENTS name). */
|
|
14
|
+
phase: ExplorePhase;
|
|
15
|
+
/** Reads of a cached MCP result count as `sufficient` regardless of length. */
|
|
16
|
+
cacheHit: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Native exploration tools (parity `apex_constants.EXPLORE_TOOLS`). */
|
|
20
|
+
const EXPLORE_TOOLS = new Set(["Glob", "Grep"]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Research tools — MCP docs + web (parity `apex_constants.RESEARCH_TOOLS`).
|
|
24
|
+
* The fuse-browser fast-path entries are a deliberate TS addition: CLAUDE.md
|
|
25
|
+
* mandates fuse-browser FIRST for web research, and `docSourceOf` (activity.ts)
|
|
26
|
+
* already credits it for doc consultation — without them here, an agent
|
|
27
|
+
* following the rules would never satisfy the freshness gate (same
|
|
28
|
+
* cross-consumer inconsistency class as the solidReadGate lesson).
|
|
29
|
+
*/
|
|
30
|
+
const RESEARCH_TOOLS = new Set([
|
|
31
|
+
"mcp__context7__query-docs",
|
|
32
|
+
"mcp__context7__resolve-library-id",
|
|
33
|
+
"mcp__exa__web_search_exa",
|
|
34
|
+
"mcp__exa__get_code_context_exa",
|
|
35
|
+
"mcp__exa__deep_researcher_start",
|
|
36
|
+
"WebSearch",
|
|
37
|
+
"WebFetch",
|
|
38
|
+
"FetchURL", // Kimi Code CLI's WebFetch-equivalent tool name (docs/reference/tools.html)
|
|
39
|
+
"mcp__fuse-browser__browser_fetch",
|
|
40
|
+
"mcp__fuse-browser__browser_fetch_batch",
|
|
41
|
+
"mcp__fuse-browser__browser_crawl",
|
|
42
|
+
"mcp__fuse-browser__browser_serp_batch",
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
/** Bash executables that count as exploration (parity `EXPLORE_BASH_CMDS`). */
|
|
46
|
+
const EXPLORE_BASH_CMDS = new Set(["grep", "rg", "find", "ls", "fd", "ast-grep", "tree", "cat", "head", "tail"]);
|
|
47
|
+
|
|
48
|
+
/** Legacy Python cache names still credited (parity `CACHE_READ_RE` + doc-helpers). */
|
|
49
|
+
const CACHE_READ_RE = /\/context\/mcp\/(exa-search|exa-code-context|context7)-/;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Real TS cache stores: `<root>/.harness/cache/<fnv16>.md` (core MCP store,
|
|
53
|
+
* `projectLayout().cacheDir`) and `~/.fuse-harness/cache/**` (ai-pilot doc
|
|
54
|
+
* caches, `cache-base.cacheBaseDir`). Segment-matched — not resolved against
|
|
55
|
+
* `homedir()` — so tilde-prefixed and absolute paths both hit; the `.md`
|
|
56
|
+
* suffix keeps session-state JSON reads from counting as research.
|
|
57
|
+
*/
|
|
58
|
+
const TS_CACHE_READ_RE = /\.(?:fuse-)?harness[\\/]cache[\\/].*\.md$/;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* First non-assignment shell token's basename, or "" — mirrors Python
|
|
62
|
+
* `_bash_executable` (skips leading `VAR=value` env prefixes, e.g. `FOO=1 grep`).
|
|
63
|
+
* @param cmd - Raw Bash `command` string.
|
|
64
|
+
* @returns The executable basename, or "" when none.
|
|
65
|
+
*/
|
|
66
|
+
function bashExecutable(cmd: string): string {
|
|
67
|
+
for (const token of cmd.trim().split(/\s+/)) {
|
|
68
|
+
if (!token) continue;
|
|
69
|
+
const last = token.split("/").pop() ?? token;
|
|
70
|
+
if (!last.includes("=")) return last;
|
|
71
|
+
}
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Classify a direct tool use into an APEX phase, or `null` when it is neither
|
|
77
|
+
* exploration nor research. Mirrors Python `track-subagent-research._classify`.
|
|
78
|
+
* @param tool - Harness tool name (e.g. "Glob", "Bash", "WebSearch").
|
|
79
|
+
* @param input - Tool input payload.
|
|
80
|
+
* @returns The credited phase + cache flag, or `null`.
|
|
81
|
+
*/
|
|
82
|
+
export function classifyExplore(tool: string, input: Record<string, unknown> | undefined): ExploreHit | null {
|
|
83
|
+
if (RESEARCH_TOOLS.has(tool)) return { phase: "research-expert", cacheHit: false };
|
|
84
|
+
if (EXPLORE_TOOLS.has(tool)) return { phase: "explore-codebase", cacheHit: false };
|
|
85
|
+
if (tool === "Read") {
|
|
86
|
+
const path = String(input?.file_path ?? input?.path ?? "");
|
|
87
|
+
if (path && (CACHE_READ_RE.test(path) || TS_CACHE_READ_RE.test(path))) return { phase: "research-expert", cacheHit: true };
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if (tool === "Bash") {
|
|
91
|
+
const cmd = String(input?.command ?? "").trim();
|
|
92
|
+
if (EXPLORE_BASH_CMDS.has(bashExecutable(cmd))) return { phase: "explore-codebase", cacheHit: false };
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework classification for doc consultations, keyed on the QUERY text
|
|
3
|
+
* (parity ai-pilot/scripts/track_doc_helpers.py::detect_framework): an MCP
|
|
4
|
+
* doc call (context7/exa/web) has no filePath, so the file-based
|
|
5
|
+
* `detectFramework()` always yields "generic" — the query keywords are the
|
|
6
|
+
* only framework signal a doc consultation carries.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Ordered patterns, EXACT parity with the Python list (same order, same
|
|
11
|
+
* case-sensitive alternations, substring search — only `go` is word-bounded).
|
|
12
|
+
* Quirks are inherited deliberately: "javascript" hits `java` (but only after
|
|
13
|
+
* nextjs/react/laravel/swift/tailwind all missed) and all-caps "NEXT" misses.
|
|
14
|
+
* The output domain matches `detectFramework()` exactly, so every credit
|
|
15
|
+
* lands on a key the apex authorization gate actually reads.
|
|
16
|
+
*/
|
|
17
|
+
const QUERY_FRAMEWORKS: ReadonlyArray<readonly [RegExp, string]> = [
|
|
18
|
+
[/(next|nextjs|Next)/, "nextjs"],
|
|
19
|
+
[/(react|React)/, "react"],
|
|
20
|
+
[/(laravel|Laravel|php|PHP)/, "laravel"],
|
|
21
|
+
[/(swift|Swift|swiftui|SwiftUI)/, "swift"],
|
|
22
|
+
[/(tailwind|Tailwind)/, "tailwind"],
|
|
23
|
+
[/(java|Java|spring|Spring)/, "java"],
|
|
24
|
+
[/\b(go|Go|golang)\b/, "go"],
|
|
25
|
+
[/(ruby|Ruby|rails|Rails)/, "ruby"],
|
|
26
|
+
[/(rust|Rust|cargo|Cargo)/, "rust"],
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The framework a doc query targets, or null when no keyword matches (the
|
|
31
|
+
* Python returns "generic"; null here lets the caller fall back to the
|
|
32
|
+
* event's file-detected framework instead of hardcoding generic).
|
|
33
|
+
* @param query - The query/url/prompt text of the doc tool call.
|
|
34
|
+
*/
|
|
35
|
+
export function frameworkFromQuery(query: string): string | null {
|
|
36
|
+
for (const [re, fw] of QUERY_FRAMEWORKS) if (re.test(query)) return fw;
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* tool_input fields carrying query text across doc tools: context7
|
|
42
|
+
* (`libraryId`/`libraryName`, parity extract_tool_info), exa/WebSearch
|
|
43
|
+
* (`query`), fuse-browser serp (`queries[]`), WebFetch (`url` + `prompt`).
|
|
44
|
+
*/
|
|
45
|
+
const QUERY_FIELDS = ["query", "queries", "libraryId", "libraryName", "prompt", "url"] as const;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Fold every query-ish tool_input field into one searchable string. Distinct
|
|
49
|
+
* from mcp-key.ts `queryOf`, which picks a SINGLE field to key the response
|
|
50
|
+
* cache — classification wants ALL the text (e.g. WebFetch url + prompt).
|
|
51
|
+
* @param input - The normalized tool_input (absent on some harnesses).
|
|
52
|
+
*/
|
|
53
|
+
export function docQueryOf(input: Record<string, unknown> | undefined): string {
|
|
54
|
+
if (!input) return "";
|
|
55
|
+
const parts: string[] = [];
|
|
56
|
+
for (const key of QUERY_FIELDS) {
|
|
57
|
+
const v = input[key];
|
|
58
|
+
if (typeof v === "string") parts.push(v);
|
|
59
|
+
else if (Array.isArray(v)) for (const s of v) if (typeof s === "string") parts.push(s);
|
|
60
|
+
}
|
|
61
|
+
return parts.join(" ");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The framework to credit for a doc consultation: the query's framework when
|
|
66
|
+
* a keyword matches (query wins over file-based detection), else `fallback`
|
|
67
|
+
* (the event's framework — "generic" for pathless MCP calls).
|
|
68
|
+
* @param input - The doc tool's tool_input.
|
|
69
|
+
* @param fallback - The event's file-detected framework.
|
|
70
|
+
*/
|
|
71
|
+
export function docFramework(input: Record<string, unknown> | undefined, fallback: string): string {
|
|
72
|
+
return frameworkFromQuery(docQueryOf(input)) ?? fallback;
|
|
73
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-transcript reconciliation for `.md` reference reads — the durable
|
|
3
|
+
* counterpart to {@link agentsRanFromTranscript} for agent freshness.
|
|
4
|
+
*
|
|
5
|
+
* WHY: the session track is persisted by a non-atomic load→mutate→save. Under
|
|
6
|
+
* the multi-plugin hook fan-out (one hook process per installed plugin, ×N) plus
|
|
7
|
+
* back-to-back tool events, concurrent writers clobber each other (lost update).
|
|
8
|
+
* `agents`/`authorizations` self-heal — every explore/research/doc call rewrites
|
|
9
|
+
* them, so a lost update lands again on the next of hundreds of writes — but a
|
|
10
|
+
* `refsRead` entry is written ONCE (when the `.md` is Read), so a single lost
|
|
11
|
+
* update erases it permanently, and the lead has no SubagentStop
|
|
12
|
+
* {@link harvestAgentEvidence} pass to reconcile it (sub-agents do, which is why
|
|
13
|
+
* only the LEAD's solidReadGate never credited). The Claude-authored transcript
|
|
14
|
+
* is append-only and race-immune, so folding its `.md` Reads back into the track
|
|
15
|
+
* restores the lost evidence; each gate still applies its own TTL/session policy.
|
|
16
|
+
*/
|
|
17
|
+
import { readAgentToolUses } from "../runtime/lifecycle/agent-transcript";
|
|
18
|
+
import { recordRefRead, type SessionTrack } from "../tracking/session-state";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Fold every `.md` `Read` in the transcript into `track` as a timestamped ref
|
|
22
|
+
* read (immutably). PURE reconciliation — the caller owns the track; each read
|
|
23
|
+
* is stamped with its transcript timestamp (unstamped → `now`, lenient, matching
|
|
24
|
+
* {@link agentsRanFromTranscript}), and an existing MORE-recent stamp is never
|
|
25
|
+
* rolled back. Fail-open: an absent/unreadable transcript returns `track`
|
|
26
|
+
* unchanged (same reference).
|
|
27
|
+
* @param track - The current (possibly race-damaged) session track.
|
|
28
|
+
* @param transcriptPath - Claude `transcript_path` for this session.
|
|
29
|
+
* @param now - Fallback epoch-ms for transcript entries the platform left unstamped.
|
|
30
|
+
* @returns The track with transcript `.md` reads merged into `refsRead`/`refsReadAt`.
|
|
31
|
+
*/
|
|
32
|
+
export function reconcileRefReadsFromTranscript(
|
|
33
|
+
track: SessionTrack,
|
|
34
|
+
transcriptPath: string | undefined,
|
|
35
|
+
now: number,
|
|
36
|
+
): SessionTrack {
|
|
37
|
+
const uses = readAgentToolUses(transcriptPath);
|
|
38
|
+
if (!uses) return track; // unreadable → unchanged (no regression)
|
|
39
|
+
let next = track;
|
|
40
|
+
for (const u of uses) {
|
|
41
|
+
if (u.name !== "Read") continue;
|
|
42
|
+
const path = String(u.input?.file_path ?? u.input?.path ?? "");
|
|
43
|
+
if (!path.endsWith(".md")) continue;
|
|
44
|
+
const ts = u.ts ?? now;
|
|
45
|
+
const prev = next.refsReadAt?.[path];
|
|
46
|
+
if (prev === undefined || prev < ts) next = recordRefRead(next, path, ts);
|
|
47
|
+
}
|
|
48
|
+
return next;
|
|
49
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-only journal of `.md` reference reads — the FRESH, race-immune companion
|
|
3
|
+
* to {@link reconcileRefReadsFromTranscript}.
|
|
4
|
+
*
|
|
5
|
+
* WHY a third source (track + transcript were not enough for teammates): the live
|
|
6
|
+
* session track write is a non-atomic load→mutate→save, so under the multi-plugin
|
|
7
|
+
* hook fan-out (one process per plugin, ×N) a lone `refsRead` write is clobbered
|
|
8
|
+
* (lost update). The transcript reconcile recovers the LEAD's lost reads because,
|
|
9
|
+
* by the time the lead edits, its Read has flushed to the platform transcript. But
|
|
10
|
+
* the platform flushes that JSONL to disk with a MULTI-MINUTE lag (measured ~230s,
|
|
11
|
+
* well past the 120s freshness TTL): a background TEAMMATE reads the exact listed
|
|
12
|
+
* ref then edits within seconds — far faster than the flush — so the transcript on
|
|
13
|
+
* disk does NOT yet contain the teammate's read, reconcile misses it, and (its live
|
|
14
|
+
* track write having been lost to the fan-out) solidReadGate blocks despite a
|
|
15
|
+
* genuine read. That is the "teammate solidRead" gap.
|
|
16
|
+
*
|
|
17
|
+
* This journal closes it: every credited `.md` read is appended (one JSON line) the
|
|
18
|
+
* instant PostToolUse fires — O_APPEND is per-write atomic, so concurrent fan-out
|
|
19
|
+
* processes each add their own line and none is lost, and there is no flush lag. The
|
|
20
|
+
* gate folds it back BEFORE any refsRead consumer, alongside the transcript.
|
|
21
|
+
*
|
|
22
|
+
* ANTI-FORGERY: it lives in the out-of-tree state dir the protected-path guard
|
|
23
|
+
* denies agents from writing (SAME boundary as the signed track); the only writer is
|
|
24
|
+
* our PostToolUse on a real `.md` Read. COST: the gate reads it once per edit, bounded
|
|
25
|
+
* by {@link appendRefRead}'s trim cap — cheaper than the multi-MB transcript parse.
|
|
26
|
+
*/
|
|
27
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
28
|
+
import { join } from "node:path";
|
|
29
|
+
import { readText } from "../util/runtime-io";
|
|
30
|
+
import { trimLogFile } from "../runtime/fs-cleanup";
|
|
31
|
+
import { recordRefRead, type SessionTrack } from "../tracking/session-state";
|
|
32
|
+
|
|
33
|
+
/** Journal filename inside the per-session state dir. */
|
|
34
|
+
const JOURNAL = "refs-read.log";
|
|
35
|
+
|
|
36
|
+
/** One journalled read: `.md` path plus the tool event's epoch-ms timestamp. */
|
|
37
|
+
interface RefReadLine {
|
|
38
|
+
p: string;
|
|
39
|
+
t: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Append a credited `.md` read to the state-dir journal (O_APPEND, atomic under
|
|
44
|
+
* the fan-out). No-op for non-`.md` paths (parity with the reconcile filter).
|
|
45
|
+
* Fully fail-open: a mkdir/append error is swallowed so recording never blocks the
|
|
46
|
+
* PostToolUse path.
|
|
47
|
+
* @param dir - Per-session state dir (`dirname(trackFile)`).
|
|
48
|
+
* @param path - The read file's absolute path.
|
|
49
|
+
* @param ts - The tool event's epoch-ms timestamp.
|
|
50
|
+
*/
|
|
51
|
+
export function appendRefRead(dir: string, path: string, ts: number): void {
|
|
52
|
+
if (!path.endsWith(".md")) return;
|
|
53
|
+
try {
|
|
54
|
+
mkdirSync(dir, { recursive: true });
|
|
55
|
+
const file = join(dir, JOURNAL);
|
|
56
|
+
appendFileSync(file, JSON.stringify({ p: path, t: ts } satisfies RefReadLine) + "\n", "utf-8");
|
|
57
|
+
trimLogFile(file, 128 * 1024, 1_000); // bound the SHARED cross-session log: keep the recent tail (only fresh reads credit a gate)
|
|
58
|
+
} catch {
|
|
59
|
+
/* fail-open: a missing dir or write error must never break recording */
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Fold every `.md` read in the state-dir journal into `track` (immutably), each
|
|
65
|
+
* stamped with its journalled timestamp and never rolling back a MORE-recent
|
|
66
|
+
* existing stamp — identical merge semantics to
|
|
67
|
+
* {@link reconcileRefReadsFromTranscript}, but from the fresh append-only journal
|
|
68
|
+
* instead of the lagged transcript. Fail-open: an absent/unreadable journal returns
|
|
69
|
+
* `track` unchanged (same reference).
|
|
70
|
+
* @param track - The current (possibly race-damaged) session track.
|
|
71
|
+
* @param dir - Per-session state dir (`dirname(trackFile)`).
|
|
72
|
+
* @param now - Fallback epoch-ms for entries with an invalid timestamp.
|
|
73
|
+
* @returns The track with journalled `.md` reads merged into `refsRead`/`refsReadAt`.
|
|
74
|
+
*/
|
|
75
|
+
export function reconcileRefReadsFromJournal(track: SessionTrack, dir: string, now: number): SessionTrack {
|
|
76
|
+
let text: string;
|
|
77
|
+
try {
|
|
78
|
+
text = readText(join(dir, JOURNAL));
|
|
79
|
+
} catch {
|
|
80
|
+
return track; // unreadable → unchanged (no regression)
|
|
81
|
+
}
|
|
82
|
+
let next = track;
|
|
83
|
+
for (const line of text.split("\n")) {
|
|
84
|
+
if (!line.trim()) continue;
|
|
85
|
+
let entry: RefReadLine;
|
|
86
|
+
try {
|
|
87
|
+
entry = JSON.parse(line) as RefReadLine;
|
|
88
|
+
} catch {
|
|
89
|
+
continue; // tolerate malformed lines
|
|
90
|
+
}
|
|
91
|
+
const path = typeof entry.p === "string" ? entry.p : "";
|
|
92
|
+
if (!path.endsWith(".md")) continue;
|
|
93
|
+
const ts = typeof entry.t === "number" && Number.isFinite(entry.t) ? entry.t : now;
|
|
94
|
+
const prev = next.refsReadAt?.[path];
|
|
95
|
+
if (prev === undefined || prev < ts) next = recordRefRead(next, path, ts);
|
|
96
|
+
}
|
|
97
|
+
return next;
|
|
98
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { dirname } from "node:path";
|
|
2
|
+
import { ensureDir, readJsonFile, writeJsonFile } from "../util/json-io";
|
|
3
|
+
|
|
4
|
+
interface TrivialEditState {
|
|
5
|
+
trivial_edits?: number[];
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Increment a session's trivial-edit counter, evicting timestamps older than
|
|
11
|
+
* `windowMs`. Decoupled + injectable `now` for testability.
|
|
12
|
+
* @param filePath - session state file path
|
|
13
|
+
* @param windowMs - sliding window in ms
|
|
14
|
+
* @param now - current epoch ms (defaults to `Date.now()`)
|
|
15
|
+
* @returns number of trivial edits within the window (including this one)
|
|
16
|
+
*/
|
|
17
|
+
export async function incrementTrivialEditCounter(
|
|
18
|
+
filePath: string,
|
|
19
|
+
windowMs: number,
|
|
20
|
+
now: number = Date.now(),
|
|
21
|
+
): Promise<number> {
|
|
22
|
+
await ensureDir(dirname(filePath));
|
|
23
|
+
const state: TrivialEditState = (await readJsonFile<TrivialEditState>(filePath)) ?? {};
|
|
24
|
+
const cutoff = now - windowMs;
|
|
25
|
+
const edits = (state.trivial_edits ?? []).filter((ts) => ts > cutoff);
|
|
26
|
+
edits.push(now);
|
|
27
|
+
state.trivial_edits = edits;
|
|
28
|
+
await writeJsonFile(filePath, state);
|
|
29
|
+
return edits.length;
|
|
30
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** @fusengine/harness - harness-agnostic toolkit for AI coding agents. */
|
|
2
|
+
export * from "./config";
|
|
3
|
+
export * from "./util";
|
|
4
|
+
export * from "./detect";
|
|
5
|
+
export * from "./policy";
|
|
6
|
+
export * from "./prompt";
|
|
7
|
+
export * from "./memory";
|
|
8
|
+
export * from "./cache";
|
|
9
|
+
export * from "./freshness";
|
|
10
|
+
export * from "./refs";
|
|
11
|
+
export * from "./statusline";
|
package/src/init/run.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import type { HarnessId } from "../detect/harness";
|
|
4
|
+
import { claudeInit, clineInit, codexInit, cursorInit, geminiInit, type InitFile } from "./templates";
|
|
5
|
+
import { STATE_GITIGNORE, STATE_ROOT } from "../config/layout";
|
|
6
|
+
|
|
7
|
+
const RUNNERS: Partial<Record<HarnessId, (command: string) => InitFile[]>> = {
|
|
8
|
+
"claude-code": claudeInit,
|
|
9
|
+
codex: codexInit,
|
|
10
|
+
cursor: cursorInit,
|
|
11
|
+
"gemini-cli": geminiInit,
|
|
12
|
+
cline: clineInit,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Build the wiring file(s) for a harness, or null when it has no hook
|
|
17
|
+
* integration (cli-mode harnesses use `harness check` in a pre-commit step).
|
|
18
|
+
*/
|
|
19
|
+
export function initFor(id: HarnessId, command: string = `npx -y @fusengine/harness hook ${id}`): InitFile[] | null {
|
|
20
|
+
const make = RUNNERS[id];
|
|
21
|
+
if (!make) return null;
|
|
22
|
+
return [...make(command), { path: `${STATE_ROOT}/.gitignore`, content: STATE_GITIGNORE }];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Write an {@link InitFile} under `root` (creates dirs; `chmod +x` when executable). */
|
|
26
|
+
export function writeInitFile(root: string, file: InitFile): string {
|
|
27
|
+
const full = join(root, file.path);
|
|
28
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
29
|
+
writeFileSync(full, file.content);
|
|
30
|
+
if (file.executable) chmodSync(full, 0o755);
|
|
31
|
+
return full;
|
|
32
|
+
}
|