@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,125 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# One-time host permission allowlist for a repo.
|
|
3
|
+
#
|
|
4
|
+
# toolu's gates are the layer that decides what should stop you; the host's
|
|
5
|
+
# permission prompts are a second, unrelated layer that stops you again for
|
|
6
|
+
# routine work — every git and gh invocation, every shell command. Once the
|
|
7
|
+
# gates ask properly (see gate-mode.sh), the second layer is mostly noise.
|
|
8
|
+
#
|
|
9
|
+
# So the first toolu session in a repo writes an allowlist into that repo's
|
|
10
|
+
# .claude/settings.local.json, and records a sentinel so it never does it
|
|
11
|
+
# again. Deleting a rule by hand therefore sticks: this is a one-time
|
|
12
|
+
# convenience, not a policy that reasserts itself every session.
|
|
13
|
+
#
|
|
14
|
+
# Deliberate choices:
|
|
15
|
+
# * settings.local.json, not settings.json — the local file is not meant to
|
|
16
|
+
# be committed, so a teammate never inherits this decision.
|
|
17
|
+
# * the sentinel lives under the git-ignored state root, NOT in
|
|
18
|
+
# toolu.config.json, which IS committed — a marker there would ride into
|
|
19
|
+
# the repo and every clone would think it had already been done.
|
|
20
|
+
# * entries are merged as a union. Nothing the user already wrote is
|
|
21
|
+
# removed or reordered.
|
|
22
|
+
# * a settings file we cannot parse is left byte-identical. Losing someone's
|
|
23
|
+
# permission rules to a clumsy rewrite is far worse than a missing
|
|
24
|
+
# convenience.
|
|
25
|
+
#
|
|
26
|
+
# Claude Code only: Codex has no equivalent permission file.
|
|
27
|
+
#
|
|
28
|
+
# Public API:
|
|
29
|
+
# toolu_permissions_autowrite ROOT 0 if written, 1 if skipped
|
|
30
|
+
|
|
31
|
+
_TOOLU_PERMISSIONS_LIB_DIR="$(cd "${BASH_SOURCE%/*}" && pwd)"
|
|
32
|
+
# shellcheck source=config.sh
|
|
33
|
+
. "$_TOOLU_PERMISSIONS_LIB_DIR/config.sh"
|
|
34
|
+
|
|
35
|
+
# Blanket shell access plus the two edit tools. Bash(*) subsumes Bash(git:*)
|
|
36
|
+
# and Bash(gh:*), so they are not listed separately.
|
|
37
|
+
TOOLU_PERMISSIONS_DEFAULT='["Bash(*)","Edit","Write"]'
|
|
38
|
+
TOOLU_PERMISSIONS_SENTINEL=".permissions-written"
|
|
39
|
+
|
|
40
|
+
_toolu_permissions_warn() {
|
|
41
|
+
printf 'toolu-permissions: %s\n' "$1" >&2
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# _toolu_permissions_entries -> the JSON array of rules to add.
|
|
45
|
+
# A configured permissions.allow replaces the default outright (a user who
|
|
46
|
+
# names their own set means that set, not that set plus ours).
|
|
47
|
+
_toolu_permissions_entries() {
|
|
48
|
+
local configured
|
|
49
|
+
toolu_load_config
|
|
50
|
+
if [ "$_TOOLU_HAS_JQ" = "1" ]; then
|
|
51
|
+
configured=$(jq -c '
|
|
52
|
+
if ((.permissions?.allow? | type) == "array")
|
|
53
|
+
and ((.permissions.allow | map(select(type == "string")) | length) > 0)
|
|
54
|
+
then (.permissions.allow | map(select(type == "string")))
|
|
55
|
+
else empty end' <<< "$TOOLU_CFG_JSON" 2>/dev/null)
|
|
56
|
+
[ -n "$configured" ] && { printf '%s' "$configured"; return 0; }
|
|
57
|
+
fi
|
|
58
|
+
printf '%s' "$TOOLU_PERMISSIONS_DEFAULT"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# toolu_permissions_autowrite ROOT
|
|
62
|
+
toolu_permissions_autowrite() {
|
|
63
|
+
local root="${1:-}" settings_file sentinel state_root entries existing merged tmp added
|
|
64
|
+
[ -n "$root" ] || root=$(toolu_project_root)
|
|
65
|
+
[ -n "$root" ] || return 1
|
|
66
|
+
command -v jq >/dev/null 2>&1 || return 1
|
|
67
|
+
|
|
68
|
+
# Claude-only.
|
|
69
|
+
[ "$(toolu_host)" = claude ] || return 1
|
|
70
|
+
|
|
71
|
+
# Explicit opt-out.
|
|
72
|
+
toolu_flag_false permissions autoAllow && return 1
|
|
73
|
+
|
|
74
|
+
# A directory that is not a repository has no project-local settings to own.
|
|
75
|
+
git -C "$root" rev-parse --git-dir >/dev/null 2>&1 || return 1
|
|
76
|
+
|
|
77
|
+
state_root=$(toolu_project_state_root "$root")
|
|
78
|
+
[ -n "$state_root" ] || return 1
|
|
79
|
+
sentinel="$state_root/$TOOLU_PERMISSIONS_SENTINEL"
|
|
80
|
+
[ -f "$sentinel" ] && return 1
|
|
81
|
+
|
|
82
|
+
settings_file="$root/$(toolu_project_dirname)/settings.local.json"
|
|
83
|
+
existing='{}'
|
|
84
|
+
if [ -f "$settings_file" ]; then
|
|
85
|
+
if ! existing=$(jq -e . "$settings_file" 2>/dev/null); then
|
|
86
|
+
_toolu_permissions_warn "malformed JSON in $settings_file; leaving it untouched"
|
|
87
|
+
return 1
|
|
88
|
+
fi
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
entries=$(_toolu_permissions_entries)
|
|
92
|
+
|
|
93
|
+
# Union, order-preserving: existing rules first, then whatever is new.
|
|
94
|
+
merged=$(jq --argjson add "$entries" '
|
|
95
|
+
((.permissions.allow // []) | map(select(type == "string"))) as $have
|
|
96
|
+
| ($add | map(select(. as $x | $have | index($x) | not))) as $new
|
|
97
|
+
| .permissions = ((.permissions // {}) + {allow: ($have + $new)})
|
|
98
|
+
' <<< "$existing" 2>/dev/null) || {
|
|
99
|
+
_toolu_permissions_warn "could not merge permissions into $settings_file"
|
|
100
|
+
return 1
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
added=$(jq -r --argjson add "$entries" '
|
|
104
|
+
((.permissions.allow // []) | map(select(type == "string"))) as $have
|
|
105
|
+
| ($add | map(select(. as $x | $have | index($x) | not)) | join(", "))
|
|
106
|
+
' <<< "$existing" 2>/dev/null)
|
|
107
|
+
|
|
108
|
+
mkdir -p "$(dirname "$settings_file")" 2>/dev/null || return 1
|
|
109
|
+
tmp=$(mktemp "${settings_file}.XXXXXX" 2>/dev/null) || return 1
|
|
110
|
+
if ! printf '%s\n' "$merged" > "$tmp" 2>/dev/null || ! mv -f "$tmp" "$settings_file" 2>/dev/null; then
|
|
111
|
+
rm -f "$tmp" 2>/dev/null
|
|
112
|
+
_toolu_permissions_warn "could not write $settings_file"
|
|
113
|
+
return 1
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# The sentinel is written only after the settings write actually landed, so a
|
|
117
|
+
# failed write is retried next session rather than silently skipped forever.
|
|
118
|
+
mkdir -p "$state_root" 2>/dev/null && : > "$sentinel" 2>/dev/null
|
|
119
|
+
|
|
120
|
+
if [ -n "$added" ]; then
|
|
121
|
+
printf 'toolu wrote %s to %s (one time only; delete a rule and it stays deleted). Add that file to .gitignore if it is not there already.\n' \
|
|
122
|
+
"$added" "$settings_file"
|
|
123
|
+
fi
|
|
124
|
+
return 0
|
|
125
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Plan-ledger parse + I/O lib: extract the machine-readable steps block from a
|
|
3
|
+
# plan doc and read/write the per-branch ledger json atomically. Pure functions,
|
|
4
|
+
# jq-only, no command execution and no `set` (this is a sourced library).
|
|
5
|
+
# Source via: . "${BASH_SOURCE%/*}/plan-ledger-parse.sh"
|
|
6
|
+
|
|
7
|
+
# pl_parse_steps PLAN_DOC_PATH
|
|
8
|
+
# Extract the FIRST ```json fenced block under the `## Steps (machine-readable)`
|
|
9
|
+
# heading and print the JSON array to stdout. Validates with jq: must be a
|
|
10
|
+
# non-empty array whose every element has non-empty string id/title/check, and
|
|
11
|
+
# whose optional `model` (the tier that should execute the step) is one of
|
|
12
|
+
# haiku|sonnet|opus|fable|inherit.
|
|
13
|
+
# On missing heading/block, empty array, malformed json, or an unroutable
|
|
14
|
+
# model: error to stderr, non-zero return, nothing on stdout. Never writes.
|
|
15
|
+
pl_parse_steps() {
|
|
16
|
+
local doc="$1" block
|
|
17
|
+
if [ ! -f "$doc" ]; then
|
|
18
|
+
echo "plan-ledger-parse: plan doc not found: $doc" >&2
|
|
19
|
+
return 1
|
|
20
|
+
fi
|
|
21
|
+
# awk state machine: once past the marker heading, capture the lines between
|
|
22
|
+
# the next ```json fence and its closing ``` fence. `in_steps` arms after the
|
|
23
|
+
# heading; `in_block` arms inside the first json fence; we stop at its close.
|
|
24
|
+
block=$(awk '
|
|
25
|
+
/^## Steps \(machine-readable\)[[:space:]]*$/ { in_steps = 1; next }
|
|
26
|
+
in_steps && !in_block && /^```json[[:space:]]*$/ { in_block = 1; next }
|
|
27
|
+
in_block && /^```[[:space:]]*$/ { exit }
|
|
28
|
+
in_block { print }
|
|
29
|
+
' "$doc")
|
|
30
|
+
if [ -z "$block" ]; then
|
|
31
|
+
echo "plan-ledger-parse: no '## Steps (machine-readable)' json block in $doc" >&2
|
|
32
|
+
return 1
|
|
33
|
+
fi
|
|
34
|
+
# Validate: parseable, non-empty array, every step has non-empty id/title/check.
|
|
35
|
+
if ! jq -e '
|
|
36
|
+
type == "array"
|
|
37
|
+
and length > 0
|
|
38
|
+
and all(.[];
|
|
39
|
+
(.id | type == "string" and length > 0) and
|
|
40
|
+
(.title | type == "string" and length > 0) and
|
|
41
|
+
(.check | type == "string" and length > 0))
|
|
42
|
+
' <<< "$block" >/dev/null 2>&1; then
|
|
43
|
+
echo "plan-ledger-parse: steps block in $doc is not a non-empty array of {id,title,check} strings" >&2
|
|
44
|
+
return 1
|
|
45
|
+
fi
|
|
46
|
+
# `model` is the tier that should execute the step. Optional, but when present
|
|
47
|
+
# it must be a routable alias — a typo here would send `execution` to delegate
|
|
48
|
+
# on a model the Agent tool rejects, failing the step for a reason that has
|
|
49
|
+
# nothing to do with the step. Reject at parse time instead.
|
|
50
|
+
# The alias list mirrors TOOLU_MODEL_ALIASES in hooks/lib/config.sh; this lib
|
|
51
|
+
# stays jq-only rather than sourcing the config loader, so parity is enforced
|
|
52
|
+
# by __tests__/plan-ledger-model.bats.
|
|
53
|
+
local bad_models
|
|
54
|
+
if ! bad_models=$(jq -r '
|
|
55
|
+
[ .[] | select(.model != null)
|
|
56
|
+
| select((.model | type) != "string"
|
|
57
|
+
or ((.model) as $m
|
|
58
|
+
| ["haiku","sonnet","opus","fable","inherit"] | index($m) | not))
|
|
59
|
+
| "\(.id)=\(.model|tostring)" ] | join(", ")
|
|
60
|
+
' <<< "$block" 2>/dev/null); then
|
|
61
|
+
# Fail closed: an unvalidated model would reach the executor as a delegation
|
|
62
|
+
# target, so an internal validator failure is an error, not a pass.
|
|
63
|
+
echo "plan-ledger-parse: could not validate step models in $doc" >&2
|
|
64
|
+
return 1
|
|
65
|
+
fi
|
|
66
|
+
if [ -n "$bad_models" ]; then
|
|
67
|
+
echo "plan-ledger-parse: invalid step model in $doc ($bad_models); allowed: haiku sonnet opus fable inherit" >&2
|
|
68
|
+
return 1
|
|
69
|
+
fi
|
|
70
|
+
# `paths` narrows what a step's freshness depends on. A step that declares it
|
|
71
|
+
# is re-run only when those paths change, instead of on every change anywhere
|
|
72
|
+
# in the branch diff. Wrong contents here would silently keep a step green
|
|
73
|
+
# after its real inputs moved, so the shape is checked rather than trusted:
|
|
74
|
+
# an array of non-empty strings, or absent.
|
|
75
|
+
local bad_paths
|
|
76
|
+
if ! bad_paths=$(jq -r '
|
|
77
|
+
[ .[] | select(.paths != null)
|
|
78
|
+
| select((.paths | type) != "array"
|
|
79
|
+
or ([.paths[] | select((type) != "string" or (. == ""))] | length) > 0)
|
|
80
|
+
| .id ] | join(", ")
|
|
81
|
+
' <<< "$block" 2>/dev/null); then
|
|
82
|
+
echo "plan-ledger-parse: could not validate step paths in $doc" >&2
|
|
83
|
+
return 2
|
|
84
|
+
fi
|
|
85
|
+
if [ -n "$bad_paths" ]; then
|
|
86
|
+
echo "plan-ledger-parse: invalid step paths in $doc ($bad_paths); expected an array of non-empty strings" >&2
|
|
87
|
+
return 2
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# Emit the normalized array (jq-compacted) on stdout. Backfill the optional
|
|
91
|
+
# authored fields so downstream serialization always sees them: ac_refs,
|
|
92
|
+
# depends_on and paths default to [], input and model to null. Legacy
|
|
93
|
+
# {id,title,check} steps thus gain the defaults; authored values are preserved.
|
|
94
|
+
# Permissive `// default` also coerces a present-but-null field to its default.
|
|
95
|
+
jq -c 'map(
|
|
96
|
+
.ac_refs = (.ac_refs // []) |
|
|
97
|
+
.depends_on = (.depends_on // []) |
|
|
98
|
+
.paths = (.paths // []) |
|
|
99
|
+
.input = (.input // null) |
|
|
100
|
+
.model = (.model // null)
|
|
101
|
+
)' <<< "$block"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# pl_doc_field DOC FIELD
|
|
105
|
+
# Print the trimmed value of an inline-bold `**<FIELD>:**` from a packed doc
|
|
106
|
+
# header line, or empty string if the field (or doc) is absent. Never errors:
|
|
107
|
+
# a missing field is a valid "spec-less / unstamped" signal, not a failure.
|
|
108
|
+
#
|
|
109
|
+
# Header lines pack fields together, e.g.:
|
|
110
|
+
# **Date:** 2026-06-17 **Status:** Draft **Spec:** path.md **Topic:** …
|
|
111
|
+
# Extraction (first matching line wins): strip up to and including `**FIELD:**`,
|
|
112
|
+
# then cut from the next `**Word:**` bold key (preceded by whitespace) or EOL,
|
|
113
|
+
# then trim surrounding whitespace. A trailing (last-on-line) field has no
|
|
114
|
+
# following bold key, so it runs to EOL. The boundary requires ≥1 space before
|
|
115
|
+
# the next `**…:**` so a value that happens to contain `**` mid-word is not
|
|
116
|
+
# truncated, and both the 3-space house style and a 1-space packing are handled.
|
|
117
|
+
pl_doc_field() {
|
|
118
|
+
local doc="$1" field="$2"
|
|
119
|
+
[ -n "$doc" ] && [ -f "$doc" ] || return 0
|
|
120
|
+
[ -n "$field" ] || return 0
|
|
121
|
+
# First line containing the bold key, then sed the value out of it.
|
|
122
|
+
grep -m1 -F "**${field}:**" "$doc" 2>/dev/null \
|
|
123
|
+
| sed -E "s/.*\\*\\*${field}:\\*\\*[[:space:]]*//; s/[[:space:]]+\\*\\*[^*]+:\\*\\*.*$//; s/^[[:space:]]+//; s/[[:space:]]+$//"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# pl_parse_acs SPEC_DOC
|
|
127
|
+
# Print the newline-separated list of acceptance-criterion ids (`AC-<n>`) declared
|
|
128
|
+
# under the spec's `## Acceptance criteria` heading, in document order, deduped.
|
|
129
|
+
# Ids come from the stable `**AC-<n>:**` bold prefix. A missing doc, a doc with no
|
|
130
|
+
# such heading, or a heading with no AC lines all yield an empty list and a zero
|
|
131
|
+
# return — "no ACs" is a valid state, never an error. Never writes.
|
|
132
|
+
pl_parse_acs() {
|
|
133
|
+
local doc="$1"
|
|
134
|
+
[ -n "$doc" ] && [ -f "$doc" ] || return 0
|
|
135
|
+
# awk state machine: arm inside the `## Acceptance criteria` section, disarm at
|
|
136
|
+
# the next `## ` heading, and emit the id from each `**AC-<n>:**` bold prefix.
|
|
137
|
+
# `seen[]` dedups while preserving first-seen order.
|
|
138
|
+
awk '
|
|
139
|
+
/^## Acceptance criteria[[:space:]]*$/ { in_ac = 1; next }
|
|
140
|
+
in_ac && /^## / { in_ac = 0 }
|
|
141
|
+
in_ac {
|
|
142
|
+
if (match($0, /\*\*AC-[0-9]+:\*\*/)) {
|
|
143
|
+
id = substr($0, RSTART + 2, RLENGTH - 5) # strip leading ** and trailing :**
|
|
144
|
+
if (!(id in seen)) { seen[id] = 1; print id }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
' "$doc"
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# pl_check_ac_refs PLAN_DOC [SPEC_DOC]
|
|
151
|
+
# Print each dangling ac_ref — an `ac_refs` id in the plan's steps block that is
|
|
152
|
+
# NOT declared in the spec — one per line, and return non-zero iff any exist.
|
|
153
|
+
# When SPEC_DOC is omitted, empty, or the literal "none" (case-insensitive), the
|
|
154
|
+
# plan is spec-less: coverage is skipped, nothing is printed, return 0. A spec
|
|
155
|
+
# present but lacking an `## Acceptance criteria` section declares zero ids, so
|
|
156
|
+
# every non-empty ac_ref is dangling. Parse failure of the plan -> non-zero.
|
|
157
|
+
pl_check_ac_refs() {
|
|
158
|
+
local plan="$1" spec="${2:-}" steps refs acs dangling
|
|
159
|
+
# Spec-less: nothing to resolve against -> pass with no dangling refs.
|
|
160
|
+
case "$(printf '%s' "$spec" | tr '[:upper:]' '[:lower:]')" in
|
|
161
|
+
""|none) return 0 ;;
|
|
162
|
+
esac
|
|
163
|
+
steps=$(pl_parse_steps "$plan") || return 1
|
|
164
|
+
# All referenced ids across every step, deduped (order irrelevant for set diff).
|
|
165
|
+
refs=$(jq -r '[.[].ac_refs[]?] | unique[]' <<< "$steps") || return 1
|
|
166
|
+
# No refs at all -> nothing can dangle.
|
|
167
|
+
[ -n "$refs" ] || return 0
|
|
168
|
+
acs=$(pl_parse_acs "$spec")
|
|
169
|
+
# Set difference: refs not present in the spec's declared ids. comm needs both
|
|
170
|
+
# operands sorted; refs is already unique. Empty `acs` -> all refs dangle.
|
|
171
|
+
dangling=$(comm -23 <(printf '%s\n' "$refs" | sort) <(printf '%s\n' "$acs" | sort)) || return 1
|
|
172
|
+
if [ -n "$dangling" ]; then
|
|
173
|
+
printf '%s\n' "$dangling"
|
|
174
|
+
return 1
|
|
175
|
+
fi
|
|
176
|
+
return 0
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# pl_read_ledger STATE_FILE
|
|
180
|
+
# Print the ledger json to stdout. Empty or absent file -> non-zero, no output.
|
|
181
|
+
pl_read_ledger() {
|
|
182
|
+
local state_file="$1" content
|
|
183
|
+
if [ ! -s "$state_file" ]; then
|
|
184
|
+
return 1
|
|
185
|
+
fi
|
|
186
|
+
content=$(cat "$state_file" 2>/dev/null) || return 1
|
|
187
|
+
if ! jq -e . <<< "$content" >/dev/null 2>&1; then
|
|
188
|
+
return 1
|
|
189
|
+
fi
|
|
190
|
+
printf '%s\n' "$content"
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# pl_write_ledger STATE_FILE JSON_STRING
|
|
194
|
+
# Validate JSON_STRING with jq, then write it atomically (temp + mv) to
|
|
195
|
+
# STATE_FILE, creating the parent dir if needed. Invalid json -> non-zero,
|
|
196
|
+
# no write and no leftover temp file.
|
|
197
|
+
pl_write_ledger() {
|
|
198
|
+
local state_file="$1" json="$2" dir tmp
|
|
199
|
+
if ! jq -e . <<< "$json" >/dev/null 2>&1; then
|
|
200
|
+
echo "plan-ledger-parse: refusing to write invalid ledger json to $state_file" >&2
|
|
201
|
+
return 1
|
|
202
|
+
fi
|
|
203
|
+
dir=$(dirname "$state_file")
|
|
204
|
+
if ! mkdir -p "$dir"; then
|
|
205
|
+
echo "plan-ledger-parse: cannot create ledger dir: $dir" >&2
|
|
206
|
+
return 1
|
|
207
|
+
fi
|
|
208
|
+
tmp="$state_file.tmp.$$"
|
|
209
|
+
if ! jq . <<< "$json" > "$tmp" 2>/dev/null; then
|
|
210
|
+
rm -f "$tmp"
|
|
211
|
+
echo "plan-ledger-parse: failed to stage ledger to $tmp" >&2
|
|
212
|
+
return 1
|
|
213
|
+
fi
|
|
214
|
+
if ! mv "$tmp" "$state_file"; then
|
|
215
|
+
rm -f "$tmp"
|
|
216
|
+
echo "plan-ledger-parse: atomic mv failed for $state_file" >&2
|
|
217
|
+
return 1
|
|
218
|
+
fi
|
|
219
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Plan-ledger preflight precondition gate. Sourced by plan-ledger.sh (keeps that
|
|
3
|
+
# file under its 500-line size budget). Pure-ish: reads the plan + spec docs and
|
|
4
|
+
# the ledger; runs no checks and writes nothing. jq-only via the parse lib.
|
|
5
|
+
#
|
|
6
|
+
# Depends on (already sourced by plan-ledger.sh before this file): pl_doc_field,
|
|
7
|
+
# pl_ledger_path, pl_read_ledger (plan-ledger-parse.sh) + detect_project_root
|
|
8
|
+
# (detect.sh).
|
|
9
|
+
#
|
|
10
|
+
# Source via: . "${BASH_SOURCE%/*}/plan-ledger-preflight.sh"
|
|
11
|
+
|
|
12
|
+
# pl_status_is_approved STATUS -> return 0 iff STATUS == "approved" (case-insensitive).
|
|
13
|
+
pl_status_is_approved() {
|
|
14
|
+
local s
|
|
15
|
+
s=$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')
|
|
16
|
+
[ "$s" = "approved" ]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Default age (seconds) after which a `running` step is considered orphaned (a
|
|
20
|
+
# crash between the two writes wedged it). Overridable for tests.
|
|
21
|
+
: "${PL_STUCK_THRESHOLD:=300}"
|
|
22
|
+
|
|
23
|
+
# pl_heal_orphans LEDGER_JSON
|
|
24
|
+
# Reset any `running` step whose started_at is older than PL_STUCK_THRESHOLD
|
|
25
|
+
# seconds (or has no/empty started_at) back to `pending`, clearing started_at +
|
|
26
|
+
# activity. A still-fresh running step is left as-is. Prints the (possibly
|
|
27
|
+
# unchanged) ledger json on stdout. Pure: no I/O, no recompute.
|
|
28
|
+
#
|
|
29
|
+
# Comparison is lexical on the fixed ISO-8601 UTC format (`YYYY-MM-DDTHH:MM:SSZ`
|
|
30
|
+
# sorts chronologically). The cutoff = now - threshold is computed in bash via
|
|
31
|
+
# epoch math + reformat — portable across BSD (`date -r`) and GNU (`date -d @`),
|
|
32
|
+
# and immune to jq mktime's local-timezone interpretation.
|
|
33
|
+
pl_heal_orphans() {
|
|
34
|
+
local ledger="$1" now_epoch cutoff_epoch cutoff
|
|
35
|
+
now_epoch=$(date -u +%s)
|
|
36
|
+
cutoff_epoch=$((now_epoch - PL_STUCK_THRESHOLD))
|
|
37
|
+
cutoff=$(date -u -r "$cutoff_epoch" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \
|
|
38
|
+
|| date -u -d "@$cutoff_epoch" +%Y-%m-%dT%H:%M:%SZ) \
|
|
39
|
+
|| return 1
|
|
40
|
+
jq --arg cutoff "$cutoff" '
|
|
41
|
+
.steps |= map(
|
|
42
|
+
if .status == "running"
|
|
43
|
+
then
|
|
44
|
+
(.started_at // "") as $st
|
|
45
|
+
| if ($st == "") or ($st < $cutoff)
|
|
46
|
+
then .status = "pending" | .started_at = null | .activity = null
|
|
47
|
+
else . end
|
|
48
|
+
else . end
|
|
49
|
+
)
|
|
50
|
+
' <<< "$ledger"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# pl_cmd_preflight [PLAN_DOC]
|
|
54
|
+
# Resolve the plan doc (arg, else the current branch ledger's plan_doc). Read its
|
|
55
|
+
# **Status:** and **Spec:** via pl_doc_field. Pass (exit 0, silent) only when the
|
|
56
|
+
# plan is Approved AND (spec-less OR the declared spec is Approved).
|
|
57
|
+
#
|
|
58
|
+
# Exit codes:
|
|
59
|
+
# 0 preconditions met.
|
|
60
|
+
# 1 preconditions unmet (plan not approved / declared spec unreadable / spec
|
|
61
|
+
# not approved / header-less plan with empty Status). One blocker line each.
|
|
62
|
+
# 2 plan doc missing / unreadable, or cannot resolve a plan to check.
|
|
63
|
+
pl_cmd_preflight() {
|
|
64
|
+
local plan="${1:-}" root ledger ledger_file
|
|
65
|
+
|
|
66
|
+
root=$(detect_project_root)
|
|
67
|
+
|
|
68
|
+
# Read the current branch's ledger if present, to (a) resolve the plan_doc when
|
|
69
|
+
# no arg was given and (b) self-heal orphaned `running` steps so a wedged step
|
|
70
|
+
# can't outlive a crash. Healing rewrites the ledger; absence is not an error.
|
|
71
|
+
if ledger_file=$(pl_ledger_path 2>/dev/null) && ledger=$(pl_read_ledger "$ledger_file" 2>/dev/null); then
|
|
72
|
+
local healed
|
|
73
|
+
if healed=$(pl_heal_orphans "$ledger" 2>/dev/null) && [ "$healed" != "$ledger" ]; then
|
|
74
|
+
pl_write_ledger "$ledger_file" "$healed" 2>/dev/null || true
|
|
75
|
+
fi
|
|
76
|
+
if [ -z "$plan" ]; then
|
|
77
|
+
plan=$(jq -r '.plan_doc // ""' <<< "$ledger" 2>/dev/null)
|
|
78
|
+
fi
|
|
79
|
+
fi
|
|
80
|
+
if [ -z "$plan" ]; then
|
|
81
|
+
echo "preflight: no plan doc given and no ledger plan_doc to resolve" >&2
|
|
82
|
+
return 2
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# A relative plan path is resolved against repo root (matching how it's run).
|
|
86
|
+
local plan_abs="$plan"
|
|
87
|
+
case "$plan" in
|
|
88
|
+
/*) : ;;
|
|
89
|
+
*) [ -n "$root" ] && plan_abs="$root/$plan" ;;
|
|
90
|
+
esac
|
|
91
|
+
if [ ! -r "$plan_abs" ]; then
|
|
92
|
+
echo "preflight: plan doc not found or unreadable: $plan" >&2
|
|
93
|
+
return 2
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# Plan status. A header-less/malformed plan yields an empty Status -> blocker
|
|
97
|
+
# (exit 1, not 2 — the doc exists, it just isn't stamped).
|
|
98
|
+
local plan_status
|
|
99
|
+
plan_status=$(pl_doc_field "$plan_abs" Status)
|
|
100
|
+
if [ -z "$plan_status" ]; then
|
|
101
|
+
echo "preflight: plan has no **Status:** header ($plan) — run /toolu:plan-review to stamp it" >&2
|
|
102
|
+
return 1
|
|
103
|
+
fi
|
|
104
|
+
if ! pl_status_is_approved "$plan_status"; then
|
|
105
|
+
echo "preflight: plan not approved (Status: $plan_status) — run /toolu:plan-review" >&2
|
|
106
|
+
return 1
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
# Spec gate. A missing **Spec:** line OR "none" is spec-less -> skip.
|
|
110
|
+
local spec
|
|
111
|
+
spec=$(pl_doc_field "$plan_abs" Spec)
|
|
112
|
+
local spec_lc
|
|
113
|
+
spec_lc=$(printf '%s' "$spec" | tr '[:upper:]' '[:lower:]')
|
|
114
|
+
if [ -z "$spec" ] || [ "$spec_lc" = "none" ]; then
|
|
115
|
+
return 0
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
# Declared spec: resolve relative to repo root, must be readable.
|
|
119
|
+
local spec_abs="$spec"
|
|
120
|
+
case "$spec" in
|
|
121
|
+
/*) : ;;
|
|
122
|
+
*) [ -n "$root" ] && spec_abs="$root/$spec" ;;
|
|
123
|
+
esac
|
|
124
|
+
if [ ! -r "$spec_abs" ]; then
|
|
125
|
+
echo "preflight: declared spec not found or unreadable: $spec" >&2
|
|
126
|
+
return 1
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
local spec_status
|
|
130
|
+
spec_status=$(pl_doc_field "$spec_abs" Status)
|
|
131
|
+
if ! pl_status_is_approved "$spec_status"; then
|
|
132
|
+
echo "preflight: spec $spec not approved (Status: ${spec_status:-none}) — run /toolu:spec-review" >&2
|
|
133
|
+
return 1
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
return 0
|
|
137
|
+
}
|