@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.
Files changed (45) hide show
  1. package/README.md +3 -3
  2. package/dist/account-salt.d.ts +7 -0
  3. package/dist/account-salt.js +43 -0
  4. package/dist/cloud.d.ts +34 -0
  5. package/dist/cloud.js +272 -0
  6. package/dist/plugin-store.d.ts +20 -0
  7. package/dist/plugin-store.js +84 -0
  8. package/dist/plugins-command.d.ts +15 -0
  9. package/dist/plugins-command.js +183 -0
  10. package/dist/review/account-plugins.d.ts +23 -0
  11. package/dist/review/account-plugins.js +54 -0
  12. package/dist/review/account-settings.d.ts +100 -0
  13. package/dist/review/account-settings.js +106 -0
  14. package/dist/review/args.d.ts +2 -1
  15. package/dist/review/args.js +5 -0
  16. package/dist/review/cloud-findings.d.ts +31 -0
  17. package/dist/review/cloud-findings.js +80 -0
  18. package/dist/review/cloud-memory.d.ts +6 -0
  19. package/dist/review/cloud-memory.js +58 -0
  20. package/dist/review/cloud-providers.d.ts +12 -0
  21. package/dist/review/cloud-providers.js +60 -0
  22. package/dist/review/cloud-review.d.ts +19 -0
  23. package/dist/review/cloud-review.js +51 -0
  24. package/dist/review/cloud-settings.d.ts +16 -0
  25. package/dist/review/cloud-settings.js +118 -0
  26. package/dist/review/cloud-upload.d.ts +60 -0
  27. package/dist/review/cloud-upload.js +180 -0
  28. package/dist/review/command.d.ts +4 -0
  29. package/dist/review/command.js +91 -14
  30. package/dist/review/config.d.ts +65 -6
  31. package/dist/review/config.js +71 -7
  32. package/dist/review/plan-render.js +1 -1
  33. package/dist/review/plugins.d.ts +10 -0
  34. package/dist/review/plugins.js +46 -1
  35. package/dist/review/provenance.js +15 -2
  36. package/dist/review/remote-config.d.ts +6 -0
  37. package/dist/review/remote-config.js +3 -1
  38. package/dist/review/render.js +7 -2
  39. package/dist/review/sarif.js +1 -1
  40. package/dist/review/settings-sources.d.ts +11 -0
  41. package/dist/review/settings-sources.js +58 -0
  42. package/dist/review/target.d.ts +1 -0
  43. package/dist/review/target.js +4 -1
  44. package/dist/run.js +36 -0
  45. package/package.json +8 -8
@@ -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 { ConfigError } from "./config.js";
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
- const { config } = target;
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 registry = await startPlugins([...builtins, ...target.plugins], {
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
- ? config.pluginSettings
52
- : { ...config.pluginSettings, [sessionJsonlPlugin.name]: session },
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
- rules: [...registry.rules, ...config.rules],
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
- ...(args.ultra ? { ultra: true } : {}),
110
+ ...(ultra ? { ultra: true } : {}),
67
111
  ...(config.maxTasks !== undefined ? { maxTasks: config.maxTasks } : {}),
68
112
  });
69
- const rendered = args.format === "json" ? `${safeJson(toPlanOutput(preview))}\n` : renderPlan(preview);
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: config.models,
80
- env: deps.env,
81
- providers: config.providers,
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: { ocraVersion: VERSION, configHash: configHash(config, args), sampling },
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
- ...(args.ultra ? { ultra: true } : {}),
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: [...registry.rules, ...config.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;
@@ -1,13 +1,37 @@
1
- import { type CustomProvider, type ModelChains, OcraError, type RepoRule } from "@open-cr-agent/core";
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 configSchema: z.ZodObject<{
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.ZodDefault<z.ZodUnion<[z.ZodEnum<{
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: RepoRule[];
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
@@ -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 configSchema = z
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
- runtime: z.enum(BUILTIN_RUNTIMES).or(z.string().min(1)).default("opencode"),
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
- for (const model of unpriced(parsed.providers, models)) {
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 { ...parsed, models, providers: toProviders(parsed.providers), rules };
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, models) {
171
- const named = new Set(Object.values(models).flat());
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:");
@@ -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
@@ -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
- const { sampling: _, providers, extends: shared, ...rest } = config;
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)
@@ -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
- if (report.remembered.length > 0) {
59
- lines.push(`${report.remembered.length} finding(s) matched the repository's memory and were not reported.`);
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") {
@@ -31,7 +31,7 @@ export function renderSarif(report, version) {
31
31
  tool: {
32
32
  driver: {
33
33
  name: "ocra",
34
- informationUri: "https://ocra.majincheng.com",
34
+ informationUri: "https://ocracloud.com",
35
35
  version,
36
36
  semanticVersion: version,
37
37
  rules: categories.map((id) => ({
@@ -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