@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,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module deny-loop-store
|
|
3
|
+
* Sidecar state + gate wiring for the mechanical anti-loop ({@link module:deny-loop}).
|
|
4
|
+
*
|
|
5
|
+
* STATE — a standalone sidecar (`deny-loop.json`) in the same per-project state
|
|
6
|
+
* dir as the session track, NOT a field in `session-state.ts`: that file is owned
|
|
7
|
+
* by another concern this batch, so a shared field would couple two owners and
|
|
8
|
+
* contend on one file for two unrelated maps. This mirrors the proven
|
|
9
|
+
* {@link module:inject-dedup} `oncePerWindow` sidecar (atomicWrite, prune-by-window,
|
|
10
|
+
* fail-open) and keeps the anti-loop self-contained.
|
|
11
|
+
*
|
|
12
|
+
* WINDOW — the hash expires with the same freshness window the gate already uses
|
|
13
|
+
* (`FUSE_ENFORCE_TTL_SEC` / `windowMs`): a loop is a burst, not a lifetime, so a
|
|
14
|
+
* stale deny past the window resets and a later identical call is a fresh #1.
|
|
15
|
+
* @packageDocumentation
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { atomicWrite } from "../util/json-io";
|
|
20
|
+
import { denyHash, denyLoopCheck, enrichRepeatDeny, type DenyEntry, type DenyLoopResult } from "../policy/deny-loop";
|
|
21
|
+
import { BURST_DEDUP_MS } from "./burst-window";
|
|
22
|
+
import type { Prompt } from "../prompt/types";
|
|
23
|
+
|
|
24
|
+
/** Sidecar basename under the per-project state dir. */
|
|
25
|
+
const SIDECAR = "deny-loop.json";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Injected clock + state dir + expiry window (no env var; the gate supplies all
|
|
29
|
+
* three). `sessionId` — when present — scopes the map key AND arms the burst
|
|
30
|
+
* dedup ({@link module:burst-window}) so the ~11 sibling plugin hooks of ONE
|
|
31
|
+
* event count once, not once each; two sessions never share a counter. Omitted
|
|
32
|
+
* by mono-process callers/tests → historical per-hash, no-dedup behaviour.
|
|
33
|
+
*/
|
|
34
|
+
export interface DenyLoopOpts { now: number; dir: string; windowMs: number; sessionId?: string; }
|
|
35
|
+
|
|
36
|
+
/** Load the `{ hash -> DenyEntry }` map, or `{}` when missing/corrupt. */
|
|
37
|
+
function loadMap(path: string): Record<string, DenyEntry> {
|
|
38
|
+
try {
|
|
39
|
+
if (!existsSync(path)) return {};
|
|
40
|
+
const data: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
41
|
+
return data && typeof data === "object" && !Array.isArray(data) ? (data as Record<string, DenyEntry>) : {};
|
|
42
|
+
} catch {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Drop entries whose last deny is older than the window (bounds size + resets stale loops). */
|
|
48
|
+
function prune(map: Record<string, DenyEntry>, now: number, windowMs: number): Record<string, DenyEntry> {
|
|
49
|
+
const out: Record<string, DenyEntry> = {};
|
|
50
|
+
for (const [k, e] of Object.entries(map)) {
|
|
51
|
+
if (e && typeof e.lastTs === "number" && now - e.lastTs < windowMs) out[k] = e;
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Record a deny for `(tool, input)`: prune, {@link denyLoopCheck}, persist the
|
|
58
|
+
* bumped entry. Fails open (`isRepeat:false`) when the sidecar is unwritable — a
|
|
59
|
+
* broken state dir must never manufacture a false `[REPEAT]`.
|
|
60
|
+
* @param tool - Tool name.
|
|
61
|
+
* @param input - Identifying tool input.
|
|
62
|
+
* @param opts - Clock + state dir + window.
|
|
63
|
+
* @returns `{ isRepeat, count, hash }`.
|
|
64
|
+
*/
|
|
65
|
+
export function recordDeny(tool: string, input: Record<string, unknown>, opts: DenyLoopOpts): DenyLoopResult {
|
|
66
|
+
const hash = denyHash(tool, input);
|
|
67
|
+
const sid = opts.sessionId?.trim();
|
|
68
|
+
const key = sid ? `${hash}::${sid}` : hash;
|
|
69
|
+
const path = join(opts.dir, SIDECAR);
|
|
70
|
+
const map = prune(loadMap(path), opts.now, opts.windowMs);
|
|
71
|
+
const res = denyLoopCheck(key, map, { now: opts.now, windowMs: opts.windowMs, dedupMs: sid ? BURST_DEDUP_MS : 0 });
|
|
72
|
+
if (!res.deduped) {
|
|
73
|
+
map[key] = { count: res.count, lastTs: opts.now };
|
|
74
|
+
try { atomicWrite(path, JSON.stringify(map)); } catch { /* fail-open: never fabricate a repeat */ }
|
|
75
|
+
}
|
|
76
|
+
return { ...res, hash };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Gate tail: record every block deny; on a repeat, return the enriched prompt.
|
|
81
|
+
* Allows (`null`) and non-block prompts (`ask`/`inform`) pass through untouched —
|
|
82
|
+
* a loop is a retried REFUSAL, and only `block` is a refusal. The decision is
|
|
83
|
+
* NEVER changed; only a repeated block's message is rewritten.
|
|
84
|
+
* @param prompt - The gate's outcome.
|
|
85
|
+
* @param tool - Tool name.
|
|
86
|
+
* @param input - Identifying tool input.
|
|
87
|
+
* @param opts - Clock + state dir + window.
|
|
88
|
+
* @returns The prompt, enriched only when it is a repeated block.
|
|
89
|
+
*/
|
|
90
|
+
export function withDenyLoop(prompt: Prompt | null, tool: string, input: Record<string, unknown>, opts: DenyLoopOpts): Prompt | null {
|
|
91
|
+
if (!prompt || prompt.kind !== "block") return prompt;
|
|
92
|
+
const { isRepeat, count } = recordDeny(tool, input, opts);
|
|
93
|
+
return isRepeat ? enrichRepeatDeny(prompt, count) : prompt;
|
|
94
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module deny-notice
|
|
3
|
+
* Attach a user-visible notice to a deny/ask hook response — the owner-reported
|
|
4
|
+
* gap where `permissionDecision: deny/ask` (the agent-only channel) left the
|
|
5
|
+
* human staring at a silent terminal. Mirrors {@link module:notices}'s
|
|
6
|
+
* compliance notices but for the BLOCKING outcomes those never covered.
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
import figures from "figures";
|
|
10
|
+
import type { Prompt } from "../prompt/types";
|
|
11
|
+
import { attachSystemMessage } from "../adapters/claude";
|
|
12
|
+
import { onceExclusive } from "./inject-dedup";
|
|
13
|
+
import { BURST_DEDUP_MS } from "./burst-window";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The human-facing line for a block/ask outcome: the gate's own
|
|
17
|
+
* {@link Prompt.userMessage} when it set one, else a generic symbol + title —
|
|
18
|
+
* text-presentation Unicode only (`figures.cross`/`?`), never emoji: terminal-safe,
|
|
19
|
+
* single-cell width, with the Windows fallbacks `figures` already resolves, matching
|
|
20
|
+
* the existing notice family (`notices.ts`'s `✓`/`⚠`, left untouched by this module).
|
|
21
|
+
* Null for `inform` ({@link module:respond}'s own `userMessage` path already
|
|
22
|
+
* covers it) or when neither applies.
|
|
23
|
+
*/
|
|
24
|
+
export function denyAskNotice(prompt: Prompt): string | null {
|
|
25
|
+
if (prompt.kind === "block") return prompt.userMessage ?? `${figures.cross} ${prompt.title}`;
|
|
26
|
+
if (prompt.kind === "ask") return prompt.userMessage ?? `? ${prompt.title}`;
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Attach {@link denyAskNotice} onto an already-rendered deny/ask `stdout`, for
|
|
32
|
+
* the harnesses whose human channel is `systemMessage` (claude-code/codex —
|
|
33
|
+
* `permissionDecision`/`permissionDecisionReason` stay byte-intact, only the
|
|
34
|
+
* top-level field is added). Every other harness passes `stdout` through
|
|
35
|
+
* unchanged: Cursor already emits `user_message` natively (respond.ts), Hermes
|
|
36
|
+
* and cline have no human channel. Deduped via {@link onceExclusive} against
|
|
37
|
+
* the ~11 sibling-plugin fan-out for one real event (same window as the
|
|
38
|
+
* sniper reminder / compliance notices).
|
|
39
|
+
* @param id - Harness id (`ctx.id` from `PreContext`).
|
|
40
|
+
* @param stdout - The rendered hook response (from {@link module:respond.respond}).
|
|
41
|
+
* @param prompt - The {@link Prompt} that produced `stdout`.
|
|
42
|
+
* @param sessionId - Current session id (dedup scope).
|
|
43
|
+
* @param dir - State-dir for the dedup marker (per-project state dir).
|
|
44
|
+
* @param now - Event clock (tests pass a fake one).
|
|
45
|
+
*/
|
|
46
|
+
export function withDenyNotice(id: string, stdout: string, prompt: Prompt, sessionId: string, dir: string, now?: number): string {
|
|
47
|
+
if (id !== "claude-code" && id !== "codex") return stdout;
|
|
48
|
+
const notice = denyAskNotice(prompt);
|
|
49
|
+
if (!notice) return stdout;
|
|
50
|
+
if (!onceExclusive(`deny-notice:${sessionId}:${prompt.title}`, BURST_DEDUP_MS, { now, dir })) return stdout;
|
|
51
|
+
return attachSystemMessage(stdout, notice);
|
|
52
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { fuseHarnessHome, sanitizeSessionId } from "./home-state";
|
|
4
|
+
|
|
5
|
+
/** `~/.fuse-harness/design-sessions/<sid>` — session-anchored design-state dir. */
|
|
6
|
+
function sessionCacheDir(sid: string, home?: string): string {
|
|
7
|
+
return join(fuseHarnessHome(home), "design-sessions", sid);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** True when `dir` already holds the design-agent flag or a `.design-state-*.json` snapshot. */
|
|
11
|
+
function hasDesignState(dir: string): boolean {
|
|
12
|
+
if (existsSync(join(dir, "design-agent-active"))) return true;
|
|
13
|
+
try {
|
|
14
|
+
return readdirSync(dir).some((name) => name.startsWith(".design-state-"));
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the cache dir the design pipeline (active-agent flag +
|
|
22
|
+
* `.design-state-<agentId>.json`) should read AND write for this hook call.
|
|
23
|
+
*
|
|
24
|
+
* Anchored on the session id, not the invoking process's `cwd`: a design
|
|
25
|
+
* agent's `SubagentStart` and its later tool writes can run with different
|
|
26
|
+
* `process.cwd()` values, and a cwd-keyed cache dir then silently misses the
|
|
27
|
+
* flag/state — every design gate fails open (see MEMORY/LESSON.md).
|
|
28
|
+
*
|
|
29
|
+
* Contract (non-regression): `resolved = sessionDirIfPopulated ??
|
|
30
|
+
* cwdDirIfPopulated ?? sessionDirDefault`. Before this fix the session-keyed
|
|
31
|
+
* dir never existed, so for every session whose state already lives under
|
|
32
|
+
* `cwdCacheDir` the FIRST branch is always empty and resolution falls through
|
|
33
|
+
* to `cwdCacheDir` — byte-identical to the pre-fix value. Only a session with
|
|
34
|
+
* NO state anywhere (today's silent-fail-open case, e.g. cwd drifted between
|
|
35
|
+
* `SubagentStart` and the write) diverges, anchoring on the session dir
|
|
36
|
+
* instead of silently missing the flag. Read and write always resolve through
|
|
37
|
+
* this same function against the same two inputs, so they can never target
|
|
38
|
+
* different locations mid-session.
|
|
39
|
+
* @param sessionId - The hook event's session id (`NormalizedEvent.sessionId`).
|
|
40
|
+
* @param cwdCacheDir - The legacy, cwd-derived cache dir (`projectLayout(cwd).cacheDir`).
|
|
41
|
+
* @param home - OS home override (tests only).
|
|
42
|
+
* @returns The cache dir to pass to the design flag/state/gate functions.
|
|
43
|
+
*/
|
|
44
|
+
export function resolveDesignCacheDir(sessionId: string, cwdCacheDir: string, home?: string): string {
|
|
45
|
+
const sid = sanitizeSessionId(sessionId);
|
|
46
|
+
if (!sid) return cwdCacheDir; // no stable session id -> unchanged legacy behavior
|
|
47
|
+
const sessDir = sessionCacheDir(sid, home);
|
|
48
|
+
if (hasDesignState(sessDir)) return sessDir;
|
|
49
|
+
if (hasDesignState(cwdCacheDir)) return cwdCacheDir;
|
|
50
|
+
return sessDir;
|
|
51
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module design-content-gate
|
|
3
|
+
* PRE content gate for design-system.md — the nominal path (validateDesignSystem
|
|
4
|
+
* used to run only behind the opt-in Gemini branch, so designSystemValid meant
|
|
5
|
+
* "a write happened", not "the content is valid"). Split out of
|
|
6
|
+
* `design-helpers.ts` to keep both files within the SOLID size budget (SRP).
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
10
|
+
import type { Prompt } from "../prompt/types";
|
|
11
|
+
import type { DesignState } from "../policy/design/state";
|
|
12
|
+
import { deny, validateDesignSystem } from "../policy/design/gates";
|
|
13
|
+
import { citedCorpusRefs, citationJoinsReads } from "../policy/design/corpus";
|
|
14
|
+
import { substituteLiteral } from "./design-files-gate";
|
|
15
|
+
|
|
16
|
+
/** Inputs for {@link designSystemContentGate} — the PRE write event plus resolved corpus context. */
|
|
17
|
+
interface ContentGateInput {
|
|
18
|
+
filePath: string;
|
|
19
|
+
tool: string;
|
|
20
|
+
content: string;
|
|
21
|
+
oldString?: string;
|
|
22
|
+
replaceAll: boolean;
|
|
23
|
+
state: DesignState;
|
|
24
|
+
corpusRoot: string;
|
|
25
|
+
corpusRequired: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Every content violation of a design-system.md text, GRANULAR (one entry per
|
|
30
|
+
* unmet requirement and per unread cited reference — so a worsening check can
|
|
31
|
+
* diff two violation sets): the four hard requirements, plus the citation↔read
|
|
32
|
+
* jointure when the corpus is delivered.
|
|
33
|
+
*/
|
|
34
|
+
export function designSystemProblems(content: string, reads: readonly string[], corpusRequired: boolean): string[] {
|
|
35
|
+
const problems = validateDesignSystem(content, corpusRequired);
|
|
36
|
+
if (corpusRequired) {
|
|
37
|
+
for (const ref of citedCorpusRefs(content)) {
|
|
38
|
+
if (!citationJoinsReads(ref, reads)) problems.push(`cited corpus reference never read: ${ref}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return problems;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Content check for a design-system.md already on disk (the Gemini create_frontend path). */
|
|
45
|
+
export function checkDesignSystemContent(content: string, reads: readonly string[], corpusRequired: boolean): Prompt | null {
|
|
46
|
+
const problems = designSystemProblems(content, reads, corpusRequired);
|
|
47
|
+
return problems.length ? deny(`BLOCKED: design-system.md too generic. Missing: ${problems.join(", ")}.`) : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The content a Write/Edit would LEAVE in design-system.md, and the on-disk
|
|
52
|
+
* original when the file exists (for BOTH tools). Edit reconstructs via
|
|
53
|
+
* literal substitution (first occurrence, or all under replace_all — the Edit
|
|
54
|
+
* tool's own semantics, mirroring edit-outcome.ts). Null when unverifiable
|
|
55
|
+
* (unreadable file or stale old_string) — fail-closed, such an Edit fails at
|
|
56
|
+
* tool level anyway.
|
|
57
|
+
*/
|
|
58
|
+
function effectiveContents(i: ContentGateInput): { result: string; original: string | null } | null {
|
|
59
|
+
if (i.tool !== "Edit") {
|
|
60
|
+
let original: string | null = null;
|
|
61
|
+
try {
|
|
62
|
+
if (existsSync(i.filePath)) original = readFileSync(i.filePath, "utf8");
|
|
63
|
+
} catch {
|
|
64
|
+
original = null;
|
|
65
|
+
}
|
|
66
|
+
return { result: i.content, original };
|
|
67
|
+
}
|
|
68
|
+
let disk: string;
|
|
69
|
+
try {
|
|
70
|
+
disk = readFileSync(i.filePath, "utf8");
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
if (!i.oldString || !disk.includes(i.oldString)) return null;
|
|
75
|
+
return { result: substituteLiteral(disk, i.oldString, i.content, i.replaceAll), original: disk };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Block only the violations the change INTRODUCES (`problems(result) ⊄
|
|
80
|
+
* problems(original)`): a legacy design-system.md that predates the doctrine
|
|
81
|
+
* stays editable — and rewritable — including byte-identically or toward
|
|
82
|
+
* conformance; a change that adds a NEW problem (or writes an invalid file
|
|
83
|
+
* from scratch) is blocked.
|
|
84
|
+
*/
|
|
85
|
+
export function designSystemContentGate(i: ContentGateInput): Prompt | null {
|
|
86
|
+
if (!i.filePath.endsWith("design-system.md")) return null;
|
|
87
|
+
const pair = effectiveContents(i);
|
|
88
|
+
if (pair === null) return deny("BLOCKED: cannot verify the edit result (unreadable file or stale old_string). RECOVERY: re-read design-system.md, then retry.");
|
|
89
|
+
const result = designSystemProblems(pair.result, i.state.corpusReads, i.corpusRequired);
|
|
90
|
+
if (!result.length) return null;
|
|
91
|
+
const original = pair.original !== null ? designSystemProblems(pair.original, i.state.corpusReads, i.corpusRequired) : [];
|
|
92
|
+
const introduced = result.filter((p) => !original.includes(p));
|
|
93
|
+
if (!introduced.length) return null;
|
|
94
|
+
return deny(`BLOCKED: design-system.md too generic. Missing: ${introduced.join(", ")}. RECOVERY: fix the content (read cited refs-design references with the Read tool), then retry.`);
|
|
95
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module design-files-gate
|
|
3
|
+
* Gates for multi-file write primitives (Codex `apply_patch`, fanned into
|
|
4
|
+
* `event.files` by normalize.ts): apply_patch IS the Write tool under Codex,
|
|
5
|
+
* so the design pipeline must see it. Per op, because `content` means
|
|
6
|
+
* different things (apply-patch.ts:10-15):
|
|
7
|
+
* - add: content IS the full new document — content-gated like a Write;
|
|
8
|
+
* - update: content is the hunk's new side, APPROXIMATE, never the document —
|
|
9
|
+
* only content-independent gates run (path, state-file, phase/quota).
|
|
10
|
+
* No BLOCKING or DEGRADING validation on this path — promote-only:
|
|
11
|
+
* the POST re-reads the real file, a clean content promotes to
|
|
12
|
+
* phase 3, a dirty one changes nothing. And it would not bite
|
|
13
|
+
* anyway: designSystemValid is consumed ONLY under
|
|
14
|
+
* FUSE_DESIGN_GEMINI=1 (off by default — an informational flag),
|
|
15
|
+
* and designSystemExists is never read after init;
|
|
16
|
+
* - delete: skipped (parity with the sibling applyPatchGate).
|
|
17
|
+
* One violating file blocks the whole envelope. `htmlCssOnlyGate` NOW applies
|
|
18
|
+
* here too (owner D2 revised): the two write paths must not diverge on what
|
|
19
|
+
* the design agent may write — an `apply_patch` `.tsx`/`.astro`/etc. add or
|
|
20
|
+
* update is just as much a framework-file write as the same content via
|
|
21
|
+
* `Write`/`Edit`, and letting it through only on this path was an unintended
|
|
22
|
+
* apply_patch-shaped bypass, not a deliberate relaxation. `uiDesignSkillGate`
|
|
23
|
+
* and the Gemini create_frontend precondition NO LONGER bypass apply_patch
|
|
24
|
+
* (fixed): the skill gate is wired in `design.ts` on `event.files` (same
|
|
25
|
+
* any-agent scope as its Write/Edit call site, before the agentId
|
|
26
|
+
* early-return); the Gemini precondition is ported into this function,
|
|
27
|
+
* right below `htmlCssOnlyGate`, gated the same way (opt-in,
|
|
28
|
+
* FUSE_DESIGN_GEMINI, state.geminiCalls === 0). REMAINING KNOWN GAPS
|
|
29
|
+
* (documented, still out of scope):
|
|
30
|
+
* `Design-System.md` (case) escapes every endsWith check, here and on
|
|
31
|
+
* Write — a real bypass on case-insensitive macOS, not widened now; and
|
|
32
|
+
* `*** Move to:` (apply-patch.ts:76-79) unconditionally OVERWRITES `cur.path`
|
|
33
|
+
* on ANY preceding op (`Add File`/`Update File`), so a `*** Update File: a.css`
|
|
34
|
+
* + `Move to: a.tsx` sequence surfaces here as `filePath` ending in `.tsx`
|
|
35
|
+
* (correctly blocked — NOT a bypass, verified against the parser). The
|
|
36
|
+
* un-verified direction is the reverse: `Add File: a.tsx` + `Move to: a.css`
|
|
37
|
+
* would surface only the PERMITTED destination extension while `f.content`
|
|
38
|
+
* is real framework code — the source path is never preserved anywhere, and
|
|
39
|
+
* this gate (like Write's htmlCssOnlyGate) checks only the extension, never
|
|
40
|
+
* the content's actual syntax. Whether Codex's real grammar allows Move-to
|
|
41
|
+
* after an Add (vs. Update-only) is unconfirmed; flagged, not fixed here.
|
|
42
|
+
* Also: the POST side still does NOT run `runDesignChecks` (tsx/jsx/css
|
|
43
|
+
* warnings) on an `apply_patch` file — `handle-post.ts` keeps `designGate` on
|
|
44
|
+
* the RAW, un-fanned envelope there (recordPost's apply_patch branch is
|
|
45
|
+
* promote-only and resolves `design-system.md` via `join(cwd, …)`; routing a
|
|
46
|
+
* fanned Write/Edit-shaped event through it instead would read `filePath`
|
|
47
|
+
* UNRESOLVED and risk degrading the state — a real regression, not a
|
|
48
|
+
* hypothetical one). Restoring the tsx/jsx/css warning would require
|
|
49
|
+
* resolving relative apply_patch paths AND preserving the promote-only
|
|
50
|
+
* doctrine inside `recordPost` at the same time — out of scope here, owner
|
|
51
|
+
* decision.
|
|
52
|
+
* @packageDocumentation
|
|
53
|
+
*/
|
|
54
|
+
import type { Prompt } from "../prompt/types";
|
|
55
|
+
import type { NormalizedFile } from "./normalize";
|
|
56
|
+
import type { DesignState } from "../policy/design/state";
|
|
57
|
+
import { pluginsWriteGuard } from "../policy/design/corpus";
|
|
58
|
+
import { htmlCssOnlyGate, stateFileGate, geminiEnabled } from "../policy/design/gates";
|
|
59
|
+
import { designSystemWriteGate, htmlCssPipelineGate } from "../policy/design/gates-pipeline";
|
|
60
|
+
import { designSystemContentGate } from "./design-content-gate";
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Literal substitution, first occurrence or all — NEVER String.replace's
|
|
64
|
+
* replacement-string semantics: an agent-controlled new_string of `$&`, `` $` ``,
|
|
65
|
+
* `$'` or `$$` must be written (and validated) literally, not interpreted as
|
|
66
|
+
* the matched text (that made the gate validate a document the tool would
|
|
67
|
+
* never write — the `$&` bypass). Used on both sides: PRE reconstruction and
|
|
68
|
+
* POST reverse-reconstruction.
|
|
69
|
+
*/
|
|
70
|
+
export function substituteLiteral(s: string, from: string, to: string, all: boolean): string {
|
|
71
|
+
return all ? s.split(from).join(to) : s.replace(from, () => to);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Gate every file of a multi-file write primitive; the first violation blocks
|
|
76
|
+
* the envelope. `designSystemFileExists` is caller-computed (`findDesignSystem`
|
|
77
|
+
* lives in `design-helpers.ts`, which itself imports `substituteLiteral` from
|
|
78
|
+
* THIS module — importing it back here would cycle) so both write paths
|
|
79
|
+
* (Write/Edit in `design.ts`, apply_patch here) consume the SAME disk read.
|
|
80
|
+
*/
|
|
81
|
+
export function designFilesGate(
|
|
82
|
+
files: readonly NormalizedFile[],
|
|
83
|
+
state: DesignState,
|
|
84
|
+
pluginsRoot: string,
|
|
85
|
+
corpusRoot: string,
|
|
86
|
+
corpusRequired: boolean,
|
|
87
|
+
cwd: string,
|
|
88
|
+
designSystemFileExists: boolean,
|
|
89
|
+
): Prompt | null {
|
|
90
|
+
for (const f of files) {
|
|
91
|
+
const hit = pluginsWriteGuard(f.filePath, pluginsRoot, cwd) ?? stateFileGate(f.filePath);
|
|
92
|
+
if (hit) return hit;
|
|
93
|
+
if (f.op === "delete") continue;
|
|
94
|
+
const htmlCssHit = htmlCssOnlyGate(f.filePath);
|
|
95
|
+
if (htmlCssHit) return htmlCssHit;
|
|
96
|
+
// Parity with design.ts's htmlCssPipelineGate call: SAME verdict for the
|
|
97
|
+
// SAME file+state on both write paths (Write/Edit vs apply_patch).
|
|
98
|
+
const pipelineHit = htmlCssPipelineGate(f.filePath, state, designSystemFileExists);
|
|
99
|
+
if (pipelineHit) return pipelineHit;
|
|
100
|
+
// apply_patch parity for the Gemini precondition (design.ts:72-74, D2 gap):
|
|
101
|
+
// same condition, same Prompt shape as the Write/Edit branch — opt-in via
|
|
102
|
+
// FUSE_DESIGN_GEMINI (OFF by default), so a no-op when Gemini is disabled.
|
|
103
|
+
if (geminiEnabled() && state.geminiCalls === 0 && /\.(html|css)$/.test(f.filePath)) {
|
|
104
|
+
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"] };
|
|
105
|
+
}
|
|
106
|
+
if (!f.filePath.endsWith("design-system.md")) continue;
|
|
107
|
+
const gate = designSystemWriteGate(f.filePath, state, corpusRequired)
|
|
108
|
+
?? (f.op === "add"
|
|
109
|
+
? designSystemContentGate({ filePath: f.filePath, tool: "Write", content: f.content, oldString: undefined, replaceAll: false, state, corpusRoot, corpusRequired })
|
|
110
|
+
: null);
|
|
111
|
+
if (gate) return gate;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module design-helpers
|
|
3
|
+
* Effectful helpers for the design pipeline gate: locate design-system.md and
|
|
4
|
+
* apply PostToolUse fuse-browser transitions to the design state. Split out of
|
|
5
|
+
* `design.ts` to keep that file within the SOLID size budget (SRP).
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
10
|
+
import type { NormalizedEvent } from "./normalize";
|
|
11
|
+
import { type DesignState, saveDesignState } from "../policy/design/state";
|
|
12
|
+
import { recordNavigate, recordScroll, recordScreenshot, recordValidDesignSystem } from "../policy/design/transitions";
|
|
13
|
+
import { SHOT_TOOLS } from "../policy/design/screenshot-tools";
|
|
14
|
+
import { designSystemProblems } from "./design-content-gate";
|
|
15
|
+
import { substituteLiteral } from "./design-files-gate";
|
|
16
|
+
import { creditRead, creditShellReads } from "./design-read-credit";
|
|
17
|
+
|
|
18
|
+
export { designSystemContentGate } from "./design-content-gate";
|
|
19
|
+
|
|
20
|
+
const NAV = "mcp__fuse-browser__browser_navigate";
|
|
21
|
+
const SCROLL = "mcp__fuse-browser__browser_scroll";
|
|
22
|
+
const GEMINI = "mcp__gemini-design__create_frontend";
|
|
23
|
+
|
|
24
|
+
/** Read design-system.md walking up to 6 parents from `cwd` ("" if absent/unreadable). */
|
|
25
|
+
export function findDesignSystem(cwd: string): string {
|
|
26
|
+
let dir = cwd;
|
|
27
|
+
for (let i = 0; i < 6; i++) {
|
|
28
|
+
const p = join(dir, "design-system.md");
|
|
29
|
+
if (existsSync(p)) {
|
|
30
|
+
try {
|
|
31
|
+
return readFileSync(p, "utf8");
|
|
32
|
+
} catch {
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const parent = dirname(dir);
|
|
37
|
+
if (parent === dir) break;
|
|
38
|
+
dir = parent;
|
|
39
|
+
}
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Apply a PostToolUse fuse-browser transition to the design state. */
|
|
44
|
+
export function recordPost(event: NormalizedEvent, cacheDir: string, state: DesignState, corpusRoot = "", corpusRequired = false, cwd = ""): void {
|
|
45
|
+
if (SHOT_TOOLS.has(event.tool)) saveDesignState(cacheDir, recordScreenshot(state, corpusRequired));
|
|
46
|
+
else if (event.tool === NAV) saveDesignState(cacheDir, recordNavigate(state));
|
|
47
|
+
else if (event.tool === SCROLL) saveDesignState(cacheDir, recordScroll(state));
|
|
48
|
+
else if (event.tool === GEMINI) saveDesignState(cacheDir, { ...state, geminiCalls: state.geminiCalls + 1 });
|
|
49
|
+
else if (event.tool === "Read") creditRead(cacheDir, state, corpusRoot, corpusRequired, event.filePath ?? "");
|
|
50
|
+
// Codex Code Mode exec (`exec_command`, canonicalized to "Bash" by codex-shell-tool.ts).
|
|
51
|
+
else if (event.tool === "Bash" && event.command) creditShellReads(cacheDir, state, corpusRoot, corpusRequired, event.command);
|
|
52
|
+
else if ((event.tool === "Write" || event.tool === "Edit") && (event.filePath ?? "").endsWith("design-system.md")) {
|
|
53
|
+
// Write access ≠ validity, same rule both sides: POST validates only a
|
|
54
|
+
// zero-problem content and DEGRADES only what PRE would have blocked.
|
|
55
|
+
const fp = event.filePath ?? "";
|
|
56
|
+
let content: string;
|
|
57
|
+
try {
|
|
58
|
+
content = readFileSync(fp, "utf8");
|
|
59
|
+
} catch {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const problems = designSystemProblems(content, state.corpusReads, corpusRequired);
|
|
63
|
+
if (!problems.length) {
|
|
64
|
+
saveDesignState(cacheDir, recordValidDesignSystem(state));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// Edit: degrade only on problems the edit INTRODUCED (original reverse-rebuilt
|
|
68
|
+
// with LITERAL substitution; a deletion edit is not reconstructable — D5).
|
|
69
|
+
if (event.tool === "Edit" && event.oldString && event.content && content.includes(event.content)) {
|
|
70
|
+
const original = substituteLiteral(content, event.content, event.oldString, event.input.replace_all === true);
|
|
71
|
+
const introduced = problems.filter((p) => !designSystemProblems(original, state.corpusReads, corpusRequired).includes(p));
|
|
72
|
+
if (introduced.length) saveDesignState(cacheDir, { ...state, designSystemValid: false });
|
|
73
|
+
}
|
|
74
|
+
// Write / unreconstructable Edit: the PRE gate enforced not-worsening — never degrade here.
|
|
75
|
+
} else if (event.files) {
|
|
76
|
+
// Codex apply_patch: PROMOTE-ONLY, never blocking nor degrading — the POST
|
|
77
|
+
// re-reads the REAL file (the hunk is never the document): clean → phase 3,
|
|
78
|
+
// dirty → nothing. designSystemValid is only consumed under FUSE_DESIGN_GEMINI.
|
|
79
|
+
const ds = event.files.find((f) => f.op !== "delete" && f.filePath.endsWith("design-system.md"));
|
|
80
|
+
if (ds) {
|
|
81
|
+
const abs = isAbsolute(ds.filePath) ? ds.filePath : join(cwd, ds.filePath);
|
|
82
|
+
let content: string;
|
|
83
|
+
try {
|
|
84
|
+
content = readFileSync(abs, "utf8");
|
|
85
|
+
} catch {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (!designSystemProblems(content, state.corpusReads, corpusRequired).length) {
|
|
89
|
+
saveDesignState(cacheDir, recordValidDesignSystem(state));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { detectMode } from "../policy/design/transitions";
|
|
4
|
+
import { initDesignState, saveDesignState, cleanupDesignStates } from "../policy/design/state";
|
|
5
|
+
import { setActiveDesignAgent, clearActiveDesignAgent, activeDesignAgent } from "../policy/design/flag";
|
|
6
|
+
import { promptText } from "./prompt-text";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Handle the design-agent SubagentStart/Stop lifecycle: init the pipeline state +
|
|
10
|
+
* raise the active flag on start, archive/cleanup + clear the flag on stop.
|
|
11
|
+
* Returns true when it handled the event (caller should respond and stop).
|
|
12
|
+
*
|
|
13
|
+
* `SubagentStop` clears the flag whenever `agent_id` matches the currently
|
|
14
|
+
* active one, INDEPENDENTLY of `agent_type` on the stop payload — a stop event
|
|
15
|
+
* is not guaranteed to echo the same `agent_type` string the start event used,
|
|
16
|
+
* and gating the clear on it left the flag stuck (blocking every subsequent
|
|
17
|
+
* top-level Write/Edit in the session — see MEMORY/LESSON.md).
|
|
18
|
+
*/
|
|
19
|
+
export function designLifecycle(payload: Record<string, unknown>, cacheDir: string, cwd: string, stamp: string, now: number): boolean {
|
|
20
|
+
const event = typeof payload.hook_event_name === "string" ? payload.hook_event_name : "";
|
|
21
|
+
const agentId = typeof payload.agent_id === "string" ? payload.agent_id : "";
|
|
22
|
+
if (event === "SubagentStop") {
|
|
23
|
+
if (!agentId || agentId !== activeDesignAgent(cacheDir)) return false;
|
|
24
|
+
cleanupDesignStates(cacheDir, agentId, stamp, now);
|
|
25
|
+
clearActiveDesignAgent(cacheDir);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
const agentType = typeof payload.agent_type === "string" ? payload.agent_type : "";
|
|
29
|
+
if (!agentType.includes("design")) return false;
|
|
30
|
+
if (event === "SubagentStart") {
|
|
31
|
+
if (!agentId) return false;
|
|
32
|
+
const dsExists = existsSync(join(cwd, "design-system.md"));
|
|
33
|
+
const prompt = promptText(payload.prompt);
|
|
34
|
+
saveDesignState(cacheDir, initDesignState(agentId, detectMode(prompt, dsExists), dsExists));
|
|
35
|
+
setActiveDesignAgent(cacheDir, agentId);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module design-read-credit
|
|
3
|
+
* Shared credit path for a design-corpus/ref read, reused by both the native
|
|
4
|
+
* `Read` branch and the shell-read branch (Codex Code Mode `exec_command`,
|
|
5
|
+
* canonicalized to `"Bash"` by `codex-shell-tool.ts`) in
|
|
6
|
+
* `design-helpers.ts::recordPost`. Split out to keep that file within the
|
|
7
|
+
* SOLID size budget — zero new parser, zero new store: both callers hit the
|
|
8
|
+
* SAME `recordCorpusRead`/`recordRead` transitions.
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import type { DesignState } from "../policy/design/state";
|
|
13
|
+
import { saveDesignState } from "../policy/design/state";
|
|
14
|
+
import { recordCorpusRead, recordRead } from "../policy/design/transitions";
|
|
15
|
+
import { classifyCorpusRead } from "../policy/design/corpus";
|
|
16
|
+
import { shellReadRefPaths } from "../policy/shell-read-refs";
|
|
17
|
+
|
|
18
|
+
/** Credit one read path as a corpus read (anchored + on-disk) or a plain ref read. */
|
|
19
|
+
export function creditRead(cacheDir: string, state: DesignState, corpusRoot: string, corpusRequired: boolean, fp: string): void {
|
|
20
|
+
if (classifyCorpusRead(fp, corpusRoot) && existsSync(fp)) {
|
|
21
|
+
saveDesignState(cacheDir, recordCorpusRead(state, fp.slice(corpusRoot.length + 1), corpusRequired));
|
|
22
|
+
} else saveDesignState(cacheDir, recordRead(state, fp, corpusRequired));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Credit every `.md` path a read-only shell `command` targets, same path as {@link creditRead}. */
|
|
26
|
+
export function creditShellReads(cacheDir: string, state: DesignState, corpusRoot: string, corpusRequired: boolean, command: string): void {
|
|
27
|
+
for (const fp of shellReadRefPaths(command)) creditRead(cacheDir, state, corpusRoot, corpusRequired, fp);
|
|
28
|
+
}
|