@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,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Changelog research tracker (PostToolUse exa/WebFetch/WebSearch). Ports
|
|
3
|
+
* `track-watch-research.py`: logs research queries to
|
|
4
|
+
* `~/.claude/logs/00-changelog/<utc-date>-research.json`.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { claudeHome } from "../home-state";
|
|
10
|
+
import { isoUtc, todayUtc } from "./security/skill-state";
|
|
11
|
+
|
|
12
|
+
/** A recorded changelog research query. */
|
|
13
|
+
interface ChangelogQuery {
|
|
14
|
+
timestamp: string;
|
|
15
|
+
tool: string;
|
|
16
|
+
query: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Append an exa/WebFetch/WebSearch query to today's changelog research log.
|
|
21
|
+
* No-op for other tools. No stdout (errors swallowed).
|
|
22
|
+
* @param tool - The tool name.
|
|
23
|
+
* @param input - The tool input (query/url/prompt).
|
|
24
|
+
* @param now - Clock.
|
|
25
|
+
* @param home - Home dir.
|
|
26
|
+
*/
|
|
27
|
+
export function trackWatchResearch(tool: string, input: Record<string, unknown>, now: number = Date.now(), home: string = homedir()): void {
|
|
28
|
+
if (!tool.includes("exa") && !tool.includes("WebFetch") && !tool.includes("WebSearch")) return;
|
|
29
|
+
const query = String(input.query ?? input.url ?? input.prompt ?? "");
|
|
30
|
+
const dir = join(claudeHome(home), "logs", "00-changelog");
|
|
31
|
+
try {
|
|
32
|
+
mkdirSync(dir, { recursive: true });
|
|
33
|
+
const path = join(dir, `${todayUtc(now)}-research.json`);
|
|
34
|
+
let state: { queries: ChangelogQuery[] } = { queries: [] };
|
|
35
|
+
if (existsSync(path)) {
|
|
36
|
+
try {
|
|
37
|
+
state = JSON.parse(readFileSync(path, "utf-8")) as { queries: ChangelogQuery[] };
|
|
38
|
+
} catch { state = { queries: [] }; }
|
|
39
|
+
}
|
|
40
|
+
state.queries.push({ timestamp: isoUtc(now), tool, query });
|
|
41
|
+
writeFileSync(path, JSON.stringify(state, null, 2), "utf-8");
|
|
42
|
+
} catch { /* best effort */ }
|
|
43
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostToolUse (matcher "Write|Edit") for the solid scope: warn when a file
|
|
3
|
+
* exceeds the adaptive `SOLID_FILE_LIMIT` (set by `solidDetectStart` at
|
|
4
|
+
* SessionStart, keyed off the detected project type). Ports `check-file-size.py`.
|
|
5
|
+
* Non-blocking (`additionalContext`), unlike the fixed-ceiling core-guards gate.
|
|
6
|
+
*/
|
|
7
|
+
import { extname } from "node:path";
|
|
8
|
+
import { readText, pathExists } from "../../util/runtime-io";
|
|
9
|
+
import { contextResponse } from "../../adapters/claude";
|
|
10
|
+
import { parseEnvInt } from "../../config/env";
|
|
11
|
+
|
|
12
|
+
/** Per-language "blank or comment" line pattern (parity `count_loc`'s `code_comment` map). */
|
|
13
|
+
const COMMENT_RE: Record<string, RegExp> = {
|
|
14
|
+
ts: /^\s*$|^\s*\/\/|^\s*\/\*|^\s*\*/,
|
|
15
|
+
tsx: /^\s*$|^\s*\/\/|^\s*\/\*|^\s*\*/,
|
|
16
|
+
js: /^\s*$|^\s*\/\/|^\s*\/\*|^\s*\*/,
|
|
17
|
+
jsx: /^\s*$|^\s*\/\/|^\s*\/\*|^\s*\*/,
|
|
18
|
+
go: /^\s*$|^\s*\/\/|^\s*\/\*|^\s*\*/,
|
|
19
|
+
rs: /^\s*$|^\s*\/\/|^\s*\/\*|^\s*\*/,
|
|
20
|
+
java: /^\s*$|^\s*\/\/|^\s*\/\*|^\s*\*/,
|
|
21
|
+
swift: /^\s*$|^\s*\/\/|^\s*\/\*|^\s*\*/,
|
|
22
|
+
php: /^\s*$|^\s*\/\/|^\s*#|^\s*\/\*|^\s*\*/,
|
|
23
|
+
py: /^\s*$|^\s*#|^\s*"""|^\s*'''/,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Count lines of code excluding comments/blanks for `filePath`'s extension (raw length when unmapped). */
|
|
27
|
+
export function countLoc(filePath: string, content: string): number {
|
|
28
|
+
const pattern = COMMENT_RE[extname(filePath).slice(1)];
|
|
29
|
+
const lines = content.split("\n");
|
|
30
|
+
return pattern ? lines.filter((l) => !pattern.test(l)).length : lines.length;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Warn when a written/edited file exceeds `SOLID_FILE_LIMIT` (default 100).
|
|
35
|
+
* Inert when `SOLID_PROJECT_TYPE` is absent/"unknown" (parity: the Python hook
|
|
36
|
+
* no-ops outside a detected project).
|
|
37
|
+
* @param tool - The tool name (only "Write"/"Edit" are checked).
|
|
38
|
+
* @param filePath - The written file's absolute path.
|
|
39
|
+
* @param env - Environment (defaults to `process.env`).
|
|
40
|
+
* @returns The PostToolUse `additionalContext` response, or `""` when clean/inert.
|
|
41
|
+
*/
|
|
42
|
+
export function checkFileSize(tool: string, filePath: string, env: Record<string, string | undefined> = process.env): string {
|
|
43
|
+
const ptype = env.SOLID_PROJECT_TYPE ?? "";
|
|
44
|
+
if (!ptype || ptype === "unknown") return "";
|
|
45
|
+
if (tool !== "Write" && tool !== "Edit") return "";
|
|
46
|
+
if (!filePath || !pathExists(filePath)) return "";
|
|
47
|
+
|
|
48
|
+
let content: string;
|
|
49
|
+
try {
|
|
50
|
+
content = readText(filePath);
|
|
51
|
+
} catch {
|
|
52
|
+
return "";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const limit = parseEnvInt(env.SOLID_FILE_LIMIT, 100);
|
|
56
|
+
const loc = countLoc(filePath, content);
|
|
57
|
+
if (loc <= limit) return "";
|
|
58
|
+
|
|
59
|
+
const name = filePath.split("/").pop() ?? filePath;
|
|
60
|
+
return contextResponse("PostToolUse", `SOLID: ${name} has ${loc} lines (limit: ${limit}). Consider splitting into smaller modules.`);
|
|
61
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { atomicWrite } from "../../../util/json-io";
|
|
5
|
+
import { pathExists } from "../../../util/runtime-io";
|
|
6
|
+
import { buildPluginRoots } from "./plugin-roots";
|
|
7
|
+
|
|
8
|
+
/** Where the last-applied plugin-cache fingerprint is persisted. */
|
|
9
|
+
function manifestPath(codexHome: string): string {
|
|
10
|
+
return join(codexHome, "fusengine", "state", "agents-cache-fingerprint.json");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** sha256 over sorted "plugin=root" pairs — stable while cache resolution is unchanged. */
|
|
14
|
+
export function fingerprint(roots: Map<string, string>): string {
|
|
15
|
+
const sorted = [...roots.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
16
|
+
const hash = createHash("sha256");
|
|
17
|
+
for (const [plugin, root] of sorted) hash.update(`${plugin}=${root}\n`);
|
|
18
|
+
return hash.digest("hex");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The last-persisted fingerprint, or `undefined` (missing/corrupt — treated as "never resynced"). */
|
|
22
|
+
export function readFingerprint(codexHome: string): string | undefined {
|
|
23
|
+
try {
|
|
24
|
+
const raw = JSON.parse(readFileSync(manifestPath(codexHome), "utf8")) as { fingerprint?: string };
|
|
25
|
+
return raw.fingerprint;
|
|
26
|
+
} catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Persist the applied fingerprint atomically (tmp+rename via {@link atomicWrite}). */
|
|
32
|
+
export function writeFingerprint(codexHome: string, value: string): void {
|
|
33
|
+
atomicWrite(manifestPath(codexHome), JSON.stringify({ fingerprint: value }));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** True when a direct symlink child of `dir` points at a now-missing target. */
|
|
37
|
+
export function hasDanglingSymlink(dir: string): boolean {
|
|
38
|
+
if (!existsSync(dir)) return false;
|
|
39
|
+
try {
|
|
40
|
+
for (const name of readdirSync(dir)) {
|
|
41
|
+
const full = join(dir, name);
|
|
42
|
+
if (lstatSync(full).isSymbolicLink() && !existsSync(full)) return true;
|
|
43
|
+
}
|
|
44
|
+
} catch { /* best-effort */ }
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the current plugin roots + fingerprint. `undefined` = nothing cached
|
|
50
|
+
* (fail-open — the initial installer, not this hook, owns that case).
|
|
51
|
+
* @param pluginsRoot - The plugin cache root.
|
|
52
|
+
*/
|
|
53
|
+
export function resolveCurrentFingerprint(pluginsRoot: string): { roots: Map<string, string>; value: string } | undefined {
|
|
54
|
+
if (!pathExists(pluginsRoot)) return undefined;
|
|
55
|
+
const roots = buildPluginRoots(pluginsRoot);
|
|
56
|
+
if (roots.size === 0) return undefined;
|
|
57
|
+
return { roots, value: fingerprint(roots) };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** True when a resync is due: fingerprint changed/never recorded, or a command symlink is dangling. */
|
|
61
|
+
export function needsResync(codexHome: string, currentValue: string, promptsDir: string): boolean {
|
|
62
|
+
return currentValue !== readFingerprint(codexHome) || hasDanglingSymlink(promptsDir);
|
|
63
|
+
}
|
|
@@ -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";
|