@fusengine/harness 0.1.90 → 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 +3 -2
- 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-CBaOcFZq.mjs → handle-C43gA-Pr.mjs} +701 -134
- 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/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
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { attachSystemMessage, contextResponse } from "../adapters/claude";
|
|
2
|
+
import { renderInform } from "./inform";
|
|
3
|
+
import { buildClaudeMdContext } from "../policy/claude-md-context";
|
|
4
|
+
import { buildApexTaskInjection } from "../policy/apex-task-context";
|
|
5
|
+
import { apexDocName } from "../policy/apex-target";
|
|
6
|
+
import { hashText } from "../util/json-io";
|
|
7
|
+
import { oncePerWindow, DEDUP_WINDOW_MS } from "./inject-dedup";
|
|
8
|
+
import { capFragment } from "./inject-budget";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build the {@link oncePerWindow} key for the CLAUDE.md preamble gate. The
|
|
12
|
+
* prompt hash keeps two distinct legitimate turns from colliding — even non-dev
|
|
13
|
+
* prompts, whose block is prompt-independent (just CLAUDE.md) and would
|
|
14
|
+
* otherwise hash-collide within the window — while the content hash still lets a
|
|
15
|
+
* same-turn double-fire of an identical block be suppressed. Single source of
|
|
16
|
+
* truth so the owner invariant test guards the real production key.
|
|
17
|
+
* @param prompt - The raw user prompt.
|
|
18
|
+
* @param ctx - The rendered CLAUDE.md (+ optional APEX) block.
|
|
19
|
+
* @returns The namespaced dedup key.
|
|
20
|
+
*/
|
|
21
|
+
export function claudeMdKey(prompt: string, ctx: string): string {
|
|
22
|
+
return `claude-md:${hashText(prompt)}:${hashText(ctx)}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Extract the APEX preamble portion of a `buildClaudeMdContext` result, i.e.
|
|
27
|
+
* everything BEFORE the `# <docName>\n` root-doc block it prepends on dev
|
|
28
|
+
* prompts. Returns "" when `ctx` has no preamble (plain prompt: `ctx` starts
|
|
29
|
+
* directly with the root-doc heading).
|
|
30
|
+
* @param ctx - The full `buildClaudeMdContext` return value.
|
|
31
|
+
* @param docName - Root doc file name (e.g. "AGENTS.md").
|
|
32
|
+
* @returns The preamble text, or "" when absent.
|
|
33
|
+
*/
|
|
34
|
+
function extractApexPreamble(ctx: string, docName: string): string {
|
|
35
|
+
const heading = `# ${docName}\n`;
|
|
36
|
+
if (ctx.startsWith(heading)) return "";
|
|
37
|
+
const sep = `\n\n${heading}`;
|
|
38
|
+
const idx = ctx.indexOf(sep);
|
|
39
|
+
return idx === -1 ? "" : ctx.slice(0, idx);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* UserPromptSubmit context injection: render the CLAUDE.md (+ optional APEX)
|
|
44
|
+
* preamble as a Claude `additionalContext` response, or "" when nothing to emit.
|
|
45
|
+
* Guarded by {@link oncePerWindow} via {@link claudeMdKey}: only a
|
|
46
|
+
* near-simultaneous double-fire of the SAME turn (identical prompt AND identical
|
|
47
|
+
* block, within {@link DEDUP_WINDOW_MS}) is suppressed. The invariant "CLAUDE.md
|
|
48
|
+
* is emitted on EVERY message" is thus preserved.
|
|
49
|
+
*
|
|
50
|
+
* On kimi specifically, the root doc body is dropped from the emitted text:
|
|
51
|
+
* Kimi loads `<kimiHome>/AGENTS.md` natively at session start (`apex-target.ts`),
|
|
52
|
+
* so re-injecting its full body on every prompt is redundant terminal noise
|
|
53
|
+
* (kimi has no model-only hook channel — `runtime/inform.ts`). The APEX
|
|
54
|
+
* preamble, which is NOT part of `AGENTS.md`, is still emitted in full when the
|
|
55
|
+
* prompt is dev-shaped; a plain prompt leaves only the notice.
|
|
56
|
+
* @param prompt - The raw user prompt.
|
|
57
|
+
* @param cwd - Project root (for project-type detection).
|
|
58
|
+
* @param id - Harness target id (defaults to "claude-code" — zero-regression default).
|
|
59
|
+
* @returns The native hook stdout (possibly empty).
|
|
60
|
+
*/
|
|
61
|
+
export function promptSubmitContext(prompt: string, cwd: string, id: string = "claude-code"): string {
|
|
62
|
+
const ctx = buildClaudeMdContext(prompt, cwd, id);
|
|
63
|
+
if (!ctx) return "";
|
|
64
|
+
if (!oncePerWindow(claudeMdKey(prompt, ctx), DEDUP_WINDOW_MS)) return "";
|
|
65
|
+
const notice = `${apexDocName(id)} injected`;
|
|
66
|
+
if (id === "kimi") {
|
|
67
|
+
const preamble = extractApexPreamble(ctx, apexDocName(id));
|
|
68
|
+
return preamble ? renderInform(id, "UserPromptSubmit", preamble, notice) : notice;
|
|
69
|
+
}
|
|
70
|
+
return renderInform(id, "UserPromptSubmit", ctx, notice);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* PreToolUse Task context injection: render the APEX sub-agent context as a
|
|
75
|
+
* Claude `additionalContext` response when `.claude/apex/` exists, else "".
|
|
76
|
+
* Harness-produced (not owner CLAUDE.md content), so it is subject to the
|
|
77
|
+
* per-fragment {@link capFragment} budget — unlike {@link promptSubmitContext}.
|
|
78
|
+
* @param cwd - Fallback project root when `CLAUDE_PROJECT_DIR` is unset.
|
|
79
|
+
* @param id - Harness target id (defaults to "claude-code" — zero-regression default).
|
|
80
|
+
* @returns The native hook stdout (possibly empty).
|
|
81
|
+
*/
|
|
82
|
+
export function taskContext(cwd: string, id: string = "claude-code"): string {
|
|
83
|
+
const ctx = buildApexTaskInjection(process.env.CLAUDE_PROJECT_DIR ?? cwd, id);
|
|
84
|
+
return ctx ? contextResponse("PreToolUse", capFragment("apex-task", ctx)) : "";
|
|
85
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module inject-dedup-exclusive
|
|
3
|
+
* Cross-process-exact-once cooldown gate, via EXCLUSIVE file creation.
|
|
4
|
+
*
|
|
5
|
+
* {@link module:inject-dedup.oncePerWindow}'s shared-JSON read-modify-write is
|
|
6
|
+
* best-effort under true concurrency: the ~11-process plugin hook fan-out for
|
|
7
|
+
* one Claude event can lose an update and let 2-3 siblings all observe "not
|
|
8
|
+
* seen yet" (lesson 2026-07-05 16:00 — the `saveTrack` lost-update race, same
|
|
9
|
+
* shape). `writeFileSync(path, data, { flag: "wx" })` sidesteps this: the OS
|
|
10
|
+
* guarantees exclusive creation is atomic, so of N concurrent siblings calling
|
|
11
|
+
* this for the SAME key, exactly one observes success and the rest get
|
|
12
|
+
* `EEXIST` — never a double-win, with no lock file or retry loop needed.
|
|
13
|
+
*
|
|
14
|
+
* One marker file per key (not a shared map) is the tradeoff for that
|
|
15
|
+
* guarantee. A bounded sweep on every call deletes markers older than
|
|
16
|
+
* `windowMs` so the directory never grows unbounded under many distinct keys.
|
|
17
|
+
*
|
|
18
|
+
* Reserve this for HIGH-CONCURRENCY callers on a short burst window (same
|
|
19
|
+
* tool-use/lifecycle event fanned out to every installed plugin) — e.g. the
|
|
20
|
+
* sniper reminder ({@link module:lifecycle/track-changes}) and compliance
|
|
21
|
+
* notices ({@link module:notices}). Low-frequency, long-window callers (e.g.
|
|
22
|
+
* the 30-min lessons Stop-reminder cooldown) are fine on the JSON mode: a
|
|
23
|
+
* single real Stop event per session is not concurrent with itself the way
|
|
24
|
+
* one PostToolUse's ~11 sibling hooks are within the same 2s burst.
|
|
25
|
+
* @packageDocumentation
|
|
26
|
+
*/
|
|
27
|
+
import { createHash } from "node:crypto";
|
|
28
|
+
import { mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
29
|
+
import { join } from "node:path";
|
|
30
|
+
import { defaultStateDir } from "./paths";
|
|
31
|
+
import type { OnceOpts } from "./inject-dedup";
|
|
32
|
+
|
|
33
|
+
/** Subdirectory (under the state dir) holding one marker file per dedup key. */
|
|
34
|
+
const EXCLUSIVE_SUBDIR = "inject-dedup-locks";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Filesystem-safe, collision-resistant basename for `key`. Full MD5 hex
|
|
38
|
+
* (unlike {@link module:util/json-io.hashText}'s 8-char truncation) — a
|
|
39
|
+
* collision here would silently merge two unrelated keys' exclusivity.
|
|
40
|
+
*/
|
|
41
|
+
function lockFileName(key: string): string {
|
|
42
|
+
return `${createHash("md5").update(key).digest("hex")}.lock`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Delete marker files older than `windowMs` in `dir` — O(n) per call, bounds
|
|
47
|
+
* directory growth. Compares against the creation timestamp STORED IN the
|
|
48
|
+
* marker's content (written by {@link onceExclusive} below), not the file's
|
|
49
|
+
* fs `mtime`: callers may pass a fake logical `now` (tests), which would never
|
|
50
|
+
* agree with the real OS clock backing `mtime`.
|
|
51
|
+
*/
|
|
52
|
+
function sweepExclusiveDir(dir: string, now: number, windowMs: number): void {
|
|
53
|
+
let entries: string[];
|
|
54
|
+
try {
|
|
55
|
+
entries = readdirSync(dir);
|
|
56
|
+
} catch {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
const path = join(dir, entry);
|
|
61
|
+
try {
|
|
62
|
+
const createdAt = Number(readFileSync(path, "utf8"));
|
|
63
|
+
if (!Number.isFinite(createdAt) || now - createdAt >= windowMs) unlinkSync(path);
|
|
64
|
+
} catch {
|
|
65
|
+
/* a sibling process already swept/removed it, or a transient read race — ignore */
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Cooldown gate via exclusive marker-file creation. Returns `true` exactly
|
|
72
|
+
* once per `key` within `windowMs` across ALL concurrent processes sharing
|
|
73
|
+
* `opts.dir` (the caller MAY emit), `false` for every other concurrent or
|
|
74
|
+
* subsequent call inside the same window (the caller SHOULD suppress).
|
|
75
|
+
*
|
|
76
|
+
* Fails open on any unwritable state dir or unexpected fs error: the emission
|
|
77
|
+
* is allowed rather than silently dropped.
|
|
78
|
+
* @param key - Stable identity of the block (same semantics as {@link module:inject-dedup.oncePerWindow}).
|
|
79
|
+
* @param windowMs - Suppression window in ms (also the sweep threshold).
|
|
80
|
+
* @param opts - Optional clock + state-dir overrides (for tests).
|
|
81
|
+
* @returns `true` to proceed/emit, `false` to suppress.
|
|
82
|
+
*/
|
|
83
|
+
export function onceExclusive(key: string, windowMs: number, opts: OnceOpts = {}): boolean {
|
|
84
|
+
const now = opts.now ?? Date.now();
|
|
85
|
+
const dir = join(opts.dir ?? defaultStateDir(), EXCLUSIVE_SUBDIR);
|
|
86
|
+
try {
|
|
87
|
+
mkdirSync(dir, { recursive: true });
|
|
88
|
+
} catch {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
sweepExclusiveDir(dir, now, windowMs);
|
|
92
|
+
try {
|
|
93
|
+
writeFileSync(join(dir, lockFileName(key)), String(now), { flag: "wx" });
|
|
94
|
+
return true;
|
|
95
|
+
} catch (err) {
|
|
96
|
+
return (err as NodeJS.ErrnoException).code !== "EEXIST";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module inject-dedup
|
|
3
|
+
* Generic "once per window" guard for hook context injection.
|
|
4
|
+
*
|
|
5
|
+
* Persists a tiny sidecar map `{ key -> lastEmittedEpochMs }` in the existing
|
|
6
|
+
* per-project state dir (`~/.fuse-harness/state/<projectHash>/inject-dedup.json`,
|
|
7
|
+
* the same dir as the session track — see {@link defaultStateDir}). Lets a caller
|
|
8
|
+
* suppress a block it has already emitted within a short window, defending
|
|
9
|
+
* against the "same content emitted by two near-simultaneous hooks" family
|
|
10
|
+
* (cf. the 0.1.48 SubagentStart double-inject). Also reused as a cooldown gate.
|
|
11
|
+
*
|
|
12
|
+
* Best-effort under true process-level concurrency (the read-modify-write is not
|
|
13
|
+
* locked); reliable for the sequential single-process callers that use it.
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { atomicWrite } from "../util/json-io";
|
|
19
|
+
import { defaultStateDir } from "./paths";
|
|
20
|
+
|
|
21
|
+
/** Default suppression window (ms): catches same-turn double-fire, not next-turn re-emits. */
|
|
22
|
+
export const DEDUP_WINDOW_MS = 3000;
|
|
23
|
+
|
|
24
|
+
/** Sidecar basename under the per-project state dir. */
|
|
25
|
+
const SIDECAR = "inject-dedup.json";
|
|
26
|
+
|
|
27
|
+
/** Optional overrides — tests inject a clock + a scratch dir; production omits both. */
|
|
28
|
+
export interface OnceOpts {
|
|
29
|
+
/** Clock in epoch-ms (default `Date.now()`). */
|
|
30
|
+
now?: number;
|
|
31
|
+
/** State-dir override (default {@link defaultStateDir}). */
|
|
32
|
+
dir?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Load the `{ key -> epochMs }` map, or `{}` when missing/corrupt. */
|
|
36
|
+
function loadMap(path: string): Record<string, number> {
|
|
37
|
+
try {
|
|
38
|
+
if (!existsSync(path)) return {};
|
|
39
|
+
const data: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
40
|
+
return data && typeof data === "object" && !Array.isArray(data) ? (data as Record<string, number>) : {};
|
|
41
|
+
} catch {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Keep only entries newer than `windowMs` before `now` (bounds sidecar size). */
|
|
47
|
+
function prune(map: Record<string, number>, now: number, windowMs: number): Record<string, number> {
|
|
48
|
+
const out: Record<string, number> = {};
|
|
49
|
+
for (const [k, t] of Object.entries(map)) {
|
|
50
|
+
if (typeof t === "number" && now - t < windowMs) out[k] = t;
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Cooldown gate. Returns `true` when `key` has NOT been recorded within the last
|
|
57
|
+
* `windowMs` (the caller MAY emit — and the emission is recorded now), or `false`
|
|
58
|
+
* when it was (the caller SHOULD suppress). The first call in a window wins;
|
|
59
|
+
* subsequent identical keys are throttled until the window elapses.
|
|
60
|
+
*
|
|
61
|
+
* Fails open: if the sidecar is unwritable, the emission is allowed rather than
|
|
62
|
+
* silently dropping context.
|
|
63
|
+
* @param key - Stable identity of the block (e.g. a content hash, or `lesson:<id>`).
|
|
64
|
+
* @param windowMs - Suppression window in ms.
|
|
65
|
+
* @param opts - Optional clock + state-dir overrides (for tests).
|
|
66
|
+
* @returns `true` to proceed/emit, `false` to suppress.
|
|
67
|
+
*/
|
|
68
|
+
export function oncePerWindow(key: string, windowMs: number, opts: OnceOpts = {}): boolean {
|
|
69
|
+
const now = opts.now ?? Date.now();
|
|
70
|
+
const path = join(opts.dir ?? defaultStateDir(), SIDECAR);
|
|
71
|
+
const map = prune(loadMap(path), now, windowMs);
|
|
72
|
+
const last = map[key];
|
|
73
|
+
if (typeof last === "number" && now - last < windowMs) return false;
|
|
74
|
+
map[key] = now;
|
|
75
|
+
try {
|
|
76
|
+
atomicWrite(path, JSON.stringify(map));
|
|
77
|
+
} catch {
|
|
78
|
+
/* sidecar unwritable → fail-open: allow the emission rather than lose context */
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Cross-process-exact-once variant (exclusive file creation, not the shared
|
|
85
|
+
* JSON above) for high-concurrency callers — split out to
|
|
86
|
+
* {@link module:inject-dedup-exclusive} to keep this file under the SOLID
|
|
87
|
+
* line budget; re-exported here so both modes share one import surface.
|
|
88
|
+
*/
|
|
89
|
+
export { onceExclusive } from "./inject-dedup-exclusive";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-agent dispatch tool detection — the single source of truth for "this
|
|
3
|
+
* tool call spawns a sub-agent". Claude Code and Codex dispatch via `Task`;
|
|
4
|
+
* Kimi Code CLI dispatches via `Agent` (and `AgentSwarm` for parallel fan-out).
|
|
5
|
+
* Centralized so the APEX task-context injection, the activity tracking and
|
|
6
|
+
* the PostToolUse evidence credit stop drifting on inline `"Task" | "Agent"`
|
|
7
|
+
* unions: every consumer imports from HERE, never re-declares the list.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Canonical set of sub-agent dispatch tool names (the ONLY definition). */
|
|
11
|
+
export const AGENT_TOOLS: ReadonlySet<string> = new Set(["Task", "Agent", "AgentSwarm"]);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* True when the tool is a sub-agent dispatch primitive on any harness.
|
|
15
|
+
* @param tool - The normalized tool name from the hook payload.
|
|
16
|
+
*/
|
|
17
|
+
export function isAgentTool(tool: string): boolean {
|
|
18
|
+
return AGENT_TOOLS.has(tool);
|
|
19
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-agent file attribution for SubagentStop. Uses the shared transcript parser
|
|
3
|
+
* ({@link readAgentToolUses}) to recover the exact set of files a sub-agent wrote
|
|
4
|
+
* via Write/Edit/MultiEdit/NotebookEdit, so the sniper reminder attributes only
|
|
5
|
+
* the files THIS agent touched instead of every file changed in the whole session
|
|
6
|
+
* (which cross-attributes other teammates' work — the bug this fixes).
|
|
7
|
+
*/
|
|
8
|
+
import { basename } from "node:path";
|
|
9
|
+
import { readAgentToolUses } from "./agent-transcript";
|
|
10
|
+
|
|
11
|
+
/** Tools whose `input.file_path`/`notebook_path` names a file the agent authored. */
|
|
12
|
+
const WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Return the distinct file paths written by the sub-agent whose own transcript is
|
|
16
|
+
* at `transcriptPath` (Write/Edit/MultiEdit/NotebookEdit). `null` when unreadable
|
|
17
|
+
* (caller MUST fall back to the session-wide list — no regression); a possibly-
|
|
18
|
+
* empty array when parsed (empty = authored nothing, so it should NOT be told to
|
|
19
|
+
* validate files other agents changed).
|
|
20
|
+
* @param transcriptPath - Absolute path to the sub-agent's own `.jsonl` transcript.
|
|
21
|
+
* @returns Distinct written paths (order-preserving), or `null` when unreadable.
|
|
22
|
+
*/
|
|
23
|
+
export function filesWrittenByAgent(transcriptPath: string | undefined): string[] | null {
|
|
24
|
+
const uses = readAgentToolUses(transcriptPath);
|
|
25
|
+
if (uses === null) return null;
|
|
26
|
+
const out: string[] = [];
|
|
27
|
+
const seen = new Set<string>();
|
|
28
|
+
for (const u of uses) {
|
|
29
|
+
if (!WRITE_TOOLS.has(u.name)) continue;
|
|
30
|
+
const fp = u.input?.file_path ?? u.input?.notebook_path;
|
|
31
|
+
if (typeof fp === "string" && fp && !seen.has(fp)) { seen.add(fp); out.push(fp); }
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Filter `sessionFiles` down to the ones this agent actually wrote. Matches the
|
|
38
|
+
* exact recorded string first, then tolerantly on `basename` so a relative-vs-
|
|
39
|
+
* absolute drift between the PostToolUse record and the transcript input never
|
|
40
|
+
* loses an owned file.
|
|
41
|
+
* @param sessionFiles - Session-wide modified files.
|
|
42
|
+
* @param written - Paths the agent wrote (from {@link filesWrittenByAgent}).
|
|
43
|
+
* @returns The subset of `sessionFiles` attributable to this agent.
|
|
44
|
+
*/
|
|
45
|
+
export function attributeFiles(sessionFiles: readonly string[], written: readonly string[]): string[] {
|
|
46
|
+
const exact = new Set(written);
|
|
47
|
+
const bases = new Set(written.map((f) => basename(f)));
|
|
48
|
+
return sessionFiles.filter((f) => exact.has(f) || bases.has(basename(f)));
|
|
49
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { contextResponse } from "../../adapters/claude";
|
|
5
|
+
import { resolveTtlSec } from "../../config/ttl";
|
|
6
|
+
import { loadSessionState, sanitizeSessionId, saveSessionState, sessionsDir } from "../home-state";
|
|
7
|
+
import { defaultStateDir, trackFile } from "../paths";
|
|
8
|
+
import { freshReceiptFromFile } from "../../tracking/receipts";
|
|
9
|
+
import { attributeFiles, filesWrittenByAgent } from "./agent-files";
|
|
10
|
+
|
|
11
|
+
/** The `changes` block written by `track-changes.ts` into unified session state. */
|
|
12
|
+
interface Changes {
|
|
13
|
+
cumulativeCodeFiles?: number;
|
|
14
|
+
modifiedFiles?: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** `~/.claude/memory/agents` — agent completion history dir. */
|
|
18
|
+
function memoryDir(home: string): string {
|
|
19
|
+
return join(home, ".claude", "memory", "agents");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const SKIP_AGENTS = /(sniper|sniper-faster|explore-codebase|research-expert|claude-code-guide|Explore|Plan)/;
|
|
23
|
+
|
|
24
|
+
/** Append the agent completion record to `agent-history.jsonl` (best effort). */
|
|
25
|
+
function recordHistory(home: string, agentId: string, agentType: string, ts: string): void {
|
|
26
|
+
const dir = memoryDir(home);
|
|
27
|
+
try {
|
|
28
|
+
mkdirSync(dir, { recursive: true });
|
|
29
|
+
appendFileSync(join(dir, "agent-history.jsonl"), JSON.stringify({ agentId, agentType, completedAt: ts }) + "\n", "utf-8");
|
|
30
|
+
} catch { /* best effort */ }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Handle SubagentStop: append the completion to agent-history.jsonl and, for a
|
|
35
|
+
* non-skipped agent that touched code, emit the sniper reminder + reset the
|
|
36
|
+
* counter. Ports `subagent-stop/track-agent-memory.py`.
|
|
37
|
+
* @param data - The raw hook payload.
|
|
38
|
+
* @param home - Home dir (defaults to `~`).
|
|
39
|
+
* @param now - Clock (defaults to `Date.now()`).
|
|
40
|
+
* @returns The native hook stdout (always a JSON message).
|
|
41
|
+
*/
|
|
42
|
+
export function trackAgentMemory(data: Record<string, unknown>, home: string = homedir(), now: number = Date.now()): string {
|
|
43
|
+
mkdirSync(sessionsDir(home), { recursive: true });
|
|
44
|
+
const agentType = String(data.agent_type ?? data.subagent_type ?? "unknown");
|
|
45
|
+
const sessionId = sanitizeSessionId(data.session_id) ?? "unknown";
|
|
46
|
+
const ts = new Date(now).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
47
|
+
recordHistory(home, String(data.agent_id ?? "unknown"), agentType, ts);
|
|
48
|
+
if (SKIP_AGENTS.test(agentType)) return JSON.stringify({ message: `Agent ${agentType} completed` });
|
|
49
|
+
const state = loadSessionState(sessionId, home);
|
|
50
|
+
const changes = state.changes as Changes | undefined;
|
|
51
|
+
const count = changes?.cumulativeCodeFiles ?? 0;
|
|
52
|
+
if (count > 0) {
|
|
53
|
+
// Attribute only the files THIS agent actually wrote, parsed from its own
|
|
54
|
+
// transcript (SubagentStop `agent_transcript_path`, CLI v2.0.42+). When the
|
|
55
|
+
// field is absent or the transcript is unreadable, `filesWrittenByAgent`
|
|
56
|
+
// returns null → fall back to the session-wide list (no regression).
|
|
57
|
+
const written = filesWrittenByAgent(typeof data.agent_transcript_path === "string" ? data.agent_transcript_path : undefined);
|
|
58
|
+
const owned = written === null ? (changes?.modifiedFiles ?? []) : attributeFiles(changes?.modifiedFiles ?? [], written);
|
|
59
|
+
// Only reset the session counter when this agent owns changes — so an agent
|
|
60
|
+
// that touched none of the tracked files never clears them for the real
|
|
61
|
+
// author, and never gets told to validate another teammate's work.
|
|
62
|
+
if (owned.length > 0) {
|
|
63
|
+
saveSessionState(sessionId, { ...state, changes: { ...changes, cumulativeCodeFiles: 0 } }, home);
|
|
64
|
+
// Never demand sniper on a file the agent deleted before stopping (e.g. a
|
|
65
|
+
// scratch probe rm -rf'd): the tracked list records writes, never removals,
|
|
66
|
+
// so re-check the disk (a relative path resolves against the hook cwd) and
|
|
67
|
+
// keep only files still present. All gone → nothing to validate → "no changes".
|
|
68
|
+
const hookCwd = typeof data.cwd === "string" ? data.cwd : process.cwd();
|
|
69
|
+
const present = owned.filter((f) => existsSync(resolve(hookCwd, f)));
|
|
70
|
+
if (present.length > 0) {
|
|
71
|
+
// Advisory (no hard block here — the TaskCompleted gate enforces): flag the
|
|
72
|
+
// missing proof when the agent owns code but posted no fresh passing receipt.
|
|
73
|
+
// Window = TTL×5, matching the TaskCompleted receipt gate.
|
|
74
|
+
const windowMs = resolveTtlSec(process.env) * 1000 * 5;
|
|
75
|
+
const noReceipt = freshReceiptFromFile(trackFile(sessionId, defaultStateDir(process.cwd())), windowMs, now) === null;
|
|
76
|
+
const note = noReceipt ? " NO VERIFICATION RECEIPT — run tsc + tests before reporting done." : "";
|
|
77
|
+
return contextResponse("SubagentStop", `SNIPER VALIDATION REQUIRED: Agent '${agentType}' modified ${present.length} code file(s): ${present.join(", ")}. Run sniper agent now.${note}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return JSON.stringify({ message: `Agent ${agentType} completed (no code changes)` });
|
|
82
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared low-level parser for a sub-agent's OWN transcript (`agent_transcript_path`
|
|
3
|
+
* — the clean JSONL the Claude Code platform writes per sub-agent, CLI v2.0.42+).
|
|
4
|
+
* Per LESSON.md this is the RELIABLE anchor for evidence: sidechain PostToolUse
|
|
5
|
+
* hooks are not guaranteed (issues #43612/#27655/#34692). One parser, reused by
|
|
6
|
+
* file-attribution (`agent-files.ts`) and retroactive evidence harvesting
|
|
7
|
+
* (`src/freshness/evidence-harvest.ts`) — no duplication.
|
|
8
|
+
*/
|
|
9
|
+
import { readText } from "../../util/runtime-io";
|
|
10
|
+
|
|
11
|
+
/** A tool_use content block inside a transcript message. */
|
|
12
|
+
interface ToolUseBlock {
|
|
13
|
+
type: string;
|
|
14
|
+
/** Tool name, e.g. "Write", "Edit", "WebSearch", "Grep". */
|
|
15
|
+
name?: string;
|
|
16
|
+
/** Raw tool input — shape varies per tool; narrowed at the call-site. */
|
|
17
|
+
input?: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** One JSONL line of a Claude Code transcript. */
|
|
21
|
+
interface TranscriptLine {
|
|
22
|
+
/** ISO-8601 or epoch-ms timestamp written by the platform (may be absent). */
|
|
23
|
+
timestamp?: string | number;
|
|
24
|
+
message?: { content?: unknown[] };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A parsed `tool_use`, carrying its message timestamp when the platform stamped one. */
|
|
28
|
+
export interface TranscriptToolUse {
|
|
29
|
+
/** Tool name, e.g. "Write", "WebSearch", "Read". */
|
|
30
|
+
name: string;
|
|
31
|
+
/** Raw tool input payload (shape varies per tool). */
|
|
32
|
+
input: Record<string, unknown> | undefined;
|
|
33
|
+
/** Epoch-ms of the enclosing message, or `undefined` when unstamped. */
|
|
34
|
+
ts?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Parse a raw `timestamp` field to epoch ms; `undefined` when absent or invalid. */
|
|
38
|
+
function parseTs(raw: string | number | undefined): number | undefined {
|
|
39
|
+
if (raw === undefined) return undefined;
|
|
40
|
+
if (typeof raw === "number") return raw;
|
|
41
|
+
const ms = Date.parse(raw);
|
|
42
|
+
return Number.isFinite(ms) ? ms : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Scan a sub-agent transcript into its ordered `tool_use` blocks (each with the
|
|
47
|
+
* enclosing message timestamp). Distinguishes "no tool use" from "cannot read":
|
|
48
|
+
* returns `null` when the path is absent or the transcript is unreadable (callers
|
|
49
|
+
* fail-open), or a possibly-empty array when parsed. Malformed lines/blocks are
|
|
50
|
+
* tolerated and skipped.
|
|
51
|
+
* @param transcriptPath - Absolute path to the sub-agent's own `.jsonl` transcript.
|
|
52
|
+
* @returns Ordered tool uses, or `null` when unreadable.
|
|
53
|
+
*/
|
|
54
|
+
export function readAgentToolUses(transcriptPath: string | undefined): TranscriptToolUse[] | null {
|
|
55
|
+
if (!transcriptPath) return null;
|
|
56
|
+
let text: string;
|
|
57
|
+
try {
|
|
58
|
+
text = readText(transcriptPath);
|
|
59
|
+
} catch {
|
|
60
|
+
return null; // fail-open: caller keeps its pre-harvest state
|
|
61
|
+
}
|
|
62
|
+
const out: TranscriptToolUse[] = [];
|
|
63
|
+
for (const line of text.split("\n")) {
|
|
64
|
+
if (!line.trim()) continue;
|
|
65
|
+
let entry: TranscriptLine;
|
|
66
|
+
try {
|
|
67
|
+
entry = JSON.parse(line) as TranscriptLine;
|
|
68
|
+
} catch {
|
|
69
|
+
continue; // tolerate malformed lines
|
|
70
|
+
}
|
|
71
|
+
const content = entry.message?.content;
|
|
72
|
+
if (!Array.isArray(content)) continue;
|
|
73
|
+
const ts = parseTs(entry.timestamp);
|
|
74
|
+
for (const block of content as ToolUseBlock[]) {
|
|
75
|
+
if (block?.type !== "tool_use" || !block.name) continue;
|
|
76
|
+
out.push({ name: block.name, input: block.input, ts });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache analytics for the ai-pilot scope: append hit/miss events to
|
|
3
|
+
* `sessions.jsonl` and aggregate them into `summary.json` on SessionEnd.
|
|
4
|
+
* Ports `cache-analytics-save.ts` + the `logCacheEvent` helper.
|
|
5
|
+
*/
|
|
6
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { readJsonFile, writeJsonFile } from "../../../util/json-io";
|
|
10
|
+
import { readText, writeText, pathExists } from "../../../util/runtime-io";
|
|
11
|
+
import { cacheBaseDir } from "./cache-base";
|
|
12
|
+
|
|
13
|
+
const TOKEN_WEIGHTS: Record<string, number> = { explore: 15000, doc: 10000, lessons: 3000, tests: 5000 };
|
|
14
|
+
const CATEGORIES = ["explore", "doc", "lessons", "tests"] as const;
|
|
15
|
+
|
|
16
|
+
/** A raw analytics event line in `sessions.jsonl`. */
|
|
17
|
+
export interface SessionEntry { ts: string; type: string; action: string; session?: string }
|
|
18
|
+
|
|
19
|
+
/** Aggregated cache analytics persisted to `summary.json`. */
|
|
20
|
+
export interface Summary {
|
|
21
|
+
updated: string;
|
|
22
|
+
total_sessions: number;
|
|
23
|
+
cache_hits: Record<string, number>;
|
|
24
|
+
cache_misses: Record<string, number>;
|
|
25
|
+
hit_rates: Record<string, string>;
|
|
26
|
+
estimated_tokens_saved: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Append a single cache event to `analytics/sessions.jsonl` (best effort). */
|
|
30
|
+
export function logCacheEvent(type: string, action: string, projHash: string, extra: Record<string, unknown> = {}, home: string = homedir()): void {
|
|
31
|
+
try {
|
|
32
|
+
const dir = join(cacheBaseDir(home), "analytics");
|
|
33
|
+
mkdirSync(dir, { recursive: true });
|
|
34
|
+
const entry = { ts: new Date().toISOString(), session: String(Math.floor(Date.now() / 1000)), type, action, project_hash: projHash, ...extra };
|
|
35
|
+
appendFileSync(join(dir, "sessions.jsonl"), JSON.stringify(entry) + "\n");
|
|
36
|
+
} catch { /* best effort */ }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Count entries matching a `type`+`action`. */
|
|
40
|
+
function countBy(entries: SessionEntry[], type: string, action: string): number {
|
|
41
|
+
return entries.filter((e) => e.type === type && e.action === action).length;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Format a hit-rate percentage string. */
|
|
45
|
+
function hitRate(hits: number, misses: number): string {
|
|
46
|
+
const total = hits + misses;
|
|
47
|
+
return total === 0 ? "0%" : `${Math.floor((hits * 100) / total)}%`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Parse the JSONL session log into typed entries. */
|
|
51
|
+
function parseEntries(raw: string): SessionEntry[] {
|
|
52
|
+
return raw.split("\n").filter(Boolean)
|
|
53
|
+
.map((line) => { try { return JSON.parse(line) as SessionEntry; } catch { return null; } })
|
|
54
|
+
.filter((e): e is SessionEntry => e !== null);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Aggregate `sessions.jsonl` into `summary.json` and prune entries > 30 days.
|
|
59
|
+
* Ports `cache-analytics-save.ts`. SessionEnd hook output is ignored by Claude,
|
|
60
|
+
* so this returns nothing — it is a pure side-effect.
|
|
61
|
+
* @param home - Home dir (defaults to `~`).
|
|
62
|
+
* @param now - Clock (defaults to `Date.now()`).
|
|
63
|
+
*/
|
|
64
|
+
export async function cacheAnalyticsSave(home: string = homedir(), now: number = Date.now()): Promise<void> {
|
|
65
|
+
const dir = join(cacheBaseDir(home), "analytics");
|
|
66
|
+
const sessionsFile = join(dir, "sessions.jsonl");
|
|
67
|
+
if (!pathExists(sessionsFile)) return;
|
|
68
|
+
const raw = readText(sessionsFile);
|
|
69
|
+
if (!raw.trim()) return;
|
|
70
|
+
const entries = parseEntries(raw);
|
|
71
|
+
if (entries.length === 0) return;
|
|
72
|
+
|
|
73
|
+
const hits: Record<string, number> = {};
|
|
74
|
+
const misses: Record<string, number> = {};
|
|
75
|
+
for (const cat of CATEGORIES) {
|
|
76
|
+
hits[cat] = countBy(entries, cat, "hit") + (cat === "doc" ? countBy(entries, cat, "blocked") : 0);
|
|
77
|
+
misses[cat] = countBy(entries, cat, "miss");
|
|
78
|
+
}
|
|
79
|
+
const tokensSaved = CATEGORIES.reduce((sum, c) => sum + (hits[c] ?? 0) * (TOKEN_WEIGHTS[c] ?? 0), 0);
|
|
80
|
+
const sessionCount = new Set(entries.map((e) => e.session).filter(Boolean)).size;
|
|
81
|
+
|
|
82
|
+
const old = await readJsonFile<Summary>(join(dir, "summary.json"));
|
|
83
|
+
const merged: Summary = {
|
|
84
|
+
updated: new Date(now).toISOString(),
|
|
85
|
+
total_sessions: (old?.total_sessions ?? 0) + sessionCount,
|
|
86
|
+
cache_hits: {}, cache_misses: {}, hit_rates: {},
|
|
87
|
+
estimated_tokens_saved: (old?.estimated_tokens_saved ?? 0) + tokensSaved,
|
|
88
|
+
};
|
|
89
|
+
for (const cat of CATEGORIES) {
|
|
90
|
+
merged.cache_hits[cat] = (old?.cache_hits?.[cat] ?? 0) + (hits[cat] ?? 0);
|
|
91
|
+
merged.cache_misses[cat] = (old?.cache_misses?.[cat] ?? 0) + (misses[cat] ?? 0);
|
|
92
|
+
merged.hit_rates[cat] = hitRate(merged.cache_hits[cat] ?? 0, merged.cache_misses[cat] ?? 0);
|
|
93
|
+
}
|
|
94
|
+
await writeJsonFile(join(dir, "summary.json"), merged, true);
|
|
95
|
+
|
|
96
|
+
const cutoff = new Date(now - 30 * 86_400_000).toISOString();
|
|
97
|
+
const kept = entries.filter((e) => e.ts >= cutoff);
|
|
98
|
+
writeText(sessionsFile, kept.map((e) => JSON.stringify(e)).join("\n") + "\n");
|
|
99
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* APEX `task.json` mutation helpers + a directory-based lock for the ai-pilot
|
|
3
|
+
* scope. Ported from the ai-pilot plugin's `apex/state.ts` + `apex/task-helpers.ts`
|
|
4
|
+
* (now removed).
|
|
5
|
+
*/
|
|
6
|
+
import { mkdir, rmdir } from "node:fs/promises";
|
|
7
|
+
import { readJsonFile, writeJsonFile } from "../../../util/json-io";
|
|
8
|
+
import { sleep } from "../../../util/runtime-io";
|
|
9
|
+
import type { ApexTaskFile } from "./types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Acquire a directory-based lock with a timeout.
|
|
13
|
+
* @param lockDir - Path to the lock directory.
|
|
14
|
+
* @param timeoutMs - Max wait in ms (default 5000).
|
|
15
|
+
* @returns A release function, or null if acquisition timed out.
|
|
16
|
+
*/
|
|
17
|
+
export async function acquireLock(lockDir: string, timeoutMs = 5000): Promise<(() => Promise<void>) | null> {
|
|
18
|
+
const start = Date.now();
|
|
19
|
+
while (Date.now() - start < timeoutMs) {
|
|
20
|
+
try {
|
|
21
|
+
await mkdir(lockDir, { recursive: false });
|
|
22
|
+
return async () => { try { await rmdir(lockDir); } catch { /* noop */ } };
|
|
23
|
+
} catch { await sleep(100); }
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Add a new task to `task.json`. */
|
|
29
|
+
export async function taskCreate(file: string, id: string, subject: string, desc: string): Promise<void> {
|
|
30
|
+
const data = await readJsonFile<ApexTaskFile>(file);
|
|
31
|
+
if (!data) return;
|
|
32
|
+
data.tasks[id] = {
|
|
33
|
+
subject, description: desc, status: "pending", phase: "pending",
|
|
34
|
+
created_at: new Date().toISOString(), doc_consulted: {}, files_modified: [], blockedBy: [],
|
|
35
|
+
};
|
|
36
|
+
await writeJsonFile(file, data);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Mark a task as in_progress in `task.json`. */
|
|
40
|
+
export async function taskStart(file: string, id: string, subject?: string, desc?: string, blocked?: string): Promise<void> {
|
|
41
|
+
const data = await readJsonFile<ApexTaskFile>(file);
|
|
42
|
+
if (!data) return;
|
|
43
|
+
if (!data.tasks[id]) {
|
|
44
|
+
data.tasks[id] = { subject: "", description: "", status: "in_progress", phase: "analyze", doc_consulted: {}, files_modified: [] };
|
|
45
|
+
}
|
|
46
|
+
data.current_task = id;
|
|
47
|
+
const task = data.tasks[id];
|
|
48
|
+
Object.assign(task, { status: "in_progress", phase: "analyze", started_at: new Date().toISOString() });
|
|
49
|
+
if (subject) task.subject = subject;
|
|
50
|
+
if (desc) task.description = desc;
|
|
51
|
+
if (blocked) task.blockedBy = blocked.split(",");
|
|
52
|
+
await writeJsonFile(file, data);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Mark a task as completed in `task.json`. */
|
|
56
|
+
export async function taskComplete(file: string, id: string): Promise<void> {
|
|
57
|
+
const data = await readJsonFile<ApexTaskFile>(file);
|
|
58
|
+
if (!data?.tasks[id]) return;
|
|
59
|
+
Object.assign(data.tasks[id], { status: "completed", phase: "completed", completed_at: new Date().toISOString() });
|
|
60
|
+
await writeJsonFile(file, data);
|
|
61
|
+
}
|