@fusengine/harness 0.1.89 → 0.1.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude/index.d.mts +1 -1
- package/dist/adapters/claude/index.mjs +1 -1
- package/dist/adapters/cline/index.mjs +1 -1
- package/dist/adapters/codex/index.d.mts +2 -2
- package/dist/adapters/codex/index.mjs +5 -4
- package/dist/adapters/cursor/index.d.mts +15 -28
- package/dist/adapters/cursor/index.mjs +35 -46
- package/dist/adapters/gemini/index.mjs +1 -1
- package/dist/adapters/hermes/index.d.mts +1 -1
- package/dist/adapters/hermes/index.mjs +1 -1
- package/dist/adapters/kimi/index.d.mts +1 -1
- package/dist/adapters/kimi/index.mjs +1 -1
- package/dist/{claude-5bC8TDAz.mjs → claude-Ckv2_TgP.mjs} +3 -43
- package/dist/cli/bin.mjs +429 -16
- package/dist/cli/index.mjs +1 -1
- package/dist/command-string-CALMTnwN.mjs +42 -0
- package/dist/{evaluate-C1VSI5pb.mjs → evaluate-CMiIqHeH.mjs} +261 -13
- package/dist/{handle-DHhukRdh.mjs → handle-C43gA-Pr.mjs} +737 -150
- package/dist/{hermes-kP6NUQlG.mjs → hermes-B9-p_3IF.mjs} +3 -2
- package/dist/{index-DBgnIZn9.d.mts → index-B8PG82su.d.mts} +1 -1
- package/dist/{index-D4C2-IIc.d.mts → index-SmKYkk2N.d.mts} +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/init/index.d.mts +1 -1
- package/dist/init/index.mjs +1 -1
- package/dist/{kimi-D6X7b8wF.mjs → kimi-G2wcSh5-.mjs} +3 -2
- package/dist/normalize-BjG6unTj.mjs +317 -0
- package/dist/permission-mode-B-SFyR0X.mjs +226 -0
- package/dist/policy/index.d.mts +1 -1
- package/dist/policy/index.mjs +2 -2
- package/dist/prompt/index.d.mts +1 -1
- package/dist/{run-CdIlUtjv.mjs → run-DkrzC0gb.mjs} +1 -1
- package/dist/{run-TAXNRSpD.mjs → run-GHAMqcOn.mjs} +26 -3
- package/dist/runtime/index.d.mts +27 -16
- package/dist/runtime/index.mjs +2 -2
- package/dist/{types-C0-igRKr.d.mts → types-Bh0jEF3_.d.mts} +2 -0
- package/dist/{validate-DZFtAbLP.mjs → validate-KjZ1X9tH.mjs} +1 -1
- package/package.json +3 -2
- package/src/adapters/claude/index.ts +92 -0
- package/src/adapters/cline/index.ts +36 -0
- package/src/adapters/codex/apply-patch.ts +94 -0
- package/src/adapters/codex/index.ts +84 -0
- package/src/adapters/codex/permission-mode.ts +16 -0
- package/src/adapters/cursor/context.ts +46 -0
- package/src/adapters/cursor/events.ts +38 -0
- package/src/adapters/cursor/index.ts +43 -0
- package/src/adapters/cursor/interfaces/types.ts +88 -0
- package/src/adapters/cursor/native-response.ts +158 -0
- package/src/adapters/cursor/normalize.ts +123 -0
- package/src/adapters/cursor/respond.ts +123 -0
- package/src/adapters/gemini/index.ts +36 -0
- package/src/adapters/hermes/index.ts +55 -0
- package/src/adapters/hermes/interfaces/types.ts +26 -0
- package/src/adapters/kimi/index.ts +96 -0
- package/src/adapters/kimi/interfaces/types.ts +55 -0
- package/src/cache/compact.ts +55 -0
- package/src/cache/index.ts +5 -0
- package/src/cache/io.ts +38 -0
- package/src/cache/mcp-response.ts +39 -0
- package/src/cache/mcp-store.ts +70 -0
- package/src/cache/store.ts +93 -0
- package/src/changelog/fetch.ts +79 -0
- package/src/cli/bin.ts +100 -0
- package/src/cli/cursor-event-scanner.ts +175 -0
- package/src/cli/cursor-stdin-reader.ts +65 -0
- package/src/cli/doctor.ts +96 -0
- package/src/cli/hook-io.ts +130 -0
- package/src/cli/hook-sound.ts +49 -0
- package/src/cli/index.ts +1 -0
- package/src/cli/json-primitive-scanner.ts +83 -0
- package/src/cli/run.ts +36 -0
- package/src/cli/scope.ts +31 -0
- package/src/codex-rules/build.ts +63 -0
- package/src/codex-rules/index.ts +8 -0
- package/src/codex-rules/mapping.ts +42 -0
- package/src/codex-rules/rules/absolute-paths.ts +52 -0
- package/src/codex-rules/rules/ask.ts +44 -0
- package/src/codex-rules/rules/chmod-ask.ts +57 -0
- package/src/codex-rules/rules/dd.ts +23 -0
- package/src/codex-rules/rules/diskutil.ts +33 -0
- package/src/codex-rules/rules/mkfs.ts +42 -0
- package/src/codex-rules/rules/privilege.ts +49 -0
- package/src/codex-rules/rules/remote-fetch.ts +31 -0
- package/src/codex-rules/rules/rm-variants.ts +58 -0
- package/src/codex-rules/rules/single-commands.ts +57 -0
- package/src/codex-rules/skip-list.ts +25 -0
- package/src/codex-rules/starlark.ts +46 -0
- package/src/codex-rules/types.ts +36 -0
- package/src/config/dotenv.ts +55 -0
- package/src/config/env.ts +10 -0
- package/src/config/home-dir.ts +36 -0
- package/src/config/index.ts +5 -0
- package/src/config/layout.ts +52 -0
- package/src/config/limits.ts +56 -0
- package/src/config/ttl.ts +30 -0
- package/src/detect/harness.ts +73 -0
- package/src/detect/index.ts +1 -0
- package/src/detect/interfaces/types.ts +18 -0
- package/src/freshness/agent-evidence-record.ts +95 -0
- package/src/freshness/agent-evidence.ts +95 -0
- package/src/freshness/codex-spawn-evidence.ts +91 -0
- package/src/freshness/doc-helpers.ts +90 -0
- package/src/freshness/evidence-harvest-io.ts +72 -0
- package/src/freshness/evidence-harvest.ts +61 -0
- package/src/freshness/explore-tools.ts +95 -0
- package/src/freshness/index.ts +2 -0
- package/src/freshness/query-framework.ts +73 -0
- package/src/freshness/ref-evidence.ts +49 -0
- package/src/freshness/ref-journal.ts +98 -0
- package/src/freshness/trivial-edit-counter.ts +30 -0
- package/src/index.ts +11 -0
- package/src/init/index.ts +2 -0
- package/src/init/run.ts +32 -0
- package/src/init/templates.ts +75 -0
- package/src/memory/gitignore.ts +14 -0
- package/src/memory/index.ts +3 -0
- package/src/memory/registry.ts +39 -0
- package/src/memory/session-roots.ts +99 -0
- package/src/memory/state.ts +58 -0
- package/src/policy/apex-authorization.ts +100 -0
- package/src/policy/apex-gates.ts +91 -0
- package/src/policy/apex-target-steps.ts +73 -0
- package/src/policy/apex-target.ts +49 -0
- package/src/policy/apex-task-context.ts +81 -0
- package/src/policy/apex.ts +94 -0
- package/src/policy/cartographer/build-tree.ts +69 -0
- package/src/policy/cartographer/describe.ts +53 -0
- package/src/policy/cartographer/entry.ts +38 -0
- package/src/policy/cartographer/frontmatter.ts +48 -0
- package/src/policy/cartographer/indicators.ts +34 -0
- package/src/policy/claude-md-context.ts +66 -0
- package/src/policy/conventions/index.ts +11 -0
- package/src/policy/conventions/interfaces.ts +71 -0
- package/src/policy/conventions/langs.ts +53 -0
- package/src/policy/conventions/query.ts +49 -0
- package/src/policy/conventions/react-hooks.ts +18 -0
- package/src/policy/conventions/store-or-query.ts +21 -0
- package/src/policy/conventions/stores.ts +56 -0
- package/src/policy/conventions/strip.ts +87 -0
- package/src/policy/conventions/verdict.ts +38 -0
- package/src/policy/creation-intent.ts +11 -0
- package/src/policy/deny-loop.ts +88 -0
- package/src/policy/design/allowed-write.ts +28 -0
- package/src/policy/design/content-checks.ts +45 -0
- package/src/policy/design/corpus-fs.ts +21 -0
- package/src/policy/design/corpus-resolve-cache.ts +49 -0
- package/src/policy/design/corpus-resolve.ts +99 -0
- package/src/policy/design/corpus.ts +92 -0
- package/src/policy/design/design-system-rules.ts +45 -0
- package/src/policy/design/flag.ts +30 -0
- package/src/policy/design/gates-pipeline.ts +105 -0
- package/src/policy/design/gates.ts +87 -0
- package/src/policy/design/screenshot-tools.ts +24 -0
- package/src/policy/design/skill-evidence.ts +34 -0
- package/src/policy/design/skill-gate.ts +81 -0
- package/src/policy/design/skill-triggers.ts +51 -0
- package/src/policy/design/state.ts +92 -0
- package/src/policy/design/template-urls.ts +54 -0
- package/src/policy/design/transitions.ts +68 -0
- package/src/policy/detect-claude-md-project-type.ts +32 -0
- package/src/policy/detect-framework.ts +99 -0
- package/src/policy/detect-primitive-checks.ts +100 -0
- package/src/policy/detect-primitive-lib.ts +55 -0
- package/src/policy/detect-project.ts +99 -0
- package/src/policy/edit-outcome.ts +53 -0
- package/src/policy/evaluate.ts +81 -0
- package/src/policy/expert-agents.ts +60 -0
- package/src/policy/file-size-scope.ts +32 -0
- package/src/policy/file-size.ts +99 -0
- package/src/policy/framework-solid-exclude.ts +52 -0
- package/src/policy/framework-solid-extended.ts +93 -0
- package/src/policy/framework-solid-gates-systems.ts +99 -0
- package/src/policy/framework-solid-gates.ts +98 -0
- package/src/policy/framework-solid.ts +88 -0
- package/src/policy/gemini-mcp-gate.ts +83 -0
- package/src/policy/git-gates.ts +40 -0
- package/src/policy/guards/bash-command-anchor.ts +40 -0
- package/src/policy/guards/bash-write-patterns.ts +140 -0
- package/src/policy/guards/bash-write-redirects.ts +201 -0
- package/src/policy/guards/bash-write-safe-paths.ts +62 -0
- package/src/policy/guards/bash-write.ts +76 -0
- package/src/policy/guards/context.ts +12 -0
- package/src/policy/guards/index.ts +61 -0
- package/src/policy/guards/install.ts +22 -0
- package/src/policy/guards/interface-separation-ext.ts +41 -0
- package/src/policy/guards/interface-separation.ts +100 -0
- package/src/policy/guards/protected-path.ts +100 -0
- package/src/policy/guards/security.ts +97 -0
- package/src/policy/index.ts +19 -0
- package/src/policy/interfaces/types.ts +46 -0
- package/src/policy/js-gate-route.ts +51 -0
- package/src/policy/lessons/lesson-gate.ts +0 -0
- package/src/policy/lessons/trigger-index.ts +84 -0
- package/src/policy/lessons/types.ts +31 -0
- package/src/policy/module-layout.ts +31 -0
- package/src/policy/nearest-manifest.ts +75 -0
- package/src/policy/never-approval.ts +95 -0
- package/src/policy/patterns.ts +80 -0
- package/src/policy/seo/validate.ts +51 -0
- package/src/policy/shadcn-project.ts +22 -0
- package/src/policy/shadcn-skill-gate.ts +97 -0
- package/src/policy/shell-read-refs.ts +96 -0
- package/src/policy/skill-path.ts +46 -0
- package/src/policy/skill-patterns/laravel.ts +40 -0
- package/src/policy/skill-patterns/nextjs.ts +29 -0
- package/src/policy/skill-patterns/react.ts +27 -0
- package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
- package/src/policy/skill-patterns/shadcn.ts +66 -0
- package/src/policy/skill-patterns/swift.ts +30 -0
- package/src/policy/skill-patterns/tailwind.ts +40 -0
- package/src/policy/skill-source.ts +52 -0
- package/src/policy/skill-trigger-patterns.ts +33 -0
- package/src/policy/skill-triggers.ts +108 -0
- package/src/policy/tailwind-skill-gate.ts +63 -0
- package/src/policy/trivial-edits.ts +52 -0
- package/src/policy/verbosity.ts +39 -0
- package/src/prompt/index.ts +1 -0
- package/src/prompt/types.ts +36 -0
- package/src/refs/discover.ts +69 -0
- package/src/refs/frontmatter.ts +24 -0
- package/src/refs/index.ts +4 -0
- package/src/refs/loader.ts +59 -0
- package/src/refs/ref-key.ts +41 -0
- package/src/refs/router.ts +63 -0
- package/src/refs/types.ts +25 -0
- package/src/runtime/activity.ts +99 -0
- package/src/runtime/apply-patch-gate.ts +41 -0
- package/src/runtime/burst-window.ts +21 -0
- package/src/runtime/codex-shell-tool.ts +37 -0
- package/src/runtime/command-string.ts +37 -0
- package/src/runtime/confirm/codex-confirm.ts +126 -0
- package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
- package/src/runtime/confirm/confirm-code.ts +33 -0
- package/src/runtime/confirm/confirm-gate.ts +65 -0
- package/src/runtime/confirm/confirm-irreversible.ts +32 -0
- package/src/runtime/confirm/confirm-pending.ts +34 -0
- package/src/runtime/confirm/confirm-state.ts +56 -0
- package/src/runtime/confirm/confirm-subagent.ts +112 -0
- package/src/runtime/confirm/confirm-submit.ts +38 -0
- package/src/runtime/core-guards-active.ts +84 -0
- package/src/runtime/core-guards-wired.ts +47 -0
- package/src/runtime/deny-loop-store.ts +94 -0
- package/src/runtime/deny-notice.ts +52 -0
- package/src/runtime/design-cache-resolve.ts +51 -0
- package/src/runtime/design-content-gate.ts +95 -0
- package/src/runtime/design-files-gate.ts +114 -0
- package/src/runtime/design-helpers.ts +93 -0
- package/src/runtime/design-lifecycle.ts +39 -0
- package/src/runtime/design-read-credit.ts +28 -0
- package/src/runtime/design.ts +116 -0
- package/src/runtime/dev-context.ts +43 -0
- package/src/runtime/dry-patterns.ts +32 -0
- package/src/runtime/dry.ts +96 -0
- package/src/runtime/fragment-registry.ts +33 -0
- package/src/runtime/framework-skill-gate.ts +79 -0
- package/src/runtime/fs-cleanup.ts +71 -0
- package/src/runtime/gate-apex.ts +81 -0
- package/src/runtime/gate-helpers.ts +52 -0
- package/src/runtime/gate-input.ts +26 -0
- package/src/runtime/gate.ts +128 -0
- package/src/runtime/handle-post.ts +104 -0
- package/src/runtime/handle-pre.ts +137 -0
- package/src/runtime/handle-scope-async.ts +29 -0
- package/src/runtime/handle-types.ts +27 -0
- package/src/runtime/handle.ts +116 -0
- package/src/runtime/home-state.ts +55 -0
- package/src/runtime/index.ts +18 -0
- package/src/runtime/inform.ts +28 -0
- package/src/runtime/inject-budget-recap.ts +47 -0
- package/src/runtime/inject-budget.ts +81 -0
- package/src/runtime/inject-context.ts +85 -0
- package/src/runtime/inject-dedup-exclusive.ts +98 -0
- package/src/runtime/inject-dedup.ts +89 -0
- package/src/runtime/is-agent-tool.ts +19 -0
- package/src/runtime/lifecycle/agent-files.ts +49 -0
- package/src/runtime/lifecycle/agent-memory.ts +82 -0
- package/src/runtime/lifecycle/agent-transcript.ts +80 -0
- package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
- package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
- package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
- package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
- package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
- package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
- package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
- package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +100 -0
- package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
- package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
- package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
- package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
- package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
- package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
- package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
- package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
- package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
- package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
- package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
- package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
- package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
- package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
- package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
- package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
- package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
- package/src/runtime/lifecycle/aipilot/types.ts +72 -0
- package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
- package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
- package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
- package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
- package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
- package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
- package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
- package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
- package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
- package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
- package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
- package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
- package/src/runtime/lifecycle/changelog-research.ts +43 -0
- package/src/runtime/lifecycle/check-file-size.ts +61 -0
- package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
- package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
- package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
- package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
- package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
- package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
- package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
- package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
- package/src/runtime/lifecycle/dispatch.ts +109 -0
- package/src/runtime/lifecycle/failure-lesson.ts +51 -0
- package/src/runtime/lifecycle/index.ts +34 -0
- package/src/runtime/lifecycle/inject-rules.ts +50 -0
- package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
- package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
- package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
- package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
- package/src/runtime/lifecycle/lessons/state.ts +23 -0
- package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
- package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
- package/src/runtime/lifecycle/memory/client.ts +65 -0
- package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
- package/src/runtime/lifecycle/memory/recall.ts +30 -0
- package/src/runtime/lifecycle/memory/salience.ts +31 -0
- package/src/runtime/lifecycle/memory/state.ts +49 -0
- package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
- package/src/runtime/lifecycle/post-compact.ts +38 -0
- package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
- package/src/runtime/lifecycle/post-tracking.ts +40 -0
- package/src/runtime/lifecycle/pre-compact.ts +38 -0
- package/src/runtime/lifecycle/rules-root.ts +83 -0
- package/src/runtime/lifecycle/security/check-skill.ts +63 -0
- package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
- package/src/runtime/lifecycle/security/scan.ts +76 -0
- package/src/runtime/lifecycle/security/skill-state.ts +47 -0
- package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
- package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
- package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
- package/src/runtime/lifecycle/session-end.ts +17 -0
- package/src/runtime/lifecycle/session-start.ts +57 -0
- package/src/runtime/lifecycle/snapshot/board.ts +27 -0
- package/src/runtime/lifecycle/snapshot/format.ts +60 -0
- package/src/runtime/lifecycle/snapshot/git.ts +71 -0
- package/src/runtime/lifecycle/snapshot/index.ts +52 -0
- package/src/runtime/lifecycle/snapshot/version.ts +34 -0
- package/src/runtime/lifecycle/solid-detect.ts +56 -0
- package/src/runtime/lifecycle/stop-core.ts +35 -0
- package/src/runtime/lifecycle/subagent-cache.ts +61 -0
- package/src/runtime/lifecycle/task-completed.ts +94 -0
- package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
- package/src/runtime/lifecycle/teammate-idle.ts +28 -0
- package/src/runtime/lifecycle/tool-failure.ts +24 -0
- package/src/runtime/lifecycle/track-changes.ts +61 -0
- package/src/runtime/lifecycle/validate-solid.ts +70 -0
- package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
- package/src/runtime/lifecycle-bridge.ts +74 -0
- package/src/runtime/mcp-key.ts +44 -0
- package/src/runtime/mcp-tool-name.ts +92 -0
- package/src/runtime/mcp.ts +91 -0
- package/src/runtime/modular.ts +77 -0
- package/src/runtime/normalize.ts +107 -0
- package/src/runtime/notices.ts +91 -0
- package/src/runtime/notification-sound.ts +49 -0
- package/src/runtime/notifications.ts +77 -0
- package/src/runtime/paths.ts +59 -0
- package/src/runtime/post-fanout.ts +41 -0
- package/src/runtime/post-outcome.ts +61 -0
- package/src/runtime/pre-allow.ts +97 -0
- package/src/runtime/precommit.ts +60 -0
- package/src/runtime/prompt-text.ts +38 -0
- package/src/runtime/receipt-capture.ts +34 -0
- package/src/runtime/record.ts +26 -0
- package/src/runtime/respond.ts +92 -0
- package/src/runtime/solid-file-size-gate.ts +58 -0
- package/src/runtime/solid-pre.ts +53 -0
- package/src/runtime/storage.ts +9 -0
- package/src/statusline/colors.ts +31 -0
- package/src/statusline/constants.ts +17 -0
- package/src/statusline/formatters.ts +54 -0
- package/src/statusline/index.ts +4 -0
- package/src/statusline/progress-bar.ts +46 -0
- package/src/tracking/codex-post-failure.ts +73 -0
- package/src/tracking/index.ts +2 -0
- package/src/tracking/integrity.ts +99 -0
- package/src/tracking/one-shot-dedup.ts +34 -0
- package/src/tracking/one-shot-failure.ts +42 -0
- package/src/tracking/one-shot-store.ts +95 -0
- package/src/tracking/one-shot.ts +93 -0
- package/src/tracking/receipts.ts +95 -0
- package/src/tracking/session-state.ts +94 -0
- package/src/tracking/store.ts +96 -0
- package/src/tracking/track-compact.ts +100 -0
- package/src/tracking/track-diff.ts +50 -0
- package/src/tracking/track-journal.ts +97 -0
- package/src/tracking/track-lock-sync.ts +83 -0
- package/src/tracking/track-lock.ts +67 -0
- package/src/util/compact-json.ts +23 -0
- package/src/util/index.ts +2 -0
- package/src/util/json-io.ts +47 -0
- package/src/util/project-root.ts +41 -0
- package/src/util/runtime-io.ts +85 -0
- package/src/util/semver.ts +30 -0
- package/dist/permission-mode-B0MUl7FT.mjs +0 -136
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the Codex execpolicy `.rules` (Starlark) generator.
|
|
3
|
+
* Verified contract (openai/codex `codex-rs/execpolicy/README.md` +
|
|
4
|
+
* learn.chatgpt.com/docs/agent-configuration/rules): `prefix_rule(pattern,
|
|
5
|
+
* decision, justification?, match?, not_match?)`; decisions are
|
|
6
|
+
* allow < prompt < forbidden (strictest wins across matching rules);
|
|
7
|
+
* `match`/`not_match` are validated as inline unit tests at file-load time.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Codex execpolicy decision severity. */
|
|
11
|
+
export type CodexDecision = "allow" | "prompt" | "forbidden";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* One argv-prefix rule, hand-mapped from a single `security.ts`
|
|
15
|
+
* `LabeledPattern`. `pattern` entries are exact argv tokens; a nested
|
|
16
|
+
* array element means "any of these tokens at this position" (Codex
|
|
17
|
+
* execpolicy alternation) — never a mechanical regex-to-prefix derivation.
|
|
18
|
+
*/
|
|
19
|
+
export interface CodexPrefixRule {
|
|
20
|
+
/** Exact `label` string from `CRITICAL_PATTERNS`/`ASK_PATTERNS` this rule maps to. */
|
|
21
|
+
sourceLabel: string;
|
|
22
|
+
/** Argv-prefix tokens; an element may be `string[]` for alternation. */
|
|
23
|
+
pattern: (string | string[])[];
|
|
24
|
+
decision: CodexDecision;
|
|
25
|
+
justification: string;
|
|
26
|
+
/** Example invocations that must satisfy this rule (validated at load time). */
|
|
27
|
+
match: string[];
|
|
28
|
+
/** Example invocations that must NOT satisfy this rule (validated at load time). */
|
|
29
|
+
notMatch?: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A `security.ts` label with no argv-prefix equivalent — named, never dropped silently. */
|
|
33
|
+
export interface CodexSkipEntry {
|
|
34
|
+
sourceLabel: string;
|
|
35
|
+
reason: string;
|
|
36
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native .env loader. Ports the claude-plugins `services/env-file.ts`
|
|
3
|
+
* (`loadEnvFile`) into the engine so a hook run hydrates `process.env` from the
|
|
4
|
+
* harness home `.env` (`~/.claude/.env`, `~/.codex/.env`, …) plus the project
|
|
5
|
+
* `<cwd>/.env`, instead of relying on Bun auto-dotenv or `BASH_ENV`. A value
|
|
6
|
+
* already present in the environment always wins (the file never overwrites it).
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { harnessHome } from "./home-dir";
|
|
12
|
+
import type { HarnessId } from "../detect/harness";
|
|
13
|
+
|
|
14
|
+
/** Home config dir holding the `.env` for each harness (defaults to `.claude`). */
|
|
15
|
+
export const HOME_DIR: Partial<Record<HarnessId, string>> = {
|
|
16
|
+
"claude-code": ".claude", codex: ".codex", cursor: ".cursor",
|
|
17
|
+
cline: ".clinerules", "gemini-cli": ".gemini", opencode: ".opencode",
|
|
18
|
+
hermes: ".hermes", kimi: ".kimi-code",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Parse a `.env` file into a key→value map (`export KEY="v"` or `KEY=v`). */
|
|
22
|
+
export function parseEnvFile(path: string): Record<string, string> {
|
|
23
|
+
if (!existsSync(path)) return {};
|
|
24
|
+
let content = "";
|
|
25
|
+
try { content = readFileSync(path, "utf-8"); } catch { return {}; }
|
|
26
|
+
const out: Record<string, string> = {};
|
|
27
|
+
for (const line of content.split(/\r?\n/)) {
|
|
28
|
+
if (line.trimStart().startsWith("#")) continue;
|
|
29
|
+
const m = line.match(/^\s*(?:export\s+)?(\w+)\s*=\s*["']?([^"'\n]*)["']?\s*$/);
|
|
30
|
+
if (m?.[1]) out[m[1]] = m[2] ?? "";
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The `.env` paths probed for a harness: home `.env` then `<cwd>/.env`. */
|
|
36
|
+
export function envCandidates(id: HarnessId, home: string = homedir(), cwd: string = process.cwd()): string[] {
|
|
37
|
+
return [join(harnessHome(id, process.env, home), ".env"), join(cwd, ".env")];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Load `.env` files into `env` without overwriting existing keys. Reads the
|
|
42
|
+
* harness home `.env` then `<cwd>/.env`. Best-effort (missing/unreadable files
|
|
43
|
+
* are skipped) so a hook never fails on env loading.
|
|
44
|
+
* @param id - Detected harness id (selects the home dir).
|
|
45
|
+
* @param env - Target environment (defaults to `process.env`).
|
|
46
|
+
* @param home - Home dir.
|
|
47
|
+
* @param cwd - Project root.
|
|
48
|
+
*/
|
|
49
|
+
export function loadDotenv(id: HarnessId, env: NodeJS.ProcessEnv = process.env, home: string = homedir(), cwd: string = process.cwd()): void {
|
|
50
|
+
for (const path of envCandidates(id, home, cwd)) {
|
|
51
|
+
for (const [k, v] of Object.entries(parseEnvFile(path))) {
|
|
52
|
+
if (env[k] === undefined) env[k] = v;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Robust integer-from-env parser.
|
|
3
|
+
* undefined / empty / whitespace / NaN / float / <= 0 all fall back to `fallback`.
|
|
4
|
+
* `Number("")` is 0, so the empty guard is required before `Number()`.
|
|
5
|
+
*/
|
|
6
|
+
export function parseEnvInt(raw: string | undefined, fallback: number): number {
|
|
7
|
+
if (!raw?.trim()) return fallback;
|
|
8
|
+
const n = Number(raw.trim());
|
|
9
|
+
return Number.isInteger(n) && n > 0 ? n : fallback;
|
|
10
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness home-dir resolution with env overrides. `dotenv.ts`'s static
|
|
3
|
+
* `HOME_DIR` table knows every harness's default config dir but cannot move
|
|
4
|
+
* with the user's data root — Kimi Code CLI documents `KIMI_CODE_HOME` for
|
|
5
|
+
* exactly that (isolated data roots get isolated skills/agents/hooks), so the
|
|
6
|
+
* override is honored here instead of hardcoding `~/.kimi-code`.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately consumed only where a USER-HOME path is built (`envCandidates`
|
|
9
|
+
* today): `harnessHomeSegment` (`policy/apex-target.ts`) also serves
|
|
10
|
+
* project-relative joins (`<project>/.kimi-code/apex`), which an absolute
|
|
11
|
+
* override would corrupt — those sites stay segment-based.
|
|
12
|
+
*/
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import type { HarnessId } from "../detect/harness";
|
|
16
|
+
import { HOME_DIR } from "./dotenv";
|
|
17
|
+
|
|
18
|
+
/** Documented env var relocating a harness home dir, when one exists. */
|
|
19
|
+
const HOME_ENV: Partial<Record<HarnessId, string>> = { kimi: "KIMI_CODE_HOME" };
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a harness's home config dir as an absolute path.
|
|
23
|
+
* @param id - Harness id.
|
|
24
|
+
* @param env - Environment (defaults to `process.env`).
|
|
25
|
+
* @param home - OS home dir (injectable for tests).
|
|
26
|
+
* @returns The override path when set, else `join(home, HOME_DIR[id])`.
|
|
27
|
+
*/
|
|
28
|
+
export function harnessHome(
|
|
29
|
+
id: string,
|
|
30
|
+
env: Record<string, string | undefined> = process.env,
|
|
31
|
+
home: string = homedir(),
|
|
32
|
+
): string {
|
|
33
|
+
const key = HOME_ENV[id as HarnessId];
|
|
34
|
+
const override = key ? env[key] : undefined;
|
|
35
|
+
return override ?? join(home, HOME_DIR[id as HarnessId] ?? ".claude");
|
|
36
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
/** Top-level project state dir — neutral + harness-agnostic (not `.claude/…`). */
|
|
4
|
+
export const STATE_ROOT = ".harness";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The selective `.gitignore` for `.harness/`: ignore the machine-local session
|
|
8
|
+
* state (track, cache, throttle) but keep the curated `memory/LESSON.md`.
|
|
9
|
+
*/
|
|
10
|
+
export const STATE_GITIGNORE = "track/\ncache/\nmemory/state.json\n";
|
|
11
|
+
|
|
12
|
+
/** Every project-state path the package uses, derived from one project root. */
|
|
13
|
+
export interface ProjectLayout {
|
|
14
|
+
root: string;
|
|
15
|
+
/** `<root>/.harness` — the single state dir. */
|
|
16
|
+
stateDir: string;
|
|
17
|
+
/** `<root>/.harness/track` — session track files. */
|
|
18
|
+
trackDir: string;
|
|
19
|
+
/** `<root>/.harness/cache` — MCP/WebFetch cache. */
|
|
20
|
+
cacheDir: string;
|
|
21
|
+
/** `<root>/.harness/memory` — lessons + throttle. */
|
|
22
|
+
memoryDir: string;
|
|
23
|
+
/** `<root>/.harness/memory/LESSON.md` — curated, committable. */
|
|
24
|
+
lessonsFile: string;
|
|
25
|
+
/** `<root>/.harness/memory/state.json` — machine-local throttle. */
|
|
26
|
+
memoryStateFile: string;
|
|
27
|
+
/** `<root>/.harness/track/solid-notice.json` — SubagentStop dedup sidecar. */
|
|
28
|
+
solidNoticeFile: string;
|
|
29
|
+
/** `<root>/.harness/.gitignore`. */
|
|
30
|
+
gitignoreFile: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The single source of truth for where every package file lives under a project
|
|
35
|
+
* root. All modules (runtime storage, memory, init) derive their paths here —
|
|
36
|
+
* nothing hardcodes a directory name elsewhere.
|
|
37
|
+
*/
|
|
38
|
+
export function projectLayout(root: string): ProjectLayout {
|
|
39
|
+
const stateDir = join(root, STATE_ROOT);
|
|
40
|
+
const memoryDir = join(stateDir, "memory");
|
|
41
|
+
return {
|
|
42
|
+
root,
|
|
43
|
+
stateDir,
|
|
44
|
+
trackDir: join(stateDir, "track"),
|
|
45
|
+
solidNoticeFile: join(stateDir, "track", "solid-notice.json"),
|
|
46
|
+
cacheDir: join(stateDir, "cache"),
|
|
47
|
+
memoryDir,
|
|
48
|
+
lessonsFile: join(memoryDir, "LESSON.md"),
|
|
49
|
+
memoryStateFile: join(memoryDir, "state.json"),
|
|
50
|
+
gitignoreFile: join(stateDir, ".gitignore"),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { parseEnvInt } from "./env";
|
|
2
|
+
|
|
3
|
+
/** Default SOLID max lines per file. */
|
|
4
|
+
export const DEFAULT_MAX_LINES = 100;
|
|
5
|
+
|
|
6
|
+
/** Default env var name carrying the max-lines override. */
|
|
7
|
+
export const MAX_LINES_ENV_KEY = "FUSE_SOLID_MAX_LINES";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the SOLID max-lines limit from an env map.
|
|
11
|
+
* @param env - environment map (defaults to `process.env`)
|
|
12
|
+
* @param key - env var name (defaults to `FUSE_SOLID_MAX_LINES`)
|
|
13
|
+
*/
|
|
14
|
+
export function resolveMaxLines(
|
|
15
|
+
env: Record<string, string | undefined> = process.env,
|
|
16
|
+
key: string = MAX_LINES_ENV_KEY,
|
|
17
|
+
): number {
|
|
18
|
+
return parseEnvInt(env[key], DEFAULT_MAX_LINES);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Advisory module-split headroom = `maxLines - 10` (never below 1). */
|
|
22
|
+
export function splitTarget(maxLines: number): number {
|
|
23
|
+
return Math.max(maxLines - 10, 1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Custom-hook line budget, derived from the global limit (ratio 0.3 — with
|
|
28
|
+
* the default 100 this yields the owner template's 30; one variable,
|
|
29
|
+
* `FUSE_SOLID_MAX_LINES`, drives every budget proportionally).
|
|
30
|
+
* @param maxLines - The global limit (from {@link resolveMaxLines}).
|
|
31
|
+
*/
|
|
32
|
+
export function hookBudget(maxLines: number): number {
|
|
33
|
+
return Math.max(Math.round(maxLines * 0.3), 1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Store line budget, derived from the global limit (ratio 0.4 — 40 at the
|
|
38
|
+
* default 100, per owner rules/07-state-management).
|
|
39
|
+
* @param maxLines - The global limit (from {@link resolveMaxLines}).
|
|
40
|
+
*/
|
|
41
|
+
export function storeBudget(maxLines: number): number {
|
|
42
|
+
return Math.max(Math.round(maxLines * 0.4), 1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Default stdin cap for hook payloads: 16 MiB. */
|
|
46
|
+
export const DEFAULT_STDIN_MAX_BYTES: number = 16 * 1024 * 1024;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the hook stdin cap (`FUSE_HOOK_STDIN_MAX_BYTES`, default 16 MiB;
|
|
50
|
+
* absent/invalid falls back to the default). The only new env var of the
|
|
51
|
+
* mission — owner-approved exception.
|
|
52
|
+
* @param env - environment map (defaults to `process.env`)
|
|
53
|
+
*/
|
|
54
|
+
export function resolveStdinMaxBytes(env: Record<string, string | undefined> = process.env): number {
|
|
55
|
+
return parseEnvInt(env.FUSE_HOOK_STDIN_MAX_BYTES, DEFAULT_STDIN_MAX_BYTES);
|
|
56
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { parseEnvInt } from "./env";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default enforcement-freshness window, in seconds (2 minutes). Matches the
|
|
5
|
+
* plugin's original `FUSE_ENFORCE_TTL_SEC` default. This is the constant that
|
|
6
|
+
* actually reaches production (via `resolveTtlSec` -> `bin.ts`); `gate.ts`'s
|
|
7
|
+
* `DEFAULT_WINDOW_MS` is only a fallback for direct programmatic callers that
|
|
8
|
+
* omit `windowMs` (e.g. tests) and never applies on the real CLI path.
|
|
9
|
+
*/
|
|
10
|
+
export const DEFAULT_TTL_SEC = 120;
|
|
11
|
+
|
|
12
|
+
/** Default env var name carrying the TTL override. */
|
|
13
|
+
export const TTL_ENV_KEY = "FUSE_ENFORCE_TTL_SEC";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolve the enforcement TTL (seconds) from an env map.
|
|
17
|
+
* @param env - environment map (defaults to `process.env`)
|
|
18
|
+
* @param key - env var name (defaults to `FUSE_ENFORCE_TTL_SEC`)
|
|
19
|
+
*/
|
|
20
|
+
export function resolveTtlSec(
|
|
21
|
+
env: Record<string, string | undefined> = process.env,
|
|
22
|
+
key: string = TTL_ENV_KEY,
|
|
23
|
+
): number {
|
|
24
|
+
return parseEnvInt(env[key], DEFAULT_TTL_SEC);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Human label for a TTL: 120 -> "2min", 240 -> "4min", 90 -> "90s". */
|
|
28
|
+
export function ttlLabel(sec: number): string {
|
|
29
|
+
return sec % 60 === 0 ? `${sec / 60}min` : `${sec}s`;
|
|
30
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime detection of the AI coding harness, and its integration mode.
|
|
3
|
+
* Env-signal names verified 2026 (agentx, agents.md#136, @vercel/detect-agent,
|
|
4
|
+
* official Claude Code / Cursor / Gemini / Codex docs). Presence-based: the
|
|
5
|
+
* value is ignored except for the `AGENT` / `AI_AGENT` standards.
|
|
6
|
+
*/
|
|
7
|
+
import type { HarnessId, HarnessMode, HarnessInfo } from "./interfaces/types";
|
|
8
|
+
|
|
9
|
+
export type { HarnessId, HarnessMode, HarnessVia, HarnessInfo } from "./interfaces/types";
|
|
10
|
+
|
|
11
|
+
/** Tool-specific env var (presence) -> harness id. Order = detection priority. */
|
|
12
|
+
const ENV_SIGNALS: ReadonlyArray<readonly [string, HarnessId]> = [
|
|
13
|
+
["CLAUDECODE", "claude-code"], ["CODEX_SANDBOX", "codex"],
|
|
14
|
+
["CURSOR_AGENT", "cursor"], ["CLINE", "cline"], ["CLINE_AGENT", "cline"],
|
|
15
|
+
["GEMINI_CLI", "gemini-cli"], ["OPENCODE", "opencode"],
|
|
16
|
+
["WINDSURF_AGENT", "windsurf"], ["CODEIUM_AGENT", "windsurf"],
|
|
17
|
+
["COPILOT_AGENT", "copilot"], ["AIDER", "aider"], ["KIRO", "kiro"],
|
|
18
|
+
["GOOSE", "goose"], ["AMP", "amp"], ["HERMES_SESSION_ID", "hermes"],
|
|
19
|
+
// No documented session-marker env var for Kimi Code CLI (unlike CLAUDECODE,
|
|
20
|
+
// CURSOR_AGENT, ...). `KIMI_CODE_HOME`/`KIMI_PLUGIN_ROOT` are injected only
|
|
21
|
+
// into PLUGIN-declared hooks (customization/plugins.html: "The hook process
|
|
22
|
+
// receives two extra environment variables"), never into config.toml hooks —
|
|
23
|
+
// verified live against kimi-code v0.27.0: a config.toml hook process sees
|
|
24
|
+
// neither var, and no session marker at all. Detection therefore falls
|
|
25
|
+
// through to the `AGENT=kimi` / `AI_AGENT=kimi` standard above.
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/** `AGENT=<name>` / `AI_AGENT=<name>` standard value -> harness id. */
|
|
29
|
+
const STD_NAMES: Record<string, HarnessId> = {
|
|
30
|
+
goose: "goose", amp: "amp", claude: "claude-code", "claude-code": "claude-code",
|
|
31
|
+
cursor: "cursor", codex: "codex", cline: "cline", aider: "aider",
|
|
32
|
+
opencode: "opencode", gemini: "gemini-cli", copilot: "copilot", kiro: "kiro",
|
|
33
|
+
hermes: "hermes", kimi: "kimi",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Harnesses exposing a native hook system (vs CLI-only integration). */
|
|
37
|
+
const HOOK_CAPABLE: ReadonlySet<HarnessId> = new Set([
|
|
38
|
+
"claude-code", "codex", "cursor", "cline", "gemini-cli", "opencode", "hermes", "kimi",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
/** Integration mode for a harness id. */
|
|
42
|
+
export function modeFor(id: HarnessId): HarnessMode {
|
|
43
|
+
return HOOK_CAPABLE.has(id) ? "hook" : "cli";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Detect the current AI coding harness from environment signals.
|
|
48
|
+
* Priority: `AGENT` standard -> `AI_AGENT` standard -> tool-specific vars -> unknown.
|
|
49
|
+
* @param env - environment map (defaults to `process.env`)
|
|
50
|
+
*/
|
|
51
|
+
export function detectHarness(
|
|
52
|
+
env: Record<string, string | undefined> = process.env,
|
|
53
|
+
): HarnessInfo {
|
|
54
|
+
const agent = env.AGENT?.trim().toLowerCase();
|
|
55
|
+
const agentId = agent ? STD_NAMES[agent] : undefined;
|
|
56
|
+
if (agentId) return { id: agentId, mode: modeFor(agentId), via: "agent-std" };
|
|
57
|
+
|
|
58
|
+
const ai = env.AI_AGENT?.trim().toLowerCase();
|
|
59
|
+
const aiId = ai ? STD_NAMES[ai] : undefined;
|
|
60
|
+
if (aiId) return { id: aiId, mode: modeFor(aiId), via: "ai-agent-std" };
|
|
61
|
+
|
|
62
|
+
for (const [key, id] of ENV_SIGNALS) {
|
|
63
|
+
if (env[key]?.trim()) return { id, mode: modeFor(id), via: "env" };
|
|
64
|
+
}
|
|
65
|
+
return { id: "unknown", mode: "cli", via: "fallback" };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Convenience: the integration mode of the current harness. */
|
|
69
|
+
export function detectMode(
|
|
70
|
+
env: Record<string, string | undefined> = process.env,
|
|
71
|
+
): HarnessMode {
|
|
72
|
+
return detectHarness(env).mode;
|
|
73
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./harness";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Known AI coding harnesses detectable at runtime. */
|
|
2
|
+
export type HarnessId =
|
|
3
|
+
| "claude-code" | "codex" | "cursor" | "cline" | "gemini-cli"
|
|
4
|
+
| "opencode" | "windsurf" | "copilot" | "aider" | "kiro"
|
|
5
|
+
| "goose" | "amp" | "hermes" | "kimi" | "unknown";
|
|
6
|
+
|
|
7
|
+
/** Integration mode: `hook` = native lifecycle hooks; `cli` = run as an external step. */
|
|
8
|
+
export type HarnessMode = "hook" | "cli";
|
|
9
|
+
|
|
10
|
+
/** How the harness was identified. */
|
|
11
|
+
export type HarnessVia = "agent-std" | "ai-agent-std" | "env" | "fallback";
|
|
12
|
+
|
|
13
|
+
/** Result of {@link detectHarness}. */
|
|
14
|
+
export interface HarnessInfo {
|
|
15
|
+
id: HarnessId;
|
|
16
|
+
mode: HarnessMode;
|
|
17
|
+
via: HarnessVia;
|
|
18
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-track agent evidence — writer (parity track-subagent-research.py) +
|
|
3
|
+
* reader scan (parity apex_agent_helpers._scan_agents). Sub-agent hooks fire
|
|
4
|
+
* with the LEAD's `session_id`, so evidence recorded here lands in the ONE
|
|
5
|
+
* session track the freshness gate scans FIRST — sidechain research and
|
|
6
|
+
* Workflow-spawned agents count, unlike the lead-transcript scan.
|
|
7
|
+
*/
|
|
8
|
+
import { classifyExplore } from "./explore-tools";
|
|
9
|
+
import { withTrack } from "../tracking/store";
|
|
10
|
+
import { recordAgent, type AgentQuality, type SessionTrack } from "../tracking/session-state";
|
|
11
|
+
import { isAgentTool } from "../runtime/is-agent-tool";
|
|
12
|
+
|
|
13
|
+
/** One classified PostToolUse call, ready to persist as freshness evidence. */
|
|
14
|
+
export interface AgentEvidence {
|
|
15
|
+
/** Track entry name — `subagent-` + APEX phase (the reader substring-matches). */
|
|
16
|
+
name: "subagent-explore-codebase" | "subagent-research-expert";
|
|
17
|
+
/** `sufficient` when the call hit an MCP cache OR its response JSON is >50 chars. */
|
|
18
|
+
quality: AgentQuality;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* `JSON.stringify` length of the raw `tool_response` OBJECT (parity Python
|
|
23
|
+
* `len(str(tool_response))`); 0 when absent or unserializable (circular).
|
|
24
|
+
*/
|
|
25
|
+
function responseJsonLength(toolResponse: unknown): number {
|
|
26
|
+
if (toolResponse === undefined || toolResponse === null) return 0;
|
|
27
|
+
try { return (JSON.stringify(toolResponse) ?? "").length; } catch { return 0; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Classify one PostToolUse call into session evidence, or null when it is
|
|
32
|
+
* neither exploration nor research (parity track-subagent-research._classify,
|
|
33
|
+
* reusing the shared {@link classifyExplore} tables — research/explore/Bash/
|
|
34
|
+
* cache-read). ANTI-DOUBLE-COUNT: calls classified as agent launches
|
|
35
|
+
* (`Task`/`Agent`) are skipped — the existing Task tracking credits those; the
|
|
36
|
+
* criterion is the CALL's classification, never its provenance (lead vs sub).
|
|
37
|
+
* @param tool - Harness tool name (e.g. "Glob", "Bash", "Read").
|
|
38
|
+
* @param input - Raw tool input payload.
|
|
39
|
+
* @param toolResponse - Raw `tool_response` OBJECT from the hook payload.
|
|
40
|
+
* @returns The evidence to record, or null to skip.
|
|
41
|
+
*/
|
|
42
|
+
export function classifyAgentEvidence(tool: string, input: Record<string, unknown> | undefined, toolResponse: unknown): AgentEvidence | null {
|
|
43
|
+
if (isAgentTool(tool)) return null;
|
|
44
|
+
const hit = classifyExplore(tool, input);
|
|
45
|
+
if (!hit) return null;
|
|
46
|
+
const name = hit.phase === "explore-codebase" ? "subagent-explore-codebase" : "subagent-research-expert";
|
|
47
|
+
return { name, quality: hit.cacheHit || responseJsonLength(toolResponse) > 50 ? "sufficient" : "insufficient" };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Persist evidence into the SESSION track via {@link recordAgent} — keyed by
|
|
52
|
+
* `session_id` alone (sub-agent hooks carry the lead's `session_id`); the TTL
|
|
53
|
+
* anchors on `ts`, the tool call's own timestamp. `agentId` is tagged as
|
|
54
|
+
* metadata when present, NEVER used as a condition (unreliable field —
|
|
55
|
+
* anthropics/claude-code#22348).
|
|
56
|
+
* @param file - Session track file path.
|
|
57
|
+
* @param evidence - Classified evidence from {@link classifyAgentEvidence}.
|
|
58
|
+
* @param ts - Epoch-ms timestamp of the tool call (the hook event's `now`).
|
|
59
|
+
* @param agentId - Optional Claude `agent_id` — metadata tag only.
|
|
60
|
+
*/
|
|
61
|
+
export async function recordAgentEvidence(file: string, evidence: AgentEvidence, ts: number, agentId?: string): Promise<void> {
|
|
62
|
+
await withTrack(file, (track) => {
|
|
63
|
+
const next = recordAgent(track, evidence.name, ts, evidence.quality);
|
|
64
|
+
const last = next.agents[next.agents.length - 1];
|
|
65
|
+
if (agentId && last) {
|
|
66
|
+
const tagged: SessionTrack["agents"][number] & { agentId?: string } = { ...last, agentId };
|
|
67
|
+
next.agents[next.agents.length - 1] = tagged;
|
|
68
|
+
}
|
|
69
|
+
return next;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parity `_scan_agents` (apex_agent_helpers.py): reverse-scan `track.agents`,
|
|
75
|
+
* STOPPING at the first entry older than `windowMs` (entries append in time
|
|
76
|
+
* order), matching each required name by SUBSTRING (`research-expert` matches
|
|
77
|
+
* `subagent-research-expert`) and counting ONLY `quality === "sufficient"`.
|
|
78
|
+
* @param track - The loaded session track.
|
|
79
|
+
* @param names - Required agent names — ALL must match to return true.
|
|
80
|
+
* @param windowMs - Freshness window (ms), anchored on each entry's `ts`.
|
|
81
|
+
* @param now - Current epoch ms.
|
|
82
|
+
* @returns True when every required name has fresh, sufficient evidence.
|
|
83
|
+
*/
|
|
84
|
+
export function agentsFreshInTrack(track: SessionTrack, names: readonly string[], windowMs: number, now: number): boolean {
|
|
85
|
+
const found = new Set<string>();
|
|
86
|
+
for (let i = track.agents.length - 1; i >= 0; i--) {
|
|
87
|
+
const entry = track.agents[i];
|
|
88
|
+
if (!entry) continue;
|
|
89
|
+
if (now - entry.ts > windowMs) break;
|
|
90
|
+
if (entry.quality !== "sufficient") continue;
|
|
91
|
+
for (const req of names) if (entry.name.includes(req)) found.add(req);
|
|
92
|
+
if (found.size === names.length) return true;
|
|
93
|
+
}
|
|
94
|
+
return names.every((n) => found.has(n));
|
|
95
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-authored transcript evidence for APEX agent freshness. Parses the
|
|
3
|
+
* Claude Code session JSONL transcript for genuine dispatch tool_use entries
|
|
4
|
+
* — forging requires writing into the platform-controlled transcript file.
|
|
5
|
+
*/
|
|
6
|
+
import { readText } from "../util/runtime-io";
|
|
7
|
+
import { classifyExplore } from "./explore-tools";
|
|
8
|
+
import { isAgentTool } from "../runtime/is-agent-tool";
|
|
9
|
+
|
|
10
|
+
/** Raw shape of one JSONL line in a Claude Code transcript. */
|
|
11
|
+
interface TranscriptLine {
|
|
12
|
+
/** ISO-8601 or epoch-ms timestamp written by the platform. */
|
|
13
|
+
timestamp?: string | number;
|
|
14
|
+
message?: { content?: unknown[] };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A tool_use content block inside a transcript message. */
|
|
18
|
+
interface ToolUseBlock {
|
|
19
|
+
type: string;
|
|
20
|
+
name?: string;
|
|
21
|
+
input?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Parse a raw `timestamp` field to epoch ms; `undefined` when absent or invalid. */
|
|
25
|
+
function parseTs(raw: string | number | undefined): number | undefined {
|
|
26
|
+
if (raw === undefined) return undefined;
|
|
27
|
+
if (typeof raw === "number") return raw;
|
|
28
|
+
const ms = Date.parse(raw);
|
|
29
|
+
return Number.isFinite(ms) ? ms : undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Return `true` ONLY when, for EVERY name in `names`, the transcript at
|
|
34
|
+
* `transcriptPath` shows a within-window genuine `tool_use`: a dispatch tool
|
|
35
|
+
* ({@link isAgentTool}) whose `subagent_type`/`name` (plugin prefix stripped)
|
|
36
|
+
* matches, OR a direct exploration/research call classified into that name by
|
|
37
|
+
* {@link classifyExplore} — issued by any sub-agent or the lead (the transcript
|
|
38
|
+
* carries no author field). Entries without `timestamp` count as within-window
|
|
39
|
+
* (format-evolution leniency; tamper-resistance comes from platform authoring).
|
|
40
|
+
* @param transcriptPath - Session `.jsonl` transcript (`transcript_path`); false when undefined.
|
|
41
|
+
* @param names - Required agent `subagent_type` values — ALL must appear.
|
|
42
|
+
* @param windowMs - Freshness window in milliseconds.
|
|
43
|
+
* @param now - Current epoch ms (pass `Date.now()` at the call-site).
|
|
44
|
+
* @returns `true` when ALL agents have real, within-window transcript evidence.
|
|
45
|
+
*/
|
|
46
|
+
export function agentsRanFromTranscript(
|
|
47
|
+
transcriptPath: string | undefined,
|
|
48
|
+
names: readonly string[],
|
|
49
|
+
windowMs: number,
|
|
50
|
+
now: number,
|
|
51
|
+
): boolean {
|
|
52
|
+
if (!transcriptPath || names.length === 0) return false;
|
|
53
|
+
let text: string;
|
|
54
|
+
try {
|
|
55
|
+
text = readText(transcriptPath);
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
const cutoff = now - windowMs;
|
|
60
|
+
const found = new Set<string>();
|
|
61
|
+
for (const line of text.split("\n")) {
|
|
62
|
+
if (!line.trim()) continue;
|
|
63
|
+
let entry: TranscriptLine;
|
|
64
|
+
try {
|
|
65
|
+
entry = JSON.parse(line) as TranscriptLine;
|
|
66
|
+
} catch {
|
|
67
|
+
continue; // tolerate malformed lines
|
|
68
|
+
}
|
|
69
|
+
const ts = parseTs(entry.timestamp);
|
|
70
|
+
if (ts !== undefined && ts <= cutoff) continue;
|
|
71
|
+
const content = entry.message?.content;
|
|
72
|
+
if (!Array.isArray(content)) continue;
|
|
73
|
+
for (const block of content as ToolUseBlock[]) {
|
|
74
|
+
if (block?.type !== "tool_use") continue;
|
|
75
|
+
if (block.name !== undefined && isAgentTool(block.name)) {
|
|
76
|
+
const raw = block.input?.subagent_type ?? block.input?.name;
|
|
77
|
+
// Strip any plugin prefix (`fuse-ai-pilot:research-expert` → `research-expert`)
|
|
78
|
+
// before matching the bare REQUIRED_AGENTS names.
|
|
79
|
+
const agent = typeof raw === "string" ? raw.split(":").pop() ?? raw : undefined;
|
|
80
|
+
if (agent !== undefined && (names as string[]).includes(agent)) found.add(agent);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
// Direct exploration/research tool_use — credited to the matching phase
|
|
84
|
+
// regardless of which sub-agent (or the lead) issued it: the transcript
|
|
85
|
+
// carries no author field distinguishing sidechains, so — mirroring the
|
|
86
|
+
// pre-existing self-recorded-track behavior in `runtime/activity.ts` —
|
|
87
|
+
// ANY Glob/Grep/mcp__context7/mcp__exa/WebSearch/WebFetch/explore-Bash
|
|
88
|
+
// tool_use in this session's transcript counts as freshness evidence.
|
|
89
|
+
const hit = classifyExplore(block.name ?? "", block.input);
|
|
90
|
+
if (hit && (names as string[]).includes(hit.phase)) found.add(hit.phase);
|
|
91
|
+
}
|
|
92
|
+
if (found.size === names.length) return true;
|
|
93
|
+
}
|
|
94
|
+
return names.every((n) => found.has(n));
|
|
95
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex multi_agent_v2 `spawn_agent` -> session evidence bridge. Codex's custom
|
|
3
|
+
* agents (fusengine multi_agent_v2, e.g. sniper/explore-codebase) are launched
|
|
4
|
+
* via a `spawn_agent` TOOL CALL inside the SAME session -- Codex has no
|
|
5
|
+
* SubagentStart/SubagentStop lifecycle for them, so the existing Claude-side
|
|
6
|
+
* evidence paths (agent-evidence-record.ts `classifyAgentEvidence`, the
|
|
7
|
+
* SubagentStop harvest) never observe them; the harness only ever saw a
|
|
8
|
+
* generic tool call. This module credits the SAME session track
|
|
9
|
+
* ({@link recordAgent}, read back by `agent-evidence-record.ts`
|
|
10
|
+
* `agentsFreshInTrack`) straight from that PostToolUse `spawn_agent` call.
|
|
11
|
+
*
|
|
12
|
+
* SOURCE (openai/codex@44918ea1, tag rust-v0.144.1 -- verified upstream,
|
|
13
|
+
* not re-checked here): the hook `tool_name` is `"spawn_agent"` normally, or
|
|
14
|
+
* the SEPARATOR-LESS namespace-prefixed `{namespace}spawn_agent` when the
|
|
15
|
+
* `namespace_tools` capability provider is active (e.g.
|
|
16
|
+
* `"fusengine_agentsspawn_agent"`, `"collaborationspawn_agent"`). Detection is
|
|
17
|
+
* `tool_name === "spawn_agent" || tool_name.endsWith("spawn_agent")` -- a tool
|
|
18
|
+
* that merely CONTAINS the substring without being an exact/suffix match
|
|
19
|
+
* (`"spawn_agentX"`, `"myspawn_agent_tool"`) is correctly rejected.
|
|
20
|
+
* `tool_input.agent_type` carries the spawned agent's type only when Codex's
|
|
21
|
+
* `hide_spawn_agent_metadata=false`; treat it as always-optional.
|
|
22
|
+
*
|
|
23
|
+
* INVARIANT (multi-harness parity): every entry point here is gated on
|
|
24
|
+
* `id === "codex"` FIRST -- claude-code/cursor/cline/gemini-cli/hermes always
|
|
25
|
+
* take the early return, touching neither the track nor disk. Byte-identical
|
|
26
|
+
* elsewhere by construction.
|
|
27
|
+
*/
|
|
28
|
+
import { recordAgent, type SessionTrack } from "../tracking/session-state";
|
|
29
|
+
import { withTrack } from "../tracking/store";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* True when `tool` is Codex's `spawn_agent` primitive, bare or
|
|
33
|
+
* namespace-prefixed (no separator). Rejects any tool that merely contains
|
|
34
|
+
* the substring elsewhere in its name.
|
|
35
|
+
* @param tool - Raw/normalized `tool_name` from the hook payload.
|
|
36
|
+
*/
|
|
37
|
+
export function isCodexSpawnAgentTool(tool: string): boolean {
|
|
38
|
+
return tool === "spawn_agent" || tool.endsWith("spawn_agent");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Extract the spawned agent's type from a `spawn_agent` `tool_input`, or
|
|
43
|
+
* `undefined` when absent/blank (`hide_spawn_agent_metadata=true`, or a
|
|
44
|
+
* future Codex build omitting it) -- never throws.
|
|
45
|
+
* @param input - Raw `tool_input` payload (harness `event.input`).
|
|
46
|
+
*/
|
|
47
|
+
export function codexSpawnAgentType(input: Record<string, unknown> | undefined): string | undefined {
|
|
48
|
+
const v = input?.agent_type;
|
|
49
|
+
return typeof v === "string" && v.trim() !== "" ? v.trim() : undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Pure classifier + folder: credit a Codex `spawn_agent` call into
|
|
54
|
+
* `track.agents` under the SAME `subagent-<name>` convention the Claude-side
|
|
55
|
+
* explore/research evidence uses -- so an `agent_type` that happens to match
|
|
56
|
+
* a `REQUIRED_AGENTS` name (e.g. `"explore-codebase"`) is picked up by
|
|
57
|
+
* `agentsFreshInTrack`'s substring match exactly like genuine Claude
|
|
58
|
+
* Task/Agent evidence. Immutable: returns `track` UNCHANGED (same reference)
|
|
59
|
+
* for any other harness `id`, any non-spawn tool, or a missing `agent_type`.
|
|
60
|
+
* @param id - Harness id (only `"codex"` ever credits).
|
|
61
|
+
* @param tool - Normalized event tool name (raw `tool_name`).
|
|
62
|
+
* @param input - Raw `tool_input` payload.
|
|
63
|
+
* @param track - The current session track.
|
|
64
|
+
* @param ts - Epoch-ms timestamp of the tool call.
|
|
65
|
+
* @returns The credited track, or `track` itself when nothing was credited.
|
|
66
|
+
*/
|
|
67
|
+
export function creditCodexSpawnAgent(id: string, tool: string, input: Record<string, unknown> | undefined, track: SessionTrack, ts: number): SessionTrack {
|
|
68
|
+
if (id !== "codex" || !isCodexSpawnAgentTool(tool)) return track;
|
|
69
|
+
const agentType = codexSpawnAgentType(input);
|
|
70
|
+
if (!agentType) return track;
|
|
71
|
+
return recordAgent(track, `subagent-${agentType}`, ts, "sufficient");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* I/O wiring for {@link creditCodexSpawnAgent}: load the session track, fold
|
|
76
|
+
* in the credit, and persist only when something actually changed (skips a
|
|
77
|
+
* write for the strict no-op cases). Mirrors `agent-evidence-record.ts`
|
|
78
|
+
* `recordAgentEvidence`'s async load/save shape.
|
|
79
|
+
* @param file - Session track file path (same one the freshness gate reads).
|
|
80
|
+
* @param id - Harness id (only `"codex"` ever credits).
|
|
81
|
+
* @param tool - Normalized event tool name (raw `tool_name`).
|
|
82
|
+
* @param input - Raw `tool_input` payload.
|
|
83
|
+
* @param ts - Epoch-ms timestamp of the tool call.
|
|
84
|
+
*/
|
|
85
|
+
export async function recordCodexSpawnEvidence(file: string, id: string, tool: string, input: Record<string, unknown> | undefined, ts: number): Promise<void> {
|
|
86
|
+
if (id !== "codex" || !isCodexSpawnAgentTool(tool)) return;
|
|
87
|
+
await withTrack(file, (track) => {
|
|
88
|
+
const next = creditCodexSpawnAgent(id, tool, input, track, ts);
|
|
89
|
+
return next !== track ? next : track;
|
|
90
|
+
});
|
|
91
|
+
}
|