omp-conductor 0.3.25 → 0.4.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 +733 -30
- package/package.json +1 -1
- package/src/board.ts +24 -5
- package/src/briefs/orchestrator.md +106 -20
- package/src/briefs/policy.md +48 -27
- package/src/briefs/worker.md +82 -31
- package/src/cli.ts +155 -2
- package/src/config.ts +669 -16
- package/src/confinement.ts +506 -25
- package/src/credentials.ts +2029 -0
- package/src/daemon.ts +1000 -32
- package/src/diff-flags.ts +696 -0
- package/src/escalate.ts +136 -9
- package/src/fleet.ts +71 -3
- package/src/omp.ts +471 -15
- package/src/orchestrator-tick.ts +42 -19
- package/src/orchestrator.ts +32 -5
- package/src/plugin.ts +267 -15
- package/src/release-policy.ts +191 -29
- package/src/reports.ts +440 -0
- package/src/session-host.ts +307 -0
- package/src/setup-host.ts +19 -1
- package/src/setup.ts +207 -18
- package/src/store.ts +506 -3
- package/src/tracker/github.ts +45 -0
- package/src/types.ts +847 -10
- package/src/usage.ts +726 -0
- package/src/verbs/actions.ts +142 -0
- package/src/verbs/client.ts +207 -0
- package/src/verbs/ledger.ts +77 -0
- package/src/verbs/protocol.ts +465 -0
- package/src/verbs/server.ts +1098 -0
- package/src/verbs/socket.ts +446 -0
- package/src/worker.ts +36 -7
- package/src/worktree.ts +202 -109
- package/systemd/omp-conductor.service.example +96 -8
package/src/cli.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* cannot drift apart.
|
|
7
7
|
*/
|
|
8
8
|
import { closeSync, openSync, readFileSync, readSync, statSync } from "node:fs";
|
|
9
|
+
import { userInfo } from "node:os";
|
|
9
10
|
import { dirname, join } from "node:path";
|
|
10
11
|
import { runBoard } from "./board.ts";
|
|
11
12
|
import {
|
|
@@ -21,7 +22,8 @@ import {
|
|
|
21
22
|
repairPolicyBannerCrumbs,
|
|
22
23
|
writeMergedBrief,
|
|
23
24
|
} from "./brief-upgrade.ts";
|
|
24
|
-
import { findProject, loadConfig, resolveCaps, stateDir } from "./config.ts";
|
|
25
|
+
import { findProject, loadConfig, resolveCaps, sharedRoot, stateDir } from "./config.ts";
|
|
26
|
+
import { boundarySetupScript } from "./credentials.ts";
|
|
25
27
|
import { runDaemon, setPaused } from "./daemon.ts";
|
|
26
28
|
import {
|
|
27
29
|
armTicks,
|
|
@@ -45,6 +47,7 @@ import {
|
|
|
45
47
|
writeRecord,
|
|
46
48
|
} from "./lifecycle.ts";
|
|
47
49
|
import { STALL_MARKER_FILE } from "./orchestrator-tick.ts";
|
|
50
|
+
import { digestDedupeKey } from "./reports.ts";
|
|
48
51
|
import {
|
|
49
52
|
briefPathForProject,
|
|
50
53
|
policyPathForProject,
|
|
@@ -54,8 +57,10 @@ import {
|
|
|
54
57
|
} from "./setup.ts";
|
|
55
58
|
import { dbPath, LIVE_STATES, openStore } from "./store.ts";
|
|
56
59
|
import { formatTranscriptLine } from "./transcript.ts";
|
|
60
|
+
import { formatVerbLedgerEntry } from "./verbs/ledger.ts";
|
|
57
61
|
import { makeTracker } from "./tracker/github.ts";
|
|
58
|
-
import
|
|
62
|
+
import { REPORT_KINDS } from "./types.ts";
|
|
63
|
+
import type { ProjectConfig, ReportKind } from "./types.ts";
|
|
59
64
|
import { formatUnblock, unblockIssue } from "./unblock.ts";
|
|
60
65
|
import { upgradeConductor } from "./upgrade.ts";
|
|
61
66
|
|
|
@@ -87,6 +92,7 @@ usage:
|
|
|
87
92
|
omp-conductor upgrade [--to VERSION] [--project NAME]
|
|
88
93
|
omp-conductor board [--project NAME]
|
|
89
94
|
omp-conductor status [--project NAME]
|
|
95
|
+
omp-conductor ledger [--issue N] [--limit N] [--project NAME]
|
|
90
96
|
omp-conductor hold [--project NAME]
|
|
91
97
|
omp-conductor halt [--pane] [--project NAME]
|
|
92
98
|
omp-conductor arm [--project NAME]
|
|
@@ -101,6 +107,7 @@ usage:
|
|
|
101
107
|
omp-conductor graph-setup [--project NAME] [--write]
|
|
102
108
|
omp-conductor brief-upgrade [--migrate|--retrofit] [--apply] [--file PATH] [--project NAME]
|
|
103
109
|
omp-conductor friction <escalation-digest|report-noise|report-surprise> --detail TEXT [--issue N] [--project NAME]
|
|
110
|
+
omp-conductor report --text TEXT [--kind material|digest] [--project NAME]
|
|
104
111
|
omp-conductor help
|
|
105
112
|
|
|
106
113
|
upgrade update the Bun-global CLI, omp plugin, Herdr recovery plugin, and
|
|
@@ -124,6 +131,11 @@ usage:
|
|
|
124
131
|
board open the live keyboard-driven fleet board. It renders queue holds,
|
|
125
132
|
every run lifecycle stage, recent merges, spend and health; Enter
|
|
126
133
|
follows a selected transcript without leaving the board.
|
|
134
|
+
ledger every conductor-verb call and how the daemon decided it: the verb,
|
|
135
|
+
the arguments, allow or refuse, the named refusal reason, and the
|
|
136
|
+
resulting sha. Sessions cannot push, open, merge, label or release
|
|
137
|
+
except through those verbs, so this is the record of what they tried
|
|
138
|
+
as well as what they did. --issue narrows it to one issue's run.
|
|
127
139
|
hold soft stop: pause claiming AND disarm ticks. Daemon and pane stay up.
|
|
128
140
|
This is "stop the conductor overnight" without killing processes.
|
|
129
141
|
halt hold, then stop the dispatch daemon (systemctl-aware). Pane stays up
|
|
@@ -153,6 +165,12 @@ usage:
|
|
|
153
165
|
Refuses when the newest attempt's work could not be committed and
|
|
154
166
|
its worktree is the only copy: re-claiming removes that tree. Use
|
|
155
167
|
--force once you have recovered it or accepted the loss.
|
|
168
|
+
report hand a rendered report to the daemon's durable outbox. The report is
|
|
169
|
+
persisted before anything is sent, delivered by the daemon with
|
|
170
|
+
bounded retries, and shown by status until it lands. Delivery is
|
|
171
|
+
at-least-once: a crash mid-send is retried and the retry says it may
|
|
172
|
+
be a repeat. --kind digest is accepted at most once per local day,
|
|
173
|
+
decided from the ledger rather than from what you remember sending.
|
|
156
174
|
friction record a bounded observation the daemon cannot classify itself:
|
|
157
175
|
an escalation that belonged in a digest, or a tick report that was
|
|
158
176
|
noise/surprising. Repeated observations feed the existing Learning
|
|
@@ -511,6 +529,91 @@ try {
|
|
|
511
529
|
break;
|
|
512
530
|
}
|
|
513
531
|
|
|
532
|
+
case "boundary-setup": {
|
|
533
|
+
// Printed, never executed: it creates system accounts, so it is the
|
|
534
|
+
// operator's `sudo` and their chance to read it first — same posture as
|
|
535
|
+
// `graph-setup`. Generated from the same constants the probe checks, so
|
|
536
|
+
// the instructions cannot drift from what the daemon then demands (#125).
|
|
537
|
+
//
|
|
538
|
+
// Nothing is read from the config unless a flag was omitted, and that is
|
|
539
|
+
// the point rather than an optimisation: per-run provisioning has to
|
|
540
|
+
// happen BEFORE `setup`, because setup writes worktree and mirror paths
|
|
541
|
+
// into the privileged shared root this command creates. Loading the
|
|
542
|
+
// config unconditionally would be a bootstrap cycle — provision needs a
|
|
543
|
+
// config, the config needs the provisioned root.
|
|
544
|
+
const slotsFlag = flag(argv, "slots");
|
|
545
|
+
let slots: number;
|
|
546
|
+
if (slotsFlag === undefined) {
|
|
547
|
+
const cfg = loadConfig();
|
|
548
|
+
slots = resolveCaps(findProject(cfg, flag(argv, "project")), cfg.defaults).maxConcurrentWorkers;
|
|
549
|
+
} else if (/^\d+$/.test(slotsFlag) && Number.parseInt(slotsFlag, 10) >= 1) {
|
|
550
|
+
// Whole string, not a prefix: `parseInt("2workers")` is 2, and silently
|
|
551
|
+
// provisioning a different pool than the operator typed is worse than
|
|
552
|
+
// refusing.
|
|
553
|
+
slots = Number.parseInt(slotsFlag, 10);
|
|
554
|
+
} else {
|
|
555
|
+
console.error(`boundary-setup: --slots must be a positive integer, got ${JSON.stringify(slotsFlag)}`);
|
|
556
|
+
process.exitCode = 2;
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
// Both explicit where given, because neither can be inferred safely here.
|
|
560
|
+
// Under `sudo` the invoking user is root, so `userInfo()` would provision
|
|
561
|
+
// the wrong account and hand the daemon group to root; and the shared
|
|
562
|
+
// root must be the *same* string the daemon later resolves, not one
|
|
563
|
+
// derived from however this process happened to be invoked.
|
|
564
|
+
const daemonUser = flag(argv, "daemon-user") ?? process.env["SUDO_USER"] ?? userInfo().username;
|
|
565
|
+
const root = flag(argv, "shared-root") ?? sharedRoot();
|
|
566
|
+
process.stdout.write(boundarySetupScript({ slots, sharedRoot: root, daemonUser }));
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* The action ledger (#126): every conductor-verb call, what it asked for,
|
|
571
|
+
* and how the daemon decided it.
|
|
572
|
+
*
|
|
573
|
+
* Its own command as well as a block in `status`, because the two questions
|
|
574
|
+
* are different sizes. `status` answers "is anything being refused right
|
|
575
|
+
* now"; this answers "what did run 3 actually try to do", which is the
|
|
576
|
+
* question an escalation about a run asks, and it needs the whole record
|
|
577
|
+
* rather than the newest five lines of every run at once.
|
|
578
|
+
*/
|
|
579
|
+
case "ledger": {
|
|
580
|
+
const cfg = loadConfig();
|
|
581
|
+
const p = findProject(cfg, flag(argv, "project"));
|
|
582
|
+
const issueFlag = flag(argv, "issue");
|
|
583
|
+
const issue = issueFlag === undefined ? undefined : issueArg("ledger", issueFlag);
|
|
584
|
+
const limitFlag = flag(argv, "limit");
|
|
585
|
+
const limit = limitFlag === undefined ? 50 : Number.parseInt(limitFlag, 10);
|
|
586
|
+
if (!Number.isSafeInteger(limit) || limit < 1) {
|
|
587
|
+
process.stderr.write(`omp-conductor: ledger --limit needs a positive integer, got "${limitFlag ?? ""}"\n`);
|
|
588
|
+
process.exitCode = 1;
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
// Read-only, WAL, same as `tail`: this never contends with the daemon
|
|
592
|
+
// writing the rows it is printing.
|
|
593
|
+
const store = openStore(dbPath());
|
|
594
|
+
try {
|
|
595
|
+
const entries = store.verbLedger(p.name, {
|
|
596
|
+
...(issue === undefined ? {} : { issue }),
|
|
597
|
+
limit,
|
|
598
|
+
});
|
|
599
|
+
if (entries.length === 0) {
|
|
600
|
+
process.stdout.write(
|
|
601
|
+
`no conductor-verb calls recorded for ${p.name}` +
|
|
602
|
+
`${issue === undefined ? "" : ` on #${String(issue)}`}\n`,
|
|
603
|
+
);
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
const refused = entries.filter((e) => e.decision === "refused").length;
|
|
607
|
+
process.stdout.write(
|
|
608
|
+
`${p.name} — ${entries.length} verb call(s), ${refused} refused (newest first)\n` +
|
|
609
|
+
`${entries.flatMap(formatVerbLedgerEntry).join("\n")}\n`,
|
|
610
|
+
);
|
|
611
|
+
} finally {
|
|
612
|
+
store.close();
|
|
613
|
+
}
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
|
|
514
617
|
case "board":
|
|
515
618
|
await runBoard(flag(argv, "project"));
|
|
516
619
|
break;
|
|
@@ -651,6 +754,56 @@ try {
|
|
|
651
754
|
break;
|
|
652
755
|
}
|
|
653
756
|
|
|
757
|
+
/**
|
|
758
|
+
* The handover point. Authorship stays with the model; from here the daemon
|
|
759
|
+
* owns delivery, so "I sent the report" stops being a claim the model makes
|
|
760
|
+
* about a tool call it may never have run and becomes a row anyone can read
|
|
761
|
+
* back out of the store (#123).
|
|
762
|
+
*/
|
|
763
|
+
case "report": {
|
|
764
|
+
const rawText = flag(argv, "text");
|
|
765
|
+
const body = rawText?.trim();
|
|
766
|
+
if (body === undefined || body.length === 0 || rawText?.startsWith("--") === true) {
|
|
767
|
+
process.stderr.write("omp-conductor: report needs --text with the rendered report\n");
|
|
768
|
+
process.exit(2);
|
|
769
|
+
}
|
|
770
|
+
const rawKind = flag(argv, "kind") ?? "material";
|
|
771
|
+
// Fail closed on an unknown kind rather than defaulting to `material`: a
|
|
772
|
+
// typo'd `--kind diggest` that silently became a material report would
|
|
773
|
+
// bypass the daily-digest guard, which is the one thing the kind is for.
|
|
774
|
+
if (!(REPORT_KINDS as readonly string[]).includes(rawKind)) {
|
|
775
|
+
process.stderr.write(
|
|
776
|
+
`omp-conductor: report --kind must be one of: ${REPORT_KINDS.join(", ")}\n`,
|
|
777
|
+
);
|
|
778
|
+
process.exit(2);
|
|
779
|
+
}
|
|
780
|
+
const kind = rawKind as ReportKind;
|
|
781
|
+
const project = findProject(loadConfig(), flag(argv, "project"));
|
|
782
|
+
const store = openStore(dbPath());
|
|
783
|
+
try {
|
|
784
|
+
const at = Date.now();
|
|
785
|
+
const { report, deduped } = store.enqueueReport({
|
|
786
|
+
project: project.name,
|
|
787
|
+
kind,
|
|
788
|
+
body,
|
|
789
|
+
// Only the digest is at-most-once. A material report describes one
|
|
790
|
+
// event as it happens, and two of those in a day are two events.
|
|
791
|
+
...(kind === "digest" ? { dedupeKey: digestDedupeKey(at) } : {}),
|
|
792
|
+
at,
|
|
793
|
+
});
|
|
794
|
+
process.stdout.write(
|
|
795
|
+
deduped
|
|
796
|
+
? `today's digest was already handed over as report ${report.id} (${report.state}) — nothing queued\n` +
|
|
797
|
+
"the ledger decides this, not your memory of the last tick; use --kind material for a second event\n"
|
|
798
|
+
: `report ${report.id} queued for ${project.name} (${kind})\n` +
|
|
799
|
+
"the daemon owns delivery from here; omp-conductor status shows it until it lands\n",
|
|
800
|
+
);
|
|
801
|
+
} finally {
|
|
802
|
+
store.close();
|
|
803
|
+
}
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
806
|
+
|
|
654
807
|
case "friction": {
|
|
655
808
|
const name = argv[1] as FrictionFeedbackName | undefined;
|
|
656
809
|
if (name === undefined || !Object.hasOwn(FRICTION_FEEDBACK_KINDS, name)) {
|