@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,116 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Push-review waivers — "yes, push anyway", remembered for exactly that diff.
|
|
3
|
+
#
|
|
4
|
+
# A PreToolUse hook never learns how the user answered its `ask`. PostToolUse
|
|
5
|
+
# does, implicitly: it runs only when the tool actually ran. So the two halves
|
|
6
|
+
# talk through a file.
|
|
7
|
+
#
|
|
8
|
+
# 1. push-review.sh asks, and records a PENDING waiver naming the diff SHA
|
|
9
|
+
# it asked about.
|
|
10
|
+
# 2. post-tools/modules/push-waiver.sh sees the push happened, and promotes
|
|
11
|
+
# the pending marker to a real waiver — but only when the push SUCCEEDED
|
|
12
|
+
# and the SHA still matches.
|
|
13
|
+
# 3. push-review.sh finds the waiver on the next push of that same diff and
|
|
14
|
+
# stays quiet. A new commit changes the SHA, so the waiver stops applying
|
|
15
|
+
# and the gate asks again.
|
|
16
|
+
#
|
|
17
|
+
# A refused ask simply leaves the pending marker behind; nothing promotes it,
|
|
18
|
+
# and the state sweeper reclaims it. Both writes are atomic (tmp + mv) so a
|
|
19
|
+
# half-written waiver can never read as a valid one.
|
|
20
|
+
#
|
|
21
|
+
# Public API:
|
|
22
|
+
# push_waiver_dir ROOT
|
|
23
|
+
# push_waiver_path ROOT SLUG
|
|
24
|
+
# push_waiver_pending_path ROOT SLUG
|
|
25
|
+
# push_waiver_matches ROOT SLUG SHA
|
|
26
|
+
# push_waiver_pend ROOT SLUG SHA BASE REASON_CODE
|
|
27
|
+
# push_waiver_promote ROOT SLUG SHA
|
|
28
|
+
|
|
29
|
+
_TOOLU_PUSH_WAIVER_LIB_DIR="$(cd "${BASH_SOURCE%/*}" && pwd)"
|
|
30
|
+
# shellcheck source=host.sh
|
|
31
|
+
. "$_TOOLU_PUSH_WAIVER_LIB_DIR/host.sh"
|
|
32
|
+
|
|
33
|
+
PUSH_WAIVER_VERSION=1
|
|
34
|
+
|
|
35
|
+
# push_waiver_dir ROOT -> the push-review state directory for ROOT.
|
|
36
|
+
# $STATE_DIR overrides, matching push-review.sh and the toolu-review state
|
|
37
|
+
# writer, so tests and worktree flows point all three at the same place.
|
|
38
|
+
push_waiver_dir() {
|
|
39
|
+
local root="${1:-}"
|
|
40
|
+
printf '%s' "${STATE_DIR:-$(toolu_project_state_dir push-review "$root")}"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
push_waiver_path() {
|
|
44
|
+
printf '%s/%s.waiver.json' "$(push_waiver_dir "$1")" "$2"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
push_waiver_pending_path() {
|
|
48
|
+
printf '%s/%s.pending-waiver.json' "$(push_waiver_dir "$1")" "$2"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# _push_waiver_write FILE JSON -> atomic write, 0 on success.
|
|
52
|
+
# A failed mktemp or write leaves no file at all rather than a partial one;
|
|
53
|
+
# the caller decides whether that is fatal (it never is — a missing waiver
|
|
54
|
+
# just means the gate asks again).
|
|
55
|
+
_push_waiver_write() {
|
|
56
|
+
local file="$1" json="$2" tmp
|
|
57
|
+
mkdir -p "$(dirname "$file")" 2>/dev/null || return 1
|
|
58
|
+
tmp=$(mktemp "${file}.XXXXXX" 2>/dev/null) || return 1
|
|
59
|
+
if printf '%s\n' "$json" > "$tmp" 2>/dev/null; then
|
|
60
|
+
mv -f "$tmp" "$file" 2>/dev/null && return 0
|
|
61
|
+
fi
|
|
62
|
+
rm -f "$tmp" 2>/dev/null
|
|
63
|
+
return 1
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# _push_waiver_sha FILE -> print the diff_sha of a v1 waiver-shaped FILE.
|
|
67
|
+
# Prints nothing (and returns 1) for a missing file, unreadable JSON, or a
|
|
68
|
+
# version this code does not understand — all of which must read as "no
|
|
69
|
+
# waiver", never as a match.
|
|
70
|
+
_push_waiver_sha() {
|
|
71
|
+
local file="$1" version sha
|
|
72
|
+
[ -f "$file" ] || return 1
|
|
73
|
+
version=$(jq -r '.version // ""' "$file" 2>/dev/null) || return 1
|
|
74
|
+
[ "$version" = "$PUSH_WAIVER_VERSION" ] || return 1
|
|
75
|
+
sha=$(jq -r '.diff_sha // ""' "$file" 2>/dev/null) || return 1
|
|
76
|
+
[ -n "$sha" ] || return 1
|
|
77
|
+
printf '%s' "$sha"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# push_waiver_matches ROOT SLUG SHA -> 0 iff a waiver covers exactly SHA.
|
|
81
|
+
push_waiver_matches() {
|
|
82
|
+
local root="$1" slug="$2" sha="$3" recorded
|
|
83
|
+
[ -n "$sha" ] || return 1
|
|
84
|
+
recorded=$(_push_waiver_sha "$(push_waiver_path "$root" "$slug")") || return 1
|
|
85
|
+
[ "$recorded" = "$sha" ]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# push_waiver_pend ROOT SLUG SHA BASE REASON_CODE
|
|
89
|
+
# Record that the user was asked about SHA. Overwrites any earlier pending
|
|
90
|
+
# marker: only the most recent question is answerable.
|
|
91
|
+
push_waiver_pend() {
|
|
92
|
+
local root="$1" slug="$2" sha="$3" base="$4" code="$5" json now
|
|
93
|
+
[ -n "$sha" ] || return 1
|
|
94
|
+
now=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
95
|
+
json=$(jq -cn --argjson version "$PUSH_WAIVER_VERSION" --arg branch "$slug" \
|
|
96
|
+
--arg sha "$sha" --arg base "$base" --arg code "$code" --arg now "$now" \
|
|
97
|
+
'{version: $version, branch: $branch, diff_sha: $sha, base_branch: $base,
|
|
98
|
+
reason_code: $code, asked_at: $now}') || return 1
|
|
99
|
+
_push_waiver_write "$(push_waiver_pending_path "$root" "$slug")" "$json"
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# push_waiver_promote ROOT SLUG SHA -> 0 iff a waiver was written.
|
|
103
|
+
# The SHA guard is the whole point: a pending marker from an older diff must
|
|
104
|
+
# not be cashed in by a push of different code.
|
|
105
|
+
push_waiver_promote() {
|
|
106
|
+
local root="$1" slug="$2" sha="$3" pending pending_sha json now
|
|
107
|
+
[ -n "$sha" ] || return 1
|
|
108
|
+
pending=$(push_waiver_pending_path "$root" "$slug")
|
|
109
|
+
pending_sha=$(_push_waiver_sha "$pending") || return 1
|
|
110
|
+
[ "$pending_sha" = "$sha" ] || return 1
|
|
111
|
+
now=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
112
|
+
json=$(jq -c --arg now "$now" 'del(.asked_at) + {waived_at: $now}' "$pending" 2>/dev/null) || return 1
|
|
113
|
+
_push_waiver_write "$(push_waiver_path "$root" "$slug")" "$json" || return 1
|
|
114
|
+
rm -f "$pending" 2>/dev/null
|
|
115
|
+
return 0
|
|
116
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Quality-threshold resolver for the rust-quality / ts-quality hooks.
|
|
3
|
+
#
|
|
4
|
+
# Limits are NOT hardcoded in the quality modules. Each threshold resolves with
|
|
5
|
+
# this precedence (first hit wins, always a positive integer):
|
|
6
|
+
# 1. Project / user override — `lang.<lang>.<key>` in the merged toolu
|
|
7
|
+
# config (.claude/toolu.config.json), via lib/config.sh.
|
|
8
|
+
# 2. Native tool config (TS + maxFileLines only) — the `max-lines` rule from
|
|
9
|
+
# .eslintrc.json / .oxlintrc.json. Flat eslint config (eslint.config.{js,mjs,ts})
|
|
10
|
+
# is JS and NOT parseable here — skipped gracefully.
|
|
11
|
+
# 3. Built-in default (DEFAULT_* below).
|
|
12
|
+
#
|
|
13
|
+
# Every reader is guarded (jq present, file present) and 2>/dev/null — any
|
|
14
|
+
# failure falls through to the next layer. Never errors, never blocks a tool.
|
|
15
|
+
#
|
|
16
|
+
# Source via: . "$TOOLU_LIB_DIR/quality-config.sh"
|
|
17
|
+
# (pulls in config.sh + detect.sh from the same dir).
|
|
18
|
+
|
|
19
|
+
_QC_LIB_DIR="${TOOLU_LIB_DIR:-${BASH_SOURCE%/*}}"
|
|
20
|
+
# shellcheck source=./config.sh
|
|
21
|
+
[ -f "$_QC_LIB_DIR/config.sh" ] && . "$_QC_LIB_DIR/config.sh"
|
|
22
|
+
# shellcheck source=./detect.sh
|
|
23
|
+
[ -f "$_QC_LIB_DIR/detect.sh" ] && . "$_QC_LIB_DIR/detect.sh"
|
|
24
|
+
|
|
25
|
+
# Built-in defaults — single source of truth, overridable from the environment
|
|
26
|
+
# (tests set these to assert fall-through without touching call sites).
|
|
27
|
+
: "${DEFAULT_TS_MAX_FILE_LINES:=300}"
|
|
28
|
+
: "${DEFAULT_TS_MAX_FN_LINES:=60}"
|
|
29
|
+
: "${DEFAULT_RUST_MAX_FILE_LINES:=500}"
|
|
30
|
+
: "${DEFAULT_RUST_MAX_FN_LINES:=50}"
|
|
31
|
+
: "${DEFAULT_RUST_MAX_IMPL_LINES:=200}"
|
|
32
|
+
: "${DEFAULT_PYTHON_MAX_FILE_LINES:=400}"
|
|
33
|
+
: "${DEFAULT_PYTHON_MAX_FN_LINES:=50}"
|
|
34
|
+
|
|
35
|
+
# One jq filter that normalizes every eslint/oxlint `max-lines` encoding to a
|
|
36
|
+
# positive integer (or empty): bare number, ["error", N], ["error", {"max": N}].
|
|
37
|
+
# "off" / 0 / negatives / missing all yield empty -> the layer is skipped.
|
|
38
|
+
_QC_ESLINT_MAXLINES_FILTER='
|
|
39
|
+
.rules?["max-lines"]?
|
|
40
|
+
| if type=="array" then (
|
|
41
|
+
if (.[0] == "off" or .[0] == 0) then empty # rule disabled by severity
|
|
42
|
+
else ( .[1] | if type=="object" then .max else . end )
|
|
43
|
+
end )
|
|
44
|
+
elif (type=="number" or type=="string") then .
|
|
45
|
+
else empty end
|
|
46
|
+
| (if type=="string" then (tonumber? // empty) else . end) # accept "120"
|
|
47
|
+
| if type=="number" and . > 0 then (.|floor|tostring) else empty end
|
|
48
|
+
'
|
|
49
|
+
|
|
50
|
+
# _qc_project_override LANG KEY -> echoes a positive int or "".
|
|
51
|
+
# Reads the already-merged config cache (config.sh handles user+project merge,
|
|
52
|
+
# malformed JSON, and missing jq).
|
|
53
|
+
_qc_project_override() {
|
|
54
|
+
local lang="$1" key="$2"
|
|
55
|
+
command -v toolu_load_config >/dev/null 2>&1 || return 0
|
|
56
|
+
toolu_load_config
|
|
57
|
+
[ "${_TOOLU_HAS_JQ:-0}" = "1" ] || return 0
|
|
58
|
+
[ -n "${TOOLU_CFG_JSON:-}" ] || return 0
|
|
59
|
+
jq -r --arg l "$lang" --arg k "$key" '
|
|
60
|
+
((.lang? // {})[$l]? // {})[$k]?
|
|
61
|
+
| (if type=="string" then (tonumber? // empty) else . end) # accept "120"
|
|
62
|
+
| if type=="number" and . > 0 then (.|floor|tostring) else empty end
|
|
63
|
+
' <<< "$TOOLU_CFG_JSON" 2>/dev/null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# quality_flag LANG KEY DEFAULT -> echoes "true" or "false".
|
|
67
|
+
# Boolean reader for `.lang.<lang>.<key>` in the merged config, for keys that
|
|
68
|
+
# ARE booleans (e.g. noMocks) — _qc_project_override above filters to positive
|
|
69
|
+
# integers and can't carry a boolean, so this is a separate reader rather than
|
|
70
|
+
# an extension of it. Only a literal JSON `true`/`false` is honored; anything
|
|
71
|
+
# else (key absent, wrong type, malformed config, no jq) falls back to DEFAULT
|
|
72
|
+
# unchanged. Never errors, never blocks a tool.
|
|
73
|
+
quality_flag() {
|
|
74
|
+
local lang="$1" key="$2" def="$3"
|
|
75
|
+
command -v toolu_load_config >/dev/null 2>&1 || { printf '%s' "$def"; return 0; }
|
|
76
|
+
toolu_load_config
|
|
77
|
+
[ "${_TOOLU_HAS_JQ:-0}" = "1" ] || { printf '%s' "$def"; return 0; }
|
|
78
|
+
[ -n "${TOOLU_CFG_JSON:-}" ] || { printf '%s' "$def"; return 0; }
|
|
79
|
+
local val
|
|
80
|
+
val=$(jq -r --arg l "$lang" --arg k "$key" '
|
|
81
|
+
((.lang? // {})[$l]? // {})[$k]?
|
|
82
|
+
| if type == "boolean" then tostring else empty end
|
|
83
|
+
' <<< "$TOOLU_CFG_JSON" 2>/dev/null)
|
|
84
|
+
case "$val" in
|
|
85
|
+
true|false) printf '%s' "$val" ;;
|
|
86
|
+
*) printf '%s' "$def" ;;
|
|
87
|
+
esac
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Memoize the git toplevel for this process. detect_project_root shells out to
|
|
91
|
+
# `git rev-parse` every call; the root can't change mid-hook, so cache it once
|
|
92
|
+
# (mirrors toolu_load_config's caching). A separate _CACHED flag is used so
|
|
93
|
+
# an EMPTY result (non-git cwd) is still treated as cached — keying on
|
|
94
|
+
# _QC_PROJECT_ROOT being non-empty would re-shell git on every call there. Tests
|
|
95
|
+
# re-source this file (resetting both) between cases.
|
|
96
|
+
_QC_PROJECT_ROOT=""
|
|
97
|
+
_QC_PROJECT_ROOT_CACHED=0
|
|
98
|
+
_qc_project_root() {
|
|
99
|
+
[ "$_QC_PROJECT_ROOT_CACHED" = "1" ] && { printf '%s' "$_QC_PROJECT_ROOT"; return 0; }
|
|
100
|
+
command -v detect_project_root >/dev/null 2>&1 || return 0
|
|
101
|
+
_QC_PROJECT_ROOT=$(detect_project_root)
|
|
102
|
+
_QC_PROJECT_ROOT_CACHED=1
|
|
103
|
+
printf '%s' "$_QC_PROJECT_ROOT"
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# _qc_native_ts_max_lines -> echoes a positive int or "".
|
|
107
|
+
# Reads the config of the linter detect_ts_linter reports as ACTIVE, so a repo
|
|
108
|
+
# carrying BOTH .eslintrc.json and .oxlintrc.json (e.g. mid-migration) gets the
|
|
109
|
+
# limit from the one it actually runs — not whichever file is listed first.
|
|
110
|
+
# detect_ts_linter precedence is biome > oxc > eslint; biome's max-lines isn't
|
|
111
|
+
# parsed here (no machine-readable rule), so it falls through to the default.
|
|
112
|
+
# Limitation: only JSON config forms are parsed — `.eslintrc.yaml/.yml`, flat
|
|
113
|
+
# `eslint.config.*`, and `package.json#eslintConfig` fall through to the default.
|
|
114
|
+
# detect_ts_linter still reports "eslint" for those, so the over-limit advisory
|
|
115
|
+
# stays honest ("limit didn't come from its config").
|
|
116
|
+
# Limitation: only the top-level `.rules["max-lines"]` is read. A per-glob
|
|
117
|
+
# `overrides[].rules["max-lines"]` is intentionally NOT traversed — picking a
|
|
118
|
+
# value without matching the override's `files` glob to the edited file could
|
|
119
|
+
# enforce a limit that doesn't apply and wrongly BLOCK an edit. We fall through
|
|
120
|
+
# to the (stricter-or-equal) default instead; the over-limit advisory says so.
|
|
121
|
+
_qc_native_ts_max_lines() {
|
|
122
|
+
command -v jq >/dev/null 2>&1 || return 0
|
|
123
|
+
local root linter f v
|
|
124
|
+
root=$(_qc_project_root)
|
|
125
|
+
[ -n "$root" ] || return 0
|
|
126
|
+
# Map the active linter to its machine-readable config file.
|
|
127
|
+
linter=""
|
|
128
|
+
command -v detect_ts_linter >/dev/null 2>&1 && linter=$(detect_ts_linter)
|
|
129
|
+
case "$linter" in
|
|
130
|
+
oxc) f="$root/.oxlintrc.json" ;;
|
|
131
|
+
eslint) f="$root/.eslintrc.json" ;;
|
|
132
|
+
*) return 0 ;; # biome / none / unparseable form -> default
|
|
133
|
+
esac
|
|
134
|
+
[ -f "$f" ] || return 0
|
|
135
|
+
v=$(jq -r "$_QC_ESLINT_MAXLINES_FILTER" "$f" 2>/dev/null)
|
|
136
|
+
[ -n "$v" ] && printf '%s' "$v"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# quality_threshold LANG KEY DEFAULT -> always echoes a positive integer.
|
|
140
|
+
quality_threshold() {
|
|
141
|
+
local lang="$1" key="$2" def="$3" v
|
|
142
|
+
v=$(_qc_project_override "$lang" "$key")
|
|
143
|
+
[ -n "$v" ] && { printf '%s' "$v"; return 0; }
|
|
144
|
+
if [ "$lang" = "ts" ] && [ "$key" = "maxFileLines" ]; then
|
|
145
|
+
v=$(_qc_native_ts_max_lines)
|
|
146
|
+
[ -n "$v" ] && { printf '%s' "$v"; return 0; }
|
|
147
|
+
fi
|
|
148
|
+
printf '%s' "$def"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# count_code_lines lives in detect.sh (sourced above) — both lang modules
|
|
152
|
+
# hard-require detect.sh, so it needs no per-module fallback.
|
|
153
|
+
|
|
154
|
+
# Echo where ts maxFileLines actually resolved from: override | native | default.
|
|
155
|
+
# Lets the lang module word its advisory honestly — claiming "eslint enforces
|
|
156
|
+
# this" only when the limit really came from a parsed eslint/oxlint config, not
|
|
157
|
+
# when a linter is merely present with a config we can't read (e.g. .eslintrc.cjs).
|
|
158
|
+
ts_max_file_lines_source() {
|
|
159
|
+
[ -n "$(_qc_project_override ts maxFileLines)" ] && { printf 'override'; return; }
|
|
160
|
+
[ -n "$(_qc_native_ts_max_lines)" ] && { printf 'native'; return; }
|
|
161
|
+
printf 'default'
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# Resolve the ts maxFileLines value AND its source in a single pass — prints
|
|
165
|
+
# "<value> <source>". The lang module needs both, and resolving them via two
|
|
166
|
+
# separate calls (ts_max_file_lines + ts_max_file_lines_source) repeats the
|
|
167
|
+
# override/native lookups; this does each lookup once.
|
|
168
|
+
ts_max_file_lines_resolved() {
|
|
169
|
+
local v
|
|
170
|
+
v=$(_qc_project_override ts maxFileLines)
|
|
171
|
+
[ -n "$v" ] && { printf '%s override' "$v"; return; }
|
|
172
|
+
v=$(_qc_native_ts_max_lines)
|
|
173
|
+
[ -n "$v" ] && { printf '%s native' "$v"; return; }
|
|
174
|
+
printf '%s default' "$DEFAULT_TS_MAX_FILE_LINES"
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# Convenience wrappers the quality modules call.
|
|
178
|
+
ts_max_file_lines() { quality_threshold ts maxFileLines "$DEFAULT_TS_MAX_FILE_LINES"; }
|
|
179
|
+
ts_max_fn_lines() { quality_threshold ts maxFnLines "$DEFAULT_TS_MAX_FN_LINES"; }
|
|
180
|
+
rust_max_file_lines() { quality_threshold rust maxFileLines "$DEFAULT_RUST_MAX_FILE_LINES"; }
|
|
181
|
+
rust_max_fn_lines() { quality_threshold rust maxFnLines "$DEFAULT_RUST_MAX_FN_LINES"; }
|
|
182
|
+
rust_max_impl_lines() { quality_threshold rust maxImplLines "$DEFAULT_RUST_MAX_IMPL_LINES"; }
|
|
183
|
+
python_max_file_lines() { quality_threshold python maxFileLines "$DEFAULT_PYTHON_MAX_FILE_LINES"; }
|
|
184
|
+
python_max_fn_lines() { quality_threshold python maxFnLines "$DEFAULT_PYTHON_MAX_FN_LINES"; }
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Shared runtime registry for hook modules contributed by other plugins.
|
|
3
|
+
# Modules placed under <root>/<event>.d/<plugin-spec>__<name>.sh are executed
|
|
4
|
+
# by the core dispatcher after its built-in modules, gated on the owning plugin
|
|
5
|
+
# being installed. The "__" separator is load-bearing: plugin specs may contain
|
|
6
|
+
# "." (e.g. name@git.example.com) but must not contain "__". The registry holds
|
|
7
|
+
# GENERATED state (synced each session by each plugin's register.sh); the
|
|
8
|
+
# source of truth for a module is its own plugin.
|
|
9
|
+
|
|
10
|
+
_TOOLU_REGISTRY_LIB_DIR="$(cd "${BASH_SOURCE%/*}" && pwd)"
|
|
11
|
+
# shellcheck source=host.sh
|
|
12
|
+
. "$_TOOLU_REGISTRY_LIB_DIR/host.sh"
|
|
13
|
+
|
|
14
|
+
# toolu_registry_root -> prints the registry root dir (not created).
|
|
15
|
+
toolu_registry_root() {
|
|
16
|
+
local agent_dir
|
|
17
|
+
agent_dir=$(toolu_config_root)
|
|
18
|
+
printf '%s/toolu' "$agent_dir"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# toolu_registry_event_dir EVENT -> prints "<root>/<event-slug>.d".
|
|
22
|
+
# Known events map to the repo's existing naming; others are CamelCase->kebab.
|
|
23
|
+
toolu_registry_event_dir() {
|
|
24
|
+
local event="$1" slug
|
|
25
|
+
case "$event" in
|
|
26
|
+
PreToolUse) slug="pre-tools" ;;
|
|
27
|
+
PostToolUse) slug="post-tools" ;;
|
|
28
|
+
*)
|
|
29
|
+
# Best-effort kebab-casing: consecutive-uppercase runs are NOT split
|
|
30
|
+
# (e.g. "MCPNotification" -> "mcpnotification"). Fine for the current
|
|
31
|
+
# Claude Code event names; extend the case table for anything odd.
|
|
32
|
+
slug=$(printf '%s' "$event" \
|
|
33
|
+
| sed -E 's/([a-z0-9])([A-Z])/\1-\2/g' \
|
|
34
|
+
| tr '[:upper:]' '[:lower:]')
|
|
35
|
+
;;
|
|
36
|
+
esac
|
|
37
|
+
printf '%s/%s.d' "$(toolu_registry_root)" "$slug"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Remove generated registry modules whose owning plugin is definitively absent
|
|
41
|
+
# from Codex's ready SessionStart snapshot. Missing/malformed/indeterminate
|
|
42
|
+
# snapshots prune nothing. Symlinks and un-namespaced files are never removed.
|
|
43
|
+
toolu_registry_prune_inactive() {
|
|
44
|
+
[ "$(toolu_host)" = codex ] || return 0
|
|
45
|
+
command -v jq >/dev/null 2>&1 || return 0
|
|
46
|
+
|
|
47
|
+
local snapshot root event_dir module base spec
|
|
48
|
+
snapshot=$(toolu_codex_plugin_snapshot_path)
|
|
49
|
+
[ -f "$snapshot" ] || return 0
|
|
50
|
+
jq -e '.version == 1 and .status == "ready" and (.plugins | type == "array")' \
|
|
51
|
+
"$snapshot" >/dev/null 2>&1 || return 0
|
|
52
|
+
|
|
53
|
+
root=$(toolu_registry_root)
|
|
54
|
+
for event_dir in "$root/pre-tools.d" "$root/post-tools.d"; do
|
|
55
|
+
[ -d "$event_dir" ] || continue
|
|
56
|
+
for module in "$event_dir"/*.sh; do
|
|
57
|
+
[ -f "$module" ] || continue
|
|
58
|
+
[ -L "$module" ] && continue
|
|
59
|
+
[ "${module%/*}" = "$event_dir" ] || continue
|
|
60
|
+
base=$(basename "$module")
|
|
61
|
+
case "$base" in
|
|
62
|
+
?*__*.sh) spec=${base%%__*} ;;
|
|
63
|
+
*) continue ;;
|
|
64
|
+
esac
|
|
65
|
+
[ -n "$spec" ] || continue
|
|
66
|
+
if ! jq -e --arg spec "$spec" 'any(.plugins[]; . == $spec)' "$snapshot" >/dev/null 2>&1; then
|
|
67
|
+
rm -f -- "$module"
|
|
68
|
+
fi
|
|
69
|
+
done
|
|
70
|
+
done
|
|
71
|
+
return 0
|
|
72
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Transient-state sweeper for <repo>/<host-dir>/tmp.
|
|
3
|
+
#
|
|
4
|
+
# toolu's gates key their state by branch, and nothing ever reclaimed it: a
|
|
5
|
+
# push-review state for a branch merged three weeks ago, a plan ledger for a
|
|
6
|
+
# branch that no longer exists, a quality-gate record naming files that were
|
|
7
|
+
# deleted. The pile is harmless right up until it is confusing, and the fix
|
|
8
|
+
# was `rm -rf .claude/tmp/*` by hand.
|
|
9
|
+
#
|
|
10
|
+
# This runs once at SessionStart and reclaims only what is provably spent:
|
|
11
|
+
#
|
|
12
|
+
# push-review/, plan-ledger/, docs-sync/ per-branch state — dropped when the
|
|
13
|
+
# branch is gone, merged into base, or older than the TTL. The CURRENT
|
|
14
|
+
# branch is never touched, however old: it is the one you are working on.
|
|
15
|
+
# quality-gate-status.json dropped when passing, or when failing about files
|
|
16
|
+
# that no longer exist. A failing record about live files SURVIVES — a
|
|
17
|
+
# violation is not fixed by forgetting it.
|
|
18
|
+
# telemetry/<slug>.jsonl trimmed to the retention window, never deleted
|
|
19
|
+
# wholesale; the history is the point.
|
|
20
|
+
#
|
|
21
|
+
# Anything else under tmp/ — including the permissions sentinel — is not ours
|
|
22
|
+
# and is left alone.
|
|
23
|
+
#
|
|
24
|
+
# Everything here is best-effort: a failure warns and returns 0, because a
|
|
25
|
+
# session must start whether or not old scratch files could be cleaned.
|
|
26
|
+
#
|
|
27
|
+
# Public API:
|
|
28
|
+
# toolu_sweep_state ROOT
|
|
29
|
+
|
|
30
|
+
_TOOLU_SWEEPER_LIB_DIR="$(cd "${BASH_SOURCE%/*}" && pwd)"
|
|
31
|
+
# shellcheck source=config.sh
|
|
32
|
+
. "$_TOOLU_SWEEPER_LIB_DIR/config.sh"
|
|
33
|
+
# shellcheck source=detect.sh
|
|
34
|
+
. "$_TOOLU_SWEEPER_LIB_DIR/detect.sh"
|
|
35
|
+
|
|
36
|
+
TOOLU_SWEEP_DEFAULT_TTL_HOURS=24
|
|
37
|
+
TOOLU_SWEEP_DEFAULT_RETENTION_DAYS=7
|
|
38
|
+
TOOLU_SWEEP_BRANCH_DIRS="push-review plan-ledger docs-sync"
|
|
39
|
+
|
|
40
|
+
_toolu_sweep_warn() {
|
|
41
|
+
printf 'toolu-sweep: %s\n' "$1" >&2
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# _toolu_sweep_number PATH DEFAULT -> a positive integer from config, or DEFAULT.
|
|
45
|
+
# A zero, a negative, or a non-number means "not configured" rather than an
|
|
46
|
+
# error — the same lenient reading the quality thresholds use.
|
|
47
|
+
_toolu_sweep_number() {
|
|
48
|
+
local path="$1" def="$2" val
|
|
49
|
+
toolu_load_config
|
|
50
|
+
if [ "$_TOOLU_HAS_JQ" != "1" ]; then
|
|
51
|
+
printf '%s' "$def"
|
|
52
|
+
return 0
|
|
53
|
+
fi
|
|
54
|
+
val=$(jq -r --arg p "$path" '
|
|
55
|
+
($p | split(".")) as $ks
|
|
56
|
+
| (getpath($ks)) as $v
|
|
57
|
+
| if ($v | type) == "number" and $v > 0 then ($v | floor | tostring) else "" end
|
|
58
|
+
' <<< "$TOOLU_CFG_JSON" 2>/dev/null)
|
|
59
|
+
[ -n "$val" ] && printf '%s' "$val" || printf '%s' "$def"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# _toolu_sweep_cutoff DAYS -> an ISO-8601 UTC timestamp DAYS ago.
|
|
63
|
+
# GNU date first, BSD/macOS second; prints nothing if neither works, which
|
|
64
|
+
# callers read as "cannot judge age, keep everything".
|
|
65
|
+
_toolu_sweep_cutoff() {
|
|
66
|
+
local days="$1"
|
|
67
|
+
date -u -d "-${days} days" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \
|
|
68
|
+
|| date -u -v-"${days}"d +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \
|
|
69
|
+
|| return 1
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# _toolu_sweep_slug_of FILE -> the branch slug a state file belongs to.
|
|
73
|
+
# Strips the compound suffixes first so feat_x.waiver.json and
|
|
74
|
+
# feat_x.pending-waiver.json both resolve to feat_x.
|
|
75
|
+
_toolu_sweep_slug_of() {
|
|
76
|
+
local base
|
|
77
|
+
base=$(basename "$1")
|
|
78
|
+
base="${base%.json}"
|
|
79
|
+
base="${base%.pending-waiver}"
|
|
80
|
+
base="${base%.waiver}"
|
|
81
|
+
printf '%s' "$base"
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# _toolu_sweep_reclaimable SLUG LIVE MERGED FILE TTL -> 0 iff FILE can go.
|
|
85
|
+
_toolu_sweep_reclaimable() {
|
|
86
|
+
local slug="$1" live="$2" merged="$3" file="$4" ttl_hours="$5"
|
|
87
|
+
case "$merged" in *" $slug "*) return 0 ;; esac
|
|
88
|
+
case "$live" in *" $slug "*) ;; *) return 0 ;; esac
|
|
89
|
+
# Branch still exists and is unmerged: age is the only remaining reason.
|
|
90
|
+
[ -n "$(find "$file" -maxdepth 0 -mmin "+$((ttl_hours * 60))" 2>/dev/null)" ]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# _toolu_sweep_branch_state ROOT STATE_ROOT TTL -> prune per-branch state dirs.
|
|
94
|
+
_toolu_sweep_branch_state() {
|
|
95
|
+
local root="$1" state_root="$2" ttl_hours="$3"
|
|
96
|
+
local base_branch current_branch current_slug live=" " merged=" " name dir file slug
|
|
97
|
+
|
|
98
|
+
current_branch=$(git -C "$root" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
|
99
|
+
current_slug=$(branch_slug "$current_branch")
|
|
100
|
+
base_branch=$(detect_base_branch "$root")
|
|
101
|
+
|
|
102
|
+
while IFS= read -r name; do
|
|
103
|
+
[ -n "$name" ] && live="${live}$(branch_slug "$name") "
|
|
104
|
+
done < <(git -C "$root" branch --format='%(refname:short)' 2>/dev/null)
|
|
105
|
+
|
|
106
|
+
# `--merged` sees ancestry only, so a SQUASH-merged branch never appears here.
|
|
107
|
+
# Those are reclaimed by the TTL path instead — which is exactly why the TTL
|
|
108
|
+
# exists alongside this check rather than being redundant with it.
|
|
109
|
+
while IFS= read -r name; do
|
|
110
|
+
[ -n "$name" ] && merged="${merged}$(branch_slug "$name") "
|
|
111
|
+
done < <(git -C "$root" branch --merged "$base_branch" --format='%(refname:short)' 2>/dev/null)
|
|
112
|
+
|
|
113
|
+
for dir in $TOOLU_SWEEP_BRANCH_DIRS; do
|
|
114
|
+
[ -d "$state_root/$dir" ] || continue
|
|
115
|
+
for file in "$state_root/$dir"/*.json; do
|
|
116
|
+
[ -f "$file" ] || continue
|
|
117
|
+
slug=$(_toolu_sweep_slug_of "$file")
|
|
118
|
+
[ "$slug" = "$current_slug" ] && continue
|
|
119
|
+
if _toolu_sweep_reclaimable "$slug" "$live" "$merged" "$file" "$ttl_hours"; then
|
|
120
|
+
rm -f "$file" 2>/dev/null || _toolu_sweep_warn "could not remove $file"
|
|
121
|
+
fi
|
|
122
|
+
done
|
|
123
|
+
done
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# _toolu_sweep_gate_file GATE_FILE -> drop a spent quality-gate record.
|
|
127
|
+
_toolu_sweep_gate_file() {
|
|
128
|
+
local gate_file="$1" status live_violation entry
|
|
129
|
+
[ -f "$gate_file" ] || return 0
|
|
130
|
+
status=$(jq -r '.status // ""' "$gate_file" 2>/dev/null) || return 0
|
|
131
|
+
if [ "$status" = "passing" ]; then
|
|
132
|
+
rm -f "$gate_file" 2>/dev/null || _toolu_sweep_warn "could not remove $gate_file"
|
|
133
|
+
return 0
|
|
134
|
+
fi
|
|
135
|
+
[ "$status" = "failing" ] || return 0
|
|
136
|
+
|
|
137
|
+
# A failing record is only spent when every file it complains about is gone.
|
|
138
|
+
# Age alone must never clear it: that would silently reopen a real violation.
|
|
139
|
+
live_violation=0
|
|
140
|
+
while IFS= read -r entry; do
|
|
141
|
+
[ -n "$entry" ] || continue
|
|
142
|
+
[ "$entry" = "__global__" ] && { live_violation=1; break; }
|
|
143
|
+
[ -e "$entry" ] && { live_violation=1; break; }
|
|
144
|
+
done < <(jq -r 'if (.entries? | type) == "object" then (.entries | keys[]) else (.file // "__global__") end' \
|
|
145
|
+
"$gate_file" 2>/dev/null)
|
|
146
|
+
|
|
147
|
+
[ "$live_violation" = "1" ] && return 0
|
|
148
|
+
rm -f "$gate_file" 2>/dev/null || _toolu_sweep_warn "could not remove $gate_file"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# _toolu_sweep_telemetry DIR DAYS -> trim each jsonl to the retention window.
|
|
152
|
+
_toolu_sweep_telemetry() {
|
|
153
|
+
local dir="$1" days="$2" cutoff file tmp
|
|
154
|
+
[ -d "$dir" ] || return 0
|
|
155
|
+
cutoff=$(_toolu_sweep_cutoff "$days") || return 0
|
|
156
|
+
for file in "$dir"/*.jsonl; do
|
|
157
|
+
[ -f "$file" ] || continue
|
|
158
|
+
tmp=$(mktemp "${file}.XXXXXX" 2>/dev/null) || continue
|
|
159
|
+
# A file jq cannot parse is left exactly as it is: it may be another
|
|
160
|
+
# tool's, or a partially written line worth keeping for debugging.
|
|
161
|
+
if jq -c --arg cutoff "$cutoff" 'select((.t // "") >= $cutoff)' "$file" > "$tmp" 2>/dev/null; then
|
|
162
|
+
if [ -s "$tmp" ]; then
|
|
163
|
+
mv -f "$tmp" "$file" 2>/dev/null || rm -f "$tmp" 2>/dev/null
|
|
164
|
+
else
|
|
165
|
+
rm -f "$tmp" "$file" 2>/dev/null
|
|
166
|
+
fi
|
|
167
|
+
else
|
|
168
|
+
rm -f "$tmp" 2>/dev/null
|
|
169
|
+
fi
|
|
170
|
+
done
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
# toolu_sweep_state ROOT -> reclaim spent transient state. Always returns 0.
|
|
174
|
+
toolu_sweep_state() {
|
|
175
|
+
local root="${1:-}" state_root ttl_hours retention_days
|
|
176
|
+
[ -n "$root" ] || root=$(toolu_project_root)
|
|
177
|
+
[ -n "$root" ] || return 0
|
|
178
|
+
command -v jq >/dev/null 2>&1 || return 0
|
|
179
|
+
|
|
180
|
+
# Opt-out, and a hard requirement of git for the branch questions below.
|
|
181
|
+
toolu_enabled gates sweep || return 0
|
|
182
|
+
command -v git >/dev/null 2>&1 || return 0
|
|
183
|
+
|
|
184
|
+
state_root=$(toolu_project_state_root "$root")
|
|
185
|
+
[ -n "$state_root" ] && [ -d "$state_root" ] || return 0
|
|
186
|
+
|
|
187
|
+
ttl_hours=$(_toolu_sweep_number gates.stateTtlHours "$TOOLU_SWEEP_DEFAULT_TTL_HOURS")
|
|
188
|
+
retention_days=$(_toolu_sweep_number gates.telemetryRetentionDays "$TOOLU_SWEEP_DEFAULT_RETENTION_DAYS")
|
|
189
|
+
|
|
190
|
+
_toolu_sweep_branch_state "$root" "$state_root" "$ttl_hours"
|
|
191
|
+
_toolu_sweep_gate_file "$state_root/quality-gate-status.json"
|
|
192
|
+
_toolu_sweep_telemetry "$state_root/telemetry" "$retention_days"
|
|
193
|
+
return 0
|
|
194
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Workflow telemetry append engine: the one write path every gate/lib event
|
|
3
|
+
# funnels through, so telemetry.enabled / jq-absent / no-branch all degrade
|
|
4
|
+
# the same way regardless of caller.
|
|
5
|
+
#
|
|
6
|
+
# Public API:
|
|
7
|
+
# telemetry_append REPO_ROOT EVENT [EXTRA_JSON_OBJECT]
|
|
8
|
+
# Appends one line `{"v":1,"t":<iso8601 UTC>,"branch":<branch>,
|
|
9
|
+
# "event":<name>, ...EXTRA_JSON_OBJECT}` to
|
|
10
|
+
# REPO_ROOT/<host-state>/tmp/telemetry/<branch_slug>.jsonl
|
|
11
|
+
# ($TELEMETRY_DIR overrides the directory). EXTRA_JSON_OBJECT defaults to
|
|
12
|
+
# "{}"; its keys are merged in but can never override v/t/branch/event.
|
|
13
|
+
# Always returns 0 — a telemetry bug must never fail the caller's real
|
|
14
|
+
# work — no-op (nothing written) when:
|
|
15
|
+
# - telemetry.enabled=false in the merged toolu config
|
|
16
|
+
# - jq is missing
|
|
17
|
+
# - HEAD is unborn/detached (mirrors push-review.sh's and docs-sync.sh's
|
|
18
|
+
# own detached-HEAD refusal: branch_slug would otherwise collapse
|
|
19
|
+
# every such caller onto the shared "_default" slug, mixing unrelated
|
|
20
|
+
# branches' events into one file)
|
|
21
|
+
# A malformed EXTRA_JSON_OBJECT, or an assembled line over 3900 bytes,
|
|
22
|
+
# is a soft failure: warns on stderr, appends nothing, still returns 0.
|
|
23
|
+
#
|
|
24
|
+
# CONCURRENCY: exactly one `printf '%s\n' >>` per call — a single O_APPEND
|
|
25
|
+
# write. That is atomic only while the line stays under the platform's
|
|
26
|
+
# atomic-pipe-write floor (historically 4 KB on POSIX), so the assembled
|
|
27
|
+
# line's byte length is checked before every write: over 3900 bytes
|
|
28
|
+
# (headroom under that floor) it is dropped rather than risking a
|
|
29
|
+
# torn/interleaved write with a concurrent writer — callers must still keep
|
|
30
|
+
# EXTRA_JSON_OBJECT small so their events aren't silently lost this way.
|
|
31
|
+
#
|
|
32
|
+
# Source via: . "${TOOLU_LIB_DIR:-${BASH_SOURCE%/*}}/telemetry.sh"
|
|
33
|
+
|
|
34
|
+
_TELEMETRY_LIB_DIR="${TOOLU_LIB_DIR:-${BASH_SOURCE%/*}}"
|
|
35
|
+
# shellcheck source=./detect.sh
|
|
36
|
+
[ -f "$_TELEMETRY_LIB_DIR/detect.sh" ] && . "$_TELEMETRY_LIB_DIR/detect.sh"
|
|
37
|
+
# shellcheck source=./config.sh
|
|
38
|
+
[ -f "$_TELEMETRY_LIB_DIR/config.sh" ] && . "$_TELEMETRY_LIB_DIR/config.sh"
|
|
39
|
+
|
|
40
|
+
# telemetry_append REPO_ROOT EVENT [EXTRA_JSON_OBJECT]
|
|
41
|
+
telemetry_append() {
|
|
42
|
+
local repo_root="$1" event="$2" extra="${3:-}"
|
|
43
|
+
[ -n "$extra" ] || extra='{}'
|
|
44
|
+
[ -n "$repo_root" ] && [ -n "$event" ] || return 0
|
|
45
|
+
|
|
46
|
+
command -v jq >/dev/null 2>&1 || return 0
|
|
47
|
+
command -v git >/dev/null 2>&1 || return 0
|
|
48
|
+
|
|
49
|
+
# Default-on: absent/malformed config -> enabled; explicit false -> no-op.
|
|
50
|
+
if command -v toolu_enabled >/dev/null 2>&1; then
|
|
51
|
+
toolu_enabled telemetry enabled || return 0
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
local branch
|
|
55
|
+
branch=$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
|
56
|
+
[[ -z "$branch" || "$branch" == "HEAD" ]] && return 0
|
|
57
|
+
|
|
58
|
+
# branch_slug comes from detect.sh, sourced softly above — an older/absent
|
|
59
|
+
# detect.sh must not turn into an unbound-function crash for every caller.
|
|
60
|
+
command -v branch_slug >/dev/null 2>&1 || return 0
|
|
61
|
+
|
|
62
|
+
local slug now line dir file line_bytes
|
|
63
|
+
slug=$(branch_slug "$branch")
|
|
64
|
+
now=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
65
|
+
|
|
66
|
+
# $extra merged FIRST so the protocol fields (v/t/branch/event) always win
|
|
67
|
+
# on key collision — a caller-supplied "branch" or "event" in extra can
|
|
68
|
+
# never spoof the real ones. A malformed or non-object $extra makes this
|
|
69
|
+
# jq call fail; treat that as the one "malformed extra JSON" soft-failure.
|
|
70
|
+
if ! line=$(jq -cn --argjson extra "$extra" --arg t "$now" --arg branch "$branch" \
|
|
71
|
+
--arg event "$event" '$extra + {v: 1, t: $t, branch: $branch, event: $event}' \
|
|
72
|
+
2>/dev/null) || [ -z "$line" ]; then
|
|
73
|
+
printf 'telemetry: malformed extra JSON for event "%s"; skipping append\n' "$event" >&2
|
|
74
|
+
return 0
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# Keep the single O_APPEND write atomic (see CONCURRENCY above): a line
|
|
78
|
+
# over 3900 bytes risks crossing the platform's atomic-pipe-write floor, so
|
|
79
|
+
# drop it rather than risk tearing/interleaving with a concurrent writer.
|
|
80
|
+
line_bytes=$(printf '%s' "$line" | wc -c | tr -d ' ')
|
|
81
|
+
if [ "$line_bytes" -gt 3900 ]; then
|
|
82
|
+
printf 'telemetry: assembled line for event "%s" is %d bytes (>3900); skipping append\n' \
|
|
83
|
+
"$event" "$line_bytes" >&2
|
|
84
|
+
return 0
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
dir="${TELEMETRY_DIR:-$(toolu_project_state_dir telemetry "$repo_root")}"
|
|
88
|
+
file="$dir/${slug}.jsonl"
|
|
89
|
+
mkdir -p "$dir" 2>/dev/null || return 0
|
|
90
|
+
|
|
91
|
+
printf '%s\n' "$line" >> "$file" 2>/dev/null
|
|
92
|
+
|
|
93
|
+
return 0
|
|
94
|
+
}
|