@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,71 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Run a git subcommand at `root` with a short timeout, returning trimmed stdout.
|
|
5
|
+
* Uses `node:child_process` (the Bun shell can hang on some git plumbing) and
|
|
6
|
+
* swallows every failure — a non-repo, missing git, or timeout yields `""` so
|
|
7
|
+
* the caller omits the section instead of throwing inside the hook.
|
|
8
|
+
* @param root - Directory to run git in.
|
|
9
|
+
* @param args - The git args (e.g. `"log --oneline -3"`).
|
|
10
|
+
* @returns Trimmed stdout, or `""` on any error.
|
|
11
|
+
*/
|
|
12
|
+
function git(root: string, args: string): string {
|
|
13
|
+
try {
|
|
14
|
+
return execSync(`git ${args}`, {
|
|
15
|
+
cwd: root,
|
|
16
|
+
encoding: "utf8",
|
|
17
|
+
timeout: 150,
|
|
18
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
19
|
+
}).trim();
|
|
20
|
+
} catch {
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Working-tree file counts parsed from `git status --porcelain`. */
|
|
26
|
+
interface Wip {
|
|
27
|
+
staged: number;
|
|
28
|
+
unstaged: number;
|
|
29
|
+
untracked: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Count staged/unstaged/untracked files from porcelain v1 output (skips the `##` branch line). */
|
|
33
|
+
function countWip(porcelain: string): Wip {
|
|
34
|
+
const w: Wip = { staged: 0, unstaged: 0, untracked: 0 };
|
|
35
|
+
for (const line of porcelain.split("\n")) {
|
|
36
|
+
if (!line || line.startsWith("#")) continue;
|
|
37
|
+
if (line.startsWith("??")) { w.untracked++; continue; }
|
|
38
|
+
const x = line[0], y = line[1];
|
|
39
|
+
if (x && x !== " " && x !== "?") w.staged++;
|
|
40
|
+
if (y === "M" || y === "D") w.unstaged++;
|
|
41
|
+
}
|
|
42
|
+
return w;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Parse the current branch from the leading `## branch...upstream` porcelain line. */
|
|
46
|
+
function parseBranch(porcelain: string): string {
|
|
47
|
+
const head = porcelain.split("\n")[0] ?? "";
|
|
48
|
+
if (!head.startsWith("## ")) return "";
|
|
49
|
+
const rest = head.slice(3);
|
|
50
|
+
const dots = rest.indexOf("...");
|
|
51
|
+
return (dots >= 0 ? rest.slice(0, dots) : rest).split(" ")[0] ?? "";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Collect a compact git reconciliation section for `root`: current branch, the
|
|
56
|
+
* last three commits (oneline), and staged/unstaged/untracked WIP counts. When
|
|
57
|
+
* `root` is not a git repo (status fails) the whole section is omitted (`""`).
|
|
58
|
+
* @param root - The project/repo root.
|
|
59
|
+
* @returns The rendered git section body, or `""` when not a repo.
|
|
60
|
+
*/
|
|
61
|
+
export function collectGit(root: string): string {
|
|
62
|
+
const status = git(root, "status --porcelain=v1 --branch");
|
|
63
|
+
if (!status) return "";
|
|
64
|
+
const branch = parseBranch(status) || "(unknown)";
|
|
65
|
+
const w = countWip(status);
|
|
66
|
+
const log = git(root, "log --oneline -3");
|
|
67
|
+
const lines = [`- branch: ${branch}`];
|
|
68
|
+
if (log) lines.push("- recent:", ...log.split("\n").map((l) => ` ${l}`));
|
|
69
|
+
lines.push(`- WIP: ${w.staged} staged, ${w.unstaged} unstaged, ${w.untracked} untracked`);
|
|
70
|
+
return lines.join("\n");
|
|
71
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { projectRootOrNull } from "../../../util/project-root";
|
|
2
|
+
import { oneShotSummary } from "../../../tracking/one-shot";
|
|
3
|
+
import { collectGit } from "./git";
|
|
4
|
+
import { collectVersion } from "./version";
|
|
5
|
+
import { collectBoard } from "./board";
|
|
6
|
+
import { renderSections, attachSnapshot, type Section } from "./format";
|
|
7
|
+
|
|
8
|
+
/** Run `fn`, swallowing any throw into `""` so no single collector can break the hook. */
|
|
9
|
+
function safe(fn: () => string): string {
|
|
10
|
+
try {
|
|
11
|
+
return fn();
|
|
12
|
+
} catch {
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build the reconciliation snapshot markdown for `cwd`: git state, running
|
|
19
|
+
* harness version + drift, the persistent board, and one-shot gate status. Each
|
|
20
|
+
* collector is isolated by {@link safe}; an all-empty result yields `""`.
|
|
21
|
+
* @param cwd - The session working directory.
|
|
22
|
+
* @param moduleUrl - `import.meta.url` of the caller (locates the running package).
|
|
23
|
+
* @returns The snapshot markdown, or `""` when nothing to report.
|
|
24
|
+
*/
|
|
25
|
+
export function renderSnapshot(cwd: string, moduleUrl: string): string {
|
|
26
|
+
const root = projectRootOrNull(cwd) ?? cwd;
|
|
27
|
+
const sections: Section[] = [
|
|
28
|
+
{ title: "Git", body: safe(() => collectGit(root)) },
|
|
29
|
+
{ title: "Version", body: safe(() => collectVersion(root, moduleUrl)) },
|
|
30
|
+
{ title: "Board", body: safe(() => collectBoard(root)) },
|
|
31
|
+
// raw `cwd` (not `root`): oneShotSummary derives the state dir via defaultStateDir(cwd),
|
|
32
|
+
// keyed on the same raw hook cwd the runtime writes gate counters under (handle.ts).
|
|
33
|
+
{ title: "One-shot gates", body: safe(() => oneShotSummary(cwd)) },
|
|
34
|
+
];
|
|
35
|
+
return renderSections(sections);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Concatenate the reconciliation snapshot onto a core SessionStart stdout. Fully
|
|
40
|
+
* fail-safe: any error returns `stdout` unchanged so the hook never breaks.
|
|
41
|
+
* @param stdout - The core SessionStart JSON stdout (may be `""`).
|
|
42
|
+
* @param cwd - The session working directory.
|
|
43
|
+
* @param moduleUrl - `import.meta.url` of the caller.
|
|
44
|
+
* @returns The merged hook stdout.
|
|
45
|
+
*/
|
|
46
|
+
export function withSnapshot(stdout: string, cwd: string, moduleUrl: string): string {
|
|
47
|
+
try {
|
|
48
|
+
return attachSnapshot(stdout, renderSnapshot(cwd, moduleUrl));
|
|
49
|
+
} catch {
|
|
50
|
+
return stdout;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { runningVersion } from "../../../cli/doctor";
|
|
4
|
+
|
|
5
|
+
/** Read the `version` field of `<root>/package.json`, or `""` if absent/unreadable. */
|
|
6
|
+
function pkgVersion(root: string): string {
|
|
7
|
+
try {
|
|
8
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { version?: string };
|
|
9
|
+
return pkg.version ?? "";
|
|
10
|
+
} catch {
|
|
11
|
+
return "";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Collect the version reconciliation section: the harness version actually
|
|
17
|
+
* running (resolved from {@link runningVersion}, no network) and, when `root`
|
|
18
|
+
* carries its own `package.json`, whether that project's version has drifted
|
|
19
|
+
* from the running harness.
|
|
20
|
+
* @param root - The project root (cwd repo).
|
|
21
|
+
* @param moduleUrl - `import.meta.url` of the calling module (locates the running package.json).
|
|
22
|
+
* @returns The rendered version section body (never `""`).
|
|
23
|
+
*/
|
|
24
|
+
export function collectVersion(root: string, moduleUrl: string): string {
|
|
25
|
+
const running = runningVersion(moduleUrl).version;
|
|
26
|
+
const lines = [`- harness running: v${running}`];
|
|
27
|
+
const project = pkgVersion(root);
|
|
28
|
+
if (project && project !== running) {
|
|
29
|
+
lines.push(`- project package.json: v${project} (DRIFT — running harness differs)`);
|
|
30
|
+
} else if (project) {
|
|
31
|
+
lines.push(`- project package.json: v${project} (in sync)`);
|
|
32
|
+
}
|
|
33
|
+
return lines.join("\n");
|
|
34
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
/** A SOLID project profile: type label, per-file line limit, interface dir. */
|
|
5
|
+
export interface SolidProfile {
|
|
6
|
+
type: string;
|
|
7
|
+
limit: number;
|
|
8
|
+
ifaceDir: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Ordered detection table (mirrors solid/scripts/detect-project.py). */
|
|
12
|
+
const CHECKS: { file: string; grep: string | null; profile: SolidProfile }[] = [
|
|
13
|
+
{ file: "package.json", grep: "next", profile: { type: "nextjs", limit: 150, ifaceDir: "modules/cores/interfaces" } },
|
|
14
|
+
{ file: "composer.json", grep: "laravel", profile: { type: "laravel", limit: 100, ifaceDir: "app/Contracts" } },
|
|
15
|
+
{ file: "go.mod", grep: null, profile: { type: "go", limit: 100, ifaceDir: "internal/interfaces" } },
|
|
16
|
+
{ file: "Cargo.toml", grep: null, profile: { type: "rust", limit: 100, ifaceDir: "src/traits" } },
|
|
17
|
+
{ file: "pyproject.toml", grep: null, profile: { type: "python", limit: 100, ifaceDir: "src/interfaces" } },
|
|
18
|
+
{ file: "requirements.txt", grep: null, profile: { type: "python", limit: 100, ifaceDir: "src/interfaces" } },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/** Detect the SOLID profile for `projectDir`, defaulting to `unknown`. */
|
|
22
|
+
export function detectSolidProfile(projectDir: string): SolidProfile {
|
|
23
|
+
for (const { file, grep, profile } of CHECKS) {
|
|
24
|
+
const path = join(projectDir, file);
|
|
25
|
+
if (!existsSync(path)) continue;
|
|
26
|
+
if (grep !== null) {
|
|
27
|
+
try { if (!readFileSync(path, "utf-8").includes(grep)) continue; } catch { continue; }
|
|
28
|
+
}
|
|
29
|
+
return profile;
|
|
30
|
+
}
|
|
31
|
+
if (existsSync(join(projectDir, "Package.swift"))) return { type: "swift", limit: 150, ifaceDir: "Protocols" };
|
|
32
|
+
try {
|
|
33
|
+
if (readdirSync(projectDir).some((e) => e.endsWith(".xcodeproj") || e.endsWith(".xcworkspace"))) {
|
|
34
|
+
return { type: "swift", limit: 150, ifaceDir: "Protocols" };
|
|
35
|
+
}
|
|
36
|
+
} catch { /* unreadable dir */ }
|
|
37
|
+
return { type: "unknown", limit: 100, ifaceDir: "" };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Handle solid SessionStart: detect the profile, append SOLID_* exports to
|
|
42
|
+
* `CLAUDE_ENV_FILE`, and return the `SOLID: …` stdout line (or "" for unknown).
|
|
43
|
+
* Ports `solid/scripts/detect-project.py`.
|
|
44
|
+
* @param env - Environment (defaults to `process.env`).
|
|
45
|
+
* @returns The plain-text stdout line (possibly empty).
|
|
46
|
+
*/
|
|
47
|
+
export function solidDetectStart(env: Record<string, string | undefined> = process.env): string {
|
|
48
|
+
const profile = detectSolidProfile(env.CLAUDE_PROJECT_DIR ?? ".");
|
|
49
|
+
const envFile = env.CLAUDE_ENV_FILE ?? "";
|
|
50
|
+
if (envFile) {
|
|
51
|
+
try {
|
|
52
|
+
appendFileSync(envFile, `export SOLID_PROJECT_TYPE=${profile.type}\nexport SOLID_FILE_LIMIT=${profile.limit}\nexport SOLID_INTERFACE_DIR=${profile.ifaceDir}\n`, "utf-8");
|
|
53
|
+
} catch { /* best effort */ }
|
|
54
|
+
}
|
|
55
|
+
return profile.type !== "unknown" ? `SOLID: ${profile.type} project (max ${profile.limit} lines)` : "";
|
|
56
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { cleanupSession } from "./session-end";
|
|
3
|
+
import { validateTaskSolid } from "./task-completed";
|
|
4
|
+
import { defaultStateDir } from "../paths";
|
|
5
|
+
import { notify } from "../notifications";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Handle the core scope's `Stop` event — Codex parity. Codex never emits
|
|
9
|
+
* `SessionEnd`/`TaskCompleted` (codex-plugins/docs/reference/hooks.md,
|
|
10
|
+
* "Harness Runtime Limits": "lifecycle dispatch includes Claude-only names
|
|
11
|
+
* such as TaskCompleted, PostToolUseFailure, and SessionEnd; Codex does not
|
|
12
|
+
* emit those events") but its own hooks.json wires `Stop` to `hook codex
|
|
13
|
+
* core`, and its own docs table defines `Stop` as "turn finishes — cleanup
|
|
14
|
+
* and completion notification". Both ported behaviors collapse onto it here
|
|
15
|
+
* rather than being invented anew: {@link cleanupSession} (normally
|
|
16
|
+
* SessionEnd-only) for cleanup, {@link validateTaskSolid} (normally
|
|
17
|
+
* TaskCompleted-only) for the SOLID/receipt completion check.
|
|
18
|
+
*
|
|
19
|
+
* Claude-side, this branch is unreachable: core-guards' Claude `Stop` hooks
|
|
20
|
+
* are a native `afplay` sound + an LLM `type:"prompt"` check, neither of
|
|
21
|
+
* which invokes the harness binary (see `hooks/hooks.json` in claude-plugins
|
|
22
|
+
* vs codex-plugins) — `dispatchLifecycle`'s `case "Stop"` for scope `"core"`
|
|
23
|
+
* only ever receives a real payload from Codex.
|
|
24
|
+
* @param payload - The raw Stop hook payload.
|
|
25
|
+
* @param cwd - Project root (drives the state dir).
|
|
26
|
+
* @param now - Clock.
|
|
27
|
+
* @returns The native hook stdout ("" when the session is clean).
|
|
28
|
+
*/
|
|
29
|
+
export function stopCore(payload: Record<string, unknown>, cwd: string, now: number): string {
|
|
30
|
+
cleanupSession(undefined, now);
|
|
31
|
+
// Turn finished — voice the "stop" sound (fire-and-forget, fail-open: never
|
|
32
|
+
// throws, never blocks; a silent no-op when opted out or no sound resolves).
|
|
33
|
+
notify("stop");
|
|
34
|
+
return validateTaskSolid(payload, homedir(), now, defaultStateDir(cwd), "Stop");
|
|
35
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { loadIndex } from "../../cache/io";
|
|
4
|
+
import { contextResponse } from "../../adapters/claude";
|
|
5
|
+
import { sanitizeSessionId, sessionsDir } from "../home-state";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_TTL_MIN = 30;
|
|
8
|
+
|
|
9
|
+
/** Resolve cache TTL (minutes) from `FUSENGINE_CACHE_TTL_MIN` or default. */
|
|
10
|
+
function ttlMinutes(env: Record<string, string | undefined>): number {
|
|
11
|
+
const raw = (env.FUSENGINE_CACHE_TTL_MIN ?? "").trim();
|
|
12
|
+
const val = Number.parseInt(raw, 10);
|
|
13
|
+
return Number.isFinite(val) && val > 0 ? val : DEFAULT_TTL_MIN;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** True when ISO ts `YYYY-MM-DDTHH:MM:SSZ` is within `ttlMin` minutes of now. */
|
|
17
|
+
function isFresh(ts: string, ttlMin: number, now: number): boolean {
|
|
18
|
+
const parsed = Date.parse(ts);
|
|
19
|
+
if (Number.isNaN(parsed)) return false;
|
|
20
|
+
const ageSec = (now - parsed) / 1000;
|
|
21
|
+
return ageSec >= 0 && ageSec <= ttlMin * 60;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Sanitize + truncate a cell value (replace `|`/newline, ellipsize). */
|
|
25
|
+
function trunc(text: unknown, limit: number): string {
|
|
26
|
+
const t = String(text ?? "").replace(/\|/g, "/").replace(/\n/g, " ");
|
|
27
|
+
return t.length <= limit ? t : t.slice(0, limit - 3) + "...";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Render fresh cache entries as the markdown injection block. */
|
|
31
|
+
function render(entries: Record<string, unknown>[]): string {
|
|
32
|
+
const lines = [
|
|
33
|
+
"# MCP cache available this session",
|
|
34
|
+
"Before calling mcp__context7/exa, check if the result is already cached.",
|
|
35
|
+
"Read the .md file via Read to retrieve the result.",
|
|
36
|
+
"APEX: reading the MCP cache via Read satisfies the research-expert step.",
|
|
37
|
+
"",
|
|
38
|
+
"| Tool | Query | File |",
|
|
39
|
+
"| --- | --- | --- |",
|
|
40
|
+
];
|
|
41
|
+
for (const e of entries) lines.push(`| ${trunc(e.tool, 40)} | ${trunc(e.query, 60)} | ${trunc(e.file, 50)} |`);
|
|
42
|
+
return lines.join("\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Handle SubagentStart: surface fresh MCP cache entries for the session as
|
|
47
|
+
* `additionalContext`. Ports `subagent-start/inject-context-cache.py`.
|
|
48
|
+
* @param sessionIdRaw - Raw session id from the payload.
|
|
49
|
+
* @param home - Home dir (defaults to `~`).
|
|
50
|
+
* @param env - Environment (defaults to `process.env`).
|
|
51
|
+
* @param now - Clock (defaults to `Date.now()`).
|
|
52
|
+
* @returns The native hook stdout (possibly empty).
|
|
53
|
+
*/
|
|
54
|
+
export function subagentCacheContext(sessionIdRaw: unknown, home: string = homedir(), env: Record<string, string | undefined> = process.env, now: number = Date.now()): string {
|
|
55
|
+
const sid = sanitizeSessionId(sessionIdRaw === "" || sessionIdRaw == null ? "unknown" : sessionIdRaw);
|
|
56
|
+
if (!sid) return "";
|
|
57
|
+
const index = loadIndex(join(sessionsDir(home), sid, "context", "index.json")) as Record<string, unknown>[];
|
|
58
|
+
if (index.length === 0) return "";
|
|
59
|
+
const fresh = index.filter((e) => isFresh(String(e.ts ?? ""), ttlMinutes(env), now));
|
|
60
|
+
return fresh.length ? contextResponse("SubagentStart", render(fresh)) : "";
|
|
61
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { basename, extname } from "node:path";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { contextResponse, systemMessage } from "../../adapters/claude";
|
|
5
|
+
import { resolveMaxLines } from "../../config/limits";
|
|
6
|
+
import { resolveTtlSec } from "../../config/ttl";
|
|
7
|
+
import { countLines } from "../../policy/file-size";
|
|
8
|
+
import { loadSessionState, sanitizeSessionId } from "../home-state";
|
|
9
|
+
import { defaultStateDir, trackFile } from "../paths";
|
|
10
|
+
import { freshReceiptFromFile } from "../../tracking/receipts";
|
|
11
|
+
|
|
12
|
+
/** Code-file extensions audited on task completion (mirrors validate-task-solid.py). */
|
|
13
|
+
const CODE_EXTENSIONS = new Set([
|
|
14
|
+
".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java", ".php",
|
|
15
|
+
".cpp", ".c", ".rb", ".swift", ".kt", ".dart", ".vue", ".svelte", ".astro",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
/** Freshness multiple on `FUSE_ENFORCE_TTL_SEC` for receipts (no new env var); a tsc+test run precedes the "done" by more than one edit window. */
|
|
19
|
+
const RECEIPT_TTL_MULTIPLIER = 5;
|
|
20
|
+
|
|
21
|
+
/** Files modified during the session, persisted by {@link trackSessionChanges}. */
|
|
22
|
+
interface Changes {
|
|
23
|
+
modifiedFiles?: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The modified files that are code (by extension) — the receipt gate's trigger set. */
|
|
27
|
+
function codeFiles(files: string[]): string[] {
|
|
28
|
+
return files.filter((fp) => CODE_EXTENSIONS.has(extname(fp)));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Refuse completion when code files changed but no fresh, passing verification
|
|
33
|
+
* receipt (`tsc`/test, exit 0, zero failures, within TTL×{@link RECEIPT_TTL_MULTIPLIER})
|
|
34
|
+
* exists in the signed track. TaskCompleted does NOT honor `decision:"block"`
|
|
35
|
+
* (verified against the official hooks docs — `TeammateIdle/TaskCreated/
|
|
36
|
+
* TaskCompleted` are excluded from that list); the documented stdout refusal is
|
|
37
|
+
* `{"continue":false,"stopReason":…}`, which halts the teammate with the reason
|
|
38
|
+
* shown to the user. Returns that JSON, or `null` when the session is clear.
|
|
39
|
+
*/
|
|
40
|
+
function receiptGate(sid: string, files: string[], now: number, stateDir: string): string | null {
|
|
41
|
+
if (codeFiles(files).length === 0) return null;
|
|
42
|
+
const windowMs = resolveTtlSec(process.env) * 1000 * RECEIPT_TTL_MULTIPLIER;
|
|
43
|
+
if (freshReceiptFromFile(trackFile(sid, stateDir), windowMs, now)) return null;
|
|
44
|
+
const stopReason =
|
|
45
|
+
"VERIFICATION RECEIPT REQUIRED: code files changed but no fresh passing tsc/test receipt " +
|
|
46
|
+
"exists. Run `bun test` + `tsc --noEmit` (exit 0, 0 failures) and re-complete.";
|
|
47
|
+
return JSON.stringify({ continue: false, stopReason });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Re-count physical lines of every modified code file and collect SOLID
|
|
52
|
+
* violations (`<basename>: <n> lines (max <max>)`) for those exceeding `max`.
|
|
53
|
+
* @param files - Candidate modified file paths.
|
|
54
|
+
* @param max - The SOLID line ceiling.
|
|
55
|
+
* @returns The list of violation strings (empty when all files comply).
|
|
56
|
+
*/
|
|
57
|
+
function collectViolations(files: string[], max: number): string[] {
|
|
58
|
+
const violations: string[] = [];
|
|
59
|
+
for (const fp of files) {
|
|
60
|
+
if (!CODE_EXTENSIONS.has(extname(fp)) || !existsSync(fp)) continue;
|
|
61
|
+
try {
|
|
62
|
+
const lines = countLines(readFileSync(fp, "utf-8"));
|
|
63
|
+
if (lines > max) violations.push(`${basename(fp)}: ${lines} lines (max ${max})`);
|
|
64
|
+
} catch { /* unreadable file — skip, matches Python's OSError pass */ }
|
|
65
|
+
}
|
|
66
|
+
return violations;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Handle TaskCompleted (ports `task-completed/validate-task-solid.py` + the
|
|
71
|
+
* receipt gate); also reused verbatim for Codex's `Stop` via {@link stopCore}
|
|
72
|
+
* (Codex never emits `TaskCompleted`) — `event` stamps the real hook name.
|
|
73
|
+
* @param payload - The TaskCompleted/Stop payload (`task_id`, `task_subject`, `session_id`).
|
|
74
|
+
* @param event - The hook event name to stamp on the SOLID-violation response (default `TaskCompleted`).
|
|
75
|
+
* @returns The native hook stdout, or `""` when the session is clean.
|
|
76
|
+
*/
|
|
77
|
+
export function validateTaskSolid(payload: Record<string, unknown>, home: string = homedir(), now: number = Date.now(), stateDir: string = defaultStateDir(process.cwd()), event: string = "TaskCompleted"): string {
|
|
78
|
+
const sid = sanitizeSessionId(payload.session_id ?? "unknown");
|
|
79
|
+
if (!sid) return "";
|
|
80
|
+
const changes = loadSessionState(sid, home).changes as Changes | undefined;
|
|
81
|
+
const files = changes?.modifiedFiles ?? [];
|
|
82
|
+
if (files.length === 0) return "";
|
|
83
|
+
const max = resolveMaxLines();
|
|
84
|
+
const violations = collectViolations(files, max);
|
|
85
|
+
if (violations.length === 0) return receiptGate(sid, files, now, stateDir) ?? "";
|
|
86
|
+
const taskId = String(payload.task_id ?? "");
|
|
87
|
+
const subject = String(payload.task_subject ?? "");
|
|
88
|
+
const msg =
|
|
89
|
+
`SOLID VIOLATION in task '${subject}' (${taskId}): ` +
|
|
90
|
+
`${violations.length} file(s) exceed ${max} lines: ` +
|
|
91
|
+
violations.slice(0, 5).join("; ");
|
|
92
|
+
// TaskCompleted rejects `hookSpecificOutput` (same gap as TeammateIdle) — ride `systemMessage`; `Stop` keeps `additionalContext`, which it DOES accept.
|
|
93
|
+
return event === "TaskCompleted" ? systemMessage(msg) : contextResponse(event, msg);
|
|
94
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module teammate-idle-check
|
|
3
|
+
* TeammateIdle anti-"false done": alongside the existing sniper suggestion
|
|
4
|
+
* ({@link validateTeammateOutput}), verify that the files this teammate ANNOUNCED
|
|
5
|
+
* (session-changes `modifiedFiles`) actually exist on disk. A claimed deliverable
|
|
6
|
+
* missing on disk is a mechanically-verifiable false-done signal → warn the lead.
|
|
7
|
+
* Deduped across the fan-out; silent when nothing is verifiable. Fail-open.
|
|
8
|
+
*
|
|
9
|
+
* Claude-Code-only: no equivalent `TeammateIdle` hook exists on Codex or Hermes,
|
|
10
|
+
* so this handler is never reached through those adapters.
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync } from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { systemMessage } from "../../adapters/claude";
|
|
16
|
+
import { loadSessionState, sanitizeSessionId } from "../home-state";
|
|
17
|
+
import { oncePerWindow } from "../inject-dedup";
|
|
18
|
+
import { defaultStateDir } from "../paths";
|
|
19
|
+
import { notify } from "../notifications";
|
|
20
|
+
import { validateTeammateOutput } from "./teammate-idle";
|
|
21
|
+
|
|
22
|
+
/** Re-warn about the same idle teammate at most once per 30s (fan-out + retries). */
|
|
23
|
+
const IDLE_DEDUP_MS = 30_000;
|
|
24
|
+
|
|
25
|
+
/** The `changes` block written by `track-changes.ts` into unified session state. */
|
|
26
|
+
interface Changes { modifiedFiles?: string[]; }
|
|
27
|
+
|
|
28
|
+
/** Announced files (session changes) that are ABSENT on disk (the false-done set). */
|
|
29
|
+
function missingDeliverables(sessionId: string, home: string): string[] {
|
|
30
|
+
const changes = loadSessionState(sessionId, home).changes as Changes | undefined;
|
|
31
|
+
return (changes?.modifiedFiles ?? []).filter((f) => typeof f === "string" && f !== "" && !existsSync(f));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Handle TeammateIdle: merge the existing sniper suggestion with a missing-
|
|
36
|
+
* deliverable warning (deduped) into one advisory `systemMessage`, or "" when
|
|
37
|
+
* neither fires. TeammateIdle rejects `hookSpecificOutput` (no `additionalContext`
|
|
38
|
+
* channel) — `systemMessage` is the only user-visible, non-blocking channel it
|
|
39
|
+
* accepts.
|
|
40
|
+
* @param data - The raw TeammateIdle payload (`teammate_name`, `session_id`).
|
|
41
|
+
* @param cwd - Project root (state dir for the dedup sidecar).
|
|
42
|
+
* @param home - Home dir (defaults to `~`).
|
|
43
|
+
* @param now - Clock (defaults to `Date.now()`).
|
|
44
|
+
* @returns The native hook stdout, or "".
|
|
45
|
+
*/
|
|
46
|
+
export function teammateIdleContext(data: Record<string, unknown>, cwd: string, home: string = homedir(), now: number = Date.now()): string {
|
|
47
|
+
const sniper = validateTeammateOutput(data, home);
|
|
48
|
+
const sessionId = sanitizeSessionId(data.session_id);
|
|
49
|
+
const teammate = String(data.teammate_name ?? data.team_name ?? "unknown");
|
|
50
|
+
let notice = "";
|
|
51
|
+
if (sessionId) {
|
|
52
|
+
const missing = missingDeliverables(sessionId, home);
|
|
53
|
+
if (missing.length > 0 && oncePerWindow(`idle:${sessionId}:${teammate}`, IDLE_DEDUP_MS, { now, dir: defaultStateDir(cwd) })) {
|
|
54
|
+
notice = `Teammate '${teammate}' idle but expected deliverable(s) not found on disk: ${missing.slice(0, 5).join(", ")} — verify before treating as done.`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const merged = [sniper, notice].filter(Boolean).join("\n\n");
|
|
58
|
+
if (!merged) return "";
|
|
59
|
+
// An actionable idle signal (false-done or sniper suggestion) warrants human
|
|
60
|
+
// attention — voice the "human" sound (fire-and-forget, fail-open no-op).
|
|
61
|
+
notify("human");
|
|
62
|
+
return systemMessage(merged);
|
|
63
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { loadSessionState, sanitizeSessionId } from "../home-state";
|
|
3
|
+
|
|
4
|
+
/** The `changes` block written by `track-changes.ts` into unified session state. */
|
|
5
|
+
interface Changes {
|
|
6
|
+
cumulativeCodeFiles?: number;
|
|
7
|
+
modifiedFiles?: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Handle TeammateIdle: when the teammate's session-changes file shows code was
|
|
12
|
+
* modified, suggest sniper validation as plain text — TeammateIdle has no
|
|
13
|
+
* `additionalContext` channel (rejects `hookSpecificOutput`), so the caller
|
|
14
|
+
* ({@link module:teammate-idle-check.teammateIdleContext}) rides this text on
|
|
15
|
+
* the `systemMessage` channel instead. Ports `teammate-idle/validate-teammate-output.py`.
|
|
16
|
+
* @param data - The raw hook payload.
|
|
17
|
+
* @param home - Home dir (defaults to `~`).
|
|
18
|
+
* @returns The plain notice text, or "" when no code file was modified.
|
|
19
|
+
*/
|
|
20
|
+
export function validateTeammateOutput(data: Record<string, unknown>, home: string = homedir()): string {
|
|
21
|
+
const teammate = String(data.teammate_name ?? "unknown");
|
|
22
|
+
const sessionId = sanitizeSessionId(data.session_id) ?? "unknown";
|
|
23
|
+
const changes = loadSessionState(sessionId, home).changes as Changes | undefined;
|
|
24
|
+
const count = changes?.cumulativeCodeFiles ?? 0;
|
|
25
|
+
if (count === 0) return "";
|
|
26
|
+
const files = (changes?.modifiedFiles ?? []).slice(0, 5).join(", ");
|
|
27
|
+
return `Teammate '${teammate}' going idle after modifying ${count} code file(s): ${files}. Consider running sniper validation.`;
|
|
28
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Handle PostToolUseFailure: append a `TOOL_FAILURE` line to
|
|
7
|
+
* `~/.claude/logs/tool-failures.log`, skipping user interrupts. Ports
|
|
8
|
+
* `post-tool-use/log-tool-failure.py`. No stdout (logging only).
|
|
9
|
+
* @param data - The raw hook payload.
|
|
10
|
+
* @param home - Home dir (defaults to `~`).
|
|
11
|
+
* @param now - Clock (defaults to `Date.now()`).
|
|
12
|
+
*/
|
|
13
|
+
export function logToolFailure(data: Record<string, unknown>, home: string = homedir(), now: number = Date.now()): void {
|
|
14
|
+
if (data.is_interrupt === true) return;
|
|
15
|
+
const tool = String(data.tool_name ?? "unknown");
|
|
16
|
+
const error = String(data.error ?? "unknown error");
|
|
17
|
+
const sessionId = String(data.session_id ?? "unknown");
|
|
18
|
+
const ts = new Date(now).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
19
|
+
const dir = join(home, ".claude", "logs");
|
|
20
|
+
try {
|
|
21
|
+
mkdirSync(dir, { recursive: true });
|
|
22
|
+
appendFileSync(join(dir, "tool-failures.log"), `[${ts}] TOOL_FAILURE session=${sessionId} tool=${tool} error=${error}\n`, "utf-8");
|
|
23
|
+
} catch { /* best effort */ }
|
|
24
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { attachSystemMessage, contextResponse } from "../../adapters/claude";
|
|
4
|
+
import { loadSessionState, sanitizeSessionId, saveSessionState, sessionsDir } from "../home-state";
|
|
5
|
+
import { onceExclusive } from "../inject-dedup";
|
|
6
|
+
import { BURST_DEDUP_MS } from "../burst-window";
|
|
7
|
+
import { sniperRequiredNotice } from "../notices";
|
|
8
|
+
|
|
9
|
+
/** Code-file extensions tracked for sniper (mirrors track-session-changes.py). */
|
|
10
|
+
const CODE_EXT = /\.(ts|tsx|js|jsx|py|go|rs|java|php|cpp|c|rb|swift|kt|vue|svelte|astro)$/;
|
|
11
|
+
|
|
12
|
+
/** Shape of the `changes` block persisted in per-session state. */
|
|
13
|
+
interface Changes {
|
|
14
|
+
cumulativeCodeFiles: number;
|
|
15
|
+
modifiedFiles: string[];
|
|
16
|
+
lastModifiedFile?: string;
|
|
17
|
+
lastCheck?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Handle PostToolUse Write/Edit: track the cumulative set of modified code
|
|
22
|
+
* files per session and emit the mandatory "SNIPER VALIDATION REQUIRED"
|
|
23
|
+
* additionalContext. Ports `post-tool-use/track-session-changes.py`.
|
|
24
|
+
* @param sessionIdRaw - Raw session id from the payload.
|
|
25
|
+
* @param filePath - The edited file path.
|
|
26
|
+
* @param home - Home dir (defaults to `~`).
|
|
27
|
+
* @param now - Clock (defaults to `Date.now()`).
|
|
28
|
+
* @returns The native hook stdout (possibly empty when not a code file).
|
|
29
|
+
*/
|
|
30
|
+
export function trackSessionChanges(sessionIdRaw: unknown, filePath: string, home: string = homedir(), now: number = Date.now()): string {
|
|
31
|
+
if (!filePath || !CODE_EXT.test(filePath)) return "";
|
|
32
|
+
const sid = sanitizeSessionId(sessionIdRaw) ?? "unknown";
|
|
33
|
+
const state = loadSessionState(sid, home);
|
|
34
|
+
const prev = (state.changes as Changes | undefined) ?? { cumulativeCodeFiles: 0, modifiedFiles: [] };
|
|
35
|
+
const files = [...prev.modifiedFiles];
|
|
36
|
+
let count = prev.cumulativeCodeFiles;
|
|
37
|
+
if (!files.includes(filePath)) { count += 1; files.push(filePath); }
|
|
38
|
+
state.changes = {
|
|
39
|
+
cumulativeCodeFiles: count,
|
|
40
|
+
modifiedFiles: files,
|
|
41
|
+
lastModifiedFile: filePath,
|
|
42
|
+
lastCheck: new Date(now).toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
43
|
+
} satisfies Changes;
|
|
44
|
+
saveSessionState(sid, state, home);
|
|
45
|
+
// Emit the mandatory reminder ONCE per (file+session) burst: the ~11 sibling
|
|
46
|
+
// plugin hooks of one PostToolUse event would otherwise inject it ~11× (token
|
|
47
|
+
// noise). A real re-edit past the window reminds again. See burst-window.
|
|
48
|
+
// Exclusive mode (not the JSON oncePerWindow): this call is exactly the
|
|
49
|
+
// high-concurrency shape that mode is best-effort against under the real
|
|
50
|
+
// ~11-process fan-out (lesson 2026-07-05 16:00).
|
|
51
|
+
if (!onceExclusive(`sniper:${sid}:${filePath}`, BURST_DEDUP_MS, { now, dir: sessionsDir(home) })) return "";
|
|
52
|
+
const fname = basename(filePath);
|
|
53
|
+
const stdout = contextResponse(
|
|
54
|
+
"PostToolUse",
|
|
55
|
+
`SNIPER VALIDATION REQUIRED: Code file '${fname}' was modified. You MUST now run the sniper agent (fuse-ai-pilot:sniper) to validate this modification before continuing. This is mandatory per CLAUDE.md rules.`,
|
|
56
|
+
);
|
|
57
|
+
// User-visible companion to the additionalContext reminder above (which only
|
|
58
|
+
// reaches the agent, never the human) — rides the SAME oncePerWindow gate above,
|
|
59
|
+
// so the ×11 hook fan-out never duplicates it.
|
|
60
|
+
return attachSystemMessage(stdout, sniperRequiredNotice(fname));
|
|
61
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PreToolUse (matcher "Write|Edit") for the solid scope: deny a Go top-level
|
|
3
|
+
* interface or a Python `ABC` subclass defined outside its language's
|
|
4
|
+
* `/interfaces/` directory. Ports `solid/scripts/validate-solid.py`'s
|
|
5
|
+
* `check_go`/`check_python` only — its `check_nextjs`/`check_laravel`/
|
|
6
|
+
* `check_swift` siblings are NOT ported here: they are a strict subset of the
|
|
7
|
+
* already-ported `framework-solid-gates.ts` (react/nextjs/laravel/swift),
|
|
8
|
+
* which fire unconditionally on file extension/content rather than being
|
|
9
|
+
* gated behind `SOLID_PROJECT_TYPE`. Inert when `SOLID_PROJECT_TYPE` is
|
|
10
|
+
* absent/"unknown"/nextjs/laravel/swift/rust (parity: the Python hook only
|
|
11
|
+
* has a `go`/`python` validator).
|
|
12
|
+
*
|
|
13
|
+
* Detection rides the conventions module (MASKED content, column-0 anchored
|
|
14
|
+
* for Go): the old naive regexes matched inside comments/strings and
|
|
15
|
+
* indented locals — all killed. LEGACY-level detections keep the exact hard
|
|
16
|
+
* deny (byte-parity); EXTENDED-level ones (Go `interface{` without the
|
|
17
|
+
* space, Python `Protocol`) follow `FUSE_CONVENTIONS_MODE` — advisory
|
|
18
|
+
* (non-blocking additionalContext) by default, deny only on `"deny"`
|
|
19
|
+
* (owner Amendment 5, external audit D0.2).
|
|
20
|
+
*/
|
|
21
|
+
import { contextResponse, denyResponse } from "../../adapters/claude";
|
|
22
|
+
import { interfaceDeclLevel } from "../../policy/conventions/interfaces";
|
|
23
|
+
import { conventionsMode } from "../../policy/conventions/verdict";
|
|
24
|
+
|
|
25
|
+
/** Go: deny a top-level interface declared outside `/interfaces/`. */
|
|
26
|
+
function checkGo(filePath: string, content: string, env: Record<string, string | undefined>): string | null {
|
|
27
|
+
if (!filePath.endsWith(".go") || filePath.includes("/interfaces/")) return null;
|
|
28
|
+
const level = interfaceDeclLevel(filePath, content);
|
|
29
|
+
if (level === null) return null;
|
|
30
|
+
const msg = "SOLID: Interfaces must be in internal/interfaces/";
|
|
31
|
+
if (level === "legacy" || conventionsMode(env) === "deny") return denyResponse("PreToolUse", msg);
|
|
32
|
+
return contextResponse("PreToolUse", msg);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Python: deny an ABC subclass declared outside `/interfaces/`. */
|
|
36
|
+
function checkPython(filePath: string, content: string, env: Record<string, string | undefined>): string | null {
|
|
37
|
+
if (!filePath.endsWith(".py") || filePath.includes("/interfaces/")) return null;
|
|
38
|
+
const level = interfaceDeclLevel(filePath, content);
|
|
39
|
+
if (level === null) return null;
|
|
40
|
+
const msg = "SOLID: Abstract classes must be in src/interfaces/";
|
|
41
|
+
if (level === "legacy" || conventionsMode(env) === "deny") return denyResponse("PreToolUse", msg);
|
|
42
|
+
return contextResponse("PreToolUse", msg);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** `SOLID_PROJECT_TYPE` values this gate validates (parity subset — see module doc). */
|
|
46
|
+
const VALIDATORS: Record<string, (filePath: string, content: string, env: Record<string, string | undefined>) => string | null> = {
|
|
47
|
+
go: checkGo,
|
|
48
|
+
python: checkPython,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Deny (or advise on) a Write/Edit that violates the Go/Python interface-location SOLID rule.
|
|
53
|
+
* @param tool - The tool name (only "Write"/"Edit" are checked).
|
|
54
|
+
* @param filePath - The written file's absolute path.
|
|
55
|
+
* @param content - The written/edited content (`new_string` snippet on Edit).
|
|
56
|
+
* @param env - Environment (defaults to `process.env`).
|
|
57
|
+
* @returns The PreToolUse response, or `""` when clean/inert.
|
|
58
|
+
*/
|
|
59
|
+
export function validateSolidGate(
|
|
60
|
+
tool: string,
|
|
61
|
+
filePath: string,
|
|
62
|
+
content: string,
|
|
63
|
+
env: Record<string, string | undefined> = process.env,
|
|
64
|
+
): string {
|
|
65
|
+
const ptype = env.SOLID_PROJECT_TYPE ?? "";
|
|
66
|
+
if (!ptype || ptype === "unknown") return "";
|
|
67
|
+
if (tool !== "Write" && tool !== "Edit") return "";
|
|
68
|
+
if (!filePath) return "";
|
|
69
|
+
return VALIDATORS[ptype]?.(filePath, content, env) ?? "";
|
|
70
|
+
}
|