@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,39 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
|
|
4
|
+
/** Registry path relative to the home dir. */
|
|
5
|
+
const SUBPATH = ".fuse-harness/cache/lessons/roots.json";
|
|
6
|
+
|
|
7
|
+
/** Absolute path of the global roots registry, or null when home is unusable. */
|
|
8
|
+
export function registryFile(home: string | undefined = process.env.HOME): string | null {
|
|
9
|
+
const h = home?.trim();
|
|
10
|
+
if (!h || !h.startsWith("/")) return null;
|
|
11
|
+
return `${h}/${SUBPATH}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Read the registered project roots (deduplicated string entries only). */
|
|
15
|
+
export function readRoots(home?: string): string[] {
|
|
16
|
+
const file = registryFile(home);
|
|
17
|
+
if (!file) return [];
|
|
18
|
+
try {
|
|
19
|
+
const parsed: unknown = JSON.parse(readFileSync(file, "utf8"));
|
|
20
|
+
return Array.isArray(parsed) ? parsed.filter((e): e is string => typeof e === "string") : [];
|
|
21
|
+
} catch {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Register a project root once (deduplicated, read-modify-write, non-throwing). */
|
|
27
|
+
export function addRoot(root: string, home?: string): void {
|
|
28
|
+
const file = registryFile(home);
|
|
29
|
+
if (!file) return;
|
|
30
|
+
try {
|
|
31
|
+
const roots = new Set(readRoots(home));
|
|
32
|
+
if (roots.has(root)) return;
|
|
33
|
+
roots.add(root);
|
|
34
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
35
|
+
writeFileSync(file, JSON.stringify([...roots]));
|
|
36
|
+
} catch {
|
|
37
|
+
/* non-fatal: a missed root never blocks the session */
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-scoped lessons roots registry. The flat {@link module:memory/registry}
|
|
3
|
+
* keeps ONE global list of pending roots — correct mono-session, but wrong with
|
|
4
|
+
* several concurrent Claude Code sessions: at Stop, one session lists (and, by
|
|
5
|
+
* bumping the throttle, STEALS) another session's pending lesson on a project it
|
|
6
|
+
* never touched. This registry keys "which project got code edits, and was its
|
|
7
|
+
* Stop reminder already fired" by `session_id`, so each Stop sees and consumes
|
|
8
|
+
* ONLY its own roots. Stored at `$HOME/.fuse-harness/cache/lessons/session-roots.json`;
|
|
9
|
+
* non-fatal on any I/O failure (a missed reminder never blocks a session).
|
|
10
|
+
*/
|
|
11
|
+
import { mkdirSync, readFileSync } from "node:fs";
|
|
12
|
+
import { dirname } from "node:path";
|
|
13
|
+
import { atomicWrite } from "../util/json-io";
|
|
14
|
+
|
|
15
|
+
/** Per-(session, root) throttle: last code edit vs. last reminder emitted. */
|
|
16
|
+
export interface RootMark {
|
|
17
|
+
editedAt: number;
|
|
18
|
+
remindedAt: number;
|
|
19
|
+
}
|
|
20
|
+
/** One session's edited roots plus an `updatedAt` purge cursor. */
|
|
21
|
+
interface SessionEntry {
|
|
22
|
+
updatedAt: number;
|
|
23
|
+
roots: Record<string, RootMark>;
|
|
24
|
+
}
|
|
25
|
+
/** Whole registry keyed by sanitized `session_id`. */
|
|
26
|
+
type Registry = Record<string, SessionEntry>;
|
|
27
|
+
|
|
28
|
+
/** Registry path (rel. home) + stale-bucket purge horizon (bounds growth). */
|
|
29
|
+
const SUBPATH = ".fuse-harness/cache/lessons/session-roots.json";
|
|
30
|
+
const PURGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
31
|
+
|
|
32
|
+
/** Absolute registry path, or null when home is unusable. */
|
|
33
|
+
function file(home: string | undefined): string | null {
|
|
34
|
+
const h = home?.trim();
|
|
35
|
+
return h && h.startsWith("/") ? `${h}/${SUBPATH}` : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Read the registry; missing/corrupt/legacy (array) shapes collapse to `{}`. */
|
|
39
|
+
function read(home: string | undefined): Registry {
|
|
40
|
+
const f = file(home);
|
|
41
|
+
if (!f) return {};
|
|
42
|
+
try {
|
|
43
|
+
const parsed: unknown = JSON.parse(readFileSync(f, "utf8"));
|
|
44
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Registry) : {};
|
|
45
|
+
} catch {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Purge stale buckets, then atomically persist (unique tmp + rename). Non-throwing. */
|
|
51
|
+
function write(home: string | undefined, reg: Registry, now: number): void {
|
|
52
|
+
const f = file(home);
|
|
53
|
+
if (!f) return;
|
|
54
|
+
for (const [sid, entry] of Object.entries(reg)) {
|
|
55
|
+
if (!entry || now - (entry.updatedAt ?? 0) > PURGE_MS) delete reg[sid];
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
mkdirSync(dirname(f), { recursive: true });
|
|
59
|
+
atomicWrite(f, JSON.stringify(reg));
|
|
60
|
+
} catch {
|
|
61
|
+
/* non-fatal: a missed root never blocks the session */
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Record `field` for `(sid, root)`, refreshing the purge cursor. `home` defaults to `$HOME`. */
|
|
66
|
+
export function markSessionRoot(
|
|
67
|
+
sid: string, root: string, field: keyof RootMark, value: number, home: string | undefined = process.env.HOME,
|
|
68
|
+
): void {
|
|
69
|
+
const reg = read(home);
|
|
70
|
+
const prev = reg[sid];
|
|
71
|
+
const entry: SessionEntry = prev && typeof prev.roots === "object" && prev.roots !== null ? prev : { updatedAt: value, roots: {} };
|
|
72
|
+
const mark = entry.roots[root] ?? { editedAt: 0, remindedAt: 0 };
|
|
73
|
+
entry.roots[root] = { ...mark, [field]: value };
|
|
74
|
+
entry.updatedAt = value;
|
|
75
|
+
reg[sid] = entry;
|
|
76
|
+
write(home, reg, value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Roots of `sid` with an unsaved code edit past the `window`; each returned
|
|
81
|
+
* root's `remindedAt` is bumped to `now` so the reminder fires at most once per
|
|
82
|
+
* window and is consumed ONLY by this session. `home` defaults to `$HOME`.
|
|
83
|
+
*/
|
|
84
|
+
export function collectSessionPending(
|
|
85
|
+
sid: string, now: number, window: number, home: string | undefined = process.env.HOME,
|
|
86
|
+
): string[] {
|
|
87
|
+
const reg = read(home);
|
|
88
|
+
const entry = reg[sid];
|
|
89
|
+
if (!entry || typeof entry.roots !== "object" || entry.roots === null) return [];
|
|
90
|
+
const pending: string[] = [];
|
|
91
|
+
for (const [root, mark] of Object.entries(entry.roots)) {
|
|
92
|
+
if (mark.editedAt <= mark.remindedAt) continue;
|
|
93
|
+
if (now - mark.remindedAt < window) continue;
|
|
94
|
+
pending.push(root);
|
|
95
|
+
entry.roots[root] = { ...mark, remindedAt: now };
|
|
96
|
+
}
|
|
97
|
+
if (pending.length > 0) write(home, reg, now);
|
|
98
|
+
return pending;
|
|
99
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { atomicWrite } from "../util/json-io";
|
|
4
|
+
import { projectLayout } from "../config/layout";
|
|
5
|
+
import { ensureMemoryGitignore } from "./gitignore";
|
|
6
|
+
|
|
7
|
+
/** Per-project reminder throttle state, stored at `<root>/MEMORY/state.json`. */
|
|
8
|
+
export interface ReminderState {
|
|
9
|
+
lastRemindedAt: number;
|
|
10
|
+
lastCodeEditAt: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Absolute path of the per-project throttle state file. */
|
|
14
|
+
export function stateFileFor(root: string): string {
|
|
15
|
+
return projectLayout(root).memoryStateFile;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Absolute path of the per-project curated lessons file (committable). */
|
|
19
|
+
export function lessonsFileFor(root: string): string {
|
|
20
|
+
return projectLayout(root).lessonsFile;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function num(v: unknown): number {
|
|
24
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Read both throttle timestamps; missing/corrupt fields default to 0. */
|
|
28
|
+
export function readState(file: string): ReminderState {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(readFileSync(file, "utf8")) as Partial<ReminderState>;
|
|
31
|
+
return { lastRemindedAt: num(parsed?.lastRemindedAt), lastCodeEditAt: num(parsed?.lastCodeEditAt) };
|
|
32
|
+
} catch {
|
|
33
|
+
return { lastRemindedAt: 0, lastCodeEditAt: 0 };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Persist one field without clobbering the other (read-modify-write). */
|
|
38
|
+
export function setStateField(file: string, key: keyof ReminderState, value: number): void {
|
|
39
|
+
const next: ReminderState = { ...readState(file), [key]: value };
|
|
40
|
+
const dir = dirname(file);
|
|
41
|
+
mkdirSync(dir, { recursive: true });
|
|
42
|
+
ensureMemoryGitignore(dir);
|
|
43
|
+
atomicWrite(file, JSON.stringify(next));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Local wall-clock timestamp for a lesson bullet: `YYYY-MM-DD HH:MM`. */
|
|
47
|
+
export function nowStamp(): string {
|
|
48
|
+
const d = new Date();
|
|
49
|
+
const p = (n: number): string => String(n).padStart(2, "0");
|
|
50
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Throttle window (ms) from `FUSE_LESSONS_THROTTLE_MIN` (default 5 min). */
|
|
54
|
+
export function throttleMs(env: Record<string, string | undefined> = process.env): number {
|
|
55
|
+
const raw = env.FUSE_LESSONS_THROTTLE_MIN?.trim();
|
|
56
|
+
const min = raw ? Number(raw) : 5;
|
|
57
|
+
return (Number.isFinite(min) ? Math.max(0, min) : 5) * 60_000;
|
|
58
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { resolveSessions, type AuthEntry } from "../freshness/doc-helpers";
|
|
2
|
+
import { routeReferences } from "../refs/router";
|
|
3
|
+
import { DEFAULT_TTL_SEC, ttlLabel } from "../config/ttl";
|
|
4
|
+
import { CONTEXT7_SOURCE, getSkillSource } from "./skill-source";
|
|
5
|
+
import type { ApexContext, ApexGate } from "./apex";
|
|
6
|
+
|
|
7
|
+
/** Pending doc-credit target written on a Check-1 deny (parity enforce-apex-phases.ts:80 `state.target`). */
|
|
8
|
+
export interface SessionTarget {
|
|
9
|
+
project: string;
|
|
10
|
+
framework: string;
|
|
11
|
+
set_by: string;
|
|
12
|
+
set_at: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Credit a doc consultation onto one framework's auth entry (parity
|
|
17
|
+
* track-doc-consultation.py): the once-per-session Check-2 fields
|
|
18
|
+
* (`doc_sessions` + `sources`) plus the per-framework Check-1 stamp
|
|
19
|
+
* (`sessions` + `doc_consulted`). Pure.
|
|
20
|
+
*/
|
|
21
|
+
export function creditDocConsultation(prev: AuthEntry | undefined, sessionId: string, source: string, nowIso: string): AuthEntry {
|
|
22
|
+
const p = prev ?? {};
|
|
23
|
+
const add = (list: string[], v: string): string[] => (v && !list.includes(v) ? [...list, v] : list);
|
|
24
|
+
return {
|
|
25
|
+
...p,
|
|
26
|
+
sessions: add(resolveSessions(p), sessionId),
|
|
27
|
+
doc_sessions: add(p.doc_sessions ?? [], sessionId),
|
|
28
|
+
sources: add(p.sources ?? (p.source ? [p.source] : []), source),
|
|
29
|
+
doc_consulted: nowIso,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check 1 (parity enforce-apex-phases.ts isAuthorized): the framework's doc or
|
|
35
|
+
* skill was consulted in THIS session (resolveSessions) AND the stamp is
|
|
36
|
+
* younger than the TTL (FUSE_ENFORCE_TTL_SEC via src/config/ttl.ts).
|
|
37
|
+
*/
|
|
38
|
+
export function isAuthorized(auth: AuthEntry | undefined, sessionId: string, now: number, ttlMs: number): boolean {
|
|
39
|
+
if (!auth?.doc_consulted || !resolveSessions(auth).includes(sessionId)) return false;
|
|
40
|
+
const readEpoch = Date.parse(auth.doc_consulted);
|
|
41
|
+
return !Number.isNaN(readEpoch) && now - readEpoch < ttlMs;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A fresh same-session `skills/**.md` read whose path names the framework also
|
|
46
|
+
* satisfies Check 1 (parity track-doc-consultation.py, which stamps
|
|
47
|
+
* `doc_consulted` on `skills/.*\.md` Reads; the harness records those Reads in
|
|
48
|
+
* `refsRead`/`refsReadAt` instead, and the track file is per-session).
|
|
49
|
+
*/
|
|
50
|
+
function skillReadAuthorized(ctx: ApexContext, now: number, ttlMs: number): boolean {
|
|
51
|
+
// Aliases keep every deny-promised path unblockable: laravel skills say "php",
|
|
52
|
+
// and tailwind's SKILL.md lives under "tailwindcss[-v4]" where \btailwind\b
|
|
53
|
+
// never matches (the boundary fails before the "c").
|
|
54
|
+
const alias: Record<string, string> = { laravel: "laravel|php", tailwind: "tailwindcss?" };
|
|
55
|
+
const nameRe = new RegExp(`\\b(${alias[ctx.framework] ?? ctx.framework})\\b`, "i");
|
|
56
|
+
return (ctx.refsRead ?? []).some((p) => {
|
|
57
|
+
if (!/skills\/.*\.md$/i.test(p) || !nameRe.test(p)) return false;
|
|
58
|
+
const at = ctx.refsReadAt?.[p];
|
|
59
|
+
return at !== undefined && now - at < ttlMs;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Gate (Check 1): deny until the framework's doc/skill is freshly consulted.
|
|
65
|
+
* The deny lists the routed SOLID refs when refs are loaded (parity
|
|
66
|
+
* formatRoutedDeny), else the framework's skill source (parity getSkillSource:
|
|
67
|
+
* a real installed SKILL.md path, or the context7 consigne). The RUNTIME
|
|
68
|
+
* caller persists `state.target` on deny (recordTarget, parity :80-81) so
|
|
69
|
+
* recordDoc can cross-credit this framework on the next doc consultation.
|
|
70
|
+
*/
|
|
71
|
+
export const apexAuthorizationGate: ApexGate = (ctx) => {
|
|
72
|
+
const windowMs = ctx.windowMs ?? DEFAULT_TTL_SEC * 1000;
|
|
73
|
+
const now = ctx.now ?? Date.now();
|
|
74
|
+
if (isAuthorized(ctx.authorizations?.[ctx.framework], ctx.sessionId, now, windowMs)) return null;
|
|
75
|
+
if (skillReadAuthorized(ctx, now, windowMs)) return null;
|
|
76
|
+
const ttl = ttlLabel(Math.round(windowMs / 1000));
|
|
77
|
+
const title = `APEX: ${ctx.framework} documentation required`;
|
|
78
|
+
const routed = ctx.refs?.length ? routeReferences(ctx.refs, ctx.filePath, ctx.content) : null;
|
|
79
|
+
if (!routed) {
|
|
80
|
+
// Parity enforce-apex-phases.ts:82-87: getSkillSource (skill-source.ts)
|
|
81
|
+
// resolves the REAL installed SKILL.md, or yields the context7 consigne —
|
|
82
|
+
// never SOLID_REF's phantom `${PLUGINS_DIR}/generic/SKILL.md`.
|
|
83
|
+
const src = getSkillSource(ctx.framework);
|
|
84
|
+
return {
|
|
85
|
+
kind: "block",
|
|
86
|
+
title,
|
|
87
|
+
reason: `APEX: Read doc first (expires every ${ttl}) for ${ctx.framework}! Source: ${src}`,
|
|
88
|
+
actions: [src === CONTEXT7_SOURCE ? `Query ${CONTEXT7_SOURCE} for ${ctx.framework} docs` : `Read ${src}`],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const optional = routed.optional.map((r) => r.meta.filePath);
|
|
92
|
+
const reason = [
|
|
93
|
+
// H2b: name the full set (required + Optional) and invite the retry.
|
|
94
|
+
`APEX: Read ALL SOLID references below (including Optional), then retry (expires every ${ttl}) for ${ctx.framework}.`,
|
|
95
|
+
`Editing: ${ctx.filePath}`,
|
|
96
|
+
...(optional.length ? [`Optional: ${optional.join(", ")}`] : []),
|
|
97
|
+
`Full skill: ${routed.skillPath}`,
|
|
98
|
+
].join("\n");
|
|
99
|
+
return { kind: "block", title, reason, actions: routed.required.map((r) => r.meta.filePath) };
|
|
100
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { routeReferences } from "../refs/router";
|
|
2
|
+
import { skillRefKey } from "../refs/ref-key";
|
|
3
|
+
import type { Prompt } from "../prompt/types";
|
|
4
|
+
import { DEFAULT_TTL_SEC, ttlLabel } from "../config/ttl";
|
|
5
|
+
import { PLUGINS_DIR, SOLID_REF } from "./file-size";
|
|
6
|
+
import type { ApexContext, ApexGate } from "./apex";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* True when the SOLID ref read at `path` still counts. Parity require-solid-read.py
|
|
10
|
+
* `_check_solid_read` (FUSE_ENFORCE_TTL_SEC, default 120s): a SOLID read older than
|
|
11
|
+
* the window has expired and must be redone. PARITY NOTE: the TTL applies ONLY to
|
|
12
|
+
* this gate — Python TTL-izes SOLID reads exclusively; the other refsRead consumers
|
|
13
|
+
* (skillTriggerGate, shadcn/tailwind/design gates) stay session-scoped, so this
|
|
14
|
+
* predicate must never leak into them. Backward compat: a path with no `refsReadAt`
|
|
15
|
+
* stamp (tracks recorded before the field existed) or a context without a clock
|
|
16
|
+
* (`now` absent) counts as read.
|
|
17
|
+
*/
|
|
18
|
+
function refReadFresh(ctx: ApexContext, path: string, windowMs: number): boolean {
|
|
19
|
+
const at = ctx.refsReadAt?.[path];
|
|
20
|
+
return at === undefined || ctx.now === undefined || ctx.now - at < windowMs;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Gate: the routed SOLID references for this edit must have been read within the TTL. */
|
|
24
|
+
export const solidReadGate: ApexGate = (ctx: ApexContext) => {
|
|
25
|
+
if (!ctx.refs?.length) return null;
|
|
26
|
+
const routed = routeReferences(ctx.refs, ctx.filePath, ctx.content);
|
|
27
|
+
const windowMs = ctx.windowMs ?? DEFAULT_TTL_SEC * 1000;
|
|
28
|
+
const ttl = ttlLabel(Math.round(windowMs / 1000));
|
|
29
|
+
if (!routed) {
|
|
30
|
+
// Parity require-solid-read.py::_build_reason (the `not routed` branch):
|
|
31
|
+
// refs ARE loaded but none score for this file — Python still denies here,
|
|
32
|
+
// pointing at the framework's SKILL.md. Note: `!ctx.refs?.length` above
|
|
33
|
+
// (no refs installed at all) intentionally stays allow — discoverRefs()'s
|
|
34
|
+
// documented contract ("with no refs the SOLID-read gate stays off").
|
|
35
|
+
const ref = SOLID_REF[ctx.framework] ?? "generic/";
|
|
36
|
+
return {
|
|
37
|
+
kind: "block",
|
|
38
|
+
title: `APEX: no SOLID reference matched ${ctx.filePath}`,
|
|
39
|
+
reason: `${ctx.refs.length} SOLID reference(s) are loaded but none scored for this edit (expires every ${ttl}) — read the framework skill directly instead.`,
|
|
40
|
+
actions: [`Read ${PLUGINS_DIR}/${ref}SKILL.md`],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// Only reads still inside the TTL window count — a stale read re-blocks
|
|
44
|
+
// (parity require-solid-read.py, which re-validates the TTL on EVERY edit,
|
|
45
|
+
// making the "(expires every …)" wording below actually true).
|
|
46
|
+
const read = new Set((ctx.refsRead ?? []).filter((p) => refReadFresh(ctx, p, windowMs)));
|
|
47
|
+
// A sub-agent/teammate reads the SAME skill material through a different
|
|
48
|
+
// root than the marketplace-first path discoverRefs put in ctx.refs (a
|
|
49
|
+
// version cache, or a standalone .codex/.cursor/.agents dir) — normalize
|
|
50
|
+
// both sides to the `skills/<skill>/...` suffix so that read still credits.
|
|
51
|
+
const readKeys = new Set([...read].map(skillRefKey).filter((k): k is string => k !== null));
|
|
52
|
+
const credited = (path: string): boolean => {
|
|
53
|
+
if (read.has(path)) return true;
|
|
54
|
+
const key = skillRefKey(path);
|
|
55
|
+
return key !== null && readKeys.has(key);
|
|
56
|
+
};
|
|
57
|
+
// Parity with the 3 other refsRead-consuming gates (skillTriggerGate's
|
|
58
|
+
// `skills/${s}/` substring match, shadcnBaseSkillRead, designSkillRead):
|
|
59
|
+
// reading the ref's PARENT SKILL.md counts as proof of consultation too,
|
|
60
|
+
// not just the exact ref file path.
|
|
61
|
+
const skillRead = !!routed.skillPath && credited(routed.skillPath);
|
|
62
|
+
const missing = skillRead ? [] : routed.required.map((r) => r.meta.filePath).filter((p) => !credited(p));
|
|
63
|
+
if (missing.length === 0) return null;
|
|
64
|
+
const optional = routed.optional.map((r) => r.meta.filePath);
|
|
65
|
+
const reason = [
|
|
66
|
+
// H2b: the credit needs the routed refs read — say ALL of them (the
|
|
67
|
+
// Optional line counts too) and that a retry is EXPECTED once read.
|
|
68
|
+
`Read ALL references below (including Optional), then retry — required before editing ${ctx.filePath} (expires every ${ttl}):`,
|
|
69
|
+
...(optional.length ? [`Optional: ${optional.join(", ")}`] : []),
|
|
70
|
+
`Full skill: ${routed.skillPath}`,
|
|
71
|
+
].join("\n");
|
|
72
|
+
return {
|
|
73
|
+
kind: "block",
|
|
74
|
+
title: `APEX: read SOLID references for ${ctx.framework}`,
|
|
75
|
+
reason,
|
|
76
|
+
actions: missing,
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** Gate: the required prior agents (explore + research) must have run within the window. */
|
|
81
|
+
export const freshnessGate: ApexGate = (ctx: ApexContext): Prompt | null => {
|
|
82
|
+
if (ctx.agentsFresh !== false) return null;
|
|
83
|
+
const missing = ctx.missingAgents?.length ? ctx.missingAgents : ["explore-codebase", "research-expert"];
|
|
84
|
+
const ttl = ttlLabel(Math.round((ctx.windowMs ?? DEFAULT_TTL_SEC * 1000) / 1000));
|
|
85
|
+
return {
|
|
86
|
+
kind: "block",
|
|
87
|
+
title: "APEX: explore + research required",
|
|
88
|
+
reason: `Run ${missing.join(" and ")} within the freshness window (${ttl} TTL) before editing ${ctx.framework}.`,
|
|
89
|
+
actions: missing.map((name) => `Launch the ${name} agent`),
|
|
90
|
+
};
|
|
91
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-target phrasing tables for the APEX task-tracking step trio/pair
|
|
3
|
+
* (view-pending / mark-in-progress / mark-done) — split out of
|
|
4
|
+
* `apex-target.ts` to keep both files under the SOLID 100-line ceiling.
|
|
5
|
+
* Kimi Code CLI's native tool is `TodoList` (single tool, full-list re-emit
|
|
6
|
+
* per call; status enum is `pending | in_progress | done` — never
|
|
7
|
+
* `completed`, that is Claude's idiom; omitting `todos` queries the current
|
|
8
|
+
* list without mutating it). Kimi has no documented auto-commit tied to a
|
|
9
|
+
* status transition, unlike Claude's `TaskUpdate(completed)` — so these
|
|
10
|
+
* phrasings must NOT promise one. `TaskList` is a distinct Kimi tool for
|
|
11
|
+
* async background tasks and must never be used here.
|
|
12
|
+
*/
|
|
13
|
+
import type { HarnessId } from "../detect/interfaces/types";
|
|
14
|
+
import type { ApexTaskSteps, ApexAgentSteps } from "./interfaces/types";
|
|
15
|
+
|
|
16
|
+
/** Codex/Kimi step-trio overrides; any other target falls back to {@link DEFAULT_TASK_STEPS}. */
|
|
17
|
+
const TASK_STEPS: Partial<Record<HarnessId, ApexTaskSteps>> = {
|
|
18
|
+
codex: {
|
|
19
|
+
step3: "update_plan → review the current plan",
|
|
20
|
+
step4: "update_plan → mark the active step in_progress before starting",
|
|
21
|
+
step7: "update_plan → mark the step completed when done",
|
|
22
|
+
},
|
|
23
|
+
kimi: {
|
|
24
|
+
step3: "TodoList → review the current list (omit todos to query)",
|
|
25
|
+
step4: "TodoList → mark the active item in_progress before starting",
|
|
26
|
+
step7: "TodoList → mark the item done when finished",
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Default (Claude Code) step trio — zero-regression fallback for any unlisted target. */
|
|
31
|
+
const DEFAULT_TASK_STEPS: ApexTaskSteps = {
|
|
32
|
+
step3: "TaskList → see pending tasks",
|
|
33
|
+
step4: "TaskUpdate(in_progress) → before starting",
|
|
34
|
+
step7: "TaskUpdate(completed) → triggers auto-commit",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the step3/step4/step7 phrasing for a harness target (APEX
|
|
39
|
+
* task-context injection). Replaces the former `isCodex` binary ternary.
|
|
40
|
+
* @param id - Harness id.
|
|
41
|
+
* @returns The {@link ApexTaskSteps} for that target.
|
|
42
|
+
*/
|
|
43
|
+
export function apexTaskSteps(id: string): ApexTaskSteps {
|
|
44
|
+
return TASK_STEPS[id as HarnessId] ?? DEFAULT_TASK_STEPS;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Codex/Kimi before/done overrides; any other target falls back to {@link DEFAULT_AGENT_STEPS}. */
|
|
48
|
+
const AGENT_STEPS: Partial<Record<HarnessId, ApexAgentSteps>> = {
|
|
49
|
+
codex: {
|
|
50
|
+
beforeStart: "Use update_plan → mark the active step in_progress before starting",
|
|
51
|
+
whenDone: "update_plan → mark the step completed when done",
|
|
52
|
+
},
|
|
53
|
+
kimi: {
|
|
54
|
+
beforeStart: "Use TodoList → mark the active item in_progress before starting",
|
|
55
|
+
whenDone: "TodoList → mark the item done when finished",
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Default (Claude Code) before/done pair — zero-regression fallback for any unlisted target. */
|
|
60
|
+
const DEFAULT_AGENT_STEPS: ApexAgentSteps = {
|
|
61
|
+
beforeStart: "Use TaskUpdate(taskId, status: in_progress) before starting",
|
|
62
|
+
whenDone: "TaskUpdate(taskId, status: completed) triggers auto-commit",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolve the beforeStart/whenDone phrasing for a harness target (APEX
|
|
67
|
+
* sub-agent injection). Replaces the former `isCodex` binary ternary.
|
|
68
|
+
* @param id - Harness id.
|
|
69
|
+
* @returns The {@link ApexAgentSteps} for that target.
|
|
70
|
+
*/
|
|
71
|
+
export function apexAgentSteps(id: string): ApexAgentSteps {
|
|
72
|
+
return AGENT_STEPS[id as HarnessId] ?? DEFAULT_AGENT_STEPS;
|
|
73
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness-target helpers for the APEX context builders. Single source of
|
|
3
|
+
* truth for the home-dir segment / root doc name / plan-tool idiom that
|
|
4
|
+
* differ between Claude Code and Codex (and beyond), so the builders in
|
|
5
|
+
* `claude-md-context.ts`, `apex-task-context.ts`, and `inject-apex.ts` never
|
|
6
|
+
* hardcode `.claude`/`CLAUDE.md`/`TaskCreate` again. Runtime `id` values flow
|
|
7
|
+
* through the pipeline as plain `string` (see `handleHook(id: string, ...)`),
|
|
8
|
+
* so these helpers accept `string` and narrow internally — same idiom as the
|
|
9
|
+
* existing `HOME_DIR[id] ?? ".claude"` precedent in `cartographer/detect.ts`.
|
|
10
|
+
*/
|
|
11
|
+
import { HOME_DIR } from "../config/dotenv";
|
|
12
|
+
import type { HarnessId } from "../detect/interfaces/types";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Home-dir segment for a harness target (".claude", ".codex", ...). Falls
|
|
16
|
+
* back to ".claude" for any harness without a {@link HOME_DIR} entry.
|
|
17
|
+
* @param id - Harness id (e.g. "claude-code", "codex").
|
|
18
|
+
* @returns The home-dir segment, e.g. ".claude".
|
|
19
|
+
*/
|
|
20
|
+
export function harnessHomeSegment(id: string): string {
|
|
21
|
+
return HOME_DIR[id as HarnessId] ?? ".claude";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Root instructions doc name for a harness target: "AGENTS.md" for Codex and
|
|
26
|
+
* Kimi Code CLI (both documented conventions), "CLAUDE.md" for every other
|
|
27
|
+
* target (including the default/unknown fallback).
|
|
28
|
+
* @param id - Harness id.
|
|
29
|
+
* @returns The doc file name.
|
|
30
|
+
*/
|
|
31
|
+
export function apexDocName(id: string): string {
|
|
32
|
+
return id === "codex" || id === "kimi" ? "AGENTS.md" : "CLAUDE.md";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Native plan/track tool name for a harness target: Codex's `update_plan`,
|
|
37
|
+
* Kimi Code CLI's `TodoList`, or Claude's `TaskCreate` (default). Covers only
|
|
38
|
+
* the PLAN-step line — other target-specific tool references (`TaskList`,
|
|
39
|
+
* `TaskUpdate`) are resolved via `apexTaskSteps`/`apexAgentSteps` in
|
|
40
|
+
* `apex-target-steps.ts` since their surrounding prose differs structurally,
|
|
41
|
+
* not just by token.
|
|
42
|
+
* @param id - Harness id.
|
|
43
|
+
* @returns The plan-tool name.
|
|
44
|
+
*/
|
|
45
|
+
export function apexPlanTool(id: string): string {
|
|
46
|
+
if (id === "codex") return "update_plan";
|
|
47
|
+
if (id === "kimi") return "TodoList";
|
|
48
|
+
return "TaskCreate";
|
|
49
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { resolveMaxLines } from "../config/limits";
|
|
4
|
+
import { harnessHomeSegment } from "./apex-target";
|
|
5
|
+
import { apexTaskSteps } from "./apex-target-steps";
|
|
6
|
+
|
|
7
|
+
/** Parsed task state injected into a Task sub-agent prompt. */
|
|
8
|
+
export interface ApexTaskState {
|
|
9
|
+
/** Current task id (defaults to "1"). */
|
|
10
|
+
id: string;
|
|
11
|
+
/** Task subject (defaults to ""). */
|
|
12
|
+
subject: string;
|
|
13
|
+
/** Current phase (defaults to "analyze"). */
|
|
14
|
+
phase: string;
|
|
15
|
+
/** Comma-joined consulted doc keys, or "none". */
|
|
16
|
+
docs: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Read the current task state from `task.json`, reproducing the legacy Python
|
|
21
|
+
* logic. Any read/parse error falls back to `("1", "", "analyze", "none")`.
|
|
22
|
+
* @param taskFile - Absolute path to `.claude/apex/task.json`.
|
|
23
|
+
* @returns The parsed {@link ApexTaskState}.
|
|
24
|
+
*/
|
|
25
|
+
export function loadApexTaskState(taskFile: string): ApexTaskState {
|
|
26
|
+
try {
|
|
27
|
+
const data = JSON.parse(readFileSync(taskFile, "utf-8")) as Record<string, unknown>;
|
|
28
|
+
const id = String(data.current_task ?? "1");
|
|
29
|
+
const tasks = (data.tasks as Record<string, unknown> | undefined) ?? {};
|
|
30
|
+
const task = (tasks[id] as Record<string, unknown> | undefined) ?? {};
|
|
31
|
+
const subject = typeof task.subject === "string" ? task.subject : "";
|
|
32
|
+
const phase = typeof task.phase === "string" ? task.phase : "analyze";
|
|
33
|
+
const consultedMap = (task.doc_consulted as Record<string, unknown> | undefined) ?? {};
|
|
34
|
+
const consulted = Object.entries(consultedMap)
|
|
35
|
+
.filter(([, v]) => typeof v === "object" && v !== null && (v as Record<string, unknown>).consulted === true)
|
|
36
|
+
.map(([k]) => k);
|
|
37
|
+
return { id, subject, phase, docs: consulted.join(", ") || "none" };
|
|
38
|
+
} catch {
|
|
39
|
+
return { id: "1", subject: "", phase: "analyze", docs: "none" };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build the APEX context string injected into a Task sub-agent prompt.
|
|
45
|
+
* @param state - The parsed task state.
|
|
46
|
+
* @param maxLines - SOLID per-file line ceiling.
|
|
47
|
+
* @param id - Harness target id (defaults to "claude-code" — zero-regression default).
|
|
48
|
+
* @returns The injection text.
|
|
49
|
+
*/
|
|
50
|
+
export function buildApexTaskContext(state: ApexTaskState, maxLines: number, id: string = "claude-code"): string {
|
|
51
|
+
const seg = harnessHomeSegment(id);
|
|
52
|
+
const { step3, step4, step7 } = apexTaskSteps(id);
|
|
53
|
+
return (
|
|
54
|
+
`⚠️ APEX MODE - Read ${seg}/apex/AGENTS.md for rules\n\n` +
|
|
55
|
+
`Current: Task #${state.id} - ${state.subject} (Phase: ${state.phase})\n` +
|
|
56
|
+
`Docs consulted: ${state.docs}\n\n` +
|
|
57
|
+
`Agent must:\n` +
|
|
58
|
+
`1. Read task.json → find last 3 completed tasks\n` +
|
|
59
|
+
`2. Read their notes in docs/ (task-{ID}-{subject}.md)\n` +
|
|
60
|
+
`3. ${step3}\n` +
|
|
61
|
+
`4. ${step4}\n` +
|
|
62
|
+
`5. Apply SOLID (files < ${maxLines} lines)\n` +
|
|
63
|
+
`6. Write notes to docs/task-{ID}-{subject}.md\n` +
|
|
64
|
+
`7. ${step7}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build the PreToolUse Task injection, gated on the existence of the
|
|
70
|
+
* project's target apex dir (`.claude/apex/`, `.codex/apex/`, ...). Returns
|
|
71
|
+
* `null` when APEX is not active (no dir).
|
|
72
|
+
* @param projectRoot - `CLAUDE_PROJECT_DIR` or cwd.
|
|
73
|
+
* @param id - Harness target id (defaults to "claude-code" — zero-regression default).
|
|
74
|
+
* @returns The injection text, or `null` to emit nothing.
|
|
75
|
+
*/
|
|
76
|
+
export function buildApexTaskInjection(projectRoot: string, id: string = "claude-code"): string | null {
|
|
77
|
+
const apexDir = join(projectRoot, harnessHomeSegment(id), "apex");
|
|
78
|
+
if (!existsSync(apexDir)) return null;
|
|
79
|
+
const state = loadApexTaskState(join(apexDir, "task.json"));
|
|
80
|
+
return buildApexTaskContext(state, resolveMaxLines(), id);
|
|
81
|
+
}
|