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.
- package/docs/commit-audits/happy/_tools/generate-plans.mjs +453 -0
- package/docs/commit-audits/happy/_tools/generate-pr-assignment.mjs +430 -0
- package/docs/commit-audits/happy/_tools/init-pr-assignment-working.mjs +107 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-analysis.md +1849 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-export.fuller-stat.md +747 -1
- package/docs/commit-audits/happy/leeroy-wip.commit-index.json +11740 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-index.tsv +252 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-inventory.md +18 -11
- package/docs/commit-audits/happy/leeroy-wip.commit-manual-review.md +1236 -92
- package/docs/commit-audits/happy/leeroy-wip.maintainers-overview.draft.md +448 -0
- package/docs/commit-audits/happy/leeroy-wip.pr-assignment.draft.tsv +252 -0
- package/docs/commit-audits/happy/leeroy-wip.pr-assignment.working.tsv +288 -0
- package/docs/commit-audits/happy/leeroy-wip.pr-catalog.draft.md +245 -0
- package/docs/commit-audits/happy/leeroy-wip.pr-stack-plan.draft.md +350 -0
- package/docs/commit-audits/happy/leeroy-wip.rewrite-deferred-fragments.tsv +65 -0
- package/docs/commit-audits/happy/leeroy-wip.rewrite-ledger.tsv +56 -0
- package/docs/commit-audits/happy/leeroy-wip.rewrite-process.md +240 -0
- package/docs/commit-audits/happy/leeroy-wip.rewrite-status.tsv +39 -0
- package/docs/commit-audits/happy/leeroy-wip.split-plan.draft.md +93 -0
- package/docs/commit-audits/happy/leeroy-wip.topic-buckets.md +76 -0
- package/docs/commit-audits/happy/pr-desc.extraction-ledger.tsv +279 -0
- package/docs/commit-audits/happy/pr-desc.original.md +0 -0
- package/docs/commit-audits/happy/pr-desc.post-audit-extraction-ledger.tsv +54 -0
- package/docs/commit-audits/happy/pr-desc.working-document.md +536 -0
- package/docs/happy-development.md +18 -1
- package/docs/isolated-linux-vm.md +23 -1
- package/docs/stacks.md +21 -1
- package/package.json +1 -1
- package/scripts/auth.mjs +46 -8
- package/scripts/daemon.mjs +44 -21
- package/scripts/doctor.mjs +2 -2
- package/scripts/doctor_cmd.test.mjs +67 -0
- package/scripts/happy.mjs +18 -5
- package/scripts/provision/linux-ubuntu-review-pr.sh +5 -1
- package/scripts/provision/macos-lima-happy-vm.sh +34 -2
- package/scripts/review.mjs +347 -124
- package/scripts/review_pr.mjs +78 -2
- package/scripts/run.mjs +2 -1
- package/scripts/stack.mjs +265 -19
- package/scripts/stack_daemon_cmd.test.mjs +196 -0
- package/scripts/stack_happy_cmd.test.mjs +103 -0
- package/scripts/utils/cli/prereqs.mjs +12 -1
- package/scripts/utils/dev/daemon.mjs +3 -1
- package/scripts/utils/proc/pm.mjs +1 -1
- package/scripts/utils/review/detached_worktree.mjs +61 -0
- package/scripts/utils/review/detached_worktree.test.mjs +62 -0
- package/scripts/utils/review/findings.mjs +133 -20
- package/scripts/utils/review/findings.test.mjs +88 -1
- package/scripts/utils/review/runners/augment.mjs +71 -0
- package/scripts/utils/review/runners/augment.test.mjs +42 -0
- package/scripts/utils/review/runners/coderabbit.mjs +54 -10
- package/scripts/utils/review/runners/coderabbit.test.mjs +15 -48
- package/scripts/utils/review/sliced_runner.mjs +39 -0
- package/scripts/utils/review/sliced_runner.test.mjs +47 -0
- package/scripts/utils/review/tool_home_seed.mjs +99 -0
- package/scripts/utils/review/tool_home_seed.test.mjs +113 -0
- package/scripts/utils/stack/cli_identities.mjs +29 -0
- package/scripts/utils/stack/startup.mjs +45 -7
- package/scripts/worktrees.mjs +8 -5
package/scripts/review.mjs
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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
|
|
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
|
|