@bompus/house-rules 0.0.0-stage → 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 (236) hide show
  1. package/CODE_OF_CONDUCT.md +83 -0
  2. package/CONTRIBUTING.md +54 -0
  3. package/LICENSE +21 -0
  4. package/README.md +378 -2
  5. package/THIRD_PARTY_NOTICES.md +188 -0
  6. package/compose.mjs +125 -0
  7. package/composition.mjs +361 -0
  8. package/config-view.mjs +209 -0
  9. package/config.mjs +381 -0
  10. package/docs/assets/banner-dark.png +0 -0
  11. package/docs/assets/banner-light.png +0 -0
  12. package/docs/assets/overview.png +0 -0
  13. package/docs/configuration.md +269 -0
  14. package/docs/skill-names.md +50 -0
  15. package/examples/person/house-rules.json +15 -0
  16. package/examples/person/rules/10-tooling.md +8 -0
  17. package/examples/person/rules/20-reporting.md +9 -0
  18. package/examples/project/AGENTS.md +18 -0
  19. package/package.json +42 -4
  20. package/rule-references.mjs +91 -0
  21. package/rules/core.md +340 -0
  22. package/rules/modifiers/backlog-outline.md +25 -0
  23. package/rules/modifiers/backlog-visual.md +16 -0
  24. package/rules/modifiers/coded-offers.md +70 -0
  25. package/rules/modifiers/cross-session-relay.md +23 -0
  26. package/rules/modifiers/effort-estimates.md +41 -0
  27. package/rules/modifiers/land-when-done.md +12 -0
  28. package/rules/modifiers/low-quota-handoff.md +20 -0
  29. package/rules/modifiers/model-default-questions.md +33 -0
  30. package/rules/modifiers/multi-agent.md +38 -0
  31. package/rules/modifiers/no-attribution.md +16 -0
  32. package/rules/modifiers/periodic-updates.md +25 -0
  33. package/rules/modifiers/plan-files.md +82 -0
  34. package/rules/modifiers/question-cards.md +48 -0
  35. package/rules/modifiers/release-batching.md +15 -0
  36. package/rules/modifiers/scratch-on-disk.md +15 -0
  37. package/rules/modifiers/shared-host-load.md +19 -0
  38. package/rules/modifiers/solo-operator.md +13 -0
  39. package/rules/modifiers/squash-landing.md +22 -0
  40. package/rules/modifiers/upstream-contributions.md +45 -0
  41. package/rules/modifiers/web-image-formats.md +35 -0
  42. package/rules/references/backlog-view.md +154 -0
  43. package/rules/references/github-actions.md +45 -0
  44. package/rules/references/landing.md +39 -0
  45. package/rules/references/multi-agent.md +51 -0
  46. package/rules/references/release-batching.md +66 -0
  47. package/rules/references/squash-landing.md +50 -0
  48. package/rules/references/thread-visuals.md +45 -0
  49. package/setup.mjs +445 -0
  50. package/skills/hr-agent-guidance-audit/LICENSE +21 -0
  51. package/skills/hr-agent-guidance-audit/SKILL.md +91 -0
  52. package/skills/hr-agent-guidance-audit/agents/openai.yaml +2 -0
  53. package/skills/hr-agent-guidance-audit/references/prune-workflow.md +72 -0
  54. package/skills/hr-agent-guidance-audit/references/records-and-lessons.md +60 -0
  55. package/skills/hr-agent-guidance-audit/references/retirement-verdicts.md +49 -0
  56. package/skills/hr-agent-guidance-refresh/LICENSE +21 -0
  57. package/skills/hr-agent-guidance-refresh/SKILL.md +36 -0
  58. package/skills/hr-agent-guidance-refresh/agents/openai.yaml +2 -0
  59. package/skills/hr-agent-guidance-refresh/references/verification.md +47 -0
  60. package/skills/hr-api-exposure-check/SKILL.md +159 -0
  61. package/skills/hr-api-exposure-check/agents/openai.yaml +3 -0
  62. package/skills/hr-audit-choices/SKILL.md +67 -0
  63. package/skills/hr-audit-choices/agents/openai.yaml +2 -0
  64. package/skills/hr-benchmarking/NOTICE.md +36 -0
  65. package/skills/hr-benchmarking/SKILL.md +279 -0
  66. package/skills/hr-benchmarking/agents/openai.yaml +2 -0
  67. package/skills/hr-benchmarking/references/javascript.md +130 -0
  68. package/skills/hr-benchmarking/references/tools.md +165 -0
  69. package/skills/hr-benchmarking/scripts/process-attribution.ps1 +20 -0
  70. package/skills/hr-benchmarking/scripts/process-attribution.py +107 -0
  71. package/skills/hr-better-accessibility/LICENSE +22 -0
  72. package/skills/hr-better-accessibility/NOTICE.md +46 -0
  73. package/skills/hr-better-accessibility/SKILL.md +119 -0
  74. package/skills/hr-better-accessibility/agents/openai.yaml +3 -0
  75. package/skills/hr-better-accessibility/focus-and-keyboard.md +130 -0
  76. package/skills/hr-better-accessibility/forms.md +82 -0
  77. package/skills/hr-better-accessibility/hit-areas.md +98 -0
  78. package/skills/hr-better-accessibility/motion-and-zoom.md +81 -0
  79. package/skills/hr-better-accessibility/screen-readers.md +102 -0
  80. package/skills/hr-better-accessibility/semantics-and-aria.md +84 -0
  81. package/skills/hr-better-colors/LICENSE +21 -0
  82. package/skills/hr-better-colors/NOTICE.md +37 -0
  83. package/skills/hr-better-colors/SKILL.md +114 -0
  84. package/skills/hr-better-colors/agents/openai.yaml +3 -0
  85. package/skills/hr-better-colors/color-formats.md +90 -0
  86. package/skills/hr-better-colors/color-usage.md +140 -0
  87. package/skills/hr-better-colors/contrast.md +80 -0
  88. package/skills/hr-better-colors/palette-generation.md +104 -0
  89. package/skills/hr-better-colors/palette-structure.md +76 -0
  90. package/skills/hr-better-colors/token-naming.md +97 -0
  91. package/skills/hr-better-layout/LICENSE +22 -0
  92. package/skills/hr-better-layout/NOTICE.md +48 -0
  93. package/skills/hr-better-layout/SKILL.md +92 -0
  94. package/skills/hr-better-layout/agents/openai.yaml +3 -0
  95. package/skills/hr-better-layout/grouping-and-alignment.md +144 -0
  96. package/skills/hr-better-layout/spacing-and-adaptivity.md +215 -0
  97. package/skills/hr-better-typography/LICENSE +22 -0
  98. package/skills/hr-better-typography/NOTICE.md +46 -0
  99. package/skills/hr-better-typography/SKILL.md +172 -0
  100. package/skills/hr-better-typography/agents/openai.yaml +3 -0
  101. package/skills/hr-better-typography/choosing-fonts.md +69 -0
  102. package/skills/hr-better-typography/css-cheat-sheet.md +65 -0
  103. package/skills/hr-better-typography/details-and-accessibility.md +119 -0
  104. package/skills/hr-better-typography/spacing-and-sizing.md +131 -0
  105. package/skills/hr-better-typography/variable-fonts-and-opentype.md +105 -0
  106. package/skills/hr-better-typography/wrapping-and-punctuation.md +57 -0
  107. package/skills/hr-better-writing/LICENSE +22 -0
  108. package/skills/hr-better-writing/NOTICE.md +46 -0
  109. package/skills/hr-better-writing/SKILL.md +122 -0
  110. package/skills/hr-better-writing/agents/openai.yaml +3 -0
  111. package/skills/hr-code-review/LICENSE +21 -0
  112. package/skills/hr-code-review/NOTICE.md +26 -0
  113. package/skills/hr-code-review/SKILL.md +163 -0
  114. package/skills/hr-code-review/agents/openai.yaml +3 -0
  115. package/skills/hr-code-review/references/acp-client.md +43 -0
  116. package/skills/hr-code-review/references/evidence.md +57 -0
  117. package/skills/hr-code-review/references/impact.md +103 -0
  118. package/skills/hr-code-review/references/panel-adjudication.md +58 -0
  119. package/skills/hr-code-review/references/panel-plan.md +61 -0
  120. package/skills/hr-code-review/references/panel-recommendations.json +105 -0
  121. package/skills/hr-code-review/references/panel-recommendations.md +46 -0
  122. package/skills/hr-code-review/references/panel-setup.md +54 -0
  123. package/skills/hr-code-review/references/reviewer-role.md +31 -0
  124. package/skills/hr-code-review/scripts/acp-panel-client.mjs +299 -0
  125. package/skills/hr-code-review/scripts/check-receipts.mjs +263 -0
  126. package/skills/hr-code-review/scripts/panel-plan.mjs +69 -0
  127. package/skills/hr-code-review/scripts/panel-preferences.mjs +220 -0
  128. package/skills/hr-design-exploration/SKILL.md +105 -0
  129. package/skills/hr-design-exploration/agents/openai.yaml +4 -0
  130. package/skills/hr-diagnosing-bugs/NOTICE.md +45 -0
  131. package/skills/hr-diagnosing-bugs/SKILL.md +108 -0
  132. package/skills/hr-diagnosing-bugs/agents/openai.yaml +3 -0
  133. package/skills/hr-diagnosing-bugs/references/reporting.md +97 -0
  134. package/skills/hr-diagnosing-bugs/scripts/check-pr-offer.mjs +47 -0
  135. package/skills/hr-explain-code/NOTICE.md +37 -0
  136. package/skills/hr-explain-code/SKILL.md +110 -0
  137. package/skills/hr-explain-code/agents/openai.yaml +5 -0
  138. package/skills/hr-explain-code/references/helper-brief.md +39 -0
  139. package/skills/hr-extract-shared-steps/SKILL.md +134 -0
  140. package/skills/hr-extract-shared-steps/agents/openai.yaml +3 -0
  141. package/skills/hr-forgotten-work-audit/SKILL.md +130 -0
  142. package/skills/hr-forgotten-work-audit/references/brief-template.md +56 -0
  143. package/skills/hr-forgotten-work-audit/references/result-format.md +46 -0
  144. package/skills/hr-handoff/LICENSE +21 -0
  145. package/skills/hr-handoff/NOTICE.md +29 -0
  146. package/skills/hr-handoff/SKILL.md +114 -0
  147. package/skills/hr-handoff/agents/openai.yaml +5 -0
  148. package/skills/hr-handoff/assets/ledger.example.json +10 -0
  149. package/skills/hr-handoff/assets/receipt.example.json +76 -0
  150. package/skills/hr-handoff/assets/source.example.json +20 -0
  151. package/skills/hr-handoff/references/reconciliation.md +124 -0
  152. package/skills/hr-handoff/references/successor-thread.md +76 -0
  153. package/skills/hr-handoff/scripts/check-reconciliation.mjs +324 -0
  154. package/skills/hr-house-rules-setup/SKILL.md +114 -0
  155. package/skills/hr-house-rules-setup/agents/openai.yaml +5 -0
  156. package/skills/hr-issue-tracker-setup/SKILL.md +49 -0
  157. package/skills/hr-issue-tracker-setup/agents/openai.yaml +5 -0
  158. package/skills/hr-lean-plan/SKILL.md +146 -0
  159. package/skills/hr-lean-plan/agents/openai.yaml +5 -0
  160. package/skills/hr-maintainability-review/SKILL.md +186 -0
  161. package/skills/hr-maintainability-review/agents/openai.yaml +5 -0
  162. package/skills/hr-manual-qa/NOTICE.md +36 -0
  163. package/skills/hr-manual-qa/SKILL.md +37 -0
  164. package/skills/hr-manual-qa/agents/openai.yaml +3 -0
  165. package/skills/hr-navigation-retro/LICENSE +21 -0
  166. package/skills/hr-navigation-retro/NOTICE.md +5 -0
  167. package/skills/hr-navigation-retro/SKILL.md +86 -0
  168. package/skills/hr-navigation-retro/agents/openai.yaml +4 -0
  169. package/skills/hr-navigation-retro/references/inputs.md +67 -0
  170. package/skills/hr-navigation-retro/scripts/retro.py +552 -0
  171. package/skills/hr-navigation-retro/scripts/transcripts.py +205 -0
  172. package/skills/hr-ordering-tests/SKILL.md +51 -0
  173. package/skills/hr-ordering-tests/agents/openai.yaml +2 -0
  174. package/skills/hr-plain-prose/SKILL.md +264 -0
  175. package/skills/hr-plain-prose/agents/openai.yaml +3 -0
  176. package/skills/hr-plain-prose/scripts/check.mjs +331 -0
  177. package/skills/hr-pr-followup/LICENSE +21 -0
  178. package/skills/hr-pr-followup/NOTICE.md +9 -0
  179. package/skills/hr-pr-followup/SKILL.md +74 -0
  180. package/skills/hr-pr-followup/agents/openai.yaml +2 -0
  181. package/skills/hr-pr-followup/references/review-helpers.md +111 -0
  182. package/skills/hr-pr-followup/scripts/check-landing.py +204 -0
  183. package/skills/hr-pr-followup/scripts/check-pr-review.ts +510 -0
  184. package/skills/hr-pr-followup/scripts/merge-pr.sh +100 -0
  185. package/skills/hr-pr-followup/scripts/resolve-pr-thread.ts +219 -0
  186. package/skills/hr-pr-followup/scripts/verify-landed.sh +184 -0
  187. package/skills/hr-progress-report/SKILL.md +49 -0
  188. package/skills/hr-progress-report/agents/openai.yaml +5 -0
  189. package/skills/hr-read-reddit/SKILL.md +44 -0
  190. package/skills/hr-read-reddit/agents/openai.yaml +2 -0
  191. package/skills/hr-read-reddit/scripts/reddit.ts +252 -0
  192. package/skills/hr-read-x-links/SKILL.md +42 -0
  193. package/skills/hr-read-x-links/agents/openai.yaml +2 -0
  194. package/skills/hr-read-x-links/scripts/x-post.ts +264 -0
  195. package/skills/hr-research-shared-links/SKILL.md +103 -0
  196. package/skills/hr-split-to-prs/LICENSE +21 -0
  197. package/skills/hr-split-to-prs/NOTICE.md +9 -0
  198. package/skills/hr-split-to-prs/SKILL.md +61 -0
  199. package/skills/hr-split-to-prs/agents/openai.yaml +2 -0
  200. package/skills/hr-sqlite/SKILL.md +174 -0
  201. package/skills/hr-sqlite/agents/openai.yaml +2 -0
  202. package/skills/hr-sqlite/references/evidence.md +94 -0
  203. package/skills/hr-sqlite/references/tool-decisions.md +39 -0
  204. package/skills/hr-sqlite/scripts/inspect-sqlite.py +245 -0
  205. package/skills/hr-stock-ui-audit/SKILL.md +132 -0
  206. package/skills/hr-stock-ui-audit/agents/openai.yaml +3 -0
  207. package/skills/hr-stock-ui-audit/scripts/scan.mjs +619 -0
  208. package/skills/hr-tdd/LICENSE +21 -0
  209. package/skills/hr-tdd/NOTICE.md +10 -0
  210. package/skills/hr-tdd/SKILL.md +40 -0
  211. package/skills/hr-tdd/agents/openai.yaml +3 -0
  212. package/skills/hr-tdd/mocking.md +39 -0
  213. package/skills/hr-tdd/tests.md +77 -0
  214. package/skills/hr-test-audit/LICENSE +21 -0
  215. package/skills/hr-test-audit/NOTICE.md +67 -0
  216. package/skills/hr-test-audit/SKILL.md +146 -0
  217. package/skills/hr-test-audit/agents/openai.yaml +3 -0
  218. package/skills/hr-test-audit/scripts/floor-guard.ts +137 -0
  219. package/skills/hr-usage-report/SKILL.md +90 -0
  220. package/skills/hr-usage-report/agents/openai.yaml +2 -0
  221. package/skills/hr-usage-report/assets/example-report.json +125 -0
  222. package/skills/hr-usage-report/assets/report-template.html +377 -0
  223. package/skills/hr-usage-report/scripts/render-report.ts +86 -0
  224. package/skills/hr-usage-report/scripts/usage-by-model.ts +499 -0
  225. package/skills/hr-what-next/SKILL.md +151 -0
  226. package/skills/hr-what-next/agents/openai.yaml +4 -0
  227. package/skills/hr-what-next/references/reporting.md +59 -0
  228. package/skills/hr-what-next/scripts/check-open-work.mjs +711 -0
  229. package/skills/hr-writing-for-agents/NOTICE.md +46 -0
  230. package/skills/hr-writing-for-agents/SKILL-MECHANICS.md +83 -0
  231. package/skills/hr-writing-for-agents/SKILL.md +108 -0
  232. package/skills/hr-writing-for-agents/agents/openai.yaml +3 -0
  233. package/skills/hr-writing-pr/NOTICE.md +48 -0
  234. package/skills/hr-writing-pr/SKILL.md +119 -0
  235. package/skills/hr-writing-pr/agents/openai.yaml +2 -0
  236. package/skills/hr-writing-pr/scripts/benefit-chart.py +200 -0
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env bun
2
+ // Read Reddit threads and subreddit searches through Reddit's public Atom (RSS)
3
+ // feeds: no key, full post and comment bodies, links kept inline.
4
+ import { parseArgs as parseCli } from "node:util";
5
+
6
+ const ORIGIN = "https://www.reddit.com";
7
+ // Reddit blocks generic agents; its API rules ask for a descriptive user agent.
8
+ const HEADERS = { "user-agent": "linux:agent-read-reddit:v1 (read-only research)" };
9
+ const HOSTS = /^(?:www\.|old\.|new\.|np\.|m\.)?reddit\.com$/;
10
+
11
+ export type Entry = {
12
+ id: string;
13
+ kind: "post" | "comment" | "other";
14
+ author: string;
15
+ published: string;
16
+ title: string;
17
+ url: string;
18
+ text: string;
19
+ };
20
+ export type Options = {
21
+ command: string | undefined;
22
+ args: string[];
23
+ json: boolean;
24
+ pages: number;
25
+ since: number | null;
26
+ sort: string;
27
+ };
28
+
29
+ /** `{ sub, id }` for a thread URL on reddit.com (any subdomain) or redd.it; null otherwise. */
30
+ export function parseThreadUrl(input: string): { sub: string | null; id: string } | null {
31
+ let url: URL;
32
+ try {
33
+ url = new URL(input);
34
+ } catch {
35
+ return null;
36
+ }
37
+ if (url.hostname === "redd.it") {
38
+ const id = /^\/([a-z0-9]+)$/i.exec(url.pathname)?.[1];
39
+ return id ? { sub: null, id } : null;
40
+ }
41
+ if (!HOSTS.test(url.hostname)) {
42
+ return null;
43
+ }
44
+ const match = /^\/r\/([^/]+)\/comments\/([a-z0-9]+)/i.exec(url.pathname);
45
+ return match ? { sub: match[1]!, id: match[2]! } : null;
46
+ }
47
+
48
+ const ENTITIES: Record<string, string> = {
49
+ amp: "&",
50
+ lt: "<",
51
+ gt: ">",
52
+ quot: '"',
53
+ apos: "'",
54
+ nbsp: " ",
55
+ };
56
+ export function decodeEntities(s: string): string {
57
+ return s.replace(/&(#x[0-9a-f]+|#\d+|\w+);/gi, (m, e: string) => {
58
+ if (e[0] === "#") {
59
+ return String.fromCodePoint(
60
+ e[1] === "x" || e[1] === "X" ? parseInt(e.slice(2), 16) : Number(e.slice(1)),
61
+ );
62
+ }
63
+ return ENTITIES[e.toLowerCase()] ?? m;
64
+ });
65
+ }
66
+
67
+ /** Entry HTML as plain text; a link becomes `text (url)`, or the bare URL when the text says nothing. */
68
+ export function htmlToText(html: string): string {
69
+ const text = html
70
+ .replace(/<!--[\s\S]*?-->/g, "")
71
+ .replace(/<a\s[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, (_, href: string, label: string) => {
72
+ const url = decodeEntities(href).replace(/^\//, `${ORIGIN}/`);
73
+ const words = decodeEntities(label.replace(/<[^>]+>/g, "")).trim();
74
+ return !words || /^\[(link|comments)\]$/.test(words) || words === url
75
+ ? url
76
+ : `${words} (${url})`;
77
+ })
78
+ .replace(/<br\s*\/?>/gi, "\n")
79
+ .replace(/<\/(p|div|li|h\d|pre|blockquote|tr)>/gi, "\n")
80
+ .replace(/<li[^>]*>/gi, "- ")
81
+ .replace(/<[^>]+>/g, "");
82
+ return decodeEntities(text)
83
+ .replace(/[ \t]+/g, " ")
84
+ .replace(/ *\n */g, "\n")
85
+ .replace(/\n{3,}/g, "\n\n")
86
+ .trim();
87
+ }
88
+
89
+ /** Atom feed entries: `{ id, kind, author, published, title, url, text }`; kind is post (t3) or comment (t1). */
90
+ export function parseFeed(xml: string): Entry[] {
91
+ const tag = (s: string, name: string) =>
92
+ new RegExp(`<${name}[^>]*>([\\s\\S]*?)</${name}>`).exec(s)?.[1] ?? "";
93
+ return [...xml.matchAll(/<entry>([\s\S]*?)<\/entry>/g)].map(([, e = ""]) => {
94
+ const id = tag(e, "id");
95
+ return {
96
+ id,
97
+ kind: id.startsWith("t3_") ? "post" : id.startsWith("t1_") ? "comment" : ("other" as const),
98
+ author: tag(tag(e, "author"), "name").replace(/^\/u\//, ""),
99
+ published: tag(e, "published") || tag(e, "updated"),
100
+ title: decodeEntities(tag(e, "title")),
101
+ url: decodeEntities(/<link href="([^"]*)"/.exec(e)?.[1] ?? ""),
102
+ text: htmlToText(decodeEntities(tag(e, "content"))),
103
+ };
104
+ });
105
+ }
106
+
107
+ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
108
+ let waitUntil = 0;
109
+
110
+ /** GET a feed, spacing requests by Reddit's rate-limit headers and retrying a 429 twice. */
111
+ async function getFeed(url: string): Promise<Entry[]> {
112
+ for (let attempt = 0; attempt < 3; attempt++) {
113
+ const wait = waitUntil - Date.now();
114
+ if (wait > 0) {
115
+ console.error(`rate limit: waiting ${Math.ceil(wait / 1000)} s`);
116
+ await sleep(wait);
117
+ }
118
+ const res = await fetch(url, { headers: HEADERS });
119
+ const reset = Number(res.headers.get("x-ratelimit-reset")) || 60;
120
+ const remaining = Number(res.headers.get("x-ratelimit-remaining") ?? 1);
121
+ if (res.status === 429 || remaining < 1) {
122
+ waitUntil = Date.now() + (reset + 1) * 1000;
123
+ }
124
+ if (res.status === 429) {
125
+ continue;
126
+ }
127
+ if (!res.ok) {
128
+ throw new Error(`HTTP ${res.status} for ${url}`);
129
+ }
130
+ return parseFeed(await res.text());
131
+ }
132
+ throw new Error(`HTTP 429 three times for ${url}`);
133
+ }
134
+
135
+ const SORTS = ["new", "relevance", "top"];
136
+
137
+ export function parseArgs(argv: string[]): Options | { error: string } {
138
+ let parsed;
139
+ try {
140
+ parsed = parseCli({
141
+ args: argv,
142
+ allowPositionals: true,
143
+ options: {
144
+ json: { type: "boolean" },
145
+ pages: { type: "string" },
146
+ since: { type: "string" },
147
+ sort: { type: "string" },
148
+ },
149
+ });
150
+ } catch (error) {
151
+ return { error: error instanceof Error ? error.message : String(error) };
152
+ }
153
+ const { values, positionals } = parsed;
154
+ const [command, ...args] = positionals;
155
+ const pages = Number(values.pages ?? 1);
156
+ const since = values.since === undefined ? null : Date.parse(values.since);
157
+ const sort = values.sort ?? "new";
158
+ if (!Number.isInteger(pages) || pages < 1) {
159
+ return { error: `bad --pages value ${values.pages}` };
160
+ }
161
+ if (Number.isNaN(since)) {
162
+ return { error: `bad --since value ${values.since}` };
163
+ }
164
+ if (!SORTS.includes(sort)) {
165
+ return { error: `bad --sort value ${sort}` };
166
+ }
167
+ const opts: Options = { command, args, json: values.json === true, pages, since, sort };
168
+ if (command === "thread" && args.length > 0) {
169
+ return opts;
170
+ }
171
+ if (command === "search" && args.length === 2) {
172
+ return opts;
173
+ }
174
+ return { error: command ? `bad arguments for ${command}` : "missing command" };
175
+ }
176
+
177
+ function print(entry: Entry, json: boolean): void {
178
+ if (json) {
179
+ console.log(JSON.stringify(entry));
180
+ return;
181
+ }
182
+ const head = entry.kind === "post" ? `### ${entry.title}\n${entry.url}\n` : "#### comment ";
183
+ console.log(`${head}u/${entry.author} · ${entry.published}\n\n${entry.text}\n`);
184
+ }
185
+
186
+ async function main(argv: string[]): Promise<number> {
187
+ const opts = parseArgs(argv);
188
+ if ("error" in opts) {
189
+ console.error(opts.error);
190
+ console.error("usage: reddit.ts thread <url>... [--json]");
191
+ console.error(
192
+ " reddit.ts search <subreddit|all> <query> [--pages N] [--since ISO-date] [--sort new|relevance|top] [--json]",
193
+ );
194
+ return 2;
195
+ }
196
+ if (opts.command === "thread") {
197
+ let failed = 0;
198
+ for (const input of opts.args) {
199
+ const ref = parseThreadUrl(input);
200
+ if (!ref) {
201
+ console.log(`## ${input}\nnot a Reddit thread URL\n`);
202
+ failed++;
203
+ continue;
204
+ }
205
+ const path = ref.sub ? `/r/${ref.sub}/comments/${ref.id}` : `/comments/${ref.id}`;
206
+ try {
207
+ if (!opts.json) {
208
+ console.log(`## ${input}`);
209
+ }
210
+ for (const entry of await getFeed(`${ORIGIN}${path}/.rss?limit=500`)) {
211
+ print(entry, opts.json);
212
+ }
213
+ } catch (error) {
214
+ console.log(`fetch failed: ${error instanceof Error ? error.message : String(error)}\n`);
215
+ failed++;
216
+ }
217
+ }
218
+ return failed ? 1 : 0;
219
+ }
220
+ const [sub, query = ""] = opts.args;
221
+ const base =
222
+ sub === "all" ? `${ORIGIN}/search.rss?` : `${ORIGIN}/r/${sub}/search.rss?restrict_sr=on&`;
223
+ let after = "";
224
+ for (let page = 0; page < opts.pages; page++) {
225
+ const url = `${base}q=${encodeURIComponent(query)}&sort=${opts.sort}&limit=100${after && `&after=${after}`}`;
226
+ let entries: Entry[];
227
+ try {
228
+ entries = await getFeed(url);
229
+ } catch (error) {
230
+ console.log(`fetch failed: ${error instanceof Error ? error.message : String(error)}\n`);
231
+ return 1;
232
+ }
233
+ let old = false;
234
+ for (const entry of entries) {
235
+ if (opts.since && Date.parse(entry.published) < opts.since) {
236
+ old = opts.sort === "new";
237
+ continue;
238
+ }
239
+ print(entry, opts.json);
240
+ }
241
+ console.error(`page ${page + 1}: ${entries.length} results`);
242
+ if (entries.length < 100 || old) {
243
+ break;
244
+ }
245
+ after = entries.at(-1)!.id;
246
+ }
247
+ return 0;
248
+ }
249
+
250
+ if (import.meta.main) {
251
+ process.exitCode = await main(process.argv.slice(2));
252
+ }
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: hr-read-x-links
3
+ description: Read X/Twitter post links (x.com, twitter.com and mirrors) in full, including quoted posts, long-form articles, expanded links and attached images. Use whenever a task needs the content of an X post; generic web fetch returns truncated text and no images.
4
+ ---
5
+
6
+ # Read X links
7
+
8
+ Generic web fetch usually sees X behind a login wall: text cut at "Show more", `t.co`
9
+ links, no images, and quoted articles reduced to a bare link. Read posts through
10
+ the bundled script instead. It calls the public [fxtwitter](https://github.com/FxEmbed/FxEmbed)
11
+ API and needs no key. It sends the post's handle and ID to that third-party
12
+ service and downloads attached images from the URLs it returns.
13
+
14
+ ```bash
15
+ bun scripts/x-post.ts --media-dir <scratch-dir>/media <url> [<url>...]
16
+ ```
17
+
18
+ Run it from this skill's directory. It prints, for each post and any quoted
19
+ post: author, date, full text with expanded links, the article body when the
20
+ post is an X article, and the local path of every downloaded image. Put
21
+ `--media-dir` in your task's scratch directory, on disk rather than in a
22
+ RAM-backed `/tmp`.
23
+
24
+ Then:
25
+
26
+ 1. Open every downloaded image with the host's image-reading tool. Prompts,
27
+ tables and charts are often posted only as screenshots.
28
+ 2. Follow expanded links (GitHub repos, blog posts) with web fetch when the post
29
+ only points at the substance.
30
+ 3. Report what stayed unread: videos (the script prints the video URL and a
31
+ thumbnail), earlier posts in a thread (a reply shows its parent's ID; fetch
32
+ that URL to walk up), and any post the script reported as failed. Say so
33
+ rather than rating content you did not see.
34
+ 4. When the post promotes a tool, method or result, keep what the post claims
35
+ apart from what its source shows. Check each claim against the repository,
36
+ docs or paper it links, report what the tool actually does, and name each
37
+ claim with no evidence behind it, such as a benchmark with no method or a
38
+ screenshot with no source.
39
+
40
+ A deleted, protected or age-restricted post fails with the API's message; ask
41
+ the user for a screenshot. If the API itself is down, fall back to web fetch
42
+ and state that text and images may be incomplete.
@@ -0,0 +1,2 @@
1
+ interface:
2
+ display_name: "HR: Read X links"
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env bun
2
+ // Read X/Twitter posts through the public fxtwitter API: full text, expanded
3
+ // links, quoted posts, long-form articles, and downloadable images.
4
+ import { mkdirSync, writeFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { parseArgs as parseCli } from "node:util";
7
+
8
+ const API = "https://api.fxtwitter.com";
9
+ // pbs.twimg.com answers 403 without a browser-like user agent.
10
+ const HEADERS = { "user-agent": "Mozilla/5.0" };
11
+ const HOSTS = /^(?:www\.|mobile\.)?(?:x|twitter|fixupx|fxtwitter|vxtwitter|fixvx)\.com$/;
12
+
13
+ // The parts of fxtwitter's response this script reads.
14
+ type Facet = { type?: string; original?: string; replacement?: string; indices: [number, number] };
15
+ type ArticleMedia = { media_id?: string; media_info?: { original_img_url?: string } };
16
+ type Block = { type?: string; text?: string; entityRanges?: { key?: number | string }[] };
17
+ type Entity = { type?: string; data?: { mediaItems?: { mediaId?: string }[] } };
18
+ export type Article = {
19
+ title?: string;
20
+ content?: { blocks?: Block[]; entityMap?: { key: number | string; value: Entity }[] };
21
+ cover_media?: ArticleMedia;
22
+ media_entities?: ArticleMedia[];
23
+ };
24
+ export type Tweet = {
25
+ id: string;
26
+ url?: string;
27
+ text?: string;
28
+ created_at?: string;
29
+ raw_text?: { text?: string; facets?: Facet[] };
30
+ author?: { screen_name?: string; name?: string };
31
+ replying_to?: string;
32
+ replying_to_status?: string;
33
+ media?: {
34
+ photos?: { url: string }[];
35
+ videos?: { url: string; thumbnail_url?: string; duration?: number }[];
36
+ };
37
+ article?: Article;
38
+ quote?: Tweet;
39
+ };
40
+
41
+ /** `{ user, id }` for a post URL on x.com or a known mirror; null otherwise. */
42
+ export function parseStatusUrl(input: string): { user: string; id: string } | null {
43
+ let url: URL;
44
+ try {
45
+ url = new URL(input);
46
+ } catch {
47
+ return null;
48
+ }
49
+ if (!HOSTS.test(url.hostname)) {
50
+ return null;
51
+ }
52
+ const match = /^\/([^/]+)\/status(?:es)?\/(\d+)/.exec(url.pathname);
53
+ return match ? { user: match[1]!, id: match[2]! } : null;
54
+ }
55
+
56
+ /** Post text with t.co links replaced by their targets. */
57
+ export function expandedText(tweet: Tweet): string {
58
+ const text = tweet.raw_text?.text ?? tweet.text ?? "";
59
+ const facets = (tweet.raw_text?.facets ?? [])
60
+ .filter((f) => f.type === "url" && f.original && f.replacement)
61
+ .sort((a, b) => b.indices[0] - a.indices[0]);
62
+ let out = text;
63
+ for (const f of facets) {
64
+ out = out.slice(0, f.indices[0]) + f.replacement + out.slice(f.indices[1]);
65
+ }
66
+ return out;
67
+ }
68
+
69
+ export type Photo = { url: string; id: string | null };
70
+
71
+ /**
72
+ * Article body as Markdown-ish text. An image block becomes `[image N]`, where N
73
+ * is that image's position in `mediaOf(tweet).photos` (and its downloaded file).
74
+ */
75
+ export function articleText(article: Article | undefined, photos: Photo[] = []): string {
76
+ const blocks = article?.content?.blocks ?? [];
77
+ const entities = new Map(
78
+ (article?.content?.entityMap ?? []).map((e) => [String(e.key), e.value]),
79
+ );
80
+ const lines: string[] = [];
81
+ let ordered = 0;
82
+ for (const block of blocks) {
83
+ const text = (block.text ?? "").trim();
84
+ if (block.type !== "ordered-list-item") {
85
+ ordered = 0;
86
+ }
87
+ if (block.type === "atomic") {
88
+ const entity = entities.get(String(block.entityRanges?.[0]?.key));
89
+ const mediaId = entity?.type === "MEDIA" ? entity.data?.mediaItems?.[0]?.mediaId : null;
90
+ if (mediaId) {
91
+ const n = photos.findIndex((p) => p.id === mediaId);
92
+ lines.push(n >= 0 ? `[image ${n + 1}]` : "[image]");
93
+ }
94
+ continue;
95
+ }
96
+ if (!text) {
97
+ continue;
98
+ }
99
+ if (block.type === "header-one") {
100
+ lines.push(`# ${text}`);
101
+ } else if (block.type === "header-two") {
102
+ lines.push(`## ${text}`);
103
+ } else if (block.type === "unordered-list-item") {
104
+ lines.push(`- ${text}`);
105
+ } else if (block.type === "ordered-list-item") {
106
+ lines.push(`${++ordered}. ${text}`);
107
+ } else if (block.type === "blockquote") {
108
+ lines.push(`> ${text}`);
109
+ } else {
110
+ lines.push(text);
111
+ }
112
+ }
113
+ return lines.join("\n");
114
+ }
115
+
116
+ /** Every image and video a post carries, including article images. */
117
+ export function mediaOf(tweet: Tweet): {
118
+ photos: Photo[];
119
+ videos: { url: string; thumbnail?: string; duration?: number }[];
120
+ } {
121
+ const photos: Photo[] = (tweet.media?.photos ?? []).map((p) => ({ url: p.url, id: null }));
122
+ const article = tweet.article;
123
+ for (const m of [article?.cover_media, ...(article?.media_entities ?? [])]) {
124
+ if (m?.media_info?.original_img_url) {
125
+ photos.push({ url: m.media_info.original_img_url, id: m.media_id ?? null });
126
+ }
127
+ }
128
+ const videos = (tweet.media?.videos ?? []).map((v) => ({
129
+ url: v.url,
130
+ thumbnail: v.thumbnail_url,
131
+ duration: v.duration,
132
+ }));
133
+ return { photos, videos };
134
+ }
135
+
136
+ async function fetchPost(user: string, id: string): Promise<Tweet> {
137
+ const res = await fetch(`${API}/${user}/status/${id}`, { headers: HEADERS });
138
+ const body = (await res.json().catch(() => null)) as { tweet?: Tweet; message?: string } | null;
139
+ if (!res.ok || !body?.tweet) {
140
+ throw new Error(`${user}/status/${id}: HTTP ${res.status} ${body?.message ?? ""}`.trim());
141
+ }
142
+ return body.tweet;
143
+ }
144
+
145
+ async function download(url: string, path: string): Promise<string> {
146
+ const res = await fetch(url, { headers: HEADERS });
147
+ if (!res.ok) {
148
+ throw new Error(`HTTP ${res.status}`);
149
+ }
150
+ writeFileSync(path, Buffer.from(await res.arrayBuffer()));
151
+ return path;
152
+ }
153
+
154
+ async function render(tweet: Tweet, mediaDir: string | null, label: string): Promise<string> {
155
+ const out: string[] = [];
156
+ const a = tweet.author ?? {};
157
+ out.push(`### ${label}: @${a.screen_name} (${a.name ?? ""}) · ${tweet.created_at}`);
158
+ out.push(tweet.url ?? "");
159
+ if (tweet.replying_to) {
160
+ out.push(
161
+ `Reply to @${tweet.replying_to} (${tweet.replying_to_status}); earlier thread posts are not included.`,
162
+ );
163
+ }
164
+ out.push("", expandedText(tweet));
165
+ const { photos, videos } = mediaOf(tweet);
166
+ if (tweet.article) {
167
+ out.push("", `#### Article: ${tweet.article.title}`, "", articleText(tweet.article, photos));
168
+ }
169
+ for (const [i, { url }] of photos.entries()) {
170
+ if (!mediaDir) {
171
+ out.push(`image ${i + 1}: ${url}`);
172
+ continue;
173
+ }
174
+ const ext = /\.(\w+)(?:\?|$)/.exec(url)?.[1] ?? "jpg";
175
+ const path = join(mediaDir, `${tweet.id}-${i + 1}.${ext}`);
176
+ out.push(
177
+ `image ${i + 1}: ${await download(url, path).catch((e: unknown) => `${url} (download failed: ${e instanceof Error ? e.message : String(e)})`)}`,
178
+ );
179
+ }
180
+ for (const [i, v] of videos.entries()) {
181
+ out.push(
182
+ `video ${i + 1} (${v.duration ?? "?"}s, not readable as text): ${v.url} thumbnail: ${v.thumbnail}`,
183
+ );
184
+ }
185
+ return out.join("\n");
186
+ }
187
+
188
+ export function parseArgs(argv: string[]): {
189
+ mediaDir: string | null;
190
+ urls: string[];
191
+ error?: string;
192
+ } {
193
+ let parsed;
194
+ try {
195
+ parsed = parseCli({
196
+ args: argv,
197
+ allowPositionals: true,
198
+ options: { "media-dir": { type: "string" } },
199
+ });
200
+ } catch (error) {
201
+ return {
202
+ mediaDir: null,
203
+ urls: [],
204
+ error: error instanceof Error ? error.message : String(error),
205
+ };
206
+ }
207
+ const mediaDir = parsed.values["media-dir"] ?? null;
208
+ // A forgotten directory would otherwise take the first URL as its value.
209
+ if (mediaDir !== null && /^https?:/i.test(mediaDir)) {
210
+ return { mediaDir: null, urls: [], error: "--media-dir needs a directory value" };
211
+ }
212
+ return { mediaDir, urls: parsed.positionals };
213
+ }
214
+
215
+ async function main(argv: string[]): Promise<number> {
216
+ const { mediaDir, urls, error } = parseArgs(argv);
217
+ if (error || urls.length === 0) {
218
+ if (error) {
219
+ console.error(error);
220
+ }
221
+ console.error("usage: x-post.ts [--media-dir DIR] <x.com status URL>...");
222
+ return 2;
223
+ }
224
+ if (mediaDir) {
225
+ mkdirSync(mediaDir, { recursive: true });
226
+ }
227
+ let failed = 0;
228
+ for (const input of urls) {
229
+ const ref = parseStatusUrl(input);
230
+ if (!ref) {
231
+ console.log(`## ${input}\nnot an X post URL\n`);
232
+ failed++;
233
+ continue;
234
+ }
235
+ try {
236
+ const tweet = await fetchPost(ref.user, ref.id);
237
+ const parts = [await render(tweet, mediaDir, "Post")];
238
+ // The embedded quote omits its article and full media; fetch it directly.
239
+ if (tweet.quote) {
240
+ const embedded = tweet.quote;
241
+ let quote = embedded;
242
+ try {
243
+ quote = await fetchPost(embedded.author?.screen_name ?? "i", embedded.id);
244
+ } catch (error) {
245
+ parts.push(
246
+ `Quoted post fetch failed (${error instanceof Error ? error.message : String(error)}); showing the embedded copy, which can lack its article and full media.`,
247
+ );
248
+ }
249
+ parts.push(await render(quote, mediaDir, "Quoted post"));
250
+ }
251
+ console.log(`## ${input}\n${parts.join("\n\n")}\n`);
252
+ } catch (error) {
253
+ console.log(
254
+ `## ${input}\nfetch failed: ${error instanceof Error ? error.message : String(error)}\n`,
255
+ );
256
+ failed++;
257
+ }
258
+ }
259
+ return failed ? 1 : 0;
260
+ }
261
+
262
+ if (import.meta.main) {
263
+ process.exitCode = await main(process.argv.slice(2));
264
+ }
@@ -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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aaron Queen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.