@toolu/opencode 6.8.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/generated/GENERATED-NOTES.md +16 -0
- package/generated/agents/toolu--architect.md +52 -0
- package/generated/agents/toolu--deep-explore.md +65 -0
- package/generated/agents/toolu--implementer.md +50 -0
- package/generated/agents/toolu--quick-task.md +54 -0
- package/generated/agents/toolu--research-agent.md +101 -0
- package/generated/commands/toolu--commit.md +8 -0
- package/generated/commands/toolu--review-and-commit.md +8 -0
- package/generated/opencode.toolu.json +116 -0
- package/generated/skills/toolu--brainstorm/SKILL.md +57 -0
- package/generated/skills/toolu--brainstorm/references/design-questions.md +36 -0
- package/generated/skills/toolu--commit--skill/SKILL.md +11 -0
- package/generated/skills/toolu--debug/SKILL.md +54 -0
- package/generated/skills/toolu--deep-research/SKILL.md +51 -0
- package/generated/skills/toolu--execution/SKILL.md +77 -0
- package/generated/skills/toolu--orchestrator/SKILL.md +145 -0
- package/generated/skills/toolu--orchestrator/references/delegation-cost.md +104 -0
- package/generated/skills/toolu--orchestrator/references/model-routing.md +112 -0
- package/generated/skills/toolu--plan/SKILL.md +56 -0
- package/generated/skills/toolu--plan/references/ledger.md +18 -0
- package/generated/skills/toolu--plan-review/SKILL.md +48 -0
- package/generated/skills/toolu--review-and-commit--skill/SKILL.md +10 -0
- package/generated/skills/toolu--setup/SKILL.md +24 -0
- package/generated/skills/toolu--setup/scripts/setup.sh +206 -0
- package/generated/skills/toolu--spec/SKILL.md +67 -0
- package/generated/skills/toolu--spec-review/SKILL.md +50 -0
- package/generated/skills/toolu--test/SKILL.md +49 -0
- package/package.json +51 -0
- package/plugins/agent-browser/.claude-plugin/plugin.json +18 -0
- package/plugins/agent-browser/.codex-plugin/plugin.json +7 -0
- package/plugins/agent-browser/README.md +18 -0
- package/plugins/agent-browser/hooks/hooks.json +15 -0
- package/plugins/agent-browser/hooks/session-start.sh +42 -0
- package/plugins/agent-browser/skills/agent-browser/SKILL.md +77 -0
- package/plugins/agent-browser/skills/agent-browser/scripts/agent-browser.sh +80 -0
- package/plugins/ast-grep/.claude-plugin/plugin.json +18 -0
- package/plugins/ast-grep/.codex-plugin/plugin.json +7 -0
- package/plugins/ast-grep/README.md +21 -0
- package/plugins/ast-grep/hooks/hooks.json +15 -0
- package/plugins/ast-grep/hooks/post-tools.d/byte-savings.sh +67 -0
- package/plugins/ast-grep/hooks/pre-tools.d/search-nudge.sh +170 -0
- package/plugins/ast-grep/hooks/register.sh +68 -0
- package/plugins/ast-grep/scripts/byte-savings-report.sh +28 -0
- package/plugins/ast-grep/skills/ast-grep/SKILL.md +127 -0
- package/plugins/ast-grep/skills/ast-grep/references/ast-grep-advanced.md +44 -0
- package/plugins/ast-grep/skills/ast-grep/scripts/ast-grep.sh +103 -0
- package/plugins/context7/.claude-plugin/plugin.json +18 -0
- package/plugins/context7/.codex-plugin/plugin.json +7 -0
- package/plugins/context7/README.md +19 -0
- package/plugins/context7/hooks/hooks.json +15 -0
- package/plugins/context7/hooks/session-start.sh +42 -0
- package/plugins/context7/skills/context7/SKILL.md +64 -0
- package/plugins/context7/skills/context7/scripts/search.sh +151 -0
- package/plugins/exa-search/.claude-plugin/plugin.json +18 -0
- package/plugins/exa-search/.codex-plugin/plugin.json +7 -0
- package/plugins/exa-search/README.md +19 -0
- package/plugins/exa-search/hooks/hooks.json +15 -0
- package/plugins/exa-search/hooks/session-start.sh +34 -0
- package/plugins/exa-search/skills/exa-search/SKILL.md +91 -0
- package/plugins/exa-search/skills/exa-search/scripts/search.sh +221 -0
- package/plugins/jev/.claude-plugin/plugin.json +19 -0
- package/plugins/jev/.codex-plugin/plugin.json +7 -0
- package/plugins/jev/README.md +61 -0
- package/plugins/jev/hooks/hooks.json +25 -0
- package/plugins/jev/hooks/lib/common.sh +49 -0
- package/plugins/jev/hooks/session-start.sh +45 -0
- package/plugins/jev/hooks/user-prompt-submit.sh +41 -0
- package/plugins/jev/skills/jev/SKILL.md +81 -0
- package/plugins/jev/skills/jev/evals/README.md +98 -0
- package/plugins/jev/skills/jev/references/problem-solving.md +198 -0
- package/plugins/jev/skills/jev/scripts/jev.sh +405 -0
- package/plugins/jira/.claude-plugin/plugin.json +18 -0
- package/plugins/jira/.codex-plugin/plugin.json +7 -0
- package/plugins/jira/README.md +42 -0
- package/plugins/jira/hooks/hooks.json +15 -0
- package/plugins/jira/hooks/session-start.sh +34 -0
- package/plugins/jira/skills/jira/SKILL.md +123 -0
- package/plugins/jira/skills/jira/scripts/jira.sh +70 -0
- package/plugins/jira/skills/jira/scripts/lib/adf.sh +17 -0
- package/plugins/jira/skills/jira/scripts/lib/attachment.sh +94 -0
- package/plugins/jira/skills/jira/scripts/lib/board.sh +58 -0
- package/plugins/jira/skills/jira/scripts/lib/http.sh +184 -0
- package/plugins/jira/skills/jira/scripts/lib/issue.sh +159 -0
- package/plugins/jira/skills/jira/scripts/lib/paginate.sh +45 -0
- package/plugins/jira/skills/jira/scripts/lib/plan-parse.sh +70 -0
- package/plugins/jira/skills/jira/scripts/lib/plan-run.sh +115 -0
- package/plugins/jira/skills/jira/scripts/lib/plan-store.sh +168 -0
- package/plugins/jira/skills/jira/scripts/lib/plan.sh +108 -0
- package/plugins/jira/skills/jira/scripts/lib/project.sh +42 -0
- package/plugins/jira/skills/jira/scripts/lib/raw.sh +15 -0
- package/plugins/jira/skills/jira/scripts/lib/search.sh +41 -0
- package/plugins/jira/skills/jira/scripts/lib/sprint.sh +88 -0
- package/plugins/jira/skills/jira/scripts/lib/user.sh +45 -0
- package/plugins/jira/skills/jira/scripts/lib/worklog.sh +56 -0
- package/plugins/pr-babysit/.claude-plugin/plugin.json +25 -0
- package/plugins/pr-babysit/.codex-plugin/plugin.json +7 -0
- package/plugins/pr-babysit/README.md +38 -0
- package/plugins/pr-babysit/commands/babysit.md +13 -0
- package/plugins/pr-babysit/hooks/check-toolu.sh +20 -0
- package/plugins/pr-babysit/hooks/hooks.json +15 -0
- package/plugins/pr-babysit/scripts/babysit-tick.sh +133 -0
- package/plugins/pr-babysit/scripts/collect-pr.sh +224 -0
- package/plugins/pr-babysit/scripts/lib/common.sh +146 -0
- package/plugins/pr-babysit/scripts/lib/gh.sh +150 -0
- package/plugins/pr-babysit/scripts/lib/lock.sh +84 -0
- package/plugins/pr-babysit/scripts/lib/normalize.sh +98 -0
- package/plugins/pr-babysit/scripts/lib/state.sh +37 -0
- package/plugins/pr-babysit/scripts/parse-verdict.sh +171 -0
- package/plugins/pr-babysit/scripts/record.sh +69 -0
- package/plugins/pr-babysit/scripts/reduce-state.sh +245 -0
- package/plugins/pr-babysit/scripts/reply-thread.sh +90 -0
- package/plugins/pr-babysit/scripts/resolve-thread.sh +71 -0
- package/plugins/pr-babysit/skills/babysit/SKILL.md +31 -0
- package/plugins/pr-babysit/skills/babysit/references/helper.md +209 -0
- package/plugins/pr-babysit/workflows/babysit.md +722 -0
- package/plugins/python-quality/.claude-plugin/plugin.json +24 -0
- package/plugins/python-quality/.codex-plugin/plugin.json +6 -0
- package/plugins/python-quality/README.md +23 -0
- package/plugins/python-quality/hooks/check-toolu.sh +20 -0
- package/plugins/python-quality/hooks/concerns/00-preamble.sh +85 -0
- package/plugins/python-quality/hooks/concerns/10-size-file.sh +6 -0
- package/plugins/python-quality/hooks/concerns/20-tests.sh +37 -0
- package/plugins/python-quality/hooks/concerns/30-suppression.sh +33 -0
- package/plugins/python-quality/hooks/concerns/50-size-fn.sh +49 -0
- package/plugins/python-quality/hooks/concerns/70-no-mocks.sh +76 -0
- package/plugins/python-quality/hooks/concerns/90-docs.sh +52 -0
- package/plugins/python-quality/hooks/concerns/99-finalize.sh +36 -0
- package/plugins/python-quality/hooks/hooks.json +19 -0
- package/plugins/python-quality/hooks/register.sh +65 -0
- package/plugins/rust-quality/.claude-plugin/plugin.json +24 -0
- package/plugins/rust-quality/.codex-plugin/plugin.json +6 -0
- package/plugins/rust-quality/README.md +24 -0
- package/plugins/rust-quality/hooks/check-toolu.sh +20 -0
- package/plugins/rust-quality/hooks/concerns/00-preamble.sh +93 -0
- package/plugins/rust-quality/hooks/concerns/10-size-file.sh +10 -0
- package/plugins/rust-quality/hooks/concerns/20-tests.sh +84 -0
- package/plugins/rust-quality/hooks/concerns/30-suppression.sh +26 -0
- package/plugins/rust-quality/hooks/concerns/40-unsafe.sh +39 -0
- package/plugins/rust-quality/hooks/concerns/50-size-fn.sh +38 -0
- package/plugins/rust-quality/hooks/concerns/55-size-impl.sh +28 -0
- package/plugins/rust-quality/hooks/concerns/60-error-handling.sh +149 -0
- package/plugins/rust-quality/hooks/concerns/70-no-mocks.sh +139 -0
- package/plugins/rust-quality/hooks/concerns/90-docs.sh +32 -0
- package/plugins/rust-quality/hooks/concerns/99-finalize.sh +36 -0
- package/plugins/rust-quality/hooks/hooks.json +19 -0
- package/plugins/rust-quality/hooks/register.sh +65 -0
- package/plugins/statusline/.claude-plugin/plugin.json +19 -0
- package/plugins/statusline/.codex-plugin/plugin.json +7 -0
- package/plugins/statusline/README.md +108 -0
- package/plugins/statusline/commands/setup.md +30 -0
- package/plugins/statusline/hooks/hooks.json +15 -0
- package/plugins/statusline/hooks/session-start.sh +45 -0
- package/plugins/statusline/scripts/collect-status.sh +147 -0
- package/plugins/statusline/scripts/setup.sh +93 -0
- package/plugins/statusline/scripts/status.sh +55 -0
- package/plugins/statusline/skills/status/SKILL.md +13 -0
- package/plugins/statusline/statusline.sh +177 -0
- package/plugins/toolu/.claude-plugin/plugin.json +19 -0
- package/plugins/toolu/.codex-plugin/plugin.json +7 -0
- package/plugins/toolu/README.md +35 -0
- package/plugins/toolu/agents/.gitkeep +0 -0
- package/plugins/toolu/agents/architect.md +52 -0
- package/plugins/toolu/agents/deep-explore.md +65 -0
- package/plugins/toolu/agents/implementer.md +50 -0
- package/plugins/toolu/agents/quick-task.md +54 -0
- package/plugins/toolu/agents/research-agent.md +107 -0
- package/plugins/toolu/assets/agents/architect.toml +9 -0
- package/plugins/toolu/assets/agents/deep-explore.toml +9 -0
- package/plugins/toolu/assets/agents/implementer.toml +9 -0
- package/plugins/toolu/assets/agents/quick-task.toml +9 -0
- package/plugins/toolu/assets/agents/research-agent.toml +9 -0
- package/plugins/toolu/commands/.gitkeep +0 -0
- package/plugins/toolu/commands/commit.md +5 -0
- package/plugins/toolu/commands/review-and-commit.md +5 -0
- package/plugins/toolu/hooks/docs/gates.md +157 -0
- package/plugins/toolu/hooks/docs/model-routing.md +14 -0
- package/plugins/toolu/hooks/docs/post-compaction.md +3 -0
- package/plugins/toolu/hooks/docs/push-review.md +123 -0
- package/plugins/toolu/hooks/docs/session-start-python.md +3 -0
- package/plugins/toolu/hooks/docs/session-start-rust.md +4 -0
- package/plugins/toolu/hooks/docs/session-start-ts.md +3 -0
- package/plugins/toolu/hooks/docs/session-start.md +16 -0
- package/plugins/toolu/hooks/hooks.json +76 -0
- package/plugins/toolu/hooks/lib/config.sh +355 -0
- package/plugins/toolu/hooks/lib/detect.sh +929 -0
- package/plugins/toolu/hooks/lib/diff-sha.sh +32 -0
- package/plugins/toolu/hooks/lib/dispatch.sh +365 -0
- package/plugins/toolu/hooks/lib/docs-sync-config.sh +81 -0
- package/plugins/toolu/hooks/lib/edit-records.sh +157 -0
- package/plugins/toolu/hooks/lib/gate-file.sh +201 -0
- package/plugins/toolu/hooks/lib/gate-mode.sh +244 -0
- package/plugins/toolu/hooks/lib/host.sh +205 -0
- package/plugins/toolu/hooks/lib/permissions.sh +125 -0
- package/plugins/toolu/hooks/lib/plan-ledger-parse.sh +219 -0
- package/plugins/toolu/hooks/lib/plan-ledger-preflight.sh +137 -0
- package/plugins/toolu/hooks/lib/plan-ledger.sh +749 -0
- package/plugins/toolu/hooks/lib/push-waiver.sh +116 -0
- package/plugins/toolu/hooks/lib/quality-config.sh +184 -0
- package/plugins/toolu/hooks/lib/registry.sh +72 -0
- package/plugins/toolu/hooks/lib/state-sweeper.sh +194 -0
- package/plugins/toolu/hooks/lib/telemetry.sh +94 -0
- package/plugins/toolu/hooks/lib/verdict.sh +477 -0
- package/plugins/toolu/hooks/post-tools/mod.sh +42 -0
- package/plugins/toolu/hooks/post-tools/modules/gate-status.sh +140 -0
- package/plugins/toolu/hooks/post-tools/modules/push-waiver.sh +97 -0
- package/plugins/toolu/hooks/pre-compact.sh +18 -0
- package/plugins/toolu/hooks/pre-tools/agent-tier.sh +111 -0
- package/plugins/toolu/hooks/pre-tools/mod.sh +37 -0
- package/plugins/toolu/hooks/pre-tools/modules/bash-commands.sh +187 -0
- package/plugins/toolu/hooks/pre-tools/modules/code-edit-rules.sh +78 -0
- package/plugins/toolu/hooks/pre-tools/modules/commit-gate.sh +70 -0
- package/plugins/toolu/hooks/pre-tools/modules/docs-sync.sh +137 -0
- package/plugins/toolu/hooks/pre-tools/modules/mcp-blocker.sh +151 -0
- package/plugins/toolu/hooks/pre-tools/modules/plan-ledger.sh +201 -0
- package/plugins/toolu/hooks/pre-tools/modules/protected-files.sh +177 -0
- package/plugins/toolu/hooks/pre-tools/modules/push-review.sh +273 -0
- package/plugins/toolu/hooks/pre-tools/modules/quality-gate.sh +87 -0
- package/plugins/toolu/hooks/session-start.sh +379 -0
- package/plugins/toolu/hooks/user-prompt-submit.sh +197 -0
- package/plugins/toolu/scripts/context-budget.sh +111 -0
- package/plugins/toolu/scripts/debug-log.sh +98 -0
- package/plugins/toolu/scripts/debug-stack.sh +108 -0
- package/plugins/toolu/scripts/debug-testfail.sh +97 -0
- package/plugins/toolu/settings/.gitkeep +0 -0
- package/plugins/toolu/settings/README.md +127 -0
- package/plugins/toolu/settings/bash-allowlist.txt +12 -0
- package/plugins/toolu/settings/bash-denylist.txt +25 -0
- package/plugins/toolu/settings/code-edit-rules.json +67 -0
- package/plugins/toolu/settings/commit-prefixes.txt +12 -0
- package/plugins/toolu/settings/mcp-blocklist.txt +8 -0
- package/plugins/toolu/settings/permissions.fragment.json +36 -0
- package/plugins/toolu/settings/protected-files.txt +30 -0
- package/plugins/toolu/settings/rust-unsafe-exemptions.txt +5 -0
- package/plugins/toolu/settings/toolu.config.example.json +130 -0
- package/plugins/toolu/skills/.gitkeep +0 -0
- package/plugins/toolu/skills/brainstorm/SKILL.md +57 -0
- package/plugins/toolu/skills/brainstorm/references/design-questions.md +36 -0
- package/plugins/toolu/skills/commit/SKILL.md +11 -0
- package/plugins/toolu/skills/debug/SKILL.md +54 -0
- package/plugins/toolu/skills/debug/evals/README.md +17 -0
- package/plugins/toolu/skills/debug/evals/cases.json +24 -0
- package/plugins/toolu/skills/debug/evals/results.json +34 -0
- package/plugins/toolu/skills/deep-research/SKILL.md +51 -0
- package/plugins/toolu/skills/execution/SKILL.md +77 -0
- package/plugins/toolu/skills/orchestrator/SKILL.md +145 -0
- package/plugins/toolu/skills/orchestrator/references/delegation-cost.md +104 -0
- package/plugins/toolu/skills/orchestrator/references/model-routing.md +112 -0
- package/plugins/toolu/skills/plan/SKILL.md +56 -0
- package/plugins/toolu/skills/plan/references/ledger.md +18 -0
- package/plugins/toolu/skills/plan-review/SKILL.md +48 -0
- package/plugins/toolu/skills/review-and-commit/SKILL.md +10 -0
- package/plugins/toolu/skills/setup/SKILL.md +24 -0
- package/plugins/toolu/skills/setup/scripts/setup.sh +206 -0
- package/plugins/toolu/skills/spec/SKILL.md +67 -0
- package/plugins/toolu/skills/spec-review/SKILL.md +50 -0
- package/plugins/toolu/skills/test/SKILL.md +49 -0
- package/plugins/toolu/workflows/commit.md +21 -0
- package/plugins/toolu/workflows/host-mapping.md +22 -0
- package/plugins/toolu/workflows/review-and-commit.md +54 -0
- package/plugins/toolu/workflows/semantic-judgments.md +35 -0
- package/plugins/toolu-review/.claude-plugin/plugin.json +18 -0
- package/plugins/toolu-review/.codex-plugin/plugin.json +7 -0
- package/plugins/toolu-review/README.md +17 -0
- package/plugins/toolu-review/hooks/hooks.json +15 -0
- package/plugins/toolu-review/hooks/session-start.sh +34 -0
- package/plugins/toolu-review/skills/review/SKILL.md +90 -0
- package/plugins/toolu-review/skills/review/scripts/write-state.sh +189 -0
- package/plugins/ts-quality/.claude-plugin/plugin.json +24 -0
- package/plugins/ts-quality/.codex-plugin/plugin.json +6 -0
- package/plugins/ts-quality/README.md +24 -0
- package/plugins/ts-quality/hooks/check-toolu.sh +20 -0
- package/plugins/ts-quality/hooks/concerns/00-preamble.sh +123 -0
- package/plugins/ts-quality/hooks/concerns/10-imports.sh +33 -0
- package/plugins/ts-quality/hooks/concerns/15-type-as.sh +9 -0
- package/plugins/ts-quality/hooks/concerns/20-tests.sh +26 -0
- package/plugins/ts-quality/hooks/concerns/25-size-file.sh +40 -0
- package/plugins/ts-quality/hooks/concerns/30-size-fn.sh +53 -0
- package/plugins/ts-quality/hooks/concerns/35-react-hooks.sh +7 -0
- package/plugins/ts-quality/hooks/concerns/40-factory.sh +5 -0
- package/plugins/ts-quality/hooks/concerns/45-typeguard.sh +6 -0
- package/plugins/ts-quality/hooks/concerns/50-type-dup.sh +16 -0
- package/plugins/ts-quality/hooks/concerns/55-naming.sh +11 -0
- package/plugins/ts-quality/hooks/concerns/60-console.sh +6 -0
- package/plugins/ts-quality/hooks/concerns/65-suppression.sh +12 -0
- package/plugins/ts-quality/hooks/concerns/70-ui-confirm.sh +8 -0
- package/plugins/ts-quality/hooks/concerns/72-ui-radix.sh +12 -0
- package/plugins/ts-quality/hooks/concerns/74-react-props.sh +6 -0
- package/plugins/ts-quality/hooks/concerns/76-toast.sh +10 -0
- package/plugins/ts-quality/hooks/concerns/78-error-ast.sh +242 -0
- package/plugins/ts-quality/hooks/concerns/80-throw-literal.sh +23 -0
- package/plugins/ts-quality/hooks/concerns/85-no-mocks.sh +109 -0
- package/plugins/ts-quality/hooks/concerns/90-duplication.sh +30 -0
- package/plugins/ts-quality/hooks/concerns/92-docs.sh +36 -0
- package/plugins/ts-quality/hooks/concerns/94-handler.sh +15 -0
- package/plugins/ts-quality/hooks/concerns/99-finalize.sh +46 -0
- package/plugins/ts-quality/hooks/hooks.json +19 -0
- package/plugins/ts-quality/hooks/register.sh +65 -0
- package/src/adapter/__tests__/evaluate.test.ts +82 -0
- package/src/adapter/__tests__/permission-map.test.ts +91 -0
- package/src/adapter/evaluate.ts +54 -0
- package/src/adapter/permission-map.ts +177 -0
- package/src/bootstrap/__tests__/bootstrap.test.ts +148 -0
- package/src/bootstrap/entrypoint.ts +18 -0
- package/src/bootstrap/index.ts +5 -0
- package/src/bootstrap/readiness.ts +59 -0
- package/src/bootstrap/result.ts +21 -0
- package/src/bootstrap/runtime.ts +99 -0
- package/src/bootstrap/test-helpers.ts +25 -0
- package/src/host/__tests__/host.test.ts +37 -0
- package/src/host/detect.ts +46 -0
- package/src/host/index.ts +10 -0
- package/src/host/roots.ts +59 -0
- package/src/host/types.ts +14 -0
- package/src/inventory/__tests__/inventory.test.ts +69 -0
- package/src/inventory/index.ts +9 -0
- package/src/inventory/manifest.ts +46 -0
- package/src/inventory/scan.ts +66 -0
- package/src/inventory/selection.ts +92 -0
- package/src/inventory/types.ts +22 -0
- package/src/lifecycle/__tests__/lifecycle.test.ts +7 -0
- package/src/lifecycle/index.ts +2 -0
- package/src/lifecycle/table.ts +42 -0
- package/src/plugin/__tests__/plugin-stub.test.ts +6 -0
- package/src/plugin/plugin-stub.ts +7 -0
- package/src/plugin/toolu.ts +142 -0
- package/src/preflight/__tests__/preflight.test.ts +20 -0
- package/src/preflight/check.ts +75 -0
- package/src/preflight/index.ts +2 -0
- package/src/select/__tests__/select.test.ts +52 -0
- package/src/select/index.ts +2 -0
- package/src/select/resolve.ts +87 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Pre-tool check: on `git push`, flag when the branch diff changes code but
|
|
3
|
+
# touches no documentation surface — the docs-sync backstop for the toolu
|
|
4
|
+
# workflow's "Docs in sync" convention. Silenced by a diff-sha-keyed
|
|
5
|
+
# attestation the agent writes when no doc change is warranted.
|
|
6
|
+
#
|
|
7
|
+
# Project-agnostic. Behavior is controlled by `docsSync.mode`
|
|
8
|
+
# (advise|block|off, via toolu_string — unrecognized values warn and fall
|
|
9
|
+
# back to advise):
|
|
10
|
+
# - advise (default): emits hookSpecificOutput.additionalContext (merged by
|
|
11
|
+
# dispatch.sh) and NEVER a permissionDecision — it informs, it does not
|
|
12
|
+
# gate (push-review.sh is the gate).
|
|
13
|
+
# - block: denies the push (permissionDecision: "deny") when code changed
|
|
14
|
+
# with no doc surface and no matching attestation. A valid attestation
|
|
15
|
+
# still allows the push in this mode.
|
|
16
|
+
# - off: the check is fully disabled — no telemetry, no output, exit 0.
|
|
17
|
+
# Not just "suppress the message": an off branch must be indistinguishable
|
|
18
|
+
# from docs-sync not existing at all.
|
|
19
|
+
#
|
|
20
|
+
# Inputs (exported by pre-tools/mod.sh): $tool_name, $input (JSON, also stdin).
|
|
21
|
+
# Attestation: <host-state>/tmp/docs-sync/<branch-slug>.json (override $DOCS_SYNC_STATE_DIR).
|
|
22
|
+
# Base branch: $DOCS_SYNC_BASE override, else detect_base_branch.
|
|
23
|
+
|
|
24
|
+
# pipefail so a `git diff | git hash-object` failure surfaces rather than
|
|
25
|
+
# yielding the well-known empty-blob SHA from a broken pipe.
|
|
26
|
+
set -o pipefail
|
|
27
|
+
|
|
28
|
+
: "${tool_name:=}"
|
|
29
|
+
: "${input:=}"
|
|
30
|
+
|
|
31
|
+
_toolu_lib="${TOOLU_LIB_DIR:-${BASH_SOURCE%/*}/../../lib}"
|
|
32
|
+
# shellcheck source=../../lib/detect.sh
|
|
33
|
+
. "$_toolu_lib/detect.sh"
|
|
34
|
+
# shellcheck source=../../lib/config.sh
|
|
35
|
+
. "$_toolu_lib/config.sh"
|
|
36
|
+
# shellcheck source=../../lib/docs-sync-config.sh
|
|
37
|
+
. "$_toolu_lib/docs-sync-config.sh"
|
|
38
|
+
# shellcheck source=../../lib/diff-sha.sh
|
|
39
|
+
. "$_toolu_lib/diff-sha.sh"
|
|
40
|
+
# shellcheck source=../../lib/telemetry.sh
|
|
41
|
+
. "$_toolu_lib/telemetry.sh"
|
|
42
|
+
# shellcheck source=../../lib/gate-mode.sh
|
|
43
|
+
. "$_toolu_lib/gate-mode.sh"
|
|
44
|
+
|
|
45
|
+
# Degrade silent on anything that isn't a real push in a real branch.
|
|
46
|
+
[[ "$tool_name" != "Bash" ]] && exit 0
|
|
47
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
48
|
+
command -v git >/dev/null 2>&1 || exit 0
|
|
49
|
+
|
|
50
|
+
command=$(echo "$input" | jq -r '.tool_input.command // ""')
|
|
51
|
+
is_git_push "$command" || exit 0
|
|
52
|
+
|
|
53
|
+
current_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
|
54
|
+
# Detached HEAD or no branch — nothing to slug/key on; stay silent.
|
|
55
|
+
[[ -z "$current_branch" || "$current_branch" == "HEAD" ]] && exit 0
|
|
56
|
+
|
|
57
|
+
base_branch="${DOCS_SYNC_BASE:-$(detect_base_branch)}"
|
|
58
|
+
# Missing base or pushing the base itself: no feature diff to judge.
|
|
59
|
+
git rev-parse --verify --quiet "$base_branch" >/dev/null 2>&1 || exit 0
|
|
60
|
+
[[ "$current_branch" == "$base_branch" ]] && exit 0
|
|
61
|
+
|
|
62
|
+
# Changed paths drive surface matching; an empty list means nothing to nudge.
|
|
63
|
+
changed=$(git diff --name-only "${base_branch}...HEAD" 2>/dev/null || echo "")
|
|
64
|
+
[[ -z "$changed" ]] && exit 0
|
|
65
|
+
|
|
66
|
+
# Content-addressed diff sha (mirrors push-review.sh) keys the attestation, so
|
|
67
|
+
# changing the code invalidates a stale "not-needed" attestation.
|
|
68
|
+
diff_sha=$(toolu_diff_sha . "$base_branch")
|
|
69
|
+
|
|
70
|
+
# Classify the changed paths against the configurable glob sets. `case` fnmatch
|
|
71
|
+
# treats a single `*` as crossing `/`, so `docs/*.md` covers nested paths.
|
|
72
|
+
surfaces=$(docs_sync_surfaces)
|
|
73
|
+
surface_excludes=$(docs_sync_surface_excludes)
|
|
74
|
+
code_surfaces=$(docs_sync_code_surfaces)
|
|
75
|
+
|
|
76
|
+
_matches_any() {
|
|
77
|
+
local path="$1" glob
|
|
78
|
+
while IFS= read -r glob; do
|
|
79
|
+
[[ -z "$glob" ]] && continue
|
|
80
|
+
# shellcheck disable=SC2254 # $glob is an intentional fnmatch pattern.
|
|
81
|
+
case "$path" in
|
|
82
|
+
$glob) return 0 ;;
|
|
83
|
+
esac
|
|
84
|
+
done <<< "$2"
|
|
85
|
+
return 1
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
has_doc=0 has_code=0
|
|
89
|
+
while IFS= read -r path; do
|
|
90
|
+
[[ -z "$path" ]] && continue
|
|
91
|
+
# A doc hit is a surface match that is NOT carved out by an exclude (e.g.
|
|
92
|
+
# release notes, which `docs/*.md` would otherwise swallow since `*` crosses `/`).
|
|
93
|
+
if _matches_any "$path" "$surfaces" && ! _matches_any "$path" "$surface_excludes"; then
|
|
94
|
+
has_doc=1
|
|
95
|
+
fi
|
|
96
|
+
if _matches_any "$path" "$code_surfaces"; then has_code=1; fi
|
|
97
|
+
done <<< "$changed"
|
|
98
|
+
|
|
99
|
+
# Nudge only when code changed AND no doc surface did.
|
|
100
|
+
{ [[ "$has_code" == 1 && "$has_doc" == 0 ]]; } || exit 0
|
|
101
|
+
|
|
102
|
+
# off must be indistinguishable from the module not existing: no telemetry
|
|
103
|
+
# (not even docs_attested for an otherwise-valid attestation below), no
|
|
104
|
+
# output. Read here (rather than at top-of-file) so an off branch still pays
|
|
105
|
+
# no cost beyond the code/doc classification already needed either way.
|
|
106
|
+
mode=$(toolu_gate_mode docsSync)
|
|
107
|
+
[[ "$mode" == "off" ]] && exit 0
|
|
108
|
+
|
|
109
|
+
# Attestation gate: a fresh (diff-sha-matching) attestation silences the nudge
|
|
110
|
+
# (in both advise and block mode).
|
|
111
|
+
slug=$(branch_slug "$current_branch")
|
|
112
|
+
state_dir=${DOCS_SYNC_STATE_DIR:-$(toolu_project_state_dir docs-sync)}
|
|
113
|
+
state_file="$state_dir/${slug}.json"
|
|
114
|
+
if [[ -f "$state_file" && -n "$diff_sha" ]]; then
|
|
115
|
+
attested_sha=$(jq -r '.diff_sha // ""' "$state_file" 2>/dev/null || echo "")
|
|
116
|
+
if [[ "$attested_sha" == "$diff_sha" ]]; then
|
|
117
|
+
attested_decision=$(jq -r '.decision // ""' "$state_file" 2>/dev/null || echo "")
|
|
118
|
+
telemetry_append "$(detect_project_root)" "docs_attested" \
|
|
119
|
+
"$(jq -cn --arg d "$attested_decision" '{decision: $d}')"
|
|
120
|
+
exit 0
|
|
121
|
+
fi
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
telemetry_append "$(detect_project_root)" "docs_nudge"
|
|
125
|
+
|
|
126
|
+
# Shared instructions (the attestation escape hatch) for both modes; only the
|
|
127
|
+
# closing sentence and the JSON shape (additionalContext vs. deny) differ.
|
|
128
|
+
# The instructions (and the attestation escape hatch) are the same in every
|
|
129
|
+
# mode; only the closing sentence changes, because what happens next does.
|
|
130
|
+
case "$mode" in
|
|
131
|
+
block) consequence="Push denied until a doc is updated or a valid attestation is written." ;;
|
|
132
|
+
ask) consequence="Approve to push anyway, or update the doc first." ;;
|
|
133
|
+
*) consequence="Advisory only — this does not block the push." ;;
|
|
134
|
+
esac
|
|
135
|
+
|
|
136
|
+
toolu_gate_emit "$mode" "docs-sync: this branch changes code but no documentation surface (README / docs/*.md / SKILL.md). Update the doc that describes this behavior, OR attest none is needed by writing $state_file with { \"version\": 1, \"diff_sha\": \"$diff_sha\", \"decision\": \"not-needed\", \"note\": \"why\" }. $consequence"
|
|
137
|
+
exit 0
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Block MCP tool usage for listed servers — redirect to CLI scripts.
|
|
3
|
+
# Data-driven: server name prefixes come from $TOOLU_SETTINGS_DIR/mcp-blocklist.txt.
|
|
4
|
+
#
|
|
5
|
+
# Delivery is a MODE (gates.mcpBlocker.mode), not a constant. It ships as
|
|
6
|
+
# `ask`: the user is prompted and decides per call, instead of the call being
|
|
7
|
+
# refused with no way through. `block` restores the old hard deny; `advise`
|
|
8
|
+
# only warns; `off` disables it. On a host that cannot prompt, `ask` degrades
|
|
9
|
+
# to `block` — see lib/gate-mode.sh.
|
|
10
|
+
#
|
|
11
|
+
# Inputs (from parent dispatcher pre-tools/mod.sh, via `export`):
|
|
12
|
+
# $tool_name - name of the tool being invoked
|
|
13
|
+
|
|
14
|
+
: "${tool_name:=}"
|
|
15
|
+
|
|
16
|
+
_toolu_lib="${TOOLU_LIB_DIR:-${BASH_SOURCE%/*}/../../lib}"
|
|
17
|
+
# shellcheck source=../../lib/detect.sh
|
|
18
|
+
. "$_toolu_lib/detect.sh"
|
|
19
|
+
# shellcheck source=../../lib/config.sh
|
|
20
|
+
. "$_toolu_lib/config.sh"
|
|
21
|
+
# shellcheck source=../../lib/gate-mode.sh
|
|
22
|
+
. "$_toolu_lib/gate-mode.sh"
|
|
23
|
+
|
|
24
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
25
|
+
|
|
26
|
+
# This module is wired as a STANDALONE PreToolUse hook (matcher mcp__), so it
|
|
27
|
+
# does NOT go through pre-tools/mod.sh and the parent never `export`s
|
|
28
|
+
# tool_name. Claude Code delivers the hook input as JSON on STDIN, including
|
|
29
|
+
# .tool_name. When tool_name is not in the env, read it from stdin. Env keeps
|
|
30
|
+
# precedence so env-driven tests still pass; stdin is consumed exactly once.
|
|
31
|
+
if [[ -z "$tool_name" ]]; then
|
|
32
|
+
_mcp_stdin=$(cat 2>/dev/null || true)
|
|
33
|
+
if [[ -n "$_mcp_stdin" ]]; then
|
|
34
|
+
tool_name=$(jq -r '.tool_name // empty' <<<"$_mcp_stdin" 2>/dev/null || true)
|
|
35
|
+
fi
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
TOOLU_SETTINGS_DIR=$(toolu_settings_dir)
|
|
39
|
+
LIST_FILE="$TOOLU_SETTINGS_DIR/mcp-blocklist.txt"
|
|
40
|
+
|
|
41
|
+
# Only inspect mcp__<server>__* tool names.
|
|
42
|
+
case "$tool_name" in
|
|
43
|
+
mcp__*__*) ;;
|
|
44
|
+
*) exit 0 ;;
|
|
45
|
+
esac
|
|
46
|
+
|
|
47
|
+
# Fast-path: the matcher is mcp__, so every MCP tool call routes through
|
|
48
|
+
# here. Skip config-load work entirely when there is no
|
|
49
|
+
# blocklist file AND no user/project config to consult — the common case for
|
|
50
|
+
# anyone who has not opted into the gate.
|
|
51
|
+
if [ ! -f "$LIST_FILE" ] && ! toolu_config_exists; then
|
|
52
|
+
exit 0
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# Extract the server segment: mcp__<server>__rest
|
|
56
|
+
rest="${tool_name#mcp__}"
|
|
57
|
+
server="${rest%%__*}"
|
|
58
|
+
|
|
59
|
+
# read_list is sourced from lib/detect.sh.
|
|
60
|
+
|
|
61
|
+
toolu_load_config
|
|
62
|
+
# `objects` guards against a malformed `.mcp` (string/array) — to_entries
|
|
63
|
+
# would error on a non-object; with the guard it is simply a no-op.
|
|
64
|
+
disabled_from_cfg=$(jq -r '.mcp // {} | objects | to_entries[] | select(.value == false) | .key' \
|
|
65
|
+
<<< "${TOOLU_CFG_JSON:-}" 2>/dev/null)
|
|
66
|
+
|
|
67
|
+
file_list="$(read_list "$LIST_FILE")"
|
|
68
|
+
|
|
69
|
+
# Track WHERE the matching entry came from so the deny reason can point the
|
|
70
|
+
# user at the right file to edit.
|
|
71
|
+
blocked_source=""
|
|
72
|
+
# Redirect hint captured from the matching entry (file list only); surfaced in
|
|
73
|
+
# the deny reason so a denied call points at its replacement. Empty when the
|
|
74
|
+
# matched entry carries no hint.
|
|
75
|
+
matched_redirect=""
|
|
76
|
+
match_entry() {
|
|
77
|
+
local list_input="$1" name prefix redirect
|
|
78
|
+
while IFS= read -r name; do
|
|
79
|
+
[ -z "$name" ] && continue
|
|
80
|
+
# An entry may carry an optional " -> <text>" redirect hint after the
|
|
81
|
+
# server prefix (e.g. `claude_ai_Atlassian -> use the jira skill instead`).
|
|
82
|
+
# Split it off so the prefix alone drives the match; without this the arrow
|
|
83
|
+
# suffix would defeat the prefix-match below. Config-derived lists are bare
|
|
84
|
+
# server keys and never carry a hint.
|
|
85
|
+
if [[ "$name" == *" -> "* ]]; then
|
|
86
|
+
prefix="${name%% -> *}"
|
|
87
|
+
redirect="${name#* -> }"
|
|
88
|
+
else
|
|
89
|
+
prefix="$name"
|
|
90
|
+
redirect=""
|
|
91
|
+
fi
|
|
92
|
+
# Trim surrounding whitespace so a stray space can't defeat the exact match.
|
|
93
|
+
prefix="${prefix#"${prefix%%[![:space:]]*}"}"
|
|
94
|
+
prefix="${prefix%"${prefix##*[![:space:]]}"}"
|
|
95
|
+
[ -z "$prefix" ] && continue
|
|
96
|
+
# Prefix match: an entry of `claude_ai_` blocks `claude_ai_Canva`. An
|
|
97
|
+
# exact server name is the special case of a prefix with no suffix.
|
|
98
|
+
case "$server" in
|
|
99
|
+
"$prefix"*) matched_redirect="$redirect"; return 0 ;;
|
|
100
|
+
esac
|
|
101
|
+
done <<< "$list_input"
|
|
102
|
+
return 1
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if match_entry "$file_list"; then
|
|
106
|
+
blocked_source="file"
|
|
107
|
+
elif match_entry "$disabled_from_cfg"; then
|
|
108
|
+
blocked_source="config"
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
if [ -n "$blocked_source" ]; then
|
|
112
|
+
# Same validate-then-fail-closed discipline as protected-files.sh: an
|
|
113
|
+
# unresolvable mode blocks, and says that it did.
|
|
114
|
+
mode_note=""
|
|
115
|
+
mode=$(toolu_gate_mode mcpBlocker) || mode=""
|
|
116
|
+
case " $TOOLU_GATE_MODES " in
|
|
117
|
+
*" $mode "*) ;;
|
|
118
|
+
*)
|
|
119
|
+
printf 'toolu-mcp-blocker: could not resolve gates.mcpBlocker.mode (got %s); blocking\n' \
|
|
120
|
+
"${mode:-<empty>}" >&2
|
|
121
|
+
mode_note=" [toolu could not resolve gates.mcpBlocker.mode, so this failed closed]"
|
|
122
|
+
mode="block"
|
|
123
|
+
;;
|
|
124
|
+
esac
|
|
125
|
+
|
|
126
|
+
if [ "$blocked_source" = "file" ]; then
|
|
127
|
+
origin="listed in settings/mcp-blocklist.txt"
|
|
128
|
+
else
|
|
129
|
+
origin="disabled in your toolu config (mcp.${server}=false)"
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
headline="Claude is calling MCP tool \"$tool_name\", on the blocked server \"$server\" ($origin)."
|
|
133
|
+
detail="Blocked MCP servers are ones this project has decided not to reach through an MCP bridge — usually because a CLI path exists that is auditable and scoped, where the MCP tool is neither."
|
|
134
|
+
[ -n "$matched_redirect" ] && detail="$detail Use instead: $matched_redirect"
|
|
135
|
+
|
|
136
|
+
case "$mode" in
|
|
137
|
+
ask)
|
|
138
|
+
reason=$(toolu_gate_guardrail_warning "$headline" "$detail") || reason=""
|
|
139
|
+
[ -n "$reason" ] || reason="$headline $detail"
|
|
140
|
+
;;
|
|
141
|
+
advise)
|
|
142
|
+
reason="MCP server \"$server\" is blocked ($origin). $detail The call was NOT stopped — gates.mcpBlocker.mode is 'advise'."
|
|
143
|
+
;;
|
|
144
|
+
*)
|
|
145
|
+
reason="MCP server \"$server\" is blocked ($origin). $detail"
|
|
146
|
+
;;
|
|
147
|
+
esac
|
|
148
|
+
|
|
149
|
+
toolu_gate_emit "$mode" "${reason}${mode_note}"
|
|
150
|
+
fi
|
|
151
|
+
exit 0
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Pre-tool gate: block `git push` until every plan-ledger step is fresh-green.
|
|
3
|
+
# READ-ONLY — this gate never runs a step's `check`; it only reads the ledger
|
|
4
|
+
# the checker (plan-ledger.sh) wrote. A step is "fresh-green" iff its recorded
|
|
5
|
+
# status is green AND its recorded diff_sha equals the current branch diff_sha,
|
|
6
|
+
# so a green that predates a later code change is treated as stale, not done.
|
|
7
|
+
#
|
|
8
|
+
# Inputs (from parent dispatcher pre-tools/mod.sh, via `export`):
|
|
9
|
+
# $tool_name - name of the tool being invoked
|
|
10
|
+
# $input - raw JSON payload (also piped to stdin; this module reads the env var)
|
|
11
|
+
#
|
|
12
|
+
# Ledger file: <project-root>/<host-state>/tmp/plan-ledger/<branch-slug>.json
|
|
13
|
+
# Override the parent dir via $LEDGER_DIR for testing.
|
|
14
|
+
#
|
|
15
|
+
# Decisions:
|
|
16
|
+
# - tool != Bash, or not a `git push` -> allow (exit 0, no output).
|
|
17
|
+
# - ledger absent, push diff touches a code file -> allow + stderr advisory.
|
|
18
|
+
# - ledger absent, no code files -> allow silently.
|
|
19
|
+
# - ledger unparseable / version != 1 -> deny (fail closed).
|
|
20
|
+
# - summary.total == 0 / steps empty -> allow (no-op).
|
|
21
|
+
# - every step fresh-green -> allow.
|
|
22
|
+
# - any red/pending/stale step -> deny, listing each.
|
|
23
|
+
# - planLedger.blockOnUncoveredAcs=true + an UNCOVERED spec AC -> deny,
|
|
24
|
+
# naming the AC id(s) (default false: advisory ac_coverage count only).
|
|
25
|
+
|
|
26
|
+
# pipefail so a `git diff | git hash-object` failure surfaces; without it an
|
|
27
|
+
# empty diff stream succeeds and yields the well-known empty-blob SHA.
|
|
28
|
+
set -o pipefail
|
|
29
|
+
|
|
30
|
+
: "${tool_name:=}"
|
|
31
|
+
: "${input:=}"
|
|
32
|
+
|
|
33
|
+
_toolu_lib="${TOOLU_LIB_DIR:-${BASH_SOURCE%/*}/../../lib}"
|
|
34
|
+
# shellcheck source=../../lib/detect.sh
|
|
35
|
+
. "$_toolu_lib/detect.sh"
|
|
36
|
+
# shellcheck source=../../lib/plan-ledger-parse.sh
|
|
37
|
+
. "$_toolu_lib/plan-ledger-parse.sh"
|
|
38
|
+
|
|
39
|
+
[[ "$tool_name" != "Bash" ]] && exit 0
|
|
40
|
+
|
|
41
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
42
|
+
command -v git >/dev/null 2>&1 || exit 0
|
|
43
|
+
|
|
44
|
+
command=$(echo "$input" | jq -r '.tool_input.command // ""')
|
|
45
|
+
|
|
46
|
+
# Push detection (strip_heredocs + boundary-anchored regex) shared via detect.sh.
|
|
47
|
+
is_git_push "$command" || exit 0
|
|
48
|
+
|
|
49
|
+
# Sourced here (past the Edit/Write/Grep and non-push cheap exits above) so
|
|
50
|
+
# every other tool call skips the extra jq-merge/telemetry-lib load these four
|
|
51
|
+
# pull in — only an actual `git push` pays for them.
|
|
52
|
+
# shellcheck source=../../lib/diff-sha.sh
|
|
53
|
+
. "$_toolu_lib/diff-sha.sh"
|
|
54
|
+
# shellcheck source=../../lib/plan-ledger.sh
|
|
55
|
+
. "$_toolu_lib/plan-ledger.sh"
|
|
56
|
+
# shellcheck source=../../lib/telemetry.sh
|
|
57
|
+
. "$_toolu_lib/telemetry.sh"
|
|
58
|
+
# shellcheck source=../../lib/config.sh
|
|
59
|
+
. "$_toolu_lib/config.sh"
|
|
60
|
+
# shellcheck source=../../lib/gate-mode.sh
|
|
61
|
+
. "$_toolu_lib/gate-mode.sh"
|
|
62
|
+
|
|
63
|
+
# How firmly this gate speaks is configurable (lib/gate-mode.sh); `off` means
|
|
64
|
+
# it has nothing to say, so skip the git plumbing entirely.
|
|
65
|
+
mode=$(toolu_gate_mode planLedger)
|
|
66
|
+
[ "$mode" = "off" ] && exit 0
|
|
67
|
+
|
|
68
|
+
# Base branch: env override > detect_base_branch (must agree with the checker).
|
|
69
|
+
base_branch="${PUSH_REVIEW_BASE:-$(detect_base_branch)}"
|
|
70
|
+
|
|
71
|
+
current_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
|
72
|
+
slug=$(branch_slug "$current_branch")
|
|
73
|
+
|
|
74
|
+
# Ledger lives next to push-review's state, keyed by the same branch slug.
|
|
75
|
+
ledger_dir="${LEDGER_DIR:-$(toolu_project_state_dir plan-ledger "$(detect_project_root)")}"
|
|
76
|
+
state_file="$ledger_dir/${slug}.json"
|
|
77
|
+
|
|
78
|
+
# Ledger absent: never deny on absence. Nudge only when the push diff touches a
|
|
79
|
+
# code file (escape hatch for mechanical work — see spec Non-Goal 3).
|
|
80
|
+
if [[ ! -f "$state_file" ]]; then
|
|
81
|
+
code_files=$(git diff --no-color "${base_branch}...HEAD" --name-only 2>/dev/null \
|
|
82
|
+
| grep -E '\.(ts|tsx|js|jsx|rs|sh|py|go)$' || true)
|
|
83
|
+
if [[ -n "$code_files" ]]; then
|
|
84
|
+
echo "plan-ledger: no plan ledger for this change; if non-trivial, run plan" >&2
|
|
85
|
+
fi
|
|
86
|
+
exit 0
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# Read + validate the ledger. Unparseable -> fail closed (deny).
|
|
90
|
+
ledger=$(pl_read_ledger "$state_file") || {
|
|
91
|
+
jq -n --arg file "$state_file" '{
|
|
92
|
+
"hookSpecificOutput": {
|
|
93
|
+
"hookEventName": "PreToolUse",
|
|
94
|
+
"permissionDecision": "deny",
|
|
95
|
+
"permissionDecisionReason": ("plan-ledger: unparseable ledger at " + $file + "; delete and re-run `bash plugins/toolu/hooks/lib/plan-ledger.sh run <plan_doc>`")
|
|
96
|
+
}
|
|
97
|
+
}'
|
|
98
|
+
exit 0
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Schema gate: version must be 1 (fail closed on mismatch).
|
|
102
|
+
version=$(jq -r '.version // ""' <<<"$ledger" 2>/dev/null || echo "")
|
|
103
|
+
if [[ "$version" != "1" ]]; then
|
|
104
|
+
toolu_gate_emit "$mode" "plan-ledger: ledger schema mismatch at $state_file (version=\"$version\", expected 1); delete and re-run \`bash plugins/toolu/hooks/lib/plan-ledger.sh run <plan_doc>\`"
|
|
105
|
+
exit 0
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# No-op ledger: total 0 or no steps -> allow (identical to absent, spec crit12).
|
|
109
|
+
total=$(jq -r '.summary.total // 0' <<<"$ledger" 2>/dev/null || echo "0")
|
|
110
|
+
step_count=$(jq -r '.steps | length' <<<"$ledger" 2>/dev/null || echo "0")
|
|
111
|
+
if [[ "$total" == "0" || "$step_count" == "0" ]]; then
|
|
112
|
+
exit 0
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# Current branch diff_sha — the content hash the checker stamps when a step runs.
|
|
116
|
+
current_diff_sha=$(toolu_diff_sha . "$base_branch")
|
|
117
|
+
if [[ -z "$current_diff_sha" ]]; then
|
|
118
|
+
# git diff failed (disk full, etc): allow so the underlying push surfaces the
|
|
119
|
+
# real error rather than denying on an indeterminate read.
|
|
120
|
+
echo "plan-ledger: git diff ${base_branch}...HEAD failed; allowing push to surface real error" >&2
|
|
121
|
+
exit 0
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# ac_coverage telemetry (report-only counts; never affects the decision below).
|
|
125
|
+
# Mirrors lib/plan-ledger.sh's pl_cmd_status AC-coverage resolution: read the
|
|
126
|
+
# ledger's plan_doc, resolve its **Spec:** field, and reuse pl_ac_coverage_lines
|
|
127
|
+
# (sourced from lib/plan-ledger.sh above) to get the per-AC report, then count
|
|
128
|
+
# covered vs UNCOVERED lines in it. Spec-less plans print no report -> no event.
|
|
129
|
+
ac_plan_doc=$(jq -r '.plan_doc // ""' <<<"$ledger" 2>/dev/null || echo "")
|
|
130
|
+
if [[ -n "$ac_plan_doc" ]]; then
|
|
131
|
+
ac_root=$(detect_project_root)
|
|
132
|
+
[[ -f "$ac_plan_doc" ]] || { [[ -n "$ac_root" && -f "$ac_root/$ac_plan_doc" ]] && ac_plan_doc="$ac_root/$ac_plan_doc"; }
|
|
133
|
+
if [[ -f "$ac_plan_doc" ]]; then
|
|
134
|
+
ac_spec_field=$(pl_doc_field "$ac_plan_doc" Spec)
|
|
135
|
+
ac_spec_path="$ac_spec_field"
|
|
136
|
+
case "$(printf '%s' "$ac_spec_field" | tr '[:upper:]' '[:lower:]')" in
|
|
137
|
+
""|none) : ;;
|
|
138
|
+
*) [[ -f "$ac_spec_path" ]] || { [[ -n "$ac_root" && -f "$ac_root/$ac_spec_field" ]] && ac_spec_path="$ac_root/$ac_spec_field"; } ;;
|
|
139
|
+
esac
|
|
140
|
+
ac_report=$(pl_ac_coverage_lines "$ledger" "$current_diff_sha" "$ac_spec_path" 2>/dev/null)
|
|
141
|
+
if [[ -n "$ac_report" ]]; then
|
|
142
|
+
ac_total=$(grep -c '^ AC-' <<<"$ac_report" || true)
|
|
143
|
+
ac_uncovered=$(grep -Ec '^ AC-[^:]+: UNCOVERED' <<<"$ac_report" || true)
|
|
144
|
+
ac_covered=$(( ac_total - ac_uncovered ))
|
|
145
|
+
telemetry_append "$ac_root" "ac_coverage" \
|
|
146
|
+
"$(jq -cn --argjson c "$ac_covered" --argjson u "$ac_uncovered" '{covered: $c, uncovered: $u}')"
|
|
147
|
+
|
|
148
|
+
# Promotion (spec component 8): planLedger.blockOnUncoveredAcs (default
|
|
149
|
+
# false) turns UNCOVERED spec ACs from an advisory count into a push
|
|
150
|
+
# deny. The event above already recorded the counts either way.
|
|
151
|
+
if [[ "$ac_uncovered" -gt 0 ]] && toolu_flag_true planLedger blockOnUncoveredAcs; then
|
|
152
|
+
ac_uncovered_lines=$(grep -E '^ AC-[^:]+: UNCOVERED' <<<"$ac_report" | sed -E 's/^ //')
|
|
153
|
+
ac_reason=$(printf 'plan-ledger: push blocked — uncovered spec AC id(s):\n%s\n\ncover with a fresh-green step referencing it in ac_refs, or set planLedger.blockOnUncoveredAcs=false' "$ac_uncovered_lines")
|
|
154
|
+
toolu_gate_emit "$mode" "$ac_reason"
|
|
155
|
+
exit 0
|
|
156
|
+
fi
|
|
157
|
+
fi
|
|
158
|
+
fi
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
plan_doc_hint=$(jq -r '.plan_doc // "<plan_doc>"' <<<"$ledger" 2>/dev/null || echo "<plan_doc>")
|
|
162
|
+
|
|
163
|
+
# Build the list of non-fresh-green steps. A step is fresh-green iff
|
|
164
|
+
# status==green AND diff_sha==current; a green with a stale diff_sha is `stale`.
|
|
165
|
+
# `effective` is the status the agent sees: green -> stale when sha drifts.
|
|
166
|
+
blockers=$(jq -r --arg cur "$current_diff_sha" '
|
|
167
|
+
.steps[]
|
|
168
|
+
| (if (.status == "green" and .diff_sha == $cur) then "green"
|
|
169
|
+
elif (.status == "green") then "stale"
|
|
170
|
+
else (.status // "pending") end) as $eff
|
|
171
|
+
| select($eff != "green")
|
|
172
|
+
| (.id // "?") + ": " + $eff + " — " + (.title // "")
|
|
173
|
+
' <<<"$ledger" 2>/dev/null || echo "")
|
|
174
|
+
|
|
175
|
+
# A step may be fresh-green on its own declared `paths` — enough to keep
|
|
176
|
+
# iteration fast, not enough to push on. The gate requires a --verify run:
|
|
177
|
+
# every step judged against the whole branch diff, so a narrow or stale scope
|
|
178
|
+
# declaration cannot hold a green through a change it failed to account for.
|
|
179
|
+
# A ledger written before per-step scope existed has no `verified_sha` key at
|
|
180
|
+
# all, and every step in it was judged against the whole branch diff — which is
|
|
181
|
+
# exactly what --verify means. Requiring the field of those would deny every
|
|
182
|
+
# push until the plan is re-run, for no safety gained. The new contract applies
|
|
183
|
+
# only to ledgers that carry the key.
|
|
184
|
+
has_verified=$(jq -r 'has("verified_sha")' <<<"$ledger" 2>/dev/null || echo "false")
|
|
185
|
+
verified_sha=$(jq -r '.verified_sha // ""' <<<"$ledger" 2>/dev/null || echo "")
|
|
186
|
+
if [[ -z "$blockers" && "$has_verified" == "true" && "$verified_sha" != "$current_diff_sha" ]]; then
|
|
187
|
+
reason=$(printf 'plan-ledger: every step is green, but not verified against the current diff.\n\nScoped runs judge a step on its own `paths`; the push gate judges it on the whole branch.\n\nrun: bash plugins/toolu/hooks/lib/plan-ledger.sh run %s --verify' "$plan_doc_hint")
|
|
188
|
+
toolu_gate_emit "$mode" "$reason"
|
|
189
|
+
exit 0
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
# All fresh-green AND verified -> allow.
|
|
193
|
+
if [[ -z "$blockers" ]]; then
|
|
194
|
+
exit 0
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
# Otherwise deny, listing each non-fresh-green step + remediation.
|
|
198
|
+
plan_doc="$plan_doc_hint"
|
|
199
|
+
reason=$(printf 'plan-ledger: push blocked — steps not fresh-green:\n%s\n\nrun: bash plugins/toolu/hooks/lib/plan-ledger.sh run %s' "$blockers" "$plan_doc")
|
|
200
|
+
toolu_gate_emit "$mode" "$reason"
|
|
201
|
+
exit 0
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Pre-tool check: Protect quality infrastructure files from edits.
|
|
3
|
+
# Data-driven: globs come from $TOOLU_SETTINGS_DIR/protected-files.txt.
|
|
4
|
+
#
|
|
5
|
+
# Delivery is a MODE (gates.protectedFiles.mode), not a constant. It ships as
|
|
6
|
+
# `ask`: the user is prompted with a loud warning and decides, per call. It
|
|
7
|
+
# used to be an unconditional deny with no way through in-session, which is
|
|
8
|
+
# what github.com/Falconiere/toolu/issues/176 reported — an agent could be told
|
|
9
|
+
# "yes, edit that .env.example" by the user and STILL be refused, with the deny
|
|
10
|
+
# text claiming an override existed that did not.
|
|
11
|
+
#
|
|
12
|
+
# `block` restores the old hard deny; `advise` only warns; `off` disables the
|
|
13
|
+
# check. On a host that cannot prompt, `ask` degrades to `block`, never to
|
|
14
|
+
# `advise` — see lib/gate-mode.sh.
|
|
15
|
+
#
|
|
16
|
+
# Inputs (from parent dispatcher pre-tools/mod.sh, via `export`):
|
|
17
|
+
# $tool_name - name of the tool being invoked
|
|
18
|
+
# $input - raw JSON payload on stdin
|
|
19
|
+
|
|
20
|
+
: "${tool_name:=}"
|
|
21
|
+
: "${input:=}"
|
|
22
|
+
|
|
23
|
+
_toolu_lib="${TOOLU_LIB_DIR:-${BASH_SOURCE%/*}/../../lib}"
|
|
24
|
+
# shellcheck source=../../lib/detect.sh
|
|
25
|
+
. "$_toolu_lib/detect.sh"
|
|
26
|
+
# shellcheck source=../../lib/gate-mode.sh
|
|
27
|
+
. "$_toolu_lib/gate-mode.sh"
|
|
28
|
+
|
|
29
|
+
# MultiEdit is in the PreToolUse matcher and carries .tool_input.file_path just
|
|
30
|
+
# like Edit/Write — omitting it here would let a MultiEdit silently bypass the
|
|
31
|
+
# protected-files deny (a security-equivalent hole).
|
|
32
|
+
#
|
|
33
|
+
# Bash/Shell are also in scope: a structured Edit/Write on a protected path is
|
|
34
|
+
# denied, but the same bytes written via `sed -i`, a redirect, or `python3 -c
|
|
35
|
+
# "open(...).write(...)"` had no path-shaped tool_input to check and sailed
|
|
36
|
+
# through — the exact gap reported in
|
|
37
|
+
# github.com/Falconiere/toolu/issues/176. bash_write_targets (lib/detect.sh)
|
|
38
|
+
# extracts candidate write targets from the command; each is checked against
|
|
39
|
+
# the same protected-files.txt patterns as Edit/Write.
|
|
40
|
+
[[ "$tool_name" != "Edit" && "$tool_name" != "Write" && "$tool_name" != "MultiEdit" \
|
|
41
|
+
&& "$tool_name" != "Bash" && "$tool_name" != "Shell" ]] && exit 0
|
|
42
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
43
|
+
|
|
44
|
+
TOOLU_SETTINGS_DIR=$(toolu_settings_dir)
|
|
45
|
+
LIST_FILE="$TOOLU_SETTINGS_DIR/protected-files.txt"
|
|
46
|
+
|
|
47
|
+
[ -f "$LIST_FILE" ] || exit 0
|
|
48
|
+
|
|
49
|
+
declare -a candidates=()
|
|
50
|
+
if [[ "$tool_name" == "Bash" || "$tool_name" == "Shell" ]]; then
|
|
51
|
+
command_str=$(echo "$input" | jq -r '.tool_input.command // ""')
|
|
52
|
+
[ -z "$command_str" ] && exit 0
|
|
53
|
+
# Deduplicated: the same path can surface twice (once in the main command,
|
|
54
|
+
# once via a command substitution the extractor recurses into), and checking
|
|
55
|
+
# one path against the whole pattern list twice buys nothing.
|
|
56
|
+
while IFS= read -r target; do
|
|
57
|
+
[ -n "$target" ] || continue
|
|
58
|
+
case " ${candidates[*]-} " in
|
|
59
|
+
*" $target "*) continue ;;
|
|
60
|
+
esac
|
|
61
|
+
candidates+=("$target")
|
|
62
|
+
done < <(bash_write_targets "$command_str")
|
|
63
|
+
[ ${#candidates[@]} -eq 0 ] && exit 0
|
|
64
|
+
else
|
|
65
|
+
file_path=$(echo "$input" | jq -r '.tool_input.file_path // ""')
|
|
66
|
+
[ -z "$file_path" ] && exit 0
|
|
67
|
+
candidates=("$file_path")
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# read_list is sourced from lib/detect.sh.
|
|
71
|
+
|
|
72
|
+
# glob_match <pattern> <path>
|
|
73
|
+
glob_match() {
|
|
74
|
+
local pattern="$1"
|
|
75
|
+
local path="$2"
|
|
76
|
+
# Enable extended globs for ** to match path segments.
|
|
77
|
+
shopt -s extglob globstar 2>/dev/null || true
|
|
78
|
+
# shellcheck disable=SC2053 # intentional glob match on RHS
|
|
79
|
+
[[ "$path" == $pattern ]]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
list=$(read_list "$LIST_FILE")
|
|
83
|
+
|
|
84
|
+
# Normalize absolute paths (Edit/Write sends absolute; a Bash write target is
|
|
85
|
+
# whatever the command wrote literally, often already relative) into
|
|
86
|
+
# repo-relative so patterns like "hooks/lib/**" can match. Falls back to the
|
|
87
|
+
# input unchanged outside a git repo (test sandboxes etc).
|
|
88
|
+
for candidate in "${candidates[@]}"; do
|
|
89
|
+
rel_path=$(to_relative_path "$candidate")
|
|
90
|
+
matched=""
|
|
91
|
+
while IFS= read -r pattern; do
|
|
92
|
+
[ -z "$pattern" ] && continue
|
|
93
|
+
# Try as-is.
|
|
94
|
+
if glob_match "$pattern" "$rel_path"; then
|
|
95
|
+
matched="$pattern"
|
|
96
|
+
break
|
|
97
|
+
fi
|
|
98
|
+
# For patterns that do not already start with **/, also try anchored
|
|
99
|
+
# anywhere under the repo. This keeps "hooks/lib/**" matching even when
|
|
100
|
+
# the path arrives as e.g. "subtree/hooks/lib/x.sh".
|
|
101
|
+
if [[ "$pattern" != \*\*/* ]] && glob_match "**/$pattern" "$rel_path"; then
|
|
102
|
+
matched="$pattern"
|
|
103
|
+
break
|
|
104
|
+
fi
|
|
105
|
+
# Also check basename for patterns without a path separator.
|
|
106
|
+
if [[ "$pattern" != */* ]]; then
|
|
107
|
+
base=$(basename "$rel_path")
|
|
108
|
+
if glob_match "$pattern" "$base"; then
|
|
109
|
+
matched="$pattern"
|
|
110
|
+
break
|
|
111
|
+
fi
|
|
112
|
+
fi
|
|
113
|
+
done <<< "$list"
|
|
114
|
+
|
|
115
|
+
if [ -n "$matched" ]; then
|
|
116
|
+
# Resolve the delivery mode, then VALIDATE it. Falling through to the
|
|
117
|
+
# emit's `*)` arm would also block, but silently: a deny the user cannot
|
|
118
|
+
# explain is a deny they work around. Fail closed AND say why.
|
|
119
|
+
mode_note=""
|
|
120
|
+
mode=$(toolu_gate_mode protectedFiles) || mode=""
|
|
121
|
+
case " $TOOLU_GATE_MODES " in
|
|
122
|
+
*" $mode "*) ;;
|
|
123
|
+
*)
|
|
124
|
+
printf 'toolu-protected-files: could not resolve gates.protectedFiles.mode (got %s); blocking\n' \
|
|
125
|
+
"${mode:-<empty>}" >&2
|
|
126
|
+
mode_note=" [toolu could not resolve gates.protectedFiles.mode, so this failed closed]"
|
|
127
|
+
mode="block"
|
|
128
|
+
;;
|
|
129
|
+
esac
|
|
130
|
+
|
|
131
|
+
if [[ "$tool_name" == "Bash" || "$tool_name" == "Shell" ]]; then
|
|
132
|
+
headline="This command would WRITE to $candidate, a protected path (matches \"$matched\")."
|
|
133
|
+
else
|
|
134
|
+
headline="Claude is trying to edit $candidate, a protected path (matches \"$matched\")."
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
# Why THIS path is guarded, in the words that matter to the person
|
|
138
|
+
# deciding. A generic "it is protected" teaches nothing and gets waved
|
|
139
|
+
# through; naming the actual stake is the difference between a real
|
|
140
|
+
# decision and a reflex.
|
|
141
|
+
case "$rel_path" in
|
|
142
|
+
*.env.example|*.env.template|*.env.sample)
|
|
143
|
+
# Matched by the .env.* glob, but this one is meant to be committed.
|
|
144
|
+
# Calling a template a secrets file is wrong, and a prompt that
|
|
145
|
+
# overstates its case is one people learn to click through.
|
|
146
|
+
detail="This is an example/template env file. It is committed on purpose, so it should carry placeholders and never live values — it is guarded because a real credential pasted here is a credential published to the repo." ;;
|
|
147
|
+
.env|.env.*|*secrets*)
|
|
148
|
+
detail="This is a secrets file. Approving lets an agent read or rewrite live credentials, and anything it writes here can leak into logs, commits, or a diff you push." ;;
|
|
149
|
+
.git/*|*/.git/*)
|
|
150
|
+
detail="This is git's internal state. Approving lets an agent rewrite refs, hooks, or config — including hooks that run on your machine at every commit." ;;
|
|
151
|
+
*hooks/*|*skills/*)
|
|
152
|
+
detail="This is toolu's own enforcement code — the hooks that run every other gate. Approving lets an agent edit the thing that is supposed to be watching it, which is how a guardrail gets quietly switched off." ;;
|
|
153
|
+
*)
|
|
154
|
+
detail="This path is listed in settings/protected-files.txt because edits to it are hard to notice and expensive to get wrong." ;;
|
|
155
|
+
esac
|
|
156
|
+
|
|
157
|
+
case "$mode" in
|
|
158
|
+
ask)
|
|
159
|
+
reason=$(toolu_gate_guardrail_warning "$headline" "$detail") || reason=""
|
|
160
|
+
# A prompt with no text is worse than a blunt one — the user is being
|
|
161
|
+
# asked to approve a security override and must see what for.
|
|
162
|
+
[ -n "$reason" ] || reason="$headline $detail"
|
|
163
|
+
;;
|
|
164
|
+
advise)
|
|
165
|
+
reason="Protected path $candidate (matches \"$matched\"). $detail The write was NOT stopped — gates.protectedFiles.mode is 'advise'."
|
|
166
|
+
;;
|
|
167
|
+
*)
|
|
168
|
+
reason="$headline $detail Blocked by gates.protectedFiles.mode='block' (see plugins/toolu/hooks/docs/gates.md)."
|
|
169
|
+
;;
|
|
170
|
+
esac
|
|
171
|
+
|
|
172
|
+
toolu_gate_emit "$mode" "${reason}${mode_note}"
|
|
173
|
+
exit 0
|
|
174
|
+
fi
|
|
175
|
+
done
|
|
176
|
+
|
|
177
|
+
exit 0
|