@bompus/house-rules 0.0.0-stage → 0.11.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 (191) hide show
  1. package/CONTRIBUTING.md +54 -0
  2. package/LICENSE +21 -0
  3. package/README.md +347 -2
  4. package/THIRD_PARTY_NOTICES.md +188 -0
  5. package/compose.mjs +119 -0
  6. package/composition.mjs +353 -0
  7. package/config-view.mjs +194 -0
  8. package/config.mjs +320 -0
  9. package/docs/assets/banner-dark.png +0 -0
  10. package/docs/assets/banner-light.png +0 -0
  11. package/docs/assets/overview.png +0 -0
  12. package/docs/configuration.md +193 -0
  13. package/docs/npm-release.md +45 -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 +40 -4
  20. package/rule-references.mjs +91 -0
  21. package/rules/core.md +431 -0
  22. package/rules/modifiers/coded-offers.md +40 -0
  23. package/rules/modifiers/effort-estimates.md +41 -0
  24. package/rules/modifiers/land-when-done.md +12 -0
  25. package/rules/modifiers/low-quota-handoff.md +19 -0
  26. package/rules/modifiers/multi-agent.md +34 -0
  27. package/rules/modifiers/no-attribution.md +10 -0
  28. package/rules/modifiers/plan-files.md +50 -0
  29. package/rules/modifiers/question-cards.md +48 -0
  30. package/rules/modifiers/release-batching.md +13 -0
  31. package/rules/modifiers/scratch-on-disk.md +15 -0
  32. package/rules/modifiers/shared-host-load.md +48 -0
  33. package/rules/modifiers/solo-operator.md +13 -0
  34. package/rules/modifiers/squash-landing.md +22 -0
  35. package/rules/modifiers/swarmail.md +12 -0
  36. package/rules/references/github-actions.md +45 -0
  37. package/rules/references/landing.md +39 -0
  38. package/rules/references/multi-agent.md +47 -0
  39. package/rules/references/release-batching.md +43 -0
  40. package/rules/references/squash-landing.md +44 -0
  41. package/setup.mjs +375 -0
  42. package/skills/hr-agent-guidance-audit/LICENSE +21 -0
  43. package/skills/hr-agent-guidance-audit/SKILL.md +86 -0
  44. package/skills/hr-agent-guidance-audit/agents/openai.yaml +2 -0
  45. package/skills/hr-agent-guidance-audit/references/records-and-lessons.md +60 -0
  46. package/skills/hr-agent-guidance-refresh/LICENSE +21 -0
  47. package/skills/hr-agent-guidance-refresh/SKILL.md +68 -0
  48. package/skills/hr-agent-guidance-refresh/agents/openai.yaml +2 -0
  49. package/skills/hr-agent-guidance-refresh/references/verification.md +47 -0
  50. package/skills/hr-api-exposure-check/SKILL.md +159 -0
  51. package/skills/hr-api-exposure-check/agents/openai.yaml +3 -0
  52. package/skills/hr-audit-choices/SKILL.md +67 -0
  53. package/skills/hr-audit-choices/agents/openai.yaml +2 -0
  54. package/skills/hr-benchmarking/NOTICE.md +36 -0
  55. package/skills/hr-benchmarking/SKILL.md +316 -0
  56. package/skills/hr-benchmarking/agents/openai.yaml +2 -0
  57. package/skills/hr-benchmarking/references/javascript.md +17 -0
  58. package/skills/hr-benchmarking/references/priority-findings-20261006.md +115 -0
  59. package/skills/hr-benchmarking/references/tools.md +160 -0
  60. package/skills/hr-benchmarking/scripts/process-attribution.ps1 +20 -0
  61. package/skills/hr-benchmarking/scripts/process-attribution.py +107 -0
  62. package/skills/hr-better-accessibility/LICENSE +22 -0
  63. package/skills/hr-better-accessibility/NOTICE.md +46 -0
  64. package/skills/hr-better-accessibility/SKILL.md +119 -0
  65. package/skills/hr-better-accessibility/agents/openai.yaml +3 -0
  66. package/skills/hr-better-accessibility/focus-and-keyboard.md +130 -0
  67. package/skills/hr-better-accessibility/forms.md +82 -0
  68. package/skills/hr-better-accessibility/hit-areas.md +98 -0
  69. package/skills/hr-better-accessibility/motion-and-zoom.md +81 -0
  70. package/skills/hr-better-accessibility/screen-readers.md +101 -0
  71. package/skills/hr-better-accessibility/semantics-and-aria.md +84 -0
  72. package/skills/hr-better-colors/LICENSE +21 -0
  73. package/skills/hr-better-colors/NOTICE.md +37 -0
  74. package/skills/hr-better-colors/SKILL.md +114 -0
  75. package/skills/hr-better-colors/agents/openai.yaml +3 -0
  76. package/skills/hr-better-colors/color-formats.md +90 -0
  77. package/skills/hr-better-colors/color-usage.md +140 -0
  78. package/skills/hr-better-colors/contrast.md +80 -0
  79. package/skills/hr-better-colors/palette-generation.md +104 -0
  80. package/skills/hr-better-colors/palette-structure.md +76 -0
  81. package/skills/hr-better-colors/token-naming.md +97 -0
  82. package/skills/hr-better-layout/LICENSE +22 -0
  83. package/skills/hr-better-layout/NOTICE.md +48 -0
  84. package/skills/hr-better-layout/SKILL.md +92 -0
  85. package/skills/hr-better-layout/agents/openai.yaml +3 -0
  86. package/skills/hr-better-layout/grouping-and-alignment.md +144 -0
  87. package/skills/hr-better-layout/spacing-and-adaptivity.md +215 -0
  88. package/skills/hr-better-typography/LICENSE +22 -0
  89. package/skills/hr-better-typography/NOTICE.md +46 -0
  90. package/skills/hr-better-typography/SKILL.md +172 -0
  91. package/skills/hr-better-typography/agents/openai.yaml +3 -0
  92. package/skills/hr-better-typography/choosing-fonts.md +69 -0
  93. package/skills/hr-better-typography/css-cheat-sheet.md +65 -0
  94. package/skills/hr-better-typography/details-and-accessibility.md +119 -0
  95. package/skills/hr-better-typography/spacing-and-sizing.md +131 -0
  96. package/skills/hr-better-typography/variable-fonts-and-opentype.md +105 -0
  97. package/skills/hr-better-typography/wrapping-and-punctuation.md +57 -0
  98. package/skills/hr-better-writing/LICENSE +22 -0
  99. package/skills/hr-better-writing/NOTICE.md +46 -0
  100. package/skills/hr-better-writing/SKILL.md +122 -0
  101. package/skills/hr-better-writing/agents/openai.yaml +3 -0
  102. package/skills/hr-change-impact/SKILL.md +113 -0
  103. package/skills/hr-change-impact/agents/openai.yaml +5 -0
  104. package/skills/hr-code-review/LICENSE +21 -0
  105. package/skills/hr-code-review/NOTICE.md +26 -0
  106. package/skills/hr-code-review/SKILL.md +147 -0
  107. package/skills/hr-code-review/agents/openai.yaml +3 -0
  108. package/skills/hr-code-review/references/evidence.md +57 -0
  109. package/skills/hr-design-exploration/SKILL.md +82 -0
  110. package/skills/hr-design-exploration/agents/openai.yaml +4 -0
  111. package/skills/hr-diagnosing-bugs/NOTICE.md +45 -0
  112. package/skills/hr-diagnosing-bugs/SKILL.md +108 -0
  113. package/skills/hr-diagnosing-bugs/agents/openai.yaml +3 -0
  114. package/skills/hr-diagnosing-bugs/references/reporting.md +72 -0
  115. package/skills/hr-explain-code/NOTICE.md +37 -0
  116. package/skills/hr-explain-code/SKILL.md +110 -0
  117. package/skills/hr-explain-code/agents/openai.yaml +5 -0
  118. package/skills/hr-explain-code/references/helper-brief.md +39 -0
  119. package/skills/hr-extract-shared-steps/SKILL.md +134 -0
  120. package/skills/hr-extract-shared-steps/agents/openai.yaml +3 -0
  121. package/skills/hr-handoff/SKILL.md +91 -0
  122. package/skills/hr-handoff/agents/openai.yaml +5 -0
  123. package/skills/hr-handoff/assets/ledger.example.json +10 -0
  124. package/skills/hr-handoff/assets/receipt.example.json +76 -0
  125. package/skills/hr-handoff/assets/source.example.json +20 -0
  126. package/skills/hr-handoff/references/reconciliation.md +105 -0
  127. package/skills/hr-handoff/scripts/check-reconciliation.mjs +282 -0
  128. package/skills/hr-house-rules-setup/SKILL.md +100 -0
  129. package/skills/hr-house-rules-setup/agents/openai.yaml +5 -0
  130. package/skills/hr-issue-tracker-setup/SKILL.md +49 -0
  131. package/skills/hr-issue-tracker-setup/agents/openai.yaml +5 -0
  132. package/skills/hr-lean-plan/SKILL.md +142 -0
  133. package/skills/hr-lean-plan/agents/openai.yaml +5 -0
  134. package/skills/hr-maintainability-review/SKILL.md +176 -0
  135. package/skills/hr-maintainability-review/agents/openai.yaml +5 -0
  136. package/skills/hr-manual-qa/NOTICE.md +36 -0
  137. package/skills/hr-manual-qa/SKILL.md +37 -0
  138. package/skills/hr-manual-qa/agents/openai.yaml +3 -0
  139. package/skills/hr-navigation-retro/LICENSE +21 -0
  140. package/skills/hr-navigation-retro/NOTICE.md +5 -0
  141. package/skills/hr-navigation-retro/SKILL.md +86 -0
  142. package/skills/hr-navigation-retro/agents/openai.yaml +4 -0
  143. package/skills/hr-navigation-retro/references/inputs.md +67 -0
  144. package/skills/hr-navigation-retro/scripts/retro.py +552 -0
  145. package/skills/hr-navigation-retro/scripts/transcripts.py +205 -0
  146. package/skills/hr-ordering-tests/SKILL.md +51 -0
  147. package/skills/hr-ordering-tests/agents/openai.yaml +2 -0
  148. package/skills/hr-plain-prose/SKILL.md +261 -0
  149. package/skills/hr-plain-prose/agents/openai.yaml +3 -0
  150. package/skills/hr-plain-prose/scripts/check.mjs +331 -0
  151. package/skills/hr-pr-followup/LICENSE +21 -0
  152. package/skills/hr-pr-followup/NOTICE.md +9 -0
  153. package/skills/hr-pr-followup/SKILL.md +71 -0
  154. package/skills/hr-pr-followup/agents/openai.yaml +2 -0
  155. package/skills/hr-pr-followup/references/review-helpers.md +90 -0
  156. package/skills/hr-pr-followup/scripts/check-pr-review.ts +510 -0
  157. package/skills/hr-pr-followup/scripts/resolve-pr-thread.ts +219 -0
  158. package/skills/hr-progress-report/SKILL.md +49 -0
  159. package/skills/hr-progress-report/agents/openai.yaml +5 -0
  160. package/skills/hr-read-reddit/SKILL.md +44 -0
  161. package/skills/hr-read-reddit/agents/openai.yaml +2 -0
  162. package/skills/hr-read-reddit/scripts/reddit.ts +252 -0
  163. package/skills/hr-read-x-links/SKILL.md +42 -0
  164. package/skills/hr-read-x-links/agents/openai.yaml +2 -0
  165. package/skills/hr-read-x-links/scripts/x-post.ts +264 -0
  166. package/skills/hr-split-to-prs/LICENSE +21 -0
  167. package/skills/hr-split-to-prs/NOTICE.md +9 -0
  168. package/skills/hr-split-to-prs/SKILL.md +61 -0
  169. package/skills/hr-split-to-prs/agents/openai.yaml +2 -0
  170. package/skills/hr-stock-ui-audit/SKILL.md +132 -0
  171. package/skills/hr-stock-ui-audit/agents/openai.yaml +3 -0
  172. package/skills/hr-stock-ui-audit/scripts/scan.mjs +619 -0
  173. package/skills/hr-tdd/LICENSE +21 -0
  174. package/skills/hr-tdd/NOTICE.md +10 -0
  175. package/skills/hr-tdd/SKILL.md +40 -0
  176. package/skills/hr-tdd/agents/openai.yaml +3 -0
  177. package/skills/hr-tdd/mocking.md +39 -0
  178. package/skills/hr-tdd/tests.md +77 -0
  179. package/skills/hr-test-audit/SKILL.md +146 -0
  180. package/skills/hr-test-audit/agents/openai.yaml +3 -0
  181. package/skills/hr-test-audit/scripts/floor-guard.ts +137 -0
  182. package/skills/hr-what-next/SKILL.md +61 -0
  183. package/skills/hr-what-next/agents/openai.yaml +4 -0
  184. package/skills/hr-what-next/references/reporting.md +68 -0
  185. package/skills/hr-writing-for-agents/NOTICE.md +46 -0
  186. package/skills/hr-writing-for-agents/SKILL-MECHANICS.md +83 -0
  187. package/skills/hr-writing-for-agents/SKILL.md +108 -0
  188. package/skills/hr-writing-for-agents/agents/openai.yaml +3 -0
  189. package/skills/hr-writing-pr/NOTICE.md +48 -0
  190. package/skills/hr-writing-pr/SKILL.md +104 -0
  191. package/skills/hr-writing-pr/agents/openai.yaml +2 -0
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env bun
2
+ // See ../references/review-helpers.md. One selected thread, dry-run by default, no mutation retries.
3
+ import { parseArgs } from "node:util";
4
+ import { author, date, githubGraphQL, record, text } from "./check-pr-review";
5
+
6
+ type Selection = { pr: string; thread: string; head: string; reply: string; apply: boolean };
7
+ type Thread = { id: string; isResolved: boolean; isOutdated: boolean; viewerCanResolve: boolean };
8
+ const ACTOR = "{__typename login}";
9
+ const META = `id isResolved isOutdated viewerCanResolve pullRequest{url headRefOid state repository{viewerPermission}}`;
10
+ const COMMENTS = `comments(first:100,after:$cursor){pageInfo{hasNextPage endCursor} nodes{id url body state publishedAt isMinimized author${ACTOR} editor${ACTOR} lastEditedAt replyTo{id}}}`;
11
+
12
+ function selection(): Selection {
13
+ const { values, positionals } = parseArgs({
14
+ options: {
15
+ thread: { type: "string" },
16
+ "expect-head": { type: "string" },
17
+ "reply-url": { type: "string" },
18
+ apply: { type: "boolean", default: false },
19
+ },
20
+ allowPositionals: true,
21
+ });
22
+ const [pr] = positionals;
23
+ if (
24
+ positionals.length !== 1 ||
25
+ !pr ||
26
+ !/^https:\/\/github\.com\/[A-Za-z0-9-]+\/[A-Za-z0-9_.-]+\/pull\/[1-9][0-9]*$/.test(pr) ||
27
+ !values.thread?.trim() ||
28
+ !values["expect-head"] ||
29
+ !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(values["expect-head"]) ||
30
+ !values["reply-url"]?.startsWith(`${pr}#discussion_r`) ||
31
+ !/^#discussion_r[1-9][0-9]*$/.test(values["reply-url"].slice(pr.length))
32
+ ) {
33
+ throw new Error(
34
+ "usage: resolve-pr-thread.ts <GitHub PR URL> --thread <id> --expect-head <full SHA> --reply-url <reply URL> [--apply]",
35
+ );
36
+ }
37
+ return {
38
+ pr,
39
+ thread: values.thread,
40
+ head: values["expect-head"],
41
+ reply: values["reply-url"],
42
+ apply: values.apply === true,
43
+ };
44
+ }
45
+
46
+ function metadata(data: Record<string, unknown>, s: Selection): Thread {
47
+ const t = record(data.node);
48
+ const pr = record(t.pullRequest);
49
+ const permission = record(pr.repository).viewerPermission;
50
+ if (
51
+ t.__typename !== "PullRequestReviewThread" ||
52
+ t.id !== s.thread ||
53
+ pr.url !== s.pr ||
54
+ typeof t.isResolved !== "boolean" ||
55
+ typeof t.isOutdated !== "boolean" ||
56
+ typeof t.viewerCanResolve !== "boolean" ||
57
+ !author(data.viewer) ||
58
+ !(
59
+ permission === null ||
60
+ (text(permission) && ["READ", "TRIAGE", "WRITE", "MAINTAIN", "ADMIN"].includes(permission))
61
+ ) ||
62
+ !text(pr.state) ||
63
+ !["OPEN", "CLOSED", "MERGED"].includes(pr.state)
64
+ ) {
65
+ throw new Error("malformed or mismatched thread/PR metadata");
66
+ }
67
+ if (pr.headRefOid !== s.head) {
68
+ throw new Error("PR head differs from expected head; stop and re-read");
69
+ }
70
+ if (
71
+ !t.isResolved &&
72
+ (pr.state !== "OPEN" ||
73
+ !t.viewerCanResolve ||
74
+ !["WRITE", "MAINTAIN", "ADMIN"].includes(permission as string))
75
+ ) {
76
+ throw new Error("open PR and write/resolve permission required");
77
+ }
78
+ return t as unknown as Thread;
79
+ }
80
+
81
+ function request(s: Selection, comments: boolean, cursor?: string): Record<string, unknown> {
82
+ const query = `query($thread:ID!${comments ? ",$cursor:String" : ""}){viewer${ACTOR} node(id:$thread){__typename ... on PullRequestReviewThread{${META} ${comments ? COMMENTS : ""}}}}`;
83
+ return githubGraphQL(query, { thread: s.thread, ...(cursor ? { cursor } : {}) }, "github.com");
84
+ }
85
+
86
+ function comment(value: unknown): Record<string, unknown> {
87
+ const c = record(value);
88
+ if (
89
+ !text(c.id) ||
90
+ !text(c.url) ||
91
+ !text(c.body) ||
92
+ !text(c.state) ||
93
+ !["PENDING", "SUBMITTED"].includes(c.state) ||
94
+ !(c.publishedAt === null || date(c.publishedAt)) ||
95
+ typeof c.isMinimized !== "boolean" ||
96
+ !author(c.author) ||
97
+ !author(c.editor) ||
98
+ !(c.lastEditedAt === null || date(c.lastEditedAt)) ||
99
+ !(c.replyTo === null || text(record(c.replyTo).id))
100
+ ) {
101
+ throw new Error("malformed thread comment");
102
+ }
103
+ return c;
104
+ }
105
+
106
+ function readSelection(s: Selection): Thread {
107
+ const seen = new Set<string>();
108
+ const comments: Record<string, unknown>[] = [];
109
+ let cursor: string | undefined;
110
+ let viewer: Record<string, unknown>;
111
+ for (;;) {
112
+ const data = request(s, true, cursor);
113
+ metadata(data, s);
114
+ viewer = record(data.viewer);
115
+ const page = record(record(data.node).comments);
116
+ const info = record(page.pageInfo);
117
+ if (
118
+ !Array.isArray(page.nodes) ||
119
+ typeof info.hasNextPage !== "boolean" ||
120
+ !(info.endCursor === null || text(info.endCursor))
121
+ ) {
122
+ throw new Error("malformed thread comment page");
123
+ }
124
+ comments.push(...page.nodes.map(comment));
125
+ if (!info.hasNextPage) {
126
+ break;
127
+ }
128
+ if (!text(info.endCursor) || !info.endCursor || seen.has(info.endCursor)) {
129
+ throw new Error("non-advancing thread comment cursor");
130
+ }
131
+ seen.add(info.endCursor);
132
+ cursor = info.endCursor;
133
+ }
134
+ const matches = comments.filter((c) => c.url === s.reply);
135
+ if (matches.length !== 1) {
136
+ throw new Error("selected reply is missing or ambiguous in this thread");
137
+ }
138
+ const reply = matches[0]!;
139
+ const writer = record(reply.author);
140
+ const editor = reply.editor === null ? null : record(reply.editor);
141
+ if (
142
+ viewer.__typename !== "User" ||
143
+ writer.__typename !== "User" ||
144
+ writer.login !== viewer.login ||
145
+ reply.state !== "SUBMITTED" ||
146
+ reply.publishedAt === null ||
147
+ !String(reply.body).trim() ||
148
+ reply.isMinimized ||
149
+ reply.replyTo === null ||
150
+ !comments.some((c) => c.id === record(reply.replyTo).id && c.id !== reply.id) ||
151
+ (reply.lastEditedAt !== null &&
152
+ (editor?.__typename !== "User" || editor.login !== viewer.login))
153
+ ) {
154
+ throw new Error(
155
+ "reply must be published, submitted, visible, nonblank, authored by the authenticated human and edited only by that human",
156
+ );
157
+ }
158
+ const final = request(s, false);
159
+ const currentViewer = record(final.viewer);
160
+ if (currentViewer.__typename !== viewer.__typename || currentViewer.login !== viewer.login) {
161
+ throw new Error("authenticated viewer changed during read");
162
+ }
163
+ return metadata(final, s);
164
+ }
165
+
166
+ function resolve(s: Selection): void {
167
+ const before = readSelection(s);
168
+ if (before.isResolved) {
169
+ console.log(`Already resolved: ${s.thread}; no mutation`);
170
+ return;
171
+ }
172
+ if (!s.apply) {
173
+ console.log(
174
+ `Dry-run: ready to resolve ${s.thread}${before.isOutdated ? " (outdated)" : ""}; judge the selected reply's explanation before applying`,
175
+ );
176
+ return;
177
+ }
178
+ let uncertain = false;
179
+ try {
180
+ const data = githubGraphQL(
181
+ "mutation($thread:ID!){resolveReviewThread(input:{threadId:$thread}){thread{id isResolved}}}",
182
+ { thread: s.thread },
183
+ "github.com",
184
+ );
185
+ const result = record(record(data.resolveReviewThread).thread);
186
+ if (result.id !== s.thread || result.isResolved !== true) {
187
+ throw new Error("unverified mutation payload");
188
+ }
189
+ } catch {
190
+ uncertain = true;
191
+ }
192
+ // A lost response may hide a successful mutation. Read before any user-initiated retry.
193
+ let after: Thread;
194
+ try {
195
+ after = readSelection(s);
196
+ } catch (error) {
197
+ throw new Error(
198
+ `Mutation attempted once; final verification failed (${error instanceof Error ? error.message : "unknown error"}). Thread may already be resolved; re-read before any retry`,
199
+ { cause: error },
200
+ );
201
+ }
202
+ if (!after.isResolved) {
203
+ throw new Error(
204
+ "thread remains unresolved after mutation attempt; no retry made; re-read before retrying",
205
+ );
206
+ }
207
+ console.log(
208
+ `Verified resolved: ${s.thread} at ${s.head}${uncertain ? "; mutation response uncertain, fresh state verified" : ""}`,
209
+ );
210
+ }
211
+
212
+ if (import.meta.main) {
213
+ try {
214
+ resolve(selection());
215
+ } catch (error) {
216
+ console.error(`resolve-pr-thread: ${error instanceof Error ? error.message : "failed"}`);
217
+ process.exitCode = 2;
218
+ }
219
+ }
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: hr-progress-report
3
+ description: "Report the current task's activity, completion evidence and remaining wall-clock time. Use for task status, progress, completion percentage or ETA requests. Session-roster and liveness checks belong to the available coordination tools."
4
+ ---
5
+
6
+ # Progress report
7
+
8
+ Produce a dated snapshot of the requested task. A status request does not
9
+ cancel authorized work or authorize new work. Resume work after the report
10
+ under the governing continuation and offer rules.
11
+
12
+ 1. Identify the task's goal, selected scope and completion criteria. Read
13
+ the relevant plan and latest results. Name the records checked and any
14
+ missing coverage. Keep unrelated backlog items outside this task report.
15
+ 2. Verify current activity with available job state or recent results.
16
+ Distinguish working, waiting, blocked and idle. Say when activity cannot
17
+ be verified; a saved task description alone does not establish current work.
18
+ 3. Reconcile completed and remaining milestones against the completion
19
+ criteria. Include validation and landing when they belong to the selected
20
+ scope. List blockers and who or what can resolve each one.
21
+ 4. State the basis for a completion percentage. Use bounded work units or
22
+ milestone weights recorded before this report. Label a simple milestone
23
+ count as milestone completion, not a share of remaining time. During
24
+ open-ended investigation, report percentage unknown and the evidence gathered.
25
+ Explain scope changes that alter an earlier percentage. Report full
26
+ completion only when every selected completion criterion is satisfied.
27
+ 5. Show elapsed wall-clock time and remaining ETA under the governing
28
+ estimate policy. Without one, use comparable finished runs or observed
29
+ job progress and name the basis. Include tests, CI, review and fix rounds,
30
+ integration and dependency waits. Give a range and confidence when the
31
+ evidence supports them. Mark missing evidence or unbounded external waits
32
+ unknown or unmeasured. Give a running job's finish forecast separately
33
+ from the whole-task ETA. Include the timezone for clock-time forecasts.
34
+ 6. Lead with the task's state and current action, followed by a compact
35
+ table. Separate verified facts, session estimates and unknowns. End with
36
+ the next action or required decision using the governing offer format.
37
+
38
+ | Field | Report |
39
+ | ---------------------- | ------------------------------------------------------------ |
40
+ | Task / state | Goal, selected scope; working, waiting, blocked or idle |
41
+ | Current action | Action and supporting job state or recent result |
42
+ | Progress | Completed milestones, percentage basis or unknown |
43
+ | Remaining | Unfinished milestones, including checks and integration |
44
+ | Elapsed / ETA | Wall-clock time, evidence, confidence and waiting conditions |
45
+ | Blockers / next action | Dependency, owner or decision needed |
46
+
47
+ This is a snapshot, not independent proof of another session's activity.
48
+ Use existing tools and records. Do not start watchers, ping other sessions
49
+ or change schedules merely to produce the report.
@@ -0,0 +1,5 @@
1
+ interface:
2
+ display_name: "HR: Progress report"
3
+ short_description: "Report task progress and remaining wall-clock time"
4
+ policy:
5
+ allow_implicit_invocation: true
@@ -0,0 +1,44 @@
1
+ ---
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.
4
+ ---
5
+
6
+ # Read Reddit
7
+
8
+ Reddit usually answers agent fetches of pages and `.json` with 403, and its
9
+ mirrors are unreliable. Pullpush's archive answers agents with 429 and asks for a
10
+ paid plan, so do not scrape it. The bundled script reads post and comment bodies
11
+ returned by Reddit's public Atom feeds without a key. Feed output can omit
12
+ comments; do not present it as an exhaustive thread.
13
+
14
+ [Reddit's announcement](https://www.reddit.com/r/modnews/comments/1wubgvt/continuing_our_infrastructure_updates_whats/)
15
+ says RSS support ends November 13, 2026. Until then, try the feeds below and
16
+ report any retrieval failure. After the cutoff, use the partial search fallback
17
+ below rather than assuming the feeds remain supported.
18
+
19
+ Read feeds with:
20
+
21
+ ```bash
22
+ bun scripts/reddit.ts thread <reddit or redd.it URL>... [--json]
23
+ bun scripts/reddit.ts search <subreddit|all> "<query>" [--pages N] [--since 2026-06-01] [--sort new|relevance|top] [--json]
24
+ ```
25
+
26
+ Run it from this skill's directory. Text output gives each post's title, URL,
27
+ author, date and body, then each comment. Every link appears as
28
+ `text (url)`, so `grep -o` can harvest links. `--json` prints one object per
29
+ entry (`id`, `kind`, `author`, `published`, `title`, `url`, `text`) for scripted
30
+ collection. Search pages hold 100 results. With `--sort new`, `--since` stops
31
+ paging at the first older result.
32
+
33
+ Limits to state when they matter:
34
+
35
+ 1. Unauthenticated feeds allow about one request per minute. The script waits
36
+ out Reddit's `x-ratelimit-reset` between requests and retries a 429 twice,
37
+ so run long searches in the background and keep `--pages` small.
38
+ 2. A thread feed is flat: comments come without reply nesting. Images and
39
+ videos appear only as links.
40
+ 3. Search covers what Reddit's own search indexes. A deleted, removed or
41
+ private thread fails with its HTTP status.
42
+
43
+ If the feeds are blocked, fall back to the host's web search tool with
44
+ `site:reddit.com`. Say that the results are partial snippets.
@@ -0,0 +1,2 @@
1
+ interface:
2
+ display_name: "HR: Read Reddit"
@@ -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"