omp-conductor 0.14.0 → 0.15.1
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 +336 -169
- package/package.json +8 -5
- package/schema/config.schema.json +609 -0
- package/src/board.ts +19 -32
- package/src/brief-upgrade.ts +1 -1
- package/src/briefs/orchestrator.md +9 -5
- package/src/briefs/policy.md +4 -4
- package/src/briefs/probes/gates.md +51 -0
- package/src/briefs/probes/project-context.md +59 -0
- package/src/briefs/probes/release-procedure.md +81 -0
- package/src/cli.ts +235 -199
- package/src/config-schema.ts +352 -0
- package/src/config.ts +1046 -796
- package/src/confinement.ts +54 -0
- package/src/daemon.ts +442 -374
- package/src/escalate.ts +43 -3
- package/src/fleet.ts +317 -43
- package/src/generate-schema.ts +21 -0
- package/src/graph.ts +3 -3
- package/src/host.ts +16 -0
- package/src/omp.ts +21 -1
- package/src/orchestrator-tick.ts +298 -39
- package/src/privileged.ts +264 -0
- package/src/reports.ts +1 -1
- package/src/session-host.ts +3 -0
- package/src/setup-host.ts +225 -25
- package/src/setup-install.ts +320 -0
- package/src/setup-probe.ts +412 -0
- package/src/{plugin.ts → setup-wizard.ts} +790 -465
- package/src/setup.ts +264 -20
- package/src/types.ts +2 -2
- package/src/upgrade.ts +44 -10
- package/src/verbs/server.ts +32 -9
- package/src/wizard-ui.ts +249 -0
- package/src/worker.ts +6 -1
- package/skills/conductor-onboarding/SKILL.md +0 -748
- package/skills/conductor-update/SKILL.md +0 -51
package/src/cli.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* Standalone entry point
|
|
4
|
-
* the loop, the caps and the state
|
|
5
|
-
*
|
|
6
|
-
* cannot drift apart.
|
|
3
|
+
* Standalone entry point — the only operator surface after #309. Everything here
|
|
4
|
+
* is argument handling and printing: the loop, the caps and the state live in
|
|
5
|
+
* ./daemon.ts and the background process lifecycle in ./lifecycle.ts.
|
|
7
6
|
*/
|
|
8
7
|
import { randomUUID } from "node:crypto";
|
|
9
8
|
import { closeSync, openSync, readFileSync, readSync, statSync } from "node:fs";
|
|
@@ -28,14 +27,15 @@ import { CONDITION_FORMS, parseCondition } from "./decisions.ts";
|
|
|
28
27
|
import { isPaused, pausedAt, runDaemon, setPaused } from "./daemon.ts";
|
|
29
28
|
import {
|
|
30
29
|
armTicks,
|
|
31
|
-
|
|
30
|
+
clearPaneHaltIfResolvable,
|
|
32
31
|
disarmTicks,
|
|
33
|
-
halt,
|
|
34
|
-
haltWithPane,
|
|
35
32
|
hold,
|
|
33
|
+
pinPaneHalt,
|
|
36
34
|
releaseHold,
|
|
37
35
|
renderStatus,
|
|
38
36
|
startHerdrFleet,
|
|
37
|
+
stopConductorPane,
|
|
38
|
+
telegramStateDir,
|
|
39
39
|
} from "./fleet.ts";
|
|
40
40
|
import { formatGraphSetup, graphRepos, writeGraphSetup, type GraphSetupWrite } from "./graph.ts";
|
|
41
41
|
import { readBaseChain } from "./gitops.ts";
|
|
@@ -53,12 +53,17 @@ import { STALL_MARKER_FILE } from "./orchestrator-tick.ts";
|
|
|
53
53
|
import { digestDedupeKey } from "./reports.ts";
|
|
54
54
|
import { digestDue } from "./digest-schedule.ts";
|
|
55
55
|
import {
|
|
56
|
+
AMEND_AREA_IDS,
|
|
56
57
|
briefPathForProject,
|
|
57
58
|
policyPathForProject,
|
|
58
59
|
renderBriefForProject,
|
|
59
60
|
renderFloorForProject,
|
|
60
61
|
shippedBriefTemplate,
|
|
62
|
+
type AmendAreaId,
|
|
61
63
|
} from "./setup.ts";
|
|
64
|
+
import { runGraphInstall, runHostInstall } from "./setup-install.ts";
|
|
65
|
+
import { DEFAULT_PROBES, NO_PROBES, setup } from "./setup-wizard.ts";
|
|
66
|
+
import { terminalUi } from "./wizard-ui.ts";
|
|
62
67
|
import { dbPath, LIVE_STATES, openStore } from "./store.ts";
|
|
63
68
|
import { formatTranscriptLine } from "./transcript.ts";
|
|
64
69
|
import { formatVerbLedgerEntry } from "./verbs/ledger.ts";
|
|
@@ -91,19 +96,18 @@ function packageVersion(): string {
|
|
|
91
96
|
const USAGE = `omp-conductor — dispatch ready issues to omp coding sessions
|
|
92
97
|
|
|
93
98
|
usage:
|
|
99
|
+
omp-conductor setup [area] [--no-ai] [--project NAME]
|
|
94
100
|
omp-conductor start [--port N] [--project NAME]
|
|
95
101
|
omp-conductor --version
|
|
96
|
-
omp-conductor stop
|
|
102
|
+
omp-conductor stop [--pane] [--project NAME | --all]
|
|
97
103
|
omp-conductor restart [--now] [--timeout SECONDS] [--port N] [--project NAME]
|
|
98
104
|
omp-conductor upgrade [--to VERSION] [--project NAME]
|
|
99
105
|
omp-conductor board [--project NAME] [--json]
|
|
100
106
|
omp-conductor status [--project NAME]
|
|
101
107
|
omp-conductor ledger [--issue N] [--limit N] [--project NAME]
|
|
102
|
-
omp-conductor hold [--project NAME]
|
|
103
|
-
omp-conductor
|
|
104
|
-
omp-conductor
|
|
105
|
-
omp-conductor disarm [--project NAME]
|
|
106
|
-
omp-conductor release-pane [--project NAME]
|
|
108
|
+
omp-conductor hold [--project NAME | --all]
|
|
109
|
+
omp-conductor arm [--project NAME | --all]
|
|
110
|
+
omp-conductor disarm [--project NAME | --all]
|
|
107
111
|
omp-conductor tail <issue> [--project NAME]
|
|
108
112
|
omp-conductor extend <issue> --turns N [--project NAME]
|
|
109
113
|
omp-conductor worker pause <issue> [--project NAME]
|
|
@@ -112,9 +116,7 @@ usage:
|
|
|
112
116
|
omp-conductor unblock <issue> [--force] [--no-requeue] [--project NAME]
|
|
113
117
|
omp-conductor verb <conductor_*> [--project NAME] [--arg k=v ...]
|
|
114
118
|
omp-conductor daemon [--once] [--port N] [--project NAME]
|
|
115
|
-
omp-conductor
|
|
116
|
-
omp-conductor resume
|
|
117
|
-
omp-conductor graph-setup [--project NAME] [--write]
|
|
119
|
+
omp-conductor resume [--project NAME | --all]
|
|
118
120
|
omp-conductor brief-upgrade [--migrate|--retrofit] [--apply] [--file PATH] [--project NAME]
|
|
119
121
|
omp-conductor friction <escalation-digest|report-noise|report-surprise> --detail TEXT [--issue N] [--project NAME]
|
|
120
122
|
omp-conductor event record --category NAME --summary TEXT --evidence REF [--occurred-at ISO] [--project NAME]
|
|
@@ -125,6 +127,11 @@ usage:
|
|
|
125
127
|
omp-conductor decision list [--project NAME]
|
|
126
128
|
omp-conductor help
|
|
127
129
|
|
|
130
|
+
setup interview, then write config.json, the labels, the briefs and the
|
|
131
|
+
staged host files behind one confirm. Bare is a full run — or, when
|
|
132
|
+
the project already exists, a chooser of which area to amend. Naming
|
|
133
|
+
an area positionally skips that chooser and amends only that area:
|
|
134
|
+
${AMEND_AREA_IDS.join(", ")}.
|
|
128
135
|
upgrade update the Bun-global CLI, omp plugin, Herdr recovery plugin, and
|
|
129
136
|
brief as one pinned release. Pauses only new claims, drains live
|
|
130
137
|
workers, reloads, verifies twice, and restores the prior dispatch
|
|
@@ -132,9 +139,14 @@ usage:
|
|
|
132
139
|
start start the installed herdr-fleet.service when present, then run the
|
|
133
140
|
dispatch loop in the background and wait until it answers GET
|
|
134
141
|
/healthz on :8787 (override with --port). Refuses if one is running.
|
|
135
|
-
stop stop the
|
|
136
|
-
|
|
137
|
-
|
|
142
|
+
stop stop the conductor: pause claiming, disarm ticks, then stop the
|
|
143
|
+
dispatch daemon (systemctl-aware, so Restart=on-failure cannot bring
|
|
144
|
+
it back). Pane stays up unless --pane is passed. To bounce the daemon
|
|
145
|
+
without stopping the fleet, use restart.
|
|
146
|
+
stop --pane
|
|
147
|
+
also stop the conductor agent's pane and pin herdr-conductor recovery
|
|
148
|
+
off for that agent only — it does NOT stop herdr-fleet.service or any
|
|
149
|
+
other herdr session. resume clears the pin.
|
|
138
150
|
restart drain the fleet by default: pause new claims, wait until live workers
|
|
139
151
|
reach 0/N (bounded by --timeout SECONDS, default 1800), restart keeping
|
|
140
152
|
the running daemon's port and project unless a flag overrides them,
|
|
@@ -159,18 +171,12 @@ usage:
|
|
|
159
171
|
as well as what they did. --issue narrows it to one issue's run.
|
|
160
172
|
hold soft stop: pause claiming AND disarm ticks. Daemon and pane stay up.
|
|
161
173
|
This is "stop the conductor overnight" without killing processes.
|
|
162
|
-
|
|
163
|
-
unless --pane is passed.
|
|
164
|
-
halt --pane
|
|
165
|
-
halt, then pin herdr-conductor recovery off for the conductor agent
|
|
166
|
-
only — does NOT stop herdr-fleet.service or any other herdr session.
|
|
167
|
-
Clear the pin with release-pane when you want recovery again.
|
|
174
|
+
Use --all to target every configured project.
|
|
168
175
|
arm proof-gated: send a Telegram challenge and write the arm marker only
|
|
169
176
|
after your reply appears as a user turn in the orchestrator transcript.
|
|
170
|
-
Never auto-armed by resume/hold.
|
|
171
|
-
disarm remove the arm marker so ticks skip. Processes untouched.
|
|
172
|
-
|
|
173
|
-
clear the halt --pane recovery pin so herdr-conductor may resume again.
|
|
177
|
+
Never auto-armed by resume/hold. Use --all for every project.
|
|
178
|
+
disarm remove the arm marker so ticks skip. Processes untouched. Use --all
|
|
179
|
+
to target every configured project.
|
|
174
180
|
tail follow the newest run for <issue>: the worker's assistant text and
|
|
175
181
|
the tools it calls, printed as they land. Workers are sessions inside
|
|
176
182
|
the daemon rather than terminals, so this is the only way to watch
|
|
@@ -228,16 +234,16 @@ usage:
|
|
|
228
234
|
loop; recording one never edits policy by itself.
|
|
229
235
|
daemon run the dispatch loop in the foreground; --once runs a single tick
|
|
230
236
|
and exits. This is what \`start\` launches.
|
|
231
|
-
pause
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
Exits 1 when no repo
|
|
237
|
+
resume clear pause and any pane-recovery pin. Does NOT re-arm: run arm after
|
|
238
|
+
an inbound Telegram proof to bring ticks back. Use --all for every project.
|
|
239
|
+
setup host
|
|
240
|
+
re-stage the systemd unit and run the install behind one confirm.
|
|
241
|
+
setup graph
|
|
242
|
+
set up the code-graph indexes workers query instead of grepping, end
|
|
243
|
+
to end: check prerequisites, clone any missing index-only clone as
|
|
244
|
+
you, install and enable the reindex timer as root, then seed one
|
|
245
|
+
indexing run and verify it. --no-seed enables without seeding;
|
|
246
|
+
--print changes nothing. Exits 1 when no repo has graphProject.
|
|
241
247
|
brief-upgrade
|
|
242
248
|
inspect the brief overlay (package floor + POLICY.md). Reports by
|
|
243
249
|
default. --migrate lifts a bannered ORCHESTRATOR.md owned half into
|
|
@@ -251,19 +257,20 @@ usage:
|
|
|
251
257
|
--version
|
|
252
258
|
print the installed omp-conductor package version (also -V, version).
|
|
253
259
|
|
|
254
|
-
Pause is a flag file under the state directory, so it applies to
|
|
255
|
-
|
|
256
|
-
reads, so both brains go quiet
|
|
257
|
-
tracked by a pidfile under
|
|
260
|
+
Pause is a flag file under the state directory, so it applies to the selected
|
|
261
|
+
project (or every configured project with --all) and survives a daemon restart.
|
|
262
|
+
Hold also removes the arm marker the heartbeat reads, so both brains go quiet
|
|
263
|
+
without killing processes. A running daemon is tracked by a pidfile under
|
|
264
|
+
$OMP_CONDUCTOR_RUNTIME_DIR (default
|
|
258
265
|
~/.omp/run/daemons/omp-conductor), written whether it was started in the
|
|
259
266
|
background or in the foreground, and probed for liveness on every read — a
|
|
260
267
|
stale one never blocks a start.
|
|
261
268
|
|
|
262
269
|
Stop the conductor:
|
|
263
270
|
hold no claims, no tick sends (inspectable)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
resume && arm clear pause, then prove inbound Telegram
|
|
271
|
+
stop hold + stop dispatch daemon
|
|
272
|
+
stop --pane stop + pin conductor-pane recovery off
|
|
273
|
+
resume && arm clear pause and any pane pin, then prove inbound Telegram`;
|
|
267
274
|
|
|
268
275
|
/** Accepts both `--port 9000` and `--port=9000`; returns undefined when absent. */
|
|
269
276
|
function flag(argv: string[], name: string): string | undefined {
|
|
@@ -474,6 +481,24 @@ async function tailRun(project: string, issue: number): Promise<void> {
|
|
|
474
481
|
|
|
475
482
|
const argv = process.argv.slice(2);
|
|
476
483
|
const cmd = argv[0];
|
|
484
|
+
/**
|
|
485
|
+
* Parsed once, because nearly every verb takes it and a per-case
|
|
486
|
+
* `projectFlag` is a line each of them can forget. Named `projectFlag`
|
|
487
|
+
* rather than `project` because many cases bind their own resolved
|
|
488
|
+
* `ProjectConfig` under that name.
|
|
489
|
+
*/
|
|
490
|
+
const projectFlag = flag(argv, "project");
|
|
491
|
+
|
|
492
|
+
function targetProjects(): ProjectConfig[] {
|
|
493
|
+
const cfg = loadConfig();
|
|
494
|
+
if (argv.includes("--all")) return cfg.projects;
|
|
495
|
+
if (projectFlag === undefined && cfg.projects.length > 1) {
|
|
496
|
+
throw new Error(
|
|
497
|
+
`config has ${cfg.projects.length} projects; use --project NAME or --all`,
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
return [findProject(cfg, projectFlag)];
|
|
501
|
+
}
|
|
477
502
|
|
|
478
503
|
try {
|
|
479
504
|
switch (cmd) {
|
|
@@ -482,10 +507,57 @@ try {
|
|
|
482
507
|
case "version":
|
|
483
508
|
process.stdout.write(`${packageVersion()}\n`);
|
|
484
509
|
break;
|
|
510
|
+
case "setup": {
|
|
511
|
+
const sub = argv[1];
|
|
512
|
+
const positional = sub !== undefined && !sub.startsWith("--") ? sub : undefined;
|
|
513
|
+
// The terminal surface owns stdin for whichever path runs, and is released
|
|
514
|
+
// in a finally: a throw with readline still attached leaves the operator's
|
|
515
|
+
// shell without an echo.
|
|
516
|
+
const ui = terminalUi();
|
|
517
|
+
try {
|
|
518
|
+
// `host` and `graph` are install subcommands, checked BEFORE the amend
|
|
519
|
+
// areas. They are not areas — routing them through the area validation
|
|
520
|
+
// is how `setup host` came to exit 2 as an "unknown setup area".
|
|
521
|
+
if (positional === "host" || positional === "graph") {
|
|
522
|
+
const cfg = loadConfig();
|
|
523
|
+
const project = findProject(cfg, projectFlag);
|
|
524
|
+
const outcome =
|
|
525
|
+
positional === "host"
|
|
526
|
+
? await runHostInstall(project, resolveCaps(project, cfg.defaults), telegramStateDir(), ui)
|
|
527
|
+
: await runGraphInstall(project, ui, {
|
|
528
|
+
noSeed: argv.includes("--no-seed"),
|
|
529
|
+
print: argv.includes("--print"),
|
|
530
|
+
});
|
|
531
|
+
// `staged` is a success on a host that has no systemd: the files are
|
|
532
|
+
// real, only the enable step is impossible.
|
|
533
|
+
if (outcome.kind === "refused" || outcome.kind === "failed") process.exit(1);
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
let area: AmendAreaId | undefined;
|
|
537
|
+
if (positional !== undefined) {
|
|
538
|
+
if (!(AMEND_AREA_IDS as readonly string[]).includes(positional)) {
|
|
539
|
+
process.stderr.write(
|
|
540
|
+
`omp-conductor: unknown setup argument "${positional}" — ` +
|
|
541
|
+
`install subcommands are host, graph; amend areas are ${AMEND_AREA_IDS.join(", ")}\n`,
|
|
542
|
+
);
|
|
543
|
+
process.exit(2);
|
|
544
|
+
}
|
|
545
|
+
area = positional as AmendAreaId;
|
|
546
|
+
}
|
|
547
|
+
// `--no-ai` is the interview with its reading half removed: every question
|
|
548
|
+
// is still asked, nothing is proposed. For a host with no omp peer, a
|
|
549
|
+
// private repo no probe can clone, or an operator who would rather type the
|
|
550
|
+
// gates than review a model's reading of their CI.
|
|
551
|
+
await setup(ui, projectFlag, area, argv.includes("--no-ai") ? NO_PROBES : DEFAULT_PROBES);
|
|
552
|
+
} finally {
|
|
553
|
+
ui.close();
|
|
554
|
+
}
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
485
557
|
case "upgrade": {
|
|
486
558
|
const result = await upgradeConductor({
|
|
487
559
|
version: flag(argv, "to"),
|
|
488
|
-
project:
|
|
560
|
+
project: projectFlag,
|
|
489
561
|
});
|
|
490
562
|
process.stdout.write(
|
|
491
563
|
`${result.alreadyCurrent ? "already current" : "upgrade complete"}:\n` +
|
|
@@ -507,7 +579,7 @@ try {
|
|
|
507
579
|
// record here closes both holes.
|
|
508
580
|
const once = argv.includes("--once");
|
|
509
581
|
const port = portFlag(argv);
|
|
510
|
-
const project =
|
|
582
|
+
const project = projectFlag;
|
|
511
583
|
|
|
512
584
|
// `--once` registers nothing, on purpose. It is precisely the single-tick
|
|
513
585
|
// drill the orphan guard exists to protect, so a drill that announced
|
|
@@ -548,7 +620,7 @@ try {
|
|
|
548
620
|
}
|
|
549
621
|
|
|
550
622
|
case "start": {
|
|
551
|
-
const project =
|
|
623
|
+
const project = projectFlag;
|
|
552
624
|
const herdr = startHerdrFleet(project);
|
|
553
625
|
const rec = await startDaemon({ port: portFlag(argv), project });
|
|
554
626
|
process.stdout.write(
|
|
@@ -560,14 +632,40 @@ try {
|
|
|
560
632
|
break;
|
|
561
633
|
}
|
|
562
634
|
|
|
635
|
+
// `stop` absorbs the old `halt`: one word for "stop the conductor", instead of
|
|
636
|
+
// an operator holding in their head that `halt` was `hold` plus stopping the
|
|
637
|
+
// daemon while `stop` killed only the process. Bouncing the daemon is
|
|
638
|
+
// `restart`, which is what the removed kill-only `stop` was reached for.
|
|
563
639
|
case "stop": {
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
640
|
+
const withPane = argv.includes("--pane");
|
|
641
|
+
const targets = targetProjects().map((project) => ({
|
|
642
|
+
project,
|
|
643
|
+
hold: hold(project.name, "halt"),
|
|
644
|
+
pin: withPane ? pinPaneHalt(project.name).path : undefined,
|
|
645
|
+
}));
|
|
646
|
+
const stop = await stopDaemon();
|
|
647
|
+
const stopLine =
|
|
648
|
+
stop.kind === "not-running"
|
|
649
|
+
? "daemon was not running"
|
|
650
|
+
: `daemon stopped — pid ${stop.pid}${stop.via === "systemctl" ? " (via systemctl)" : ""}`;
|
|
651
|
+
for (const target of targets) {
|
|
652
|
+
if (target.pin !== undefined) {
|
|
653
|
+
const pane = await stopConductorPane(target.project.name);
|
|
654
|
+
process.stdout.write(
|
|
655
|
+
`stopped — claiming paused; ticks disarmed at ${target.hold.disarmed.path}\n` +
|
|
656
|
+
`${stopLine}\n` +
|
|
657
|
+
`pane recovery pinned at ${target.pin}\n` +
|
|
658
|
+
`pane stop: ${pane.stopped} — ${pane.detail}\n` +
|
|
659
|
+
` (conductor agent "${pane.agentName}" only — herdr-fleet.service was NOT stopped;\n` +
|
|
660
|
+
` resume clears the pin when you want recovery again)\n`,
|
|
661
|
+
);
|
|
662
|
+
} else {
|
|
663
|
+
process.stdout.write(
|
|
664
|
+
`stopped — claiming paused; ticks disarmed at ${target.hold.disarmed.path}\n` +
|
|
665
|
+
`${stopLine}\n` +
|
|
666
|
+
`pane left running (pass --pane to stop the conductor agent and pin recovery)\n`,
|
|
667
|
+
);
|
|
668
|
+
}
|
|
571
669
|
}
|
|
572
670
|
break;
|
|
573
671
|
}
|
|
@@ -580,7 +678,7 @@ try {
|
|
|
580
678
|
// goes through systemctl so the replacement stays supervised.
|
|
581
679
|
const { previous, record, via } = await restartDaemon({
|
|
582
680
|
port: portFlag(argv),
|
|
583
|
-
project:
|
|
681
|
+
project: projectFlag,
|
|
584
682
|
});
|
|
585
683
|
if (previous !== undefined) {
|
|
586
684
|
process.stdout.write(
|
|
@@ -606,16 +704,17 @@ try {
|
|
|
606
704
|
let result!: RestartResult;
|
|
607
705
|
const deps: UpgradeDeps = {
|
|
608
706
|
...DEFAULT_DEPS,
|
|
609
|
-
setPaused: (v) =>
|
|
707
|
+
setPaused: (v, project) =>
|
|
708
|
+
setPaused(v, { source: "restart", reason: "restart, draining" }, project),
|
|
610
709
|
restartDaemon: async () => {
|
|
611
710
|
result = await restartDaemon({
|
|
612
711
|
port: portFlag(argv),
|
|
613
|
-
project:
|
|
712
|
+
project: projectFlag,
|
|
614
713
|
});
|
|
615
714
|
},
|
|
616
715
|
};
|
|
617
716
|
try {
|
|
618
|
-
await drainAndRestart(deps, { project:
|
|
717
|
+
await drainAndRestart(deps, { project: projectFlag, timeoutMs });
|
|
619
718
|
} catch (err) {
|
|
620
719
|
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
621
720
|
process.exit(1);
|
|
@@ -634,7 +733,7 @@ try {
|
|
|
634
733
|
}
|
|
635
734
|
|
|
636
735
|
case "status": {
|
|
637
|
-
const project =
|
|
736
|
+
const project = projectFlag;
|
|
638
737
|
const text = await renderStatus(project);
|
|
639
738
|
const stalled = stallLine();
|
|
640
739
|
process.stdout.write(`${text}${stalled === undefined ? "\n" : `\n\n${stalled}\n`}`);
|
|
@@ -650,7 +749,7 @@ try {
|
|
|
650
749
|
*/
|
|
651
750
|
case "ledger": {
|
|
652
751
|
const cfg = loadConfig();
|
|
653
|
-
const p = findProject(cfg,
|
|
752
|
+
const p = findProject(cfg, projectFlag);
|
|
654
753
|
const issueFlag = flag(argv, "issue");
|
|
655
754
|
const issue = issueFlag === undefined ? undefined : issueArg("ledger", issueFlag);
|
|
656
755
|
const limitFlag = flag(argv, "limit");
|
|
@@ -707,7 +806,7 @@ try {
|
|
|
707
806
|
}
|
|
708
807
|
|
|
709
808
|
case "board": {
|
|
710
|
-
const project =
|
|
809
|
+
const project = projectFlag;
|
|
711
810
|
// Headless one-shot mode: `--json` asks for it explicitly, and a
|
|
712
811
|
// non-interactive stdin or stdout (pipe, redirect, cron, script) cannot
|
|
713
812
|
// host the key-driven board anyway. The TTY throw inside runBoard stays
|
|
@@ -721,89 +820,78 @@ try {
|
|
|
721
820
|
}
|
|
722
821
|
|
|
723
822
|
case "hold": {
|
|
724
|
-
const
|
|
725
|
-
|
|
726
|
-
`held — claiming paused` +
|
|
727
|
-
`${r.wasPaused ? " (already paused)" : ""}` +
|
|
728
|
-
`; ticks disarmed at ${r.disarmed.path}` +
|
|
729
|
-
`${r.disarmed.wasArmed ? "" : " (was already disarmed)"}\n` +
|
|
730
|
-
`daemon and pane left running; halt to stop the daemon\n`,
|
|
731
|
-
);
|
|
732
|
-
break;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
case "halt": {
|
|
736
|
-
const project = flag(argv, "project");
|
|
737
|
-
const withPane = argv.includes("--pane");
|
|
738
|
-
if (withPane) {
|
|
739
|
-
const r = await haltWithPane(project);
|
|
740
|
-
const stopLine =
|
|
741
|
-
r.stop.kind === "not-running"
|
|
742
|
-
? "daemon was not running"
|
|
743
|
-
: `daemon stopped — pid ${r.stop.pid}${r.stop.via === "systemctl" ? " (via systemctl)" : ""}`;
|
|
823
|
+
for (const project of targetProjects()) {
|
|
824
|
+
const r = hold(project.name);
|
|
744
825
|
process.stdout.write(
|
|
745
|
-
`
|
|
746
|
-
`${
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
`
|
|
750
|
-
` release-pane clears the pin when you want recovery again)\n`,
|
|
751
|
-
);
|
|
752
|
-
} else {
|
|
753
|
-
const r = await halt(project);
|
|
754
|
-
const stopLine =
|
|
755
|
-
r.stop.kind === "not-running"
|
|
756
|
-
? "daemon was not running"
|
|
757
|
-
: `daemon stopped — pid ${r.stop.pid}${r.stop.via === "systemctl" ? " (via systemctl)" : ""}`;
|
|
758
|
-
process.stdout.write(
|
|
759
|
-
`halted — claiming paused; ticks disarmed at ${r.hold.disarmed.path}\n` +
|
|
760
|
-
`${stopLine}\n` +
|
|
761
|
-
`pane left running (pass --pane to stop the conductor agent and pin recovery)\n`,
|
|
826
|
+
`held — claiming paused` +
|
|
827
|
+
`${r.wasPaused ? " (already paused)" : ""}` +
|
|
828
|
+
`; ticks disarmed at ${r.disarmed.path}` +
|
|
829
|
+
`${r.disarmed.wasArmed ? "" : " (was already disarmed)"}\n` +
|
|
830
|
+
`daemon and pane left running; stop to stop the daemon too\n`,
|
|
762
831
|
);
|
|
763
832
|
}
|
|
764
833
|
break;
|
|
765
834
|
}
|
|
766
835
|
|
|
767
|
-
case
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
);
|
|
836
|
+
// Removed verbs keep a case rather than falling through to usage: an operator
|
|
837
|
+
// who typed the old word gets the new one, not a wall of help text. Exit 2 is
|
|
838
|
+
// the same code an unknown verb uses.
|
|
839
|
+
case "halt":
|
|
840
|
+
process.stderr.write('omp-conductor: "halt" is now "stop" — see help\n');
|
|
841
|
+
process.exit(2);
|
|
774
842
|
break;
|
|
775
|
-
}
|
|
776
843
|
|
|
777
|
-
case "
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
`disarmed — ticks will be skipped` +
|
|
781
|
-
`${r.wasArmed ? "" : " (was already disarmed)"}\n` +
|
|
782
|
-
`marker ${r.path}\n`,
|
|
844
|
+
case "pause":
|
|
845
|
+
process.stderr.write(
|
|
846
|
+
'omp-conductor: "pause" is gone — "hold" pauses claiming AND disarms ticks; see help\n',
|
|
783
847
|
);
|
|
848
|
+
process.exit(2);
|
|
849
|
+
break;
|
|
850
|
+
|
|
851
|
+
case "release-pane":
|
|
852
|
+
process.stderr.write('omp-conductor: "release-pane" is now part of "resume" — see help\n');
|
|
853
|
+
process.exit(2);
|
|
854
|
+
break;
|
|
855
|
+
|
|
856
|
+
case "graph-setup":
|
|
857
|
+
process.stderr.write('omp-conductor: "graph-setup" is now "setup graph" — see help\n');
|
|
858
|
+
process.exit(2);
|
|
859
|
+
break;
|
|
860
|
+
|
|
861
|
+
case "arm": {
|
|
862
|
+
for (const project of targetProjects()) {
|
|
863
|
+
process.stdout.write("arm: sending inbound Telegram challenge…\n");
|
|
864
|
+
const r = await armTicks(project.name);
|
|
865
|
+
process.stdout.write(
|
|
866
|
+
`ARMED — inbound round-trip proved with owner ${r.owner}; ticks are now live.\n` +
|
|
867
|
+
`marker ${r.path}${r.alreadyArmed ? " (replaced previous marker)" : ""}\n`,
|
|
868
|
+
);
|
|
869
|
+
}
|
|
784
870
|
break;
|
|
785
871
|
}
|
|
786
872
|
|
|
787
|
-
case "
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
873
|
+
case "disarm": {
|
|
874
|
+
for (const project of targetProjects()) {
|
|
875
|
+
const r = disarmTicks(project.name);
|
|
876
|
+
process.stdout.write(
|
|
877
|
+
`disarmed — ticks will be skipped` +
|
|
878
|
+
`${r.wasArmed ? "" : " (was already disarmed)"}\n` +
|
|
879
|
+
`marker ${r.path}\n`,
|
|
880
|
+
);
|
|
881
|
+
}
|
|
794
882
|
break;
|
|
795
883
|
}
|
|
796
884
|
|
|
797
885
|
case "tail": {
|
|
798
886
|
const issue = issueArg("tail", argv[1]);
|
|
799
|
-
await tailRun(findProject(loadConfig(),
|
|
887
|
+
await tailRun(findProject(loadConfig(), projectFlag).name, issue);
|
|
800
888
|
break;
|
|
801
889
|
}
|
|
802
890
|
|
|
803
891
|
case "extend": {
|
|
804
892
|
const issue = issueArg("extend", argv[1]);
|
|
805
893
|
const maxTurns = turnsFlag(argv);
|
|
806
|
-
const project = findProject(loadConfig(),
|
|
894
|
+
const project = findProject(loadConfig(), projectFlag);
|
|
807
895
|
const daemon = livingDaemon();
|
|
808
896
|
if (daemon === undefined) throw new Error("daemon is not running");
|
|
809
897
|
if (daemon.project !== undefined && daemon.project !== project.name) {
|
|
@@ -871,7 +959,7 @@ try {
|
|
|
871
959
|
);
|
|
872
960
|
process.exit(2);
|
|
873
961
|
}
|
|
874
|
-
const project = findProject(loadConfig(),
|
|
962
|
+
const project = findProject(loadConfig(), projectFlag);
|
|
875
963
|
const daemon = livingDaemon();
|
|
876
964
|
if (daemon === undefined) throw new Error("daemon is not running");
|
|
877
965
|
if (daemon.project !== undefined && daemon.project !== project.name) {
|
|
@@ -947,7 +1035,7 @@ try {
|
|
|
947
1035
|
case "unblock": {
|
|
948
1036
|
const issue = issueArg("unblock", argv[1]);
|
|
949
1037
|
const cfg = loadConfig();
|
|
950
|
-
const project = findProject(cfg,
|
|
1038
|
+
const project = findProject(cfg, projectFlag);
|
|
951
1039
|
const store = openStore(dbPath());
|
|
952
1040
|
try {
|
|
953
1041
|
const outcome = await unblockIssue(project, makeTracker(project), store, issue, {
|
|
@@ -989,7 +1077,7 @@ try {
|
|
|
989
1077
|
process.exit(2);
|
|
990
1078
|
}
|
|
991
1079
|
const cfg = loadConfig();
|
|
992
|
-
const project = findProject(cfg,
|
|
1080
|
+
const project = findProject(cfg, projectFlag);
|
|
993
1081
|
const args: Record<string, string> = {};
|
|
994
1082
|
for (let i = 2; i < argv.length; i++) {
|
|
995
1083
|
const token = argv[i];
|
|
@@ -1003,7 +1091,7 @@ try {
|
|
|
1003
1091
|
args[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
1004
1092
|
i++;
|
|
1005
1093
|
} else if (token === "--project" || token?.startsWith("--project=") === true) {
|
|
1006
|
-
// Consumed by `
|
|
1094
|
+
// Consumed by `projectFlag` above; both spellings skip here.
|
|
1007
1095
|
if (token === "--project") i++;
|
|
1008
1096
|
} else {
|
|
1009
1097
|
process.stderr.write(
|
|
@@ -1027,10 +1115,10 @@ try {
|
|
|
1027
1115
|
tracker: makeTracker(project),
|
|
1028
1116
|
actions: githubVerbActions(project),
|
|
1029
1117
|
fleetStop: () =>
|
|
1030
|
-
isPaused()
|
|
1031
|
-
? "claiming is paused for this fleet (omp-conductor
|
|
1118
|
+
isPaused(project.name)
|
|
1119
|
+
? "claiming is paused for this fleet (omp-conductor hold or stop)"
|
|
1032
1120
|
: undefined,
|
|
1033
|
-
pausedAt,
|
|
1121
|
+
pausedAt: () => pausedAt(project.name),
|
|
1034
1122
|
log: (m) => process.stderr.write(`${m}\n`),
|
|
1035
1123
|
now: () => Date.now(),
|
|
1036
1124
|
chain: { readBaseChain },
|
|
@@ -1079,7 +1167,7 @@ try {
|
|
|
1079
1167
|
process.stderr.write("omp-conductor: event record --occurred-at needs an ISO timestamp\n");
|
|
1080
1168
|
process.exit(2);
|
|
1081
1169
|
}
|
|
1082
|
-
const project = findProject(loadConfig(),
|
|
1170
|
+
const project = findProject(loadConfig(), projectFlag);
|
|
1083
1171
|
const store = openStore(dbPath());
|
|
1084
1172
|
try {
|
|
1085
1173
|
const event = store.recordMaterialEvent({
|
|
@@ -1129,7 +1217,7 @@ try {
|
|
|
1129
1217
|
);
|
|
1130
1218
|
process.exit(2);
|
|
1131
1219
|
}
|
|
1132
|
-
const project = findProject(loadConfig(),
|
|
1220
|
+
const project = findProject(loadConfig(), projectFlag);
|
|
1133
1221
|
const store = openStore(dbPath());
|
|
1134
1222
|
try {
|
|
1135
1223
|
const at = Date.now();
|
|
@@ -1215,7 +1303,7 @@ try {
|
|
|
1215
1303
|
|
|
1216
1304
|
case "decision": {
|
|
1217
1305
|
const sub = argv[1];
|
|
1218
|
-
const project = findProject(loadConfig(),
|
|
1306
|
+
const project = findProject(loadConfig(), projectFlag);
|
|
1219
1307
|
const store = openStore(dbPath());
|
|
1220
1308
|
try {
|
|
1221
1309
|
if (sub === "open") {
|
|
@@ -1326,7 +1414,7 @@ try {
|
|
|
1326
1414
|
}
|
|
1327
1415
|
const issueText = flag(argv, "issue");
|
|
1328
1416
|
const issue = issueText === undefined ? undefined : issueArg("friction --issue", issueText);
|
|
1329
|
-
const project = findProject(loadConfig(),
|
|
1417
|
+
const project = findProject(loadConfig(), projectFlag);
|
|
1330
1418
|
const store = openStore(dbPath());
|
|
1331
1419
|
try {
|
|
1332
1420
|
store.recordFriction(project.name, {
|
|
@@ -1343,70 +1431,18 @@ try {
|
|
|
1343
1431
|
break;
|
|
1344
1432
|
}
|
|
1345
1433
|
|
|
1346
|
-
case "
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
process.stdout.write(
|
|
1357
|
-
"resumed — claiming allowed on the next tick\n" +
|
|
1358
|
-
"note: did NOT re-arm; run arm after an inbound Telegram proof to resume ticks\n",
|
|
1359
|
-
);
|
|
1360
|
-
break;
|
|
1361
|
-
|
|
1362
|
-
case "graph-setup": {
|
|
1363
|
-
// Refused rather than accommodated. Under sudo every path this command
|
|
1364
|
-
// derives — the config it loads, the state directory it writes to, the
|
|
1365
|
-
// HOME and User= it bakes into the unit — resolves as root instead of the
|
|
1366
|
-
// fleet's account, and the result is a timer that goes green while
|
|
1367
|
-
// building indexes in a store no worker session ever reads. Nothing about
|
|
1368
|
-
// that announces itself, so the only safe answer is to stop.
|
|
1369
|
-
if (process.env["SUDO_USER"] !== undefined) {
|
|
1370
|
-
process.stderr.write(
|
|
1371
|
-
"omp-conductor: run graph-setup as the account the fleet runs as, not under sudo.\n" +
|
|
1372
|
-
`Under sudo the config, ~/.cache and the unit's User= all resolve as root, and the\n` +
|
|
1373
|
-
`indexes land where no worker can read them. Only installing the units needs root,\n` +
|
|
1374
|
-
"and this command prints those two lines for you at the end.\n",
|
|
1375
|
-
);
|
|
1376
|
-
process.exit(1);
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
const project = findProject(loadConfig(), flag(argv, "project"));
|
|
1380
|
-
if (graphRepos(project).length === 0) {
|
|
1381
|
-
// Not a warning: with nothing configured there is nothing to print, and
|
|
1382
|
-
// the fix is a wizard answer rather than a flag on this command.
|
|
1383
|
-
process.stderr.write(
|
|
1384
|
-
`omp-conductor: no repo in project "${project.name}" has graphProject set — re-run\n` +
|
|
1385
|
-
"/conductor setup and say yes to code-graph discovery.\n",
|
|
1386
|
-
);
|
|
1387
|
-
process.exit(1);
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
if (!argv.includes("--write")) {
|
|
1391
|
-
process.stdout.write(`${formatGraphSetup(project)}\n`);
|
|
1392
|
-
break;
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
let result: GraphSetupWrite;
|
|
1396
|
-
try {
|
|
1397
|
-
result = writeGraphSetup(project);
|
|
1398
|
-
} catch (err) {
|
|
1399
|
-
// No longer the permissions case — all three files go to this account's
|
|
1400
|
-
// own state directory — so this is a full disk, a read-only mount or a
|
|
1401
|
-
// state directory someone else owns. Say what failed and offer the
|
|
1402
|
-
// printed plan, which is a complete substitute for the write.
|
|
1403
|
-
process.stderr.write(
|
|
1404
|
-
`omp-conductor: could not stage the files (${err instanceof Error ? err.message : String(err)}).\n` +
|
|
1405
|
-
"Drop --write and copy the printed text yourself — it is the same content.\n",
|
|
1434
|
+
case "resume": {
|
|
1435
|
+
for (const project of targetProjects()) {
|
|
1436
|
+
releaseHold(project.name);
|
|
1437
|
+
const pin = clearPaneHaltIfResolvable(project.name);
|
|
1438
|
+
process.stdout.write(
|
|
1439
|
+
"resumed — claiming allowed on the next tick\n" +
|
|
1440
|
+
(pin.wasHalted
|
|
1441
|
+
? `pane recovery pin cleared — ${pin.path}\n`
|
|
1442
|
+
: "no pane recovery pin to clear\n") +
|
|
1443
|
+
"ticks stay disarmed — run `omp-conductor arm`\n",
|
|
1406
1444
|
);
|
|
1407
|
-
process.exit(1);
|
|
1408
1445
|
}
|
|
1409
|
-
process.stdout.write(`wrote:\n${result.written.map((f) => ` ${f}`).join("\n")}\n\n${result.next}\n`);
|
|
1410
1446
|
break;
|
|
1411
1447
|
}
|
|
1412
1448
|
|
|
@@ -1415,12 +1451,12 @@ try {
|
|
|
1415
1451
|
let project: ProjectConfig | undefined;
|
|
1416
1452
|
let path: string;
|
|
1417
1453
|
if (override === undefined) {
|
|
1418
|
-
project = findProject(loadConfig(),
|
|
1454
|
+
project = findProject(loadConfig(), projectFlag);
|
|
1419
1455
|
path = briefPathForProject(project);
|
|
1420
1456
|
} else {
|
|
1421
1457
|
path = override;
|
|
1422
1458
|
try {
|
|
1423
|
-
project = findProject(loadConfig(),
|
|
1459
|
+
project = findProject(loadConfig(), projectFlag);
|
|
1424
1460
|
} catch {
|
|
1425
1461
|
project = undefined;
|
|
1426
1462
|
}
|
|
@@ -1545,7 +1581,7 @@ try {
|
|
|
1545
1581
|
} catch {
|
|
1546
1582
|
process.stderr.write(
|
|
1547
1583
|
`omp-conductor: no brief at ${path}` +
|
|
1548
|
-
`${override === undefined ? " — run
|
|
1584
|
+
`${override === undefined ? " — run `omp-conductor setup brief` and say yes to writing ORCHESTRATOR.md." : "."}\n`,
|
|
1549
1585
|
);
|
|
1550
1586
|
process.exit(1);
|
|
1551
1587
|
}
|