@pugi/cli 0.1.0-beta.13 → 0.1.0-beta.14
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.
|
@@ -196,6 +196,25 @@ export class AnvilEngineLoopClient {
|
|
|
196
196
|
// for the broader "misleading error" pattern.)
|
|
197
197
|
try {
|
|
198
198
|
const parsed = text ? JSON.parse(text) : null;
|
|
199
|
+
// 2026-05-27 dogfood cycle 2: distinct error code for the
|
|
200
|
+
// infra-side "PII scrubber down" case. Previously the engine
|
|
201
|
+
// server returned `privacy_strict_upstream_blocked` here even
|
|
202
|
+
// when the tenant was on BALANCED (the scrubber crash forced
|
|
203
|
+
// a fail-closed). Operators chased the wrong fix ("switch
|
|
204
|
+
// privacy") for hours. Server now emits
|
|
205
|
+
// `pii_scrubber_unavailable` — surface a distinct remediation
|
|
206
|
+
// that points at the infra side, not the operator's privacy
|
|
207
|
+
// posture.
|
|
208
|
+
if (parsed?.code === 'pii_scrubber_unavailable') {
|
|
209
|
+
return {
|
|
210
|
+
stop: 'error',
|
|
211
|
+
code: 'privacy_blocked',
|
|
212
|
+
message: parsed.message ?? 'PII scrubber unavailable; privacy filter refused dispatch.',
|
|
213
|
+
remediation: 'Infra-side issue (not your tenant privacy mode). Wait for ops to restore ' +
|
|
214
|
+
'the PiiScrubberService, OR temporarily switch your tenant to permissive via ' +
|
|
215
|
+
'`pugi config set privacy=permissive`.',
|
|
216
|
+
};
|
|
217
|
+
}
|
|
199
218
|
if (parsed?.code === 'privacy_strict_upstream_blocked' || parsed?.code === 'privacy_blocked') {
|
|
200
219
|
return {
|
|
201
220
|
stop: 'error',
|
package/dist/runtime/cli.js
CHANGED
|
@@ -763,6 +763,28 @@ function parseArgs(argv) {
|
|
|
763
763
|
}
|
|
764
764
|
flags.maxTurns = parsed;
|
|
765
765
|
}
|
|
766
|
+
else if (arg.startsWith('--commit=')) {
|
|
767
|
+
// `pugi review --triple --commit <SHA>` activates the multi-
|
|
768
|
+
// provider routing path against a specific revision.
|
|
769
|
+
flags.commit = arg.slice('--commit='.length);
|
|
770
|
+
}
|
|
771
|
+
else if (arg === '--commit') {
|
|
772
|
+
const next = argv[index + 1];
|
|
773
|
+
if (!next)
|
|
774
|
+
throw new Error('--commit requires a SHA or ref');
|
|
775
|
+
flags.commit = next;
|
|
776
|
+
index += 1;
|
|
777
|
+
}
|
|
778
|
+
else if (arg.startsWith('--base=')) {
|
|
779
|
+
flags.base = arg.slice('--base='.length);
|
|
780
|
+
}
|
|
781
|
+
else if (arg === '--base') {
|
|
782
|
+
const next = argv[index + 1];
|
|
783
|
+
if (!next)
|
|
784
|
+
throw new Error('--base requires a ref');
|
|
785
|
+
flags.base = next;
|
|
786
|
+
index += 1;
|
|
787
|
+
}
|
|
766
788
|
else {
|
|
767
789
|
args.push(arg);
|
|
768
790
|
}
|
|
@@ -830,6 +852,9 @@ async function help(_args, flags, _session) {
|
|
|
830
852
|
'',
|
|
831
853
|
'Review gate:',
|
|
832
854
|
' pugi review --triple Prepare the Anvil-backed triple-review gate.',
|
|
855
|
+
' pugi review --triple --commit <SHA>',
|
|
856
|
+
' 3-model consensus via Anvil (Anthropic · OpenAI · Google).',
|
|
857
|
+
' Optional: --base <ref> | "<prompt>". Quota: 1 slot per call.',
|
|
833
858
|
' pugi review --consensus 3-model consensus review (codex · claude · deepseek).',
|
|
834
859
|
' Optional: --commit <sha> | --pr <num> | --branch <name>.',
|
|
835
860
|
' Exits 0 PASS · 1 WARN · 2 BLOCK.',
|
|
@@ -992,7 +1017,19 @@ export async function scaffoldPugiWorkspace(input) {
|
|
|
992
1017
|
}, created, skipped);
|
|
993
1018
|
writeJsonIfMissing(resolve(pugiDir, 'mcp.json'), {
|
|
994
1019
|
schema: 1,
|
|
995
|
-
|
|
1020
|
+
// 2026-05-27 dogfood: `servers` MUST be an object keyed by server
|
|
1021
|
+
// name (z.record(mcpServerConfigSchema) in
|
|
1022
|
+
// apps/pugi-cli/src/core/mcp/registry.ts:51). A bare `[]` array
|
|
1023
|
+
// here passed schema validation на pugi init exit но crashed
|
|
1024
|
+
// the next dispatch with
|
|
1025
|
+
// "MCP config at .pugi/mcp.json failed validation:
|
|
1026
|
+
// servers: Expected object, received array"
|
|
1027
|
+
// and the operator's first command after `pugi init` printed an
|
|
1028
|
+
// error banner before the actual reply. Empty object matches the
|
|
1029
|
+
// schema default and keeps the file forwards-compatible with
|
|
1030
|
+
// `pugi mcp install <name> ...` which merges into the same
|
|
1031
|
+
// record shape.
|
|
1032
|
+
servers: {},
|
|
996
1033
|
}, created, skipped);
|
|
997
1034
|
writeJsonIfMissing(resolve(pugiDir, 'index.json'), emptyIndex(), created, skipped);
|
|
998
1035
|
writeTextIfMissing(resolve(pugiDir, 'PUGI.md'), [
|
|
@@ -1414,10 +1451,20 @@ async function review(args, flags, session) {
|
|
|
1414
1451
|
// streaming UX and rubric-driven exit codes don't disturb the existing
|
|
1415
1452
|
// pugi-cli surfaces that depend on the old shape.
|
|
1416
1453
|
if (flags.consensus) {
|
|
1454
|
+
// 2026-05-27 (Codex r0 P1 on PR #489): pass the globally-parsed
|
|
1455
|
+
// --commit / --base flags to consensus so `pugi review --consensus
|
|
1456
|
+
// --commit X` reviews the requested SHA instead of silently falling
|
|
1457
|
+
// back to the working-tree diff. parseConsensusArgs gives the inline
|
|
1458
|
+
// args (`--commit Y` after the command name) precedence; the
|
|
1459
|
+
// fallback only fires when `args` does not carry the token.
|
|
1417
1460
|
const exitCode = await runReviewConsensus(args, {
|
|
1418
1461
|
cwd: root,
|
|
1419
1462
|
config: resolveRuntimeConfig(),
|
|
1420
1463
|
json: flags.json,
|
|
1464
|
+
flagsFallback: {
|
|
1465
|
+
...(flags.commit ? { commit: flags.commit } : {}),
|
|
1466
|
+
...(flags.base ? { base: flags.base } : {}),
|
|
1467
|
+
},
|
|
1421
1468
|
emit: (line) => {
|
|
1422
1469
|
if (!flags.json)
|
|
1423
1470
|
process.stdout.write(line);
|
|
@@ -1429,6 +1476,15 @@ async function review(args, flags, session) {
|
|
|
1429
1476
|
process.exitCode = exitCode;
|
|
1430
1477
|
return;
|
|
1431
1478
|
}
|
|
1479
|
+
if (flags.triple && flags.commit) {
|
|
1480
|
+
// CEO directive 2026-05-27: `pugi review --triple --commit <SHA>`
|
|
1481
|
+
// dispatches to the customer-facing 3-model consensus path through
|
|
1482
|
+
// Anvil's already-paid Anthropic / OpenAI / Google routes. Replaces
|
|
1483
|
+
// the dev-only Codex/Claude/Gemini OAuth CLIs the `/triple-review`
|
|
1484
|
+
// skill uses.
|
|
1485
|
+
await performTripleProviderReview(root, session, flags, prompt);
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1432
1488
|
if (flags.triple && flags.remote) {
|
|
1433
1489
|
await performRemoteTripleReview(root, session, flags, prompt);
|
|
1434
1490
|
return;
|
|
@@ -1866,6 +1922,274 @@ async function performRemoteTripleReview(root, session, flags, prompt) {
|
|
|
1866
1922
|
.join('\n'));
|
|
1867
1923
|
process.exitCode = outcome.exitCode;
|
|
1868
1924
|
}
|
|
1925
|
+
/**
|
|
1926
|
+
* `pugi review --triple --commit <SHA>` — customer-facing 3-model
|
|
1927
|
+
* consensus review via Anvil multi-provider routing.
|
|
1928
|
+
*
|
|
1929
|
+
* Dispatches the same diff to Anthropic / OpenAI / Google models
|
|
1930
|
+
* (routed through Anvil's already-paid fleet, NOT OAuth-bound dev
|
|
1931
|
+
* CLIs) and renders the per-reviewer verdict + cross-model
|
|
1932
|
+
* disagreement summary at the end. Quota: one `reviewPerMonth` slot
|
|
1933
|
+
* per call regardless of provider count — the controller-level
|
|
1934
|
+
* `@QuotaGated('reviewPerMonth')` decorator enforces single-slot
|
|
1935
|
+
* debit (see apps/admin-api/src/pugi/pugi.controller.ts).
|
|
1936
|
+
*
|
|
1937
|
+
* CEO directive 2026-05-27: replaces the dev-only `/triple-review`
|
|
1938
|
+
* skill's Codex/Claude/Gemini OAuth dependency with a customer-
|
|
1939
|
+
* runnable Pugi product surface. Dogfood loop: Pugi reviews Pugi PRs.
|
|
1940
|
+
*/
|
|
1941
|
+
async function performTripleProviderReview(root, session, flags, prompt) {
|
|
1942
|
+
const config = resolveRuntimeConfig();
|
|
1943
|
+
const artifactDir = createArtifactDir(root, prompt || 'triple-providers');
|
|
1944
|
+
const requestPath = resolve(artifactDir, 'triple-review-request.json');
|
|
1945
|
+
const resultPath = resolve(artifactDir, 'triple-review-result.json');
|
|
1946
|
+
const summaryPath = resolve(artifactDir, 'triple-review.md');
|
|
1947
|
+
const toolCallId = recordToolCall(session, 'review:triple-providers', prompt || `review ${flags.commit ?? 'HEAD'} via providers`);
|
|
1948
|
+
// Resolve base ref. CLI flag wins over settings → so an operator
|
|
1949
|
+
// can target a specific integration branch without editing settings.
|
|
1950
|
+
const settings = loadSettings(root);
|
|
1951
|
+
const baseRef = flags.base ?? resolveBaseRef(root, settings) ?? 'origin/main';
|
|
1952
|
+
// Normalise both the commit and the base to short SHAs so the audit
|
|
1953
|
+
// log stores a stable reference even if branches move.
|
|
1954
|
+
const commitRef = flags.commit ?? 'HEAD';
|
|
1955
|
+
// 2026-05-27 (Codex r0 P2 on PR #489): safeGit returns '' on a bad ref
|
|
1956
|
+
// (it swallows the git exit code so callers don't have to wrap every
|
|
1957
|
+
// probe). Without an explicit refusal, a misspelled --commit or --base
|
|
1958
|
+
// produced an EMPTY diff that the gate then PASSED — operators saw a
|
|
1959
|
+
// green review for changes that were never reviewed. Resolve both refs
|
|
1960
|
+
// through `rev-parse --verify` first; an empty result is a hard error.
|
|
1961
|
+
const verifiedCommit = safeGit(root, ['rev-parse', '--verify', commitRef]).trim();
|
|
1962
|
+
if (!verifiedCommit) {
|
|
1963
|
+
throw new Error(`pugi review --triple: cannot resolve --commit '${commitRef}' — ` +
|
|
1964
|
+
`check the SHA or branch name. ` +
|
|
1965
|
+
`Refusing to submit an empty diff for review.`);
|
|
1966
|
+
}
|
|
1967
|
+
const verifiedBase = safeGit(root, ['rev-parse', '--verify', baseRef]).trim();
|
|
1968
|
+
if (!verifiedBase) {
|
|
1969
|
+
throw new Error(`pugi review --triple: cannot resolve --base '${baseRef}' — ` +
|
|
1970
|
+
`check the ref or set base via 'pugi config set review.base=<ref>'. ` +
|
|
1971
|
+
`Refusing to submit an empty diff for review.`);
|
|
1972
|
+
}
|
|
1973
|
+
const resolvedCommit = safeGit(root, ['rev-parse', '--short', commitRef]).trim() || commitRef;
|
|
1974
|
+
const mergeBase = safeGit(root, ['merge-base', baseRef, commitRef]).trim() || '';
|
|
1975
|
+
const diffRange = mergeBase || `${baseRef}..${commitRef}`;
|
|
1976
|
+
const diffArgs = ['diff', diffRange, commitRef, '--', '.', ...PROTECTED_DIFF_EXCLUDES];
|
|
1977
|
+
const diffStatArgs = ['diff', '--shortstat', diffRange, commitRef, '--', '.', ...PROTECTED_DIFF_EXCLUDES];
|
|
1978
|
+
const diffPatch = safeGit(root, diffArgs);
|
|
1979
|
+
const diffStats = parseDiffStats(safeGit(root, diffStatArgs));
|
|
1980
|
+
const requestBody = pugiTripleReviewRequestSchema.parse({
|
|
1981
|
+
schema: 1,
|
|
1982
|
+
workspace: {
|
|
1983
|
+
rootName: root.split('/').at(-1) ?? 'workspace',
|
|
1984
|
+
gitBranch: safeGit(root, ['branch', '--show-current']).trim() || null,
|
|
1985
|
+
gitHead: resolvedCommit || null,
|
|
1986
|
+
baseRef,
|
|
1987
|
+
dirty: Boolean(safeGit(root, ['status', '--short']).trim()),
|
|
1988
|
+
},
|
|
1989
|
+
diffPatch,
|
|
1990
|
+
diffStats,
|
|
1991
|
+
prompt: prompt || undefined,
|
|
1992
|
+
locale: 'en-US',
|
|
1993
|
+
reviewerPersona: 'oes-dev',
|
|
1994
|
+
commit: resolvedCommit,
|
|
1995
|
+
modelProviders: ['claude', 'gpt', 'gemini'],
|
|
1996
|
+
});
|
|
1997
|
+
writeFileSync(requestPath, `${JSON.stringify(requestBody, null, 2)}\n`, {
|
|
1998
|
+
encoding: 'utf8',
|
|
1999
|
+
mode: 0o600,
|
|
2000
|
+
});
|
|
2001
|
+
registerArtifact(root, {
|
|
2002
|
+
id: artifactIdFromDir(artifactDir),
|
|
2003
|
+
kind: 'triple-review',
|
|
2004
|
+
path: relative(root, artifactDir),
|
|
2005
|
+
sessionId: session.id,
|
|
2006
|
+
createdAt: new Date().toISOString(),
|
|
2007
|
+
files: ['triple-review-request.json'],
|
|
2008
|
+
});
|
|
2009
|
+
if (!config) {
|
|
2010
|
+
const reason = 'No active Pugi credentials. Run `pugi login --token <PAT>` or set PUGI_API_KEY for CI use.';
|
|
2011
|
+
recordToolResult(session, toolCallId, 'error', reason);
|
|
2012
|
+
writeFileSync(summaryPath, buildTripleReviewMarkdown({
|
|
2013
|
+
prompt,
|
|
2014
|
+
requestPath: relative(root, requestPath),
|
|
2015
|
+
verdict: null,
|
|
2016
|
+
reason,
|
|
2017
|
+
response: null,
|
|
2018
|
+
}), { encoding: 'utf8', mode: 0o600 });
|
|
2019
|
+
writeOutput(flags, {
|
|
2020
|
+
status: 'auth_missing',
|
|
2021
|
+
request: relative(root, requestPath),
|
|
2022
|
+
summary: relative(root, summaryPath),
|
|
2023
|
+
}, [
|
|
2024
|
+
'Pugi triple-provider review request prepared but not sent — no active credentials.',
|
|
2025
|
+
`Request: ${relative(root, requestPath)}`,
|
|
2026
|
+
`Run \`pugi login --token <PAT>\` (or export PUGI_API_KEY for CI) then retry \`pugi review --triple --commit ${resolvedCommit}\`.`,
|
|
2027
|
+
].join('\n'));
|
|
2028
|
+
process.exitCode = 5;
|
|
2029
|
+
return;
|
|
2030
|
+
}
|
|
2031
|
+
const submitResult = await submitTripleReview(config, requestBody);
|
|
2032
|
+
if (submitResult.status !== 'ok') {
|
|
2033
|
+
const outcome = describeSubmitFailure(submitResult);
|
|
2034
|
+
writeFileSync(summaryPath, buildTripleReviewMarkdown({
|
|
2035
|
+
prompt,
|
|
2036
|
+
requestPath: relative(root, requestPath),
|
|
2037
|
+
verdict: null,
|
|
2038
|
+
reason: outcome.message,
|
|
2039
|
+
response: null,
|
|
2040
|
+
}), { encoding: 'utf8', mode: 0o600 });
|
|
2041
|
+
recordToolResult(session, toolCallId, 'error', outcome.message);
|
|
2042
|
+
writeOutput(flags, {
|
|
2043
|
+
status: submitResult.status,
|
|
2044
|
+
code: submitResult.code,
|
|
2045
|
+
message: outcome.message,
|
|
2046
|
+
request: relative(root, requestPath),
|
|
2047
|
+
summary: relative(root, summaryPath),
|
|
2048
|
+
}, [
|
|
2049
|
+
outcome.headline,
|
|
2050
|
+
`Request: ${relative(root, requestPath)}`,
|
|
2051
|
+
`Summary: ${relative(root, summaryPath)}`,
|
|
2052
|
+
outcome.next ? `Next: ${outcome.next}` : '',
|
|
2053
|
+
]
|
|
2054
|
+
.filter(Boolean)
|
|
2055
|
+
.join('\n'));
|
|
2056
|
+
process.exitCode = outcome.exitCode;
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
const response = submitResult.response;
|
|
2060
|
+
persistTripleReviewResult(resultPath, response);
|
|
2061
|
+
writeFileSync(summaryPath, buildTripleReviewMarkdown({
|
|
2062
|
+
prompt,
|
|
2063
|
+
requestPath: relative(root, requestPath),
|
|
2064
|
+
verdict: response.verdict,
|
|
2065
|
+
reason: response.reason,
|
|
2066
|
+
response,
|
|
2067
|
+
}), { encoding: 'utf8', mode: 0o600 });
|
|
2068
|
+
recordToolResult(session, toolCallId, response.verdict === 'BLOCK' ? 'error' : 'success', `Verdict: ${response.verdict} (${response.reason})`);
|
|
2069
|
+
const verdictReport = renderTripleProviderVerdict({
|
|
2070
|
+
response,
|
|
2071
|
+
commit: resolvedCommit,
|
|
2072
|
+
baseRef,
|
|
2073
|
+
});
|
|
2074
|
+
writeOutput(flags, {
|
|
2075
|
+
status: 'completed',
|
|
2076
|
+
verdict: response.verdict,
|
|
2077
|
+
reason: response.reason,
|
|
2078
|
+
counts: response.counts,
|
|
2079
|
+
reviewerCount: response.reviewerCount,
|
|
2080
|
+
effectiveTier: response.effectiveTier,
|
|
2081
|
+
commit: resolvedCommit,
|
|
2082
|
+
baseRef,
|
|
2083
|
+
reviewers: response.reviewers.map((r) => ({
|
|
2084
|
+
provider: r.provider ?? null,
|
|
2085
|
+
model: r.model,
|
|
2086
|
+
declaredVerdict: r.declaredVerdict,
|
|
2087
|
+
findings: r.findings,
|
|
2088
|
+
latencyMs: r.latencyMs,
|
|
2089
|
+
tokensUsed: r.tokensUsed,
|
|
2090
|
+
error: r.error,
|
|
2091
|
+
})),
|
|
2092
|
+
result: relative(root, resultPath),
|
|
2093
|
+
summary: relative(root, summaryPath),
|
|
2094
|
+
}, verdictReport);
|
|
2095
|
+
if (response.verdict === 'BLOCK') {
|
|
2096
|
+
process.exitCode = 9;
|
|
2097
|
+
}
|
|
2098
|
+
else if (response.verdict === 'WARN') {
|
|
2099
|
+
process.exitCode = 1;
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
/**
|
|
2103
|
+
* Pretty-printer for the `pugi review --triple --commit <SHA>` verdict.
|
|
2104
|
+
* Mirrors the `/triple-review` skill's verdict block (per-reviewer
|
|
2105
|
+
* counts table → final GATE line → per-reviewer verbatim → cross-
|
|
2106
|
+
* model disagreement summary → tokens/cost note) so the output is
|
|
2107
|
+
* familiar to operators who already use the dev-only skill.
|
|
2108
|
+
*/
|
|
2109
|
+
export function renderTripleProviderVerdict(input) {
|
|
2110
|
+
const { response, commit, baseRef } = input;
|
|
2111
|
+
const divider = '═'.repeat(68);
|
|
2112
|
+
const subDivider = '─'.repeat(68);
|
|
2113
|
+
// Per-reviewer counts table.
|
|
2114
|
+
const reviewerRows = response.reviewers.map((reviewer) => {
|
|
2115
|
+
const c = { P0: 0, P1: 0, P2: 0, P3: 0 };
|
|
2116
|
+
for (const f of reviewer.findings)
|
|
2117
|
+
c[f.severity] += 1;
|
|
2118
|
+
const status = reviewer.error
|
|
2119
|
+
? 'ERROR'
|
|
2120
|
+
: reviewer.declaredVerdict ?? 'UNKNOWN';
|
|
2121
|
+
const label = reviewer.provider
|
|
2122
|
+
? reviewer.provider.toUpperCase().padEnd(8)
|
|
2123
|
+
: reviewer.model.slice(0, 8).padEnd(8);
|
|
2124
|
+
return ` ${label} ${pad(c.P0)} ${pad(c.P1)} ${pad(c.P2)} ${pad(c.P3)} ${status}`;
|
|
2125
|
+
});
|
|
2126
|
+
// Cross-model disagreement: list severities flagged by 1 of N but not
|
|
2127
|
+
// the others. Surfaces the "highest-signal moment" per the skill.
|
|
2128
|
+
const disagreements = [];
|
|
2129
|
+
const allFindings = response.reviewers.flatMap((r) => r.findings.map((f) => ({
|
|
2130
|
+
provider: r.provider ?? r.model,
|
|
2131
|
+
severity: f.severity,
|
|
2132
|
+
line: f.line,
|
|
2133
|
+
issue: f.issue,
|
|
2134
|
+
})));
|
|
2135
|
+
const p1Flaggers = new Set(response.reviewers
|
|
2136
|
+
.filter((r) => r.findings.some((f) => f.severity === 'P1'))
|
|
2137
|
+
.map((r) => r.provider ?? r.model));
|
|
2138
|
+
if (p1Flaggers.size === 1) {
|
|
2139
|
+
const sole = [...p1Flaggers][0];
|
|
2140
|
+
disagreements.push(`Only ${sole} flagged a P1 — examine the disagreement, often the highest-signal moment.`);
|
|
2141
|
+
}
|
|
2142
|
+
const p0Flaggers = new Set(response.reviewers
|
|
2143
|
+
.filter((r) => r.findings.some((f) => f.severity === 'P0'))
|
|
2144
|
+
.map((r) => r.provider ?? r.model));
|
|
2145
|
+
if (p0Flaggers.size > 0 && p0Flaggers.size < response.reviewers.length) {
|
|
2146
|
+
disagreements.push(`P0 flagged by ${[...p0Flaggers].join(', ')} but not ${response.reviewers
|
|
2147
|
+
.filter((r) => !p0Flaggers.has(r.provider ?? r.model))
|
|
2148
|
+
.map((r) => r.provider ?? r.model)
|
|
2149
|
+
.join(', ')} — verify the finding before merging.`);
|
|
2150
|
+
}
|
|
2151
|
+
// Tokens / cost summary. Tokens are best-effort (some providers
|
|
2152
|
+
// return null). Cost is a placeholder pending billing wire-up; we
|
|
2153
|
+
// surface the quota note inline so the operator knows it counts as
|
|
2154
|
+
// one slot, not three.
|
|
2155
|
+
const totalTokens = response.reviewers.reduce((sum, r) => sum + (r.tokensUsed ?? 0), 0);
|
|
2156
|
+
// Verbatim reviewer outputs. Each section gets a header so operators
|
|
2157
|
+
// can scroll quickly and copy any individual reviewer's text into
|
|
2158
|
+
// their own notes / triage doc.
|
|
2159
|
+
const reviewerSections = response.reviewers.map((reviewer) => {
|
|
2160
|
+
const label = reviewer.provider
|
|
2161
|
+
? reviewer.provider.toUpperCase()
|
|
2162
|
+
: reviewer.model;
|
|
2163
|
+
const body = reviewer.error
|
|
2164
|
+
? `(reviewer errored: ${reviewer.error})`
|
|
2165
|
+
: reviewer.rawContent.trim() || '(empty response)';
|
|
2166
|
+
return [subDivider, `${label} SAYS (${reviewer.model}):`, '', body].join('\n');
|
|
2167
|
+
});
|
|
2168
|
+
return [
|
|
2169
|
+
`PUGI TRIPLE-PROVIDER REVIEW — commit ${commit} vs ${baseRef}`,
|
|
2170
|
+
divider,
|
|
2171
|
+
'',
|
|
2172
|
+
` P0 P1 P2 P3 Status`,
|
|
2173
|
+
...reviewerRows,
|
|
2174
|
+
'',
|
|
2175
|
+
`GATE: ${response.verdict}`,
|
|
2176
|
+
`Reason: ${response.reason}`,
|
|
2177
|
+
'',
|
|
2178
|
+
...reviewerSections,
|
|
2179
|
+
'',
|
|
2180
|
+
subDivider,
|
|
2181
|
+
'CROSS-MODEL DISAGREEMENT:',
|
|
2182
|
+
disagreements.length === 0
|
|
2183
|
+
? ' (none — all reviewers agreed within rubric tolerance)'
|
|
2184
|
+
: disagreements.map((d) => ` - ${d}`).join('\n'),
|
|
2185
|
+
'',
|
|
2186
|
+
`Tokens: ~${totalTokens} total across ${response.reviewers.length} reviewers`,
|
|
2187
|
+
'Quota: charged as 1 review slot (multi-provider counts as a single call).',
|
|
2188
|
+
].join('\n');
|
|
2189
|
+
}
|
|
2190
|
+
function pad(n) {
|
|
2191
|
+
return String(n).padStart(2, ' ');
|
|
2192
|
+
}
|
|
1869
2193
|
function describeSubmitFailure(result) {
|
|
1870
2194
|
switch (result.status) {
|
|
1871
2195
|
case 'endpoint_missing':
|
|
@@ -40,8 +40,23 @@ import { aggregate, exitCodeFor, reviewerVerdictFromRaw, } from '../../core/cons
|
|
|
40
40
|
* `--branch <name>` / `--branch=<name>`
|
|
41
41
|
* `--base <ref>` / `--base=<ref>` (override default origin/main)
|
|
42
42
|
*/
|
|
43
|
-
export function parseConsensusArgs(args
|
|
43
|
+
export function parseConsensusArgs(args,
|
|
44
|
+
/**
|
|
45
|
+
* 2026-05-27 (Codex r0 P1 on PR #489): cli.ts now parses --commit /
|
|
46
|
+
* --base in the GLOBAL flag pass for the new triple-provider path.
|
|
47
|
+
* Those tokens are consumed BEFORE this function sees `args`, so
|
|
48
|
+
* `pugi review --consensus --commit X` would silently fall back to
|
|
49
|
+
* the default diff and review the wrong changes. Pass the global
|
|
50
|
+
* flags through here so consensus picks them up when present.
|
|
51
|
+
* Inline `--commit`/`--base` tokens in args still win — explicit
|
|
52
|
+
* caller intent is preserved.
|
|
53
|
+
*/
|
|
54
|
+
fallback) {
|
|
44
55
|
const spec = {};
|
|
56
|
+
if (fallback?.commit)
|
|
57
|
+
spec.commit = fallback.commit;
|
|
58
|
+
if (fallback?.base)
|
|
59
|
+
spec.baseRef = fallback.base;
|
|
45
60
|
for (let i = 0; i < args.length; i += 1) {
|
|
46
61
|
const arg = args[i] ?? '';
|
|
47
62
|
const equalsIdx = arg.indexOf('=');
|
|
@@ -119,7 +134,7 @@ export async function runReviewConsensus(args, ctx) {
|
|
|
119
134
|
// exit 2 — same as BLOCK because the gate could not even run.
|
|
120
135
|
let captured;
|
|
121
136
|
try {
|
|
122
|
-
const spec = parseConsensusArgs(args);
|
|
137
|
+
const spec = parseConsensusArgs(args, ctx.flagsFallback);
|
|
123
138
|
captured = captureDiff({ ...spec, cwd: ctx.cwd });
|
|
124
139
|
}
|
|
125
140
|
catch (error) {
|
package/dist/runtime/version.js
CHANGED
|
@@ -44,7 +44,7 @@ export function sanitizeSemver(raw) {
|
|
|
44
44
|
* during import). When bumping the CLI version BOTH literals must be
|
|
45
45
|
* updated; the release smoke-test (`pack:smoke`) verifies they agree.
|
|
46
46
|
*/
|
|
47
|
-
export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.
|
|
47
|
+
export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.14');
|
|
48
48
|
/**
|
|
49
49
|
* Outbound: the CLI's installed semver. Read at request time by
|
|
50
50
|
* `version-interceptor.ts` and injected on every `fetch` call.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pugi/cli",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.14",
|
|
4
4
|
"description": "Pugi CLI - terminal-native software execution system",
|
|
5
5
|
"homepage": "https://pugi.io",
|
|
6
6
|
"repository": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"undici": "^8.3.0",
|
|
55
55
|
"zod": "^3.23.0",
|
|
56
56
|
"@pugi/personas": "0.1.2",
|
|
57
|
-
"@pugi/sdk": "0.1.0-beta.
|
|
57
|
+
"@pugi/sdk": "0.1.0-beta.14"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/node": "^22.0.0",
|