@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.
Files changed (111) hide show
  1. package/CODE_OF_CONDUCT.md +83 -0
  2. package/README.md +91 -60
  3. package/compose.mjs +8 -2
  4. package/composition.mjs +11 -3
  5. package/config-view.mjs +18 -3
  6. package/config.mjs +74 -13
  7. package/docs/assets/overview.png +0 -0
  8. package/docs/configuration.md +85 -9
  9. package/package.json +4 -2
  10. package/rules/core.md +118 -209
  11. package/rules/modifiers/backlog-outline.md +25 -0
  12. package/rules/modifiers/backlog-visual.md +16 -0
  13. package/rules/modifiers/coded-offers.md +47 -17
  14. package/rules/modifiers/cross-session-relay.md +23 -0
  15. package/rules/modifiers/low-quota-handoff.md +4 -3
  16. package/rules/modifiers/model-default-questions.md +33 -0
  17. package/rules/modifiers/multi-agent.md +6 -2
  18. package/rules/modifiers/no-attribution.md +7 -1
  19. package/rules/modifiers/periodic-updates.md +25 -0
  20. package/rules/modifiers/plan-files.md +35 -3
  21. package/rules/modifiers/release-batching.md +4 -2
  22. package/rules/modifiers/shared-host-load.md +13 -42
  23. package/rules/modifiers/squash-landing.md +1 -1
  24. package/rules/modifiers/upstream-contributions.md +45 -0
  25. package/rules/modifiers/web-image-formats.md +35 -0
  26. package/rules/references/backlog-view.md +154 -0
  27. package/rules/references/landing.md +14 -14
  28. package/rules/references/multi-agent.md +6 -2
  29. package/rules/references/release-batching.md +27 -4
  30. package/rules/references/squash-landing.md +10 -4
  31. package/rules/references/thread-visuals.md +45 -0
  32. package/setup.mjs +87 -17
  33. package/skills/hr-agent-guidance-audit/SKILL.md +10 -5
  34. package/skills/hr-agent-guidance-audit/references/prune-workflow.md +72 -0
  35. package/skills/hr-agent-guidance-audit/references/records-and-lessons.md +1 -1
  36. package/skills/hr-agent-guidance-audit/references/retirement-verdicts.md +49 -0
  37. package/skills/hr-agent-guidance-refresh/SKILL.md +25 -57
  38. package/skills/hr-agent-guidance-refresh/references/verification.md +1 -1
  39. package/skills/hr-audit-choices/SKILL.md +1 -1
  40. package/skills/hr-benchmarking/SKILL.md +122 -159
  41. package/skills/hr-benchmarking/references/javascript.md +113 -0
  42. package/skills/hr-benchmarking/references/tools.md +62 -57
  43. package/skills/hr-better-accessibility/SKILL.md +1 -1
  44. package/skills/hr-better-accessibility/screen-readers.md +1 -0
  45. package/skills/hr-code-review/SKILL.md +19 -3
  46. package/skills/hr-code-review/agents/openai.yaml +1 -1
  47. package/skills/hr-code-review/references/acp-client.md +43 -0
  48. package/skills/{hr-change-impact/SKILL.md → hr-code-review/references/impact.md} +2 -12
  49. package/skills/hr-code-review/references/panel-adjudication.md +58 -0
  50. package/skills/hr-code-review/references/panel-plan.md +61 -0
  51. package/skills/hr-code-review/references/panel-recommendations.json +105 -0
  52. package/skills/hr-code-review/references/panel-recommendations.md +46 -0
  53. package/skills/hr-code-review/references/panel-setup.md +54 -0
  54. package/skills/hr-code-review/references/reviewer-role.md +31 -0
  55. package/skills/hr-code-review/scripts/acp-panel-client.mjs +299 -0
  56. package/skills/hr-code-review/scripts/check-receipts.mjs +263 -0
  57. package/skills/hr-code-review/scripts/panel-plan.mjs +69 -0
  58. package/skills/hr-code-review/scripts/panel-preferences.mjs +220 -0
  59. package/skills/hr-design-exploration/SKILL.md +24 -1
  60. package/skills/hr-design-exploration/agents/openai.yaml +2 -2
  61. package/skills/hr-diagnosing-bugs/SKILL.md +2 -2
  62. package/skills/hr-diagnosing-bugs/references/reporting.md +29 -4
  63. package/skills/hr-diagnosing-bugs/scripts/check-pr-offer.mjs +47 -0
  64. package/skills/hr-explain-code/SKILL.md +1 -1
  65. package/skills/hr-extract-shared-steps/SKILL.md +1 -1
  66. package/skills/hr-forgotten-work-audit/SKILL.md +130 -0
  67. package/skills/hr-forgotten-work-audit/references/brief-template.md +56 -0
  68. package/skills/hr-forgotten-work-audit/references/result-format.md +46 -0
  69. package/skills/hr-handoff/LICENSE +21 -0
  70. package/skills/hr-handoff/NOTICE.md +29 -0
  71. package/skills/hr-handoff/SKILL.md +24 -1
  72. package/skills/hr-handoff/references/reconciliation.md +25 -6
  73. package/skills/hr-handoff/references/successor-thread.md +76 -0
  74. package/skills/hr-handoff/scripts/check-reconciliation.mjs +48 -6
  75. package/skills/hr-house-rules-setup/SKILL.md +22 -8
  76. package/skills/hr-lean-plan/SKILL.md +4 -0
  77. package/skills/hr-maintainability-review/SKILL.md +14 -4
  78. package/skills/hr-maintainability-review/agents/openai.yaml +1 -1
  79. package/skills/hr-manual-qa/SKILL.md +1 -1
  80. package/skills/hr-ordering-tests/SKILL.md +1 -1
  81. package/skills/hr-plain-prose/SKILL.md +7 -4
  82. package/skills/hr-pr-followup/SKILL.md +6 -3
  83. package/skills/hr-pr-followup/references/review-helpers.md +21 -0
  84. package/skills/hr-pr-followup/scripts/check-landing.py +204 -0
  85. package/skills/hr-pr-followup/scripts/merge-pr.sh +100 -0
  86. package/skills/hr-pr-followup/scripts/verify-landed.sh +184 -0
  87. package/skills/hr-read-reddit/SKILL.md +1 -1
  88. package/skills/hr-research-shared-links/SKILL.md +103 -0
  89. package/skills/hr-sqlite/SKILL.md +174 -0
  90. package/skills/hr-sqlite/agents/openai.yaml +2 -0
  91. package/skills/hr-sqlite/references/evidence.md +94 -0
  92. package/skills/hr-sqlite/references/tool-decisions.md +39 -0
  93. package/skills/hr-sqlite/scripts/inspect-sqlite.py +245 -0
  94. package/skills/hr-stock-ui-audit/SKILL.md +1 -1
  95. package/skills/hr-test-audit/LICENSE +21 -0
  96. package/skills/hr-test-audit/NOTICE.md +67 -0
  97. package/skills/hr-usage-report/SKILL.md +90 -0
  98. package/skills/hr-usage-report/agents/openai.yaml +2 -0
  99. package/skills/hr-usage-report/assets/example-report.json +125 -0
  100. package/skills/hr-usage-report/assets/report-template.html +377 -0
  101. package/skills/hr-usage-report/scripts/render-report.ts +86 -0
  102. package/skills/hr-usage-report/scripts/usage-by-model.ts +499 -0
  103. package/skills/hr-what-next/SKILL.md +100 -10
  104. package/skills/hr-what-next/references/reporting.md +13 -22
  105. package/skills/hr-what-next/scripts/check-open-work.mjs +711 -0
  106. package/skills/hr-writing-pr/SKILL.md +29 -14
  107. package/skills/hr-writing-pr/scripts/benefit-chart.py +200 -0
  108. package/docs/npm-release.md +0 -45
  109. package/rules/modifiers/swarmail.md +0 -12
  110. package/skills/hr-benchmarking/references/priority-findings-20261006.md +0 -115
  111. 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 threads (post plus comments) and search subreddits in full text, 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.
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.