@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,116 @@
|
|
|
1
|
+
import type { Prompt } from "../prompt/types";
|
|
2
|
+
import type { NormalizedEvent } from "./normalize";
|
|
3
|
+
import { loadDesignState, saveDesignState, initDesignState } from "../policy/design/state";
|
|
4
|
+
import { recordValidDesignSystem } from "../policy/design/transitions";
|
|
5
|
+
import { activeDesignAgent } from "../policy/design/flag";
|
|
6
|
+
import { runDesignChecks } from "../policy/design/content-checks";
|
|
7
|
+
import { uiDesignSkillGate } from "../policy/design/skill-gate";
|
|
8
|
+
import { collectDesignEvidence } from "../policy/design/skill-evidence";
|
|
9
|
+
import { findDesignSystem, recordPost, designSystemContentGate } from "./design-helpers";
|
|
10
|
+
import { checkDesignSystemContent } from "./design-content-gate";
|
|
11
|
+
import { designFilesGate } from "./design-files-gate";
|
|
12
|
+
import { resolveCorpusRoot, resolvePluginsRoot, pluginsWriteGuard } from "../policy/design/corpus";
|
|
13
|
+
import {
|
|
14
|
+
htmlCssOnlyGate, stateFileGate, screenshotScrollGate,
|
|
15
|
+
geminiEnabled,
|
|
16
|
+
} from "../policy/design/gates";
|
|
17
|
+
import { designSystemWriteGate, geminiCreateGate, browserNavigateGate, htmlCssPipelineGate } from "../policy/design/gates-pipeline";
|
|
18
|
+
|
|
19
|
+
const NAV = "mcp__fuse-browser__browser_navigate";
|
|
20
|
+
const SHOT = "mcp__fuse-browser__browser_screenshot";
|
|
21
|
+
const GEMINI = "mcp__gemini-design__create_frontend";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Design-pipeline gate (effectful: reads/writes the design state + design-system.md).
|
|
25
|
+
* Returns a Prompt to block, or null when this isn't a design-agent context / nothing fires.
|
|
26
|
+
*/
|
|
27
|
+
export function designGate(payload: Record<string, unknown>, event: NormalizedEvent, cacheDir: string, cwd: string, corpusRootOverride?: string, pluginsRootOverride?: string): Prompt | null {
|
|
28
|
+
// UI design-skill gate (ports check-design-skill.py): fires for ANY agent on a
|
|
29
|
+
// UI write — requires a design-skill read + ANY doc source (Context7/Exa/web).
|
|
30
|
+
// Gemini is NEVER required. Runs before the design-agent pipeline state logic.
|
|
31
|
+
if (event.phase !== "post" && (event.tool === "Write" || event.tool === "Edit")) {
|
|
32
|
+
const skillBlock = uiDesignSkillGate(event.tool, event.filePath ?? "", event.content ?? "", collectDesignEvidence(event.sessionId, cwd));
|
|
33
|
+
if (skillBlock) return skillBlock;
|
|
34
|
+
}
|
|
35
|
+
// Codex apply_patch parity (D2 gap, docstring design-files-gate.ts:17-24): the
|
|
36
|
+
// same UI write can arrive fanned into event.files instead of a single
|
|
37
|
+
// Write/Edit. Map each non-delete file to its Write/Edit-equivalent tool
|
|
38
|
+
// ("add" -> Write, "update" -> Edit) and run the SAME gate, so the skill
|
|
39
|
+
// requirement cannot be bypassed just by routing the write through apply_patch.
|
|
40
|
+
// Scope matches the Write/Edit block above: ANY agent, before the agentId
|
|
41
|
+
// early-return — never narrowed to design-agent-only.
|
|
42
|
+
if (event.phase !== "post" && event.files && event.files.length > 0) {
|
|
43
|
+
const evidence = collectDesignEvidence(event.sessionId, cwd);
|
|
44
|
+
for (const f of event.files) {
|
|
45
|
+
if (f.op === "delete") continue;
|
|
46
|
+
const skillBlock = uiDesignSkillGate(f.op === "add" ? "Write" : "Edit", f.filePath, f.content, evidence);
|
|
47
|
+
if (skillBlock) return skillBlock;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const agentId = typeof payload.agent_id === "string" ? payload.agent_id : "";
|
|
52
|
+
if (!agentId) return null; // top-level (lead) calls are never design-agent-scoped
|
|
53
|
+
const active = activeDesignAgent(cacheDir);
|
|
54
|
+
if (active && agentId !== active) return null;
|
|
55
|
+
const id = agentId;
|
|
56
|
+
// P5 fail-open fix: when the design flag is active but the state file is missing
|
|
57
|
+
// (e.g. teammate context), auto-init a fresh state instead of disabling all
|
|
58
|
+
// gating (parity with pipeline-gate.py:38-60). Without the flag, stay inert.
|
|
59
|
+
let state = loadDesignState(cacheDir, id);
|
|
60
|
+
if (!state) {
|
|
61
|
+
if (!active) return null;
|
|
62
|
+
const dsExists = findDesignSystem(cwd) !== "";
|
|
63
|
+
state = initDesignState(id, dsExists ? "page" : "full", dsExists);
|
|
64
|
+
saveDesignState(cacheDir, state);
|
|
65
|
+
}
|
|
66
|
+
// Corpus + plugin roots, resolved INDEPENDENTLY (corpusRoot never decides
|
|
67
|
+
// pluginsRoot — F4). Corpus absent ⇒ requirement waived (pre-doctrine quotas);
|
|
68
|
+
// the plugin-root write guard stays active against refs-design fabrication.
|
|
69
|
+
const corpusRoot = corpusRootOverride ?? resolveCorpusRoot();
|
|
70
|
+
const corpusRequired = corpusRoot !== "";
|
|
71
|
+
const pluginsRoot = pluginsRootOverride ?? resolvePluginsRoot();
|
|
72
|
+
|
|
73
|
+
if (event.phase === "post") {
|
|
74
|
+
recordPost(event, cacheDir, state, corpusRoot, corpusRequired, cwd);
|
|
75
|
+
if ((event.tool === "Write" || event.tool === "Edit") && /\.(tsx|jsx|css)$/.test(event.filePath ?? "")) {
|
|
76
|
+
const warnings = runDesignChecks(event.content ?? "");
|
|
77
|
+
if (warnings.length) return { kind: "inform", title: "Design review", reason: warnings.join(" "), actions: [] };
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
if (event.tool === "Write" || event.tool === "Edit") {
|
|
82
|
+
const fp = event.filePath ?? "";
|
|
83
|
+
// Parity: only design-system.md is screenshot-quota-gated (designSystemWriteGate).
|
|
84
|
+
const base = pluginsWriteGuard(fp, pluginsRoot) ?? stateFileGate(fp) ?? htmlCssOnlyGate(fp)
|
|
85
|
+
?? htmlCssPipelineGate(fp, state, findDesignSystem(cwd) !== "") ?? designSystemWriteGate(fp, state, corpusRequired)
|
|
86
|
+
?? designSystemContentGate({ filePath: fp, tool: event.tool, content: event.content ?? "", oldString: event.oldString, replaceAll: event.input.replace_all === true, state, corpusRoot, corpusRequired });
|
|
87
|
+
if (base) return base;
|
|
88
|
+
if (geminiEnabled() && state.geminiCalls === 0 && /\.(html|css)$/.test(fp)) {
|
|
89
|
+
return { kind: "block", title: "Design pipeline", reason: "BLOCKED: generate the frontend via create_frontend before hand-writing HTML/CSS.", actions: ["Call mcp__gemini-design__create_frontend first"] };
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
// Codex apply_patch (D2): gate each fanned-out file like a Write.
|
|
94
|
+
if (event.files && event.files.length > 0) return designFilesGate(event.files, state, pluginsRoot, corpusRoot, corpusRequired, cwd, findDesignSystem(cwd) !== "");
|
|
95
|
+
if (event.tool === NAV) {
|
|
96
|
+
return browserNavigateGate(state, typeof event.input.url === "string" ? event.input.url : "");
|
|
97
|
+
}
|
|
98
|
+
if (event.tool === SHOT) return screenshotScrollGate(state);
|
|
99
|
+
if (event.tool === GEMINI) {
|
|
100
|
+
if (!geminiEnabled()) return null;
|
|
101
|
+
const block = geminiCreateGate(state);
|
|
102
|
+
if (block) return block;
|
|
103
|
+
const ds = findDesignSystem(cwd);
|
|
104
|
+
// Parity validate-design-system.py DENY_NOT_FOUND: a MISSING file gets its own
|
|
105
|
+
// recovery message, distinct from the "too generic" message for a present-but-thin one.
|
|
106
|
+
if (ds === "") {
|
|
107
|
+
return { kind: "block", title: "Design pipeline", reason: "BLOCKED: design-system.md not found. RECOVERY: 1) Read the identity templates 2) Read design-inspiration.md 3) Read the refs-design corpus (README.md + relevant tokens-*.md) 4) Screenshot 1-2 real sector sites 5) Write design-system.md, then retry create_frontend.", actions: ["Create design-system.md via the pipeline, then retry create_frontend"] };
|
|
108
|
+
}
|
|
109
|
+
// Same content rules as the nominal write path (jointure included): the
|
|
110
|
+
// Gemini branch must not be weaker than the gate the Write just went through.
|
|
111
|
+
const contentBlock = checkDesignSystemContent(ds, state.corpusReads, corpusRequired);
|
|
112
|
+
if (contentBlock) return contentBlock;
|
|
113
|
+
saveDesignState(cacheDir, recordValidDesignSystem(state));
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { capFragment } from "./inject-budget";
|
|
5
|
+
|
|
6
|
+
/** Run a git subcommand in `cwd`, returning trimmed stdout or "" on error. */
|
|
7
|
+
function git(cwd: string, args: string[]): string {
|
|
8
|
+
try {
|
|
9
|
+
return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: "pipe", timeout: 5000 }).trim();
|
|
10
|
+
} catch {
|
|
11
|
+
return "";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Build the git portion of the dev context (branch + up to 5 changed files). */
|
|
16
|
+
export function gitContext(cwd: string): string[] {
|
|
17
|
+
if (!existsSync(join(cwd, ".git"))) return [];
|
|
18
|
+
const parts = [`Git branch: ${git(cwd, ["branch", "--show-current"]) || "unknown"}`];
|
|
19
|
+
const status = git(cwd, ["status", "--porcelain"]);
|
|
20
|
+
if (status) parts.push("Modified files:\n" + status.split("\n").slice(0, 5).join("\n"));
|
|
21
|
+
return parts;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Build the project-type portion (mirrors load-dev-context.py exactly). */
|
|
25
|
+
export function projectContext(cwd: string): string[] {
|
|
26
|
+
const parts: string[] = [];
|
|
27
|
+
const has = (f: string): boolean => existsSync(join(cwd, f));
|
|
28
|
+
if (["next.config.js", "next.config.ts", "next.config.mjs"].some(has)) parts.push("Project: Next.js");
|
|
29
|
+
else if (has("package.json")) parts.push("Project: Node.js");
|
|
30
|
+
if (has("composer.json") && has("artisan")) parts.push("Project: Laravel");
|
|
31
|
+
if (has("Package.swift")) parts.push("Project: Swift");
|
|
32
|
+
return parts;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build the SessionStart dev-context block (git + project type), or "" when
|
|
37
|
+
* nothing applies. Ports `core-guards/scripts/session-start/load-dev-context.py`.
|
|
38
|
+
* @param cwd - Project root to inspect.
|
|
39
|
+
* @returns The joined additionalContext text (possibly empty).
|
|
40
|
+
*/
|
|
41
|
+
export function devContext(cwd: string): string {
|
|
42
|
+
return capFragment("dev-context", [...gitContext(cwd), ...projectContext(cwd)].join("\n"));
|
|
43
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Short identifiers never worth a duplication check (control flow, tiny names). */
|
|
2
|
+
export const DRY_KEYWORDS: ReadonlySet<string> = new Set([
|
|
3
|
+
"if", "for", "while", "switch", "catch", "return", "async",
|
|
4
|
+
"new", "get", "set", "map", "run", "use", "test", "main",
|
|
5
|
+
]);
|
|
6
|
+
|
|
7
|
+
/** Extensions treated as TS/JS-family for symbol extraction. */
|
|
8
|
+
export const TS_EXT: ReadonlySet<string> = new Set([".ts", ".tsx", ".js", ".jsx", ".astro"]);
|
|
9
|
+
|
|
10
|
+
/** Declaration patterns whose capture group 1 is the declared symbol name (TS/JS). */
|
|
11
|
+
export const TS_PATTERNS: readonly RegExp[] = [
|
|
12
|
+
/(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*[(<]/g,
|
|
13
|
+
/(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:async\s*)?\(/g,
|
|
14
|
+
/class\s+(\w+)\b/g,
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Declaration patterns for PHP (capture group 1 = symbol name). The modifier run
|
|
19
|
+
* is bounded (`{0,6}`) on purpose: an unbounded `(?:…\s+)*` is quadratic (O(n²))
|
|
20
|
+
* on a long whitespace/keyword run with no trailing `function`, which would block
|
|
21
|
+
* the hook for seconds on a crafted file. A real PHP signature has at most a few
|
|
22
|
+
* leading keywords, so the bound is behavior-equivalent and keeps matching linear.
|
|
23
|
+
*/
|
|
24
|
+
export const PHP_PATTERNS: readonly RegExp[] = [
|
|
25
|
+
/(?:(?:public|protected|private|static|final|abstract|readonly)\s+){0,6}function\s+(\w+)\s*\(/g,
|
|
26
|
+
/(?:class|interface|trait)\s+(\w+)\b/g,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/** Directories grep skips when scanning for existing declarations. */
|
|
30
|
+
export const EXCLUDE_DIRS: readonly string[] = [
|
|
31
|
+
"vendor", "node_modules", ".next", ".git", "dist", "build", "coverage", ".turbo",
|
|
32
|
+
];
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { extname, resolve, sep } from "node:path";
|
|
3
|
+
import type { Prompt } from "../prompt/types";
|
|
4
|
+
import { DRY_KEYWORDS as KEYWORDS, TS_EXT, TS_PATTERNS, PHP_PATTERNS, EXCLUDE_DIRS } from "./dry-patterns";
|
|
5
|
+
|
|
6
|
+
/** Extract long (>12 char) declared symbol names from new file content. */
|
|
7
|
+
export function extractSymbols(content: string, ext: string): string[] {
|
|
8
|
+
const pats = TS_EXT.has(ext) ? TS_PATTERNS : ext === ".php" ? PHP_PATTERNS : [];
|
|
9
|
+
const names = new Set<string>();
|
|
10
|
+
for (const re of pats) {
|
|
11
|
+
for (const m of content.matchAll(re)) {
|
|
12
|
+
const n = m[1];
|
|
13
|
+
if (n && !KEYWORDS.has(n) && n.length > 12) names.add(n);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return [...names];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** `modules/X/...` -> `"X"`, else `""` (module-boundary key). */
|
|
20
|
+
function moduleOf(path: string): string {
|
|
21
|
+
const parts = path.split(sep);
|
|
22
|
+
const i = parts.indexOf("modules");
|
|
23
|
+
return i >= 0 && i + 1 < parts.length ? (parts[i + 1] ?? "") : "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Verdict from {@link detectDuplication}. */
|
|
27
|
+
export interface DuplicationVerdict {
|
|
28
|
+
names: string[];
|
|
29
|
+
duplicates: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Grep the codebase for existing declarations of the symbols a write introduces,
|
|
34
|
+
* honoring module boundaries (cross-`modules/` matches are ignored). Effectful:
|
|
35
|
+
* shells out to `grep`. Fails open (returns no duplicates) on any grep error,
|
|
36
|
+
* timeout, or no-match — matching the original Python hook.
|
|
37
|
+
*/
|
|
38
|
+
export function detectDuplication(filePath: string, content: string, cwd: string): DuplicationVerdict {
|
|
39
|
+
const ext = extname(filePath).toLowerCase();
|
|
40
|
+
if (!TS_EXT.has(ext) && ext !== ".php") return { names: [], duplicates: [] };
|
|
41
|
+
const names = extractSymbols(content, ext);
|
|
42
|
+
if (!names.length) return { names, duplicates: [] };
|
|
43
|
+
const include = TS_EXT.has(ext)
|
|
44
|
+
? ["--include=*.ts", "--include=*.tsx", "--include=*.js", "--include=*.jsx"]
|
|
45
|
+
: ["--include=*.php"];
|
|
46
|
+
const decl = TS_EXT.has(ext) ? "(function|const|let|class|interface)\\s+" : "(function|class|interface|trait)\\s+";
|
|
47
|
+
const escaped = names.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
48
|
+
const pattern = `${decl}(${escaped})\\b`;
|
|
49
|
+
let out = "";
|
|
50
|
+
try {
|
|
51
|
+
const args = ["-rEl", ...EXCLUDE_DIRS.map((d) => `--exclude-dir=${d}`), ...include, "--", pattern, cwd];
|
|
52
|
+
out = execFileSync("grep", args, { encoding: "utf8", timeout: 1500 });
|
|
53
|
+
} catch {
|
|
54
|
+
return { names, duplicates: [] };
|
|
55
|
+
}
|
|
56
|
+
const self = resolve(filePath);
|
|
57
|
+
const targetMod = moduleOf(filePath);
|
|
58
|
+
const duplicates: string[] = [];
|
|
59
|
+
for (const line of out.split("\n")) {
|
|
60
|
+
const f = line.trim();
|
|
61
|
+
if (!f || resolve(f) === self) continue;
|
|
62
|
+
const dupMod = moduleOf(f);
|
|
63
|
+
if (targetMod && dupMod && dupMod !== targetMod) continue;
|
|
64
|
+
duplicates.push(f);
|
|
65
|
+
}
|
|
66
|
+
return { names, duplicates };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Prompt when a Write/Edit re-declares an existing symbol: a single match is a
|
|
71
|
+
* softer non-blocking "inform" (could be a false positive / same-name coincidence),
|
|
72
|
+
* escalating to a hard "block" once 2+ existing declarations are found. Returns
|
|
73
|
+
* `null` when no duplicate symbol is found at all.
|
|
74
|
+
*/
|
|
75
|
+
export function dryGate(tool: string, filePath: string, content: string | undefined, cwd: string | undefined): Prompt | null {
|
|
76
|
+
if (!cwd || (tool !== "Write" && tool !== "Edit") || !content) return null;
|
|
77
|
+
const dup = detectDuplication(filePath, content, cwd);
|
|
78
|
+
if (dup.duplicates.length === 0) return null;
|
|
79
|
+
const names = dup.names.slice(0, 5).join(", ");
|
|
80
|
+
let files = dup.duplicates.slice(0, 3).join(", ");
|
|
81
|
+
if (dup.duplicates.length > 3) files += ` (+${dup.duplicates.length - 3} more)`;
|
|
82
|
+
if (dup.duplicates.length === 1) {
|
|
83
|
+
return {
|
|
84
|
+
kind: "inform",
|
|
85
|
+
title: "Possible duplicate code (DRY)",
|
|
86
|
+
reason: `[${names}] already declared in: ${files}. Consider importing and reusing instead of re-declaring.`,
|
|
87
|
+
actions: ["Import the existing symbol instead of re-declaring it", "Extend the existing module"],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
kind: "block",
|
|
92
|
+
title: "Duplicate code (DRY)",
|
|
93
|
+
reason: `[${names}] already declared in: ${files}. Import and reuse instead of re-declaring.`,
|
|
94
|
+
actions: ["Import the existing symbol instead of re-declaring it", "Extend the existing module"],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module fragment-registry
|
|
3
|
+
* Per-process registry of injected fragment sizes, populated by
|
|
4
|
+
* {@link module:inject-budget.capFragment} as a side effect and consumed by
|
|
5
|
+
* {@link module:inject-budget-recap} to build ONE event-level recap instead
|
|
6
|
+
* of the scattered per-injection-point reports (each caller of `capFragment`
|
|
7
|
+
* only ever saw its OWN fragment, never the total for the whole event).
|
|
8
|
+
*
|
|
9
|
+
* Module-level state is safe here: each hook invocation is a fresh short-lived
|
|
10
|
+
* CLI process (one per Claude Code hook event), so there is no cross-event
|
|
11
|
+
* leakage in production. {@link resetFragmentRegistry} exists so a single
|
|
12
|
+
* process handling several events in sequence (unit tests; any future
|
|
13
|
+
* long-lived host) can still isolate one event's fragments from the next.
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
import type { FragmentSize } from "./inject-budget";
|
|
17
|
+
|
|
18
|
+
let registry: FragmentSize[] = [];
|
|
19
|
+
|
|
20
|
+
/** Clear the registry. Call once before dispatching a hook event. */
|
|
21
|
+
export function resetFragmentRegistry(): void {
|
|
22
|
+
registry = [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Record one fragment's post-cap size. Called by {@link module:inject-budget.capFragment}. */
|
|
26
|
+
export function recordFragment(label: string, chars: number): void {
|
|
27
|
+
registry.push({ label, chars });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Snapshot of every fragment recorded since the last {@link resetFragmentRegistry}. */
|
|
31
|
+
export function fragmentRegistry(): FragmentSize[] {
|
|
32
|
+
return [...registry];
|
|
33
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { frameworkSolidGate } from "../policy/framework-solid";
|
|
2
|
+
import { skillTriggerGate } from "../policy/skill-triggers";
|
|
3
|
+
import { requiredArchSkill } from "../policy/detect-project";
|
|
4
|
+
import { countFrameworkCodeLines } from "../policy/file-size";
|
|
5
|
+
import { isExcludedJsPath, isExcludedSwiftPath } from "../policy/framework-solid-exclude";
|
|
6
|
+
import type { GateInput } from "./gate-input";
|
|
7
|
+
import type { Prompt } from "../prompt/types";
|
|
8
|
+
|
|
9
|
+
/** PHP paths the Laravel skill check skips — broader than the SOLID vendor-only exclude (parity check-laravel-skill.py `/(vendor|storage|bootstrap/cache)/`). */
|
|
10
|
+
const PHP_SKILL_EXCLUDE_RE: RegExp = /\/(vendor|storage|bootstrap\/cache)\//;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Whether the sub-skill gate should skip this path — parity with the early return
|
|
14
|
+
* in check-*-skill.py (build/vendor artifacts never require a sub-skill).
|
|
15
|
+
* @param framework - the detected framework ("laravel" | "swift" | JS otherwise).
|
|
16
|
+
* @param filePath - the file being written.
|
|
17
|
+
*/
|
|
18
|
+
function skillGateExcluded(framework: string, filePath: string): boolean {
|
|
19
|
+
if (framework === "laravel") return PHP_SKILL_EXCLUDE_RE.test(filePath);
|
|
20
|
+
if (framework === "swift") return isExcludedSwiftPath(filePath);
|
|
21
|
+
return isExcludedJsPath(filePath);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Effective line count for the SOLID size check. On an Edit, `content` is only
|
|
26
|
+
* the `new_string` snippet, so for Next.js — mirroring the base file-size guard
|
|
27
|
+
* and the Python `get_full_file_content` — judge the larger of the snippet and
|
|
28
|
+
* the full on-disk file (`existingCodeLines`). Other frameworks (react/laravel/
|
|
29
|
+
* swift) judge the edited snippet alone, never the on-disk full-file max — their
|
|
30
|
+
* Python source never imports `get_full_file_content`, only nextjs does. On
|
|
31
|
+
* Write, `content` IS the full file, so the snippet count stands regardless of
|
|
32
|
+
* framework (undefined → the gate falls back to `countFrameworkCodeLines`).
|
|
33
|
+
* @param tool - the tool name ("Edit" | "Write" | ...).
|
|
34
|
+
* @param framework - the detected framework; only "nextjs" applies the full-file max.
|
|
35
|
+
* @param content - the written content (snippet on Edit, full file on Write).
|
|
36
|
+
* @param existingCodeLines - full on-disk code-only line count, when known.
|
|
37
|
+
*/
|
|
38
|
+
function effectiveLines(
|
|
39
|
+
tool: string,
|
|
40
|
+
framework: string,
|
|
41
|
+
content: string,
|
|
42
|
+
existingCodeLines?: number,
|
|
43
|
+
): number | undefined {
|
|
44
|
+
if (tool !== "Edit" || existingCodeLines === undefined) return undefined;
|
|
45
|
+
if (framework !== "nextjs") return countFrameworkCodeLines(content);
|
|
46
|
+
return Math.max(countFrameworkCodeLines(content), existingCodeLines);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Framework-aware SOLID + sub-skill gate, run on the Write/Edit path once a
|
|
51
|
+
* `filePath` is present. Combines:
|
|
52
|
+
* - {@link frameworkSolidGate}: framework-specific SOLID rules (line limits,
|
|
53
|
+
* interface/protocol separation, `'use client'`, @MainActor...).
|
|
54
|
+
* - {@link skillTriggerGate}: blocks when written APIs need a sub-skill that
|
|
55
|
+
* was not read this session, also forcing the modular-architecture skill
|
|
56
|
+
* resolved from disk via {@link requiredArchSkill}.
|
|
57
|
+
*
|
|
58
|
+
* @param input - the gated tool-use (filePath + content + framework + cwd).
|
|
59
|
+
* @param refsRead - in-session read reference paths (from the loaded track).
|
|
60
|
+
* @param existingCodeLines - full on-disk code-only line count (so an Edit on
|
|
61
|
+
* an oversized file still fires the framework SOLID size rule). Omit on Write.
|
|
62
|
+
* @returns the first blocking {@link Prompt}, or `null` to allow.
|
|
63
|
+
*/
|
|
64
|
+
export function frameworkSkillGate(
|
|
65
|
+
input: GateInput,
|
|
66
|
+
refsRead: string[],
|
|
67
|
+
existingCodeLines?: number,
|
|
68
|
+
): Prompt | null {
|
|
69
|
+
if (!input.filePath) return null;
|
|
70
|
+
const content = input.content ?? "";
|
|
71
|
+
const solid = frameworkSolidGate(input.filePath, content, effectiveLines(input.tool, input.framework, content, existingCodeLines));
|
|
72
|
+
if (solid) return solid;
|
|
73
|
+
// Skill-gate path exclusions (parity check-*-skill.py early return): a build/
|
|
74
|
+
// vendor artifact never triggers the sub-skill requirement. Applied only to the
|
|
75
|
+
// skill check, not frameworkSolidGate (whose PHP exclude is vendor-only).
|
|
76
|
+
if (skillGateExcluded(input.framework, input.filePath)) return null;
|
|
77
|
+
const forced = input.cwd ? requiredArchSkill(input.cwd) : null;
|
|
78
|
+
return skillTriggerGate(input.framework, content, refsRead, forced, input.cwd, input.filePath);
|
|
79
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, rmdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, relative, sep } from "node:path";
|
|
3
|
+
|
|
4
|
+
/** Age of a file in seconds (now - mtime). Infinity when unstat-able. */
|
|
5
|
+
function ageSec(path: string, now: number): number {
|
|
6
|
+
try {
|
|
7
|
+
return (now - statSync(path).mtimeMs) / 1000;
|
|
8
|
+
} catch {
|
|
9
|
+
return Infinity;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Remove files directly under `dir` matching `test` older than `maxAgeSec`. */
|
|
14
|
+
export function removeOldFiles(dir: string, test: (name: string) => boolean, maxAgeSec: number, now: number = Date.now()): void {
|
|
15
|
+
if (!existsSync(dir)) return;
|
|
16
|
+
for (const name of readdirSync(dir)) {
|
|
17
|
+
const path = join(dir, name);
|
|
18
|
+
if (test(name) && ageSec(path, now) > maxAgeSec) {
|
|
19
|
+
try { rmSync(path, { force: true }); } catch { /* best effort */ }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Trim `file` to its last `keepLines` lines when it exceeds `maxBytes`. */
|
|
25
|
+
export function trimLogFile(file: string, maxBytes: number, keepLines: number): void {
|
|
26
|
+
try {
|
|
27
|
+
if (!existsSync(file) || statSync(file).size <= maxBytes) return;
|
|
28
|
+
const lines = readFileSync(file, "utf-8").split("\n");
|
|
29
|
+
writeFileSync(file, lines.slice(-keepLines).join("\n"), "utf-8");
|
|
30
|
+
} catch { /* best effort */ }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Recursively purge files under `root/<top>` older than `ttls[top]` seconds. */
|
|
34
|
+
export function purgeTtlTree(root: string, ttls: Record<string, number>, now: number = Date.now()): void {
|
|
35
|
+
if (!existsSync(root)) return;
|
|
36
|
+
for (const [top, ttlSec] of Object.entries(ttls)) {
|
|
37
|
+
walkPurge(join(root, top), ttlSec, now);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function walkPurge(dir: string, ttlSec: number, now: number): void {
|
|
42
|
+
if (!existsSync(dir)) return;
|
|
43
|
+
for (const name of readdirSync(dir)) {
|
|
44
|
+
const path = join(dir, name);
|
|
45
|
+
let isDir = false;
|
|
46
|
+
try { isDir = statSync(path).isDirectory(); } catch { continue; }
|
|
47
|
+
if (isDir) { walkPurge(path, ttlSec, now); continue; }
|
|
48
|
+
if (ageSec(path, now) > ttlSec) {
|
|
49
|
+
try { rmSync(path, { force: true }); } catch { /* best effort */ }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Bottom-up removal of empty subdirs under each `root/<top>` (best effort). */
|
|
55
|
+
export function pruneEmptyDirs(root: string, tops: string[]): void {
|
|
56
|
+
for (const top of tops) {
|
|
57
|
+
const sub = join(root, top);
|
|
58
|
+
if (!existsSync(sub)) continue;
|
|
59
|
+
pruneEmpty(sub, sub);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function pruneEmpty(dir: string, stopAt: string): void {
|
|
64
|
+
for (const name of existsSync(dir) ? readdirSync(dir) : []) {
|
|
65
|
+
const path = join(dir, name);
|
|
66
|
+
try { if (statSync(path).isDirectory()) pruneEmpty(path, stopAt); } catch { /* skip */ }
|
|
67
|
+
}
|
|
68
|
+
if (dir !== stopAt && relative(stopAt, dir).split(sep)[0] !== "..") {
|
|
69
|
+
try { rmdirSync(dir); } catch { /* non-empty or gone */ }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { evaluateApex, PRE_AUTH_GATES, POST_AUTH_GATES, type ApexContext } from "../policy/apex";
|
|
2
|
+
import { apexAuthorizationGate } from "../policy/apex-authorization";
|
|
3
|
+
import { isTrivialEdit, protectedPathGate, SKIP_DIRS } from "../policy/trivial-edits";
|
|
4
|
+
import { FAIL_CLOSED } from "../policy/guards";
|
|
5
|
+
import { agentsFresh, recordTarget, recordTrivialEdit, trivialCount, type SessionTrack } from "../tracking/session-state";
|
|
6
|
+
import { agentsRanFromTranscript } from "../freshness/agent-evidence";
|
|
7
|
+
import { agentsFreshInTrack } from "../freshness/agent-evidence-record";
|
|
8
|
+
import { withTrack } from "../tracking/store";
|
|
9
|
+
import { REQUIRED_AGENTS, TRIVIAL_BUDGET } from "./gate";
|
|
10
|
+
import type { GateInput } from "./gate-input";
|
|
11
|
+
import type { Prompt } from "../prompt/types";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The APEX-scoped portion of {@link gate}, in the Python enforce-apex-phases.ts
|
|
15
|
+
* order: protected-paths guard, skip-dirs allow, trivial-edit fast path, then
|
|
16
|
+
* brainstorm/freshness, Check 1 (per-framework doc authorization — a deny
|
|
17
|
+
* persists `track.target` so recordDoc can cross-credit the framework) and
|
|
18
|
+
* Check 2 + SOLID refs. Freshness scans the SESSION track first (parity
|
|
19
|
+
* _scan_agents: sub-agent hooks share the lead's session_id, so sidechain/
|
|
20
|
+
* Workflow research is visible); the platform-authored transcript scan and the
|
|
21
|
+
* legacy exact-name track check are fallbacks.
|
|
22
|
+
* @param input - The tool-use to gate.
|
|
23
|
+
* @param track - The loaded session track.
|
|
24
|
+
* @param window - Freshness window in ms.
|
|
25
|
+
* @returns The first blocking prompt, or null to fall through to the DRY check.
|
|
26
|
+
*/
|
|
27
|
+
export async function apexScopedGate(input: GateInput, track: SessionTrack, window: number): Promise<Prompt | null> {
|
|
28
|
+
// (1) Hook-managed paths: absolute deny BEFORE any other filter (parity :49-52).
|
|
29
|
+
const protectedDeny = protectedPathGate(input.tool, input.filePath);
|
|
30
|
+
if (protectedDeny) return protectedDeny;
|
|
31
|
+
// (2) CODE_EXT runs upstream (gate.ts isApexScoped); dependency/build dirs skip (parity :53-54).
|
|
32
|
+
if (SKIP_DIRS.test(input.filePath ?? "")) return null;
|
|
33
|
+
|
|
34
|
+
// (3) Trivial-edit fast path: a few tiny, non-replace Edits skip the APEX gates
|
|
35
|
+
// (parity :58-65 — only Edit ever qualifies as "trivial"; a Write always
|
|
36
|
+
// creates/replaces a file wholesale and must go through the full gate).
|
|
37
|
+
if (isTrivialEdit(input.tool, input.content, input.isReplaceAll) && trivialCount(track, window, input.now) < TRIVIAL_BUDGET) {
|
|
38
|
+
await withTrack(input.trackFile, (fresh) => recordTrivialEdit(fresh, input.now, window, input.now));
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const freshnessFor = (names: string[], windowMs: number = window): boolean =>
|
|
43
|
+
agentsFreshInTrack(track, names, windowMs, input.now) ||
|
|
44
|
+
(input.transcriptPath ? agentsRanFromTranscript(input.transcriptPath, names, windowMs, input.now) : agentsFresh(track, names, windowMs, input.now));
|
|
45
|
+
|
|
46
|
+
const ctx: ApexContext = {
|
|
47
|
+
sessionId: input.sessionId,
|
|
48
|
+
framework: input.framework,
|
|
49
|
+
filePath: input.filePath ?? "",
|
|
50
|
+
content: input.content ?? "",
|
|
51
|
+
authorizations: track.authorizations,
|
|
52
|
+
refs: input.refs,
|
|
53
|
+
refsRead: track.refsRead,
|
|
54
|
+
refsReadAt: track.refsReadAt, // SOLID-read TTL stamps (absent → no expiry)
|
|
55
|
+
now: input.now,
|
|
56
|
+
agentsFresh: freshnessFor([...REQUIRED_AGENTS]),
|
|
57
|
+
missingAgents: REQUIRED_AGENTS.filter((name) => !freshnessFor([name])),
|
|
58
|
+
windowMs: window,
|
|
59
|
+
// Parity require-apex-agents.py:41 — Edit never creates a file (skip brainstorm),
|
|
60
|
+
// and a subagent (agent_id present) inherits the lead's brainstorm decision, so
|
|
61
|
+
// only a lead Write is ever brainstorm-gated.
|
|
62
|
+
brainstormRequired: input.tool === "Edit" || input.agentId ? false : track.brainstormRequired,
|
|
63
|
+
brainstormFresh: freshnessFor(["brainstorming"], Number.MAX_SAFE_INTEGER),
|
|
64
|
+
};
|
|
65
|
+
try {
|
|
66
|
+
// (4) Brainstorm + explore/research freshness (pre-existing harness gates).
|
|
67
|
+
const pre = evaluateApex(ctx, PRE_AUTH_GATES);
|
|
68
|
+
if (pre) return pre;
|
|
69
|
+
// (5) Check 1 — a deny persists `target` (parity :80-81) for recordDoc's cross-credit.
|
|
70
|
+
const authDeny = apexAuthorizationGate(ctx);
|
|
71
|
+
if (authDeny) {
|
|
72
|
+
const set_at = new Date(input.now).toISOString();
|
|
73
|
+
await withTrack(input.trackFile, (fresh) => recordTarget(fresh, { project: input.cwd ?? "", framework: input.framework, set_by: "apex-authorization", set_at }));
|
|
74
|
+
return authDeny;
|
|
75
|
+
}
|
|
76
|
+
// (6) Check 2 (doc consulted once per session) + SOLID refs.
|
|
77
|
+
return evaluateApex(ctx, POST_AUTH_GATES);
|
|
78
|
+
} catch {
|
|
79
|
+
return FAIL_CLOSED;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { countFrameworkCodeLines, countLines } from "../policy/file-size";
|
|
3
|
+
|
|
4
|
+
/** Source extensions the APEX gates apply to (parity: require-apex-agents.py CODE_EXT). */
|
|
5
|
+
const CODE_EXT = /\.(ts|tsx|js|jsx|py|go|rs|java|php|cpp|c|rb|swift|kt|dart|vue|svelte|astro)$/;
|
|
6
|
+
|
|
7
|
+
/** Paths exempt from the APEX gates even when they are code (parity: require-apex-agents.py EXEMPT_PATTERNS). */
|
|
8
|
+
const EXEMPT_PATTERNS: readonly RegExp[] = [
|
|
9
|
+
/\.claude-plugin\//,
|
|
10
|
+
/CHANGELOG\.md$/,
|
|
11
|
+
/marketplace\.json$/,
|
|
12
|
+
/\/\.(claude|codex)\/(apex|memory|logs|fusengine-cache)\//,
|
|
13
|
+
/\/\.fuse-harness\//,
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* True when `filePath` is a code file the APEX freshness/doc/SOLID gates should
|
|
18
|
+
* police — i.e. it matches a tracked source extension AND no exempt pattern.
|
|
19
|
+
* Mirrors require-apex-agents.py so non-code (and exempt) paths skip those gates.
|
|
20
|
+
* @param filePath - Absolute path of the edit target (or undefined).
|
|
21
|
+
* @returns Whether the APEX gates apply to this path.
|
|
22
|
+
*/
|
|
23
|
+
export function isApexScoped(filePath: string | undefined): boolean {
|
|
24
|
+
if (!filePath || !CODE_EXT.test(filePath)) return false;
|
|
25
|
+
return !EXEMPT_PATTERNS.some((p) => p.test(filePath));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Raw + code-only line counts of the existing on-disk file. */
|
|
29
|
+
export interface ExistingLineCounts {
|
|
30
|
+
/** Raw physical count — parity with core-guards `enforce-file-size.py` (`sum(1 for _ in f)`), fed to the generic `evaluate()` ceiling. */
|
|
31
|
+
raw?: number;
|
|
32
|
+
/** Code-only count (blank/comment lines excluded) — parity with the framework validators' `count_code_lines`, fed to `frameworkSkillGate`. */
|
|
33
|
+
code?: number;
|
|
34
|
+
/** Full on-disk content, read once alongside the counts — fed to the Edit-outcome file-size gate (policy/edit-outcome.ts via evaluate.ts) so it can compute the exact post-edit line count without a second read. */
|
|
35
|
+
content?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Read `path` once and derive both line-count metrics, so the generic
|
|
40
|
+
* core-guards ceiling (`raw`) and the framework SOLID gates (`code`) each get
|
|
41
|
+
* the metric their own Python origin measures, without reading the file twice.
|
|
42
|
+
* @param path - Absolute path of the file being edited (or undefined).
|
|
43
|
+
*/
|
|
44
|
+
export function existingLineCounts(path: string | undefined): ExistingLineCounts {
|
|
45
|
+
if (!path || !existsSync(path)) return {};
|
|
46
|
+
try {
|
|
47
|
+
const content = readFileSync(path, "utf8");
|
|
48
|
+
return { raw: countLines(content), code: countFrameworkCodeLines(content), content };
|
|
49
|
+
} catch {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { RefMeta } from "../refs/types";
|
|
2
|
+
|
|
3
|
+
/** A tool-use to gate, plus the session pointers needed for the stateful gates. */
|
|
4
|
+
export interface GateInput {
|
|
5
|
+
sessionId: string;
|
|
6
|
+
framework: string;
|
|
7
|
+
tool: string;
|
|
8
|
+
filePath?: string;
|
|
9
|
+
content?: string;
|
|
10
|
+
command?: string;
|
|
11
|
+
cwd?: string;
|
|
12
|
+
refs?: RefMeta[];
|
|
13
|
+
now: number;
|
|
14
|
+
trackFile: string;
|
|
15
|
+
windowMs?: number;
|
|
16
|
+
isReplaceAll?: boolean;
|
|
17
|
+
/** Edit only: the tool_input.old_string being replaced (runtime/normalize.ts) — threaded to evaluate()'s file-size gate so it can compute the post-edit outcome (policy/edit-outcome.ts) instead of judging the stale on-disk count alone. */
|
|
18
|
+
oldString?: string;
|
|
19
|
+
agentType?: string;
|
|
20
|
+
/** Claude `agent_id` when the tool-use comes from a subagent (parity require-apex-agents.py:41 — subagents inherit the lead's brainstorm decision). */
|
|
21
|
+
agentId?: string;
|
|
22
|
+
/** Absolute path to the session transcript (Claude `transcript_path`) for evidence-based freshness. */
|
|
23
|
+
transcriptPath?: string;
|
|
24
|
+
/** See PolicyContext.neverApproval — populated only by handle-pre.ts for id==="codex" (approval_policy=never has no interactive ask channel). */
|
|
25
|
+
neverApproval?: boolean;
|
|
26
|
+
}
|