@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.
- package/CODE_OF_CONDUCT.md +83 -0
- package/CONTRIBUTING.md +54 -0
- package/LICENSE +21 -0
- package/README.md +378 -2
- package/THIRD_PARTY_NOTICES.md +188 -0
- package/compose.mjs +125 -0
- package/composition.mjs +361 -0
- package/config-view.mjs +209 -0
- package/config.mjs +381 -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 +269 -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 +42 -4
- package/rule-references.mjs +91 -0
- package/rules/core.md +340 -0
- package/rules/modifiers/backlog-outline.md +25 -0
- package/rules/modifiers/backlog-visual.md +16 -0
- package/rules/modifiers/coded-offers.md +70 -0
- package/rules/modifiers/cross-session-relay.md +23 -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 +20 -0
- package/rules/modifiers/model-default-questions.md +33 -0
- package/rules/modifiers/multi-agent.md +38 -0
- package/rules/modifiers/no-attribution.md +16 -0
- package/rules/modifiers/periodic-updates.md +25 -0
- package/rules/modifiers/plan-files.md +82 -0
- package/rules/modifiers/question-cards.md +48 -0
- package/rules/modifiers/release-batching.md +15 -0
- package/rules/modifiers/scratch-on-disk.md +15 -0
- package/rules/modifiers/shared-host-load.md +19 -0
- package/rules/modifiers/solo-operator.md +13 -0
- package/rules/modifiers/squash-landing.md +22 -0
- package/rules/modifiers/upstream-contributions.md +45 -0
- package/rules/modifiers/web-image-formats.md +35 -0
- package/rules/references/backlog-view.md +154 -0
- package/rules/references/github-actions.md +45 -0
- package/rules/references/landing.md +39 -0
- package/rules/references/multi-agent.md +51 -0
- package/rules/references/release-batching.md +66 -0
- package/rules/references/squash-landing.md +50 -0
- package/rules/references/thread-visuals.md +45 -0
- package/setup.mjs +445 -0
- package/skills/hr-agent-guidance-audit/LICENSE +21 -0
- package/skills/hr-agent-guidance-audit/SKILL.md +91 -0
- package/skills/hr-agent-guidance-audit/agents/openai.yaml +2 -0
- package/skills/hr-agent-guidance-audit/references/prune-workflow.md +72 -0
- package/skills/hr-agent-guidance-audit/references/records-and-lessons.md +60 -0
- package/skills/hr-agent-guidance-audit/references/retirement-verdicts.md +49 -0
- package/skills/hr-agent-guidance-refresh/LICENSE +21 -0
- package/skills/hr-agent-guidance-refresh/SKILL.md +36 -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 +279 -0
- package/skills/hr-benchmarking/agents/openai.yaml +2 -0
- package/skills/hr-benchmarking/references/javascript.md +130 -0
- package/skills/hr-benchmarking/references/tools.md +165 -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 +102 -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-code-review/LICENSE +21 -0
- package/skills/hr-code-review/NOTICE.md +26 -0
- package/skills/hr-code-review/SKILL.md +163 -0
- package/skills/hr-code-review/agents/openai.yaml +3 -0
- package/skills/hr-code-review/references/acp-client.md +43 -0
- package/skills/hr-code-review/references/evidence.md +57 -0
- package/skills/hr-code-review/references/impact.md +103 -0
- package/skills/hr-code-review/references/panel-adjudication.md +58 -0
- package/skills/hr-code-review/references/panel-plan.md +61 -0
- package/skills/hr-code-review/references/panel-recommendations.json +105 -0
- package/skills/hr-code-review/references/panel-recommendations.md +46 -0
- package/skills/hr-code-review/references/panel-setup.md +54 -0
- package/skills/hr-code-review/references/reviewer-role.md +31 -0
- package/skills/hr-code-review/scripts/acp-panel-client.mjs +299 -0
- package/skills/hr-code-review/scripts/check-receipts.mjs +263 -0
- package/skills/hr-code-review/scripts/panel-plan.mjs +69 -0
- package/skills/hr-code-review/scripts/panel-preferences.mjs +220 -0
- package/skills/hr-design-exploration/SKILL.md +105 -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 +97 -0
- package/skills/hr-diagnosing-bugs/scripts/check-pr-offer.mjs +47 -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-forgotten-work-audit/SKILL.md +130 -0
- package/skills/hr-forgotten-work-audit/references/brief-template.md +56 -0
- package/skills/hr-forgotten-work-audit/references/result-format.md +46 -0
- package/skills/hr-handoff/LICENSE +21 -0
- package/skills/hr-handoff/NOTICE.md +29 -0
- package/skills/hr-handoff/SKILL.md +114 -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 +124 -0
- package/skills/hr-handoff/references/successor-thread.md +76 -0
- package/skills/hr-handoff/scripts/check-reconciliation.mjs +324 -0
- package/skills/hr-house-rules-setup/SKILL.md +114 -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 +146 -0
- package/skills/hr-lean-plan/agents/openai.yaml +5 -0
- package/skills/hr-maintainability-review/SKILL.md +186 -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 +264 -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 +74 -0
- package/skills/hr-pr-followup/agents/openai.yaml +2 -0
- package/skills/hr-pr-followup/references/review-helpers.md +111 -0
- package/skills/hr-pr-followup/scripts/check-landing.py +204 -0
- package/skills/hr-pr-followup/scripts/check-pr-review.ts +510 -0
- package/skills/hr-pr-followup/scripts/merge-pr.sh +100 -0
- package/skills/hr-pr-followup/scripts/resolve-pr-thread.ts +219 -0
- package/skills/hr-pr-followup/scripts/verify-landed.sh +184 -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-research-shared-links/SKILL.md +103 -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-sqlite/SKILL.md +174 -0
- package/skills/hr-sqlite/agents/openai.yaml +2 -0
- package/skills/hr-sqlite/references/evidence.md +94 -0
- package/skills/hr-sqlite/references/tool-decisions.md +39 -0
- package/skills/hr-sqlite/scripts/inspect-sqlite.py +245 -0
- package/skills/hr-stock-ui-audit/SKILL.md +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/LICENSE +21 -0
- package/skills/hr-test-audit/NOTICE.md +67 -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-usage-report/SKILL.md +90 -0
- package/skills/hr-usage-report/agents/openai.yaml +2 -0
- package/skills/hr-usage-report/assets/example-report.json +125 -0
- package/skills/hr-usage-report/assets/report-template.html +377 -0
- package/skills/hr-usage-report/scripts/render-report.ts +86 -0
- package/skills/hr-usage-report/scripts/usage-by-model.ts +499 -0
- package/skills/hr-what-next/SKILL.md +151 -0
- package/skills/hr-what-next/agents/openai.yaml +4 -0
- package/skills/hr-what-next/references/reporting.md +59 -0
- package/skills/hr-what-next/scripts/check-open-work.mjs +711 -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 +119 -0
- package/skills/hr-writing-pr/agents/openai.yaml +2 -0
- package/skills/hr-writing-pr/scripts/benefit-chart.py +200 -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,184 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Post-merge landing checks: containment, local-default state, leftover remote branches.
|
|
3
|
+
# Fetches and verifies both remote containment and local-default equality. A stale local default
|
|
4
|
+
# fails even when the remote contains the commit. Never resets, switches or deletes.
|
|
5
|
+
# With --ff it updates a default checked out nowhere or here with a clean tree.
|
|
6
|
+
# --idle-primary <path> additionally permits a fast-forward of this repository's primary checkout:
|
|
7
|
+
# use existing ownership evidence or the operator's standing idle direction, unless current
|
|
8
|
+
# evidence contradicts it. Git preserves
|
|
9
|
+
# unrelated local edits and refuses a fast-forward that would overwrite them. It moves the
|
|
10
|
+
# primary to the verified remote commit with `merge --ff-only <sha>` rather than `pull`, because
|
|
11
|
+
# `pull` re-fetches into the shared FETCH_HEAD, which a concurrent fetch in the same checkout can
|
|
12
|
+
# overwrite ("Cannot fast-forward to multiple branches"). Other linked worktrees
|
|
13
|
+
# remain their owners' responsibility. After a blocked post-merge check, rerun this script,
|
|
14
|
+
# not merge-pr.sh: the PR is already merged.
|
|
15
|
+
# --preflight checks synchronization eligibility before landing, without moving local branches.
|
|
16
|
+
# It requires --ff and rejects dirty or diverged targets and unconfirmed checkout ownership,
|
|
17
|
+
# even when the local default is current. The caller must keep the target idle until landing
|
|
18
|
+
# finishes; this check cannot establish session ownership or prevent another process starting.
|
|
19
|
+
# Usage:
|
|
20
|
+
# bash scripts/verify-landed.sh --commit <sha> [--default <branch>]
|
|
21
|
+
# [--branch <merged-head>] [--remote <name>] [--ff [--idle-primary <path>]]
|
|
22
|
+
# bash scripts/verify-landed.sh --preflight --ff [--default <branch>]
|
|
23
|
+
# [--remote <name>] [--idle-primary <path>]
|
|
24
|
+
set -euo pipefail
|
|
25
|
+
|
|
26
|
+
REMOTE=origin
|
|
27
|
+
DEFAULT=""
|
|
28
|
+
COMMIT=""
|
|
29
|
+
BRANCH=""
|
|
30
|
+
FF=0
|
|
31
|
+
IDLE_PRIMARY=""
|
|
32
|
+
PREFLIGHT=0
|
|
33
|
+
|
|
34
|
+
while [ $# -gt 0 ]; do
|
|
35
|
+
case "$1" in
|
|
36
|
+
--commit) COMMIT="${2:?}"; shift 2;;
|
|
37
|
+
--default) DEFAULT="${2:?}"; shift 2;;
|
|
38
|
+
--branch) BRANCH="${2:?}"; shift 2;;
|
|
39
|
+
--remote) REMOTE="${2:?}"; shift 2;;
|
|
40
|
+
--ff) FF=1; shift;;
|
|
41
|
+
--idle-primary) IDLE_PRIMARY="${2:?}"; shift 2;;
|
|
42
|
+
--preflight) PREFLIGHT=1; shift;;
|
|
43
|
+
*) echo "unknown argument: $1" >&2; exit 2;;
|
|
44
|
+
esac
|
|
45
|
+
done
|
|
46
|
+
if [ "$PREFLIGHT" = 1 ]; then
|
|
47
|
+
[ "$FF" = 1 ] && [ -z "$COMMIT$BRANCH" ] \
|
|
48
|
+
|| { echo "--preflight requires --ff and excludes --commit/--branch" >&2; exit 2; }
|
|
49
|
+
else
|
|
50
|
+
[ -n "$COMMIT" ] || { echo "missing --commit <sha>" >&2; exit 2; }
|
|
51
|
+
fi
|
|
52
|
+
if [ -n "$IDLE_PRIMARY" ]; then
|
|
53
|
+
[ "$FF" = 1 ] || { echo "--idle-primary requires --ff" >&2; exit 2; }
|
|
54
|
+
PRIMARY="$(git worktree list --porcelain | sed -n '1s/^worktree //p')"
|
|
55
|
+
IDLE_PRIMARY="$(git -C "$IDLE_PRIMARY" rev-parse --show-toplevel)"
|
|
56
|
+
[ "$IDLE_PRIMARY" = "$PRIMARY" ] \
|
|
57
|
+
|| { echo "--idle-primary is not this repository's primary checkout: $IDLE_PRIMARY" >&2; exit 2; }
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
if [ -z "$DEFAULT" ]; then
|
|
61
|
+
# The remote's own answer wins; the cached origin/HEAD goes stale when the default branch changes.
|
|
62
|
+
DEFAULT="$(git ls-remote --symref "$REMOTE" HEAD 2>/dev/null | sed -n 's#^ref: refs/heads/\([^[:space:]]*\).*#\1#p' || true)"
|
|
63
|
+
if [ -z "$DEFAULT" ]; then
|
|
64
|
+
DEFAULT="$(git symbolic-ref "refs/remotes/$REMOTE/HEAD" 2>/dev/null | sed "s#^refs/remotes/$REMOTE/##" || true)"
|
|
65
|
+
fi
|
|
66
|
+
[ -n "$DEFAULT" ] || { echo "cannot resolve remote default branch; pass --default" >&2; exit 2; }
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
git fetch "$REMOTE" "+refs/heads/$DEFAULT:refs/remotes/$REMOTE/$DEFAULT" >/dev/null 2>&1 \
|
|
70
|
+
|| { echo "fetch failed: $REMOTE $DEFAULT" >&2; exit 1; }
|
|
71
|
+
|
|
72
|
+
WTLS="$(git worktree list --porcelain)"
|
|
73
|
+
HOLDER="$(printf '%s\n' "$WTLS" | awk -v want="branch refs/heads/$DEFAULT" '
|
|
74
|
+
/^worktree / { path = substr($0, 10) }
|
|
75
|
+
$0 == want { print path; exit }
|
|
76
|
+
')"
|
|
77
|
+
HERE="$(git rev-parse --show-toplevel)"
|
|
78
|
+
|
|
79
|
+
if [ "$PREFLIGHT" = 1 ]; then
|
|
80
|
+
# A rebase or bisect detaches HEAD while still reserving its original branch.
|
|
81
|
+
if [ -z "$HOLDER" ]; then
|
|
82
|
+
while IFS= read -r RECORD; do
|
|
83
|
+
case "$RECORD" in worktree\ *) CHECKOUT="${RECORD#worktree }";; *) continue;; esac
|
|
84
|
+
[ -d "$CHECKOUT" ] || continue
|
|
85
|
+
for ASSOCIATION in rebase-merge/head-name rebase-apply/head-name BISECT_START; do
|
|
86
|
+
ASSOCIATION_PATH="$(git -C "$CHECKOUT" rev-parse --path-format=absolute --git-path "$ASSOCIATION")"
|
|
87
|
+
if [ -f "$ASSOCIATION_PATH" ]; then
|
|
88
|
+
IFS= read -r ASSOCIATED_BRANCH < "$ASSOCIATION_PATH"
|
|
89
|
+
if [ "$ASSOCIATED_BRANCH" = "refs/heads/$DEFAULT" ] || [ "$ASSOCIATED_BRANCH" = "$DEFAULT" ]; then
|
|
90
|
+
HOLDER="$CHECKOUT"
|
|
91
|
+
break 2
|
|
92
|
+
fi
|
|
93
|
+
fi
|
|
94
|
+
done
|
|
95
|
+
done <<< "$WTLS"
|
|
96
|
+
fi
|
|
97
|
+
if [ -n "$HOLDER" ]; then
|
|
98
|
+
if [ "$HOLDER" != "$HERE" ] && [ "$HOLDER" != "$IDLE_PRIMARY" ]; then
|
|
99
|
+
echo "sync blocked at $HOLDER: that checkout has the default branch. If nothing is using it, rerun with --idle-primary <primary-path>" >&2
|
|
100
|
+
exit 1
|
|
101
|
+
fi
|
|
102
|
+
STATUS="$(git -C "$HOLDER" status --porcelain)" \
|
|
103
|
+
|| { echo "sync blocked at $HOLDER: cannot inspect working tree" >&2; exit 1; }
|
|
104
|
+
[ -z "$STATUS" ] \
|
|
105
|
+
|| { echo "sync blocked at $HOLDER: working tree dirty; leave it for its owner" >&2; exit 1; }
|
|
106
|
+
for OPERATION in MERGE_HEAD CHERRY_PICK_HEAD REVERT_HEAD rebase-merge rebase-apply sequencer BISECT_START; do
|
|
107
|
+
OPERATION_PATH="$(git -C "$HOLDER" rev-parse --path-format=absolute --git-path "$OPERATION")"
|
|
108
|
+
[ ! -e "$OPERATION_PATH" ] \
|
|
109
|
+
|| { echo "sync blocked at $HOLDER: unfinished Git operation: $OPERATION" >&2; exit 1; }
|
|
110
|
+
done
|
|
111
|
+
fi
|
|
112
|
+
if git show-ref --verify --quiet "refs/heads/$DEFAULT"; then
|
|
113
|
+
git merge-base --is-ancestor "$DEFAULT" "$REMOTE/$DEFAULT" \
|
|
114
|
+
|| { echo "sync blocked: $DEFAULT cannot fast-forward to $REMOTE/$DEFAULT" >&2; exit 1; }
|
|
115
|
+
fi
|
|
116
|
+
echo "synchronization preflight passed: $DEFAULT (${HOLDER:-not checked out})"
|
|
117
|
+
exit 0
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
FAIL=0
|
|
121
|
+
|
|
122
|
+
if git merge-base --is-ancestor "$COMMIT" "$REMOTE/$DEFAULT" 2>/dev/null; then
|
|
123
|
+
echo "contained: $COMMIT in $REMOTE/$DEFAULT"
|
|
124
|
+
else
|
|
125
|
+
echo "NOT CONTAINED: $COMMIT missing from $REMOTE/$DEFAULT" >&2
|
|
126
|
+
FAIL=1
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
if git show-ref --verify --quiet "refs/heads/$DEFAULT"; then
|
|
130
|
+
LOCAL="$(git rev-parse "$DEFAULT")"
|
|
131
|
+
UPSTREAM="$(git rev-parse "$REMOTE/$DEFAULT")"
|
|
132
|
+
if [ "$LOCAL" = "$UPSTREAM" ]; then
|
|
133
|
+
echo "local $DEFAULT: current ($LOCAL)"
|
|
134
|
+
elif git merge-base --is-ancestor "$LOCAL" "$UPSTREAM" 2>/dev/null; then
|
|
135
|
+
BEHIND="$(git rev-list --count "$LOCAL..$UPSTREAM")"
|
|
136
|
+
if [ -z "${HOLDER:-}" ]; then
|
|
137
|
+
if [ "$FF" = 1 ] && [ "$FAIL" = 0 ]; then
|
|
138
|
+
git update-ref -m "verify-landed: fast-forward $DEFAULT" "refs/heads/$DEFAULT" "$UPSTREAM" "$LOCAL"
|
|
139
|
+
echo "local $DEFAULT: fast-forwarded $BEHIND commit(s) to $UPSTREAM (not checked out anywhere)"
|
|
140
|
+
else
|
|
141
|
+
echo "local $DEFAULT: $BEHIND behind, not checked out anywhere — safe: git fetch $REMOTE $DEFAULT:$DEFAULT"
|
|
142
|
+
fi
|
|
143
|
+
elif [ "$HOLDER" != "$HERE" ]; then
|
|
144
|
+
if [ "$FF" = 1 ] && [ "$FAIL" = 0 ] && [ "$HOLDER" = "$IDLE_PRIMARY" ]; then
|
|
145
|
+
[ "$(git -C "$HOLDER" symbolic-ref --quiet --short HEAD)" = "$DEFAULT" ] \
|
|
146
|
+
|| { echo "primary checkout changed branches; refusing update" >&2; exit 1; }
|
|
147
|
+
git -C "$HOLDER" merge --quiet --ff-only "$UPSTREAM"
|
|
148
|
+
echo "local $DEFAULT: fast-forwarded $BEHIND commit(s) in confirmed idle primary $HOLDER"
|
|
149
|
+
else
|
|
150
|
+
echo "local $DEFAULT: $BEHIND behind, checked out at $HOLDER (not here) — leave for its owner; if you know it is idle: git -C $HOLDER merge --ff-only $UPSTREAM"
|
|
151
|
+
echo "If nothing is using that checkout, rerun verify-landed.sh with --ff --idle-primary <primary-path>." >&2
|
|
152
|
+
fi
|
|
153
|
+
elif [ -n "$(git status --porcelain)" ]; then
|
|
154
|
+
echo "local $DEFAULT: $BEHIND behind but working tree dirty — report, do not touch"
|
|
155
|
+
elif [ "$FF" = 1 ] && [ "$FAIL" = 0 ]; then
|
|
156
|
+
git merge --quiet --ff-only "$UPSTREAM"
|
|
157
|
+
echo "local $DEFAULT: fast-forwarded $BEHIND commit(s) to $UPSTREAM (checked out here)"
|
|
158
|
+
else
|
|
159
|
+
echo "local $DEFAULT: $BEHIND behind, clean, checked out here — safe to fast-forward"
|
|
160
|
+
fi
|
|
161
|
+
else
|
|
162
|
+
echo "local $DEFAULT: diverged from $REMOTE/$DEFAULT — report, do not touch"
|
|
163
|
+
fi
|
|
164
|
+
if [ "$(git rev-parse "$DEFAULT")" != "$(git rev-parse "$REMOTE/$DEFAULT")" ]; then
|
|
165
|
+
echo "LOCAL DEFAULT NOT CURRENT: $DEFAULT differs from $REMOTE/$DEFAULT; landing incomplete" >&2
|
|
166
|
+
FAIL=1
|
|
167
|
+
fi
|
|
168
|
+
else
|
|
169
|
+
echo "local $DEFAULT: no local branch (nothing to update)"
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
if [ -n "$BRANCH" ]; then
|
|
173
|
+
if ! REMOTE_REFS="$(git ls-remote --heads "$REMOTE" "$BRANCH" 2>/dev/null)"; then
|
|
174
|
+
echo "remote branch lookup failed: $REMOTE $BRANCH" >&2
|
|
175
|
+
FAIL=1
|
|
176
|
+
elif [ -n "$REMOTE_REFS" ]; then
|
|
177
|
+
echo "leftover branch: $REMOTE/$BRANCH still exists after merge" >&2
|
|
178
|
+
FAIL=1
|
|
179
|
+
else
|
|
180
|
+
echo "no leftover branch: $REMOTE/$BRANCH gone"
|
|
181
|
+
fi
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
exit "$FAIL"
|
|
@@ -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 posts and comments from Reddit's public feeds (output can omit comments) and search subreddits, with links kept inline. Use whenever a task needs Reddit content or needs to collect links posted on Reddit; generic web fetch and reddit.com JSON are blocked for agents. Reddit's RSS support ends November 13, 2026; a partial search fallback remains.
|
|
4
|
+
---
|
|
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.
|