great-cto 3.72.0 → 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/scripts/codex-pipeline.mjs +42 -7
- 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/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"
|
|
@@ -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') {
|
|
@@ -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) {
|