@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,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,264 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Read X/Twitter posts through the public fxtwitter API: full text, expanded
|
|
3
|
+
// links, quoted posts, long-form articles, and downloadable images.
|
|
4
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { parseArgs as parseCli } from "node:util";
|
|
7
|
+
|
|
8
|
+
const API = "https://api.fxtwitter.com";
|
|
9
|
+
// pbs.twimg.com answers 403 without a browser-like user agent.
|
|
10
|
+
const HEADERS = { "user-agent": "Mozilla/5.0" };
|
|
11
|
+
const HOSTS = /^(?:www\.|mobile\.)?(?:x|twitter|fixupx|fxtwitter|vxtwitter|fixvx)\.com$/;
|
|
12
|
+
|
|
13
|
+
// The parts of fxtwitter's response this script reads.
|
|
14
|
+
type Facet = { type?: string; original?: string; replacement?: string; indices: [number, number] };
|
|
15
|
+
type ArticleMedia = { media_id?: string; media_info?: { original_img_url?: string } };
|
|
16
|
+
type Block = { type?: string; text?: string; entityRanges?: { key?: number | string }[] };
|
|
17
|
+
type Entity = { type?: string; data?: { mediaItems?: { mediaId?: string }[] } };
|
|
18
|
+
export type Article = {
|
|
19
|
+
title?: string;
|
|
20
|
+
content?: { blocks?: Block[]; entityMap?: { key: number | string; value: Entity }[] };
|
|
21
|
+
cover_media?: ArticleMedia;
|
|
22
|
+
media_entities?: ArticleMedia[];
|
|
23
|
+
};
|
|
24
|
+
export type Tweet = {
|
|
25
|
+
id: string;
|
|
26
|
+
url?: string;
|
|
27
|
+
text?: string;
|
|
28
|
+
created_at?: string;
|
|
29
|
+
raw_text?: { text?: string; facets?: Facet[] };
|
|
30
|
+
author?: { screen_name?: string; name?: string };
|
|
31
|
+
replying_to?: string;
|
|
32
|
+
replying_to_status?: string;
|
|
33
|
+
media?: {
|
|
34
|
+
photos?: { url: string }[];
|
|
35
|
+
videos?: { url: string; thumbnail_url?: string; duration?: number }[];
|
|
36
|
+
};
|
|
37
|
+
article?: Article;
|
|
38
|
+
quote?: Tweet;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** `{ user, id }` for a post URL on x.com or a known mirror; null otherwise. */
|
|
42
|
+
export function parseStatusUrl(input: string): { user: string; id: string } | null {
|
|
43
|
+
let url: URL;
|
|
44
|
+
try {
|
|
45
|
+
url = new URL(input);
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (!HOSTS.test(url.hostname)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const match = /^\/([^/]+)\/status(?:es)?\/(\d+)/.exec(url.pathname);
|
|
53
|
+
return match ? { user: match[1]!, id: match[2]! } : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Post text with t.co links replaced by their targets. */
|
|
57
|
+
export function expandedText(tweet: Tweet): string {
|
|
58
|
+
const text = tweet.raw_text?.text ?? tweet.text ?? "";
|
|
59
|
+
const facets = (tweet.raw_text?.facets ?? [])
|
|
60
|
+
.filter((f) => f.type === "url" && f.original && f.replacement)
|
|
61
|
+
.sort((a, b) => b.indices[0] - a.indices[0]);
|
|
62
|
+
let out = text;
|
|
63
|
+
for (const f of facets) {
|
|
64
|
+
out = out.slice(0, f.indices[0]) + f.replacement + out.slice(f.indices[1]);
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type Photo = { url: string; id: string | null };
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Article body as Markdown-ish text. An image block becomes `[image N]`, where N
|
|
73
|
+
* is that image's position in `mediaOf(tweet).photos` (and its downloaded file).
|
|
74
|
+
*/
|
|
75
|
+
export function articleText(article: Article | undefined, photos: Photo[] = []): string {
|
|
76
|
+
const blocks = article?.content?.blocks ?? [];
|
|
77
|
+
const entities = new Map(
|
|
78
|
+
(article?.content?.entityMap ?? []).map((e) => [String(e.key), e.value]),
|
|
79
|
+
);
|
|
80
|
+
const lines: string[] = [];
|
|
81
|
+
let ordered = 0;
|
|
82
|
+
for (const block of blocks) {
|
|
83
|
+
const text = (block.text ?? "").trim();
|
|
84
|
+
if (block.type !== "ordered-list-item") {
|
|
85
|
+
ordered = 0;
|
|
86
|
+
}
|
|
87
|
+
if (block.type === "atomic") {
|
|
88
|
+
const entity = entities.get(String(block.entityRanges?.[0]?.key));
|
|
89
|
+
const mediaId = entity?.type === "MEDIA" ? entity.data?.mediaItems?.[0]?.mediaId : null;
|
|
90
|
+
if (mediaId) {
|
|
91
|
+
const n = photos.findIndex((p) => p.id === mediaId);
|
|
92
|
+
lines.push(n >= 0 ? `[image ${n + 1}]` : "[image]");
|
|
93
|
+
}
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (!text) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (block.type === "header-one") {
|
|
100
|
+
lines.push(`# ${text}`);
|
|
101
|
+
} else if (block.type === "header-two") {
|
|
102
|
+
lines.push(`## ${text}`);
|
|
103
|
+
} else if (block.type === "unordered-list-item") {
|
|
104
|
+
lines.push(`- ${text}`);
|
|
105
|
+
} else if (block.type === "ordered-list-item") {
|
|
106
|
+
lines.push(`${++ordered}. ${text}`);
|
|
107
|
+
} else if (block.type === "blockquote") {
|
|
108
|
+
lines.push(`> ${text}`);
|
|
109
|
+
} else {
|
|
110
|
+
lines.push(text);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Every image and video a post carries, including article images. */
|
|
117
|
+
export function mediaOf(tweet: Tweet): {
|
|
118
|
+
photos: Photo[];
|
|
119
|
+
videos: { url: string; thumbnail?: string; duration?: number }[];
|
|
120
|
+
} {
|
|
121
|
+
const photos: Photo[] = (tweet.media?.photos ?? []).map((p) => ({ url: p.url, id: null }));
|
|
122
|
+
const article = tweet.article;
|
|
123
|
+
for (const m of [article?.cover_media, ...(article?.media_entities ?? [])]) {
|
|
124
|
+
if (m?.media_info?.original_img_url) {
|
|
125
|
+
photos.push({ url: m.media_info.original_img_url, id: m.media_id ?? null });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const videos = (tweet.media?.videos ?? []).map((v) => ({
|
|
129
|
+
url: v.url,
|
|
130
|
+
thumbnail: v.thumbnail_url,
|
|
131
|
+
duration: v.duration,
|
|
132
|
+
}));
|
|
133
|
+
return { photos, videos };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function fetchPost(user: string, id: string): Promise<Tweet> {
|
|
137
|
+
const res = await fetch(`${API}/${user}/status/${id}`, { headers: HEADERS });
|
|
138
|
+
const body = (await res.json().catch(() => null)) as { tweet?: Tweet; message?: string } | null;
|
|
139
|
+
if (!res.ok || !body?.tweet) {
|
|
140
|
+
throw new Error(`${user}/status/${id}: HTTP ${res.status} ${body?.message ?? ""}`.trim());
|
|
141
|
+
}
|
|
142
|
+
return body.tweet;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function download(url: string, path: string): Promise<string> {
|
|
146
|
+
const res = await fetch(url, { headers: HEADERS });
|
|
147
|
+
if (!res.ok) {
|
|
148
|
+
throw new Error(`HTTP ${res.status}`);
|
|
149
|
+
}
|
|
150
|
+
writeFileSync(path, Buffer.from(await res.arrayBuffer()));
|
|
151
|
+
return path;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function render(tweet: Tweet, mediaDir: string | null, label: string): Promise<string> {
|
|
155
|
+
const out: string[] = [];
|
|
156
|
+
const a = tweet.author ?? {};
|
|
157
|
+
out.push(`### ${label}: @${a.screen_name} (${a.name ?? ""}) · ${tweet.created_at}`);
|
|
158
|
+
out.push(tweet.url ?? "");
|
|
159
|
+
if (tweet.replying_to) {
|
|
160
|
+
out.push(
|
|
161
|
+
`Reply to @${tweet.replying_to} (${tweet.replying_to_status}); earlier thread posts are not included.`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
out.push("", expandedText(tweet));
|
|
165
|
+
const { photos, videos } = mediaOf(tweet);
|
|
166
|
+
if (tweet.article) {
|
|
167
|
+
out.push("", `#### Article: ${tweet.article.title}`, "", articleText(tweet.article, photos));
|
|
168
|
+
}
|
|
169
|
+
for (const [i, { url }] of photos.entries()) {
|
|
170
|
+
if (!mediaDir) {
|
|
171
|
+
out.push(`image ${i + 1}: ${url}`);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const ext = /\.(\w+)(?:\?|$)/.exec(url)?.[1] ?? "jpg";
|
|
175
|
+
const path = join(mediaDir, `${tweet.id}-${i + 1}.${ext}`);
|
|
176
|
+
out.push(
|
|
177
|
+
`image ${i + 1}: ${await download(url, path).catch((e: unknown) => `${url} (download failed: ${e instanceof Error ? e.message : String(e)})`)}`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
for (const [i, v] of videos.entries()) {
|
|
181
|
+
out.push(
|
|
182
|
+
`video ${i + 1} (${v.duration ?? "?"}s, not readable as text): ${v.url} thumbnail: ${v.thumbnail}`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
return out.join("\n");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function parseArgs(argv: string[]): {
|
|
189
|
+
mediaDir: string | null;
|
|
190
|
+
urls: string[];
|
|
191
|
+
error?: string;
|
|
192
|
+
} {
|
|
193
|
+
let parsed;
|
|
194
|
+
try {
|
|
195
|
+
parsed = parseCli({
|
|
196
|
+
args: argv,
|
|
197
|
+
allowPositionals: true,
|
|
198
|
+
options: { "media-dir": { type: "string" } },
|
|
199
|
+
});
|
|
200
|
+
} catch (error) {
|
|
201
|
+
return {
|
|
202
|
+
mediaDir: null,
|
|
203
|
+
urls: [],
|
|
204
|
+
error: error instanceof Error ? error.message : String(error),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const mediaDir = parsed.values["media-dir"] ?? null;
|
|
208
|
+
// A forgotten directory would otherwise take the first URL as its value.
|
|
209
|
+
if (mediaDir !== null && /^https?:/i.test(mediaDir)) {
|
|
210
|
+
return { mediaDir: null, urls: [], error: "--media-dir needs a directory value" };
|
|
211
|
+
}
|
|
212
|
+
return { mediaDir, urls: parsed.positionals };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function main(argv: string[]): Promise<number> {
|
|
216
|
+
const { mediaDir, urls, error } = parseArgs(argv);
|
|
217
|
+
if (error || urls.length === 0) {
|
|
218
|
+
if (error) {
|
|
219
|
+
console.error(error);
|
|
220
|
+
}
|
|
221
|
+
console.error("usage: x-post.ts [--media-dir DIR] <x.com status URL>...");
|
|
222
|
+
return 2;
|
|
223
|
+
}
|
|
224
|
+
if (mediaDir) {
|
|
225
|
+
mkdirSync(mediaDir, { recursive: true });
|
|
226
|
+
}
|
|
227
|
+
let failed = 0;
|
|
228
|
+
for (const input of urls) {
|
|
229
|
+
const ref = parseStatusUrl(input);
|
|
230
|
+
if (!ref) {
|
|
231
|
+
console.log(`## ${input}\nnot an X post URL\n`);
|
|
232
|
+
failed++;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
const tweet = await fetchPost(ref.user, ref.id);
|
|
237
|
+
const parts = [await render(tweet, mediaDir, "Post")];
|
|
238
|
+
// The embedded quote omits its article and full media; fetch it directly.
|
|
239
|
+
if (tweet.quote) {
|
|
240
|
+
const embedded = tweet.quote;
|
|
241
|
+
let quote = embedded;
|
|
242
|
+
try {
|
|
243
|
+
quote = await fetchPost(embedded.author?.screen_name ?? "i", embedded.id);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
parts.push(
|
|
246
|
+
`Quoted post fetch failed (${error instanceof Error ? error.message : String(error)}); showing the embedded copy, which can lack its article and full media.`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
parts.push(await render(quote, mediaDir, "Quoted post"));
|
|
250
|
+
}
|
|
251
|
+
console.log(`## ${input}\n${parts.join("\n\n")}\n`);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.log(
|
|
254
|
+
`## ${input}\nfetch failed: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
255
|
+
);
|
|
256
|
+
failed++;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return failed ? 1 : 0;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (import.meta.main) {
|
|
263
|
+
process.exitCode = await main(process.argv.slice(2));
|
|
264
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hr-research-shared-links
|
|
3
|
+
description: "Research a shared link or repo in depth, compare it with the user's setup and rate each idea. Use when a session opens with a link or asks for thoughts or what to borrow. Not for factual lookups, and not for implementing or fixing things the link describes."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Research shared links
|
|
7
|
+
|
|
8
|
+
A shared link asks "is this worth anything to us?", not "what does it say?".
|
|
9
|
+
The failure to avoid is a first answer built from the page that was shared,
|
|
10
|
+
so the user has to ask again to "look closer", "compare it to ours" or "find
|
|
11
|
+
the hidden gems". Do that work the first time.
|
|
12
|
+
|
|
13
|
+
## Before reading
|
|
14
|
+
|
|
15
|
+
1. If the link looks unrelated to the session's work or carries stray
|
|
16
|
+
characters, say what you think it is and confirm before a long analysis.
|
|
17
|
+
2. For several links, give each its own verdict and add one synthesis.
|
|
18
|
+
3. Tell the user in one line what you will do: read the primary source, compare
|
|
19
|
+
with their setup, rate each idea.
|
|
20
|
+
|
|
21
|
+
## Read the source, not the pointer
|
|
22
|
+
|
|
23
|
+
1. Use the reader for the site when one is available (X posts, Reddit threads);
|
|
24
|
+
generic fetch truncates them.
|
|
25
|
+
2. Follow an article, post or video to what it is about: the repository, paper,
|
|
26
|
+
docs or data. The shared page is a claim; the source is the evidence.
|
|
27
|
+
3. For a repository, read past the README: agent guidance files, skills, docs,
|
|
28
|
+
tests and the code behind any headline feature. These hold the transferable
|
|
29
|
+
ideas. Search its open issues and security disclosures for the shared claim
|
|
30
|
+
and for each idea you may recommend, check every hit against the current
|
|
31
|
+
code, and read the latest release notes. Record the terms you searched. The
|
|
32
|
+
README says what the maintainer wants to be true; the tracker shows what
|
|
33
|
+
users and reviewers found, and a fix may already have landed.
|
|
34
|
+
4. Say what you did not read: videos, images, earlier posts in a thread,
|
|
35
|
+
private or login-gated pages, source behind a README claim. Do not rate what
|
|
36
|
+
you did not see.
|
|
37
|
+
5. Check headline claims against the project's own data. Recompute a cost or
|
|
38
|
+
speed figure, compare the README with the results file, and name each claim
|
|
39
|
+
that has no evidence behind it.
|
|
40
|
+
6. A license does not stop you borrowing an idea. If it bars an AI from reading
|
|
41
|
+
the material, flag that and offer another reader.
|
|
42
|
+
|
|
43
|
+
## Compare with what the user has
|
|
44
|
+
|
|
45
|
+
1. Search their rules, skills, docs and settings for the same idea before judging
|
|
46
|
+
it. "We lack this" needs a search showing the gap; "we have this" needs the
|
|
47
|
+
path.
|
|
48
|
+
2. Search the earlier sessions and decisions you can reach for standing
|
|
49
|
+
constraints and options already rejected. Name any records you could not
|
|
50
|
+
reach, and do not read their absence as "never decided". Do not recommend
|
|
51
|
+
what was ruled out without saying so and saying what changed.
|
|
52
|
+
3. State exactly what the comparison ran against: which fork or version of ours,
|
|
53
|
+
which release of theirs, the competitor's real name.
|
|
54
|
+
|
|
55
|
+
## Rate each idea
|
|
56
|
+
|
|
57
|
+
Give each idea one row: source path, the technique in one to three lines, what it
|
|
58
|
+
would change here and what that is worth (faster, cheaper, safer, clearer),
|
|
59
|
+
the evidence on both sides (their file and line, our search), and a verdict.
|
|
60
|
+
|
|
61
|
+
- **Adopt**: clear gain, fits a stated need, small to carry.
|
|
62
|
+
- **Consider**: real idea, unproven here or costly. Say what would settle it.
|
|
63
|
+
- **Skip**: already covered, ruled out, or tied to the vendor's model or product.
|
|
64
|
+
|
|
65
|
+
Separate the idea that transfers from the vendor and model details around it.
|
|
66
|
+
Weigh build against buy against borrow: a maintained tool that is good enough
|
|
67
|
+
beats building, and an idea is often worth taking without the tool. List the
|
|
68
|
+
ideas you dropped and why, so the recommended set and the table agree.
|
|
69
|
+
|
|
70
|
+
## Route what is worth keeping
|
|
71
|
+
|
|
72
|
+
1. Name the layer that owns each kept idea: shared portable rules, host or
|
|
73
|
+
machine setup, one project, or a portfolio-level record. Use the user's own
|
|
74
|
+
ownership map when they have one.
|
|
75
|
+
2. A deferred idea gets the condition that reopens it and a proposed home in
|
|
76
|
+
the owning repository; offer to save it there rather than leaving it only in
|
|
77
|
+
this reply.
|
|
78
|
+
3. If an idea means adopting something vendored, offer to record where it came
|
|
79
|
+
from so upstream changes are noticed. If evaluating it exposed an upstream
|
|
80
|
+
bug, offer to file it.
|
|
81
|
+
4. Wording adopted into rules describes behavior and carries no "borrowed from"
|
|
82
|
+
history. Provenance belongs in the research record.
|
|
83
|
+
|
|
84
|
+
## Answer
|
|
85
|
+
|
|
86
|
+
1. Lead with a table, one row per link or repository: what it is, its status in
|
|
87
|
+
the user's setup (have, partly, missing, ruled out) with the evidence, and the
|
|
88
|
+
verdict.
|
|
89
|
+
2. Then the ranked ideas, then what is not worth borrowing and why.
|
|
90
|
+
3. Explain jargon in plain words or a small diagram. Before changing anything
|
|
91
|
+
from this research, show the exact before and after wording or real sample
|
|
92
|
+
output.
|
|
93
|
+
4. End with offers under the user's offer rules. Candidates: write a research
|
|
94
|
+
note; test it against what they use on quality, speed or cost (give the
|
|
95
|
+
expected wall-clock time and check host load first); get independent opinions
|
|
96
|
+
from other models or a review panel, with a brief that includes your findings
|
|
97
|
+
and the candidates you rejected, choosing the reviewers yourself; build or
|
|
98
|
+
adopt one idea, saying which steps that option includes (file changes,
|
|
99
|
+
installing the tool, a pull request, landing) and which it leaves for a later
|
|
100
|
+
offer; stop here.
|
|
101
|
+
|
|
102
|
+
"Tell me more" and "thoughts?" authorize research and nothing else. Changing
|
|
103
|
+
files, installing the tool or opening a pull request needs the user's selection.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aaron Queen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|