omp-conductor 0.16.0 → 0.16.2
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 +25 -0
- package/REFERENCE.md +3 -0
- package/package.json +2 -1
- package/schema/config.schema.json +1 -0
- package/src/admission.ts +111 -7
- package/src/briefs/orchestrator.md +21 -6
- package/src/briefs/policy.md +13 -5
- package/src/cli.ts +110 -381
- package/src/command-help.ts +220 -0
- package/src/command-manifest.ts +471 -0
- package/src/commands/complete.ts +93 -0
- package/src/commands/context.ts +1 -0
- package/src/commands/setup.ts +68 -2
- package/src/commands/tail.ts +204 -44
- package/src/config-schema.ts +4 -0
- package/src/config.ts +31 -1
- package/src/daemon.ts +16 -0
- package/src/depends-on.ts +77 -28
- package/src/doctor.ts +120 -1
- package/src/escalate.ts +77 -4
- package/src/fleet.ts +127 -42
- package/src/setup-host.ts +29 -11
- package/src/setup-wizard.ts +10 -9
- package/src/transcript.ts +1 -1
- package/src/upgrade-verify.ts +25 -3
- package/src/upgrade.ts +61 -2
- package/src/worker.ts +196 -0
- package/src/worktree.ts +13 -1
- package/systemd/omp-conductor-recover.sh +1 -1
- package/systemd/recover-unit-test.sh +2 -2
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import { COMMAND_DETAILS, COMMAND_HELP_TAIL } from "./command-help.ts";
|
|
2
|
+
import { AMEND_AREA_IDS } from "./setup.ts";
|
|
3
|
+
import type { CommandScope } from "./commands/context.ts";
|
|
4
|
+
|
|
5
|
+
export type CommandCompleter = "project" | "shell" | "area";
|
|
6
|
+
|
|
7
|
+
export interface CommandFlag {
|
|
8
|
+
name: string;
|
|
9
|
+
alias?: string;
|
|
10
|
+
description: string;
|
|
11
|
+
takesValue: boolean;
|
|
12
|
+
completer?: CommandCompleter;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CommandPositional {
|
|
16
|
+
name: string;
|
|
17
|
+
completer?: CommandCompleter;
|
|
18
|
+
variadic?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CommandManifestEntry {
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
scope: CommandScope;
|
|
25
|
+
usage: readonly string[];
|
|
26
|
+
subcommands?: readonly { name: string; description: string }[];
|
|
27
|
+
flags: readonly CommandFlag[];
|
|
28
|
+
positionals?: readonly CommandPositional[];
|
|
29
|
+
/** Long-form operator help, kept on the manifest so generated help cannot drift. */
|
|
30
|
+
details?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const project = (): CommandFlag => ({
|
|
34
|
+
name: "--project",
|
|
35
|
+
description: "target the named configured project",
|
|
36
|
+
takesValue: true,
|
|
37
|
+
completer: "project",
|
|
38
|
+
});
|
|
39
|
+
const all = (): CommandFlag => ({
|
|
40
|
+
name: "--all",
|
|
41
|
+
description: "target every configured project",
|
|
42
|
+
takesValue: false,
|
|
43
|
+
});
|
|
44
|
+
const value = (
|
|
45
|
+
name: string,
|
|
46
|
+
description: string,
|
|
47
|
+
completer?: CommandCompleter,
|
|
48
|
+
): CommandFlag => ({ name, description, takesValue: true, completer });
|
|
49
|
+
const toggle = (name: string, description: string, alias?: string): CommandFlag => ({
|
|
50
|
+
name,
|
|
51
|
+
alias,
|
|
52
|
+
description,
|
|
53
|
+
takesValue: false,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const COMMAND_MANIFEST: readonly CommandManifestEntry[] = [
|
|
57
|
+
{
|
|
58
|
+
name: "setup",
|
|
59
|
+
description: "interview, configure the fleet, or install host and graph surfaces",
|
|
60
|
+
scope: "project",
|
|
61
|
+
usage: [
|
|
62
|
+
"setup [area] [--no-ai] [--project NAME]",
|
|
63
|
+
"setup host [NAME] [--project NAME]",
|
|
64
|
+
"setup graph [--no-seed] [--print] [--project NAME]",
|
|
65
|
+
],
|
|
66
|
+
details: COMMAND_DETAILS,
|
|
67
|
+
subcommands: [
|
|
68
|
+
{ name: "host", description: "stage and install the host services" },
|
|
69
|
+
{ name: "graph", description: "install and seed code-graph indexes" },
|
|
70
|
+
],
|
|
71
|
+
flags: [
|
|
72
|
+
project(),
|
|
73
|
+
toggle("--no-ai", "ask every question without AI repository probes"),
|
|
74
|
+
toggle("--no-seed", "enable graph indexing without the initial seed"),
|
|
75
|
+
toggle("--print", "print the graph install plan without changing anything"),
|
|
76
|
+
],
|
|
77
|
+
positionals: [
|
|
78
|
+
{ name: AMEND_AREA_IDS.join("|"), completer: "area" },
|
|
79
|
+
{ name: "project", completer: "project" },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "start",
|
|
84
|
+
description: "start the installed service or background dispatch daemon",
|
|
85
|
+
scope: "host",
|
|
86
|
+
usage: ["start [--port N] [--project NAME]"],
|
|
87
|
+
flags: [value("--port", "health endpoint port"), project()],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "version",
|
|
91
|
+
description: "print the installed package version (also --version, -V)",
|
|
92
|
+
scope: "none",
|
|
93
|
+
usage: ["--version"],
|
|
94
|
+
flags: [],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "stop",
|
|
98
|
+
description: "hold the fleet and stop the dispatch daemon",
|
|
99
|
+
scope: "fleet",
|
|
100
|
+
usage: ["stop [--pane] [--project NAME | --all]"],
|
|
101
|
+
flags: [toggle("--pane", "also stop and pin off pane recovery"), project(), all()],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "restart",
|
|
105
|
+
description: "drain and restart the shared dispatch daemon",
|
|
106
|
+
scope: "host",
|
|
107
|
+
usage: ["restart [--now] [--timeout SECONDS] [--port N] [--project NAME]"],
|
|
108
|
+
flags: [
|
|
109
|
+
toggle("--now", "restart immediately without draining workers"),
|
|
110
|
+
value("--timeout", "maximum drain time in seconds"),
|
|
111
|
+
value("--port", "health endpoint port"),
|
|
112
|
+
project(),
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: "upgrade",
|
|
117
|
+
description: "upgrade all installed conductor surfaces as one transaction",
|
|
118
|
+
scope: "host",
|
|
119
|
+
usage: ["upgrade [--to VERSION] [--project NAME]"],
|
|
120
|
+
flags: [value("--to", "package version to install"), project()],
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: "upgrade-install",
|
|
124
|
+
description: "run the detached upgrade transaction",
|
|
125
|
+
scope: "host",
|
|
126
|
+
usage: ["upgrade-install --to VERSION [--project NAME]"],
|
|
127
|
+
flags: [value("--to", "package version to install"), project()],
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "upgrade-rollback",
|
|
131
|
+
description: "restore surfaces from the durable pre-upgrade snapshot",
|
|
132
|
+
scope: "host",
|
|
133
|
+
usage: ["upgrade-rollback"],
|
|
134
|
+
flags: [],
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: "board",
|
|
138
|
+
description: "open the live keyboard-driven fleet board",
|
|
139
|
+
scope: "fleet",
|
|
140
|
+
usage: ["board [--project NAME] [--json]"],
|
|
141
|
+
flags: [project(), toggle("--json", "print the stable board JSON shape")],
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "dashboard",
|
|
145
|
+
description: "serve the read-only browser dashboard",
|
|
146
|
+
scope: "none",
|
|
147
|
+
usage: ["dashboard [--port N] [--host ADDR]"],
|
|
148
|
+
flags: [value("--port", "dashboard port"), value("--host", "dashboard bind address")],
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: "status",
|
|
152
|
+
description: "print layered fleet and deployment status",
|
|
153
|
+
scope: "fleet",
|
|
154
|
+
usage: ["status [--project NAME]"],
|
|
155
|
+
flags: [project()],
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: "stats",
|
|
159
|
+
description: "summarize fleet throughput, failures, spend, and GitHub calls",
|
|
160
|
+
scope: "project",
|
|
161
|
+
usage: ["stats [--since 7d|30d|YYYY-MM-DD] [--project NAME] [--json]"],
|
|
162
|
+
flags: [
|
|
163
|
+
value("--since", "reporting window start"),
|
|
164
|
+
project(),
|
|
165
|
+
toggle("--json", "print the stable stats JSON shape"),
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: "doctor",
|
|
170
|
+
description: "run read-only deployment health checks",
|
|
171
|
+
scope: "fleet",
|
|
172
|
+
usage: ["doctor [--project NAME] [--json] [--probe-telegram]"],
|
|
173
|
+
flags: [
|
|
174
|
+
project(),
|
|
175
|
+
toggle("--json", "print the stable doctor JSON shape"),
|
|
176
|
+
toggle("--probe-telegram", "send the self-identified Telegram health probe"),
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: "restore-db",
|
|
181
|
+
description: "restore conductor.db from a safe snapshot",
|
|
182
|
+
scope: "host",
|
|
183
|
+
usage: ["restore-db [SNAPSHOT]"],
|
|
184
|
+
flags: [],
|
|
185
|
+
positionals: [{ name: "snapshot" }],
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: "ledger",
|
|
189
|
+
description: "print conductor verb and daemon decision history",
|
|
190
|
+
scope: "project",
|
|
191
|
+
usage: ["ledger [--issue N] [--limit N] [--project NAME]"],
|
|
192
|
+
flags: [value("--issue", "filter by issue number"), value("--limit", "maximum rows"), project()],
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: "hold",
|
|
196
|
+
description: "pause claims and disarm ticks while keeping processes up",
|
|
197
|
+
scope: "fleet",
|
|
198
|
+
usage: ["hold [--keep-ticks] [--project NAME | --all]"],
|
|
199
|
+
flags: [toggle("--keep-ticks", "pause claims without disarming ticks"), project(), all()],
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: "arm",
|
|
203
|
+
description: "prove Telegram delivery and arm scheduled ticks",
|
|
204
|
+
scope: "fleet",
|
|
205
|
+
usage: ["arm [--project NAME | --all]"],
|
|
206
|
+
flags: [project(), all()],
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
name: "disarm",
|
|
210
|
+
description: "disarm scheduled ticks without stopping processes",
|
|
211
|
+
scope: "fleet",
|
|
212
|
+
usage: ["disarm [--project NAME | --all]"],
|
|
213
|
+
flags: [project(), all()],
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: "tail",
|
|
217
|
+
description: "follow the newest worker run for an issue",
|
|
218
|
+
scope: "project",
|
|
219
|
+
usage: ["tail <issue> [--project NAME]"],
|
|
220
|
+
flags: [project()],
|
|
221
|
+
positionals: [{ name: "issue" }],
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: "extend",
|
|
225
|
+
description: "raise a live or next-attempt worker turn ceiling",
|
|
226
|
+
scope: "project",
|
|
227
|
+
usage: ["extend <issue> --turns N [--project NAME]"],
|
|
228
|
+
flags: [value("--turns", "positive turn ceiling"), project()],
|
|
229
|
+
positionals: [{ name: "issue" }],
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: "worker",
|
|
233
|
+
description: "pause, resume, or stop one worker session",
|
|
234
|
+
scope: "project",
|
|
235
|
+
usage: [
|
|
236
|
+
"worker pause <issue> [--project NAME]",
|
|
237
|
+
"worker resume <issue> [--project NAME]",
|
|
238
|
+
"worker stop <issue> --reason TEXT [--project NAME]",
|
|
239
|
+
],
|
|
240
|
+
subcommands: [
|
|
241
|
+
{ name: "pause", description: "park the worker at harness idle" },
|
|
242
|
+
{ name: "resume", description: "resume the parked worker" },
|
|
243
|
+
{ name: "stop", description: "stop the worker with a recorded reason" },
|
|
244
|
+
],
|
|
245
|
+
flags: [value("--reason", "reason for stopping the worker"), project()],
|
|
246
|
+
positionals: [{ name: "action" }, { name: "issue" }],
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: "unblock",
|
|
250
|
+
description: "clear blocked and failed labels so an issue can be claimed again",
|
|
251
|
+
scope: "project",
|
|
252
|
+
usage: ["unblock <issue> [--force] [--no-requeue] [--project NAME]"],
|
|
253
|
+
flags: [
|
|
254
|
+
toggle("--force", "accept missing salvage and continue"),
|
|
255
|
+
toggle("--no-requeue", "clear state labels without restoring the queue label"),
|
|
256
|
+
project(),
|
|
257
|
+
],
|
|
258
|
+
positionals: [{ name: "issue" }],
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
name: "unfreeze",
|
|
262
|
+
description: "clear a repository freeze with a recorded reason",
|
|
263
|
+
scope: "project",
|
|
264
|
+
usage: ["unfreeze <repo> [--reason TEXT] [--project NAME]"],
|
|
265
|
+
flags: [value("--reason", "reason for clearing the freeze"), project()],
|
|
266
|
+
positionals: [{ name: "repo" }],
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
name: "verb",
|
|
270
|
+
description: "run a gated conductor_* verb as the orchestrator",
|
|
271
|
+
scope: "project",
|
|
272
|
+
usage: ["verb <conductor_*> [--project NAME] [--arg k=v ...]"],
|
|
273
|
+
flags: [project(), value("--arg", "repeatable conductor verb argument")],
|
|
274
|
+
positionals: [{ name: "conductor-verb" }],
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: "daemon",
|
|
278
|
+
description: "run the dispatch loop in the foreground",
|
|
279
|
+
scope: "host",
|
|
280
|
+
usage: ["daemon [--once] [--port N] [--project NAME]"],
|
|
281
|
+
flags: [toggle("--once", "run one tick and exit"), value("--port", "health endpoint port"), project()],
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
name: "resume",
|
|
285
|
+
description: "clear fleet pause and pane recovery pins without arming ticks",
|
|
286
|
+
scope: "fleet",
|
|
287
|
+
usage: ["resume [--project NAME | --all]"],
|
|
288
|
+
flags: [project(), all()],
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
name: "brief-upgrade",
|
|
292
|
+
description: "inspect or migrate the project brief overlay",
|
|
293
|
+
scope: "project",
|
|
294
|
+
usage: ["brief-upgrade [--migrate|--retrofit] [--apply] [--file PATH] [--project NAME]"],
|
|
295
|
+
flags: [
|
|
296
|
+
toggle("--migrate", "migrate the owned brief half into POLICY.md"),
|
|
297
|
+
toggle("--retrofit", "add the ownership cut to an older brief"),
|
|
298
|
+
toggle("--apply", "write the proposed brief changes"),
|
|
299
|
+
value("--file", "brief path"),
|
|
300
|
+
project(),
|
|
301
|
+
],
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
name: "friction",
|
|
305
|
+
description: "record a bounded escalation or report-quality observation",
|
|
306
|
+
scope: "project",
|
|
307
|
+
usage: [
|
|
308
|
+
"friction <escalation-digest|report-noise|report-surprise> --detail TEXT [--issue N] [--project NAME]",
|
|
309
|
+
],
|
|
310
|
+
flags: [value("--detail", "bounded observation detail"), value("--issue", "related issue number"), project()],
|
|
311
|
+
positionals: [{ name: "category" }],
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
name: "event",
|
|
315
|
+
description: "persist one ordinary material outcome without sending it",
|
|
316
|
+
scope: "project",
|
|
317
|
+
usage: [
|
|
318
|
+
"event record --category NAME --summary TEXT --evidence REF [--occurred-at ISO] [--project NAME]",
|
|
319
|
+
],
|
|
320
|
+
subcommands: [{ name: "record", description: "record a material outcome" }],
|
|
321
|
+
flags: [
|
|
322
|
+
value("--category", "short lowercase event category"),
|
|
323
|
+
value("--summary", "event summary"),
|
|
324
|
+
value("--evidence", "verifiable evidence reference"),
|
|
325
|
+
value("--occurred-at", "ISO occurrence timestamp"),
|
|
326
|
+
project(),
|
|
327
|
+
],
|
|
328
|
+
positionals: [{ name: "action" }],
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
name: "report",
|
|
332
|
+
description: "hand a rendered report to the daemon's durable outbox",
|
|
333
|
+
scope: "project",
|
|
334
|
+
usage: [
|
|
335
|
+
"report --text TEXT [--kind material|digest|tier2|decision-needed|fleet-stopped|confirmed-failure] [--events IDS] [--notices IDS] [--project NAME]",
|
|
336
|
+
],
|
|
337
|
+
flags: [
|
|
338
|
+
value("--text", "rendered report text"),
|
|
339
|
+
value("--kind", "material, digest, or escalation category"),
|
|
340
|
+
value("--events", "comma-separated event ledger ids"),
|
|
341
|
+
value("--notices", "comma-separated notice ledger ids"),
|
|
342
|
+
project(),
|
|
343
|
+
],
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: "message",
|
|
347
|
+
description: "deliver one direct message to the project's Telegram topic",
|
|
348
|
+
scope: "project",
|
|
349
|
+
usage: ["message --text TEXT [--category CATEGORY] [--blocks TEXT] [--project NAME]"],
|
|
350
|
+
flags: [
|
|
351
|
+
value("--text", "message text"),
|
|
352
|
+
value("--category", "escalation category"),
|
|
353
|
+
value("--blocks", "what the message blocks"),
|
|
354
|
+
project(),
|
|
355
|
+
],
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
name: "decision",
|
|
359
|
+
description: "record, list, resolve, and withdraw operator decisions",
|
|
360
|
+
scope: "project",
|
|
361
|
+
usage: [
|
|
362
|
+
"decision open --question TEXT [--blocks TEXT] [--resolves-when COND] [--project NAME]",
|
|
363
|
+
"decision resolve <id> --answer TEXT [--project NAME]",
|
|
364
|
+
"decision withdraw <id> [--reason TEXT] [--project NAME]",
|
|
365
|
+
"decision list [--project NAME]",
|
|
366
|
+
],
|
|
367
|
+
subcommands: [
|
|
368
|
+
{ name: "open", description: "record a question for the operator" },
|
|
369
|
+
{ name: "resolve", description: "record the operator's answer" },
|
|
370
|
+
{ name: "withdraw", description: "withdraw an obsolete question" },
|
|
371
|
+
{ name: "list", description: "list open operator decisions" },
|
|
372
|
+
],
|
|
373
|
+
flags: [
|
|
374
|
+
value("--question", "question for the operator"),
|
|
375
|
+
value("--blocks", "what the decision blocks"),
|
|
376
|
+
value("--resolves-when", "automatic resolution condition"),
|
|
377
|
+
value("--answer", "operator answer"),
|
|
378
|
+
value("--reason", "withdrawal reason"),
|
|
379
|
+
project(),
|
|
380
|
+
],
|
|
381
|
+
positionals: [{ name: "action" }, { name: "id" }],
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
name: "watch",
|
|
385
|
+
description: "record or list orchestrator-only conditions and carry notes",
|
|
386
|
+
scope: "project",
|
|
387
|
+
usage: [
|
|
388
|
+
"watch add --note TEXT [--blocks TEXT] [--resolves-when COND] [--project NAME]",
|
|
389
|
+
"watch list [--project NAME]",
|
|
390
|
+
],
|
|
391
|
+
subcommands: [
|
|
392
|
+
{ name: "add", description: "record a watch" },
|
|
393
|
+
{ name: "list", description: "list open watches" },
|
|
394
|
+
],
|
|
395
|
+
flags: [
|
|
396
|
+
value("--note", "note carried when the watch resolves"),
|
|
397
|
+
value("--blocks", "what the watch blocks"),
|
|
398
|
+
value("--resolves-when", "automatic resolution condition"),
|
|
399
|
+
project(),
|
|
400
|
+
],
|
|
401
|
+
positionals: [{ name: "action" }],
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
name: "intake",
|
|
405
|
+
description: "capture and groom raw ideas durably",
|
|
406
|
+
scope: "project",
|
|
407
|
+
usage: [
|
|
408
|
+
'intake "<text>" [--project NAME]',
|
|
409
|
+
"intake list [--project NAME]",
|
|
410
|
+
"intake dismiss <id> [--project NAME]",
|
|
411
|
+
"intake groomed <id> --issue <url> [--project NAME]",
|
|
412
|
+
],
|
|
413
|
+
subcommands: [
|
|
414
|
+
{ name: "list", description: "list pending intake items" },
|
|
415
|
+
{ name: "dismiss", description: "dismiss an intake item" },
|
|
416
|
+
{ name: "groomed", description: "link an intake item to its issue" },
|
|
417
|
+
],
|
|
418
|
+
flags: [value("--issue", "groomed issue URL"), project()],
|
|
419
|
+
positionals: [{ name: "text|action" }, { name: "id" }],
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
name: "help",
|
|
423
|
+
description: "print command usage (also --help, -h)",
|
|
424
|
+
scope: "none",
|
|
425
|
+
usage: ["help"],
|
|
426
|
+
details: COMMAND_HELP_TAIL,
|
|
427
|
+
flags: [],
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
name: "complete",
|
|
431
|
+
description: "generate shell setup or answer one completion request",
|
|
432
|
+
scope: "none",
|
|
433
|
+
usage: ["complete <zsh|bash|fish|powershell>", "complete -- <args...>"],
|
|
434
|
+
flags: [],
|
|
435
|
+
positionals: [
|
|
436
|
+
{ name: "shell", completer: "shell" },
|
|
437
|
+
{ name: "args", variadic: true },
|
|
438
|
+
],
|
|
439
|
+
},
|
|
440
|
+
];
|
|
441
|
+
|
|
442
|
+
export function renderUsage(manifest: readonly CommandManifestEntry[] = COMMAND_MANIFEST): string {
|
|
443
|
+
const synopsis = manifest.flatMap((command) =>
|
|
444
|
+
command.usage.map((line) => ` omp-conductor ${line}`),
|
|
445
|
+
);
|
|
446
|
+
const width = Math.max(...manifest.map((command) => command.name.length));
|
|
447
|
+
const commands = manifest.map(
|
|
448
|
+
(command) => ` ${command.name.padEnd(width)} ${command.description}`,
|
|
449
|
+
);
|
|
450
|
+
const details = manifest.flatMap((command) =>
|
|
451
|
+
command.details === undefined ? [] : [...command.details.split("\n"), ""],
|
|
452
|
+
);
|
|
453
|
+
return [
|
|
454
|
+
"omp-conductor — dispatch ready issues to omp coding sessions",
|
|
455
|
+
"",
|
|
456
|
+
"usage:",
|
|
457
|
+
...synopsis,
|
|
458
|
+
"",
|
|
459
|
+
"commands:",
|
|
460
|
+
...commands,
|
|
461
|
+
"",
|
|
462
|
+
"details:",
|
|
463
|
+
...details,
|
|
464
|
+
"recipes:",
|
|
465
|
+
" hold no claims, no tick sends (inspectable)",
|
|
466
|
+
" stop hold + stop dispatch daemon",
|
|
467
|
+
" stop --pane stop + pin conductor-pane recovery off",
|
|
468
|
+
" resume clear pause and pane pin (ticks stay disarmed)",
|
|
469
|
+
" resume && arm clear pause and pane pin, then prove inbound Telegram",
|
|
470
|
+
].join("\n");
|
|
471
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { RootCommand, type Complete } from "@bomb.sh/tab";
|
|
2
|
+
import { COMMAND_MANIFEST, type CommandCompleter } from "../command-manifest.ts";
|
|
3
|
+
import { loadConfig } from "../config.ts";
|
|
4
|
+
import { AMEND_AREA_IDS } from "../setup.ts";
|
|
5
|
+
|
|
6
|
+
export const COMPLETION_SHELLS = ["zsh", "bash", "fish", "powershell"] as const;
|
|
7
|
+
export type CompletionShell = (typeof COMPLETION_SHELLS)[number];
|
|
8
|
+
|
|
9
|
+
function suggestions(kind: CommandCompleter, complete: Complete): void {
|
|
10
|
+
try {
|
|
11
|
+
if (kind === "project") {
|
|
12
|
+
for (const project of loadConfig().projects) complete(project.name, "configured project");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (kind === "area") {
|
|
16
|
+
for (const area of AMEND_AREA_IDS) complete(area, "setup area");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
for (const shell of COMPLETION_SHELLS) complete(shell, "supported shell");
|
|
20
|
+
} catch {
|
|
21
|
+
// Completion runs on every TAB press. A missing or invalid config must make
|
|
22
|
+
// completion empty, never turn an interactive shell into an error surface.
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function completionRoot(): RootCommand {
|
|
27
|
+
const root = new RootCommand();
|
|
28
|
+
for (const entry of COMMAND_MANIFEST) {
|
|
29
|
+
const command = root.command(entry.name, entry.description);
|
|
30
|
+
for (const flag of entry.flags) {
|
|
31
|
+
const name = flag.name.replace(/^--/, "");
|
|
32
|
+
const alias = flag.alias?.replace(/^-+/, "");
|
|
33
|
+
const completer = flag.completer;
|
|
34
|
+
const complete =
|
|
35
|
+
completer === undefined ? undefined : ((add: Complete) => suggestions(completer, add));
|
|
36
|
+
if (flag.takesValue) {
|
|
37
|
+
command.option(name, flag.description, complete ?? (() => undefined), alias);
|
|
38
|
+
} else if (alias !== undefined) {
|
|
39
|
+
command.option(name, flag.description, alias);
|
|
40
|
+
} else {
|
|
41
|
+
command.option(name, flag.description);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (const [index, positional] of (entry.positionals ?? []).entries()) {
|
|
45
|
+
const subcommands = index === 0 ? entry.subcommands : undefined;
|
|
46
|
+
command.argument(
|
|
47
|
+
positional.name,
|
|
48
|
+
positional.completer === undefined && subcommands === undefined
|
|
49
|
+
? undefined
|
|
50
|
+
: (complete) => {
|
|
51
|
+
for (const subcommand of subcommands ?? []) {
|
|
52
|
+
complete(subcommand.name, subcommand.description);
|
|
53
|
+
}
|
|
54
|
+
if (positional.completer !== undefined) suggestions(positional.completer, complete);
|
|
55
|
+
},
|
|
56
|
+
positional.variadic,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return root;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function shellFrom(value: string | undefined): CompletionShell {
|
|
64
|
+
if ((COMPLETION_SHELLS as readonly string[]).includes(value ?? "")) {
|
|
65
|
+
return value as CompletionShell;
|
|
66
|
+
}
|
|
67
|
+
throw new Error(`complete expects one of: ${COMPLETION_SHELLS.join(", ")}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function printCompletionScript(shell: CompletionShell): void {
|
|
71
|
+
completionRoot().setup("omp-conductor", "omp-conductor", shell);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function renderCompletionScript(shell: CompletionShell): string {
|
|
75
|
+
const lines: string[] = [];
|
|
76
|
+
const original = console.log;
|
|
77
|
+
console.log = (...args: unknown[]) => lines.push(args.map(String).join(" "));
|
|
78
|
+
try {
|
|
79
|
+
printCompletionScript(shell);
|
|
80
|
+
} finally {
|
|
81
|
+
console.log = original;
|
|
82
|
+
}
|
|
83
|
+
return `${lines.join("\n")}\n`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function completeCommand(args: string[]): void {
|
|
87
|
+
if (args[0] === "--") {
|
|
88
|
+
const request = args[1] === "omp-conductor" ? args.slice(2) : args.slice(1);
|
|
89
|
+
completionRoot().parse(request);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
printCompletionScript(shellFrom(args[0]));
|
|
93
|
+
}
|
package/src/commands/context.ts
CHANGED
package/src/commands/setup.ts
CHANGED
|
@@ -6,13 +6,18 @@
|
|
|
6
6
|
* changed from the original bodies.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { basename, join } from "node:path";
|
|
12
|
+
|
|
9
13
|
import type { CommandContext } from "./context.ts";
|
|
14
|
+
import type { CompletionShell } from "./complete.ts";
|
|
10
15
|
import { findProject, loadConfig, resolveCaps } from "../config.ts";
|
|
11
16
|
import { AMEND_AREA_IDS, type AmendAreaId } from "../setup.ts";
|
|
12
17
|
import { runGraphInstall, runHostInstall, type InstallOutcome } from "../setup-install.ts";
|
|
13
18
|
import { DEFAULT_PROBES, NO_PROBES, setup } from "../setup-wizard.ts";
|
|
14
19
|
import { telegramStateDir } from "../fleet.ts";
|
|
15
|
-
import { terminalUi } from "../wizard-ui.ts";
|
|
20
|
+
import { terminalUi, type WizardUi } from "../wizard-ui.ts";
|
|
16
21
|
import type { ConductorConfig, ProjectConfig } from "../types.ts";
|
|
17
22
|
|
|
18
23
|
/**
|
|
@@ -63,6 +68,52 @@ export function setupInstallProject(
|
|
|
63
68
|
return findProject(cfg, projectFlag ?? positionalName);
|
|
64
69
|
}
|
|
65
70
|
|
|
71
|
+
export async function installShellCompletions(
|
|
72
|
+
shell: Extract<CompletionShell, "zsh" | "bash">,
|
|
73
|
+
home: string = homedir(),
|
|
74
|
+
): Promise<{ scriptPath: string; rcPath: string }> {
|
|
75
|
+
const completionDir = join(home, ".omp", "conductor");
|
|
76
|
+
mkdirSync(completionDir, { recursive: true });
|
|
77
|
+
const scriptPath = join(completionDir, `completions.${shell}`);
|
|
78
|
+
writeFileSync(
|
|
79
|
+
scriptPath,
|
|
80
|
+
(await import("./complete.ts")).renderCompletionScript(shell),
|
|
81
|
+
"utf8",
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const rcPath = join(home, shell === "zsh" ? ".zshrc" : ".bashrc");
|
|
85
|
+
const sourceLine =
|
|
86
|
+
`[ -f ~/.omp/conductor/completions.${shell} ] && ` +
|
|
87
|
+
`source ~/.omp/conductor/completions.${shell}`;
|
|
88
|
+
const current = existsSync(rcPath) ? readFileSync(rcPath, "utf8") : "";
|
|
89
|
+
if (!current.split(/\r?\n/).includes(sourceLine)) {
|
|
90
|
+
const separator = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
|
|
91
|
+
appendFileSync(rcPath, `${separator}${sourceLine}\n`, "utf8");
|
|
92
|
+
}
|
|
93
|
+
return { scriptPath, rcPath };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function offerCompletionInstall(ui: WizardUi): Promise<void> {
|
|
97
|
+
const install = await ui.confirm(
|
|
98
|
+
"Install shell completions?",
|
|
99
|
+
"Writes a generated completion script and one idempotent source line to your shell rc file.",
|
|
100
|
+
);
|
|
101
|
+
if (install !== true) return;
|
|
102
|
+
|
|
103
|
+
const detected = basename(process.env.SHELL ?? "");
|
|
104
|
+
const selected = await ui.select(
|
|
105
|
+
"Shell",
|
|
106
|
+
[
|
|
107
|
+
{ label: "zsh", description: "Install through ~/.zshrc" },
|
|
108
|
+
{ label: "bash", description: "Install through ~/.bashrc" },
|
|
109
|
+
],
|
|
110
|
+
{ initialIndex: detected === "bash" ? 1 : 0 },
|
|
111
|
+
);
|
|
112
|
+
if (selected !== "zsh" && selected !== "bash") return;
|
|
113
|
+
const installed = await installShellCompletions(selected);
|
|
114
|
+
ui.notify(`Installed completions at ${installed.scriptPath}; sourced from ${installed.rcPath}.`);
|
|
115
|
+
}
|
|
116
|
+
|
|
66
117
|
export async function setupCommand(ctx: CommandContext): Promise<void> {
|
|
67
118
|
// Help first, and only in the first trailing position: a help request
|
|
68
119
|
// must never open a UI, read config, probe GitHub or pause dispatch.
|
|
@@ -136,7 +187,22 @@ try {
|
|
|
136
187
|
// is still asked, nothing is proposed. For a host with no omp peer, a
|
|
137
188
|
// private repo no probe can clone, or an operator who would rather type the
|
|
138
189
|
// gates than review a model's reading of their CI.
|
|
139
|
-
|
|
190
|
+
const completed = await setup(
|
|
191
|
+
ui,
|
|
192
|
+
ctx.projectFlag,
|
|
193
|
+
area,
|
|
194
|
+
ctx.argv.includes("--no-ai") ? NO_PROBES : DEFAULT_PROBES,
|
|
195
|
+
);
|
|
196
|
+
if (completed && process.stdin.isTTY && process.stdout.isTTY) {
|
|
197
|
+
try {
|
|
198
|
+
await offerCompletionInstall(ui);
|
|
199
|
+
} catch (err) {
|
|
200
|
+
ui.notify(
|
|
201
|
+
`Skipped completion install: ${err instanceof Error ? err.message : String(err)}`,
|
|
202
|
+
"warning",
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
140
206
|
} finally {
|
|
141
207
|
ui.close();
|
|
142
208
|
}
|