@webpresso/agent-kit 3.1.30 → 3.3.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.
- package/README.md +53 -15
- package/bin/_run.js +183 -20
- package/catalog/AGENTS.md.tpl +3 -3
- package/catalog/agent/agents/implementer.md +45 -0
- package/catalog/agent/rules/parallel-orchestration-quota.md +100 -0
- package/catalog/agent/rules/pre-implementation.md +28 -1
- package/catalog/agent/rules/supported-agent-clis.md +9 -0
- package/catalog/agent/rules/typescript-version.md +50 -0
- package/catalog/agent/skills/codex/SKILL.md +25 -1
- package/catalog/agent/skills/verify/SKILL.md +4 -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 +4 -0
- package/dist/esm/audit/blueprint-trust.js +14 -10
- package/dist/esm/audit/changeset-evidence.d.ts +23 -0
- package/dist/esm/audit/changeset-evidence.js +30 -0
- package/dist/esm/audit/changeset-required.d.ts +10 -0
- package/dist/esm/audit/changeset-required.js +73 -0
- package/dist/esm/audit/ci-guardrails-detection.d.ts +27 -0
- package/dist/esm/audit/ci-guardrails-detection.js +68 -0
- package/dist/esm/audit/ci-guardrails-wiring.d.ts +22 -0
- package/dist/esm/audit/ci-guardrails-wiring.js +80 -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 +13 -1
- 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 +101 -2
- package/dist/esm/blueprint/core/schema.d.ts +1 -0
- package/dist/esm/blueprint/core/schema.js +26 -0
- 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 +87 -20
- package/dist/esm/blueprint/lifecycle/canonicalize.d.ts +6 -0
- package/dist/esm/blueprint/lifecycle/canonicalize.js +135 -0
- 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 +68 -12
- package/dist/esm/blueprint/lifecycle/rollout-anchor.d.ts +97 -0
- package/dist/esm/blueprint/lifecycle/rollout-anchor.js +161 -0
- 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/sync/client.d.ts +2 -1
- package/dist/esm/blueprint/sync/client.js +1 -1
- 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/dossier.d.ts +1 -0
- package/dist/esm/blueprint/trust/dossier.js +40 -6
- 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 +74 -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 +15 -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 +43 -0
- package/dist/esm/cli/commands/agent-launch.d.ts +8 -1
- package/dist/esm/cli/commands/agent-launch.js +93 -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 +21 -5
- package/dist/esm/cli/commands/blueprint/execution.js +18 -6
- package/dist/esm/cli/commands/blueprint/mutations.js +59 -40
- 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 +13 -1
- package/dist/esm/cli/commands/ci-preflight.d.ts +13 -2
- package/dist/esm/cli/commands/ci-preflight.js +30 -6
- 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/doctor.js +5 -0
- 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.js +2 -2
- package/dist/esm/cli/commands/init/convergence-apply.d.ts +88 -0
- package/dist/esm/cli/commands/init/convergence-apply.js +325 -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 +3 -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/package-root.d.ts +7 -0
- package/dist/esm/cli/commands/init/package-root.js +10 -2
- 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 +20 -0
- package/dist/esm/cli/commands/init/scaffold-base-kit.js +61 -5
- 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 +54 -24
- 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 +288 -202
- package/dist/esm/cli/commands/init/scaffolders/file-transaction.d.ts +22 -0
- package/dist/esm/cli/commands/init/scaffolders/file-transaction.js +230 -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/opencode-probe.d.ts +107 -0
- package/dist/esm/cli/commands/opencode-probe.js +323 -0
- package/dist/esm/cli/commands/opencode-rotate.d.ts +72 -0
- package/dist/esm/cli/commands/opencode-rotate.js +147 -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-runner.js +3 -3
- package/dist/esm/cli/commands/review.d.ts +35 -3
- package/dist/esm/cli/commands/review.js +751 -209
- 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/rust-check.d.ts +24 -0
- package/dist/esm/cli/commands/rust-check.js +110 -0
- 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/typecheck.d.ts +1 -0
- package/dist/esm/cli/commands/typecheck.js +30 -1
- package/dist/esm/cli/commands/worktree/router-dispatch.d.ts +6 -0
- package/dist/esm/cli/commands/worktree/router-dispatch.js +126 -9
- package/dist/esm/cli/commands/worktree/router.js +10 -0
- package/dist/esm/cli/direct-provider-launch.js +9 -5
- 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 +5 -3
- package/dist/esm/hooks/doctor.js +67 -16
- 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 +54 -0
- 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/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.js +6 -1
- 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 +50 -9
- package/dist/esm/hooks/stop/qa-changed-files.d.ts +11 -0
- package/dist/esm/hooks/stop/qa-changed-files.js +92 -0
- package/dist/esm/mcp/blueprint/_shared/lifecycle.js +14 -1
- package/dist/esm/mcp/blueprint/handlers/document-mutations.d.ts +5 -0
- package/dist/esm/mcp/blueprint/handlers/document-mutations.js +109 -11
- 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 +42 -0
- package/dist/esm/mcp/tools/audits.d.ts +4 -0
- 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-fetch-and-index.d.ts +1 -1
- 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/package.json +2 -0
- package/dist/esm/platform/client.d.ts +7 -1
- package/dist/esm/platform/github-contract-source.d.ts +2 -1
- package/dist/esm/pr-description/contract.js +24 -4
- package/dist/esm/review/authority.js +27 -14
- package/dist/esm/review/availability.d.ts +33 -0
- package/dist/esm/review/availability.js +122 -20
- 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.d.ts +11 -1
- package/dist/esm/review/execution/adapters.js +36 -10
- package/dist/esm/review/execution/artifacts.d.ts +1 -1
- 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 +121 -21
- package/dist/esm/review/execution/supervisor.js +36 -4
- package/dist/esm/review/execution/types.d.ts +9 -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 +25 -0
- package/dist/esm/review/subject.js +116 -26
- package/dist/esm/runtime/opencode-account-materializer.d.ts +34 -0
- package/dist/esm/runtime/opencode-account-materializer.js +33 -0
- package/dist/esm/rust/affected-manifests.d.ts +38 -0
- package/dist/esm/rust/affected-manifests.js +69 -0
- package/dist/esm/session-memory/fetch-index.d.ts +1 -1
- package/dist/esm/status/snapshot.js +32 -25
- 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 -2
- package/dist/esm/test-helpers/global-setup.js +79 -32
- package/dist/esm/typecheck/planner.d.ts +9 -1
- package/dist/esm/typecheck/planner.js +31 -0
- 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/orphan-scan.d.ts +50 -0
- package/dist/esm/worktrees/orphan-scan.js +211 -0
- package/dist/esm/worktrees/registry.d.ts +8 -0
- package/dist/esm/worktrees/registry.js +18 -3
- package/package.json +18 -13
|
@@ -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`);
|
|
@@ -118,15 +118,17 @@ export declare function checkCodexConfigKeys(configFilePath?: string, deps?: Hos
|
|
|
118
118
|
export declare function checkProjectMcpPins(cwd?: string, deps?: HostConfigDoctorDeps): DoctorCheck;
|
|
119
119
|
export interface ClaudeMcpDuplicateRegistrationOptions {
|
|
120
120
|
readonly home?: string;
|
|
121
|
+
/** Project root used to discover a project-local `.mcp.json` registration. */
|
|
122
|
+
readonly projectRoot?: string;
|
|
121
123
|
readonly exists?: (path: string) => boolean;
|
|
122
124
|
readonly readFile?: (path: string) => string;
|
|
123
125
|
}
|
|
124
126
|
/**
|
|
125
127
|
* `claude-mcp-duplicate-registration` — advisory. A machine can register the
|
|
126
128
|
* webpresso MCP server twice for the same Claude Code session: a manual
|
|
127
|
-
* `claude mcp add webpresso` entry in
|
|
128
|
-
* project scope) plus the agent-kit marketplace plugin's bundled
|
|
129
|
-
*
|
|
129
|
+
* `claude mcp add webpresso` entry in Claude Code's home JSON config (user
|
|
130
|
+
* scope or a project scope) plus the agent-kit marketplace plugin's bundled
|
|
131
|
+
* server from the Claude plugin cache. Both spawn `wp mcp`, so affected
|
|
130
132
|
* sessions run two server processes and carry a duplicated wp_* tool list.
|
|
131
133
|
* Degrades to ok on missing/unreadable config — loud only on real duplication.
|
|
132
134
|
*/
|
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
|
|
@@ -1793,9 +1799,9 @@ export function checkProjectMcpPins(cwd = process.cwd(), deps = {}) {
|
|
|
1793
1799
|
/**
|
|
1794
1800
|
* `claude-mcp-duplicate-registration` — advisory. A machine can register the
|
|
1795
1801
|
* webpresso MCP server twice for the same Claude Code session: a manual
|
|
1796
|
-
* `claude mcp add webpresso` entry in
|
|
1797
|
-
* project scope) plus the agent-kit marketplace plugin's bundled
|
|
1798
|
-
*
|
|
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
|
|
1799
1805
|
* sessions run two server processes and carry a duplicated wp_* tool list.
|
|
1800
1806
|
* Degrades to ok on missing/unreadable config — loud only on real duplication.
|
|
1801
1807
|
*/
|
|
@@ -1810,17 +1816,17 @@ export function checkClaudeMcpDuplicateRegistration(options = {}) {
|
|
|
1810
1816
|
}
|
|
1811
1817
|
const claudeJsonPath = join(home, ".claude.json");
|
|
1812
1818
|
if (!exists(claudeJsonPath)) {
|
|
1813
|
-
return { name, ok: true, detail: "skipped (no
|
|
1819
|
+
return { name, ok: true, detail: "skipped (no Claude Code home JSON config)" };
|
|
1814
1820
|
}
|
|
1815
1821
|
let parsed;
|
|
1816
1822
|
try {
|
|
1817
1823
|
parsed = JSON.parse(readFile(claudeJsonPath));
|
|
1818
1824
|
}
|
|
1819
1825
|
catch {
|
|
1820
|
-
return { name, ok: true, detail: "skipped (
|
|
1826
|
+
return { name, ok: true, detail: "skipped (Claude Code home JSON config unreadable)" };
|
|
1821
1827
|
}
|
|
1822
1828
|
if (parsed === null || typeof parsed !== "object") {
|
|
1823
|
-
return { name, ok: true, detail: "skipped (
|
|
1829
|
+
return { name, ok: true, detail: "skipped (Claude Code home JSON config is not an object)" };
|
|
1824
1830
|
}
|
|
1825
1831
|
const config = parsed;
|
|
1826
1832
|
const manualSources = [];
|
|
@@ -1832,6 +1838,29 @@ export function checkClaudeMcpDuplicateRegistration(options = {}) {
|
|
|
1832
1838
|
manualSources.push(`project scope ${projectDir}`);
|
|
1833
1839
|
}
|
|
1834
1840
|
}
|
|
1841
|
+
// Also scan project-local `.mcp.json` (and ancestors). Claude Code honors
|
|
1842
|
+
// this file as a third registration surface; the home JSON projects map
|
|
1843
|
+
// does not always mirror it, so a plugin + project-file pair still doubles
|
|
1844
|
+
// the tool list without the home-JSON check ever firing.
|
|
1845
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
1846
|
+
for (const candidate of collectProjectMcpJsonCandidates(projectRoot)) {
|
|
1847
|
+
if (!exists(candidate))
|
|
1848
|
+
continue;
|
|
1849
|
+
try {
|
|
1850
|
+
const projectParsed = JSON.parse(readFile(candidate));
|
|
1851
|
+
if (projectParsed !== null &&
|
|
1852
|
+
typeof projectParsed === "object" &&
|
|
1853
|
+
"mcpServers" in projectParsed &&
|
|
1854
|
+
projectParsed.mcpServers &&
|
|
1855
|
+
"webpresso" in
|
|
1856
|
+
(projectParsed.mcpServers ?? {})) {
|
|
1857
|
+
manualSources.push(`project file ${candidate}`);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
catch {
|
|
1861
|
+
// Unreadable/malformed .mcp.json is not a duplicate signal — degrade.
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1835
1864
|
if (manualSources.length === 0) {
|
|
1836
1865
|
return { name, ok: true, detail: "plugin-scoped registration only" };
|
|
1837
1866
|
}
|
|
@@ -1839,12 +1868,25 @@ export function checkClaudeMcpDuplicateRegistration(options = {}) {
|
|
|
1839
1868
|
name,
|
|
1840
1869
|
ok: false,
|
|
1841
1870
|
advisory: true,
|
|
1842
|
-
detail: `the agent-kit Claude plugin bundles the webpresso MCP server, but
|
|
1871
|
+
detail: `the agent-kit Claude plugin bundles the webpresso MCP server, but Claude Code's home JSON config also ` +
|
|
1843
1872
|
`registers "webpresso" manually (${manualSources.join(", ")}). Affected sessions run two ` +
|
|
1844
1873
|
`wp mcp processes with a duplicated wp_* tool list. Remove the manual entry: ` +
|
|
1845
1874
|
`claude mcp remove webpresso (run inside the project directory for project-scope entries).`,
|
|
1846
1875
|
};
|
|
1847
1876
|
}
|
|
1877
|
+
/** Walk projectRoot → filesystem root collecting `.mcp.json` paths that may register MCP servers. */
|
|
1878
|
+
function collectProjectMcpJsonCandidates(projectRoot) {
|
|
1879
|
+
const candidates = [];
|
|
1880
|
+
let current = projectRoot;
|
|
1881
|
+
for (;;) {
|
|
1882
|
+
candidates.push(join(current, ".mcp.json"));
|
|
1883
|
+
const parent = dirname(current);
|
|
1884
|
+
if (parent === current)
|
|
1885
|
+
break;
|
|
1886
|
+
current = parent;
|
|
1887
|
+
}
|
|
1888
|
+
return candidates;
|
|
1889
|
+
}
|
|
1848
1890
|
const QUOTED_TOML_STRING = /"((?:[^"\\]|\\.)*)"/gu;
|
|
1849
1891
|
const DANGLING_SYMLINK_SCAN_DEPTH = 3;
|
|
1850
1892
|
function collectDanglingWorktreePaths(raw, worktreesRoot, exists, alreadyReported) {
|
|
@@ -2049,7 +2091,7 @@ export async function runHooksDoctor(opts = {}) {
|
|
|
2049
2091
|
checks.push(checkCodexConfigKeys());
|
|
2050
2092
|
}
|
|
2051
2093
|
checks.push(checkProjectMcpPins(opts.cwd));
|
|
2052
|
-
checks.push(checkClaudeMcpDuplicateRegistration());
|
|
2094
|
+
checks.push(checkClaudeMcpDuplicateRegistration({ projectRoot: opts.cwd ?? process.cwd() }));
|
|
2053
2095
|
checks.push({ advisory: true, ...checkDanglingHostPaths() });
|
|
2054
2096
|
checks.push({ advisory: true, ...checkRootLauncherContract() });
|
|
2055
2097
|
checks.push({ advisory: true, ...checkNativePluginRuntime() });
|
|
@@ -2142,6 +2184,15 @@ export async function runHooksDoctor(opts = {}) {
|
|
|
2142
2184
|
detail: "claude required but not on PATH",
|
|
2143
2185
|
});
|
|
2144
2186
|
}
|
|
2187
|
+
if (host === "grok") {
|
|
2188
|
+
const available = await runCommand("grok", ["--version"]);
|
|
2189
|
+
if (!available.ok)
|
|
2190
|
+
checks.push({
|
|
2191
|
+
name: "Grok host integration",
|
|
2192
|
+
ok: false,
|
|
2193
|
+
detail: "grok required but not on PATH",
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2145
2196
|
}
|
|
2146
2197
|
}
|
|
2147
2198
|
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)
|