omp-conductor 0.18.1 → 0.19.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 +106 -41
- package/REFERENCE.md +866 -31
- package/agents/to-spec.md +6 -2
- package/package.json +1 -1
- package/schema/config.schema.json +32 -1
- package/src/admission.ts +212 -26
- package/src/arm-challenge.ts +250 -57
- package/src/ask.ts +288 -1
- package/src/briefs/orchestrator.md +27 -13
- package/src/briefs/to-spec.md +6 -2
- package/src/cli.ts +127 -2
- package/src/command-help.ts +9 -1
- package/src/command-manifest.ts +52 -8
- package/src/commands/arm.ts +6 -2
- package/src/commands/context.ts +2 -0
- package/src/commands/intake.ts +4 -19
- package/src/commands/message.ts +26 -2
- package/src/commands/reconcile-units.ts +104 -0
- package/src/commands/release-composition.ts +232 -0
- package/src/commands/resume.ts +2 -27
- package/src/commands/setup.ts +101 -16
- package/src/commands/stats.ts +11 -30
- package/src/commands/tail.ts +31 -1
- package/src/commands/upgrade.ts +20 -3
- package/src/commands/verb.ts +2 -1
- package/src/commands/watch.ts +4 -17
- package/src/config-schema.ts +38 -6
- package/src/config.ts +103 -8
- package/src/credential-class.ts +366 -0
- package/src/daemon.ts +1368 -529
- package/src/dashboard/app.js +504 -2
- package/src/dashboard/controls.ts +336 -0
- package/src/dashboard/index.html +30 -0
- package/src/dashboard/server.ts +271 -30
- package/src/dashboard/style.css +116 -0
- package/src/dashboard/transcript.ts +173 -0
- package/src/decisions.ts +19 -11
- package/src/doctor.ts +431 -148
- package/src/escalate.ts +22 -11
- package/src/failure-class.ts +59 -0
- package/src/fleet.ts +587 -230
- package/src/host.ts +6 -455
- package/src/omp-settings.ts +19 -0
- package/src/omp.ts +40 -56
- package/src/orchestrator-tick.ts +564 -121
- package/src/pause.ts +233 -0
- package/src/session-host.ts +6 -41
- package/src/settlement.ts +159 -2
- package/src/setup-answers.ts +97 -0
- package/src/setup-host.ts +343 -1160
- package/src/setup-install.ts +204 -27
- package/src/setup-wizard.ts +252 -51
- package/src/setup.ts +87 -4
- package/src/spend-telemetry.ts +117 -0
- package/src/stats.ts +35 -0
- package/src/status-render.ts +485 -19
- package/src/store.ts +1229 -55
- package/src/telegram-freshness.ts +269 -0
- package/src/to-spec.ts +50 -2
- package/src/types.ts +759 -10
- package/src/unblock.ts +22 -0
- package/src/unit-reconcile.ts +303 -0
- package/src/upgrade-verify.ts +8 -1
- package/src/upgrade.ts +299 -12
- package/src/verbs/actions.ts +124 -10
- package/src/verbs/protocol.ts +70 -2
- package/src/verbs/server.ts +485 -11
- package/src/wake.ts +48 -0
- package/src/worker.ts +401 -14
|
@@ -238,11 +238,12 @@ Keep the queue worth draining.
|
|
|
238
238
|
When the queue is below the grooming trigger, the tick's **bounded to-spec
|
|
239
239
|
batch** block (#777) carries the mechanical contract: it names the launch
|
|
240
240
|
token, the candidates the conductor selected mechanically from the live
|
|
241
|
-
open-issue snapshot (parked, parent/epic, already-groomed,
|
|
242
|
-
lane/dependency-blocked and dispatched
|
|
243
|
-
by your judgement), the exclusions that
|
|
244
|
-
shape. Do exactly what it says — those
|
|
245
|
-
|
|
241
|
+
open-issue snapshot (parked, parent/epic, already-groomed, refused inside
|
|
242
|
+
their retry cooldown, in-flight, lane/dependency-blocked and dispatched
|
|
243
|
+
issues were excluded by code, never by your judgement), the exclusions that
|
|
244
|
+
selection applied, and the item shape. Do exactly what it says — those
|
|
245
|
+
candidates are the only batch this tick authorizes: render
|
|
246
|
+
`omp/src/briefs/to-spec.md` for each of them with
|
|
246
247
|
the current source head, launch exactly one `task` batch through the
|
|
247
248
|
`to-spec` agent with the listed items and no substitutes, and persist every
|
|
248
249
|
returned result with the `conductor_to_spec_result` tool. The `tool_call`
|
|
@@ -254,18 +255,31 @@ Keep the queue worth draining.
|
|
|
254
255
|
source/ref/freshness is refused as blocked, never groomed. The quality bar
|
|
255
256
|
above does not move.
|
|
256
257
|
- **Every to-spec agent answers the same return contract**, or its output is
|
|
257
|
-
refused: the strict schema carries
|
|
258
|
-
|
|
258
|
+
refused: the strict schema carries exactly these fields — the generated task
|
|
259
|
+
schema and the persistence parser are that one contract, so an extra or
|
|
260
|
+
missing field is refused as malformed:
|
|
261
|
+
- `verdict` — `ALREADY DONE` / `PROMOTABLE` / `NEEDS DECOMPOSITION` / `BLOCKED` /
|
|
259
262
|
`NEEDS PRODUCT DECISION`
|
|
260
|
-
- routing — exactly one repo
|
|
261
|
-
-
|
|
262
|
-
-
|
|
263
|
-
- existing tests covering the behaviour, by path
|
|
264
|
-
- the one thing most likely to be silently faked
|
|
265
|
-
- source — where the code was read: the clone/ref and how fresh it is. A
|
|
263
|
+
- `routing` — exactly one `owner/repo`, or `MULTI`
|
|
264
|
+
- `routingSplit` — required iff `routing` is `MULTI`: what each slice goes to
|
|
265
|
+
- `source` — where the code was read: the clone/ref and how fresh it is. A
|
|
266
266
|
scout that cannot reach a source it trusts returns `BLOCKED` and says so;
|
|
267
267
|
silent fallback to an unnamed source is the failure mode of delegated
|
|
268
268
|
research — stale evidence reads exactly like good evidence.
|
|
269
|
+
- `evidence` — the files/symbols proving the verdict; required for
|
|
270
|
+
`ALREADY DONE` (the file or symbol that already does the work, never a
|
|
271
|
+
title match)
|
|
272
|
+
- `laterWorkInvalidates` — whether later work retired the premise
|
|
273
|
+
- `laterWorkNote` — what that check searched and found
|
|
274
|
+
- `entryPoints` — the 3-6 files to change or read first
|
|
275
|
+
- `existingTests` — existing tests covering the behaviour, by path
|
|
276
|
+
- `likelySilentFake` — the one thing most likely to be silently faked
|
|
277
|
+
- `proofCommands` — the focused commands that prove the work
|
|
278
|
+
- `fileLane` — the files and directories this slice writes
|
|
279
|
+
- `dependencies` — open prerequisite issue numbers, bare (`875`) or string
|
|
280
|
+
(`"875"`); `[]` when none
|
|
281
|
+
- `proposedBrief` — required iff `verdict` is `PROMOTABLE`
|
|
282
|
+
- `reasonNotToPromote` — required for every other verdict
|
|
269
283
|
- **Disqualifying an issue is a successful grooming outcome.** Measured on this
|
|
270
284
|
package's own fleet: four scouts over sixteen backlog issues promoted four and
|
|
271
285
|
*disqualified six* that looked promotable from their titles — four written
|
package/src/briefs/to-spec.md
CHANGED
|
@@ -44,6 +44,9 @@ required field and no extra keys. The fields are:
|
|
|
44
44
|
the exact ref, and `freshAt` = epoch milliseconds when you actually observed
|
|
45
45
|
it. Conductor refuses results whose source is older than 24 hours or missing
|
|
46
46
|
name/ref/freshAt — an unsourced verdict is not grooming, it is prose.
|
|
47
|
+
- `evidence` — the files/symbols that prove the verdict. Required for
|
|
48
|
+
`ALREADY DONE`: name the symbol/file that already does the work, never a
|
|
49
|
+
title match. Welcome on every other verdict.
|
|
47
50
|
- `laterWorkInvalidates` — boolean: did later work (an epic or issue committed
|
|
48
51
|
after this candidate was filed) retire its premise?
|
|
49
52
|
- `laterWorkNote` — what you searched for that check and what you found. Even
|
|
@@ -57,8 +60,9 @@ required field and no extra keys. The fields are:
|
|
|
57
60
|
implementing, and how to prove it is not.
|
|
58
61
|
- `proofCommands` — the focused commands that prove the work, each with its
|
|
59
62
|
`cwd` when it matters.
|
|
60
|
-
- `fileLane` — the files and directories this slice writes
|
|
61
|
-
|
|
63
|
+
- `fileLane` — the files and directories this slice writes.
|
|
64
|
+
- `dependencies` — open prerequisite issue numbers, each a bare number
|
|
65
|
+
(`875`) or a string (`"875"`); `[]` when none.
|
|
62
66
|
- `proposedBrief` — required iff `verdict` is `PROMOTABLE`: the brief a worker
|
|
63
67
|
would be dispatched with, including the silent fake and the proof commands.
|
|
64
68
|
- `reasonNotToPromote` — required for every other verdict: why this must not
|
package/src/cli.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { userInfo } from "node:os";
|
|
11
11
|
import { loadConfig } from "./config.ts";
|
|
12
|
-
import { COMMAND_MANIFEST, renderUsage } from "./command-manifest.ts";
|
|
12
|
+
import { COMMAND_MANIFEST, renderUsage, type CommandManifestEntry } from "./command-manifest.ts";
|
|
13
13
|
import { armCommand } from "./commands/arm.ts";
|
|
14
14
|
import { boardCommand } from "./commands/board.ts";
|
|
15
15
|
import { briefUpgradeCommand } from "./commands/brief-upgrade.ts";
|
|
@@ -28,6 +28,7 @@ import { intakeCommand } from "./commands/intake.ts";
|
|
|
28
28
|
import { ledgerCommand } from "./commands/ledger.ts";
|
|
29
29
|
import { messageCommand } from "./commands/message.ts";
|
|
30
30
|
import { reportCommand } from "./commands/report.ts";
|
|
31
|
+
import { releaseCompositionCommand } from "./commands/release-composition.ts";
|
|
31
32
|
import { restartCommand } from "./commands/restart.ts";
|
|
32
33
|
import { restoreDbCommand } from "./commands/restore-db.ts";
|
|
33
34
|
import { resumeCommand } from "./commands/resume.ts";
|
|
@@ -40,6 +41,7 @@ import { tailCommand } from "./commands/tail.ts";
|
|
|
40
41
|
import { unblockCommand } from "./commands/unblock.ts";
|
|
41
42
|
import { unfreezeCommand } from "./commands/unfreeze.ts";
|
|
42
43
|
import { upgradeInstallCommand } from "./commands/upgrade-install.ts";
|
|
44
|
+
import { reconcileUnitsCommand } from "./commands/reconcile-units.ts";
|
|
43
45
|
import { upgradeRollbackCommand } from "./commands/upgrade-rollback.ts";
|
|
44
46
|
import { upgradeCommand } from "./commands/upgrade.ts";
|
|
45
47
|
import { verbCommand } from "./commands/verb.ts";
|
|
@@ -55,6 +57,104 @@ import {
|
|
|
55
57
|
|
|
56
58
|
const USAGE = renderUsage(COMMAND_MANIFEST);
|
|
57
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Verbs that define every `--help`/`-h` shape themselves, so the dispatch
|
|
62
|
+
* gate below must not pre-empt them: `help` and the version aliases print
|
|
63
|
+
* their own text, `setup` accepts help only as the first trailing token
|
|
64
|
+
* (`setup bogus --help` must reject `bogus`, never print a help read), and
|
|
65
|
+
* the hand-rolled usage verbs print help at `argv[1]` or refuse a trailing
|
|
66
|
+
* unknown token with exit 2 — every one of them already before any side
|
|
67
|
+
* effect. `intake` and `watch` are deliberately absent: they validate their
|
|
68
|
+
* tails only after `loadConfig`/`openStore`, so an unconsumed trailing help
|
|
69
|
+
* must be answered by the gate before the handler can create the store
|
|
70
|
+
* (#863).
|
|
71
|
+
*/
|
|
72
|
+
const HELP_OWNED_VERBS: Record<string, true> = {
|
|
73
|
+
"--help": true,
|
|
74
|
+
"-h": true,
|
|
75
|
+
help: true,
|
|
76
|
+
"--version": true,
|
|
77
|
+
"-V": true,
|
|
78
|
+
version: true,
|
|
79
|
+
setup: true,
|
|
80
|
+
dashboard: true,
|
|
81
|
+
doctor: true,
|
|
82
|
+
stats: true,
|
|
83
|
+
drain: true,
|
|
84
|
+
"restore-db": true,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* `setup`'s install subcommands — the one `setup` shape the wizard does not
|
|
89
|
+
* safely own: help is recognized only at the first trailing token, so
|
|
90
|
+
* `setup host --help` would reach the privileged install planner (creating
|
|
91
|
+
* the state db and staging the host install) instead of printing help. The
|
|
92
|
+
* dispatch gate answers real help requests in their tail instead.
|
|
93
|
+
*/
|
|
94
|
+
const SETUP_INSTALL_SUBCOMMANDS: Record<string, true> = {
|
|
95
|
+
host: true,
|
|
96
|
+
graph: true,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Whether an invocation carries an actual `--help`/`-h` request — a token the
|
|
101
|
+
* verb's own grammar does not consume. A token immediately after a
|
|
102
|
+
* value-taking flag is that flag's value (`report --text --help` sends the
|
|
103
|
+
* literal text "--help", exactly as before), and a bare `--` makes everything
|
|
104
|
+
* after it positional; only an unconsumed help token is really a request.
|
|
105
|
+
*/
|
|
106
|
+
function helpRequested(argv: readonly string[], entry: CommandManifestEntry): boolean {
|
|
107
|
+
let consumeNext = false;
|
|
108
|
+
for (const token of argv.slice(1)) {
|
|
109
|
+
if (consumeNext) {
|
|
110
|
+
consumeNext = false;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (token === "--") {
|
|
114
|
+
// A bare `--` ends option parsing for a verb whose manifest actually
|
|
115
|
+
// declares positionals: after it, `--help` is positional data, not a
|
|
116
|
+
// request. A flag-only verb has no positional grammar for `--` to
|
|
117
|
+
// protect, so `stop --all -- --help` (#863) must stay a help read.
|
|
118
|
+
if ((entry.positionals?.length ?? 0) > 0) return false;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (token === "--help" || token === "-h") return true;
|
|
122
|
+
// `--flag VALUE` consumes the next token as the flag's value, but
|
|
123
|
+
// `--flag=VALUE` binds the value inline — so only the split form can
|
|
124
|
+
// swallow a following `--help`. `stop --project=conductor --help` is a
|
|
125
|
+
// help read (#863), and `--project conductor --help` must read the next
|
|
126
|
+
// token as the value, exactly as `report --text --help` sends literal
|
|
127
|
+
// text. A token without `=` is a separate argument: when it names a
|
|
128
|
+
// value-taking flag, its value is the next token.
|
|
129
|
+
consumeNext =
|
|
130
|
+
token.startsWith("--") &&
|
|
131
|
+
!token.includes("=") &&
|
|
132
|
+
entry.flags.some((flag) => flag.name === token && flag.takesValue);
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* One verb's usage block from the manifest — what `VERB --help` prints. The
|
|
139
|
+
* manifest's `details` is that verb's long-form semantics, and rendering it
|
|
140
|
+
* here is what keeps the generated read complete: the dispatch gate above
|
|
141
|
+
* answers help for every verb that does not own it, so a verb whose prose
|
|
142
|
+
* lived only in its own hand-rolled usage string would silently lose it
|
|
143
|
+
* (#863 — `watch`'s no-answer/auto-withdraw semantics and `intake`'s
|
|
144
|
+
* durability/groomed no-op semantics).
|
|
145
|
+
*/
|
|
146
|
+
function renderCommandHelp(entry: CommandManifestEntry): string {
|
|
147
|
+
const lines = [`omp-conductor ${entry.name} — ${entry.description}`, "", "usage:"];
|
|
148
|
+
for (const usage of entry.usage) lines.push(` omp-conductor ${usage}`);
|
|
149
|
+
if (entry.flags.length > 0) {
|
|
150
|
+
lines.push("", "flags:");
|
|
151
|
+
const width = Math.max(...entry.flags.map((flag) => flag.name.length));
|
|
152
|
+
for (const flag of entry.flags) lines.push(` ${flag.name.padEnd(width)} ${flag.description}`);
|
|
153
|
+
}
|
|
154
|
+
if (entry.details !== undefined) lines.push("", entry.details);
|
|
155
|
+
return lines.join("\n");
|
|
156
|
+
}
|
|
157
|
+
|
|
58
158
|
/** Applies both `--port 9000` and `--port=9000`; returns undefined when absent. */
|
|
59
159
|
function flag(argv: string[], name: string): string | undefined {
|
|
60
160
|
const i = argv.indexOf(`--${name}`);
|
|
@@ -167,6 +267,7 @@ export function commandHandlers(ctx: CommandContext): Record<string, CommandHand
|
|
|
167
267
|
setup: () => setupCommand(ctx),
|
|
168
268
|
upgrade: () => upgradeCommand(ctx),
|
|
169
269
|
"upgrade-install": () => upgradeInstallCommand(ctx),
|
|
270
|
+
"reconcile-units": () => reconcileUnitsCommand(ctx),
|
|
170
271
|
"upgrade-rollback": () => upgradeRollbackCommand(ctx),
|
|
171
272
|
daemon: () => daemonCommand(ctx),
|
|
172
273
|
start: () => startCommand(ctx),
|
|
@@ -186,8 +287,9 @@ export function commandHandlers(ctx: CommandContext): Record<string, CommandHand
|
|
|
186
287
|
extend: () => extendCommand(ctx),
|
|
187
288
|
worker: () => workerCommand(ctx),
|
|
188
289
|
unblock: () => unblockCommand(ctx),
|
|
189
|
-
|
|
290
|
+
"release-composition": () => releaseCompositionCommand(ctx),
|
|
190
291
|
verb: () => verbCommand(ctx),
|
|
292
|
+
unfreeze: () => unfreezeCommand(ctx),
|
|
191
293
|
event: () => eventCommand(ctx),
|
|
192
294
|
report: () => reportCommand(ctx),
|
|
193
295
|
message: () => messageCommand(ctx),
|
|
@@ -254,6 +356,29 @@ export async function runCli(argv: string[] = process.argv.slice(2)): Promise<vo
|
|
|
254
356
|
);
|
|
255
357
|
process.exit(2);
|
|
256
358
|
}
|
|
359
|
+
// A real `--help`/`-h` (one no declared flag consumes) is a help read and
|
|
360
|
+
// must never reach a mutating command: `stop --all --help` is how #863
|
|
361
|
+
// disarmed the fleet. Recognized here — before project-selection
|
|
362
|
+
// (`targetProjects` is a lazy closure; no config is loaded) and before
|
|
363
|
+
// handler execution — so a verb that does not validate its own tail can
|
|
364
|
+
// no longer treat the request as an argument.
|
|
365
|
+
const entry = cmd === undefined ? undefined : COMMAND_MANIFEST.find((entry) => entry.name === cmd);
|
|
366
|
+
let help = false;
|
|
367
|
+
if (entry !== undefined) {
|
|
368
|
+
if (cmd === "setup") {
|
|
369
|
+
// `setup` owns its help (only at the first trailing token, which is
|
|
370
|
+
// what keeps `setup bogus --help` rejecting `bogus`). The install
|
|
371
|
+
// subcommands are the exception: a help token in their tail must be
|
|
372
|
+
// answered here, before `loadConfig` and the install plan (#863).
|
|
373
|
+
help = SETUP_INSTALL_SUBCOMMANDS[argv[1] ?? ""] === true && helpRequested(argv, entry);
|
|
374
|
+
} else if (HELP_OWNED_VERBS[cmd ?? ""] !== true) {
|
|
375
|
+
help = helpRequested(argv, entry);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (help && entry !== undefined) {
|
|
379
|
+
process.stdout.write(`${renderCommandHelp(entry)}\n`);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
257
382
|
// A "none"-scope verb does no per-project work, so --project cannot change
|
|
258
383
|
// what it does: refuse it with a reason instead of silently ignoring it.
|
|
259
384
|
if (COMMAND_SCOPES[cmd ?? ""] === "none" && projectFlag !== undefined) {
|
package/src/command-help.ts
CHANGED
|
@@ -10,6 +10,11 @@ export const COMMAND_DETAILS = ` setup interview, then write config.json, th
|
|
|
10
10
|
brief as one pinned release. Pauses only new claims, drains live
|
|
11
11
|
workers, reloads, verifies twice, and restores the prior dispatch
|
|
12
12
|
state. Run it from a shell outside the target Herdr session.
|
|
13
|
+
--bootstrap SHA --source PATH installs an exact commit instead of a
|
|
14
|
+
published version, for when the installed conductor is what is
|
|
15
|
+
broken: the source must be a checkout AT that sha, the package's own
|
|
16
|
+
checks run there first, and a check failure refuses rather than
|
|
17
|
+
installing a partly verified tree.
|
|
13
18
|
upgrade-install
|
|
14
19
|
the detached executor half of the fleet-installs-itself request:
|
|
15
20
|
the same transaction, journaled per surface, leaving verification
|
|
@@ -207,7 +212,10 @@ export const COMMAND_DETAILS = ` setup interview, then write config.json, th
|
|
|
207
212
|
re-stage the systemd unit and run the install behind one confirm.
|
|
208
213
|
The units are host-global; NAME (or --project NAME) says which
|
|
209
214
|
project's per-project tail (tick config, brief link) to write, and
|
|
210
|
-
is required on a host with several configured projects.
|
|
215
|
+
is required on a host with several configured projects. On a Herdr
|
|
216
|
+
host it restarts the session unit, so it must be run from a shell
|
|
217
|
+
outside that session: from a pane inside it, it refuses before
|
|
218
|
+
staging or installing anything.
|
|
211
219
|
setup graph
|
|
212
220
|
set up the code-graph indexes workers query instead of grepping, end
|
|
213
221
|
to end: check prerequisites, clone any missing index-only clone as
|
package/src/command-manifest.ts
CHANGED
|
@@ -59,13 +59,15 @@ export const COMMAND_MANIFEST: readonly CommandManifestEntry[] = [
|
|
|
59
59
|
description: "interview, configure the fleet, or install host and graph surfaces",
|
|
60
60
|
scope: "project",
|
|
61
61
|
usage: [
|
|
62
|
-
"setup [area] [--no-ai] [--answers FILE] [--save-answers FILE] [--project NAME]",
|
|
62
|
+
"setup [area] [--no-ai] [--answers FILE] [--save-answers FILE] [--resume FILE] [--project NAME]",
|
|
63
63
|
"setup host [NAME] [--project NAME]",
|
|
64
64
|
"setup graph [--no-seed] [--print] [--project NAME]",
|
|
65
65
|
],
|
|
66
|
-
details: COMMAND_DETAILS,
|
|
67
66
|
subcommands: [
|
|
68
|
-
{
|
|
67
|
+
{
|
|
68
|
+
name: "host",
|
|
69
|
+
description: "stage and install the host services (run it from a shell outside the Herdr session it restarts)",
|
|
70
|
+
},
|
|
69
71
|
{ name: "graph", description: "install and seed code-graph indexes" },
|
|
70
72
|
],
|
|
71
73
|
flags: [
|
|
@@ -118,7 +120,7 @@ export const COMMAND_MANIFEST: readonly CommandManifestEntry[] = [
|
|
|
118
120
|
name: "upgrade",
|
|
119
121
|
description: "upgrade all installed conductor surfaces as one transaction",
|
|
120
122
|
scope: "host",
|
|
121
|
-
usage: ["upgrade [--to VERSION] [--project NAME]"],
|
|
123
|
+
usage: ["upgrade [--to VERSION] [--bootstrap SHA --source PATH] [--project NAME]"],
|
|
122
124
|
flags: [value("--to", "package version to install"), project()],
|
|
123
125
|
},
|
|
124
126
|
{
|
|
@@ -135,6 +137,18 @@ export const COMMAND_MANIFEST: readonly CommandManifestEntry[] = [
|
|
|
135
137
|
usage: ["upgrade-rollback"],
|
|
136
138
|
flags: [],
|
|
137
139
|
},
|
|
140
|
+
{
|
|
141
|
+
name: "reconcile-units",
|
|
142
|
+
description: "install the host unit files this version renders (units only)",
|
|
143
|
+
scope: "host",
|
|
144
|
+
usage: ["reconcile-units [--yes] [--dry-run] [--protect PATH]... [--project NAME]"],
|
|
145
|
+
flags: [
|
|
146
|
+
toggle("--yes", "skip the confirm (the upgrade transaction's own consent)"),
|
|
147
|
+
toggle("--dry-run", "print what would be installed and run nothing"),
|
|
148
|
+
value("--protect", "a destination to leave alone (repeatable)"),
|
|
149
|
+
project(),
|
|
150
|
+
],
|
|
151
|
+
},
|
|
138
152
|
{
|
|
139
153
|
name: "board",
|
|
140
154
|
description: "open the live keyboard-driven fleet board",
|
|
@@ -293,6 +307,24 @@ export const COMMAND_MANIFEST: readonly CommandManifestEntry[] = [
|
|
|
293
307
|
flags: [value("--reason", "reason for clearing the freeze"), project()],
|
|
294
308
|
positionals: [{ name: "repo" }],
|
|
295
309
|
},
|
|
310
|
+
{
|
|
311
|
+
name: "release-composition",
|
|
312
|
+
description: "declare, override, complete or cancel the active release",
|
|
313
|
+
scope: "project",
|
|
314
|
+
usage: [
|
|
315
|
+
"release-composition declare --campaign ID [--pr URL ...] [--reason TEXT] [--project NAME]",
|
|
316
|
+
"release-composition override --pr URL [--reason TEXT] [--project NAME]",
|
|
317
|
+
"release-composition complete [--reason TEXT] [--project NAME]",
|
|
318
|
+
"release-composition cancel [--reason TEXT] [--project NAME]",
|
|
319
|
+
"release-composition status [--project NAME]",
|
|
320
|
+
],
|
|
321
|
+
flags: [
|
|
322
|
+
value("--campaign", "campaign identifier for the release being assembled"),
|
|
323
|
+
value("--pr", "full PR URL allowed into (or overridden into) the release"),
|
|
324
|
+
value("--reason", "reason recorded with the transition"),
|
|
325
|
+
project(),
|
|
326
|
+
],
|
|
327
|
+
},
|
|
296
328
|
{
|
|
297
329
|
name: "verb",
|
|
298
330
|
description: "run a gated conductor_* verb as the orchestrator",
|
|
@@ -418,6 +450,11 @@ export const COMMAND_MANIFEST: readonly CommandManifestEntry[] = [
|
|
|
418
450
|
"watch list [--project NAME] [--json]",
|
|
419
451
|
"watch withdraw <id> [--reason TEXT] [--project NAME]",
|
|
420
452
|
],
|
|
453
|
+
details: `add records a row the daemon checks for you and the next tick reads, with no
|
|
454
|
+
operator answer needed. list shows open watches, oldest first. withdraw ends
|
|
455
|
+
one with a recorded reason — the verb that creates a watch is the verb that
|
|
456
|
+
ends it. A watch whose PR condition can no longer be observed (the PR merged
|
|
457
|
+
or closed first) is withdrawn by the daemon itself.`,
|
|
421
458
|
subcommands: [
|
|
422
459
|
{ name: "add", description: "record a watch" },
|
|
423
460
|
{ name: "list", description: "list open watches" },
|
|
@@ -443,6 +480,12 @@ export const COMMAND_MANIFEST: readonly CommandManifestEntry[] = [
|
|
|
443
480
|
"intake dismiss <id> [--project NAME]",
|
|
444
481
|
"intake groomed <id> --issue <url> [--project NAME]",
|
|
445
482
|
],
|
|
483
|
+
details: `Captures one raw idea into the local store and prints its id. list shows what
|
|
484
|
+
is still pending (id, age, text), oldest first; dismiss drops one by id. The
|
|
485
|
+
capture is durable — it lives in the sqlite store, not in a session — so it
|
|
486
|
+
survives daemon restarts. The orchestrator files the idea as an issue and then
|
|
487
|
+
marks that provenance with groomed: an id already resolved is a no-op with a
|
|
488
|
+
message, never an error, because ticks retry.`,
|
|
446
489
|
subcommands: [
|
|
447
490
|
{ name: "list", description: "list pending intake items" },
|
|
448
491
|
{ name: "dismiss", description: "dismiss an intake item" },
|
|
@@ -456,7 +499,6 @@ export const COMMAND_MANIFEST: readonly CommandManifestEntry[] = [
|
|
|
456
499
|
description: "print command usage (also --help, -h)",
|
|
457
500
|
scope: "none",
|
|
458
501
|
usage: ["help"],
|
|
459
|
-
details: COMMAND_HELP_TAIL,
|
|
460
502
|
flags: [],
|
|
461
503
|
},
|
|
462
504
|
{
|
|
@@ -480,9 +522,11 @@ export function renderUsage(manifest: readonly CommandManifestEntry[] = COMMAND_
|
|
|
480
522
|
const commands = manifest.map(
|
|
481
523
|
(command) => ` ${command.name.padEnd(width)} ${command.description}`,
|
|
482
524
|
);
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
525
|
+
// The fleet-wide operator reference, printed once. It is *not* harvested
|
|
526
|
+
// from `details`: that field is one verb's own long-form help, which
|
|
527
|
+
// `VERB --help` renders, so collecting it here would print `setup`'s
|
|
528
|
+
// carrier copy of this very blob and repeat each verb's prose twice.
|
|
529
|
+
const details = [...COMMAND_DETAILS.split("\n"), "", ...COMMAND_HELP_TAIL.split("\n"), ""];
|
|
486
530
|
return [
|
|
487
531
|
"omp-conductor — dispatch ready issues to omp coding sessions",
|
|
488
532
|
"",
|
package/src/commands/arm.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `arm` — proof-gated: send a Telegram challenge and write the arm marker only after the
|
|
2
|
+
* `arm` — proof-gated: send a Telegram challenge and write the arm marker only after the orchestrator's inbound adapter acknowledges the reply in conductor state (#614).
|
|
3
3
|
*
|
|
4
4
|
* Moved out of cli.ts's switch by the per-verb module split (#462);
|
|
5
5
|
* only the case wrapper, the injected `ctx` lookups and the imports
|
|
@@ -15,10 +15,14 @@ for (const project of ctx.targetProjects()) {
|
|
|
15
15
|
// Proof-neutral wording: `claim-only` performs no Telegram send, so the
|
|
16
16
|
// progress line cannot promise a challenge that never goes out (#613). The
|
|
17
17
|
// result line names the proof that actually armed it.
|
|
18
|
+
// The pending-proof heartbeat goes straight to stdout (#861): a challenge
|
|
19
|
+
// waits on a human for up to five minutes, and `withProgress`'s single step
|
|
20
|
+
// line cannot say how much of that window is left. Written unconditionally —
|
|
21
|
+
// a piped or plain-UI run needs it more than a TTY does, not less.
|
|
18
22
|
const r = await withProgress(
|
|
19
23
|
"arm: verifying the arming proof…",
|
|
20
24
|
"Arming proof verified",
|
|
21
|
-
() => armTicks(project.name),
|
|
25
|
+
() => armTicks(project.name, { progress: (line) => process.stdout.write(`${line}\n`) }),
|
|
22
26
|
{ plainMessage: true },
|
|
23
27
|
);
|
|
24
28
|
process.stdout.write(
|
package/src/commands/context.ts
CHANGED
|
@@ -89,6 +89,7 @@ export const COMMAND_SCOPES: Readonly<Record<string, CommandScope>> = {
|
|
|
89
89
|
intake: "project",
|
|
90
90
|
ledger: "project",
|
|
91
91
|
message: "project",
|
|
92
|
+
"release-composition": "project",
|
|
92
93
|
report: "project",
|
|
93
94
|
setup: "project", // the wizard and `setup graph` configure one project
|
|
94
95
|
stats: "project",
|
|
@@ -106,6 +107,7 @@ export const COMMAND_SCOPES: Readonly<Record<string, CommandScope>> = {
|
|
|
106
107
|
upgrade: "host",
|
|
107
108
|
"upgrade-install": "host",
|
|
108
109
|
"upgrade-rollback": "host",
|
|
110
|
+
"reconcile-units": "host",
|
|
109
111
|
// fleet — every project by default, --project narrows
|
|
110
112
|
arm: "fleet",
|
|
111
113
|
board: "fleet",
|
package/src/commands/intake.ts
CHANGED
|
@@ -12,21 +12,6 @@ import type { CommandContext } from "./context.ts";
|
|
|
12
12
|
import { findProject, loadConfig } from "../config.ts";
|
|
13
13
|
import { dbPath, openStore } from "../store.ts";
|
|
14
14
|
|
|
15
|
-
const INTAKE_USAGE = `omp-conductor intake — capture raw ideas durably.
|
|
16
|
-
|
|
17
|
-
usage:
|
|
18
|
-
omp-conductor intake "<text>" [--project NAME]
|
|
19
|
-
omp-conductor intake list [--project NAME]
|
|
20
|
-
omp-conductor intake dismiss <id> [--project NAME]
|
|
21
|
-
omp-conductor intake groomed <id> --issue <url> [--project NAME]
|
|
22
|
-
|
|
23
|
-
Captures one raw idea into the local store and prints its id. list shows what
|
|
24
|
-
is still pending (id, age, text), oldest first; dismiss drops one by id. The
|
|
25
|
-
capture is durable — it lives in the sqlite store, not in a session — so it
|
|
26
|
-
survives daemon restarts. The orchestrator files the idea as an issue and then
|
|
27
|
-
marks that provenance with groomed: an id already resolved is a no-op with a
|
|
28
|
-
message, never an error, because ticks retry.`;
|
|
29
|
-
|
|
30
15
|
/** Flags the intake surface understands. `--project` is consumed by
|
|
31
16
|
* {@link CommandContext.projectFlag}; the value token stays in argv. */
|
|
32
17
|
const INTAKE_FLAGS: Record<string, true> = { "--project": true, "--issue": true };
|
|
@@ -49,11 +34,11 @@ function assertKnownArgs(ctx: CommandContext, from: number): void {
|
|
|
49
34
|
}
|
|
50
35
|
|
|
51
36
|
export async function intakeCommand(ctx: CommandContext): Promise<void> {
|
|
37
|
+
// `--help`/`-h` never arrives here: the CLI dispatch gate answers a real
|
|
38
|
+
// help request from the manifest before this handler runs (#863), which is
|
|
39
|
+
// why the long-form prose lives on the manifest's `details` field rather
|
|
40
|
+
// than in a usage string this module would print itself.
|
|
52
41
|
const sub = ctx.argv[1];
|
|
53
|
-
if (sub === "--help" || sub === "-h") {
|
|
54
|
-
process.stdout.write(INTAKE_USAGE);
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
42
|
const project = findProject(loadConfig(), ctx.projectFlag);
|
|
58
43
|
const store = openStore(dbPath());
|
|
59
44
|
try {
|
package/src/commands/message.ts
CHANGED
|
@@ -101,6 +101,28 @@ export async function messageCommand(ctx: CommandContext): Promise<void> {
|
|
|
101
101
|
);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/**
|
|
105
|
+
* A hold reaches nobody now, so it is the wrong path for an answer somebody is
|
|
106
|
+
* waiting for (#882).
|
|
107
|
+
*
|
|
108
|
+
* 2026-08-21T20:48Z: a tick was interrupted by an operator message, the mandated
|
|
109
|
+
* targetless `telegram_send` failed, and the session fell back to this command.
|
|
110
|
+
* It behaved correctly — the category was non-bypass, so the answer was held for
|
|
111
|
+
* the 23:30 digest — and the operator sat waiting in the topic they had written
|
|
112
|
+
* in. "nothing was sent" was already printed and was already true; what was
|
|
113
|
+
* missing is that the caller is *mid-conversation* and a live path exists.
|
|
114
|
+
*
|
|
115
|
+
* So every hold names it. Conditionally, because this process cannot know
|
|
116
|
+
* whether an inbound message is waiting: it is a separate process from the
|
|
117
|
+
* session, and inventing a guess here would either nag every legitimate
|
|
118
|
+
* digest-only note or, worse, quietly bypass availability policy on a hunch.
|
|
119
|
+
* The caller knows, and one sentence is enough to make holding a choice rather
|
|
120
|
+
* than a default.
|
|
121
|
+
*/
|
|
122
|
+
const LIVE_REPLY_HINT =
|
|
123
|
+
"if you are answering an operator who is waiting now, this is not the path: " +
|
|
124
|
+
"reply in their own topic with telegram_send (telegram_ask for a choice) — a held notice reaches nobody until then\n";
|
|
125
|
+
|
|
104
126
|
/** The held-notice line names the future that releases it, in words an
|
|
105
127
|
* operator can act on: the working-hours window opening, or the daily digest.
|
|
106
128
|
* The earlier spelling — "the next digest or working-hours catch-up" — read
|
|
@@ -118,7 +140,8 @@ function heldLine(
|
|
|
118
140
|
return (
|
|
119
141
|
`held notice ${outcome.noticeId} queued for ${project.name} (${outcome.category}; ` +
|
|
120
142
|
`held until the working-hours window opens${opening})\n` +
|
|
121
|
-
"nothing was sent; the daemon releases it with the working-hours catch-up when your window opens\n"
|
|
143
|
+
"nothing was sent; the daemon releases it with the working-hours catch-up when your window opens\n" +
|
|
144
|
+
LIVE_REPLY_HINT
|
|
122
145
|
);
|
|
123
146
|
}
|
|
124
147
|
const digestAt = project.reporting?.digest.at;
|
|
@@ -126,6 +149,7 @@ function heldLine(
|
|
|
126
149
|
return (
|
|
127
150
|
`held notice ${outcome.noticeId} queued for ${project.name} (${outcome.category}; ` +
|
|
128
151
|
`digest-only — held until ${when})\n` +
|
|
129
|
-
`nothing was sent; the daemon releases it with ${when}\n`
|
|
152
|
+
`nothing was sent; the daemon releases it with ${when}\n` +
|
|
153
|
+
LIVE_REPLY_HINT
|
|
130
154
|
);
|
|
131
155
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `reconcile-units` — install the host unit files this version renders, and
|
|
3
|
+
* nothing else (#905).
|
|
4
|
+
*
|
|
5
|
+
* Scope: `"host"`. The units are host-global (one daemon serves every
|
|
6
|
+
* configured project), so this verb takes no project and reconciles the shared
|
|
7
|
+
* destinations. The per-project tail — tick config, brief link — stays
|
|
8
|
+
* `setup host`'s.
|
|
9
|
+
*
|
|
10
|
+
* It exists as its own verb because the render is code, not data: the upgrade
|
|
11
|
+
* transaction replaces the package underneath itself, so only a freshly
|
|
12
|
+
* spawned process can render what the new release ships. `upgrade` runs this
|
|
13
|
+
* verb with `--yes` after its install phase; an operator can run it by hand
|
|
14
|
+
* any time `doctor` reports `systemd-unit` drift.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { CommandContext } from "./context.ts";
|
|
18
|
+
import { findProject, loadConfig, resolveCaps } from "../config.ts";
|
|
19
|
+
import { telegramStateDir } from "../fleet.ts";
|
|
20
|
+
import {
|
|
21
|
+
planHostRuntime,
|
|
22
|
+
RECOVER_SCRIPT_INSTALL_PATH,
|
|
23
|
+
SYSTEMD_UNIT_DIR,
|
|
24
|
+
totalConfiguredWorkers,
|
|
25
|
+
} from "../setup-host.ts";
|
|
26
|
+
import {
|
|
27
|
+
formatUnitReconcile,
|
|
28
|
+
planUnitReconcile,
|
|
29
|
+
reconcileUnits,
|
|
30
|
+
UNIT_RECONCILE_ANSWER_KEY,
|
|
31
|
+
} from "../unit-reconcile.ts";
|
|
32
|
+
import { answersUi } from "../setup-answers.ts";
|
|
33
|
+
import { terminalUi } from "../wizard-ui.ts";
|
|
34
|
+
|
|
35
|
+
/** Repeatable `--protect PATH`: destinations this run must not overwrite. */
|
|
36
|
+
function protectedPaths(argv: readonly string[]): string[] {
|
|
37
|
+
const paths: string[] = [];
|
|
38
|
+
for (const [index, arg] of argv.entries()) {
|
|
39
|
+
if (arg === "--protect") {
|
|
40
|
+
const value = argv[index + 1];
|
|
41
|
+
if (value !== undefined && !value.startsWith("--")) paths.push(value);
|
|
42
|
+
} else if (arg.startsWith("--protect=")) {
|
|
43
|
+
paths.push(arg.slice("--protect=".length));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return paths;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function reconcileUnitsCommand(ctx: CommandContext): Promise<void> {
|
|
50
|
+
const cfg = loadConfig();
|
|
51
|
+
// The units are host-global. A `--project` narrows only the per-project tail
|
|
52
|
+
// this verb deliberately does not write, so it is accepted (the recovery
|
|
53
|
+
// unit encodes one project's name on a single-project host) and ignored
|
|
54
|
+
// otherwise.
|
|
55
|
+
const project = ctx.projectFlag === undefined && cfg.projects.length !== 1
|
|
56
|
+
? undefined
|
|
57
|
+
: findProject(cfg, ctx.projectFlag);
|
|
58
|
+
const plan = planHostRuntime(
|
|
59
|
+
project,
|
|
60
|
+
project === undefined ? cfg.defaults : resolveCaps(project, cfg.defaults),
|
|
61
|
+
telegramStateDir(),
|
|
62
|
+
undefined,
|
|
63
|
+
totalConfiguredWorkers(cfg),
|
|
64
|
+
SYSTEMD_UNIT_DIR,
|
|
65
|
+
RECOVER_SCRIPT_INSTALL_PATH,
|
|
66
|
+
cfg.projects.length > 1,
|
|
67
|
+
);
|
|
68
|
+
const protect = protectedPaths(ctx.argv);
|
|
69
|
+
// `--no-refresh` retires without refreshing (#895). The upgrade transaction
|
|
70
|
+
// uses it when it could not read a pre-install drift baseline: there a
|
|
71
|
+
// refresh cannot tell the operator's own edits from the upgrade's, but the
|
|
72
|
+
// retirement is not a comparison against any render and must still happen —
|
|
73
|
+
// otherwise a host that cannot be baselined keeps its obsolete mount forever.
|
|
74
|
+
const refresh = ctx.argv.includes("--no-refresh") === false;
|
|
75
|
+
|
|
76
|
+
if (ctx.argv.includes("--dry-run")) {
|
|
77
|
+
const preview = planUnitReconcile(plan, protect, refresh);
|
|
78
|
+
process.stdout.write(
|
|
79
|
+
preview.steps.length === 0
|
|
80
|
+
? `${formatUnitReconcile({ kind: "current", protectedDrift: preview.protectedDrift, outOfScope: preview.outOfScope })}\n`
|
|
81
|
+
: `${[
|
|
82
|
+
"would reconcile:",
|
|
83
|
+
...preview.retire.map((path) => ` retire ${path}`),
|
|
84
|
+
...preview.refresh.map((d) => ` ${d.installed} (from ${d.source})`),
|
|
85
|
+
...preview.steps.map((s) => ` $ ${s.argv.join(" ")}`),
|
|
86
|
+
].join("\n")}\n`,
|
|
87
|
+
);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// `--yes` is the non-interactive consent the upgrade transaction carries:
|
|
92
|
+
// its operator already authorised the upgrade, and the detached installer
|
|
93
|
+
// has no terminal to prompt on. Interactive runs get the real confirm.
|
|
94
|
+
const ui = ctx.argv.includes("--yes")
|
|
95
|
+
? answersUi({ [UNIT_RECONCILE_ANSWER_KEY]: true })
|
|
96
|
+
: terminalUi();
|
|
97
|
+
try {
|
|
98
|
+
const outcome = await reconcileUnits(plan, ui, { protect, refresh });
|
|
99
|
+
process.stdout.write(`${formatUnitReconcile(outcome)}\n`);
|
|
100
|
+
if (outcome.kind === "failed") process.exitCode = 1;
|
|
101
|
+
} finally {
|
|
102
|
+
ui.close();
|
|
103
|
+
}
|
|
104
|
+
}
|