@webpresso/agent-kit 3.1.28 → 3.2.0
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.
Potentially problematic release.
This version of @webpresso/agent-kit might be problematic. Click here for more details.
- package/README.md +53 -15
- package/bin/_run.js +179 -19
- package/catalog/AGENTS.md.tpl +6 -6
- package/catalog/agent/agents/implementer.md +45 -0
- package/catalog/agent/rules/ci-cost-local-first.md +139 -0
- package/catalog/agent/rules/ci-test-perf.md +5 -1
- package/catalog/agent/rules/cmd-execution.md +9 -0
- package/catalog/agent/rules/parallel-orchestration-quota.md +100 -0
- package/catalog/agent/rules/pre-implementation.md +30 -2
- package/catalog/agent/rules/supported-agent-clis.md +9 -0
- package/catalog/agent/rules/test-scan-perf.md +80 -0
- package/catalog/agent/rules/typescript-version.md +50 -0
- package/catalog/agent/rules/workflow-skills-routing.md +4 -2
- package/catalog/agent/skills/codex/SKILL.md +25 -1
- package/catalog/agent/skills/fix/SKILL.md +3 -0
- package/catalog/agent/skills/verify/SKILL.md +22 -2
- package/catalog/docs/templates/blueprint.md +1 -1
- package/dist/esm/audit/agents.js +1 -0
- package/dist/esm/audit/blueprint-lifecycle-sql.d.ts +12 -0
- package/dist/esm/audit/blueprint-lifecycle-sql.js +23 -2
- package/dist/esm/audit/blueprint-pr-coverage.js +72 -7
- package/dist/esm/audit/ci-guardrails-wiring.d.ts +22 -0
- package/dist/esm/audit/ci-guardrails-wiring.js +90 -0
- package/dist/esm/audit/ci-test-perf.js +30 -0
- package/dist/esm/audit/command-surface.js +4 -2
- package/dist/esm/audit/host-substitution-risk.d.ts +2 -0
- package/dist/esm/audit/host-substitution-risk.js +256 -0
- package/dist/esm/audit/registry.d.ts +4 -4
- package/dist/esm/audit/registry.js +11 -1
- package/dist/esm/audit/roadmap-links.js +6 -0
- package/dist/esm/audit/test-scan-perf.d.ts +18 -0
- package/dist/esm/audit/test-scan-perf.js +275 -0
- package/dist/esm/audit/toolchain-isolation.js +1 -2
- package/dist/esm/audit/typescript-version.d.ts +2 -0
- package/dist/esm/audit/typescript-version.js +123 -0
- package/dist/esm/blueprint/core/parser.js +43 -1
- package/dist/esm/blueprint/core/validation/criteria.d.ts +14 -0
- package/dist/esm/blueprint/core/validation/criteria.js +41 -2
- package/dist/esm/blueprint/core/validation/state.js +25 -14
- package/dist/esm/blueprint/db/migrations/run.js +4 -2
- package/dist/esm/blueprint/lifecycle/audit.d.ts +20 -0
- package/dist/esm/blueprint/lifecycle/audit.js +65 -10
- package/dist/esm/blueprint/lifecycle/canonicalize.d.ts +6 -0
- package/dist/esm/blueprint/lifecycle/canonicalize.js +135 -0
- package/dist/esm/blueprint/lifecycle/engine.d.ts +9 -0
- package/dist/esm/blueprint/lifecycle/engine.js +53 -6
- package/dist/esm/blueprint/lifecycle/local.d.ts +2 -1
- package/dist/esm/blueprint/lifecycle/local.js +19 -3
- package/dist/esm/blueprint/lifecycle/review-provenance.d.ts +6 -0
- package/dist/esm/blueprint/lifecycle/review-provenance.js +118 -12
- package/dist/esm/blueprint/markdown/helpers.js +6 -2
- package/dist/esm/blueprint/service/BlueprintCreationService.d.ts +1 -0
- package/dist/esm/blueprint/service/BlueprintCreationService.js +53 -4
- package/dist/esm/blueprint/service/blueprint-tech-debt-links.js +16 -2
- package/dist/esm/blueprint/trust/command-runner.d.ts +26 -0
- package/dist/esm/blueprint/trust/command-runner.js +148 -0
- package/dist/esm/blueprint/trust/gates.d.ts +7 -0
- package/dist/esm/blueprint/trust/gates.js +20 -0
- package/dist/esm/blueprint/trust/promotion.d.ts +8 -0
- package/dist/esm/blueprint/trust/promotion.js +62 -112
- package/dist/esm/blueprint/trust/scaffold.d.ts +19 -0
- package/dist/esm/blueprint/trust/scaffold.js +64 -0
- package/dist/esm/blueprint/trust/validator.d.ts +1 -0
- package/dist/esm/blueprint/trust/validator.js +7 -1
- package/dist/esm/blueprint/utils/archive.js +27 -5
- package/dist/esm/blueprint/utils/document-paths.d.ts +18 -0
- package/dist/esm/blueprint/utils/document-paths.js +45 -12
- package/dist/esm/build/cli-mcp-parity.js +10 -0
- package/dist/esm/ci/act-replay.js +110 -7
- package/dist/esm/ci/act-runner.d.ts +4 -0
- package/dist/esm/ci/act-runner.js +85 -26
- package/dist/esm/ci/act-worktree-git.d.ts +11 -0
- package/dist/esm/ci/act-worktree-git.js +219 -0
- package/dist/esm/cli/bundle/commands/blueprint.d.ts +1 -0
- package/dist/esm/cli/bundle/commands/blueprint.js +1 -0
- package/dist/esm/cli/cli.d.ts +1 -1
- package/dist/esm/cli/cli.js +34 -0
- package/dist/esm/cli/commands/agent-launch.d.ts +6 -1
- package/dist/esm/cli/commands/agent-launch.js +82 -4
- package/dist/esm/cli/commands/audit-core.d.ts +1 -1
- package/dist/esm/cli/commands/audit-core.js +8 -1
- package/dist/esm/cli/commands/audit.js +20 -5
- package/dist/esm/cli/commands/blueprint/execution.js +18 -6
- package/dist/esm/cli/commands/blueprint/mutations.js +83 -47
- package/dist/esm/cli/commands/blueprint/router-dispatch.d.ts +2 -1
- package/dist/esm/cli/commands/blueprint/router-dispatch.js +20 -1
- package/dist/esm/cli/commands/blueprint/router-output.d.ts +21 -0
- package/dist/esm/cli/commands/blueprint/router-output.js +48 -17
- package/dist/esm/cli/commands/blueprint/router.d.ts +7 -0
- package/dist/esm/cli/commands/blueprint/router.js +40 -3
- package/dist/esm/cli/commands/ci-preflight.d.ts +13 -2
- package/dist/esm/cli/commands/ci-preflight.js +6 -3
- package/dist/esm/cli/commands/ci.d.ts +8 -1
- package/dist/esm/cli/commands/ci.js +80 -21
- package/dist/esm/cli/commands/compile.js +5 -31
- package/dist/esm/cli/commands/dash/delivery-status.d.ts +1 -0
- package/dist/esm/cli/commands/dash/delivery-status.js +11 -4
- package/dist/esm/cli/commands/dash/keymap.js +1 -0
- package/dist/esm/cli/commands/dash/launch-preferences.d.ts +15 -0
- package/dist/esm/cli/commands/dash/launch-preferences.js +46 -17
- package/dist/esm/cli/commands/dash/plan-once-label.d.ts +7 -0
- package/dist/esm/cli/commands/dash/plan-once-label.js +5 -0
- package/dist/esm/cli/commands/dash/plan-once-membership.d.ts +67 -0
- package/dist/esm/cli/commands/dash/plan-once-membership.js +151 -0
- package/dist/esm/cli/commands/dash/plan-once-merge.d.ts +143 -0
- package/dist/esm/cli/commands/dash/plan-once-merge.js +238 -0
- package/dist/esm/cli/commands/dash/plan-once.d.ts +22 -2
- package/dist/esm/cli/commands/dash/plan-once.js +41 -4
- package/dist/esm/cli/commands/dash/tui/attention-hud.d.ts +9 -0
- package/dist/esm/cli/commands/dash/tui/attention-hud.js +9 -0
- package/dist/esm/cli/commands/dash/tui/chat-bridge.d.ts +41 -0
- package/dist/esm/cli/commands/dash/tui/chat-bridge.js +73 -0
- package/dist/esm/cli/commands/dash/tui/chat-mode.d.ts +47 -0
- package/dist/esm/cli/commands/dash/tui/chat-mode.js +98 -0
- package/dist/esm/cli/commands/dash/tui/frame.d.ts +3 -0
- package/dist/esm/cli/commands/dash/tui/frame.js +3 -1
- package/dist/esm/cli/commands/dash/tui/palette.d.ts +1 -1
- package/dist/esm/cli/commands/dash/tui/palette.js +14 -0
- package/dist/esm/cli/commands/dash/tui/runtime.js +239 -4
- package/dist/esm/cli/commands/dash/tui/state.d.ts +41 -0
- package/dist/esm/cli/commands/dash/tui/state.js +257 -4
- package/dist/esm/cli/commands/doctor.js +5 -0
- package/dist/esm/cli/commands/format.js +0 -2
- package/dist/esm/cli/commands/hook.d.ts +5 -0
- package/dist/esm/cli/commands/hook.js +15 -9
- package/dist/esm/cli/commands/init/config.d.ts +0 -1
- package/dist/esm/cli/commands/init/config.js +3 -6
- package/dist/esm/cli/commands/init/convergence-apply.d.ts +24 -0
- package/dist/esm/cli/commands/init/convergence-apply.js +221 -0
- package/dist/esm/cli/commands/init/convergence-state.d.ts +13 -0
- package/dist/esm/cli/commands/init/convergence-state.js +54 -0
- package/dist/esm/cli/commands/init/convergence.d.ts +23 -0
- package/dist/esm/cli/commands/init/convergence.js +57 -0
- package/dist/esm/cli/commands/init/git-convergence.d.ts +19 -0
- package/dist/esm/cli/commands/init/git-convergence.js +168 -0
- package/dist/esm/cli/commands/init/gitignore-patcher.js +2 -0
- package/dist/esm/cli/commands/init/grok-privacy-smoke.d.ts +52 -0
- package/dist/esm/cli/commands/init/grok-privacy-smoke.js +91 -0
- package/dist/esm/cli/commands/init/host-native-proof.d.ts +18 -0
- package/dist/esm/cli/commands/init/host-native-proof.js +88 -0
- package/dist/esm/cli/commands/init/host-visibility.d.ts +2 -2
- package/dist/esm/cli/commands/init/host-visibility.js +94 -5
- package/dist/esm/cli/commands/init/index.d.ts +1 -0
- package/dist/esm/cli/commands/init/index.js +326 -173
- package/dist/esm/cli/commands/init/mcp-spec.d.ts +57 -0
- package/dist/esm/cli/commands/init/mcp-spec.js +273 -0
- package/dist/esm/cli/commands/init/merge.d.ts +1 -0
- package/dist/esm/cli/commands/init/merge.js +30 -1
- package/dist/esm/cli/commands/init/plugin-cache-prune.d.ts +0 -3
- package/dist/esm/cli/commands/init/plugin-cache-prune.js +0 -1
- package/dist/esm/cli/commands/init/scaffold-agents-md.d.ts +1 -1
- package/dist/esm/cli/commands/init/scaffold-agents-md.js +9 -3
- package/dist/esm/cli/commands/init/scaffold-base-kit.d.ts +11 -0
- package/dist/esm/cli/commands/init/scaffold-base-kit.js +56 -1
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.d.ts +5 -6
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.js +6 -13
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/grok.d.ts +14 -8
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/grok.js +41 -24
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.d.ts +2 -0
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.js +51 -23
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.d.ts +0 -2
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.js +0 -2
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.d.ts +6 -3
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.js +39 -26
- package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.d.ts +12 -31
- package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +297 -173
- package/dist/esm/cli/commands/init/scaffolders/file-transaction.d.ts +7 -0
- package/dist/esm/cli/commands/init/scaffolders/file-transaction.js +58 -0
- package/dist/esm/cli/commands/init/scaffolders/grok-mcp/index.d.ts +38 -0
- package/dist/esm/cli/commands/init/scaffolders/grok-mcp/index.js +73 -0
- package/dist/esm/cli/commands/init/scaffolders/opencode-plugin/index.d.ts +21 -0
- package/dist/esm/cli/commands/init/scaffolders/opencode-plugin/index.js +559 -63
- package/dist/esm/cli/commands/init/untracked-collision.d.ts +14 -0
- package/dist/esm/cli/commands/init/untracked-collision.js +42 -0
- package/dist/esm/cli/commands/logs.d.ts +15 -0
- package/dist/esm/cli/commands/logs.js +192 -3
- package/dist/esm/cli/commands/opencode-rotate.d.ts +49 -0
- package/dist/esm/cli/commands/opencode-rotate.js +135 -0
- package/dist/esm/cli/commands/public-readiness.d.ts +11 -0
- package/dist/esm/cli/commands/public-readiness.js +42 -0
- package/dist/esm/cli/commands/quality-log-store.d.ts +1 -0
- package/dist/esm/cli/commands/quality-log-store.js +187 -51
- package/dist/esm/cli/commands/review.d.ts +35 -3
- package/dist/esm/cli/commands/review.js +734 -217
- package/dist/esm/cli/commands/run-wait.d.ts +2 -0
- package/dist/esm/cli/commands/run-wait.js +122 -0
- package/dist/esm/cli/commands/secrets.d.ts +1 -0
- package/dist/esm/cli/commands/secrets.js +4 -1
- package/dist/esm/cli/commands/setup-help.js +1 -1
- package/dist/esm/cli/commands/setup-repair-help.js +1 -1
- package/dist/esm/cli/commands/worktree/router-dispatch.js +13 -8
- package/dist/esm/cli/direct-provider-launch.js +31 -3
- package/dist/esm/cli/optional-tool-freshness.d.ts +27 -0
- package/dist/esm/cli/optional-tool-freshness.js +219 -9
- package/dist/esm/daemon/domains/git-actions.d.ts +29 -0
- package/dist/esm/daemon/domains/git-actions.js +136 -0
- package/dist/esm/daemon/domains/run-registry-interop.d.ts +16 -0
- package/dist/esm/daemon/domains/run-registry-interop.js +45 -0
- package/dist/esm/daemon/domains/sessions-search.d.ts +11 -0
- package/dist/esm/daemon/domains/sessions-search.js +29 -0
- package/dist/esm/daemon/handlers/blueprints-handlers.d.ts +5 -0
- package/dist/esm/daemon/handlers/blueprints-handlers.js +36 -0
- package/dist/esm/daemon/handlers/deploy-handlers.d.ts +6 -0
- package/dist/esm/daemon/handlers/deploy-handlers.js +5 -0
- package/dist/esm/daemon/handlers/fleet-handlers.d.ts +15 -0
- package/dist/esm/daemon/handlers/fleet-handlers.js +91 -0
- package/dist/esm/daemon/handlers/git-handlers.d.ts +6 -0
- package/dist/esm/daemon/handlers/git-handlers.js +27 -0
- package/dist/esm/daemon/handlers/product.d.ts +7 -0
- package/dist/esm/daemon/handlers/product.js +10 -119
- package/dist/esm/daemon/handlers/sessions-handlers.d.ts +12 -0
- package/dist/esm/daemon/handlers/sessions-handlers.js +31 -0
- package/dist/esm/daemon/handlers/shared.d.ts +5 -0
- package/dist/esm/daemon/handlers/shared.js +16 -0
- package/dist/esm/daemon/methods.d.ts +8 -1
- package/dist/esm/daemon/methods.js +7 -0
- package/dist/esm/daemon/protocol/contract.d.ts +43 -0
- package/dist/esm/daemon/protocol/contract.js +26 -0
- package/dist/esm/daemon/protocol/index.d.ts +1 -1
- package/dist/esm/daemon/protocol/index.js +1 -1
- package/dist/esm/hooks/__conformance__/matrix.d.ts +6 -0
- package/dist/esm/hooks/__conformance__/matrix.js +137 -0
- package/dist/esm/hooks/conformance/batch.js +5 -0
- package/dist/esm/hooks/doctor.d.ts +15 -0
- package/dist/esm/hooks/doctor.js +79 -8
- package/dist/esm/hooks/permission-request/index.js +9 -3
- package/dist/esm/hooks/permission-request/policy.d.ts +9 -0
- package/dist/esm/hooks/permission-request/policy.js +13 -0
- package/dist/esm/hooks/pretool-guard/dev-routing.d.ts +9 -0
- package/dist/esm/hooks/pretool-guard/dev-routing.js +41 -5
- package/dist/esm/hooks/pretool-guard/logger.d.ts +7 -0
- package/dist/esm/hooks/pretool-guard/logger.js +16 -0
- package/dist/esm/hooks/pretool-guard/runner.d.ts +6 -0
- package/dist/esm/hooks/pretool-guard/runner.js +68 -16
- package/dist/esm/hooks/pretool-guard/validators/blueprint.js +30 -10
- package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +58 -4
- package/dist/esm/hooks/pretool-guard/validators/index.d.ts +7 -0
- package/dist/esm/hooks/pretool-guard/validators/index.js +21 -0
- package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.d.ts +0 -1
- package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.js +8 -5
- package/dist/esm/hooks/pretool-guard/validators/path-contract.d.ts +19 -0
- package/dist/esm/hooks/pretool-guard/validators/path-contract.js +25 -8
- package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.d.ts +9 -7
- package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.js +126 -14
- package/dist/esm/hooks/shared/guard-outcome.d.ts +149 -0
- package/dist/esm/hooks/shared/guard-outcome.js +215 -0
- package/dist/esm/hooks/shared/hook-bootstrap.d.ts +26 -0
- package/dist/esm/hooks/shared/hook-bootstrap.js +64 -1
- package/dist/esm/hooks/shared/types.d.ts +8 -5
- package/dist/esm/hooks/shared/types.js +23 -8
- package/dist/esm/hooks/stop/qa-changed-files.d.ts +11 -0
- package/dist/esm/hooks/stop/qa-changed-files.js +74 -0
- package/dist/esm/mcp/blueprint/_shared/lifecycle.js +14 -1
- package/dist/esm/mcp/blueprint/handlers/document-mutations.js +78 -4
- package/dist/esm/mcp/blueprint/handlers/review-log.js +8 -1
- package/dist/esm/mcp/blueprint/handlers/task-advance.js +113 -54
- package/dist/esm/mcp/blueprint/handlers/task-verify.js +39 -19
- package/dist/esm/mcp/blueprint/registration.js +3 -2
- package/dist/esm/mcp/server.d.ts +1 -0
- package/dist/esm/mcp/server.js +27 -0
- package/dist/esm/mcp/tools/_names.d.ts +1 -1
- package/dist/esm/mcp/tools/_names.js +5 -0
- package/dist/esm/mcp/tools/_registry.js +10 -0
- package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
- package/dist/esm/mcp/tools/_shared/result.js +44 -1
- package/dist/esm/mcp/tools/audit.d.ts +5 -1
- package/dist/esm/mcp/tools/audit.js +40 -0
- package/dist/esm/mcp/tools/audits.d.ts +4 -0
- package/dist/esm/mcp/tools/ci-act.js +9 -11
- package/dist/esm/mcp/tools/ci-preflight.d.ts +50 -0
- package/dist/esm/mcp/tools/ci-preflight.js +183 -0
- package/dist/esm/mcp/tools/pr-upsert.js +39 -1
- package/dist/esm/mcp/tools/review-run.d.ts +28 -0
- package/dist/esm/mcp/tools/review-run.js +216 -0
- package/dist/esm/mcp/tools/run-wait.d.ts +86 -0
- package/dist/esm/mcp/tools/run-wait.js +593 -0
- package/dist/esm/mcp/tools/session-id.d.ts +3 -0
- package/dist/esm/mcp/tools/session-id.js +45 -0
- package/dist/esm/mcp/tools/session-info.d.ts +3 -0
- package/dist/esm/mcp/tools/session-info.js +49 -0
- package/dist/esm/output-transforms/audit.d.ts +23 -0
- package/dist/esm/output-transforms/audit.js +47 -0
- package/dist/esm/output-transforms/index.js +2 -0
- package/dist/esm/paths/state-root.js +3 -0
- package/dist/esm/review/authority.js +30 -7
- package/dist/esm/review/availability.d.ts +22 -0
- package/dist/esm/review/availability.js +123 -23
- package/dist/esm/review/delivery-verifier.d.ts +27 -0
- package/dist/esm/review/delivery-verifier.js +115 -0
- package/dist/esm/review/events.d.ts +3 -2
- package/dist/esm/review/events.js +23 -3
- package/dist/esm/review/execution/adapters.js +26 -9
- package/dist/esm/review/execution/output-policy.d.ts +2 -0
- package/dist/esm/review/execution/output-policy.js +12 -0
- package/dist/esm/review/execution/review-checkout.js +87 -6
- package/dist/esm/review/execution/supervisor.js +5 -0
- package/dist/esm/review/execution/types.d.ts +8 -0
- package/dist/esm/review/opencode-account.d.ts +24 -0
- package/dist/esm/review/opencode-account.js +68 -0
- package/dist/esm/review/opencode-auth.d.ts +33 -0
- package/dist/esm/review/opencode-auth.js +79 -0
- package/dist/esm/review/subject.d.ts +27 -0
- package/dist/esm/review/subject.js +179 -32
- package/dist/esm/runtime/executor.d.ts +1 -0
- package/dist/esm/runtime/executor.js +2 -1
- package/dist/esm/runtime/opencode-account-materializer.d.ts +34 -0
- package/dist/esm/runtime/opencode-account-materializer.js +33 -0
- package/dist/esm/secret-gate/runner.d.ts +1 -0
- package/dist/esm/secret-gate/runner.js +4 -1
- package/dist/esm/status/snapshot.js +30 -19
- package/dist/esm/test/duration-sequencer.d.ts +20 -0
- package/dist/esm/test/duration-sequencer.js +54 -0
- package/dist/esm/test/shard-durations.json +922 -0
- package/dist/esm/test/worker-budget.d.ts +6 -0
- package/dist/esm/test/worker-budget.js +59 -1
- package/dist/esm/test-helpers/global-setup.d.ts +1 -1
- package/dist/esm/test-helpers/global-setup.js +68 -24
- package/dist/esm/ultragoal/service.d.ts +7 -0
- package/dist/esm/ultragoal/service.js +42 -2
- package/dist/esm/utils/write-json-file.js +3 -1
- package/dist/esm/worktrees/codex-project-trust.d.ts +15 -0
- package/dist/esm/worktrees/codex-project-trust.js +56 -0
- package/dist/esm/worktrees/current.d.ts +1 -0
- package/dist/esm/worktrees/current.js +1 -0
- package/dist/esm/worktrees/project-agent-surfaces.d.ts +1 -0
- package/dist/esm/worktrees/project-agent-surfaces.js +2 -1
- package/dist/esm/worktrees/registry.d.ts +8 -0
- package/dist/esm/worktrees/registry.js +18 -3
- package/package.json +18 -15
|
@@ -110,6 +110,96 @@ function denyDecision(obj) {
|
|
|
110
110
|
return "deny";
|
|
111
111
|
return null;
|
|
112
112
|
}
|
|
113
|
+
/** Extract the host-displayed deny reason from either envelope shape. */
|
|
114
|
+
function denyReasonText(obj) {
|
|
115
|
+
const hookSpecific = obj?.hookSpecificOutput;
|
|
116
|
+
if (hookSpecific && typeof hookSpecific === "object") {
|
|
117
|
+
const reason = hookSpecific.permissionDecisionReason;
|
|
118
|
+
if (typeof reason === "string")
|
|
119
|
+
return reason;
|
|
120
|
+
}
|
|
121
|
+
// Grok dual envelope carries a top-level reason.
|
|
122
|
+
if (typeof obj?.reason === "string")
|
|
123
|
+
return obj.reason;
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const GUARD_CODED_REASON_RE = /^GUARD_[A-Z0-9_]+: /u;
|
|
127
|
+
/**
|
|
128
|
+
* Every PreToolUse deny reason on a parseable stdout envelope carries a stable
|
|
129
|
+
* `GUARD_<CODE>: ` prefix within the 80-char permissionDecisionReason budget.
|
|
130
|
+
* The code is the cross-host contract; the prose is truncated to fit.
|
|
131
|
+
*
|
|
132
|
+
* This reads `result.stdout` directly (NOT the decision-path `obj`, which is
|
|
133
|
+
* null for a Claude exit-2 deny) so the coded reason is asserted on BOTH hosts —
|
|
134
|
+
* pretool-guard always prints the deny envelope to stdout (dual for claude/grok,
|
|
135
|
+
* hookSpecificOutput-only for codex) even when Claude reads the exit code for the
|
|
136
|
+
* decision itself. Non-JSON stdout (the synthetic "stdout is ignored" fixture)
|
|
137
|
+
* and the empty-stdout exit-code-deny convention carry no reason and are skipped.
|
|
138
|
+
*/
|
|
139
|
+
/** Parse an optional deny reason out of a JSON stdout deny envelope. */
|
|
140
|
+
function stdoutDenyReason(stdout) {
|
|
141
|
+
const text = stdout.trim();
|
|
142
|
+
if (text === "")
|
|
143
|
+
return null;
|
|
144
|
+
let parsed;
|
|
145
|
+
try {
|
|
146
|
+
parsed = JSON.parse(text);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
if (parsed === null || typeof parsed !== "object")
|
|
152
|
+
return null;
|
|
153
|
+
return denyReasonText(parsed);
|
|
154
|
+
}
|
|
155
|
+
/** First GUARD_-coded line in a captured stderr blob (Claude's exit-2 channel). */
|
|
156
|
+
function stderrCodedReason(stderr) {
|
|
157
|
+
if (!stderr)
|
|
158
|
+
return null;
|
|
159
|
+
for (const line of stderr.split("\n")) {
|
|
160
|
+
if (GUARD_CODED_REASON_RE.test(line.trim()))
|
|
161
|
+
return line.trim();
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
/** Assert a single reason is GUARD_-coded and within the 80-code-point budget. */
|
|
166
|
+
function assertCodedWithinBudget(row, label, reason) {
|
|
167
|
+
if (!GUARD_CODED_REASON_RE.test(reason)) {
|
|
168
|
+
throw new Error(`[${row.name}] ${label} must start with a GUARD_ code prefix, got ${JSON.stringify(reason)}`);
|
|
169
|
+
}
|
|
170
|
+
const points = [...reason].length;
|
|
171
|
+
if (points > 80) {
|
|
172
|
+
throw new Error(`[${row.name}] ${label} exceeds the 80-code-point budget (${points}): ${JSON.stringify(reason)}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function assertGuardCodedDenyReason(row, result) {
|
|
176
|
+
// The coded reason must reach the host on the channel it reads: the stdout
|
|
177
|
+
// deny envelope (Codex/Grok) or stderr (Claude reads stderr on an exit-2
|
|
178
|
+
// PreToolUse deny). Single-spawn tests capture the host boundary directly;
|
|
179
|
+
// the batch runner installs a row-scoped intentional-stderr capture so this
|
|
180
|
+
// same assertion also applies to source/compiled parity.
|
|
181
|
+
const reason = stdoutDenyReason(result.stdout) ?? stderrCodedReason(result.stderr);
|
|
182
|
+
if (reason === null) {
|
|
183
|
+
throw new Error(`[${row.name}] a PreToolUse deny must carry a GUARD_-coded reason on the stdout envelope or stderr; found none`);
|
|
184
|
+
}
|
|
185
|
+
assertCodedWithinBudget(row, "deny reason", reason);
|
|
186
|
+
}
|
|
187
|
+
function assertCodexExitTwoBlockingReason(row, result) {
|
|
188
|
+
if (row.host !== "codex" ||
|
|
189
|
+
row.event !== "PreToolUse" ||
|
|
190
|
+
result.exitCode !== 2 ||
|
|
191
|
+
result.stderr === undefined) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const stderrReason = stderrCodedReason(result.stderr);
|
|
195
|
+
if (stderrReason === null) {
|
|
196
|
+
throw new Error(`[${row.name}] Codex exit-2 PreToolUse deny must write a GUARD_-coded blocking reason to stderr`);
|
|
197
|
+
}
|
|
198
|
+
const stdoutReason = stdoutDenyReason(result.stdout);
|
|
199
|
+
if (stdoutReason !== null && stderrReason !== stdoutReason) {
|
|
200
|
+
throw new Error(`[${row.name}] Codex stdout and stderr deny reasons must match exactly`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
113
203
|
/** PermissionRequest golden wire: hookSpecificOutput.decision.behavior allow|deny. */
|
|
114
204
|
function permissionRequestBehavior(obj) {
|
|
115
205
|
const hookSpecific = obj?.hookSpecificOutput;
|
|
@@ -121,6 +211,45 @@ function permissionRequestBehavior(obj) {
|
|
|
121
211
|
const behavior = decision.behavior;
|
|
122
212
|
return typeof behavior === "string" ? behavior : null;
|
|
123
213
|
}
|
|
214
|
+
/** PermissionRequest golden wire message: hookSpecificOutput.decision.message. */
|
|
215
|
+
function permissionRequestMessage(obj) {
|
|
216
|
+
const hookSpecific = obj?.hookSpecificOutput;
|
|
217
|
+
if (!hookSpecific || typeof hookSpecific !== "object")
|
|
218
|
+
return null;
|
|
219
|
+
const decision = hookSpecific.decision;
|
|
220
|
+
if (!decision || typeof decision !== "object")
|
|
221
|
+
return null;
|
|
222
|
+
const message = decision.message;
|
|
223
|
+
return typeof message === "string" ? message : null;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* A PermissionRequest allow/deny message on a parseable envelope carries the
|
|
227
|
+
* same `GUARD_<CODE>: ` prefix within the 80-char budget as PreToolUse denies —
|
|
228
|
+
* the vocabulary is central, not PreToolUse-only. Skipped when there is no
|
|
229
|
+
* envelope message (e.g. a pure exit-2 deny with no stdout).
|
|
230
|
+
*/
|
|
231
|
+
function assertGuardCodedPermissionMessage(row, result, expect) {
|
|
232
|
+
let stdoutMessage = null;
|
|
233
|
+
const text = result.stdout.trim();
|
|
234
|
+
if (text !== "") {
|
|
235
|
+
try {
|
|
236
|
+
const parsed = JSON.parse(text);
|
|
237
|
+
if (parsed !== null && typeof parsed === "object") {
|
|
238
|
+
stdoutMessage = permissionRequestMessage(parsed);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
stdoutMessage = null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// A deny exits 2 and also carries the coded message on stderr; an allow exits 0
|
|
246
|
+
// with the coded message on the stdout envelope.
|
|
247
|
+
const message = stdoutMessage ?? stderrCodedReason(result.stderr);
|
|
248
|
+
if (message === null) {
|
|
249
|
+
throw new Error(`[${row.name}] PermissionRequest ${expect} must carry a GUARD_-coded message on the stdout envelope or stderr; found none`);
|
|
250
|
+
}
|
|
251
|
+
assertCodedWithinBudget(row, "PermissionRequest message", message);
|
|
252
|
+
}
|
|
124
253
|
function assertNoCodexUnsupportedFields(row, obj) {
|
|
125
254
|
if (row.host !== "codex" || obj === null)
|
|
126
255
|
return;
|
|
@@ -170,6 +299,11 @@ export function assertConformance(row, result) {
|
|
|
170
299
|
// exit-code deny convention). A deny envelope printed just before the hook
|
|
171
300
|
// crashes (exit 1) is not a valid decision.
|
|
172
301
|
assertExitCode(row, result, true);
|
|
302
|
+
// Every deny reason is GUARD_-coded and within the 80-char budget on
|
|
303
|
+
// both Tier-1 hosts (read from stdout, which carries the envelope even
|
|
304
|
+
// when Claude uses the exit code for the decision).
|
|
305
|
+
assertGuardCodedDenyReason(row, result);
|
|
306
|
+
assertCodexExitTwoBlockingReason(row, result);
|
|
173
307
|
}
|
|
174
308
|
else {
|
|
175
309
|
if (decision === "deny") {
|
|
@@ -196,6 +330,8 @@ export function assertConformance(row, result) {
|
|
|
196
330
|
if (result.exitCode !== 2) {
|
|
197
331
|
throw new Error(`[${row.name}] expected PermissionRequest deny via exit 2, got exit ${result.exitCode ?? "null"}`);
|
|
198
332
|
}
|
|
333
|
+
// The deny message carries the central GUARD_ code contract too.
|
|
334
|
+
assertGuardCodedPermissionMessage(row, result, "deny");
|
|
199
335
|
return;
|
|
200
336
|
}
|
|
201
337
|
if (row.expect === "allow") {
|
|
@@ -203,6 +339,7 @@ export function assertConformance(row, result) {
|
|
|
203
339
|
throw new Error(`[${row.name}] expected PermissionRequest allow envelope, got behavior=${behavior ?? "none"} (stdout: ${result.stdout.slice(0, 160)})`);
|
|
204
340
|
}
|
|
205
341
|
assertExitCode(row, result, false);
|
|
342
|
+
assertGuardCodedPermissionMessage(row, result, "allow");
|
|
206
343
|
return;
|
|
207
344
|
}
|
|
208
345
|
// pass-through: host default UI — empty object (or empty stdout), no decision, exit 0.
|
|
@@ -85,6 +85,7 @@ export async function runHookConformanceBatch() {
|
|
|
85
85
|
}
|
|
86
86
|
const raw = await readBatchStdin();
|
|
87
87
|
const { isHookName, runHookCommand } = await import("#cli/commands/hook.js");
|
|
88
|
+
const { installGuardStderrCapture } = await import("#hooks/shared/hook-bootstrap");
|
|
88
89
|
const payload = parseBatchPayload(raw, isHookName);
|
|
89
90
|
const protocolWrite = process.stdout.write.bind(process.stdout);
|
|
90
91
|
for (const row of payload.rows) {
|
|
@@ -92,6 +93,9 @@ export async function runHookConformanceBatch() {
|
|
|
92
93
|
let stdout = "";
|
|
93
94
|
let stderr = "";
|
|
94
95
|
const restoreEnv = setRowEnvironment(row);
|
|
96
|
+
const restoreGuardStderrCapture = installGuardStderrCapture((text) => {
|
|
97
|
+
stderr += text;
|
|
98
|
+
});
|
|
95
99
|
const originalStdoutWrite = process.stdout.write;
|
|
96
100
|
const originalStderrWrite = process.stderr.write;
|
|
97
101
|
process.stdout.write = (chunk) => {
|
|
@@ -112,6 +116,7 @@ export async function runHookConformanceBatch() {
|
|
|
112
116
|
finally {
|
|
113
117
|
process.stdout.write = originalStdoutWrite;
|
|
114
118
|
process.stderr.write = originalStderrWrite;
|
|
119
|
+
restoreGuardStderrCapture();
|
|
115
120
|
restoreEnv();
|
|
116
121
|
}
|
|
117
122
|
protocolWrite(`${JSON.stringify({ type: "result", id: row.id, stdout, stderr, exitCode })}\n`);
|
|
@@ -116,6 +116,21 @@ export declare function checkCodexConfigKeys(configFilePath?: string, deps?: Hos
|
|
|
116
116
|
* (exact `vp dlx` pins, launcher rules) at doctor time instead of only at setup.
|
|
117
117
|
*/
|
|
118
118
|
export declare function checkProjectMcpPins(cwd?: string, deps?: HostConfigDoctorDeps): DoctorCheck;
|
|
119
|
+
export interface ClaudeMcpDuplicateRegistrationOptions {
|
|
120
|
+
readonly home?: string;
|
|
121
|
+
readonly exists?: (path: string) => boolean;
|
|
122
|
+
readonly readFile?: (path: string) => string;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* `claude-mcp-duplicate-registration` — advisory. A machine can register the
|
|
126
|
+
* webpresso MCP server twice for the same Claude Code session: a manual
|
|
127
|
+
* `claude mcp add webpresso` entry in Claude Code's home JSON config (user
|
|
128
|
+
* scope or a project scope) plus the agent-kit marketplace plugin's bundled
|
|
129
|
+
* server from the Claude plugin cache. Both spawn `wp mcp`, so affected
|
|
130
|
+
* sessions run two server processes and carry a duplicated wp_* tool list.
|
|
131
|
+
* Degrades to ok on missing/unreadable config — loud only on real duplication.
|
|
132
|
+
*/
|
|
133
|
+
export declare function checkClaudeMcpDuplicateRegistration(options?: ClaudeMcpDuplicateRegistrationOptions): DoctorCheck;
|
|
119
134
|
export interface DanglingHostPathsOptions {
|
|
120
135
|
readonly configFilePath?: string;
|
|
121
136
|
readonly home?: string;
|
package/dist/esm/hooks/doctor.js
CHANGED
|
@@ -658,11 +658,15 @@ function probeDecisionRow(wpCli, row) {
|
|
|
658
658
|
detached: process.platform !== "win32",
|
|
659
659
|
});
|
|
660
660
|
let stdout = "";
|
|
661
|
+
let stderr = "";
|
|
661
662
|
const timeoutMs = hookProbeTimeoutMs();
|
|
662
663
|
const settle = createOwnedProbeSettler(child, timeoutMs, () => ({ ok: false, detail: `decision probe timed out after ${timeoutMs}ms` }), resolve);
|
|
663
664
|
child.stdout.on("data", (chunk) => {
|
|
664
665
|
stdout += String(chunk);
|
|
665
666
|
});
|
|
667
|
+
child.stderr.on("data", (chunk) => {
|
|
668
|
+
stderr += String(chunk);
|
|
669
|
+
});
|
|
666
670
|
child.stdin.on?.("error", (err) => {
|
|
667
671
|
settle({ ok: false, detail: `stdin write failed: ${err.message}` });
|
|
668
672
|
});
|
|
@@ -674,7 +678,7 @@ function probeDecisionRow(wpCli, row) {
|
|
|
674
678
|
});
|
|
675
679
|
child.on("close", (code) => {
|
|
676
680
|
try {
|
|
677
|
-
assertConformance(row, { stdout, exitCode: code });
|
|
681
|
+
assertConformance(row, { stdout, stderr, exitCode: code });
|
|
678
682
|
settle({ ok: true });
|
|
679
683
|
}
|
|
680
684
|
catch (error) {
|
|
@@ -1256,13 +1260,15 @@ async function checkClaudeHost() {
|
|
|
1256
1260
|
}
|
|
1257
1261
|
async function checkGrokHost(cwd = process.cwd()) {
|
|
1258
1262
|
const available = await runCommand("grok", ["--version"]);
|
|
1259
|
-
const hooksPath = join(cwd, ".grok", "hooks.json");
|
|
1263
|
+
const hooksPath = join(cwd, ".grok", "hooks", "webpresso.json");
|
|
1260
1264
|
let hooksPresent = false;
|
|
1261
1265
|
let hostLabeled = false;
|
|
1262
1266
|
try {
|
|
1263
1267
|
const body = readFileSync(hooksPath, "utf8");
|
|
1264
1268
|
hooksPresent = true;
|
|
1265
|
-
|
|
1269
|
+
// The generated launcher chain shell-quotes the value (WP_HOOK_HOST='grok');
|
|
1270
|
+
// accept any quoting variant so the doctor recognizes its own output.
|
|
1271
|
+
hostLabeled = /WP_HOOK_HOST=(['"]?)grok\1/u.test(body);
|
|
1266
1272
|
}
|
|
1267
1273
|
catch {
|
|
1268
1274
|
// Missing project hooks is advisory when binary absent; fail when binary present.
|
|
@@ -1273,8 +1279,8 @@ async function checkGrokHost(cwd = process.cwd()) {
|
|
|
1273
1279
|
ok: true,
|
|
1274
1280
|
detail: hooksPresent
|
|
1275
1281
|
? hostLabeled
|
|
1276
|
-
? "skipped (grok not on PATH); native .grok/hooks.json present with WP_HOOK_HOST=grok"
|
|
1277
|
-
: "skipped (grok not on PATH); .grok/hooks.json present but missing WP_HOOK_HOST=grok"
|
|
1282
|
+
? "skipped (grok not on PATH); native .grok/hooks/webpresso.json present with WP_HOOK_HOST=grok"
|
|
1283
|
+
: "skipped (grok not on PATH); .grok/hooks/webpresso.json present but missing WP_HOOK_HOST=grok"
|
|
1278
1284
|
: "skipped (grok not on PATH)",
|
|
1279
1285
|
};
|
|
1280
1286
|
}
|
|
@@ -1282,20 +1288,20 @@ async function checkGrokHost(cwd = process.cwd()) {
|
|
|
1282
1288
|
return {
|
|
1283
1289
|
name: "Grok host integration",
|
|
1284
1290
|
ok: false,
|
|
1285
|
-
detail: "grok on PATH but missing project .grok/hooks.json — run wp setup to scaffold native hooks",
|
|
1291
|
+
detail: "grok on PATH but missing project .grok/hooks/webpresso.json — run wp setup to scaffold native hooks, then run /hooks-trust once in Grok",
|
|
1286
1292
|
};
|
|
1287
1293
|
}
|
|
1288
1294
|
if (!hostLabeled) {
|
|
1289
1295
|
return {
|
|
1290
1296
|
name: "Grok host integration",
|
|
1291
1297
|
ok: false,
|
|
1292
|
-
detail: ".grok/hooks.json present but managed commands lack WP_HOOK_HOST=grok",
|
|
1298
|
+
detail: ".grok/hooks/webpresso.json present but managed commands lack WP_HOOK_HOST=grok",
|
|
1293
1299
|
};
|
|
1294
1300
|
}
|
|
1295
1301
|
return {
|
|
1296
1302
|
name: "Grok host integration",
|
|
1297
1303
|
ok: true,
|
|
1298
|
-
detail: "native .grok/hooks.json with WP_HOOK_HOST=grok (PreToolUse fail-open residual applies)",
|
|
1304
|
+
detail: "native .grok/hooks/webpresso.json with WP_HOOK_HOST=grok (trust-gated: run /hooks-trust once; PreToolUse fail-open residual applies)",
|
|
1299
1305
|
};
|
|
1300
1306
|
}
|
|
1301
1307
|
// Markers for the direct hook commands `wp setup` writes into
|
|
@@ -1790,6 +1796,61 @@ export function checkProjectMcpPins(cwd = process.cwd(), deps = {}) {
|
|
|
1790
1796
|
: `${parsed.definitions.length} exact-pinned vp dlx server(s) validated`,
|
|
1791
1797
|
};
|
|
1792
1798
|
}
|
|
1799
|
+
/**
|
|
1800
|
+
* `claude-mcp-duplicate-registration` — advisory. A machine can register the
|
|
1801
|
+
* webpresso MCP server twice for the same Claude Code session: a manual
|
|
1802
|
+
* `claude mcp add webpresso` entry in Claude Code's home JSON config (user
|
|
1803
|
+
* scope or a project scope) plus the agent-kit marketplace plugin's bundled
|
|
1804
|
+
* server from the Claude plugin cache. Both spawn `wp mcp`, so affected
|
|
1805
|
+
* sessions run two server processes and carry a duplicated wp_* tool list.
|
|
1806
|
+
* Degrades to ok on missing/unreadable config — loud only on real duplication.
|
|
1807
|
+
*/
|
|
1808
|
+
export function checkClaudeMcpDuplicateRegistration(options = {}) {
|
|
1809
|
+
const name = "claude MCP duplicate registration";
|
|
1810
|
+
const home = options.home ?? homedir();
|
|
1811
|
+
const exists = options.exists ?? existsSync;
|
|
1812
|
+
const readFile = options.readFile ?? ((candidate) => readFileSync(candidate, "utf8"));
|
|
1813
|
+
const pluginCacheDir = join(home, ".claude", "plugins", "cache", "webpresso", "agent-kit");
|
|
1814
|
+
if (!exists(pluginCacheDir)) {
|
|
1815
|
+
return { name, ok: true, detail: "skipped (agent-kit Claude plugin not installed)" };
|
|
1816
|
+
}
|
|
1817
|
+
const claudeJsonPath = join(home, ".claude.json");
|
|
1818
|
+
if (!exists(claudeJsonPath)) {
|
|
1819
|
+
return { name, ok: true, detail: "skipped (no Claude Code home JSON config)" };
|
|
1820
|
+
}
|
|
1821
|
+
let parsed;
|
|
1822
|
+
try {
|
|
1823
|
+
parsed = JSON.parse(readFile(claudeJsonPath));
|
|
1824
|
+
}
|
|
1825
|
+
catch {
|
|
1826
|
+
return { name, ok: true, detail: "skipped (Claude Code home JSON config unreadable)" };
|
|
1827
|
+
}
|
|
1828
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
1829
|
+
return { name, ok: true, detail: "skipped (Claude Code home JSON config is not an object)" };
|
|
1830
|
+
}
|
|
1831
|
+
const config = parsed;
|
|
1832
|
+
const manualSources = [];
|
|
1833
|
+
if (config.mcpServers && "webpresso" in config.mcpServers) {
|
|
1834
|
+
manualSources.push("user scope");
|
|
1835
|
+
}
|
|
1836
|
+
for (const [projectDir, project] of Object.entries(config.projects ?? {})) {
|
|
1837
|
+
if (project?.mcpServers && "webpresso" in project.mcpServers) {
|
|
1838
|
+
manualSources.push(`project scope ${projectDir}`);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
if (manualSources.length === 0) {
|
|
1842
|
+
return { name, ok: true, detail: "plugin-scoped registration only" };
|
|
1843
|
+
}
|
|
1844
|
+
return {
|
|
1845
|
+
name,
|
|
1846
|
+
ok: false,
|
|
1847
|
+
advisory: true,
|
|
1848
|
+
detail: `the agent-kit Claude plugin bundles the webpresso MCP server, but Claude Code's home JSON config also ` +
|
|
1849
|
+
`registers "webpresso" manually (${manualSources.join(", ")}). Affected sessions run two ` +
|
|
1850
|
+
`wp mcp processes with a duplicated wp_* tool list. Remove the manual entry: ` +
|
|
1851
|
+
`claude mcp remove webpresso (run inside the project directory for project-scope entries).`,
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1793
1854
|
const QUOTED_TOML_STRING = /"((?:[^"\\]|\\.)*)"/gu;
|
|
1794
1855
|
const DANGLING_SYMLINK_SCAN_DEPTH = 3;
|
|
1795
1856
|
function collectDanglingWorktreePaths(raw, worktreesRoot, exists, alreadyReported) {
|
|
@@ -1994,6 +2055,7 @@ export async function runHooksDoctor(opts = {}) {
|
|
|
1994
2055
|
checks.push(checkCodexConfigKeys());
|
|
1995
2056
|
}
|
|
1996
2057
|
checks.push(checkProjectMcpPins(opts.cwd));
|
|
2058
|
+
checks.push(checkClaudeMcpDuplicateRegistration());
|
|
1997
2059
|
checks.push({ advisory: true, ...checkDanglingHostPaths() });
|
|
1998
2060
|
checks.push({ advisory: true, ...checkRootLauncherContract() });
|
|
1999
2061
|
checks.push({ advisory: true, ...checkNativePluginRuntime() });
|
|
@@ -2086,6 +2148,15 @@ export async function runHooksDoctor(opts = {}) {
|
|
|
2086
2148
|
detail: "claude required but not on PATH",
|
|
2087
2149
|
});
|
|
2088
2150
|
}
|
|
2151
|
+
if (host === "grok") {
|
|
2152
|
+
const available = await runCommand("grok", ["--version"]);
|
|
2153
|
+
if (!available.ok)
|
|
2154
|
+
checks.push({
|
|
2155
|
+
name: "Grok host integration",
|
|
2156
|
+
ok: false,
|
|
2157
|
+
detail: "grok required but not on PATH",
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2089
2160
|
}
|
|
2090
2161
|
}
|
|
2091
2162
|
checks.push(checkHooksManifest(opts.cwd));
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* Stdin: uses shared `readStdinJson` so conformance-batch can inject payloads via
|
|
10
10
|
* `WP_CONFORMANCE_STDIN` (same contract as other managed hooks).
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
12
|
+
import { formatGuardReason } from "#hooks/shared/guard-outcome";
|
|
13
|
+
import { readStdinJson, writeGuardStderr, writeHookOutput } from "#hooks/shared/hook-bootstrap";
|
|
13
14
|
import { buildPermissionRequestEnvelope } from "./envelope.js";
|
|
14
15
|
import { evaluatePermissionRequest } from "./policy.js";
|
|
15
16
|
function isRecord(value) {
|
|
@@ -53,9 +54,14 @@ export async function main() {
|
|
|
53
54
|
return;
|
|
54
55
|
}
|
|
55
56
|
// Claude + Codex share the golden PermissionRequest wire shape
|
|
56
|
-
// (hookSpecificOutput.hookEventName + decision.behavior).
|
|
57
|
-
|
|
57
|
+
// (hookSpecificOutput.hookEventName + decision.behavior). The wire message
|
|
58
|
+
// carries the GUARD_* code so allow/deny decisions are keyable across hosts.
|
|
59
|
+
const codedMessage = formatGuardReason(result.code, result.reason);
|
|
60
|
+
writeHookOutput(JSON.stringify(buildPermissionRequestEnvelope(result.decision, codedMessage)));
|
|
58
61
|
if (result.decision === "deny") {
|
|
62
|
+
// A deny exits 2; deliver the coded message to the real stderr too so it is
|
|
63
|
+
// observable even when a host prefers the exit-code channel.
|
|
64
|
+
writeGuardStderr(`${codedMessage}\n`);
|
|
59
65
|
// Must use process.exit so runHookCommand's synthetic-exit trap records exit 2.
|
|
60
66
|
process.exit(2);
|
|
61
67
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* authoritative when policy is uncertain. Explicit deny only for high-confidence
|
|
6
6
|
* unsafe escalations; explicit allow only for clearly safe read-only escalations.
|
|
7
7
|
*/
|
|
8
|
+
import type { GuardReasonCode } from "#hooks/shared/guard-outcome";
|
|
8
9
|
export type PermissionRequestDecision = "allow" | "deny";
|
|
9
10
|
export type PermissionRequestPolicyInput = {
|
|
10
11
|
readonly toolName?: string;
|
|
@@ -15,13 +16,21 @@ export type PermissionRequestPolicyInput = {
|
|
|
15
16
|
export type PermissionRequestPolicyResult = {
|
|
16
17
|
readonly decision: "allow";
|
|
17
18
|
readonly reason: string;
|
|
19
|
+
readonly code: GuardReasonCode;
|
|
18
20
|
} | {
|
|
19
21
|
readonly decision: "deny";
|
|
20
22
|
readonly reason: string;
|
|
23
|
+
readonly code: GuardReasonCode;
|
|
21
24
|
} | {
|
|
22
25
|
readonly decision: undefined;
|
|
23
26
|
readonly reason: string;
|
|
27
|
+
readonly code: GuardReasonCode;
|
|
24
28
|
};
|
|
29
|
+
/**
|
|
30
|
+
* Every GUARD_* code the permission-request policy can emit. Consumed by the
|
|
31
|
+
* guard-outcome registry-completeness test.
|
|
32
|
+
*/
|
|
33
|
+
export declare const PERMISSION_GUARD_CODES: readonly ["GUARD_PERMISSION_ALLOWED", "GUARD_PERMISSION_DENIED", "GUARD_HOST_DEFAULT"];
|
|
25
34
|
/**
|
|
26
35
|
* Decide allow / deny / pass-through for a PermissionRequest payload.
|
|
27
36
|
*/
|
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
* authoritative when policy is uncertain. Explicit deny only for high-confidence
|
|
6
6
|
* unsafe escalations; explicit allow only for clearly safe read-only escalations.
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* Every GUARD_* code the permission-request policy can emit. Consumed by the
|
|
10
|
+
* guard-outcome registry-completeness test.
|
|
11
|
+
*/
|
|
12
|
+
export const PERMISSION_GUARD_CODES = [
|
|
13
|
+
"GUARD_PERMISSION_ALLOWED",
|
|
14
|
+
"GUARD_PERMISSION_DENIED",
|
|
15
|
+
"GUARD_HOST_DEFAULT",
|
|
16
|
+
];
|
|
8
17
|
const SECRET_PATH_RE = /(?:^|\/)(?:\.env(?:\..+)?|.*\.(?:pem|key|p12|pfx)|id_rsa|id_ed25519|credentials(?:\.json)?|secrets?(?:\.ya?ml)?|\.npmrc|\.netrc)(?:$|\/)/iu;
|
|
9
18
|
const DANGEROUS_BASH_RE = /\b(?:rm\s+-[a-zA-Z]*f|sudo\s+|curl\s+[^\n]*\|\s*(?:ba)?sh|wget\s+[^\n]*\|\s*(?:ba)?sh|mkfs\.|dd\s+if=)/iu;
|
|
10
19
|
function asCommand(input) {
|
|
@@ -39,12 +48,14 @@ export function evaluatePermissionRequest(input) {
|
|
|
39
48
|
return {
|
|
40
49
|
decision: "deny",
|
|
41
50
|
reason: "PermissionRequest denied: secret or credential path escalation",
|
|
51
|
+
code: "GUARD_PERMISSION_DENIED",
|
|
42
52
|
};
|
|
43
53
|
}
|
|
44
54
|
if (DANGEROUS_BASH_RE.test(command) || DANGEROUS_BASH_RE.test(blob)) {
|
|
45
55
|
return {
|
|
46
56
|
decision: "deny",
|
|
47
57
|
reason: "PermissionRequest denied: dangerous shell escalation",
|
|
58
|
+
code: "GUARD_PERMISSION_DENIED",
|
|
48
59
|
};
|
|
49
60
|
}
|
|
50
61
|
// Explicit safe read-only escalations (narrow allow).
|
|
@@ -54,10 +65,12 @@ export function evaluatePermissionRequest(input) {
|
|
|
54
65
|
return {
|
|
55
66
|
decision: "allow",
|
|
56
67
|
reason: "PermissionRequest allowed: read-only tool escalation",
|
|
68
|
+
code: "GUARD_PERMISSION_ALLOWED",
|
|
57
69
|
};
|
|
58
70
|
}
|
|
59
71
|
return {
|
|
60
72
|
decision: undefined,
|
|
61
73
|
reason: "PermissionRequest pass-through: no high-confidence policy match",
|
|
74
|
+
code: "GUARD_HOST_DEFAULT",
|
|
62
75
|
};
|
|
63
76
|
}
|
|
@@ -1,14 +1,23 @@
|
|
|
1
|
+
import type { GuardReasonCode } from "#hooks/shared/guard-outcome";
|
|
1
2
|
export type GuidanceType = "test" | "lint" | "typecheck" | "qa" | "format" | "e2e" | "worktree";
|
|
2
3
|
export type RouteAction = {
|
|
3
4
|
action: "deny";
|
|
4
5
|
tool: string;
|
|
5
6
|
guidance: string;
|
|
7
|
+
code: GuardReasonCode;
|
|
6
8
|
} | {
|
|
7
9
|
action: "sandbox";
|
|
8
10
|
guidance: string;
|
|
11
|
+
code: GuardReasonCode;
|
|
9
12
|
} | {
|
|
10
13
|
action: "passthrough";
|
|
11
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* Every GUARD_* code a dev-routing deny/sandbox can emit. The guard-outcome
|
|
17
|
+
* registry-completeness test asserts this stays a subset of GUARD_REASON_CODES
|
|
18
|
+
* and that each listed code is reachable from a construction site below.
|
|
19
|
+
*/
|
|
20
|
+
export declare const DEV_ROUTING_GUARD_CODES: readonly ["GUARD_RAW_GIT_MUTATION", "GUARD_MANUAL_PR_WAIT", "GUARD_PR_DISCLOSURE", "GUARD_WRAPPED_WP", "GUARD_SOURCE_ENTRYPOINT", "GUARD_DEV_TOOL_REDIRECT", "GUARD_DATA_HEAVY_SANDBOX", "GUARD_SESSION_MEMORY_REDIRECT"];
|
|
12
21
|
export interface RouteDecision {
|
|
13
22
|
action: RouteAction;
|
|
14
23
|
}
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { detectWrappedWpCommand, stripLeadingSecretWrappers, wrappedWpGuidanceForArgs, } from "#cli/wrapped-wp";
|
|
2
2
|
import { isSessionSandboxToolName } from "#hooks/shared/types";
|
|
3
|
+
/**
|
|
4
|
+
* Every GUARD_* code a dev-routing deny/sandbox can emit. The guard-outcome
|
|
5
|
+
* registry-completeness test asserts this stays a subset of GUARD_REASON_CODES
|
|
6
|
+
* and that each listed code is reachable from a construction site below.
|
|
7
|
+
*/
|
|
8
|
+
export const DEV_ROUTING_GUARD_CODES = [
|
|
9
|
+
"GUARD_RAW_GIT_MUTATION",
|
|
10
|
+
"GUARD_MANUAL_PR_WAIT",
|
|
11
|
+
"GUARD_PR_DISCLOSURE",
|
|
12
|
+
"GUARD_WRAPPED_WP",
|
|
13
|
+
"GUARD_SOURCE_ENTRYPOINT",
|
|
14
|
+
"GUARD_DEV_TOOL_REDIRECT",
|
|
15
|
+
"GUARD_DATA_HEAVY_SANDBOX",
|
|
16
|
+
"GUARD_SESSION_MEMORY_REDIRECT",
|
|
17
|
+
];
|
|
3
18
|
const RAW_MUTATING_GIT_WORKTREE_RE = /^git\s+worktree\s+(?:add|remove|move|prune)(?:\s|$)/u;
|
|
4
19
|
const MANUAL_PR_WAIT_RE = /^sleep\s+\S+[\s\S]*(?:gh\s+pr\s+(?:view|checks|status)|wp(?:_pr_status|\s+pr\s+status))/u;
|
|
5
20
|
function routeRawMutatingGitWorktree(command) {
|
|
@@ -9,6 +24,7 @@ function routeRawMutatingGitWorktree(command) {
|
|
|
9
24
|
action: "deny",
|
|
10
25
|
tool: "wp_worktree",
|
|
11
26
|
guidance: "Use the `wp_worktree` MCP tool (or `wp worktree` CLI when MCP is unavailable) instead — managed repositories may mutate linked worktrees only through agent-kit so owner bindings and the global registry stay consistent.",
|
|
27
|
+
code: "GUARD_RAW_GIT_MUTATION",
|
|
12
28
|
};
|
|
13
29
|
}
|
|
14
30
|
function routeManualPrWait(command) {
|
|
@@ -18,6 +34,7 @@ function routeManualPrWait(command) {
|
|
|
18
34
|
action: "deny",
|
|
19
35
|
tool: "wp_pr_wait",
|
|
20
36
|
guidance: "Use wp_pr_wait MCP tool instead — it blocks on PR checks or merge state with bounded final output and avoids manual sleep plus status polling loops",
|
|
37
|
+
code: "GUARD_MANUAL_PR_WAIT",
|
|
21
38
|
};
|
|
22
39
|
}
|
|
23
40
|
const PR_DESCRIPTION_WRITE_GUIDANCE = "Use wp_pr_upsert MCP tool instead — it validates the repository PR disclosure contract before creating or updating a PR title/body.";
|
|
@@ -27,6 +44,7 @@ function prDescriptionWriteDeny() {
|
|
|
27
44
|
action: "deny",
|
|
28
45
|
tool: "wp_pr_upsert",
|
|
29
46
|
guidance: PR_DESCRIPTION_WRITE_GUIDANCE,
|
|
47
|
+
code: "GUARD_PR_DISCLOSURE",
|
|
30
48
|
};
|
|
31
49
|
}
|
|
32
50
|
function isGhPrDescriptionFlag(token) {
|
|
@@ -214,7 +232,7 @@ const ROUTING_RULES = [
|
|
|
214
232
|
tool: "wp_gain",
|
|
215
233
|
},
|
|
216
234
|
{
|
|
217
|
-
prefixes: ["
|
|
235
|
+
prefixes: ["wp run changeset:status"],
|
|
218
236
|
guidanceType: "qa",
|
|
219
237
|
guidance: "Use wp_release_readiness MCP tool instead — release gates are aggregated without publishing, tagging, versioning, or merging",
|
|
220
238
|
tool: "wp_release_readiness",
|
|
@@ -851,7 +869,13 @@ function extractStringField(record, keys) {
|
|
|
851
869
|
return null;
|
|
852
870
|
}
|
|
853
871
|
function sessionSandboxDecision(tool, guidance) {
|
|
854
|
-
return {
|
|
872
|
+
return {
|
|
873
|
+
action: {
|
|
874
|
+
action: "sandbox",
|
|
875
|
+
guidance: `Use ${tool} instead — ${guidance}`,
|
|
876
|
+
code: "GUARD_SESSION_MEMORY_REDIRECT",
|
|
877
|
+
},
|
|
878
|
+
};
|
|
855
879
|
}
|
|
856
880
|
const MAX_SAFE_GREP_HEAD_LIMIT = 100;
|
|
857
881
|
function numericField(record, keys) {
|
|
@@ -952,6 +976,7 @@ export function routeCommand(command, _sessionId) {
|
|
|
952
976
|
action: "deny",
|
|
953
977
|
tool,
|
|
954
978
|
guidance,
|
|
979
|
+
code: "GUARD_WRAPPED_WP",
|
|
955
980
|
},
|
|
956
981
|
};
|
|
957
982
|
}
|
|
@@ -973,6 +998,7 @@ export function routeCommand(command, _sessionId) {
|
|
|
973
998
|
action: "deny",
|
|
974
999
|
tool: sourceEntrypointRule.tool,
|
|
975
1000
|
guidance: sourceEntrypointRule.guidance,
|
|
1001
|
+
code: "GUARD_SOURCE_ENTRYPOINT",
|
|
976
1002
|
},
|
|
977
1003
|
};
|
|
978
1004
|
}
|
|
@@ -981,20 +1007,30 @@ export function routeCommand(command, _sessionId) {
|
|
|
981
1007
|
for (const prefix of rule.prefixes) {
|
|
982
1008
|
if (matchesPrefix(trimmed, prefix) || matchesDirectToolCommand(trimmed, rule)) {
|
|
983
1009
|
return {
|
|
984
|
-
action: {
|
|
1010
|
+
action: {
|
|
1011
|
+
action: "deny",
|
|
1012
|
+
tool: rule.tool,
|
|
1013
|
+
guidance: rule.guidance,
|
|
1014
|
+
code: "GUARD_DEV_TOOL_REDIRECT",
|
|
1015
|
+
},
|
|
985
1016
|
};
|
|
986
1017
|
}
|
|
987
1018
|
}
|
|
988
1019
|
if (matchesPackageManagerDirectToolCommand(trimmed, rule)) {
|
|
989
1020
|
return {
|
|
990
|
-
action: {
|
|
1021
|
+
action: {
|
|
1022
|
+
action: "deny",
|
|
1023
|
+
tool: rule.tool,
|
|
1024
|
+
guidance: rule.guidance,
|
|
1025
|
+
code: "GUARD_DEV_TOOL_REDIRECT",
|
|
1026
|
+
},
|
|
991
1027
|
};
|
|
992
1028
|
}
|
|
993
1029
|
}
|
|
994
1030
|
// Sandbox rules (data-heavy commands)
|
|
995
1031
|
for (const { prefix, guidance } of SANDBOX_PREFIXES) {
|
|
996
1032
|
if (matchesPrefix(trimmed, prefix)) {
|
|
997
|
-
return { action: { action: "sandbox", guidance } };
|
|
1033
|
+
return { action: { action: "sandbox", guidance, code: "GUARD_DATA_HEAVY_SANDBOX" } };
|
|
998
1034
|
}
|
|
999
1035
|
}
|
|
1000
1036
|
// Unknown — null (let callers decide)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type GuardOutcome } from "#hooks/shared/guard-outcome";
|
|
1
2
|
export type LogStatus = "PASS" | "BLOCK" | "WARN" | "ERROR";
|
|
2
3
|
export type ToolType = string;
|
|
3
4
|
export interface LogEntry {
|
|
@@ -6,6 +7,10 @@ export interface LogEntry {
|
|
|
6
7
|
tool: ToolType;
|
|
7
8
|
failures?: string[];
|
|
8
9
|
error?: string;
|
|
10
|
+
/** GUARD_* reason code for the decision (additive; present on BLOCK/WARN). */
|
|
11
|
+
code?: string;
|
|
12
|
+
/** Two-axis guard outcome for the decision (additive). */
|
|
13
|
+
outcome?: GuardOutcome;
|
|
9
14
|
}
|
|
10
15
|
export interface LogConfig {
|
|
11
16
|
logDir: string;
|
|
@@ -20,6 +25,8 @@ export interface ParsedLogLine {
|
|
|
20
25
|
target: string;
|
|
21
26
|
failures?: string[];
|
|
22
27
|
error?: string;
|
|
28
|
+
code?: string;
|
|
29
|
+
outcome?: GuardOutcome;
|
|
23
30
|
}
|
|
24
31
|
export declare function createLogConfig(): LogConfig;
|
|
25
32
|
export declare function parseLogLine(line: string): ParsedLogLine | null;
|