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/src/cli.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * Standalone entry point. Everything here is argument handling and printing
4
- * the loop, the caps and the state all live in ./daemon.ts and the background
5
- * process lifecycle in ./lifecycle.ts, so the CLI and the `/conductor` plugin
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
- clearPaneHalt,
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 halt [--pane] [--project NAME]
104
- omp-conductor arm [--project NAME]
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 pause [--reason TEXT]
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 running daemon. Uses systemctl when the omp-conductor
136
- unit owns the process (so Restart=on-failure cannot bring it back);
137
- otherwise SIGTERM then SIGKILL.
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
- halt hold, then stop the dispatch daemon (systemctl-aware). Pane stays up
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
- release-pane
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 stop claiming new work only (ticks keep firing if armed). Prefer hold.
232
- --reason TEXT is recorded in the pause sentinel (\`status\` shows it).
233
- resume clear pause only — does NOT re-arm. Prefer hold's inverse: resume + arm.
234
- graph-setup
235
- print how to set up the code-graph indexes workers query instead of
236
- grepping: the clone commands for any missing index-only clone, the
237
- index command per repo, and a systemd service+timer that keeps them
238
- current. --write writes the two units and the script they run, and
239
- prints the systemctl line to run — it never runs systemctl itself.
240
- Exits 1 when no repo in the project has graphProject configured.
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 every project
255
- and survives a daemon restart. Hold also removes the arm marker the heartbeat
256
- reads, so both brains go quiet without killing processes. A running daemon is
257
- tracked by a pidfile under $OMP_CONDUCTOR_RUNTIME_DIR (default
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
- halt hold + stop dispatch daemon
265
- halt --pane halt + pin conductor-pane recovery off
266
- resume && arm clear pause, then prove inbound Telegram before ticks resume`;
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: flag(argv, "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 = flag(argv, "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 = flag(argv, "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 result = await stopDaemon();
565
- if (result.kind === "not-running") {
566
- process.stdout.write("not running\n");
567
- } else {
568
- process.stdout.write(
569
- `stopped — pid ${result.pid}${result.via === "systemctl" ? " (via systemctl)" : ""}\n`,
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: flag(argv, "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) => setPaused(v, { source: "restart", reason: "restart, draining" }),
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: flag(argv, "project"),
712
+ project: projectFlag,
614
713
  });
615
714
  },
616
715
  };
617
716
  try {
618
- await drainAndRestart(deps, { project: flag(argv, "project"), timeoutMs });
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 = flag(argv, "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, flag(argv, "project"));
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 = flag(argv, "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 r = hold(flag(argv, "project"));
725
- process.stdout.write(
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
- `halted — claiming paused; ticks disarmed at ${r.hold.disarmed.path}\n` +
746
- `${stopLine}\n` +
747
- `pane recovery pinned at ${r.pane.pinPath}\n` +
748
- `pane stop: ${r.pane.stopped} ${r.pane.detail}\n` +
749
- ` (conductor agent "${r.pane.agentName}" only herdr-fleet.service was NOT stopped;\n` +
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 "arm": {
768
- process.stdout.write("arm: sending inbound Telegram challenge…\n");
769
- const r = await armTicks(flag(argv, "project"));
770
- process.stdout.write(
771
- `ARMED — inbound round-trip proved with owner ${r.owner}; ticks are now live.\n` +
772
- `marker ${r.path}${r.alreadyArmed ? " (replaced previous marker)" : ""}\n`,
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 "disarm": {
778
- const r = disarmTicks(flag(argv, "project"));
779
- process.stdout.write(
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 "release-pane": {
788
- const r = clearPaneHalt(flag(argv, "project"));
789
- process.stdout.write(
790
- r.wasHalted
791
- ? `pane recovery pin cleared ${r.path}\nherdr-conductor may resume the fleet agent again\n`
792
- : `no pane recovery pin at ${r.path}\n`,
793
- );
873
+ case "disarm": {
874
+ for (const project of targetProjects()) {
875
+ const r = disarmTicks(project.name);
876
+ process.stdout.write(
877
+ `disarmedticks 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(), flag(argv, "project")).name, issue);
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(), flag(argv, "project"));
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(), flag(argv, "project"));
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, flag(argv, "project"));
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, flag(argv, "project"));
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 `flag(argv, "project")` above; both spellings skip here.
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 pause, hold or halt)"
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(), flag(argv, "project"));
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(), flag(argv, "project"));
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(), flag(argv, "project"));
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(), flag(argv, "project"));
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 "pause":
1347
- setPaused(true, { source: "pause", reason: flag(argv, "reason") });
1348
- process.stdout.write(
1349
- "paused no new work will be claimed\n" +
1350
- "note: ticks keep firing if armed; use hold to silence both\n",
1351
- );
1352
- break;
1353
-
1354
- case "resume":
1355
- releaseHold();
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(), flag(argv, "project"));
1454
+ project = findProject(loadConfig(), projectFlag);
1419
1455
  path = briefPathForProject(project);
1420
1456
  } else {
1421
1457
  path = override;
1422
1458
  try {
1423
- project = findProject(loadConfig(), flag(argv, "project"));
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 /conductor setup and say yes to writing ORCHESTRATOR.md." : "."}\n`,
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
  }