@bompus/house-rules 0.11.0 → 0.13.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 +91 -60
- 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 +27 -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 +21 -0
- package/skills/hr-pr-followup/scripts/check-landing.py +204 -0
- 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())
|
|
@@ -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
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hr-research-shared-links
|
|
3
|
+
description: "Research a shared link or repo in depth, compare it with the user's setup and rate each idea. Use when a session opens with a link or asks for thoughts or what to borrow. Not for factual lookups, and not for implementing or fixing things the link describes."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Research shared links
|
|
7
|
+
|
|
8
|
+
A shared link asks "is this worth anything to us?", not "what does it say?".
|
|
9
|
+
The failure to avoid is a first answer built from the page that was shared,
|
|
10
|
+
so the user has to ask again to "look closer", "compare it to ours" or "find
|
|
11
|
+
the hidden gems". Do that work the first time.
|
|
12
|
+
|
|
13
|
+
## Before reading
|
|
14
|
+
|
|
15
|
+
1. If the link looks unrelated to the session's work or carries stray
|
|
16
|
+
characters, say what you think it is and confirm before a long analysis.
|
|
17
|
+
2. For several links, give each its own verdict and add one synthesis.
|
|
18
|
+
3. Tell the user in one line what you will do: read the primary source, compare
|
|
19
|
+
with their setup, rate each idea.
|
|
20
|
+
|
|
21
|
+
## Read the source, not the pointer
|
|
22
|
+
|
|
23
|
+
1. Use the reader for the site when one is available (X posts, Reddit threads);
|
|
24
|
+
generic fetch truncates them.
|
|
25
|
+
2. Follow an article, post or video to what it is about: the repository, paper,
|
|
26
|
+
docs or data. The shared page is a claim; the source is the evidence.
|
|
27
|
+
3. For a repository, read past the README: agent guidance files, skills, docs,
|
|
28
|
+
tests and the code behind any headline feature. These hold the transferable
|
|
29
|
+
ideas. Search its open issues and security disclosures for the shared claim
|
|
30
|
+
and for each idea you may recommend, check every hit against the current
|
|
31
|
+
code, and read the latest release notes. Record the terms you searched. The
|
|
32
|
+
README says what the maintainer wants to be true; the tracker shows what
|
|
33
|
+
users and reviewers found, and a fix may already have landed.
|
|
34
|
+
4. Say what you did not read: videos, images, earlier posts in a thread,
|
|
35
|
+
private or login-gated pages, source behind a README claim. Do not rate what
|
|
36
|
+
you did not see.
|
|
37
|
+
5. Check headline claims against the project's own data. Recompute a cost or
|
|
38
|
+
speed figure, compare the README with the results file, and name each claim
|
|
39
|
+
that has no evidence behind it.
|
|
40
|
+
6. A license does not stop you borrowing an idea. If it bars an AI from reading
|
|
41
|
+
the material, flag that and offer another reader.
|
|
42
|
+
|
|
43
|
+
## Compare with what the user has
|
|
44
|
+
|
|
45
|
+
1. Search their rules, skills, docs and settings for the same idea before judging
|
|
46
|
+
it. "We lack this" needs a search showing the gap; "we have this" needs the
|
|
47
|
+
path.
|
|
48
|
+
2. Search the earlier sessions and decisions you can reach for standing
|
|
49
|
+
constraints and options already rejected. Name any records you could not
|
|
50
|
+
reach, and do not read their absence as "never decided". Do not recommend
|
|
51
|
+
what was ruled out without saying so and saying what changed.
|
|
52
|
+
3. State exactly what the comparison ran against: which fork or version of ours,
|
|
53
|
+
which release of theirs, the competitor's real name.
|
|
54
|
+
|
|
55
|
+
## Rate each idea
|
|
56
|
+
|
|
57
|
+
Give each idea one row: source path, the technique in one to three lines, what it
|
|
58
|
+
would change here and what that is worth (faster, cheaper, safer, clearer),
|
|
59
|
+
the evidence on both sides (their file and line, our search), and a verdict.
|
|
60
|
+
|
|
61
|
+
- **Adopt**: clear gain, fits a stated need, small to carry.
|
|
62
|
+
- **Consider**: real idea, unproven here or costly. Say what would settle it.
|
|
63
|
+
- **Skip**: already covered, ruled out, or tied to the vendor's model or product.
|
|
64
|
+
|
|
65
|
+
Separate the idea that transfers from the vendor and model details around it.
|
|
66
|
+
Weigh build against buy against borrow: a maintained tool that is good enough
|
|
67
|
+
beats building, and an idea is often worth taking without the tool. List the
|
|
68
|
+
ideas you dropped and why, so the recommended set and the table agree.
|
|
69
|
+
|
|
70
|
+
## Route what is worth keeping
|
|
71
|
+
|
|
72
|
+
1. Name the layer that owns each kept idea: shared portable rules, host or
|
|
73
|
+
machine setup, one project, or a portfolio-level record. Use the user's own
|
|
74
|
+
ownership map when they have one.
|
|
75
|
+
2. A deferred idea gets the condition that reopens it and a proposed home in
|
|
76
|
+
the owning repository; offer to save it there rather than leaving it only in
|
|
77
|
+
this reply.
|
|
78
|
+
3. If an idea means adopting something vendored, offer to record where it came
|
|
79
|
+
from so upstream changes are noticed. If evaluating it exposed an upstream
|
|
80
|
+
bug, offer to file it.
|
|
81
|
+
4. Wording adopted into rules describes behavior and carries no "borrowed from"
|
|
82
|
+
history. Provenance belongs in the research record.
|
|
83
|
+
|
|
84
|
+
## Answer
|
|
85
|
+
|
|
86
|
+
1. Lead with a table, one row per link or repository: what it is, its status in
|
|
87
|
+
the user's setup (have, partly, missing, ruled out) with the evidence, and the
|
|
88
|
+
verdict.
|
|
89
|
+
2. Then the ranked ideas, then what is not worth borrowing and why.
|
|
90
|
+
3. Explain jargon in plain words or a small diagram. Before changing anything
|
|
91
|
+
from this research, show the exact before and after wording or real sample
|
|
92
|
+
output.
|
|
93
|
+
4. End with offers under the user's offer rules. Candidates: write a research
|
|
94
|
+
note; test it against what they use on quality, speed or cost (give the
|
|
95
|
+
expected wall-clock time and check host load first); get independent opinions
|
|
96
|
+
from other models or a review panel, with a brief that includes your findings
|
|
97
|
+
and the candidates you rejected, choosing the reviewers yourself; build or
|
|
98
|
+
adopt one idea, saying which steps that option includes (file changes,
|
|
99
|
+
installing the tool, a pull request, landing) and which it leaves for a later
|
|
100
|
+
offer; stop here.
|
|
101
|
+
|
|
102
|
+
"Tell me more" and "thoughts?" authorize research and nothing else. Changing
|
|
103
|
+
files, installing the tool or opening a pull request needs the user's selection.
|