@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,65 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SessionStart registry sync for the ts-quality plugin.
|
|
3
|
+
#
|
|
4
|
+
# Assembles hooks/concerns/[0-9][0-9]-*.sh (ordered preamble → concern partials
|
|
5
|
+
# → finalize) into ONE runtime module in the toolu registry
|
|
6
|
+
# (${TOOLU_CONFIG_DIR:-${CLAUDE_CONFIG_DIR:-$HOME/.claude}}/toolu/post-tools.d/) under the
|
|
7
|
+
# namespaced filename ts-quality@toolu__ts-quality.sh. The fragments are
|
|
8
|
+
# partials of a single script; concatenating them in numeric order rebuilds the
|
|
9
|
+
# original monolith so one process does one gate write (preserving
|
|
10
|
+
# byte-identical behavior). Prunes any stale entry bearing OUR prefix, and
|
|
11
|
+
# clears our own crashed tmp residue. Other plugins' entries are never touched.
|
|
12
|
+
#
|
|
13
|
+
# Silent on success (SessionStart stdout becomes context); errors are
|
|
14
|
+
# non-fatal — a failed sync means the registry copy is stale, not broken.
|
|
15
|
+
|
|
16
|
+
SPEC="ts-quality@toolu"
|
|
17
|
+
OUT="ts-quality.sh"
|
|
18
|
+
SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
19
|
+
SRC_DIR="$SELF_DIR/concerns"
|
|
20
|
+
if [ -n "${TOOLU_CONFIG_DIR:-}" ]; then
|
|
21
|
+
CONFIG_ROOT="$TOOLU_CONFIG_DIR"
|
|
22
|
+
elif [ "${TOOLU_HOST_OVERRIDE:-}" = codex ] || { [ -z "${TOOLU_HOST_OVERRIDE:-}" ] && [ -n "${PLUGIN_ROOT:-}" ]; }; then
|
|
23
|
+
CONFIG_ROOT="${CODEX_HOME:-$HOME/.codex}"
|
|
24
|
+
else
|
|
25
|
+
CONFIG_ROOT="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
26
|
+
fi
|
|
27
|
+
REG_DIR="$CONFIG_ROOT/toolu/post-tools.d"
|
|
28
|
+
|
|
29
|
+
# Consume stdin so Claude Code's hook IPC never stalls.
|
|
30
|
+
cat > /dev/null 2>&1 || true
|
|
31
|
+
|
|
32
|
+
[ -d "$SRC_DIR" ] || exit 0
|
|
33
|
+
mkdir -p "$REG_DIR" 2>/dev/null || exit 0
|
|
34
|
+
|
|
35
|
+
# Clear OUR orphaned atomic-write residue from prior crashed runs. Age-gated
|
|
36
|
+
# so a concurrent SessionStart's in-flight tmp (seconds old) is never clobbered.
|
|
37
|
+
find "$REG_DIR" -maxdepth 1 -name "${SPEC}__*.sh.tmp.*" -mmin +1 -delete 2>/dev/null
|
|
38
|
+
|
|
39
|
+
# Assemble: concatenate the numeric-prefixed fragments in lexical order into one
|
|
40
|
+
# module. A newline guard between fragments keeps a fragment that lacks a
|
|
41
|
+
# trailing newline from running into the next. __tests__/ and any non-prefixed
|
|
42
|
+
# .sh are skipped by the glob.
|
|
43
|
+
dst="$REG_DIR/${SPEC}__${OUT}"
|
|
44
|
+
tmp="${dst}.tmp.$$"
|
|
45
|
+
: > "$tmp"
|
|
46
|
+
for src in "$SRC_DIR"/[0-9][0-9]-*.sh; do
|
|
47
|
+
[ -f "$src" ] || continue
|
|
48
|
+
cat "$src" >> "$tmp" 2>/dev/null || { rm -f "$tmp"; exit 0; }
|
|
49
|
+
printf '\n' >> "$tmp" 2>/dev/null || { rm -f "$tmp"; exit 0; }
|
|
50
|
+
done
|
|
51
|
+
if [ -s "$tmp" ] && { [ ! -f "$dst" ] || ! cmp -s "$tmp" "$dst"; }; then
|
|
52
|
+
mv "$tmp" "$dst" 2>/dev/null || rm -f "$tmp"
|
|
53
|
+
else
|
|
54
|
+
rm -f "$tmp"
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# Prune: remove any entry bearing OUR prefix that is not the current assembled
|
|
58
|
+
# module (e.g. per-concern files left by an older version). Never glob outside
|
|
59
|
+
# our own spec prefix.
|
|
60
|
+
for old in "$REG_DIR/${SPEC}__"*.sh; do
|
|
61
|
+
[ -f "$old" ] || continue
|
|
62
|
+
[ "$(basename "$old")" = "${SPEC}__${OUT}" ] || rm -f "$old"
|
|
63
|
+
done
|
|
64
|
+
|
|
65
|
+
exit 0
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { createPermissionEvaluateHandler } from "../evaluate.ts";
|
|
6
|
+
import type { PermissionEvaluationEvent } from "../permission-map.ts";
|
|
7
|
+
|
|
8
|
+
const tmpBase = process.env.TMPDIR ?? "/tmp";
|
|
9
|
+
|
|
10
|
+
function repoRoot(): string {
|
|
11
|
+
return join(dirname(fileURLToPath(import.meta.url)), "../../../../..");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function protectedEnvProject(): Promise<{ projectRoot: string; envPath: string }> {
|
|
15
|
+
const projectRoot = await mkdtemp(join(tmpBase, "toolu-oc-eval-"));
|
|
16
|
+
const envPath = join(projectRoot, ".env");
|
|
17
|
+
await writeFile(envPath, "SECRET=1\n", "utf8");
|
|
18
|
+
await mkdir(join(projectRoot, ".opencode"), { recursive: true });
|
|
19
|
+
await writeFile(
|
|
20
|
+
join(projectRoot, ".opencode/toolu.config.json"),
|
|
21
|
+
JSON.stringify({
|
|
22
|
+
version: 1,
|
|
23
|
+
gates: { protectedFiles: { mode: "block" } },
|
|
24
|
+
}),
|
|
25
|
+
"utf8",
|
|
26
|
+
);
|
|
27
|
+
return { projectRoot, envPath };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function editEvent(envPath: string): PermissionEvaluationEvent {
|
|
31
|
+
return {
|
|
32
|
+
sessionID: "sess_eval_1",
|
|
33
|
+
action: "edit",
|
|
34
|
+
resources: [envPath],
|
|
35
|
+
effect: "allow",
|
|
36
|
+
metadata: { toolCallId: "call_eval_1" },
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test("AC-2: evaluate handler + real bridge on protected .env yields deny", async () => {
|
|
41
|
+
const root = repoRoot();
|
|
42
|
+
const { projectRoot, envPath } = await protectedEnvProject();
|
|
43
|
+
const handler = createPermissionEvaluateHandler({
|
|
44
|
+
repoRoot: root,
|
|
45
|
+
bridgeContext: {
|
|
46
|
+
cwd: projectRoot,
|
|
47
|
+
projectRoot,
|
|
48
|
+
worktree: projectRoot,
|
|
49
|
+
host: "opencode",
|
|
50
|
+
},
|
|
51
|
+
env: {
|
|
52
|
+
TOOLU_SETTINGS_DIR: join(root, "plugins/toolu/settings"),
|
|
53
|
+
TOOLU_HOST_OVERRIDE: "opencode",
|
|
54
|
+
TOOLU_PROJECT_CONFIG_DIRNAME: ".opencode",
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const event = editEvent(envPath);
|
|
59
|
+
await handler(event);
|
|
60
|
+
|
|
61
|
+
expect(event.effect).toBe("deny");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("AC-3: runtime_failure from bad repoRoot maps to deny", async () => {
|
|
65
|
+
const { projectRoot, envPath } = await protectedEnvProject();
|
|
66
|
+
const handler = createPermissionEvaluateHandler({
|
|
67
|
+
repoRoot: join(tmpBase, "toolu-nonexistent-repo-root"),
|
|
68
|
+
bridgeContext: {
|
|
69
|
+
cwd: projectRoot,
|
|
70
|
+
projectRoot,
|
|
71
|
+
worktree: projectRoot,
|
|
72
|
+
host: "opencode",
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const event = editEvent(envPath);
|
|
77
|
+
await handler(event);
|
|
78
|
+
|
|
79
|
+
expect(event.effect).toBe("deny");
|
|
80
|
+
expect(typeof event.message).toBe("string");
|
|
81
|
+
expect(event.message && event.message.length > 0).toBe(true);
|
|
82
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import type { Decision } from "@toolu/core/decision";
|
|
3
|
+
import {
|
|
4
|
+
applyDecisionToPermission,
|
|
5
|
+
mapPermissionEventToBridge,
|
|
6
|
+
type PermissionEvaluationEvent,
|
|
7
|
+
} from "../permission-map.ts";
|
|
8
|
+
|
|
9
|
+
const ctx = {
|
|
10
|
+
cwd: "/proj",
|
|
11
|
+
projectRoot: "/proj",
|
|
12
|
+
worktree: "/proj",
|
|
13
|
+
host: "opencode",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function event(overrides: Partial<PermissionEvaluationEvent> = {}): PermissionEvaluationEvent {
|
|
17
|
+
return {
|
|
18
|
+
sessionID: "sess_1",
|
|
19
|
+
action: "edit",
|
|
20
|
+
resources: ["/proj/.env"],
|
|
21
|
+
effect: "allow",
|
|
22
|
+
...overrides,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test("AC-4: mapPermissionEventToBridge maps edit to Edit tool/pre", () => {
|
|
27
|
+
const mapping = mapPermissionEventToBridge(event(), ctx);
|
|
28
|
+
expect(mapping.kind).toBe("request");
|
|
29
|
+
if (mapping.kind !== "request") {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
expect(mapping.request.protocolVersion).toBe(1);
|
|
33
|
+
expect(mapping.request.event).toBe("tool/pre");
|
|
34
|
+
expect(mapping.request.toolName).toBe("Edit");
|
|
35
|
+
expect(mapping.request.toolInput).toEqual({ file_path: "/proj/.env" });
|
|
36
|
+
expect(mapping.request.host).toBe("opencode");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("AC-4: mapPermissionEventToBridge skips unknown actions", () => {
|
|
40
|
+
expect(mapPermissionEventToBridge(event({ action: "read" }), ctx).kind).toBe("skip");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("AC-4: gated write without path fails closed in mapper", () => {
|
|
44
|
+
const mapping = mapPermissionEventToBridge(event({ action: "write", resources: [] }), ctx);
|
|
45
|
+
expect(mapping.kind).toBe("deny");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("AC-4: bash maps command from metadata", () => {
|
|
49
|
+
const mapping = mapPermissionEventToBridge(
|
|
50
|
+
event({
|
|
51
|
+
action: "bash",
|
|
52
|
+
resources: [],
|
|
53
|
+
metadata: { command: "rm -rf /" },
|
|
54
|
+
}),
|
|
55
|
+
ctx,
|
|
56
|
+
);
|
|
57
|
+
expect(mapping.kind).toBe("request");
|
|
58
|
+
if (mapping.kind !== "request") {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
expect(mapping.request.toolName).toBe("Bash");
|
|
62
|
+
expect(mapping.request.toolInput).toEqual({ command: "rm -rf /" });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const decisionCases: Array<{
|
|
66
|
+
decision: Decision;
|
|
67
|
+
effect: PermissionEvaluationEvent["effect"];
|
|
68
|
+
message?: string;
|
|
69
|
+
}> = [
|
|
70
|
+
{ decision: { kind: "deny", reason: "blocked" }, effect: "deny", message: "blocked" },
|
|
71
|
+
{ decision: { kind: "ask", reason: "confirm" }, effect: "ask", message: "confirm" },
|
|
72
|
+
{ decision: { kind: "allow" }, effect: "allow" },
|
|
73
|
+
{ decision: { kind: "advisory", message: "hint" }, effect: "allow", message: "hint" },
|
|
74
|
+
{ decision: { kind: "post_block", reason: "post" }, effect: "deny", message: "post" },
|
|
75
|
+
{
|
|
76
|
+
decision: { kind: "runtime_failure", reason: "fail", code: "parse" },
|
|
77
|
+
effect: "deny",
|
|
78
|
+
message: "fail",
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
for (const { decision, effect, message } of decisionCases) {
|
|
83
|
+
test(`AC-4: applyDecisionToPermission ${decision.kind}`, () => {
|
|
84
|
+
const ev = event();
|
|
85
|
+
applyDecisionToPermission(decision, ev);
|
|
86
|
+
expect(ev.effect).toBe(effect);
|
|
87
|
+
if (message !== undefined) {
|
|
88
|
+
expect(ev.message).toBe(message);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** permission.hook("evaluate") handler backed by runPreToolBridge (#204). */
|
|
2
|
+
import { runPreToolBridge } from "@toolu/core/bridge";
|
|
3
|
+
import type { BashRunner } from "@toolu/core/runner";
|
|
4
|
+
import {
|
|
5
|
+
applyDecisionToPermission,
|
|
6
|
+
mapPermissionEventToBridge,
|
|
7
|
+
type PermissionBridgeContext,
|
|
8
|
+
type PermissionEvaluationEvent,
|
|
9
|
+
} from "./permission-map.ts";
|
|
10
|
+
|
|
11
|
+
export type PermissionEvaluateHandlerOptions = {
|
|
12
|
+
repoRoot: string;
|
|
13
|
+
bridgeContext: PermissionBridgeContext;
|
|
14
|
+
runner?: BashRunner;
|
|
15
|
+
env?: Record<string, string>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function runtimeFailureDeny(event: PermissionEvaluationEvent, reason: string): void {
|
|
19
|
+
applyDecisionToPermission({ kind: "runtime_failure", reason, code: "parse" }, event);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Handler for OpenCode permission.evaluate — real bash bridge, fail closed on errors. */
|
|
23
|
+
export function createPermissionEvaluateHandler(
|
|
24
|
+
opts: PermissionEvaluateHandlerOptions,
|
|
25
|
+
): (event: PermissionEvaluationEvent) => Promise<void> {
|
|
26
|
+
return async (event: PermissionEvaluationEvent): Promise<void> => {
|
|
27
|
+
const mapping = mapPermissionEventToBridge(event, opts.bridgeContext);
|
|
28
|
+
if (mapping.kind === "skip") {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (mapping.kind === "deny") {
|
|
32
|
+
applyDecisionToPermission({ kind: "deny", reason: mapping.reason }, event);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const response = await runPreToolBridge(mapping.request, {
|
|
37
|
+
repoRoot: opts.repoRoot,
|
|
38
|
+
...(opts.runner !== undefined ? { runner: opts.runner } : {}),
|
|
39
|
+
...(opts.env !== undefined ? { env: opts.env } : {}),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
applyDecisionToPermission(response.decision, event);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Fail-closed evaluate handler when bootstrap/preflight is NotReady. */
|
|
47
|
+
export function createDenyAllPermissionHandler(
|
|
48
|
+
reason: string,
|
|
49
|
+
): (event: PermissionEvaluationEvent) => Promise<void> {
|
|
50
|
+
return (event: PermissionEvaluationEvent): Promise<void> => {
|
|
51
|
+
runtimeFailureDeny(event, reason);
|
|
52
|
+
return Promise.resolve();
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/** OpenCode permission.evaluate ↔ portable bridge (#204). */
|
|
2
|
+
import type { BridgeRequest } from "@toolu/core/bridge";
|
|
3
|
+
import type { Decision } from "@toolu/core/decision";
|
|
4
|
+
|
|
5
|
+
export type PermissionEffect = "allow" | "ask" | "deny";
|
|
6
|
+
|
|
7
|
+
/** OpenCode-shaped permission evaluation event (mutable effect for hook handlers). */
|
|
8
|
+
export type PermissionEvaluationEvent = {
|
|
9
|
+
readonly sessionID: string;
|
|
10
|
+
readonly agent?: string;
|
|
11
|
+
readonly action: string;
|
|
12
|
+
readonly resources: ReadonlyArray<string>;
|
|
13
|
+
readonly metadata?: Record<string, unknown>;
|
|
14
|
+
effect: PermissionEffect;
|
|
15
|
+
message?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type PermissionBridgeContext = {
|
|
19
|
+
cwd: string;
|
|
20
|
+
projectRoot: string;
|
|
21
|
+
worktree: string;
|
|
22
|
+
host: string;
|
|
23
|
+
deadlineMs?: number;
|
|
24
|
+
maxStdoutBytes?: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type PermissionBridgeMapping =
|
|
28
|
+
| { kind: "skip" }
|
|
29
|
+
| { kind: "request"; request: BridgeRequest }
|
|
30
|
+
| { kind: "deny"; reason: string };
|
|
31
|
+
|
|
32
|
+
type GatedAction = {
|
|
33
|
+
toolName: string;
|
|
34
|
+
inputKind: "file" | "shell";
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const GATED: Record<string, GatedAction> = {
|
|
38
|
+
edit: { toolName: "Edit", inputKind: "file" },
|
|
39
|
+
write: { toolName: "Write", inputKind: "file" },
|
|
40
|
+
bash: { toolName: "Bash", inputKind: "shell" },
|
|
41
|
+
shell: { toolName: "Shell", inputKind: "shell" },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const DEFAULT_DEADLINE_MS = 15_000;
|
|
45
|
+
const DEFAULT_MAX_STDOUT_BYTES = 1_048_576;
|
|
46
|
+
|
|
47
|
+
function normalizeAction(action: string): string {
|
|
48
|
+
return action.trim().toLowerCase();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readString(meta: Record<string, unknown> | undefined, key: string): string | undefined {
|
|
52
|
+
if (!meta) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const value = meta[key];
|
|
56
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function filePathFromEvent(event: PermissionEvaluationEvent): string | undefined {
|
|
60
|
+
const fromResource = event.resources[0];
|
|
61
|
+
if (typeof fromResource === "string" && fromResource.length > 0) {
|
|
62
|
+
return fromResource;
|
|
63
|
+
}
|
|
64
|
+
const meta = event.metadata;
|
|
65
|
+
return readString(meta, "file_path") ?? readString(meta, "filePath") ?? readString(meta, "path");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function commandFromEvent(event: PermissionEvaluationEvent): string | undefined {
|
|
69
|
+
const fromMeta = readString(event.metadata, "command");
|
|
70
|
+
if (fromMeta) {
|
|
71
|
+
return fromMeta;
|
|
72
|
+
}
|
|
73
|
+
const fromResource = event.resources[0];
|
|
74
|
+
return typeof fromResource === "string" && fromResource.length > 0 ? fromResource : undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function toolCallId(event: PermissionEvaluationEvent): string {
|
|
78
|
+
const meta = event.metadata;
|
|
79
|
+
const fromMeta = readString(meta, "toolCallId") ?? readString(meta, "tool_call_id");
|
|
80
|
+
if (fromMeta) {
|
|
81
|
+
return fromMeta;
|
|
82
|
+
}
|
|
83
|
+
const source = meta?.source;
|
|
84
|
+
if (typeof source === "object" && source !== null && !Array.isArray(source)) {
|
|
85
|
+
const sourceRecord: Record<string, unknown> = {};
|
|
86
|
+
for (const [key, value] of Object.entries(source)) {
|
|
87
|
+
sourceRecord[key] = value;
|
|
88
|
+
}
|
|
89
|
+
const fromSource = readString(sourceRecord, "id");
|
|
90
|
+
if (fromSource) {
|
|
91
|
+
return fromSource;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return `perm_${event.sessionID}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Map a permission.evaluate payload to a bridge tool/pre request, or skip/deny when incomplete. */
|
|
98
|
+
export function mapPermissionEventToBridge(
|
|
99
|
+
event: PermissionEvaluationEvent,
|
|
100
|
+
ctx: PermissionBridgeContext,
|
|
101
|
+
): PermissionBridgeMapping {
|
|
102
|
+
const gated = GATED[normalizeAction(event.action)];
|
|
103
|
+
if (!gated) {
|
|
104
|
+
return { kind: "skip" };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let toolInput: Record<string, unknown>;
|
|
108
|
+
if (gated.inputKind === "file") {
|
|
109
|
+
const filePath = filePathFromEvent(event);
|
|
110
|
+
if (!filePath) {
|
|
111
|
+
return {
|
|
112
|
+
kind: "deny",
|
|
113
|
+
reason: `toolu: ${event.action} permission missing file path (resources or metadata)`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
toolInput = { file_path: filePath };
|
|
117
|
+
} else {
|
|
118
|
+
const command = commandFromEvent(event);
|
|
119
|
+
if (!command) {
|
|
120
|
+
return {
|
|
121
|
+
kind: "deny",
|
|
122
|
+
reason: `toolu: ${event.action} permission missing shell command (metadata.command or resources)`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
toolInput = { command };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const request: BridgeRequest = {
|
|
129
|
+
protocolVersion: 1,
|
|
130
|
+
event: "tool/pre",
|
|
131
|
+
sessionId: event.sessionID,
|
|
132
|
+
toolCallId: toolCallId(event),
|
|
133
|
+
cwd: ctx.cwd,
|
|
134
|
+
projectRoot: ctx.projectRoot,
|
|
135
|
+
worktree: ctx.worktree,
|
|
136
|
+
toolName: gated.toolName,
|
|
137
|
+
toolInput,
|
|
138
|
+
host: ctx.host,
|
|
139
|
+
deadlineMs: ctx.deadlineMs ?? DEFAULT_DEADLINE_MS,
|
|
140
|
+
maxStdoutBytes: ctx.maxStdoutBytes ?? DEFAULT_MAX_STDOUT_BYTES,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return { kind: "request", request };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Apply a portable Decision onto an OpenCode permission evaluation (mutates event). */
|
|
147
|
+
export function applyDecisionToPermission(
|
|
148
|
+
decision: Decision,
|
|
149
|
+
event: PermissionEvaluationEvent,
|
|
150
|
+
): void {
|
|
151
|
+
switch (decision.kind) {
|
|
152
|
+
case "deny":
|
|
153
|
+
event.effect = "deny";
|
|
154
|
+
event.message = decision.reason;
|
|
155
|
+
return;
|
|
156
|
+
case "ask":
|
|
157
|
+
event.effect = "ask";
|
|
158
|
+
event.message = decision.reason;
|
|
159
|
+
return;
|
|
160
|
+
case "allow":
|
|
161
|
+
event.effect = "allow";
|
|
162
|
+
delete event.message;
|
|
163
|
+
return;
|
|
164
|
+
case "advisory":
|
|
165
|
+
event.effect = "allow";
|
|
166
|
+
event.message = decision.message;
|
|
167
|
+
return;
|
|
168
|
+
case "post_block":
|
|
169
|
+
event.effect = "deny";
|
|
170
|
+
event.message = decision.reason;
|
|
171
|
+
return;
|
|
172
|
+
case "runtime_failure":
|
|
173
|
+
event.effect = "deny";
|
|
174
|
+
event.message = decision.reason;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { bootstrapRuntime } from "../runtime.ts";
|
|
6
|
+
import { bootstrapWithNoOpRunner } from "../test-helpers.ts";
|
|
7
|
+
import { listPluginManifests } from "../../inventory/scan.ts";
|
|
8
|
+
import { selectPluginsWithDependencies } from "../../select/resolve.ts";
|
|
9
|
+
import { opencodePluginSelectionPath } from "../../host/roots.ts";
|
|
10
|
+
const tmpBase = process.env.TMPDIR ?? "/tmp";
|
|
11
|
+
|
|
12
|
+
function repoRoot(): string {
|
|
13
|
+
return join(dirname(fileURLToPath(import.meta.url)), "../../../../..");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isolatedHome(): string {
|
|
17
|
+
return mkdtempSync(join(tmpBase, "toolu-oc-home-"));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test("AC-4: exit 0 without output artifacts is NotReady", async () => {
|
|
21
|
+
const project = mkdtempSync(join(tmpBase, "toolu-bs-nr-"));
|
|
22
|
+
const dataRoot = mkdtempSync(join(tmpBase, "toolu-bs-nr-data-"));
|
|
23
|
+
const pluginsRoot = join(repoRoot(), "plugins");
|
|
24
|
+
const manifests = listPluginManifests(pluginsRoot);
|
|
25
|
+
const toolu = manifests?.find((m) => m.name === "toolu");
|
|
26
|
+
expect(toolu).toBeDefined();
|
|
27
|
+
if (!toolu) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const result = await bootstrapWithNoOpRunner({
|
|
31
|
+
repoRoot: repoRoot(),
|
|
32
|
+
projectRoot: project,
|
|
33
|
+
dataRoot,
|
|
34
|
+
plugins: [toolu],
|
|
35
|
+
isolatedHome: isolatedHome(),
|
|
36
|
+
});
|
|
37
|
+
expect(result.status).toBe("not-ready");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("AC-3: two project roots do not share bootstrap state", async () => {
|
|
41
|
+
const root = repoRoot();
|
|
42
|
+
const pluginsRoot = join(root, "plugins");
|
|
43
|
+
const projectA = mkdtempSync(join(tmpBase, "toolu-bs-a-"));
|
|
44
|
+
const projectB = mkdtempSync(join(tmpBase, "toolu-bs-b-"));
|
|
45
|
+
mkdirSync(join(projectA, ".opencode", "toolu"), { recursive: true });
|
|
46
|
+
mkdirSync(join(projectB, ".opencode", "toolu"), { recursive: true });
|
|
47
|
+
writeFileSync(
|
|
48
|
+
opencodePluginSelectionPath(projectA),
|
|
49
|
+
JSON.stringify({ version: 1, enabled: ["ts-quality"] }),
|
|
50
|
+
);
|
|
51
|
+
writeFileSync(
|
|
52
|
+
opencodePluginSelectionPath(projectB),
|
|
53
|
+
JSON.stringify({ version: 1, enabled: ["ts-quality"] }),
|
|
54
|
+
);
|
|
55
|
+
const selectA = selectPluginsWithDependencies(pluginsRoot, projectA);
|
|
56
|
+
const selectB = selectPluginsWithDependencies(pluginsRoot, projectB);
|
|
57
|
+
expect(selectA.ok && selectB.ok).toBe(true);
|
|
58
|
+
if (!selectA.ok || !selectB.ok) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const dataA = join(projectA, ".opencode", "toolu", "state");
|
|
63
|
+
const dataB = join(projectB, ".opencode", "toolu", "state");
|
|
64
|
+
const homeA = isolatedHome();
|
|
65
|
+
const homeB = isolatedHome();
|
|
66
|
+
|
|
67
|
+
const resultA = await bootstrapRuntime({
|
|
68
|
+
repoRoot: root,
|
|
69
|
+
projectRoot: projectA,
|
|
70
|
+
dataRoot: dataA,
|
|
71
|
+
plugins: selectA.plugins,
|
|
72
|
+
isolatedHome: homeA,
|
|
73
|
+
});
|
|
74
|
+
const resultB = await bootstrapRuntime({
|
|
75
|
+
repoRoot: root,
|
|
76
|
+
projectRoot: projectB,
|
|
77
|
+
dataRoot: dataB,
|
|
78
|
+
plugins: selectB.plugins,
|
|
79
|
+
isolatedHome: homeB,
|
|
80
|
+
});
|
|
81
|
+
expect(resultA.status).toBe("ready");
|
|
82
|
+
expect(resultB.status).toBe("ready");
|
|
83
|
+
if (resultA.status !== "ready" || resultB.status !== "ready") {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const postA = resultA.artifacts.find((a) => /\/post-tools\.d\/[^/]+\.sh$/.test(a));
|
|
87
|
+
const postB = resultB.artifacts.find((a) => /\/post-tools\.d\/[^/]+\.sh$/.test(a));
|
|
88
|
+
expect(postA).toBeDefined();
|
|
89
|
+
expect(postB).toBeDefined();
|
|
90
|
+
if (!postA || !postB) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
expect(postA.startsWith(dataA + "/")).toBe(true);
|
|
94
|
+
expect(postB.startsWith(dataB + "/")).toBe(true);
|
|
95
|
+
expect(postA).not.toBe(postB);
|
|
96
|
+
expect(readFileSync(postA, "utf8").length).toBeGreaterThan(0);
|
|
97
|
+
expect(existsSync(join(dataA, "toolu"))).toBe(true);
|
|
98
|
+
expect(existsSync(join(dataB, "toolu"))).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("AC-1: core-only session-start and core+quality register bootstrap", async () => {
|
|
102
|
+
const root = repoRoot();
|
|
103
|
+
const pluginsRoot = join(root, "plugins");
|
|
104
|
+
const projectCore = mkdtempSync(join(tmpBase, "toolu-bs-core-"));
|
|
105
|
+
mkdirSync(join(projectCore, ".opencode", "toolu"), { recursive: true });
|
|
106
|
+
writeFileSync(
|
|
107
|
+
opencodePluginSelectionPath(projectCore),
|
|
108
|
+
JSON.stringify({ version: 1, enabled: ["toolu"] }),
|
|
109
|
+
);
|
|
110
|
+
const coreSelect = selectPluginsWithDependencies(pluginsRoot, projectCore);
|
|
111
|
+
expect(coreSelect.ok).toBe(true);
|
|
112
|
+
if (!coreSelect.ok) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const coreData = mkdtempSync(join(tmpBase, "toolu-bs-core-data-"));
|
|
116
|
+
const coreResult = await bootstrapRuntime({
|
|
117
|
+
repoRoot: root,
|
|
118
|
+
projectRoot: projectCore,
|
|
119
|
+
dataRoot: coreData,
|
|
120
|
+
plugins: coreSelect.plugins,
|
|
121
|
+
isolatedHome: isolatedHome(),
|
|
122
|
+
});
|
|
123
|
+
expect(coreResult.status).toBe("ready");
|
|
124
|
+
|
|
125
|
+
const projectBoth = mkdtempSync(join(tmpBase, "toolu-bs-both-"));
|
|
126
|
+
mkdirSync(join(projectBoth, ".opencode", "toolu"), { recursive: true });
|
|
127
|
+
writeFileSync(
|
|
128
|
+
opencodePluginSelectionPath(projectBoth),
|
|
129
|
+
JSON.stringify({ version: 1, enabled: ["toolu", "ts-quality"] }),
|
|
130
|
+
);
|
|
131
|
+
const bothSelect = selectPluginsWithDependencies(pluginsRoot, projectBoth);
|
|
132
|
+
expect(bothSelect.ok).toBe(true);
|
|
133
|
+
if (!bothSelect.ok) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const bothData = mkdtempSync(join(tmpBase, "toolu-bs-both-data-"));
|
|
137
|
+
const bothResult = await bootstrapRuntime({
|
|
138
|
+
repoRoot: root,
|
|
139
|
+
projectRoot: projectBoth,
|
|
140
|
+
dataRoot: bothData,
|
|
141
|
+
plugins: bothSelect.plugins,
|
|
142
|
+
isolatedHome: isolatedHome(),
|
|
143
|
+
});
|
|
144
|
+
expect(bothResult.status).toBe("ready");
|
|
145
|
+
if (bothResult.status === "ready") {
|
|
146
|
+
expect(bothResult.artifacts.some((a) => /\/post-tools\.d\/[^/]+\.sh$/.test(a))).toBe(true);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Discover SessionStart / register entrypoints per plugin (#211).
|
|
2
|
+
* Prefer hooks/register.sh; fall back to toolu's hooks/session-start.sh.
|
|
3
|
+
* Missing both → null (caller skips; not every plugin registers).
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
8
|
+
export function pluginBootstrapScript(pluginDir: string): string | null {
|
|
9
|
+
const register = join(pluginDir, "hooks", "register.sh");
|
|
10
|
+
if (existsSync(register)) {
|
|
11
|
+
return register;
|
|
12
|
+
}
|
|
13
|
+
const sessionStart = join(pluginDir, "hooks", "session-start.sh");
|
|
14
|
+
if (existsSync(sessionStart)) {
|
|
15
|
+
return sessionStart;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { bootstrapRuntime } from "./runtime.ts";
|
|
2
|
+
export type { BootstrapRuntimeOptions } from "./runtime.ts";
|
|
3
|
+
export { collectBootstrapArtifacts, evaluateBootstrapReadiness } from "./readiness.ts";
|
|
4
|
+
export { pluginBootstrapScript } from "./entrypoint.ts";
|
|
5
|
+
export { bootstrapWithNoOpRunner, createNoOpRunner } from "./test-helpers.ts";
|