@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.
- package/CONTRIBUTING.md +54 -0
- package/LICENSE +21 -0
- package/README.md +347 -2
- package/THIRD_PARTY_NOTICES.md +188 -0
- package/compose.mjs +119 -0
- package/composition.mjs +353 -0
- package/config-view.mjs +194 -0
- package/config.mjs +320 -0
- package/docs/assets/banner-dark.png +0 -0
- package/docs/assets/banner-light.png +0 -0
- package/docs/assets/overview.png +0 -0
- package/docs/configuration.md +193 -0
- package/docs/npm-release.md +45 -0
- package/docs/skill-names.md +50 -0
- package/examples/person/house-rules.json +15 -0
- package/examples/person/rules/10-tooling.md +8 -0
- package/examples/person/rules/20-reporting.md +9 -0
- package/examples/project/AGENTS.md +18 -0
- package/package.json +40 -4
- package/rule-references.mjs +91 -0
- package/rules/core.md +431 -0
- package/rules/modifiers/coded-offers.md +40 -0
- package/rules/modifiers/effort-estimates.md +41 -0
- package/rules/modifiers/land-when-done.md +12 -0
- package/rules/modifiers/low-quota-handoff.md +19 -0
- package/rules/modifiers/multi-agent.md +34 -0
- package/rules/modifiers/no-attribution.md +10 -0
- package/rules/modifiers/plan-files.md +50 -0
- package/rules/modifiers/question-cards.md +48 -0
- package/rules/modifiers/release-batching.md +13 -0
- package/rules/modifiers/scratch-on-disk.md +15 -0
- package/rules/modifiers/shared-host-load.md +48 -0
- package/rules/modifiers/solo-operator.md +13 -0
- package/rules/modifiers/squash-landing.md +22 -0
- package/rules/modifiers/swarmail.md +12 -0
- package/rules/references/github-actions.md +45 -0
- package/rules/references/landing.md +39 -0
- package/rules/references/multi-agent.md +47 -0
- package/rules/references/release-batching.md +43 -0
- package/rules/references/squash-landing.md +44 -0
- package/setup.mjs +375 -0
- package/skills/hr-agent-guidance-audit/LICENSE +21 -0
- package/skills/hr-agent-guidance-audit/SKILL.md +86 -0
- package/skills/hr-agent-guidance-audit/agents/openai.yaml +2 -0
- package/skills/hr-agent-guidance-audit/references/records-and-lessons.md +60 -0
- package/skills/hr-agent-guidance-refresh/LICENSE +21 -0
- package/skills/hr-agent-guidance-refresh/SKILL.md +68 -0
- package/skills/hr-agent-guidance-refresh/agents/openai.yaml +2 -0
- package/skills/hr-agent-guidance-refresh/references/verification.md +47 -0
- package/skills/hr-api-exposure-check/SKILL.md +159 -0
- package/skills/hr-api-exposure-check/agents/openai.yaml +3 -0
- package/skills/hr-audit-choices/SKILL.md +67 -0
- package/skills/hr-audit-choices/agents/openai.yaml +2 -0
- package/skills/hr-benchmarking/NOTICE.md +36 -0
- package/skills/hr-benchmarking/SKILL.md +316 -0
- package/skills/hr-benchmarking/agents/openai.yaml +2 -0
- package/skills/hr-benchmarking/references/javascript.md +17 -0
- package/skills/hr-benchmarking/references/priority-findings-20261006.md +115 -0
- package/skills/hr-benchmarking/references/tools.md +160 -0
- package/skills/hr-benchmarking/scripts/process-attribution.ps1 +20 -0
- package/skills/hr-benchmarking/scripts/process-attribution.py +107 -0
- package/skills/hr-better-accessibility/LICENSE +22 -0
- package/skills/hr-better-accessibility/NOTICE.md +46 -0
- package/skills/hr-better-accessibility/SKILL.md +119 -0
- package/skills/hr-better-accessibility/agents/openai.yaml +3 -0
- package/skills/hr-better-accessibility/focus-and-keyboard.md +130 -0
- package/skills/hr-better-accessibility/forms.md +82 -0
- package/skills/hr-better-accessibility/hit-areas.md +98 -0
- package/skills/hr-better-accessibility/motion-and-zoom.md +81 -0
- package/skills/hr-better-accessibility/screen-readers.md +101 -0
- package/skills/hr-better-accessibility/semantics-and-aria.md +84 -0
- package/skills/hr-better-colors/LICENSE +21 -0
- package/skills/hr-better-colors/NOTICE.md +37 -0
- package/skills/hr-better-colors/SKILL.md +114 -0
- package/skills/hr-better-colors/agents/openai.yaml +3 -0
- package/skills/hr-better-colors/color-formats.md +90 -0
- package/skills/hr-better-colors/color-usage.md +140 -0
- package/skills/hr-better-colors/contrast.md +80 -0
- package/skills/hr-better-colors/palette-generation.md +104 -0
- package/skills/hr-better-colors/palette-structure.md +76 -0
- package/skills/hr-better-colors/token-naming.md +97 -0
- package/skills/hr-better-layout/LICENSE +22 -0
- package/skills/hr-better-layout/NOTICE.md +48 -0
- package/skills/hr-better-layout/SKILL.md +92 -0
- package/skills/hr-better-layout/agents/openai.yaml +3 -0
- package/skills/hr-better-layout/grouping-and-alignment.md +144 -0
- package/skills/hr-better-layout/spacing-and-adaptivity.md +215 -0
- package/skills/hr-better-typography/LICENSE +22 -0
- package/skills/hr-better-typography/NOTICE.md +46 -0
- package/skills/hr-better-typography/SKILL.md +172 -0
- package/skills/hr-better-typography/agents/openai.yaml +3 -0
- package/skills/hr-better-typography/choosing-fonts.md +69 -0
- package/skills/hr-better-typography/css-cheat-sheet.md +65 -0
- package/skills/hr-better-typography/details-and-accessibility.md +119 -0
- package/skills/hr-better-typography/spacing-and-sizing.md +131 -0
- package/skills/hr-better-typography/variable-fonts-and-opentype.md +105 -0
- package/skills/hr-better-typography/wrapping-and-punctuation.md +57 -0
- package/skills/hr-better-writing/LICENSE +22 -0
- package/skills/hr-better-writing/NOTICE.md +46 -0
- package/skills/hr-better-writing/SKILL.md +122 -0
- package/skills/hr-better-writing/agents/openai.yaml +3 -0
- package/skills/hr-change-impact/SKILL.md +113 -0
- package/skills/hr-change-impact/agents/openai.yaml +5 -0
- package/skills/hr-code-review/LICENSE +21 -0
- package/skills/hr-code-review/NOTICE.md +26 -0
- package/skills/hr-code-review/SKILL.md +147 -0
- package/skills/hr-code-review/agents/openai.yaml +3 -0
- package/skills/hr-code-review/references/evidence.md +57 -0
- package/skills/hr-design-exploration/SKILL.md +82 -0
- package/skills/hr-design-exploration/agents/openai.yaml +4 -0
- package/skills/hr-diagnosing-bugs/NOTICE.md +45 -0
- package/skills/hr-diagnosing-bugs/SKILL.md +108 -0
- package/skills/hr-diagnosing-bugs/agents/openai.yaml +3 -0
- package/skills/hr-diagnosing-bugs/references/reporting.md +72 -0
- package/skills/hr-explain-code/NOTICE.md +37 -0
- package/skills/hr-explain-code/SKILL.md +110 -0
- package/skills/hr-explain-code/agents/openai.yaml +5 -0
- package/skills/hr-explain-code/references/helper-brief.md +39 -0
- package/skills/hr-extract-shared-steps/SKILL.md +134 -0
- package/skills/hr-extract-shared-steps/agents/openai.yaml +3 -0
- package/skills/hr-handoff/SKILL.md +91 -0
- package/skills/hr-handoff/agents/openai.yaml +5 -0
- package/skills/hr-handoff/assets/ledger.example.json +10 -0
- package/skills/hr-handoff/assets/receipt.example.json +76 -0
- package/skills/hr-handoff/assets/source.example.json +20 -0
- package/skills/hr-handoff/references/reconciliation.md +105 -0
- package/skills/hr-handoff/scripts/check-reconciliation.mjs +282 -0
- package/skills/hr-house-rules-setup/SKILL.md +100 -0
- package/skills/hr-house-rules-setup/agents/openai.yaml +5 -0
- package/skills/hr-issue-tracker-setup/SKILL.md +49 -0
- package/skills/hr-issue-tracker-setup/agents/openai.yaml +5 -0
- package/skills/hr-lean-plan/SKILL.md +142 -0
- package/skills/hr-lean-plan/agents/openai.yaml +5 -0
- package/skills/hr-maintainability-review/SKILL.md +176 -0
- package/skills/hr-maintainability-review/agents/openai.yaml +5 -0
- package/skills/hr-manual-qa/NOTICE.md +36 -0
- package/skills/hr-manual-qa/SKILL.md +37 -0
- package/skills/hr-manual-qa/agents/openai.yaml +3 -0
- package/skills/hr-navigation-retro/LICENSE +21 -0
- package/skills/hr-navigation-retro/NOTICE.md +5 -0
- package/skills/hr-navigation-retro/SKILL.md +86 -0
- package/skills/hr-navigation-retro/agents/openai.yaml +4 -0
- package/skills/hr-navigation-retro/references/inputs.md +67 -0
- package/skills/hr-navigation-retro/scripts/retro.py +552 -0
- package/skills/hr-navigation-retro/scripts/transcripts.py +205 -0
- package/skills/hr-ordering-tests/SKILL.md +51 -0
- package/skills/hr-ordering-tests/agents/openai.yaml +2 -0
- package/skills/hr-plain-prose/SKILL.md +261 -0
- package/skills/hr-plain-prose/agents/openai.yaml +3 -0
- package/skills/hr-plain-prose/scripts/check.mjs +331 -0
- package/skills/hr-pr-followup/LICENSE +21 -0
- package/skills/hr-pr-followup/NOTICE.md +9 -0
- package/skills/hr-pr-followup/SKILL.md +71 -0
- package/skills/hr-pr-followup/agents/openai.yaml +2 -0
- package/skills/hr-pr-followup/references/review-helpers.md +90 -0
- package/skills/hr-pr-followup/scripts/check-pr-review.ts +510 -0
- package/skills/hr-pr-followup/scripts/resolve-pr-thread.ts +219 -0
- package/skills/hr-progress-report/SKILL.md +49 -0
- package/skills/hr-progress-report/agents/openai.yaml +5 -0
- package/skills/hr-read-reddit/SKILL.md +44 -0
- package/skills/hr-read-reddit/agents/openai.yaml +2 -0
- package/skills/hr-read-reddit/scripts/reddit.ts +252 -0
- package/skills/hr-read-x-links/SKILL.md +42 -0
- package/skills/hr-read-x-links/agents/openai.yaml +2 -0
- package/skills/hr-read-x-links/scripts/x-post.ts +264 -0
- package/skills/hr-split-to-prs/LICENSE +21 -0
- package/skills/hr-split-to-prs/NOTICE.md +9 -0
- package/skills/hr-split-to-prs/SKILL.md +61 -0
- package/skills/hr-split-to-prs/agents/openai.yaml +2 -0
- package/skills/hr-stock-ui-audit/SKILL.md +132 -0
- package/skills/hr-stock-ui-audit/agents/openai.yaml +3 -0
- package/skills/hr-stock-ui-audit/scripts/scan.mjs +619 -0
- package/skills/hr-tdd/LICENSE +21 -0
- package/skills/hr-tdd/NOTICE.md +10 -0
- package/skills/hr-tdd/SKILL.md +40 -0
- package/skills/hr-tdd/agents/openai.yaml +3 -0
- package/skills/hr-tdd/mocking.md +39 -0
- package/skills/hr-tdd/tests.md +77 -0
- package/skills/hr-test-audit/SKILL.md +146 -0
- package/skills/hr-test-audit/agents/openai.yaml +3 -0
- package/skills/hr-test-audit/scripts/floor-guard.ts +137 -0
- package/skills/hr-what-next/SKILL.md +61 -0
- package/skills/hr-what-next/agents/openai.yaml +4 -0
- package/skills/hr-what-next/references/reporting.md +68 -0
- package/skills/hr-writing-for-agents/NOTICE.md +46 -0
- package/skills/hr-writing-for-agents/SKILL-MECHANICS.md +83 -0
- package/skills/hr-writing-for-agents/SKILL.md +108 -0
- package/skills/hr-writing-for-agents/agents/openai.yaml +3 -0
- package/skills/hr-writing-pr/NOTICE.md +48 -0
- package/skills/hr-writing-pr/SKILL.md +104 -0
- 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,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,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.
|