@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,201 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Multi-slot writer for .claude/tmp/quality-gate-status.json.
|
|
3
|
+
#
|
|
4
|
+
# The gate file used to be single-slot (last writer wins): a TS failure
|
|
5
|
+
# followed by a Rust failure overwrote the TS record, and clearing the Rust
|
|
6
|
+
# file then re-opened the gate with the TS violation still live. The same
|
|
7
|
+
# clobber happened between two files of the SAME language.
|
|
8
|
+
#
|
|
9
|
+
# Now every failing file owns an entry under `entries` (keyed by file path).
|
|
10
|
+
# Top-level fields mirror the most recent failure and aggregate all violations,
|
|
11
|
+
# so readers (pre-tool gate, statusline, session-start, user-prompt-submit)
|
|
12
|
+
# keep their existing .status / .reason / .violations contract untouched.
|
|
13
|
+
#
|
|
14
|
+
# A legacy or foreign single-slot failing record (e.g. gate-status-hook's
|
|
15
|
+
# command failure) is preserved by seeding it into `entries` under its `file`,
|
|
16
|
+
# or "__global__" when it has none. Both jq programs below start with the same
|
|
17
|
+
# seed step — keep them in sync.
|
|
18
|
+
#
|
|
19
|
+
# CONCURRENCY: single-writer by assumption. Both functions read-merge-write via
|
|
20
|
+
# `mktemp` + `mv -f`, which has a TOCTOU window — two writers that read the same
|
|
21
|
+
# `existing` blob would each rewrite it and the last `mv` would drop the other's
|
|
22
|
+
# entry. This is safe today because PostToolUse hooks fire serially per tool
|
|
23
|
+
# call (one writer at a time). If a parallel-edit flow is ever added, guard both
|
|
24
|
+
# functions — AND the single-slot fallback write below, AND the separate
|
|
25
|
+
# single-slot passing write in post-tools/modules/gate-status.sh (which writes
|
|
26
|
+
# the same gate file) — with `flock` against a `${gate_file}.lock` sentinel; all
|
|
27
|
+
# of those paths race identically.
|
|
28
|
+
#
|
|
29
|
+
# Public API:
|
|
30
|
+
# gate_record_failure GATE_FILE FILE SOURCE REASON VIOLATIONS
|
|
31
|
+
# gate_clear_file GATE_FILE FILE SOURCE
|
|
32
|
+
#
|
|
33
|
+
# Telemetry (gate_fail / gate_clear): this file has no repo root of its own to
|
|
34
|
+
# hand telemetry_append, so it derives one from GATE_FILE's own path — every
|
|
35
|
+
# caller passes a path shaped <root>/.claude/tmp/quality-gate-status.json, so
|
|
36
|
+
# three dirname hops off GATE_FILE land back on <root>. telemetry.sh is
|
|
37
|
+
# sourced LAZILY (only on first actual emit, not at file load) and every emit
|
|
38
|
+
# is guarded by `command -v telemetry_append`, so an absent or older
|
|
39
|
+
# telemetry.sh is never a hard dependency for this file's callers.
|
|
40
|
+
|
|
41
|
+
# _gate_file_root GATE_FILE -> print <root> (three dirname hops off GATE_FILE).
|
|
42
|
+
_gate_file_root() {
|
|
43
|
+
dirname "$(dirname "$(dirname "$1")")"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# _gate_file_ensure_telemetry -> 0 iff telemetry_append is callable (already
|
|
47
|
+
# loaded, or successfully lazy-sourced from telemetry.sh next to this file).
|
|
48
|
+
_gate_file_ensure_telemetry() {
|
|
49
|
+
command -v telemetry_append >/dev/null 2>&1 && return 0
|
|
50
|
+
local lib_dir="${TOOLU_LIB_DIR:-${BASH_SOURCE%/*}}"
|
|
51
|
+
[ -f "$lib_dir/telemetry.sh" ] || return 1
|
|
52
|
+
# shellcheck source=./telemetry.sh
|
|
53
|
+
. "$lib_dir/telemetry.sh"
|
|
54
|
+
command -v telemetry_append >/dev/null 2>&1
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# gate_record_failure GATE_FILE FILE SOURCE REASON VIOLATIONS
|
|
58
|
+
# Adds/replaces this file's entry and marks the gate failing. Writes via a
|
|
59
|
+
# temp file so a jq failure can never truncate the gate to an unreadable
|
|
60
|
+
# (and therefore silently passing) state; on any error it falls back to the
|
|
61
|
+
# legacy single-slot write rather than dropping the failure.
|
|
62
|
+
gate_record_failure() {
|
|
63
|
+
local gate_file="$1" file="$2" source="$3" reason="$4" violations="$5"
|
|
64
|
+
local existing='{}' now tmp
|
|
65
|
+
now=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
66
|
+
if [ -f "$gate_file" ]; then
|
|
67
|
+
existing=$(cat "$gate_file" 2>/dev/null) || existing='{}'
|
|
68
|
+
jq -e . <<< "$existing" >/dev/null 2>&1 || existing='{}'
|
|
69
|
+
fi
|
|
70
|
+
tmp=$(mktemp "${gate_file}.XXXXXX" 2>/dev/null) || tmp=""
|
|
71
|
+
if [ -n "$tmp" ] && jq \
|
|
72
|
+
--arg file "$file" --arg source "$source" --arg reason "$reason" \
|
|
73
|
+
--arg violations "$violations" --arg now "$now" '
|
|
74
|
+
(if (.entries? | type) == "object" then .entries
|
|
75
|
+
elif (.status // "") == "failing" then
|
|
76
|
+
{ (.file // "__global__"): {
|
|
77
|
+
source: (.source // ""), reason: (.reason // ""),
|
|
78
|
+
violations: (.violations // ""), updatedAt: (.updatedAt // $now) } }
|
|
79
|
+
else {} end) as $prev
|
|
80
|
+
| ($prev + { ($file): {source: $source, reason: $reason,
|
|
81
|
+
violations: $violations, updatedAt: $now} }) as $entries
|
|
82
|
+
# `violations` aggregates every open entry, intentionally: readers (statusline,
|
|
83
|
+
# session-start, user-prompt-submit) must surface ALL unresolved violations,
|
|
84
|
+
# not just the latest. Growth is bounded by the count of distinct failing
|
|
85
|
+
# files in a session (each clears on the next passing edit), so it stays
|
|
86
|
+
# small in practice. If a long session ever shows this string bloating
|
|
87
|
+
# hot-path reads, add a per-entry cap here rather than dropping entries.
|
|
88
|
+
# sort_by updatedAt then key: the timestamp is second-resolution (BSD `date`
|
|
89
|
+
# on macOS has no %N sub-second granularity), so two failures in the SAME
|
|
90
|
+
# second tiebreak by filename — the top-level .file/.reason mirror may then
|
|
91
|
+
# not be the truly-latest. Cosmetic and self-correcting on the next edit;
|
|
92
|
+
# readers see every failure via `entries` regardless. Accepted over a
|
|
93
|
+
# non-portable %N or a cross-call monotonic counter.
|
|
94
|
+
| { status: "failing", reason: $reason, source: $source, file: $file,
|
|
95
|
+
violations: ([$entries | to_entries | sort_by(.value.updatedAt // "", .key)[]
|
|
96
|
+
| (.value.violations // "")] | join("")),
|
|
97
|
+
entries: $entries, updatedAt: $now }
|
|
98
|
+
' <<< "$existing" > "$tmp" 2>/dev/null; then
|
|
99
|
+
mv -f "$tmp" "$gate_file"
|
|
100
|
+
else
|
|
101
|
+
[ -n "$tmp" ] && rm -f "$tmp"
|
|
102
|
+
# Fallback (mktemp or the merge jq failed): write a single-slot record so
|
|
103
|
+
# THIS failure is never lost. That collapses any pre-existing multi-slot
|
|
104
|
+
# `entries` to one record — emit a stderr breadcrumb naming the dropped
|
|
105
|
+
# count so a vanished-state debugging session can see it happened, rather
|
|
106
|
+
# than the entries disappearing silently. The count uses the SAME seed step
|
|
107
|
+
# both merge programs do (a legacy single-slot failing record becomes one
|
|
108
|
+
# `entries` slot under its file/__global__), so a clobbered legacy global
|
|
109
|
+
# record is counted too — not reported as 0.
|
|
110
|
+
local _dropped
|
|
111
|
+
_dropped=$(jq -r --arg f "$file" '
|
|
112
|
+
(if (.entries? | type) == "object" then .entries
|
|
113
|
+
elif (.status // "") == "failing" then { (.file // "__global__"): {} }
|
|
114
|
+
else {} end)
|
|
115
|
+
| keys | map(select(. != $f)) | length' <<< "$existing" 2>/dev/null || echo "")
|
|
116
|
+
if [ -n "$_dropped" ] && [ "$_dropped" -gt 0 ] 2>/dev/null; then
|
|
117
|
+
printf 'gate-file: primary write failed at %s; single-slot fallback dropped %s other entry(ies)\n' \
|
|
118
|
+
"$gate_file" "$_dropped" >&2
|
|
119
|
+
# stderr is easily lost in a hook-driven pipeline; also append a durable
|
|
120
|
+
# breadcrumb beside the gate file so a vanished-state investigation has
|
|
121
|
+
# something to read. Best-effort: never let a logging failure abort the write.
|
|
122
|
+
printf '%s primary write failed; single-slot fallback dropped %s entry(ies)\n' \
|
|
123
|
+
"$now" "$_dropped" >> "${gate_file}.dropped.log" 2>/dev/null || true
|
|
124
|
+
fi
|
|
125
|
+
jq -n --arg reason "$reason" --arg source "$source" --arg file "$file" \
|
|
126
|
+
--arg violations "$violations" --arg updatedAt "$now" \
|
|
127
|
+
'{status: "failing", reason: $reason, source: $source, file: $file,
|
|
128
|
+
violations: $violations, updatedAt: $updatedAt}' > "$gate_file" 2>/dev/null || true
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
# gate_fail telemetry: this function always ends in an attempted write (primary
|
|
132
|
+
# or fallback), so it always represents a failure being recorded — one event
|
|
133
|
+
# per call, best-effort (never affects the write above). Semantics: gate_fail
|
|
134
|
+
# fires on every failing EDIT, including a re-record of the same violation on
|
|
135
|
+
# the same file — it is not deduped per distinct failure. gate_clear (below)
|
|
136
|
+
# is the complementary half: it fires only on a true failing->passing
|
|
137
|
+
# transition, never on a re-clear of an already-passing gate.
|
|
138
|
+
if _gate_file_ensure_telemetry; then
|
|
139
|
+
telemetry_append "$(_gate_file_root "$gate_file")" "gate_fail" \
|
|
140
|
+
"$(jq -cn --arg file "$file" --arg source "$source" '{file: $file, source: $source}')"
|
|
141
|
+
fi
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# gate_clear_file GATE_FILE FILE SOURCE
|
|
145
|
+
# Removes this file's entry IF this source owns it. Other entries are promoted
|
|
146
|
+
# back to the top level (gate stays failing); "passing" is written only when
|
|
147
|
+
# no entry remains. A failing record owned by another source/file is left
|
|
148
|
+
# untouched, matching the old single-slot clear semantics.
|
|
149
|
+
gate_clear_file() {
|
|
150
|
+
local gate_file="$1" file="$2" source="$3"
|
|
151
|
+
local existing now owns tmp
|
|
152
|
+
[ -f "$gate_file" ] || return 0
|
|
153
|
+
existing=$(cat "$gate_file" 2>/dev/null) || return 0
|
|
154
|
+
# A malformed gate file can't be parsed, so a clear silently no-ops and the
|
|
155
|
+
# gate stays stuck failing until the next gate_record_failure rewrites it.
|
|
156
|
+
# Emit a breadcrumb so that stuck state is debuggable, not invisible.
|
|
157
|
+
if ! jq -e . <<< "$existing" >/dev/null 2>&1; then
|
|
158
|
+
printf 'gate-file: malformed JSON at %s; ignoring clear (gate stays failing until next write)\n' "$gate_file" >&2
|
|
159
|
+
return 0
|
|
160
|
+
fi
|
|
161
|
+
[ "$(jq -r '.status // ""' <<< "$existing" 2>/dev/null)" = "failing" ] || return 0
|
|
162
|
+
owns=$(jq -r --arg f "$file" --arg s "$source" '
|
|
163
|
+
if (.entries? | type) == "object"
|
|
164
|
+
then (((.entries[$f]? // {}) | .source // "") == $s)
|
|
165
|
+
else ((.source // "") == $s and (.file // "") == $f)
|
|
166
|
+
end' <<< "$existing" 2>/dev/null)
|
|
167
|
+
[ "$owns" = "true" ] || return 0
|
|
168
|
+
now=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
169
|
+
tmp=$(mktemp "${gate_file}.XXXXXX" 2>/dev/null) || return 0
|
|
170
|
+
if jq --arg file "$file" --arg source "$source" --arg now "$now" '
|
|
171
|
+
((if (.entries? | type) == "object" then .entries
|
|
172
|
+
elif (.status // "") == "failing" then
|
|
173
|
+
{ (.file // "__global__"): {
|
|
174
|
+
source: (.source // ""), reason: (.reason // ""),
|
|
175
|
+
violations: (.violations // ""), updatedAt: (.updatedAt // $now) } }
|
|
176
|
+
else {} end) | del(.[$file])) as $left
|
|
177
|
+
| if ($left | length) == 0
|
|
178
|
+
then { status: "passing", source: $source, updatedAt: $now }
|
|
179
|
+
else (($left | to_entries | sort_by(.value.updatedAt // "", .key)) as $sorted
|
|
180
|
+
| ($sorted | last) as $latest
|
|
181
|
+
| { status: "failing",
|
|
182
|
+
reason: ($latest.value.reason // "Quality gate failing"),
|
|
183
|
+
source: ($latest.value.source // ""),
|
|
184
|
+
file: $latest.key,
|
|
185
|
+
violations: ([$sorted[] | (.value.violations // "")] | join("")),
|
|
186
|
+
entries: $left, updatedAt: $now })
|
|
187
|
+
end
|
|
188
|
+
' <<< "$existing" > "$tmp" 2>/dev/null; then
|
|
189
|
+
mv -f "$tmp" "$gate_file"
|
|
190
|
+
# gate_clear telemetry: ONLY here, after the write actually lands — every
|
|
191
|
+
# early return above (no file / unreadable / malformed / not failing /
|
|
192
|
+
# other-source entry) emits nothing, and so does a mktemp or jq failure
|
|
193
|
+
# below this point (the `else` branch), since no transition happened.
|
|
194
|
+
if _gate_file_ensure_telemetry; then
|
|
195
|
+
telemetry_append "$(_gate_file_root "$gate_file")" "gate_clear" \
|
|
196
|
+
"$(jq -cn --arg file "$file" --arg source "$source" '{file: $file, source: $source}')"
|
|
197
|
+
fi
|
|
198
|
+
else
|
|
199
|
+
rm -f "$tmp"
|
|
200
|
+
fi
|
|
201
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Gate enforcement modes — how a failed gate check reaches the user.
|
|
3
|
+
#
|
|
4
|
+
# Every toolu gate answers one question ("is this push reviewed?", "is the
|
|
5
|
+
# quality gate green?") and then has to decide how forcefully to say no. That
|
|
6
|
+
# second decision is this file's job, so a gate module owns its check and its
|
|
7
|
+
# reason text while the delivery is uniform and user-configurable.
|
|
8
|
+
#
|
|
9
|
+
# Modes:
|
|
10
|
+
# block permissionDecision "deny" — the tool call does not happen
|
|
11
|
+
# ask permissionDecision "ask" — the user is prompted, and decides
|
|
12
|
+
# advise additionalContext — the agent is told, nothing is stopped
|
|
13
|
+
# off nothing at all
|
|
14
|
+
#
|
|
15
|
+
# Two CLASSES of gate, and the difference only shows up when `ask` cannot be
|
|
16
|
+
# delivered:
|
|
17
|
+
#
|
|
18
|
+
# judgement gates (pushReview, qualityGate, commitGate, planLedger,
|
|
19
|
+
# docsSync, agentTier) — soft calls about process. `ask` on a host that
|
|
20
|
+
# cannot prompt degrades to `advise`: say the thing, don't stop the work.
|
|
21
|
+
#
|
|
22
|
+
# SECURITY GUARDRAILS ($TOOLU_GATE_GUARDRAILS: protectedFiles, mcpBlocker,
|
|
23
|
+
# bashCommands) — the ones standing between an agent and your secrets,
|
|
24
|
+
# your git internals, the hook framework itself, and arbitrary code
|
|
25
|
+
# execution. These now ASK by default instead of denying outright, so a
|
|
26
|
+
# human decides in the moment. That only holds if the human can actually
|
|
27
|
+
# be reached: on a host with no prompt, `ask` degrades to `block`, NOT to
|
|
28
|
+
# `advise`. Degrading a guardrail to advise would silently turn it off
|
|
29
|
+
# exactly where nobody is watching.
|
|
30
|
+
#
|
|
31
|
+
# Resolution precedence, first hit wins:
|
|
32
|
+
# 1. .gates.<name>.mode
|
|
33
|
+
# 2. the legacy top-level key (.docsSync.mode / .agentTier.mode only)
|
|
34
|
+
# 3. the .gates.preset table below
|
|
35
|
+
# 4. the built-in default preset (balanced)
|
|
36
|
+
#
|
|
37
|
+
# A value that is present but not a known mode warns once on stderr and falls
|
|
38
|
+
# through to the next layer — the same warn-and-fall-back discipline
|
|
39
|
+
# toolu_model uses, so a typo mis-delivers nothing.
|
|
40
|
+
#
|
|
41
|
+
# Public API:
|
|
42
|
+
# toolu_gate_preset print the resolved preset name
|
|
43
|
+
# toolu_gate_mode NAME print block|ask|advise|off for gate NAME
|
|
44
|
+
# toolu_gate_emit MODE REASON print the PreToolUse JSON for MODE
|
|
45
|
+
# toolu_gate_guardrail_warning HEADLINE DETAIL
|
|
46
|
+
# compose a guardrail's `ask` prompt text
|
|
47
|
+
|
|
48
|
+
_TOOLU_GATE_MODE_LIB_DIR="$(cd "${BASH_SOURCE%/*}" && pwd)"
|
|
49
|
+
# shellcheck source=config.sh
|
|
50
|
+
. "$_TOOLU_GATE_MODE_LIB_DIR/config.sh"
|
|
51
|
+
|
|
52
|
+
TOOLU_GATE_MODES="block ask advise off"
|
|
53
|
+
TOOLU_GATE_PRESETS="strict balanced relaxed"
|
|
54
|
+
TOOLU_GATE_NAMES="pushReview qualityGate commitGate bashCommands planLedger docsSync agentTier protectedFiles mcpBlocker"
|
|
55
|
+
TOOLU_GATE_DEFAULT_PRESET="balanced"
|
|
56
|
+
|
|
57
|
+
# The security guardrails. Two things are special about this list:
|
|
58
|
+
# 1. `ask` degrades to `block` (not `advise`) where the host cannot prompt.
|
|
59
|
+
# 2. They ask at every preset except `strict`, which still denies outright.
|
|
60
|
+
# `off` remains available per-gate for anyone who wants it, deliberately and
|
|
61
|
+
# in writing, in their own config.
|
|
62
|
+
TOOLU_GATE_GUARDRAILS="protectedFiles mcpBlocker bashCommands"
|
|
63
|
+
|
|
64
|
+
# Gates whose pre-`gates.*` config key is still honored, as "<gate>:<path>".
|
|
65
|
+
# Only these two ever had a documented top-level mode key; a gate absent here
|
|
66
|
+
# has no legacy layer to consult.
|
|
67
|
+
_TOOLU_GATE_LEGACY_PATHS="docsSync:docsSync.mode agentTier:agentTier.mode"
|
|
68
|
+
|
|
69
|
+
_toolu_gate_warn() {
|
|
70
|
+
printf 'toolu-gate: %s\n' "$1" >&2
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# _toolu_gate_preset_mode PRESET NAME -> mode for NAME under PRESET.
|
|
74
|
+
# The table is the whole policy; balanced is what ships.
|
|
75
|
+
_toolu_gate_preset_mode() {
|
|
76
|
+
local preset="$1" name="$2"
|
|
77
|
+
case "$preset" in
|
|
78
|
+
strict)
|
|
79
|
+
printf 'block'
|
|
80
|
+
;;
|
|
81
|
+
balanced)
|
|
82
|
+
case "$name" in
|
|
83
|
+
qualityGate) printf 'block' ;;
|
|
84
|
+
protectedFiles|mcpBlocker|bashCommands) printf 'ask' ;;
|
|
85
|
+
*) printf 'advise' ;;
|
|
86
|
+
esac
|
|
87
|
+
;;
|
|
88
|
+
relaxed)
|
|
89
|
+
case "$name" in
|
|
90
|
+
# `relaxed` relaxes judgement calls. A guardrail still asks: relaxed
|
|
91
|
+
# means "stop lecturing me", not "write my .env without telling me".
|
|
92
|
+
protectedFiles|mcpBlocker|bashCommands) printf 'ask' ;;
|
|
93
|
+
pushReview|qualityGate) printf 'advise' ;;
|
|
94
|
+
*) printf 'off' ;;
|
|
95
|
+
esac
|
|
96
|
+
;;
|
|
97
|
+
*)
|
|
98
|
+
return 1
|
|
99
|
+
;;
|
|
100
|
+
esac
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# toolu_gate_preset -> the configured preset, or the built-in default.
|
|
104
|
+
toolu_gate_preset() {
|
|
105
|
+
# shellcheck disable=SC2086 # intentional word-split of the allowed-value list
|
|
106
|
+
toolu_string gates.preset "$TOOLU_GATE_DEFAULT_PRESET" $TOOLU_GATE_PRESETS
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# _toolu_gate_read_mode PATH -> print the mode at PATH, or return 1.
|
|
110
|
+
# Returns 1 both when the key is absent and when it held something that is not
|
|
111
|
+
# a mode (toolu_string has already warned in the latter case), which is exactly
|
|
112
|
+
# the "fall through to the next layer" behavior both cases want.
|
|
113
|
+
_toolu_gate_read_mode() {
|
|
114
|
+
local value
|
|
115
|
+
# shellcheck disable=SC2086 # intentional word-split of the allowed-value list
|
|
116
|
+
value=$(toolu_string "$1" "__unset__" $TOOLU_GATE_MODES)
|
|
117
|
+
[ "$value" = "__unset__" ] && return 1
|
|
118
|
+
printf '%s' "$value"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# _toolu_gate_legacy_path NAME -> print NAME's legacy config path, or return 1.
|
|
122
|
+
_toolu_gate_legacy_path() {
|
|
123
|
+
local name="$1" entry
|
|
124
|
+
for entry in $_TOOLU_GATE_LEGACY_PATHS; do
|
|
125
|
+
if [ "${entry%%:*}" = "$name" ]; then
|
|
126
|
+
printf '%s' "${entry#*:}"
|
|
127
|
+
return 0
|
|
128
|
+
fi
|
|
129
|
+
done
|
|
130
|
+
return 1
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# toolu_gate_mode NAME -> block|ask|advise|off.
|
|
134
|
+
#
|
|
135
|
+
# An unknown gate name is a caller typo, not a config problem: warn and print
|
|
136
|
+
# `block`, so a misspelled gate keeps enforcing rather than silently opening.
|
|
137
|
+
toolu_gate_mode() {
|
|
138
|
+
local name="$1" mode="" legacy_path preset
|
|
139
|
+
case " $TOOLU_GATE_NAMES " in
|
|
140
|
+
*" $name "*) ;;
|
|
141
|
+
*)
|
|
142
|
+
_toolu_gate_warn "unknown gate '$name' (known: $TOOLU_GATE_NAMES); enforcing block"
|
|
143
|
+
printf 'block'
|
|
144
|
+
return 1
|
|
145
|
+
;;
|
|
146
|
+
esac
|
|
147
|
+
|
|
148
|
+
mode=$(_toolu_gate_read_mode "gates.${name}.mode") || mode=""
|
|
149
|
+
|
|
150
|
+
if [ -z "$mode" ] && legacy_path=$(_toolu_gate_legacy_path "$name"); then
|
|
151
|
+
mode=$(_toolu_gate_read_mode "$legacy_path") || mode=""
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
if [ -z "$mode" ]; then
|
|
155
|
+
preset=$(toolu_gate_preset)
|
|
156
|
+
mode=$(_toolu_gate_preset_mode "$preset" "$name") || mode=""
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
# Both fallbacks exhausted (only reachable if the preset table and the name
|
|
160
|
+
# list ever drift apart): enforce rather than open.
|
|
161
|
+
[ -n "$mode" ] || mode="block"
|
|
162
|
+
|
|
163
|
+
# `ask` needs a host that can prompt. Where none can, the gate still speaks —
|
|
164
|
+
# but WHAT it degrades to depends on the gate's class:
|
|
165
|
+
#
|
|
166
|
+
# judgement gate -> advise. Say it, don't stop the work.
|
|
167
|
+
# guardrail -> block. The whole point of asking is that a human
|
|
168
|
+
# decides; with no human reachable the safe answer is
|
|
169
|
+
# no. Degrading these to advise would hand an agent
|
|
170
|
+
# silent write access to .env on exactly the hosts
|
|
171
|
+
# where nobody is watching.
|
|
172
|
+
if [ "$mode" = "ask" ] && ! toolu_supports_ask; then
|
|
173
|
+
case " $TOOLU_GATE_GUARDRAILS " in
|
|
174
|
+
*" $name "*) mode="block" ;;
|
|
175
|
+
*) mode="advise" ;;
|
|
176
|
+
esac
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
printf '%s' "$mode"
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# toolu_gate_guardrail_warning HEADLINE DETAIL
|
|
183
|
+
# Compose the prompt text for a SECURITY GUARDRAIL's `ask`.
|
|
184
|
+
#
|
|
185
|
+
# Loud on purpose. Answering yes here overrides a protection that exists
|
|
186
|
+
# precisely so an agent cannot do this quietly, and a prompt that reads like an
|
|
187
|
+
# ordinary "allow this command?" trains people to wave it through. The banner,
|
|
188
|
+
# the WHY, and the "this call only" line are the three things a person needs to
|
|
189
|
+
# make a real decision in the two seconds they will actually spend on it.
|
|
190
|
+
toolu_gate_guardrail_warning() {
|
|
191
|
+
local headline="$1" detail="$2"
|
|
192
|
+
printf '%s' "\
|
|
193
|
+
############################################################
|
|
194
|
+
## ⚠️ SECURITY GUARDRAIL — OVERRIDE REQUESTED ⚠️ ##
|
|
195
|
+
############################################################
|
|
196
|
+
|
|
197
|
+
$headline
|
|
198
|
+
|
|
199
|
+
WHY THIS IS GUARDED
|
|
200
|
+
$detail
|
|
201
|
+
|
|
202
|
+
Approving covers THIS ONE CALL. Nothing is remembered and the next attempt
|
|
203
|
+
asks again. If you did not just ask for this, the answer is no."
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
# toolu_gate_emit MODE REASON
|
|
207
|
+
# Print the PreToolUse hook JSON for MODE carrying REASON. `off` prints
|
|
208
|
+
# nothing. An unknown MODE is treated as `block`: a delivery bug must not
|
|
209
|
+
# become a silent allow.
|
|
210
|
+
toolu_gate_emit() {
|
|
211
|
+
local mode="$1" reason="$2"
|
|
212
|
+
case "$mode" in
|
|
213
|
+
off)
|
|
214
|
+
return 0
|
|
215
|
+
;;
|
|
216
|
+
advise)
|
|
217
|
+
jq -n --arg reason "$reason" '{
|
|
218
|
+
hookSpecificOutput: {
|
|
219
|
+
hookEventName: "PreToolUse",
|
|
220
|
+
additionalContext: $reason
|
|
221
|
+
}
|
|
222
|
+
}'
|
|
223
|
+
;;
|
|
224
|
+
ask)
|
|
225
|
+
jq -n --arg reason "$reason" '{
|
|
226
|
+
hookSpecificOutput: {
|
|
227
|
+
hookEventName: "PreToolUse",
|
|
228
|
+
permissionDecision: "ask",
|
|
229
|
+
permissionDecisionReason: $reason
|
|
230
|
+
}
|
|
231
|
+
}'
|
|
232
|
+
;;
|
|
233
|
+
*)
|
|
234
|
+
[ "$mode" = "block" ] || _toolu_gate_warn "unknown mode '$mode'; emitting block"
|
|
235
|
+
jq -n --arg reason "$reason" '{
|
|
236
|
+
hookSpecificOutput: {
|
|
237
|
+
hookEventName: "PreToolUse",
|
|
238
|
+
permissionDecision: "deny",
|
|
239
|
+
permissionDecisionReason: $reason
|
|
240
|
+
}
|
|
241
|
+
}'
|
|
242
|
+
;;
|
|
243
|
+
esac
|
|
244
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Host adapter shared by toolu's hook runtime.
|
|
3
|
+
#
|
|
4
|
+
# Public API:
|
|
5
|
+
# toolu_host claude | codex
|
|
6
|
+
# toolu_config_root host-native writable config/data root
|
|
7
|
+
# toolu_project_root explicit/project git root, if any
|
|
8
|
+
# toolu_project_dirname .claude | .codex (overrideable)
|
|
9
|
+
# toolu_project_config project toolu.config.json path
|
|
10
|
+
# toolu_project_state_root <repo>/<host-dir>/tmp
|
|
11
|
+
# toolu_project_state_dir NAME state root child
|
|
12
|
+
# toolu_plugin_root / _data plugin paths supplied by the host
|
|
13
|
+
# toolu_invocation NS NAME /ns:name | $ns:name
|
|
14
|
+
# toolu_plugin_install_command SPEC host-native exact install command
|
|
15
|
+
# toolu_codex_plugin_snapshot_path canonical snapshot path
|
|
16
|
+
# toolu_snapshot_codex_plugins refresh snapshot at SessionStart
|
|
17
|
+
# toolu_codex_plugin_installed SPEC tri-state lookup (0 hit/miss, 2 unknown)
|
|
18
|
+
|
|
19
|
+
_toolu_host_warn() {
|
|
20
|
+
printf 'toolu-host: %s\n' "$1" >&2
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
toolu_host() {
|
|
24
|
+
case "${TOOLU_HOST_OVERRIDE:-}" in
|
|
25
|
+
claude|codex) printf '%s\n' "$TOOLU_HOST_OVERRIDE"; return 0 ;;
|
|
26
|
+
'') ;;
|
|
27
|
+
*) _toolu_host_warn "invalid TOOLU_HOST_OVERRIDE '${TOOLU_HOST_OVERRIDE}' (using environment detection)" ;;
|
|
28
|
+
esac
|
|
29
|
+
|
|
30
|
+
# Codex sets PLUGIN_ROOT and also exports CLAUDE_PLUGIN_ROOT for compatibility.
|
|
31
|
+
# The Codex-native signal must therefore be checked first.
|
|
32
|
+
if [ -n "${PLUGIN_ROOT:-}" ]; then
|
|
33
|
+
printf 'codex\n'
|
|
34
|
+
else
|
|
35
|
+
printf 'claude\n'
|
|
36
|
+
fi
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
toolu_config_root() {
|
|
40
|
+
if [ -n "${TOOLU_CONFIG_DIR:-}" ]; then
|
|
41
|
+
printf '%s\n' "$TOOLU_CONFIG_DIR"
|
|
42
|
+
elif [ "$(toolu_host)" = codex ]; then
|
|
43
|
+
printf '%s\n' "${CODEX_HOME:-$HOME/.codex}"
|
|
44
|
+
elif [ -n "${CLAUDE_CONFIG_DIR:-}" ]; then
|
|
45
|
+
printf '%s\n' "$CLAUDE_CONFIG_DIR"
|
|
46
|
+
else
|
|
47
|
+
printf '%s/.claude\n' "$HOME"
|
|
48
|
+
fi
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
toolu_project_root() {
|
|
52
|
+
local root="${TOOLU_PROJECT_DIR:-}"
|
|
53
|
+
if [ -z "$root" ] && [ "$(toolu_host)" = claude ]; then
|
|
54
|
+
root="${CLAUDE_PROJECT_DIR:-}"
|
|
55
|
+
fi
|
|
56
|
+
[ -n "$root" ] || root=$(git rev-parse --show-toplevel 2>/dev/null || true)
|
|
57
|
+
[ -n "$root" ] && printf '%s\n' "$root"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
toolu_project_dirname() {
|
|
61
|
+
if [ -n "${TOOLU_PROJECT_CONFIG_DIRNAME:-}" ]; then
|
|
62
|
+
printf '%s\n' "$TOOLU_PROJECT_CONFIG_DIRNAME"
|
|
63
|
+
elif [ "$(toolu_host)" = codex ]; then
|
|
64
|
+
printf '.codex\n'
|
|
65
|
+
else
|
|
66
|
+
printf '.claude\n'
|
|
67
|
+
fi
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
toolu_project_config() {
|
|
71
|
+
local root
|
|
72
|
+
root=$(toolu_project_root)
|
|
73
|
+
[ -n "$root" ] || return 0
|
|
74
|
+
printf '%s/%s/toolu.config.json\n' "$root" "$(toolu_project_dirname)"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
toolu_project_state_root() {
|
|
78
|
+
local root="${1:-}"
|
|
79
|
+
[ -n "$root" ] || root=$(toolu_project_root)
|
|
80
|
+
[ -n "$root" ] || return 0
|
|
81
|
+
printf '%s/%s/tmp\n' "$root" "$(toolu_project_dirname)"
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
toolu_project_state_dir() {
|
|
85
|
+
local name="${1:-}" root="${2:-}" base
|
|
86
|
+
[ -n "$name" ] || return 1
|
|
87
|
+
base=$(toolu_project_state_root "$root")
|
|
88
|
+
[ -n "$base" ] || return 0
|
|
89
|
+
printf '%s/%s\n' "$base" "$name"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
toolu_plugin_root() {
|
|
93
|
+
if [ "$(toolu_host)" = codex ] && [ -n "${PLUGIN_ROOT:-}" ]; then
|
|
94
|
+
printf '%s\n' "$PLUGIN_ROOT"
|
|
95
|
+
elif [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
|
|
96
|
+
printf '%s\n' "$CLAUDE_PLUGIN_ROOT"
|
|
97
|
+
fi
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
toolu_plugin_data() {
|
|
101
|
+
if [ "$(toolu_host)" = codex ] && [ -n "${PLUGIN_DATA:-}" ]; then
|
|
102
|
+
printf '%s\n' "$PLUGIN_DATA"
|
|
103
|
+
elif [ -n "${CLAUDE_PLUGIN_DATA:-}" ]; then
|
|
104
|
+
printf '%s\n' "$CLAUDE_PLUGIN_DATA"
|
|
105
|
+
fi
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
toolu_invocation() {
|
|
109
|
+
local namespace="${1:-}" name="${2:-}"
|
|
110
|
+
[ -n "$namespace" ] && [ -n "$name" ] || return 1
|
|
111
|
+
if [ "$(toolu_host)" = codex ]; then
|
|
112
|
+
printf '$%s:%s\n' "$namespace" "$name"
|
|
113
|
+
else
|
|
114
|
+
printf '/%s:%s\n' "$namespace" "$name"
|
|
115
|
+
fi
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
toolu_plugin_install_command() {
|
|
119
|
+
local spec="${1:-}"
|
|
120
|
+
[ -n "$spec" ] || return 1
|
|
121
|
+
if [ "$(toolu_host)" = codex ]; then
|
|
122
|
+
printf 'codex plugin add %s\n' "$spec"
|
|
123
|
+
else
|
|
124
|
+
printf '/plugin install %s\n' "$spec"
|
|
125
|
+
fi
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
toolu_codex_plugin_snapshot_path() {
|
|
129
|
+
printf '%s\n' "${TOOLU_CODEX_PLUGIN_SNAPSHOT:-$(toolu_config_root)/toolu/codex-plugins.json}"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
_toolu_write_codex_snapshot() {
|
|
133
|
+
local path="$1" payload="$2" dir tmp
|
|
134
|
+
dir=$(dirname "$path")
|
|
135
|
+
mkdir -p "$dir" 2>/dev/null || return 0
|
|
136
|
+
tmp="${path}.tmp.$$"
|
|
137
|
+
if printf '%s\n' "$payload" > "$tmp" 2>/dev/null; then
|
|
138
|
+
mv "$tmp" "$path" 2>/dev/null || rm -f "$tmp"
|
|
139
|
+
else
|
|
140
|
+
rm -f "$tmp"
|
|
141
|
+
fi
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# Snapshot once on Codex SessionStart. Failures are represented explicitly so
|
|
145
|
+
# hot-path readers never invoke the CLI and never mistake an old cache for a
|
|
146
|
+
# definitive installed/absent result.
|
|
147
|
+
toolu_snapshot_codex_plugins() {
|
|
148
|
+
[ "$(toolu_host)" = codex ] || return 0
|
|
149
|
+
local path raw canonical
|
|
150
|
+
path=$(toolu_codex_plugin_snapshot_path)
|
|
151
|
+
canonical='{"version":1,"status":"indeterminate","plugins":[]}'
|
|
152
|
+
|
|
153
|
+
if command -v codex >/dev/null 2>&1 && raw=$(codex plugin list --json 2>/dev/null); then
|
|
154
|
+
if canonical=$(jq -ce '
|
|
155
|
+
if (.installed | type) != "array" then error("installed must be an array") else
|
|
156
|
+
{
|
|
157
|
+
version: 1,
|
|
158
|
+
status: "ready",
|
|
159
|
+
plugins: ([.installed[]
|
|
160
|
+
| select((has("installed") | not) or (.installed == true))
|
|
161
|
+
| select((has("enabled") | not) or (.enabled == true))
|
|
162
|
+
| (.pluginId // (if (.name | type) == "string" and (.marketplaceName | type) == "string"
|
|
163
|
+
then (.name + "@" + .marketplaceName) else empty end))
|
|
164
|
+
| select(type == "string" and length > 0)] | unique | sort)
|
|
165
|
+
}
|
|
166
|
+
end
|
|
167
|
+
' <<<"$raw" 2>/dev/null); then
|
|
168
|
+
:
|
|
169
|
+
else
|
|
170
|
+
canonical='{"version":1,"status":"indeterminate","plugins":[]}'
|
|
171
|
+
fi
|
|
172
|
+
fi
|
|
173
|
+
_toolu_write_codex_snapshot "$path" "$canonical"
|
|
174
|
+
return 0
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# Return contract matches detect_plugin_installed:
|
|
178
|
+
# 0 + spec => installed, 0 + empty => definitively absent, 2 => unknown.
|
|
179
|
+
toolu_codex_plugin_installed() {
|
|
180
|
+
local spec="${1:-}" path status
|
|
181
|
+
[ -n "$spec" ] || return 0
|
|
182
|
+
command -v jq >/dev/null 2>&1 || return 2
|
|
183
|
+
path=$(toolu_codex_plugin_snapshot_path)
|
|
184
|
+
[ -f "$path" ] || return 2
|
|
185
|
+
jq -e '.version == 1 and (.status | type == "string") and (.plugins | type == "array")' \
|
|
186
|
+
"$path" >/dev/null 2>&1 || return 2
|
|
187
|
+
status=$(jq -r '.status' "$path" 2>/dev/null) || return 2
|
|
188
|
+
[ "$status" = ready ] || return 2
|
|
189
|
+
if jq -e --arg spec "$spec" 'any(.plugins[]; . == $spec)' "$path" >/dev/null 2>&1; then
|
|
190
|
+
printf '%s\n' "$spec"
|
|
191
|
+
fi
|
|
192
|
+
return 0
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# toolu_supports_ask -> 0 iff this host can prompt the user for a PreToolUse
|
|
196
|
+
# decision (`permissionDecision: "ask"`).
|
|
197
|
+
#
|
|
198
|
+
# Claude Code prompts; Codex's hook contract has no ask, so a gate that asked
|
|
199
|
+
# there would emit a decision the host drops on the floor — silently disabling
|
|
200
|
+
# the gate. Callers (toolu_gate_mode) degrade `ask` to `advise` instead, which
|
|
201
|
+
# still reaches the agent.
|
|
202
|
+
toolu_supports_ask() {
|
|
203
|
+
[ "$(toolu_host)" = codex ] && return 1
|
|
204
|
+
return 0
|
|
205
|
+
}
|