omp-conductor 0.17.0 → 0.18.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/REFERENCE.md +12 -8
- package/package.json +1 -1
- package/schema/config.schema.json +40 -1
- package/src/admission.ts +263 -44
- package/src/ask.ts +39 -3
- package/src/availability.ts +27 -1
- package/src/backups.ts +2 -2
- package/src/briefs/orchestrator.md +1 -0
- package/src/briefs/worker.md +38 -19
- package/src/command-help.ts +8 -1
- package/src/command-manifest.ts +5 -2
- package/src/commands/arm.ts +6 -3
- package/src/commands/message.ts +32 -4
- package/src/commands/watch.ts +62 -3
- package/src/config-schema.ts +53 -0
- package/src/config.ts +97 -1
- package/src/daemon.ts +1479 -1483
- package/src/decisions.ts +51 -6
- package/src/depends-on.ts +261 -1
- package/src/diff-flags.ts +350 -0
- package/src/digest-schedule.ts +37 -0
- package/src/doctor.ts +310 -22
- package/src/escalate.ts +560 -57
- package/src/failure-class.ts +71 -15
- package/src/fleet.ts +189 -34
- package/src/gitops.ts +103 -24
- package/src/graph-health.ts +20 -7
- package/src/graph.ts +313 -68
- package/src/lifecycle.ts +43 -7
- package/src/omp.ts +42 -0
- package/src/orchestrator-tick.ts +430 -162
- package/src/release-policy.ts +177 -5
- package/src/routing.ts +11 -3
- package/src/session-host.ts +16 -0
- package/src/settlement.ts +1728 -0
- package/src/setup-host.ts +193 -4
- package/src/setup-install.ts +91 -30
- package/src/setup-wizard.ts +1257 -78
- package/src/setup.ts +153 -6
- package/src/status-render.ts +36 -4
- package/src/store.ts +411 -17
- package/src/tracker/github.ts +607 -12
- package/src/types.ts +331 -5
- package/src/upgrade.ts +50 -19
- package/src/verbs/actions.ts +66 -18
- package/src/verbs/protocol.ts +45 -0
- package/src/verbs/server.ts +270 -13
- package/src/worker.ts +239 -6
- package/src/worktree.ts +115 -8
- package/systemd/omp-conductor-recover.sh +73 -0
- package/systemd/recover-unit-test.sh +61 -0
package/src/release-policy.ts
CHANGED
|
@@ -280,7 +280,7 @@ function wholePackageBunTest(segment: string): boolean {
|
|
|
280
280
|
* the host were all plain invocations, and refusing the sanctioned parse check
|
|
281
281
|
* would teach workers to skip it rather than stop the load.
|
|
282
282
|
*/
|
|
283
|
-
const SHARED_HOST_SCRIPTS = [
|
|
283
|
+
export const SHARED_HOST_SCRIPTS: readonly string[] = [
|
|
284
284
|
"herdr/test/recover-test.sh",
|
|
285
285
|
"test/setup-test.sh",
|
|
286
286
|
"setup.sh",
|
|
@@ -298,6 +298,147 @@ function sharedHostScriptMatch(segment: string): string | undefined {
|
|
|
298
298
|
return undefined;
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
/** The `git [-C <dir>] tag` prefix; the tail after it decides read vs write. */
|
|
302
|
+
const GIT_TAG_HEAD = /^git(?:\s+-[Cc]\s+\S+)*\s+tag(?=\s|$)/;
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Whether a segment that opens with `git [-C <dir>] tag` is a write — tag
|
|
306
|
+
* creation or deletion — rather than a read. The tripwire used to match the
|
|
307
|
+
* `git tag` prefix alone (#696), so a worker orienting itself with
|
|
308
|
+
* `git tag | head -5` was refused as an attempted release and the refusal
|
|
309
|
+
* reached the digest as release-policy drift. Git writes tags in exactly two
|
|
310
|
+
* shapes, and every other form is a read:
|
|
311
|
+
*
|
|
312
|
+
* - creation needs a tagname operand: `git tag <name>` and the
|
|
313
|
+
* `-a`/`-s`/`-u`/`-t`/`-m`/`-F`/`-f` forms;
|
|
314
|
+
* - deletion needs `-d`/`--delete`;
|
|
315
|
+
* - bare `git tag`, `-l`/`--list` (with an optional pattern), `-n`,
|
|
316
|
+
* `--contains`, `--points-at`, `--sort`, `--merged`/`--no-merged`,
|
|
317
|
+
* `--format`, `--column` and `-v`/`--verify` all only read.
|
|
318
|
+
*
|
|
319
|
+
* A creation flag with no name is still gated: git rejects the command, but a
|
|
320
|
+
* create attempt is not a read, and the tripwire errs toward refusing a
|
|
321
|
+
* release act.
|
|
322
|
+
*/
|
|
323
|
+
function gitTagIsWrite(segment: string): boolean {
|
|
324
|
+
const head = GIT_TAG_HEAD.exec(segment);
|
|
325
|
+
if (head === null) return false;
|
|
326
|
+
const tokens = segment.slice(head[0].length).trim().split(/\s+/).filter((token) => token.length > 0);
|
|
327
|
+
|
|
328
|
+
let deletion = false; // -d / --delete
|
|
329
|
+
let listRead = false; // a read-only list flag
|
|
330
|
+
let verifyRead = false; // -v / --verify
|
|
331
|
+
let createSignal = false; // -a / -s / -f / -m / -u / -t / -F
|
|
332
|
+
let hasOperand = false; // a positional tagname, pattern or commit
|
|
333
|
+
|
|
334
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
335
|
+
const token = tokens[i]!;
|
|
336
|
+
// A `--` ends option parsing: whatever follows is a plain operand, the
|
|
337
|
+
// created or deleted tag's name.
|
|
338
|
+
if (token === "--") {
|
|
339
|
+
hasOperand = true;
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
if (token.startsWith("--")) {
|
|
343
|
+
const flag = token.includes("=") ? token.slice(0, token.indexOf("=")) : token;
|
|
344
|
+
switch (flag) {
|
|
345
|
+
case "--delete":
|
|
346
|
+
deletion = true;
|
|
347
|
+
break;
|
|
348
|
+
case "--list":
|
|
349
|
+
case "--contains":
|
|
350
|
+
case "--no-contains":
|
|
351
|
+
case "--points-at":
|
|
352
|
+
case "--merged":
|
|
353
|
+
case "--no-merged":
|
|
354
|
+
case "--sort":
|
|
355
|
+
case "--format":
|
|
356
|
+
case "--column":
|
|
357
|
+
case "--no-column":
|
|
358
|
+
case "--color":
|
|
359
|
+
case "--no-color":
|
|
360
|
+
case "--ignore-case":
|
|
361
|
+
case "--no-ignore-case":
|
|
362
|
+
listRead = true;
|
|
363
|
+
break;
|
|
364
|
+
case "--verify":
|
|
365
|
+
verifyRead = true;
|
|
366
|
+
break;
|
|
367
|
+
case "--annotate":
|
|
368
|
+
case "--sign":
|
|
369
|
+
case "--force":
|
|
370
|
+
createSignal = true;
|
|
371
|
+
break;
|
|
372
|
+
case "--local-user":
|
|
373
|
+
case "--object":
|
|
374
|
+
case "--message":
|
|
375
|
+
case "--file":
|
|
376
|
+
// The flag's operand is part of the create parameters, not a
|
|
377
|
+
// tagname; `--flag value` must not be read as `--flag` plus a name.
|
|
378
|
+
createSignal = true;
|
|
379
|
+
if (!token.includes("=")) i++;
|
|
380
|
+
break;
|
|
381
|
+
default:
|
|
382
|
+
break; // an unknown flag: no conclusion either way
|
|
383
|
+
}
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (token.startsWith("-") && token.length > 1) {
|
|
387
|
+
// Combined short options (`-an5` is `-a -n 5`). An option with a
|
|
388
|
+
// required operand takes the rest of its token or the next token.
|
|
389
|
+
let consumeNext = false;
|
|
390
|
+
for (let j = 1; j < token.length; j++) {
|
|
391
|
+
const flag = token[j];
|
|
392
|
+
switch (flag) {
|
|
393
|
+
case "d":
|
|
394
|
+
deletion = true;
|
|
395
|
+
break;
|
|
396
|
+
case "a":
|
|
397
|
+
case "s":
|
|
398
|
+
case "f":
|
|
399
|
+
createSignal = true;
|
|
400
|
+
break;
|
|
401
|
+
case "l":
|
|
402
|
+
listRead = true;
|
|
403
|
+
break;
|
|
404
|
+
case "v":
|
|
405
|
+
verifyRead = true;
|
|
406
|
+
break;
|
|
407
|
+
case "n":
|
|
408
|
+
// `-n[<num>]` — the digit run belongs to the flag, so `-n5`
|
|
409
|
+
// is one token and never an operand.
|
|
410
|
+
listRead = true;
|
|
411
|
+
while (j + 1 < token.length && token[j + 1]! >= "0" && token[j + 1]! <= "9") j++;
|
|
412
|
+
break;
|
|
413
|
+
case "m":
|
|
414
|
+
case "u":
|
|
415
|
+
case "t":
|
|
416
|
+
case "F":
|
|
417
|
+
createSignal = true;
|
|
418
|
+
if (j + 1 < token.length) {
|
|
419
|
+
j = token.length;
|
|
420
|
+
} else {
|
|
421
|
+
consumeNext = true;
|
|
422
|
+
}
|
|
423
|
+
break;
|
|
424
|
+
default:
|
|
425
|
+
break; // an unknown short flag: no conclusion either way
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (consumeNext) i++;
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
// A plain operand: with a list flag it is a pattern or commit for the
|
|
432
|
+
// read; without one it is the tagname a creation needs.
|
|
433
|
+
hasOperand = true;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (deletion) return true;
|
|
437
|
+
if (listRead || verifyRead) return false;
|
|
438
|
+
if (hasOperand) return true;
|
|
439
|
+
return createSignal;
|
|
440
|
+
}
|
|
441
|
+
|
|
301
442
|
/**
|
|
302
443
|
* Classify a shell command and the segment that fired. The segment is what a
|
|
303
444
|
* triager reads: `bun test src/foo.test.ts && npm publish` matches on its
|
|
@@ -323,7 +464,7 @@ function releaseCommandMatch(command: string): { shape: GateShape; matched: stri
|
|
|
323
464
|
// any future spelling under the upgrade family all need the install shape,
|
|
324
465
|
// and a session without that grant is refused before the shell runs.
|
|
325
466
|
if (/^omp-conductor\s+upgrade(?:\b|$)/.test(segment)) return { shape: "install", matched: segment };
|
|
326
|
-
if (
|
|
467
|
+
if (gitTagIsWrite(segment)) return { shape: "git-tag", matched: segment };
|
|
327
468
|
if (
|
|
328
469
|
/^git(?:\s+-[Cc]\s+\S+)*\s+push\b/.test(segment) &&
|
|
329
470
|
GIT_PUSH_TAG_SHAPE.test(segment)
|
|
@@ -513,8 +654,10 @@ export function releaseShapeFromTool(
|
|
|
513
654
|
/**
|
|
514
655
|
* The refusal wording for the shared-host gate (#428). The whole-package
|
|
515
656
|
* `bun test` form names the focused alternative (a guard that only denies
|
|
516
|
-
* teaches nothing and gets worked around);
|
|
517
|
-
*
|
|
657
|
+
* teaches nothing and gets worked around); a refused shell suite names the
|
|
658
|
+
* script that fired and the sanctioned `bash -n` parse check (#687), because
|
|
659
|
+
* there is no focused form of a suite script — the alternative is to drop the
|
|
660
|
+
* execution segment and keep the allowed prefix, not to run something else.
|
|
518
661
|
*/
|
|
519
662
|
function sharedHostRefusalReason(matched: string | undefined): string {
|
|
520
663
|
if (matched !== undefined && /^bun\s+test\b/.test(matched)) {
|
|
@@ -524,10 +667,39 @@ function sharedHostRefusalReason(matched: string | undefined): string {
|
|
|
524
667
|
"Run a focused `bun test <file>.test.ts` instead."
|
|
525
668
|
);
|
|
526
669
|
}
|
|
670
|
+
if (matched !== undefined) {
|
|
671
|
+
return (
|
|
672
|
+
`Blocked by sharedHostPolicy: \`${matched}\` is a shared-host shell suite and not a worker's proof ` +
|
|
673
|
+
"path on this shared host (it overloads the 4-core VPS that also runs Langfuse and the fleet). " +
|
|
674
|
+
`\`bash -n ${matched}\` is the sanctioned local check — parsing never executes the script — and CI's ` +
|
|
675
|
+
"`herdr plugin + installer shell suites` job owns executing it. Drop the segment that fired and keep " +
|
|
676
|
+
"the `bash -n` parse gate."
|
|
677
|
+
);
|
|
678
|
+
}
|
|
527
679
|
return (
|
|
528
680
|
"Blocked by sharedHostPolicy: this shell suite is not a worker's proof path on this shared host " +
|
|
529
681
|
"(it overloads the 4-core VPS that also runs Langfuse and the fleet). " +
|
|
530
|
-
"
|
|
682
|
+
"`bash -n <path>` is the sanctioned local check; CI's `herdr plugin + installer shell suites` job owns " +
|
|
683
|
+
"executing it."
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* The worker-brief notice for the shared-host gate (#687): the guarded suite
|
|
689
|
+
* paths and the sanctioned local check, derived from
|
|
690
|
+
* {@link SHARED_HOST_SCRIPTS} so the rendered brief and the refusal can never
|
|
691
|
+
* disagree about what is guarded. One line and non-empty whenever the list is:
|
|
692
|
+
* the brief's rendered line count must equal its template's, which the
|
|
693
|
+
* buildBrief tests pin, so the notice must never carry an internal newline.
|
|
694
|
+
*/
|
|
695
|
+
export function sharedHostBriefNotice(): string {
|
|
696
|
+
if (SHARED_HOST_SCRIPTS.length === 0) return "";
|
|
697
|
+
const paths = SHARED_HOST_SCRIPTS.map((script) => `\`${script}\``).join(", ");
|
|
698
|
+
return (
|
|
699
|
+
`**Shared-host guard:** ${paths} are never executed on this shared host — \`bash -n\` on them is the ` +
|
|
700
|
+
"sanctioned local check (parsing never executes), and CI's `herdr plugin + installer shell suites` job " +
|
|
701
|
+
"owns executing them. A command that executes one is refused, and the refusal names the segment; drop " +
|
|
702
|
+
"that segment and keep the `bash -n` parse gate."
|
|
531
703
|
);
|
|
532
704
|
}
|
|
533
705
|
|
package/src/routing.ts
CHANGED
|
@@ -39,7 +39,10 @@ export type Unroutable = {
|
|
|
39
39
|
const MAX_BRANCH_LEN = 60;
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* True only when a human has queued the issue
|
|
42
|
+
* True only when a human has queued the issue, no run already owns it, and
|
|
43
|
+
* the operator has not parked it. The park label beats the queue label: an
|
|
44
|
+
* issue carrying both is not eligible, because the queue listing was fetched
|
|
45
|
+
* before the park landed and must not turn into a claim (#734).
|
|
43
46
|
*
|
|
44
47
|
* The state labels are the interlock against double-dispatch across daemon
|
|
45
48
|
* restarts: the tracker, not the local store, is the source of truth for
|
|
@@ -51,8 +54,13 @@ export function isEligible(issue: ReadyIssue, p: ProjectConfig): boolean {
|
|
|
51
54
|
// sides here, which is the only place labels are matched.
|
|
52
55
|
const labels = new Set(issue.labels);
|
|
53
56
|
if (!labels.has(p.queueLabel)) return false;
|
|
54
|
-
const { inProgress, blocked, failed } = p.stateLabels;
|
|
55
|
-
return
|
|
57
|
+
const { inProgress, blocked, failed, backlog } = p.stateLabels;
|
|
58
|
+
return (
|
|
59
|
+
!labels.has(inProgress) &&
|
|
60
|
+
!labels.has(blocked) &&
|
|
61
|
+
!labels.has(failed) &&
|
|
62
|
+
!labels.has(backlog)
|
|
63
|
+
);
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
/**
|
package/src/session-host.ts
CHANGED
|
@@ -55,6 +55,19 @@ export interface SessionHostSpec {
|
|
|
55
55
|
* Carried as a plain scalar like the other fields; absent, nothing is staged
|
|
56
56
|
* on the far side either. */
|
|
57
57
|
ompSettingsFile?: string;
|
|
58
|
+
/**
|
|
59
|
+
* The structured-settlement contract (#540): a JSON Schema the session's
|
|
60
|
+
* `yield` tool validates its `data` payload against. Carried as plain JSON
|
|
61
|
+
* through the spec, like everything else; absent, the far side passes nothing
|
|
62
|
+
* to the harness.
|
|
63
|
+
*/
|
|
64
|
+
outputSchema?: unknown;
|
|
65
|
+
/** Enforcement policy for {@link outputSchema} — the worker contract is
|
|
66
|
+
* `"permissive"`: a schema violation settles with whatever the worker
|
|
67
|
+
* produced, never a lost report. */
|
|
68
|
+
outputSchemaMode?: "permissive" | "strict";
|
|
69
|
+
/** Force the hidden `yield` tool into this session's toolset (#540). */
|
|
70
|
+
requireYieldTool?: boolean;
|
|
58
71
|
}
|
|
59
72
|
|
|
60
73
|
/** Parent → child. */
|
|
@@ -192,6 +205,9 @@ export async function runSessionHost(
|
|
|
192
205
|
...(spec.verbSocketPath === undefined ? {} : { verbSocketPath: spec.verbSocketPath }),
|
|
193
206
|
...(spec.readOnly === undefined ? {} : { readOnly: spec.readOnly }),
|
|
194
207
|
...(spec.ompSettingsFile === undefined ? {} : { ompSettingsFile: spec.ompSettingsFile }),
|
|
208
|
+
...(spec.outputSchema === undefined ? {} : { outputSchema: spec.outputSchema }),
|
|
209
|
+
...(spec.outputSchemaMode === undefined ? {} : { outputSchemaMode: spec.outputSchemaMode }),
|
|
210
|
+
...(spec.requireYieldTool === undefined ? {} : { requireYieldTool: spec.requireYieldTool }),
|
|
195
211
|
// The release audit lives in the daemon's state directory, which this
|
|
196
212
|
// process may not be able to write and must not be trusted to. It
|
|
197
213
|
// becomes a message; the parent performs the durable write.
|