harness-auto-docs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/.nvmrc +1 -0
  2. package/AGENTS.md +69 -0
  3. package/ARCHITECTURE.md +123 -0
  4. package/README.md +52 -0
  5. package/dist/ai/anthropic.d.ts +7 -0
  6. package/dist/ai/anthropic.js +20 -0
  7. package/dist/ai/interface.d.ts +3 -0
  8. package/dist/ai/interface.js +1 -0
  9. package/dist/ai/minimax.d.ts +7 -0
  10. package/dist/ai/minimax.js +21 -0
  11. package/dist/ai/openai.d.ts +7 -0
  12. package/dist/ai/openai.js +16 -0
  13. package/dist/cli.d.ts +2 -0
  14. package/dist/cli.js +103 -0
  15. package/dist/core/diff.d.ts +17 -0
  16. package/dist/core/diff.js +46 -0
  17. package/dist/core/generator.d.ts +10 -0
  18. package/dist/core/generator.js +238 -0
  19. package/dist/core/relevance.d.ts +3 -0
  20. package/dist/core/relevance.js +29 -0
  21. package/dist/core/writer.d.ts +2 -0
  22. package/dist/core/writer.js +23 -0
  23. package/dist/providers/github.d.ts +13 -0
  24. package/dist/providers/github.js +43 -0
  25. package/dist/providers/gitlab.d.ts +9 -0
  26. package/dist/providers/gitlab.js +6 -0
  27. package/dist/providers/interface.d.ts +8 -0
  28. package/dist/providers/interface.js +1 -0
  29. package/docs/DESIGN.md +94 -0
  30. package/docs/QUALITY_SCORE.md +74 -0
  31. package/docs/design-docs/core-beliefs.md +71 -0
  32. package/docs/design-docs/index.md +32 -0
  33. package/docs/exec-plans/tech-debt-tracker.md +26 -0
  34. package/docs/product-specs/index.md +39 -0
  35. package/docs/references/anthropic-sdk-llms.txt +40 -0
  36. package/docs/references/octokit-rest-llms.txt +44 -0
  37. package/docs/references/openai-sdk-llms.txt +38 -0
  38. package/docs/superpowers/plans/2026-04-03-harness-engineering-auto-docs.md +1863 -0
  39. package/docs/superpowers/specs/2026-04-03-harness-engineering-auto-docs-design.md +169 -0
  40. package/examples/github-workflow.yml +32 -0
  41. package/markdown/harness-engineering-codex-agent-first-world.md +215 -0
  42. package/package.json +30 -0
  43. package/src/ai/anthropic.ts +23 -0
  44. package/src/ai/interface.ts +3 -0
  45. package/src/ai/minimax.ts +25 -0
  46. package/src/ai/openai.ts +20 -0
  47. package/src/cli.ts +122 -0
  48. package/src/core/diff.ts +77 -0
  49. package/src/core/generator.ts +294 -0
  50. package/src/core/relevance.ts +53 -0
  51. package/src/core/writer.ts +25 -0
  52. package/src/providers/github.ts +53 -0
  53. package/src/providers/gitlab.ts +16 -0
  54. package/src/providers/interface.ts +9 -0
  55. package/tests/core/anthropic.test.ts +33 -0
  56. package/tests/core/diff.test.ts +49 -0
  57. package/tests/core/generator.test.ts +93 -0
  58. package/tests/core/openai.test.ts +38 -0
  59. package/tests/core/relevance.test.ts +62 -0
  60. package/tests/core/writer.test.ts +56 -0
  61. package/tests/fixtures/diff-frontend.txt +11 -0
  62. package/tests/fixtures/diff-schema.txt +12 -0
  63. package/tests/fixtures/diff-small.txt +16 -0
  64. package/tests/integration/generate.test.ts +49 -0
  65. package/tests/providers/github.test.ts +69 -0
  66. package/tsconfig.json +15 -0
  67. package/vitest.config.ts +7 -0
@@ -0,0 +1,1863 @@
1
+ # Harness Engineering Auto Docs Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Build and publish an npm CLI package (`harness-engineering-auto-docs`) that runs via `npx`, extracts the git diff between two tags, generates Harness Engineering-style documentation using a configurable AI model, and opens a PR.
6
+
7
+ **Architecture:** Three independent layers — `core/` (diff, relevance, generation, file writing), `ai/` (Anthropic + OpenAI providers behind a shared interface), and `providers/` (GitHub PR creation; GitLab stub). The CLI (`cli.ts`) wires them together: write docs to disk → git commit + push → platform API creates PR.
8
+
9
+ **Tech Stack:** TypeScript 5, Node.js 18+, `@anthropic-ai/sdk`, `openai`, `@octokit/rest`, `tsx` (CLI runner), Vitest (tests)
10
+
11
+ ---
12
+
13
+ ## File Structure
14
+
15
+ ```
16
+ harness-engineering-auto-docs/
17
+ ├── package.json
18
+ ├── tsconfig.json
19
+ ├── vitest.config.ts
20
+ ├── src/
21
+ │ ├── cli.ts # Entry point: orchestrates full pipeline
22
+ │ ├── ai/
23
+ │ │ ├── interface.ts # AIProvider interface
24
+ │ │ ├── anthropic.ts # Claude implementation
25
+ │ │ └── openai.ts # GPT implementation
26
+ │ ├── core/
27
+ │ │ ├── diff.ts # git diff extraction + file grouping
28
+ │ │ ├── relevance.ts # diff → target document selection
29
+ │ │ ├── generator.ts # AI prompts + concurrent doc generation
30
+ │ │ └── writer.ts # append-to / create-file utilities
31
+ │ └── providers/
32
+ │ ├── interface.ts # PlatformProvider interface
33
+ │ ├── github.ts # GitHub PR creation via Octokit
34
+ │ └── gitlab.ts # GitLab stub (not yet implemented)
35
+ ├── tests/
36
+ │ ├── fixtures/
37
+ │ │ ├── diff-small.txt
38
+ │ │ ├── diff-schema.txt
39
+ │ │ └── diff-frontend.txt
40
+ │ ├── core/
41
+ │ │ ├── diff.test.ts
42
+ │ │ ├── relevance.test.ts
43
+ │ │ ├── writer.test.ts
44
+ │ │ └── generator.test.ts
45
+ │ ├── providers/
46
+ │ │ └── github.test.ts
47
+ │ └── integration/
48
+ │ └── generate.test.ts
49
+ └── examples/
50
+ └── github-workflow.yml
51
+ ```
52
+
53
+ ---
54
+
55
+ ## Task 1: Project Scaffolding
56
+
57
+ **Files:**
58
+ - Create: `package.json`
59
+ - Create: `tsconfig.json`
60
+ - Create: `vitest.config.ts`
61
+
62
+ - [ ] **Step 1: Create package.json**
63
+
64
+ ```json
65
+ {
66
+ "name": "harness-engineering-auto-docs",
67
+ "version": "0.1.0",
68
+ "description": "Auto-generate Harness Engineering docs on git tag",
69
+ "type": "module",
70
+ "bin": {
71
+ "harness-engineering-auto-docs": "./dist/cli.js"
72
+ },
73
+ "scripts": {
74
+ "build": "tsc",
75
+ "dev": "tsx src/cli.ts",
76
+ "test": "vitest run",
77
+ "test:watch": "vitest"
78
+ },
79
+ "dependencies": {
80
+ "@anthropic-ai/sdk": "^0.39.0",
81
+ "@octokit/rest": "^21.0.0",
82
+ "openai": "^4.85.0"
83
+ },
84
+ "devDependencies": {
85
+ "@types/node": "^22.0.0",
86
+ "tsx": "^4.19.0",
87
+ "typescript": "^5.7.0",
88
+ "vitest": "^3.0.0"
89
+ },
90
+ "engines": {
91
+ "node": ">=18"
92
+ }
93
+ }
94
+ ```
95
+
96
+ - [ ] **Step 2: Create tsconfig.json**
97
+
98
+ ```json
99
+ {
100
+ "compilerOptions": {
101
+ "target": "ES2022",
102
+ "module": "NodeNext",
103
+ "moduleResolution": "NodeNext",
104
+ "outDir": "./dist",
105
+ "rootDir": "./src",
106
+ "strict": true,
107
+ "esModuleInterop": true,
108
+ "skipLibCheck": true,
109
+ "declaration": true
110
+ },
111
+ "include": ["src/**/*"],
112
+ "exclude": ["node_modules", "dist", "tests"]
113
+ }
114
+ ```
115
+
116
+ - [ ] **Step 3: Create vitest.config.ts**
117
+
118
+ ```typescript
119
+ import { defineConfig } from 'vitest/config';
120
+
121
+ export default defineConfig({
122
+ test: {
123
+ globals: true,
124
+ },
125
+ });
126
+ ```
127
+
128
+ - [ ] **Step 4: Install dependencies**
129
+
130
+ ```bash
131
+ npm install
132
+ ```
133
+
134
+ Expected: `node_modules/` created, no errors.
135
+
136
+ - [ ] **Step 5: Commit**
137
+
138
+ ```bash
139
+ git add package.json tsconfig.json vitest.config.ts
140
+ git commit -m "chore: project scaffolding"
141
+ ```
142
+
143
+ ---
144
+
145
+ ## Task 2: AI Provider Interface + Anthropic
146
+
147
+ **Files:**
148
+ - Create: `src/ai/interface.ts`
149
+ - Create: `src/ai/anthropic.ts`
150
+ - Create: `tests/core/anthropic.test.ts`
151
+
152
+ - [ ] **Step 1: Write the failing test**
153
+
154
+ ```typescript
155
+ // tests/core/anthropic.test.ts
156
+ import { describe, it, expect, vi } from 'vitest';
157
+ import { AnthropicProvider } from '../../src/ai/anthropic.js';
158
+
159
+ vi.mock('@anthropic-ai/sdk', () => ({
160
+ default: vi.fn().mockImplementation(() => ({
161
+ messages: {
162
+ create: vi.fn().mockResolvedValue({
163
+ content: [{ type: 'text', text: 'generated content' }],
164
+ }),
165
+ },
166
+ })),
167
+ }));
168
+
169
+ describe('AnthropicProvider', () => {
170
+ it('returns text content from the API response', async () => {
171
+ const provider = new AnthropicProvider('test-key', 'claude-sonnet-4-6');
172
+ const result = await provider.generate('write docs for this diff');
173
+ expect(result).toBe('generated content');
174
+ });
175
+
176
+ it('throws if response content is not text', async () => {
177
+ const { default: Anthropic } = await import('@anthropic-ai/sdk');
178
+ vi.mocked(Anthropic).mockImplementationOnce(() => ({
179
+ messages: {
180
+ create: vi.fn().mockResolvedValue({
181
+ content: [{ type: 'image', source: {} }],
182
+ }),
183
+ },
184
+ }) as any);
185
+ const provider = new AnthropicProvider('test-key', 'claude-sonnet-4-6');
186
+ await expect(provider.generate('prompt')).rejects.toThrow('Unexpected response type');
187
+ });
188
+ });
189
+ ```
190
+
191
+ - [ ] **Step 2: Run test to verify it fails**
192
+
193
+ ```bash
194
+ npm test -- tests/core/anthropic.test.ts
195
+ ```
196
+
197
+ Expected: FAIL — `Cannot find module '../../src/ai/anthropic.js'`
198
+
199
+ - [ ] **Step 3: Create src/ai/interface.ts**
200
+
201
+ ```typescript
202
+ // src/ai/interface.ts
203
+ export interface AIProvider {
204
+ generate(prompt: string): Promise<string>;
205
+ }
206
+ ```
207
+
208
+ - [ ] **Step 4: Create src/ai/anthropic.ts**
209
+
210
+ ```typescript
211
+ // src/ai/anthropic.ts
212
+ import Anthropic from '@anthropic-ai/sdk';
213
+ import type { AIProvider } from './interface.js';
214
+
215
+ export class AnthropicProvider implements AIProvider {
216
+ private client: Anthropic;
217
+ private model: string;
218
+
219
+ constructor(apiKey: string, model: string) {
220
+ this.client = new Anthropic({ apiKey });
221
+ this.model = model;
222
+ }
223
+
224
+ async generate(prompt: string): Promise<string> {
225
+ const message = await this.client.messages.create({
226
+ model: this.model,
227
+ max_tokens: 4096,
228
+ messages: [{ role: 'user', content: prompt }],
229
+ });
230
+ const content = message.content[0];
231
+ if (content.type !== 'text') throw new Error('Unexpected response type');
232
+ return content.text;
233
+ }
234
+ }
235
+ ```
236
+
237
+ - [ ] **Step 5: Run test to verify it passes**
238
+
239
+ ```bash
240
+ npm test -- tests/core/anthropic.test.ts
241
+ ```
242
+
243
+ Expected: PASS
244
+
245
+ - [ ] **Step 6: Commit**
246
+
247
+ ```bash
248
+ git add src/ai/interface.ts src/ai/anthropic.ts tests/core/anthropic.test.ts
249
+ git commit -m "feat: add AI provider interface and Anthropic implementation"
250
+ ```
251
+
252
+ ---
253
+
254
+ ## Task 3: OpenAI Provider
255
+
256
+ **Files:**
257
+ - Create: `src/ai/openai.ts`
258
+ - Create: `tests/core/openai.test.ts`
259
+
260
+ - [ ] **Step 1: Write the failing test**
261
+
262
+ ```typescript
263
+ // tests/core/openai.test.ts
264
+ import { describe, it, expect, vi } from 'vitest';
265
+ import { OpenAIProvider } from '../../src/ai/openai.js';
266
+
267
+ vi.mock('openai', () => ({
268
+ default: vi.fn().mockImplementation(() => ({
269
+ chat: {
270
+ completions: {
271
+ create: vi.fn().mockResolvedValue({
272
+ choices: [{ message: { content: 'gpt generated content' } }],
273
+ }),
274
+ },
275
+ },
276
+ })),
277
+ }));
278
+
279
+ describe('OpenAIProvider', () => {
280
+ it('returns text content from the API response', async () => {
281
+ const provider = new OpenAIProvider('test-key', 'gpt-4o');
282
+ const result = await provider.generate('write docs for this diff');
283
+ expect(result).toBe('gpt generated content');
284
+ });
285
+
286
+ it('returns empty string when message content is null', async () => {
287
+ const { default: OpenAI } = await import('openai');
288
+ vi.mocked(OpenAI).mockImplementationOnce(() => ({
289
+ chat: {
290
+ completions: {
291
+ create: vi.fn().mockResolvedValue({
292
+ choices: [{ message: { content: null } }],
293
+ }),
294
+ },
295
+ },
296
+ }) as any);
297
+ const provider = new OpenAIProvider('test-key', 'gpt-4o');
298
+ const result = await provider.generate('prompt');
299
+ expect(result).toBe('');
300
+ });
301
+ });
302
+ ```
303
+
304
+ - [ ] **Step 2: Run test to verify it fails**
305
+
306
+ ```bash
307
+ npm test -- tests/core/openai.test.ts
308
+ ```
309
+
310
+ Expected: FAIL — `Cannot find module '../../src/ai/openai.js'`
311
+
312
+ - [ ] **Step 3: Create src/ai/openai.ts**
313
+
314
+ ```typescript
315
+ // src/ai/openai.ts
316
+ import OpenAI from 'openai';
317
+ import type { AIProvider } from './interface.js';
318
+
319
+ export class OpenAIProvider implements AIProvider {
320
+ private client: OpenAI;
321
+ private model: string;
322
+
323
+ constructor(apiKey: string, model: string) {
324
+ this.client = new OpenAI({ apiKey });
325
+ this.model = model;
326
+ }
327
+
328
+ async generate(prompt: string): Promise<string> {
329
+ const completion = await this.client.chat.completions.create({
330
+ model: this.model,
331
+ messages: [{ role: 'user', content: prompt }],
332
+ });
333
+ return completion.choices[0].message.content ?? '';
334
+ }
335
+ }
336
+ ```
337
+
338
+ - [ ] **Step 4: Run test to verify it passes**
339
+
340
+ ```bash
341
+ npm test -- tests/core/openai.test.ts
342
+ ```
343
+
344
+ Expected: PASS
345
+
346
+ - [ ] **Step 5: Commit**
347
+
348
+ ```bash
349
+ git add src/ai/openai.ts tests/core/openai.test.ts
350
+ git commit -m "feat: add OpenAI provider"
351
+ ```
352
+
353
+ ---
354
+
355
+ ## Task 4: Diff Extraction
356
+
357
+ **Files:**
358
+ - Create: `src/core/diff.ts`
359
+ - Create: `tests/fixtures/diff-small.txt`
360
+ - Create: `tests/fixtures/diff-schema.txt`
361
+ - Create: `tests/fixtures/diff-frontend.txt`
362
+ - Create: `tests/core/diff.test.ts`
363
+
364
+ - [ ] **Step 1: Create test fixtures**
365
+
366
+ `tests/fixtures/diff-small.txt`:
367
+ ```
368
+ diff --git a/src/auth/login.ts b/src/auth/login.ts
369
+ index abc1234..def5678 100644
370
+ --- a/src/auth/login.ts
371
+ +++ b/src/auth/login.ts
372
+ @@ -1,5 +1,10 @@
373
+ +export function login(email: string, password: string) {
374
+ + return { token: 'jwt' };
375
+ +}
376
+ diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts
377
+ index 111aaaa..222bbbb 100644
378
+ --- a/src/utils/helpers.ts
379
+ +++ b/src/utils/helpers.ts
380
+ @@ -1,3 +1,6 @@
381
+ +export function sleep(ms: number) {
382
+ + return new Promise(r => setTimeout(r, ms));
383
+ +}
384
+ ```
385
+
386
+ `tests/fixtures/diff-schema.txt`:
387
+ ```
388
+ diff --git a/migrations/001_add_users.sql b/migrations/001_add_users.sql
389
+ new file mode 100644
390
+ index 0000000..aabbcc
391
+ --- /dev/null
392
+ +++ b/migrations/001_add_users.sql
393
+ @@ -0,0 +1,5 @@
394
+ +CREATE TABLE users (
395
+ + id UUID PRIMARY KEY,
396
+ + email TEXT NOT NULL UNIQUE
397
+ +);
398
+ diff --git a/src/db/schema.ts b/src/db/schema.ts
399
+ new file mode 100644
400
+ ```
401
+
402
+ `tests/fixtures/diff-frontend.txt`:
403
+ ```
404
+ diff --git a/src/ui/Button.tsx b/src/ui/Button.tsx
405
+ new file mode 100644
406
+ index 0000000..aabbcc
407
+ --- /dev/null
408
+ +++ b/src/ui/Button.tsx
409
+ @@ -0,0 +1,8 @@
410
+ +export function Button({ label }: { label: string }) {
411
+ + return <button>{label}</button>;
412
+ +}
413
+ diff --git a/src/ui/styles.css b/src/ui/styles.css
414
+ new file mode 100644
415
+ ```
416
+
417
+ - [ ] **Step 2: Write the failing test**
418
+
419
+ ```typescript
420
+ // tests/core/diff.test.ts
421
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
422
+ import { readFileSync } from 'fs';
423
+ import { join } from 'path';
424
+ import { parseChangedFiles, groupFiles } from '../../src/core/diff.js';
425
+
426
+ const smallDiff = readFileSync(
427
+ join(import.meta.dirname, '../fixtures/diff-small.txt'), 'utf-8'
428
+ );
429
+ const schemaDiff = readFileSync(
430
+ join(import.meta.dirname, '../fixtures/diff-schema.txt'), 'utf-8'
431
+ );
432
+ const frontendDiff = readFileSync(
433
+ join(import.meta.dirname, '../fixtures/diff-frontend.txt'), 'utf-8'
434
+ );
435
+
436
+ describe('parseChangedFiles', () => {
437
+ it('extracts file paths from diff output', () => {
438
+ const files = parseChangedFiles(smallDiff);
439
+ expect(files).toContain('src/auth/login.ts');
440
+ expect(files).toContain('src/utils/helpers.ts');
441
+ expect(files).toHaveLength(2);
442
+ });
443
+
444
+ it('returns empty array for empty diff', () => {
445
+ expect(parseChangedFiles('')).toEqual([]);
446
+ });
447
+ });
448
+
449
+ describe('groupFiles', () => {
450
+ it('puts auth files in auth group', () => {
451
+ const groups = groupFiles(['src/auth/login.ts', 'src/utils/helpers.ts']);
452
+ expect(groups.auth).toContain('src/auth/login.ts');
453
+ expect(groups.auth).not.toContain('src/utils/helpers.ts');
454
+ });
455
+
456
+ it('puts sql files in schema group', () => {
457
+ const files = parseChangedFiles(schemaDiff);
458
+ const groups = groupFiles(files);
459
+ expect(groups.schema.length).toBeGreaterThan(0);
460
+ });
461
+
462
+ it('puts tsx files in frontend group', () => {
463
+ const files = parseChangedFiles(frontendDiff);
464
+ const groups = groupFiles(files);
465
+ expect(groups.frontend).toContain('src/ui/Button.tsx');
466
+ expect(groups.frontend).toContain('src/ui/styles.css');
467
+ });
468
+ });
469
+ ```
470
+
471
+ - [ ] **Step 3: Run test to verify it fails**
472
+
473
+ ```bash
474
+ npm test -- tests/core/diff.test.ts
475
+ ```
476
+
477
+ Expected: FAIL — `Cannot find module '../../src/core/diff.js'`
478
+
479
+ - [ ] **Step 4: Create src/core/diff.ts**
480
+
481
+ ```typescript
482
+ // src/core/diff.ts
483
+ import { execSync } from 'child_process';
484
+
485
+ export interface FileGroups {
486
+ frontend: string[];
487
+ schema: string[];
488
+ auth: string[];
489
+ infra: string[];
490
+ other: string[];
491
+ }
492
+
493
+ export interface DiffResult {
494
+ raw: string;
495
+ prevTag: string;
496
+ currentTag: string;
497
+ changedFiles: string[];
498
+ fileGroups: FileGroups;
499
+ }
500
+
501
+ export function extractDiff(): DiffResult {
502
+ const tags = execSync('git tag --sort=version:refname')
503
+ .toString()
504
+ .trim()
505
+ .split('\n')
506
+ .filter(Boolean);
507
+
508
+ if (tags.length === 0) throw new Error('No tags found in repository');
509
+
510
+ const currentTag = tags[tags.length - 1];
511
+ const prevTag = tags.length >= 2 ? tags[tags.length - 2] : null;
512
+
513
+ const raw = prevTag
514
+ ? execSync(`git diff ${prevTag} ${currentTag}`).toString()
515
+ : execSync(`git show ${currentTag} --format='' -p`).toString();
516
+
517
+ const changedFiles = parseChangedFiles(raw);
518
+ const fileGroups = groupFiles(changedFiles);
519
+
520
+ return {
521
+ raw,
522
+ prevTag: prevTag ?? '(initial)',
523
+ currentTag,
524
+ changedFiles,
525
+ fileGroups,
526
+ };
527
+ }
528
+
529
+ export function parseChangedFiles(diff: string): string[] {
530
+ const matches = diff.match(/^diff --git a\/.+ b\/(.+)$/gm) ?? [];
531
+ return matches.map(line => {
532
+ const match = line.match(/^diff --git a\/.+ b\/(.+)$/);
533
+ return match ? match[1] : '';
534
+ }).filter(Boolean);
535
+ }
536
+
537
+ export function groupFiles(files: string[]): FileGroups {
538
+ const isFrontend = (f: string) =>
539
+ /\.(tsx?|jsx?|css|scss|html|vue|svelte)$/.test(f) ||
540
+ /\b(ui|frontend|components|pages|views)\b/.test(f);
541
+
542
+ const isSchema = (f: string) =>
543
+ /\.sql$/.test(f) || /\b(schema|migration|migrate)\b/i.test(f);
544
+
545
+ const isAuth = (f: string) =>
546
+ /\b(auth|permission|security|oauth|jwt|session|token)\b/i.test(f);
547
+
548
+ const isInfra = (f: string) =>
549
+ /\b(infra|deploy|k8s|docker|compose|helm|terraform|service|gateway)\b/i.test(f);
550
+
551
+ return {
552
+ frontend: files.filter(isFrontend),
553
+ schema: files.filter(isSchema),
554
+ auth: files.filter(isAuth),
555
+ infra: files.filter(isInfra),
556
+ other: files.filter(f => !isFrontend(f) && !isSchema(f) && !isAuth(f) && !isInfra(f)),
557
+ };
558
+ }
559
+ ```
560
+
561
+ - [ ] **Step 5: Run test to verify it passes**
562
+
563
+ ```bash
564
+ npm test -- tests/core/diff.test.ts
565
+ ```
566
+
567
+ Expected: PASS
568
+
569
+ - [ ] **Step 6: Commit**
570
+
571
+ ```bash
572
+ git add src/core/diff.ts tests/core/diff.test.ts tests/fixtures/
573
+ git commit -m "feat: add diff extraction and file grouping"
574
+ ```
575
+
576
+ ---
577
+
578
+ ## Task 5: Relevance Judgment
579
+
580
+ **Files:**
581
+ - Create: `src/core/relevance.ts`
582
+ - Create: `tests/core/relevance.test.ts`
583
+
584
+ - [ ] **Step 1: Write the failing test**
585
+
586
+ ```typescript
587
+ // tests/core/relevance.test.ts
588
+ import { describe, it, expect } from 'vitest';
589
+ import { selectTargets } from '../../src/core/relevance.js';
590
+ import type { FileGroups } from '../../src/core/diff.js';
591
+
592
+ const emptyGroups: FileGroups = {
593
+ frontend: [], schema: [], auth: [], infra: [], other: [],
594
+ };
595
+
596
+ describe('selectTargets', () => {
597
+ it('always includes core targets', () => {
598
+ const targets = selectTargets(emptyGroups, []);
599
+ expect(targets).toContain('AGENTS.md');
600
+ expect(targets).toContain('ARCHITECTURE.md');
601
+ expect(targets).toContain('DESIGN.md');
602
+ expect(targets).toContain('QUALITY_SCORE.md');
603
+ expect(targets).toContain('changelog');
604
+ expect(targets).toContain('design-doc');
605
+ expect(targets).toContain('design-doc-index');
606
+ expect(targets).toContain('tech-debt-tracker');
607
+ });
608
+
609
+ it('includes FRONTEND.md only when frontend files changed', () => {
610
+ const withFrontend = selectTargets(
611
+ { ...emptyGroups, frontend: ['src/ui/Button.tsx'] }, []
612
+ );
613
+ expect(withFrontend).toContain('FRONTEND.md');
614
+
615
+ const withoutFrontend = selectTargets(emptyGroups, []);
616
+ expect(withoutFrontend).not.toContain('FRONTEND.md');
617
+ });
618
+
619
+ it('includes SECURITY.md only when auth files changed', () => {
620
+ const targets = selectTargets(
621
+ { ...emptyGroups, auth: ['src/auth/login.ts'] }, []
622
+ );
623
+ expect(targets).toContain('SECURITY.md');
624
+ });
625
+
626
+ it('includes RELIABILITY.md only when infra files changed', () => {
627
+ const targets = selectTargets(
628
+ { ...emptyGroups, infra: ['infra/deploy.yaml'] }, []
629
+ );
630
+ expect(targets).toContain('RELIABILITY.md');
631
+ });
632
+
633
+ it('includes db-schema only when schema files changed', () => {
634
+ const targets = selectTargets(
635
+ { ...emptyGroups, schema: ['migrations/001.sql'] }, []
636
+ );
637
+ expect(targets).toContain('db-schema');
638
+ });
639
+
640
+ it('includes references when package.json changed', () => {
641
+ const targets = selectTargets(emptyGroups, ['package.json']);
642
+ expect(targets).toContain('references');
643
+ });
644
+
645
+ it('returns no duplicates', () => {
646
+ const targets = selectTargets(emptyGroups, []);
647
+ expect(targets.length).toBe(new Set(targets).size);
648
+ });
649
+ });
650
+ ```
651
+
652
+ - [ ] **Step 2: Run test to verify it fails**
653
+
654
+ ```bash
655
+ npm test -- tests/core/relevance.test.ts
656
+ ```
657
+
658
+ Expected: FAIL — `Cannot find module '../../src/core/relevance.js'`
659
+
660
+ - [ ] **Step 3: Create src/core/relevance.ts**
661
+
662
+ ```typescript
663
+ // src/core/relevance.ts
664
+ import type { FileGroups } from './diff.js';
665
+
666
+ export type DocTarget =
667
+ | 'AGENTS.md'
668
+ | 'ARCHITECTURE.md'
669
+ | 'DESIGN.md'
670
+ | 'FRONTEND.md'
671
+ | 'SECURITY.md'
672
+ | 'RELIABILITY.md'
673
+ | 'QUALITY_SCORE.md'
674
+ | 'changelog'
675
+ | 'design-doc'
676
+ | 'design-doc-index'
677
+ | 'tech-debt-tracker'
678
+ | 'db-schema'
679
+ | 'product-specs-index'
680
+ | 'references';
681
+
682
+ const CORE_TARGETS: DocTarget[] = [
683
+ 'AGENTS.md',
684
+ 'ARCHITECTURE.md',
685
+ 'DESIGN.md',
686
+ 'QUALITY_SCORE.md',
687
+ 'changelog',
688
+ 'design-doc',
689
+ 'design-doc-index',
690
+ 'tech-debt-tracker',
691
+ ];
692
+
693
+ export function selectTargets(
694
+ fileGroups: FileGroups,
695
+ changedFiles: string[]
696
+ ): DocTarget[] {
697
+ const targets: DocTarget[] = [...CORE_TARGETS];
698
+
699
+ if (fileGroups.frontend.length > 0) targets.push('FRONTEND.md');
700
+ if (fileGroups.auth.length > 0) targets.push('SECURITY.md');
701
+ if (fileGroups.infra.length > 0) targets.push('RELIABILITY.md');
702
+ if (fileGroups.schema.length > 0) targets.push('db-schema');
703
+
704
+ const packageFiles = ['package.json', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'];
705
+ if (changedFiles.some(f => packageFiles.includes(f))) {
706
+ targets.push('references');
707
+ }
708
+
709
+ const looksLikeNewFeature = changedFiles.some(f =>
710
+ /\b(feature|feat|new)\b/i.test(f) || f.startsWith('src/features/')
711
+ );
712
+ if (looksLikeNewFeature) targets.push('product-specs-index');
713
+
714
+ return [...new Set(targets)];
715
+ }
716
+ ```
717
+
718
+ - [ ] **Step 4: Run test to verify it passes**
719
+
720
+ ```bash
721
+ npm test -- tests/core/relevance.test.ts
722
+ ```
723
+
724
+ Expected: PASS
725
+
726
+ - [ ] **Step 5: Commit**
727
+
728
+ ```bash
729
+ git add src/core/relevance.ts tests/core/relevance.test.ts
730
+ git commit -m "feat: add relevance judgment for document targets"
731
+ ```
732
+
733
+ ---
734
+
735
+ ## Task 6: File Writer
736
+
737
+ **Files:**
738
+ - Create: `src/core/writer.ts`
739
+ - Create: `tests/core/writer.test.ts`
740
+
741
+ - [ ] **Step 1: Write the failing test**
742
+
743
+ ```typescript
744
+ // tests/core/writer.test.ts
745
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
746
+ import { mkdirSync, rmSync, existsSync, readFileSync } from 'fs';
747
+ import { join } from 'path';
748
+ import { appendSection, createFile } from '../../src/core/writer.js';
749
+
750
+ const TMP = join(import.meta.dirname, '../../tmp-writer-test');
751
+
752
+ beforeEach(() => mkdirSync(TMP, { recursive: true }));
753
+ afterEach(() => rmSync(TMP, { recursive: true, force: true }));
754
+
755
+ describe('appendSection', () => {
756
+ it('creates file if it does not exist', () => {
757
+ const path = join(TMP, 'AGENTS.md');
758
+ appendSection(path, 'Changes in v1.1.0', 'New auth module added.');
759
+ expect(existsSync(path)).toBe(true);
760
+ const content = readFileSync(path, 'utf-8');
761
+ expect(content).toContain('## Changes in v1.1.0');
762
+ expect(content).toContain('New auth module added.');
763
+ });
764
+
765
+ it('appends to existing file', () => {
766
+ const path = join(TMP, 'ARCHITECTURE.md');
767
+ appendSection(path, 'Changes in v1.0.0', 'Initial architecture.');
768
+ appendSection(path, 'Changes in v1.1.0', 'Added caching layer.');
769
+ const content = readFileSync(path, 'utf-8');
770
+ expect(content).toContain('## Changes in v1.0.0');
771
+ expect(content).toContain('## Changes in v1.1.0');
772
+ });
773
+
774
+ it('creates missing parent directories', () => {
775
+ const path = join(TMP, 'docs/design-docs/index.md');
776
+ appendSection(path, 'v1.0.0', 'entry');
777
+ expect(existsSync(path)).toBe(true);
778
+ });
779
+ });
780
+
781
+ describe('createFile', () => {
782
+ it('creates a file with the given content', () => {
783
+ const path = join(TMP, 'changelog/v1.0.0.md');
784
+ createFile(path, '# Changelog: v1.0.0\n\n## Added\n- First release');
785
+ expect(readFileSync(path, 'utf-8')).toContain('First release');
786
+ });
787
+
788
+ it('overwrites an existing file', () => {
789
+ const path = join(TMP, 'test.md');
790
+ createFile(path, 'first content');
791
+ createFile(path, 'second content');
792
+ expect(readFileSync(path, 'utf-8')).toBe('second content');
793
+ });
794
+
795
+ it('creates missing parent directories', () => {
796
+ const path = join(TMP, 'a/b/c/file.md');
797
+ createFile(path, 'content');
798
+ expect(existsSync(path)).toBe(true);
799
+ });
800
+ });
801
+ ```
802
+
803
+ - [ ] **Step 2: Run test to verify it fails**
804
+
805
+ ```bash
806
+ npm test -- tests/core/writer.test.ts
807
+ ```
808
+
809
+ Expected: FAIL — `Cannot find module '../../src/core/writer.js'`
810
+
811
+ - [ ] **Step 3: Create src/core/writer.ts**
812
+
813
+ ```typescript
814
+ // src/core/writer.ts
815
+ import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs';
816
+ import { dirname } from 'path';
817
+
818
+ export function appendSection(filePath: string, heading: string, content: string): void {
819
+ ensureDir(filePath);
820
+ const section = `\n## ${heading}\n\n${content}\n`;
821
+ if (existsSync(filePath)) {
822
+ const existing = readFileSync(filePath, 'utf-8');
823
+ writeFileSync(filePath, existing + section, 'utf-8');
824
+ } else {
825
+ writeFileSync(filePath, section.trimStart(), 'utf-8');
826
+ }
827
+ }
828
+
829
+ export function createFile(filePath: string, content: string): void {
830
+ ensureDir(filePath);
831
+ writeFileSync(filePath, content, 'utf-8');
832
+ }
833
+
834
+ function ensureDir(filePath: string): void {
835
+ const dir = dirname(filePath);
836
+ if (!existsSync(dir)) {
837
+ mkdirSync(dir, { recursive: true });
838
+ }
839
+ }
840
+ ```
841
+
842
+ - [ ] **Step 4: Run test to verify it passes**
843
+
844
+ ```bash
845
+ npm test -- tests/core/writer.test.ts
846
+ ```
847
+
848
+ Expected: PASS
849
+
850
+ - [ ] **Step 5: Commit**
851
+
852
+ ```bash
853
+ git add src/core/writer.ts tests/core/writer.test.ts
854
+ git commit -m "feat: add file writer utilities"
855
+ ```
856
+
857
+ ---
858
+
859
+ ## Task 7: Document Generator
860
+
861
+ **Files:**
862
+ - Create: `src/core/generator.ts`
863
+ - Create: `tests/core/generator.test.ts`
864
+
865
+ - [ ] **Step 1: Write the failing test**
866
+
867
+ ```typescript
868
+ // tests/core/generator.test.ts
869
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
870
+ import { mkdirSync, rmSync, existsSync, readFileSync } from 'fs';
871
+ import { join } from 'path';
872
+ import { generateDocs, writeResults } from '../../src/core/generator.js';
873
+ import type { AIProvider } from '../../src/ai/interface.js';
874
+ import type { DiffResult } from '../../src/core/diff.js';
875
+
876
+ const TMP = join(import.meta.dirname, '../../tmp-generator-test');
877
+
878
+ beforeEach(() => mkdirSync(TMP, { recursive: true }));
879
+ afterEach(() => rmSync(TMP, { recursive: true, force: true }));
880
+
881
+ const mockAI: AIProvider = {
882
+ generate: vi.fn().mockResolvedValue('AI generated content for this section'),
883
+ };
884
+
885
+ const fakeDiff: DiffResult = {
886
+ raw: 'diff --git a/src/auth/login.ts b/src/auth/login.ts\n+new code',
887
+ prevTag: 'v1.0.0',
888
+ currentTag: 'v1.1.0',
889
+ changedFiles: ['src/auth/login.ts'],
890
+ fileGroups: {
891
+ frontend: [],
892
+ schema: [],
893
+ auth: ['src/auth/login.ts'],
894
+ infra: [],
895
+ other: [],
896
+ },
897
+ };
898
+
899
+ describe('generateDocs', () => {
900
+ it('calls AI for each target and returns results', async () => {
901
+ const results = await generateDocs(mockAI, fakeDiff, ['AGENTS.md', 'ARCHITECTURE.md']);
902
+ expect(results).toHaveLength(2);
903
+ expect(results[0].target).toBe('AGENTS.md');
904
+ expect(results[0].content).toBe('AI generated content for this section');
905
+ expect(mockAI.generate).toHaveBeenCalledTimes(2);
906
+ });
907
+
908
+ it('captures errors without throwing', async () => {
909
+ const failingAI: AIProvider = {
910
+ generate: vi.fn().mockRejectedValue(new Error('API rate limit')),
911
+ };
912
+ const results = await generateDocs(failingAI, fakeDiff, ['AGENTS.md']);
913
+ expect(results[0].error).toContain('API rate limit');
914
+ expect(results[0].content).toBe('');
915
+ });
916
+
917
+ it('runs targets concurrently', async () => {
918
+ const delays: number[] = [];
919
+ const timedAI: AIProvider = {
920
+ generate: vi.fn().mockImplementation(async () => {
921
+ delays.push(Date.now());
922
+ await new Promise(r => setTimeout(r, 50));
923
+ return 'content';
924
+ }),
925
+ };
926
+ const start = Date.now();
927
+ await generateDocs(timedAI, fakeDiff, ['AGENTS.md', 'ARCHITECTURE.md', 'DESIGN.md']);
928
+ const elapsed = Date.now() - start;
929
+ // Three 50ms tasks running concurrently should finish in ~50-100ms, not ~150ms
930
+ expect(elapsed).toBeLessThan(130);
931
+ });
932
+ });
933
+
934
+ describe('writeResults', () => {
935
+ it('writes AGENTS.md as appended section', async () => {
936
+ const results = await generateDocs(mockAI, fakeDiff, ['AGENTS.md']);
937
+ const written = writeResults(results, fakeDiff, TMP);
938
+ expect(written).toContain(`${TMP}/AGENTS.md`);
939
+ const content = readFileSync(`${TMP}/AGENTS.md`, 'utf-8');
940
+ expect(content).toContain('## Changes in v1.1.0');
941
+ });
942
+
943
+ it('creates changelog file at versioned path', async () => {
944
+ const results = await generateDocs(mockAI, fakeDiff, ['changelog']);
945
+ writeResults(results, fakeDiff, TMP);
946
+ expect(existsSync(`${TMP}/changelog/v1.1.0.md`)).toBe(true);
947
+ });
948
+
949
+ it('creates design-doc at versioned path', async () => {
950
+ const results = await generateDocs(mockAI, fakeDiff, ['design-doc']);
951
+ writeResults(results, fakeDiff, TMP);
952
+ expect(existsSync(`${TMP}/docs/design-docs/v1.1.0.md`)).toBe(true);
953
+ });
954
+
955
+ it('skips results with errors', async () => {
956
+ const failingAI: AIProvider = {
957
+ generate: vi.fn().mockRejectedValue(new Error('fail')),
958
+ };
959
+ const results = await generateDocs(failingAI, fakeDiff, ['AGENTS.md']);
960
+ const written = writeResults(results, fakeDiff, TMP);
961
+ expect(written).toHaveLength(0);
962
+ });
963
+ });
964
+ ```
965
+
966
+ - [ ] **Step 2: Run test to verify it fails**
967
+
968
+ ```bash
969
+ npm test -- tests/core/generator.test.ts
970
+ ```
971
+
972
+ Expected: FAIL — `Cannot find module '../../src/core/generator.js'`
973
+
974
+ - [ ] **Step 3: Create src/core/generator.ts**
975
+
976
+ ```typescript
977
+ // src/core/generator.ts
978
+ import type { AIProvider } from '../ai/interface.js';
979
+ import type { DocTarget } from './relevance.js';
980
+ import type { DiffResult } from './diff.js';
981
+ import { appendSection, createFile } from './writer.js';
982
+
983
+ type PromptFn = (diff: DiffResult) => string;
984
+
985
+ const PROMPTS: Record<DocTarget, PromptFn> = {
986
+ 'AGENTS.md': (diff) =>
987
+ `You are a technical writer following Harness Engineering documentation style.
988
+
989
+ Based on the git diff between ${diff.prevTag} and ${diff.currentTag}, write a section for AGENTS.md.
990
+ Describe what AI coding agents need to know: new modules, interfaces, APIs, navigation patterns, new conventions.
991
+ Write in present tense. Be specific and actionable. 2-4 paragraphs max.
992
+
993
+ Git diff:
994
+ ${diff.raw.slice(0, 8000)}`,
995
+
996
+ 'ARCHITECTURE.md': (diff) =>
997
+ `You are a technical writer following Harness Engineering documentation style.
998
+
999
+ Based on the git diff between ${diff.prevTag} and ${diff.currentTag}, write a section for ARCHITECTURE.md.
1000
+ Focus on: new layers, modules, dependency changes, new abstractions, removed or restructured components.
1001
+ Write in present tense. 2-3 paragraphs max.
1002
+
1003
+ Git diff:
1004
+ ${diff.raw.slice(0, 8000)}`,
1005
+
1006
+ 'DESIGN.md': (diff) =>
1007
+ `You are a technical writer following Harness Engineering documentation style.
1008
+
1009
+ Based on the git diff between ${diff.prevTag} and ${diff.currentTag}, write a section for DESIGN.md.
1010
+ Focus on key design decisions, why certain approaches were chosen, trade-offs made.
1011
+ Write in present tense. 2-3 paragraphs max.
1012
+
1013
+ Git diff:
1014
+ ${diff.raw.slice(0, 8000)}`,
1015
+
1016
+ 'FRONTEND.md': (diff) =>
1017
+ `You are a technical writer following Harness Engineering documentation style.
1018
+
1019
+ Based on the git diff between ${diff.prevTag} and ${diff.currentTag}, write a section for FRONTEND.md.
1020
+ Focus on new components, UI patterns, styling changes, frontend architecture changes.
1021
+ Write in present tense. 2-3 paragraphs max.
1022
+
1023
+ Git diff:
1024
+ ${diff.raw.slice(0, 8000)}`,
1025
+
1026
+ 'SECURITY.md': (diff) =>
1027
+ `You are a technical writer following Harness Engineering documentation style.
1028
+
1029
+ Based on the git diff between ${diff.prevTag} and ${diff.currentTag}, write a section for SECURITY.md.
1030
+ Focus on auth changes, permission model updates, new security boundaries, data handling changes.
1031
+ Write in present tense. 2-3 paragraphs max.
1032
+
1033
+ Git diff:
1034
+ ${diff.raw.slice(0, 8000)}`,
1035
+
1036
+ 'RELIABILITY.md': (diff) =>
1037
+ `You are a technical writer following Harness Engineering documentation style.
1038
+
1039
+ Based on the git diff between ${diff.prevTag} and ${diff.currentTag}, write a section for RELIABILITY.md.
1040
+ Focus on error handling improvements, retry logic, circuit breakers, observability changes.
1041
+ Write in present tense. 2-3 paragraphs max.
1042
+
1043
+ Git diff:
1044
+ ${diff.raw.slice(0, 8000)}`,
1045
+
1046
+ 'QUALITY_SCORE.md': (diff) =>
1047
+ `You are a technical writer following Harness Engineering documentation style.
1048
+
1049
+ Based on the git diff between ${diff.prevTag} and ${diff.currentTag}, write a quality assessment section.
1050
+ Assess test coverage changes, code complexity, technical debt introduced or resolved.
1051
+ Write in present tense. 1-2 paragraphs max.
1052
+
1053
+ Git diff:
1054
+ ${diff.raw.slice(0, 8000)}`,
1055
+
1056
+ 'changelog': (diff) =>
1057
+ `Write a changelog entry in Markdown for changes from ${diff.prevTag} to ${diff.currentTag}.
1058
+
1059
+ Format:
1060
+ # Changelog: ${diff.currentTag}
1061
+
1062
+ ## Added
1063
+ - ...
1064
+
1065
+ ## Changed
1066
+ - ...
1067
+
1068
+ ## Fixed
1069
+ - ...
1070
+
1071
+ ## Removed
1072
+ - ...
1073
+
1074
+ Be specific and engineer-focused. Only include sections with actual content.
1075
+
1076
+ Git diff:
1077
+ ${diff.raw.slice(0, 8000)}`,
1078
+
1079
+ 'design-doc': (diff) =>
1080
+ `You are a technical writer following Harness Engineering documentation style.
1081
+
1082
+ Write a design document for changes from ${diff.prevTag} to ${diff.currentTag}.
1083
+
1084
+ Format:
1085
+ # Design: ${diff.currentTag}
1086
+
1087
+ ## Summary
1088
+ [What changed and why]
1089
+
1090
+ ## Design Decisions
1091
+ [Key decisions with rationale]
1092
+
1093
+ ## Agent Legibility Notes
1094
+ [What an AI coding agent needs to know to work in the updated codebase]
1095
+
1096
+ ## Technical Debt
1097
+ [Any shortcuts taken, what should be cleaned up later — or "None identified"]
1098
+
1099
+ Git diff:
1100
+ ${diff.raw.slice(0, 8000)}`,
1101
+
1102
+ 'design-doc-index': (diff) =>
1103
+ `Return only a single Markdown list item for a docs index. Format:
1104
+ - [${diff.currentTag}](${diff.currentTag}.md) — One-sentence summary of changes.
1105
+
1106
+ Changed files: ${diff.changedFiles.slice(0, 20).join(', ')}
1107
+
1108
+ Return only the list item, nothing else.`,
1109
+
1110
+ 'tech-debt-tracker': (diff) =>
1111
+ `You are a technical writer following Harness Engineering documentation style.
1112
+
1113
+ Based on the git diff between ${diff.prevTag} and ${diff.currentTag}, identify technical debt.
1114
+
1115
+ Write this section:
1116
+ ## ${diff.currentTag}
1117
+
1118
+ ### New debt
1119
+ - [Shortcuts, hacks, or deferred work visible in the diff — or "None identified"]
1120
+
1121
+ ### Resolved debt
1122
+ - [Cleanup or refactoring that resolves known issues — or "None identified"]
1123
+
1124
+ Git diff:
1125
+ ${diff.raw.slice(0, 8000)}`,
1126
+
1127
+ 'db-schema': (diff) =>
1128
+ `You are a technical writer.
1129
+
1130
+ Based on the git diff between ${diff.prevTag} and ${diff.currentTag}, document database schema changes.
1131
+ Focus only on schema changes visible in the diff: new tables, columns, indexes, constraints.
1132
+ Format as Markdown with clear table descriptions.
1133
+ If no schema changes: write "No schema changes in this release."
1134
+
1135
+ Git diff:
1136
+ ${diff.raw.slice(0, 8000)}`,
1137
+
1138
+ 'product-specs-index': (diff) =>
1139
+ `Return only a single Markdown list item for a product specs index, or an empty string if no clear new feature.
1140
+ Format (if applicable): - [Feature Name](feature-name.md) — One-sentence description.
1141
+
1142
+ Changed files: ${diff.changedFiles.slice(0, 20).join(', ')}
1143
+
1144
+ Return only the list item or empty string, nothing else.`,
1145
+
1146
+ 'references': (diff) =>
1147
+ `You are a technical writer.
1148
+
1149
+ Based on the git diff between ${diff.prevTag} and ${diff.currentTag}, identify new external libraries introduced.
1150
+ For each new library write:
1151
+
1152
+ # library-name
1153
+
1154
+ One paragraph: what it does and how it is used in this codebase.
1155
+
1156
+ If no new external libraries: return an empty string.
1157
+
1158
+ Git diff:
1159
+ ${diff.raw.slice(0, 8000)}`,
1160
+ };
1161
+
1162
+ export interface GenerationResult {
1163
+ target: DocTarget;
1164
+ content: string;
1165
+ error?: string;
1166
+ }
1167
+
1168
+ export async function generateDocs(
1169
+ ai: AIProvider,
1170
+ diff: DiffResult,
1171
+ targets: DocTarget[]
1172
+ ): Promise<GenerationResult[]> {
1173
+ return Promise.all(
1174
+ targets.map(async (target): Promise<GenerationResult> => {
1175
+ try {
1176
+ const prompt = PROMPTS[target](diff);
1177
+ const content = await ai.generate(prompt);
1178
+ return { target, content };
1179
+ } catch (err) {
1180
+ return { target, content: '', error: String(err) };
1181
+ }
1182
+ })
1183
+ );
1184
+ }
1185
+
1186
+ export function writeResults(
1187
+ results: GenerationResult[],
1188
+ diff: DiffResult,
1189
+ cwd: string
1190
+ ): string[] {
1191
+ const written: string[] = [];
1192
+ for (const result of results) {
1193
+ if (result.error || !result.content.trim()) continue;
1194
+ const path = writeResult(result, diff, cwd);
1195
+ if (path) written.push(path);
1196
+ }
1197
+ return written;
1198
+ }
1199
+
1200
+ function writeResult(
1201
+ result: GenerationResult,
1202
+ diff: DiffResult,
1203
+ cwd: string
1204
+ ): string | null {
1205
+ const tag = diff.currentTag;
1206
+ const heading = `Changes in ${tag}`;
1207
+
1208
+ const appendTargets: DocTarget[] = [
1209
+ 'AGENTS.md', 'ARCHITECTURE.md', 'DESIGN.md', 'FRONTEND.md',
1210
+ 'SECURITY.md', 'RELIABILITY.md', 'QUALITY_SCORE.md',
1211
+ ];
1212
+
1213
+ if ((appendTargets as string[]).includes(result.target)) {
1214
+ const path = `${cwd}/${result.target}`;
1215
+ appendSection(path, heading, result.content);
1216
+ return path;
1217
+ }
1218
+
1219
+ switch (result.target) {
1220
+ case 'changelog': {
1221
+ const path = `${cwd}/changelog/${tag}.md`;
1222
+ createFile(path, result.content);
1223
+ return path;
1224
+ }
1225
+ case 'design-doc': {
1226
+ const path = `${cwd}/docs/design-docs/${tag}.md`;
1227
+ createFile(path, result.content);
1228
+ return path;
1229
+ }
1230
+ case 'design-doc-index': {
1231
+ const path = `${cwd}/docs/design-docs/index.md`;
1232
+ appendSection(path, heading, result.content);
1233
+ return path;
1234
+ }
1235
+ case 'tech-debt-tracker': {
1236
+ const path = `${cwd}/docs/exec-plans/tech-debt-tracker.md`;
1237
+ appendSection(path, heading, result.content);
1238
+ return path;
1239
+ }
1240
+ case 'db-schema': {
1241
+ const path = `${cwd}/docs/generated/db-schema.md`;
1242
+ appendSection(path, heading, result.content);
1243
+ return path;
1244
+ }
1245
+ case 'product-specs-index': {
1246
+ if (!result.content.trim()) return null;
1247
+ const path = `${cwd}/docs/product-specs/index.md`;
1248
+ appendSection(path, heading, result.content);
1249
+ return path;
1250
+ }
1251
+ case 'references': {
1252
+ if (!result.content.trim()) return null;
1253
+ const libName = extractLibName(result.content);
1254
+ if (!libName) return null;
1255
+ const path = `${cwd}/docs/references/${libName}-llms.txt`;
1256
+ createFile(path, result.content);
1257
+ return path;
1258
+ }
1259
+ default:
1260
+ return null;
1261
+ }
1262
+ }
1263
+
1264
+ function extractLibName(content: string): string | null {
1265
+ const match = content.match(/^#\s+(.+)$/m);
1266
+ return match ? match[1].toLowerCase().replace(/[^a-z0-9-]/g, '-') : null;
1267
+ }
1268
+ ```
1269
+
1270
+ - [ ] **Step 4: Run test to verify it passes**
1271
+
1272
+ ```bash
1273
+ npm test -- tests/core/generator.test.ts
1274
+ ```
1275
+
1276
+ Expected: PASS
1277
+
1278
+ - [ ] **Step 5: Commit**
1279
+
1280
+ ```bash
1281
+ git add src/core/generator.ts tests/core/generator.test.ts
1282
+ git commit -m "feat: add document generator with prompts and concurrent execution"
1283
+ ```
1284
+
1285
+ ---
1286
+
1287
+ ## Task 8: Platform Provider Interface + GitHub
1288
+
1289
+ **Files:**
1290
+ - Create: `src/providers/interface.ts`
1291
+ - Create: `src/providers/github.ts`
1292
+ - Create: `tests/providers/github.test.ts`
1293
+
1294
+ - [ ] **Step 1: Write the failing test**
1295
+
1296
+ ```typescript
1297
+ // tests/providers/github.test.ts
1298
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
1299
+ import { GitHubProvider } from '../../src/providers/github.js';
1300
+
1301
+ vi.mock('child_process', () => ({
1302
+ execSync: vi.fn().mockReturnValue('git@github.com:myorg/myrepo.git\n'),
1303
+ }));
1304
+
1305
+ const mockOctokit = {
1306
+ pulls: {
1307
+ list: vi.fn().mockResolvedValue({ data: [] }),
1308
+ create: vi.fn().mockResolvedValue({
1309
+ data: { html_url: 'https://github.com/myorg/myrepo/pull/42', number: 42 },
1310
+ }),
1311
+ update: vi.fn().mockResolvedValue({
1312
+ data: { html_url: 'https://github.com/myorg/myrepo/pull/1' },
1313
+ }),
1314
+ },
1315
+ };
1316
+
1317
+ vi.mock('@octokit/rest', () => ({
1318
+ Octokit: vi.fn().mockImplementation(() => mockOctokit),
1319
+ }));
1320
+
1321
+ describe('GitHubProvider', () => {
1322
+ beforeEach(() => vi.clearAllMocks());
1323
+
1324
+ it('parses owner and repo from SSH remote URL', () => {
1325
+ expect(() => new GitHubProvider('token')).not.toThrow();
1326
+ });
1327
+
1328
+ it('creates a PR and returns its URL', async () => {
1329
+ const provider = new GitHubProvider('token');
1330
+ const url = await provider.createOrUpdatePR({
1331
+ branch: 'harness-docs/v1.1.0',
1332
+ title: 'docs: update for v1.1.0',
1333
+ body: 'Auto-generated docs update',
1334
+ baseBranch: 'main',
1335
+ });
1336
+ expect(url).toBe('https://github.com/myorg/myrepo/pull/42');
1337
+ expect(mockOctokit.pulls.create).toHaveBeenCalledWith(
1338
+ expect.objectContaining({
1339
+ head: 'harness-docs/v1.1.0',
1340
+ base: 'main',
1341
+ })
1342
+ );
1343
+ });
1344
+
1345
+ it('updates existing PR if one already exists for the branch', async () => {
1346
+ mockOctokit.pulls.list.mockResolvedValueOnce({
1347
+ data: [{ number: 1, html_url: 'https://github.com/myorg/myrepo/pull/1' }],
1348
+ });
1349
+ const provider = new GitHubProvider('token');
1350
+ const url = await provider.createOrUpdatePR({
1351
+ branch: 'harness-docs/v1.1.0',
1352
+ title: 'docs: update for v1.1.0',
1353
+ body: 'Updated',
1354
+ baseBranch: 'main',
1355
+ });
1356
+ expect(mockOctokit.pulls.update).toHaveBeenCalled();
1357
+ expect(mockOctokit.pulls.create).not.toHaveBeenCalled();
1358
+ expect(url).toBe('https://github.com/myorg/myrepo/pull/1');
1359
+ });
1360
+
1361
+ it('throws on unrecognized remote URL', () => {
1362
+ const { execSync } = vi.mocked(await import('child_process'));
1363
+ execSync.mockReturnValueOnce('https://not-github.com/something\n');
1364
+ expect(() => new GitHubProvider('token')).toThrow('Cannot parse GitHub remote URL');
1365
+ });
1366
+ });
1367
+ ```
1368
+
1369
+ - [ ] **Step 2: Run test to verify it fails**
1370
+
1371
+ ```bash
1372
+ npm test -- tests/providers/github.test.ts
1373
+ ```
1374
+
1375
+ Expected: FAIL — `Cannot find module '../../src/providers/github.js'`
1376
+
1377
+ - [ ] **Step 3: Create src/providers/interface.ts**
1378
+
1379
+ ```typescript
1380
+ // src/providers/interface.ts
1381
+ export interface PlatformProvider {
1382
+ createOrUpdatePR(opts: {
1383
+ branch: string;
1384
+ title: string;
1385
+ body: string;
1386
+ baseBranch: string;
1387
+ }): Promise<string>; // returns PR/MR URL
1388
+ }
1389
+ ```
1390
+
1391
+ - [ ] **Step 4: Create src/providers/github.ts**
1392
+
1393
+ Note: The branch is already pushed by `cli.ts` via `git push`. This provider only creates/updates the PR via the GitHub API — it does NOT manage git refs.
1394
+
1395
+ ```typescript
1396
+ // src/providers/github.ts
1397
+ import { Octokit } from '@octokit/rest';
1398
+ import { execSync } from 'child_process';
1399
+ import type { PlatformProvider } from './interface.js';
1400
+
1401
+ export class GitHubProvider implements PlatformProvider {
1402
+ private octokit: Octokit;
1403
+ private owner: string;
1404
+ private repo: string;
1405
+
1406
+ constructor(token: string) {
1407
+ this.octokit = new Octokit({ auth: token });
1408
+ const remoteUrl = execSync('git remote get-url origin').toString().trim();
1409
+ const match = remoteUrl.match(/github\.com[/:](.+?)\/(.+?)(?:\.git)?$/);
1410
+ if (!match) throw new Error(`Cannot parse GitHub remote URL: ${remoteUrl}`);
1411
+ this.owner = match[1];
1412
+ this.repo = match[2];
1413
+ }
1414
+
1415
+ async createOrUpdatePR(opts: {
1416
+ branch: string;
1417
+ title: string;
1418
+ body: string;
1419
+ baseBranch: string;
1420
+ }): Promise<string> {
1421
+ // Check for an existing open PR for this branch
1422
+ const { data: existingPRs } = await this.octokit.pulls.list({
1423
+ owner: this.owner,
1424
+ repo: this.repo,
1425
+ head: `${this.owner}:${opts.branch}`,
1426
+ state: 'open',
1427
+ });
1428
+
1429
+ if (existingPRs.length > 0) {
1430
+ const { data: pr } = await this.octokit.pulls.update({
1431
+ owner: this.owner,
1432
+ repo: this.repo,
1433
+ pull_number: existingPRs[0].number,
1434
+ title: opts.title,
1435
+ body: opts.body,
1436
+ });
1437
+ return pr.html_url;
1438
+ }
1439
+
1440
+ const { data: pr } = await this.octokit.pulls.create({
1441
+ owner: this.owner,
1442
+ repo: this.repo,
1443
+ title: opts.title,
1444
+ body: opts.body,
1445
+ head: opts.branch,
1446
+ base: opts.baseBranch,
1447
+ });
1448
+ return pr.html_url;
1449
+ }
1450
+ }
1451
+ ```
1452
+
1453
+ - [ ] **Step 5: Run test to verify it passes**
1454
+
1455
+ ```bash
1456
+ npm test -- tests/providers/github.test.ts
1457
+ ```
1458
+
1459
+ Expected: PASS
1460
+
1461
+ - [ ] **Step 6: Commit**
1462
+
1463
+ ```bash
1464
+ git add src/providers/interface.ts src/providers/github.ts tests/providers/github.test.ts
1465
+ git commit -m "feat: add platform provider interface and GitHub implementation"
1466
+ ```
1467
+
1468
+ ---
1469
+
1470
+ ## Task 9: GitLab Stub
1471
+
1472
+ **Files:**
1473
+ - Create: `src/providers/gitlab.ts`
1474
+
1475
+ - [ ] **Step 1: Create src/providers/gitlab.ts**
1476
+
1477
+ ```typescript
1478
+ // src/providers/gitlab.ts
1479
+ import type { PlatformProvider } from './interface.js';
1480
+
1481
+ export class GitLabProvider implements PlatformProvider {
1482
+ async createOrUpdatePR(_opts: {
1483
+ branch: string;
1484
+ title: string;
1485
+ body: string;
1486
+ baseBranch: string;
1487
+ }): Promise<string> {
1488
+ throw new Error(
1489
+ 'GitLab provider is not yet implemented. ' +
1490
+ 'Contributions welcome: https://github.com/your-org/harness-engineering-auto-docs'
1491
+ );
1492
+ }
1493
+ }
1494
+ ```
1495
+
1496
+ - [ ] **Step 2: Commit**
1497
+
1498
+ ```bash
1499
+ git add src/providers/gitlab.ts
1500
+ git commit -m "feat: add GitLab provider stub"
1501
+ ```
1502
+
1503
+ ---
1504
+
1505
+ ## Task 10: CLI Entry Point
1506
+
1507
+ **Files:**
1508
+ - Create: `src/cli.ts`
1509
+ - Create: `tests/integration/generate.test.ts`
1510
+
1511
+ - [ ] **Step 1: Write the integration test**
1512
+
1513
+ ```typescript
1514
+ // tests/integration/generate.test.ts
1515
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
1516
+ import { mkdirSync, rmSync, existsSync } from 'fs';
1517
+ import { join } from 'path';
1518
+ import { generateDocs, writeResults } from '../../src/core/generator.js';
1519
+ import { selectTargets } from '../../src/core/relevance.js';
1520
+ import { parseChangedFiles, groupFiles } from '../../src/core/diff.js';
1521
+ import { readFileSync } from 'fs';
1522
+ import type { AIProvider } from '../../src/ai/interface.js';
1523
+
1524
+ const TMP = join(import.meta.dirname, '../../tmp-integration-test');
1525
+ const FIXTURE = readFileSync(
1526
+ join(import.meta.dirname, '../fixtures/diff-schema.txt'), 'utf-8'
1527
+ );
1528
+
1529
+ beforeEach(() => mkdirSync(TMP, { recursive: true }));
1530
+ afterEach(() => rmSync(TMP, { recursive: true, force: true }));
1531
+
1532
+ describe('full pipeline: diff → relevance → generate → write', () => {
1533
+ it('generates and writes expected documents for schema diff', async () => {
1534
+ const changedFiles = parseChangedFiles(FIXTURE);
1535
+ const fileGroups = groupFiles(changedFiles);
1536
+ const targets = selectTargets(fileGroups, changedFiles);
1537
+
1538
+ expect(targets).toContain('db-schema');
1539
+ expect(targets).toContain('AGENTS.md');
1540
+
1541
+ const mockAI: AIProvider = {
1542
+ generate: vi.fn().mockResolvedValue('Auto-generated documentation content'),
1543
+ };
1544
+
1545
+ const diff = {
1546
+ raw: FIXTURE,
1547
+ prevTag: 'v1.0.0',
1548
+ currentTag: 'v1.1.0',
1549
+ changedFiles,
1550
+ fileGroups,
1551
+ };
1552
+
1553
+ const results = await generateDocs(mockAI, diff, targets);
1554
+ const written = writeResults(results, diff, TMP);
1555
+
1556
+ expect(written.length).toBeGreaterThan(0);
1557
+ expect(existsSync(`${TMP}/AGENTS.md`)).toBe(true);
1558
+ expect(existsSync(`${TMP}/docs/generated/db-schema.md`)).toBe(true);
1559
+ expect(existsSync(`${TMP}/changelog/v1.1.0.md`)).toBe(true);
1560
+ expect(existsSync(`${TMP}/docs/design-docs/v1.1.0.md`)).toBe(true);
1561
+ });
1562
+ });
1563
+ ```
1564
+
1565
+ - [ ] **Step 2: Run integration test to verify it passes**
1566
+
1567
+ ```bash
1568
+ npm test -- tests/integration/generate.test.ts
1569
+ ```
1570
+
1571
+ Expected: PASS (this tests the core pipeline, mocking AI)
1572
+
1573
+ - [ ] **Step 3: Create src/cli.ts**
1574
+
1575
+ ```typescript
1576
+ #!/usr/bin/env node
1577
+ // src/cli.ts
1578
+ import { execSync } from 'child_process';
1579
+ import { extractDiff } from './core/diff.js';
1580
+ import { selectTargets } from './core/relevance.js';
1581
+ import { generateDocs, writeResults } from './core/generator.js';
1582
+ import { GitHubProvider } from './providers/github.js';
1583
+ import { GitLabProvider } from './providers/gitlab.js';
1584
+ import { AnthropicProvider } from './ai/anthropic.js';
1585
+ import { OpenAIProvider } from './ai/openai.js';
1586
+ import type { AIProvider } from './ai/interface.js';
1587
+ import type { PlatformProvider } from './providers/interface.js';
1588
+
1589
+ function requireEnv(name: string): string {
1590
+ const value = process.env[name];
1591
+ if (!value) {
1592
+ console.error(`Error: ${name} environment variable is required`);
1593
+ process.exit(1);
1594
+ }
1595
+ return value;
1596
+ }
1597
+
1598
+ function detectPlatform(): PlatformProvider {
1599
+ if (process.env.GITHUB_ACTIONS) {
1600
+ return new GitHubProvider(requireEnv('GITHUB_TOKEN'));
1601
+ }
1602
+ if (process.env.GITLAB_CI) {
1603
+ return new GitLabProvider();
1604
+ }
1605
+ // Default to GitHub for local runs
1606
+ const token = process.env.GITHUB_TOKEN;
1607
+ if (!token) {
1608
+ console.error('Error: GITHUB_TOKEN is required (or run in GITHUB_ACTIONS / GITLAB_CI)');
1609
+ process.exit(1);
1610
+ }
1611
+ return new GitHubProvider(token);
1612
+ }
1613
+
1614
+ function selectAI(model: string, apiKey: string): AIProvider {
1615
+ if (model.startsWith('claude-')) return new AnthropicProvider(apiKey, model);
1616
+ if (model.startsWith('gpt-')) return new OpenAIProvider(apiKey, model);
1617
+ console.error(`Error: Unknown model "${model}". Use a claude-* or gpt-* model.`);
1618
+ process.exit(1);
1619
+ }
1620
+
1621
+ function getDefaultBranch(): string {
1622
+ try {
1623
+ return execSync('git symbolic-ref refs/remotes/origin/HEAD --short')
1624
+ .toString().trim().replace('origin/', '');
1625
+ } catch {
1626
+ return 'main';
1627
+ }
1628
+ }
1629
+
1630
+ async function main() {
1631
+ const model = requireEnv('AI_MODEL');
1632
+ const apiKey = requireEnv('AI_API_KEY');
1633
+
1634
+ const diff = extractDiff();
1635
+
1636
+ if (!diff.raw.trim()) {
1637
+ console.log('No diff between tags. Nothing to document.');
1638
+ process.exit(0);
1639
+ }
1640
+
1641
+ console.log(`Generating docs: ${diff.prevTag} → ${diff.currentTag}`);
1642
+
1643
+ const ai = selectAI(model, apiKey);
1644
+ const targets = selectTargets(diff.fileGroups, diff.changedFiles);
1645
+ console.log(`Updating ${targets.length} documents: ${targets.join(', ')}`);
1646
+
1647
+ const results = await generateDocs(ai, diff, targets);
1648
+
1649
+ const failures = results.filter(r => r.error);
1650
+ if (failures.length > 0) {
1651
+ console.warn(`\nWarning: ${failures.length} document(s) failed:`);
1652
+ failures.forEach(f => console.warn(` - ${f.target}: ${f.error}`));
1653
+ }
1654
+
1655
+ const cwd = process.cwd();
1656
+ const writtenFiles = writeResults(results.filter(r => !r.error), diff, cwd);
1657
+ console.log(`\nWrote ${writtenFiles.length} files.`);
1658
+
1659
+ if (writtenFiles.length === 0) {
1660
+ console.log('No files written. Skipping PR creation.');
1661
+ process.exit(0);
1662
+ }
1663
+
1664
+ // Git: create branch, commit, push
1665
+ const branch = `harness-docs/${diff.currentTag}`;
1666
+ const baseBranch = getDefaultBranch();
1667
+
1668
+ execSync(`git checkout -b ${branch}`);
1669
+ execSync(`git add ${writtenFiles.map(f => `"${f}"`).join(' ')}`);
1670
+ execSync(
1671
+ `git commit -m "docs: Harness Engineering docs update for ${diff.currentTag}"`,
1672
+ { env: { ...process.env, GIT_AUTHOR_NAME: 'harness-engineering-auto-docs', GIT_AUTHOR_EMAIL: 'bot@harness-engineering-auto-docs' } }
1673
+ );
1674
+ execSync(`git push -u origin ${branch}`);
1675
+
1676
+ // Create PR
1677
+ const platform = detectPlatform();
1678
+ const prUrl = await platform.createOrUpdatePR({
1679
+ branch,
1680
+ title: `docs: Harness Engineering docs for ${diff.currentTag}`,
1681
+ body: [
1682
+ `Auto-generated by [harness-engineering-auto-docs](https://github.com/your-org/harness-engineering-auto-docs).`,
1683
+ ``,
1684
+ `Updates documentation based on changes from **${diff.prevTag}** to **${diff.currentTag}**.`,
1685
+ ``,
1686
+ `**Updated files:**`,
1687
+ writtenFiles.map(f => `- \`${f.replace(cwd + '/', '')}\``).join('\n'),
1688
+ ].join('\n'),
1689
+ baseBranch,
1690
+ });
1691
+
1692
+ console.log(`\nPR created: ${prUrl}`);
1693
+ }
1694
+
1695
+ main().catch(err => {
1696
+ console.error('Fatal:', err);
1697
+ process.exit(1);
1698
+ });
1699
+ ```
1700
+
1701
+ - [ ] **Step 4: Run all tests**
1702
+
1703
+ ```bash
1704
+ npm test
1705
+ ```
1706
+
1707
+ Expected: All tests PASS
1708
+
1709
+ - [ ] **Step 5: Commit**
1710
+
1711
+ ```bash
1712
+ git add src/cli.ts tests/integration/generate.test.ts
1713
+ git commit -m "feat: add CLI entry point"
1714
+ ```
1715
+
1716
+ ---
1717
+
1718
+ ## Task 11: Example Workflow + Build
1719
+
1720
+ **Files:**
1721
+ - Create: `examples/github-workflow.yml`
1722
+ - Modify: `README.md`
1723
+
1724
+ - [ ] **Step 1: Create examples/github-workflow.yml**
1725
+
1726
+ ```yaml
1727
+ # examples/github-workflow.yml
1728
+ # Copy this file to .github/workflows/harness-docs.yml in your target project.
1729
+
1730
+ name: Harness Engineering Docs
1731
+
1732
+ on:
1733
+ push:
1734
+ tags:
1735
+ - 'v*'
1736
+
1737
+ jobs:
1738
+ update-docs:
1739
+ runs-on: ubuntu-latest
1740
+ permissions:
1741
+ contents: write
1742
+ pull-requests: write
1743
+
1744
+ steps:
1745
+ - uses: actions/checkout@v4
1746
+ with:
1747
+ fetch-depth: 0 # Required: fetch all tags for diff
1748
+
1749
+ - uses: actions/setup-node@v4
1750
+ with:
1751
+ node-version: '20'
1752
+
1753
+ - name: Generate and PR Harness Engineering docs
1754
+ run: npx harness-engineering-auto-docs@latest
1755
+ env:
1756
+ AI_MODEL: claude-sonnet-4-6 # or gpt-4o
1757
+ AI_API_KEY: ${{ secrets.AI_API_KEY }}
1758
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1759
+ ```
1760
+
1761
+ - [ ] **Step 2: Write README.md**
1762
+
1763
+ ```markdown
1764
+ # harness-engineering-auto-docs
1765
+
1766
+ Auto-generate [Harness Engineering](https://openai.com/research/harness-engineering) style documentation when your project creates a git tag. Opens a PR with updated docs.
1767
+
1768
+ ## Usage
1769
+
1770
+ Add `.github/workflows/harness-docs.yml` to your project (see `examples/github-workflow.yml`), then set:
1771
+
1772
+ - `AI_API_KEY` — your Anthropic or OpenAI API key (repository secret)
1773
+ - `AI_MODEL` — model name, e.g. `claude-sonnet-4-6` or `gpt-4o`
1774
+ - `GITHUB_TOKEN` — provided automatically by GitHub Actions
1775
+
1776
+ When you push a tag (`git tag v1.2.0 && git push --tags`), a PR is automatically opened with updated documentation.
1777
+
1778
+ ## What gets updated
1779
+
1780
+ | File | Always | Conditional |
1781
+ |------|--------|-------------|
1782
+ | `AGENTS.md` | ✓ | |
1783
+ | `ARCHITECTURE.md` | ✓ | |
1784
+ | `DESIGN.md` | ✓ | |
1785
+ | `QUALITY_SCORE.md` | ✓ | |
1786
+ | `changelog/vX.Y.Z.md` | ✓ | |
1787
+ | `docs/design-docs/vX.Y.Z.md` | ✓ | |
1788
+ | `docs/design-docs/index.md` | ✓ | |
1789
+ | `docs/exec-plans/tech-debt-tracker.md` | ✓ | |
1790
+ | `FRONTEND.md` | | frontend files changed |
1791
+ | `SECURITY.md` | | auth/security files changed |
1792
+ | `RELIABILITY.md` | | infra files changed |
1793
+ | `docs/generated/db-schema.md` | | SQL/schema files changed |
1794
+ | `docs/product-specs/index.md` | | new features detected |
1795
+ | `docs/references/` | | new dependencies added |
1796
+
1797
+ ## Local run
1798
+
1799
+ ```bash
1800
+ AI_MODEL=claude-sonnet-4-6 AI_API_KEY=sk-ant-... GITHUB_TOKEN=ghp_... npx harness-engineering-auto-docs
1801
+ ```
1802
+
1803
+ ## Supported models
1804
+
1805
+ - Claude: `claude-sonnet-4-6`, `claude-opus-4-6`, `claude-haiku-4-5-20251001`
1806
+ - OpenAI: `gpt-4o`, `gpt-4o-mini`, `o3`
1807
+
1808
+ ## Future
1809
+
1810
+ - GitLab support (MR creation)
1811
+ ```
1812
+
1813
+ - [ ] **Step 3: Build the project**
1814
+
1815
+ ```bash
1816
+ npm run build
1817
+ ```
1818
+
1819
+ Expected: `dist/` directory created with `cli.js` and all compiled files, no TypeScript errors.
1820
+
1821
+ - [ ] **Step 4: Verify CLI is executable**
1822
+
1823
+ ```bash
1824
+ node dist/cli.js 2>&1 | head -5
1825
+ ```
1826
+
1827
+ Expected: Output starts with `Error: AI_MODEL environment variable is required` — confirms the CLI loads correctly.
1828
+
1829
+ - [ ] **Step 5: Commit**
1830
+
1831
+ ```bash
1832
+ git add examples/github-workflow.yml README.md dist/
1833
+ git commit -m "feat: add example workflow, README, and build output"
1834
+ ```
1835
+
1836
+ ---
1837
+
1838
+ ## Task 12: Full Test Suite
1839
+
1840
+ - [ ] **Step 1: Run complete test suite**
1841
+
1842
+ ```bash
1843
+ npm test
1844
+ ```
1845
+
1846
+ Expected: All tests PASS, zero failures.
1847
+
1848
+ - [ ] **Step 2: Verify test count**
1849
+
1850
+ ```bash
1851
+ npm test -- --reporter=verbose 2>&1 | tail -20
1852
+ ```
1853
+
1854
+ Expected: At minimum 20 tests across all files, all green.
1855
+
1856
+ - [ ] **Step 3: Final commit**
1857
+
1858
+ ```bash
1859
+ git add -A
1860
+ git status
1861
+ # Verify nothing unexpected is staged
1862
+ git commit -m "chore: final cleanup and verified test suite"
1863
+ ```