happy-stacks 0.6.12 → 0.6.13

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 (59) hide show
  1. package/docs/commit-audits/happy/_tools/generate-plans.mjs +453 -0
  2. package/docs/commit-audits/happy/_tools/generate-pr-assignment.mjs +430 -0
  3. package/docs/commit-audits/happy/_tools/init-pr-assignment-working.mjs +107 -0
  4. package/docs/commit-audits/happy/leeroy-wip.commit-analysis.md +1849 -0
  5. package/docs/commit-audits/happy/leeroy-wip.commit-export.fuller-stat.md +747 -1
  6. package/docs/commit-audits/happy/leeroy-wip.commit-index.json +11740 -0
  7. package/docs/commit-audits/happy/leeroy-wip.commit-index.tsv +252 -0
  8. package/docs/commit-audits/happy/leeroy-wip.commit-inventory.md +18 -11
  9. package/docs/commit-audits/happy/leeroy-wip.commit-manual-review.md +1236 -92
  10. package/docs/commit-audits/happy/leeroy-wip.maintainers-overview.draft.md +448 -0
  11. package/docs/commit-audits/happy/leeroy-wip.pr-assignment.draft.tsv +252 -0
  12. package/docs/commit-audits/happy/leeroy-wip.pr-assignment.working.tsv +288 -0
  13. package/docs/commit-audits/happy/leeroy-wip.pr-catalog.draft.md +245 -0
  14. package/docs/commit-audits/happy/leeroy-wip.pr-stack-plan.draft.md +350 -0
  15. package/docs/commit-audits/happy/leeroy-wip.rewrite-deferred-fragments.tsv +65 -0
  16. package/docs/commit-audits/happy/leeroy-wip.rewrite-ledger.tsv +56 -0
  17. package/docs/commit-audits/happy/leeroy-wip.rewrite-process.md +240 -0
  18. package/docs/commit-audits/happy/leeroy-wip.rewrite-status.tsv +39 -0
  19. package/docs/commit-audits/happy/leeroy-wip.split-plan.draft.md +93 -0
  20. package/docs/commit-audits/happy/leeroy-wip.topic-buckets.md +76 -0
  21. package/docs/commit-audits/happy/pr-desc.extraction-ledger.tsv +279 -0
  22. package/docs/commit-audits/happy/pr-desc.original.md +0 -0
  23. package/docs/commit-audits/happy/pr-desc.post-audit-extraction-ledger.tsv +54 -0
  24. package/docs/commit-audits/happy/pr-desc.working-document.md +536 -0
  25. package/docs/happy-development.md +18 -1
  26. package/docs/isolated-linux-vm.md +23 -1
  27. package/docs/stacks.md +21 -1
  28. package/package.json +1 -1
  29. package/scripts/auth.mjs +46 -8
  30. package/scripts/daemon.mjs +44 -21
  31. package/scripts/doctor.mjs +2 -2
  32. package/scripts/doctor_cmd.test.mjs +67 -0
  33. package/scripts/happy.mjs +18 -5
  34. package/scripts/provision/linux-ubuntu-review-pr.sh +5 -1
  35. package/scripts/provision/macos-lima-happy-vm.sh +34 -2
  36. package/scripts/review.mjs +347 -124
  37. package/scripts/review_pr.mjs +78 -2
  38. package/scripts/run.mjs +2 -1
  39. package/scripts/stack.mjs +265 -19
  40. package/scripts/stack_daemon_cmd.test.mjs +196 -0
  41. package/scripts/stack_happy_cmd.test.mjs +103 -0
  42. package/scripts/utils/cli/prereqs.mjs +12 -1
  43. package/scripts/utils/dev/daemon.mjs +3 -1
  44. package/scripts/utils/proc/pm.mjs +1 -1
  45. package/scripts/utils/review/detached_worktree.mjs +61 -0
  46. package/scripts/utils/review/detached_worktree.test.mjs +62 -0
  47. package/scripts/utils/review/findings.mjs +133 -20
  48. package/scripts/utils/review/findings.test.mjs +88 -1
  49. package/scripts/utils/review/runners/augment.mjs +71 -0
  50. package/scripts/utils/review/runners/augment.test.mjs +42 -0
  51. package/scripts/utils/review/runners/coderabbit.mjs +54 -10
  52. package/scripts/utils/review/runners/coderabbit.test.mjs +15 -48
  53. package/scripts/utils/review/sliced_runner.mjs +39 -0
  54. package/scripts/utils/review/sliced_runner.test.mjs +47 -0
  55. package/scripts/utils/review/tool_home_seed.mjs +99 -0
  56. package/scripts/utils/review/tool_home_seed.test.mjs +113 -0
  57. package/scripts/utils/stack/cli_identities.mjs +29 -0
  58. package/scripts/utils/stack/startup.mjs +45 -7
  59. package/scripts/worktrees.mjs +8 -5
@@ -12,16 +12,20 @@ import { createHeadSliceCommits, getChangedOps } from './utils/review/head_slice
12
12
  import { runWithConcurrencyLimit } from './utils/proc/parallel.mjs';
13
13
  import { runCodeRabbitReview } from './utils/review/runners/coderabbit.mjs';
14
14
  import { extractCodexReviewFromJsonl, runCodexReview } from './utils/review/runners/codex.mjs';
15
+ import { detectAugmentAuthError, runAugmentReview } from './utils/review/runners/augment.mjs';
15
16
  import { formatTriageMarkdown, parseCodeRabbitPlainOutput, parseCodexReviewText } from './utils/review/findings.mjs';
17
+ import { runSlicedJobs } from './utils/review/sliced_runner.mjs';
18
+ import { seedAugmentHomeFromRealHome, seedCodeRabbitHomeFromRealHome, seedCodexHomeFromRealHome } from './utils/review/tool_home_seed.mjs';
16
19
  import { join } from 'node:path';
17
20
  import { ensureDir } from './utils/fs/ops.mjs';
18
21
  import { copyFile, writeFile } from 'node:fs/promises';
19
22
  import { existsSync } from 'node:fs';
20
23
  import { runCapture } from './utils/proc/proc.mjs';
24
+ import { withDetachedWorktree } from './utils/review/detached_worktree.mjs';
21
25
 
22
26
  const DEFAULT_COMPONENTS = ['happy', 'happy-cli', 'happy-server-light', 'happy-server'];
23
27
  const VALID_COMPONENTS = DEFAULT_COMPONENTS;
24
- const VALID_REVIEWERS = ['coderabbit', 'codex'];
28
+ const VALID_REVIEWERS = ['coderabbit', 'codex', 'augment'];
25
29
  const VALID_DEPTHS = ['deep', 'normal'];
26
30
  const DEFAULT_REVIEW_MAX_FILES = 50;
27
31
 
@@ -42,7 +46,7 @@ function normalizeReviewers(list) {
42
46
  function usage() {
43
47
  return [
44
48
  '[review] usage:',
45
- ' happys review [component...] [--reviewers=coderabbit,codex] [--base-remote=<remote>] [--base-branch=<branch>] [--base-ref=<ref>] [--concurrency=N] [--depth=deep|normal] [--chunks|--no-chunks] [--chunking=auto|head-slice|commit-window] [--chunk-max-files=N] [--coderabbit-type=committed|uncommitted|all] [--coderabbit-max-files=N] [--coderabbit-chunks|--no-coderabbit-chunks] [--codex-chunks|--no-codex-chunks] [--run-label=<label>] [--no-stream] [--json]',
49
+ ' happys review [component...] [--reviewers=coderabbit,codex,augment] [--base-remote=<remote>] [--base-branch=<branch>] [--base-ref=<ref>] [--concurrency=N] [--depth=deep|normal] [--chunks|--no-chunks] [--chunking=auto|head-slice|commit-window] [--chunk-max-files=N] [--coderabbit-type=committed|uncommitted|all] [--coderabbit-max-files=N] [--coderabbit-chunks|--no-coderabbit-chunks] [--codex-chunks|--no-codex-chunks] [--augment-chunks|--no-augment-chunks] [--augment-model=<id>] [--augment-max-turns=N] [--run-label=<label>] [--no-stream] [--json]',
46
50
  '',
47
51
  'components:',
48
52
  ` ${VALID_COMPONENTS.join(' | ')}`,
@@ -90,6 +94,16 @@ function tailLines(text, n) {
90
94
  return lines;
91
95
  }
92
96
 
97
+ function detectCodeRabbitAuthError({ stdout, stderr }) {
98
+ const combined = `${stdout ?? ''}\n${stderr ?? ''}`;
99
+ return combined.includes('Authentication required') && combined.includes("coderabbit auth login");
100
+ }
101
+
102
+ function detectCodexUsageLimit({ stdout, stderr }) {
103
+ const combined = `${stdout ?? ''}\n${stderr ?? ''}`.toLowerCase();
104
+ return combined.includes('usage limit') || combined.includes('http 429') || combined.includes('status code: 429');
105
+ }
106
+
93
107
  function printReviewOperatorGuidance() {
94
108
  // Guidance for the human/LLM running the review (not the reviewer model itself).
95
109
  // eslint-disable-next-line no-console
@@ -153,6 +167,35 @@ function buildCodexDeepPrompt({ component, baseRef }) {
153
167
  ].join('\n');
154
168
  }
155
169
 
170
+ function buildCodexMonorepoDeepPrompt({ baseRef }) {
171
+ const diffCmd = `cd \"$(git rev-parse --show-toplevel)\" && git diff ${baseRef}...HEAD`;
172
+ return [
173
+ 'Run a deep, long-form code review on the monorepo.',
174
+ '',
175
+ `Base for review: ${baseRef}`,
176
+ 'Scope: full repo',
177
+ '',
178
+ 'Instructions:',
179
+ `- Use: ${diffCmd}`,
180
+ '- You may inspect any file in the repo for cross-references (server/cli/ui).',
181
+ '- Focus on correctness, edge cases, reliability, performance, and security.',
182
+ '- Prefer unified/coherent fixes; avoid duplication.',
183
+ '- Avoid brittle tests that assert on wording/phrasing/config; test real behavior and observable outcomes.',
184
+ '- Ensure i18n coverage is complete: do not introduce hardcoded user-visible strings; add translation keys across locales as needed.',
185
+ '- Treat every recommendation as a suggestion: validate it against best practices and this codebase’s existing patterns. Do not propose changes that violate project invariants.',
186
+ '- Be exhaustive: list all findings you notice, not only the highest-signal ones.',
187
+ '- Clearly mark any item that is uncertain, has tradeoffs, or needs product/UX decisions as "needs discussion".',
188
+ '',
189
+ 'Output format:',
190
+ '- Start with a short overall verdict.',
191
+ '- Then list findings as bullets with severity (blocker/major/minor/nit) and a concrete fix suggestion.',
192
+ '',
193
+ 'Machine-readable output (required):',
194
+ '- After your review, output a JSON array of findings preceded by a line containing exactly: ===FINDINGS_JSON===',
195
+ '- Each finding should include: severity, file, (optional) lines, title, description, recommendation, needsDiscussion (boolean).',
196
+ ].join('\n');
197
+ }
198
+
156
199
  function buildCodexMonorepoSlicePrompt({ sliceLabel, baseCommit, baseRef }) {
157
200
  const diffCmd = `cd \"$(git rev-parse --show-toplevel)\" && git diff ${baseCommit}...HEAD`;
158
201
  return [
@@ -215,32 +258,6 @@ async function listCommitsBetween({ cwd, base, head, env }) {
215
258
  return await gitLines({ cwd, env, args: ['rev-list', '--reverse', `${base}..${head}`] });
216
259
  }
217
260
 
218
- async function withDetachedWorktree({ repoDir, headCommit, label, env }, fn) {
219
- const root = (await runCapture('git', ['rev-parse', '--show-toplevel'], { cwd: repoDir, env })).toString().trim();
220
- if (!root) throw new Error('[review] failed to resolve git toplevel');
221
-
222
- const safeLabel = String(label ?? 'worktree')
223
- .toLowerCase()
224
- .replace(/[^a-z0-9._-]+/g, '-')
225
- .replace(/^-+|-+$/g, '');
226
- const short = String(headCommit).slice(0, 12);
227
- const dir = join(root, '.project', 'review-worktrees', `${safeLabel}-${short}`);
228
-
229
- await ensureDir(join(root, '.project', 'review-worktrees'));
230
-
231
- try {
232
- await runCapture('git', ['worktree', 'add', '--detach', dir, headCommit], { cwd: repoDir, env });
233
- return await fn(dir);
234
- } finally {
235
- try {
236
- await runCapture('git', ['worktree', 'remove', '--force', dir], { cwd: repoDir, env });
237
- await runCapture('git', ['worktree', 'prune'], { cwd: repoDir, env });
238
- } catch {
239
- // best-effort cleanup; leave an orphaned worktree if needed
240
- }
241
- }
242
- }
243
-
244
261
  async function pickCoderabbitBaseCommitForMaxFiles({ cwd, baseRef, maxFiles, env }) {
245
262
  const commits = await gitLines({ cwd, env, args: ['rev-list', '--reverse', `${baseRef}..HEAD`] });
246
263
  if (!commits.length) return null;
@@ -297,6 +314,7 @@ async function main() {
297
314
  git: true,
298
315
  coderabbit: reviewers.includes('coderabbit'),
299
316
  codex: reviewers.includes('codex'),
317
+ augment: reviewers.includes('augment'),
300
318
  });
301
319
 
302
320
  const inferred = positionals.length === 0 ? resolveComponentFromCwdOrNull({ rootDir, invokedCwd }) : null;
@@ -336,6 +354,8 @@ async function main() {
336
354
  const depth = (kv.get('--depth') ?? 'deep').toString().trim().toLowerCase();
337
355
  const coderabbitType = (kv.get('--coderabbit-type') ?? 'committed').toString().trim().toLowerCase();
338
356
  const chunkingMode = (kv.get('--chunking') ?? 'auto').toString().trim().toLowerCase();
357
+ const augmentModelFlag = (kv.get('--augment-model') ?? '').toString().trim();
358
+ const augmentMaxTurnsFlag = (kv.get('--augment-max-turns') ?? '').toString().trim();
339
359
  const chunkMaxFilesRaw = (kv.get('--chunk-max-files') ?? '').toString().trim();
340
360
  const coderabbitMaxFilesRaw = (kv.get('--coderabbit-max-files') ?? '').toString().trim();
341
361
  const coderabbitMaxFiles = coderabbitMaxFilesRaw ? Number(coderabbitMaxFilesRaw) : DEFAULT_REVIEW_MAX_FILES;
@@ -347,6 +367,7 @@ async function main() {
347
367
  ? false
348
368
  : null;
349
369
  const codexChunksOverride = flags.has('--codex-chunks') ? true : flags.has('--no-codex-chunks') ? false : null;
370
+ const augmentChunksOverride = flags.has('--augment-chunks') ? true : flags.has('--no-augment-chunks') ? false : null;
350
371
  if (!VALID_DEPTHS.includes(depth)) {
351
372
  throw new Error(`[review] invalid --depth=${depth} (expected: ${VALID_DEPTHS.join(' | ')})`);
352
373
  }
@@ -354,6 +375,9 @@ async function main() {
354
375
  throw new Error('[review] invalid --chunking (expected: auto|head-slice|commit-window)');
355
376
  }
356
377
 
378
+ if (augmentModelFlag) process.env.HAPPY_STACKS_AUGMENT_MODEL = augmentModelFlag;
379
+ if (augmentMaxTurnsFlag) process.env.HAPPY_STACKS_AUGMENT_MAX_TURNS = augmentMaxTurnsFlag;
380
+
357
381
  const deepInstructionsPath = join(rootDir, 'scripts', 'utils', 'review', 'instructions', 'deep.md');
358
382
  const coderabbitConfigFiles = depth === 'deep' ? [deepInstructionsPath] : [];
359
383
 
@@ -363,6 +387,18 @@ async function main() {
363
387
  process.env[coderabbitHomeKey] = join(rootDir, '.project', 'coderabbit-home');
364
388
  }
365
389
  await ensureDir(process.env[coderabbitHomeKey]);
390
+
391
+ // Seed CodeRabbit auth/config into the isolated home dir so review runs can be non-interactive.
392
+ // We never print or inspect auth contents.
393
+ try {
394
+ const realHome = (process.env.HOME ?? '').toString().trim();
395
+ const overrideHome = (process.env[coderabbitHomeKey] ?? '').toString().trim();
396
+ if (realHome && overrideHome && realHome !== overrideHome) {
397
+ await seedCodeRabbitHomeFromRealHome({ realHomeDir: realHome, isolatedHomeDir: overrideHome });
398
+ }
399
+ } catch {
400
+ // ignore (coderabbit will surface auth issues if seeding fails)
401
+ }
366
402
  }
367
403
 
368
404
  if (reviewers.includes('codex')) {
@@ -382,18 +418,33 @@ async function main() {
382
418
  const realHome = (process.env.HOME ?? '').toString().trim();
383
419
  const overrideHome = process.env[codexHomeKey];
384
420
  if (realHome && overrideHome && realHome !== overrideHome) {
385
- const srcAuth = join(realHome, '.codex', 'auth.json');
386
- const srcCfg = join(realHome, '.codex', 'config.toml');
387
- const destAuth = join(overrideHome, 'auth.json');
388
- const destCfg = join(overrideHome, 'config.toml');
389
- if (existsSync(srcAuth) && !existsSync(destAuth)) await copyFile(srcAuth, destAuth);
390
- if (existsSync(srcCfg) && !existsSync(destCfg)) await copyFile(srcCfg, destCfg);
421
+ await seedCodexHomeFromRealHome({ realHomeDir: realHome, isolatedHomeDir: overrideHome });
391
422
  }
392
423
  } catch {
393
424
  // ignore (codex will surface auth issues if seeding fails)
394
425
  }
395
426
  }
396
427
 
428
+ if (reviewers.includes('augment')) {
429
+ const augmentHomeKey = 'HAPPY_STACKS_AUGMENT_CACHE_DIR';
430
+ if (!(process.env[augmentHomeKey] ?? '').toString().trim()) {
431
+ process.env[augmentHomeKey] = join(rootDir, '.project', 'augment-home');
432
+ }
433
+ await ensureDir(process.env[augmentHomeKey]);
434
+
435
+ // Seed Auggie auth/config into the isolated cache dir so review runs can be non-interactive.
436
+ // We never print or inspect auth contents.
437
+ try {
438
+ const realHome = (process.env.HOME ?? '').toString().trim();
439
+ const overrideHome = process.env[augmentHomeKey];
440
+ if (realHome && overrideHome && realHome !== overrideHome) {
441
+ await seedAugmentHomeFromRealHome({ realHomeDir: realHome, isolatedHomeDir: overrideHome });
442
+ }
443
+ } catch {
444
+ // ignore (auggie will surface auth issues if seeding fails)
445
+ }
446
+ }
447
+
397
448
  if (stream) {
398
449
  // eslint-disable-next-line no-console
399
450
  console.log('[review] note: this can take a long time (up to 60+ minutes per reviewer). No timeout is enforced.');
@@ -445,13 +496,17 @@ async function main() {
445
496
  });
446
497
 
447
498
  const maxFiles = Number.isFinite(chunkMaxFiles) && chunkMaxFiles > 0 ? chunkMaxFiles : 300;
499
+ const sliceConcurrency = Math.max(1, Math.floor(limit / Math.max(1, reviewers.length)));
448
500
  const wantChunksCoderabbit = coderabbitChunksOverride ?? globalChunks;
449
501
  const wantChunksCodex = codexChunksOverride ?? globalChunks;
502
+ const wantChunksAugment = augmentChunksOverride ?? globalChunks;
450
503
  const effectiveChunking = chunkingMode === 'auto' ? (monorepo ? 'head-slice' : 'commit-window') : chunkingMode;
451
504
 
452
505
  if (monorepo && stream) {
453
506
  // eslint-disable-next-line no-console
454
- console.log(`[review] monorepo detected at ${repoDir}; running a single unified review (chunking=${effectiveChunking}).`);
507
+ console.log(
508
+ `[review] monorepo detected at ${repoDir}; running a single unified review (chunking=${effectiveChunking}, concurrency=${sliceConcurrency}).`
509
+ );
455
510
  }
456
511
 
457
512
  const perReviewer = await Promise.all(
@@ -470,49 +525,58 @@ async function main() {
470
525
  const ops = await getChangedOps({ cwd: repoDir, baseRef: baseCommit, headRef: headCommit, env: process.env });
471
526
  const slices = planPathSlices({ changedPaths: Array.from(ops.all), maxFiles });
472
527
 
473
- const sliceResults = [];
474
- for (let i = 0; i < slices.length; i += 1) {
475
- const slice = slices[i];
476
- const logFile = join(runDir, 'raw', `coderabbit-slice-${i + 1}-of-${slices.length}-${sanitizeLabel(slice.label)}.log`);
477
- // eslint-disable-next-line no-await-in-loop
478
- const rr = await withDetachedWorktree(
479
- { repoDir, headCommit: baseCommit, label: `coderabbit-${i + 1}-of-${slices.length}`, env: process.env },
480
- async (worktreeDir) => {
481
- const { baseSliceCommit } = await createHeadSliceCommits({
482
- cwd: worktreeDir,
483
- env: process.env,
484
- baseRef: baseCommit,
485
- headCommit,
486
- ops,
487
- slicePaths: slice.paths,
488
- label: slice.label.replace(/\/+$/g, ''),
489
- });
490
- return await runCodeRabbitReview({
491
- repoDir: worktreeDir,
492
- baseRef: null,
493
- baseCommit: baseSliceCommit,
494
- env: process.env,
495
- type: coderabbitType,
496
- configFiles: coderabbitConfigFiles,
497
- streamLabel: stream ? `monorepo:coderabbit:${i + 1}/${slices.length}` : undefined,
498
- teeFile: logFile,
499
- teeLabel: `monorepo:coderabbit:${i + 1}/${slices.length}`,
500
- });
501
- }
502
- );
503
- sliceResults.push({
504
- index: i + 1,
505
- of: slices.length,
506
- slice: slice.label,
507
- fileCount: slice.paths.length,
508
- logFile,
509
- ok: Boolean(rr.ok),
510
- exitCode: rr.exitCode,
511
- signal: rr.signal,
512
- durationMs: rr.durationMs,
513
- stdout: rr.stdout ?? '',
514
- stderr: rr.stderr ?? '',
515
- });
528
+ const sliceItems = slices.map((slice, i) => ({ slice, index: i + 1, of: slices.length }));
529
+ const sliceResults = await runSlicedJobs({
530
+ items: sliceItems,
531
+ limit: sliceConcurrency,
532
+ run: async ({ slice, index, of }) => {
533
+ const logFile = join(runDir, 'raw', `coderabbit-slice-${index}-of-${of}-${sanitizeLabel(slice.label)}.log`);
534
+ const rr = await withDetachedWorktree(
535
+ { repoDir, headCommit: baseCommit, label: `coderabbit-${index}-of-${of}`, env: process.env },
536
+ async (worktreeDir) => {
537
+ const { baseSliceCommit } = await createHeadSliceCommits({
538
+ cwd: worktreeDir,
539
+ env: process.env,
540
+ baseRef: baseCommit,
541
+ headCommit,
542
+ ops,
543
+ slicePaths: slice.paths,
544
+ label: slice.label.replace(/\/+$/g, ''),
545
+ });
546
+ return await runCodeRabbitReview({
547
+ repoDir: worktreeDir,
548
+ baseRef: null,
549
+ baseCommit: baseSliceCommit,
550
+ env: process.env,
551
+ type: coderabbitType,
552
+ configFiles: coderabbitConfigFiles,
553
+ streamLabel: stream ? `monorepo:coderabbit:${index}/${of}` : undefined,
554
+ teeFile: logFile,
555
+ teeLabel: `monorepo:coderabbit:${index}/${of}`,
556
+ });
557
+ }
558
+ );
559
+ return {
560
+ index,
561
+ of,
562
+ slice: slice.label,
563
+ fileCount: slice.paths.length,
564
+ logFile,
565
+ ok: Boolean(rr.ok),
566
+ exitCode: rr.exitCode,
567
+ signal: rr.signal,
568
+ durationMs: rr.durationMs,
569
+ stdout: rr.stdout ?? '',
570
+ stderr: rr.stderr ?? '',
571
+ };
572
+ },
573
+ shouldAbortEarly: (r) => detectCodeRabbitAuthError({ stdout: r?.stdout, stderr: r?.stderr }),
574
+ });
575
+
576
+ if (sliceResults.length === 1 && detectCodeRabbitAuthError(sliceResults[0])) {
577
+ const msg = `[review] coderabbit auth required: run 'coderabbit auth login' in an interactive session, then re-run this review.`;
578
+ // eslint-disable-next-line no-console
579
+ console.error(msg);
516
580
  }
517
581
 
518
582
  const okAll = sliceResults.every((r) => r.ok);
@@ -658,51 +722,64 @@ async function main() {
658
722
  const ops = await getChangedOps({ cwd: repoDir, baseRef: baseCommit, headRef: headCommit, env: process.env });
659
723
  const slices = planPathSlices({ changedPaths: Array.from(ops.all), maxFiles });
660
724
 
661
- const sliceResults = [];
662
- for (let i = 0; i < slices.length; i += 1) {
663
- const slice = slices[i];
664
- const logFile = join(runDir, 'raw', `codex-slice-${i + 1}-of-${slices.length}-${sanitizeLabel(slice.label)}.log`);
665
- // eslint-disable-next-line no-await-in-loop
666
- const rr = await withDetachedWorktree(
667
- { repoDir, headCommit: baseCommit, label: `codex-${i + 1}-of-${slices.length}`, env: process.env },
668
- async (worktreeDir) => {
669
- const { baseSliceCommit } = await createHeadSliceCommits({
670
- cwd: worktreeDir,
671
- env: process.env,
672
- baseRef: baseCommit,
673
- headCommit,
674
- ops,
675
- slicePaths: slice.paths,
676
- label: slice.label.replace(/\/+$/g, ''),
677
- });
678
- const prompt = buildCodexMonorepoSlicePrompt({ sliceLabel: slice.label, baseCommit: baseSliceCommit, baseRef: base.baseRef });
679
- return await runCodexReview({
680
- repoDir: worktreeDir,
681
- baseRef: null,
682
- env: process.env,
683
- jsonMode,
684
- prompt,
685
- streamLabel: stream && !jsonMode ? `monorepo:codex:${i + 1}/${slices.length}` : undefined,
686
- teeFile: logFile,
687
- teeLabel: `monorepo:codex:${i + 1}/${slices.length}`,
688
- });
689
- }
690
- );
691
- const extracted = jsonMode ? extractCodexReviewFromJsonl(rr.stdout ?? '') : null;
692
- sliceResults.push({
693
- index: i + 1,
694
- of: slices.length,
695
- slice: slice.label,
696
- fileCount: slice.paths.length,
697
- logFile,
698
- ok: Boolean(rr.ok),
699
- exitCode: rr.exitCode,
700
- signal: rr.signal,
701
- durationMs: rr.durationMs,
702
- stdout: rr.stdout ?? '',
703
- stderr: rr.stderr ?? '',
704
- review_output: extracted,
705
- });
725
+ const sliceItems = slices.map((slice, i) => ({ slice, index: i + 1, of: slices.length }));
726
+ const sliceResults = await runSlicedJobs({
727
+ items: sliceItems,
728
+ limit: sliceConcurrency,
729
+ run: async ({ slice, index, of }) => {
730
+ const logFile = join(runDir, 'raw', `codex-slice-${index}-of-${of}-${sanitizeLabel(slice.label)}.log`);
731
+ const rr = await withDetachedWorktree(
732
+ { repoDir, headCommit: baseCommit, label: `codex-${index}-of-${of}`, env: process.env },
733
+ async (worktreeDir) => {
734
+ const { baseSliceCommit } = await createHeadSliceCommits({
735
+ cwd: worktreeDir,
736
+ env: process.env,
737
+ baseRef: baseCommit,
738
+ headCommit,
739
+ ops,
740
+ slicePaths: slice.paths,
741
+ label: slice.label.replace(/\/+$/g, ''),
742
+ });
743
+ const prompt = buildCodexMonorepoSlicePrompt({
744
+ sliceLabel: slice.label,
745
+ baseCommit: baseSliceCommit,
746
+ baseRef: base.baseRef,
747
+ });
748
+ return await runCodexReview({
749
+ repoDir: worktreeDir,
750
+ baseRef: null,
751
+ env: process.env,
752
+ jsonMode,
753
+ prompt,
754
+ streamLabel: stream && !jsonMode ? `monorepo:codex:${index}/${of}` : undefined,
755
+ teeFile: logFile,
756
+ teeLabel: `monorepo:codex:${index}/${of}`,
757
+ });
758
+ }
759
+ );
760
+ const extracted = jsonMode ? extractCodexReviewFromJsonl(rr.stdout ?? '') : null;
761
+ return {
762
+ index,
763
+ of,
764
+ slice: slice.label,
765
+ fileCount: slice.paths.length,
766
+ logFile,
767
+ ok: Boolean(rr.ok),
768
+ exitCode: rr.exitCode,
769
+ signal: rr.signal,
770
+ durationMs: rr.durationMs,
771
+ stdout: rr.stdout ?? '',
772
+ stderr: rr.stderr ?? '',
773
+ review_output: extracted,
774
+ };
775
+ },
776
+ shouldAbortEarly: (r) => detectCodexUsageLimit({ stdout: r?.stdout, stderr: r?.stderr }),
777
+ });
778
+
779
+ if (sliceResults.length === 1 && detectCodexUsageLimit(sliceResults[0])) {
780
+ const msg = `[review] codex usage limit detected; resolve Codex credits/limits, then re-run this review.`;
781
+ // eslint-disable-next-line no-console
782
+ console.error(msg);
706
783
  }
707
784
 
708
785
  const okAll = sliceResults.every((r) => r.ok);
@@ -719,7 +796,11 @@ async function main() {
719
796
  };
720
797
  }
721
798
 
722
- const prompt = usePromptMode ? buildCodexDeepPrompt({ component, baseRef: base.baseRef }) : '';
799
+ const prompt = usePromptMode
800
+ ? monorepo
801
+ ? buildCodexMonorepoDeepPrompt({ baseRef: base.baseRef })
802
+ : buildCodexDeepPrompt({ component, baseRef: base.baseRef })
803
+ : '';
723
804
  const logFile = join(runDir, 'raw', `codex-${sanitizeLabel(component)}.log`);
724
805
  const res = await runCodexReview({
725
806
  repoDir,
@@ -744,6 +825,122 @@ async function main() {
744
825
  logFile,
745
826
  };
746
827
  }
828
+ if (reviewer === 'augment') {
829
+ const usePromptMode = depth === 'deep';
830
+ const fileCount = await countChangedFiles({ cwd: repoDir, env: process.env, base: base.baseRef });
831
+ const autoChunks = usePromptMode && fileCount > maxFiles;
832
+ const cacheDir = (process.env.HAPPY_STACKS_AUGMENT_CACHE_DIR ?? '').toString().trim();
833
+ const model = (process.env.HAPPY_STACKS_AUGMENT_MODEL ?? '').toString().trim();
834
+ const maxTurnsRaw = (process.env.HAPPY_STACKS_AUGMENT_MAX_TURNS ?? '').toString().trim();
835
+ const maxTurns = maxTurnsRaw ? Number(maxTurnsRaw) : null;
836
+
837
+ if (monorepo && effectiveChunking === 'head-slice' && usePromptMode && (wantChunksAugment ?? autoChunks)) {
838
+ const headCommit = (await runCapture('git', ['rev-parse', 'HEAD'], { cwd: repoDir, env: process.env })).trim();
839
+ const baseCommit = (await runCapture('git', ['rev-parse', base.baseRef], { cwd: repoDir, env: process.env })).trim();
840
+ const ops = await getChangedOps({ cwd: repoDir, baseRef: baseCommit, headRef: headCommit, env: process.env });
841
+ const slices = planPathSlices({ changedPaths: Array.from(ops.all), maxFiles });
842
+
843
+ const sliceItems = slices.map((slice, i) => ({ slice, index: i + 1, of: slices.length }));
844
+ const sliceResults = await runSlicedJobs({
845
+ items: sliceItems,
846
+ limit: sliceConcurrency,
847
+ run: async ({ slice, index, of }) => {
848
+ const logFile = join(runDir, 'raw', `augment-slice-${index}-of-${of}-${sanitizeLabel(slice.label)}.log`);
849
+ const rr = await withDetachedWorktree(
850
+ { repoDir, headCommit: baseCommit, label: `augment-${index}-of-${of}`, env: process.env },
851
+ async (worktreeDir) => {
852
+ const { baseSliceCommit } = await createHeadSliceCommits({
853
+ cwd: worktreeDir,
854
+ env: process.env,
855
+ baseRef: baseCommit,
856
+ headCommit,
857
+ ops,
858
+ slicePaths: slice.paths,
859
+ label: slice.label.replace(/\/+$/g, ''),
860
+ });
861
+ const prompt = buildCodexMonorepoSlicePrompt({
862
+ sliceLabel: slice.label,
863
+ baseCommit: baseSliceCommit,
864
+ baseRef: base.baseRef,
865
+ });
866
+ return await runAugmentReview({
867
+ repoDir: worktreeDir,
868
+ prompt,
869
+ env: process.env,
870
+ cacheDir,
871
+ model,
872
+ maxTurns: Number.isFinite(maxTurns) ? String(maxTurns) : undefined,
873
+ streamLabel: stream ? `monorepo:augment:${index}/${of}` : undefined,
874
+ teeFile: logFile,
875
+ teeLabel: `monorepo:augment:${index}/${of}`,
876
+ });
877
+ }
878
+ );
879
+ return {
880
+ index,
881
+ of,
882
+ slice: slice.label,
883
+ fileCount: slice.paths.length,
884
+ logFile,
885
+ ok: Boolean(rr.ok),
886
+ exitCode: rr.exitCode,
887
+ signal: rr.signal,
888
+ durationMs: rr.durationMs,
889
+ stdout: rr.stdout ?? '',
890
+ stderr: rr.stderr ?? '',
891
+ };
892
+ },
893
+ shouldAbortEarly: (r) => detectAugmentAuthError({ stdout: r?.stdout, stderr: r?.stderr }),
894
+ });
895
+
896
+ if (sliceResults.length === 1 && detectAugmentAuthError(sliceResults[0])) {
897
+ const msg = `[review] augment auth required: run 'auggie login' in an interactive session, then re-run this review.`;
898
+ // eslint-disable-next-line no-console
899
+ console.error(msg);
900
+ }
901
+
902
+ const okAll = sliceResults.every((r) => r.ok);
903
+ return {
904
+ reviewer,
905
+ ok: okAll,
906
+ exitCode: okAll ? 0 : 1,
907
+ signal: null,
908
+ durationMs: sliceResults.reduce((acc, r) => acc + (r.durationMs ?? 0), 0),
909
+ stdout: '',
910
+ stderr: '',
911
+ note: `monorepo head-slice: ${sliceResults.length} slices (maxFiles=${maxFiles})`,
912
+ slices: sliceResults,
913
+ };
914
+ }
915
+
916
+ const prompt = usePromptMode
917
+ ? monorepo
918
+ ? buildCodexMonorepoDeepPrompt({ baseRef: base.baseRef })
919
+ : buildCodexDeepPrompt({ component, baseRef: base.baseRef })
920
+ : '';
921
+ const logFile = join(runDir, 'raw', `augment-${sanitizeLabel(component)}.log`);
922
+ const res = await runAugmentReview({
923
+ repoDir,
924
+ prompt,
925
+ env: process.env,
926
+ cacheDir,
927
+ model,
928
+ maxTurns: Number.isFinite(maxTurns) ? String(maxTurns) : undefined,
929
+ streamLabel: stream ? `${component}:augment` : undefined,
930
+ teeFile: logFile,
931
+ teeLabel: `${component}:augment`,
932
+ });
933
+ return {
934
+ reviewer,
935
+ ok: Boolean(res.ok),
936
+ exitCode: res.exitCode,
937
+ signal: res.signal,
938
+ durationMs: res.durationMs,
939
+ stdout: res.stdout ?? '',
940
+ stderr: res.stderr ?? '',
941
+ logFile,
942
+ };
943
+ }
747
944
  return { reviewer, ok: false, exitCode: null, signal: null, durationMs: 0, stdout: '', stderr: 'unknown reviewer\n' };
748
945
  })
749
946
  );
@@ -771,6 +968,7 @@ async function main() {
771
968
  const allFindings = [];
772
969
  let cr = 0;
773
970
  let cx = 0;
971
+ let au = 0;
774
972
 
775
973
  for (const job of jobResults) {
776
974
  for (const rr of job.results) {
@@ -831,6 +1029,31 @@ async function main() {
831
1029
  consumeText(reviewText, null, rr.logFile ?? null);
832
1030
  }
833
1031
  }
1032
+
1033
+ if (rr.reviewer === 'augment') {
1034
+ const sliceLike = rr.slices ?? rr.chunks ?? null;
1035
+ const consumeText = (reviewText, slice, sourceLog) => {
1036
+ const parsed = parseCodexReviewText(reviewText).map((f) => ({ ...f, reviewer: 'augment' }));
1037
+ for (const f of parsed) {
1038
+ au += 1;
1039
+ allFindings.push({
1040
+ ...f,
1041
+ id: `AU-${String(au).padStart(3, '0')}`,
1042
+ job: job.component,
1043
+ slice,
1044
+ sourceLog: sourceLog ?? null,
1045
+ });
1046
+ }
1047
+ };
1048
+
1049
+ if (Array.isArray(sliceLike)) {
1050
+ for (const s of sliceLike) {
1051
+ consumeText(s.stdout ?? '', s.slice ?? `${s.index}/${s.of}`, s.logFile ?? null);
1052
+ }
1053
+ } else {
1054
+ consumeText(rr.stdout ?? '', null, rr.logFile ?? null);
1055
+ }
1056
+ }
834
1057
  }
835
1058
  }
836
1059