@open-cr-agent/cli 0.3.0 → 0.5.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/README.md +3 -3
- package/dist/account-salt.d.ts +7 -0
- package/dist/account-salt.js +43 -0
- package/dist/cloud.d.ts +34 -0
- package/dist/cloud.js +272 -0
- package/dist/plugin-store.d.ts +20 -0
- package/dist/plugin-store.js +84 -0
- package/dist/plugins-command.d.ts +15 -0
- package/dist/plugins-command.js +183 -0
- package/dist/review/account-plugins.d.ts +23 -0
- package/dist/review/account-plugins.js +54 -0
- package/dist/review/account-settings.d.ts +100 -0
- package/dist/review/account-settings.js +106 -0
- package/dist/review/args.d.ts +2 -1
- package/dist/review/args.js +5 -0
- package/dist/review/cloud-findings.d.ts +31 -0
- package/dist/review/cloud-findings.js +80 -0
- package/dist/review/cloud-memory.d.ts +6 -0
- package/dist/review/cloud-memory.js +58 -0
- package/dist/review/cloud-providers.d.ts +12 -0
- package/dist/review/cloud-providers.js +60 -0
- package/dist/review/cloud-review.d.ts +19 -0
- package/dist/review/cloud-review.js +51 -0
- package/dist/review/cloud-settings.d.ts +16 -0
- package/dist/review/cloud-settings.js +118 -0
- package/dist/review/cloud-upload.d.ts +60 -0
- package/dist/review/cloud-upload.js +180 -0
- package/dist/review/command.d.ts +4 -0
- package/dist/review/command.js +91 -14
- package/dist/review/config.d.ts +65 -6
- package/dist/review/config.js +71 -7
- package/dist/review/plan-render.js +1 -1
- package/dist/review/plugins.d.ts +10 -0
- package/dist/review/plugins.js +46 -1
- package/dist/review/provenance.js +15 -2
- package/dist/review/remote-config.d.ts +6 -0
- package/dist/review/remote-config.js +3 -1
- package/dist/review/render.js +7 -2
- package/dist/review/sarif.js +1 -1
- package/dist/review/settings-sources.d.ts +11 -0
- package/dist/review/settings-sources.js +58 -0
- package/dist/review/target.d.ts +1 -0
- package/dist/review/target.js +4 -1
- package/dist/run.js +36 -0
- package/package.json +8 -8
package/dist/review/command.js
CHANGED
|
@@ -5,14 +5,21 @@ import { githubPlugin } from "@open-cr-agent/vcs-github";
|
|
|
5
5
|
import { gitlabPlugin } from "@open-cr-agent/vcs-gitlab";
|
|
6
6
|
import { localGitPlugin } from "@open-cr-agent/vcs-local";
|
|
7
7
|
import { findRepositoryRoot } from "@open-cr-agent/vcs-local/internal";
|
|
8
|
+
import { pluginsDir } from "../plugin-store.js";
|
|
8
9
|
import { VERSION } from "../version.js";
|
|
9
|
-
import {
|
|
10
|
+
import { accountPlugins } from "./account-plugins.js";
|
|
11
|
+
import { withCloudProviders } from "./cloud-providers.js";
|
|
12
|
+
import { prepareCloudReview, sendToCloud } from "./cloud-review.js";
|
|
13
|
+
import { fetchAccountSettings, layerAccountSettings } from "./cloud-settings.js";
|
|
14
|
+
import { cloudEnabled } from "./cloud-upload.js";
|
|
15
|
+
import { agentChains, ConfigError } from "./config.js";
|
|
10
16
|
import { renderPlan } from "./plan-render.js";
|
|
11
17
|
import { ProgressPrinter } from "./progress.js";
|
|
12
18
|
import { configHash, requestedSampling } from "./provenance.js";
|
|
13
19
|
import { renderJson, renderText, safeJson } from "./render.js";
|
|
14
20
|
import { renderSarif } from "./sarif.js";
|
|
15
21
|
import { loadSarifLogs } from "./sarif-input.js";
|
|
22
|
+
import { effectiveSettings, renderSettings } from "./settings-sources.js";
|
|
16
23
|
import { localTarget, mergeRequestTarget, pullRequestTarget } from "./target.js";
|
|
17
24
|
import { forTerminal } from "./terminal.js";
|
|
18
25
|
export const SESSIONS_DIR = ".ocra/sessions";
|
|
@@ -39,34 +46,74 @@ export async function reviewCommand(args, io, deps) {
|
|
|
39
46
|
: args.mergeRequest
|
|
40
47
|
? await mergeRequestTarget(args.mergeRequest, deps.cwd, root, deps.env, warn, deps.fetch, ignoreRepoConfig, configFile)
|
|
41
48
|
: await localTarget(localArgs, deps.cwd, root, deps.env, warn, deps.fetch);
|
|
42
|
-
|
|
49
|
+
// The account's settings fill what the repository leaves out (ADR-0027);
|
|
50
|
+
// unreachable, they cost a warning, so a plan still works offline.
|
|
51
|
+
const cloudDeps = deps.cloud;
|
|
52
|
+
const signedIn = cloudDeps !== undefined && (await cloudEnabled(cloudDeps));
|
|
53
|
+
let config = target.config;
|
|
54
|
+
let filled = [];
|
|
55
|
+
let fromAccount = { plugins: [], pluginSettings: {} };
|
|
56
|
+
if (cloudDeps && signedIn) {
|
|
57
|
+
const account = await fetchAccountSettings(cloudDeps, warn);
|
|
58
|
+
if (account?.settings) {
|
|
59
|
+
({ config, filled } = layerAccountSettings(config, account.settings));
|
|
60
|
+
// The account's default for --ultra; there is no --no-ultra to refuse it.
|
|
61
|
+
if (account.settings.ultra === true && !args.ultra)
|
|
62
|
+
filled.push("ultra");
|
|
63
|
+
if (filled.length > 0) {
|
|
64
|
+
io.err.write(`[ocra] From your ocra Cloud settings: ${filled.join(", ")}\n`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (account)
|
|
68
|
+
fromAccount = account.plugins;
|
|
69
|
+
}
|
|
70
|
+
const ultra = args.ultra === true || filled.includes("ultra");
|
|
43
71
|
const session = { dir: join(root, SESSIONS_DIR), id: newRunId() };
|
|
44
72
|
// A plan calls no model, writes no session log and imports no runtime: it
|
|
45
73
|
// works without the optional runtime-opencode.
|
|
46
74
|
const builtins = args.plan
|
|
47
75
|
? deps.builtinPlugins.filter((p) => p.name !== sessionJsonlPlugin.name)
|
|
48
76
|
: [...deps.builtinPlugins, ...(await builtinRuntime(deps.runtimes, config.runtime))];
|
|
49
|
-
const
|
|
77
|
+
const account = await accountPlugins(fromAccount, {
|
|
78
|
+
allowed: target.accountPlugins,
|
|
79
|
+
configured: config.plugins,
|
|
80
|
+
taken: [...builtins, ...target.plugins],
|
|
81
|
+
dir: pluginsDir(deps.env),
|
|
82
|
+
warn,
|
|
83
|
+
});
|
|
84
|
+
// The repository's own settings win; a repository plugin's come from it alone.
|
|
85
|
+
const pluginSettings = { ...account.settings, ...config.pluginSettings };
|
|
86
|
+
const registry = await startPlugins([...builtins, ...target.plugins, ...account.plugins], {
|
|
50
87
|
settings: args.plan
|
|
51
|
-
?
|
|
52
|
-
: { ...
|
|
88
|
+
? pluginSettings
|
|
89
|
+
: { ...pluginSettings, [sessionJsonlPlugin.name]: session },
|
|
53
90
|
env: deps.env,
|
|
54
91
|
warn,
|
|
55
92
|
});
|
|
56
93
|
const vcs = target.createVcs(registry);
|
|
94
|
+
const rules = [
|
|
95
|
+
...registry.rules.map((rule) => ({ ...rule, source: "plugin" })),
|
|
96
|
+
...config.rules,
|
|
97
|
+
];
|
|
57
98
|
const overrides = reviewerOverrides(config, args, registry.reviewers.map((r) => r.id));
|
|
58
99
|
if (args.plan) {
|
|
59
100
|
const preview = await previewReview({
|
|
60
101
|
vcs,
|
|
61
102
|
reviewers: registry.reviewers,
|
|
62
103
|
reviewerOverrides: overrides,
|
|
63
|
-
|
|
104
|
+
effort: config.effort,
|
|
105
|
+
roles: config.roles,
|
|
106
|
+
models: config.models,
|
|
107
|
+
rules,
|
|
64
108
|
selection: { ...defaultSelectionPolicy, include: config.include, exclude: config.exclude },
|
|
65
109
|
...(target.readTrusted ? { readTrusted: target.readTrusted } : {}),
|
|
66
|
-
...(
|
|
110
|
+
...(ultra ? { ultra: true } : {}),
|
|
67
111
|
...(config.maxTasks !== undefined ? { maxTasks: config.maxTasks } : {}),
|
|
68
112
|
});
|
|
69
|
-
const
|
|
113
|
+
const settings = effectiveSettings(target.config, config, filled);
|
|
114
|
+
const rendered = args.format === "json"
|
|
115
|
+
? `${safeJson({ ...toPlanOutput(preview), settings, ...accountOf(config) })}\n`
|
|
116
|
+
: renderPlan(preview) + renderSettings(settings, config.accountSettings);
|
|
70
117
|
if (args.output === undefined)
|
|
71
118
|
io.out.write(rendered);
|
|
72
119
|
else
|
|
@@ -74,11 +121,17 @@ export async function reviewCommand(args, io, deps) {
|
|
|
74
121
|
return EXIT.ok;
|
|
75
122
|
}
|
|
76
123
|
const sarif = await loadSarifLogs(args.importSarif ?? [], deps.cwd);
|
|
124
|
+
// The repository's hash and the account's memory for it (ADR-0028).
|
|
125
|
+
const cloudReview = cloudDeps && signedIn ? await prepareCloudReview(root, cloudDeps, warn) : undefined;
|
|
77
126
|
const sampling = requestedSampling(config, args);
|
|
127
|
+
const models = config.models;
|
|
128
|
+
const agentModels = agentChains({ reviewers: overrides, roles: config.roles });
|
|
129
|
+
const cloud = await withCloudProviders([...Object.values(models), ...Object.values(agentModels)], config.providers, deps.env, cloudDeps, warn);
|
|
78
130
|
const runtime = registry.createRuntime(config.runtime, {
|
|
79
|
-
models
|
|
80
|
-
|
|
81
|
-
|
|
131
|
+
models,
|
|
132
|
+
...(Object.keys(agentModels).length > 0 ? { agentModels } : {}),
|
|
133
|
+
env: cloud.env,
|
|
134
|
+
providers: cloud.providers,
|
|
82
135
|
...(Object.keys(sampling).length > 0 ? { sampling } : {}),
|
|
83
136
|
});
|
|
84
137
|
const progress = new ProgressPrinter(io.err, { heartbeatMs: deps.heartbeatMs, now: deps.now });
|
|
@@ -88,22 +141,30 @@ export async function reviewCommand(args, io, deps) {
|
|
|
88
141
|
interrupt.abort();
|
|
89
142
|
});
|
|
90
143
|
let report;
|
|
144
|
+
const started = deps.now();
|
|
91
145
|
try {
|
|
92
146
|
report = await review({
|
|
93
147
|
runId: session.id,
|
|
94
148
|
signal: interrupt.signal,
|
|
95
149
|
...runOptions(config),
|
|
150
|
+
models,
|
|
96
151
|
reviewerOverrides: overrides,
|
|
97
|
-
provenance: {
|
|
152
|
+
provenance: {
|
|
153
|
+
ocraVersion: VERSION,
|
|
154
|
+
configHash: configHash(config, ultra ? { ...args, ultra: true } : args),
|
|
155
|
+
sampling,
|
|
156
|
+
...accountOf(config),
|
|
157
|
+
},
|
|
98
158
|
...(args.maxCostUsd !== undefined ? { maxCostUsd: args.maxCostUsd } : {}),
|
|
99
|
-
...(
|
|
159
|
+
...(ultra ? { ultra: true } : {}),
|
|
100
160
|
...(args.full ? { fullReview: true } : {}),
|
|
101
161
|
...(sarif.length > 0 ? { sarif } : {}),
|
|
102
162
|
...(target.readTrusted ? { readTrusted: target.readTrusted } : {}),
|
|
103
163
|
vcs,
|
|
104
164
|
runtime,
|
|
105
165
|
reviewers: registry.reviewers,
|
|
106
|
-
rules
|
|
166
|
+
rules,
|
|
167
|
+
...(cloudReview?.memory.length ? { accountMemory: cloudReview.memory } : {}),
|
|
107
168
|
onEvent: (event) => {
|
|
108
169
|
registry.emit(event);
|
|
109
170
|
progress.onEvent(event);
|
|
@@ -128,6 +189,9 @@ export async function reviewCommand(args, io, deps) {
|
|
|
128
189
|
await deps.writeFile(resolve(deps.cwd, args.output), rendered);
|
|
129
190
|
io.err.write(`[ocra] Wrote ${args.output}\n`);
|
|
130
191
|
}
|
|
192
|
+
const hidden = report.remembered.filter((e) => e.source === "account").length;
|
|
193
|
+
if (hidden > 0)
|
|
194
|
+
io.err.write(`[ocra] ${hidden} finding(s) hidden by your ocra Cloud memory\n`);
|
|
131
195
|
if (interrupt.signal.aborted)
|
|
132
196
|
return EXIT.interrupted;
|
|
133
197
|
if (target.publish) {
|
|
@@ -146,8 +210,19 @@ export async function reviewCommand(args, io, deps) {
|
|
|
146
210
|
}
|
|
147
211
|
io.err.write(`[ocra] Published the review to the ${target.publishesTo ?? "change request"}\n`);
|
|
148
212
|
}
|
|
213
|
+
if (cloudReview && cloudDeps && !args.noUpload) {
|
|
214
|
+
await sendToCloud(report, cloudReview, cloudDeps, {
|
|
215
|
+
source: args.pullRequest ? "github" : args.mergeRequest ? "gitlab" : "local",
|
|
216
|
+
durationMs: deps.now() - started,
|
|
217
|
+
err: io.err,
|
|
218
|
+
warn,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
149
221
|
return exitCode(report, io.err);
|
|
150
222
|
}
|
|
223
|
+
function accountOf(config) {
|
|
224
|
+
return config.accountSettings ? { accountSettings: config.accountSettings } : {};
|
|
225
|
+
}
|
|
151
226
|
// A runtime not built in comes from a plugin of the configuration.
|
|
152
227
|
async function builtinRuntime(runtimes, name) {
|
|
153
228
|
const load = Object.hasOwn(runtimes, name) ? runtimes[name] : undefined;
|
|
@@ -156,6 +231,8 @@ async function builtinRuntime(runtimes, name) {
|
|
|
156
231
|
function runOptions(config) {
|
|
157
232
|
const options = {
|
|
158
233
|
selection: { ...defaultSelectionPolicy, include: config.include, exclude: config.exclude },
|
|
234
|
+
effort: config.effort,
|
|
235
|
+
roles: config.roles,
|
|
159
236
|
};
|
|
160
237
|
if (config.concurrency !== undefined)
|
|
161
238
|
options.concurrency = config.concurrency;
|
package/dist/review/config.d.ts
CHANGED
|
@@ -1,13 +1,37 @@
|
|
|
1
|
-
import { type CustomProvider, type ModelChains, OcraError, type
|
|
1
|
+
import { type CustomProvider, type ModelChains, OcraError, type ReviewerOverrides, type RoleSettings, type SourcedRule, type TierEfforts } from "@open-cr-agent/core";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
export declare const CONFIG_PATH = ".ocra/config.json";
|
|
4
|
-
declare const
|
|
4
|
+
export declare const DEFAULT_RUNTIME = "opencode";
|
|
5
|
+
export declare const configSchema: z.ZodObject<{
|
|
5
6
|
$schema: z.ZodOptional<z.ZodString>;
|
|
6
7
|
models: z.ZodDefault<z.ZodObject<{
|
|
7
8
|
top: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>, z.ZodTransform<string[], string | string[]>>>;
|
|
8
9
|
standard: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>, z.ZodTransform<string[], string | string[]>>>;
|
|
9
10
|
light: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>, z.ZodTransform<string[], string | string[]>>>;
|
|
10
11
|
}, z.core.$strict>>;
|
|
12
|
+
effort: z.ZodDefault<z.ZodObject<{
|
|
13
|
+
top: z.ZodOptional<z.ZodEnum<{
|
|
14
|
+
high: "high";
|
|
15
|
+
low: "low";
|
|
16
|
+
medium: "medium";
|
|
17
|
+
minimal: "minimal";
|
|
18
|
+
none: "none";
|
|
19
|
+
}>>;
|
|
20
|
+
standard: z.ZodOptional<z.ZodEnum<{
|
|
21
|
+
high: "high";
|
|
22
|
+
low: "low";
|
|
23
|
+
medium: "medium";
|
|
24
|
+
minimal: "minimal";
|
|
25
|
+
none: "none";
|
|
26
|
+
}>>;
|
|
27
|
+
light: z.ZodOptional<z.ZodEnum<{
|
|
28
|
+
high: "high";
|
|
29
|
+
low: "low";
|
|
30
|
+
medium: "medium";
|
|
31
|
+
minimal: "minimal";
|
|
32
|
+
none: "none";
|
|
33
|
+
}>>;
|
|
34
|
+
}, z.core.$strict>>;
|
|
11
35
|
concurrency: z.ZodOptional<z.ZodNumber>;
|
|
12
36
|
taskTimeoutMinutes: z.ZodOptional<z.ZodNumber>;
|
|
13
37
|
runTimeoutMinutes: z.ZodOptional<z.ZodNumber>;
|
|
@@ -25,7 +49,7 @@ declare const configSchema: z.ZodObject<{
|
|
|
25
49
|
}, z.core.$strict>>;
|
|
26
50
|
include: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
27
51
|
exclude: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
28
|
-
runtime: z.
|
|
52
|
+
runtime: z.ZodOptional<z.ZodUnion<[z.ZodEnum<{
|
|
29
53
|
direct: "direct";
|
|
30
54
|
opencode: "opencode";
|
|
31
55
|
}>, z.ZodString]>>;
|
|
@@ -37,6 +61,28 @@ declare const configSchema: z.ZodObject<{
|
|
|
37
61
|
lite: "lite";
|
|
38
62
|
trivial: "trivial";
|
|
39
63
|
}>>;
|
|
64
|
+
models: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>, z.ZodTransform<string[], string | string[]>>>;
|
|
65
|
+
effort: z.ZodOptional<z.ZodEnum<{
|
|
66
|
+
high: "high";
|
|
67
|
+
low: "low";
|
|
68
|
+
medium: "medium";
|
|
69
|
+
minimal: "minimal";
|
|
70
|
+
none: "none";
|
|
71
|
+
}>>;
|
|
72
|
+
}, z.core.$strict>>>;
|
|
73
|
+
roles: z.ZodDefault<z.ZodRecord<z.ZodEnum<{
|
|
74
|
+
helper: "helper";
|
|
75
|
+
judge: "judge";
|
|
76
|
+
verifier: "verifier";
|
|
77
|
+
}> & z.core.$partial, z.ZodObject<{
|
|
78
|
+
models: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>, z.ZodTransform<string[], string | string[]>>>;
|
|
79
|
+
effort: z.ZodOptional<z.ZodEnum<{
|
|
80
|
+
high: "high";
|
|
81
|
+
low: "low";
|
|
82
|
+
medium: "medium";
|
|
83
|
+
minimal: "minimal";
|
|
84
|
+
none: "none";
|
|
85
|
+
}>>;
|
|
40
86
|
}, z.core.$strict>>>;
|
|
41
87
|
pluginSettings: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
42
88
|
providers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
@@ -48,15 +94,25 @@ declare const configSchema: z.ZodObject<{
|
|
|
48
94
|
output: z.ZodNumber;
|
|
49
95
|
cachedInput: z.ZodOptional<z.ZodNumber>;
|
|
50
96
|
}, z.core.$strict>>;
|
|
97
|
+
effort: z.ZodOptional<z.ZodEnum<{
|
|
98
|
+
openai: "openai";
|
|
99
|
+
openrouter: "openrouter";
|
|
100
|
+
}>>;
|
|
51
101
|
}, z.core.$strict>>>;
|
|
52
102
|
extends: z.ZodOptional<z.ZodString>;
|
|
53
103
|
}, z.core.$strict>;
|
|
54
104
|
export declare const CONFIG_SCHEMA_ID = "https://ocra.majincheng.com/schema/config.v1.json";
|
|
55
105
|
export declare function configJsonSchema(): Record<string, unknown>;
|
|
56
|
-
export type CliConfig = Omit<z.infer<typeof configSchema>, "models" | "providers"> & {
|
|
106
|
+
export type CliConfig = Omit<z.infer<typeof configSchema>, "models" | "effort" | "providers" | "runtime"> & {
|
|
107
|
+
runtime: string;
|
|
108
|
+
runtimeSet: boolean;
|
|
57
109
|
models: ModelChains;
|
|
110
|
+
effort: TierEfforts;
|
|
58
111
|
providers: Record<string, CustomProvider>;
|
|
59
|
-
rules:
|
|
112
|
+
rules: SourcedRule[];
|
|
113
|
+
accountSettings?: {
|
|
114
|
+
version: string | null;
|
|
115
|
+
};
|
|
60
116
|
};
|
|
61
117
|
export interface LoadOptions {
|
|
62
118
|
repository: boolean;
|
|
@@ -71,5 +127,8 @@ export declare class ConfigError extends OcraError {
|
|
|
71
127
|
});
|
|
72
128
|
}
|
|
73
129
|
export declare function loadConfig(root: string, env: Readonly<Record<string, string | undefined>>, options?: LoadOptions): Promise<CliConfig>;
|
|
74
|
-
export {
|
|
130
|
+
export declare function agentChains(config: {
|
|
131
|
+
reviewers: ReviewerOverrides;
|
|
132
|
+
roles: RoleSettings;
|
|
133
|
+
}): Record<string, readonly string[]>;
|
|
75
134
|
//# sourceMappingURL=config.d.ts.map
|
package/dist/review/config.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { OcraError, } from "@open-cr-agent/core";
|
|
4
|
-
import { RISK_TIERS } from "@open-cr-agent/core/internal";
|
|
4
|
+
import { AGENT_ROLES, EFFORT_LEVELS, RISK_TIERS } from "@open-cr-agent/core/internal";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { fetchRemoteConfig, mergeConfig } from "./remote-config.js";
|
|
7
7
|
export const CONFIG_PATH = ".ocra/config.json";
|
|
8
|
+
// How much a model reasons before answering (ADR-0025); unset leaves the
|
|
9
|
+
// provider's default.
|
|
10
|
+
const effort = z.enum(EFFORT_LEVELS);
|
|
8
11
|
const modelChain = z
|
|
9
12
|
.union([z.string().min(1), z.array(z.string().min(1)).min(1)])
|
|
10
13
|
.transform((value) => (typeof value === "string" ? [value] : value));
|
|
@@ -40,12 +43,16 @@ const providerSchema = z
|
|
|
40
43
|
models: z
|
|
41
44
|
.record(z.string().regex(/^[\w./:@-]{1,200}$/), z.object({ input: price, output: price, cachedInput: price.optional() }).strict())
|
|
42
45
|
.refine((models) => Object.keys(models).length > 0, "must list at least one model"),
|
|
46
|
+
// How the endpoint takes a reasoning effort: "openai" sends
|
|
47
|
+
// reasoning_effort (the default), "openrouter" sends reasoning.effort.
|
|
48
|
+
effort: z.enum(["openai", "openrouter"]).optional(),
|
|
43
49
|
})
|
|
44
50
|
.strict();
|
|
45
51
|
// The runtimes ocra ships; a plugin may register another name, so the
|
|
46
52
|
// schema lists these for editors and still accepts any other string.
|
|
47
53
|
const BUILTIN_RUNTIMES = ["opencode", "direct"];
|
|
48
|
-
const
|
|
54
|
+
export const DEFAULT_RUNTIME = "opencode";
|
|
55
|
+
export const configSchema = z
|
|
49
56
|
.object({
|
|
50
57
|
$schema: z.string().optional(),
|
|
51
58
|
models: z
|
|
@@ -53,6 +60,11 @@ const configSchema = z
|
|
|
53
60
|
.partial()
|
|
54
61
|
.strict()
|
|
55
62
|
.default({}),
|
|
63
|
+
effort: z
|
|
64
|
+
.object({ top: effort, standard: effort, light: effort })
|
|
65
|
+
.partial()
|
|
66
|
+
.strict()
|
|
67
|
+
.default({}),
|
|
56
68
|
concurrency: z.number().int().min(1).max(32).optional(),
|
|
57
69
|
taskTimeoutMinutes: z.number().positive().optional(),
|
|
58
70
|
runTimeoutMinutes: z.number().positive().optional(),
|
|
@@ -81,17 +93,25 @@ const configSchema = z
|
|
|
81
93
|
.default({}),
|
|
82
94
|
include: z.array(z.string().min(1)).default([]),
|
|
83
95
|
exclude: z.array(z.string().min(1)).default([]),
|
|
84
|
-
|
|
96
|
+
// Unset means "opencode"; loadConfig fills it in and records whether the
|
|
97
|
+
// file chose, so account defaults (ADR-0025) apply only where it did not.
|
|
98
|
+
runtime: z.enum(BUILTIN_RUNTIMES).or(z.string().min(1)).optional(),
|
|
85
99
|
plugins: z.array(z.string().min(1)).default([]),
|
|
86
100
|
reviewers: z
|
|
87
101
|
.record(z.string(), z
|
|
88
102
|
.object({
|
|
89
103
|
enabled: z.boolean(),
|
|
90
104
|
minTier: z.enum(RISK_TIERS),
|
|
105
|
+
// The reviewer's own chain, for its review tasks and plan call.
|
|
106
|
+
models: modelChain,
|
|
107
|
+
effort,
|
|
91
108
|
})
|
|
92
109
|
.partial()
|
|
93
110
|
.strict())
|
|
94
111
|
.default({}),
|
|
112
|
+
roles: z
|
|
113
|
+
.partialRecord(z.enum(AGENT_ROLES), z.object({ models: modelChain, effort }).partial().strict())
|
|
114
|
+
.default({}),
|
|
95
115
|
pluginSettings: z.record(z.string(), z.unknown()).default({}),
|
|
96
116
|
providers: z.record(z.string().regex(/^[a-z][a-z0-9-]{0,39}$/), providerSchema).default({}),
|
|
97
117
|
extends: z.string().min(1).optional(),
|
|
@@ -116,6 +136,11 @@ const MODEL_ENV = {
|
|
|
116
136
|
standard: "OCRA_MODEL_STANDARD",
|
|
117
137
|
light: "OCRA_MODEL_LIGHT",
|
|
118
138
|
};
|
|
139
|
+
const EFFORT_ENV = {
|
|
140
|
+
top: "OCRA_EFFORT_TOP",
|
|
141
|
+
standard: "OCRA_EFFORT_STANDARD",
|
|
142
|
+
light: "OCRA_EFFORT_LIGHT",
|
|
143
|
+
};
|
|
119
144
|
export class ConfigError extends OcraError {
|
|
120
145
|
constructor(message, options) {
|
|
121
146
|
super("CONFIG_INVALID", message, options);
|
|
@@ -144,10 +169,34 @@ export async function loadConfig(root, env, options = { repository: true }) {
|
|
|
144
169
|
if (chain.length > 0)
|
|
145
170
|
models[tier] = chain;
|
|
146
171
|
}
|
|
147
|
-
|
|
172
|
+
const efforts = {
|
|
173
|
+
...parsed.effort,
|
|
174
|
+
};
|
|
175
|
+
for (const [tier, name] of Object.entries(EFFORT_ENV)) {
|
|
176
|
+
const value = env[name]?.trim();
|
|
177
|
+
if (!value)
|
|
178
|
+
continue;
|
|
179
|
+
const level = effort.safeParse(value);
|
|
180
|
+
if (!level.success) {
|
|
181
|
+
throw new ConfigError(`${name} must be one of ${EFFORT_LEVELS.join(", ")}`);
|
|
182
|
+
}
|
|
183
|
+
efforts[tier] = level.data;
|
|
184
|
+
}
|
|
185
|
+
for (const model of unpriced(parsed.providers, [
|
|
186
|
+
...Object.values(models),
|
|
187
|
+
...Object.values(agentChains(parsed)),
|
|
188
|
+
])) {
|
|
148
189
|
options.warn?.(`${model} has a price of 0: reported cost and --max-cost-usd do not count its tokens`);
|
|
149
190
|
}
|
|
150
|
-
return {
|
|
191
|
+
return {
|
|
192
|
+
...parsed,
|
|
193
|
+
runtime: parsed.runtime ?? DEFAULT_RUNTIME,
|
|
194
|
+
runtimeSet: parsed.runtime !== undefined,
|
|
195
|
+
models,
|
|
196
|
+
effort: efforts,
|
|
197
|
+
providers: toProviders(parsed.providers),
|
|
198
|
+
rules: rules.map((rule) => ({ ...rule, source: "shared" })),
|
|
199
|
+
};
|
|
151
200
|
}
|
|
152
201
|
function toProviders(declared) {
|
|
153
202
|
return Object.fromEntries(Object.entries(declared).map(([id, p]) => [
|
|
@@ -155,6 +204,7 @@ function toProviders(declared) {
|
|
|
155
204
|
{
|
|
156
205
|
baseUrl: p.baseUrl,
|
|
157
206
|
...(p.apiKeyEnv ? { apiKeyEnv: p.apiKeyEnv } : {}),
|
|
207
|
+
...(p.effort ? { effort: p.effort } : {}),
|
|
158
208
|
models: Object.fromEntries(Object.entries(p.models).map(([model, price]) => [
|
|
159
209
|
model,
|
|
160
210
|
{
|
|
@@ -166,9 +216,23 @@ function toProviders(declared) {
|
|
|
166
216
|
},
|
|
167
217
|
]));
|
|
168
218
|
}
|
|
219
|
+
// The reviewers' and roles' own chains, by agent id (ADR-0025); a disabled
|
|
220
|
+
// reviewer's is left out, since it makes no call.
|
|
221
|
+
export function agentChains(config) {
|
|
222
|
+
const chains = {};
|
|
223
|
+
for (const [id, setting] of Object.entries(config.reviewers)) {
|
|
224
|
+
if (setting.models && setting.enabled !== false)
|
|
225
|
+
chains[id] = setting.models;
|
|
226
|
+
}
|
|
227
|
+
for (const [role, setting] of Object.entries(config.roles)) {
|
|
228
|
+
if (setting?.models)
|
|
229
|
+
chains[role] = setting.models;
|
|
230
|
+
}
|
|
231
|
+
return chains;
|
|
232
|
+
}
|
|
169
233
|
// Models of declared providers, named in a chain, priced at 0 per token.
|
|
170
|
-
function unpriced(providers,
|
|
171
|
-
const named = new Set(
|
|
234
|
+
function unpriced(providers, chains) {
|
|
235
|
+
const named = new Set(chains.flatMap((chain) => chain ?? []));
|
|
172
236
|
return [...named].filter((model) => {
|
|
173
237
|
const slash = model.indexOf("/");
|
|
174
238
|
const price = providers[model.slice(0, slash)]?.models[model.slice(slash + 1)];
|
|
@@ -20,7 +20,7 @@ export function renderPlan(preview) {
|
|
|
20
20
|
lines.push("", `Review tasks: ${preview.tasks.length}`);
|
|
21
21
|
const width = Math.max(0, ...preview.tasks.map((t) => t.taskId.length));
|
|
22
22
|
for (const t of preview.tasks) {
|
|
23
|
-
lines.push(` ${t.taskId.padEnd(width)} ~${number.format(t.promptTokens)} prompt tokens${t.planPromptTokens === undefined ? "" : ` + plan ~${number.format(t.planPromptTokens)}`} ${t.bundle}`);
|
|
23
|
+
lines.push(` ${t.taskId.padEnd(width)} ~${number.format(t.promptTokens)} prompt tokens${t.planPromptTokens === undefined ? "" : ` + plan ~${number.format(t.planPromptTokens)}`}${t.effort === undefined ? "" : ` effort ${t.effort}`}${t.models === undefined ? "" : ` models ${t.models.join(", ")}`} ${t.bundle}`);
|
|
24
24
|
}
|
|
25
25
|
if (preview.skipped.length > 0) {
|
|
26
26
|
lines.push("", "Skipped by the review matrix:");
|
package/dist/review/plugins.d.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
1
|
import type { OcraPlugin } from "@open-cr-agent/core";
|
|
2
2
|
export declare function loadExternalPlugins(specifiers: readonly string[], root: string): Promise<OcraPlugin[]>;
|
|
3
|
+
/**
|
|
4
|
+
* The plugins an ocra Cloud account names that this machine allowed: each
|
|
5
|
+
* from the allowed directory alone, at the version and integrity recorded
|
|
6
|
+
* when it was allowed. Anything else is skipped with a warning; an account
|
|
7
|
+
* plugin never fails the review.
|
|
8
|
+
*/
|
|
9
|
+
export declare function loadAccountPlugins(names: readonly string[], dir: string, warn: (message: string) => void): Promise<{
|
|
10
|
+
name: string;
|
|
11
|
+
plugin: OcraPlugin;
|
|
12
|
+
}[]>;
|
|
3
13
|
//# sourceMappingURL=plugins.d.ts.map
|
package/dist/review/plugins.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
1
2
|
import { createRequire } from "node:module";
|
|
2
|
-
import { isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { isAbsolute, join, resolve, sep } from "node:path";
|
|
3
4
|
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { installedEntry, readAllowed } from "../plugin-store.js";
|
|
4
6
|
import { ConfigError } from "./config.js";
|
|
5
7
|
// Plugins listed in the repository config execute code, so they are resolved
|
|
6
8
|
// only from the repository's own dependencies or relative paths inside it.
|
|
@@ -29,4 +31,47 @@ function resolvePlugin(specifier, root) {
|
|
|
29
31
|
function isPlugin(value) {
|
|
30
32
|
return (typeof value === "object" && value !== null && typeof value.name === "string");
|
|
31
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* The plugins an ocra Cloud account names that this machine allowed: each
|
|
36
|
+
* from the allowed directory alone, at the version and integrity recorded
|
|
37
|
+
* when it was allowed. Anything else is skipped with a warning; an account
|
|
38
|
+
* plugin never fails the review.
|
|
39
|
+
*/
|
|
40
|
+
export async function loadAccountPlugins(names, dir, warn) {
|
|
41
|
+
const allowed = new Map((await readAllowed(dir)).map((p) => [p.name, p]));
|
|
42
|
+
const missing = names.filter((name) => !allowed.has(name));
|
|
43
|
+
if (missing.length > 0) {
|
|
44
|
+
warn(`your ocra Cloud settings name plugins this machine has not allowed, so they do not load: ${missing.join(", ")}; allow each with ocra plugins allow <name>@<version>`);
|
|
45
|
+
}
|
|
46
|
+
const loaded = [];
|
|
47
|
+
for (const name of names) {
|
|
48
|
+
const record = allowed.get(name);
|
|
49
|
+
if (!record)
|
|
50
|
+
continue;
|
|
51
|
+
try {
|
|
52
|
+
const installed = await installedEntry(dir, name);
|
|
53
|
+
if (installed?.version !== record.version || installed.integrity !== record.integrity) {
|
|
54
|
+
throw new Error(`it is not the ${record.version} allowed here; run ocra plugins allow ${name}@<version> again`);
|
|
55
|
+
}
|
|
56
|
+
const module = await import(pathToFileURL(await allowedEntry(dir, name)).href);
|
|
57
|
+
const plugin = module.default ?? module.plugin;
|
|
58
|
+
if (!isPlugin(plugin))
|
|
59
|
+
throw new Error('it exports no ocra plugin as default or "plugin"');
|
|
60
|
+
loaded.push({ name, plugin });
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
warn(`skipping plugin ${name} from your ocra Cloud settings: ${error.message}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return loaded;
|
|
67
|
+
}
|
|
68
|
+
// Node's resolution walks up past the directory to any node_modules above
|
|
69
|
+
// it; the entry must be inside the allowed package itself.
|
|
70
|
+
async function allowedEntry(dir, name) {
|
|
71
|
+
const entry = await realpath(createRequire(join(dir, "package.json")).resolve(name));
|
|
72
|
+
const root = await realpath(join(dir, "node_modules", name));
|
|
73
|
+
if (!entry.startsWith(`${root}${sep}`))
|
|
74
|
+
throw new Error(`it resolves outside ${dir}`);
|
|
75
|
+
return entry;
|
|
76
|
+
}
|
|
32
77
|
//# sourceMappingURL=plugins.js.map
|
|
@@ -12,11 +12,24 @@ export function requestedSampling(config, args) {
|
|
|
12
12
|
// The effective configuration and the flags that change what is reviewed
|
|
13
13
|
// and how, so two runs can tell they were set up alike. Keys are names of
|
|
14
14
|
// environment variables, never their values; a password written into a
|
|
15
|
-
// URL is dropped. Sampling is recorded on its own.
|
|
15
|
+
// URL is dropped. Sampling is recorded on its own. Effort is covered; left
|
|
16
|
+
// unset it adds nothing, so configurations that set none keep their hash.
|
|
17
|
+
// The ocra Cloud account settings (ADR-0027) add their version and rules
|
|
18
|
+
// only when they were layered in, so a signed-out run keeps its hash.
|
|
16
19
|
export function configHash(config, args) {
|
|
17
|
-
|
|
20
|
+
// runtimeSet says where the runtime came from, not what ran: the runtime is hashed.
|
|
21
|
+
const { sampling: _, runtimeSet: _set, providers, extends: shared, effort, roles, rules, accountSettings, ...rest } = config;
|
|
22
|
+
const bare = (account) => rules
|
|
23
|
+
.filter((rule) => (rule.source === "account") === account)
|
|
24
|
+
.map(({ path, rule }) => ({ path, rule }));
|
|
18
25
|
return stableHash({
|
|
19
26
|
...rest,
|
|
27
|
+
rules: bare(false),
|
|
28
|
+
...(accountSettings
|
|
29
|
+
? { account: { version: accountSettings.version, rules: bare(true) } }
|
|
30
|
+
: {}),
|
|
31
|
+
...(Object.keys(effort).length > 0 ? { effort } : {}),
|
|
32
|
+
...(Object.keys(roles).length > 0 ? { roles } : {}),
|
|
20
33
|
...(shared === undefined ? {} : { extends: withoutCredentials(shared) }),
|
|
21
34
|
providers: Object.fromEntries(Object.entries(providers).map(([id, p]) => [
|
|
22
35
|
id,
|
|
@@ -6,6 +6,11 @@ export declare const remoteConfigSchema: z.ZodObject<{
|
|
|
6
6
|
standard: "standard";
|
|
7
7
|
top: "top";
|
|
8
8
|
}>, z.ZodUnknown>>;
|
|
9
|
+
effort: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
|
|
10
|
+
light: "light";
|
|
11
|
+
standard: "standard";
|
|
12
|
+
top: "top";
|
|
13
|
+
}> & z.core.$partial, z.ZodUnknown>>;
|
|
9
14
|
concurrency: z.ZodOptional<z.ZodUnknown>;
|
|
10
15
|
taskTimeoutMinutes: z.ZodOptional<z.ZodUnknown>;
|
|
11
16
|
runTimeoutMinutes: z.ZodOptional<z.ZodUnknown>;
|
|
@@ -16,6 +21,7 @@ export declare const remoteConfigSchema: z.ZodObject<{
|
|
|
16
21
|
include: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
17
22
|
exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
18
23
|
reviewers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
24
|
+
roles: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
19
25
|
github: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
20
26
|
providers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
21
27
|
rules: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -11,6 +11,7 @@ export const remoteConfigSchema = z
|
|
|
11
11
|
.object({
|
|
12
12
|
$schema: z.string().optional(),
|
|
13
13
|
models: z.record(z.enum(["top", "standard", "light"]), z.unknown()).optional(),
|
|
14
|
+
effort: z.partialRecord(z.enum(["top", "standard", "light"]), z.unknown()).optional(),
|
|
14
15
|
concurrency: z.unknown().optional(),
|
|
15
16
|
taskTimeoutMinutes: z.unknown().optional(),
|
|
16
17
|
runTimeoutMinutes: z.unknown().optional(),
|
|
@@ -21,6 +22,7 @@ export const remoteConfigSchema = z
|
|
|
21
22
|
include: z.array(z.string()).optional(),
|
|
22
23
|
exclude: z.array(z.string()).optional(),
|
|
23
24
|
reviewers: z.record(z.string(), z.unknown()).optional(),
|
|
25
|
+
roles: z.record(z.string(), z.unknown()).optional(),
|
|
24
26
|
github: z.record(z.string(), z.unknown()).optional(),
|
|
25
27
|
providers: z.record(z.string(), z.unknown()).optional(),
|
|
26
28
|
rules: z.array(repoRuleSchema).max(500).optional(),
|
|
@@ -83,7 +85,7 @@ async function readLimited(response, limit, name) {
|
|
|
83
85
|
export function mergeConfig(remote, local) {
|
|
84
86
|
const { rules: _rules, ...settings } = remote;
|
|
85
87
|
const merged = { ...settings, ...local };
|
|
86
|
-
for (const key of ["models", "reviewers", "github", "providers"]) {
|
|
88
|
+
for (const key of ["models", "effort", "reviewers", "roles", "github", "providers"]) {
|
|
87
89
|
const a = settings[key];
|
|
88
90
|
const b = local[key];
|
|
89
91
|
if (a !== undefined || b !== undefined)
|
package/dist/review/render.js
CHANGED
|
@@ -55,8 +55,13 @@ export function renderText(report, sessionDir) {
|
|
|
55
55
|
if (refuted.length > 0) {
|
|
56
56
|
lines.push(`Verification dropped ${refuted.length} finding(s) the code disproves (see the JSON report).`);
|
|
57
57
|
}
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
const fromRepository = report.remembered.filter((e) => e.source === "repository").length;
|
|
59
|
+
const fromAccount = report.remembered.length - fromRepository;
|
|
60
|
+
if (fromRepository > 0) {
|
|
61
|
+
lines.push(`${fromRepository} finding(s) matched the repository's memory and were not reported.`);
|
|
62
|
+
}
|
|
63
|
+
if (fromAccount > 0) {
|
|
64
|
+
lines.push(`${fromAccount} finding(s) matched your ocra Cloud memory and were not reported.`);
|
|
60
65
|
}
|
|
61
66
|
const rereview = report.rereview;
|
|
62
67
|
if (report.scope?.mode === "incremental") {
|
package/dist/review/sarif.js
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CliConfig } from "./config.js";
|
|
2
|
+
export type SettingSource = "file" | "account" | "file+account" | "default";
|
|
3
|
+
export interface EffectiveSetting {
|
|
4
|
+
key: string;
|
|
5
|
+
value?: unknown;
|
|
6
|
+
source: SettingSource;
|
|
7
|
+
}
|
|
8
|
+
/** file: the configuration before the account's settings; effective: after; filled: what they filled. */
|
|
9
|
+
export declare function effectiveSettings(file: CliConfig, effective: CliConfig, filled: readonly string[]): EffectiveSetting[];
|
|
10
|
+
export declare function renderSettings(settings: readonly EffectiveSetting[], account: CliConfig["accountSettings"]): string;
|
|
11
|
+
//# sourceMappingURL=settings-sources.d.ts.map
|