@fusengine/harness 0.1.90 → 0.1.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude/index.d.mts +1 -1
- package/dist/adapters/claude/index.mjs +1 -1
- package/dist/adapters/cline/index.mjs +1 -1
- package/dist/adapters/codex/index.d.mts +2 -2
- package/dist/adapters/codex/index.mjs +3 -2
- package/dist/adapters/cursor/index.d.mts +15 -28
- package/dist/adapters/cursor/index.mjs +35 -46
- package/dist/adapters/gemini/index.mjs +1 -1
- package/dist/adapters/hermes/index.d.mts +1 -1
- package/dist/adapters/hermes/index.mjs +1 -1
- package/dist/adapters/kimi/index.d.mts +1 -1
- package/dist/adapters/kimi/index.mjs +1 -1
- package/dist/{claude-5bC8TDAz.mjs → claude-Ckv2_TgP.mjs} +3 -43
- package/dist/cli/bin.mjs +429 -16
- package/dist/cli/index.mjs +1 -1
- package/dist/command-string-CALMTnwN.mjs +42 -0
- package/dist/{evaluate-C1VSI5pb.mjs → evaluate-CMiIqHeH.mjs} +261 -13
- package/dist/{handle-CBaOcFZq.mjs → handle-C43gA-Pr.mjs} +701 -134
- package/dist/{hermes-kP6NUQlG.mjs → hermes-B9-p_3IF.mjs} +3 -2
- package/dist/{index-DBgnIZn9.d.mts → index-B8PG82su.d.mts} +1 -1
- package/dist/{index-D4C2-IIc.d.mts → index-SmKYkk2N.d.mts} +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/init/index.d.mts +1 -1
- package/dist/init/index.mjs +1 -1
- package/dist/{kimi-D6X7b8wF.mjs → kimi-G2wcSh5-.mjs} +3 -2
- package/dist/normalize-BjG6unTj.mjs +317 -0
- package/dist/policy/index.d.mts +1 -1
- package/dist/policy/index.mjs +2 -2
- package/dist/prompt/index.d.mts +1 -1
- package/dist/{run-CdIlUtjv.mjs → run-DkrzC0gb.mjs} +1 -1
- package/dist/{run-TAXNRSpD.mjs → run-GHAMqcOn.mjs} +26 -3
- package/dist/runtime/index.d.mts +27 -16
- package/dist/runtime/index.mjs +2 -2
- package/dist/{types-C0-igRKr.d.mts → types-Bh0jEF3_.d.mts} +2 -0
- package/dist/{validate-DZFtAbLP.mjs → validate-KjZ1X9tH.mjs} +1 -1
- package/package.json +3 -2
- package/src/adapters/claude/index.ts +92 -0
- package/src/adapters/cline/index.ts +36 -0
- package/src/adapters/codex/apply-patch.ts +94 -0
- package/src/adapters/codex/index.ts +84 -0
- package/src/adapters/codex/permission-mode.ts +16 -0
- package/src/adapters/cursor/context.ts +46 -0
- package/src/adapters/cursor/events.ts +38 -0
- package/src/adapters/cursor/index.ts +43 -0
- package/src/adapters/cursor/interfaces/types.ts +88 -0
- package/src/adapters/cursor/native-response.ts +158 -0
- package/src/adapters/cursor/normalize.ts +123 -0
- package/src/adapters/cursor/respond.ts +123 -0
- package/src/adapters/gemini/index.ts +36 -0
- package/src/adapters/hermes/index.ts +55 -0
- package/src/adapters/hermes/interfaces/types.ts +26 -0
- package/src/adapters/kimi/index.ts +96 -0
- package/src/adapters/kimi/interfaces/types.ts +55 -0
- package/src/cache/compact.ts +55 -0
- package/src/cache/index.ts +5 -0
- package/src/cache/io.ts +38 -0
- package/src/cache/mcp-response.ts +39 -0
- package/src/cache/mcp-store.ts +70 -0
- package/src/cache/store.ts +93 -0
- package/src/changelog/fetch.ts +79 -0
- package/src/cli/bin.ts +100 -0
- package/src/cli/cursor-event-scanner.ts +175 -0
- package/src/cli/cursor-stdin-reader.ts +65 -0
- package/src/cli/doctor.ts +96 -0
- package/src/cli/hook-io.ts +130 -0
- package/src/cli/hook-sound.ts +49 -0
- package/src/cli/index.ts +1 -0
- package/src/cli/json-primitive-scanner.ts +83 -0
- package/src/cli/run.ts +36 -0
- package/src/cli/scope.ts +31 -0
- package/src/codex-rules/build.ts +63 -0
- package/src/codex-rules/index.ts +8 -0
- package/src/codex-rules/mapping.ts +42 -0
- package/src/codex-rules/rules/absolute-paths.ts +52 -0
- package/src/codex-rules/rules/ask.ts +44 -0
- package/src/codex-rules/rules/chmod-ask.ts +57 -0
- package/src/codex-rules/rules/dd.ts +23 -0
- package/src/codex-rules/rules/diskutil.ts +33 -0
- package/src/codex-rules/rules/mkfs.ts +42 -0
- package/src/codex-rules/rules/privilege.ts +49 -0
- package/src/codex-rules/rules/remote-fetch.ts +31 -0
- package/src/codex-rules/rules/rm-variants.ts +58 -0
- package/src/codex-rules/rules/single-commands.ts +57 -0
- package/src/codex-rules/skip-list.ts +25 -0
- package/src/codex-rules/starlark.ts +46 -0
- package/src/codex-rules/types.ts +36 -0
- package/src/config/dotenv.ts +55 -0
- package/src/config/env.ts +10 -0
- package/src/config/home-dir.ts +36 -0
- package/src/config/index.ts +5 -0
- package/src/config/layout.ts +52 -0
- package/src/config/limits.ts +56 -0
- package/src/config/ttl.ts +30 -0
- package/src/detect/harness.ts +73 -0
- package/src/detect/index.ts +1 -0
- package/src/detect/interfaces/types.ts +18 -0
- package/src/freshness/agent-evidence-record.ts +95 -0
- package/src/freshness/agent-evidence.ts +95 -0
- package/src/freshness/codex-spawn-evidence.ts +91 -0
- package/src/freshness/doc-helpers.ts +90 -0
- package/src/freshness/evidence-harvest-io.ts +72 -0
- package/src/freshness/evidence-harvest.ts +61 -0
- package/src/freshness/explore-tools.ts +95 -0
- package/src/freshness/index.ts +2 -0
- package/src/freshness/query-framework.ts +73 -0
- package/src/freshness/ref-evidence.ts +49 -0
- package/src/freshness/ref-journal.ts +98 -0
- package/src/freshness/trivial-edit-counter.ts +30 -0
- package/src/index.ts +11 -0
- package/src/init/index.ts +2 -0
- package/src/init/run.ts +32 -0
- package/src/init/templates.ts +75 -0
- package/src/memory/gitignore.ts +14 -0
- package/src/memory/index.ts +3 -0
- package/src/memory/registry.ts +39 -0
- package/src/memory/session-roots.ts +99 -0
- package/src/memory/state.ts +58 -0
- package/src/policy/apex-authorization.ts +100 -0
- package/src/policy/apex-gates.ts +91 -0
- package/src/policy/apex-target-steps.ts +73 -0
- package/src/policy/apex-target.ts +49 -0
- package/src/policy/apex-task-context.ts +81 -0
- package/src/policy/apex.ts +94 -0
- package/src/policy/cartographer/build-tree.ts +69 -0
- package/src/policy/cartographer/describe.ts +53 -0
- package/src/policy/cartographer/entry.ts +38 -0
- package/src/policy/cartographer/frontmatter.ts +48 -0
- package/src/policy/cartographer/indicators.ts +34 -0
- package/src/policy/claude-md-context.ts +66 -0
- package/src/policy/conventions/index.ts +11 -0
- package/src/policy/conventions/interfaces.ts +71 -0
- package/src/policy/conventions/langs.ts +53 -0
- package/src/policy/conventions/query.ts +49 -0
- package/src/policy/conventions/react-hooks.ts +18 -0
- package/src/policy/conventions/store-or-query.ts +21 -0
- package/src/policy/conventions/stores.ts +56 -0
- package/src/policy/conventions/strip.ts +87 -0
- package/src/policy/conventions/verdict.ts +38 -0
- package/src/policy/creation-intent.ts +11 -0
- package/src/policy/deny-loop.ts +88 -0
- package/src/policy/design/allowed-write.ts +28 -0
- package/src/policy/design/content-checks.ts +45 -0
- package/src/policy/design/corpus-fs.ts +21 -0
- package/src/policy/design/corpus-resolve-cache.ts +49 -0
- package/src/policy/design/corpus-resolve.ts +99 -0
- package/src/policy/design/corpus.ts +92 -0
- package/src/policy/design/design-system-rules.ts +45 -0
- package/src/policy/design/flag.ts +30 -0
- package/src/policy/design/gates-pipeline.ts +105 -0
- package/src/policy/design/gates.ts +87 -0
- package/src/policy/design/screenshot-tools.ts +24 -0
- package/src/policy/design/skill-evidence.ts +34 -0
- package/src/policy/design/skill-gate.ts +81 -0
- package/src/policy/design/skill-triggers.ts +51 -0
- package/src/policy/design/state.ts +92 -0
- package/src/policy/design/template-urls.ts +54 -0
- package/src/policy/design/transitions.ts +68 -0
- package/src/policy/detect-claude-md-project-type.ts +32 -0
- package/src/policy/detect-framework.ts +99 -0
- package/src/policy/detect-primitive-checks.ts +100 -0
- package/src/policy/detect-primitive-lib.ts +55 -0
- package/src/policy/detect-project.ts +99 -0
- package/src/policy/edit-outcome.ts +53 -0
- package/src/policy/evaluate.ts +81 -0
- package/src/policy/expert-agents.ts +60 -0
- package/src/policy/file-size-scope.ts +32 -0
- package/src/policy/file-size.ts +99 -0
- package/src/policy/framework-solid-exclude.ts +52 -0
- package/src/policy/framework-solid-extended.ts +93 -0
- package/src/policy/framework-solid-gates-systems.ts +99 -0
- package/src/policy/framework-solid-gates.ts +98 -0
- package/src/policy/framework-solid.ts +88 -0
- package/src/policy/gemini-mcp-gate.ts +83 -0
- package/src/policy/git-gates.ts +40 -0
- package/src/policy/guards/bash-command-anchor.ts +40 -0
- package/src/policy/guards/bash-write-patterns.ts +140 -0
- package/src/policy/guards/bash-write-redirects.ts +201 -0
- package/src/policy/guards/bash-write-safe-paths.ts +62 -0
- package/src/policy/guards/bash-write.ts +76 -0
- package/src/policy/guards/context.ts +12 -0
- package/src/policy/guards/index.ts +61 -0
- package/src/policy/guards/install.ts +22 -0
- package/src/policy/guards/interface-separation-ext.ts +41 -0
- package/src/policy/guards/interface-separation.ts +100 -0
- package/src/policy/guards/protected-path.ts +100 -0
- package/src/policy/guards/security.ts +97 -0
- package/src/policy/index.ts +19 -0
- package/src/policy/interfaces/types.ts +46 -0
- package/src/policy/js-gate-route.ts +51 -0
- package/src/policy/lessons/lesson-gate.ts +0 -0
- package/src/policy/lessons/trigger-index.ts +84 -0
- package/src/policy/lessons/types.ts +31 -0
- package/src/policy/module-layout.ts +31 -0
- package/src/policy/nearest-manifest.ts +75 -0
- package/src/policy/never-approval.ts +95 -0
- package/src/policy/patterns.ts +80 -0
- package/src/policy/seo/validate.ts +51 -0
- package/src/policy/shadcn-project.ts +22 -0
- package/src/policy/shadcn-skill-gate.ts +97 -0
- package/src/policy/shell-read-refs.ts +96 -0
- package/src/policy/skill-path.ts +46 -0
- package/src/policy/skill-patterns/laravel.ts +40 -0
- package/src/policy/skill-patterns/nextjs.ts +29 -0
- package/src/policy/skill-patterns/react.ts +27 -0
- package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
- package/src/policy/skill-patterns/shadcn.ts +66 -0
- package/src/policy/skill-patterns/swift.ts +30 -0
- package/src/policy/skill-patterns/tailwind.ts +40 -0
- package/src/policy/skill-source.ts +52 -0
- package/src/policy/skill-trigger-patterns.ts +33 -0
- package/src/policy/skill-triggers.ts +108 -0
- package/src/policy/tailwind-skill-gate.ts +63 -0
- package/src/policy/trivial-edits.ts +52 -0
- package/src/policy/verbosity.ts +39 -0
- package/src/prompt/index.ts +1 -0
- package/src/prompt/types.ts +36 -0
- package/src/refs/discover.ts +69 -0
- package/src/refs/frontmatter.ts +24 -0
- package/src/refs/index.ts +4 -0
- package/src/refs/loader.ts +59 -0
- package/src/refs/ref-key.ts +41 -0
- package/src/refs/router.ts +63 -0
- package/src/refs/types.ts +25 -0
- package/src/runtime/activity.ts +99 -0
- package/src/runtime/apply-patch-gate.ts +41 -0
- package/src/runtime/burst-window.ts +21 -0
- package/src/runtime/codex-shell-tool.ts +37 -0
- package/src/runtime/command-string.ts +37 -0
- package/src/runtime/confirm/codex-confirm.ts +126 -0
- package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
- package/src/runtime/confirm/confirm-code.ts +33 -0
- package/src/runtime/confirm/confirm-gate.ts +65 -0
- package/src/runtime/confirm/confirm-irreversible.ts +32 -0
- package/src/runtime/confirm/confirm-pending.ts +34 -0
- package/src/runtime/confirm/confirm-state.ts +56 -0
- package/src/runtime/confirm/confirm-subagent.ts +112 -0
- package/src/runtime/confirm/confirm-submit.ts +38 -0
- package/src/runtime/core-guards-active.ts +84 -0
- package/src/runtime/core-guards-wired.ts +47 -0
- package/src/runtime/deny-loop-store.ts +94 -0
- package/src/runtime/deny-notice.ts +52 -0
- package/src/runtime/design-cache-resolve.ts +51 -0
- package/src/runtime/design-content-gate.ts +95 -0
- package/src/runtime/design-files-gate.ts +114 -0
- package/src/runtime/design-helpers.ts +93 -0
- package/src/runtime/design-lifecycle.ts +39 -0
- package/src/runtime/design-read-credit.ts +28 -0
- package/src/runtime/design.ts +116 -0
- package/src/runtime/dev-context.ts +43 -0
- package/src/runtime/dry-patterns.ts +32 -0
- package/src/runtime/dry.ts +96 -0
- package/src/runtime/fragment-registry.ts +33 -0
- package/src/runtime/framework-skill-gate.ts +79 -0
- package/src/runtime/fs-cleanup.ts +71 -0
- package/src/runtime/gate-apex.ts +81 -0
- package/src/runtime/gate-helpers.ts +52 -0
- package/src/runtime/gate-input.ts +26 -0
- package/src/runtime/gate.ts +128 -0
- package/src/runtime/handle-post.ts +104 -0
- package/src/runtime/handle-pre.ts +137 -0
- package/src/runtime/handle-scope-async.ts +29 -0
- package/src/runtime/handle-types.ts +27 -0
- package/src/runtime/handle.ts +116 -0
- package/src/runtime/home-state.ts +55 -0
- package/src/runtime/index.ts +18 -0
- package/src/runtime/inform.ts +28 -0
- package/src/runtime/inject-budget-recap.ts +47 -0
- package/src/runtime/inject-budget.ts +81 -0
- package/src/runtime/inject-context.ts +85 -0
- package/src/runtime/inject-dedup-exclusive.ts +98 -0
- package/src/runtime/inject-dedup.ts +89 -0
- package/src/runtime/is-agent-tool.ts +19 -0
- package/src/runtime/lifecycle/agent-files.ts +49 -0
- package/src/runtime/lifecycle/agent-memory.ts +82 -0
- package/src/runtime/lifecycle/agent-transcript.ts +80 -0
- package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
- package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
- package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
- package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
- package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
- package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
- package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
- package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +100 -0
- package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
- package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
- package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
- package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
- package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
- package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
- package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
- package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
- package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
- package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
- package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
- package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
- package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
- package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
- package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
- package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
- package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
- package/src/runtime/lifecycle/aipilot/types.ts +72 -0
- package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
- package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
- package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
- package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
- package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
- package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
- package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
- package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
- package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
- package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
- package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
- package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
- package/src/runtime/lifecycle/changelog-research.ts +43 -0
- package/src/runtime/lifecycle/check-file-size.ts +61 -0
- package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
- package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
- package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
- package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
- package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
- package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
- package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
- package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
- package/src/runtime/lifecycle/dispatch.ts +109 -0
- package/src/runtime/lifecycle/failure-lesson.ts +51 -0
- package/src/runtime/lifecycle/index.ts +34 -0
- package/src/runtime/lifecycle/inject-rules.ts +50 -0
- package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
- package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
- package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
- package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
- package/src/runtime/lifecycle/lessons/state.ts +23 -0
- package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
- package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
- package/src/runtime/lifecycle/memory/client.ts +65 -0
- package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
- package/src/runtime/lifecycle/memory/recall.ts +30 -0
- package/src/runtime/lifecycle/memory/salience.ts +31 -0
- package/src/runtime/lifecycle/memory/state.ts +49 -0
- package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
- package/src/runtime/lifecycle/post-compact.ts +38 -0
- package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
- package/src/runtime/lifecycle/post-tracking.ts +40 -0
- package/src/runtime/lifecycle/pre-compact.ts +38 -0
- package/src/runtime/lifecycle/rules-root.ts +83 -0
- package/src/runtime/lifecycle/security/check-skill.ts +63 -0
- package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
- package/src/runtime/lifecycle/security/scan.ts +76 -0
- package/src/runtime/lifecycle/security/skill-state.ts +47 -0
- package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
- package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
- package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
- package/src/runtime/lifecycle/session-end.ts +17 -0
- package/src/runtime/lifecycle/session-start.ts +57 -0
- package/src/runtime/lifecycle/snapshot/board.ts +27 -0
- package/src/runtime/lifecycle/snapshot/format.ts +60 -0
- package/src/runtime/lifecycle/snapshot/git.ts +71 -0
- package/src/runtime/lifecycle/snapshot/index.ts +52 -0
- package/src/runtime/lifecycle/snapshot/version.ts +34 -0
- package/src/runtime/lifecycle/solid-detect.ts +56 -0
- package/src/runtime/lifecycle/stop-core.ts +35 -0
- package/src/runtime/lifecycle/subagent-cache.ts +61 -0
- package/src/runtime/lifecycle/task-completed.ts +94 -0
- package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
- package/src/runtime/lifecycle/teammate-idle.ts +28 -0
- package/src/runtime/lifecycle/tool-failure.ts +24 -0
- package/src/runtime/lifecycle/track-changes.ts +61 -0
- package/src/runtime/lifecycle/validate-solid.ts +70 -0
- package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
- package/src/runtime/lifecycle-bridge.ts +74 -0
- package/src/runtime/mcp-key.ts +44 -0
- package/src/runtime/mcp-tool-name.ts +92 -0
- package/src/runtime/mcp.ts +91 -0
- package/src/runtime/modular.ts +77 -0
- package/src/runtime/normalize.ts +107 -0
- package/src/runtime/notices.ts +91 -0
- package/src/runtime/notification-sound.ts +49 -0
- package/src/runtime/notifications.ts +77 -0
- package/src/runtime/paths.ts +59 -0
- package/src/runtime/post-fanout.ts +41 -0
- package/src/runtime/post-outcome.ts +61 -0
- package/src/runtime/pre-allow.ts +97 -0
- package/src/runtime/precommit.ts +60 -0
- package/src/runtime/prompt-text.ts +38 -0
- package/src/runtime/receipt-capture.ts +34 -0
- package/src/runtime/record.ts +26 -0
- package/src/runtime/respond.ts +92 -0
- package/src/runtime/solid-file-size-gate.ts +58 -0
- package/src/runtime/solid-pre.ts +53 -0
- package/src/runtime/storage.ts +9 -0
- package/src/statusline/colors.ts +31 -0
- package/src/statusline/constants.ts +17 -0
- package/src/statusline/formatters.ts +54 -0
- package/src/statusline/index.ts +4 -0
- package/src/statusline/progress-bar.ts +46 -0
- package/src/tracking/codex-post-failure.ts +73 -0
- package/src/tracking/index.ts +2 -0
- package/src/tracking/integrity.ts +99 -0
- package/src/tracking/one-shot-dedup.ts +34 -0
- package/src/tracking/one-shot-failure.ts +42 -0
- package/src/tracking/one-shot-store.ts +95 -0
- package/src/tracking/one-shot.ts +93 -0
- package/src/tracking/receipts.ts +95 -0
- package/src/tracking/session-state.ts +94 -0
- package/src/tracking/store.ts +96 -0
- package/src/tracking/track-compact.ts +100 -0
- package/src/tracking/track-diff.ts +50 -0
- package/src/tracking/track-journal.ts +97 -0
- package/src/tracking/track-lock-sync.ts +83 -0
- package/src/tracking/track-lock.ts +67 -0
- package/src/util/compact-json.ts +23 -0
- package/src/util/index.ts +2 -0
- package/src/util/json-io.ts +47 -0
- package/src/util/project-root.ts +41 -0
- package/src/util/runtime-io.ts +85 -0
- package/src/util/semver.ts +30 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostToolUse (matcher "Write|Edit") for the tailwindcss scope: warn
|
|
3
|
+
* (non-blocking) on a deprecated `@tailwind` directive, excessive `@apply`
|
|
4
|
+
* usage, and an overlong `className` string on a just-written CSS/TSX/JSX
|
|
5
|
+
* file inside a detected Tailwind project. Ports `validate-tailwind.py`.
|
|
6
|
+
*/
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { readText, pathExists } from "../../util/runtime-io";
|
|
9
|
+
import { contextResponse } from "../../adapters/claude";
|
|
10
|
+
|
|
11
|
+
/** Tailwind config file names checked at each ancestor directory. */
|
|
12
|
+
const TAILWIND_CONFIGS = ["tailwind.config.js", "tailwind.config.ts", "tailwind.config.mjs", "tailwind.config.cjs"];
|
|
13
|
+
|
|
14
|
+
/** Walk up to 20 levels from `filePath`'s directory to the nearest ancestor holding `package.json` or a Tailwind config (parity `find_project_root`). */
|
|
15
|
+
function findProjectRoot(filePath: string): string | null {
|
|
16
|
+
let dir = dirname(filePath);
|
|
17
|
+
for (let i = 0; i < 20; i++) {
|
|
18
|
+
if (pathExists(join(dir, "package.json")) || TAILWIND_CONFIGS.some((cfg) => pathExists(join(dir, cfg)))) return dir;
|
|
19
|
+
const parent = dirname(dir);
|
|
20
|
+
if (parent === dir) break;
|
|
21
|
+
dir = parent;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** True when `filePath` belongs to a Tailwind project (v3 config, or `tailwindcss` in package.json deps). Parity `is_tailwind_project`. */
|
|
27
|
+
function isTailwindProject(filePath: string): boolean {
|
|
28
|
+
const root = findProjectRoot(filePath);
|
|
29
|
+
if (!root) return false;
|
|
30
|
+
if (TAILWIND_CONFIGS.some((cfg) => pathExists(join(root, cfg)))) return true;
|
|
31
|
+
const pkgPath = join(root, "package.json");
|
|
32
|
+
if (!pathExists(pkgPath)) return false;
|
|
33
|
+
try {
|
|
34
|
+
const pkg = JSON.parse(readText(pkgPath)) as { dependencies?: Record<string, string>; devDependencies?: Record<string, string> };
|
|
35
|
+
return Boolean(pkg.dependencies?.tailwindcss) || Boolean(pkg.devDependencies?.tailwindcss);
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validate Tailwind best practices on a just-written file.
|
|
43
|
+
* @param tool - The tool name (only "Write"/"Edit" are checked).
|
|
44
|
+
* @param filePath - The written file's absolute path.
|
|
45
|
+
* @returns The PostToolUse `additionalContext` response, or `""` when clean/inert.
|
|
46
|
+
*/
|
|
47
|
+
export function validateTailwind(tool: string, filePath: string): string {
|
|
48
|
+
if (tool !== "Write" && tool !== "Edit") return "";
|
|
49
|
+
if (!/\.(css|tsx|jsx)$/.test(filePath)) return "";
|
|
50
|
+
if (!pathExists(filePath) || !isTailwindProject(filePath)) return "";
|
|
51
|
+
|
|
52
|
+
let content: string;
|
|
53
|
+
try {
|
|
54
|
+
content = readText(filePath);
|
|
55
|
+
} catch {
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const warnings: string[] = [];
|
|
60
|
+
|
|
61
|
+
if (filePath.endsWith(".css")) {
|
|
62
|
+
if (/@tailwind (base|components|utilities)/.test(content)) {
|
|
63
|
+
warnings.push("Tailwind v4: @tailwind directives are deprecated - use @import 'tailwindcss'.");
|
|
64
|
+
}
|
|
65
|
+
const applyCount = (content.match(/@apply/g) ?? []).length;
|
|
66
|
+
if (applyCount > 10) {
|
|
67
|
+
warnings.push(`Excessive @apply usage (${applyCount}) - prefer utility classes directly.`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (/\.(tsx|jsx)$/.test(filePath)) {
|
|
72
|
+
const longClasses = (content.match(/className="[^"]{150,}"/g) ?? []).length;
|
|
73
|
+
if (longClasses > 0) {
|
|
74
|
+
warnings.push(`Very long className (${longClasses} lines) - extract to @utility or use cn().`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (warnings.length === 0) return "";
|
|
79
|
+
return contextResponse("PostToolUse", warnings.join(" "));
|
|
80
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { dispatchLifecycle, postEditTypescript, trackSessionChanges, type PluginScope } from "./lifecycle";
|
|
2
|
+
import { autoDocumentRead } from "./lifecycle/auto-document-reads";
|
|
3
|
+
import { contextResponse } from "../adapters/claude";
|
|
4
|
+
import { cursorEventContract } from "../adapters/cursor/events";
|
|
5
|
+
import { toCursorLifecycleResponse } from "../adapters/cursor/respond";
|
|
6
|
+
import type { NormalizedEvent } from "./normalize";
|
|
7
|
+
|
|
8
|
+
/** Raw event name from a payload (Cline lacks one; lifecycle is Claude-only). */
|
|
9
|
+
function rawEvent(payload: Record<string, unknown>): string {
|
|
10
|
+
return typeof payload.hook_event_name === "string" ? payload.hook_event_name : "";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Extract the `additionalContext` body from a `contextResponse(...)` stdout string ("" when empty/unparseable). */
|
|
14
|
+
function additionalContextOf(stdout: string): string {
|
|
15
|
+
if (!stdout) return "";
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(stdout) as { hookSpecificOutput?: { additionalContext?: string } };
|
|
18
|
+
return parsed.hookSpecificOutput?.additionalContext ?? "";
|
|
19
|
+
} catch {
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Run the ported lifecycle/session/context hooks (SessionStart, SubagentStart/
|
|
26
|
+
* Stop, TeammateIdle, PostToolUseFailure, PreCompact, SessionEnd,
|
|
27
|
+
* InstructionsLoaded, rules-scope UserPromptSubmit). Returns the native stdout
|
|
28
|
+
* when handled, or `null` to fall through to the tool-use pipeline.
|
|
29
|
+
* @param payload - The raw hook payload.
|
|
30
|
+
* @param cwd - Project root.
|
|
31
|
+
* @param scope - The invoking plugin scope (defaults to `core`).
|
|
32
|
+
* @param now - Clock.
|
|
33
|
+
* @param id - Harness target id (defaults to "claude-code" — zero-regression default).
|
|
34
|
+
* @returns The native stdout, or `null` when unhandled.
|
|
35
|
+
*/
|
|
36
|
+
export function lifecycleStdout(payload: Record<string, unknown>, cwd: string, scope: PluginScope, now: number, id: string = "claude-code"): string | null {
|
|
37
|
+
const wireEvent = rawEvent(payload);
|
|
38
|
+
if (id !== "cursor") return dispatchLifecycle({ event: wireEvent, payload, cwd, scope, now, id });
|
|
39
|
+
const lifecycleEvent = cursorEventContract(wireEvent).lifecycle;
|
|
40
|
+
if (lifecycleEvent === null) return null;
|
|
41
|
+
const sessionId = typeof payload.session_id === "string" ? payload.session_id : payload.conversation_id;
|
|
42
|
+
const stdout = dispatchLifecycle({
|
|
43
|
+
event: lifecycleEvent,
|
|
44
|
+
payload: { ...payload, session_id: sessionId },
|
|
45
|
+
cwd,
|
|
46
|
+
scope,
|
|
47
|
+
now,
|
|
48
|
+
id,
|
|
49
|
+
});
|
|
50
|
+
return stdout === null ? null : toCursorLifecycleResponse(stdout, wireEvent);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Post-edit additions for core-scope PostToolUse: auto-document a Read of a
|
|
55
|
+
* SKILL.md/README/docs file; else (Write/Edit) track cumulative session
|
|
56
|
+
* changes (sniper reminder) AND report eslint/prettier issues — parity
|
|
57
|
+
* core-guards hooks.json, which runs both as independent PostToolUse commands
|
|
58
|
+
* rather than short-circuiting one on the other. Returns the combined extra
|
|
59
|
+
* stdout, or "" when nothing to emit.
|
|
60
|
+
* @param scope - The invoking plugin scope.
|
|
61
|
+
* @param event - The normalized event.
|
|
62
|
+
* @param now - Clock.
|
|
63
|
+
* @param id - Harness target id (defaults to "claude-code" — zero-regression default).
|
|
64
|
+
* @returns The extra stdout (possibly empty).
|
|
65
|
+
*/
|
|
66
|
+
export async function postEditContext(scope: PluginScope, event: NormalizedEvent, now: number, id: string = "claude-code"): Promise<string> {
|
|
67
|
+
if (scope !== "core" || !event.filePath) return "";
|
|
68
|
+
if (event.tool === "Read") return autoDocumentRead(event.filePath, now, id);
|
|
69
|
+
if (event.tool !== "Write" && event.tool !== "Edit") return "";
|
|
70
|
+
const sniper = trackSessionChanges(event.sessionId, event.filePath, undefined, now);
|
|
71
|
+
const lint = postEditTypescript(event.filePath);
|
|
72
|
+
if (!sniper || !lint) return sniper || lint;
|
|
73
|
+
return contextResponse("PostToolUse", `${additionalContextOf(sniper)}\n\n${additionalContextOf(lint)}`);
|
|
74
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Cache-key + tool-class helpers for the MCP/WebFetch interception pipeline. */
|
|
2
|
+
import { parseEnvInt } from "../config/env";
|
|
3
|
+
|
|
4
|
+
/** Default cached-MCP freshness in seconds (48h). */
|
|
5
|
+
const MCP_TTL_DEFAULT_SEC = 172_800;
|
|
6
|
+
/** Default WebFetch freshness in seconds (24h, parity with Python). */
|
|
7
|
+
const WEBFETCH_TTL_DEFAULT_SEC = 86_400;
|
|
8
|
+
|
|
9
|
+
/** Cached-MCP freshness (ms) from `FUSE_MCP_TTL_SEC` (default 48h). */
|
|
10
|
+
export const MCP_TTL_MS: number = parseEnvInt(process.env.FUSE_MCP_TTL_SEC, MCP_TTL_DEFAULT_SEC) * 1000;
|
|
11
|
+
|
|
12
|
+
/** WebFetch freshness (ms) from `FUSE_WEBFETCH_TTL_SEC` (default 24h; pages stale faster than docs). */
|
|
13
|
+
export const WEBFETCH_TTL_MS: number = parseEnvInt(process.env.FUSE_WEBFETCH_TTL_SEC, WEBFETCH_TTL_DEFAULT_SEC) * 1000;
|
|
14
|
+
|
|
15
|
+
/** WebFetch prompt slice folded into its cache key (parity with Python prompt[:500]). */
|
|
16
|
+
const PROMPT_TRUNC = 500;
|
|
17
|
+
|
|
18
|
+
/** True for the WebFetch tool (exact name) or Kimi Code CLI's `FetchURL` equivalent. */
|
|
19
|
+
export function isWebFetch(tool: string): boolean {
|
|
20
|
+
return tool === "WebFetch" || tool === "FetchURL";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** MCP doc tools + WebFetch/FetchURL whose calls are cached / verbosity-capped. */
|
|
24
|
+
export function isMcpTool(tool: string): boolean {
|
|
25
|
+
return /context7|exa|webfetch|web_fetch/i.test(tool) || isWebFetch(tool);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The query/url that keys the cache. */
|
|
29
|
+
export function queryOf(input: Record<string, unknown>): string {
|
|
30
|
+
const q = input.query ?? input.url ?? input.libraryId ?? "";
|
|
31
|
+
return typeof q === "string" ? q : JSON.stringify(q);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The string that keys the cache for `tool`. WebFetch folds `url + "\n" +
|
|
36
|
+
* prompt[:500]` (parity with Python) so distinct prompts on the same URL never
|
|
37
|
+
* collide; every other tool keys on {@link queryOf}.
|
|
38
|
+
*/
|
|
39
|
+
export function cacheQueryOf(tool: string, input: Record<string, unknown>): string {
|
|
40
|
+
if (!isWebFetch(tool)) return queryOf(input);
|
|
41
|
+
const url = typeof input.url === "string" ? input.url.trim() : "";
|
|
42
|
+
const prompt = typeof input.prompt === "string" ? input.prompt : "";
|
|
43
|
+
return `${url}\n${prompt.slice(0, PROMPT_TRUNC)}`;
|
|
44
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module mcp-tool-name
|
|
3
|
+
* Canonicalizes a Codex-only MCP `tool_name` alias back to its original
|
|
4
|
+
* hyphenated form, so every existing dash-keyed consumer (`SHOT_TOOLS`,
|
|
5
|
+
* `RESEARCH_TOOLS`/`classifyExplore`, `docSourceOf`, the design-pipeline
|
|
6
|
+
* `NAV`/`SCROLL`/`GEMINI` constants, …) recognizes a Codex MCP call exactly
|
|
7
|
+
* like the same call on Claude/Kimi/Cursor/Cline/Gemini — no per-consumer
|
|
8
|
+
* change needed.
|
|
9
|
+
*
|
|
10
|
+
* Codex CLI globally rewrites every `-` to `_` across the FULL qualified MCP
|
|
11
|
+
* tool identifier before it ever reaches a hook, in `sanitize_responses_api_tool_name()`
|
|
12
|
+
* (`codex-rs/core/src/mcp_connection_manager.rs`, introduced by
|
|
13
|
+
* openai/codex#14605 — "Normalize MCP tool names to code-mode safe form",
|
|
14
|
+
* code-mode forbids `-` in identifiers). openai/codex#18385's own
|
|
15
|
+
* `SCENARIO_DASH_TOOL` test proves the REWRITTEN form is what reaches hooks:
|
|
16
|
+
* the `echo-tool` tool of server `rmcp` is delivered to hooks as
|
|
17
|
+
* `mcp__rmcp__echo_tool`. Confirmed independently in THIS repo's terrain:
|
|
18
|
+
* `~/.codex/config.toml` declares `[mcp_servers.fuse-browser]` (dash) while
|
|
19
|
+
* Codex session logs and this harness's own dual-marketplace hook payloads
|
|
20
|
+
* (`~/.codex/plugins/.../hooks.json` matchers) observe `mcp__fuse_browser__*`
|
|
21
|
+
* (underscore) — Claude never emits that form (0 occurrences across its
|
|
22
|
+
* transcripts vs 137x the dash form).
|
|
23
|
+
*
|
|
24
|
+
* The rewrite is deliberately NOT undone with a generic `_` -> `-` regex:
|
|
25
|
+
* openai/codex#15832 and #15565 document it as NON-INVERSIBLE — a server
|
|
26
|
+
* literally named `autok_local` collides with a dash-named `autok-local`
|
|
27
|
+
* once both pass through the same rewrite, and OpenAI's own fix was a closed
|
|
28
|
+
* canonical table, never a general regex. The SAME ambiguity exists on our
|
|
29
|
+
* side: a generic reversal would corrupt tool segments that legitimately
|
|
30
|
+
* contain an underscore already (`browser_screenshot`, `node_repl`,
|
|
31
|
+
* `build_sim`, …). So this module only reverses the FOUR entries proven
|
|
32
|
+
* above to matter to this repo's gates — every other segment passes through
|
|
33
|
+
* untouched.
|
|
34
|
+
* @packageDocumentation
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Codex-mangled MCP server segment -> its real (dash) server name. Closed set
|
|
39
|
+
* — see module doc. `Object.create(null)` (never a `{}` literal): `serverKey`
|
|
40
|
+
* is an attacker-influenced string (an MCP server can register itself as
|
|
41
|
+
* `"__proto__"`/`"constructor"`/`"toString"`), and a `{}`-literal lookup by
|
|
42
|
+
* such a key returns the INHERITED `Object.prototype` member instead of
|
|
43
|
+
* `undefined` — an object/function, so `?? serverKey` never falls back and
|
|
44
|
+
* the result silently corrupts to `"mcp__[object Object]__…"` (proven in
|
|
45
|
+
* `test/mcp-tool-name.test.ts`, prototype-pollution-read case). A null-
|
|
46
|
+
* prototype object has no inherited members, so every lookup outside the two
|
|
47
|
+
* literal keys below is a clean miss.
|
|
48
|
+
*/
|
|
49
|
+
const SERVER_ALIASES: Readonly<Record<string, string>> = Object.assign(Object.create(null), {
|
|
50
|
+
fuse_browser: "fuse-browser",
|
|
51
|
+
gemini_design: "gemini-design",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Codex-mangled MCP tool segment -> its real (dash) tool name. Both entries
|
|
56
|
+
* are `context7`-exclusive tool names — applied ONLY when the canonical
|
|
57
|
+
* server is `"context7"` (see the explicit guard in
|
|
58
|
+
* {@link canonicalizeMcpToolName}), never globally. A same-named tool on any
|
|
59
|
+
* other server (e.g. a hypothetical `exa__query_docs`) passes through
|
|
60
|
+
* untouched: openai/codex#15832 documents exactly this class of collision as
|
|
61
|
+
* the reason the rewrite must stay a closed, scoped table, never a blanket
|
|
62
|
+
* one. Closed set, same null-prototype rationale as {@link SERVER_ALIASES}.
|
|
63
|
+
*/
|
|
64
|
+
const TOOL_ALIASES: Readonly<Record<string, string>> = Object.assign(Object.create(null), {
|
|
65
|
+
query_docs: "query-docs",
|
|
66
|
+
resolve_library_id: "resolve-library-id",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Canonicalize a Codex-only MCP tool-name alias to its dash form. Scoped to
|
|
71
|
+
* `id === "codex"` — every other harness id passes through unchanged. The
|
|
72
|
+
* server segment is rewritten via the closed {@link SERVER_ALIASES} table;
|
|
73
|
+
* the tool segment is rewritten via {@link TOOL_ALIASES} ONLY when the
|
|
74
|
+
* (already-resolved) canonical server is `"context7"` — every other server's
|
|
75
|
+
* tool segment is left untouched even if it happens to share a name with a
|
|
76
|
+
* context7 tool. Every other `mcp__<server>__<tool>` segment (or non-MCP
|
|
77
|
+
* tool name) is returned byte-identical (see `test/mcp-tool-name.test.ts`
|
|
78
|
+
* for the non-regression proof across ids, trap tool names, and the
|
|
79
|
+
* server-scoping cases).
|
|
80
|
+
* @param id - Harness adapter id (e.g. "codex", "claude-code", "kimi").
|
|
81
|
+
* @param tool - Raw `tool_name` from the hook payload.
|
|
82
|
+
*/
|
|
83
|
+
export function canonicalizeMcpToolName(id: string, tool: string): string {
|
|
84
|
+
if (id !== "codex") return tool;
|
|
85
|
+
const parts = tool.split("__");
|
|
86
|
+
if (parts.length < 3 || parts[0] !== "mcp") return tool;
|
|
87
|
+
const serverKey = parts[1] ?? "";
|
|
88
|
+
const toolKey = parts.slice(2).join("__");
|
|
89
|
+
const server = SERVER_ALIASES[serverKey] ?? serverKey;
|
|
90
|
+
const rest = server === "context7" ? (TOOL_ALIASES[toolKey] ?? toolKey) : toolKey;
|
|
91
|
+
return `mcp__${server}__${rest}`;
|
|
92
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { capVerbosity } from "../policy/verbosity";
|
|
2
|
+
import { cacheLookupMeta, cacheLookupSubstringMeta, type CacheHit } from "../cache/store";
|
|
3
|
+
import { extractText } from "../cache/mcp-response";
|
|
4
|
+
import { mcpCacheWrite, webfetchCacheWrite } from "../cache/mcp-store";
|
|
5
|
+
import { WEBFETCH_TTL_MS, cacheQueryOf, isMcpTool, isWebFetch } from "./mcp-key";
|
|
6
|
+
import { kimiDenyResponse } from "../adapters/kimi";
|
|
7
|
+
|
|
8
|
+
export { MCP_TTL_MS, WEBFETCH_TTL_MS, cacheQueryOf, isMcpTool, queryOf } from "./mcp-key";
|
|
9
|
+
|
|
10
|
+
function denyWith(id: string, content: string): string {
|
|
11
|
+
if (id === "claude-code" || id === "codex") {
|
|
12
|
+
return JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: content } });
|
|
13
|
+
}
|
|
14
|
+
if (id === "gemini-cli") return JSON.stringify({ decision: "deny", reason: content });
|
|
15
|
+
if (id === "kimi") return kimiDenyResponse(content);
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function mutateWith(id: string, input: Record<string, unknown>): string {
|
|
20
|
+
if (id === "claude-code" || id === "codex") {
|
|
21
|
+
return JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: input } });
|
|
22
|
+
}
|
|
23
|
+
if (id === "gemini-cli") return JSON.stringify({ hookSpecificOutput: { tool_input: input } });
|
|
24
|
+
// Kimi has no input-mutation contract (`updatedInput` is undocumented
|
|
25
|
+
// upstream) — the exa verbosity cap stays inoperative there.
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The doc provider a served cache-hit satisfies (`exa`/`context7`), else undefined. */
|
|
30
|
+
function docSourceOf(tool: string): string | undefined {
|
|
31
|
+
if (/exa/i.test(tool)) return "exa";
|
|
32
|
+
if (/context7/i.test(tool)) return "context7";
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Wrap a cache hit's body in the harness-facing `CACHE HIT` notice — parity with
|
|
38
|
+
* `webfetch-cache-lookup.py` / `mcp-cache-lookup.py` (KB = body chars/1024 + 1,
|
|
39
|
+
* age hours = ageMs/3_600_000, both floored).
|
|
40
|
+
*/
|
|
41
|
+
function cacheHitText(web: boolean, hit: CacheHit): string {
|
|
42
|
+
const kb = Math.floor(hit.body.length / 1024) + 1;
|
|
43
|
+
const hours = Math.floor(hit.ageMs / 3_600_000);
|
|
44
|
+
return web
|
|
45
|
+
? `CACHE HIT WebFetch (~${kb}KB saved, cached ${hours}h ago):\n\n${hit.body}\n\nModify the URL or query to force a fresh fetch.`
|
|
46
|
+
: `CACHE HIT (~${kb}KB saved, cached ${hours}h ago): ${hit.body}\n\nRephrase to force a re-call.`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A pre-event MCP interception: the native response + any doc source it satisfied. */
|
|
50
|
+
export interface McpIntercept {
|
|
51
|
+
stdout: string;
|
|
52
|
+
docSource?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Pre-event MCP interception: serve a fresh cache hit (deny + cached content),
|
|
57
|
+
* else cap exa verbosity (allow + mutated input), else null to allow normally.
|
|
58
|
+
* Harnesses without input-mutation/cache support fall through to null.
|
|
59
|
+
*/
|
|
60
|
+
export function mcpPreIntercept(id: string, tool: string, input: Record<string, unknown>, dir: string, ttlMs: number, now: number): McpIntercept | null {
|
|
61
|
+
if (!isMcpTool(tool)) return null;
|
|
62
|
+
const web = isWebFetch(tool);
|
|
63
|
+
const ttl = web ? WEBFETCH_TTL_MS : ttlMs;
|
|
64
|
+
const key = cacheQueryOf(tool, input);
|
|
65
|
+
// Exact key first; for MCP docs fall back to a substring hit (Python `rg -i -F`).
|
|
66
|
+
const hit = cacheLookupMeta(dir, tool, key, ttl, now) ?? (web ? null : cacheLookupSubstringMeta(dir, key, ttl, now));
|
|
67
|
+
if (hit) {
|
|
68
|
+
const served = denyWith(id, cacheHitText(web, hit));
|
|
69
|
+
if (served) return { stdout: served, docSource: docSourceOf(tool) };
|
|
70
|
+
}
|
|
71
|
+
const capped = capVerbosity(tool, input);
|
|
72
|
+
if (capped) {
|
|
73
|
+
const mutated = mutateWith(id, capped);
|
|
74
|
+
if (mutated) return { stdout: mutated };
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Post-event: persist the MCP/WebFetch response (extracted to markdown). MCP docs
|
|
81
|
+
* go through {@link mcpCacheWrite} (compact + Jaccard-dedup + `index.json`);
|
|
82
|
+
* WebFetch uses {@link webfetchCacheWrite} (compact, exact key, no index).
|
|
83
|
+
* @param now - Current epoch ms (timestamp source; defaults to `Date.now()`).
|
|
84
|
+
*/
|
|
85
|
+
export function mcpPostStore(tool: string, input: Record<string, unknown>, response: unknown, dir: string, now: number = Date.now()): void {
|
|
86
|
+
if (!isMcpTool(tool)) return;
|
|
87
|
+
const text = extractText(response);
|
|
88
|
+
const key = cacheQueryOf(tool, input);
|
|
89
|
+
if (isWebFetch(tool)) webfetchCacheWrite(dir, tool, key, text, now);
|
|
90
|
+
else mcpCacheWrite(dir, tool, key, text, now);
|
|
91
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join, relative, basename } from "node:path";
|
|
3
|
+
import type { Prompt } from "../prompt/types";
|
|
4
|
+
import { detectModularArchitecture } from "../policy/detect-project";
|
|
5
|
+
|
|
6
|
+
const NEXT_CONVENTION = /^(page|layout|loading|error|not-found|route|template|default|global-error|opengraph-image|twitter-image|icon|apple-icon|sitemap|robots|manifest|middleware)\.(tsx|ts|js|jsx)$/;
|
|
7
|
+
const NEXT_STATIC = /\.(css|ico|png|jpg|svg|json)$/;
|
|
8
|
+
const PHP_BLOCKED_IN_APP: readonly string[] = [
|
|
9
|
+
"/app/Models/", "/app/Services/", "/app/Actions/", "/app/Http/Controllers/",
|
|
10
|
+
"/app/Http/Requests/", "/app/Http/Resources/", "/app/Contracts/", "/app/DTOs/",
|
|
11
|
+
"/app/Repositories/", "/app/Events/", "/app/Listeners/", "/app/Jobs/",
|
|
12
|
+
"/app/Notifications/", "/app/Policies/",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const block = (reason: string): Prompt => ({
|
|
16
|
+
kind: "block", title: "Modular architecture", reason,
|
|
17
|
+
actions: ["Move the code into the correct feature module", "Import only from the shared core module"],
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/** Next.js `modules/` architecture: `app/` convention + cross-module import rules. */
|
|
21
|
+
function nextModular(filePath: string, content: string, cwd: string): Prompt | null {
|
|
22
|
+
const rel = relative(cwd, filePath);
|
|
23
|
+
const bn = basename(filePath);
|
|
24
|
+
if ((rel.startsWith("app/") || rel.startsWith("src/app/")) && !NEXT_CONVENTION.test(bn) && !NEXT_STATIC.test(bn)) {
|
|
25
|
+
return block(`BLOCKED: modular Next.js — '${bn}' is not an app/ convention file. Move business logic to modules/[feature]/. Only page.tsx, layout.tsx, route.ts allowed in app/.`);
|
|
26
|
+
}
|
|
27
|
+
const mod = filePath.match(/\/modules\/([^/]+)\//);
|
|
28
|
+
if (!mod) return null;
|
|
29
|
+
const current = mod[1] ?? "";
|
|
30
|
+
for (const m of content.matchAll(/from\s+['"][@.][^'"]*?\/modules\/([^/]+)\//g)) {
|
|
31
|
+
const imported = m[1] ?? "";
|
|
32
|
+
if (current === "cores") {
|
|
33
|
+
if (imported !== "cores" && imported !== "core") return block(`BLOCKED: modules/cores/ must not import from modules/${imported}/. Cores must be independent.`);
|
|
34
|
+
} else if (imported !== current && imported !== "cores" && imported !== "core") {
|
|
35
|
+
return block(`BLOCKED: cross-module import — '${current}' imports from '${imported}'. Only modules/cores/ is shared.`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Laravel FuseCore architecture: `app/` domain ban + module.json + cross-module `use` rules. */
|
|
42
|
+
function fusecore(filePath: string, content: string, cwd: string): Prompt | null {
|
|
43
|
+
for (const b of PHP_BLOCKED_IN_APP) {
|
|
44
|
+
if (filePath.includes(b)) {
|
|
45
|
+
// Parity validate-fusecore.py:52-55 — name the precise target sub-path per
|
|
46
|
+
// entry (blocked.replace("/app/","/FuseCore/{Module}/App/")), not a generic dir.
|
|
47
|
+
const from = b.replace(/^\/|\/$/g, "");
|
|
48
|
+
const to = b.replaceAll("/app/", "/FuseCore/{Module}/App/").replace(/^\/|\/$/g, "");
|
|
49
|
+
return block(`BLOCKED: FuseCore project. Code in '${from}' FORBIDDEN. Move to '${to}'.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const mod = filePath.match(/\/FuseCore\/([A-Za-z]+)\//);
|
|
53
|
+
if (!mod) return null;
|
|
54
|
+
const name = mod[1] ?? "";
|
|
55
|
+
const mj = join(cwd, "FuseCore", name, "module.json");
|
|
56
|
+
// Parity validate-fusecore.py: name the full path to create ("Create {mj} first.").
|
|
57
|
+
if (!existsSync(mj)) return block(`BLOCKED: Module '${name}' missing module.json. Create ${mj} first.`);
|
|
58
|
+
for (const m of content.matchAll(/use\s+FuseCore\\(\w+)\\/g)) {
|
|
59
|
+
const imported = m[1] ?? "";
|
|
60
|
+
if (name === "Core") {
|
|
61
|
+
if (imported !== "Core") return block(`BLOCKED: FuseCore\\Core\\ must not use FuseCore\\${imported}\\.`);
|
|
62
|
+
} else if (imported !== name && imported !== "Core") {
|
|
63
|
+
return block(`BLOCKED: cross-module use — '${name}' uses '${imported}'. Only FuseCore\\Core\\ is shared.`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Enforce the project's modular architecture (Next.js `modules/` or Laravel FuseCore) on a Write/Edit. */
|
|
70
|
+
export function modularGate(tool: string, filePath: string | undefined, content: string | undefined, cwd: string | undefined): Prompt | null {
|
|
71
|
+
if ((tool !== "Write" && tool !== "Edit") || !filePath || !cwd) return null;
|
|
72
|
+
if (/\/(node_modules|dist|build|\.next|vendor|storage)\//.test(filePath)) return null;
|
|
73
|
+
const arch = detectModularArchitecture(cwd);
|
|
74
|
+
if (arch === "nextjs-modular" && /\.(tsx|ts|jsx|js)$/.test(filePath)) return nextModular(filePath, content ?? "", cwd);
|
|
75
|
+
if (arch === "fusecore" && filePath.endsWith(".php")) return fusecore(filePath, content ?? "", cwd);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { parseApplyPatch } from "../adapters/codex/apply-patch";
|
|
2
|
+
import { extractCursorEvent } from "../adapters/cursor/normalize";
|
|
3
|
+
import { commandToString } from "./command-string";
|
|
4
|
+
import { canonicalizeCodexShellTool } from "./codex-shell-tool";
|
|
5
|
+
import { canonicalizeMcpToolName } from "./mcp-tool-name";
|
|
6
|
+
|
|
7
|
+
/** One file/edit fanned out from Codex `apply_patch` or Cursor `afterFileEdit`. */
|
|
8
|
+
export interface NormalizedFile {
|
|
9
|
+
filePath: string;
|
|
10
|
+
content: string;
|
|
11
|
+
oldString?: string;
|
|
12
|
+
op: "add" | "update" | "delete";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A hook event normalized across harnesses. */
|
|
16
|
+
export interface NormalizedEvent {
|
|
17
|
+
/** Native event name when the adapter exposes one explicitly (Cursor). */
|
|
18
|
+
eventName?: string;
|
|
19
|
+
phase: "pre" | "post";
|
|
20
|
+
tool: string;
|
|
21
|
+
input: Record<string, unknown>;
|
|
22
|
+
sessionId: string;
|
|
23
|
+
filePath?: string;
|
|
24
|
+
content?: string;
|
|
25
|
+
/** Edit only: the tool_input.old_string being replaced — lets the file-size gate (policy/evaluate.ts + policy/edit-outcome.ts) compute the post-edit outcome instead of judging the stale on-disk count alone. Undefined for Write (no such field) and for cline/apply_patch (parsed separately, no equivalent field). */
|
|
26
|
+
oldString?: string;
|
|
27
|
+
command?: string;
|
|
28
|
+
/** Cursor-only independent command candidates from beforeMCPExecution. */
|
|
29
|
+
commandCandidates?: string[];
|
|
30
|
+
/** Subagent type, if the tool-use came from one (Explore/Plan are file-size-exempt). */
|
|
31
|
+
agentType?: string;
|
|
32
|
+
/** Harness-resolved permission mode (Claude emits it natively; Codex maps `AskForApproval::Never` to the same "bypassPermissions" string — see adapters/codex/permission-mode.ts). Generic field, Codex-only consumer today. */
|
|
33
|
+
permissionMode?: string;
|
|
34
|
+
/** Codex logical tool-use identity, shared by sibling hook callbacks. */
|
|
35
|
+
toolUseId?: string;
|
|
36
|
+
/** Harness-reported working directory used to scope Codex authorization. */
|
|
37
|
+
cwd?: string;
|
|
38
|
+
/** Validated Cursor multi-root workspace paths in wire order. */
|
|
39
|
+
workspaceRoots?: string[];
|
|
40
|
+
/**
|
|
41
|
+
* Per-file changes for Codex `apply_patch` and per-edit changes for Cursor
|
|
42
|
+
* `afterFileEdit`; undefined for single-file events.
|
|
43
|
+
*/
|
|
44
|
+
files?: NormalizedFile[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function str(v: unknown): string | undefined {
|
|
48
|
+
return typeof v === "string" ? v : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Normalize a hook payload, including Cline nesting and native Cursor events.
|
|
53
|
+
*/
|
|
54
|
+
export function normalizeEvent(id: string, payload: Record<string, unknown>): NormalizedEvent {
|
|
55
|
+
if (id === "cline") {
|
|
56
|
+
const post = payload.postToolUse as Record<string, unknown> | undefined;
|
|
57
|
+
const node = (post ?? (payload.preToolUse as Record<string, unknown> | undefined)) ?? {};
|
|
58
|
+
const params = (node.parameters as Record<string, unknown> | undefined) ?? {};
|
|
59
|
+
return {
|
|
60
|
+
phase: post ? "post" : "pre",
|
|
61
|
+
tool: str(node.toolName) ?? "",
|
|
62
|
+
input: params,
|
|
63
|
+
sessionId: str(payload.taskId) ?? "",
|
|
64
|
+
filePath: str(params.path),
|
|
65
|
+
content: str(params.content),
|
|
66
|
+
command: str(params.command),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (id === "cursor") {
|
|
70
|
+
return {
|
|
71
|
+
...extractCursorEvent(payload),
|
|
72
|
+
sessionId: str(payload.session_id) ?? str(payload.conversation_id) ?? "",
|
|
73
|
+
agentType: str(payload.agent_type),
|
|
74
|
+
permissionMode: str(payload.permission_mode),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const event = str(payload.hook_event_name) ?? "";
|
|
78
|
+
const input = (payload.tool_input as Record<string, unknown> | undefined) ?? payload;
|
|
79
|
+
const tool = canonicalizeCodexShellTool(id, canonicalizeMcpToolName(id, str(payload.tool_name) ?? ""));
|
|
80
|
+
const base = {
|
|
81
|
+
phase: (/post|after/i.test(event) ? "post" : "pre") as "pre" | "post",
|
|
82
|
+
tool,
|
|
83
|
+
input,
|
|
84
|
+
sessionId: str(payload.session_id) ?? str(payload.conversation_id) ?? "",
|
|
85
|
+
agentType: str(payload.agent_type) ?? str(input.subagent_type),
|
|
86
|
+
permissionMode: str(payload.permission_mode),
|
|
87
|
+
toolUseId: str(payload.tool_use_id),
|
|
88
|
+
cwd: str(payload.cwd),
|
|
89
|
+
};
|
|
90
|
+
// Codex's `apply_patch` (its PRIMARY edit primitive) carries the whole change
|
|
91
|
+
// set as a freeform patch in `command` — no `file_path`/`content`, so the
|
|
92
|
+
// file-size/DRY/protected-path gates saw nothing (enforcement 0%). Fan it out
|
|
93
|
+
// into `files` and DROP the patch text from `command` so it can't false-match
|
|
94
|
+
// the git guards. Every other tool is untouched below.
|
|
95
|
+
if (tool === "apply_patch") {
|
|
96
|
+
const patch = str(input.command) ?? str(payload.command) ?? "";
|
|
97
|
+
const files = parseApplyPatch(patch).map((f) => ({ filePath: f.path, content: f.content, op: f.op }));
|
|
98
|
+
return { ...base, phase: base.phase, files: files.length > 0 ? files : undefined };
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
...base,
|
|
102
|
+
filePath: str(input.file_path) ?? str(input.path) ?? str(payload.file_path),
|
|
103
|
+
content: str(input.content) ?? str(input.new_string),
|
|
104
|
+
oldString: str(input.old_string),
|
|
105
|
+
command: commandToString(input.command) ?? commandToString(payload.command),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module notices
|
|
3
|
+
* Compact, uniform "compliance" notices for the user-visible `systemMessage`
|
|
4
|
+
* channel — the visual counterpart to the additionalContext-only gate/credit
|
|
5
|
+
* signals, which land in agent-only context and stay invisible to the human in
|
|
6
|
+
* the Claude Code UI (deny reasons show in red; these did not show at all).
|
|
7
|
+
* Owner-reported gap: pass-notices existed in v0.1.49 for design gates, but
|
|
8
|
+
* nothing surfaced skill credits, freshness, or the sniper reminder to the human.
|
|
9
|
+
*
|
|
10
|
+
* The text builders are pure. `refCreditNoticeFor` is the one exception — like
|
|
11
|
+
* `pre-allow.ts` uses the JSON `oncePerWindow`, this one uses the exclusive
|
|
12
|
+
* {@link onceExclusive} mode instead: it fires on the same short burst window
|
|
13
|
+
* as `track-changes.ts`'s sniper reminder, fanned out across every installed
|
|
14
|
+
* plugin's PostToolUse hook for ONE real edit, so the same lost-update race
|
|
15
|
+
* applies (lesson 2026-07-05 16:00). Rendering onto a harness's native stdout
|
|
16
|
+
* always goes through the existing adapter helpers (`respond`/
|
|
17
|
+
* `attachSystemMessage`); a harness with no `systemMessage` channel (e.g.
|
|
18
|
+
* cline) silently drops the notice there (documented no-op, never a crash) —
|
|
19
|
+
* nothing in this module renders directly.
|
|
20
|
+
*
|
|
21
|
+
* CHANNEL CONTRACT — human-only, exactly once: the `userMessage`/`systemMessage`
|
|
22
|
+
* these notices ride is a HUMAN channel by platform contract. It never reaches
|
|
23
|
+
* the agent — `formatPrompt` (src/prompt/types.ts) excludes it — so the emitting
|
|
24
|
+
* sub-agent does NOT see its own notice; only the human owner does, and exactly
|
|
25
|
+
* ONCE even under the multi-plugin fan-out, because the emitter is gated by
|
|
26
|
+
* {@link onceExclusive}. Verified live 2026-07-06: under the real ×11 fan-out a
|
|
27
|
+
* single skill-ref Read surfaced one `✓ SOLID refs read` line in the owner's
|
|
28
|
+
* terminal (not duplicated, and correctly invisible to the agent).
|
|
29
|
+
*/
|
|
30
|
+
import { onceExclusive } from "./inject-dedup";
|
|
31
|
+
import { BURST_DEDUP_MS } from "./burst-window";
|
|
32
|
+
|
|
33
|
+
/** One compliance line: `✓ <gate> — <detail>` (detail omitted when empty). */
|
|
34
|
+
export function complianceNotice(gate: string, detail: string): string {
|
|
35
|
+
return detail ? `✓ ${gate} — ${detail}` : `✓ ${gate}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** One non-blocking requirement line: `⚠ <requirement> — <detail>`. */
|
|
39
|
+
export function requirementNotice(requirement: string, detail: string): string {
|
|
40
|
+
return detail ? `⚠ ${requirement} — ${detail}` : `⚠ ${requirement}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Extract the skill name from a `.md` ref path (`.../skills/<name>/...`), or null when it isn't a skill reference (a banal doc Read — no notice). */
|
|
44
|
+
export function skillNameFromRefPath(path: string): string | null {
|
|
45
|
+
const m = /skills\/([^/]+)\//.exec(path);
|
|
46
|
+
return m?.[1] ?? null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Notice for a SOLID/skill reference credited via an in-session Read, or null for a non-skill `.md`. */
|
|
50
|
+
export function refCreditedNotice(path: string): string | null {
|
|
51
|
+
const skill = skillNameFromRefPath(path);
|
|
52
|
+
return skill ? complianceNotice("SOLID refs read", skill) : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Notice for the APEX freshness gate (explore+research) currently satisfied. */
|
|
56
|
+
export function evidenceFreshNotice(): string {
|
|
57
|
+
return complianceNotice("evidence fresh", "explore+research");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Notice mirroring the existing sniper-required additionalContext reminder. */
|
|
61
|
+
export function sniperRequiredNotice(fileName: string): string {
|
|
62
|
+
return requirementNotice("sniper required", fileName);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The one `✓ SOLID refs read (<skill>)` notice to show for this PostToolUse
|
|
67
|
+
* call, or null. Scans the activities `activityFor` recorded from this event
|
|
68
|
+
* for a skill-ref Read, deduped per (session, path) against the same burst
|
|
69
|
+
* window as the sniper reminder — the ~11 sibling-plugin fan-out for one real
|
|
70
|
+
* Read must never repeat it (lesson 2026-07-05 15:21).
|
|
71
|
+
* @param activities - This event's recorded activities (only `ref` entries matter).
|
|
72
|
+
* @param sessionId - Current session id (dedup scope).
|
|
73
|
+
* @param now - Event clock.
|
|
74
|
+
* @param dir - State-dir override for the dedup sidecar (tests MUST pass an
|
|
75
|
+
* isolated dir; production passes the per-project state dir).
|
|
76
|
+
*/
|
|
77
|
+
export function refCreditNoticeFor(
|
|
78
|
+
activities: ReadonlyArray<{ kind: string; path?: string }>,
|
|
79
|
+
sessionId: string,
|
|
80
|
+
now: number,
|
|
81
|
+
dir?: string,
|
|
82
|
+
): string | null {
|
|
83
|
+
for (const a of activities) {
|
|
84
|
+
if (a.kind !== "ref" || !a.path) continue;
|
|
85
|
+
const notice = refCreditedNotice(a.path);
|
|
86
|
+
if (!notice) continue;
|
|
87
|
+
if (!onceExclusive(`ref-credited:${sessionId}:${a.path}`, BURST_DEDUP_MS, { now, dir })) continue;
|
|
88
|
+
return notice;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|