@golden-frijoles/kit 0.27.0 → 0.27.2
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.
|
@@ -22,6 +22,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
22
22
|
import { readFileSync, existsSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs';
|
|
23
23
|
import { tmpdir } from 'node:os';
|
|
24
24
|
import { join } from 'node:path';
|
|
25
|
+
import { assertReviewOutput } from './review-shape.mjs';
|
|
25
26
|
|
|
26
27
|
// label per agent. Drives both the CLI dispatch and the human-readable header.
|
|
27
28
|
export const AGENTS = {
|
|
@@ -551,6 +552,52 @@ export function isTransientAgyError(stderr) {
|
|
|
551
552
|
);
|
|
552
553
|
}
|
|
553
554
|
|
|
555
|
+
// WHICH transient failure, because the two that matter mean opposite things to the person reading the error
|
|
556
|
+
// (probed live, agy 1.2.16, 2026-10-04 — it now exits 3 and says why on stderr):
|
|
557
|
+
// quota "RESOURCE_EXHAUSTED (code 429): Individual quota reached … Resets in 155h5m1s." Every Gemini tier
|
|
558
|
+
// shares one pool, and it is a weekly window: waiting a minute does nothing.
|
|
559
|
+
// capacity "UNAVAILABLE (code 503): No capacity available for model gpt-oss-120b-medium" / "high traffic,
|
|
560
|
+
// please try again in a minute". The SAME call answered fine 14 s later.
|
|
561
|
+
// Both used to end in one message that said "quota", so agents reported agy capped while it worked on their
|
|
562
|
+
// next task. Returns 'quota' | 'capacity' | null; a null on a transient error is some other blip.
|
|
563
|
+
export function agyFailureKind(output) {
|
|
564
|
+
const text = output || '';
|
|
565
|
+
if (/RESOURCE_EXHAUSTED|quota (?:reached|exceeded|exhausted)/i.test(text)) return 'quota';
|
|
566
|
+
if (/no capacity|high traffic|UNAVAILABLE \(code 503\)|overloaded/i.test(text)) return 'capacity';
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function agyQuotaReset(output) {
|
|
571
|
+
return /Resets in ([0-9hms]+)/i.exec(output || '')?.[1] ?? null;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// How often, and how long apart, a 'capacity' answer is retried on the same model before moving on.
|
|
575
|
+
export const AGY_CAPACITY_RETRIES = Number(process.env.AGY_CAPACITY_RETRIES ?? 2);
|
|
576
|
+
export const AGY_CAPACITY_WAIT_MS = Number(process.env.AGY_CAPACITY_WAIT_MS ?? 30_000);
|
|
577
|
+
|
|
578
|
+
// The reason behind an EMPTY answer. agy can still exit 0 with nothing on stdout, and then the only record of why
|
|
579
|
+
// is its own log (seen on PR #267's security lens, 2026-10-04, which answered on the re-run). The last real error
|
|
580
|
+
// line, skipping the side calls that fail without costing the answer (telemetry, conversation-title generation).
|
|
581
|
+
export function agyLogError(log) {
|
|
582
|
+
const errors = String(log || '')
|
|
583
|
+
.split('\n')
|
|
584
|
+
.filter((l) => /^E\d{4} /.test(l) && !/telemetry|title generation|recordTrajectory/i.test(l));
|
|
585
|
+
const last = errors.pop();
|
|
586
|
+
return last ? last.replace(/^E\S+ \S+\s+\d+ \S+\] /, '').slice(0, 240) : null;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function readOrNull(path) {
|
|
590
|
+
try {
|
|
591
|
+
return readFileSync(path, 'utf8');
|
|
592
|
+
} catch {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function sleepSync(ms) {
|
|
598
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
599
|
+
}
|
|
600
|
+
|
|
554
601
|
export function isContextWindowOverflow(output) {
|
|
555
602
|
// ['’] covers both a straight and a "smart"/curly apostrophe — CLIs are inconsistent about which they emit.
|
|
556
603
|
return /ran out of room in the model['’]?s context window/i.test(output || '');
|
|
@@ -884,8 +931,8 @@ export function agyArgs(fullArgv, model = AGY_MODEL) {
|
|
|
884
931
|
// One `agy -p "<prompt>" --model "<MODEL>"` invocation. The prompt+framed context ride in `fullArgv` (stdin is
|
|
885
932
|
// NOT the prompt and must be at EOF — input:'' gives an immediate EOF or print mode blocks forever). Returns
|
|
886
933
|
// the raw spawn result; the caller classifies status/stdout.
|
|
887
|
-
function execAgy(fullArgv, model, spawn) {
|
|
888
|
-
return spawn('agy', agyArgs(fullArgv, model), {
|
|
934
|
+
function execAgy(fullArgv, model, spawn, logFile) {
|
|
935
|
+
return spawn('agy', [...agyArgs(fullArgv, model), ...(logFile ? ['--log-file', logFile] : [])], {
|
|
889
936
|
input: '',
|
|
890
937
|
encoding: 'utf8',
|
|
891
938
|
maxBuffer: 64 * 1024 * 1024,
|
|
@@ -1106,7 +1153,7 @@ export function renderFileContext({ attached, omitted }) {
|
|
|
1106
1153
|
// separate quota pool) so a Gemini quota exhaustion degrades to GPT-OSS instead of silently blanking the review.
|
|
1107
1154
|
// We enforce the argv size cap up front (clear message, not an opaque E2BIG). `deps.spawn` is injectable for tests.
|
|
1108
1155
|
export function runAntigravity(fullArgv, opts = {}, deps = {}) {
|
|
1109
|
-
const { spawn = spawnSync, warn = (m) => process.stderr.write(`${m}\n`) } = deps;
|
|
1156
|
+
const { spawn = spawnSync, warn = (m) => process.stderr.write(`${m}\n`), sleep = sleepSync } = deps;
|
|
1110
1157
|
if (Buffer.byteLength(fullArgv, 'utf8') > AGY_ARG_LIMIT) {
|
|
1111
1158
|
return fail(
|
|
1112
1159
|
opts.soft,
|
|
@@ -1124,47 +1171,76 @@ export function runAntigravity(fullArgv, opts = {}, deps = {}) {
|
|
|
1124
1171
|
? [AGY_MODEL]
|
|
1125
1172
|
: [AGY_MODEL, AGY_FALLBACK_MODEL];
|
|
1126
1173
|
const tried = [];
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
// Classify on the FULL output, not on `last`. `last` is only the final non-empty stderr line,
|
|
1141
|
-
// chosen because it makes the best human-readable message — but agy can emit the transient
|
|
1142
|
-
// notice followed by a stack trace or a trailing status line, which would push the phrase we
|
|
1143
|
-
// match on out of view and abort instead of falling back (cross-review, PR #29). Both streams
|
|
1144
|
-
// are checked because agy is already known to split diagnostics across them inconsistently —
|
|
1145
|
-
// the same reason isContextWindowOverflow is called on stdout and stderr above.
|
|
1146
|
-
const failureOutput = `${r.stderr || ''}\n${r.stdout || ''}`;
|
|
1147
|
-
if (isTransientAgyError(failureOutput) && model !== modelPair[modelPair.length - 1]) {
|
|
1148
|
-
warn(`⚠ agy "${model}" is temporarily unavailable (${last}) → trying the fallback model.`);
|
|
1149
|
-
tried.push(model);
|
|
1150
|
-
continue;
|
|
1174
|
+
const causes = []; // one plain line per model that did not answer, for the final message
|
|
1175
|
+
const logDir = mkdtempSync(join(tmpdir(), 'agy-log-'));
|
|
1176
|
+
try {
|
|
1177
|
+
for (const model of modelPair) {
|
|
1178
|
+
const logFile = join(logDir, `${model}.log`);
|
|
1179
|
+
let r = execAgy(fullArgv, model, spawn, logFile);
|
|
1180
|
+
for (let retry = 1; retry <= AGY_CAPACITY_RETRIES; retry += 1) {
|
|
1181
|
+
if (r.status === 0 || agyFailureKind(`${r.stderr || ''}\n${r.stdout || ''}`) !== 'capacity') break;
|
|
1182
|
+
warn(
|
|
1183
|
+
`⚠ agy "${model}" has no capacity right now (not quota) → retry ${retry}/${AGY_CAPACITY_RETRIES} in ${Math.round(AGY_CAPACITY_WAIT_MS / 1000)}s.`
|
|
1184
|
+
);
|
|
1185
|
+
sleep(AGY_CAPACITY_WAIT_MS);
|
|
1186
|
+
r = execAgy(fullArgv, model, spawn, logFile);
|
|
1151
1187
|
}
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1188
|
+
if (r.status !== 0) {
|
|
1189
|
+
const last = (r.stderr || '').trim().split('\n').filter(Boolean).pop() || 'unknown error';
|
|
1190
|
+
// A non-zero exit is USUALLY a real agy error (bad flags, crash) — surface those directly
|
|
1191
|
+
// rather than burning the fallback on them.
|
|
1192
|
+
//
|
|
1193
|
+
// But not always, and this cost a real run to learn (2026-07-25, commit-report's first live
|
|
1194
|
+
// use after merge): `gpt-oss-120b-medium` answered "Our servers are experiencing high traffic
|
|
1195
|
+
// right now, please try again in a minute" with a NON-ZERO exit. That is precisely the
|
|
1196
|
+
// situation the second model exists for — a different provider with a separate capacity pool
|
|
1197
|
+
// — and the old branch refused to try it, because the fallback was wired only to the
|
|
1198
|
+
// EMPTY-output signal. Same transient condition, different exit code, no fallback.
|
|
1199
|
+
// Classify on the FULL output, not on `last`. `last` is only the final non-empty stderr line,
|
|
1200
|
+
// chosen because it makes the best human-readable message — but agy can emit the transient
|
|
1201
|
+
// notice followed by a stack trace or a trailing status line, which would push the phrase we
|
|
1202
|
+
// match on out of view and abort instead of falling back (cross-review, PR #29). Both streams
|
|
1203
|
+
// are checked because agy is already known to split diagnostics across them inconsistently —
|
|
1204
|
+
// the same reason isContextWindowOverflow is called on stdout and stderr above.
|
|
1205
|
+
const failureOutput = `${r.stderr || ''}\n${r.stdout || ''}`;
|
|
1206
|
+
const kind = agyFailureKind(failureOutput);
|
|
1207
|
+
const cause =
|
|
1208
|
+
kind === 'quota'
|
|
1209
|
+
? `quota reached${agyQuotaReset(failureOutput) ? `, resets in ${agyQuotaReset(failureOutput)}` : ''}`
|
|
1210
|
+
: kind === 'capacity'
|
|
1211
|
+
? `no capacity after ${AGY_CAPACITY_RETRIES + 1} tries (transient, NOT quota; re-run in a few minutes)`
|
|
1212
|
+
: last;
|
|
1213
|
+
causes.push(`"${model}": ${cause}`);
|
|
1214
|
+
if (isTransientAgyError(failureOutput) && model !== modelPair[modelPair.length - 1]) {
|
|
1215
|
+
warn(`⚠ agy "${model}" is unavailable (${cause}) → trying the fallback model.`);
|
|
1216
|
+
tried.push(model);
|
|
1217
|
+
continue;
|
|
1218
|
+
}
|
|
1219
|
+
return fail(opts.soft, `agy -p failed — ${causes.join('; ')}`);
|
|
1220
|
+
}
|
|
1221
|
+
const out = (r.stdout || '').trim();
|
|
1222
|
+
if (out) {
|
|
1223
|
+
if (model !== modelPair[0])
|
|
1224
|
+
warn(`⚠ agy "${modelPair[0]}" did not answer (${causes[0]}) → used "${model}".`);
|
|
1225
|
+
// Tell the caller WHICH model answered. Added because the prose rail's footer said "agy",
|
|
1226
|
+
// which cannot distinguish the primary from the fallback — so a silent switch between two
|
|
1227
|
+
// models with very different registers was unattributable in the channel, and that is exactly
|
|
1228
|
+
// the confusion that took a human to notice. Optional and side-effect-only, so every existing
|
|
1229
|
+
// caller keeps its byte-identical string return.
|
|
1230
|
+
opts.onModel?.(model);
|
|
1231
|
+
return out;
|
|
1232
|
+
}
|
|
1233
|
+
const why = agyLogError(readOrNull(logFile));
|
|
1234
|
+
causes.push(
|
|
1235
|
+
`"${model}": empty output${why ? ` — agy's log says: ${why}` : ' and no error in its log'}`
|
|
1236
|
+
);
|
|
1237
|
+
tried.push(model);
|
|
1165
1238
|
}
|
|
1166
|
-
|
|
1239
|
+
} finally {
|
|
1240
|
+
rmSync(logDir, { recursive: true, force: true });
|
|
1167
1241
|
}
|
|
1242
|
+
if (causes.some((c) => c.includes("agy's log says")))
|
|
1243
|
+
return fail(opts.soft, `agy returned no review — ${causes.join('; ')}`);
|
|
1168
1244
|
return fail(
|
|
1169
1245
|
opts.soft,
|
|
1170
1246
|
`agy returned no output for ${tried.map((m) => `"${m}"`).join(' and ')} — likely a quota cap ` +
|
|
@@ -1338,14 +1414,18 @@ export function runVibe(fullArgv, opts = {}, deps = {}) {
|
|
|
1338
1414
|
* Two signals, both cheap and neither dependent on the model's wording:
|
|
1339
1415
|
* 1. the output STARTS with a tool call (`read_file{…}`, `grep {…}`) — the literal shape vibe emits
|
|
1340
1416
|
* when it is cut off between turns, or when it asks for a tool that is disabled;
|
|
1341
|
-
* 2. it
|
|
1417
|
+
* 2. it is not a review by review-guard's own test: no severity heading and no clean verdict.
|
|
1418
|
+
*
|
|
1419
|
+
* (2) used to be its own list — Blocking / Should-fix / Nit only — so `Clean.`, the one-line verdict the
|
|
1420
|
+
* prompt asks for on a clean diff, failed here as "truncated" before review-guard ever saw it. Every clean
|
|
1421
|
+
* vibe review died that way (reproduced on PR #265, 2026-10-04). One definition of "a review" now.
|
|
1342
1422
|
*
|
|
1343
1423
|
* Exported for the unit layer: this is the guard whose absence let a non-review reach a PR.
|
|
1344
1424
|
*/
|
|
1345
1425
|
export function isTruncatedReview(out) {
|
|
1346
1426
|
const text = String(out || '').trim();
|
|
1347
1427
|
if (/^\w+\s*\{/.test(text)) return true;
|
|
1348
|
-
return
|
|
1428
|
+
return !assertReviewOutput(text).ok;
|
|
1349
1429
|
}
|
|
1350
1430
|
|
|
1351
1431
|
// One `claude -p "<prompt>"` invocation with the context piped on stdin (same shape as codex, which is why
|
package/dist/lib/log-branch.mjs
CHANGED
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
|
|
13
13
|
import { spawnSync } from 'node:child_process';
|
|
14
14
|
|
|
15
|
+
// Every log commit also carries this vercel.json. A log branch is an orphan with no package.json, and a
|
|
16
|
+
// repo whose Vercel project builds every pushed branch tried to build it as a preview and failed on each
|
|
17
|
+
// write ("No Next.js version detected", claude/session-journal, 2026-10-04). Vercel reads vercel.json from
|
|
18
|
+
// the commit it is about to build, so the branch itself has to say "don't".
|
|
19
|
+
export const LOG_BRANCH_VERCEL_JSON = `${JSON.stringify({ git: { deploymentEnabled: false } }, null, 2)}\n`;
|
|
20
|
+
|
|
15
21
|
function git(args, opts = {}) {
|
|
16
22
|
return spawnSync('git', args, { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, ...opts });
|
|
17
23
|
}
|
|
@@ -34,7 +40,7 @@ export function readLogFromBranch({ cwd, branch, path }) {
|
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
// Writes `content` to `path` on `branch`, creating the branch if it doesn't exist yet. Pure plumbing:
|
|
37
|
-
// hash-object (write a blob) → mktree (
|
|
43
|
+
// hash-object (write a blob) → mktree (the file plus LOG_BRANCH_VERCEL_JSON) → commit-tree (parented on the branch's
|
|
38
44
|
// current tip, if any) → push the new commit straight to the branch ref. Returns true on success.
|
|
39
45
|
//
|
|
40
46
|
// `path` MUST be a flat filename (no `/`) — `git mktree` builds a single-level tree, and a slash needs a
|
|
@@ -58,7 +64,17 @@ export function writeLogToBranch({ cwd, branch, path, content, message }) {
|
|
|
58
64
|
}
|
|
59
65
|
const blobSha = hashObj.stdout.trim();
|
|
60
66
|
|
|
61
|
-
|
|
67
|
+
let tree = `100644 blob ${blobSha}\t${path}\n`;
|
|
68
|
+
if (path !== 'vercel.json') {
|
|
69
|
+
const vercelObj = git(['hash-object', '-w', '--stdin'], { cwd, input: LOG_BRANCH_VERCEL_JSON });
|
|
70
|
+
if (vercelObj.status !== 0) {
|
|
71
|
+
console.error(`log-branch: git hash-object (vercel.json) failed: ${(vercelObj.stderr || '').trim()}`);
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
tree += `100644 blob ${vercelObj.stdout.trim()}\tvercel.json\n`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const mktree = git(['mktree'], { cwd, input: tree });
|
|
62
78
|
if (mktree.status !== 0) {
|
|
63
79
|
console.error(`log-branch: git mktree failed: ${(mktree.stderr || '').trim()}`);
|
|
64
80
|
return false;
|
|
@@ -38,39 +38,13 @@
|
|
|
38
38
|
import { spawnSync } from 'node:child_process';
|
|
39
39
|
import { jevContext } from './jev.mjs';
|
|
40
40
|
import { loadQuestions, wireQuestion } from './jev-questions.mjs';
|
|
41
|
+
import { TOOL_TRANSCRIPT, assertReviewOutput } from './review-shape.mjs';
|
|
42
|
+
|
|
43
|
+
export { assertReviewOutput };
|
|
41
44
|
|
|
42
45
|
/** Codex CLI version last observed producing a real, structured review. Bump after a verified run. */
|
|
43
46
|
export const CODEX_VERIFIED = '0.154.0';
|
|
44
47
|
|
|
45
|
-
const SEVERITY_HEADING =
|
|
46
|
-
/^\s{0,3}(?:[-*+]\s+|\d+\.\s+|>\s*|\|\s*)?(?:#{1,6}\s*)?(?:\*\*|__)?\s*(?:🔴|🟠|🟡|⚪)?\s*(?:blocking|should[- ]fix|nits?|important|critical|findings?|correctness(?:\s*(?:&|and)\s*architecture)?|security(?: review)? findings?)\b/im;
|
|
47
|
-
/^\s{0,3}(?:#{1,6}\s*)?(?:\*\*|__)?\s*(?:🔴|🟠|🟡|⚪)?\s*(?:blocking|should[- ]fix|nits?|important|critical)\b/im;
|
|
48
|
-
const CLEAN_VERDICT =
|
|
49
|
-
/(?:^\s{0,3}(?:[-*+]\s+)?(?:#{1,6}\s*)?(?:\*\*|__)?\s*(?:clean|none)\b|\bassessment:\s*(?:\*\*)?\s*clean\b|\bno\s+(?:\*\*)?(?:blocking|security|should[- ]fix|nit)\b[^.\n]{0,80}\b(?:findings|issues|problems)\b|\bno (?:findings|issues|problems)\b|\bnothing (?:to report|found|blocking)\b|\blooks clean\b|\bdiff (?:is|looks) clean\b|\bno concerns\b)/im;
|
|
50
|
-
/(?:^\s{0,3}(?:#{1,6}\s*)?(?:\*\*|__)?\s*clean\b|\bassessment:\s*(?:\*\*)?\s*clean\b|\bno (?:blocking |security )?(?:findings|issues|problems)\b|\bnothing (?:to report|found|blocking)\b|\blooks clean\b|\bdiff (?:is|looks) clean\b|\bno concerns\b)/im;
|
|
51
|
-
// A reviewer that emitted a raw tool call instead of a review — observed twice from vibe on real PRs
|
|
52
|
-
// (`read_file{"path": …}`, `write_file…{"file_path": …}`). Its JSON can CONTAIN review-shaped words, so
|
|
53
|
-
// this is checked first and wins.
|
|
54
|
-
const TOOL_TRANSCRIPT = /^\s*[a-z_]+\S*\s*\{\s*"/i;
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* THE GUARD. Pure. Returns { ok, reason }.
|
|
58
|
-
* ok when the reply has a severity heading or an explicit clean verdict; fails on empty, whitespace,
|
|
59
|
-
* or prose with neither (a CLI banner, an error page, a truncated tool transcript).
|
|
60
|
-
*/
|
|
61
|
-
export function assertReviewOutput(text) {
|
|
62
|
-
const t = String(text ?? '').trim();
|
|
63
|
-
if (!t) return { ok: false, reason: 'the reviewer returned no output' };
|
|
64
|
-
if (TOOL_TRANSCRIPT.test(t))
|
|
65
|
-
return { ok: false, reason: `the reviewer emitted a raw tool call, not a review ("${t.slice(0, 80)}")` };
|
|
66
|
-
if (SEVERITY_HEADING.test(t)) return { ok: true, reason: 'severity-structured findings' };
|
|
67
|
-
if (CLEAN_VERDICT.test(t)) return { ok: true, reason: 'explicit clean verdict' };
|
|
68
|
-
return {
|
|
69
|
-
ok: false,
|
|
70
|
-
reason: `the reviewer's reply has no severity heading and no clean verdict — not a review (first line: "${t.split('\n')[0].slice(0, 120)}")`,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
48
|
/** Minimal glob → RegExp: `**` any depth, `*` within a segment, `?` one char. Anchored, case-sensitive. */
|
|
75
49
|
export function globToRegExp(glob) {
|
|
76
50
|
let re = '';
|
|
@@ -289,7 +263,7 @@ export const RE_REVIEW_NOTE =
|
|
|
289
263
|
'\n\n## RE-REVIEW\nA previous pass already reviewed this PR and the author has pushed changes since. Report **Blocking and Should-fix findings only** — no nits, and do not repeat a finding the author has already fixed or answered on the PR.\n';
|
|
290
264
|
|
|
291
265
|
// ── 4. Jev decides the SEMANTIC question (jev-semantic-guards D4/D5) ────────────────────────────────
|
|
292
|
-
// `assertReviewOutput`
|
|
266
|
+
// `assertReviewOutput` (review-shape.mjs) is a regex asking a language question — "did the reviewer actually review?" —
|
|
293
267
|
// and on 2026-09-19 it answered it wrong in both directions: it REJECTED a real finding written as plain
|
|
294
268
|
// prose, and ACCEPTED `## Findings` followed by "(reviewer timed out before completing analysis)". Jev
|
|
295
269
|
// (TypeSafe's calibrated typed-judgement model) got both right. So the judge below asks Jev, by
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// review-shape.mjs — "is this text a review at all?", with no dependencies, so a runner can ask it without
|
|
2
|
+
// pulling review-guard's Jev machinery into its closure. review-guard re-exports assertReviewOutput and uses it as
|
|
3
|
+
// the offline fallback; cross-agent-cli's isTruncatedReview asks the same function, so there is one definition.
|
|
4
|
+
|
|
5
|
+
const SEVERITY_HEADING =
|
|
6
|
+
/^\s{0,3}(?:[-*+]\s+|\d+\.\s+|>\s*|\|\s*)?(?:#{1,6}\s*)?(?:\*\*|__)?\s*(?:🔴|🟠|🟡|⚪)?\s*(?:blocking|should[- ]fix|nits?|important|critical|findings?|correctness(?:\s*(?:&|and)\s*architecture)?|security(?: review)? findings?)\b/im;
|
|
7
|
+
const CLEAN_VERDICT =
|
|
8
|
+
/(?:^\s{0,3}(?:[-*+]\s+)?(?:#{1,6}\s*)?(?:\*\*|__)?\s*(?:clean|none)\b|\bassessment:\s*(?:\*\*)?\s*clean\b|\bno\s+(?:\*\*)?(?:blocking|security|should[- ]fix|nit)\b[^.\n]{0,80}\b(?:findings|issues|problems)\b|\bno (?:findings|issues|problems)\b|\bnothing (?:to report|found|blocking)\b|\blooks clean\b|\bdiff (?:is|looks) clean\b|\bno concerns\b)/im;
|
|
9
|
+
// A reviewer that emitted a raw tool call instead of a review — observed twice from vibe on real PRs
|
|
10
|
+
// (`read_file{"path": …}`, `write_file…{"file_path": …}`). Its JSON can CONTAIN review-shaped words, so
|
|
11
|
+
// this is checked first and wins.
|
|
12
|
+
export const TOOL_TRANSCRIPT = /^\s*[a-z_]+\S*\s*\{\s*"/i;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* THE GUARD. Pure. Returns { ok, reason }.
|
|
16
|
+
* ok when the reply has a severity heading or an explicit clean verdict; fails on empty, whitespace,
|
|
17
|
+
* or prose with neither (a CLI banner, an error page, a truncated tool transcript).
|
|
18
|
+
*/
|
|
19
|
+
export function assertReviewOutput(text) {
|
|
20
|
+
const t = String(text ?? '').trim();
|
|
21
|
+
if (!t) return { ok: false, reason: 'the reviewer returned no output' };
|
|
22
|
+
if (TOOL_TRANSCRIPT.test(t))
|
|
23
|
+
return { ok: false, reason: `the reviewer emitted a raw tool call, not a review ("${t.slice(0, 80)}")` };
|
|
24
|
+
if (SEVERITY_HEADING.test(t)) return { ok: true, reason: 'severity-structured findings' };
|
|
25
|
+
if (CLEAN_VERDICT.test(t)) return { ok: true, reason: 'explicit clean verdict' };
|
|
26
|
+
return {
|
|
27
|
+
ok: false,
|
|
28
|
+
reason: `the reviewer's reply has no severity heading and no clean verdict — not a review (first line: "${t.split('\n')[0].slice(0, 120)}")`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -64,8 +64,10 @@ outcome genuinely changes the next one's scope). Six things make it work, and th
|
|
|
64
64
|
4. **Merges are pre-authorized on green** in a named run: that removes the round-trip, not the gate or the
|
|
65
65
|
review layers, and never extends to a new category of production mutation (TLS/IAM/secrets, money or
|
|
66
66
|
entitlement writes, a new external dependency) — name those in one focused question.
|
|
67
|
-
5. **Generate the kickoff, don't compose it** —
|
|
68
|
-
|
|
67
|
+
5. **Generate the kickoff, don't compose it** — `/build <slug>` in Claude Code with the plugin's mod; anywhere
|
|
68
|
+
else, from the project root, `npx -y @golden-frijoles/kit emit-epic-kickoff --epic <slug>` (if the
|
|
69
|
+
project has its own `scripts/emit-epic-kickoff.mjs`, run that instead). It reads the epic README and every sprint
|
|
70
|
+
file and prints the orchestrator prompt.
|
|
69
71
|
Hand-composing it is how the architecture lock gets summarised away and the review policy silently
|
|
70
72
|
reverts to whatever the composing agent remembered.
|
|
71
73
|
6. **Derive state, journal intent** — at session start run `node scripts/session-resume.mjs` (it ships in
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@golden-frijoles/kit",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.2",
|
|
4
4
|
"description": "The scripts the Golden Frijoles skills run, packaged so they work in any repo: planning, build-order, reporting and verification rails. Zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|