@bompus/house-rules 0.11.0 → 0.14.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/CODE_OF_CONDUCT.md +83 -0
- package/README.md +102 -63
- package/compose.mjs +8 -2
- package/composition.mjs +11 -3
- package/config-view.mjs +18 -3
- package/config.mjs +74 -13
- package/docs/assets/overview.png +0 -0
- package/docs/configuration.md +85 -9
- package/package.json +4 -2
- package/rules/core.md +118 -209
- package/rules/modifiers/backlog-outline.md +25 -0
- package/rules/modifiers/backlog-visual.md +16 -0
- package/rules/modifiers/coded-offers.md +47 -17
- package/rules/modifiers/cross-session-relay.md +23 -0
- package/rules/modifiers/low-quota-handoff.md +4 -3
- package/rules/modifiers/model-default-questions.md +33 -0
- package/rules/modifiers/multi-agent.md +6 -2
- package/rules/modifiers/no-attribution.md +7 -1
- package/rules/modifiers/periodic-updates.md +25 -0
- package/rules/modifiers/plan-files.md +35 -3
- package/rules/modifiers/release-batching.md +4 -2
- package/rules/modifiers/shared-host-load.md +13 -42
- package/rules/modifiers/squash-landing.md +1 -1
- package/rules/modifiers/upstream-contributions.md +45 -0
- package/rules/modifiers/web-image-formats.md +35 -0
- package/rules/references/backlog-view.md +154 -0
- package/rules/references/landing.md +14 -14
- package/rules/references/multi-agent.md +6 -2
- package/rules/references/release-batching.md +37 -4
- package/rules/references/squash-landing.md +10 -4
- package/rules/references/thread-visuals.md +45 -0
- package/setup.mjs +87 -17
- package/skills/hr-agent-guidance-audit/SKILL.md +10 -5
- package/skills/hr-agent-guidance-audit/references/prune-workflow.md +72 -0
- package/skills/hr-agent-guidance-audit/references/records-and-lessons.md +1 -1
- package/skills/hr-agent-guidance-audit/references/retirement-verdicts.md +49 -0
- package/skills/hr-agent-guidance-refresh/SKILL.md +25 -57
- package/skills/hr-agent-guidance-refresh/references/verification.md +1 -1
- package/skills/hr-audit-choices/SKILL.md +1 -1
- package/skills/hr-benchmarking/SKILL.md +122 -159
- package/skills/hr-benchmarking/references/javascript.md +113 -0
- package/skills/hr-benchmarking/references/tools.md +62 -57
- package/skills/hr-better-accessibility/SKILL.md +1 -1
- package/skills/hr-better-accessibility/screen-readers.md +1 -0
- package/skills/hr-code-review/SKILL.md +19 -3
- package/skills/hr-code-review/agents/openai.yaml +1 -1
- package/skills/hr-code-review/references/acp-client.md +43 -0
- package/skills/{hr-change-impact/SKILL.md → hr-code-review/references/impact.md} +2 -12
- package/skills/hr-code-review/references/panel-adjudication.md +58 -0
- package/skills/hr-code-review/references/panel-plan.md +61 -0
- package/skills/hr-code-review/references/panel-recommendations.json +105 -0
- package/skills/hr-code-review/references/panel-recommendations.md +46 -0
- package/skills/hr-code-review/references/panel-setup.md +54 -0
- package/skills/hr-code-review/references/reviewer-role.md +31 -0
- package/skills/hr-code-review/scripts/acp-panel-client.mjs +299 -0
- package/skills/hr-code-review/scripts/check-receipts.mjs +263 -0
- package/skills/hr-code-review/scripts/panel-plan.mjs +69 -0
- package/skills/hr-code-review/scripts/panel-preferences.mjs +220 -0
- package/skills/hr-design-exploration/SKILL.md +24 -1
- package/skills/hr-design-exploration/agents/openai.yaml +2 -2
- package/skills/hr-diagnosing-bugs/SKILL.md +2 -2
- package/skills/hr-diagnosing-bugs/references/reporting.md +29 -4
- package/skills/hr-diagnosing-bugs/scripts/check-pr-offer.mjs +47 -0
- package/skills/hr-explain-code/SKILL.md +1 -1
- package/skills/hr-extract-shared-steps/SKILL.md +1 -1
- package/skills/hr-forgotten-work-audit/SKILL.md +130 -0
- package/skills/hr-forgotten-work-audit/references/brief-template.md +56 -0
- package/skills/hr-forgotten-work-audit/references/result-format.md +46 -0
- package/skills/hr-handoff/LICENSE +21 -0
- package/skills/hr-handoff/NOTICE.md +29 -0
- package/skills/hr-handoff/SKILL.md +24 -1
- package/skills/hr-handoff/references/reconciliation.md +25 -6
- package/skills/hr-handoff/references/successor-thread.md +76 -0
- package/skills/hr-handoff/scripts/check-reconciliation.mjs +48 -6
- package/skills/hr-house-rules-setup/SKILL.md +22 -8
- package/skills/hr-lean-plan/SKILL.md +4 -0
- package/skills/hr-maintainability-review/SKILL.md +14 -4
- package/skills/hr-maintainability-review/agents/openai.yaml +1 -1
- package/skills/hr-manual-qa/SKILL.md +1 -1
- package/skills/hr-ordering-tests/SKILL.md +1 -1
- package/skills/hr-plain-prose/SKILL.md +7 -4
- package/skills/hr-pr-followup/SKILL.md +6 -3
- package/skills/hr-pr-followup/references/review-helpers.md +40 -7
- package/skills/hr-pr-followup/scripts/check-landing.py +204 -0
- package/skills/hr-pr-followup/scripts/check-pr-review.ts +19 -8
- package/skills/hr-pr-followup/scripts/merge-pr.sh +100 -0
- package/skills/hr-pr-followup/scripts/verify-landed.sh +184 -0
- package/skills/hr-read-reddit/SKILL.md +1 -1
- package/skills/hr-research-shared-links/SKILL.md +103 -0
- package/skills/hr-sqlite/SKILL.md +174 -0
- package/skills/hr-sqlite/agents/openai.yaml +2 -0
- package/skills/hr-sqlite/references/evidence.md +94 -0
- package/skills/hr-sqlite/references/tool-decisions.md +39 -0
- package/skills/hr-sqlite/scripts/inspect-sqlite.py +245 -0
- package/skills/hr-stock-ui-audit/SKILL.md +1 -1
- package/skills/hr-test-audit/LICENSE +21 -0
- package/skills/hr-test-audit/NOTICE.md +67 -0
- package/skills/hr-usage-report/SKILL.md +90 -0
- package/skills/hr-usage-report/agents/openai.yaml +2 -0
- package/skills/hr-usage-report/assets/example-report.json +125 -0
- package/skills/hr-usage-report/assets/report-template.html +377 -0
- package/skills/hr-usage-report/scripts/render-report.ts +86 -0
- package/skills/hr-usage-report/scripts/usage-by-model.ts +499 -0
- package/skills/hr-what-next/SKILL.md +100 -10
- package/skills/hr-what-next/references/reporting.md +13 -22
- package/skills/hr-what-next/scripts/check-open-work.mjs +711 -0
- package/skills/hr-writing-pr/SKILL.md +29 -14
- package/skills/hr-writing-pr/scripts/benefit-chart.py +200 -0
- package/docs/npm-release.md +0 -45
- package/rules/modifiers/swarmail.md +0 -12
- package/skills/hr-benchmarking/references/priority-findings-20261006.md +0 -115
- package/skills/hr-change-impact/agents/openai.yaml +0 -5
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Report whether a checkout's work has landed in the base branch.
|
|
3
|
+
|
|
4
|
+
Squash-aware: a session branch whose commits were squash-merged is never an
|
|
5
|
+
ancestor of the base, so ancestry alone would cry unlanded forever. The check
|
|
6
|
+
therefore compares content: files unique to the checkout must be identical in
|
|
7
|
+
the base. Read-only; never fetches — the caller fetches first.
|
|
8
|
+
|
|
9
|
+
It also lists live processes whose working directory is inside the checkout
|
|
10
|
+
(Linux /proc only), excluding the checker's ancestors and its pipeline. Each listed process
|
|
11
|
+
blocks worktree removal unless its PID is one you recorded when you started it. `own_session`
|
|
12
|
+
is true only for a process that descends from the nearest agent CLI above the checker (a
|
|
13
|
+
process whose comm is one of AGENT_COMMS); a process under another session of the same agent
|
|
14
|
+
host's server, or any process when the checker has no agent ancestor, is false. A process
|
|
15
|
+
reparented to init after its parent exited is false too.
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
python3 scripts/check-landing.py [--path <checkout>] [--base <ref>]
|
|
19
|
+
python3 scripts/check-landing.py --path <checkout> --fail-on-unlanded
|
|
20
|
+
|
|
21
|
+
Without --base the base is origin/HEAD (the remote's default branch, which
|
|
22
|
+
need not be main), falling back to origin/main when origin/HEAD is unset.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import json
|
|
27
|
+
import os
|
|
28
|
+
import subprocess
|
|
29
|
+
import sys
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def git(path, *args):
|
|
33
|
+
return subprocess.run(
|
|
34
|
+
["git", *args],
|
|
35
|
+
cwd=path,
|
|
36
|
+
capture_output=True,
|
|
37
|
+
text=True,
|
|
38
|
+
timeout=60,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def default_base(path):
|
|
43
|
+
"""origin/HEAD's target (e.g. origin/fork/consolidated), else origin/main."""
|
|
44
|
+
r = git(path, "symbolic-ref", "--quiet", "--short",
|
|
45
|
+
"refs/remotes/origin/HEAD")
|
|
46
|
+
return r.stdout.strip() if r.returncode == 0 and r.stdout.strip() \
|
|
47
|
+
else "origin/main"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _ppid(pid):
|
|
51
|
+
try:
|
|
52
|
+
with open(f"/proc/{pid}/stat", encoding="utf-8", errors="replace") as f:
|
|
53
|
+
return int(f.read().rsplit(")", 1)[1].split()[1])
|
|
54
|
+
except (OSError, ValueError, IndexError):
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# comm names of the agent CLIs. "opencode.exe" is matched without its suffix.
|
|
59
|
+
AGENT_COMMS = {"claude", "codex", "cursor-agent", "opencode", "grok", "devin", "agy"}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _comm(pid):
|
|
63
|
+
try:
|
|
64
|
+
with open(f"/proc/{pid}/comm", encoding="utf-8", errors="replace") as f:
|
|
65
|
+
return f.read().strip()
|
|
66
|
+
except OSError:
|
|
67
|
+
return ""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _chain(pid):
|
|
71
|
+
"""pid and its parents up to, but excluding, PID 1."""
|
|
72
|
+
chain = []
|
|
73
|
+
while pid > 1 and pid not in chain:
|
|
74
|
+
chain.append(pid)
|
|
75
|
+
pid = _ppid(pid)
|
|
76
|
+
return chain
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def nearest_agent(chain, read_comm=_comm):
|
|
80
|
+
"""First PID in chain whose comm is an agent CLI, else None."""
|
|
81
|
+
for pid in chain:
|
|
82
|
+
name = read_comm(pid)
|
|
83
|
+
if name.endswith(".exe"):
|
|
84
|
+
name = name[:-4]
|
|
85
|
+
if name in AGENT_COMMS:
|
|
86
|
+
return pid
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def checkout_processes(path):
|
|
91
|
+
"""Processes whose cwd is inside path, or None without /proc."""
|
|
92
|
+
if not os.path.isdir("/proc/self"):
|
|
93
|
+
return None
|
|
94
|
+
root = os.path.realpath(path)
|
|
95
|
+
mine = _chain(os.getpid())
|
|
96
|
+
ancestors = set(mine)
|
|
97
|
+
agent = nearest_agent(mine)
|
|
98
|
+
own_group = os.getpgid(0)
|
|
99
|
+
found = []
|
|
100
|
+
for entry in os.listdir("/proc"):
|
|
101
|
+
if not entry.isdigit() or int(entry) in ancestors:
|
|
102
|
+
continue
|
|
103
|
+
try:
|
|
104
|
+
if os.getpgid(int(entry)) == own_group:
|
|
105
|
+
continue
|
|
106
|
+
cwd = os.readlink(f"/proc/{entry}/cwd")
|
|
107
|
+
with open(f"/proc/{entry}/comm", encoding="utf-8", errors="replace") as f:
|
|
108
|
+
comm = f.read().strip()
|
|
109
|
+
except (OSError, ProcessLookupError):
|
|
110
|
+
continue
|
|
111
|
+
if cwd != root and not cwd.startswith(root + os.sep):
|
|
112
|
+
continue
|
|
113
|
+
own = agent is not None and agent in _chain(int(entry))
|
|
114
|
+
found.append({"pid": int(entry), "own_session": own,
|
|
115
|
+
"comm": comm})
|
|
116
|
+
return found
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def landing_state(path, base):
|
|
120
|
+
"""Return a JSON-serializable report dict for the checkout at path."""
|
|
121
|
+
report = {"path": str(path), "base": base, "ok": False, "reason": ""}
|
|
122
|
+
r = git(path, "rev-parse", "--verify", "--quiet", base)
|
|
123
|
+
if r.returncode != 0:
|
|
124
|
+
report["reason"] = f"base ref missing: {base}"
|
|
125
|
+
return report
|
|
126
|
+
report["base_head"] = r.stdout.strip()
|
|
127
|
+
r = git(path, "rev-parse", "HEAD")
|
|
128
|
+
if r.returncode != 0:
|
|
129
|
+
report["reason"] = "not a git checkout (no HEAD)"
|
|
130
|
+
return report
|
|
131
|
+
report["head"] = r.stdout.strip()
|
|
132
|
+
r = git(path, "symbolic-ref", "--quiet", "--short", "HEAD")
|
|
133
|
+
report["branch"] = r.stdout.strip() if r.returncode == 0 else None
|
|
134
|
+
r = git(path, "status", "--porcelain")
|
|
135
|
+
if r.returncode != 0:
|
|
136
|
+
report["reason"] = "git status failed"
|
|
137
|
+
return report
|
|
138
|
+
report["clean"] = r.stdout.strip() == ""
|
|
139
|
+
report["processes"] = checkout_processes(path)
|
|
140
|
+
r = git(path, "merge-base", "--is-ancestor", "HEAD", base)
|
|
141
|
+
report["ancestor"] = r.returncode == 0
|
|
142
|
+
if report["ancestor"]:
|
|
143
|
+
report["content_landed"] = True
|
|
144
|
+
else:
|
|
145
|
+
r = git(path, "merge-base", base, "HEAD")
|
|
146
|
+
if r.returncode != 0:
|
|
147
|
+
report["reason"] = "no merge base with base ref"
|
|
148
|
+
return report
|
|
149
|
+
mb = r.stdout.strip()
|
|
150
|
+
r = git(path, "diff", "--name-only", "--no-renames", "-z", mb, "HEAD")
|
|
151
|
+
if r.returncode != 0:
|
|
152
|
+
report["reason"] = "git diff failed"
|
|
153
|
+
return report
|
|
154
|
+
files = [f for f in r.stdout.split("\0") if f]
|
|
155
|
+
report["unique_files"] = len(files)
|
|
156
|
+
if not files:
|
|
157
|
+
report["content_landed"] = True
|
|
158
|
+
else:
|
|
159
|
+
r = git(path, "diff", "--quiet", "HEAD", base, "--", *files)
|
|
160
|
+
report["content_landed"] = r.returncode == 0
|
|
161
|
+
report["indeterminate"] = False
|
|
162
|
+
if not report["content_landed"]:
|
|
163
|
+
# Base may have advanced on the same files after the merge
|
|
164
|
+
# base: then a diff is expected even for landed content.
|
|
165
|
+
r = git(path, "diff", "--name-only", "--no-renames", "-z", mb, base,
|
|
166
|
+
"--", *files)
|
|
167
|
+
if r.returncode == 0 and any(
|
|
168
|
+
f for f in r.stdout.split("\0") if f):
|
|
169
|
+
report["indeterminate"] = True
|
|
170
|
+
landed = bool(report["content_landed"])
|
|
171
|
+
if landed and report["clean"]:
|
|
172
|
+
report["ok"] = True
|
|
173
|
+
report["reason"] = "landed and clean"
|
|
174
|
+
elif landed:
|
|
175
|
+
report["reason"] = "content landed but tree is dirty"
|
|
176
|
+
elif report.get("indeterminate"):
|
|
177
|
+
report["reason"] = ("indeterminate - base advanced on touched "
|
|
178
|
+
"files, confirm PR")
|
|
179
|
+
else:
|
|
180
|
+
report["reason"] = "unlanded content vs base"
|
|
181
|
+
return report
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def main(argv=None):
|
|
185
|
+
ap = argparse.ArgumentParser(description=__doc__)
|
|
186
|
+
ap.add_argument("--path", default=".", help="checkout to inspect")
|
|
187
|
+
ap.add_argument(
|
|
188
|
+
"--base", help="base ref (default: origin/HEAD, else origin/main)"
|
|
189
|
+
)
|
|
190
|
+
ap.add_argument(
|
|
191
|
+
"--fail-on-unlanded",
|
|
192
|
+
action="store_true",
|
|
193
|
+
help="exit 1 unless the checkout is clean and landed",
|
|
194
|
+
)
|
|
195
|
+
args = ap.parse_args(argv)
|
|
196
|
+
report = landing_state(args.path, args.base or default_base(args.path))
|
|
197
|
+
print(json.dumps(report, indent=2))
|
|
198
|
+
if args.fail_on_unlanded and not report["ok"]:
|
|
199
|
+
return 1
|
|
200
|
+
return 0
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
if __name__ == "__main__":
|
|
204
|
+
sys.exit(main())
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// Refuses to land a pull request while review feedback is open: an unresolved review thread, a
|
|
3
|
-
// review bot (CodeRabbit) whose check is still running, or a bot review whose body carries findings
|
|
3
|
+
// review bot (CodeRabbit or cubic) whose check is still running, or a bot review whose body carries findings
|
|
4
4
|
// GitHub cannot show as threads ("Outside diff range comments", "Nitpick comments", "Duplicate comments") with no human
|
|
5
5
|
// explicit acknowledgement for that review and the current head. Run only when the repository
|
|
6
6
|
// selects this gate. It also refuses when CodeRabbit's status on the
|
|
7
7
|
// head commit says anything but "Review completed": CodeRabbit marks a skipped review (for example
|
|
8
8
|
// on a non-default base) as success with "Review skipped: …". When the head commit has
|
|
9
|
-
// `.coderabbit.yaml` but no CodeRabbit check, it refuses too.
|
|
9
|
+
// `.coderabbit.yaml` but no CodeRabbit check, it refuses too. A cubic check ("cubic · AI code
|
|
10
|
+
// reviewer") on the head commit must also complete with SUCCESS: a running one refuses, and so does
|
|
11
|
+
// any other conclusion, NEUTRAL and skipped included, because NEUTRAL means cubic did not review the
|
|
12
|
+
// head ("Incremental review could not start", "AI review line limit reached"). That PR stays blocked
|
|
13
|
+
// until someone comments `@cubic review` or the limit clears. A head commit with no cubic check is
|
|
14
|
+
// not blocked, since cubic's `cubic.yaml` is optional and its absence does not show that cubic
|
|
15
|
+
// is expected, and --wait does not wait for one to appear. Missing or incomplete reviews
|
|
10
16
|
// require inspection of the review integration and the repository's selected review policy.
|
|
11
17
|
// bun scripts/check-pr-review.ts <pr> [--repo <owner>/<name>] [--wait [--timeout <minutes>]]
|
|
12
18
|
// Exit 1 while feedback is open, 2 when gh fails. Without --repo, gh resolves the repository from
|
|
13
|
-
// the current checkout. --wait polls every 30 s while CodeRabbit's review is still running, and for
|
|
19
|
+
// the current checkout. --wait polls every 30 s while CodeRabbit's or cubic's review is still running, and for
|
|
14
20
|
// up to 3 minutes while the head commit has no CodeRabbit check yet (CodeRabbit posts it a little
|
|
15
21
|
// after a push), then reports as usual; after --timeout minutes (default 30) it reports what is
|
|
16
22
|
// still open.
|
|
@@ -30,13 +36,17 @@
|
|
|
30
36
|
import { spawnSync } from "node:child_process";
|
|
31
37
|
import { parseArgs } from "node:util";
|
|
32
38
|
|
|
33
|
-
const
|
|
39
|
+
const CODERABBIT = /coderabbit/i;
|
|
40
|
+
const CUBIC = /^cubic · AI code reviewer$/i;
|
|
41
|
+
const isReviewBot = (name: string) => CODERABBIT.test(name) || CUBIC.test(name);
|
|
34
42
|
const REVIEW_BOT_CONFIG = ".coderabbit.yaml";
|
|
35
43
|
const BODY_FINDINGS = /Outside diff range comments|Nitpick comments|Duplicate comments/;
|
|
36
44
|
const REVIEWED = /^Review completed/;
|
|
37
45
|
const FOLLOW_UP = "check the review integration and repository review policy";
|
|
38
46
|
const RUNNING = "review still running: ";
|
|
39
47
|
const NO_CHECK = "no CodeRabbit check on the head commit";
|
|
48
|
+
// A neutral cubic result means it did not review the head; a person has to ask again.
|
|
49
|
+
const CUBIC_RETRY = "comment `@cubic review` or wait for its limit to clear";
|
|
40
50
|
const POLL_MS = 30_000;
|
|
41
51
|
const NO_CHECK_GRACE_MS = 180_000;
|
|
42
52
|
|
|
@@ -364,25 +374,26 @@ export function openFeedback(pr: ReviewState): string[] {
|
|
|
364
374
|
const head = pr.commits.nodes[0]?.commit;
|
|
365
375
|
const contexts = head?.statusCheckRollup?.contexts.nodes ?? [];
|
|
366
376
|
const hasConfig = head?.tree.entries.some((e) => e.name === REVIEW_BOT_CONFIG) ?? false;
|
|
367
|
-
if (hasConfig && !contexts.some((ctx) =>
|
|
377
|
+
if (hasConfig && !contexts.some((ctx) => CODERABBIT.test(ctx.context ?? ctx.name ?? ""))) {
|
|
368
378
|
open.push(`${NO_CHECK}: ${FOLLOW_UP}`);
|
|
369
379
|
}
|
|
370
380
|
for (const ctx of contexts) {
|
|
371
381
|
const name = ctx.context ?? ctx.name ?? "";
|
|
372
382
|
const running =
|
|
373
383
|
ctx.state === "PENDING" || (ctx.status !== undefined && ctx.status !== "COMPLETED");
|
|
374
|
-
if (!
|
|
384
|
+
if (!isReviewBot(name)) {
|
|
375
385
|
continue;
|
|
376
386
|
}
|
|
377
387
|
if (running) {
|
|
378
388
|
open.push(`${RUNNING}${name}`);
|
|
379
389
|
} else if (ctx.name !== undefined && ctx.conclusion !== "SUCCESS") {
|
|
390
|
+
const retry = CUBIC.test(name) ? `; ${CUBIC_RETRY}` : "";
|
|
380
391
|
open.push(
|
|
381
|
-
`${name} review concluded ${ctx.conclusion ?? "(missing conclusion)"}: ${FOLLOW_UP}`,
|
|
392
|
+
`${name} review concluded ${ctx.conclusion ?? "(missing conclusion)"}: ${FOLLOW_UP}${retry}`,
|
|
382
393
|
);
|
|
383
394
|
} else if (
|
|
384
395
|
ctx.context !== undefined &&
|
|
385
|
-
(ctx.state !== "SUCCESS" || !REVIEWED.test(ctx.description ?? ""))
|
|
396
|
+
(ctx.state !== "SUCCESS" || (CODERABBIT.test(name) && !REVIEWED.test(ctx.description ?? "")))
|
|
386
397
|
) {
|
|
387
398
|
open.push(
|
|
388
399
|
`${name} did not review the head commit ("${ctx.description ?? ""}"): ${FOLLOW_UP}`,
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Squash-merge a pull request (or, with --merge, merge-commit it), delete its remote head, and verify the landing, without gh's
|
|
3
|
+
# --delete-branch. Since gh 2.98 that flag runs `git worktree remove` on any linked worktree that
|
|
4
|
+
# has the head branch checked out, another session's included when it is clean, and switches the
|
|
5
|
+
# current checkout to the base when the head is checked out here. This script never removes a
|
|
6
|
+
# worktree or local branch: it lists the worktrees still on the head so their owners clean up.
|
|
7
|
+
#
|
|
8
|
+
# Refuses a PR that is not open, one with open review feedback (check-pr-review.ts: an unresolved
|
|
9
|
+
# thread, a review bot still running, unanswered findings in a bot's review body), or one another
|
|
10
|
+
# open PR targets (deleting the head would close that PR; retarget it first with
|
|
11
|
+
# `gh pr edit <n> --base <base>`). The merge passes
|
|
12
|
+
# --match-head-commit, so a push after the head was read fails the merge instead of landing it.
|
|
13
|
+
#
|
|
14
|
+
# Refuses, after those checks, a repository listed in HR_PUSH_LANDED (space-separated owner/repo names;
|
|
15
|
+
# empty by default): its own guidance lands by fast-forwarding the base to the head, because a GitHub
|
|
16
|
+
# merge writes the account's profile name into public history. It prints that push instead.
|
|
17
|
+
#
|
|
18
|
+
# Before merging, verifies that the local default can be synchronized, even if it is current.
|
|
19
|
+
# Refuses a dirty/diverged target or a default held in another checkout without --idle-primary.
|
|
20
|
+
# After merging, fast-forwards the confirmed local default and fails if it is still stale.
|
|
21
|
+
# To update the primary checkout from a task worktree, pass --idle-primary <path>
|
|
22
|
+
# using existing ownership evidence or the operator's standing idle direction, unless current
|
|
23
|
+
# evidence contradicts it; keep it idle through landing.
|
|
24
|
+
# If verification
|
|
25
|
+
# fails after "merged", rerun verify-landed.sh with the printed commit, not this script.
|
|
26
|
+
#
|
|
27
|
+
# --merge lands with a merge commit instead of a squash. Use it only for a PR whose second parent
|
|
28
|
+
# must stay in history, such as an `-s ours` merge that records an upstream reconcile; a squash
|
|
29
|
+
# would drop that parent. Every other PR is squashed.
|
|
30
|
+
#
|
|
31
|
+
# Usage: bash scripts/merge-pr.sh <pr> [--merge] [--remote <name>] [--idle-primary <path>]
|
|
32
|
+
set -euo pipefail
|
|
33
|
+
|
|
34
|
+
PUSH_LANDED=" ${HR_PUSH_LANDED:-} "
|
|
35
|
+
|
|
36
|
+
PR="${1:?usage: merge-pr.sh <pr> [--merge] [--remote <name>] [--idle-primary <path>]}"
|
|
37
|
+
shift
|
|
38
|
+
REMOTE=origin
|
|
39
|
+
STRATEGY=--squash
|
|
40
|
+
SYNC_ARGS=(--ff)
|
|
41
|
+
while [ $# -gt 0 ]; do
|
|
42
|
+
case "$1" in
|
|
43
|
+
--merge) STRATEGY=--merge; shift;;
|
|
44
|
+
--remote) REMOTE="${2:?}"; shift 2;;
|
|
45
|
+
--idle-primary) SYNC_ARGS+=(--idle-primary "${2:?}"); shift 2;;
|
|
46
|
+
*) echo "unknown argument: $1" >&2; exit 2;;
|
|
47
|
+
esac
|
|
48
|
+
done
|
|
49
|
+
|
|
50
|
+
IFS=$'\t' read -r HEAD OID BASE STATE FORK < <(gh pr view "$PR" \
|
|
51
|
+
--json headRefName,headRefOid,baseRefName,state,isCrossRepository \
|
|
52
|
+
--jq '[.headRefName, .headRefOid, .baseRefName, .state, .isCrossRepository] | @tsv')
|
|
53
|
+
[ "$STATE" = OPEN ] || { echo "PR $PR is $STATE, not OPEN" >&2; exit 1; }
|
|
54
|
+
|
|
55
|
+
DEPENDENTS="$(gh pr list --state open --base "$HEAD" --json number --jq '.[].number' | tr '\n' ' ')"
|
|
56
|
+
if [ -n "${DEPENDENTS// /}" ]; then
|
|
57
|
+
echo "open PRs target $HEAD: ${DEPENDENTS% }; retarget them first: gh pr edit <n> --base $BASE" >&2
|
|
58
|
+
exit 1
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
bun "$(dirname "${BASH_SOURCE[0]}")/check-pr-review.ts" "$PR"
|
|
62
|
+
|
|
63
|
+
REPO="$(gh repo view --json nameWithOwner --jq .nameWithOwner)"
|
|
64
|
+
if [[ "$PUSH_LANDED" == *" $REPO "* ]]; then
|
|
65
|
+
echo "$REPO lands by fast-forward push, not a GitHub merge (see its AGENTS.md). Run:" >&2
|
|
66
|
+
echo " git push $REMOTE $HEAD:$BASE && git push $REMOTE --delete $HEAD" >&2
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
bash "$(dirname "${BASH_SOURCE[0]}")/verify-landed.sh" --preflight \
|
|
71
|
+
--default "$BASE" --remote "$REMOTE" "${SYNC_ARGS[@]}"
|
|
72
|
+
|
|
73
|
+
gh pr merge "$PR" "$STRATEGY" --match-head-commit "$OID"
|
|
74
|
+
SHA="$(gh pr view "$PR" --json mergeCommit --jq '.mergeCommit.oid // empty')"
|
|
75
|
+
if ! [[ "$SHA" =~ ^([0-9a-f]{40}|[0-9a-f]{64})$ ]]; then
|
|
76
|
+
echo "PR $PR has no merge commit yet (a merge queue may still hold it); leaving its head branch alone" >&2
|
|
77
|
+
exit 1
|
|
78
|
+
fi
|
|
79
|
+
echo "merged: PR $PR as $SHA"
|
|
80
|
+
|
|
81
|
+
# A fork's head lives in another repository; a repository may also delete merged heads itself.
|
|
82
|
+
# Delete only a branch that still sits at the pull request's head commit, so a same-named branch on
|
|
83
|
+
# an unrelated remote survives.
|
|
84
|
+
if [ "$FORK" != true ]; then
|
|
85
|
+
REMOTE_HEAD="$(git ls-remote --heads "$REMOTE" "refs/heads/$HEAD" | awk '{print $1}')"
|
|
86
|
+
if [ "$REMOTE_HEAD" = "$OID" ]; then
|
|
87
|
+
git push --quiet --force-with-lease="refs/heads/$HEAD:$OID" "$REMOTE" --delete "$HEAD"
|
|
88
|
+
echo "deleted: $REMOTE/$HEAD"
|
|
89
|
+
elif [ -n "$REMOTE_HEAD" ]; then
|
|
90
|
+
echo "kept: $REMOTE/$HEAD is at $REMOTE_HEAD, not the pull request head $OID" >&2
|
|
91
|
+
fi
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
bash "$(dirname "${BASH_SOURCE[0]}")/verify-landed.sh" --commit "$SHA" --branch "$HEAD" \
|
|
95
|
+
--default "$BASE" --remote "$REMOTE" "${SYNC_ARGS[@]}"
|
|
96
|
+
|
|
97
|
+
git worktree list --porcelain | awk -v want="branch refs/heads/$HEAD" '
|
|
98
|
+
/^worktree / { path = substr($0, 10) }
|
|
99
|
+
$0 == want { print "still on " substr(want, 19) ": " path " (its owner retires it)" }
|
|
100
|
+
'
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Post-merge landing checks: containment, local-default state, leftover remote branches.
|
|
3
|
+
# Fetches and verifies both remote containment and local-default equality. A stale local default
|
|
4
|
+
# fails even when the remote contains the commit. Never resets, switches or deletes.
|
|
5
|
+
# With --ff it updates a default checked out nowhere or here with a clean tree.
|
|
6
|
+
# --idle-primary <path> additionally permits a fast-forward of this repository's primary checkout:
|
|
7
|
+
# use existing ownership evidence or the operator's standing idle direction, unless current
|
|
8
|
+
# evidence contradicts it. Git preserves
|
|
9
|
+
# unrelated local edits and refuses a fast-forward that would overwrite them. It moves the
|
|
10
|
+
# primary to the verified remote commit with `merge --ff-only <sha>` rather than `pull`, because
|
|
11
|
+
# `pull` re-fetches into the shared FETCH_HEAD, which a concurrent fetch in the same checkout can
|
|
12
|
+
# overwrite ("Cannot fast-forward to multiple branches"). Other linked worktrees
|
|
13
|
+
# remain their owners' responsibility. After a blocked post-merge check, rerun this script,
|
|
14
|
+
# not merge-pr.sh: the PR is already merged.
|
|
15
|
+
# --preflight checks synchronization eligibility before landing, without moving local branches.
|
|
16
|
+
# It requires --ff and rejects dirty or diverged targets and unconfirmed checkout ownership,
|
|
17
|
+
# even when the local default is current. The caller must keep the target idle until landing
|
|
18
|
+
# finishes; this check cannot establish session ownership or prevent another process starting.
|
|
19
|
+
# Usage:
|
|
20
|
+
# bash scripts/verify-landed.sh --commit <sha> [--default <branch>]
|
|
21
|
+
# [--branch <merged-head>] [--remote <name>] [--ff [--idle-primary <path>]]
|
|
22
|
+
# bash scripts/verify-landed.sh --preflight --ff [--default <branch>]
|
|
23
|
+
# [--remote <name>] [--idle-primary <path>]
|
|
24
|
+
set -euo pipefail
|
|
25
|
+
|
|
26
|
+
REMOTE=origin
|
|
27
|
+
DEFAULT=""
|
|
28
|
+
COMMIT=""
|
|
29
|
+
BRANCH=""
|
|
30
|
+
FF=0
|
|
31
|
+
IDLE_PRIMARY=""
|
|
32
|
+
PREFLIGHT=0
|
|
33
|
+
|
|
34
|
+
while [ $# -gt 0 ]; do
|
|
35
|
+
case "$1" in
|
|
36
|
+
--commit) COMMIT="${2:?}"; shift 2;;
|
|
37
|
+
--default) DEFAULT="${2:?}"; shift 2;;
|
|
38
|
+
--branch) BRANCH="${2:?}"; shift 2;;
|
|
39
|
+
--remote) REMOTE="${2:?}"; shift 2;;
|
|
40
|
+
--ff) FF=1; shift;;
|
|
41
|
+
--idle-primary) IDLE_PRIMARY="${2:?}"; shift 2;;
|
|
42
|
+
--preflight) PREFLIGHT=1; shift;;
|
|
43
|
+
*) echo "unknown argument: $1" >&2; exit 2;;
|
|
44
|
+
esac
|
|
45
|
+
done
|
|
46
|
+
if [ "$PREFLIGHT" = 1 ]; then
|
|
47
|
+
[ "$FF" = 1 ] && [ -z "$COMMIT$BRANCH" ] \
|
|
48
|
+
|| { echo "--preflight requires --ff and excludes --commit/--branch" >&2; exit 2; }
|
|
49
|
+
else
|
|
50
|
+
[ -n "$COMMIT" ] || { echo "missing --commit <sha>" >&2; exit 2; }
|
|
51
|
+
fi
|
|
52
|
+
if [ -n "$IDLE_PRIMARY" ]; then
|
|
53
|
+
[ "$FF" = 1 ] || { echo "--idle-primary requires --ff" >&2; exit 2; }
|
|
54
|
+
PRIMARY="$(git worktree list --porcelain | sed -n '1s/^worktree //p')"
|
|
55
|
+
IDLE_PRIMARY="$(git -C "$IDLE_PRIMARY" rev-parse --show-toplevel)"
|
|
56
|
+
[ "$IDLE_PRIMARY" = "$PRIMARY" ] \
|
|
57
|
+
|| { echo "--idle-primary is not this repository's primary checkout: $IDLE_PRIMARY" >&2; exit 2; }
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
if [ -z "$DEFAULT" ]; then
|
|
61
|
+
# The remote's own answer wins; the cached origin/HEAD goes stale when the default branch changes.
|
|
62
|
+
DEFAULT="$(git ls-remote --symref "$REMOTE" HEAD 2>/dev/null | sed -n 's#^ref: refs/heads/\([^[:space:]]*\).*#\1#p' || true)"
|
|
63
|
+
if [ -z "$DEFAULT" ]; then
|
|
64
|
+
DEFAULT="$(git symbolic-ref "refs/remotes/$REMOTE/HEAD" 2>/dev/null | sed "s#^refs/remotes/$REMOTE/##" || true)"
|
|
65
|
+
fi
|
|
66
|
+
[ -n "$DEFAULT" ] || { echo "cannot resolve remote default branch; pass --default" >&2; exit 2; }
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
git fetch "$REMOTE" "+refs/heads/$DEFAULT:refs/remotes/$REMOTE/$DEFAULT" >/dev/null 2>&1 \
|
|
70
|
+
|| { echo "fetch failed: $REMOTE $DEFAULT" >&2; exit 1; }
|
|
71
|
+
|
|
72
|
+
WTLS="$(git worktree list --porcelain)"
|
|
73
|
+
HOLDER="$(printf '%s\n' "$WTLS" | awk -v want="branch refs/heads/$DEFAULT" '
|
|
74
|
+
/^worktree / { path = substr($0, 10) }
|
|
75
|
+
$0 == want { print path; exit }
|
|
76
|
+
')"
|
|
77
|
+
HERE="$(git rev-parse --show-toplevel)"
|
|
78
|
+
|
|
79
|
+
if [ "$PREFLIGHT" = 1 ]; then
|
|
80
|
+
# A rebase or bisect detaches HEAD while still reserving its original branch.
|
|
81
|
+
if [ -z "$HOLDER" ]; then
|
|
82
|
+
while IFS= read -r RECORD; do
|
|
83
|
+
case "$RECORD" in worktree\ *) CHECKOUT="${RECORD#worktree }";; *) continue;; esac
|
|
84
|
+
[ -d "$CHECKOUT" ] || continue
|
|
85
|
+
for ASSOCIATION in rebase-merge/head-name rebase-apply/head-name BISECT_START; do
|
|
86
|
+
ASSOCIATION_PATH="$(git -C "$CHECKOUT" rev-parse --path-format=absolute --git-path "$ASSOCIATION")"
|
|
87
|
+
if [ -f "$ASSOCIATION_PATH" ]; then
|
|
88
|
+
IFS= read -r ASSOCIATED_BRANCH < "$ASSOCIATION_PATH"
|
|
89
|
+
if [ "$ASSOCIATED_BRANCH" = "refs/heads/$DEFAULT" ] || [ "$ASSOCIATED_BRANCH" = "$DEFAULT" ]; then
|
|
90
|
+
HOLDER="$CHECKOUT"
|
|
91
|
+
break 2
|
|
92
|
+
fi
|
|
93
|
+
fi
|
|
94
|
+
done
|
|
95
|
+
done <<< "$WTLS"
|
|
96
|
+
fi
|
|
97
|
+
if [ -n "$HOLDER" ]; then
|
|
98
|
+
if [ "$HOLDER" != "$HERE" ] && [ "$HOLDER" != "$IDLE_PRIMARY" ]; then
|
|
99
|
+
echo "sync blocked at $HOLDER: that checkout has the default branch. If nothing is using it, rerun with --idle-primary <primary-path>" >&2
|
|
100
|
+
exit 1
|
|
101
|
+
fi
|
|
102
|
+
STATUS="$(git -C "$HOLDER" status --porcelain)" \
|
|
103
|
+
|| { echo "sync blocked at $HOLDER: cannot inspect working tree" >&2; exit 1; }
|
|
104
|
+
[ -z "$STATUS" ] \
|
|
105
|
+
|| { echo "sync blocked at $HOLDER: working tree dirty; leave it for its owner" >&2; exit 1; }
|
|
106
|
+
for OPERATION in MERGE_HEAD CHERRY_PICK_HEAD REVERT_HEAD rebase-merge rebase-apply sequencer BISECT_START; do
|
|
107
|
+
OPERATION_PATH="$(git -C "$HOLDER" rev-parse --path-format=absolute --git-path "$OPERATION")"
|
|
108
|
+
[ ! -e "$OPERATION_PATH" ] \
|
|
109
|
+
|| { echo "sync blocked at $HOLDER: unfinished Git operation: $OPERATION" >&2; exit 1; }
|
|
110
|
+
done
|
|
111
|
+
fi
|
|
112
|
+
if git show-ref --verify --quiet "refs/heads/$DEFAULT"; then
|
|
113
|
+
git merge-base --is-ancestor "$DEFAULT" "$REMOTE/$DEFAULT" \
|
|
114
|
+
|| { echo "sync blocked: $DEFAULT cannot fast-forward to $REMOTE/$DEFAULT" >&2; exit 1; }
|
|
115
|
+
fi
|
|
116
|
+
echo "synchronization preflight passed: $DEFAULT (${HOLDER:-not checked out})"
|
|
117
|
+
exit 0
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
FAIL=0
|
|
121
|
+
|
|
122
|
+
if git merge-base --is-ancestor "$COMMIT" "$REMOTE/$DEFAULT" 2>/dev/null; then
|
|
123
|
+
echo "contained: $COMMIT in $REMOTE/$DEFAULT"
|
|
124
|
+
else
|
|
125
|
+
echo "NOT CONTAINED: $COMMIT missing from $REMOTE/$DEFAULT" >&2
|
|
126
|
+
FAIL=1
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
if git show-ref --verify --quiet "refs/heads/$DEFAULT"; then
|
|
130
|
+
LOCAL="$(git rev-parse "$DEFAULT")"
|
|
131
|
+
UPSTREAM="$(git rev-parse "$REMOTE/$DEFAULT")"
|
|
132
|
+
if [ "$LOCAL" = "$UPSTREAM" ]; then
|
|
133
|
+
echo "local $DEFAULT: current ($LOCAL)"
|
|
134
|
+
elif git merge-base --is-ancestor "$LOCAL" "$UPSTREAM" 2>/dev/null; then
|
|
135
|
+
BEHIND="$(git rev-list --count "$LOCAL..$UPSTREAM")"
|
|
136
|
+
if [ -z "${HOLDER:-}" ]; then
|
|
137
|
+
if [ "$FF" = 1 ] && [ "$FAIL" = 0 ]; then
|
|
138
|
+
git update-ref -m "verify-landed: fast-forward $DEFAULT" "refs/heads/$DEFAULT" "$UPSTREAM" "$LOCAL"
|
|
139
|
+
echo "local $DEFAULT: fast-forwarded $BEHIND commit(s) to $UPSTREAM (not checked out anywhere)"
|
|
140
|
+
else
|
|
141
|
+
echo "local $DEFAULT: $BEHIND behind, not checked out anywhere — safe: git fetch $REMOTE $DEFAULT:$DEFAULT"
|
|
142
|
+
fi
|
|
143
|
+
elif [ "$HOLDER" != "$HERE" ]; then
|
|
144
|
+
if [ "$FF" = 1 ] && [ "$FAIL" = 0 ] && [ "$HOLDER" = "$IDLE_PRIMARY" ]; then
|
|
145
|
+
[ "$(git -C "$HOLDER" symbolic-ref --quiet --short HEAD)" = "$DEFAULT" ] \
|
|
146
|
+
|| { echo "primary checkout changed branches; refusing update" >&2; exit 1; }
|
|
147
|
+
git -C "$HOLDER" merge --quiet --ff-only "$UPSTREAM"
|
|
148
|
+
echo "local $DEFAULT: fast-forwarded $BEHIND commit(s) in confirmed idle primary $HOLDER"
|
|
149
|
+
else
|
|
150
|
+
echo "local $DEFAULT: $BEHIND behind, checked out at $HOLDER (not here) — leave for its owner; if you know it is idle: git -C $HOLDER merge --ff-only $UPSTREAM"
|
|
151
|
+
echo "If nothing is using that checkout, rerun verify-landed.sh with --ff --idle-primary <primary-path>." >&2
|
|
152
|
+
fi
|
|
153
|
+
elif [ -n "$(git status --porcelain)" ]; then
|
|
154
|
+
echo "local $DEFAULT: $BEHIND behind but working tree dirty — report, do not touch"
|
|
155
|
+
elif [ "$FF" = 1 ] && [ "$FAIL" = 0 ]; then
|
|
156
|
+
git merge --quiet --ff-only "$UPSTREAM"
|
|
157
|
+
echo "local $DEFAULT: fast-forwarded $BEHIND commit(s) to $UPSTREAM (checked out here)"
|
|
158
|
+
else
|
|
159
|
+
echo "local $DEFAULT: $BEHIND behind, clean, checked out here — safe to fast-forward"
|
|
160
|
+
fi
|
|
161
|
+
else
|
|
162
|
+
echo "local $DEFAULT: diverged from $REMOTE/$DEFAULT — report, do not touch"
|
|
163
|
+
fi
|
|
164
|
+
if [ "$(git rev-parse "$DEFAULT")" != "$(git rev-parse "$REMOTE/$DEFAULT")" ]; then
|
|
165
|
+
echo "LOCAL DEFAULT NOT CURRENT: $DEFAULT differs from $REMOTE/$DEFAULT; landing incomplete" >&2
|
|
166
|
+
FAIL=1
|
|
167
|
+
fi
|
|
168
|
+
else
|
|
169
|
+
echo "local $DEFAULT: no local branch (nothing to update)"
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
if [ -n "$BRANCH" ]; then
|
|
173
|
+
if ! REMOTE_REFS="$(git ls-remote --heads "$REMOTE" "$BRANCH" 2>/dev/null)"; then
|
|
174
|
+
echo "remote branch lookup failed: $REMOTE $BRANCH" >&2
|
|
175
|
+
FAIL=1
|
|
176
|
+
elif [ -n "$REMOTE_REFS" ]; then
|
|
177
|
+
echo "leftover branch: $REMOTE/$BRANCH still exists after merge" >&2
|
|
178
|
+
FAIL=1
|
|
179
|
+
else
|
|
180
|
+
echo "no leftover branch: $REMOTE/$BRANCH gone"
|
|
181
|
+
fi
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
exit "$FAIL"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hr-read-reddit
|
|
3
|
-
description: Read Reddit
|
|
3
|
+
description: Read Reddit posts and comments from Reddit's public feeds (output can omit comments) and search subreddits, with links kept inline. Use whenever a task needs Reddit content or needs to collect links posted on Reddit; generic web fetch and reddit.com JSON are blocked for agents. Reddit's RSS support ends November 13, 2026; a partial search fallback remains.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Read Reddit
|