@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,477 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Unified READ-ONLY view over the four toolu push gates (quality, plan, review,
|
|
3
|
+
# docs): "is this branch done, and why not" in one place, instead of four state
|
|
4
|
+
# files with no shared view. Never writes state and never runs a step's check —
|
|
5
|
+
# each gate function below MIRRORS the logic of its real enforcement point
|
|
6
|
+
# (quality-gate.sh, pre-tools/modules/{plan-ledger,push-review,docs-sync}.sh)
|
|
7
|
+
# without sourcing it, since those scripts are independently owned/evolving;
|
|
8
|
+
# drift between this view and a real gate is a bug to fix here, not a reason to
|
|
9
|
+
# couple to them.
|
|
10
|
+
#
|
|
11
|
+
# CLI:
|
|
12
|
+
# bash plugins/toolu/hooks/lib/verdict.sh status # human-readable table
|
|
13
|
+
# bash plugins/toolu/hooks/lib/verdict.sh json # machine-readable
|
|
14
|
+
#
|
|
15
|
+
# Exit codes: 0 ready, 1 blocked (>=1 gate fail/escalate), 2 error (not a git
|
|
16
|
+
# repo, or git/jq missing, or bad CLI usage).
|
|
17
|
+
#
|
|
18
|
+
# Push-review v2 schema (reviewed_files, version 2) is implemented HERE
|
|
19
|
+
# directly against the state file, mirroring rather than sourcing the
|
|
20
|
+
# push-review.sh gate (both are v2) — see spec component 3 / component 7.
|
|
21
|
+
|
|
22
|
+
set -o pipefail
|
|
23
|
+
|
|
24
|
+
_toolu_lib="${TOOLU_LIB_DIR:-${BASH_SOURCE%/*}}"
|
|
25
|
+
# shellcheck source=./detect.sh
|
|
26
|
+
. "$_toolu_lib/detect.sh"
|
|
27
|
+
# shellcheck source=./diff-sha.sh
|
|
28
|
+
. "$_toolu_lib/diff-sha.sh"
|
|
29
|
+
# shellcheck source=./config.sh
|
|
30
|
+
. "$_toolu_lib/config.sh"
|
|
31
|
+
# shellcheck source=./docs-sync-config.sh
|
|
32
|
+
. "$_toolu_lib/docs-sync-config.sh"
|
|
33
|
+
# shellcheck source=./plan-ledger-parse.sh
|
|
34
|
+
. "$_toolu_lib/plan-ledger-parse.sh"
|
|
35
|
+
|
|
36
|
+
# Reviewer allow-list — keep in sync with pre-tools/modules/push-review.sh's
|
|
37
|
+
# accepted_reviewers; parity is asserted by the parity test in
|
|
38
|
+
# __tests__/verdict.bats.
|
|
39
|
+
ACCEPTED_REVIEWERS='["code-review","toolu-review:review","code-review:xhigh","review","security-review"]'
|
|
40
|
+
|
|
41
|
+
# vd_gate STATE REASON [EXTRA_JSON]
|
|
42
|
+
# Print one gate object: {state, reason} merged with EXTRA_JSON (default "{}").
|
|
43
|
+
# EXTRA_JSON supplies gate-specific fields (plan's summary/ac_uncovered,
|
|
44
|
+
# review's reason_code/round) and must not itself carry state/reason.
|
|
45
|
+
vd_gate() {
|
|
46
|
+
local state="$1" reason="$2" extra="$3"
|
|
47
|
+
[ -n "$extra" ] || extra='{}'
|
|
48
|
+
jq -cn --arg state "$state" --arg reason "$reason" --argjson extra "$extra" \
|
|
49
|
+
'{state: $state, reason: $reason} + $extra'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# _vd_matches_any PATH GLOB_LIST -> 0 iff PATH fnmatches any newline-separated
|
|
53
|
+
# glob in GLOB_LIST. Local reimplementation of docs-sync.sh's own helper (that
|
|
54
|
+
# module is under concurrent edit; this is a ~10-line mirror, not a coupling).
|
|
55
|
+
_vd_matches_any() {
|
|
56
|
+
local path="$1" glob
|
|
57
|
+
while IFS= read -r glob; do
|
|
58
|
+
[ -z "$glob" ] && continue
|
|
59
|
+
# shellcheck disable=SC2254 # $glob is an intentional fnmatch pattern.
|
|
60
|
+
case "$path" in
|
|
61
|
+
$glob) return 0 ;;
|
|
62
|
+
esac
|
|
63
|
+
done <<< "$2"
|
|
64
|
+
return 1
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# vd_gate_quality REPO_ROOT
|
|
68
|
+
# Mirrors quality-gate.sh: linked worktree -> skip (gate never enforces there,
|
|
69
|
+
# checked BEFORE the file so a worktree always reads skip regardless of a
|
|
70
|
+
# stale/failing file left over from the main checkout); else read the
|
|
71
|
+
# multi-slot gate file's top-level status.
|
|
72
|
+
vd_gate_quality() {
|
|
73
|
+
local repo_root="$1" gate_file git_dir common_dir
|
|
74
|
+
|
|
75
|
+
git_dir=$(git -C "$repo_root" rev-parse --path-format=absolute --git-dir 2>/dev/null || true)
|
|
76
|
+
common_dir=$(git -C "$repo_root" rev-parse --path-format=absolute --git-common-dir 2>/dev/null || true)
|
|
77
|
+
git_dir="${git_dir%/}"
|
|
78
|
+
common_dir="${common_dir%/}"
|
|
79
|
+
if [ -n "$git_dir" ] && [ -n "$common_dir" ] && [ "$git_dir" != "$common_dir" ]; then
|
|
80
|
+
vd_gate skip "gate disabled in linked worktree"
|
|
81
|
+
return
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
gate_file="$(toolu_project_state_root "$repo_root")/quality-gate-status.json"
|
|
85
|
+
if [ ! -f "$gate_file" ]; then
|
|
86
|
+
vd_gate pass "no quality-gate failure recorded"
|
|
87
|
+
return
|
|
88
|
+
fi
|
|
89
|
+
if [ "$(jq -r '.status // ""' "$gate_file" 2>/dev/null)" != "failing" ]; then
|
|
90
|
+
vd_gate pass "quality gate passing"
|
|
91
|
+
return
|
|
92
|
+
fi
|
|
93
|
+
vd_gate fail "$(jq -r '.reason // "Quality gate failing"' "$gate_file" 2>/dev/null || echo "Quality gate failing")"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# vd_gate_plan REPO_ROOT BRANCH BASE CUR
|
|
97
|
+
# Mirrors pre-tools/modules/plan-ledger.sh's fresh-green push check: absent
|
|
98
|
+
# ledger + code files in the diff -> advise; absent ledger + no code files, or
|
|
99
|
+
# an empty (no-op) ledger -> skip; unparseable/schema-mismatched ledger -> fail
|
|
100
|
+
# (fail closed, matching the real gate); else fail iff any step isn't
|
|
101
|
+
# fresh-green. Always reports summary{total,fresh_green} (recomputed against
|
|
102
|
+
# CUR, never trusting a possibly-stale on-disk summary) and a report-only
|
|
103
|
+
# ac_uncovered count (mirrors lib/plan-ledger.sh's pl_ac_coverage_lines).
|
|
104
|
+
vd_gate_plan() {
|
|
105
|
+
local repo_root="$1" branch="$2" base="$3" cur="$4"
|
|
106
|
+
local slug ledger_dir state_file zero_extra='{"summary":{"total":0,"fresh_green":0},"ac_uncovered":0}'
|
|
107
|
+
slug=$(branch_slug "$branch")
|
|
108
|
+
ledger_dir="${LEDGER_DIR:-$(toolu_project_state_dir plan-ledger "$repo_root")}"
|
|
109
|
+
state_file="$ledger_dir/${slug}.json"
|
|
110
|
+
|
|
111
|
+
if [ ! -f "$state_file" ]; then
|
|
112
|
+
local code_files
|
|
113
|
+
code_files=$(git -C "$repo_root" diff --no-color "${base}...HEAD" --name-only 2>/dev/null \
|
|
114
|
+
| grep -E '\.(ts|tsx|js|jsx|rs|sh|py|go)$' || true)
|
|
115
|
+
if [ -n "$code_files" ]; then
|
|
116
|
+
vd_gate advise "no plan ledger for this change; if non-trivial, run plan" "$zero_extra"
|
|
117
|
+
else
|
|
118
|
+
vd_gate skip "no plan ledger and no code files in diff" "$zero_extra"
|
|
119
|
+
fi
|
|
120
|
+
return
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
local ledger
|
|
124
|
+
if ! ledger=$(pl_read_ledger "$state_file" 2>/dev/null); then
|
|
125
|
+
vd_gate fail "unparseable ledger at $state_file" "$zero_extra"
|
|
126
|
+
return
|
|
127
|
+
fi
|
|
128
|
+
local version
|
|
129
|
+
version=$(jq -r '.version // ""' <<< "$ledger" 2>/dev/null)
|
|
130
|
+
if [ "$version" != "1" ]; then
|
|
131
|
+
vd_gate fail "ledger schema mismatch at $state_file (version=\"$version\", expected 1)" "$zero_extra"
|
|
132
|
+
return
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
local total steps_count
|
|
136
|
+
total=$(jq -r '.summary.total // 0' <<< "$ledger" 2>/dev/null || echo 0)
|
|
137
|
+
steps_count=$(jq -r '.steps | length' <<< "$ledger" 2>/dev/null || echo 0)
|
|
138
|
+
if [ "$total" = "0" ] || [ "$steps_count" = "0" ]; then
|
|
139
|
+
vd_gate skip "empty plan ledger" "$zero_extra"
|
|
140
|
+
return
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
local summary_json
|
|
144
|
+
summary_json=$(jq --arg cur "$cur" '
|
|
145
|
+
def is_fresh: (.status == "green") and (.diff_sha == $cur);
|
|
146
|
+
{ total: (.steps | length), fresh_green: ([.steps[] | select(is_fresh)] | length) }
|
|
147
|
+
' <<< "$ledger")
|
|
148
|
+
|
|
149
|
+
local blockers
|
|
150
|
+
blockers=$(jq -r --arg cur "$cur" '
|
|
151
|
+
.steps[]
|
|
152
|
+
| (if (.status == "green" and .diff_sha == $cur) then "green"
|
|
153
|
+
elif (.status == "green") then "stale"
|
|
154
|
+
else (.status // "pending") end) as $eff
|
|
155
|
+
| select($eff != "green")
|
|
156
|
+
| (.id // "?") + ": " + $eff
|
|
157
|
+
' <<< "$ledger" 2>/dev/null || echo "")
|
|
158
|
+
|
|
159
|
+
local ac_uncovered=0 plan_doc spec_field spec_abs acs_json
|
|
160
|
+
plan_doc=$(jq -r '.plan_doc // ""' <<< "$ledger" 2>/dev/null || echo "")
|
|
161
|
+
if [ -n "$plan_doc" ]; then
|
|
162
|
+
local plan_abs="$plan_doc"
|
|
163
|
+
case "$plan_doc" in
|
|
164
|
+
/*) : ;;
|
|
165
|
+
*) [ -f "$plan_abs" ] || plan_abs="$repo_root/$plan_doc" ;;
|
|
166
|
+
esac
|
|
167
|
+
if [ -f "$plan_abs" ]; then
|
|
168
|
+
spec_field=$(pl_doc_field "$plan_abs" Spec)
|
|
169
|
+
case "$(printf '%s' "$spec_field" | tr '[:upper:]' '[:lower:]')" in
|
|
170
|
+
""|none) : ;;
|
|
171
|
+
*)
|
|
172
|
+
spec_abs="$spec_field"
|
|
173
|
+
case "$spec_field" in
|
|
174
|
+
/*) : ;;
|
|
175
|
+
*) [ -f "$spec_abs" ] || spec_abs="$repo_root/$spec_field" ;;
|
|
176
|
+
esac
|
|
177
|
+
if [ -f "$spec_abs" ]; then
|
|
178
|
+
acs_json=$(pl_parse_acs "$spec_abs" | jq -Rsc 'split("\n") | map(select(length > 0))')
|
|
179
|
+
if [ -n "$acs_json" ] && [ "$acs_json" != "[]" ]; then
|
|
180
|
+
ac_uncovered=$(jq -n --argjson l "$ledger" --arg cur "$cur" --argjson acs "$acs_json" '
|
|
181
|
+
[ $acs[] as $ac
|
|
182
|
+
| ([$l.steps[] | select((.ac_refs // []) | index($ac))]) as $cov
|
|
183
|
+
| ($cov | any(.status == "green" and .diff_sha == $cur)) as $fresh
|
|
184
|
+
| select($fresh | not)
|
|
185
|
+
] | length
|
|
186
|
+
' 2>/dev/null) || ac_uncovered=0
|
|
187
|
+
fi
|
|
188
|
+
fi
|
|
189
|
+
;;
|
|
190
|
+
esac
|
|
191
|
+
fi
|
|
192
|
+
fi
|
|
193
|
+
|
|
194
|
+
local extra
|
|
195
|
+
extra=$(jq -cn --argjson summary "$summary_json" --argjson ac "$ac_uncovered" '{summary: $summary, ac_uncovered: $ac}')
|
|
196
|
+
|
|
197
|
+
if [ -z "$blockers" ]; then
|
|
198
|
+
vd_gate pass "all plan-ledger steps fresh-green" "$extra"
|
|
199
|
+
else
|
|
200
|
+
vd_gate fail "steps not fresh-green: $(printf '%s' "$blockers" | tr '\n' ',' | sed 's/,$//')" "$extra"
|
|
201
|
+
fi
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
# vd_gate_review REPO_ROOT BRANCH BASE CUR
|
|
205
|
+
# Implements the push-review v2 schema directly (version normalizes to "2",
|
|
206
|
+
# reviewed_files must equal the sorted current diff) against the same state
|
|
207
|
+
# file pre-tools/modules/push-review.sh reads — independent of that gate's own
|
|
208
|
+
# v1-vs-v2 migration (spec component 3 + 7). reason_code is a closed set:
|
|
209
|
+
# empty-diff | stale-diff | schema-v1 | file-coverage | no-state | round-cap |
|
|
210
|
+
# findings | reviewer | pass.
|
|
211
|
+
vd_gate_review() {
|
|
212
|
+
local repo_root="$1" branch="$2" base="$3" cur="$4"
|
|
213
|
+
local empty_blob_sha="e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"
|
|
214
|
+
|
|
215
|
+
if [ "$branch" = "$base" ]; then
|
|
216
|
+
vd_gate skip "current branch is the base branch" '{"reason_code":null,"round":null}'
|
|
217
|
+
return
|
|
218
|
+
fi
|
|
219
|
+
if [ -z "$cur" ]; then
|
|
220
|
+
vd_gate skip "could not compute diff against $base" '{"reason_code":null,"round":null}'
|
|
221
|
+
return
|
|
222
|
+
fi
|
|
223
|
+
if [ "$cur" = "$empty_blob_sha" ]; then
|
|
224
|
+
vd_gate fail "diff against $base is empty; verify intent before pushing" \
|
|
225
|
+
'{"reason_code":"empty-diff","round":null}'
|
|
226
|
+
return
|
|
227
|
+
fi
|
|
228
|
+
|
|
229
|
+
local slug state_dir state_file
|
|
230
|
+
slug=$(branch_slug "$branch")
|
|
231
|
+
state_dir="${STATE_DIR:-$(toolu_project_state_dir push-review "$repo_root")}"
|
|
232
|
+
state_file="$state_dir/${slug}.json"
|
|
233
|
+
|
|
234
|
+
if [ ! -f "$state_file" ]; then
|
|
235
|
+
vd_gate fail "no push-review state file; run a reviewer and write the state" \
|
|
236
|
+
'{"reason_code":"no-state","round":null}'
|
|
237
|
+
return
|
|
238
|
+
fi
|
|
239
|
+
|
|
240
|
+
local state_json
|
|
241
|
+
state_json=$(cat "$state_file" 2>/dev/null) || state_json=""
|
|
242
|
+
if [ -z "$state_json" ] || ! jq -e . <<< "$state_json" >/dev/null 2>&1; then
|
|
243
|
+
# Present-but-garbage is a schema problem, not an absence — "no-state" is
|
|
244
|
+
# reserved for the file-absent case above, matching push-review.sh (an
|
|
245
|
+
# unparseable state there falls through to its version-mismatch "schema"
|
|
246
|
+
# deny, never its file-absent "no-state" one).
|
|
247
|
+
vd_gate fail "push-review state file is unparseable at $state_file" \
|
|
248
|
+
'{"reason_code":"schema","round":null}'
|
|
249
|
+
return
|
|
250
|
+
fi
|
|
251
|
+
|
|
252
|
+
local round_json
|
|
253
|
+
round_json=$(jq -c '(.review_round // 1) | if type == "number" then . else null end' <<< "$state_json" 2>/dev/null)
|
|
254
|
+
[ -n "$round_json" ] || round_json="null"
|
|
255
|
+
|
|
256
|
+
local version_norm state_sha state_findings
|
|
257
|
+
version_norm=$(jq -r '(.version // "") | tostring' <<< "$state_json" 2>/dev/null)
|
|
258
|
+
state_sha=$(jq -r '.diff_sha // ""' <<< "$state_json" 2>/dev/null)
|
|
259
|
+
state_findings=$(jq -r '(.findings_count // "") | tostring' <<< "$state_json" 2>/dev/null)
|
|
260
|
+
|
|
261
|
+
# schema-v1 is reserved for a state whose version normalizes to exactly "1"
|
|
262
|
+
# (the one-time-upgrade case push-review.sh gives its own deny message for);
|
|
263
|
+
# anything else malformed — version outside {1,2}, or a v2 missing diff_sha/
|
|
264
|
+
# findings_count — is the generic "schema" code, mirroring push-review.sh's
|
|
265
|
+
# own split between its version==1 check and its version!=2-or-corrupt check.
|
|
266
|
+
# A v2 state with reviewed_files missing/non-array deliberately falls THROUGH
|
|
267
|
+
# to the file-coverage check (reads as empty set, denies naming every changed
|
|
268
|
+
# path) — exactly the gate's documented choice; do not type-check it here.
|
|
269
|
+
if [ "$version_norm" = "1" ]; then
|
|
270
|
+
vd_gate fail "push-review state is schema v1; harness v2 requires reviewed_files — re-run the review to regenerate the state file" \
|
|
271
|
+
"$(jq -cn --argjson round "$round_json" '{reason_code:"schema-v1", round:$round}')"
|
|
272
|
+
return
|
|
273
|
+
fi
|
|
274
|
+
if [ "$version_norm" != "2" ] || [ -z "$state_sha" ] || [ -z "$state_findings" ]; then
|
|
275
|
+
vd_gate fail "push-review state file is corrupted or missing required v2 fields at $state_file" \
|
|
276
|
+
"$(jq -cn --argjson round "$round_json" '{reason_code:"schema", round:$round}')"
|
|
277
|
+
return
|
|
278
|
+
fi
|
|
279
|
+
|
|
280
|
+
if ! jq -e --argjson acc "$ACCEPTED_REVIEWERS" \
|
|
281
|
+
'(.reviewers // []) as $r | any($acc[]; . as $x | $r | index($x) != null)' \
|
|
282
|
+
<<< "$state_json" >/dev/null 2>&1; then
|
|
283
|
+
vd_gate fail "state file lists no accepted reviewer" \
|
|
284
|
+
"$(jq -cn --argjson round "$round_json" '{reason_code:"reviewer", round:$round}')"
|
|
285
|
+
return
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
local round_num
|
|
289
|
+
round_num=$(jq -r '(.review_round // 1) | if type == "number" then (floor|tostring) else "" end' <<< "$state_json" 2>/dev/null)
|
|
290
|
+
if [ -n "$round_num" ] && [ "$round_num" -gt 5 ]; then
|
|
291
|
+
vd_gate escalate "review loop hit $round_num rounds (max 5) on an unchanged diff" \
|
|
292
|
+
"$(jq -cn --argjson round "$round_json" '{reason_code:"round-cap", round:$round}')"
|
|
293
|
+
return
|
|
294
|
+
fi
|
|
295
|
+
|
|
296
|
+
if [ "$state_sha" != "$cur" ]; then
|
|
297
|
+
vd_gate fail "diff changed since review (state stale)" \
|
|
298
|
+
"$(jq -cn --argjson round "$round_json" '{reason_code:"stale-diff", round:$round}')"
|
|
299
|
+
return
|
|
300
|
+
fi
|
|
301
|
+
|
|
302
|
+
local changed_sorted reviewed_sorted
|
|
303
|
+
changed_sorted=$(git -C "$repo_root" diff --no-color "${base}...HEAD" --name-only 2>/dev/null | sort -u)
|
|
304
|
+
reviewed_sorted=$(jq -r '.reviewed_files[]' <<< "$state_json" 2>/dev/null | sort -u)
|
|
305
|
+
if [ "$changed_sorted" != "$reviewed_sorted" ]; then
|
|
306
|
+
vd_gate fail "reviewed_files does not match the current diff's changed paths" \
|
|
307
|
+
"$(jq -cn --argjson round "$round_json" '{reason_code:"file-coverage", round:$round}')"
|
|
308
|
+
return
|
|
309
|
+
fi
|
|
310
|
+
|
|
311
|
+
if [ "$state_findings" != "0" ]; then
|
|
312
|
+
vd_gate fail "code review has open findings ($state_findings)" \
|
|
313
|
+
"$(jq -cn --argjson round "$round_json" '{reason_code:"findings", round:$round}')"
|
|
314
|
+
return
|
|
315
|
+
fi
|
|
316
|
+
|
|
317
|
+
vd_gate pass "review satisfied" "$(jq -cn --argjson round "$round_json" '{reason_code:"pass", round:$round}')"
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
# vd_gate_docs REPO_ROOT BRANCH
|
|
321
|
+
# Mirrors pre-tools/modules/docs-sync.sh's has_code/has_doc classification
|
|
322
|
+
# (via docs-sync-config.sh's glob readers) and attestation check, reading the
|
|
323
|
+
# attestation dir via docs-sync's OWN path resolution — NOT REPO_ROOT — so a
|
|
324
|
+
# worktree + CLAUDE_PROJECT_DIR setup is found the same way the real writer
|
|
325
|
+
# resolves it. "fail" is only reachable under docsSync.mode=block.
|
|
326
|
+
vd_gate_docs() {
|
|
327
|
+
local repo_root="$1" branch="$2" base mode
|
|
328
|
+
|
|
329
|
+
base="${DOCS_SYNC_BASE:-$(detect_base_branch "$repo_root")}"
|
|
330
|
+
mode=$(toolu_string "docsSync.mode" "advise" "advise" "block" "off")
|
|
331
|
+
if [ "$mode" = "off" ]; then
|
|
332
|
+
vd_gate skip "docsSync.mode is off"
|
|
333
|
+
return
|
|
334
|
+
fi
|
|
335
|
+
|
|
336
|
+
if [ -z "$branch" ] || [ "$branch" = "HEAD" ]; then
|
|
337
|
+
vd_gate skip "detached HEAD"
|
|
338
|
+
return
|
|
339
|
+
fi
|
|
340
|
+
if ! git -C "$repo_root" rev-parse --verify --quiet "$base" >/dev/null 2>&1; then
|
|
341
|
+
vd_gate skip "base branch '$base' not found locally"
|
|
342
|
+
return
|
|
343
|
+
fi
|
|
344
|
+
if [ "$branch" = "$base" ]; then
|
|
345
|
+
vd_gate skip "current branch is the base branch"
|
|
346
|
+
return
|
|
347
|
+
fi
|
|
348
|
+
|
|
349
|
+
local changed
|
|
350
|
+
changed=$(git -C "$repo_root" diff --no-color "${base}...HEAD" --name-only 2>/dev/null || echo "")
|
|
351
|
+
if [ -z "$changed" ]; then
|
|
352
|
+
vd_gate skip "no diff against $base"
|
|
353
|
+
return
|
|
354
|
+
fi
|
|
355
|
+
|
|
356
|
+
local surfaces surface_excludes code_surfaces
|
|
357
|
+
surfaces=$(docs_sync_surfaces)
|
|
358
|
+
surface_excludes=$(docs_sync_surface_excludes)
|
|
359
|
+
code_surfaces=$(docs_sync_code_surfaces)
|
|
360
|
+
|
|
361
|
+
local has_doc=0 has_code=0 path
|
|
362
|
+
while IFS= read -r path; do
|
|
363
|
+
[ -z "$path" ] && continue
|
|
364
|
+
if _vd_matches_any "$path" "$surfaces" && ! _vd_matches_any "$path" "$surface_excludes"; then
|
|
365
|
+
has_doc=1
|
|
366
|
+
fi
|
|
367
|
+
if _vd_matches_any "$path" "$code_surfaces"; then has_code=1; fi
|
|
368
|
+
done <<< "$changed"
|
|
369
|
+
|
|
370
|
+
if [ "$has_code" != "1" ] || [ "$has_doc" = "1" ]; then
|
|
371
|
+
vd_gate pass "doc surface in sync"
|
|
372
|
+
return
|
|
373
|
+
fi
|
|
374
|
+
|
|
375
|
+
# Code changed, no doc surface touched. Attestation dir mirrors docs-sync.sh
|
|
376
|
+
# EXACTLY (not repo_root): a worktree running with CLAUDE_PROJECT_DIR set
|
|
377
|
+
# must find the same file the writer wrote.
|
|
378
|
+
local diff_sha state_dir state_file
|
|
379
|
+
diff_sha=$(toolu_diff_sha "$repo_root" "$base") || diff_sha=""
|
|
380
|
+
state_dir="${DOCS_SYNC_STATE_DIR:-$(toolu_project_state_dir docs-sync)}"
|
|
381
|
+
state_file="$state_dir/$(branch_slug "$branch").json"
|
|
382
|
+
if [ -f "$state_file" ] && [ -n "$diff_sha" ]; then
|
|
383
|
+
local attested_sha
|
|
384
|
+
attested_sha=$(jq -r '.diff_sha // ""' "$state_file" 2>/dev/null || echo "")
|
|
385
|
+
if [ "$attested_sha" = "$diff_sha" ]; then
|
|
386
|
+
vd_gate pass "doc change attested as not needed"
|
|
387
|
+
return
|
|
388
|
+
fi
|
|
389
|
+
fi
|
|
390
|
+
|
|
391
|
+
if [ "$mode" = "block" ]; then
|
|
392
|
+
vd_gate fail "code changed without a doc update (docsSync.mode=block)"
|
|
393
|
+
else
|
|
394
|
+
vd_gate advise "code changed without a doc update"
|
|
395
|
+
fi
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
# vd_render_status JSON
|
|
399
|
+
# Print JSON's data as a compact, aligned human table (same data as `json`).
|
|
400
|
+
vd_render_status() {
|
|
401
|
+
local json="$1" branch base sha overall gate state reason
|
|
402
|
+
branch=$(jq -r '.branch' <<< "$json")
|
|
403
|
+
base=$(jq -r '.base_branch' <<< "$json")
|
|
404
|
+
sha=$(jq -r '.diff_sha' <<< "$json")
|
|
405
|
+
overall=$(jq -r '.overall' <<< "$json")
|
|
406
|
+
|
|
407
|
+
printf 'verdict: %s vs %s (diff %s)\n' "$branch" "$base" "${sha:0:12}"
|
|
408
|
+
printf '%-8s %-9s %s\n' "GATE" "STATE" "REASON"
|
|
409
|
+
for gate in quality plan review docs; do
|
|
410
|
+
state=$(jq -r --arg g "$gate" '.gates[$g].state' <<< "$json")
|
|
411
|
+
reason=$(jq -r --arg g "$gate" '.gates[$g].reason' <<< "$json")
|
|
412
|
+
printf '%-8s %-9s %s\n' "$gate" "$state" "$reason"
|
|
413
|
+
done
|
|
414
|
+
printf 'overall: %s\n' "$overall"
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
# main MODE (status|json) -> print the report, return 0 ready / 1 blocked / 2 error.
|
|
418
|
+
main() {
|
|
419
|
+
local mode="${1:-}"
|
|
420
|
+
case "$mode" in
|
|
421
|
+
status|json) : ;;
|
|
422
|
+
*) echo "verdict: usage: status | json" >&2; return 2 ;;
|
|
423
|
+
esac
|
|
424
|
+
|
|
425
|
+
command -v git >/dev/null 2>&1 || { echo "verdict: git is required" >&2; return 2; }
|
|
426
|
+
command -v jq >/dev/null 2>&1 || { echo "verdict: jq is required" >&2; return 2; }
|
|
427
|
+
|
|
428
|
+
local repo_root
|
|
429
|
+
repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || repo_root=""
|
|
430
|
+
[ -n "$repo_root" ] || { echo "verdict: not a git repository" >&2; return 2; }
|
|
431
|
+
|
|
432
|
+
local branch base cur
|
|
433
|
+
branch=$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
|
434
|
+
base="${PUSH_REVIEW_BASE:-$(detect_base_branch "$repo_root")}"
|
|
435
|
+
cur=$(toolu_diff_sha "$repo_root" "$base") || cur=""
|
|
436
|
+
|
|
437
|
+
local g_quality g_plan g_review g_docs
|
|
438
|
+
g_quality=$(vd_gate_quality "$repo_root")
|
|
439
|
+
g_plan=$(vd_gate_plan "$repo_root" "$branch" "$base" "$cur")
|
|
440
|
+
g_review=$(vd_gate_review "$repo_root" "$branch" "$base" "$cur")
|
|
441
|
+
g_docs=$(vd_gate_docs "$repo_root" "$branch")
|
|
442
|
+
|
|
443
|
+
local overall
|
|
444
|
+
if ! overall=$(jq -rn --argjson q "$g_quality" --argjson p "$g_plan" --argjson r "$g_review" --argjson d "$g_docs" '
|
|
445
|
+
if ([$q, $p, $r, $d] | any(.state == "fail" or .state == "escalate"))
|
|
446
|
+
then "blocked" else "ready" end
|
|
447
|
+
' 2>/dev/null) || [ -z "$overall" ]; then
|
|
448
|
+
echo "verdict: failed to assemble gate results (one of the four gate functions produced empty/invalid JSON)" >&2
|
|
449
|
+
return 2
|
|
450
|
+
fi
|
|
451
|
+
|
|
452
|
+
local out
|
|
453
|
+
out=$(jq -n \
|
|
454
|
+
--arg branch "$branch" --arg base "$base" --arg sha "$cur" \
|
|
455
|
+
--arg overall "$overall" --arg now "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
456
|
+
--argjson quality "$g_quality" --argjson plan "$g_plan" \
|
|
457
|
+
--argjson review "$g_review" --argjson docs "$g_docs" '
|
|
458
|
+
{ version: 1, branch: $branch, base_branch: $base, diff_sha: $sha,
|
|
459
|
+
overall: $overall,
|
|
460
|
+
gates: { quality: $quality, plan: $plan, review: $review, docs: $docs },
|
|
461
|
+
generated_at: $now }
|
|
462
|
+
')
|
|
463
|
+
|
|
464
|
+
if [ "$mode" = "json" ]; then
|
|
465
|
+
printf '%s\n' "$out"
|
|
466
|
+
else
|
|
467
|
+
vd_render_status "$out"
|
|
468
|
+
fi
|
|
469
|
+
|
|
470
|
+
[ "$overall" = "ready" ] && return 0 || return 1
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
# Guarded main: run only when executed directly, not when sourced.
|
|
474
|
+
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
|
|
475
|
+
main "$@"
|
|
476
|
+
exit $?
|
|
477
|
+
fi
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Post-tool-use hook dispatcher (entrypoint).
|
|
3
|
+
# Runs all scripts in .claude/hooks/post-tools/modules/*.sh sequentially.
|
|
4
|
+
# Each script receives $input, $tool_name, and $PROJECT_ROOT as env vars plus
|
|
5
|
+
# $input on stdin.
|
|
6
|
+
#
|
|
7
|
+
# Dispatch, decision short-circuit (PostToolUse uses decision:"block" +
|
|
8
|
+
# reason; permissionDecision is PreToolUse-only and ignored here), advisory
|
|
9
|
+
# merging, and module exit-code semantics live in lib/dispatch.sh.
|
|
10
|
+
|
|
11
|
+
HOOK_LIB="$(cd "$(dirname "$0")/../lib" && pwd)"
|
|
12
|
+
# shellcheck source=../lib/config.sh
|
|
13
|
+
. "$HOOK_LIB/config.sh"
|
|
14
|
+
# shellcheck source=../lib/dispatch.sh
|
|
15
|
+
. "$HOOK_LIB/dispatch.sh"
|
|
16
|
+
# shellcheck source=../lib/detect.sh
|
|
17
|
+
. "$HOOK_LIB/detect.sh"
|
|
18
|
+
# shellcheck source=../lib/registry.sh
|
|
19
|
+
. "$HOOK_LIB/registry.sh"
|
|
20
|
+
|
|
21
|
+
if ! toolu_enabled hooks post-tools; then
|
|
22
|
+
cat > /dev/null 2>&1 || true
|
|
23
|
+
exit 0
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Exported only once the hook is enabled, so disabled runs keep the env clean.
|
|
27
|
+
export TOOLU_LIB_DIR="$HOOK_LIB"
|
|
28
|
+
TOOLU_CONFIG_DIR="$(toolu_config_root)"
|
|
29
|
+
export TOOLU_CONFIG_DIR
|
|
30
|
+
|
|
31
|
+
input=$(cat 2>/dev/null || echo "{}")
|
|
32
|
+
tool_name=$(jq -r '.tool_name // ""' <<<"$input" 2>/dev/null || echo "")
|
|
33
|
+
|
|
34
|
+
PROJECT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
35
|
+
export PATH="$PROJECT_ROOT/node_modules/.bin:$PATH"
|
|
36
|
+
|
|
37
|
+
export input tool_name PROJECT_ROOT
|
|
38
|
+
|
|
39
|
+
HOOK_DIR="$(cd "$(dirname "$0")" && pwd)/modules"
|
|
40
|
+
|
|
41
|
+
# Exits 2 if a module hard-blocks via exit code 2 (stderr forwarded).
|
|
42
|
+
toolu_dispatch_hook "$HOOK_DIR" "PostToolUse" "$(toolu_registry_event_dir PostToolUse)"
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Post-tool check: Quality gate state tracking
|
|
3
|
+
# Persists global gate state to the host-native project state directory based on
|
|
4
|
+
# the exit code of recognized quality/test commands.
|
|
5
|
+
#
|
|
6
|
+
# Inputs (from parent dispatcher post-tools/mod.sh, via `export`):
|
|
7
|
+
# $tool_name - name of the tool being invoked
|
|
8
|
+
# $input - raw JSON payload on stdin
|
|
9
|
+
# $PROJECT_ROOT - repository root
|
|
10
|
+
|
|
11
|
+
: "${tool_name:=}"
|
|
12
|
+
: "${input:=}"
|
|
13
|
+
: "${PROJECT_ROOT:=$(pwd)}"
|
|
14
|
+
|
|
15
|
+
# Cursor Agent uses tool_name "Shell"; Claude Code uses "Bash".
|
|
16
|
+
[[ "$tool_name" != "Bash" && "$tool_name" != "Shell" ]] && exit 0
|
|
17
|
+
|
|
18
|
+
_toolu_lib="${TOOLU_LIB_DIR:-${BASH_SOURCE%/*}/../../lib}"
|
|
19
|
+
# shellcheck source=../../lib/host.sh
|
|
20
|
+
. "$_toolu_lib/host.sh"
|
|
21
|
+
GATE_DIR="$(toolu_project_state_root "$PROJECT_ROOT")"
|
|
22
|
+
GATE_FILE="$GATE_DIR/quality-gate-status.json"
|
|
23
|
+
mkdir -p "$GATE_DIR"
|
|
24
|
+
|
|
25
|
+
command=$(echo "$input" | jq -r '.tool_input.command // ""' 2>/dev/null || echo "")
|
|
26
|
+
exit_code=$(echo "$input" | jq -r '.tool_response.metadata.exit_code // .tool_response.exit_code // .tool_output.exit_code // empty' 2>/dev/null || echo "")
|
|
27
|
+
if [[ -z "$exit_code" || "$exit_code" == "null" ]]; then
|
|
28
|
+
tool_output_raw=$(echo "$input" | jq -r '.tool_output // empty' 2>/dev/null || echo "")
|
|
29
|
+
if [[ -n "$tool_output_raw" ]]; then
|
|
30
|
+
exit_code=$(echo "$tool_output_raw" | jq -r '.exitCode // .exit_code // empty' 2>/dev/null || echo "")
|
|
31
|
+
fi
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# A failing gate owned by a file-level quality hook (ts-quality-hook,
|
|
35
|
+
# rust-quality-hook, ...) is NOT clobbered by what follows: this module records
|
|
36
|
+
# and clears only its OWN command-channel slot (key "__global__", source
|
|
37
|
+
# "gate-status-hook") through gate-file.sh's ownership-checked helpers, so a file
|
|
38
|
+
# hook's entry — owned by a different source — survives until that file is fixed.
|
|
39
|
+
# This replaces an earlier top-level `source == *-quality-hook` guard that, while
|
|
40
|
+
# a file failure was live, dropped a failing command outright; fixing the file
|
|
41
|
+
# then cleared the only tracked entry and flipped the gate to passing even though
|
|
42
|
+
# the command was still failing.
|
|
43
|
+
|
|
44
|
+
# Quality / test commands that should toggle the global gate:
|
|
45
|
+
# * Project tool wrappers: tools/<name>/{check,test,format}.sh (any project basename)
|
|
46
|
+
# * TS/JS: bun run script aliases, bun test, vitest, jest, tsc, ./scripts/ts-check.sh
|
|
47
|
+
# * Rust: cargo clippy/test/build/nextest
|
|
48
|
+
# The wrapper-path regex is project-agnostic; per-project naming lives in the
|
|
49
|
+
# wrapper script, not here.
|
|
50
|
+
#
|
|
51
|
+
# Anchor every alternative at a COMMAND boundary so substring false positives
|
|
52
|
+
# (`cat tsconfig.json` → `tsc`, `vitests-helper` → `vitest`) no longer flip the
|
|
53
|
+
# gate. Unlike quality-gate.sh (which only anchors at start-of-line, inspecting
|
|
54
|
+
# the leading command of a push), gate-status must still detect a quality
|
|
55
|
+
# command that appears AFTER a shell operator in a compound command
|
|
56
|
+
# (`cd crate && cargo test`), so the prefix also matches after a separator.
|
|
57
|
+
# The trailing `|` (single pipe) in this alternation is BY DESIGN: a quality
|
|
58
|
+
# command after a pipe (`find . | cargo test`) still ran, so it must trigger
|
|
59
|
+
# gate registration. Do not drop it. (Locked in by gate-status.bats.)
|
|
60
|
+
GATE_TRIGGER_PREFIX='(^|[[:space:]]|&&|\|\||;|\|)[[:space:]]*'
|
|
61
|
+
GATE_TRIGGER_SUFFIX='([[:space:]]|$|&&|\|\||;|\|)'
|
|
62
|
+
GATE_TRIGGER_ALTERNATION='tools/[A-Za-z0-9_.-]+/(check|test|format)\.sh|bun run (check|check:fix|check:duplication|ts:check|ts:check:fix|rust:check|rust:test|check-types|lint|lint:fix|format|format:check|format:fix|build|test)|bun test|vitest|jest|tsc|\./scripts/ts-check\.sh|cargo (clippy|test|build|nextest)'
|
|
63
|
+
if ! echo "$command" | grep -qE "${GATE_TRIGGER_PREFIX}(${GATE_TRIGGER_ALTERNATION})${GATE_TRIGGER_SUFFIX}"; then
|
|
64
|
+
exit 0
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# Multi-slot gate writer/clearer (entries keyed by file/channel) so a command
|
|
68
|
+
# failure no longer clobbers a file hook's failure and vice-versa. Soft if
|
|
69
|
+
# absent: the fallbacks keep legacy single-slot behavior when the toolu lib
|
|
70
|
+
# predates gate-file.sh.
|
|
71
|
+
# shellcheck source=../../lib/gate-file.sh
|
|
72
|
+
[ -f "$_toolu_lib/gate-file.sh" ] && . "$_toolu_lib/gate-file.sh"
|
|
73
|
+
command -v gate_record_failure >/dev/null 2>&1 || gate_record_failure() {
|
|
74
|
+
jq -n --arg reason "$4" --arg source "$3" --arg file "$2" --arg violations "$5" \
|
|
75
|
+
--arg updatedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
76
|
+
'{status: "failing", reason: $reason, source: $source, file: $file,
|
|
77
|
+
violations: $violations, updatedAt: $updatedAt}' > "$1"
|
|
78
|
+
}
|
|
79
|
+
command -v gate_clear_file >/dev/null 2>&1 || gate_clear_file() {
|
|
80
|
+
[ -f "$1" ] || return 0
|
|
81
|
+
local _src _file
|
|
82
|
+
_src=$(jq -r '.source // ""' "$1" 2>/dev/null || echo "")
|
|
83
|
+
_file=$(jq -r '.file // ""' "$1" 2>/dev/null || echo "")
|
|
84
|
+
if [ "$_src" = "$3" ] && [ "$_file" = "$2" ]; then
|
|
85
|
+
jq -n --arg source "$3" --arg updatedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
86
|
+
'{status: "passing", source: $source, updatedAt: $updatedAt}' > "$1"
|
|
87
|
+
fi
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# The quality-command gate slot. "__global__" is the same key gate-file.sh
|
|
91
|
+
# assigns a no-`file` command failure when it seeds a legacy record, so a record
|
|
92
|
+
# written here and a promoted legacy command failure occupy one slot.
|
|
93
|
+
GATE_COMMAND_KEY="__global__"
|
|
94
|
+
|
|
95
|
+
if [[ "$exit_code" =~ ^[0-9]+$ && "$exit_code" -ne 0 ]]; then
|
|
96
|
+
# Record the command failure as its OWN slot. When a file hook's failure is
|
|
97
|
+
# also live this coexists with it (different source/key); fixing the file later
|
|
98
|
+
# promotes this slot instead of flipping the gate to passing while the command
|
|
99
|
+
# is still broken.
|
|
100
|
+
gate_record_failure "$GATE_FILE" "$GATE_COMMAND_KEY" "gate-status-hook" \
|
|
101
|
+
"Quality command failed: $command (exit $exit_code)" ""
|
|
102
|
+
|
|
103
|
+
jq -n --arg ctx "Global quality gate failing. Fix all errors/warnings/tests before new tasks.\nFailed: $command (exit $exit_code)" '{
|
|
104
|
+
"hookSpecificOutput": {
|
|
105
|
+
"hookEventName": "PostToolUse",
|
|
106
|
+
"additionalContext": $ctx
|
|
107
|
+
}
|
|
108
|
+
}'
|
|
109
|
+
exit 0
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
if [[ "$exit_code" == "0" ]]; then
|
|
113
|
+
# Clear ONLY this command-channel slot. gate_clear_file is ownership-checked: a
|
|
114
|
+
# live *-quality-hook file entry is owned by a different source, so it is left
|
|
115
|
+
# intact and the gate stays failing until that file is fixed too.
|
|
116
|
+
gate_clear_file "$GATE_FILE" "$GATE_COMMAND_KEY" "gate-status-hook"
|
|
117
|
+
|
|
118
|
+
# Assert an affirmative passing record ONLY when no gate file exists yet — the
|
|
119
|
+
# first-ever passing command, with nothing tracked. In every other case
|
|
120
|
+
# gate_clear_file already did the right thing: it flips the gate to passing when
|
|
121
|
+
# our slot was the last entry, and leaves a live file-hook failure (owned by a
|
|
122
|
+
# different source) untouched. Gating on `! -f` alone means we never re-write a
|
|
123
|
+
# file the helper just wrote (no hot-path double-write) and never clobber a
|
|
124
|
+
# foreign failure (the file is present, so we skip).
|
|
125
|
+
# CONCURRENCY: this write shares gate-file.sh's single-writer assumption — safe
|
|
126
|
+
# only while PostToolUse hooks fire serially; see the CONCURRENCY note there.
|
|
127
|
+
if [ ! -f "$GATE_FILE" ]; then
|
|
128
|
+
jq -n \
|
|
129
|
+
--arg status "passing" \
|
|
130
|
+
--arg source "$command" \
|
|
131
|
+
--arg updatedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
132
|
+
'{
|
|
133
|
+
status: $status,
|
|
134
|
+
source: $source,
|
|
135
|
+
updatedAt: $updatedAt
|
|
136
|
+
}' > "$GATE_FILE"
|
|
137
|
+
fi
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
exit 0
|