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.
@@ -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.71.1",
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: //' || echo "[]")
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
- grep -rn "postgresql\|mysql\|sqlite\|aurora\|cockroach\|planetscale" .great_cto/PROJECT.md 2>/dev/null | head -1 | grep -oE "postgresql|mysql|sqlite|aurora|cockroach|planetscale" | head -1 || echo "unknown")
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, before/parallel to 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.
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
@@ -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-//' || echo "unknown")
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: //' || echo "not specified")
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
- cat ~/.great_cto/decisions.md 2>/dev/null | head -300 || echo "(none)"
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: //' || echo "not specified")
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
@@ -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: //' || echo "")
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: //' || echo "[]")
290
- QA_EXTRAS=$(grep "^qa-extras:" .great_cto/PROJECT.md 2>/dev/null | sed 's/qa-extras: //' || echo "[]")
291
- PERF_SLA=$(grep "^performance-sla:" .great_cto/PROJECT.md 2>/dev/null | sed 's/performance-sla: //' || echo "")
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|.*/||' || echo "main")
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: //' || echo "[]")
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> "audit=$REPORT" findings=C:<n>,H:<n>,M:<n> not_checked=<n> need=<implementer|decision> finding=<id>
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)); process.exit(result.state === 'ready' ? 0 : 2);
89
+ console.log(JSON.stringify(result, null, 2)); await exitAfterOutput(result.state === 'ready' ? 0 : 2);
51
90
  }
52
91
  if (command === 'list') {
53
- const output = JSON.stringify(listCodexRuns({ root: value('--dir') ? realpathSync(resolve(value('--dir'))) : null, store }), null, 2) + '\n';
54
- // Piped stdout is asynchronous: exiting before the callback can truncate JSON.
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
- * SKILL.md already states these carry a `strict` minimum; encoding it here means
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 (0 if usage or price unknown)
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 three sources in a fixed order:
38
- * an explicit `--provider`, then the project's `capabilities: second_opinion`,
39
- * then — for compatibility with every script that set it — the OpenRouter env.
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 !== 'undeclared') return { ...fromProject, source: 'PROJECT.md' };
52
- if (env.OPENROUTER_API_KEY) {
53
- return { state: 'declared', provider: 'openrouter', why: '', source: 'OPENROUTER_API_KEY (second_opinion undeclared)' };
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 cost = res.usage && priced ? round4(costForUsage({ model: res.model, usage: res.usage })) : null;
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) {