@monte3l/groundwork 1.0.0-rc.2 → 1.0.0-rc.3

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.
Files changed (170) hide show
  1. package/README.md +13 -5
  2. package/bin/m3l-groundwork.mjs +36 -2
  3. package/dist/assets.d.ts +10 -0
  4. package/dist/assets.js +14 -0
  5. package/dist/baseline-stage.d.ts +173 -0
  6. package/dist/baseline-stage.js +215 -0
  7. package/dist/caps.d.ts +4 -1
  8. package/dist/caps.js +17 -3
  9. package/dist/conflicts.d.ts +23 -2
  10. package/dist/conflicts.js +89 -11
  11. package/dist/customize-paths.d.ts +92 -0
  12. package/dist/customize-paths.js +115 -0
  13. package/dist/emit.d.ts +50 -1
  14. package/dist/emit.js +121 -13
  15. package/dist/fatal.d.ts +70 -0
  16. package/dist/fatal.js +132 -0
  17. package/dist/format-error.d.ts +113 -0
  18. package/dist/format-error.js +553 -0
  19. package/dist/fs-guard.d.ts +142 -0
  20. package/dist/fs-guard.js +222 -0
  21. package/dist/git.js +10 -1
  22. package/dist/harness/conformance.js +2 -0
  23. package/dist/harness/frontmatter.js +2 -13
  24. package/dist/harness/grade.js +37 -8
  25. package/dist/harness/rules.d.ts +20 -3
  26. package/dist/harness/rules.js +108 -6
  27. package/dist/harness/types.d.ts +13 -0
  28. package/dist/harness/types.js +3 -0
  29. package/dist/inventory.d.ts +77 -3
  30. package/dist/inventory.js +103 -12
  31. package/dist/jsonc.d.ts +49 -2
  32. package/dist/jsonc.js +140 -7
  33. package/dist/main.d.ts +62 -3
  34. package/dist/main.js +538 -67
  35. package/dist/merge-json.d.ts +47 -4
  36. package/dist/merge-json.js +120 -8
  37. package/dist/mode.js +12 -2
  38. package/dist/pack-stage.d.ts +210 -0
  39. package/dist/pack-stage.js +287 -0
  40. package/dist/packs.d.ts +19 -13
  41. package/dist/packs.js +231 -28
  42. package/dist/palette.d.ts +23 -0
  43. package/dist/palette.js +22 -0
  44. package/dist/plugin.d.ts +122 -6
  45. package/dist/plugin.js +687 -47
  46. package/dist/report.d.ts +21 -2
  47. package/dist/report.js +93 -5
  48. package/dist/staging.d.ts +176 -0
  49. package/dist/staging.js +375 -0
  50. package/dist/survey/fs-walk.d.ts +34 -2
  51. package/dist/survey/fs-walk.js +70 -5
  52. package/dist/survey/internal/blocked-path.d.ts +27 -0
  53. package/dist/survey/internal/blocked-path.js +129 -0
  54. package/dist/survey/internal/package-json.d.ts +13 -0
  55. package/dist/survey/internal/package-json.js +37 -0
  56. package/dist/survey/internal/read-guard.d.ts +178 -0
  57. package/dist/survey/internal/read-guard.js +276 -0
  58. package/dist/survey/survey-docs.d.ts +17 -2
  59. package/dist/survey/survey-docs.js +61 -21
  60. package/dist/survey/survey-harness.d.ts +20 -2
  61. package/dist/survey/survey-harness.js +87 -46
  62. package/dist/survey/survey-shape.d.ts +17 -2
  63. package/dist/survey/survey-shape.js +60 -46
  64. package/dist/survey/survey-toolchain.d.ts +16 -1
  65. package/dist/survey/survey-toolchain.js +69 -66
  66. package/dist/survey/survey.js +6 -4
  67. package/dist/survey/types.d.ts +79 -0
  68. package/dist/survey/types.js +2 -6
  69. package/dist/term.d.ts +80 -0
  70. package/dist/term.js +145 -0
  71. package/dist/tokens.js +2 -0
  72. package/dist/toolchain/conformance.js +2 -0
  73. package/dist/toolchain/grade.js +26 -8
  74. package/dist/toolchain/rules.d.ts +13 -4
  75. package/dist/toolchain/rules.js +16 -0
  76. package/dist/toolchain/tsconfig-chain.d.ts +2 -0
  77. package/dist/toolchain/tsconfig-chain.js +32 -8
  78. package/dist/toolchain/types.d.ts +16 -4
  79. package/dist/toolchain/types.js +3 -0
  80. package/package.json +4 -3
  81. package/plugin/skills/customize/SKILL.md +292 -18
  82. package/plugin/src/domain-map.ts +39 -14
  83. package/plugin/src/index.ts +5 -1
  84. package/plugin/src/kind-facet-map.ts +25 -8
  85. package/plugin/src/pack-map.ts +147 -25
  86. package/plugin/src/plugin-map.ts +236 -0
  87. package/templates/core/.claude/agents/Explore.md +0 -1
  88. package/templates/core/.claude/agents/code-implementer.md +4 -4
  89. package/templates/core/.claude/agents/code-reviewer.md +4 -4
  90. package/templates/core/.claude/agents/silent-failure-hunter.md +2 -2
  91. package/templates/core/.claude/agents/test-author.md +7 -5
  92. package/templates/core/.claude/hooks/guard-branch-isolation.mjs +56 -10
  93. package/templates/core/.claude/hooks/guard-double-background.mjs +13 -8
  94. package/templates/core/.claude/hooks/guard-git-push-signed.mjs +12 -9
  95. package/templates/core/.claude/hooks/guard-hub-src-writes.mjs +1890 -38
  96. package/templates/core/.claude/hooks/guard-js-extension.mjs +2 -1
  97. package/templates/core/.claude/hooks/guard-no-commonjs.mjs +7 -5
  98. package/templates/core/.claude/hooks/guard-secret-writes.mjs +7 -5
  99. package/templates/core/.claude/hooks/inject-decision-gate.mjs +12 -9
  100. package/templates/core/.claude/hooks/post-edit-verify.mjs +276 -98
  101. package/templates/core/.claude/rules/agent-dispatch.md +7 -0
  102. package/templates/core/.claude/rules/tests.md +2 -2
  103. package/templates/core/.claude/settings.json +5 -0
  104. package/templates/core/.claude/skills/finishing-work/SKILL.md +58 -10
  105. package/templates/core/.claude/skills/harness-guidance/SKILL.md +16 -7
  106. package/templates/core/.claude/skills/starting-work/SKILL.md +5 -0
  107. package/templates/core/.claude/skills/triaging-ci/SKILL.md +9 -8
  108. package/templates/core/.claude/skills/typescript-guidance/SKILL.md +137 -20
  109. package/templates/core/.claude/skills/typescript-guidance/references/area-catalog.md +135 -0
  110. package/templates/core/.claude/skills/typescript-guidance/references/tooling-sources.md +113 -0
  111. package/templates/core/.claude/skills/writing-commits/SKILL.md +2 -2
  112. package/templates/core/.github/dependabot.yml +18 -0
  113. package/templates/core/.github/workflows/ci.yml +15 -15
  114. package/templates/core/.github/workflows/dependency-review.yml +2 -2
  115. package/templates/core/.github/workflows/security-audit.yml +10 -4
  116. package/templates/core/.prettierignore +4 -0
  117. package/templates/core/CLAUDE.md +53 -3
  118. package/templates/core/README.md +30 -10
  119. package/templates/core/_gitignore +17 -0
  120. package/templates/core/bin/check-exports.mjs +11 -5
  121. package/templates/core/bin/lib/agent-roster.mjs +1 -1
  122. package/templates/core/bin/lib/frontmatter.mjs +4 -2
  123. package/templates/core/bin/lib/harness-rules.mjs +169 -20
  124. package/templates/core/bin/lib/protected-paths.mjs +93 -5
  125. package/templates/core/bin/lib/toolchain-rules.mjs +187 -26
  126. package/templates/core/bin/lib/verify-steps.mjs +29 -11
  127. package/templates/core/bin/verify.mjs +12 -0
  128. package/templates/core/eslint.config.js +5 -0
  129. package/templates/core/package.json +7 -7
  130. package/templates/core/vitest.config.ts +8 -1
  131. package/templates/packs/README.md +35 -24
  132. package/templates/packs/github/files/.claude/skills/reviewing-dependabot-prs/SKILL.md +133 -0
  133. package/templates/packs/github/files/.claude/skills/triaging-scan-alerts/SKILL.md +88 -0
  134. package/templates/packs/github/files/.claude/skills/watching-pr-checks/SKILL.md +94 -0
  135. package/templates/packs/github/files/.github/workflows/claude-pr-review.yml +267 -0
  136. package/templates/packs/github/files/.github/workflows/claude.yml +106 -0
  137. package/templates/packs/github/pack.json +19 -0
  138. package/templates/packs/harness-extras/files/.claude/hooks/guard-readonly-bash.mjs +1122 -65
  139. package/templates/packs/harness-extras/files/.claude/hooks/reinject-compact-handoff.mjs +147 -13
  140. package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/statusline.mjs +6 -2
  141. package/templates/packs/harness-extras/files/.claude/hooks/write-compact-handoff.mjs +227 -44
  142. package/templates/packs/harness-extras/pack.json +18 -13
  143. package/templates/packs/publishing/files/.changeset/README.md +25 -0
  144. package/templates/packs/publishing/files/.changeset/config.json +7 -0
  145. package/templates/packs/publishing/files/.github/release-tools/package.json +9 -0
  146. package/templates/packs/publishing/files/.github/workflows/release.yml +293 -0
  147. package/templates/packs/publishing/files/REUSE.toml +28 -0
  148. package/templates/packs/publishing/files/bin/check-dts-deps.mjs +234 -0
  149. package/templates/packs/publishing/files/bin/check-license-headers.mjs +217 -0
  150. package/templates/packs/publishing/files/bin/check-publish-version.mjs +149 -0
  151. package/templates/packs/publishing/files/bin/lib/npm-publish-args.mjs +86 -0
  152. package/templates/packs/publishing/files/bin/pnpm-publish-shim.mjs +86 -0
  153. package/templates/packs/publishing/pack.json +35 -0
  154. package/templates/packs/{harness-extras → quality}/files/bin/check-file-budget.mjs +6 -4
  155. package/templates/packs/quality/pack.json +29 -0
  156. package/templates/packs/supply-chain/files/.github/workflows/gitleaks.yml +52 -0
  157. package/templates/packs/supply-chain/files/.github/workflows/scorecard.yml +48 -0
  158. package/templates/packs/supply-chain/files/.gitleaks.toml +2 -0
  159. package/templates/packs/supply-chain/pack.json +19 -0
  160. package/templates/packs/worktrees/files/.claude/hooks/ensure-worktree-deps.mjs +283 -0
  161. package/templates/packs/worktrees/files/.claude/hooks/guard-worktree-only.mjs +245 -0
  162. package/templates/packs/worktrees/files/.claude/hooks/repair-core-bare.mjs +335 -0
  163. package/templates/packs/worktrees/files/.claude/skills/working-in-worktrees/SKILL.md +139 -0
  164. package/templates/packs/worktrees/files/.worktreeinclude +11 -0
  165. package/templates/packs/worktrees/pack.json +64 -0
  166. package/templates/packs/statusline/pack.json +0 -31
  167. /package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/statusline-layout.mjs +0 -0
  168. /package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/subagent-statusline.mjs +0 -0
  169. /package/templates/packs/{harness-extras → quality}/files/.claude/agents/type-design-analyzer.md +0 -0
  170. /package/templates/packs/{harness-extras → quality}/files/bin/file-budget-baseline.json +0 -0
@@ -0,0 +1,133 @@
1
+ ---
2
+ name: reviewing-dependabot-prs
3
+ description: >-
4
+ Reviews every open Dependabot pull request, classifies each by semver bump
5
+ level and CI check status, and proposes merge/hold/close per PR -- then
6
+ acts only on the batch the user confirms, never a PR the user didn't
7
+ approve. Use for /reviewing-dependabot-prs, "review the dependabot PRs",
8
+ "triage dependency updates", or when several Dependabot PRs have
9
+ accumulated. GitHub stance: gh CLI.
10
+ ---
11
+
12
+ Triage every open Dependabot pull request in this repository: classify each
13
+ by how risky its bump is and whether its checks are green, propose an
14
+ action per PR, then act only on the batch the user actually confirms. This
15
+ skill never merges or closes a PR the user hasn't approved, and never
16
+ guesses at a version bump's safety from the checks alone -- a major bump is
17
+ always held for a human look, checks or no checks.
18
+
19
+ ## Steps
20
+
21
+ ### 1 — List every open Dependabot PR
22
+
23
+ ```bash
24
+ gh pr list --search "author:app/dependabot" --state open --limit 100 --json \
25
+ number,title,headRefName,isDraft,mergeable,statusCheckRollup
26
+ ```
27
+
28
+ `--limit 100` avoids `gh pr list`'s 30-result default silently truncating a
29
+ large backlog. If this returns nothing, report that there is nothing to
30
+ triage and stop.
31
+
32
+ ### 2 — Classify each PR's bump level
33
+
34
+ Dependabot's own titles follow `Bump <package> from <old> to <new>` (or
35
+ `Bump <package> in <dir> from <old> to <new>` for a monorepo/directory-scoped
36
+ config) -- but a project with a `commit-message.prefix` in its
37
+ `dependabot.yml` gets a prefixed, often-lowercased title instead, e.g.
38
+ `chore(deps): bump <package> from <old> to <new>`. Match case-insensitively
39
+ and allow an optional prefix before `bump`, then parse `<old>`/`<new>` and
40
+ compare them as semver:
41
+
42
+ - **major**: the leftmost _non-zero_ version segment differs (not simply
43
+ the first segment -- under semver a `0.x` package treats its second
44
+ segment as breaking, so `0.3.0 -> 0.4.0` is major, not minor). A major
45
+ bump is a breaking-change risk by definition, regardless of check status.
46
+ - **minor**/**patch**: lower risk, but still gated on checks (next step).
47
+ - **unparseable** (a non-semver ecosystem, a grouped-update PR covering
48
+ several packages, or a title that doesn't match the pattern even with the
49
+ prefix/case allowance above): mark it **unknown** and never guess a risk
50
+ level for it -- always propose _hold_ for a human look, and say why
51
+ parsing failed.
52
+
53
+ ### 3 — Classify each PR's check status and mergeability
54
+
55
+ `statusCheckRollup` mixes two entry shapes: a GitHub Actions **check run**
56
+ (carries `conclusion`) and a third-party **commit status** (carries `state`
57
+ instead -- no `conclusion` field at all). Read whichever field the entry
58
+ actually has, and reduce the whole set to one of:
59
+
60
+ - **green**: every entry's `conclusion` is `SUCCESS`, `NEUTRAL`, or
61
+ `SKIPPED` (not a failure), or its `state` is `SUCCESS`.
62
+ - **red**: at least one entry's `conclusion` is `FAILURE`, `CANCELLED`,
63
+ `TIMED_OUT`, `ACTION_REQUIRED`, `STARTUP_FAILURE`, or `STALE`, or its
64
+ `state` is `FAILURE`/`ERROR`.
65
+ - **pending**: at least one entry is still queued/in-progress (`conclusion`
66
+ absent, or `state: PENDING`) and none are red.
67
+ - **no checks configured**: `statusCheckRollup` is empty -- this is _not_
68
+ the same as green; propose **hold** and say the repository has no CI
69
+ configured for this PR, rather than treating silence as success.
70
+
71
+ Also read `mergeable`: a PR reported as `CONFLICTING` is never proposed for
72
+ merge regardless of bump level or check status. A draft PR (`isDraft:
73
+ true`) is never proposed for merge either.
74
+
75
+ ### 4 — Propose an action per PR
76
+
77
+ | Bump level | Checks | Mergeable | Draft | Proposal |
78
+ | ------------------- | --------- | ---------------- | ----- | ----------------------------------------------------- |
79
+ | patch/minor | green | yes | no | **merge** |
80
+ | patch/minor | red | any | no | **hold** -- name the failing check |
81
+ | patch/minor | pending | any | no | **hold** -- checks still running, re-check later |
82
+ | patch/minor | no checks | any | no | **hold** -- no CI configured, needs a human look |
83
+ | any | any | no (conflicting) | any | **hold** -- merge conflict, needs a rebase/human look |
84
+ | major | any | any | no | **hold** -- breaking-change risk, needs a human read |
85
+ | unknown/unparseable | any | any | any | **hold** -- couldn't classify the bump safely |
86
+ | any | any | any | yes | **skip** -- draft PR |
87
+
88
+ For a **held** PR whose changelog or release notes look clearly abandoned,
89
+ irrelevant, or superseded by a newer PR for the same package, note **close**
90
+ as an alternative to raise with the user in the next step -- this skill
91
+ never closes a PR on its own initiative, only on explicit confirmation.
92
+
93
+ ### 5 — Present the report and confirm the batch
94
+
95
+ Show one table: PR number, title, bump level, check status, mergeable,
96
+ proposed action. Then ask the user which of the **merge**-proposed PRs to
97
+ actually merge, and separately whether any **held** PR should instead be
98
+ **closed** -- offer "all of them", a subset by number, or none for each.
99
+ Never treat silence or a general "looks good" as consent to act; get an
100
+ explicit list or an explicit "yes, all of them" for merges, and a separate
101
+ explicit confirmation for any close.
102
+
103
+ ### 6 — Act only on the confirmed batch
104
+
105
+ For each confirmed merge:
106
+
107
+ ```bash
108
+ gh pr merge <number> --squash --auto
109
+ ```
110
+
111
+ Prefer `--squash` (the common convention for a Dependabot PR), but if `gh`
112
+ rejects it (squash merges disabled by the repository, or "Allow auto-merge"
113
+ itself is off -- GitHub's own default), fall back to whichever merge method
114
+ the repository actually allows (`--merge`/`--rebase`, or drop `--auto` and
115
+ report that the PR is ready but must be merged by hand) rather than treating
116
+ the rejection as this skill's own failure. `--auto` arms auto-merge rather
117
+ than forcing an immediate merge -- if the repository's branch protection
118
+ requires checks or reviews, the merge still waits for those; this skill does
119
+ not bypass a repository's own merge rules. If `gh pr merge` reports the PR
120
+ isn't mergeable (a conflict, a newly-failed check since step 1), report that
121
+ PR's failure and continue with the rest of the batch rather than aborting it.
122
+
123
+ For each confirmed close:
124
+
125
+ ```bash
126
+ gh pr close <number> --comment "<one-line reason from step 4>"
127
+ ```
128
+
129
+ ### 7 — Report outcomes
130
+
131
+ Summarize what was merged, what's queued behind `--auto`, what was closed,
132
+ and what was held or skipped, with a one-line reason for every held/skipped/
133
+ closed PR.
@@ -0,0 +1,88 @@
1
+ ---
2
+ name: triaging-scan-alerts
3
+ description: >-
4
+ Fetches this repository's open code-scanning alerts via gh api, groups
5
+ them by tool and severity, and maps each to its file:line -- the output
6
+ is a triage report and options only, it never edits code. Use for
7
+ /triaging-scan-alerts, "review the scan alerts", "what does CodeQL flag",
8
+ "triage the security findings", or after a code-scanning workflow run.
9
+ GitHub stance: gh CLI.
10
+ ---
11
+
12
+ Fetch every open code-scanning alert for this repository, group it by
13
+ which tool raised it and how severe it is, and map each to the exact
14
+ file and line it points at. This skill produces a report and a set of
15
+ options per alert -- it never edits code, dismisses an alert, or opens a
16
+ PR on its own.
17
+
18
+ ## Steps
19
+
20
+ ### 1 — Fetch open alerts
21
+
22
+ ```bash
23
+ gh api 'repos/{owner}/{repo}/code-scanning/alerts?state=open' \
24
+ --paginate \
25
+ -q '.[] | {number, tool: .tool.name, severity: .rule.security_severity_level // .rule.severity, rule: .rule.id, path: .most_recent_instance.location.path, line: .most_recent_instance.location.start_line, description: .rule.description}'
26
+ ```
27
+
28
+ (`state=open` is in the query string, not a `-f` field -- `gh api` switches
29
+ to `POST` the moment any `-f`/`-F` field is passed without an explicit
30
+ `--method GET`, which would break this list call. The whole path is single-
31
+ quoted because `?` is a glob character in zsh -- macOS's default shell --
32
+ and an unquoted one there fails with "no matches found" before `gh` runs.)
33
+
34
+ Use the current repository (`gh` infers `{owner}/{repo}` from the working
35
+ directory's remote when the placeholders are left as-is). If the endpoint
36
+ returns a 404 or a permissions error, code scanning may not be enabled on
37
+ this repository -- report that plainly rather than treating it as "no
38
+ alerts".
39
+
40
+ ### 2 — Group by tool and severity
41
+
42
+ Group the results by `tool` (CodeQL, a third-party SARIF uploader, etc.)
43
+ first, then by `severity` within each tool (`critical`/`high`/`medium`/
44
+ `low`, or the tool's own scale if it doesn't use those exact words --
45
+ report the tool's own label rather than forcing it into one of the four).
46
+
47
+ ### 3 — Map each alert to file:line
48
+
49
+ Every alert in the fetched output already carries `path`/`line` from its
50
+ most recent instance -- present these verbatim (`path:line`) so the user
51
+ can jump straight to the code. If `path`/`line` is absent (some
52
+ alert-generating tools don't localize to a line), say so rather than
53
+ inventing a location.
54
+
55
+ ### 4 — Present the report
56
+
57
+ One table per tool, ordered severity-first: alert number, rule id, one-line
58
+ description, `path:line`, severity. Note the total open-alert count at the
59
+ top.
60
+
61
+ ### 5 — Offer options per alert -- never act automatically
62
+
63
+ For each alert, describe (not perform) the realistic options:
64
+
65
+ - **fix now**: a one-line description of what the fix would look like, if
66
+ obvious from the rule/description -- this skill still does not make the
67
+ edit itself; the option is to hand it to a code-writing task.
68
+ - **dismiss as false positive** / **won't fix** / **used in tests**: the
69
+ three reasons GitHub's own dismissal UI accepts, named but not run:
70
+ ```bash
71
+ gh api -X PATCH repos/{owner}/{repo}/code-scanning/alerts/{number} \
72
+ -f state=dismissed -f "dismissed_reason=false positive"
73
+ ```
74
+ (swap the quoted value for `won't fix` or `used in tests` -- each contains
75
+ a space or apostrophe, so it must stay quoted as one shell argument).
76
+ - **needs a human decision**: for anything ambiguous (a finding in
77
+ generated code, a suppressed pattern that might be intentional).
78
+
79
+ Ask the user which option applies before running any dismissal command --
80
+ this skill's own default is to report, not to close anything out.
81
+
82
+ ### 6 — Note the adjacent alert surfaces this skill doesn't cover
83
+
84
+ Dependabot alerts (`gh api repos/{owner}/{repo}/dependabot/alerts`) and
85
+ secret-scanning alerts (`gh api repos/{owner}/{repo}/secret-scanning/alerts`)
86
+ are separate GitHub endpoints from code-scanning -- if the user asks about
87
+ either, say so and fetch that endpoint too rather than assuming this
88
+ skill's code-scanning report already covers it.
@@ -0,0 +1,94 @@
1
+ ---
2
+ name: watching-pr-checks
3
+ description: >-
4
+ Checks a pull request's CI status via gh pr checks: reports ready-to-merge
5
+ and asks before merging on green, or reads the real failing job log and
6
+ hands off to /triaging-ci on red -- never guesses at a failure from the
7
+ check name alone. Use for /watching-pr-checks, "check this PR's status",
8
+ "is this PR ready to merge", "did CI pass on my PR". GitHub stance: gh
9
+ CLI.
10
+ ---
11
+
12
+ Check one pull request's CI status and act on what the checks actually
13
+ say, rather than assuming green or guessing at a failure. This is a
14
+ single check, not a polling loop -- if checks are still running, report
15
+ that plainly and let the user decide whether to ask again later or wait.
16
+
17
+ ## Steps
18
+
19
+ ### 1 — Resolve the PR
20
+
21
+ If the user gave an explicit PR number or URL, use it. Otherwise, resolve
22
+ the PR for the current branch:
23
+
24
+ ```bash
25
+ gh pr view --json number,headRefName,state
26
+ ```
27
+
28
+ If there is no PR for the current branch, or its `state` isn't `OPEN` (a
29
+ merged or closed PR still resolves by branch name), say so and stop --
30
+ there is nothing to watch.
31
+
32
+ ### 2 — Check status
33
+
34
+ ```bash
35
+ gh pr checks <number> --json name,bucket,link,workflow
36
+ ```
37
+
38
+ `bucket` (not `conclusion` -- `gh pr checks --json` has no `conclusion`
39
+ field) is the ready-made summary each check reduces to: `pass`, `fail`,
40
+ `pending`, `skipping`, or `cancel`. Reduce the whole set to one of:
41
+
42
+ - **all green**: every check's `bucket` is `pass` or `skipping`.
43
+ - **still running**: at least one `pending` and none `fail`/`cancel`.
44
+ - **failed**: at least one `fail` or `cancel`.
45
+ - **no checks configured**: the list is empty -- report this plainly rather
46
+ than treating it as either green or failed; there is nothing to watch
47
+ until the repository has CI wired up for this PR.
48
+
49
+ ### 3a — All green
50
+
51
+ Report the PR is ready to merge, name every check that passed, and ask the
52
+ user before merging -- never merge without that confirmation, even when
53
+ every check is green:
54
+
55
+ ```bash
56
+ gh pr merge <number> --squash --auto
57
+ ```
58
+
59
+ If `gh` rejects `--squash --auto` (squash merges disabled, or "Allow
60
+ auto-merge" itself is off -- GitHub's own default), fall back to whichever
61
+ merge method the repository actually allows, or drop `--auto` and report
62
+ that the PR is ready but must be merged by hand -- don't report the
63
+ rejection as this skill's own failure. (`--auto` respects any
64
+ branch-protection requirement still pending -- see
65
+ `reviewing-dependabot-prs`'s step 6 for the same reasoning if that skill is
66
+ also installed.)
67
+
68
+ ### 3b — Still running
69
+
70
+ Report which checks are still pending and which have already passed. Do
71
+ not guess at the eventual outcome. Suggest the user re-invoke this skill
72
+ in a few minutes, or -- if the calling session supports scheduling a
73
+ recurring check -- offer to check again automatically rather than blocking
74
+ on a manual re-ask.
75
+
76
+ ### 3c — Failed
77
+
78
+ Do not report the failure from the check's name alone -- pull the actual
79
+ job log before describing what went wrong. The failing check's `workflow`
80
+ field distinguishes a GitHub Actions run (has a log `gh run view` can read)
81
+ from a third-party check (a status-only integration with no run to fetch):
82
+
83
+ ```bash
84
+ gh run view <run-id> --log-failed
85
+ ```
86
+
87
+ (`<run-id>` comes from the failing check's `link`, a
88
+ `.../actions/runs/<run-id>/job/...` URL -- a check with no such `link`, or
89
+ whose `workflow` is empty, is from an external app; report its `link` and
90
+ that no log is available to read rather than guessing at why it failed.)
91
+ Once the real failure is in hand, hand off to `/triaging-ci` (if the
92
+ project has it installed) to map the failure to its root cause and present
93
+ fix options -- this skill's own job ends at "here's what actually failed
94
+ and why," not at proposing or applying a fix.
@@ -0,0 +1,267 @@
1
+ name: Claude PR Review
2
+
3
+ # What this does: reviews every pull request when it's opened, pushed to,
4
+ # marked ready or reopened, checking the same things a human reviewer would
5
+ # (see the prompt below), then posts the result as one sticky summary comment
6
+ # plus inline comments. It never approves, blocks, or pushes code, and it
7
+ # skips bot-authored and fork pull requests.
8
+ #
9
+ # Two jobs, because Claude does not post the review itself. `review` runs
10
+ # Claude read-only and gets its findings back as schema-validated structured
11
+ # output (`--json-schema`); `post` is plain `gh` + `jq` and is the only job
12
+ # holding `pull-requests: write`. Letting the model post through `gh pr
13
+ # comment` or the inline-comment MCP tool fails silently and green: runs
14
+ # complete successfully with real spend and permission denials and post
15
+ # nothing, so a new push looks unreviewed until the PR is closed and
16
+ # reopened. The trigger is not the problem -- `synchronize` and `reopened`
17
+ # both start runs. Likely cause: the action loads the repository's own
18
+ # `.claude/` and CLAUDE.md, and a project that documents subagent workflows
19
+ # makes the model dispatch review subagents that have no MCP tool and no
20
+ # `gh api`; background subagents also end the action's run at the first
21
+ # result message. Upstream reports the same shape: claude-code-action#1523
22
+ # (success, nothing posted), #1823 (a `--edit-last` re-review silently
23
+ # skipped), #1852/#1499/#1646 (background subagents), #1679 (the buffered
24
+ # inline-comment step exits 0 after posting nothing). There is no input that
25
+ # fails a run which posted nothing, and `use_sticky_comment` does nothing in
26
+ # this mode, so posting moved out of the model: without `structured_output`
27
+ # the action itself fails the run (docs/usage.md), and the `post` job fails
28
+ # on any API error other than an inline comment GitHub can't anchor
29
+ # (HTTP 422), which is folded into the summary instead.
30
+ #
31
+ # One run is deliberately skipped: claude-code-action will not run on a PR
32
+ # that edits this very file (its copy must match the default branch's, a
33
+ # guard against a PR rewriting its own reviewer). It logs a warning and exits
34
+ # success with no outputs, so the `gate` step tells that case apart from a
35
+ # real empty result -- no `conclusion` AND this file differs from the base --
36
+ # and `post` is skipped with a notice. Any other empty result fails `review`.
37
+ # A PR touching this file therefore gets no review until it merges.
38
+ #
39
+ # `--disallowedTools` keeps subagents and writes off in `review`, and the job
40
+ # has no write token at all (contents/pull-requests read), so a prompt
41
+ # injected through a PR's contents can at worst return a wrong review, not
42
+ # post, push or approve. A review is never a formal GitHub review either
43
+ # (Anthropic's own docs/capabilities-and-limitations.md), so it cannot
44
+ # satisfy or bypass any branch-protection required check or approval count.
45
+ # Comments appear as github-actions[bot] rather than claude[bot]. Excludes
46
+ # bot-authored and fork PRs explicitly below, rather than relying on
47
+ # anthropics/claude-code-action's own internal bot/permission checks
48
+ # (docs/security.md) -- that way a Dependabot or fork PR never starts a run
49
+ # that would just fail on missing secrets. Runs but fails cleanly on an auth
50
+ # error until the one-time setup described in claude.yml's comments is done
51
+ # by hand.
52
+ on:
53
+ pull_request:
54
+ types: [opened, synchronize, ready_for_review, reopened]
55
+
56
+ # Cancel a stale run when the PR gets a new push, same as this project's own
57
+ # CI -- a `synchronize` fires on every push, and a paid review run per push
58
+ # queues up otherwise. This also cancels a `post` job still running for the
59
+ # old head.
60
+ concurrency:
61
+ group: ${{ github.workflow }}-${{ github.ref }}
62
+ cancel-in-progress: true
63
+
64
+ # Reset, then grant per job.
65
+ permissions: {}
66
+
67
+ jobs:
68
+ review:
69
+ if: |
70
+ github.event.pull_request.user.type != 'Bot' &&
71
+ github.event.pull_request.head.repo.full_name == github.repository
72
+ runs-on: ubuntu-latest
73
+ timeout-minutes: 30
74
+ permissions:
75
+ contents: read
76
+ pull-requests: read
77
+ id-token: write # see claude.yml for why this is needed regardless of auth method
78
+ outputs:
79
+ review: ${{ steps.claude.outputs.structured_output }}
80
+ skipped: ${{ steps.gate.outputs.skipped }}
81
+ steps:
82
+ # Full history, not depth 1: the prompt tells Claude to `git diff` the
83
+ # base against the head, and a shallow clone has no base to diff.
84
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
85
+ with:
86
+ persist-credentials: false
87
+ fetch-depth: 0
88
+
89
+ - id: claude
90
+ uses: anthropics/claude-code-action@ed670b4cf9de2a5a570d130d2f6197b9e543cd64 # v1.0.240
91
+ with:
92
+ anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
93
+ # Swap in CLAUDE_CODE_OAUTH_TOKEN or the Workload Identity
94
+ # Federation inputs instead -- see claude.yml's comments for all
95
+ # three auth options; this workflow needs the same one configured.
96
+
97
+ # Pinned rather than left on the action's default: there is no
98
+ # `model:` input (anthropics/claude-code-action's action.yml has
99
+ # none), the action's own docs defer the default's definition to
100
+ # a separate page rather than naming it, and no official source
101
+ # states whether that default can change between action releases
102
+ # -- an automated, unattended job like this one is exactly where a
103
+ # silent shift would go unnoticed.
104
+ #
105
+ # `--fallback-model` (both `model:` and `fallback_model:` are
106
+ # deprecated action inputs -- claude-code-action's docs/usage.md
107
+ # says to configure both through `claude_args` now) tries the
108
+ # listed model only when the primary is overloaded, unavailable,
109
+ # or returns another non-retryable server error -- never for an
110
+ # auth, billing, rate-limit, request-size, transport, or
111
+ # org-policy failure, a real, currently-unfixed gap
112
+ # (anthropics/claude-code-action#594, redirected to and
113
+ # auto-closed `not_planned` as anthropics/claude-code#8413) -- but
114
+ # worth having for the failure mode it does cover.
115
+ #
116
+ # `--allowedTools` is read-only on purpose: the diff and PR
117
+ # metadata, plus git history (Read/Grep/Glob are allowed by
118
+ # default). `--disallowedTools` removes subagents and every write.
119
+ # `--json-schema` makes the result machine-readable: the action
120
+ # fails the step when Claude returns no `structured_output`.
121
+ claude_args: |
122
+ --model claude-opus-5-5
123
+ --fallback-model claude-sonnet-5-5
124
+ --allowedTools "Bash(gh pr diff:*),Bash(gh pr view:*),Bash(git diff:*),Bash(git log:*),Bash(git show:*)"
125
+ --disallowedTools "Agent,Task,Write,Edit,NotebookEdit"
126
+ --json-schema '{"type":"object","properties":{"summary":{"type":"string"},"inline":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"line":{"type":"integer"},"body":{"type":"string"}},"required":["path","line","body"],"additionalProperties":false}}},"required":["summary","inline"],"additionalProperties":false}'
127
+ prompt: |
128
+ REPO: ${{ github.repository }}
129
+ PR NUMBER: ${{ github.event.pull_request.number }}
130
+ BASE: origin/${{ github.base_ref }}
131
+ HEAD: ${{ github.event.pull_request.head.sha }}
132
+ PREVIOUSLY REVIEWED HEAD (empty unless this is a push to an
133
+ already-open PR): ${{ github.event.before }}
134
+
135
+ Review this PR. You already have this repository's CLAUDE.md
136
+ loaded -- use it, don't restate it. Do not dispatch subagents
137
+ and do not try to post anything to GitHub: you have no write
138
+ access, and your only output is the structured result described
139
+ below. Specifically check:
140
+
141
+ 1. If this project's CLAUDE.md documents a hub-and-spoke
142
+ workflow for src/tests changes (test-author before
143
+ code-implementer, review spokes run over the diff), judge
144
+ from the PR description and commit history whether it was
145
+ followed, and say what you could and could not establish
146
+ rather than guessing.
147
+ 2. Anything the project's own code-review checklist would catch
148
+ (`.claude/agents/code-reviewer.md`, if present): correctness,
149
+ security, SOLID/simplification, test coverage.
150
+ 3. If this project uses changesets or another release-versioning
151
+ convention CLAUDE.md documents, whether this PR's public-
152
+ surface change carries the entry that convention requires.
153
+
154
+ Note: the PR branch is already checked out in the current
155
+ working directory with full history. Read the change with
156
+ `git diff BASE...HEAD` (or `gh pr diff`).
157
+
158
+ Return exactly the structured result:
159
+ - `summary`: one markdown summary covering every check above
160
+ (say so explicitly for a check that passes, or that doesn't
161
+ apply to this project), always non-empty. It always covers
162
+ the whole PR, not just the latest push.
163
+ - `inline`: specific code issues, each with the file `path`, the
164
+ 1-based `line` in the new version of that file, and a
165
+ `body`. Use an empty array when there are none. When
166
+ PREVIOUSLY REVIEWED HEAD is set, limit these to lines changed
167
+ since that commit (`git diff <previous>..HEAD`), because
168
+ earlier findings are already on the PR. Only use lines that
169
+ appear in the diff, since GitHub rejects any other.
170
+
171
+ # claude-code-action refuses to run on a PR that edits its own workflow
172
+ # file (the file must match the default branch's copy), logs a warning
173
+ # and still exits success with no outputs -- a green job with no review.
174
+ # That one case is expected, so it is detected by two independent facts:
175
+ # the action set no `conclusion` AND this PR really changes this file.
176
+ # Any other empty result is a failure, never a quiet pass.
177
+ - id: gate
178
+ env:
179
+ CONCLUSION: ${{ steps.claude.outputs.conclusion }}
180
+ REVIEW: ${{ steps.claude.outputs.structured_output }}
181
+ BASE: origin/${{ github.base_ref }}
182
+ run: |
183
+ set -euo pipefail
184
+ if [ -n "$REVIEW" ]; then
185
+ echo "skipped=false" >>"$GITHUB_OUTPUT"
186
+ exit 0
187
+ fi
188
+ if [ -z "$CONCLUSION" ] \
189
+ && ! git diff --quiet "$BASE" HEAD -- .github/workflows/claude-pr-review.yml; then
190
+ echo "::notice::Claude was skipped: this PR edits claude-pr-review.yml, so claude-code-action will not run it until it merges. No review is posted."
191
+ echo "skipped=true" >>"$GITHUB_OUTPUT"
192
+ exit 0
193
+ fi
194
+ echo "::error::Claude returned no structured review (conclusion: '${CONCLUSION:-none}')."
195
+ exit 1
196
+
197
+ post:
198
+ needs: review
199
+ if: needs.review.result == 'success' && needs.review.outputs.skipped != 'true'
200
+ runs-on: ubuntu-latest
201
+ timeout-minutes: 10
202
+ permissions:
203
+ pull-requests: write
204
+ steps:
205
+ # Everything attacker-influenced (the review JSON, comment bodies) goes
206
+ # through `env:` and is only ever quoted as a variable, never
207
+ # interpolated into the script text.
208
+ - name: Post the review
209
+ env:
210
+ GH_TOKEN: ${{ github.token }}
211
+ REPO: ${{ github.repository }}
212
+ PR: ${{ github.event.pull_request.number }}
213
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
214
+ REVIEW: ${{ needs.review.outputs.review }}
215
+ run: |
216
+ set -euo pipefail
217
+ marker='<!-- claude-pr-review -->'
218
+ me='github-actions[bot]'
219
+
220
+ # A review without a summary is a failure, not an empty comment.
221
+ jq -e '(.summary | type == "string" and length > 0) and (.inline | type == "array")' \
222
+ <<<"$REVIEW" >/dev/null
223
+
224
+ # Inline comments first, so any that can't be anchored land in the
225
+ # summary. Skip a finding already on the PR from an earlier push.
226
+ existing=$(mktemp)
227
+ gh api --paginate "repos/$REPO/pulls/$PR/comments" \
228
+ --jq ".[] | select(.user.login == \"$me\") | [.path, (.line // .original_line), .body] | @json" \
229
+ >"$existing"
230
+ unanchored=$(mktemp)
231
+ err=$(mktemp)
232
+ while IFS= read -r finding; do
233
+ path=$(jq -r '.path' <<<"$finding")
234
+ line=$(jq -r '.line' <<<"$finding")
235
+ body=$(jq -r '.body' <<<"$finding")
236
+ key=$(jq -cn --arg p "$path" --argjson l "$line" --arg b "$body" '[$p, $l, $b]')
237
+ if grep -Fxq -- "$key" "$existing"; then
238
+ continue
239
+ fi
240
+ if ! gh api -X POST "repos/$REPO/pulls/$PR/comments" \
241
+ -f body="$body" -f commit_id="$HEAD_SHA" -f path="$path" \
242
+ -F line="$line" -f side=RIGHT >/dev/null 2>"$err"; then
243
+ # 422: the line isn't part of the diff. Anything else is real.
244
+ grep -q 'HTTP 422' "$err" || { cat "$err" >&2; exit 1; }
245
+ printf -- '- `%s:%s` -- %s\n' "$path" "$line" "$body" >>"$unanchored"
246
+ fi
247
+ done < <(jq -c '.inline[]' <<<"$REVIEW")
248
+
249
+ # One sticky summary: edit the marked comment, else create it.
250
+ summary=$(mktemp)
251
+ {
252
+ printf '%s\n' "$marker"
253
+ printf '_Reviewed commit `%s`._\n\n' "${HEAD_SHA:0:7}"
254
+ jq -r '.summary' <<<"$REVIEW"
255
+ if [ -s "$unanchored" ]; then
256
+ printf '\n### Findings that could not be anchored to a diff line\n\n'
257
+ cat "$unanchored"
258
+ fi
259
+ } >"$summary"
260
+ id=$(gh api --paginate "repos/$REPO/issues/$PR/comments" \
261
+ --jq ".[] | select(.user.login == \"$me\" and (.body | startswith(\"$marker\"))) | .id" \
262
+ | tail -n 1)
263
+ if [ -n "$id" ]; then
264
+ gh api -X PATCH "repos/$REPO/issues/comments/$id" -F body=@"$summary" >/dev/null
265
+ else
266
+ gh api -X POST "repos/$REPO/issues/$PR/comments" -F body=@"$summary" >/dev/null
267
+ fi