@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,749 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Plan-ledger checker lib + CLI: run each plan step's `check`, stamp mechanical
|
|
3
|
+
# status + content-addressed diff_sha into the per-branch ledger, and report
|
|
4
|
+
# fresh-green/next. The SCRIPT sets status (exit-code truth) — the agent cannot
|
|
5
|
+
# claim green. Sourceable (functions only) and runnable (guarded `main`).
|
|
6
|
+
# jq-only. Parse/IO errors fail closed (exit 2).
|
|
7
|
+
#
|
|
8
|
+
# Run: bash plan-ledger.sh run <doc.md> [--step <id>] [--activity <label>] [--force] [--verify]
|
|
9
|
+
# bash plan-ledger.sh status | preflight [<doc.md>] | path | root | --self-test
|
|
10
|
+
# Source: . "${BASH_SOURCE%/*}/plan-ledger.sh" (defines pl_* helpers, no run)
|
|
11
|
+
|
|
12
|
+
# pipefail so the `git diff | git hash-object` pipe surfaces failures instead of
|
|
13
|
+
# silently yielding the empty-blob sha. NOT -euo: this file sources libs and runs
|
|
14
|
+
# user `check` commands whose non-zero exits are expected signal, not fatal.
|
|
15
|
+
set -o pipefail
|
|
16
|
+
|
|
17
|
+
_toolu_lib="${TOOLU_LIB_DIR:-${BASH_SOURCE%/*}}"
|
|
18
|
+
# shellcheck source=plan-ledger-parse.sh
|
|
19
|
+
. "$_toolu_lib/plan-ledger-parse.sh"
|
|
20
|
+
# shellcheck source=detect.sh
|
|
21
|
+
. "$_toolu_lib/detect.sh"
|
|
22
|
+
# shellcheck source=plan-ledger-preflight.sh
|
|
23
|
+
. "$_toolu_lib/plan-ledger-preflight.sh"
|
|
24
|
+
# shellcheck source=diff-sha.sh
|
|
25
|
+
. "$_toolu_lib/diff-sha.sh"
|
|
26
|
+
# shellcheck source=telemetry.sh
|
|
27
|
+
. "$_toolu_lib/telemetry.sh"
|
|
28
|
+
|
|
29
|
+
# pl_diff_sha BASE
|
|
30
|
+
# Print the content-addressed diff hash of BASE...HEAD (matches push-review.sh:88).
|
|
31
|
+
# Empty stdout + non-zero on git failure so callers can fail closed. Both
|
|
32
|
+
# callers below run with cwd already at the project root (pl_cmd_run cd's
|
|
33
|
+
# there; pl_cmd_status assumes it like every other bare git call in this file),
|
|
34
|
+
# so "." is the equivalent repo root.
|
|
35
|
+
pl_diff_sha() {
|
|
36
|
+
local base="$1"
|
|
37
|
+
toolu_diff_sha . "$base"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# pl_ledger_path
|
|
41
|
+
# Print the ledger path for the current branch:
|
|
42
|
+
# <project_root>/<host-state>/tmp/plan-ledger/<branch_slug>.json
|
|
43
|
+
# Non-zero if the project root can't be resolved (not a git repo).
|
|
44
|
+
pl_ledger_path() {
|
|
45
|
+
local root branch slug
|
|
46
|
+
root=$(detect_project_root)
|
|
47
|
+
[ -n "$root" ] || return 1
|
|
48
|
+
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) || return 1
|
|
49
|
+
slug=$(branch_slug "$branch")
|
|
50
|
+
printf '%s/%s.json\n' "$(toolu_project_state_dir plan-ledger "$root")" "$slug"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# pl_evidence COMBINED_OUTPUT
|
|
54
|
+
# JSON-encode the last 10 lines of a check's combined stdout+stderr, capped to
|
|
55
|
+
# ~2000 bytes, via `jq -Rs` (handles null bytes / invalid UTF-8 safely). Prints a
|
|
56
|
+
# JSON string (quoted) on stdout.
|
|
57
|
+
pl_evidence() {
|
|
58
|
+
printf '%s' "$1" \
|
|
59
|
+
| tail -n 10 \
|
|
60
|
+
| head -c 2000 \
|
|
61
|
+
| jq -Rs .
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# pl_scope_sha BASE PATHS_JSON
|
|
65
|
+
# Print the content hash of BASE...HEAD restricted to PATHS_JSON (a JSON array
|
|
66
|
+
# of pathspecs), or nothing when the array is empty.
|
|
67
|
+
#
|
|
68
|
+
# This is what lets a step stay green through a change it does not depend on. A
|
|
69
|
+
# branch-wide hash says "something moved"; this says "something YOU read moved".
|
|
70
|
+
pl_scope_sha() {
|
|
71
|
+
local base="$1" paths_json="$2"
|
|
72
|
+
local -a paths=()
|
|
73
|
+
local p
|
|
74
|
+
while IFS= read -r p; do
|
|
75
|
+
[ -n "$p" ] && paths+=("$p")
|
|
76
|
+
done < <(jq -r '.[]?' <<< "$paths_json" 2>/dev/null)
|
|
77
|
+
[ "${#paths[@]}" -gt 0 ] || return 0
|
|
78
|
+
|
|
79
|
+
# Capture the diff separately rather than piping straight into hash-object.
|
|
80
|
+
# A failed `git diff` writes nothing, and hashing nothing yields the
|
|
81
|
+
# well-known empty-blob sha — indistinguishable from "these paths are
|
|
82
|
+
# genuinely unchanged". Stored, that sha would match on every later run and
|
|
83
|
+
# pin the step green forever. Returning non-zero instead drops the step back
|
|
84
|
+
# to the branch-wide rule, which re-runs it. (push-review.sh guards the same
|
|
85
|
+
# empty-blob hazard on its own diff.)
|
|
86
|
+
local diff_out
|
|
87
|
+
diff_out=$(git diff --no-color "${base}...HEAD" -- "${paths[@]}" 2>/dev/null) || return 1
|
|
88
|
+
|
|
89
|
+
# The declaration is part of the identity: hashing only the diff would let a
|
|
90
|
+
# step keep a green after its `paths` were edited, as long as the new set
|
|
91
|
+
# happened to produce identical content — widening a scope must invalidate it.
|
|
92
|
+
{ printf '%s\0' "${paths[@]}"; printf '%s' "$diff_out"; } | git hash-object --stdin
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# pl_scope_map STEPS_JSON BASE
|
|
96
|
+
# Print {step_id: scope_sha} for every step declaring `paths`. Steps without
|
|
97
|
+
# paths are absent from the map and fall back to the branch-wide hash.
|
|
98
|
+
pl_scope_map() {
|
|
99
|
+
local steps="$1" base="$2" id paths sha out="{}"
|
|
100
|
+
while IFS=$'\t' read -r id paths; do
|
|
101
|
+
[ -n "$id" ] || continue
|
|
102
|
+
sha=$(pl_scope_sha "$base" "$paths") || sha=""
|
|
103
|
+
if [ -z "$sha" ]; then
|
|
104
|
+
# Falling back to the branch-wide rule is the safe direction — it re-runs
|
|
105
|
+
# the step more often, never less — but a `paths` declaration that quietly
|
|
106
|
+
# does nothing looks identical to one that works. Say so.
|
|
107
|
+
printf 'plan-ledger: step %s declares paths that could not be hashed; judging it on the whole branch diff\n' \
|
|
108
|
+
"$id" >&2
|
|
109
|
+
continue
|
|
110
|
+
fi
|
|
111
|
+
out=$(jq -c --arg id "$id" --arg sha "$sha" '. + {($id): $sha}' <<< "$out") || return 1
|
|
112
|
+
done < <(jq -r '.[] | select((.paths // []) | length > 0) | "\(.id)\t\(.paths | tojson)"' <<< "$steps" 2>/dev/null)
|
|
113
|
+
printf '%s' "$out"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# pl_recompute LEDGER_JSON CURRENT_DIFF_SHA
|
|
117
|
+
# Recompute summary{total,green,red,pending,running,stale,fresh_green} and next
|
|
118
|
+
# against CURRENT_DIFF_SHA (a step is fresh-green iff status==green AND diff_sha
|
|
119
|
+
# matches; a running step is never fresh; next = first non-fresh-green step id,
|
|
120
|
+
# null when all fresh-green). Print the updated ledger json on stdout.
|
|
121
|
+
pl_recompute() {
|
|
122
|
+
local ledger="$1" cur="$2" scope_map="${3:-}" verify="${4:-}"
|
|
123
|
+
[ -n "$scope_map" ] || scope_map='{}'
|
|
124
|
+
jq --arg cur "$cur" --argjson scope "$scope_map" --arg verify "$verify" '
|
|
125
|
+
# A step is fresh when it is green and nothing it depends on has moved.
|
|
126
|
+
# "Depends on" is the branch diff by default; a step that declared `paths`
|
|
127
|
+
# is judged on the hash of just those, so an unrelated edit no longer
|
|
128
|
+
# re-runs it. `--verify` ignores scope entirely and judges every step on
|
|
129
|
+
# the branch hash — the pre-push contract, where a narrow or stale `paths`
|
|
130
|
+
# declaration must not be able to hold a green.
|
|
131
|
+
def scoped_sha: $scope[.id] // null;
|
|
132
|
+
def is_fresh:
|
|
133
|
+
(.status == "green")
|
|
134
|
+
and (if ($verify != "1") and (scoped_sha != null)
|
|
135
|
+
then (.scope_sha != null) and (.scope_sha == scoped_sha)
|
|
136
|
+
else (.diff_sha == $cur) end);
|
|
137
|
+
.summary = {
|
|
138
|
+
total: (.steps | length),
|
|
139
|
+
green: ([.steps[] | select(.status == "green")] | length),
|
|
140
|
+
red: ([.steps[] | select(.status == "red")] | length),
|
|
141
|
+
pending: ([.steps[] | select(.status == "pending")]| length),
|
|
142
|
+
running: ([.steps[] | select(.status == "running")]| length),
|
|
143
|
+
stale: ([.steps[] | select(.status == "green" and (is_fresh | not))] | length),
|
|
144
|
+
fresh_green: ([.steps[] | select(is_fresh)] | length),
|
|
145
|
+
retried: ([.steps[] | select((.retries // []) | length > 0)] | length)
|
|
146
|
+
}
|
|
147
|
+
| .next = (first(.steps[] | select(is_fresh | not) | .id) // null)
|
|
148
|
+
' <<< "$ledger"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# pl_summary_line LEDGER_JSON SLUG
|
|
152
|
+
# Print the single-line, parseable summary:
|
|
153
|
+
# plan-ledger <slug>: <fresh_green>/<total> fresh-green, next=<id|none>
|
|
154
|
+
# When the next step declares a model tier, ` model=<alias>` is appended so the
|
|
155
|
+
# executor knows which model to delegate it to without re-reading the plan doc.
|
|
156
|
+
# The suffix is omitted entirely for steps with no declared tier, keeping the
|
|
157
|
+
# line byte-identical for plans that never opted in.
|
|
158
|
+
pl_summary_line() {
|
|
159
|
+
local ledger="$1" slug="$2"
|
|
160
|
+
jq -r --arg slug "$slug" '
|
|
161
|
+
(.next) as $n
|
|
162
|
+
| ([.steps[] | select(.id == $n) | .model] | map(select(. != null)) | first) as $m
|
|
163
|
+
| "plan-ledger " + $slug + ": "
|
|
164
|
+
+ (.summary.fresh_green | tostring) + "/" + (.summary.total | tostring)
|
|
165
|
+
+ " fresh-green, next=" + ($n // "none")
|
|
166
|
+
+ (if $m == null then "" else " model=" + $m end)
|
|
167
|
+
' <<< "$ledger"
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# pl_all_fresh LEDGER_JSON -> return 0 iff every step is fresh-green.
|
|
171
|
+
pl_all_fresh() {
|
|
172
|
+
local ledger="$1"
|
|
173
|
+
[ "$(jq -r '.next == null' <<< "$ledger")" = "true" ]
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# pl_now -> UTC ISO-8601 timestamp.
|
|
177
|
+
pl_now() { date -u +%Y-%m-%dT%H:%M:%SZ; }
|
|
178
|
+
|
|
179
|
+
# pl_build_step_entry STEPS_JSON ID STATUS EXIT_CODE DIFF_SHA EVIDENCE_JSON \
|
|
180
|
+
# [STARTED_AT] [ACTIVITY] [PRIOR_ENTRY_JSON]
|
|
181
|
+
# Print a single ledger step object merging the doc fields (id/title/check plus
|
|
182
|
+
# the authored ac_refs/depends_on/input/model from STEPS_JSON) with the run results.
|
|
183
|
+
# EVIDENCE_JSON is an already-JSON-encoded string. STARTED_AT/ACTIVITY are
|
|
184
|
+
# optional; an empty arg becomes JSON null. started_at is the ISO-8601 time the
|
|
185
|
+
# step entered `running`; activity is an optional short label.
|
|
186
|
+
#
|
|
187
|
+
# PRIOR_ENTRY_JSON is the step's prior on-disk ledger entry (or "" / "null" when
|
|
188
|
+
# none). retries[] is the chronological list of reds preceding this state: if the
|
|
189
|
+
# prior entry was red, it is archived as a retry record (attempt, exit_code,
|
|
190
|
+
# diff_sha, evidence_tail, at=prior.last_run) appended after the prior's own
|
|
191
|
+
# retries; otherwise the prior retries (or []) carry forward unchanged. Only reds
|
|
192
|
+
# are archived. All additive — version stays 1.
|
|
193
|
+
pl_build_step_entry() {
|
|
194
|
+
local steps="$1" id="$2" status="$3" code="$4" sha="$5" evidence="$6"
|
|
195
|
+
local started_at="${7:-}" activity="${8:-}" prior="${9:-}"
|
|
196
|
+
[ -n "$prior" ] || prior="null"
|
|
197
|
+
jq -n \
|
|
198
|
+
--argjson steps "$steps" \
|
|
199
|
+
--arg id "$id" \
|
|
200
|
+
--arg status "$status" \
|
|
201
|
+
--argjson code "$code" \
|
|
202
|
+
--arg sha "$sha" \
|
|
203
|
+
--arg now "$(pl_now)" \
|
|
204
|
+
--argjson evidence "$evidence" \
|
|
205
|
+
--arg started_at "$started_at" \
|
|
206
|
+
--arg activity "$activity" \
|
|
207
|
+
--argjson prior "$prior" '
|
|
208
|
+
($steps[] | select(.id == $id)) as $s
|
|
209
|
+
| ($prior.retries // []) as $prior_retries
|
|
210
|
+
| (if ($prior.status // null) == "red"
|
|
211
|
+
then $prior_retries + [{
|
|
212
|
+
attempt: (($prior_retries | length) + 1),
|
|
213
|
+
exit_code: $prior.exit_code,
|
|
214
|
+
diff_sha: $prior.diff_sha,
|
|
215
|
+
evidence_tail: $prior.evidence_tail,
|
|
216
|
+
at: $prior.last_run }]
|
|
217
|
+
else $prior_retries end) as $retries
|
|
218
|
+
| { id: $s.id, title: $s.title, check: $s.check,
|
|
219
|
+
status: $status,
|
|
220
|
+
started_at: (if $started_at == "" then null else $started_at end),
|
|
221
|
+
activity: (if $activity == "" then null else $activity end),
|
|
222
|
+
exit_code: $code, diff_sha: $sha,
|
|
223
|
+
last_run: $now, evidence_tail: $evidence,
|
|
224
|
+
ac_refs: ($s.ac_refs // []),
|
|
225
|
+
depends_on: ($s.depends_on // []),
|
|
226
|
+
input: ($s.input // null),
|
|
227
|
+
model: ($s.model // null),
|
|
228
|
+
retries: $retries }
|
|
229
|
+
'
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
# pl_cmd_run DOC [--step ID] [--activity LABEL]
|
|
233
|
+
# Parse DOC's steps; cd to project root; run checks (all, or only --step ID
|
|
234
|
+
# preserving other entries from an existing ledger); recompute and write the
|
|
235
|
+
# ledger; print the summary line. Exit 0 iff all fresh-green, else 1; parse/IO
|
|
236
|
+
# error -> exit 2 (writes nothing).
|
|
237
|
+
#
|
|
238
|
+
# When --step is given, the step is written `running` + started_at (+ activity)
|
|
239
|
+
# in a first atomic write BEFORE its check runs, then rewritten green|red after
|
|
240
|
+
# (two writes) so a watcher sees the in-flight state. --activity sets an optional
|
|
241
|
+
# short label; it only applies with --step.
|
|
242
|
+
# Seconds a single step's check may run before it is killed and marked red.
|
|
243
|
+
# A check that waits on something that never comes (a prompt, a lock, a network
|
|
244
|
+
# read) would otherwise hang the whole run with no way to tell from outside.
|
|
245
|
+
# 0 disables the bound.
|
|
246
|
+
PL_STEP_TIMEOUT="${PLAN_LEDGER_STEP_TIMEOUT:-1800}"
|
|
247
|
+
|
|
248
|
+
# pl_progress MESSAGE — a line to stderr, so stdout stays the summary contract.
|
|
249
|
+
# A full run can take minutes; silence for that long is indistinguishable from
|
|
250
|
+
# a hang, which is exactly how it was being read.
|
|
251
|
+
pl_progress() {
|
|
252
|
+
printf 'plan-ledger: %s\n' "$1" >&2
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
# pl_run_check CHECK OUTFILE -> exit code of CHECK.
|
|
256
|
+
#
|
|
257
|
+
# stdin is /dev/null: a check that reads stdin (a `git commit` without -m, any
|
|
258
|
+
# prompt) would otherwise inherit the caller's terminal and block forever.
|
|
259
|
+
# `timeout` bounds the rest; 124 is its kill signal and reads as red.
|
|
260
|
+
pl_run_check() {
|
|
261
|
+
local check="$1" outfile="$2"
|
|
262
|
+
if [ "${PL_STEP_TIMEOUT:-0}" != "0" ] && command -v timeout >/dev/null 2>&1; then
|
|
263
|
+
timeout "$PL_STEP_TIMEOUT" bash -c "$check" >"$outfile" 2>&1 </dev/null
|
|
264
|
+
return $?
|
|
265
|
+
fi
|
|
266
|
+
bash -c "$check" >"$outfile" 2>&1 </dev/null
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
pl_cmd_run() {
|
|
270
|
+
local doc="$1"; shift
|
|
271
|
+
local only_step="" activity="" force="" verify="" steps base cur ledger_file root
|
|
272
|
+
while [ "$#" -gt 0 ]; do
|
|
273
|
+
case "$1" in
|
|
274
|
+
--step)
|
|
275
|
+
only_step="${2:-}"
|
|
276
|
+
[ -n "$only_step" ] || { echo "plan-ledger: --step requires an id" >&2; return 2; }
|
|
277
|
+
shift 2
|
|
278
|
+
;;
|
|
279
|
+
--activity)
|
|
280
|
+
activity="${2:-}"
|
|
281
|
+
[ -n "$activity" ] || { echo "plan-ledger: --activity requires a label" >&2; return 2; }
|
|
282
|
+
shift 2
|
|
283
|
+
;;
|
|
284
|
+
--force)
|
|
285
|
+
force=1
|
|
286
|
+
shift
|
|
287
|
+
;;
|
|
288
|
+
--verify)
|
|
289
|
+
# Judge every step on the branch diff, ignoring per-step `paths`. This
|
|
290
|
+
# is the pre-push contract: a narrow or stale scope declaration must not
|
|
291
|
+
# be able to hold a green through a change it failed to account for.
|
|
292
|
+
verify=1
|
|
293
|
+
shift
|
|
294
|
+
;;
|
|
295
|
+
*)
|
|
296
|
+
echo "plan-ledger: unknown run flag: $1" >&2; return 2
|
|
297
|
+
;;
|
|
298
|
+
esac
|
|
299
|
+
done
|
|
300
|
+
[ -z "$activity" ] || [ -n "$only_step" ] || { echo "plan-ledger: --activity requires --step" >&2; return 2; }
|
|
301
|
+
|
|
302
|
+
# Parse first — on failure write NOTHING (crit8).
|
|
303
|
+
steps=$(pl_parse_steps "$doc") || return 2
|
|
304
|
+
|
|
305
|
+
base="${PUSH_REVIEW_BASE:-$(detect_base_branch)}"
|
|
306
|
+
root=$(detect_project_root)
|
|
307
|
+
[ -n "$root" ] || { echo "plan-ledger: not in a git repo" >&2; return 2; }
|
|
308
|
+
ledger_file=$(pl_ledger_path) || { echo "plan-ledger: cannot resolve ledger path" >&2; return 2; }
|
|
309
|
+
|
|
310
|
+
# cd to project root so checks run there (and diff_sha is repo-relative).
|
|
311
|
+
cd "$root" || { echo "plan-ledger: cannot cd to $root" >&2; return 2; }
|
|
312
|
+
|
|
313
|
+
cur=$(pl_diff_sha "$base") || { echo "plan-ledger: git diff ${base}...HEAD failed" >&2; return 2; }
|
|
314
|
+
|
|
315
|
+
local branch slug
|
|
316
|
+
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) || return 2
|
|
317
|
+
slug=$(branch_slug "$branch")
|
|
318
|
+
|
|
319
|
+
# Prior on-disk entries, indexed by id. Read for BOTH full and --step runs: a
|
|
320
|
+
# --step run preserves the other steps' entries verbatim, and EVERY run passes
|
|
321
|
+
# each step's prior entry to the serializer so a prior red is archived into
|
|
322
|
+
# retries[] (and prior retries carry forward). Captured once here from the
|
|
323
|
+
# ORIGINAL ledger, before any write below — so the --step running pre-write
|
|
324
|
+
# cannot overwrite the prior we archive from. Fail-closed: a present-but-corrupt
|
|
325
|
+
# ledger returns 2 (never silently dropped); an absent ledger is the empty map.
|
|
326
|
+
local existing="{}"
|
|
327
|
+
local prior
|
|
328
|
+
if prior=$(pl_read_ledger "$ledger_file" 2>/dev/null); then
|
|
329
|
+
existing=$(jq '[.steps[] | {key: .id, value: .}] | from_entries' <<< "$prior") \
|
|
330
|
+
|| { echo "plan-ledger: corrupt prior ledger at $ledger_file" >&2; return 2; }
|
|
331
|
+
elif [ -s "$ledger_file" ]; then
|
|
332
|
+
# File exists and is non-empty but pl_read_ledger refused it (unparseable
|
|
333
|
+
# json): corrupt, not absent. Fail closed rather than silently archiving
|
|
334
|
+
# nothing and clobbering it with a fresh run.
|
|
335
|
+
echo "plan-ledger: corrupt prior ledger at $ledger_file" >&2; return 2
|
|
336
|
+
fi
|
|
337
|
+
|
|
338
|
+
# Write #1 (only with --step): mark the target step `running` + started_at
|
|
339
|
+
# (+ activity) and persist BEFORE running its check, so a watcher sees the
|
|
340
|
+
# in-flight state. Other steps keep their prior entry (or seed pending). The
|
|
341
|
+
# green|red rewrite (write #2) happens after the check, below.
|
|
342
|
+
local started_at=""
|
|
343
|
+
if [ -n "$only_step" ]; then
|
|
344
|
+
started_at=$(pl_now)
|
|
345
|
+
local pre_steps
|
|
346
|
+
pre_steps=$(jq -n \
|
|
347
|
+
--argjson ex "$existing" --argjson steps "$steps" \
|
|
348
|
+
--arg only "$only_step" --arg now "$started_at" --arg act "$activity" '
|
|
349
|
+
[ $steps[] | .id as $id
|
|
350
|
+
| ($ex[$id]) as $p
|
|
351
|
+
| ($steps[] | select(.id==$id)) as $s
|
|
352
|
+
| if $id == $only
|
|
353
|
+
then { id: $s.id, title: $s.title, check: $s.check,
|
|
354
|
+
status: "running",
|
|
355
|
+
started_at: $now,
|
|
356
|
+
activity: (if $act == "" then null else $act end),
|
|
357
|
+
exit_code: null, diff_sha: null,
|
|
358
|
+
last_run: $now, evidence_tail: null,
|
|
359
|
+
ac_refs: ($s.ac_refs // []),
|
|
360
|
+
depends_on: ($s.depends_on // []),
|
|
361
|
+
input: ($s.input // null),
|
|
362
|
+
model: ($s.model // null),
|
|
363
|
+
retries: ($p.retries // []) }
|
|
364
|
+
elif $p != null
|
|
365
|
+
then $p
|
|
366
|
+
| .ac_refs = ($s.ac_refs // [])
|
|
367
|
+
| .depends_on = ($s.depends_on // [])
|
|
368
|
+
| .input = ($s.input // null)
|
|
369
|
+
| .model = ($s.model // null)
|
|
370
|
+
else { id: $s.id, title: $s.title, check: $s.check,
|
|
371
|
+
status: "pending", started_at: null, activity: null,
|
|
372
|
+
exit_code: null, diff_sha: null,
|
|
373
|
+
last_run: null, evidence_tail: null,
|
|
374
|
+
ac_refs: ($s.ac_refs // []),
|
|
375
|
+
depends_on: ($s.depends_on // []),
|
|
376
|
+
input: ($s.input // null),
|
|
377
|
+
model: ($s.model // null),
|
|
378
|
+
retries: [] }
|
|
379
|
+
end
|
|
380
|
+
]') || { echo "plan-ledger: failed to assemble running pre-write" >&2; return 2; }
|
|
381
|
+
local pre_ledger
|
|
382
|
+
pre_ledger=$(jq -n \
|
|
383
|
+
--arg branch "$branch" --arg base "$base" --arg doc "$doc" \
|
|
384
|
+
--arg now "$started_at" --argjson steps "$pre_steps" '
|
|
385
|
+
{ version: 1, branch: $branch, base_branch: $base, plan_doc: $doc,
|
|
386
|
+
updated_at: $now, summary: {}, next: null, steps: $steps }') \
|
|
387
|
+
|| { echo "plan-ledger: failed to assemble running pre-ledger" >&2; return 2; }
|
|
388
|
+
pre_ledger=$(pl_recompute "$pre_ledger" "$cur" "$scope_map" "$verify") \
|
|
389
|
+
|| { echo "plan-ledger: failed to recompute running pre-ledger" >&2; return 2; }
|
|
390
|
+
pl_write_ledger "$ledger_file" "$pre_ledger" \
|
|
391
|
+
|| { echo "plan-ledger: running pre-write failed" >&2; return 2; }
|
|
392
|
+
fi
|
|
393
|
+
|
|
394
|
+
# Build the steps array.
|
|
395
|
+
# Per-step scope hashes for this run. Empty for steps that declare no
|
|
396
|
+
# `paths`, which keeps them on the branch-wide rule they have always used.
|
|
397
|
+
local scope_map
|
|
398
|
+
scope_map=$(pl_scope_map "$steps" "$base") || scope_map='{}'
|
|
399
|
+
[ -n "$scope_map" ] || scope_map='{}'
|
|
400
|
+
|
|
401
|
+
local out_steps id check status code evidence tmpout new_entry
|
|
402
|
+
local _pl_index=0 _pl_total
|
|
403
|
+
_pl_total=$(jq 'length' <<< "$steps")
|
|
404
|
+
out_steps="[]"
|
|
405
|
+
while IFS= read -r id; do
|
|
406
|
+
[ -n "$id" ] || continue
|
|
407
|
+
check=$(jq -r --arg id "$id" '.[] | select(.id==$id) | .check' <<< "$steps") \
|
|
408
|
+
|| { echo "plan-ledger: failed to read check for step $id" >&2; return 2; }
|
|
409
|
+
if [ -n "$only_step" ] && [ "$id" != "$only_step" ]; then
|
|
410
|
+
# Carry the prior entry forward, but RE-DERIVE the authored fields
|
|
411
|
+
# (ac_refs/depends_on/input/model) from the current parsed step $s — they live in
|
|
412
|
+
# the plan doc and must reflect any edit since the last run (spec: authored
|
|
413
|
+
# fields are re-derived every run), while all engine state (status,
|
|
414
|
+
# exit_code, diff_sha, last_run, evidence_tail, retries) is preserved. No
|
|
415
|
+
# prior entry -> seed a pending step with the current authored fields.
|
|
416
|
+
new_entry=$(jq -n --argjson ex "$existing" --argjson steps "$steps" --arg id "$id" '
|
|
417
|
+
($ex[$id]) as $p
|
|
418
|
+
| ($steps[] | select(.id==$id)) as $s
|
|
419
|
+
| if $p != null
|
|
420
|
+
then $p
|
|
421
|
+
| .scope_sha = (.scope_sha // null)
|
|
422
|
+
| .ac_refs = ($s.ac_refs // [])
|
|
423
|
+
| .depends_on = ($s.depends_on // [])
|
|
424
|
+
| .input = ($s.input // null)
|
|
425
|
+
| .model = ($s.model // null)
|
|
426
|
+
| .title = $s.title
|
|
427
|
+
| .check = $s.check
|
|
428
|
+
| .status = (.status // "pending")
|
|
429
|
+
| .started_at = (.started_at // null)
|
|
430
|
+
| .activity = (.activity // null)
|
|
431
|
+
| .exit_code = (.exit_code // null)
|
|
432
|
+
| .diff_sha = (.diff_sha // null)
|
|
433
|
+
| .last_run = (.last_run // null)
|
|
434
|
+
| .evidence_tail = (.evidence_tail // null)
|
|
435
|
+
| .retries = (.retries // [])
|
|
436
|
+
else { id: $s.id, title: $s.title, check: $s.check,
|
|
437
|
+
status: "pending", started_at: null, activity: null,
|
|
438
|
+
exit_code: null, diff_sha: null,
|
|
439
|
+
last_run: null, evidence_tail: null,
|
|
440
|
+
ac_refs: ($s.ac_refs // []),
|
|
441
|
+
depends_on: ($s.depends_on // []),
|
|
442
|
+
input: ($s.input // null),
|
|
443
|
+
model: ($s.model // null),
|
|
444
|
+
retries: [] }
|
|
445
|
+
end
|
|
446
|
+
') || { echo "plan-ledger: failed to assemble entry for step $id" >&2; return 2; }
|
|
447
|
+
else
|
|
448
|
+
# Prior on-disk entry for this id (or "null"): the builder archives it into
|
|
449
|
+
# retries[] iff it was red, and carries its prior retries forward.
|
|
450
|
+
local prior_entry
|
|
451
|
+
prior_entry=$(jq -cn --argjson ex "$existing" --arg id "$id" '$ex[$id] // null') \
|
|
452
|
+
|| { echo "plan-ledger: failed to read prior entry for step $id" >&2; return 2; }
|
|
453
|
+
|
|
454
|
+
_pl_index=$((_pl_index + 1))
|
|
455
|
+
|
|
456
|
+
# A step already green AT THIS diff sha has nothing new to prove: re-running
|
|
457
|
+
# it burns the same minutes to reach the same answer. A full run after one
|
|
458
|
+
# edited step used to re-execute every check, which is what made "run the
|
|
459
|
+
# whole plan" feel like a hang. An explicit --step always runs (you asked
|
|
460
|
+
# for that one); --force re-runs everything.
|
|
461
|
+
# Fresh means "nothing this step depends on moved". For a step with
|
|
462
|
+
# `paths` that is its own scope hash; otherwise the branch hash, as before.
|
|
463
|
+
# `--verify` forces the branch hash for everything.
|
|
464
|
+
local _pl_scope_now _pl_prior_key _pl_now_key
|
|
465
|
+
_pl_scope_now=$(jq -r --arg id "$id" '.[$id] // ""' <<< "$scope_map")
|
|
466
|
+
if [ -z "$verify" ] && [ -n "$_pl_scope_now" ]; then
|
|
467
|
+
_pl_prior_key=$(jq -r '.scope_sha // ""' <<< "$prior_entry")
|
|
468
|
+
_pl_now_key="$_pl_scope_now"
|
|
469
|
+
else
|
|
470
|
+
_pl_prior_key=$(jq -r '.diff_sha // ""' <<< "$prior_entry")
|
|
471
|
+
_pl_now_key="$cur"
|
|
472
|
+
fi
|
|
473
|
+
|
|
474
|
+
if [ -z "$force" ] && [ -z "$only_step" ] \
|
|
475
|
+
&& [ "$(jq -r '.status // ""' <<< "$prior_entry")" = "green" ] \
|
|
476
|
+
&& [ -n "$_pl_now_key" ] && [ "$_pl_prior_key" = "$_pl_now_key" ]; then
|
|
477
|
+
pl_progress "[$_pl_index/$_pl_total] $id: fresh-green, skipped (--force re-runs)"
|
|
478
|
+
new_entry=$(jq -n --argjson p "$prior_entry" --argjson steps "$steps" --arg id "$id" '
|
|
479
|
+
($steps[] | select(.id==$id)) as $s
|
|
480
|
+
| $p
|
|
481
|
+
| .scope_sha = (.scope_sha // null)
|
|
482
|
+
| .ac_refs = ($s.ac_refs // [])
|
|
483
|
+
| .depends_on = ($s.depends_on // [])
|
|
484
|
+
| .input = ($s.input // null)
|
|
485
|
+
| .model = ($s.model // null)
|
|
486
|
+
| .title = $s.title
|
|
487
|
+
| .check = $s.check
|
|
488
|
+
| .status = (.status // "pending")
|
|
489
|
+
| .started_at = (.started_at // null)
|
|
490
|
+
| .activity = (.activity // null)
|
|
491
|
+
| .exit_code = (.exit_code // null)
|
|
492
|
+
| .diff_sha = (.diff_sha // null)
|
|
493
|
+
| .last_run = (.last_run // null)
|
|
494
|
+
| .evidence_tail = (.evidence_tail // null)
|
|
495
|
+
| .retries = (.retries // [])
|
|
496
|
+
') || { echo "plan-ledger: failed to carry forward step $id" >&2; return 2; }
|
|
497
|
+
out_steps=$(jq --argjson e "$new_entry" '. + [$e]' <<< "$out_steps") \
|
|
498
|
+
|| { echo "plan-ledger: failed to append step $id" >&2; return 2; }
|
|
499
|
+
continue
|
|
500
|
+
fi
|
|
501
|
+
|
|
502
|
+
tmpout="$ledger_file.run.$$.$id"
|
|
503
|
+
mkdir -p "$(dirname "$ledger_file")" 2>/dev/null || true
|
|
504
|
+
local _pl_t0 _pl_t1 _pl_duration
|
|
505
|
+
pl_progress "[$_pl_index/$_pl_total] $id: running check"
|
|
506
|
+
_pl_t0=$(date +%s)
|
|
507
|
+
pl_run_check "$check" "$tmpout"
|
|
508
|
+
code=$?
|
|
509
|
+
_pl_t1=$(date +%s)
|
|
510
|
+
_pl_duration=$((_pl_t1 - _pl_t0))
|
|
511
|
+
[ "$code" -eq 0 ] && status="green" || status="red"
|
|
512
|
+
evidence=$(pl_evidence "$(cat "$tmpout")")
|
|
513
|
+
if [ "$code" -eq 124 ]; then
|
|
514
|
+
# Name the timeout in the evidence, or the ledger just shows a red step
|
|
515
|
+
# with whatever partial output the check managed before it was killed.
|
|
516
|
+
evidence=$(pl_evidence "timed out after ${PL_STEP_TIMEOUT}s (PLAN_LEDGER_STEP_TIMEOUT)
|
|
517
|
+
$evidence")
|
|
518
|
+
fi
|
|
519
|
+
rm -f "$tmpout"
|
|
520
|
+
pl_progress "[$_pl_index/$_pl_total] $id: $status (${_pl_duration}s)"
|
|
521
|
+
new_entry=$(pl_build_step_entry "$steps" "$id" "$status" "$code" "$cur" "$evidence" "" "" "$prior_entry") \
|
|
522
|
+
|| { echo "plan-ledger: failed to build entry for step $id" >&2; return 2; }
|
|
523
|
+
# Record what the step's declared scope hashed to when it ran, so the next
|
|
524
|
+
# run can tell whether anything it depends on has moved since.
|
|
525
|
+
new_entry=$(jq -c --arg sha "$_pl_scope_now" \
|
|
526
|
+
'.scope_sha = (if $sha == "" then null else $sha end)' <<< "$new_entry") \
|
|
527
|
+
|| { echo "plan-ledger: failed to stamp scope for step $id" >&2; return 2; }
|
|
528
|
+
# step_run telemetry: this branch is the ONE code path both the full-run
|
|
529
|
+
# (every id lands here) and --step run (only the targeted id lands here)
|
|
530
|
+
# share, so instrumenting it covers both per the spec. attempt =
|
|
531
|
+
# archived-retry count + 1, read off the entry pl_build_step_entry just
|
|
532
|
+
# archived, so it reflects the SAME retry history just computed.
|
|
533
|
+
local _pl_attempt
|
|
534
|
+
_pl_attempt=$(jq -r '(.retries | length) + 1' <<< "$new_entry")
|
|
535
|
+
telemetry_append "$root" "step_run" \
|
|
536
|
+
"$(jq -cn --arg id "$id" --arg status "$status" --argjson code "$code" \
|
|
537
|
+
--argjson duration "$_pl_duration" --argjson attempt "$_pl_attempt" \
|
|
538
|
+
'{step_id: $id, status: $status, exit_code: $code, duration_s: $duration, attempt: $attempt}')"
|
|
539
|
+
fi
|
|
540
|
+
out_steps=$(jq --argjson e "$new_entry" '. + [$e]' <<< "$out_steps") \
|
|
541
|
+
|| { echo "plan-ledger: failed to append step $id" >&2; return 2; }
|
|
542
|
+
done < <(jq -r '.[].id' <<< "$steps")
|
|
543
|
+
|
|
544
|
+
# Assemble the full ledger, then recompute summary/next against current sha.
|
|
545
|
+
local ledger
|
|
546
|
+
ledger=$(jq -n \
|
|
547
|
+
--arg branch "$branch" \
|
|
548
|
+
--arg base "$base" \
|
|
549
|
+
--arg doc "$doc" \
|
|
550
|
+
--arg now "$(pl_now)" \
|
|
551
|
+
--argjson steps "$out_steps" '
|
|
552
|
+
{ version: 1, branch: $branch, base_branch: $base, plan_doc: $doc,
|
|
553
|
+
updated_at: $now,
|
|
554
|
+
summary: {}, next: null, steps: $steps }
|
|
555
|
+
') || { echo "plan-ledger: failed to assemble ledger" >&2; return 2; }
|
|
556
|
+
ledger=$(pl_recompute "$ledger" "$cur" "$scope_map" "$verify") \
|
|
557
|
+
|| { echo "plan-ledger: failed to recompute summary" >&2; return 2; }
|
|
558
|
+
|
|
559
|
+
# verified_sha is the branch hash at which a --verify run last found every
|
|
560
|
+
# step green. The push gate reads it rather than per-step freshness, so a
|
|
561
|
+
# scoped green can speed up iteration without ever standing in for the
|
|
562
|
+
# full check before a push.
|
|
563
|
+
local prior_verified
|
|
564
|
+
local prior_json="${prior:-}"
|
|
565
|
+
[ -n "$prior_json" ] || prior_json='{}'
|
|
566
|
+
prior_verified=$(jq -r '.verified_sha // ""' <<< "$prior_json" 2>/dev/null) || prior_verified=""
|
|
567
|
+
# Read freshness off the summary pl_recompute just wrote rather than
|
|
568
|
+
# restating the rule here. The two were equivalent, but only by an argument
|
|
569
|
+
# about what --verify does to the skip logic — and an argument is not a
|
|
570
|
+
# guarantee once someone edits is_fresh. summary.fresh_green is computed by
|
|
571
|
+
# that same definition, so the stamp cannot drift from it.
|
|
572
|
+
if [ -n "$verify" ] && [ -z "$only_step" ] \
|
|
573
|
+
&& [ "$(jq -r '.summary.fresh_green == .summary.total' <<< "$ledger")" = "true" ]; then
|
|
574
|
+
ledger=$(jq -c --arg sha "$cur" '.verified_sha = $sha' <<< "$ledger") || return 2
|
|
575
|
+
else
|
|
576
|
+
ledger=$(jq -c --arg sha "$prior_verified" \
|
|
577
|
+
'.verified_sha = (if $sha == "" then null else $sha end)' <<< "$ledger") || return 2
|
|
578
|
+
fi
|
|
579
|
+
|
|
580
|
+
pl_write_ledger "$ledger_file" "$ledger" || { echo "plan-ledger: ledger write failed" >&2; return 2; }
|
|
581
|
+
|
|
582
|
+
pl_summary_line "$ledger" "$slug"
|
|
583
|
+
pl_all_fresh "$ledger" && return 0 || return 1
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
# pl_ac_coverage_lines LEDGER_JSON CUR SPEC_DOC
|
|
587
|
+
# Print a human-readable AC-coverage report to stdout (REPORT-ONLY — the caller
|
|
588
|
+
# must never let it affect exit codes or the gate). For each AC id declared in
|
|
589
|
+
# SPEC_DOC (via pl_parse_acs), list the ledger steps whose ac_refs name it and
|
|
590
|
+
# whether at least one of those covering steps is fresh-green (status==green AND
|
|
591
|
+
# diff_sha==CUR); an AC with no fresh-green covering step is flagged UNCOVERED.
|
|
592
|
+
# Spec-less (SPEC_DOC empty / "none" / missing / no AC ids) -> print nothing and
|
|
593
|
+
# return 0: coverage is skipped, never a blocker. Never writes, never errors out.
|
|
594
|
+
pl_ac_coverage_lines() {
|
|
595
|
+
local ledger="$1" cur="$2" spec="$3" acs
|
|
596
|
+
case "$(printf '%s' "$spec" | tr '[:upper:]' '[:lower:]')" in
|
|
597
|
+
""|none) return 0 ;;
|
|
598
|
+
esac
|
|
599
|
+
acs=$(pl_parse_acs "$spec")
|
|
600
|
+
[ -n "$acs" ] || return 0
|
|
601
|
+
printf 'AC coverage (report-only):\n'
|
|
602
|
+
# For each AC id, jq finds covering steps and whether any is fresh-green.
|
|
603
|
+
local ac line
|
|
604
|
+
while IFS= read -r ac; do
|
|
605
|
+
[ -n "$ac" ] || continue
|
|
606
|
+
line=$(jq -rn --argjson l "$ledger" --arg cur "$cur" --arg ac "$ac" '
|
|
607
|
+
[ $l.steps[] | select((.ac_refs // []) | index($ac)) ] as $cov
|
|
608
|
+
| ($cov | map(.id)) as $ids
|
|
609
|
+
| ($cov | any(.status == "green" and .diff_sha == $cur)) as $fresh
|
|
610
|
+
| if ($ids | length) == 0
|
|
611
|
+
then " " + $ac + ": UNCOVERED (no step references it)"
|
|
612
|
+
elif $fresh
|
|
613
|
+
then " " + $ac + ": covered by " + ($ids | join(", "))
|
|
614
|
+
else " " + $ac + ": UNCOVERED (" + ($ids | join(", ")) + " not fresh-green)"
|
|
615
|
+
end
|
|
616
|
+
') || { echo "plan-ledger: failed to compute AC coverage for $ac" >&2; return 0; }
|
|
617
|
+
printf '%s\n' "$line"
|
|
618
|
+
done <<< "$acs"
|
|
619
|
+
return 0
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
# pl_cmd_status
|
|
623
|
+
# Read the current branch's ledger (absent -> exit 2), recompute summary/next vs
|
|
624
|
+
# the current diff_sha WITHOUT running checks, write the refreshed ledger, print
|
|
625
|
+
# the summary line, then the AC-coverage report (report-only). Exit 0 iff all
|
|
626
|
+
# fresh-green, else 1 — AC coverage NEVER changes the exit code.
|
|
627
|
+
pl_cmd_status() {
|
|
628
|
+
local base cur ledger_file ledger slug branch
|
|
629
|
+
base="${PUSH_REVIEW_BASE:-$(detect_base_branch)}"
|
|
630
|
+
ledger_file=$(pl_ledger_path) || { echo "plan-ledger: cannot resolve ledger path" >&2; return 2; }
|
|
631
|
+
ledger=$(pl_read_ledger "$ledger_file") || { echo "plan-ledger: no ledger at $ledger_file" >&2; return 2; }
|
|
632
|
+
cur=$(pl_diff_sha "$base") || { echo "plan-ledger: git diff ${base}...HEAD failed" >&2; return 2; }
|
|
633
|
+
|
|
634
|
+
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) || return 2
|
|
635
|
+
slug=$(branch_slug "$branch")
|
|
636
|
+
|
|
637
|
+
# Self-heal orphaned `running` steps (crash between the two run writes) back to
|
|
638
|
+
# pending before recomputing, so a stale running can't wedge status/push-gate.
|
|
639
|
+
ledger=$(pl_heal_orphans "$ledger") \
|
|
640
|
+
|| { echo "plan-ledger: failed to heal orphaned running steps" >&2; return 2; }
|
|
641
|
+
ledger=$(pl_recompute "$ledger" "$cur") \
|
|
642
|
+
|| { echo "plan-ledger: failed to recompute summary" >&2; return 2; }
|
|
643
|
+
pl_write_ledger "$ledger_file" "$ledger" || { echo "plan-ledger: ledger write failed" >&2; return 2; }
|
|
644
|
+
|
|
645
|
+
pl_summary_line "$ledger" "$slug"
|
|
646
|
+
|
|
647
|
+
# AC-coverage report (report-only): resolve the plan's **Spec:** doc relative to
|
|
648
|
+
# the project root, parse its AC ids, and report which are covered by a
|
|
649
|
+
# fresh-green step. REPORT-ONLY — coverage must never change the status exit
|
|
650
|
+
# code or wedge the gate (spec Non-Goal). A coverage failure is reported to
|
|
651
|
+
# stderr (not silently dropped), then status proceeds with its real exit code.
|
|
652
|
+
local plan_doc spec_field spec_path root
|
|
653
|
+
plan_doc=$(jq -r '.plan_doc // ""' <<< "$ledger") \
|
|
654
|
+
|| { echo "plan-ledger: could not read plan_doc for AC coverage (skipping report)" >&2; plan_doc=""; }
|
|
655
|
+
if [ -n "$plan_doc" ]; then
|
|
656
|
+
root=$(detect_project_root)
|
|
657
|
+
[ -f "$plan_doc" ] || { [ -n "$root" ] && [ -f "$root/$plan_doc" ] && plan_doc="$root/$plan_doc"; }
|
|
658
|
+
if [ -f "$plan_doc" ]; then
|
|
659
|
+
spec_field=$(pl_doc_field "$plan_doc" Spec)
|
|
660
|
+
spec_path="$spec_field"
|
|
661
|
+
case "$(printf '%s' "$spec_field" | tr '[:upper:]' '[:lower:]')" in
|
|
662
|
+
""|none) spec_path="$spec_field" ;;
|
|
663
|
+
*) [ -f "$spec_path" ] || { [ -n "$root" ] && [ -f "$root/$spec_field" ] && spec_path="$root/$spec_field"; } ;;
|
|
664
|
+
esac
|
|
665
|
+
# pl_ac_coverage_lines is itself report-only: it returns 0 even on an
|
|
666
|
+
# internal failure (after emitting a stderr diagnostic), so status's exit
|
|
667
|
+
# code below is decided solely by fresh-green state.
|
|
668
|
+
pl_ac_coverage_lines "$ledger" "$cur" "$spec_path"
|
|
669
|
+
fi
|
|
670
|
+
fi
|
|
671
|
+
|
|
672
|
+
pl_all_fresh "$ledger" && return 0 || return 1
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
# pl_self_test
|
|
676
|
+
# Parse a tiny inline fixture doc and assert pl_parse_steps yields the expected
|
|
677
|
+
# two-step array. Minimal but real. Exit 0/1.
|
|
678
|
+
pl_self_test() {
|
|
679
|
+
local dir doc out
|
|
680
|
+
dir=$(mktemp -d) || return 1
|
|
681
|
+
doc="$dir/selftest-plan.md"
|
|
682
|
+
cat > "$doc" <<'EOF'
|
|
683
|
+
# Self-test Plan
|
|
684
|
+
|
|
685
|
+
## Steps (machine-readable)
|
|
686
|
+
|
|
687
|
+
```json
|
|
688
|
+
[
|
|
689
|
+
{ "id": "s1", "title": "ok", "check": "true" },
|
|
690
|
+
{ "id": "s2", "title": "fail", "check": "false" }
|
|
691
|
+
]
|
|
692
|
+
```
|
|
693
|
+
EOF
|
|
694
|
+
if ! out=$(pl_parse_steps "$doc"); then
|
|
695
|
+
rm -rf "$dir"; echo "plan-ledger --self-test: parse failed" >&2; return 1
|
|
696
|
+
fi
|
|
697
|
+
rm -rf "$dir"
|
|
698
|
+
if [ "$(jq -r 'length' <<< "$out")" != "2" ] \
|
|
699
|
+
|| [ "$(jq -r '.[0].id' <<< "$out")" != "s1" ] \
|
|
700
|
+
|| [ "$(jq -r '.[1].check' <<< "$out")" != "false" ]; then
|
|
701
|
+
echo "plan-ledger --self-test: unexpected parse result" >&2; return 1
|
|
702
|
+
fi
|
|
703
|
+
echo "plan-ledger --self-test: ok"
|
|
704
|
+
return 0
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
# main "$@"
|
|
708
|
+
# CLI dispatch. Requires jq + git. Unknown command -> exit 2.
|
|
709
|
+
main() {
|
|
710
|
+
command -v jq >/dev/null 2>&1 || { echo "plan-ledger: jq is required" >&2; exit 2; }
|
|
711
|
+
command -v git >/dev/null 2>&1 || { echo "plan-ledger: git is required" >&2; exit 2; }
|
|
712
|
+
|
|
713
|
+
local cmd="${1:-}"; shift || true
|
|
714
|
+
case "$cmd" in
|
|
715
|
+
run)
|
|
716
|
+
[ -n "${1:-}" ] || { echo "plan-ledger: run requires a plan doc path" >&2; exit 2; }
|
|
717
|
+
pl_cmd_run "$@"; exit $?
|
|
718
|
+
;;
|
|
719
|
+
status)
|
|
720
|
+
pl_cmd_status; exit $?
|
|
721
|
+
;;
|
|
722
|
+
preflight)
|
|
723
|
+
pl_cmd_preflight "${1:-}"; exit $?
|
|
724
|
+
;;
|
|
725
|
+
path)
|
|
726
|
+
pl_ledger_path || { echo "plan-ledger: cannot resolve ledger path" >&2; exit 2; }
|
|
727
|
+
exit 0
|
|
728
|
+
;;
|
|
729
|
+
root)
|
|
730
|
+
local root
|
|
731
|
+
root=$(detect_project_root)
|
|
732
|
+
[ -n "$root" ] || { echo "plan-ledger: not in a git repo" >&2; exit 2; }
|
|
733
|
+
printf '%s\n' "$root"
|
|
734
|
+
exit 0
|
|
735
|
+
;;
|
|
736
|
+
--self-test)
|
|
737
|
+
pl_self_test; exit $?
|
|
738
|
+
;;
|
|
739
|
+
*)
|
|
740
|
+
echo "plan-ledger: usage: run <doc> [--step <id>] [--activity <label>] | status | preflight [<doc>] | path | root | --self-test" >&2
|
|
741
|
+
exit 2
|
|
742
|
+
;;
|
|
743
|
+
esac
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
# Guarded main: run only when executed directly, not when sourced.
|
|
747
|
+
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
|
|
748
|
+
main "$@"
|
|
749
|
+
fi
|