@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,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module codex-post-failure
|
|
3
|
+
* Codex has no native `PostToolUseFailure` event (Claude Code's own failure
|
|
4
|
+
* signal, ported in `src/runtime/lifecycle/tool-failure.ts`'s `logToolFailure`)
|
|
5
|
+
* — every Codex tool outcome, success or failure, arrives on the SAME
|
|
6
|
+
* `PostToolUse` event. This infers a failure from the regular payload's
|
|
7
|
+
* `tool_result`/`tool_response` and journals it into the EXISTING one-shot
|
|
8
|
+
* failure tally ({@link recordFailure}) — no new store, no new gate.
|
|
9
|
+
*
|
|
10
|
+
* Design choice (fail-open): a payload this module cannot positively prove
|
|
11
|
+
* failed — `null`/`undefined`, a non-object shape, or any parse exception —
|
|
12
|
+
* classifies as `"success"` (ignored), NEVER `"failure"`. Many legitimate
|
|
13
|
+
* successful tool calls return no structured result at all (see
|
|
14
|
+
* `handle-post.ts`'s `spawn_agent` `tool_response: { nickname: "scout" }`),
|
|
15
|
+
* so treating "can't tell" as a failure would flood the tally with false
|
|
16
|
+
* positives. Only an EXPLICIT signal (non-zero exit, an error field) counts.
|
|
17
|
+
* @packageDocumentation
|
|
18
|
+
*/
|
|
19
|
+
import { recordFailure } from "./one-shot-failure";
|
|
20
|
+
import type { OneShotOpts } from "./one-shot";
|
|
21
|
+
|
|
22
|
+
/** Truthy interruption markers across harness result shapes (parity `tool-failure.ts`'s `is_interrupt`). */
|
|
23
|
+
const INTERRUPT_KEYS = ["is_interrupt", "isInterrupted", "interrupted", "aborted", "cancelled", "canceled"];
|
|
24
|
+
|
|
25
|
+
/** True when `result` carries any known interruption marker — an interruption is never a failure. */
|
|
26
|
+
function isInterruption(result: Record<string, unknown>): boolean {
|
|
27
|
+
return INTERRUPT_KEYS.some((k) => result[k] === true);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** True when `result` reports a non-zero exit code (Bash-shaped tool result). */
|
|
31
|
+
function hasNonZeroExit(result: Record<string, unknown>): boolean {
|
|
32
|
+
const exit = result.exit_code ?? result.exitCode;
|
|
33
|
+
return typeof exit === "number" && exit !== 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** True when `result` carries an explicit error signal (`error`, `is_error`/`isError`, or `success: false`). */
|
|
37
|
+
function hasErrorField(result: Record<string, unknown>): boolean {
|
|
38
|
+
if (result.success === false) return true;
|
|
39
|
+
if (result.is_error === true || result.isError === true) return true;
|
|
40
|
+
const err = result.error;
|
|
41
|
+
return typeof err === "string" ? err.length > 0 : err !== undefined && err !== null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Classify a Codex PostToolUse `tool_response`/`tool_result` payload.
|
|
46
|
+
* Fail-open (see module doc): unprovable shapes resolve to `"success"`,
|
|
47
|
+
* never `"failure"`, and this NEVER throws.
|
|
48
|
+
* @param result - The raw result value off the payload (any shape).
|
|
49
|
+
* @returns `"success"` | `"failure"` | `"interrupted"`.
|
|
50
|
+
*/
|
|
51
|
+
export function classifyCodexOutcome(result: unknown): "success" | "failure" | "interrupted" {
|
|
52
|
+
try {
|
|
53
|
+
if (result === null || typeof result !== "object" || Array.isArray(result)) return "success";
|
|
54
|
+
const r = result as Record<string, unknown>;
|
|
55
|
+
if (isInterruption(r)) return "interrupted";
|
|
56
|
+
if (hasNonZeroExit(r) || hasErrorField(r)) return "failure";
|
|
57
|
+
return "success";
|
|
58
|
+
} catch {
|
|
59
|
+
return "success";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Journal a Codex tool failure into the existing one-shot failure tally.
|
|
65
|
+
* No-op for `"success"`/`"interrupted"` — only a genuine failure is recorded.
|
|
66
|
+
* @param tool - The tool name.
|
|
67
|
+
* @param result - The raw PostToolUse result payload.
|
|
68
|
+
* @param opts - Clock + state dir (+ optional session id), forwarded as-is to {@link recordFailure}.
|
|
69
|
+
*/
|
|
70
|
+
export function recordCodexPostFailure(tool: string, result: unknown, opts: OneShotOpts): void {
|
|
71
|
+
if (classifyCodexOutcome(result) !== "failure") return;
|
|
72
|
+
recordFailure(tool, opts);
|
|
73
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module integrity
|
|
3
|
+
* HMAC-SHA256 tamper-evident wrapping for {@link SessionTrack}.
|
|
4
|
+
*
|
|
5
|
+
* Failure policy: ONLY a MAC mismatch causes fail-closed (returns null).
|
|
6
|
+
* The nonce is included in the signed payload and written to disk for advisory
|
|
7
|
+
* diagnostics, but is NOT checked during verification — concurrent hook
|
|
8
|
+
* invocations (PostToolUse, SubagentStop, …) legitimately load the same
|
|
9
|
+
* envelope after the nonce watermark advances; a monotonic check would trigger
|
|
10
|
+
* spurious fail-closed behaviour mid-session.
|
|
11
|
+
*
|
|
12
|
+
* The machine key is readable by the same agent process, so this deters naive
|
|
13
|
+
* out-of-band tampering — not a determined re-sign. Item B (transcript-grounded
|
|
14
|
+
* freshness) is the primary guarantee.
|
|
15
|
+
* @packageDocumentation
|
|
16
|
+
*/
|
|
17
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { fuseHarnessHome } from "../runtime/home-state";
|
|
21
|
+
import type { SessionTrack } from "./session-state";
|
|
22
|
+
|
|
23
|
+
const HARNESS_DIR = fuseHarnessHome();
|
|
24
|
+
const KEY_PATH = join(HARNESS_DIR, ".key");
|
|
25
|
+
const NONCE_PATH = join(HARNESS_DIR, ".nonce");
|
|
26
|
+
|
|
27
|
+
/** Signed envelope wrapping a JSON-serialised {@link SessionTrack}. */
|
|
28
|
+
export interface TrackEnvelope {
|
|
29
|
+
/** JSON-serialised SessionTrack. */
|
|
30
|
+
data: string;
|
|
31
|
+
/** Advisory timestamp (Date.now() at sign time); embedded in the MAC. */
|
|
32
|
+
nonce: number;
|
|
33
|
+
/** HMAC-SHA256 hex digest over `"${nonce}:${data}"`. */
|
|
34
|
+
mac: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Load (or create on first use) the per-machine HMAC key stored at mode 0600. */
|
|
38
|
+
export function loadOrCreateKey(): string {
|
|
39
|
+
mkdirSync(HARNESS_DIR, { recursive: true });
|
|
40
|
+
if (existsSync(KEY_PATH)) return readFileSync(KEY_PATH, "utf8").trim();
|
|
41
|
+
// Race-tolerant creation ("wx" crowns ONE creator): before, N concurrent
|
|
42
|
+
// first-time processes (virgin home — CI) each signed with their OWN key.
|
|
43
|
+
const candidate = randomBytes(32).toString("hex");
|
|
44
|
+
try {
|
|
45
|
+
writeFileSync(KEY_PATH, candidate, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
46
|
+
return candidate;
|
|
47
|
+
} catch (e) {
|
|
48
|
+
if ((e as NodeJS.ErrnoException).code !== "EEXIST") throw e;
|
|
49
|
+
}
|
|
50
|
+
// The winner's 64-byte write is one write(2); bounded re-reads cover the gap.
|
|
51
|
+
for (let i = 0; i < 5; i++) {
|
|
52
|
+
const existing = readFileSync(KEY_PATH, "utf8").trim();
|
|
53
|
+
if (existing) return existing;
|
|
54
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
|
|
55
|
+
}
|
|
56
|
+
const key = readFileSync(KEY_PATH, "utf8").trim();
|
|
57
|
+
if (!key) throw new Error("fuse: empty .key after write contention");
|
|
58
|
+
return key;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Persist the last-seen nonce for advisory diagnostics (mode 0600).
|
|
63
|
+
* Never read during {@link verifyTrack} — see module JSDoc for rationale.
|
|
64
|
+
*/
|
|
65
|
+
export function writeLastNonce(nonce: number): void {
|
|
66
|
+
mkdirSync(HARNESS_DIR, { recursive: true });
|
|
67
|
+
writeFileSync(NONCE_PATH, String(nonce), { encoding: "utf8", mode: 0o600 });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Compute HMAC-SHA256 over `"${nonce}:${data}"` (nonce: envelope number or journal per-line string). */
|
|
71
|
+
export function computeMac(key: string, data: string, nonce: number | string): string {
|
|
72
|
+
return createHmac("sha256", key).update(`${nonce}:${data}`).digest("hex");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Sign a {@link SessionTrack} into a tamper-evident envelope. The nonce
|
|
77
|
+
* (`Date.now()`) is embedded in the MAC to bind the timestamp to the payload.
|
|
78
|
+
*/
|
|
79
|
+
export function signTrack(track: SessionTrack): TrackEnvelope {
|
|
80
|
+
const key = loadOrCreateKey();
|
|
81
|
+
const data = JSON.stringify(track);
|
|
82
|
+
const nonce = Date.now();
|
|
83
|
+
return { data, nonce, mac: computeMac(key, data, nonce) };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Verify a {@link TrackEnvelope}. Returns the parsed {@link SessionTrack} on
|
|
88
|
+
* success, or `null` ONLY on MAC mismatch / parse failure (fail-closed on
|
|
89
|
+
* tampering). The nonce is NOT checked — see module JSDoc.
|
|
90
|
+
*/
|
|
91
|
+
export function verifyTrack(envelope: TrackEnvelope): SessionTrack | null {
|
|
92
|
+
try {
|
|
93
|
+
const key = loadOrCreateKey();
|
|
94
|
+
if (envelope.mac !== computeMac(key, envelope.data, envelope.nonce)) return null;
|
|
95
|
+
return JSON.parse(envelope.data) as SessionTrack;
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module one-shot-dedup
|
|
3
|
+
* Burst-dedup guard for the one-shot metric ({@link module:one-shot}).
|
|
4
|
+
*
|
|
5
|
+
* ONE Claude tool event fans out to ~11 sibling plugin-hook processes, each
|
|
6
|
+
* calling {@link recordOneShot}; without this the metric would count a single
|
|
7
|
+
* deny/allow ~11×. Reuses the proven {@link oncePerWindow} cooldown sidecar:
|
|
8
|
+
* the FIRST process in the {@link module:burst-window} window mutates the
|
|
9
|
+
* metric, the rest skip. The dedup key includes the outcome KIND (deny-title vs
|
|
10
|
+
* allow) so a deny and its later fix — different kinds — are never folded into
|
|
11
|
+
* each other. No `sessionId` → always the first (mono-process + unit-test
|
|
12
|
+
* parity; a burst can only exist when a real session drives the fan-out).
|
|
13
|
+
* @packageDocumentation
|
|
14
|
+
*/
|
|
15
|
+
import { oncePerWindow } from "../runtime/inject-dedup";
|
|
16
|
+
import { BURST_DEDUP_MS } from "../runtime/burst-window";
|
|
17
|
+
|
|
18
|
+
/** Opts needed to decide + persist a burst claim (a structural subset of `OneShotOpts`). */
|
|
19
|
+
export interface BurstOpts { now: number; dir: string; sessionId?: string; }
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* True when this `(op, kind)` is the FIRST of its burst for the session — the
|
|
23
|
+
* process that should actually mutate the metric. Sibling processes firing the
|
|
24
|
+
* SAME event within {@link BURST_DEDUP_MS} return false and skip the write.
|
|
25
|
+
* @param op - Content-free operation key ({@link denyHash}("op", …)).
|
|
26
|
+
* @param kind - Outcome discriminator (`deny:<title>` or `allow`).
|
|
27
|
+
* @param opts - Clock + state dir + optional session id.
|
|
28
|
+
* @returns `true` to apply the record, `false` to skip (already counted).
|
|
29
|
+
*/
|
|
30
|
+
export function burstFirst(op: string, kind: string, opts: BurstOpts): boolean {
|
|
31
|
+
const sid = opts.sessionId?.trim();
|
|
32
|
+
if (!sid) return true;
|
|
33
|
+
return oncePerWindow(`oneshot:${sid}:${op}:${kind}`, BURST_DEDUP_MS, { now: opts.now, dir: opts.dir });
|
|
34
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module one-shot-failure
|
|
3
|
+
* PostToolUseFailure tally for the one-shot sidecar. A pure per-tool counter
|
|
4
|
+
* ({@link applyFailure}) plus its burst-deduped IO writer ({@link recordFailure}),
|
|
5
|
+
* kept out of {@link module:one-shot-store} so that file stays under the SOLID
|
|
6
|
+
* size limit. Reuses the SAME sidecar, prune window, and burst-dedup as
|
|
7
|
+
* {@link module:one-shot} — failures never touch the deny/allow gate rate.
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { atomicWrite } from "../util/json-io";
|
|
12
|
+
import { pruneState, type OneShotState } from "./one-shot-store";
|
|
13
|
+
import { SIDECAR, WINDOW_MS, loadState, type OneShotOpts } from "./one-shot";
|
|
14
|
+
import { burstFirst } from "./one-shot-dedup";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Bump the per-tool failure count. Orthogonal to gates/pending, so it can never
|
|
18
|
+
* distort the one-shot rate.
|
|
19
|
+
* @param s - Current state.
|
|
20
|
+
* @param tool - The failing tool name (`unknown` when absent).
|
|
21
|
+
* @param now - Clock.
|
|
22
|
+
* @returns The next state with `failures[tool]` incremented.
|
|
23
|
+
*/
|
|
24
|
+
export function applyFailure(s: OneShotState, tool: string, now: number): OneShotState {
|
|
25
|
+
return { ...s, failures: { ...(s.failures ?? {}), [tool]: (s.failures?.[tool] ?? 0) + 1 }, updatedAt: now };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Persist a PostToolUseFailure into the one-shot sidecar, burst-deduped across the
|
|
30
|
+
* ~11-process fan-out (same window/store as {@link recordOneShot}). Fail-safe: a
|
|
31
|
+
* write error never propagates out of the hook.
|
|
32
|
+
* @param tool - The failing tool name.
|
|
33
|
+
* @param opts - Clock + state dir + optional session id (arms the burst dedup).
|
|
34
|
+
*/
|
|
35
|
+
export function recordFailure(tool: string, opts: OneShotOpts): void {
|
|
36
|
+
try {
|
|
37
|
+
if (!burstFirst(`fail:${tool}`, "failure", opts)) return;
|
|
38
|
+
const path = join(opts.dir, SIDECAR);
|
|
39
|
+
const next = applyFailure(pruneState(loadState(path), opts.now, WINDOW_MS), tool, opts.now);
|
|
40
|
+
atomicWrite(path, JSON.stringify(next));
|
|
41
|
+
} catch { /* fail-safe: metrics never break the hook */ }
|
|
42
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module one-shot-store
|
|
3
|
+
* Pure, IO-free state model for the per-gate one-shot metric ({@link module:one-shot}).
|
|
4
|
+
*
|
|
5
|
+
* Measures the owner's "precision / one-shot" criterion: per gate (keyed by the
|
|
6
|
+
* block prompt's title) how many times it DENIED and how many were later cleared
|
|
7
|
+
* by an allow of the same operation (a fix). Globally: `firstTry` (a gateable op
|
|
8
|
+
* that passed with NO prior deny = one-shot) vs `corrected` (passed after a deny).
|
|
9
|
+
* The store ({@link module:one-shot}) loads, persists, and wires this into the gate.
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Per-gate tally: total denies, denies later fixed by an allow, last-seen clock. */
|
|
14
|
+
export interface GateStat { denies: number; corrected: number; lastTs: number; }
|
|
15
|
+
|
|
16
|
+
/** One open deny awaiting its fix: the blocking gate title and when it landed. */
|
|
17
|
+
export interface Pending { title: string; ts: number; }
|
|
18
|
+
|
|
19
|
+
/** Aggregate one-shot state (a sidecar JSON). `pending` links a deny to its later allow. */
|
|
20
|
+
export interface OneShotState {
|
|
21
|
+
gates: Record<string, GateStat>;
|
|
22
|
+
firstTry: number;
|
|
23
|
+
corrected: number;
|
|
24
|
+
pending: Record<string, Pending>;
|
|
25
|
+
updatedAt: number;
|
|
26
|
+
/** Per-tool PostToolUseFailure tally. Optional — absent in pre-0.1.57 sidecars (additive, back-compatible). */
|
|
27
|
+
failures?: Record<string, number>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A fresh, empty state — always spread (`{ ...EMPTY }`) so the const is never shared. */
|
|
31
|
+
export const EMPTY: OneShotState = { gates: {}, firstTry: 0, corrected: 0, pending: {}, updatedAt: 0 };
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Drop stale data: whole-state idle reset past the window, else per-entry prune of
|
|
35
|
+
* gates/pending older than `windowMs`. Keeps the "7d" window honest, bounds size.
|
|
36
|
+
*/
|
|
37
|
+
export function pruneState(s: OneShotState, now: number, windowMs: number): OneShotState {
|
|
38
|
+
if (now - s.updatedAt >= windowMs) return { ...EMPTY };
|
|
39
|
+
const gates: Record<string, GateStat> = {};
|
|
40
|
+
for (const [k, g] of Object.entries(s.gates)) if (now - g.lastTs < windowMs) gates[k] = g;
|
|
41
|
+
const pending: Record<string, Pending> = {};
|
|
42
|
+
for (const [k, p] of Object.entries(s.pending)) if (now - p.ts < windowMs) pending[k] = p;
|
|
43
|
+
return { ...s, gates, pending };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Record a deny for gate `title` on operation `op` (content-free tool identity):
|
|
48
|
+
* bump the gate's deny count and mark `op` pending for a later fix.
|
|
49
|
+
*/
|
|
50
|
+
export function applyDeny(s: OneShotState, title: string, op: string, now: number): OneShotState {
|
|
51
|
+
const g = s.gates[title] ?? { denies: 0, corrected: 0, lastTs: 0 };
|
|
52
|
+
return {
|
|
53
|
+
...s,
|
|
54
|
+
gates: { ...s.gates, [title]: { denies: g.denies + 1, corrected: g.corrected, lastTs: now } },
|
|
55
|
+
pending: { ...s.pending, [op]: { title, ts: now } },
|
|
56
|
+
updatedAt: now,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Record an allow for a gateable `op`. A non-gateable allow (Read/Task/MCP) leaves
|
|
62
|
+
* state untouched — it never counts and never clears a pending deny. Otherwise: a
|
|
63
|
+
* pending deny → `corrected` (a fix, credited to the blocking gate); no pending →
|
|
64
|
+
* `firstTry` (one-shot).
|
|
65
|
+
*/
|
|
66
|
+
export function applyAllow(s: OneShotState, op: string, now: number, gateable: boolean): OneShotState {
|
|
67
|
+
if (!gateable) return s;
|
|
68
|
+
const pend = s.pending[op];
|
|
69
|
+
if (pend) {
|
|
70
|
+
const g = s.gates[pend.title] ?? { denies: 0, corrected: 0, lastTs: 0 };
|
|
71
|
+
const { [op]: _drop, ...pending } = s.pending;
|
|
72
|
+
return {
|
|
73
|
+
...s,
|
|
74
|
+
gates: { ...s.gates, [pend.title]: { ...g, corrected: g.corrected + 1, lastTs: now } },
|
|
75
|
+
corrected: s.corrected + 1, pending, updatedAt: now,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return gateable ? { ...s, firstTry: s.firstTry + 1, updatedAt: now } : s;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Compact injectable summary (one line); "" when there is nothing to report.
|
|
83
|
+
* @returns e.g. `gates 7d: 88% one-shot (44/50 clean); SOLID file-size limit 4den/3fix`.
|
|
84
|
+
*/
|
|
85
|
+
export function formatSummary(s: OneShotState): string {
|
|
86
|
+
const keys = Object.keys(s.gates);
|
|
87
|
+
const total = s.firstTry + s.corrected;
|
|
88
|
+
if (keys.length === 0 && total === 0) return "";
|
|
89
|
+
const head = total > 0 ? `${Math.round((s.firstTry / total) * 100)}% one-shot (${s.firstTry}/${total} clean)` : "no clean pass yet";
|
|
90
|
+
const parts = keys
|
|
91
|
+
.map((k) => ({ k, g: s.gates[k]! }))
|
|
92
|
+
.sort((a, b) => b.g.denies - a.g.denies)
|
|
93
|
+
.map(({ k, g }) => `${k} ${g.denies}den/${g.corrected}fix`);
|
|
94
|
+
return `gates 7d: ${head}${parts.length ? `; ${parts.join("; ")}` : ""}`;
|
|
95
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module one-shot
|
|
3
|
+
* Sidecar store + gate wiring for the per-gate one-shot metric.
|
|
4
|
+
*
|
|
5
|
+
* STATE — a standalone sidecar (`one-shot.json`) in the same per-project state dir
|
|
6
|
+
* as the session track, mirroring {@link module:deny-loop-store} (atomicWrite,
|
|
7
|
+
* prune-by-window, fail-safe). A write error NEVER changes a gate decision nor its
|
|
8
|
+
* prompt — metrics are pure observation.
|
|
9
|
+
*
|
|
10
|
+
* KEY — the operation identity is content-FREE (`tool + filePath/command`): a fix
|
|
11
|
+
* changes the content, so a content hash would make every retry a new op and hide
|
|
12
|
+
* the deny→allow transition this metric exists to see. The pure model lives in
|
|
13
|
+
* {@link module:one-shot-store}; this file is the only IO surface.
|
|
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 { denyHash } from "../policy/deny-loop";
|
|
20
|
+
import { defaultStateDir } from "../runtime/paths";
|
|
21
|
+
import { applyAllow, applyDeny, EMPTY, formatSummary, pruneState, type OneShotState } from "./one-shot-store";
|
|
22
|
+
import { burstFirst } from "./one-shot-dedup";
|
|
23
|
+
import type { Prompt } from "../prompt/types";
|
|
24
|
+
|
|
25
|
+
/** Sidecar basename under the per-project state dir. */
|
|
26
|
+
export const SIDECAR = "one-shot.json";
|
|
27
|
+
|
|
28
|
+
/** Retention window: 7 days. Aggregates and pending denies older than this are pruned. */
|
|
29
|
+
export const WINDOW_MS: number = 7 * 24 * 60 * 60 * 1000;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Injected clock + state dir (no env var; the gate supplies both). `sessionId` —
|
|
33
|
+
* when present — arms the burst dedup ({@link module:one-shot-dedup}) so the ~11
|
|
34
|
+
* sibling plugin hooks of ONE event count once, not once each. Omitted by
|
|
35
|
+
* mono-process callers/tests → un-deduped historical behaviour.
|
|
36
|
+
*/
|
|
37
|
+
export interface OneShotOpts { now: number; dir: string; sessionId?: string; }
|
|
38
|
+
|
|
39
|
+
/** Identifying tool input (content-free); `content` only decides gateability. */
|
|
40
|
+
export interface OneShotInput { filePath?: string; content?: string; command?: string; }
|
|
41
|
+
|
|
42
|
+
/** Load the state, or a fresh copy when missing/corrupt. */
|
|
43
|
+
export function loadState(path: string): OneShotState {
|
|
44
|
+
try {
|
|
45
|
+
if (!existsSync(path)) return { ...EMPTY };
|
|
46
|
+
const d: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
47
|
+
return d && typeof d === "object" && !Array.isArray(d) ? { ...EMPTY, ...(d as OneShotState) } : { ...EMPTY };
|
|
48
|
+
} catch {
|
|
49
|
+
return { ...EMPTY };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Record a gate outcome: a `block` is a deny for its gate title; a `null` allow is
|
|
55
|
+
* a fix (if the op was pending) or a one-shot (if gateable). `ask`/`inform` are
|
|
56
|
+
* neither and are skipped. Fails silently — a metric write NEVER affects a decision.
|
|
57
|
+
*
|
|
58
|
+
* The op key is tool-INDEPENDENT (`filePath`/`command` only, constant `"op"` tool):
|
|
59
|
+
* a deny (a `Write`) and its fix (an `Edit`) on the same file must link.
|
|
60
|
+
* @param prompt - The gate's outcome (block, allow=null, or ask/inform).
|
|
61
|
+
* @param input - Identifying tool input (content decides gateability only).
|
|
62
|
+
* @param opts - Clock + state dir.
|
|
63
|
+
*/
|
|
64
|
+
export function recordOneShot(prompt: Prompt | null, input: OneShotInput, opts: OneShotOpts): void {
|
|
65
|
+
try {
|
|
66
|
+
if (prompt && prompt.kind !== "block") return;
|
|
67
|
+
const op = denyHash("op", { filePath: input.filePath, command: input.command });
|
|
68
|
+
if (!burstFirst(op, prompt ? `deny:${prompt.title}` : "allow", opts)) return;
|
|
69
|
+
const path = join(opts.dir, SIDECAR);
|
|
70
|
+
let s = pruneState(loadState(path), opts.now, WINDOW_MS);
|
|
71
|
+
s = prompt
|
|
72
|
+
? applyDeny(s, prompt.title, op, opts.now)
|
|
73
|
+
: applyAllow(s, op, opts.now, input.content != null || input.command != null);
|
|
74
|
+
atomicWrite(path, JSON.stringify(s));
|
|
75
|
+
} catch { /* fail-safe: metrics never change a gate decision */ }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Compact, injection-ready one-shot summary for the project rooted at `cwd`. The
|
|
80
|
+
* state dir is derived EXACTLY like the runtime writer ({@link defaultStateDir},
|
|
81
|
+
* mirroring `handle.ts` `trackFile(sid, defaultStateDir(cwd))`), so the file read
|
|
82
|
+
* here is the same one {@link recordOneShot} wrote. "" when no data or read error.
|
|
83
|
+
* @param cwd - The project working directory (Claude `cwd`), NOT the state dir.
|
|
84
|
+
* @returns One line, e.g. `gates 7d: 88% one-shot (44/50 clean); ...`, or "".
|
|
85
|
+
*/
|
|
86
|
+
export function oneShotSummary(cwd: string): string {
|
|
87
|
+
try {
|
|
88
|
+
const s = pruneState(loadState(join(defaultStateDir(cwd), SIDECAR)), Date.now(), WINDOW_MS);
|
|
89
|
+
return formatSummary(s);
|
|
90
|
+
} catch {
|
|
91
|
+
return "";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module receipts
|
|
3
|
+
* Verification receipts: capture `tsc`/test runs at PostToolUse and query the
|
|
4
|
+
* freshest PASSING one for the TaskCompleted (hard) and SubagentStop (advisory)
|
|
5
|
+
* gates. A "done" over modified code files is refused unless such a receipt
|
|
6
|
+
* exists — mechanising "no proof, no done".
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
import { withTrack, trackJournalEnabled } from "./store";
|
|
10
|
+
import { readTrackSync } from "./track-compact";
|
|
11
|
+
import type { SessionTrack } from "./session-state";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A verification receipt captured at PostToolUse from a Bash verification
|
|
15
|
+
* command. Feeds the TaskCompleted receipt gate: a "done" over modified code
|
|
16
|
+
* files is refused unless a fresh, passing receipt (`exitCode === 0`, `fail === 0`)
|
|
17
|
+
* exists. `pass`/`fail` are parsed only for test runs; `tsc` carries the code alone.
|
|
18
|
+
*/
|
|
19
|
+
export interface Receipt {
|
|
20
|
+
kind: "tsc" | "test";
|
|
21
|
+
exitCode: number;
|
|
22
|
+
pass?: number;
|
|
23
|
+
fail?: number;
|
|
24
|
+
ts: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** `tsc` / `bunx tsc` / `npx tsc -p .` — a type-check invocation. */
|
|
28
|
+
const TSC_RE = /(?:^|\s|\/)(?:bunx\s+|npx\s+|pnpm\s+|yarn\s+)?tsc\b/;
|
|
29
|
+
/** `bun test`, `vitest`, `jest`, `npm test`, `npm run test`, … — a test run. */
|
|
30
|
+
const TEST_RE = /\b(?:bun\s+test|vitest|jest|(?:npm|pnpm|yarn)\s+(?:run\s+)?test)\b/;
|
|
31
|
+
|
|
32
|
+
/** Append a verification receipt to the track. Immutable. */
|
|
33
|
+
export function recordReceipt(track: SessionTrack, receipt: Receipt): SessionTrack {
|
|
34
|
+
return { ...track, receipts: [...(track.receipts ?? []), receipt] };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Parse `N pass` / `M fail` counts from bun/vitest/jest output (undefined when absent). */
|
|
38
|
+
function parseCounts(output: string): { pass?: number; fail?: number } {
|
|
39
|
+
const pass = output.match(/(\d+)\s+pass/i);
|
|
40
|
+
const fail = output.match(/(\d+)\s+fail/i);
|
|
41
|
+
return { pass: pass ? Number(pass[1]) : undefined, fail: fail ? Number(fail[1]) : undefined };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Classify a Bash command as a verification receipt, or `null` when it is not a
|
|
46
|
+
* recognised `tsc`/test invocation. Test runs additionally carry parsed
|
|
47
|
+
* pass/fail counts.
|
|
48
|
+
* @param command - The Bash command line.
|
|
49
|
+
* @param output - Combined stdout+stderr (bun writes its summary to stderr).
|
|
50
|
+
* @param exitCode - The command's exit code.
|
|
51
|
+
* @param now - Capture timestamp (epoch ms).
|
|
52
|
+
*/
|
|
53
|
+
export function classifyReceipt(command: string, output: string, exitCode: number, now: number): Receipt | null {
|
|
54
|
+
if (TEST_RE.test(command)) {
|
|
55
|
+
const { pass, fail } = parseCounts(output);
|
|
56
|
+
return { kind: "test", exitCode, pass, fail, ts: now };
|
|
57
|
+
}
|
|
58
|
+
if (TSC_RE.test(command)) return { kind: "tsc", exitCode, ts: now };
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A receipt PROVES success: exit 0 and (for tests) zero reported failures. */
|
|
63
|
+
function isPassing(r: Receipt): boolean {
|
|
64
|
+
return r.exitCode === 0 && (r.fail ?? 0) === 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The newest passing receipt within `windowMs`, or `null`. */
|
|
68
|
+
export function freshPassingReceipt(track: SessionTrack, windowMs: number, now: number): Receipt | null {
|
|
69
|
+
const cutoff = now - windowMs;
|
|
70
|
+
const hits = (track.receipts ?? []).filter((r) => r.ts > cutoff && isPassing(r));
|
|
71
|
+
return hits.length ? hits.reduce((a, b) => (b.ts > a.ts ? b : a)) : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Sync variant reading the track directly (snapshot ⊕ journal) — for the sync
|
|
76
|
+
* gates (TaskCompleted / SubagentStop). Returns the newest passing receipt, or
|
|
77
|
+
* `null` on any read/verify failure (fail-closed: no proof ⇒ unverified).
|
|
78
|
+
*/
|
|
79
|
+
export function freshReceiptFromFile(file: string, windowMs: number, now: number): Receipt | null {
|
|
80
|
+
try {
|
|
81
|
+
return freshPassingReceipt(readTrackSync(file, trackJournalEnabled()), windowMs, now);
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Capture a verification receipt from a PostToolUse Bash command into the signed
|
|
89
|
+
* track (best effort — a non-verification command is a no-op).
|
|
90
|
+
*/
|
|
91
|
+
export async function captureReceipt(file: string, command: string, output: string, exitCode: number, now: number): Promise<void> {
|
|
92
|
+
const receipt = classifyReceipt(command, output, exitCode, now);
|
|
93
|
+
if (!receipt) return;
|
|
94
|
+
await withTrack(file, (track) => recordReceipt(track, receipt));
|
|
95
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { AuthEntry } from "../freshness/doc-helpers";
|
|
2
|
+
import { creditDocConsultation, type SessionTarget } from "../policy/apex-authorization";
|
|
3
|
+
import type { Receipt } from "./receipts";
|
|
4
|
+
|
|
5
|
+
/** Quality of a recorded agent call (the freshness gate ignores insufficient ones). */
|
|
6
|
+
export type AgentQuality = "sufficient" | "insufficient";
|
|
7
|
+
|
|
8
|
+
/** Recorded session activity that feeds the APEX gates. */
|
|
9
|
+
export interface SessionTrack {
|
|
10
|
+
authorizations: Record<string, AuthEntry>;
|
|
11
|
+
refsRead: string[];
|
|
12
|
+
/**
|
|
13
|
+
* Epoch-ms timestamp of each reference read, keyed by `refsRead` path (parity
|
|
14
|
+
* track-solid-reads.py `solid_reads[].timestamp`). Optional: tracks persisted
|
|
15
|
+
* before this field existed only carry `refsRead`, and such paths count as
|
|
16
|
+
* read (backward compat). PARITY: only the SOLID-read gate TTL-checks these —
|
|
17
|
+
* Python TTL-izes SOLID reads exclusively (require-solid-read.py); the other
|
|
18
|
+
* refsRead consumers (skill-trigger/shadcn/tailwind/design) stay session-scoped.
|
|
19
|
+
*/
|
|
20
|
+
refsReadAt?: Record<string, number>;
|
|
21
|
+
/** Framework awaiting doc credit after a Check-1 deny. PERSISTS — {@link recordDoc} cross-credits it on EVERY consultation, never clears it; only the next deny replaces it via {@link recordTarget} (parity track-doc-consultation.py:62). */
|
|
22
|
+
target?: SessionTarget;
|
|
23
|
+
agents: { name: string; ts: number; quality?: AgentQuality }[];
|
|
24
|
+
trivialEdits: number[];
|
|
25
|
+
brainstormRequired?: boolean;
|
|
26
|
+
/** Verification receipts (tsc/test) at PostToolUse; absent/empty reads as unverified in the TaskCompleted gate (backward compat, fail-closed). See {@link Receipt}. */
|
|
27
|
+
receipts?: Receipt[];
|
|
28
|
+
}
|
|
29
|
+
/** A fresh, empty track. */
|
|
30
|
+
export function emptyTrack(): SessionTrack {
|
|
31
|
+
return { authorizations: {}, refsRead: [], agents: [], trivialEdits: [] };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Record a doc consultation (Context7/Exa/web) for a framework in this session:
|
|
36
|
+
* the Check-2 credit (`doc_sessions` + `sources`) plus the Check-1 stamp
|
|
37
|
+
* (`sessions` + `doc_consulted` = ISO of `now`). PARITY track-doc-consultation.py
|
|
38
|
+
* :62-70 — a `target` left by a Check-1 deny (enforce-apex-phases.ts:80) is
|
|
39
|
+
* cross-credited on EVERY consultation, with NO TTL on `target.set_at`, and the
|
|
40
|
+
* target PERSISTS (only the next deny replaces it via {@link recordTarget});
|
|
41
|
+
* single-shot clearing or TTL-gating the target re-opened the deny loop when a
|
|
42
|
+
* consultation landed > TTL after the deny. Immutable.
|
|
43
|
+
*/
|
|
44
|
+
export function recordDoc(track: SessionTrack, framework: string, sessionId: string, source: string, now: number = Date.now()): SessionTrack {
|
|
45
|
+
const nowIso = new Date(now).toISOString();
|
|
46
|
+
const authorizations = { ...track.authorizations, [framework]: creditDocConsultation(track.authorizations[framework], sessionId, source, nowIso) };
|
|
47
|
+
const t = track.target;
|
|
48
|
+
if (t && t.framework !== framework) {
|
|
49
|
+
authorizations[t.framework] = creditDocConsultation(track.authorizations[t.framework], sessionId, source, nowIso);
|
|
50
|
+
}
|
|
51
|
+
return { ...track, authorizations };
|
|
52
|
+
}
|
|
53
|
+
/** Set the pending doc-credit target (written by the runtime on a Check-1 deny). Immutable. */
|
|
54
|
+
export function recordTarget(track: SessionTrack, target: SessionTarget): SessionTrack {
|
|
55
|
+
return { ...track, target };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Record that a SOLID reference file was read (deduped). Immutable. When `now`
|
|
60
|
+
* (epoch ms — the tool event's own timestamp, never a fresh `Date.now()`) is
|
|
61
|
+
* supplied, the read is stamped in `refsReadAt`, refreshed on re-reads so the
|
|
62
|
+
* LATEST read drives the SOLID TTL (parity track-solid-reads.py, which appends
|
|
63
|
+
* a timestamped entry per read; require-solid-read.py checks the most recent).
|
|
64
|
+
* Callers that omit `now` keep the legacy untimestamped behavior.
|
|
65
|
+
*/
|
|
66
|
+
export function recordRefRead(track: SessionTrack, path: string, now?: number): SessionTrack {
|
|
67
|
+
const refsRead = track.refsRead.includes(path) ? track.refsRead : [...track.refsRead, path];
|
|
68
|
+
if (now === undefined) return refsRead === track.refsRead ? track : { ...track, refsRead };
|
|
69
|
+
return { ...track, refsRead, refsReadAt: { ...track.refsReadAt, [path]: now } };
|
|
70
|
+
}
|
|
71
|
+
/** Record an agent/tool call with a timestamp + optional quality. Immutable. */
|
|
72
|
+
export function recordAgent(track: SessionTrack, name: string, ts: number, quality?: AgentQuality): SessionTrack {
|
|
73
|
+
const entry = quality ? { name, ts, quality } : { name, ts };
|
|
74
|
+
return { ...track, agents: [...track.agents, entry] };
|
|
75
|
+
}
|
|
76
|
+
/** True when ALL of `names` ran within `windowMs` with non-insufficient quality. */
|
|
77
|
+
export function agentsFresh(track: SessionTrack, names: string[], windowMs: number, now: number): boolean {
|
|
78
|
+
const cutoff = now - windowMs;
|
|
79
|
+
return names.every((n) => track.agents.some((a) => a.name === n && a.ts > cutoff && a.quality !== "insufficient"));
|
|
80
|
+
}
|
|
81
|
+
/** Record a trivial edit timestamp (sliding window; old evicted). Immutable. */
|
|
82
|
+
export function recordTrivialEdit(track: SessionTrack, ts: number, windowMs: number, now: number): SessionTrack {
|
|
83
|
+
const cutoff = now - windowMs;
|
|
84
|
+
return { ...track, trivialEdits: [...(track.trivialEdits ?? []).filter((t) => t > cutoff), ts] };
|
|
85
|
+
}
|
|
86
|
+
/** Count trivial edits within the sliding window. */
|
|
87
|
+
export function trivialCount(track: SessionTrack, windowMs: number, now: number): number {
|
|
88
|
+
const cutoff = now - windowMs;
|
|
89
|
+
return (track.trivialEdits ?? []).filter((t) => t > cutoff).length;
|
|
90
|
+
}
|
|
91
|
+
/** Set the brainstorm-required flag (from creation-intent detection). Immutable. */
|
|
92
|
+
export function recordBrainstormRequired(track: SessionTrack, required: boolean): SessionTrack {
|
|
93
|
+
return { ...track, brainstormRequired: required };
|
|
94
|
+
}
|