great-cto 3.71.1 → 3.73.0
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/board/.claude-plugin/plugin.json +1 -1
- package/board/agents/architect.md +1 -1
- package/board/agents/db-migration-reviewer.md +3 -2
- package/board/agents/design-advisor.md +1 -1
- package/board/agents/devops.md +1 -1
- package/board/agents/growth-engineer.md +1 -1
- package/board/agents/knowledge-extractor.md +1 -1
- package/board/agents/l3-support.md +1 -1
- package/board/agents/performance-engineer.md +1 -1
- package/board/agents/pm.md +5 -1
- package/board/agents/qa-engineer.md +4 -4
- package/board/agents/security-officer.md +2 -2
- package/board/agents/smart-contract-auditor.md +1 -1
- package/board/scripts/codex-pipeline.mjs +42 -7
- package/board/scripts/lib/approval-level.mjs +4 -2
- package/board/scripts/lib/cost-meter.mjs +34 -2
- package/board/scripts/lib/cross-model-review.mjs +107 -9
- package/board/scripts/lib/kimi-review-profile.mjs +551 -0
- package/board/scripts/lib/kimi-review-store.mjs +469 -0
- package/board/scripts/lib/work-publication.mjs +10 -9
- package/board/shared/pipeline.toml +7 -1
- package/dist/main.js +82 -16
- package/dist/release-activation.mjs +3 -1
- package/dist/telemetry.js +2 -2
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "great-cto",
|
|
3
3
|
"description": "You already have the agent. This is everything around it. great_cto runs Claude Code as a pipeline of 72 specialist agents \u2014 an independent model checks each stage before the next builds on it, spending caps refuse rather than warn, and three decisions stay yours: what gets built, how, and whether it ships.",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.73.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Alexander Velikiy",
|
|
7
7
|
"url": "https://hashnode.com/@Greatcto"
|
|
@@ -376,7 +376,7 @@ constraint in the new ARCH doc — document why this design choice was not taken
|
|
|
376
376
|
ARCHETYPE=$(grep "^archetype:" .great_cto/PROJECT.md 2>/dev/null | awk '{print $2}'); ARCHETYPE=${ARCHETYPE:-web-service}
|
|
377
377
|
PROJECT_SIZE=$(grep "^project_size:" .great_cto/PROJECT.md 2>/dev/null | awk '{print $2}'); PROJECT_SIZE=${PROJECT_SIZE:-medium}
|
|
378
378
|
SECURITY_GATE=$(grep "^security-gate:" .great_cto/PROJECT.md 2>/dev/null | awk '{print $2}'); SECURITY_GATE=${SECURITY_GATE:-conditional}
|
|
379
|
-
COMPLIANCE=$(grep "^compliance:" .great_cto/PROJECT.md 2>/dev/null | sed 's/compliance: //'
|
|
379
|
+
COMPLIANCE=$(grep "^compliance:" .great_cto/PROJECT.md 2>/dev/null | sed 's/compliance: //'); COMPLIANCE=${COMPLIANCE:-[]}
|
|
380
380
|
```
|
|
381
381
|
Read ARCHETYPES.md for QA strategy, deploy method, and thresholds matching `$ARCHETYPE`. Use these as constraints for the architecture design.
|
|
382
382
|
- **Greenfield check** — determines how deep to read existing code:
|
|
@@ -48,8 +48,9 @@ fi
|
|
|
48
48
|
echo "Migrations to review:"
|
|
49
49
|
echo "$MIGRATIONS"
|
|
50
50
|
|
|
51
|
-
DB_ENGINE=$(grep "^db:" .great_cto/PROJECT.md 2>/dev/null | awk '{print $2}'
|
|
52
|
-
|
|
51
|
+
DB_ENGINE=$(grep "^db:" .great_cto/PROJECT.md 2>/dev/null | awk '{print $2}')
|
|
52
|
+
[ -n "$DB_ENGINE" ] || DB_ENGINE=$(grep -oE "postgresql|mysql|sqlite|aurora|cockroach|planetscale" .great_cto/PROJECT.md 2>/dev/null | head -1)
|
|
53
|
+
DB_ENGINE=${DB_ENGINE:-unknown}
|
|
53
54
|
|
|
54
55
|
# Slug from latest ARCH doc; date fallback must be an explicit branch —
|
|
55
56
|
# `|| echo` after a pipeline never fires (basename "" exits 0 with empty output)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: design-advisor
|
|
3
|
-
description: Use after architect
|
|
3
|
+
description: Use after architect and before pm, for any UI-bearing feature (landing pages, dashboards, admin panels, web apps, React Native apps). Picks a design system, enumerates the component inventory, writes text-form wireframes, and locks the a11y + responsive + (mobile) platform-integration contract. Outputs docs/design/DESIGN-{slug}.md. Plan altitude only — never writes implementation code.
|
|
4
4
|
model: claude-opus-5
|
|
5
5
|
authority: autonomous
|
|
6
6
|
tools: Read, Write, Glob, Grep, WebFetch, Bash(git:*), Bash(bd:*), Bash(ls:*), Bash(cat:*), Bash(find:*), Bash(touch:*), Bash(head:*), Bash(tail:*), Bash(grep:*), Bash(wc:*), Bash(date:*), Bash(printf:*), Bash(echo:*), Bash(mkdir:*), memory_20250929, advisor_20260301
|
package/board/agents/devops.md
CHANGED
|
@@ -572,7 +572,7 @@ step for that condition before proceeding to Step 1 (gate:ship check).
|
|
|
572
572
|
ESTIMATED=${ESTIMATED_RAW:-0}
|
|
573
573
|
CONSOLE_URL=$(grep "console-url:" .great_cto/PROJECT.md 2>/dev/null | awk '{print $2}')
|
|
574
574
|
SERVICE=$(grep "^service:" .great_cto/PROJECT.md 2>/dev/null | awk '{print $2}' || basename "$PWD")
|
|
575
|
-
FEATURE=$(basename "$ARCH_DOC" .md 2>/dev/null | sed 's/^ARCH-//'
|
|
575
|
+
FEATURE=$(basename "$ARCH_DOC" .md 2>/dev/null | sed 's/^ARCH-//'); FEATURE=${FEATURE:-unknown}
|
|
576
576
|
|
|
577
577
|
COST_LOG=.great_cto/cost-history.log
|
|
578
578
|
[ ! -f "$COST_LOG" ] && printf '# Cost history — append only. Format: ISO8601 | service | estimated_usd_month | actual_usd_month | source | feature\n' > "$COST_LOG"
|
|
@@ -91,7 +91,7 @@ ARCH_FILE=$(ls -t docs/architecture/ARCH-*.md 2>/dev/null | head -1)
|
|
|
91
91
|
SLUG=$(basename "$ARCH_FILE" .md | sed 's/^ARCH-//')
|
|
92
92
|
|
|
93
93
|
ARCHETYPE=$(grep "^archetype:" .great_cto/PROJECT.md 2>/dev/null | awk '{print $2}')
|
|
94
|
-
GROWTH_GOAL=$(grep "^growth-goal:" .great_cto/PROJECT.md 2>/dev/null | sed 's/growth-goal: //'
|
|
94
|
+
GROWTH_GOAL=$(grep "^growth-goal:" .great_cto/PROJECT.md 2>/dev/null | sed 's/growth-goal: //'); GROWTH_GOAL=${GROWTH_GOAL:-not specified}
|
|
95
95
|
# Is there an analytics/connector layer we can read from?
|
|
96
96
|
CONNECT=$(ls docs/connectors/CONNECT-*.md 2>/dev/null | head -1)
|
|
97
97
|
echo "slug=$SLUG archetype=$ARCHETYPE goal='$GROWTH_GOAL' connector=${CONNECT:-none}"
|
|
@@ -28,7 +28,7 @@ Run in parallel:
|
|
|
28
28
|
cat .great_cto/lessons.md 2>/dev/null || echo "(no lessons yet)"
|
|
29
29
|
|
|
30
30
|
# Cross-project decisions (supplement)
|
|
31
|
-
|
|
31
|
+
D=$(head -300 ~/.great_cto/decisions.md 2>/dev/null); [ -n "$D" ] && printf '%s\n' "$D" || echo "(none)"
|
|
32
32
|
|
|
33
33
|
# Session log pattern lines across all sessions
|
|
34
34
|
grep -h "^## pattern:" .great_cto/logs/session-*-end.md 2>/dev/null \
|
|
@@ -327,7 +327,7 @@ node "$PLUGIN_DIR/scripts/lib/pattern-lookup.mjs" --role incident
|
|
|
327
327
|
|
|
328
328
|
# Priority 4: systemd / journalctl
|
|
329
329
|
if [ -z "$LOG_PATH" ] && [ -z "$APP_CONTAINER" ]; then
|
|
330
|
-
journalctl --since "30 min ago" -p err 2>/dev/null | tail -50 || \
|
|
330
|
+
{ J=$(journalctl --since "30 min ago" -p err 2>/dev/null | tail -50); [ -n "$J" ] && printf '%s\n' "$J"; } || \
|
|
331
331
|
echo "No log source found. Add 'error-log: /path/to/log' to PROJECT.md L3 section."
|
|
332
332
|
fi
|
|
333
333
|
|
|
@@ -76,7 +76,7 @@ ARCH_FILE=$(ls -t docs/architecture/ARCH-*.md 2>/dev/null | head -1)
|
|
|
76
76
|
[ -z "$ARCH_FILE" ] && { echo "BLOCKED: no ARCH doc" >&2; exit 1; }
|
|
77
77
|
SLUG=$(basename "$ARCH_FILE" .md | sed 's/^ARCH-//')
|
|
78
78
|
|
|
79
|
-
PERF_SLA=$(grep "^performance-sla:" .great_cto/PROJECT.md 2>/dev/null | sed 's/performance-sla: //'
|
|
79
|
+
PERF_SLA=$(grep "^performance-sla:" .great_cto/PROJECT.md 2>/dev/null | sed 's/performance-sla: //'); PERF_SLA=${PERF_SLA:-not specified}
|
|
80
80
|
MONTHLY_RPS=$(grep "^expected-rps:" .great_cto/PROJECT.md 2>/dev/null | awk '{print $2}'); MONTHLY_RPS=${MONTHLY_RPS:-unknown}
|
|
81
81
|
|
|
82
82
|
# Check for existing baseline
|
package/board/agents/pm.md
CHANGED
|
@@ -115,6 +115,10 @@ PHASE=$(grep "^phase:" .great_cto/PROJECT.md 2>/dev/null | awk '{print $2}'); PH
|
|
|
115
115
|
TEAM_SIZE=$(grep "^team-size:" .great_cto/PROJECT.md 2>/dev/null | awk '{print $2}'); TEAM_SIZE=${TEAM_SIZE:-1}
|
|
116
116
|
MONTHLY_BUDGET=$(grep "^monthly-budget-llm-usd:" .great_cto/PROJECT.md 2>/dev/null | awk '{print $2}')
|
|
117
117
|
|
|
118
|
+
# Design contract — design-advisor runs before pm on UI features; plan UI tasks against it
|
|
119
|
+
DESIGN_DOC=$(ls -t docs/design/DESIGN-*.md 2>/dev/null | head -1)
|
|
120
|
+
[ -n "$DESIGN_DOC" ] && echo "=== DESIGN CONTRACT: $DESIGN_DOC (components, a11y, responsive — one task per screen/component group) ==="
|
|
121
|
+
|
|
118
122
|
# Past lessons — calibrate cost/time estimates against actuals
|
|
119
123
|
if [ -f .great_cto/lessons.md ]; then
|
|
120
124
|
COST_LESSONS=$(grep -B1 -A4 "shape: B" .great_cto/lessons.md 2>/dev/null | head -20)
|
|
@@ -138,7 +142,7 @@ FEATURE_SLUG=$(basename "$ARCH_FILE" .md | sed 's/^ARCH-//' | tr '[:upper:]' '[:
|
|
|
138
142
|
|
|
139
143
|
# Goal ancestry (Paperclip pattern) — injected into every bd create call.
|
|
140
144
|
# Built AFTER FEATURE_SLUG is known (it interpolates the slug).
|
|
141
|
-
COMPLIANCE=$(grep "^compliance:" .great_cto/PROJECT.md 2>/dev/null | sed 's/compliance: //'
|
|
145
|
+
COMPLIANCE=$(grep "^compliance:" .great_cto/PROJECT.md 2>/dev/null | sed 's/compliance: //')
|
|
142
146
|
_COMP_CLEAN=$(echo "$COMPLIANCE" | sed 's/^\[none\]$//;s/^\[none, *\]//;s/, *\[none\]//')
|
|
143
147
|
GOAL_ANCESTRY="[archetype:${ARCHETYPE}]$([ -n "$_COMP_CLEAN" ] && echo " [compliance:${_COMP_CLEAN}]") [feature:${FEATURE_SLUG}] [phase:${PHASE}] | Why: see docs/plans/PLAN-${FEATURE_SLUG}.md"
|
|
144
148
|
|
|
@@ -286,9 +286,9 @@ PROJECT_SIZE=$(grep "^project_size:" .great_cto/PROJECT.md 2>/dev/null | awk '{p
|
|
|
286
286
|
|
|
287
287
|
```bash
|
|
288
288
|
ARCHETYPE=$(grep "^archetype:" .great_cto/PROJECT.md 2>/dev/null | awk '{print $2}'); ARCHETYPE=${ARCHETYPE:-web-service}
|
|
289
|
-
COMPLIANCE=$(grep "^compliance:" .great_cto/PROJECT.md 2>/dev/null | sed 's/compliance: //'
|
|
290
|
-
QA_EXTRAS=$(grep "^qa-extras:" .great_cto/PROJECT.md 2>/dev/null | sed 's/qa-extras: //'
|
|
291
|
-
PERF_SLA=$(grep "^performance-sla:" .great_cto/PROJECT.md 2>/dev/null | sed 's/performance-sla: //'
|
|
289
|
+
COMPLIANCE=$(grep "^compliance:" .great_cto/PROJECT.md 2>/dev/null | sed 's/compliance: //'); COMPLIANCE=${COMPLIANCE:-[]}
|
|
290
|
+
QA_EXTRAS=$(grep "^qa-extras:" .great_cto/PROJECT.md 2>/dev/null | sed 's/qa-extras: //'); QA_EXTRAS=${QA_EXTRAS:-[]}
|
|
291
|
+
PERF_SLA=$(grep "^performance-sla:" .great_cto/PROJECT.md 2>/dev/null | sed 's/performance-sla: //')
|
|
292
292
|
```
|
|
293
293
|
|
|
294
294
|
Read ARCHETYPES.md → find QA strategy row for `$ARCHETYPE`. This gives base QA plan.
|
|
@@ -362,7 +362,7 @@ PII column additions) that functional tests miss by design — the
|
|
|
362
362
|
specialist agent owns this.
|
|
363
363
|
|
|
364
364
|
```bash
|
|
365
|
-
BASE=$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null | sed 's|.*/||'
|
|
365
|
+
BASE=$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null | sed 's|.*/||'); BASE=${BASE:-main}
|
|
366
366
|
MIGRATION_HIT=$(git diff "$BASE"...HEAD --name-only 2>/dev/null | \
|
|
367
367
|
grep -E '(^|/)(migrations|alembic/versions|db/migrate|prisma/migrations|flyway|knex/migrations)/.*\.(sql|py|ts|js|rb)$' | head -5)
|
|
368
368
|
|
|
@@ -424,7 +424,7 @@ Continues from the original Workflow below — produces `CSO-{slug}-{date}.md`,
|
|
|
424
424
|
1b. **Read** `.great_cto/PROJECT.md` → get `archetype`, `type`, `stack`, and `compliance` params:
|
|
425
425
|
```bash
|
|
426
426
|
ARCHETYPE=$(grep "^archetype:" .great_cto/PROJECT.md 2>/dev/null | awk '{print $2}'); ARCHETYPE=${ARCHETYPE:-web-service}
|
|
427
|
-
COMPLIANCE=$(grep "^compliance:" .great_cto/PROJECT.md 2>/dev/null | sed 's/compliance: //'
|
|
427
|
+
COMPLIANCE=$(grep "^compliance:" .great_cto/PROJECT.md 2>/dev/null | sed 's/compliance: //'); COMPLIANCE=${COMPLIANCE:-[]}
|
|
428
428
|
```
|
|
429
429
|
Read ARCHETYPES.md → find security rules for `$ARCHETYPE`.
|
|
430
430
|
The `compliance:` list in PROJECT.md determines which checklists to run (see Step 5).
|
|
@@ -670,7 +670,7 @@ Observations (signal < 2 or no direct evidence): record in a separate `## Observ
|
|
|
670
670
|
the pipeline dispatcher and the board parse this line):
|
|
671
671
|
```bash
|
|
672
672
|
bash scripts/log-verdict.sh security-officer <APPROVED|BLOCKED> auto \
|
|
673
|
-
findings=P0:<n>,P1:<n>,P2:<n> triaged=<n> valid=<n> invalid=<n> feature=<slug> need=<implementer|decision> finding=<id>
|
|
673
|
+
findings=P0:<n>,P1:<n>,P2:<n> triaged=<n> valid=<n> invalid=<n> feature=<slug> report=docs/security/CSO-<YYYY-MM-DD>.md need=<implementer|decision> finding=<id>
|
|
674
674
|
```
|
|
675
675
|
On BLOCKED, `need` mirrors the report's `need` line: `implementer` for
|
|
676
676
|
"senior-dev fix <finding>", `decision` for "CTO waive risk on <finding>".
|
|
@@ -318,7 +318,7 @@ REPORT=docs/security/AUDIT-{slug}.md
|
|
|
318
318
|
[ -s "$REPORT" ] || { echo "STOP: no audit report at $REPORT — write it first." >&2; exit 1; }
|
|
319
319
|
|
|
320
320
|
bash scripts/log-verdict.sh smart-contract-auditor <PASS|FAIL|BLOCKED> auto \
|
|
321
|
-
feature=<slug> "
|
|
321
|
+
feature=<slug> "report=$REPORT" findings=C:<n>,H:<n>,M:<n> not_checked=<n> need=<implementer|decision> finding=<id>
|
|
322
322
|
```
|
|
323
323
|
|
|
324
324
|
In prepare mode add `package=docs/security/audit-package-{slug}` and
|
|
@@ -43,19 +43,54 @@ const save = state => {
|
|
|
43
43
|
renameSync(`${file}.tmp`, file);
|
|
44
44
|
observeWorkRun(state);
|
|
45
45
|
};
|
|
46
|
+
// Keep this local to the entrypoint: source controller and packaged CLI have
|
|
47
|
+
// separate runtime closures, but both must complete queued pipe writes.
|
|
48
|
+
function drainOutput(stream) {
|
|
49
|
+
if (stream.destroyed || stream.closed || stream.writableFinished) return Promise.resolve(true);
|
|
50
|
+
return new Promise(resolve => {
|
|
51
|
+
let settled = false;
|
|
52
|
+
const finish = complete => {
|
|
53
|
+
if (settled) return;
|
|
54
|
+
settled = true;
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
stream.off('error', completed);
|
|
57
|
+
stream.off('close', completed);
|
|
58
|
+
stream.off('finish', completed);
|
|
59
|
+
resolve(complete);
|
|
60
|
+
};
|
|
61
|
+
const completed = () => finish(true);
|
|
62
|
+
const timer = setTimeout(() => finish(false), 1000);
|
|
63
|
+
timer.unref();
|
|
64
|
+
stream.once('error', completed);
|
|
65
|
+
stream.once('close', completed);
|
|
66
|
+
stream.once('finish', completed);
|
|
67
|
+
if (!stream.writableEnded) {
|
|
68
|
+
try {
|
|
69
|
+
stream.write('', error => {
|
|
70
|
+
// The error event follows an errored write callback.
|
|
71
|
+
if (error) queueMicrotask(completed);
|
|
72
|
+
else completed();
|
|
73
|
+
});
|
|
74
|
+
} catch { completed(); }
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async function exitAfterOutput(code) {
|
|
79
|
+
// An unresolved promise and unref timer must not allow an early rc0.
|
|
80
|
+
process.exitCode = code || 1;
|
|
81
|
+
const completed = await Promise.all([drainOutput(process.stdout), drainOutput(process.stderr)]);
|
|
82
|
+
process.exit(completed.every(Boolean) ? code : code || 1);
|
|
83
|
+
}
|
|
84
|
+
|
|
46
85
|
let locked = null, projectLease = null, work = null, taskForFinish = null;
|
|
47
86
|
try {
|
|
48
87
|
if (command === 'doctor') {
|
|
49
88
|
const result = codexHostDoctor({ pluginRoot: resolve(value('--plugin-root') || join(dirname(fileURLToPath(import.meta.url)), '..')), store });
|
|
50
|
-
console.log(JSON.stringify(result, null, 2));
|
|
89
|
+
console.log(JSON.stringify(result, null, 2)); await exitAfterOutput(result.state === 'ready' ? 0 : 2);
|
|
51
90
|
}
|
|
52
91
|
if (command === 'list') {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
await new Promise((resolve, reject) => {
|
|
56
|
-
process.stdout.write(output, error => error ? reject(error) : resolve());
|
|
57
|
-
});
|
|
58
|
-
process.exit(0);
|
|
92
|
+
console.log(JSON.stringify(listCodexRuns({ root: value('--dir') ? realpathSync(resolve(value('--dir'))) : null, store }), null, 2));
|
|
93
|
+
await exitAfterOutput(0);
|
|
59
94
|
}
|
|
60
95
|
let state;
|
|
61
96
|
if (command === 'start') {
|
|
@@ -64,13 +64,15 @@ export const DEFAULT_LEVEL = 'gates-only';
|
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* Archetypes where a compliance/security sign-off is not the operator's to skip.
|
|
67
|
-
*
|
|
68
|
-
* a new level cannot become a bypass by omission.
|
|
67
|
+
* The floor is gate:security + gate:compliance (with gate:ship) at every level, so
|
|
68
|
+
* a new level cannot become a bypass by omission. It is not `strict`: it adds no
|
|
69
|
+
* gate:code. reference/approval-level.md lists this set and a test holds the two equal.
|
|
69
70
|
*/
|
|
70
71
|
const REGULATED = new Set([
|
|
71
72
|
'ai-system', 'agent-product', 'commerce', 'web3', 'iot-embedded', 'regulated',
|
|
72
73
|
'fintech', 'healthcare', 'insurance', 'legal', 'gov-public', 'defense-govcon',
|
|
73
74
|
]);
|
|
75
|
+
export const REGULATED_ARCHETYPES = Object.freeze([...REGULATED]);
|
|
74
76
|
const REGULATED_FLOOR = ['security', 'compliance'];
|
|
75
77
|
|
|
76
78
|
/**
|
|
@@ -60,6 +60,9 @@ export const DEFAULT_PRICES = {
|
|
|
60
60
|
// override via ~/.great_cto/model-prices.json or GREAT_CTO_MODEL_PRICES).
|
|
61
61
|
'moonshotai/kimi-k2': { input: 0.55, output: 2.2 },
|
|
62
62
|
'moonshotai/kimi-k3': { input: 3, output: 15 },
|
|
63
|
+
// Standard endpoint snapshot, 2026-10-24. Highspeed is a different rate.
|
|
64
|
+
'moonshotai/kimi-k3.1-pro': { input: 2, output: 10, cacheRead: 0.2 },
|
|
65
|
+
'moonshotai/kimi-k3.1-pro-20261023': { input: 2, output: 10, cacheRead: 0.2 },
|
|
63
66
|
// Read from OpenRouter's /models on 2026-08-27, not guessed. Until now these
|
|
64
67
|
// reported `priced: false` — correctly, and that honesty is why an eval run on
|
|
65
68
|
// glm-5.3-flash showed $0.000: not free, unpriced. Now they are priced exactly.
|
|
@@ -159,7 +162,7 @@ export function resolvePrice(model, prices = effectivePrices()) {
|
|
|
159
162
|
* 172 turns of `claude-fable-5` were billed at $0.00 for exactly that reason,
|
|
160
163
|
* and the total looked like a total rather than a total plus a hole.
|
|
161
164
|
*
|
|
162
|
-
* @returns {{usd:number, priced:boolean, assumed:boolean, source:string, model:string}}
|
|
165
|
+
* @returns {{usd:number|null, priced:boolean, assumed:boolean, source:string, model:string}}
|
|
163
166
|
*/
|
|
164
167
|
/**
|
|
165
168
|
* Cache-write dollars. Anthropic bills a 5-minute cache write at 1.25× the input
|
|
@@ -180,7 +183,30 @@ export function cacheWriteUsd(usage, inputPrice) {
|
|
|
180
183
|
return total * 1.25 * inputPrice;
|
|
181
184
|
}
|
|
182
185
|
|
|
186
|
+
const KIMI_STANDARD_MODELS = new Set(['moonshotai/kimi-k3.1-pro', 'moonshotai/kimi-k3.1-pro-20261023']);
|
|
187
|
+
|
|
188
|
+
function priceKimiUsage({ model, usage, prices }) {
|
|
189
|
+
const { price, source } = resolvePrice(model, prices);
|
|
190
|
+
const input = usage?.prompt_tokens;
|
|
191
|
+
const output = usage?.completion_tokens;
|
|
192
|
+
const cached = usage?.prompt_tokens_details?.cached_tokens;
|
|
193
|
+
const reasoning = usage?.completion_tokens_details?.reasoning_tokens;
|
|
194
|
+
const providerReportedUsd = typeof usage?.cost === 'number' && Number.isFinite(usage.cost) && usage.cost >= 0 ? usage.cost : null;
|
|
195
|
+
const base = { model, source, assumed: false, providerReportedUsd, costProvenance: 'calculated-standard-token-rates' };
|
|
196
|
+
if (source !== 'exact' || !price || ![price.input, price.output, price.cacheRead].every(n => Number.isFinite(n) && n >= 0)
|
|
197
|
+
|| ![input, output, cached, reasoning].every(n => Number.isSafeInteger(n) && n >= 0)
|
|
198
|
+
|| (Object.hasOwn(usage, 'total_tokens') && (!Number.isSafeInteger(usage.total_tokens) || usage.total_tokens !== input + output))
|
|
199
|
+
|| (Object.hasOwn(usage, 'cost') && providerReportedUsd === null)
|
|
200
|
+
|| cached > input || reasoning > output) {
|
|
201
|
+
return { ...base, usd: null, priced: false, usageState: 'unknown', costState: 'unknown' };
|
|
202
|
+
}
|
|
203
|
+
// OpenRouter includes cache reads in prompt and reasoning in completion.
|
|
204
|
+
const usd = ((input - cached) * price.input + cached * price.cacheRead + output * price.output) / 1_000_000;
|
|
205
|
+
return { ...base, usd, priced: true, usageState: 'known', costState: 'known' };
|
|
206
|
+
}
|
|
207
|
+
|
|
183
208
|
export function priceUsage({ model, usage, prices }) {
|
|
209
|
+
if (KIMI_STANDARD_MODELS.has(model)) return priceKimiUsage({ model, usage, prices: prices || effectivePrices() });
|
|
184
210
|
const { price, source } = resolvePrice(model, prices || effectivePrices());
|
|
185
211
|
if (!usage || !price) {
|
|
186
212
|
return { usd: 0, priced: false, assumed: false, source, model: model || '' };
|
|
@@ -199,9 +225,11 @@ export function priceUsage({ model, usage, prices }) {
|
|
|
199
225
|
* @param {string} opts.model
|
|
200
226
|
* @param {{input_tokens?:number, output_tokens?:number}} opts.usage Anthropic response.usage
|
|
201
227
|
* @param {object} [opts.prices] override table (for tests)
|
|
202
|
-
* @returns {number} USD
|
|
228
|
+
* @returns {number|null} USD; exact Kimi requires complete raw OpenRouter usage
|
|
229
|
+
* and returns null when unknown. Legacy models retain the numeric 0 fallback.
|
|
203
230
|
*/
|
|
204
231
|
export function costForUsage({ model, usage, prices }) {
|
|
232
|
+
if (KIMI_STANDARD_MODELS.has(model)) return priceKimiUsage({ model, usage, prices: prices || effectivePrices() }).usd;
|
|
205
233
|
if (!usage) return 0;
|
|
206
234
|
const p = priceForModel(model, prices);
|
|
207
235
|
if (!p) return 0;
|
|
@@ -246,6 +274,10 @@ function main(argv) {
|
|
|
246
274
|
return process.exit(2);
|
|
247
275
|
}
|
|
248
276
|
const cost = costForUsage({ model, usage: { input_tokens: inTok, output_tokens: outTok } });
|
|
277
|
+
if (cost === null) {
|
|
278
|
+
process.stderr.write('cost-meter: complete provider usage required — cost unknown\n');
|
|
279
|
+
return process.exit(2);
|
|
280
|
+
}
|
|
249
281
|
process.stdout.write(String(round4(cost)));
|
|
250
282
|
}
|
|
251
283
|
|
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
// node scripts/lib/cross-model-review.mjs --diff /tmp/d.diff --spec docs/architecture/ARCH-x.md
|
|
14
14
|
// GREAT_CTO_CROSS_REVIEW_MODEL=google/gemini-2.5-pro node ... --diff -
|
|
15
15
|
|
|
16
|
-
import { readFileSync } from 'node:fs';
|
|
16
|
+
import { readFileSync, lstatSync, openSync, closeSync, fstatSync, readSync, constants } from 'node:fs';
|
|
17
17
|
import { fileURLToPath } from 'node:url';
|
|
18
18
|
import { execFileSync } from 'node:child_process';
|
|
19
19
|
import { costForUsage, round4, resolvePrice } from './cost-meter.mjs';
|
|
20
20
|
import { resolveSecondOpinion, codexReview } from './second-opinion.mjs';
|
|
21
21
|
import { principalError } from './provider-exhaustion.mjs';
|
|
22
22
|
import { existsSync, appendFileSync, mkdirSync } from 'node:fs';
|
|
23
|
-
import { join } from 'node:path';
|
|
23
|
+
import { join, resolve, parse, sep } from 'node:path';
|
|
24
24
|
import { reviewInputBinding, parseLegacyReview } from './review-contract.mjs';
|
|
25
25
|
|
|
26
26
|
/**
|
|
@@ -34,9 +34,14 @@ import { reviewInputBinding, parseLegacyReview } from './review-contract.mjs';
|
|
|
34
34
|
export const EXIT = Object.freeze({ PASS: 0, BLOCK: 1, USAGE: 2, SKIPPED: 3 });
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* Which provider reviews, decided from
|
|
38
|
-
* an explicit `--provider`, then the project's `capabilities: second_opinion
|
|
39
|
-
*
|
|
37
|
+
* Which provider reviews, decided from two sources in a fixed order:
|
|
38
|
+
* an explicit `--provider`, then the project's `capabilities: second_opinion`.
|
|
39
|
+
*
|
|
40
|
+
* An exported OPENROUTER_API_KEY used to be a third source: with nothing
|
|
41
|
+
* declared, the key chose OpenRouter and the diff left the machine without
|
|
42
|
+
* anyone deciding it should (CSO SEC-2, 27.10). The key is a credential, not a
|
|
43
|
+
* decision — undeclared stays undeclared, and the skip line names the
|
|
44
|
+
* declaration that would turn the review on.
|
|
40
45
|
*
|
|
41
46
|
* Returns the resolver's four states plus `source`, so the log line can say
|
|
42
47
|
* why this provider and not another. Pure: `codex` and `env` are injected.
|
|
@@ -48,9 +53,9 @@ export function decideProvider({ argv = [], projectMd = '', codex = null, env =
|
|
|
48
53
|
return { ...r, source: '--provider' };
|
|
49
54
|
}
|
|
50
55
|
const fromProject = resolveSecondOpinion({ projectMd, codex, env });
|
|
51
|
-
if (fromProject.state
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
if (fromProject.state === 'undeclared' && env.OPENROUTER_API_KEY) {
|
|
57
|
+
return { ...fromProject, source: 'PROJECT.md',
|
|
58
|
+
why: `${fromProject.why}; OPENROUTER_API_KEY is set but does not choose the provider — declare \`second_opinion: openrouter\` or pass --provider openrouter` };
|
|
54
59
|
}
|
|
55
60
|
return { ...fromProject, source: 'PROJECT.md' };
|
|
56
61
|
}
|
|
@@ -128,6 +133,93 @@ export async function callOpenRouter({ apiKey, model, system, user, maxTokens =
|
|
|
128
133
|
|
|
129
134
|
function readArg(argv, name) { const i = argv.indexOf(name); return i > -1 ? argv[i + 1] : null; }
|
|
130
135
|
|
|
136
|
+
/** Read explicitly selected input without following symlinks or special files. */
|
|
137
|
+
function readProfileFile(file, root, cap) {
|
|
138
|
+
if (typeof file !== 'string' || !file || file === '-' || Buffer.byteLength(file) > 4096
|
|
139
|
+
|| file.split(/[\\/]/).includes('..') || /[\x00-\x1f\x7f]/.test(file)) throw new Error('invalid-input-file');
|
|
140
|
+
const target = resolve(root, file), parsed = parse(target);
|
|
141
|
+
const parts = target.slice(parsed.root.length).split(sep);
|
|
142
|
+
let current = parsed.root;
|
|
143
|
+
for (let i = 0; i < parts.length; i++) {
|
|
144
|
+
if (parts[i] === '.git' || /^\.env(?:\.|$)/i.test(parts[i])) throw new Error('invalid-input-file');
|
|
145
|
+
current = join(current, parts[i]);
|
|
146
|
+
const st = lstatSync(current);
|
|
147
|
+
if (st.isSymbolicLink() || (i < parts.length - 1 ? !st.isDirectory() : !st.isFile())) throw new Error('invalid-input-file');
|
|
148
|
+
}
|
|
149
|
+
const fd = openSync(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
150
|
+
try {
|
|
151
|
+
const st = fstatSync(fd);
|
|
152
|
+
if (!st.isFile() || st.size > cap) throw new Error('invalid-input-file');
|
|
153
|
+
const bytes = Buffer.alloc(cap + 1);
|
|
154
|
+
let size = 0, n;
|
|
155
|
+
while (size <= cap && (n = readSync(fd, bytes, size, cap + 1 - size, null)) > 0) size += n;
|
|
156
|
+
if (size > cap) throw new Error('invalid-input-file');
|
|
157
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bytes.subarray(0, size));
|
|
158
|
+
} finally { closeSync(fd); }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Check explicit runtime roots before any credential getter is evaluated. */
|
|
162
|
+
export function validateKimiRuntimePaths(root, operatorRoot) {
|
|
163
|
+
for (const [directory, allowMissing] of [[root, false], [operatorRoot, true]]) {
|
|
164
|
+
if (typeof directory !== 'string' || !directory || Buffer.byteLength(directory) > 4096
|
|
165
|
+
|| /[\x00-\x1f\x7f]/.test(directory)) throw new Error('invalid-runtime-path');
|
|
166
|
+
const target = resolve(directory), parsed = parse(target);
|
|
167
|
+
let current = parsed.root, missing = false;
|
|
168
|
+
for (const part of target.slice(parsed.root.length).split(sep).filter(Boolean)) {
|
|
169
|
+
if (Buffer.byteLength(part) > 255 || part === '.git' || /^\.env(?:\.|$)/i.test(part)) throw new Error('invalid-runtime-path');
|
|
170
|
+
current = join(current, part);
|
|
171
|
+
if (missing) continue;
|
|
172
|
+
try {
|
|
173
|
+
const st = lstatSync(current);
|
|
174
|
+
if (st.isSymbolicLink() || !st.isDirectory()) throw new Error('invalid-runtime-path');
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (allowMissing && error.code === 'ENOENT') missing = true;
|
|
177
|
+
else throw error;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Explicit extra opinion. Runtime capabilities are resolved only after admission. */
|
|
184
|
+
export async function runProfileReview(options = {}) {
|
|
185
|
+
const argv = options.argv ?? [];
|
|
186
|
+
if (!argv.includes('--profile')) return null;
|
|
187
|
+
const denied = () => ({ exitCode: EXIT.USAGE, summary: { status: 'usage-error', verdict: null,
|
|
188
|
+
advisory: true, authority: 'NONE', releaseEligibility: 'NOT_CHECKED', reasonCode: 'invalid-profile-invocation' } });
|
|
189
|
+
const allowed = new Set(['--profile', '--envelope', '--task', '--context-class', '--reason', '--root', '--operator-root', '--diff', '--spec']);
|
|
190
|
+
const flags = new Map();
|
|
191
|
+
for (let i = 0; i < argv.length; i += 2) {
|
|
192
|
+
const flag = argv[i], value = argv[i + 1];
|
|
193
|
+
if (!allowed.has(flag) || flags.has(flag) || typeof value !== 'string' || !value || value.startsWith('--')) return denied();
|
|
194
|
+
flags.set(flag, value);
|
|
195
|
+
}
|
|
196
|
+
if (flags.get('--profile') !== 'kimi-readonly-v1'
|
|
197
|
+
|| ['--envelope', '--task', '--context-class', '--reason', '--operator-root'].some(k => !flags.has(k))) return denied();
|
|
198
|
+
let root, operatorRoot, envelope, profileApi;
|
|
199
|
+
try {
|
|
200
|
+
root = resolve(flags.get('--root') || process.cwd());
|
|
201
|
+
operatorRoot = resolve(root, flags.get('--operator-root'));
|
|
202
|
+
validateKimiRuntimePaths(root, operatorRoot);
|
|
203
|
+
envelope = JSON.parse(readProfileFile(flags.get('--envelope'), root, 131072));
|
|
204
|
+
if (!envelope || envelope.mode !== 'diff-review' || envelope.taskId !== flags.get('--task')
|
|
205
|
+
|| envelope.contextClass !== flags.get('--context-class') || envelope.reason !== flags.get('--reason')) return denied();
|
|
206
|
+
for (const [flag, field, cap] of [['--diff', 'diff', 96000], ['--spec', 'spec', 16000]]) {
|
|
207
|
+
if (flags.has(flag) && readProfileFile(flags.get(flag), root, cap) !== envelope[field]) return denied();
|
|
208
|
+
}
|
|
209
|
+
profileApi = await import('./kimi-review-profile.mjs');
|
|
210
|
+
profileApi.buildKimiInputBinding(envelope);
|
|
211
|
+
} catch { return denied(); }
|
|
212
|
+
const result = await profileApi.runKimiReview({ profile: flags.get('--profile'), envelope, root,
|
|
213
|
+
operatorRoot,
|
|
214
|
+
get apiKey() { return options.apiKey ?? (options.env ?? process.env).OPENROUTER_API_KEY; },
|
|
215
|
+
fetchImpl: options.fetchImpl, now: options.now, uuid: options.uuid, signal: options.signal, timeoutMs: options.timeoutMs });
|
|
216
|
+
const complete = result.status === 'complete' && ['PASS', 'BLOCK'].includes(result.verdict);
|
|
217
|
+
return { exitCode: complete ? result.verdict === 'BLOCK' ? EXIT.BLOCK : EXIT.PASS : EXIT.SKIPPED,
|
|
218
|
+
summary: { status: result.status, reasonCode: result.reasonCode, verdict: complete ? result.verdict : null,
|
|
219
|
+
advisory: true, authority: 'NONE', releaseEligibility: 'NOT_CHECKED', bindingDigest: result.bindingDigest,
|
|
220
|
+
findings: result.result?.findings?.length ?? null, costState: result.costState, costUsd: result.costUsd } };
|
|
221
|
+
}
|
|
222
|
+
|
|
131
223
|
/**
|
|
132
224
|
* The tree's identity at review time — git HEAD and whether it was dirty.
|
|
133
225
|
* CLI-only: the pure `reviewLogLine` above never shells out; per this file's
|
|
@@ -147,6 +239,11 @@ function gitIdentity(cwd) {
|
|
|
147
239
|
}
|
|
148
240
|
|
|
149
241
|
async function main(argv) {
|
|
242
|
+
if (argv.includes('--profile')) {
|
|
243
|
+
const r = await runProfileReview({ argv, fetchImpl: fetch });
|
|
244
|
+
console.log(JSON.stringify(r.summary));
|
|
245
|
+
process.exit(r.exitCode);
|
|
246
|
+
}
|
|
150
247
|
const diffPath = readArg(argv, '--diff');
|
|
151
248
|
if (!diffPath) { console.error('Usage: cross-model-review.mjs --diff <file|-> [--spec <file>] [--model <slug>] [--provider codex|openrouter]'); process.exit(EXIT.USAGE); }
|
|
152
249
|
|
|
@@ -216,7 +313,8 @@ async function main(argv) {
|
|
|
216
313
|
// was present and costForUsage prices an unknown model at nothing. A reviewer
|
|
217
314
|
// that reads as free is the defect this repository has removed twice already.
|
|
218
315
|
const priced = resolvePrice(res.model).price != null;
|
|
219
|
-
const
|
|
316
|
+
const calculated = res.usage && priced ? costForUsage({ model: res.model, usage: res.usage }) : null;
|
|
317
|
+
const cost = calculated === null ? null : round4(calculated);
|
|
220
318
|
|
|
221
319
|
for (const f of findings) console.log(` ${f.severity} ${f.file}:${f.line} — ${f.issue}`);
|
|
222
320
|
if (verdict == null) {
|