@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,98 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# normalize.sh — the jq vocabulary shared by collect-pr.sh and reduce-state.sh.
|
|
3
|
+
#
|
|
4
|
+
# Sourced. Every shape rule lives here so the collector, the reducer and the
|
|
5
|
+
# tests read one definition of "CI reviewer", "check state" and "thread
|
|
6
|
+
# comment". Programs are functions that print jq text (not variables) so a
|
|
7
|
+
# sourced-only lib stays shellcheck-clean without exporting large strings into
|
|
8
|
+
# every child process's environment.
|
|
9
|
+
|
|
10
|
+
# CI-reviewer login set: REST (`user.login`) keeps the `[bot]` suffix, GraphQL
|
|
11
|
+
# (`author.login`) drops it. Exact membership — never a `[bot]` substring test,
|
|
12
|
+
# which misreads the GraphQL form as human.
|
|
13
|
+
pb_ci_reviewers_json() { printf '%s' '["github-actions","github-actions[bot]","claude","claude[bot]"]'; }
|
|
14
|
+
|
|
15
|
+
# pb_jq_defs -> jq `def`s to prepend to any program.
|
|
16
|
+
pb_jq_defs() {
|
|
17
|
+
cat <<EOF
|
|
18
|
+
def ci_reviewers: $(pb_ci_reviewers_json);
|
|
19
|
+
def is_ci_reviewer: . as \$l | (ci_reviewers | index(\$l)) != null;
|
|
20
|
+
def check_state:
|
|
21
|
+
if .__typename == "StatusContext" then
|
|
22
|
+
(if .state == "SUCCESS" then "pass"
|
|
23
|
+
elif (.state == "PENDING" or .state == "EXPECTED") then "pending"
|
|
24
|
+
else "fail" end)
|
|
25
|
+
else
|
|
26
|
+
(if .status != "COMPLETED" then "pending"
|
|
27
|
+
elif (.conclusion == "SUCCESS" or .conclusion == "NEUTRAL" or .conclusion == "SKIPPED") then "pass"
|
|
28
|
+
else "fail" end)
|
|
29
|
+
end;
|
|
30
|
+
def check_name: (.name // .context // "unknown");
|
|
31
|
+
def check_url: (.detailsUrl // .targetUrl // null);
|
|
32
|
+
def ci_status:
|
|
33
|
+
(map(check_state)) as \$s
|
|
34
|
+
| if (\$s | length) == 0 then "pending"
|
|
35
|
+
elif (\$s | index("fail")) != null then "fail"
|
|
36
|
+
elif (\$s | index("pending")) != null then "pending"
|
|
37
|
+
else "pass" end;
|
|
38
|
+
def thread_comment: {id, databaseId, body, author: (.author.login // null), authorType: (.author.__typename // null), createdAt, url};
|
|
39
|
+
EOF
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Merge `gh api graphql --paginate --slurp` reviewThreads pages (array of page
|
|
43
|
+
# objects) into the snapshot thread shape.
|
|
44
|
+
pb_jq_threads_from_pages() {
|
|
45
|
+
pb_jq_defs
|
|
46
|
+
cat <<'EOF'
|
|
47
|
+
[.[].data.repository.pullRequest.reviewThreads.nodes[]]
|
|
48
|
+
| map({id, isResolved, isOutdated, path, line,
|
|
49
|
+
comments: (.comments.nodes | map(thread_comment)),
|
|
50
|
+
commentsHasNextPage: .comments.pageInfo.hasNextPage,
|
|
51
|
+
commentsEndCursor: .comments.pageInfo.endCursor})
|
|
52
|
+
EOF
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Merge the per-thread `node(id:)` comment pages (array of page objects).
|
|
56
|
+
pb_jq_thread_comments_from_pages() {
|
|
57
|
+
pb_jq_defs
|
|
58
|
+
printf '%s\n' '[.[].data.node.comments.nodes[]] | map(thread_comment)'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Flatten `gh api --paginate --slurp` REST pages (array of arrays).
|
|
62
|
+
pb_jq_rest_items() { printf '%s\n' '[.[][]]'; }
|
|
63
|
+
|
|
64
|
+
pb_jq_issue_comment() {
|
|
65
|
+
printf '%s\n' '{id, body, author: (.user.login // null), authorType: (.user.type // null), createdAt: .created_at, updatedAt: .updated_at, url: .html_url}'
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
pb_jq_review() {
|
|
69
|
+
printf '%s\n' '{id, state, body, author: (.user.login // null), authorType: (.user.type // null), submittedAt: .submitted_at, url: .html_url, commitId: .commit_id}'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Fields requested from `gh pr view --json`.
|
|
73
|
+
pb_pr_view_fields() {
|
|
74
|
+
printf '%s' 'number,title,url,author,state,baseRefName,headRefName,headRefOid,statusCheckRollup,mergeable,reviewDecision'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# GraphQL documents. Both declare $endCursor so `gh api graphql --paginate`
|
|
78
|
+
# drives the cursor; $pageSize keeps pagination testable on real PRs.
|
|
79
|
+
pb_gql_threads() {
|
|
80
|
+
cat <<'EOF'
|
|
81
|
+
query($owner:String!,$repo:String!,$number:Int!,$pageSize:Int!,$endCursor:String){
|
|
82
|
+
repository(owner:$owner,name:$repo){ pullRequest(number:$number){
|
|
83
|
+
reviewThreads(first:$pageSize,after:$endCursor){
|
|
84
|
+
pageInfo{hasNextPage endCursor}
|
|
85
|
+
nodes{ id isResolved isOutdated path line
|
|
86
|
+
comments(first:$pageSize){ pageInfo{hasNextPage endCursor}
|
|
87
|
+
nodes{ id databaseId body author{login __typename} createdAt url } } } } } } }
|
|
88
|
+
EOF
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
pb_gql_thread_comments() {
|
|
92
|
+
cat <<'EOF'
|
|
93
|
+
query($id:ID!,$pageSize:Int!,$endCursor:String){
|
|
94
|
+
node(id:$id){ ... on PullRequestReviewThread {
|
|
95
|
+
comments(first:$pageSize,after:$endCursor){ pageInfo{hasNextPage endCursor}
|
|
96
|
+
nodes{ id databaseId body author{login __typename} createdAt url } } } } }
|
|
97
|
+
EOF
|
|
98
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# state.sh — load and update one slot's state file for the write-side scripts.
|
|
3
|
+
#
|
|
4
|
+
# Sourced after common.sh and lock.sh. The write side (reply-thread.sh,
|
|
5
|
+
# resolve-thread.sh, record.sh) always: takes the slot lock, validates the
|
|
6
|
+
# state, performs its action, and records the outcome atomically. This file
|
|
7
|
+
# holds the shared load/update so each script carries only its own action.
|
|
8
|
+
|
|
9
|
+
# pb_state_load STATE_FILE — validate (version 2) and export the slot
|
|
10
|
+
# identity: PB_STATE_REPO, PB_STATE_NUMBER, PB_STATE_HEAD. Exits through
|
|
11
|
+
# pb_fail on a missing or malformed file.
|
|
12
|
+
pb_state_load() {
|
|
13
|
+
local state_file="$1"
|
|
14
|
+
[ -f "$state_file" ] || pb_fail state_malformed "state file not found: $state_file (run babysit-tick.sh first)" '{"source":"state"}'
|
|
15
|
+
pb_json_valid "$state_file" || pb_fail state_malformed "state file is not valid JSON: $state_file" '{"source":"state"}'
|
|
16
|
+
jq -e '.version == 2 and (.repo | type == "string") and (.number | type == "number")' "$state_file" >/dev/null 2>&1 \
|
|
17
|
+
|| pb_fail state_malformed "state file is not a version-2 pr-babysit state: $state_file" "$(jq -c '{source:"state", version:(.version // null)}' "$state_file")"
|
|
18
|
+
PB_STATE_REPO=$(jq -r '.repo' "$state_file")
|
|
19
|
+
PB_STATE_NUMBER=$(jq -r '.number' "$state_file")
|
|
20
|
+
# These are spliced into REST paths: refuse anything but owner/name and an
|
|
21
|
+
# integer, whatever a hand-edited state file says.
|
|
22
|
+
# GitHub owner/name: no leading dot, so `.`/`..` segments cannot form.
|
|
23
|
+
[[ "$PB_STATE_REPO" =~ ^[A-Za-z0-9][A-Za-z0-9_-]*/[A-Za-z0-9_][A-Za-z0-9._-]*$ ]] \
|
|
24
|
+
|| pb_fail state_malformed "state file repo is not owner/name: $PB_STATE_REPO" '{"source":"state"}'
|
|
25
|
+
[[ "$PB_STATE_NUMBER" =~ ^[0-9]+$ ]] \
|
|
26
|
+
|| pb_fail state_malformed "state file number is not an integer: $PB_STATE_NUMBER" '{"source":"state"}'
|
|
27
|
+
PB_STATE_HEAD=$(jq -r '.pr.headSha // ""' "$state_file")
|
|
28
|
+
export PB_STATE_REPO PB_STATE_NUMBER PB_STATE_HEAD
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# pb_state_update STATE_FILE FILTER [JQ_ARGS...] — apply a jq filter to the
|
|
32
|
+
# state and write it back atomically. The filter must yield the whole state.
|
|
33
|
+
pb_state_update() {
|
|
34
|
+
local state_file="$1" filter="$2"; shift 2
|
|
35
|
+
pb_atomic_write_json "$state_file" jq -c "$@" "$filter" "$state_file" \
|
|
36
|
+
|| pb_fail state_malformed "could not update $state_file" '{"source":"state"}'
|
|
37
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# parse-verdict.sh — turn the CI review bot's issue comment (stdin) into a
|
|
3
|
+
# deterministic JSON verdict the babysit loop acts on. Keeps the parsing OUT of
|
|
4
|
+
# the LLM prompt so behaviour is testable (scripts/__tests__/parse-verdict.bats
|
|
5
|
+
# against a captured real comment) and stable.
|
|
6
|
+
#
|
|
7
|
+
# stdin : raw comment body (markdown)
|
|
8
|
+
# stdout: { is_review_comment, state, complete, verdict, verdict_label, findings[], must_fix[] }
|
|
9
|
+
# state: in_progress | complete | unknown
|
|
10
|
+
# - unknown → no checkbox checklist found (cannot judge completeness; the
|
|
11
|
+
# caller degrades to GitHub check-conclusion behaviour)
|
|
12
|
+
# - in_progress → ≥1 unchecked `- [ ]` (review still running; do not act)
|
|
13
|
+
# - complete → ≥1 checkbox AND none unchecked
|
|
14
|
+
# findings[]: { path, line|null, severity, text, key } (key = path:line:sha1(text)[:8])
|
|
15
|
+
# must_fix[]: free-text lines from `### Top-N must-fix`
|
|
16
|
+
#
|
|
17
|
+
# state can also be "provider_error": the action posted a comment but the model
|
|
18
|
+
# never produced a usable review — "Review incomplete — provider error", every
|
|
19
|
+
# file marked unreviewed. That is NOT a verdict about the code, and the check
|
|
20
|
+
# still reports success, so a caller that reads only `verdict` sees a plain
|
|
21
|
+
# `changes` with no findings and cannot tell "reviewed, nothing actionable"
|
|
22
|
+
# from "never reviewed". complete is false for this state.
|
|
23
|
+
#
|
|
24
|
+
# must_fix is NOT a duplicate of findings. The bot populates the two sections
|
|
25
|
+
# independently, and they disagree in both directions: a review has reported
|
|
26
|
+
# `Findings (0)` while Top-N listed three actionable items, and an `approved`
|
|
27
|
+
# verdict has shipped with Top-N still populated. A caller that reads only
|
|
28
|
+
# findings[] therefore both misses work and over-trusts a pass. These are plain
|
|
29
|
+
# sentences, not `path:line` findings, so they carry no key and cannot be
|
|
30
|
+
# resolved as threads — surface them to a human rather than acting on them
|
|
31
|
+
# mechanically.
|
|
32
|
+
#
|
|
33
|
+
# Identification is by MARKER, robust to both header states the bot uses
|
|
34
|
+
# ("PR Review in Progress" → "Code Review —"): a CI job link, a "Code Review"
|
|
35
|
+
# header, or an "agent-merge" verdict label. No marker → is_review_comment:false.
|
|
36
|
+
set -o pipefail
|
|
37
|
+
|
|
38
|
+
command -v jq >/dev/null 2>&1 || { echo "parse-verdict.sh: jq required" >&2; exit 2; }
|
|
39
|
+
|
|
40
|
+
input=$(cat 2>/dev/null || true)
|
|
41
|
+
|
|
42
|
+
_empty() { jq -nc '{is_review_comment:false, state:"unknown", complete:false, verdict:"none", verdict_label:"", findings:[]}'; }
|
|
43
|
+
|
|
44
|
+
# Empty / unreadable stdin → not a review comment.
|
|
45
|
+
[ -n "${input//[[:space:]]/}" ] || { _empty; exit 0; }
|
|
46
|
+
|
|
47
|
+
# --- Identify: marker-based, both states ---
|
|
48
|
+
# Precondition: the babysit pre-filters comments to the CI Toolu Code Review
|
|
49
|
+
# poster (`github-actions[bot]`; legacy `claude[bot]` still accepted) before calling
|
|
50
|
+
# this; the markers below are anchored to the bot's own structures
|
|
51
|
+
# (review headings, the [View job] CI link, a backticked agent-merge label) so a
|
|
52
|
+
# stray "actions/runs/…" or "agent-merge-…" in arbitrary prose won't false-positive.
|
|
53
|
+
is_review=false
|
|
54
|
+
if printf '%s' "$input" | grep -qE '^### Code Review|^### PR Review in Progress|\[View job\]\([^)]*actions/runs/[0-9]+|`agent-merge-[a-z]|^`(merge-approved|request-changes)`$'; then
|
|
55
|
+
is_review=true
|
|
56
|
+
fi
|
|
57
|
+
if [ "$is_review" != true ]; then _empty; exit 0; fi
|
|
58
|
+
|
|
59
|
+
# --- Completeness: checkbox state only ---
|
|
60
|
+
unchecked=$(printf '%s\n' "$input" | grep -cE '^[[:space:]]*-[[:space:]]\[[[:space:]]\]' || true)
|
|
61
|
+
checked=$(printf '%s\n' "$input" | grep -cE '^[[:space:]]*-[[:space:]]\[[xX]\]' || true)
|
|
62
|
+
boxes=$((unchecked + checked))
|
|
63
|
+
if [ "$boxes" -eq 0 ]; then state="unknown"
|
|
64
|
+
elif [ "$unchecked" -gt 0 ]; then state="in_progress"
|
|
65
|
+
else state="complete"
|
|
66
|
+
fi
|
|
67
|
+
complete=false; [ "$state" = complete ] && complete=true
|
|
68
|
+
|
|
69
|
+
# --- Verdict --- the machine-readable label is AUTHORITATIVE.
|
|
70
|
+
# Don't infer from prose when a label exists: finding TEXT routinely discusses
|
|
71
|
+
# "Changes requested"/"approved" (e.g. a finding about verdict parsing itself),
|
|
72
|
+
# and a whole-body grep would misclassify an approved PR as "changes". Fall back
|
|
73
|
+
# to prose only when no label is present (changes-first there, as the safe bias).
|
|
74
|
+
#
|
|
75
|
+
# The label has two historical shapes and BOTH must parse:
|
|
76
|
+
# legacy (<=PR#31): `agent-merge-approved` / `agent-merge-blocked`
|
|
77
|
+
# current: `merge-approved` / `request-changes`
|
|
78
|
+
# Resolution order matters. The checklist line `- [x] Set verdict label (`X`)`
|
|
79
|
+
# is preferred because it is emitted before the findings section, so a finding
|
|
80
|
+
# that quotes a label verbatim cannot shadow it. Next the standalone trailing
|
|
81
|
+
# label chip (current shape emits only that). Only then a bare legacy scan.
|
|
82
|
+
verdict_label=$(printf '%s\n' "$input" \
|
|
83
|
+
| grep -oE 'Set verdict label \(`[^`]+`\)' | head -1 | grep -oE '`[^`]+`' | tr -d '`' || true)
|
|
84
|
+
if [ -z "$verdict_label" ]; then
|
|
85
|
+
verdict_label=$(printf '%s\n' "$input" \
|
|
86
|
+
| grep -oE '^`(agent-merge-[a-z-]+|merge-approved|request-changes)`$' | tail -1 | tr -d '`' || true)
|
|
87
|
+
fi
|
|
88
|
+
if [ -z "$verdict_label" ]; then
|
|
89
|
+
verdict_label=$(printf '%s' "$input" | grep -oE 'agent-merge-[a-z-]+' | head -1 || true)
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# `merge-approved` contains "approved"; `request-changes` contains "changes";
|
|
93
|
+
# `agent-merge-blocked` contains "blocked". Test approved first so the shared
|
|
94
|
+
# substring "merge" in either label can never decide the branch.
|
|
95
|
+
if [[ "$verdict_label" == *approved* ]]; then
|
|
96
|
+
verdict="approved"
|
|
97
|
+
elif [[ "$verdict_label" == *blocked* || "$verdict_label" == *changes* ]]; then
|
|
98
|
+
verdict="changes"
|
|
99
|
+
else
|
|
100
|
+
# No label. Prefer the single `**Verdict:**` summary line over a whole-body
|
|
101
|
+
# grep: the current bot bolds the KEY (`**Verdict:** ✅ Approved`), not the
|
|
102
|
+
# word, so the legacy `**Approved**` pattern never matches it.
|
|
103
|
+
vline=$(printf '%s\n' "$input" | grep -m1 -E '^\*\*Verdict:\*\*' || true)
|
|
104
|
+
if printf '%s' "$vline" | grep -qiE 'changes requested'; then verdict="changes"
|
|
105
|
+
elif printf '%s' "$vline" | grep -qiE 'approved'; then verdict="approved"
|
|
106
|
+
elif printf '%s' "$input" | grep -qiE '\*\*Changes requested\*\*|changes-requested'; then verdict="changes"
|
|
107
|
+
elif printf '%s' "$input" | grep -qiE '\*\*Approved\*\*'; then verdict="approved"
|
|
108
|
+
else verdict="none"
|
|
109
|
+
fi
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
# --- Provider error: the action ran, the model did not. Detected before the
|
|
113
|
+
# findings block because there is nothing to find — the review never happened,
|
|
114
|
+
# and reporting its absence as a normal `changes` verdict is what lets a
|
|
115
|
+
# transient failure look like a considered judgement.
|
|
116
|
+
verdict_line=$(printf '%s\n' "$input" | grep -m1 -E '^\*\*Verdict:\*\*' || true)
|
|
117
|
+
if printf '%s' "$verdict_line" | grep -qiE 'review incomplete|provider error'; then
|
|
118
|
+
state="provider_error"
|
|
119
|
+
complete=false
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
# --- Findings: only the `### Findings` … next `### ` block, lines of the form
|
|
123
|
+
# `path[:line]`: severity: text
|
|
124
|
+
_sha1() { (sha1sum 2>/dev/null || shasum 2>/dev/null || echo nohash) | cut -c1-8; }
|
|
125
|
+
# Tolerate a decorated header (`### Findings`, `### Findings (6)`) — exact-match
|
|
126
|
+
# would miss a count suffix and silently report zero findings.
|
|
127
|
+
findings_block=$(printf '%s\n' "$input" | awk '/^### Findings([[:space:]]|$)/{f=1;next} /^### /{f=0} f')
|
|
128
|
+
findings_json="[]"
|
|
129
|
+
while IFS= read -r line; do
|
|
130
|
+
[[ "$line" =~ ^\`([^\`]+)\`:\ (blocker|high|medium|low|nit):\ (.*)$ ]] || continue
|
|
131
|
+
raw_path="${BASH_REMATCH[1]}"; sev="${BASH_REMATCH[2]}"; text="${BASH_REMATCH[3]}"
|
|
132
|
+
if [[ "$raw_path" =~ ^(.+):([0-9]+)$ ]]; then path="${BASH_REMATCH[1]}"; ln="${BASH_REMATCH[2]}"; else path="$raw_path"; ln=""; fi
|
|
133
|
+
h=$(printf '%s' "$text" | _sha1)
|
|
134
|
+
key="${path}:${ln}:${h}"
|
|
135
|
+
obj=$(jq -nc --arg path "$path" --arg line "$ln" --arg severity "$sev" --arg text "$text" --arg key "$key" \
|
|
136
|
+
'{path:$path, line:(if $line=="" then null else ($line|tonumber) end), severity:$severity, text:$text, key:$key}') || continue
|
|
137
|
+
findings_json=$(jq -c --argjson o "$obj" '. + [$o]' <<<"$findings_json")
|
|
138
|
+
done <<< "$findings_block"
|
|
139
|
+
|
|
140
|
+
# --- Top-N must-fix: free-text lines until the next heading or <details>.
|
|
141
|
+
# Same decorated-header tolerance as Findings above; entries may be bare lines
|
|
142
|
+
# or markdown list items.
|
|
143
|
+
must_fix_block=$(printf '%s\n' "$input" | awk 'BEGIN{IGNORECASE=1} /^###[[:space:]]+Top-N[[:space:]]+must-fix([[:space:]]|$)/{f=1;next} /^### |^<details>/{f=0} f')
|
|
144
|
+
must_fix_json="[]"
|
|
145
|
+
while IFS= read -r line; do
|
|
146
|
+
line="${line#"${line%%[![:space:]]*}"}"
|
|
147
|
+
line="${line%"${line##*[![:space:]]}"}"
|
|
148
|
+
[ -n "$line" ] || continue
|
|
149
|
+
# Strip a leading list marker only — "- " or "1. ". A blanket "first word"
|
|
150
|
+
# strip ate the first word of every unmarked line ("Fix the silent
|
|
151
|
+
# swallowing..." became "the silent swallowing...").
|
|
152
|
+
line="${line#- }"
|
|
153
|
+
line="${line#\* }"
|
|
154
|
+
[[ "$line" =~ ^[0-9]+\.[[:space:]]+(.*)$ ]] && line="${BASH_REMATCH[1]}"
|
|
155
|
+
[ -n "$line" ] || continue
|
|
156
|
+
if ! next_json=$(jq -c --arg t "$line" '. + [$t]' <<<"$must_fix_json"); then
|
|
157
|
+
printf 'parse-verdict: could not record a Top-N line; dropping it: %s\n' "$line" >&2
|
|
158
|
+
continue
|
|
159
|
+
fi
|
|
160
|
+
must_fix_json="$next_json"
|
|
161
|
+
done <<< "$must_fix_block"
|
|
162
|
+
|
|
163
|
+
jq -nc \
|
|
164
|
+
--argjson is_review "$is_review" \
|
|
165
|
+
--arg state "$state" \
|
|
166
|
+
--argjson complete "$complete" \
|
|
167
|
+
--arg verdict "$verdict" \
|
|
168
|
+
--arg verdict_label "${verdict_label:-}" \
|
|
169
|
+
--argjson findings "$findings_json" \
|
|
170
|
+
--argjson must_fix "$must_fix_json" \
|
|
171
|
+
'{is_review_comment:$is_review, state:$state, complete:$complete, verdict:$verdict, verdict_label:$verdict_label, findings:$findings, must_fix:$must_fix}'
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# record.sh — hand the agent's decisions back to the reducer.
|
|
3
|
+
#
|
|
4
|
+
# Usage: record.sh flag-injection --state-file <path> --thread <graphqlId>
|
|
5
|
+
# record.sh round --state-file <path> --had-rejection true|false [--fix-pushed]
|
|
6
|
+
# record.sh status --state-file <path> --status complete|escalated|cancelled
|
|
7
|
+
#
|
|
8
|
+
# flag-injection the reducer drops the thread from actionable and from the
|
|
9
|
+
# resolution audit, and lists it under flaggedInjection.
|
|
10
|
+
# round end of Step 4, before the push: this round's finding keys
|
|
11
|
+
# become lastRoundFindingKeys, lastRoundHadRejection is set,
|
|
12
|
+
# and --fix-pushed bumps fixAttempts (cap 5). Recurrence and
|
|
13
|
+
# fix budgets therefore advance on real rounds, never polls.
|
|
14
|
+
# status the workflow's terminal transition; the reducer only ever
|
|
15
|
+
# recommends through `decision`.
|
|
16
|
+
# Every subcommand takes the slot lock, validates the state, writes atomically
|
|
17
|
+
# and prints {ok:true,...}. Exit: 0 · 2 usage · 3 state_malformed · 75 locked.
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
PB_SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
|
|
21
|
+
# shellcheck source=lib/common.sh
|
|
22
|
+
. "$PB_SCRIPT_DIR/lib/common.sh"
|
|
23
|
+
# shellcheck source=lib/lock.sh
|
|
24
|
+
. "$PB_SCRIPT_DIR/lib/lock.sh"
|
|
25
|
+
# shellcheck source=lib/state.sh
|
|
26
|
+
. "$PB_SCRIPT_DIR/lib/state.sh"
|
|
27
|
+
|
|
28
|
+
sub="${1:-}"; [ $# -gt 0 ] && shift
|
|
29
|
+
state_file=""; thread=""; had_rejection=""; fix_pushed=0; status=""
|
|
30
|
+
while [ $# -gt 0 ]; do
|
|
31
|
+
case "$1" in
|
|
32
|
+
--state-file) state_file="${2:-}"; shift 2 ;;
|
|
33
|
+
--thread) thread="${2:-}"; shift 2 ;;
|
|
34
|
+
--had-rejection) had_rejection="${2:-}"; shift 2 ;;
|
|
35
|
+
--fix-pushed) fix_pushed=1; shift ;;
|
|
36
|
+
--status) status="${2:-}"; shift 2 ;;
|
|
37
|
+
*) pb_fail usage "record.sh: unknown argument: $1" ;;
|
|
38
|
+
esac
|
|
39
|
+
done
|
|
40
|
+
[ -n "$state_file" ] || pb_fail usage "record.sh: --state-file required"
|
|
41
|
+
case "$sub" in
|
|
42
|
+
flag-injection) [ -n "$thread" ] || pb_fail usage "record.sh flag-injection: --thread <graphqlId> required" ;;
|
|
43
|
+
round) [[ "$had_rejection" =~ ^(true|false)$ ]] || pb_fail usage "record.sh round: --had-rejection true|false required" ;;
|
|
44
|
+
status) [[ "$status" =~ ^(complete|escalated|cancelled)$ ]] || pb_fail usage "record.sh status: --status complete|escalated|cancelled required" ;;
|
|
45
|
+
*) pb_fail usage "record.sh: subcommand must be flag-injection, round or status" ;;
|
|
46
|
+
esac
|
|
47
|
+
|
|
48
|
+
pb_require jq
|
|
49
|
+
pb_init
|
|
50
|
+
pb_lock_acquire "$state_file" || pb_lock_fail "$state_file"
|
|
51
|
+
pb_state_load "$state_file"
|
|
52
|
+
now=$(pb_now)
|
|
53
|
+
|
|
54
|
+
case "$sub" in
|
|
55
|
+
flag-injection)
|
|
56
|
+
pb_state_update "$state_file" '.actions.flagged[$t] = {reason:"injection", at:$now}' --arg t "$thread" --arg now "$now"
|
|
57
|
+
jq -nc --arg t "$thread" --arg now "$now" '{ok:true, recorded:"flag-injection", thread:$t, at:$now}' ;;
|
|
58
|
+
round)
|
|
59
|
+
pb_state_update "$state_file" \
|
|
60
|
+
'.pr.lastRoundFindingKeys = (.pr.botFindingKeys // [])
|
|
61
|
+
| .pr.lastRoundHadRejection = $rej
|
|
62
|
+
| .pr.fixAttempts = (if $fix then ([(.pr.fixAttempts // 0) + 1, 5] | min) else (.pr.fixAttempts // 0) end)
|
|
63
|
+
| .lastRound = {at:$now, hadRejection:$rej, fixPushed:$fix, headSha:(.pr.headSha // null)}' \
|
|
64
|
+
--argjson rej "$had_rejection" --argjson fix "$([ "$fix_pushed" -eq 1 ] && echo true || echo false)" --arg now "$now"
|
|
65
|
+
jq -c '{ok:true, recorded:"round", lastRoundFindingKeys:.pr.lastRoundFindingKeys, lastRoundHadRejection:.pr.lastRoundHadRejection, fixAttempts:.pr.fixAttempts}' "$state_file" ;;
|
|
66
|
+
status)
|
|
67
|
+
pb_state_update "$state_file" '.status = $s | .statusChangedAt = $now' --arg s "$status" --arg now "$now"
|
|
68
|
+
jq -nc --arg s "$status" --arg now "$now" '{ok:true, recorded:"status", status:$s, at:$now}' ;;
|
|
69
|
+
esac
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# reduce-state.sh — the pure decision layer of the pr-babysit helper.
|
|
3
|
+
#
|
|
4
|
+
# Reads one snapshot (collect-pr.sh output) and the previous slot state, and
|
|
5
|
+
# emits the next state plus the compact agent-facing result. No network, no
|
|
6
|
+
# clock (--now is an input), no side effects beyond the two output files, so
|
|
7
|
+
# every decision rule below is testable against captured real snapshots.
|
|
8
|
+
#
|
|
9
|
+
# Usage: reduce-state.sh --snapshot <path> --state <path> --now <iso8601>
|
|
10
|
+
# [--state-out <path>] [--result-out <path>]
|
|
11
|
+
# [--state-path <path>] [--snapshot-path <path>]
|
|
12
|
+
# --state previous state file; absent file = first tick
|
|
13
|
+
# --state-out where to write the next state (skipped when omitted)
|
|
14
|
+
# --result-out where to write the result (stdout when omitted)
|
|
15
|
+
# --state-path / --snapshot-path the paths reported inside the result
|
|
16
|
+
#
|
|
17
|
+
# The rules encoded here are the workflow's (workflows/babysit.md): the Step 1
|
|
18
|
+
# actionable filter and resolution audit, the Step 4 recurrence gate and the
|
|
19
|
+
# Step 6 stop conditions. Reason and error codes are closed sets — see the spec
|
|
20
|
+
# docs/toolu/specs/2026-09-19-pr-babysit-helper-design.md.
|
|
21
|
+
# Exit: 0 result produced · 2 usage · 3 state_malformed | slot_mismatch | invalid_json.
|
|
22
|
+
set -euo pipefail
|
|
23
|
+
|
|
24
|
+
PB_SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
|
|
25
|
+
# shellcheck source=lib/common.sh
|
|
26
|
+
. "$PB_SCRIPT_DIR/lib/common.sh"
|
|
27
|
+
# shellcheck source=lib/normalize.sh
|
|
28
|
+
. "$PB_SCRIPT_DIR/lib/normalize.sh"
|
|
29
|
+
|
|
30
|
+
snapshot=""; state=""; now=""; state_out=""; result_out=""; state_path=""; snapshot_path=""
|
|
31
|
+
while [ $# -gt 0 ]; do
|
|
32
|
+
case "$1" in
|
|
33
|
+
--snapshot) snapshot="${2:-}"; shift 2 ;;
|
|
34
|
+
--state) state="${2:-}"; shift 2 ;;
|
|
35
|
+
--now) now="${2:-}"; shift 2 ;;
|
|
36
|
+
--state-out) state_out="${2:-}"; shift 2 ;;
|
|
37
|
+
--result-out) result_out="${2:-}"; shift 2 ;;
|
|
38
|
+
--state-path) state_path="${2:-}"; shift 2 ;;
|
|
39
|
+
--snapshot-path) snapshot_path="${2:-}"; shift 2 ;;
|
|
40
|
+
*) pb_fail usage "reduce-state.sh: unknown argument: $1" ;;
|
|
41
|
+
esac
|
|
42
|
+
done
|
|
43
|
+
[ -n "$snapshot" ] || pb_fail usage "reduce-state.sh: --snapshot <path> required"
|
|
44
|
+
[ -n "$state" ] || pb_fail usage "reduce-state.sh: --state <path> required"
|
|
45
|
+
[[ "$now" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]] || pb_fail usage "reduce-state.sh: --now must be an ISO-8601 UTC timestamp (YYYY-MM-DDTHH:MM:SSZ)"
|
|
46
|
+
[ -n "$state_path" ] || state_path="$state"
|
|
47
|
+
[ -n "$snapshot_path" ] || snapshot_path="$snapshot"
|
|
48
|
+
pb_require jq
|
|
49
|
+
|
|
50
|
+
# --- validate inputs (fail closed, never write) ------------------------------
|
|
51
|
+
[ -f "$snapshot" ] || pb_fail invalid_json "reduce-state.sh: snapshot not found: $snapshot" '{"source":"snapshot"}'
|
|
52
|
+
pb_json_valid "$snapshot" || pb_fail invalid_json "reduce-state.sh: snapshot is not valid JSON: $snapshot" '{"source":"snapshot"}'
|
|
53
|
+
jq -e '.version == 1 and (.repo | type == "string") and (.number | type == "number") and (.head.sha | type == "string") and (.pr | type == "object") and (.threads | type == "array")' "$snapshot" >/dev/null 2>&1 \
|
|
54
|
+
|| pb_fail invalid_json "reduce-state.sh: snapshot is not a version-1 pr-babysit snapshot" '{"source":"snapshot"}'
|
|
55
|
+
|
|
56
|
+
prev_json="null"
|
|
57
|
+
if [ -e "$state" ]; then
|
|
58
|
+
pb_json_valid "$state" || pb_fail state_malformed "reduce-state.sh: state file is not valid JSON: $state" '{"source":"state"}'
|
|
59
|
+
jq -e '.version == 2' "$state" >/dev/null 2>&1 \
|
|
60
|
+
|| pb_fail state_malformed "reduce-state.sh: state file is not version 2: $state" "$(jq -c '{source:"state", version: (.version // null)}' "$state")"
|
|
61
|
+
if ! jq -e --slurpfile s "$snapshot" '.repo == $s[0].repo and .number == $s[0].number' "$state" >/dev/null 2>&1; then
|
|
62
|
+
pb_fail slot_mismatch "reduce-state.sh: state belongs to $(jq -r '"\(.repo)#\(.number)"' "$state"), snapshot is $(jq -r '"\(.repo)#\(.number)"' "$snapshot")" \
|
|
63
|
+
"$(jq -c --slurpfile s "$snapshot" '{source:"state", state:{repo, number}, snapshot:{repo:$s[0].repo, number:$s[0].number}}' "$state")"
|
|
64
|
+
fi
|
|
65
|
+
prev_json=$(cat "$state")
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# --- the reducer -------------------------------------------------------------
|
|
69
|
+
pb_jq_reduce() {
|
|
70
|
+
pb_jq_defs
|
|
71
|
+
cat <<'EOF'
|
|
72
|
+
# ---- injection heuristics (advisory only): the agent keeps the decision.
|
|
73
|
+
def injection_pattern:
|
|
74
|
+
. as $b
|
|
75
|
+
| [ "ignore (all |any |the )?(previous|prior|above|earlier) (instructions|prompts?|rules)",
|
|
76
|
+
"disregard (all |any |the |your )?(previous|prior|system|above) ",
|
|
77
|
+
"you are (now )?(an? )?(ai|assistant|llm|language model|claude|codex|copilot)",
|
|
78
|
+
"(^|\\n)\\s*(system|assistant)\\s*:",
|
|
79
|
+
"<(system|instructions?)>",
|
|
80
|
+
"(run|execute) (the following|this|these) (command|shell|script)" ]
|
|
81
|
+
| map(. as $p | select($b | test($p; "i"))) | first // null;
|
|
82
|
+
# author_class: ci_reviewer (by exact login) | bot (GraphQL Bot / REST type) | human.
|
|
83
|
+
def author_class: if (.author | is_ci_reviewer) then "ci_reviewer" elif .authorType == "Bot" then "bot" else "human" end;
|
|
84
|
+
# ---- inputs
|
|
85
|
+
$snap as $snap | $prev as $prev
|
|
86
|
+
| ($snap.pr) as $pr
|
|
87
|
+
| ($snap.head.sha) as $head
|
|
88
|
+
| ($pr.author) as $author
|
|
89
|
+
| (($snap.repo | ascii_downcase | gsub("/"; "-")) + "-" + ($snap.number | tostring)) as $slot
|
|
90
|
+
| ($snap.repo + "#" + ($snap.number | tostring)) as $key
|
|
91
|
+
| (if $prev == null then {replied:{}, resolved:{}, flagged:{}} else ($prev.actions // {replied:{}, resolved:{}, flagged:{}}) end) as $actions
|
|
92
|
+
| ($actions.flagged | keys) as $flagged
|
|
93
|
+
| ($actions.replied | keys) as $repliedKeys
|
|
94
|
+
# ---- CI
|
|
95
|
+
| ($pr.statusCheckRollup // []) as $rollup
|
|
96
|
+
| ($rollup | ci_status) as $ciStatus
|
|
97
|
+
| ($rollup | map({name: check_name, status: check_state, url: check_url})) as $checks
|
|
98
|
+
# ---- verdict
|
|
99
|
+
| ($snap.bot.verdict // {}) as $v
|
|
100
|
+
| ($snap.bot.comment) as $botComment
|
|
101
|
+
| ($v.state // "absent") as $botState
|
|
102
|
+
| ($v.verdict // "none") as $botVerdict
|
|
103
|
+
| (($v.findings // []) | map(.key)) as $keys
|
|
104
|
+
| (($v.findings // []) | length) as $findingsCount
|
|
105
|
+
| ($botState == "absent" or $botState == "unknown" or ($v.is_review_comment == false)) as $degraded
|
|
106
|
+
| (if $botState == "absent" then "review_absent" elif $degraded then "review_unknown_format" else null end) as $degradedReason
|
|
107
|
+
| ($prev != null and $botComment != null and ($prev.pr.botCommentId // null) == $botComment.id and ($prev.pr.botCommentUpdatedAt // null) == $botComment.updatedAt) as $sameRun
|
|
108
|
+
# ---- thread classification
|
|
109
|
+
| ($snap.threads | map(
|
|
110
|
+
. as $t
|
|
111
|
+
| ($t.comments | last) as $lastComment
|
|
112
|
+
| ($t.comments | map(select(.author != $author)) | last) as $lastNonAuthor
|
|
113
|
+
| ($flagged | index($t.id) != null) as $isFlagged
|
|
114
|
+
| (if $lastNonAuthor == null then "none" else ($lastNonAuthor | author_class) end) as $class
|
|
115
|
+
| ($t.isResolved == false) as $open
|
|
116
|
+
| ($open and ($isFlagged | not) and $lastComment != null and $lastComment.author != $author and ($class == "human" or $class == "ci_reviewer")) as $answerable
|
|
117
|
+
| (if $t.isOutdated then ($answerable and $class == "human") else $answerable end) as $actionable
|
|
118
|
+
| ($open and ($t.isOutdated | not) and ($isFlagged | not)) as $audited
|
|
119
|
+
| { id: $t.id, path: $t.path, line: $t.line, isOutdated: $t.isOutdated, isResolved: $t.isResolved,
|
|
120
|
+
rootCommentId: ($t.comments | first | .databaseId // null),
|
|
121
|
+
inReplyTo: ($lastNonAuthor.databaseId // null),
|
|
122
|
+
authorClass: $class,
|
|
123
|
+
lastCommentAuthor: ($lastComment.author // null),
|
|
124
|
+
lastCommentAt: ($lastComment.createdAt // null),
|
|
125
|
+
injectionSuspect: ((($lastNonAuthor.body // "") | injection_pattern) != null),
|
|
126
|
+
injectionPattern: (($lastNonAuthor.body // "") | injection_pattern),
|
|
127
|
+
comments: $t.comments,
|
|
128
|
+
flags: {actionable: $actionable, audited: $audited, flagged: $isFlagged,
|
|
129
|
+
skippedOutdated: ($open and $t.isOutdated and $class == "ci_reviewer"),
|
|
130
|
+
replied: ($repliedKeys | index("thread:" + $t.id + "@" + (($lastNonAuthor.databaseId // 0) | tostring)) != null)} }
|
|
131
|
+
)) as $threads
|
|
132
|
+
| ($threads | map(select(.flags.actionable)) | map(del(.flags, .isResolved))) as $actionable
|
|
133
|
+
| ($threads | map(select(.flags.audited))) as $audited
|
|
134
|
+
| ($audited | map(select(.flags.actionable | not)) | map({id, path, line, repliedAt: .lastCommentAt, lastCommentAuthor})) as $staleUnresolved
|
|
135
|
+
| ($threads | map(select(.flags.skippedOutdated)) | map(.id)) as $skippedOutdated
|
|
136
|
+
| ($threads | map(select(.flags.flagged)) | map(.id)) as $flaggedInjection
|
|
137
|
+
| ($audited | length) as $unresolved
|
|
138
|
+
# ---- conversation and review-level comments
|
|
139
|
+
| (($snap.comments // []) | map(select(.author != $author and .authorType != "Bot"))
|
|
140
|
+
| map(. as $c | select(($snap.comments | map(select(.author == $author and .createdAt > $c.createdAt)) | length) == 0))
|
|
141
|
+
| map(. as $c | select(($repliedKeys | index("conversation:" + ($c.id | tostring))) == null))
|
|
142
|
+
| map({id, author, body, createdAt, url})) as $convActionable
|
|
143
|
+
| (($snap.reviews // []) | map(select(.author != $author and .authorType != "Bot" and .state != "APPROVED" and ((.body // "") | length) > 0))
|
|
144
|
+
| map(. as $r | select(($repliedKeys | index("review:" + ($r.id | tostring))) == null))
|
|
145
|
+
| map({id, author, state, body, submittedAt, url})) as $reviewActionable
|
|
146
|
+
# ---- recurrence (Step 4 gate) — only a NEW verdict run can recur
|
|
147
|
+
| (if $prev == null then [] else ($prev.pr.lastRoundFindingKeys // []) end) as $lastRoundKeys
|
|
148
|
+
| (if $prev == null then false else ($prev.pr.lastRoundHadRejection // false) end) as $lastRoundHadRejection
|
|
149
|
+
| (if $prev == null then 0 else ($prev.pr.recurrenceStreak // 0) end) as $prevStreak
|
|
150
|
+
| (if $prev == null then 0 else ($prev.pr.fixAttempts // 0) end) as $fixAttempts
|
|
151
|
+
| (if $sameRun or $prev == null then [] else ($keys | map(select(. as $k | $lastRoundKeys | index($k) != null))) end) as $recurringKeys
|
|
152
|
+
| (if $sameRun then $prevStreak elif ($recurringKeys | length) > 0 then ($prevStreak + 1) else 0 end) as $streak
|
|
153
|
+
# ---- change detection + backoff
|
|
154
|
+
| ({ciStatus: $ciStatus, reviewDecision: $pr.reviewDecision, mergeable: $pr.mergeable, unresolvedThreads: $unresolved,
|
|
155
|
+
headSha: $head, botVerdict: $botVerdict, botState: $botState, botFindingKeys: $keys}) as $cmp
|
|
156
|
+
| ($prev == null or ($prev.pr | {ciStatus, reviewDecision, mergeable, unresolvedThreads, headSha, botVerdict, botState, botFindingKeys}) != $cmp) as $changed
|
|
157
|
+
| (if $changed then 0 else (($prev.idleStreak // 0) + 1) end) as $idleStreak
|
|
158
|
+
| (if $idleStreak >= 9 then 15 elif $idleStreak >= 6 then 12 elif $idleStreak >= 3 then 6 elif $ciStatus == "fail" then 1 else 3 end) as $intervalMinutes
|
|
159
|
+
| (if $idleStreak >= 6 then 60 elif $idleStreak >= 3 then 30 else 15 end) as $waitSeconds
|
|
160
|
+
# ---- decision (Step 6)
|
|
161
|
+
| ($prev != null and $botState == "provider_error" and ($prev.pr.botState // "") == "provider_error" and ($prev.pr.headSha // "") == $head) as $providerErrorRepeated
|
|
162
|
+
| ([ (if $pr.state == "MERGED" then {code:"pr_merged", detail:"PR is merged"} else empty end),
|
|
163
|
+
(if $pr.state == "CLOSED" then {code:"pr_closed", detail:"PR is closed"} else empty end),
|
|
164
|
+
(if $pr.mergeable == "CONFLICTING" then {code:"merge_conflict", detail:"mergeable is CONFLICTING"} else empty end),
|
|
165
|
+
(if $fixAttempts >= 5 then {code:"fix_attempts_exhausted", detail:"\($fixAttempts) fix attempts recorded"} else empty end),
|
|
166
|
+
(if ($recurringKeys | length) > 0 and $lastRoundHadRejection then {code:"recurrence_after_rejection", detail:"\($recurringKeys | length) finding key(s) recurred after a Won't-fix round"} else empty end),
|
|
167
|
+
(if ($recurringKeys | length) > 0 and $streak >= 2 then {code:"recurrence_streak", detail:"finding keys recurred on \($streak) consecutive rounds"} else empty end),
|
|
168
|
+
(if $providerErrorRepeated then {code:"provider_error_repeated", detail:"review provider error twice on head \($head[0:8])"} else empty end)
|
|
169
|
+
]) as $escalations
|
|
170
|
+
| ([ (if $ciStatus == "pass" then {code:"ci_pass", detail:"\($checks | length) check(s) passed"}
|
|
171
|
+
elif $ciStatus == "fail" then {code:"ci_failed", detail:($checks | map(select(.status == "fail")) | map(.name) | join(", "))}
|
|
172
|
+
else {code:"ci_pending", detail:(if ($checks | length) == 0 then "no checks reported yet" else ($checks | map(select(.status == "pending")) | map(.name) | join(", ")) end)} end),
|
|
173
|
+
(if $unresolved == 0 then {code:"threads_clear", detail:"no unresolved review threads"} else empty end),
|
|
174
|
+
(if ($actionable | length) > 0 then {code:"threads_unresolved", detail:"\($actionable | length) actionable thread(s)"} else empty end),
|
|
175
|
+
(if ($staleUnresolved | length) > 0 then {code:"threads_stale_unresolved", detail:"\($staleUnresolved | length) replied-but-unresolved thread(s)"} else empty end),
|
|
176
|
+
(if $botState == "in_progress" then {code:"review_in_progress", detail:"review bot still running"}
|
|
177
|
+
elif $botState == "provider_error" and ($providerErrorRepeated | not) then {code:"provider_error", detail:"review bot reported a provider error; rerun the review job once"}
|
|
178
|
+
elif $botState == "complete" and $botVerdict == "approved" and $findingsCount == 0 then {code:"review_approved", detail:"bot verdict approved with zero findings"}
|
|
179
|
+
elif $botState == "complete" then {code:"review_changes", detail:"bot verdict \($botVerdict) with \($findingsCount) finding(s)"}
|
|
180
|
+
elif $degraded then {code:$degradedReason, detail:"bot verdict cannot be read"} else empty end),
|
|
181
|
+
(if $degraded then {code:"manual_verify", detail:"verify review findings manually: \($botComment.url // "no bot comment")"} else empty end),
|
|
182
|
+
(if $pr.mergeable == "UNKNOWN" and $pr.state == "OPEN" then {code:"mergeable_unknown", detail:"GitHub has not computed mergeability yet"} else empty end),
|
|
183
|
+
(if $changed then empty else {code:"unchanged", detail:"nothing changed since the last tick"} end)
|
|
184
|
+
]) as $signals
|
|
185
|
+
| (($escalations | length) > 0) as $escalate
|
|
186
|
+
| ($pr.state == "OPEN" and $ciStatus == "pass" and $unresolved == 0 and $pr.mergeable != "UNKNOWN"
|
|
187
|
+
and ( ($botState == "complete" and $botVerdict == "approved" and $findingsCount == 0) or $degraded )) as $successReady
|
|
188
|
+
| (if $escalate then "escalate" elif $successReady then "success" else "keep_going" end) as $decision
|
|
189
|
+
| ($escalations + $signals) as $reasons
|
|
190
|
+
# ---- outputs
|
|
191
|
+
| {
|
|
192
|
+
state: {
|
|
193
|
+
version: 2, slot: $slot, repo: $snap.repo, number: $snap.number,
|
|
194
|
+
cronName: ("pr-babysit:" + $slot),
|
|
195
|
+
lastUpdate: $now,
|
|
196
|
+
totalTicks: ((if $prev == null then 0 else ($prev.totalTicks // 0) end) + 1),
|
|
197
|
+
idleStreak: $idleStreak, currentInterval: $intervalMinutes, waitSeconds: $waitSeconds,
|
|
198
|
+
status: (if $prev == null then "active" else ($prev.status // "active") end),
|
|
199
|
+
worktree: (if $prev == null then null else ($prev.worktree // null) end),
|
|
200
|
+
pr: {
|
|
201
|
+
key: $key, ciStatus: $ciStatus, reviewDecision: $pr.reviewDecision, mergeable: $pr.mergeable,
|
|
202
|
+
unresolvedThreads: $unresolved, headSha: $head, fixAttempts: $fixAttempts,
|
|
203
|
+
botVerdict: $botVerdict, botState: $botState,
|
|
204
|
+
botCommentId: ($botComment.id // null), botCommentUpdatedAt: ($botComment.updatedAt // null),
|
|
205
|
+
botFindingKeys: $keys, lastRoundFindingKeys: $lastRoundKeys, lastRoundHadRejection: $lastRoundHadRejection,
|
|
206
|
+
recurrenceStreak: $streak, unresolvedAfterClearance: $unresolved, lastError: null
|
|
207
|
+
},
|
|
208
|
+
actions: $actions,
|
|
209
|
+
lastGoodSnapshot: $snapshotPath
|
|
210
|
+
},
|
|
211
|
+
result: {
|
|
212
|
+
version: 1, slot: $slot, changed: $changed, decision: $decision, reasons: $reasons,
|
|
213
|
+
pr: {number: $pr.number, url: $pr.url, head: $head, branch: $pr.headRefName, base: $pr.baseRefName, author: $author,
|
|
214
|
+
state: $pr.state, mergeable: $pr.mergeable, reviewDecision: $pr.reviewDecision},
|
|
215
|
+
ci: {status: $ciStatus, checks: $checks},
|
|
216
|
+
verdict: {state: $botState, verdict: $botVerdict, findingsCount: $findingsCount, findingKeys: $keys,
|
|
217
|
+
mustFix: ($v.must_fix // []), commentUrl: ($botComment.url // null), commentId: ($botComment.id // null),
|
|
218
|
+
degraded: $degraded, degradedReason: $degradedReason, sameRunAsLastTick: $sameRun},
|
|
219
|
+
threads: {total: ($snap.threads | length), unresolved: $unresolved,
|
|
220
|
+
actionable: $actionable, staleUnresolved: $staleUnresolved,
|
|
221
|
+
skippedOutdated: $skippedOutdated, flaggedInjection: $flaggedInjection},
|
|
222
|
+
conversation: {actionable: $convActionable},
|
|
223
|
+
reviews: {actionable: $reviewActionable},
|
|
224
|
+
recurrence: {streak: $streak, lastRoundHadRejection: $lastRoundHadRejection, recurringKeys: $recurringKeys, fixAttempts: $fixAttempts},
|
|
225
|
+
backoff: {idleStreak: $idleStreak, intervalMinutes: $intervalMinutes, waitSeconds: $waitSeconds},
|
|
226
|
+
errors: [], snapshotPath: $snapshotPath, statePath: $statePath
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
EOF
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
combined=$(jq -n \
|
|
233
|
+
--slurpfile snapfile "$snapshot" --argjson prev "$prev_json" \
|
|
234
|
+
--arg now "$now" --arg statePath "$state_path" --arg snapshotPath "$snapshot_path" \
|
|
235
|
+
'$snapfile[0] as $snap | $prev as $prev | $now as $now | $statePath as $statePath | $snapshotPath as $snapshotPath | '"$(pb_jq_reduce)") \
|
|
236
|
+
|| pb_fail invalid_json "reduce-state.sh: reducer failed on $snapshot" '{"source":"reduce"}'
|
|
237
|
+
|
|
238
|
+
if [ -n "$state_out" ]; then
|
|
239
|
+
pb_atomic_write_json "$state_out" jq -c '.state' <<<"$combined" || pb_fail invalid_json "reduce-state.sh: could not write $state_out" '{"source":"state_out"}'
|
|
240
|
+
fi
|
|
241
|
+
if [ -n "$result_out" ]; then
|
|
242
|
+
pb_atomic_write_json "$result_out" jq -c '.result' <<<"$combined" || pb_fail invalid_json "reduce-state.sh: could not write $result_out" '{"source":"result_out"}'
|
|
243
|
+
else
|
|
244
|
+
jq -c '.result' <<<"$combined"
|
|
245
|
+
fi
|