omp-conductor 0.18.1 → 0.18.2
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 +1 -1
- package/REFERENCE.md +1 -1
- package/agents/to-spec.md +6 -2
- package/package.json +1 -1
- package/schema/config.schema.json +6 -1
- package/src/arm-challenge.ts +250 -57
- package/src/briefs/orchestrator.md +21 -8
- package/src/briefs/to-spec.md +6 -2
- package/src/cli.ts +122 -1
- package/src/command-manifest.ts +16 -5
- package/src/commands/arm.ts +1 -1
- package/src/commands/intake.ts +4 -19
- package/src/commands/watch.ts +4 -17
- package/src/config-schema.ts +19 -6
- package/src/config.ts +23 -8
- package/src/daemon.ts +150 -241
- package/src/decisions.ts +19 -11
- package/src/doctor.ts +55 -129
- package/src/escalate.ts +22 -11
- package/src/fleet.ts +93 -218
- package/src/host.ts +7 -332
- package/src/omp-settings.ts +19 -0
- package/src/omp.ts +11 -56
- package/src/orchestrator-tick.ts +225 -37
- package/src/session-host.ts +6 -41
- package/src/setup-host.ts +27 -10
- package/src/setup-wizard.ts +141 -1
- package/src/setup.ts +54 -4
- package/src/status-render.ts +138 -1
- package/src/to-spec.ts +23 -2
- package/src/types.ts +62 -6
- package/src/verbs/server.ts +38 -3
- package/src/worker.ts +0 -13
package/src/cli.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { userInfo } from "node:os";
|
|
11
11
|
import { loadConfig } from "./config.ts";
|
|
12
|
-
import { COMMAND_MANIFEST, renderUsage } from "./command-manifest.ts";
|
|
12
|
+
import { COMMAND_MANIFEST, renderUsage, type CommandManifestEntry } from "./command-manifest.ts";
|
|
13
13
|
import { armCommand } from "./commands/arm.ts";
|
|
14
14
|
import { boardCommand } from "./commands/board.ts";
|
|
15
15
|
import { briefUpgradeCommand } from "./commands/brief-upgrade.ts";
|
|
@@ -55,6 +55,104 @@ import {
|
|
|
55
55
|
|
|
56
56
|
const USAGE = renderUsage(COMMAND_MANIFEST);
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Verbs that define every `--help`/`-h` shape themselves, so the dispatch
|
|
60
|
+
* gate below must not pre-empt them: `help` and the version aliases print
|
|
61
|
+
* their own text, `setup` accepts help only as the first trailing token
|
|
62
|
+
* (`setup bogus --help` must reject `bogus`, never print a help read), and
|
|
63
|
+
* the hand-rolled usage verbs print help at `argv[1]` or refuse a trailing
|
|
64
|
+
* unknown token with exit 2 — every one of them already before any side
|
|
65
|
+
* effect. `intake` and `watch` are deliberately absent: they validate their
|
|
66
|
+
* tails only after `loadConfig`/`openStore`, so an unconsumed trailing help
|
|
67
|
+
* must be answered by the gate before the handler can create the store
|
|
68
|
+
* (#863).
|
|
69
|
+
*/
|
|
70
|
+
const HELP_OWNED_VERBS: Record<string, true> = {
|
|
71
|
+
"--help": true,
|
|
72
|
+
"-h": true,
|
|
73
|
+
help: true,
|
|
74
|
+
"--version": true,
|
|
75
|
+
"-V": true,
|
|
76
|
+
version: true,
|
|
77
|
+
setup: true,
|
|
78
|
+
dashboard: true,
|
|
79
|
+
doctor: true,
|
|
80
|
+
stats: true,
|
|
81
|
+
drain: true,
|
|
82
|
+
"restore-db": true,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* `setup`'s install subcommands — the one `setup` shape the wizard does not
|
|
87
|
+
* safely own: help is recognized only at the first trailing token, so
|
|
88
|
+
* `setup host --help` would reach the privileged install planner (creating
|
|
89
|
+
* the state db and staging the host install) instead of printing help. The
|
|
90
|
+
* dispatch gate answers real help requests in their tail instead.
|
|
91
|
+
*/
|
|
92
|
+
const SETUP_INSTALL_SUBCOMMANDS: Record<string, true> = {
|
|
93
|
+
host: true,
|
|
94
|
+
graph: true,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Whether an invocation carries an actual `--help`/`-h` request — a token the
|
|
99
|
+
* verb's own grammar does not consume. A token immediately after a
|
|
100
|
+
* value-taking flag is that flag's value (`report --text --help` sends the
|
|
101
|
+
* literal text "--help", exactly as before), and a bare `--` makes everything
|
|
102
|
+
* after it positional; only an unconsumed help token is really a request.
|
|
103
|
+
*/
|
|
104
|
+
function helpRequested(argv: readonly string[], entry: CommandManifestEntry): boolean {
|
|
105
|
+
let consumeNext = false;
|
|
106
|
+
for (const token of argv.slice(1)) {
|
|
107
|
+
if (consumeNext) {
|
|
108
|
+
consumeNext = false;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (token === "--") {
|
|
112
|
+
// A bare `--` ends option parsing for a verb whose manifest actually
|
|
113
|
+
// declares positionals: after it, `--help` is positional data, not a
|
|
114
|
+
// request. A flag-only verb has no positional grammar for `--` to
|
|
115
|
+
// protect, so `stop --all -- --help` (#863) must stay a help read.
|
|
116
|
+
if ((entry.positionals?.length ?? 0) > 0) return false;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (token === "--help" || token === "-h") return true;
|
|
120
|
+
// `--flag VALUE` consumes the next token as the flag's value, but
|
|
121
|
+
// `--flag=VALUE` binds the value inline — so only the split form can
|
|
122
|
+
// swallow a following `--help`. `stop --project=conductor --help` is a
|
|
123
|
+
// help read (#863), and `--project conductor --help` must read the next
|
|
124
|
+
// token as the value, exactly as `report --text --help` sends literal
|
|
125
|
+
// text. A token without `=` is a separate argument: when it names a
|
|
126
|
+
// value-taking flag, its value is the next token.
|
|
127
|
+
consumeNext =
|
|
128
|
+
token.startsWith("--") &&
|
|
129
|
+
!token.includes("=") &&
|
|
130
|
+
entry.flags.some((flag) => flag.name === token && flag.takesValue);
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* One verb's usage block from the manifest — what `VERB --help` prints. The
|
|
137
|
+
* manifest's `details` is that verb's long-form semantics, and rendering it
|
|
138
|
+
* here is what keeps the generated read complete: the dispatch gate above
|
|
139
|
+
* answers help for every verb that does not own it, so a verb whose prose
|
|
140
|
+
* lived only in its own hand-rolled usage string would silently lose it
|
|
141
|
+
* (#863 — `watch`'s no-answer/auto-withdraw semantics and `intake`'s
|
|
142
|
+
* durability/groomed no-op semantics).
|
|
143
|
+
*/
|
|
144
|
+
function renderCommandHelp(entry: CommandManifestEntry): string {
|
|
145
|
+
const lines = [`omp-conductor ${entry.name} — ${entry.description}`, "", "usage:"];
|
|
146
|
+
for (const usage of entry.usage) lines.push(` omp-conductor ${usage}`);
|
|
147
|
+
if (entry.flags.length > 0) {
|
|
148
|
+
lines.push("", "flags:");
|
|
149
|
+
const width = Math.max(...entry.flags.map((flag) => flag.name.length));
|
|
150
|
+
for (const flag of entry.flags) lines.push(` ${flag.name.padEnd(width)} ${flag.description}`);
|
|
151
|
+
}
|
|
152
|
+
if (entry.details !== undefined) lines.push("", entry.details);
|
|
153
|
+
return lines.join("\n");
|
|
154
|
+
}
|
|
155
|
+
|
|
58
156
|
/** Applies both `--port 9000` and `--port=9000`; returns undefined when absent. */
|
|
59
157
|
function flag(argv: string[], name: string): string | undefined {
|
|
60
158
|
const i = argv.indexOf(`--${name}`);
|
|
@@ -254,6 +352,29 @@ export async function runCli(argv: string[] = process.argv.slice(2)): Promise<vo
|
|
|
254
352
|
);
|
|
255
353
|
process.exit(2);
|
|
256
354
|
}
|
|
355
|
+
// A real `--help`/`-h` (one no declared flag consumes) is a help read and
|
|
356
|
+
// must never reach a mutating command: `stop --all --help` is how #863
|
|
357
|
+
// disarmed the fleet. Recognized here — before project-selection
|
|
358
|
+
// (`targetProjects` is a lazy closure; no config is loaded) and before
|
|
359
|
+
// handler execution — so a verb that does not validate its own tail can
|
|
360
|
+
// no longer treat the request as an argument.
|
|
361
|
+
const entry = cmd === undefined ? undefined : COMMAND_MANIFEST.find((entry) => entry.name === cmd);
|
|
362
|
+
let help = false;
|
|
363
|
+
if (entry !== undefined) {
|
|
364
|
+
if (cmd === "setup") {
|
|
365
|
+
// `setup` owns its help (only at the first trailing token, which is
|
|
366
|
+
// what keeps `setup bogus --help` rejecting `bogus`). The install
|
|
367
|
+
// subcommands are the exception: a help token in their tail must be
|
|
368
|
+
// answered here, before `loadConfig` and the install plan (#863).
|
|
369
|
+
help = SETUP_INSTALL_SUBCOMMANDS[argv[1] ?? ""] === true && helpRequested(argv, entry);
|
|
370
|
+
} else if (HELP_OWNED_VERBS[cmd ?? ""] !== true) {
|
|
371
|
+
help = helpRequested(argv, entry);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (help && entry !== undefined) {
|
|
375
|
+
process.stdout.write(`${renderCommandHelp(entry)}\n`);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
257
378
|
// A "none"-scope verb does no per-project work, so --project cannot change
|
|
258
379
|
// what it does: refuse it with a reason instead of silently ignoring it.
|
|
259
380
|
if (COMMAND_SCOPES[cmd ?? ""] === "none" && projectFlag !== undefined) {
|
package/src/command-manifest.ts
CHANGED
|
@@ -63,7 +63,6 @@ export const COMMAND_MANIFEST: readonly CommandManifestEntry[] = [
|
|
|
63
63
|
"setup host [NAME] [--project NAME]",
|
|
64
64
|
"setup graph [--no-seed] [--print] [--project NAME]",
|
|
65
65
|
],
|
|
66
|
-
details: COMMAND_DETAILS,
|
|
67
66
|
subcommands: [
|
|
68
67
|
{ name: "host", description: "stage and install the host services" },
|
|
69
68
|
{ name: "graph", description: "install and seed code-graph indexes" },
|
|
@@ -418,6 +417,11 @@ export const COMMAND_MANIFEST: readonly CommandManifestEntry[] = [
|
|
|
418
417
|
"watch list [--project NAME] [--json]",
|
|
419
418
|
"watch withdraw <id> [--reason TEXT] [--project NAME]",
|
|
420
419
|
],
|
|
420
|
+
details: `add records a row the daemon checks for you and the next tick reads, with no
|
|
421
|
+
operator answer needed. list shows open watches, oldest first. withdraw ends
|
|
422
|
+
one with a recorded reason — the verb that creates a watch is the verb that
|
|
423
|
+
ends it. A watch whose PR condition can no longer be observed (the PR merged
|
|
424
|
+
or closed first) is withdrawn by the daemon itself.`,
|
|
421
425
|
subcommands: [
|
|
422
426
|
{ name: "add", description: "record a watch" },
|
|
423
427
|
{ name: "list", description: "list open watches" },
|
|
@@ -443,6 +447,12 @@ export const COMMAND_MANIFEST: readonly CommandManifestEntry[] = [
|
|
|
443
447
|
"intake dismiss <id> [--project NAME]",
|
|
444
448
|
"intake groomed <id> --issue <url> [--project NAME]",
|
|
445
449
|
],
|
|
450
|
+
details: `Captures one raw idea into the local store and prints its id. list shows what
|
|
451
|
+
is still pending (id, age, text), oldest first; dismiss drops one by id. The
|
|
452
|
+
capture is durable — it lives in the sqlite store, not in a session — so it
|
|
453
|
+
survives daemon restarts. The orchestrator files the idea as an issue and then
|
|
454
|
+
marks that provenance with groomed: an id already resolved is a no-op with a
|
|
455
|
+
message, never an error, because ticks retry.`,
|
|
446
456
|
subcommands: [
|
|
447
457
|
{ name: "list", description: "list pending intake items" },
|
|
448
458
|
{ name: "dismiss", description: "dismiss an intake item" },
|
|
@@ -456,7 +466,6 @@ export const COMMAND_MANIFEST: readonly CommandManifestEntry[] = [
|
|
|
456
466
|
description: "print command usage (also --help, -h)",
|
|
457
467
|
scope: "none",
|
|
458
468
|
usage: ["help"],
|
|
459
|
-
details: COMMAND_HELP_TAIL,
|
|
460
469
|
flags: [],
|
|
461
470
|
},
|
|
462
471
|
{
|
|
@@ -480,9 +489,11 @@ export function renderUsage(manifest: readonly CommandManifestEntry[] = COMMAND_
|
|
|
480
489
|
const commands = manifest.map(
|
|
481
490
|
(command) => ` ${command.name.padEnd(width)} ${command.description}`,
|
|
482
491
|
);
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
492
|
+
// The fleet-wide operator reference, printed once. It is *not* harvested
|
|
493
|
+
// from `details`: that field is one verb's own long-form help, which
|
|
494
|
+
// `VERB --help` renders, so collecting it here would print `setup`'s
|
|
495
|
+
// carrier copy of this very blob and repeat each verb's prose twice.
|
|
496
|
+
const details = [...COMMAND_DETAILS.split("\n"), "", ...COMMAND_HELP_TAIL.split("\n"), ""];
|
|
486
497
|
return [
|
|
487
498
|
"omp-conductor — dispatch ready issues to omp coding sessions",
|
|
488
499
|
"",
|
package/src/commands/arm.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `arm` — proof-gated: send a Telegram challenge and write the arm marker only after the
|
|
2
|
+
* `arm` — proof-gated: send a Telegram challenge and write the arm marker only after the orchestrator's inbound adapter acknowledges the reply in conductor state (#614).
|
|
3
3
|
*
|
|
4
4
|
* Moved out of cli.ts's switch by the per-verb module split (#462);
|
|
5
5
|
* only the case wrapper, the injected `ctx` lookups and the imports
|
package/src/commands/intake.ts
CHANGED
|
@@ -12,21 +12,6 @@ import type { CommandContext } from "./context.ts";
|
|
|
12
12
|
import { findProject, loadConfig } from "../config.ts";
|
|
13
13
|
import { dbPath, openStore } from "../store.ts";
|
|
14
14
|
|
|
15
|
-
const INTAKE_USAGE = `omp-conductor intake — capture raw ideas durably.
|
|
16
|
-
|
|
17
|
-
usage:
|
|
18
|
-
omp-conductor intake "<text>" [--project NAME]
|
|
19
|
-
omp-conductor intake list [--project NAME]
|
|
20
|
-
omp-conductor intake dismiss <id> [--project NAME]
|
|
21
|
-
omp-conductor intake groomed <id> --issue <url> [--project NAME]
|
|
22
|
-
|
|
23
|
-
Captures one raw idea into the local store and prints its id. list shows what
|
|
24
|
-
is still pending (id, age, text), oldest first; dismiss drops one by id. The
|
|
25
|
-
capture is durable — it lives in the sqlite store, not in a session — so it
|
|
26
|
-
survives daemon restarts. The orchestrator files the idea as an issue and then
|
|
27
|
-
marks that provenance with groomed: an id already resolved is a no-op with a
|
|
28
|
-
message, never an error, because ticks retry.`;
|
|
29
|
-
|
|
30
15
|
/** Flags the intake surface understands. `--project` is consumed by
|
|
31
16
|
* {@link CommandContext.projectFlag}; the value token stays in argv. */
|
|
32
17
|
const INTAKE_FLAGS: Record<string, true> = { "--project": true, "--issue": true };
|
|
@@ -49,11 +34,11 @@ function assertKnownArgs(ctx: CommandContext, from: number): void {
|
|
|
49
34
|
}
|
|
50
35
|
|
|
51
36
|
export async function intakeCommand(ctx: CommandContext): Promise<void> {
|
|
37
|
+
// `--help`/`-h` never arrives here: the CLI dispatch gate answers a real
|
|
38
|
+
// help request from the manifest before this handler runs (#863), which is
|
|
39
|
+
// why the long-form prose lives on the manifest's `details` field rather
|
|
40
|
+
// than in a usage string this module would print itself.
|
|
52
41
|
const sub = ctx.argv[1];
|
|
53
|
-
if (sub === "--help" || sub === "-h") {
|
|
54
|
-
process.stdout.write(INTAKE_USAGE);
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
42
|
const project = findProject(loadConfig(), ctx.projectFlag);
|
|
58
43
|
const store = openStore(dbPath());
|
|
59
44
|
try {
|
package/src/commands/watch.ts
CHANGED
|
@@ -24,19 +24,6 @@ import { CONDITION_FORMS, parseCondition } from "../decisions.ts";
|
|
|
24
24
|
import { shellQuote } from "../shell.ts";
|
|
25
25
|
import { dbPath, openStore } from "../store.ts";
|
|
26
26
|
|
|
27
|
-
const WATCH_USAGE = `omp-conductor watch — set a condition or carry note for the orchestrator itself.
|
|
28
|
-
|
|
29
|
-
usage:
|
|
30
|
-
omp-conductor watch add --note TEXT [--blocks TEXT] [--resolves-when COND] [--project NAME]
|
|
31
|
-
omp-conductor watch list [--project NAME] [--json]
|
|
32
|
-
omp-conductor watch withdraw <id> [--reason TEXT] [--project NAME]
|
|
33
|
-
|
|
34
|
-
add records a row the daemon checks for you and the next tick reads, with no
|
|
35
|
-
operator answer needed. list shows open watches, oldest first. withdraw ends
|
|
36
|
-
one with a recorded reason — the verb that creates a watch is the verb that
|
|
37
|
-
ends it. A watch whose PR condition can no longer be observed (the PR merged
|
|
38
|
-
or closed first) is withdrawn by the daemon itself.`;
|
|
39
|
-
|
|
40
27
|
/** The flags each watch subcommand accepts, after the subcommand itself.
|
|
41
28
|
* `add`'s positional note and `withdraw`'s positional id are validated by
|
|
42
29
|
* the branches below, not through this table. */
|
|
@@ -76,11 +63,11 @@ function assertKnownWatchArgs(ctx: CommandContext, sub: string, from: number): v
|
|
|
76
63
|
}
|
|
77
64
|
|
|
78
65
|
export async function watchCommand(ctx: CommandContext): Promise<void> {
|
|
66
|
+
// `--help`/`-h` never arrives here: the CLI dispatch gate answers a real
|
|
67
|
+
// help request from the manifest before this handler runs (#863), which is
|
|
68
|
+
// why the long-form prose lives on the manifest's `details` field rather
|
|
69
|
+
// than in a usage string this module would print itself.
|
|
79
70
|
const sub = ctx.argv[1];
|
|
80
|
-
if (sub === "--help" || sub === "-h") {
|
|
81
|
-
process.stdout.write(WATCH_USAGE);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
71
|
const project = findProject(loadConfig(), ctx.projectFlag);
|
|
85
72
|
const store = openStore(dbPath());
|
|
86
73
|
try {
|
package/src/config-schema.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
DEFAULT_AUTHORITY,
|
|
32
32
|
DEFAULT_CAPS,
|
|
33
33
|
DEFAULT_PROJECT_POLICY,
|
|
34
|
+
DEFAULT_REVIEW_ADJUDICATOR_ROLE,
|
|
34
35
|
DEFAULT_REVIEW_MAX_ROUNDS,
|
|
35
36
|
DEFAULT_REVIEW_STRICTNESS,
|
|
36
37
|
DRAFT_POLICIES,
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
RELEASE_REQUIREMENTS,
|
|
42
43
|
RELEASE_SHAPES,
|
|
43
44
|
REPORT_SCOPES,
|
|
45
|
+
REVIEW_ADJUDICATOR_RE,
|
|
44
46
|
REVIEW_MAX_ROUNDS_MAX,
|
|
45
47
|
REVIEW_MAX_ROUNDS_MIN,
|
|
46
48
|
REVIEW_STRICTNESS,
|
|
@@ -265,11 +267,15 @@ const armSchema = z
|
|
|
265
267
|
|
|
266
268
|
/**
|
|
267
269
|
* The per-project review policy (#678): how strictly green PRs are reviewed
|
|
268
|
-
* and returned,
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
270
|
+
* and returned, the hard ceiling on review rounds per PR lifecycle, and which
|
|
271
|
+
* OMP model role carries the terminal review-ceiling adjudication (#875).
|
|
272
|
+
* Absent or a legacy config without the keys loads as `medium` with
|
|
273
|
+
* {@link DEFAULT_REVIEW_MAX_ROUNDS} rounds and
|
|
274
|
+
* {@link DEFAULT_REVIEW_ADJUDICATOR_ROLE} as adjudicator — the recommended
|
|
275
|
+
* defaults for a new project, materialised deterministically for every
|
|
276
|
+
* existing one, with the schema bounds and the runtime defaults read from the
|
|
277
|
+
* same constants. The adjudicator names a role only; a provider/model is
|
|
278
|
+
* refused here, because OMP owns exact provider selection.
|
|
273
279
|
*/
|
|
274
280
|
const reviewSchema = z
|
|
275
281
|
.object({
|
|
@@ -280,9 +286,16 @@ const reviewSchema = z
|
|
|
280
286
|
.min(REVIEW_MAX_ROUNDS_MIN)
|
|
281
287
|
.max(REVIEW_MAX_ROUNDS_MAX)
|
|
282
288
|
.default(DEFAULT_REVIEW_MAX_ROUNDS),
|
|
289
|
+
adjudicator: z
|
|
290
|
+
.string()
|
|
291
|
+
.regex(
|
|
292
|
+
REVIEW_ADJUDICATOR_RE,
|
|
293
|
+
"must name one OMP model role — a single role token like \"task\" (never a provider/model, which omp owns)",
|
|
294
|
+
)
|
|
295
|
+
.default(DEFAULT_REVIEW_ADJUDICATOR_ROLE),
|
|
283
296
|
})
|
|
284
297
|
.strict()
|
|
285
|
-
.describe("Review strictness
|
|
298
|
+
.describe("Review strictness, round ceiling and adjudicator role for green PRs");
|
|
286
299
|
|
|
287
300
|
const releasePolicySchema = z.union([
|
|
288
301
|
releasePolicyLegacyEnum,
|
package/src/config.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
DEFAULT_PROJECT_POLICY,
|
|
29
29
|
DEFAULT_REPORT_POLICY,
|
|
30
30
|
DEFAULT_REPORT_SCOPE,
|
|
31
|
+
DEFAULT_REVIEW_ADJUDICATOR_ROLE,
|
|
31
32
|
DEFAULT_REVIEW_MAX_ROUNDS,
|
|
32
33
|
DEFAULT_REVIEW_POLICY,
|
|
33
34
|
DEFAULT_REVIEW_STRICTNESS,
|
|
@@ -349,12 +350,20 @@ export function resolveArmProof(p: ProjectConfig): ArmProof {
|
|
|
349
350
|
* The loader always materialises `review` (finalizeProject), so this is only
|
|
350
351
|
* ever the fallback for a `ProjectConfig` that never went through it — a
|
|
351
352
|
* hand-built one in a test, or a config written before the key existed.
|
|
352
|
-
* Absent
|
|
353
|
-
*
|
|
354
|
-
*
|
|
353
|
+
* Absent (or a legacy config that stopped at strictness and maxRounds)
|
|
354
|
+
* resolves to {@link DEFAULT_REVIEW_POLICY}, the documented migration default
|
|
355
|
+
* every existing install deterministically upgrades to — the adjudicator role
|
|
356
|
+
* included, so no consumer ever reads a policy without one (#875). A fresh
|
|
357
|
+
* object each call, so mutating the result cannot reach the config it came
|
|
358
|
+
* from.
|
|
355
359
|
*/
|
|
356
360
|
export function resolveReview(p: ProjectConfig): ReviewPolicy {
|
|
357
|
-
|
|
361
|
+
const base = p.review ?? DEFAULT_REVIEW_POLICY;
|
|
362
|
+
return {
|
|
363
|
+
strictness: base.strictness ?? DEFAULT_REVIEW_STRICTNESS,
|
|
364
|
+
maxRounds: base.maxRounds ?? DEFAULT_REVIEW_MAX_ROUNDS,
|
|
365
|
+
adjudicator: base.adjudicator ?? DEFAULT_REVIEW_ADJUDICATOR_ROLE,
|
|
366
|
+
};
|
|
358
367
|
}
|
|
359
368
|
|
|
360
369
|
/**
|
|
@@ -889,11 +898,14 @@ function clauseFor(rel: readonly PropertyKey[], issue: {
|
|
|
889
898
|
return `policy must be an object with "merge" and "release" sections`;
|
|
890
899
|
}
|
|
891
900
|
if (rel.length === 1 && relStr === "review") {
|
|
892
|
-
return `review must be an object with "strictness" and "
|
|
901
|
+
return `review must be an object with "strictness", "maxRounds" and "adjudicator"`;
|
|
893
902
|
}
|
|
894
903
|
if (rel.length === 2 && rel[0] === "review" && rel[1] === "maxRounds") {
|
|
895
904
|
return `review.maxRounds must be an integer between ${REVIEW_MAX_ROUNDS_MIN} and ${REVIEW_MAX_ROUNDS_MAX}, found ${found()}`;
|
|
896
905
|
}
|
|
906
|
+
if (rel.length === 2 && rel[0] === "review" && rel[1] === "adjudicator") {
|
|
907
|
+
return `review.adjudicator must be a string naming one OMP model role, found ${found()}`;
|
|
908
|
+
}
|
|
897
909
|
if (rel.length === 2 && relStr === "policy") return `${pathStr} must be an object`;
|
|
898
910
|
if (rel.length === 1 && relStr === "caps") return `caps must be an object`;
|
|
899
911
|
if (rel.length === 2 && relStr === "caps" && rel[1] === "planUsage") {
|
|
@@ -944,7 +956,7 @@ function clauseFor(rel: readonly PropertyKey[], issue: {
|
|
|
944
956
|
if (issue.code === "unrecognized_keys") {
|
|
945
957
|
const keys = (issue.keys ?? []).join(", ");
|
|
946
958
|
if (pathStr === "policy") return `policy has unknown key(s): ${keys} — expected "merge" or "release"`;
|
|
947
|
-
if (pathStr === "review") return `review has unknown key(s): ${keys} — expected "strictness" and "
|
|
959
|
+
if (pathStr === "review") return `review has unknown key(s): ${keys} — expected "strictness", "maxRounds" and "adjudicator"`;
|
|
948
960
|
if (pathStr === "policy.merge") return `${pathStr} has unknown key(s): ${keys} — expected ${POLICY_MERGE_KEYS}`;
|
|
949
961
|
if (pathStr === "policy.release") return `${pathStr} has unknown key(s): ${keys} — expected ${POLICY_RELEASE_KEYS}`;
|
|
950
962
|
if (pathStr === "caps.planUsage") {
|
|
@@ -1320,14 +1332,17 @@ function finalizeArm(parsed: Raw | undefined): ProjectConfig["arm"] {
|
|
|
1320
1332
|
* {@link DEFAULT_REVIEW_POLICY}, the documented migration default, so an
|
|
1321
1333
|
* upgrade changes no existing project's behaviour beyond what its operators
|
|
1322
1334
|
* then choose in setup. zod has already rejected anything outside the
|
|
1323
|
-
* strictness vocabulary
|
|
1324
|
-
* absent-case
|
|
1335
|
+
* strictness vocabulary, the validated round range or the adjudicator role
|
|
1336
|
+
* token grammar, so this only fills the absent-case defaults — the
|
|
1337
|
+
* adjudicator included (#875), deterministically the same role a new
|
|
1338
|
+
* project's setup starts from.
|
|
1325
1339
|
*/
|
|
1326
1340
|
function finalizeReview(parsed: Raw | undefined): ProjectConfig["review"] {
|
|
1327
1341
|
if (parsed === undefined) return { ...DEFAULT_REVIEW_POLICY };
|
|
1328
1342
|
return {
|
|
1329
1343
|
strictness: (parsed["strictness"] as ReviewStrictness | undefined) ?? DEFAULT_REVIEW_STRICTNESS,
|
|
1330
1344
|
maxRounds: (parsed["maxRounds"] as number | undefined) ?? DEFAULT_REVIEW_MAX_ROUNDS,
|
|
1345
|
+
adjudicator: (parsed["adjudicator"] as string | undefined) ?? DEFAULT_REVIEW_ADJUDICATOR_ROLE,
|
|
1331
1346
|
};
|
|
1332
1347
|
}
|
|
1333
1348
|
|