Skip to content

Commit a425c42

Browse files
Merge pull request #176 from cobusgreyling/feat/contributor-growth
feat: contributor growth — CLI CTAs, CONTRIBUTORS.md, backlog refresh
2 parents ccb188c + 7663701 commit a425c42

23 files changed

Lines changed: 305 additions & 7 deletions

‎CONTRIBUTING.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ Automation posts a welcome comment on new story/adopter PRs (see `.github/workfl
7979
- **Show your loop**: [Add Adopter issue](https://github.com/cobusgreyling/loop-engineering/issues/new?template=add-adopter.yml), Discussions, or a row in [docs/adopters.md](./docs/adopters.md)
8080
- **Loop Ready badge**: `npx @cobusgreyling/loop-audit . --badge` — paste into your README
8181
- **Good first issues**: look for label [`good first issue`](https://github.com/cobusgreyling/loop-engineering/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
82+
- **Hall of fame**: [CONTRIBUTORS.md](./CONTRIBUTORS.md) — regenerate after merges with `npm run contributors:generate`
8283
- **Security**: see [SECURITY.md](./SECURITY.md) — do not file public issues for exploitable vulnerabilities
8384

8485
Thank you for helping make this the go-to reference for loop engineering.

‎CONTRIBUTORS.md‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Contributors
2+
3+
Thank you to everyone who shipped docs, stories, examples, and tool fixes.
4+
5+
*Generated 2026-07-06 via `node scripts/generate-contributors.mjs` — re-run after merges.*
6+
7+
| Contributor | Highlights |
8+
|-------------|------------|
9+
| [@KhaiTrang1995](https://github.com/KhaiTrang1995) | #165 feat(loop-guard): size the token budget from loop-cost per pattern<br>#140 feat(loop-init): wire loop-context circuit breaker into scaffolding + ci-sweeper |
10+
| [@50thycal](https://github.com/50thycal) | story: `quant-loop-out-of-time.md`<br>story: `quant-loop-the-verifier-problem.md` |
11+
| [@neerajdad123-byte](https://github.com/neerajdad123-byte) | 3 commits |
12+
| [@ulises-jeremias](https://github.com/ulises-jeremias) | #98 docs(examples): add opencode tool coverage |
13+
| [@jiaobotao](https://github.com/jiaobotao) | #141 Add Hermes Agent example (daily-triage) + primitives matrix column |
14+
| [@k-anushka14](https://github.com/k-anushka14) | #156 docs: add Cursor PR Babysitter example |
15+
| [@roian6](https://github.com/roian6) | #127 docs: expand Aider primitives appendix |
16+
| [@Zhang-986](https://github.com/Zhang-986) | #138 fix(loop-sync): honor json output flag |
17+
18+
## Your first PR
19+
20+
Pick a scoped ~15 min task: [Contributor quickstart](https://github.com/cobusgreyling/loop-engineering/discussions/123).
21+
22+
Same-day review on stories, adopters, and docs — see [CONTRIBUTING.md](./CONTRIBUTING.md).

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ Addy Osmani:
247247
248248
## Help wanted
249249

250-
**First PR?** Start with the [contributor quickstart](https://github.com/cobusgreyling/loop-engineering/discussions/123) — ~10 min to ~1 hr tasks with same-day review on stories and adopters.
250+
**First PR?** Start with the [contributor quickstart](https://github.com/cobusgreyling/loop-engineering/discussions/123) — ~10 min to ~1 hr tasks with same-day review on stories and adopters. See [CONTRIBUTORS.md](CONTRIBUTORS.md) for everyone who has shipped so far.
251251

252252
| Pick one | Issue |
253253
|----------|-------|

‎package.json‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
"test:loop-context": "cd tools/loop-context && npm test",
1313
"test:mcp-server": "cd tools/mcp-server && npm test",
1414
"test:tools": "npm run test:loop-audit && npm run test:loop-init && npm run test:loop-cost && npm run test:loop-sync && npm run test:loop-context && npm run test:mcp-server",
15-
"build:tools": "cd tools/loop-audit && npm run build && cd ../loop-init && npm run build && cd ../loop-cost && npm run build && cd ../loop-sync && npm run build && cd ../loop-context && npm run build && cd ../mcp-server && npm run build"
15+
"build:tools": "cd tools/loop-audit && npm run build && cd ../loop-init && npm run build && cd ../loop-cost && npm run build && cd ../loop-sync && npm run build && cd ../loop-context && npm run build && cd ../mcp-server && npm run build",
16+
"contributors:generate": "node scripts/generate-contributors.mjs",
17+
"contributors:check": "node scripts/generate-contributors.mjs --check"
1618
},
1719
"devDependencies": {
1820
"ajv": "^8.17.1",

‎scripts/create-good-first-issues.sh‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,14 @@ create_issue "Add examples/hermes/README.md index" "good first issue,docs" "$BOD
3535
create_issue "Add Windsurf PR Babysitter example doc" "good first issue,docs" "$BODY_DIR/windsurf-pr-babysitter-example.md"
3636
create_issue "Share a Post-Merge Cleanup production story" "good first issue,story" "$BODY_DIR/post-merge-cleanup-story.md"
3737

38+
# Wave 2 — refresh backlog after Jul 2026 merges (idempotent; skips existing titles)
39+
create_issue "Add loop-context subsection to QUICKSTART" "good first issue,docs" "$BODY_DIR/quickstart-loop-context.md"
40+
create_issue "Add loop-mcp-server subsection to QUICKSTART" "good first issue,docs" "$BODY_DIR/quickstart-mcp-server.md"
41+
create_issue "Link Aider appendix from examples README" "good first issue,docs" "$BODY_DIR/examples-aider-link.md"
42+
create_issue "Add Hermes to examples README tool directory" "good first issue,docs" "$BODY_DIR/hermes-examples-readme.md"
43+
create_issue "Share an Issue Triage week-one story" "good first issue,story" "$BODY_DIR/issue-triage-story.md"
44+
create_issue "Add Opencode constraints example doc" "good first issue,docs" "$BODY_DIR/opencode-constraints-example.md"
45+
create_issue "Share a multi-loop coordination story" "good first issue,story" "$BODY_DIR/multi-loop-story.md"
46+
3847
echo "Done. Open backlog:"
3948
echo "https://github.com/cobusgreyling/loop-engineering/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22"

‎scripts/generate-contributors.mjs‎

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Regenerate CONTRIBUTORS.md from GitHub API + story attributions.
4+
* Requires: gh auth login (read access to public repo is enough)
5+
*
6+
* node scripts/generate-contributors.mjs
7+
* node scripts/generate-contributors.mjs --check # exit 1 if file would change
8+
*/
9+
import { execFileSync } from 'node:child_process';
10+
import { readFileSync, readdirSync, writeFileSync } from 'node:fs';
11+
import path from 'node:path';
12+
import { fileURLToPath } from 'node:url';
13+
14+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
15+
const ROOT = path.resolve(__dirname, '..');
16+
const OUT = path.join(ROOT, 'CONTRIBUTORS.md');
17+
const REPO = 'cobusgreyling/loop-engineering';
18+
const MAINTAINER = 'cobusgreyling';
19+
const BOTS = new Set(['github-actions[bot]', 'dependabot[bot]', 'dependabot']);
20+
21+
const CONTRIBUTOR_QUICKSTART =
22+
'https://github.com/cobusgreyling/loop-engineering/discussions/123';
23+
24+
function ghJson(args) {
25+
return JSON.parse(execFileSync('gh', args, { encoding: 'utf8', cwd: ROOT }));
26+
}
27+
28+
function ghApiPaginated(route) {
29+
const out = execFileSync('gh', ['api', route, '--paginate'], { encoding: 'utf8', cwd: ROOT });
30+
const items = [];
31+
for (const line of out.split('\n')) {
32+
const trimmed = line.trim();
33+
if (!trimmed) continue;
34+
const parsed = JSON.parse(trimmed);
35+
if (Array.isArray(parsed)) items.push(...parsed);
36+
else items.push(parsed);
37+
}
38+
return items;
39+
}
40+
41+
function storyAttributions() {
42+
const storiesDir = path.join(ROOT, 'stories');
43+
const attributions = new Map();
44+
let files;
45+
try {
46+
files = readdirSync(storiesDir).filter((f) => f.endsWith('.md'));
47+
} catch {
48+
return attributions;
49+
}
50+
const re = /Contributed by \[@([^\]]+)\]/g;
51+
for (const file of files) {
52+
const text = readFileSync(path.join(storiesDir, file), 'utf8');
53+
for (const m of text.matchAll(re)) {
54+
const login = m[1];
55+
if (!attributions.has(login)) attributions.set(login, []);
56+
attributions.get(login).push(`story: \`${file}\``);
57+
}
58+
}
59+
return attributions;
60+
}
61+
62+
async function main() {
63+
const check = process.argv.includes('--check');
64+
65+
const apiContributors = ghApiPaginated(`repos/${REPO}/contributors`);
66+
67+
const mergedPrs = ghJson([
68+
'pr',
69+
'list',
70+
'--repo',
71+
REPO,
72+
'--state',
73+
'merged',
74+
'--limit',
75+
'200',
76+
'--json',
77+
'author,title,number',
78+
]);
79+
80+
const highlights = new Map();
81+
for (const pr of mergedPrs) {
82+
const login = pr.author?.login;
83+
if (!login || login === MAINTAINER || pr.author?.is_bot) continue;
84+
if (!highlights.has(login)) highlights.set(login, []);
85+
const list = highlights.get(login);
86+
if (list.length < 2) list.push(`#${pr.number} ${pr.title}`);
87+
}
88+
89+
const storyAttrs = storyAttributions();
90+
91+
const people = new Map();
92+
for (const c of apiContributors) {
93+
const login = c.login;
94+
if (login === MAINTAINER || BOTS.has(login) || login.endsWith('[bot]')) continue;
95+
people.set(login, {
96+
login,
97+
contributions: c.contributions ?? 0,
98+
highlights: highlights.get(login) ?? [],
99+
stories: storyAttrs.get(login) ?? [],
100+
});
101+
}
102+
103+
for (const [login, stories] of storyAttrs) {
104+
if (people.has(login)) {
105+
people.get(login).stories = stories;
106+
continue;
107+
}
108+
people.set(login, {
109+
login,
110+
contributions: 0,
111+
highlights: highlights.get(login) ?? [],
112+
stories,
113+
});
114+
}
115+
116+
const sorted = [...people.values()].sort((a, b) => {
117+
const score = (p) => p.contributions + p.stories.length * 2 + p.highlights.length;
118+
return score(b) - score(a) || a.login.localeCompare(b.login);
119+
});
120+
121+
const generated = new Date().toISOString().slice(0, 10);
122+
const lines = [
123+
'# Contributors',
124+
'',
125+
'Thank you to everyone who shipped docs, stories, examples, and tool fixes.',
126+
'',
127+
`*Generated ${generated} via \`node scripts/generate-contributors.mjs\` — re-run after merges.*`,
128+
'',
129+
'| Contributor | Highlights |',
130+
'|-------------|------------|',
131+
];
132+
133+
for (const p of sorted) {
134+
const bits = [...p.highlights, ...p.stories];
135+
const cell =
136+
bits.length > 0
137+
? bits.map((b) => b.replace(/\|/g, '\\|')).join('<br>')
138+
: `${p.contributions} commit${p.contributions === 1 ? '' : 's'}`;
139+
lines.push(`| [@${p.login}](https://github.com/${p.login}) | ${cell} |`);
140+
}
141+
142+
lines.push(
143+
'',
144+
'## Your first PR',
145+
'',
146+
'Pick a scoped ~15 min task: [Contributor quickstart](' + CONTRIBUTOR_QUICKSTART + ').',
147+
'',
148+
'Same-day review on stories, adopters, and docs — see [CONTRIBUTING.md](./CONTRIBUTING.md).',
149+
'',
150+
);
151+
152+
const content = lines.join('\n');
153+
154+
if (check) {
155+
const existing = readFileSync(OUT, 'utf8');
156+
if (existing !== content) {
157+
console.error('CONTRIBUTORS.md is out of date. Run: node scripts/generate-contributors.mjs');
158+
process.exit(1);
159+
}
160+
console.log('CONTRIBUTORS.md is up to date.');
161+
return;
162+
}
163+
164+
writeFileSync(OUT, content);
165+
console.log(`Wrote ${OUT} (${sorted.length} contributors)`);
166+
}
167+
168+
main().catch((err) => {
169+
console.error(err instanceof Error ? err.message : err);
170+
process.exit(1);
171+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
## Goal
2+
Share an honest **Issue Triage** week-one story — win or failure — so others know how to run label-only automation safely.
3+
4+
## Files
5+
- New file under `stories/` (follow format in existing stories)
6+
7+
## Acceptance criteria
8+
- [ ] Which tool you used and how issues were scheduled (cron, `/loop`, GitHub Action, etc.)
9+
- [ ] Week-one constraints (propose labels only, no auto-close, human gate)
10+
- [ ] One surprise (good or bad)
11+
- [ ] What you would change before week two
12+
13+
**Estimated time:** ~15 minutes
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
## Goal
2+
Share a **multi-loop coordination** story — running two or more loops (e.g. Daily Triage + PR Babysitter) without them fighting each other.
3+
4+
## Files
5+
- New file under `stories/`
6+
7+
## Acceptance criteria
8+
- [ ] Name the patterns and tools you combined
9+
- [ ] How you separated state files, schedules, or budgets
10+
- [ ] At least one failure mode (overlap, duplicate PRs, context bleed)
11+
- [ ] One actionable lesson for the next reader
12+
13+
**Estimated time:** ~20 minutes
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
## Goal
2+
Add an **Opencode constraints** example so CLI-first users can copy a `loop-triage` skill with explicit guardrails.
3+
4+
## Files
5+
- `examples/opencode/constraints-example.md` (new)
6+
- `examples/opencode/README.md` — link the new doc
7+
8+
## Acceptance criteria
9+
- [ ] Show a minimal `skills/loop-triage/SKILL.md` snippet with 3–5 constraints (read-only week one, no force-push, etc.)
10+
- [ ] Show how it pairs with `loop-init --tool opencode`
11+
- [ ] Link to `docs/safety.md` and the Cursor constraints example for comparison
12+
- [ ] No secrets or internal URLs
13+
14+
**Estimated time:** ~30 minutes

‎tools/loop-audit/dist/cli.js‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/usr/bin/env node
22
import { auditProject } from './auditor.js';
3+
import { printContributorCta } from './contributor-cta.js';
34
import { formatBadge, formatHuman, formatJson, formatMarkdown } from './reporter.js';
45
const args = process.argv.slice(2);
56
const target = args.find((a) => !a.startsWith('-')) || '.';
@@ -92,6 +93,8 @@ try {
9293
console.log('');
9394
console.log('See docs/loop-design-checklist.md and patterns/ for full guidance.');
9495
}
96+
if (!json && !badge && !md)
97+
printContributorCta();
9598
if (result.score < 40)
9699
process.exitCode = 2;
97100
}

0 commit comments

Comments
 (0)