@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,46 @@
|
|
|
1
|
+
import { mkdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
|
|
4
|
+
/** Stale-after window (ms) — survives a session crashing before its `finally`. */
|
|
5
|
+
const STALE_MS = 30_000;
|
|
6
|
+
|
|
7
|
+
function lockPath(codexHome: string): string {
|
|
8
|
+
return join(codexHome, "fusengine", "state", "agents-resync.lock");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Best-effort inter-process lock for the agents resync. `writeFileSync(path,
|
|
13
|
+
* pid, { flag: "wx" })` uses `O_EXCL`: it fails with `EEXIST` when the lock
|
|
14
|
+
* file already exists, so two Codex sessions starting at once cannot both
|
|
15
|
+
* resolve then write DIFFERENT versions of the agents cache in interleave (a
|
|
16
|
+
* torn TOML). Stale after {@link STALE_MS} to survive a crash that never
|
|
17
|
+
* reached its `finally`. No live-PID check (a permanent, accepted limit of any
|
|
18
|
+
* mtime-based lock — same tradeoff `proper-lockfile` makes).
|
|
19
|
+
* @param codexHome - The Codex home directory.
|
|
20
|
+
* @returns `true` when the lock was acquired; `false` when another live
|
|
21
|
+
* holder has it.
|
|
22
|
+
*/
|
|
23
|
+
export function acquireResyncLock(codexHome: string): boolean {
|
|
24
|
+
const path = lockPath(codexHome);
|
|
25
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
26
|
+
try {
|
|
27
|
+
writeFileSync(path, String(process.pid), { flag: "wx" });
|
|
28
|
+
return true;
|
|
29
|
+
} catch {
|
|
30
|
+
try {
|
|
31
|
+
if (Date.now() - statSync(path).mtimeMs <= STALE_MS) return false;
|
|
32
|
+
unlinkSync(path);
|
|
33
|
+
writeFileSync(path, String(process.pid), { flag: "wx" });
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Release the lock. Best-effort — never throws. */
|
|
42
|
+
export function releaseResyncLock(codexHome: string): void {
|
|
43
|
+
try {
|
|
44
|
+
unlinkSync(lockPath(codexHome));
|
|
45
|
+
} catch { /* best-effort */ }
|
|
46
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { lstatSync, readFileSync, readlinkSync, unlinkSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
/** Marker line stamped at the top of a materialized (non-symlinked) agent TOML. */
|
|
4
|
+
export const MANAGED_AGENT_MARKER = "# Managed by fusengine-codex; source:";
|
|
5
|
+
|
|
6
|
+
/** True when a symlink target lives under a plugin cache dir (ours to manage). */
|
|
7
|
+
function isManagedPluginTarget(target: string): boolean {
|
|
8
|
+
return target.includes("/plugins/") || target.includes("\\plugins\\");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Clear a previously-installed destination before reinstalling it, but ONLY
|
|
13
|
+
* when it is clearly ours: a symlink into a plugin cache dir, or a text file
|
|
14
|
+
* stamped with {@link MANAGED_AGENT_MARKER}. Anything else (a user's own file,
|
|
15
|
+
* a foreign symlink) is left untouched. Ports the merged behavior of
|
|
16
|
+
* `plugin-managed-destination.ts` + the local helper in
|
|
17
|
+
* `plugin-file-symlinks.ts` (this harness always runs hook-silent, so the
|
|
18
|
+
* `@clack/prompts` warnings those ports had are dropped, not translated).
|
|
19
|
+
* @param path - The destination path to check/clear.
|
|
20
|
+
* @returns `"missing"` (nothing there), `"removed"` (ours, cleared), or
|
|
21
|
+
* `"skip"` (foreign — left alone).
|
|
22
|
+
*/
|
|
23
|
+
export function clearManagedDestination(path: string): "missing" | "removed" | "skip" {
|
|
24
|
+
try {
|
|
25
|
+
const stat = lstatSync(path);
|
|
26
|
+
if (stat.isSymbolicLink()) {
|
|
27
|
+
if (!isManagedPluginTarget(readlinkSync(path))) return "skip";
|
|
28
|
+
unlinkSync(path);
|
|
29
|
+
return "removed";
|
|
30
|
+
}
|
|
31
|
+
if (readFileSync(path, "utf8").startsWith(MANAGED_AGENT_MARKER)) {
|
|
32
|
+
unlinkSync(path);
|
|
33
|
+
return "removed";
|
|
34
|
+
}
|
|
35
|
+
return "skip";
|
|
36
|
+
} catch {
|
|
37
|
+
return "missing";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { clearManagedDestination, MANAGED_AGENT_MARKER } from "./managed-destination";
|
|
4
|
+
import type { PluginFile } from "./plugin-files";
|
|
5
|
+
|
|
6
|
+
const PORTABLE_SKILL_PATH_RE = /^plugins\/([^/]+)\/skills\/(.+)$/;
|
|
7
|
+
const CACHE_SKILL_PATH_RE = /\/\.codex\/plugins\/cache\/fusengine-codex\/([^/]+)\/[^/]+\/skills\/(.+)$/;
|
|
8
|
+
|
|
9
|
+
function tomlString(value: string): string {
|
|
10
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Rewrite one `path = "..."` value to an absolute path (portable/cache skill refs, or relative-to-source). */
|
|
14
|
+
function rewriteAgentSkillPath(value: string, item: PluginFile, pluginRoots: Map<string, string>): string {
|
|
15
|
+
const portable = PORTABLE_SKILL_PATH_RE.exec(value);
|
|
16
|
+
if (portable) {
|
|
17
|
+
const root = pluginRoots.get(portable[1] ?? "");
|
|
18
|
+
return root ? join(root, "skills", portable[2] ?? "") : value;
|
|
19
|
+
}
|
|
20
|
+
const cached = CACHE_SKILL_PATH_RE.exec(value);
|
|
21
|
+
if (cached) {
|
|
22
|
+
const root = pluginRoots.get(cached[1] ?? "");
|
|
23
|
+
return root ? join(root, "skills", cached[2] ?? "") : value;
|
|
24
|
+
}
|
|
25
|
+
if (value.startsWith("./") || value.startsWith("../")) return resolve(dirname(item.src), value);
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Rewrite every `path = "..."` line in an agent TOML + stamp the managed-source marker. */
|
|
30
|
+
export function materializeAgentToml(raw: string, item: PluginFile, pluginRoots: Map<string, string>): string {
|
|
31
|
+
const rewritten = raw.replace(/^(\s*path\s*=\s*)"([^"]+)"/gm, (_m, prefix: string, value: string) =>
|
|
32
|
+
`${prefix}${tomlString(rewriteAgentSkillPath(value, item, pluginRoots))}`);
|
|
33
|
+
return rewritten.startsWith(MANAGED_AGENT_MARKER) ? rewritten : `${MANAGED_AGENT_MARKER} ${item.src}\n${rewritten}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Materialize agent TOMLs into `destDir` (Codex ignores symlinked agent
|
|
38
|
+
* TOMLs — openai/codex#15345, never fixed — so these are COPIED, not linked).
|
|
39
|
+
* Silent: no `@clack/prompts` (this always runs inside a hook, which must
|
|
40
|
+
* stay stdout-clean). Ports `agent-materializer.ts::materializeAgentFiles`.
|
|
41
|
+
* @param files - Discovered agent files (see {@link import("./plugin-files").listPluginFiles}).
|
|
42
|
+
* @param destDir - Destination dir (`<codexHome>/agents`).
|
|
43
|
+
* @param pluginRoots - Resolved plugin roots, for `path=` rewriting.
|
|
44
|
+
*/
|
|
45
|
+
export function materializeAgentFiles(files: PluginFile[], destDir: string, pluginRoots: Map<string, string>): void {
|
|
46
|
+
mkdirSync(destDir, { recursive: true });
|
|
47
|
+
const seen = new Set<string>();
|
|
48
|
+
for (const item of files) {
|
|
49
|
+
if (seen.has(item.file)) continue;
|
|
50
|
+
seen.add(item.file);
|
|
51
|
+
clearManagedDestination(join(destDir, `${item.plugin}-${item.file}`)); // legacy naming cleanup
|
|
52
|
+
const destPath = join(destDir, item.file);
|
|
53
|
+
if (clearManagedDestination(destPath) === "skip") continue;
|
|
54
|
+
const raw = readFileSync(item.src, "utf8");
|
|
55
|
+
writeFileSync(destPath, materializeAgentToml(raw, item, pluginRoots));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { compareVersionsDesc } from "./plugin-roots";
|
|
4
|
+
|
|
5
|
+
/** One discovered plugin file: which plugin owns it, its filename, and its source path. */
|
|
6
|
+
export interface PluginFile {
|
|
7
|
+
plugin: string;
|
|
8
|
+
file: string;
|
|
9
|
+
src: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Files matching `extension` directly under `dir`, tagged with `plugin`. */
|
|
13
|
+
function filesIn(dir: string, plugin: string, extension: string): PluginFile[] {
|
|
14
|
+
if (!existsSync(dir)) return [];
|
|
15
|
+
return readdirSync(dir)
|
|
16
|
+
.filter((file) => file.endsWith(extension))
|
|
17
|
+
.map((file) => ({ plugin, file, src: join(dir, file) }));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Versioned plugin layout: newest version subdir wins, first match returned. */
|
|
21
|
+
function listVersionedFiles(pluginRoot: string, plugin: string, subdir: string, extension: string): PluginFile[] {
|
|
22
|
+
let versions: string[];
|
|
23
|
+
try {
|
|
24
|
+
versions = readdirSync(pluginRoot, { withFileTypes: true })
|
|
25
|
+
.filter((v) => v.isDirectory() && !v.name.startsWith("."))
|
|
26
|
+
.map((v) => v.name)
|
|
27
|
+
.sort(compareVersionsDesc);
|
|
28
|
+
} catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
for (const version of versions) {
|
|
32
|
+
const found = filesIn(join(pluginRoot, version, subdir), plugin, extension);
|
|
33
|
+
if (found.length > 0) return found;
|
|
34
|
+
}
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Discover every plugin's files of a given kind (agents `.toml`, commands
|
|
40
|
+
* `.md`) across a plugins cache root — unversioned layout first, else the
|
|
41
|
+
* newest version subdir. Ports `plugin-file-discovery.ts::listPluginFiles`.
|
|
42
|
+
* @param pluginsRoot - The plugin cache root.
|
|
43
|
+
* @param subdir - `"agents"` or `"commands"`.
|
|
44
|
+
* @param extension - File extension to match, including the dot.
|
|
45
|
+
*/
|
|
46
|
+
export function listPluginFiles(pluginsRoot: string, subdir: string, extension: string): PluginFile[] {
|
|
47
|
+
const out: PluginFile[] = [];
|
|
48
|
+
let entries: string[];
|
|
49
|
+
try {
|
|
50
|
+
entries = readdirSync(pluginsRoot, { withFileTypes: true })
|
|
51
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "_shared")
|
|
52
|
+
.map((e) => e.name);
|
|
53
|
+
} catch {
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
for (const plugin of entries) {
|
|
57
|
+
const direct = filesIn(join(pluginsRoot, plugin, subdir), plugin, extension);
|
|
58
|
+
if (direct.length > 0) {
|
|
59
|
+
out.push(...direct);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
out.push(...listVersionedFiles(join(pluginsRoot, plugin), plugin, subdir, extension));
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
/** Frozen marketplace name (matches codex-plugins' own `scripts/install-codex.ts`). */
|
|
5
|
+
export const MARKETPLACE = "fusengine-codex";
|
|
6
|
+
|
|
7
|
+
/** The plugin cache root under a Codex home. */
|
|
8
|
+
export function pluginsCacheRoot(codexHome: string): string {
|
|
9
|
+
return join(codexHome, "plugins", "cache", MARKETPLACE);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Descending semver-ish compare (`"2.1" > "10.0"` stays false — numeric per segment). */
|
|
13
|
+
export function compareVersionsDesc(a: string, b: string): number {
|
|
14
|
+
const left = a.split(/[.-]/).map((part) => Number.parseInt(part, 10));
|
|
15
|
+
const right = b.split(/[.-]/).map((part) => Number.parseInt(part, 10));
|
|
16
|
+
const max = Math.max(left.length, right.length);
|
|
17
|
+
for (let i = 0; i < max; i++) {
|
|
18
|
+
const av = Number.isNaN(left[i]) ? 0 : (left[i] ?? 0);
|
|
19
|
+
const bv = Number.isNaN(right[i]) ? 0 : (right[i] ?? 0);
|
|
20
|
+
if (av !== bv) return bv - av;
|
|
21
|
+
}
|
|
22
|
+
return b.localeCompare(a);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Resolve one plugin's active root: unversioned dir first, else the newest version dir. */
|
|
26
|
+
function resolvePluginRoot(pluginsRoot: string, plugin: string): string | undefined {
|
|
27
|
+
const root = join(pluginsRoot, plugin);
|
|
28
|
+
if (existsSync(join(root, ".codex-plugin")) || existsSync(join(root, "skills"))) return root;
|
|
29
|
+
let entries: string[];
|
|
30
|
+
try {
|
|
31
|
+
entries = readdirSync(root, { withFileTypes: true })
|
|
32
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith("."))
|
|
33
|
+
.map((e) => e.name);
|
|
34
|
+
} catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
const versions = entries.sort(compareVersionsDesc);
|
|
38
|
+
return versions[0] ? join(root, versions[0]) : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the active root directory of every installed plugin under a plugins
|
|
43
|
+
* cache root, keyed by plugin name. Ports `plugin-root-resolver.ts::buildPluginRoots`.
|
|
44
|
+
* @param pluginsRoot - The plugin cache root (see {@link pluginsCacheRoot}).
|
|
45
|
+
* @returns Plugin name -> resolved root dir (missing entries are silently skipped).
|
|
46
|
+
*/
|
|
47
|
+
export function buildPluginRoots(pluginsRoot: string): Map<string, string> {
|
|
48
|
+
const roots = new Map<string, string>();
|
|
49
|
+
let entries: string[];
|
|
50
|
+
try {
|
|
51
|
+
entries = readdirSync(pluginsRoot, { withFileTypes: true })
|
|
52
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "_shared")
|
|
53
|
+
.map((e) => e.name);
|
|
54
|
+
} catch {
|
|
55
|
+
return roots;
|
|
56
|
+
}
|
|
57
|
+
for (const plugin of entries) {
|
|
58
|
+
const root = resolvePluginRoot(pluginsRoot, plugin);
|
|
59
|
+
if (root) roots.set(plugin, root);
|
|
60
|
+
}
|
|
61
|
+
return roots;
|
|
62
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { acquireResyncLock, releaseResyncLock } from "./lock";
|
|
4
|
+
import { needsResync, resolveCurrentFingerprint, writeFingerprint } from "./fingerprint";
|
|
5
|
+
import { pluginsCacheRoot } from "./plugin-roots";
|
|
6
|
+
import { listPluginFiles } from "./plugin-files";
|
|
7
|
+
import { materializeAgentFiles } from "./materialize-agents";
|
|
8
|
+
import { symlinkPluginFiles } from "./symlink-commands";
|
|
9
|
+
|
|
10
|
+
/** The Codex home dir: `$CODEX_HOME`, else `~/.codex`. */
|
|
11
|
+
export function defaultCodexHome(): string {
|
|
12
|
+
return process.env.CODEX_HOME ?? join(homedir(), ".codex");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Re-materialize the Codex plugin agents/commands cache on SessionStart, but
|
|
17
|
+
* only when the plugin-cache fingerprint changed (or a command symlink dangles)
|
|
18
|
+
* since the last apply — otherwise a cheap no-op. Agents are COPIED into
|
|
19
|
+
* `<codexHome>/agents` (Codex won't load symlinked agent TOMLs), commands are
|
|
20
|
+
* SYMLINKED into `<codexHome>/prompts`. Guarded by a best-effort inter-process
|
|
21
|
+
* lock so two sessions starting at once can't write a torn cache; the sha256
|
|
22
|
+
* fingerprint is the real correctness backstop (idempotent skip), the lock only
|
|
23
|
+
* reduces redundant concurrent rebuilds. ABSOLUTELY fail-open: any error is
|
|
24
|
+
* swallowed so a resync can never break SessionStart.
|
|
25
|
+
* @param codexHome - The Codex home directory (defaults to {@link defaultCodexHome}).
|
|
26
|
+
*/
|
|
27
|
+
export function resyncCodexAgents(codexHome: string = defaultCodexHome()): void {
|
|
28
|
+
try {
|
|
29
|
+
const pluginsRoot = pluginsCacheRoot(codexHome);
|
|
30
|
+
const current = resolveCurrentFingerprint(pluginsRoot);
|
|
31
|
+
if (!current) return; // nothing cached — the installer, not this hook, owns first-time setup
|
|
32
|
+
const promptsDir = join(codexHome, "prompts");
|
|
33
|
+
if (!needsResync(codexHome, current.value, promptsDir)) return; // unchanged since last apply — no-op
|
|
34
|
+
if (!acquireResyncLock(codexHome)) return; // another session already holds the resync lock
|
|
35
|
+
try {
|
|
36
|
+
materializeAgentFiles(listPluginFiles(pluginsRoot, "agents", ".toml"), join(codexHome, "agents"), current.roots);
|
|
37
|
+
symlinkPluginFiles(listPluginFiles(pluginsRoot, "commands", ".md"), promptsDir);
|
|
38
|
+
writeFingerprint(codexHome, current.value);
|
|
39
|
+
} finally {
|
|
40
|
+
releaseResyncLock(codexHome); // best-effort, never throws (see lock.ts)
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
/* absolute fail-open: a resync must never break SessionStart */
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { mkdirSync, symlinkSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { clearManagedDestination } from "./managed-destination";
|
|
4
|
+
import type { PluginFile } from "./plugin-files";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Symlink command files (`.md`) into `destDir` — unlike agents, Codex has no
|
|
8
|
+
* known symlink-loading issue for prompts, so these stay linked rather than
|
|
9
|
+
* copied. Silent, no `@clack/prompts` (see {@link materializeAgentFiles} for
|
|
10
|
+
* why). Ports `plugin-file-symlinks.ts::symlinkPluginFiles`.
|
|
11
|
+
* @param files - Discovered command files (see {@link import("./plugin-files").listPluginFiles}).
|
|
12
|
+
* @param destDir - Destination dir (`<codexHome>/prompts`).
|
|
13
|
+
*/
|
|
14
|
+
export function symlinkPluginFiles(files: PluginFile[], destDir: string): void {
|
|
15
|
+
mkdirSync(destDir, { recursive: true });
|
|
16
|
+
const seen = new Set<string>();
|
|
17
|
+
for (const item of files) {
|
|
18
|
+
if (seen.has(item.file)) continue;
|
|
19
|
+
seen.add(item.file);
|
|
20
|
+
clearManagedDestination(join(destDir, `${item.plugin}-${item.file}`)); // legacy naming cleanup
|
|
21
|
+
const linkPath = join(destDir, item.file);
|
|
22
|
+
if (clearManagedDestination(linkPath) === "skip") continue;
|
|
23
|
+
symlinkSync(item.src, linkPath);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { sessionStartCore } from "./session-start";
|
|
2
|
+
import { injectRules } from "./inject-rules";
|
|
3
|
+
import { resolveRulesRoot } from "./rules-root";
|
|
4
|
+
import { solidDetectStart } from "./solid-detect";
|
|
5
|
+
import { subagentCacheContext } from "./subagent-cache";
|
|
6
|
+
import { trackAgentMemory } from "./agent-memory";
|
|
7
|
+
import { harvestSubagentTrack } from "../../freshness/evidence-harvest-io";
|
|
8
|
+
import { markSubagentSeen } from "../confirm/confirm-subagent";
|
|
9
|
+
import { teammateIdleContext } from "./teammate-idle-check";
|
|
10
|
+
import { failureLessonContext } from "./failure-lesson";
|
|
11
|
+
import { postCompactContext } from "./post-compact";
|
|
12
|
+
import { saveApexState } from "./pre-compact";
|
|
13
|
+
import { cleanupSession } from "./session-end";
|
|
14
|
+
import { validateRulesLoaded } from "./instructions-loaded";
|
|
15
|
+
import { validateTaskSolid } from "./task-completed";
|
|
16
|
+
import { cartoSessionStart } from "./cartographer/session-start";
|
|
17
|
+
import { dispatchLessons } from "./lessons/dispatch";
|
|
18
|
+
import { withSnapshot } from "./snapshot";
|
|
19
|
+
import { stopCore } from "./stop-core";
|
|
20
|
+
|
|
21
|
+
/** Which plugin's hooks.json invoked the harness (selects SessionStart behavior). */
|
|
22
|
+
export type PluginScope = "core" | "solid" | "rules" | "carto" | "security" | "changelog" | "aipilot" | "lessons" | "seo" | "memory" | "tailwindcss";
|
|
23
|
+
|
|
24
|
+
/** Inputs the lifecycle dispatcher needs (clock + roots injected). */
|
|
25
|
+
export interface LifecycleInput {
|
|
26
|
+
event: string;
|
|
27
|
+
payload: Record<string, unknown>;
|
|
28
|
+
cwd: string;
|
|
29
|
+
scope: PluginScope;
|
|
30
|
+
now: number;
|
|
31
|
+
/** Harness target id (defaults to "claude-code" — zero-regression default). */
|
|
32
|
+
id?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** SessionStart handler keyed on plugin scope. */
|
|
36
|
+
function sessionStart(input: LifecycleInput): string {
|
|
37
|
+
if (input.scope === "solid") return solidDetectStart();
|
|
38
|
+
if (input.scope === "rules") return injectRules(resolveRulesRoot(input.id ?? "claude-code", input.cwd), input.event, input.id ?? "claude-code");
|
|
39
|
+
if (input.scope === "carto") return cartoSessionStart(input.cwd, input.now);
|
|
40
|
+
if (input.scope === "lessons") return dispatchLessons("SessionStart", input.payload, input.cwd, input.now, input.id ?? "claude-code");
|
|
41
|
+
const core = sessionStartCore(input.cwd, undefined, input.now, input.id ?? "claude-code");
|
|
42
|
+
// core scope only: concatenate the reconciliation snapshot onto the existing
|
|
43
|
+
// additionalContext (CLAUDE.md + dev-context) — never replaces it, fail-safe.
|
|
44
|
+
return input.scope === "core" ? withSnapshot(core, input.cwd, import.meta.url) : core;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Route a lifecycle/session/context hook event to its ported handler. Returns
|
|
49
|
+
* the native stdout when handled, or `null` when the event is not a lifecycle
|
|
50
|
+
* event (so the caller falls through to the PreToolUse/PostToolUse pipeline).
|
|
51
|
+
* @param input - The dispatch input.
|
|
52
|
+
* @returns The native hook stdout, or `null` when unhandled.
|
|
53
|
+
*/
|
|
54
|
+
export function dispatchLifecycle(input: LifecycleInput): string | null {
|
|
55
|
+
switch (input.event) {
|
|
56
|
+
case "SessionStart":
|
|
57
|
+
return sessionStart(input);
|
|
58
|
+
case "UserPromptSubmit":
|
|
59
|
+
if (input.scope === "rules") return injectRules(resolveRulesRoot(input.id ?? "claude-code", input.cwd), input.event, input.id ?? "claude-code");
|
|
60
|
+
if (input.scope === "lessons") return dispatchLessons("UserPromptSubmit", input.payload, input.cwd, input.now, input.id ?? "claude-code");
|
|
61
|
+
return null;
|
|
62
|
+
case "SubagentStart":
|
|
63
|
+
// G0 (CONFIRM-token mechanism, confirm-subagent.ts): mark this session
|
|
64
|
+
// as having recent sub-agent activity BEFORE the scope branching below,
|
|
65
|
+
// so it fires regardless of which plugin scope dispatched it. A
|
|
66
|
+
// monotone max-write timestamp, NOT a counter — see confirm-subagent.ts's
|
|
67
|
+
// subagentWindowMs doc for why a start/stop counter desyncs unsafely
|
|
68
|
+
// under this same multi-plugin fan-out.
|
|
69
|
+
markSubagentSeen(input.payload.session_id, input.now);
|
|
70
|
+
if (input.scope === "rules") return injectRules(resolveRulesRoot(input.id ?? "claude-code", input.cwd), input.event, input.id ?? "claude-code");
|
|
71
|
+
if (input.scope === "aipilot") return "";
|
|
72
|
+
if (input.scope === "lessons") return dispatchLessons("SubagentStart", input.payload, input.cwd, input.now, input.id ?? "claude-code");
|
|
73
|
+
return subagentCacheContext(input.payload.session_id);
|
|
74
|
+
case "Stop":
|
|
75
|
+
if (input.scope === "lessons") return dispatchLessons("Stop", input.payload, input.cwd, input.now, input.id ?? "claude-code");
|
|
76
|
+
return input.scope === "core" ? stopCore(input.payload, input.cwd, input.now) : null;
|
|
77
|
+
case "SubagentStop":
|
|
78
|
+
// G0 counterpart of the SubagentStart branch above — the SAME
|
|
79
|
+
// monotone max-write, never a decrement (see confirm-subagent.ts).
|
|
80
|
+
markSubagentSeen(input.payload.session_id, input.now);
|
|
81
|
+
if (input.scope === "aipilot") return "";
|
|
82
|
+
// Retroactively harvest the finishing sub-agent's transcript into the session
|
|
83
|
+
// track BEFORE the reminder — so next turn's freshness gate sees research/
|
|
84
|
+
// explore evidence even when sidechain PostToolUse hooks never fired
|
|
85
|
+
// (#43612/#27655/#34692). SubagentStop is main-session-dispatched (reliable).
|
|
86
|
+
harvestSubagentTrack(input.payload, input.cwd, input.now);
|
|
87
|
+
return trackAgentMemory(input.payload, undefined, input.now);
|
|
88
|
+
case "TeammateIdle":
|
|
89
|
+
return teammateIdleContext(input.payload, input.cwd, undefined, input.now);
|
|
90
|
+
case "PostToolUseFailure":
|
|
91
|
+
return failureLessonContext(input.payload, input.cwd, undefined, input.now);
|
|
92
|
+
case "PostCompact":
|
|
93
|
+
return input.scope === "core" ? postCompactContext(input.payload, input.cwd, import.meta.url, input.now) : "";
|
|
94
|
+
case "PreCompact":
|
|
95
|
+
return saveApexState(input.cwd, input.now, input.id ?? "claude-code");
|
|
96
|
+
case "SessionEnd":
|
|
97
|
+
if (input.scope !== "aipilot") cleanupSession(undefined, input.now);
|
|
98
|
+
return "";
|
|
99
|
+
case "InstructionsLoaded":
|
|
100
|
+
validateRulesLoaded(input.payload);
|
|
101
|
+
return "";
|
|
102
|
+
case "TaskCompleted":
|
|
103
|
+
return validateTaskSolid(input.payload);
|
|
104
|
+
default:
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export { dispatchAipilot, aipilotPostToolUse } from "./aipilot/dispatch-aipilot";
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module failure-lesson
|
|
3
|
+
* PostToolUseFailure handler: keep the failure log, record the failure in the
|
|
4
|
+
* one-shot metric, and inject the ONE most-specific lesson whose `error:` trigger
|
|
5
|
+
* matches the failure message — reusing the PreToolUse {@link lessonFor} index and
|
|
6
|
+
* its cooldown (idempotent under the ~11-process fan-out). Fail-open throughout.
|
|
7
|
+
*
|
|
8
|
+
* Claude-Code-only: no equivalent `PostToolUseFailure` hook exists on Codex or
|
|
9
|
+
* Hermes, so this handler is never reached through those adapters.
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { contextResponse } from "../../adapters/claude";
|
|
14
|
+
import { lessonFor } from "../../policy/lessons/lesson-gate";
|
|
15
|
+
import { lessonsFileFor } from "./lessons/state";
|
|
16
|
+
import { projectRoot } from "../../util/project-root";
|
|
17
|
+
import { oncePerWindow } from "../inject-dedup";
|
|
18
|
+
import { defaultStateDir } from "../paths";
|
|
19
|
+
import { recordFailure } from "../../tracking/one-shot-failure";
|
|
20
|
+
import { logToolFailure } from "./tool-failure";
|
|
21
|
+
import type { OncePerWindow } from "../../policy/lessons/types";
|
|
22
|
+
|
|
23
|
+
/** The failure message across the documented `error` field and defensive fallbacks; "" when none. */
|
|
24
|
+
function failureError(data: Record<string, unknown>): string {
|
|
25
|
+
const raw = data.error ?? data.tool_error ?? data.tool_output;
|
|
26
|
+
if (typeof raw === "string") return raw;
|
|
27
|
+
return raw != null ? JSON.stringify(raw) : "";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Handle PostToolUseFailure: log the failure, tally it per tool, and inject the
|
|
32
|
+
* matching `error:`-triggered lesson as `additionalContext` ("" when none).
|
|
33
|
+
* @param data - The raw PostToolUseFailure payload (`tool_name`, `error`, `session_id`).
|
|
34
|
+
* @param cwd - Project root.
|
|
35
|
+
* @param home - Home dir (defaults to `~`).
|
|
36
|
+
* @param now - Clock (defaults to `Date.now()`).
|
|
37
|
+
* @param once - Cooldown gate (injected for tests; defaults to {@link oncePerWindow}).
|
|
38
|
+
* @returns The native hook stdout, or "" when nothing to inject.
|
|
39
|
+
*/
|
|
40
|
+
export function failureLessonContext(data: Record<string, unknown>, cwd: string, home: string = homedir(), now: number = Date.now(), once: OncePerWindow = oncePerWindow): string {
|
|
41
|
+
logToolFailure(data, home, now);
|
|
42
|
+
const tool = typeof data.tool_name === "string" ? data.tool_name : "unknown";
|
|
43
|
+
const sessionId = typeof data.session_id === "string" ? data.session_id : undefined;
|
|
44
|
+
try { recordFailure(tool, { now, dir: defaultStateDir(cwd), sessionId }); } catch { /* metric never breaks the hook */ }
|
|
45
|
+
const errorMsg = failureError(data);
|
|
46
|
+
if (!errorMsg) return "";
|
|
47
|
+
// tool="" + empty input isolates matching to the `error:` regex (rank 1),
|
|
48
|
+
// reusing the PreToolUse gate's index + cooldown — no second lesson engine.
|
|
49
|
+
const lesson = lessonFor("", {}, { file: lessonsFileFor(projectRoot(cwd)), once, prevError: errorMsg });
|
|
50
|
+
return lesson?.reason ? contextResponse("PostToolUseFailure", lesson.reason) : "";
|
|
51
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export * from "./session-start";
|
|
2
|
+
export * from "./inject-rules";
|
|
3
|
+
export * from "./solid-detect";
|
|
4
|
+
export * from "./check-file-size";
|
|
5
|
+
export * from "./validate-solid";
|
|
6
|
+
export * from "./validate-tailwind";
|
|
7
|
+
export * from "./subagent-cache";
|
|
8
|
+
export * from "./agent-memory";
|
|
9
|
+
export * from "./teammate-idle";
|
|
10
|
+
export * from "./tool-failure";
|
|
11
|
+
export * from "./pre-compact";
|
|
12
|
+
export * from "./session-end";
|
|
13
|
+
export * from "./instructions-loaded";
|
|
14
|
+
export * from "./track-changes";
|
|
15
|
+
export * from "./post-edit-ts";
|
|
16
|
+
export * from "./dispatch";
|
|
17
|
+
export * from "./post-tracking";
|
|
18
|
+
export * from "./changelog-research";
|
|
19
|
+
export * from "./cartographer/session-start";
|
|
20
|
+
export * from "./cartographer/project-map";
|
|
21
|
+
export * from "./cartographer/track-enrichment";
|
|
22
|
+
export * from "./cartographer/write-tree";
|
|
23
|
+
export * from "./cartographer/merge";
|
|
24
|
+
export * from "./cartographer/fs-util";
|
|
25
|
+
export * from "./security/check-skill";
|
|
26
|
+
export * from "./security/track-skill-read";
|
|
27
|
+
export * from "./security/track-mcp";
|
|
28
|
+
export * from "./security/skill-state";
|
|
29
|
+
export * from "./lessons/dispatch";
|
|
30
|
+
export * from "./lessons/state";
|
|
31
|
+
export * from "./memory/dispatch";
|
|
32
|
+
export * from "./seo/post-tool-use";
|
|
33
|
+
export * from "./cartographer/ecosystem-map";
|
|
34
|
+
export * from "./cartographer/write-plugin-map";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { renderInform } from "../inform";
|
|
4
|
+
import { rulesInAgentsMd } from "./kimi-rules-native";
|
|
5
|
+
|
|
6
|
+
/** Read & concatenate all `*.md` files (sorted) under `rulesDir`. */
|
|
7
|
+
export function readRules(rulesDir: string): string {
|
|
8
|
+
if (!existsSync(rulesDir)) return "";
|
|
9
|
+
let names: string[];
|
|
10
|
+
try {
|
|
11
|
+
names = readdirSync(rulesDir).filter((n) => n.endsWith(".md")).sort();
|
|
12
|
+
} catch {
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
const parts: string[] = [];
|
|
16
|
+
for (const name of names) {
|
|
17
|
+
try {
|
|
18
|
+
parts.push(readFileSync(join(rulesDir, name), "utf-8"));
|
|
19
|
+
} catch {
|
|
20
|
+
/* skip unreadable rule file */
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return parts.join("\n\n");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build the rules injection for claude-rules (SessionStart, UserPromptSubmit,
|
|
28
|
+
* SubagentStart): read `<pluginRoot>/rules/*.md` and emit via {@link renderInform}
|
|
29
|
+
* (Claude-shaped `additionalContext`, or raw text for kimi). Tags the output
|
|
30
|
+
* with the *actual* `hookEventName` — the spec requires it to match the firing
|
|
31
|
+
* event (a hardcoded "SessionStart" is non-conforming and may be dropped on
|
|
32
|
+
* UserPromptSubmit/SubagentStart).
|
|
33
|
+
*
|
|
34
|
+
* On kimi's `UserPromptSubmit` specifically, when the corpus is already fenced
|
|
35
|
+
* inside `<kimiHome>/AGENTS.md` (native load, {@link rulesInAgentsMd}), only the
|
|
36
|
+
* notice is emitted — the re-injected corpus would otherwise dump into the
|
|
37
|
+
* user's terminal on every single prompt for no model-context benefit.
|
|
38
|
+
* `SessionStart`/`SubagentStart` always keep the full corpus (session bootstrap
|
|
39
|
+
* / sub-agent contexts need it regardless of the native AGENTS.md load).
|
|
40
|
+
* @param pluginRoot - `CLAUDE_PLUGIN_ROOT` of the claude-rules plugin.
|
|
41
|
+
* @param event - The firing hook event name (e.g. "SessionStart").
|
|
42
|
+
* @param id - Harness target id (defaults to "claude-code" — zero-regression default).
|
|
43
|
+
* @returns The native hook stdout (possibly empty).
|
|
44
|
+
*/
|
|
45
|
+
export function injectRules(pluginRoot: string, event: string, id: string = "claude-code"): string {
|
|
46
|
+
const content = readRules(join(pluginRoot, "rules"));
|
|
47
|
+
if (!content) return "";
|
|
48
|
+
if (id === "kimi" && event === "UserPromptSubmit" && rulesInAgentsMd()) return "rules 00-08 injected";
|
|
49
|
+
return renderInform(id, event, content, "rules 00-08 injected");
|
|
50
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Handle InstructionsLoaded: append `load_reason | memory_type | file_path` to
|
|
7
|
+
* the per-session debug log. Ports `instructions-loaded/validate-rules-loaded.py`.
|
|
8
|
+
* No stdout (logging only; InstructionsLoaded has no decision control).
|
|
9
|
+
* @param data - The raw hook payload.
|
|
10
|
+
* @param home - Home dir (defaults to `~`).
|
|
11
|
+
*/
|
|
12
|
+
export function validateRulesLoaded(data: Record<string, unknown>, home: string = homedir()): void {
|
|
13
|
+
const filePath = String(data.file_path ?? "");
|
|
14
|
+
const loadReason = String(data.load_reason ?? "");
|
|
15
|
+
const memoryType = String(data.memory_type ?? "");
|
|
16
|
+
const sessionId = String(data.session_id ?? "unknown");
|
|
17
|
+
const dir = join(home, ".claude", "logs", "instructions-loaded");
|
|
18
|
+
try {
|
|
19
|
+
mkdirSync(dir, { recursive: true });
|
|
20
|
+
appendFileSync(join(dir, `${sessionId}.log`), `${loadReason} | ${memoryType} | ${filePath}\n`, "utf-8");
|
|
21
|
+
} catch { /* best effort */ }
|
|
22
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard for the kimi notice-only rules injection. Kimi Code CLI has no
|
|
3
|
+
* model-only hook channel (`src/runtime/inform.ts`) — every `UserPromptSubmit`
|
|
4
|
+
* stdout is BOTH appended to the model's context AND rendered raw in the
|
|
5
|
+
* user's terminal. `injectRules` (`inject-rules.ts`) normally re-injects the
|
|
6
|
+
* full ~18 Ko rules corpus on every prompt; when the installer has already
|
|
7
|
+
* merged that same corpus into `<kimiHome>/AGENTS.md` — which Kimi loads
|
|
8
|
+
* natively at session start via its documented `${agents_md}` mechanism,
|
|
9
|
+
* independent of hooks — the re-injection is pure noise the user has to
|
|
10
|
+
* scroll past. Confirmed present, fenced, in the local install:
|
|
11
|
+
* `~/.kimi-code/AGENTS.md:87` / `:335`.
|
|
12
|
+
*
|
|
13
|
+
* Fail-safe semantics: fences absent, file absent, or any read error ⟹
|
|
14
|
+
* `false` — the caller then falls back to the full corpus. A dump is
|
|
15
|
+
* verbose but never wrong; a false `true` would silently drop the rules.
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { harnessHome } from "../../config/home-dir";
|
|
20
|
+
|
|
21
|
+
/** Opening fence the fusengine installer writes around the merged corpus. */
|
|
22
|
+
const FENCE_START = "<!-- fusengine:kimi-rules:start -->";
|
|
23
|
+
|
|
24
|
+
/** Closing fence the fusengine installer writes around the merged corpus. */
|
|
25
|
+
const FENCE_END = "<!-- fusengine:kimi-rules:end -->";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check whether the kimi rules corpus is already present, fenced, inside
|
|
29
|
+
* `<kimiHome>/AGENTS.md`.
|
|
30
|
+
* @param home - Kimi home dir override (defaults to `harnessHome("kimi")`,
|
|
31
|
+
* which honors `KIMI_CODE_HOME`).
|
|
32
|
+
* @returns `true` only when both fences are found in a readable file.
|
|
33
|
+
*/
|
|
34
|
+
export function rulesInAgentsMd(home?: string): boolean {
|
|
35
|
+
try {
|
|
36
|
+
const kimiHome = home ?? harnessHome("kimi");
|
|
37
|
+
const agentsMd = join(kimiHome, "AGENTS.md");
|
|
38
|
+
if (!existsSync(agentsMd)) return false;
|
|
39
|
+
const content = readFileSync(agentsMd, "utf-8");
|
|
40
|
+
return content.includes(FENCE_START) && content.includes(FENCE_END);
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|