omp-conductor 0.13.0 → 0.15.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 +549 -234
- package/package.json +8 -5
- package/schema/config.schema.json +609 -0
- package/src/availability.ts +165 -0
- package/src/board.ts +19 -32
- package/src/brief-upgrade.ts +1 -1
- package/src/briefs/orchestrator.md +72 -31
- package/src/briefs/policy.md +48 -36
- 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 +356 -212
- package/src/config-schema.ts +352 -0
- package/src/config.ts +1037 -679
- package/src/confinement.ts +54 -0
- package/src/daemon.ts +644 -390
- package/src/diff-flags.ts +73 -4
- package/src/digest-schedule.ts +92 -24
- package/src/escalate.ts +89 -22
- package/src/fleet.ts +351 -46
- 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 +732 -56
- package/src/privileged.ts +264 -0
- package/src/reports.ts +203 -6
- package/src/session-host.ts +3 -0
- package/src/setup-host.ts +209 -24
- package/src/setup-install.ts +320 -0
- package/src/setup-probe.ts +412 -0
- package/src/setup-wizard.ts +1946 -0
- package/src/setup.ts +457 -53
- package/src/store.ts +610 -98
- package/src/tracker/github.ts +43 -5
- package/src/types.ts +153 -14
- package/src/upgrade.ts +44 -10
- package/src/verbs/actions.ts +131 -13
- package/src/verbs/server.ts +40 -18
- package/src/wizard-ui.ts +249 -0
- package/src/worker.ts +24 -7
- package/skills/conductor-onboarding/SKILL.md +0 -748
- package/skills/conductor-update/SKILL.md +0 -51
- package/src/plugin.ts +0 -1495
package/src/cli.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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
|
*/
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
8
|
import { closeSync, openSync, readFileSync, readSync, statSync } from "node:fs";
|
|
9
9
|
import { userInfo } from "node:os";
|
|
10
10
|
import { dirname, join } from "node:path";
|
|
@@ -21,19 +21,21 @@ import {
|
|
|
21
21
|
proposeRetrofit,
|
|
22
22
|
repairPolicyBannerCrumbs,
|
|
23
23
|
} from "./brief-upgrade.ts";
|
|
24
|
+
import { interruptDisposition } from "./availability.ts";
|
|
24
25
|
import { findProject, loadConfig, resolveCaps, stateDir } from "./config.ts";
|
|
25
26
|
import { CONDITION_FORMS, parseCondition } from "./decisions.ts";
|
|
26
27
|
import { isPaused, pausedAt, runDaemon, setPaused } from "./daemon.ts";
|
|
27
28
|
import {
|
|
28
29
|
armTicks,
|
|
29
|
-
|
|
30
|
+
clearPaneHaltIfResolvable,
|
|
30
31
|
disarmTicks,
|
|
31
|
-
halt,
|
|
32
|
-
haltWithPane,
|
|
33
32
|
hold,
|
|
33
|
+
pinPaneHalt,
|
|
34
34
|
releaseHold,
|
|
35
35
|
renderStatus,
|
|
36
36
|
startHerdrFleet,
|
|
37
|
+
stopConductorPane,
|
|
38
|
+
telegramStateDir,
|
|
37
39
|
} from "./fleet.ts";
|
|
38
40
|
import { formatGraphSetup, graphRepos, writeGraphSetup, type GraphSetupWrite } from "./graph.ts";
|
|
39
41
|
import { readBaseChain } from "./gitops.ts";
|
|
@@ -51,12 +53,17 @@ import { STALL_MARKER_FILE } from "./orchestrator-tick.ts";
|
|
|
51
53
|
import { digestDedupeKey } from "./reports.ts";
|
|
52
54
|
import { digestDue } from "./digest-schedule.ts";
|
|
53
55
|
import {
|
|
56
|
+
AMEND_AREA_IDS,
|
|
54
57
|
briefPathForProject,
|
|
55
58
|
policyPathForProject,
|
|
56
59
|
renderBriefForProject,
|
|
57
60
|
renderFloorForProject,
|
|
58
61
|
shippedBriefTemplate,
|
|
62
|
+
type AmendAreaId,
|
|
59
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";
|
|
60
67
|
import { dbPath, LIVE_STATES, openStore } from "./store.ts";
|
|
61
68
|
import { formatTranscriptLine } from "./transcript.ts";
|
|
62
69
|
import { formatVerbLedgerEntry } from "./verbs/ledger.ts";
|
|
@@ -89,19 +96,18 @@ function packageVersion(): string {
|
|
|
89
96
|
const USAGE = `omp-conductor — dispatch ready issues to omp coding sessions
|
|
90
97
|
|
|
91
98
|
usage:
|
|
99
|
+
omp-conductor setup [area] [--no-ai] [--project NAME]
|
|
92
100
|
omp-conductor start [--port N] [--project NAME]
|
|
93
101
|
omp-conductor --version
|
|
94
|
-
omp-conductor stop
|
|
102
|
+
omp-conductor stop [--pane] [--project NAME | --all]
|
|
95
103
|
omp-conductor restart [--now] [--timeout SECONDS] [--port N] [--project NAME]
|
|
96
104
|
omp-conductor upgrade [--to VERSION] [--project NAME]
|
|
97
105
|
omp-conductor board [--project NAME] [--json]
|
|
98
106
|
omp-conductor status [--project NAME]
|
|
99
107
|
omp-conductor ledger [--issue N] [--limit N] [--project NAME]
|
|
100
|
-
omp-conductor hold [--project NAME]
|
|
101
|
-
omp-conductor
|
|
102
|
-
omp-conductor
|
|
103
|
-
omp-conductor disarm [--project NAME]
|
|
104
|
-
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]
|
|
105
111
|
omp-conductor tail <issue> [--project NAME]
|
|
106
112
|
omp-conductor extend <issue> --turns N [--project NAME]
|
|
107
113
|
omp-conductor worker pause <issue> [--project NAME]
|
|
@@ -110,18 +116,22 @@ usage:
|
|
|
110
116
|
omp-conductor unblock <issue> [--force] [--no-requeue] [--project NAME]
|
|
111
117
|
omp-conductor verb <conductor_*> [--project NAME] [--arg k=v ...]
|
|
112
118
|
omp-conductor daemon [--once] [--port N] [--project NAME]
|
|
113
|
-
omp-conductor
|
|
114
|
-
omp-conductor resume
|
|
115
|
-
omp-conductor graph-setup [--project NAME] [--write]
|
|
119
|
+
omp-conductor resume [--project NAME | --all]
|
|
116
120
|
omp-conductor brief-upgrade [--migrate|--retrofit] [--apply] [--file PATH] [--project NAME]
|
|
117
121
|
omp-conductor friction <escalation-digest|report-noise|report-surprise> --detail TEXT [--issue N] [--project NAME]
|
|
118
|
-
omp-conductor
|
|
122
|
+
omp-conductor event record --category NAME --summary TEXT --evidence REF [--occurred-at ISO] [--project NAME]
|
|
123
|
+
omp-conductor report --text TEXT [--kind material|digest] [--events IDS] [--notices IDS] [--project NAME]
|
|
119
124
|
omp-conductor decision open --question TEXT [--blocks TEXT] [--resolves-when COND] [--project NAME]
|
|
120
125
|
omp-conductor decision resolve <id> --answer TEXT [--project NAME]
|
|
121
126
|
omp-conductor decision withdraw <id> [--reason TEXT] [--project NAME]
|
|
122
127
|
omp-conductor decision list [--project NAME]
|
|
123
128
|
omp-conductor help
|
|
124
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(", ")}.
|
|
125
135
|
upgrade update the Bun-global CLI, omp plugin, Herdr recovery plugin, and
|
|
126
136
|
brief as one pinned release. Pauses only new claims, drains live
|
|
127
137
|
workers, reloads, verifies twice, and restores the prior dispatch
|
|
@@ -129,9 +139,14 @@ usage:
|
|
|
129
139
|
start start the installed herdr-fleet.service when present, then run the
|
|
130
140
|
dispatch loop in the background and wait until it answers GET
|
|
131
141
|
/healthz on :8787 (override with --port). Refuses if one is running.
|
|
132
|
-
stop stop the
|
|
133
|
-
|
|
134
|
-
|
|
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.
|
|
135
150
|
restart drain the fleet by default: pause new claims, wait until live workers
|
|
136
151
|
reach 0/N (bounded by --timeout SECONDS, default 1800), restart keeping
|
|
137
152
|
the running daemon's port and project unless a flag overrides them,
|
|
@@ -156,18 +171,12 @@ usage:
|
|
|
156
171
|
as well as what they did. --issue narrows it to one issue's run.
|
|
157
172
|
hold soft stop: pause claiming AND disarm ticks. Daemon and pane stay up.
|
|
158
173
|
This is "stop the conductor overnight" without killing processes.
|
|
159
|
-
|
|
160
|
-
unless --pane is passed.
|
|
161
|
-
halt --pane
|
|
162
|
-
halt, then pin herdr-conductor recovery off for the conductor agent
|
|
163
|
-
only — does NOT stop herdr-fleet.service or any other herdr session.
|
|
164
|
-
Clear the pin with release-pane when you want recovery again.
|
|
174
|
+
Use --all to target every configured project.
|
|
165
175
|
arm proof-gated: send a Telegram challenge and write the arm marker only
|
|
166
176
|
after your reply appears as a user turn in the orchestrator transcript.
|
|
167
|
-
Never auto-armed by resume/hold.
|
|
168
|
-
disarm remove the arm marker so ticks skip. Processes untouched.
|
|
169
|
-
|
|
170
|
-
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.
|
|
171
180
|
tail follow the newest run for <issue>: the worker's assistant text and
|
|
172
181
|
the tools it calls, printed as they land. Workers are sessions inside
|
|
173
182
|
the daemon rather than terminals, so this is the only way to watch
|
|
@@ -197,12 +206,17 @@ usage:
|
|
|
197
206
|
strings, one per --arg (e.g. --arg prUrl=https://x --arg headSha=y).
|
|
198
207
|
A refusal exits 3. See conductor_pr_merge/conductor_label/
|
|
199
208
|
conductor_release/conductor_pr_update_branch in the brief.
|
|
209
|
+
event persist one ordinary material outcome without sending it. Category
|
|
210
|
+
is a short lowercase slug; summary and evidence name what happened
|
|
211
|
+
and where it can be verified. --occurred-at defaults to now.
|
|
200
212
|
report hand a rendered report to the daemon's durable outbox. The report is
|
|
201
213
|
persisted before anything is sent, delivered by the daemon with
|
|
202
214
|
bounded retries, and shown by status until it lands. Delivery is
|
|
203
215
|
at-least-once: a crash mid-send is retried and the retry says it may
|
|
204
216
|
be a repeat. --kind digest is accepted at most once per local day,
|
|
205
217
|
decided from the ledger rather than from what you remember sending.
|
|
218
|
+
A digest associates the comma-separated --events and --notices rows
|
|
219
|
+
atomically; omitted rows stay owed.
|
|
206
220
|
decision record, list and close the questions you have put to your operator.
|
|
207
221
|
A question that lives only in a session's context is lost to the next
|
|
208
222
|
compaction, so \`decision open\` writes it down and every tick's prompt
|
|
@@ -220,16 +234,16 @@ usage:
|
|
|
220
234
|
loop; recording one never edits policy by itself.
|
|
221
235
|
daemon run the dispatch loop in the foreground; --once runs a single tick
|
|
222
236
|
and exits. This is what \`start\` launches.
|
|
223
|
-
pause
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
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.
|
|
233
247
|
brief-upgrade
|
|
234
248
|
inspect the brief overlay (package floor + POLICY.md). Reports by
|
|
235
249
|
default. --migrate lifts a bannered ORCHESTRATOR.md owned half into
|
|
@@ -243,19 +257,20 @@ usage:
|
|
|
243
257
|
--version
|
|
244
258
|
print the installed omp-conductor package version (also -V, version).
|
|
245
259
|
|
|
246
|
-
Pause is a flag file under the state directory, so it applies to
|
|
247
|
-
|
|
248
|
-
reads, so both brains go quiet
|
|
249
|
-
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
|
|
250
265
|
~/.omp/run/daemons/omp-conductor), written whether it was started in the
|
|
251
266
|
background or in the foreground, and probed for liveness on every read — a
|
|
252
267
|
stale one never blocks a start.
|
|
253
268
|
|
|
254
269
|
Stop the conductor:
|
|
255
270
|
hold no claims, no tick sends (inspectable)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
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`;
|
|
259
274
|
|
|
260
275
|
/** Accepts both `--port 9000` and `--port=9000`; returns undefined when absent. */
|
|
261
276
|
function flag(argv: string[], name: string): string | undefined {
|
|
@@ -265,6 +280,26 @@ function flag(argv: string[], name: string): string | undefined {
|
|
|
265
280
|
return prefixed?.slice(name.length + 3);
|
|
266
281
|
}
|
|
267
282
|
|
|
283
|
+
function digestIdsFlag(argv: string[], name: "events" | "notices"): string[] {
|
|
284
|
+
const raw = flag(argv, name);
|
|
285
|
+
if (raw === undefined) {
|
|
286
|
+
if (argv.includes(`--${name}`)) {
|
|
287
|
+
process.stderr.write(`omp-conductor: report --${name} needs comma-separated row ids\n`);
|
|
288
|
+
process.exit(2);
|
|
289
|
+
}
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
const ids = [...new Set(raw.split(",").map((id) => id.trim()).filter((id) => id.length > 0))];
|
|
293
|
+
if (
|
|
294
|
+
ids.length === 0 ||
|
|
295
|
+
ids.some((id) => !/^(?:[0-9a-f]{12}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/.test(id))
|
|
296
|
+
) {
|
|
297
|
+
process.stderr.write(`omp-conductor: report --${name} needs comma-separated ledger row ids\n`);
|
|
298
|
+
process.exit(2);
|
|
299
|
+
}
|
|
300
|
+
return ids;
|
|
301
|
+
}
|
|
302
|
+
|
|
268
303
|
/**
|
|
269
304
|
* `--port` for the three verbs that take one. Exits 2 rather than defaulting,
|
|
270
305
|
* because silently ignoring a typo'd port would leave the operator probing an
|
|
@@ -446,6 +481,24 @@ async function tailRun(project: string, issue: number): Promise<void> {
|
|
|
446
481
|
|
|
447
482
|
const argv = process.argv.slice(2);
|
|
448
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
|
+
}
|
|
449
502
|
|
|
450
503
|
try {
|
|
451
504
|
switch (cmd) {
|
|
@@ -454,10 +507,57 @@ try {
|
|
|
454
507
|
case "version":
|
|
455
508
|
process.stdout.write(`${packageVersion()}\n`);
|
|
456
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
|
+
}
|
|
457
557
|
case "upgrade": {
|
|
458
558
|
const result = await upgradeConductor({
|
|
459
559
|
version: flag(argv, "to"),
|
|
460
|
-
project:
|
|
560
|
+
project: projectFlag,
|
|
461
561
|
});
|
|
462
562
|
process.stdout.write(
|
|
463
563
|
`${result.alreadyCurrent ? "already current" : "upgrade complete"}:\n` +
|
|
@@ -479,7 +579,7 @@ try {
|
|
|
479
579
|
// record here closes both holes.
|
|
480
580
|
const once = argv.includes("--once");
|
|
481
581
|
const port = portFlag(argv);
|
|
482
|
-
const project =
|
|
582
|
+
const project = projectFlag;
|
|
483
583
|
|
|
484
584
|
// `--once` registers nothing, on purpose. It is precisely the single-tick
|
|
485
585
|
// drill the orphan guard exists to protect, so a drill that announced
|
|
@@ -520,7 +620,7 @@ try {
|
|
|
520
620
|
}
|
|
521
621
|
|
|
522
622
|
case "start": {
|
|
523
|
-
const project =
|
|
623
|
+
const project = projectFlag;
|
|
524
624
|
const herdr = startHerdrFleet(project);
|
|
525
625
|
const rec = await startDaemon({ port: portFlag(argv), project });
|
|
526
626
|
process.stdout.write(
|
|
@@ -532,14 +632,40 @@ try {
|
|
|
532
632
|
break;
|
|
533
633
|
}
|
|
534
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.
|
|
535
639
|
case "stop": {
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
+
}
|
|
543
669
|
}
|
|
544
670
|
break;
|
|
545
671
|
}
|
|
@@ -552,7 +678,7 @@ try {
|
|
|
552
678
|
// goes through systemctl so the replacement stays supervised.
|
|
553
679
|
const { previous, record, via } = await restartDaemon({
|
|
554
680
|
port: portFlag(argv),
|
|
555
|
-
project:
|
|
681
|
+
project: projectFlag,
|
|
556
682
|
});
|
|
557
683
|
if (previous !== undefined) {
|
|
558
684
|
process.stdout.write(
|
|
@@ -578,16 +704,17 @@ try {
|
|
|
578
704
|
let result!: RestartResult;
|
|
579
705
|
const deps: UpgradeDeps = {
|
|
580
706
|
...DEFAULT_DEPS,
|
|
581
|
-
setPaused: (v) =>
|
|
707
|
+
setPaused: (v, project) =>
|
|
708
|
+
setPaused(v, { source: "restart", reason: "restart, draining" }, project),
|
|
582
709
|
restartDaemon: async () => {
|
|
583
710
|
result = await restartDaemon({
|
|
584
711
|
port: portFlag(argv),
|
|
585
|
-
project:
|
|
712
|
+
project: projectFlag,
|
|
586
713
|
});
|
|
587
714
|
},
|
|
588
715
|
};
|
|
589
716
|
try {
|
|
590
|
-
await drainAndRestart(deps, { project:
|
|
717
|
+
await drainAndRestart(deps, { project: projectFlag, timeoutMs });
|
|
591
718
|
} catch (err) {
|
|
592
719
|
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
593
720
|
process.exit(1);
|
|
@@ -606,7 +733,7 @@ try {
|
|
|
606
733
|
}
|
|
607
734
|
|
|
608
735
|
case "status": {
|
|
609
|
-
const project =
|
|
736
|
+
const project = projectFlag;
|
|
610
737
|
const text = await renderStatus(project);
|
|
611
738
|
const stalled = stallLine();
|
|
612
739
|
process.stdout.write(`${text}${stalled === undefined ? "\n" : `\n\n${stalled}\n`}`);
|
|
@@ -622,7 +749,7 @@ try {
|
|
|
622
749
|
*/
|
|
623
750
|
case "ledger": {
|
|
624
751
|
const cfg = loadConfig();
|
|
625
|
-
const p = findProject(cfg,
|
|
752
|
+
const p = findProject(cfg, projectFlag);
|
|
626
753
|
const issueFlag = flag(argv, "issue");
|
|
627
754
|
const issue = issueFlag === undefined ? undefined : issueArg("ledger", issueFlag);
|
|
628
755
|
const limitFlag = flag(argv, "limit");
|
|
@@ -679,7 +806,7 @@ try {
|
|
|
679
806
|
}
|
|
680
807
|
|
|
681
808
|
case "board": {
|
|
682
|
-
const project =
|
|
809
|
+
const project = projectFlag;
|
|
683
810
|
// Headless one-shot mode: `--json` asks for it explicitly, and a
|
|
684
811
|
// non-interactive stdin or stdout (pipe, redirect, cron, script) cannot
|
|
685
812
|
// host the key-driven board anyway. The TTY throw inside runBoard stays
|
|
@@ -693,89 +820,78 @@ try {
|
|
|
693
820
|
}
|
|
694
821
|
|
|
695
822
|
case "hold": {
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
`held — claiming paused` +
|
|
699
|
-
`${r.wasPaused ? " (already paused)" : ""}` +
|
|
700
|
-
`; ticks disarmed at ${r.disarmed.path}` +
|
|
701
|
-
`${r.disarmed.wasArmed ? "" : " (was already disarmed)"}\n` +
|
|
702
|
-
`daemon and pane left running; halt to stop the daemon\n`,
|
|
703
|
-
);
|
|
704
|
-
break;
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
case "halt": {
|
|
708
|
-
const project = flag(argv, "project");
|
|
709
|
-
const withPane = argv.includes("--pane");
|
|
710
|
-
if (withPane) {
|
|
711
|
-
const r = await haltWithPane(project);
|
|
712
|
-
const stopLine =
|
|
713
|
-
r.stop.kind === "not-running"
|
|
714
|
-
? "daemon was not running"
|
|
715
|
-
: `daemon stopped — pid ${r.stop.pid}${r.stop.via === "systemctl" ? " (via systemctl)" : ""}`;
|
|
716
|
-
process.stdout.write(
|
|
717
|
-
`halted — claiming paused; ticks disarmed at ${r.hold.disarmed.path}\n` +
|
|
718
|
-
`${stopLine}\n` +
|
|
719
|
-
`pane recovery pinned at ${r.pane.pinPath}\n` +
|
|
720
|
-
`pane stop: ${r.pane.stopped} — ${r.pane.detail}\n` +
|
|
721
|
-
` (conductor agent "${r.pane.agentName}" only — herdr-fleet.service was NOT stopped;\n` +
|
|
722
|
-
` release-pane clears the pin when you want recovery again)\n`,
|
|
723
|
-
);
|
|
724
|
-
} else {
|
|
725
|
-
const r = await halt(project);
|
|
726
|
-
const stopLine =
|
|
727
|
-
r.stop.kind === "not-running"
|
|
728
|
-
? "daemon was not running"
|
|
729
|
-
: `daemon stopped — pid ${r.stop.pid}${r.stop.via === "systemctl" ? " (via systemctl)" : ""}`;
|
|
823
|
+
for (const project of targetProjects()) {
|
|
824
|
+
const r = hold(project.name);
|
|
730
825
|
process.stdout.write(
|
|
731
|
-
`
|
|
732
|
-
`${
|
|
733
|
-
|
|
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`,
|
|
734
831
|
);
|
|
735
832
|
}
|
|
736
833
|
break;
|
|
737
834
|
}
|
|
738
835
|
|
|
739
|
-
case
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
);
|
|
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);
|
|
746
842
|
break;
|
|
747
|
-
}
|
|
748
843
|
|
|
749
|
-
case "
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
`disarmed — ticks will be skipped` +
|
|
753
|
-
`${r.wasArmed ? "" : " (was already disarmed)"}\n` +
|
|
754
|
-
`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',
|
|
755
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
|
+
}
|
|
756
870
|
break;
|
|
757
871
|
}
|
|
758
872
|
|
|
759
|
-
case "
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|
+
}
|
|
766
882
|
break;
|
|
767
883
|
}
|
|
768
884
|
|
|
769
885
|
case "tail": {
|
|
770
886
|
const issue = issueArg("tail", argv[1]);
|
|
771
|
-
await tailRun(findProject(loadConfig(),
|
|
887
|
+
await tailRun(findProject(loadConfig(), projectFlag).name, issue);
|
|
772
888
|
break;
|
|
773
889
|
}
|
|
774
890
|
|
|
775
891
|
case "extend": {
|
|
776
892
|
const issue = issueArg("extend", argv[1]);
|
|
777
893
|
const maxTurns = turnsFlag(argv);
|
|
778
|
-
const project = findProject(loadConfig(),
|
|
894
|
+
const project = findProject(loadConfig(), projectFlag);
|
|
779
895
|
const daemon = livingDaemon();
|
|
780
896
|
if (daemon === undefined) throw new Error("daemon is not running");
|
|
781
897
|
if (daemon.project !== undefined && daemon.project !== project.name) {
|
|
@@ -843,7 +959,7 @@ try {
|
|
|
843
959
|
);
|
|
844
960
|
process.exit(2);
|
|
845
961
|
}
|
|
846
|
-
const project = findProject(loadConfig(),
|
|
962
|
+
const project = findProject(loadConfig(), projectFlag);
|
|
847
963
|
const daemon = livingDaemon();
|
|
848
964
|
if (daemon === undefined) throw new Error("daemon is not running");
|
|
849
965
|
if (daemon.project !== undefined && daemon.project !== project.name) {
|
|
@@ -919,7 +1035,7 @@ try {
|
|
|
919
1035
|
case "unblock": {
|
|
920
1036
|
const issue = issueArg("unblock", argv[1]);
|
|
921
1037
|
const cfg = loadConfig();
|
|
922
|
-
const project = findProject(cfg,
|
|
1038
|
+
const project = findProject(cfg, projectFlag);
|
|
923
1039
|
const store = openStore(dbPath());
|
|
924
1040
|
try {
|
|
925
1041
|
const outcome = await unblockIssue(project, makeTracker(project), store, issue, {
|
|
@@ -961,7 +1077,7 @@ try {
|
|
|
961
1077
|
process.exit(2);
|
|
962
1078
|
}
|
|
963
1079
|
const cfg = loadConfig();
|
|
964
|
-
const project = findProject(cfg,
|
|
1080
|
+
const project = findProject(cfg, projectFlag);
|
|
965
1081
|
const args: Record<string, string> = {};
|
|
966
1082
|
for (let i = 2; i < argv.length; i++) {
|
|
967
1083
|
const token = argv[i];
|
|
@@ -975,7 +1091,7 @@ try {
|
|
|
975
1091
|
args[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
976
1092
|
i++;
|
|
977
1093
|
} else if (token === "--project" || token?.startsWith("--project=") === true) {
|
|
978
|
-
// Consumed by `
|
|
1094
|
+
// Consumed by `projectFlag` above; both spellings skip here.
|
|
979
1095
|
if (token === "--project") i++;
|
|
980
1096
|
} else {
|
|
981
1097
|
process.stderr.write(
|
|
@@ -999,10 +1115,10 @@ try {
|
|
|
999
1115
|
tracker: makeTracker(project),
|
|
1000
1116
|
actions: githubVerbActions(project),
|
|
1001
1117
|
fleetStop: () =>
|
|
1002
|
-
isPaused()
|
|
1003
|
-
? "claiming is paused for this fleet (omp-conductor
|
|
1118
|
+
isPaused(project.name)
|
|
1119
|
+
? "claiming is paused for this fleet (omp-conductor hold or stop)"
|
|
1004
1120
|
: undefined,
|
|
1005
|
-
pausedAt,
|
|
1121
|
+
pausedAt: () => pausedAt(project.name),
|
|
1006
1122
|
log: (m) => process.stderr.write(`${m}\n`),
|
|
1007
1123
|
now: () => Date.now(),
|
|
1008
1124
|
chain: { readBaseChain },
|
|
@@ -1019,6 +1135,58 @@ try {
|
|
|
1019
1135
|
break;
|
|
1020
1136
|
}
|
|
1021
1137
|
|
|
1138
|
+
case "event": {
|
|
1139
|
+
if (argv[1] !== "record") {
|
|
1140
|
+
process.stderr.write("omp-conductor: event needs the record subcommand\n");
|
|
1141
|
+
process.exit(2);
|
|
1142
|
+
}
|
|
1143
|
+
const category = flag(argv, "category");
|
|
1144
|
+
const summary = flag(argv, "summary")?.replace(/\s+/g, " ").trim();
|
|
1145
|
+
const evidence = flag(argv, "evidence")?.replace(/\s+/g, " ").trim();
|
|
1146
|
+
if (
|
|
1147
|
+
category === undefined ||
|
|
1148
|
+
!/^[a-z0-9][a-z0-9-]{0,31}$/.test(category) ||
|
|
1149
|
+
summary === undefined ||
|
|
1150
|
+
summary.length === 0 ||
|
|
1151
|
+
summary.length > 240 ||
|
|
1152
|
+
summary.startsWith("--") ||
|
|
1153
|
+
evidence === undefined ||
|
|
1154
|
+
evidence.length === 0 ||
|
|
1155
|
+
evidence.length > 500 ||
|
|
1156
|
+
evidence.startsWith("--")
|
|
1157
|
+
) {
|
|
1158
|
+
process.stderr.write(
|
|
1159
|
+
"omp-conductor: event record needs --category with a lowercase slug, --summary (1-240 chars), and --evidence (1-500 chars)\n",
|
|
1160
|
+
);
|
|
1161
|
+
process.exit(2);
|
|
1162
|
+
}
|
|
1163
|
+
const rawOccurredAt = flag(argv, "occurred-at");
|
|
1164
|
+
const recordedAt = Date.now();
|
|
1165
|
+
const occurredAt = rawOccurredAt === undefined ? recordedAt : Date.parse(rawOccurredAt);
|
|
1166
|
+
if (!Number.isFinite(occurredAt)) {
|
|
1167
|
+
process.stderr.write("omp-conductor: event record --occurred-at needs an ISO timestamp\n");
|
|
1168
|
+
process.exit(2);
|
|
1169
|
+
}
|
|
1170
|
+
const project = findProject(loadConfig(), projectFlag);
|
|
1171
|
+
const store = openStore(dbPath());
|
|
1172
|
+
try {
|
|
1173
|
+
const event = store.recordMaterialEvent({
|
|
1174
|
+
project: project.name,
|
|
1175
|
+
category,
|
|
1176
|
+
summary,
|
|
1177
|
+
evidence,
|
|
1178
|
+
occurredAt,
|
|
1179
|
+
recordedAt,
|
|
1180
|
+
});
|
|
1181
|
+
process.stdout.write(
|
|
1182
|
+
`event ${event.id} recorded for ${project.name} (${event.category}, ${new Date(event.occurredAt).toISOString()}) — nothing sent\n`,
|
|
1183
|
+
);
|
|
1184
|
+
} finally {
|
|
1185
|
+
store.close();
|
|
1186
|
+
}
|
|
1187
|
+
break;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1022
1190
|
/**
|
|
1023
1191
|
* The handover point. Authorship stays with the model; from here the daemon
|
|
1024
1192
|
* owns delivery, so "I sent the report" stops being a claim the model makes
|
|
@@ -1043,7 +1211,13 @@ try {
|
|
|
1043
1211
|
process.exit(2);
|
|
1044
1212
|
}
|
|
1045
1213
|
const kind = rawKind as ReportKind;
|
|
1046
|
-
|
|
1214
|
+
if (kind !== "digest" && (argv.includes("--events") || argv.includes("--notices"))) {
|
|
1215
|
+
process.stderr.write(
|
|
1216
|
+
"omp-conductor: report --events and --notices are valid only with --kind digest\n",
|
|
1217
|
+
);
|
|
1218
|
+
process.exit(2);
|
|
1219
|
+
}
|
|
1220
|
+
const project = findProject(loadConfig(), projectFlag);
|
|
1047
1221
|
const store = openStore(dbPath());
|
|
1048
1222
|
try {
|
|
1049
1223
|
const at = Date.now();
|
|
@@ -1051,8 +1225,8 @@ try {
|
|
|
1051
1225
|
// policy defers is refused here rather than silently turning into a
|
|
1052
1226
|
// page, or a digest going out off-schedule.
|
|
1053
1227
|
const policy = project.reporting;
|
|
1228
|
+
const digestPolicy = policy?.digest ?? { cadence: "per-tick" };
|
|
1054
1229
|
if (kind === "digest") {
|
|
1055
|
-
const digestPolicy = policy?.digest ?? { cadence: "per-tick" };
|
|
1056
1230
|
if (digestPolicy.cadence === "none") {
|
|
1057
1231
|
process.stderr.write(`omp-conductor: report: digest cadence is "none" for this project\n`);
|
|
1058
1232
|
process.exit(2);
|
|
@@ -1070,32 +1244,54 @@ try {
|
|
|
1070
1244
|
}
|
|
1071
1245
|
}
|
|
1072
1246
|
if (kind === "material") {
|
|
1073
|
-
const
|
|
1074
|
-
if (
|
|
1247
|
+
const disposition = interruptDisposition(policy, "material", at);
|
|
1248
|
+
if (disposition === "digest") {
|
|
1075
1249
|
process.stderr.write(
|
|
1076
1250
|
"omp-conductor: report: material updates are digest-only under this reporting policy; fold this into the next digest (--kind digest)\n",
|
|
1077
1251
|
);
|
|
1078
1252
|
process.exit(2);
|
|
1079
1253
|
}
|
|
1254
|
+
if (disposition === "availability") {
|
|
1255
|
+
const noticeId = randomUUID();
|
|
1256
|
+
store.addHeldNotice({
|
|
1257
|
+
id: noticeId,
|
|
1258
|
+
project: project.name,
|
|
1259
|
+
category: "material",
|
|
1260
|
+
summary: body.split("\n", 1)[0]!.slice(0, 240),
|
|
1261
|
+
detail: body,
|
|
1262
|
+
createdAt: at,
|
|
1263
|
+
releaseOnAvailable: true,
|
|
1264
|
+
});
|
|
1265
|
+
process.stdout.write(
|
|
1266
|
+
`held notice ${noticeId} queued for ${project.name} (material; quiet hours)\n` +
|
|
1267
|
+
"the daemon will include it in the next digest or working-hours catch-up\n",
|
|
1268
|
+
);
|
|
1269
|
+
break;
|
|
1270
|
+
}
|
|
1080
1271
|
}
|
|
1081
|
-
const
|
|
1272
|
+
const draft = {
|
|
1082
1273
|
project: project.name,
|
|
1083
1274
|
kind,
|
|
1084
1275
|
body,
|
|
1085
|
-
// Only
|
|
1086
|
-
//
|
|
1087
|
-
...(kind === "digest"
|
|
1088
|
-
? { dedupeKey: digestDedupeKey(at,
|
|
1276
|
+
// Only a daily digest is at-most-once. Per-tick digests and material
|
|
1277
|
+
// reports describe new outcomes each time, so they carry no daily key.
|
|
1278
|
+
...(kind === "digest" && digestPolicy.cadence === "daily"
|
|
1279
|
+
? { dedupeKey: digestDedupeKey(at, digestPolicy.timezone) }
|
|
1089
1280
|
: {}),
|
|
1090
1281
|
at,
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1282
|
+
};
|
|
1283
|
+
const { report, deduped } =
|
|
1284
|
+
kind === "digest"
|
|
1285
|
+
? store.enqueueDigestReport(
|
|
1286
|
+
draft,
|
|
1287
|
+
digestIdsFlag(argv, "events"),
|
|
1288
|
+
digestIdsFlag(argv, "notices"),
|
|
1289
|
+
)
|
|
1290
|
+
: store.enqueueReport(draft);
|
|
1095
1291
|
process.stdout.write(
|
|
1096
1292
|
deduped
|
|
1097
1293
|
? `today's digest was already handed over as report ${report.id} (${report.state}) — nothing queued\n` +
|
|
1098
|
-
"the ledger decides this, not your memory of the last tick;
|
|
1294
|
+
"the ledger decides this, not your memory of the last tick; newly named rows remain owed for a later digest\n"
|
|
1099
1295
|
: `report ${report.id} queued for ${project.name} (${kind})\n` +
|
|
1100
1296
|
"the daemon owns delivery from here; omp-conductor status shows it until it lands\n",
|
|
1101
1297
|
);
|
|
@@ -1107,7 +1303,7 @@ try {
|
|
|
1107
1303
|
|
|
1108
1304
|
case "decision": {
|
|
1109
1305
|
const sub = argv[1];
|
|
1110
|
-
const project = findProject(loadConfig(),
|
|
1306
|
+
const project = findProject(loadConfig(), projectFlag);
|
|
1111
1307
|
const store = openStore(dbPath());
|
|
1112
1308
|
try {
|
|
1113
1309
|
if (sub === "open") {
|
|
@@ -1218,7 +1414,7 @@ try {
|
|
|
1218
1414
|
}
|
|
1219
1415
|
const issueText = flag(argv, "issue");
|
|
1220
1416
|
const issue = issueText === undefined ? undefined : issueArg("friction --issue", issueText);
|
|
1221
|
-
const project = findProject(loadConfig(),
|
|
1417
|
+
const project = findProject(loadConfig(), projectFlag);
|
|
1222
1418
|
const store = openStore(dbPath());
|
|
1223
1419
|
try {
|
|
1224
1420
|
store.recordFriction(project.name, {
|
|
@@ -1235,70 +1431,18 @@ try {
|
|
|
1235
1431
|
break;
|
|
1236
1432
|
}
|
|
1237
1433
|
|
|
1238
|
-
case "
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
process.stdout.write(
|
|
1249
|
-
"resumed — claiming allowed on the next tick\n" +
|
|
1250
|
-
"note: did NOT re-arm; run arm after an inbound Telegram proof to resume ticks\n",
|
|
1251
|
-
);
|
|
1252
|
-
break;
|
|
1253
|
-
|
|
1254
|
-
case "graph-setup": {
|
|
1255
|
-
// Refused rather than accommodated. Under sudo every path this command
|
|
1256
|
-
// derives — the config it loads, the state directory it writes to, the
|
|
1257
|
-
// HOME and User= it bakes into the unit — resolves as root instead of the
|
|
1258
|
-
// fleet's account, and the result is a timer that goes green while
|
|
1259
|
-
// building indexes in a store no worker session ever reads. Nothing about
|
|
1260
|
-
// that announces itself, so the only safe answer is to stop.
|
|
1261
|
-
if (process.env["SUDO_USER"] !== undefined) {
|
|
1262
|
-
process.stderr.write(
|
|
1263
|
-
"omp-conductor: run graph-setup as the account the fleet runs as, not under sudo.\n" +
|
|
1264
|
-
`Under sudo the config, ~/.cache and the unit's User= all resolve as root, and the\n` +
|
|
1265
|
-
`indexes land where no worker can read them. Only installing the units needs root,\n` +
|
|
1266
|
-
"and this command prints those two lines for you at the end.\n",
|
|
1267
|
-
);
|
|
1268
|
-
process.exit(1);
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
const project = findProject(loadConfig(), flag(argv, "project"));
|
|
1272
|
-
if (graphRepos(project).length === 0) {
|
|
1273
|
-
// Not a warning: with nothing configured there is nothing to print, and
|
|
1274
|
-
// the fix is a wizard answer rather than a flag on this command.
|
|
1275
|
-
process.stderr.write(
|
|
1276
|
-
`omp-conductor: no repo in project "${project.name}" has graphProject set — re-run\n` +
|
|
1277
|
-
"/conductor setup and say yes to code-graph discovery.\n",
|
|
1278
|
-
);
|
|
1279
|
-
process.exit(1);
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
if (!argv.includes("--write")) {
|
|
1283
|
-
process.stdout.write(`${formatGraphSetup(project)}\n`);
|
|
1284
|
-
break;
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
let result: GraphSetupWrite;
|
|
1288
|
-
try {
|
|
1289
|
-
result = writeGraphSetup(project);
|
|
1290
|
-
} catch (err) {
|
|
1291
|
-
// No longer the permissions case — all three files go to this account's
|
|
1292
|
-
// own state directory — so this is a full disk, a read-only mount or a
|
|
1293
|
-
// state directory someone else owns. Say what failed and offer the
|
|
1294
|
-
// printed plan, which is a complete substitute for the write.
|
|
1295
|
-
process.stderr.write(
|
|
1296
|
-
`omp-conductor: could not stage the files (${err instanceof Error ? err.message : String(err)}).\n` +
|
|
1297
|
-
"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",
|
|
1298
1444
|
);
|
|
1299
|
-
process.exit(1);
|
|
1300
1445
|
}
|
|
1301
|
-
process.stdout.write(`wrote:\n${result.written.map((f) => ` ${f}`).join("\n")}\n\n${result.next}\n`);
|
|
1302
1446
|
break;
|
|
1303
1447
|
}
|
|
1304
1448
|
|
|
@@ -1307,12 +1451,12 @@ try {
|
|
|
1307
1451
|
let project: ProjectConfig | undefined;
|
|
1308
1452
|
let path: string;
|
|
1309
1453
|
if (override === undefined) {
|
|
1310
|
-
project = findProject(loadConfig(),
|
|
1454
|
+
project = findProject(loadConfig(), projectFlag);
|
|
1311
1455
|
path = briefPathForProject(project);
|
|
1312
1456
|
} else {
|
|
1313
1457
|
path = override;
|
|
1314
1458
|
try {
|
|
1315
|
-
project = findProject(loadConfig(),
|
|
1459
|
+
project = findProject(loadConfig(), projectFlag);
|
|
1316
1460
|
} catch {
|
|
1317
1461
|
project = undefined;
|
|
1318
1462
|
}
|
|
@@ -1437,7 +1581,7 @@ try {
|
|
|
1437
1581
|
} catch {
|
|
1438
1582
|
process.stderr.write(
|
|
1439
1583
|
`omp-conductor: no brief at ${path}` +
|
|
1440
|
-
`${override === undefined ? " — run
|
|
1584
|
+
`${override === undefined ? " — run `omp-conductor setup brief` and say yes to writing ORCHESTRATOR.md." : "."}\n`,
|
|
1441
1585
|
);
|
|
1442
1586
|
process.exit(1);
|
|
1443
1587
|
}
|