#!/usr/bin/env -S bun --no-env-file --config=/dev/null /** * gstack-review-plan — how much review the next /ship step dispatches (docs/review-policy.md#review-depth). * * gstack-review-plan --base [--pass ] [--last none|full|fix-check] [--fixed-blocking] [--fixed-below] * [--blocking-open] [--touched-gate] [--critical-found] [--adversarial-ran] [--gate-tagged ] [--gated a,b] [--force a,b|all] * [--reviewed-numbers ] [--depth-floor standard|full] [--range-base ] [--pass1-depth light|standard|full] [--no-tiers] [--json] * gstack-review-plan --fix-check-prompt --fix-list --hunks --tests [--callers ] * * Reads the diff against the merge base with (committed, tracked uncommitted * edits and untracked files, through lib/review-collect.ts collectLive), runs * gstack-diff-scope and, unless --gate-tagged is given, gstack-gate-diff; prints * REVIEW_CLASS:, REVIEW_DEPTH: (depth, runtime lines, the rules hash, reasons and * result codes), GATE_SCAN:, FIX_RANGE: (with --range-base), REVIEW_PLAN:, one DEPTH_ERROR: per input that failed * the depth closed, one DISPATCH: plus DIFF_CMD: (and DIFF_EXCLUDE: none) per * reviewer and one SKIP: per reviewer left out. A plan that fails closed to full * exits 0; a diff git cannot list exits 1. Logic: lib/review-depth.ts. */ import * as fs from 'node:fs'; import * as path from 'node:path'; import { EXIT, exitTableHelp } from '../lib/headless-artifacts'; import { diffCommand, reviewDepthLine, reviewPlanLine } from '../lib/review-depth'; import { flagReader, livePlan, parsePlanFlags, planPass, PLAN_FLAGS_HELP } from '../lib/review-plan-live'; import { renderFixCheckPrompt } from '../lib/review-prompts'; const HELP = `gstack-review-plan — the review depth of the next /ship step (docs/review-policy.md#review-depth) ${PLAN_FLAGS_HELP} --no-tiers audits and watchers at the host's default effort too (as gstack-config review_effort_tiers off does) --json the plan as JSON (depth, runtime_lines, rules, errors with input and repair, gate_scan, per-dispatch exclude) --fix-check-prompt print the fix-check reviewer's prompt from --fix-list, --hunks, --tests and --callers files Depth: light (runtime lines <= 50), standard (above 50), full (gate class or any DEPTH_ERROR). NEXT: full, scoped (a full pass on the range: fix-check, Red Team, native adversarial), fix-check, stop or done. A full plan from an error exits 0; exit 1 means no plan could be built (git could not list the diff). ${exitTableHelp()}`; const args = process.argv.slice(2); const { flag, has } = flagReader(args); function main(): number { if (has('--help') || has('-h')) { process.stdout.write(HELP + '\n'); return EXIT.ok; } if (has('--fix-check-prompt')) { const read = (n: string, required: boolean) => { const f = flag(n); if (!f) return required ? null : ''; try { return fs.readFileSync(f, 'utf8'); } catch { return null; } }; const [fixList, touchedHunks, testResults, callers] = [read('--fix-list', true), read('--hunks', true), read('--tests', true), read('--callers', false)]; if (fixList === null || touchedHunks === null || testResults === null || callers === null) { process.stderr.write('gstack-review-plan: --fix-check-prompt needs readable --fix-list, --hunks and --tests files (--callers optional)\n'); return EXIT.usage; } process.stdout.write(renderFixCheckPrompt({ fixList, touchedHunks, testResults, callers })); return EXIT.ok; } const flags = parsePlanFlags(args, 'gstack-review-plan'); if ('usage' in flags) { process.stderr.write(`${flags.usage}\n${flag('--base') ? '' : `${HELP}\n`}`); return EXIT.usage; } const live = livePlan(flags, path.dirname(Bun.fileURLToPath(import.meta.url)), 'gstack-review-plan'); if ('errors' in live) { process.stderr.write(live.errors.map(e => e + '\n').join('')); return EXIT.fail; } const { plan, collected, head, gateTagged, auditEffort, scope, fixRange } = live; const { pass, floor } = flags; const { diffLines, paths, deleted, stampOnly, perPath, registry } = collected; const gateScan = { tagged: gateTagged, head }; if (has('--json')) { const { runtimeLines, stepDepth, depthReasons, ...rest } = plan; process.stdout.write(JSON.stringify({ ...rest, step_depth: stepDepth, runtime_lines: runtimeLines, depth_reasons: depthReasons, depth_floor: floor ?? null, gate_scan: gateScan, fix_range: fixRange ?? null, registry: { sha: registry.sha, outputs: registry.outputs }, per_path: perPath, diff_lines: diffLines, pass, audit_effort: auditEffort, scope, gate_tagged: gateTagged, paths, deleted, stamp_only: stampOnly }) + '\n'); return EXIT.ok; } const out = [`REVIEW_CLASS: ${plan.class} (${plan.reasons.slice(0, 3).join('; ') || 'no signals'})`, reviewDepthLine(plan), `GATE_SCAN: ${gateTagged === null ? 'unavailable' : `tagged=${gateTagged}`} head=${head}`]; if (fixRange) out.push(`FIX_RANGE: ${fixRange.runtime_lines === null ? `unread (${fixRange.error})` : `runtime_lines=${fixRange.runtime_lines}`} base=${fixRange.base}`); out.push(reviewPlanLine(plan, planPass(plan, pass)), `NEXT: ${plan.next.step} (${plan.next.reason})`); for (const e of plan.errors) out.push(`DEPTH_ERROR: ${e.code} input=${e.input} detail=${e.detail}; fix: ${e.repair}${e.result ? ` (${e.result})` : ''}`); for (const d of plan.dispatches) { const key = d.name.split(' ')[0]; out.push(`DISPATCH: ${d.role} ${d.name} effort=${d.effort}${d.checklists ? ` checklists=${d.checklists.join(',')}` : ''}${d.reason ? ` reason=${d.reason}` : ''}`, `DIFF_CMD: ${key} ${diffCommand(d)}`); if (!d.exclude.length) out.push(`DIFF_EXCLUDE: ${key} none`); } for (const s of plan.skipped) out.push(`SKIP: ${s}`); out.push(`AUDIT_EFFORT: ${auditEffort} (coverage, plan-completion and docs audits, decision-trail reviewers, watchers; reviewers above stay full)`); process.stdout.write(out.join('\n') + '\n'); return EXIT.ok; } process.exit(main());