omp-conductor 0.16.2 → 0.17.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 +38 -4
- package/REFERENCE.md +17 -11
- package/package.json +2 -1
- package/src/clack-ui.ts +83 -0
- package/src/command-manifest.ts +16 -7
- package/src/commands/arm.ts +7 -2
- package/src/commands/decision.ts +17 -7
- package/src/commands/doctor.ts +18 -1
- package/src/commands/hold.ts +9 -7
- package/src/commands/ledger.ts +25 -4
- package/src/commands/setup.ts +61 -10
- package/src/commands/stats.ts +9 -5
- package/src/commands/status.ts +32 -5
- package/src/commands/tail.ts +13 -1
- package/src/commands/watch.ts +16 -7
- package/src/fleet.ts +55 -15
- package/src/orchestrator-tick.ts +46 -7
- package/src/privileged.ts +3 -0
- package/src/setup-answers.ts +135 -0
- package/src/setup-install.ts +2 -0
- package/src/setup-probe.ts +1 -0
- package/src/setup-wizard.ts +120 -29
- package/src/ui/progress.ts +32 -0
- package/src/ui/style.ts +11 -0
- package/src/wizard-ui.ts +14 -5
package/src/setup-wizard.ts
CHANGED
|
@@ -107,6 +107,7 @@ import {
|
|
|
107
107
|
type ReportScopeChoice,
|
|
108
108
|
type ResolvedGrants,
|
|
109
109
|
} from "./types.ts";
|
|
110
|
+
import { withProgress } from "./ui/progress.ts";
|
|
110
111
|
import type { WizardUi } from "./wizard-ui.ts";
|
|
111
112
|
|
|
112
113
|
/**
|
|
@@ -132,8 +133,8 @@ const REPO_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
|
132
133
|
* takes it, because the harness has no pre-filled input dialog — so "Enter
|
|
133
134
|
* accepts what you see" is the contract the whole wizard is built on.
|
|
134
135
|
*/
|
|
135
|
-
async function ask(ui: WizardUi, title: string, fallback: string): Promise<string> {
|
|
136
|
-
const raw = await ui.input(title, fallback.length > 0 ? fallback : undefined);
|
|
136
|
+
async function ask(ui: WizardUi, key: string, title: string, fallback: string): Promise<string> {
|
|
137
|
+
const raw = await ui.input(title, fallback.length > 0 ? fallback : undefined, { key });
|
|
137
138
|
if (raw === undefined) throw new Cancelled();
|
|
138
139
|
const trimmed = raw.trim();
|
|
139
140
|
return trimmed.length > 0 ? trimmed : fallback;
|
|
@@ -147,8 +148,8 @@ async function ask(ui: WizardUi, title: string, fallback: string): Promise<strin
|
|
|
147
148
|
* recorded a silent no and walked on to the next question, which is not what
|
|
148
149
|
* abandoning a run means. `undefined` is the surface saying the operator left.
|
|
149
150
|
*/
|
|
150
|
-
async function askYesNo(ui: WizardUi, title: string, message: string): Promise<boolean> {
|
|
151
|
-
const answer = await ui.confirm(title, message);
|
|
151
|
+
async function askYesNo(ui: WizardUi, key: string, title: string, message: string): Promise<boolean> {
|
|
152
|
+
const answer = await ui.confirm(title, message, { key });
|
|
152
153
|
if (answer === undefined) throw new Cancelled();
|
|
153
154
|
return answer;
|
|
154
155
|
}
|
|
@@ -160,12 +161,13 @@ async function askYesNo(ui: WizardUi, title: string, message: string): Promise<b
|
|
|
160
161
|
*/
|
|
161
162
|
async function askValid(
|
|
162
163
|
ui: WizardUi,
|
|
164
|
+
key: string,
|
|
163
165
|
title: string,
|
|
164
166
|
fallback: string,
|
|
165
167
|
check: (value: string) => string | undefined,
|
|
166
168
|
): Promise<string> {
|
|
167
169
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
168
|
-
const value = await ask(ui, title, fallback);
|
|
170
|
+
const value = await ask(ui, key, title, fallback);
|
|
169
171
|
const problem = check(value);
|
|
170
172
|
if (problem === undefined) return value;
|
|
171
173
|
ui.notify(problem, "warning");
|
|
@@ -176,8 +178,8 @@ async function askValid(
|
|
|
176
178
|
|
|
177
179
|
/** A cap. Unparseable input keeps the current value rather than writing a NaN
|
|
178
180
|
* the validator would later reject — the operator sees why, immediately. */
|
|
179
|
-
async function askNumber(ui: WizardUi, title: string, fallback: number): Promise<number> {
|
|
180
|
-
const raw = await ask(ui, title, String(fallback));
|
|
181
|
+
async function askNumber(ui: WizardUi, key: string, title: string, fallback: number): Promise<number> {
|
|
182
|
+
const raw = await ask(ui, key, title, String(fallback));
|
|
181
183
|
const value = Number(raw);
|
|
182
184
|
if (!Number.isFinite(value) || value < 0) {
|
|
183
185
|
ui.notify(`"${raw}" is not a non-negative number — keeping ${fallback}.`, "warning");
|
|
@@ -193,11 +195,12 @@ async function askNumber(ui: WizardUi, title: string, fallback: number): Promise
|
|
|
193
195
|
*/
|
|
194
196
|
async function askSpendCap(
|
|
195
197
|
ui: WizardUi,
|
|
198
|
+
key: string,
|
|
196
199
|
title: string,
|
|
197
200
|
fallback: number | null,
|
|
198
201
|
): Promise<number | null> {
|
|
199
202
|
const seed = fallback === null ? "" : String(fallback);
|
|
200
|
-
const raw = (await ask(ui, title, seed)).trim().toLowerCase();
|
|
203
|
+
const raw = (await ask(ui, key, title, seed)).trim().toLowerCase();
|
|
201
204
|
if (raw === "" || raw === "none" || raw === "off" || raw === "null") return null;
|
|
202
205
|
const value = Number(raw);
|
|
203
206
|
if (!Number.isFinite(value) || value < 0) {
|
|
@@ -232,6 +235,7 @@ async function askGates(
|
|
|
232
235
|
// found, so it is on screen either way.
|
|
233
236
|
const raw = await ask(
|
|
234
237
|
ui,
|
|
238
|
+
`pre-push-gates.${repoName}`,
|
|
235
239
|
`Pre-push gates for ${repoName} — exactly what CI runs, comma separated`,
|
|
236
240
|
formatGates(seed.length > 0 ? seed : probed),
|
|
237
241
|
);
|
|
@@ -263,6 +267,7 @@ async function askReportScope(ui: WizardUi, current: ReportScopeChoice): Promise
|
|
|
263
267
|
const options = REPORT_SCOPE_CHOICES.map((c) => ({ label: c.label, description: c.description }));
|
|
264
268
|
const at = REPORT_SCOPE_CHOICES.findIndex((c) => c.scope === current);
|
|
265
269
|
const picked = await ui.select("What should the orchestrator report unprompted?", options, {
|
|
270
|
+
key: "report-scope",
|
|
266
271
|
initialIndex: at === -1 ? 0 : at,
|
|
267
272
|
});
|
|
268
273
|
if (picked === undefined) throw new Cancelled();
|
|
@@ -292,6 +297,7 @@ async function askReportScope(ui: WizardUi, current: ReportScopeChoice): Promise
|
|
|
292
297
|
*/
|
|
293
298
|
async function askLiteral<T extends string>(
|
|
294
299
|
ui: WizardUi,
|
|
300
|
+
key: string,
|
|
295
301
|
title: string,
|
|
296
302
|
values: readonly T[],
|
|
297
303
|
described: { readonly [K in T]: string },
|
|
@@ -301,7 +307,7 @@ async function askLiteral<T extends string>(
|
|
|
301
307
|
const picked = await ui.select(
|
|
302
308
|
title,
|
|
303
309
|
values.map((v) => ({ label: v, description: described[v] })),
|
|
304
|
-
{ initialIndex: at === -1 ? 0 : at },
|
|
310
|
+
{ key, initialIndex: at === -1 ? 0 : at },
|
|
305
311
|
);
|
|
306
312
|
if (picked === undefined) throw new Cancelled();
|
|
307
313
|
|
|
@@ -336,8 +342,13 @@ function parseNameList(answer: string): string[] {
|
|
|
336
342
|
|
|
337
343
|
/** Check names, artefacts, environments: open-ended lists this package cannot
|
|
338
344
|
* enumerate, so the only validation is the shape. */
|
|
339
|
-
async function askNameList(
|
|
340
|
-
|
|
345
|
+
async function askNameList(
|
|
346
|
+
ui: WizardUi,
|
|
347
|
+
key: string,
|
|
348
|
+
title: string,
|
|
349
|
+
seed: readonly string[],
|
|
350
|
+
): Promise<string[]> {
|
|
351
|
+
return parseNameList(await ask(ui, key, title, formatNameList(seed)));
|
|
341
352
|
}
|
|
342
353
|
|
|
343
354
|
/**
|
|
@@ -362,6 +373,7 @@ async function askReleaseRequirements(
|
|
|
362
373
|
);
|
|
363
374
|
const answered = await askValid(
|
|
364
375
|
ui,
|
|
376
|
+
"release-requirements",
|
|
365
377
|
`Release — what must have landed first (any of ${accepted}, comma separated, or "${EMPTY_LIST}")`,
|
|
366
378
|
formatNameList(prior),
|
|
367
379
|
(value) => {
|
|
@@ -398,11 +410,13 @@ async function askPolicyPreconditions(
|
|
|
398
410
|
const merge = {
|
|
399
411
|
requiredChecks: await askNameList(
|
|
400
412
|
ui,
|
|
413
|
+
"merge-required-checks",
|
|
401
414
|
`Merge — required checks (comma separated, "${EMPTY_LIST}" = every check the PR reports)`,
|
|
402
415
|
prior.merge.requiredChecks,
|
|
403
416
|
),
|
|
404
417
|
baseFreshness: await askLiteral(
|
|
405
418
|
ui,
|
|
419
|
+
"merge-base-freshness",
|
|
406
420
|
"Merge — must the PR be level with its base?",
|
|
407
421
|
BASE_FRESHNESS,
|
|
408
422
|
BASE_FRESHNESS_CHOICES,
|
|
@@ -410,6 +424,7 @@ async function askPolicyPreconditions(
|
|
|
410
424
|
),
|
|
411
425
|
drafts: await askLiteral(
|
|
412
426
|
ui,
|
|
427
|
+
"merge-drafts",
|
|
413
428
|
"Merge — draft pull requests",
|
|
414
429
|
DRAFT_POLICIES,
|
|
415
430
|
DRAFT_POLICY_CHOICES,
|
|
@@ -417,6 +432,7 @@ async function askPolicyPreconditions(
|
|
|
417
432
|
),
|
|
418
433
|
whenBehindBase: await askLiteral(
|
|
419
434
|
ui,
|
|
435
|
+
"merge-behind-base",
|
|
420
436
|
"Merge — a green PR that fell behind its base",
|
|
421
437
|
BEHIND_BASE_ACTIONS,
|
|
422
438
|
BEHIND_BASE_CHOICES,
|
|
@@ -430,16 +446,19 @@ async function askPolicyPreconditions(
|
|
|
430
446
|
requires: await askReleaseRequirements(ui, prior.release.requires),
|
|
431
447
|
requiredChecks: await askNameList(
|
|
432
448
|
ui,
|
|
449
|
+
"release-required-checks",
|
|
433
450
|
`Release — required checks (comma separated, "${EMPTY_LIST}" = every check the branch reports)`,
|
|
434
451
|
prior.release.requiredChecks,
|
|
435
452
|
),
|
|
436
453
|
artefacts: await askNameList(
|
|
437
454
|
ui,
|
|
455
|
+
"release-artefacts",
|
|
438
456
|
`Release — artefacts this project ships (comma separated, or "${EMPTY_LIST}")`,
|
|
439
457
|
prior.release.artefacts,
|
|
440
458
|
),
|
|
441
459
|
environments: await askNameList(
|
|
442
460
|
ui,
|
|
461
|
+
"release-environments",
|
|
443
462
|
`Release — environments a deploy may target (comma separated, or "${EMPTY_LIST}")`,
|
|
444
463
|
prior.release.environments,
|
|
445
464
|
),
|
|
@@ -487,6 +506,7 @@ async function askAuthority(
|
|
|
487
506
|
): Promise<ProjectConfig["authority"]> {
|
|
488
507
|
const merge = await askYesNo(
|
|
489
508
|
ui,
|
|
509
|
+
"merge-authority",
|
|
490
510
|
"Merge authority",
|
|
491
511
|
"Delegate PR merging to the orchestrator session? It would land green PRs one at a time, each " +
|
|
492
512
|
"re-checked against the base branch first. Default: humans merge" +
|
|
@@ -496,6 +516,7 @@ async function askAuthority(
|
|
|
496
516
|
const unlockCount = unlock.gates.length + unlock.policy.length;
|
|
497
517
|
const release = await askYesNo(
|
|
498
518
|
ui,
|
|
519
|
+
"release-authority",
|
|
499
520
|
"Release authority",
|
|
500
521
|
"Delegate release cutting to the orchestrator session? It would tag, pin and publish by the " +
|
|
501
522
|
"procedure you write into its brief — and its brief forbids cutting one before you have. " +
|
|
@@ -506,6 +527,7 @@ async function askAuthority(
|
|
|
506
527
|
);
|
|
507
528
|
const promotion = await askYesNo(
|
|
508
529
|
ui,
|
|
530
|
+
"promotion-authority",
|
|
509
531
|
"Promotion authority",
|
|
510
532
|
"Delegate promotion to the orchestrator session? Promotion is adding the queue label to an " +
|
|
511
533
|
"issue — the sign-off that lets a worker claim it, and the act that starts spend. " +
|
|
@@ -576,13 +598,14 @@ async function askJudgment(
|
|
|
576
598
|
const picked = await ui.select(
|
|
577
599
|
"Roadmap candidates found in GitHub",
|
|
578
600
|
[...roadmapChoices.map((label) => ({ label })), { label: other }],
|
|
579
|
-
{ initialIndex: 0 },
|
|
601
|
+
{ key: "roadmap-candidate", initialIndex: 0 },
|
|
580
602
|
);
|
|
581
603
|
if (picked === undefined) throw new Cancelled();
|
|
582
604
|
roadmapSeed = picked === other ? "" : picked;
|
|
583
605
|
}
|
|
584
606
|
const roadmap = await ask(
|
|
585
607
|
ui,
|
|
608
|
+
"roadmap",
|
|
586
609
|
"Where does the roadmap live, and what is the current priority?",
|
|
587
610
|
roadmapSeed,
|
|
588
611
|
);
|
|
@@ -622,26 +645,40 @@ async function askJudgment(
|
|
|
622
645
|
// so an operator has something to calibrate against.
|
|
623
646
|
judgment.boundary = await ask(
|
|
624
647
|
ui,
|
|
648
|
+
"release-boundary",
|
|
625
649
|
"Where does the orchestrator's leg END? One sentence " +
|
|
626
650
|
'(a real answer: "at the merged version pin — deploying it is operator territory")',
|
|
627
651
|
prior.boundary ?? "",
|
|
628
652
|
);
|
|
629
653
|
|
|
630
654
|
// All five, because the brief needs each and a missing one is a hole.
|
|
631
|
-
judgment.releaseWhat = await ask(
|
|
655
|
+
judgment.releaseWhat = await ask(
|
|
656
|
+
ui,
|
|
657
|
+
"release-what",
|
|
658
|
+
"Release — WHAT may be released, and from which branch?",
|
|
659
|
+
prior.releaseWhat ?? "",
|
|
660
|
+
);
|
|
632
661
|
judgment.releaseWhen = await ask(
|
|
633
662
|
ui,
|
|
663
|
+
"release-when",
|
|
634
664
|
"Release — WHEN: batched how, after which named checks are green?",
|
|
635
665
|
prior.releaseWhen ?? "",
|
|
636
666
|
);
|
|
637
667
|
judgment.releaseProof = await ask(
|
|
638
668
|
ui,
|
|
669
|
+
"release-proof",
|
|
639
670
|
"Release — WHAT PROOF must be held first (results actually read, not an impression)?",
|
|
640
671
|
prior.releaseProof ?? "",
|
|
641
672
|
);
|
|
642
|
-
judgment.releaseAsk = await ask(
|
|
673
|
+
judgment.releaseAsk = await ask(
|
|
674
|
+
ui,
|
|
675
|
+
"release-ask",
|
|
676
|
+
"Release — what must still be ASKED, every time?",
|
|
677
|
+
prior.releaseAsk ?? "",
|
|
678
|
+
);
|
|
643
679
|
judgment.releaseForbidden = await ask(
|
|
644
680
|
ui,
|
|
681
|
+
"release-forbidden",
|
|
645
682
|
"Release — what stays permanently FORBIDDEN?",
|
|
646
683
|
prior.releaseForbidden ?? "force-push, secrets, production data",
|
|
647
684
|
);
|
|
@@ -652,17 +689,19 @@ async function askJudgment(
|
|
|
652
689
|
// `RELEASE_REQUIREMENTS`, which are mechanical preconditions, not a unit.
|
|
653
690
|
judgment.worthCutting = await ask(
|
|
654
691
|
ui,
|
|
692
|
+
"release-worth-cutting",
|
|
655
693
|
"What is a release worth cutting? (a sprint, an epic's children all closed, N merged issues, urgency)",
|
|
656
694
|
prior.worthCutting ?? "",
|
|
657
695
|
);
|
|
658
696
|
|
|
659
|
-
judgment.rollbackOwner = await ask(ui, "Who owns the rollback?", prior.rollbackOwner ?? "");
|
|
697
|
+
judgment.rollbackOwner = await ask(ui, "rollback-owner", "Who owns the rollback?", prior.rollbackOwner ?? "");
|
|
660
698
|
if (namesAPerson(judgment.rollbackOwner)) {
|
|
661
699
|
// Honour the consequence rather than recording a contradiction: if a person
|
|
662
700
|
// rolls it back, that person owns the release, and the boundary belongs
|
|
663
701
|
// before the irreversible step whatever the authority answer sounded like.
|
|
664
702
|
judgment.rollbackMovesBoundary = await askYesNo(
|
|
665
703
|
ui,
|
|
704
|
+
"rollback-moves-boundary",
|
|
666
705
|
"Rollback owner is a person",
|
|
667
706
|
`You named "${judgment.rollbackOwner}" as the rollback owner. That person already owns the release, ` +
|
|
668
707
|
"so the honest configuration puts the orchestrator's boundary *before* the irreversible step — " +
|
|
@@ -671,6 +710,7 @@ async function askJudgment(
|
|
|
671
710
|
if (judgment.rollbackMovesBoundary) {
|
|
672
711
|
judgment.boundary = await ask(
|
|
673
712
|
ui,
|
|
713
|
+
"release-boundary",
|
|
674
714
|
"Restate the boundary, ending before the irreversible step",
|
|
675
715
|
judgment.boundary ?? "",
|
|
676
716
|
);
|
|
@@ -713,6 +753,7 @@ async function askReleaseGrants(
|
|
|
713
753
|
for (const shape of RELEASE_SHAPES) {
|
|
714
754
|
const open = await askYesNo(
|
|
715
755
|
ui,
|
|
756
|
+
`release-tool-gate.${shape}`,
|
|
716
757
|
`Release tool gate — ${shape}`,
|
|
717
758
|
`Allow the orchestrator session to ${RELEASE_SHAPE_QUESTIONS[shape]}? Grant this only when the ` +
|
|
718
759
|
"operator brief carries the procedure it must follow. A worker session is refused this " +
|
|
@@ -733,6 +774,7 @@ async function askReleaseGrants(
|
|
|
733
774
|
async function askOrchestratorMode(ui: WizardUi, prior: OrchestratorMode): Promise<OrchestratorMode> {
|
|
734
775
|
const external = await askYesNo(
|
|
735
776
|
ui,
|
|
777
|
+
"orchestrator-mode",
|
|
736
778
|
"Orchestrator session",
|
|
737
779
|
"Do you already run your own orchestrator session for this project — a visible TUI session, say? " +
|
|
738
780
|
"Then the daemon starts none of its own, and posts tier-1 escalations as issue comments for yours " +
|
|
@@ -762,6 +804,7 @@ async function askGraphRoot(
|
|
|
762
804
|
): Promise<string | undefined> {
|
|
763
805
|
const wanted = await askYesNo(
|
|
764
806
|
ui,
|
|
807
|
+
"code-graph-enabled",
|
|
765
808
|
"Code-graph discovery",
|
|
766
809
|
"Set up code-graph discovery for workers? Workers spend most of their turn budget finding code; " +
|
|
767
810
|
'a graph answers "who calls this" in one call. Conductor keeps one disposable clone per repo, ' +
|
|
@@ -772,6 +815,7 @@ async function askGraphRoot(
|
|
|
772
815
|
|
|
773
816
|
return await askValid(
|
|
774
817
|
ui,
|
|
818
|
+
"code-graph-root",
|
|
775
819
|
`Root for those clones — one per repo (${repoNames.join(", ")}) is created under it`,
|
|
776
820
|
prior ?? defaultGraphRoot(trackerRepo),
|
|
777
821
|
(v) =>
|
|
@@ -791,6 +835,7 @@ async function askOrchestratorBrief(ui: WizardUi, a: SetupAnswers): Promise<bool
|
|
|
791
835
|
const path = orchestratorBriefPath(a);
|
|
792
836
|
const wanted = await askYesNo(
|
|
793
837
|
ui,
|
|
838
|
+
"write-operator-brief",
|
|
794
839
|
`Write ${ORCHESTRATOR_BRIEF_NAME} + ${POLICY_BRIEF_NAME} under ${dirname(path)}?`,
|
|
795
840
|
`Writes composed ${ORCHESTRATOR_BRIEF_NAME} (package floor, refreshed each tick) and ${POLICY_BRIEF_NAME} ` +
|
|
796
841
|
`(Releases, Project context, Reporting, Amendments — yours to edit via the Learning loop). ` +
|
|
@@ -801,6 +846,7 @@ async function askOrchestratorBrief(ui: WizardUi, a: SetupAnswers): Promise<bool
|
|
|
801
846
|
|
|
802
847
|
return await askYesNo(
|
|
803
848
|
ui,
|
|
849
|
+
"overwrite-operator-brief",
|
|
804
850
|
`Overwrite existing ${ORCHESTRATOR_BRIEF_NAME} / ${POLICY_BRIEF_NAME}?`,
|
|
805
851
|
`${path} already exists. Overwriting replaces the composed brief and POLICY.md scaffold — any policy you wrote is lost.`,
|
|
806
852
|
);
|
|
@@ -990,6 +1036,7 @@ async function proseFromRepos(ui: WizardUi, a: SetupAnswers): Promise<ProbedPros
|
|
|
990
1036
|
const askTrackerAndRepos: AreaAsker = async (ui, a, probes) => {
|
|
991
1037
|
const trackerRepo = await askValid(
|
|
992
1038
|
ui,
|
|
1039
|
+
"tracker-repo",
|
|
993
1040
|
"Tracker repo (owner/repo) — where ready issues live",
|
|
994
1041
|
a.trackerRepo,
|
|
995
1042
|
(v) => (REPO_RE.test(v) ? undefined : `"${v}" is not owner/repo — e.g. acme/planning.`),
|
|
@@ -997,6 +1044,7 @@ const askTrackerAndRepos: AreaAsker = async (ui, a, probes) => {
|
|
|
997
1044
|
|
|
998
1045
|
const queueLabel = await ask(
|
|
999
1046
|
ui,
|
|
1047
|
+
"queue-label",
|
|
1000
1048
|
"Queue label — the human sign-off that makes an issue claimable",
|
|
1001
1049
|
a.queueLabel,
|
|
1002
1050
|
);
|
|
@@ -1007,18 +1055,20 @@ const askTrackerAndRepos: AreaAsker = async (ui, a, probes) => {
|
|
|
1007
1055
|
const stateLabels: SetupAnswers["stateLabels"] = { ...a.stateLabels };
|
|
1008
1056
|
const customiseStates = await askYesNo(
|
|
1009
1057
|
ui,
|
|
1058
|
+
"customise-state-labels",
|
|
1010
1059
|
"State labels",
|
|
1011
1060
|
`The conductor writes back "${stateLabels.inProgress}", "${stateLabels.blocked}" and ` +
|
|
1012
1061
|
`"${stateLabels.failed}" so the tracker alone shows live state. Rename them?`,
|
|
1013
1062
|
);
|
|
1014
1063
|
if (customiseStates) {
|
|
1015
|
-
stateLabels.inProgress = await ask(ui, "Label for a run in progress", stateLabels.inProgress);
|
|
1016
|
-
stateLabels.blocked = await ask(ui, "Label for a run parked on a human", stateLabels.blocked);
|
|
1017
|
-
stateLabels.failed = await ask(ui, "Label for a run that gave up", stateLabels.failed);
|
|
1064
|
+
stateLabels.inProgress = await ask(ui, "state-label-in-progress", "Label for a run in progress", stateLabels.inProgress);
|
|
1065
|
+
stateLabels.blocked = await ask(ui, "state-label-blocked", "Label for a run parked on a human", stateLabels.blocked);
|
|
1066
|
+
stateLabels.failed = await ask(ui, "state-label-failed", "Label for a run that gave up", stateLabels.failed);
|
|
1018
1067
|
}
|
|
1019
1068
|
|
|
1020
1069
|
const routingLabelPrefix = await ask(
|
|
1021
1070
|
ui,
|
|
1071
|
+
"routing-label-prefix",
|
|
1022
1072
|
"Routing label prefix — an issue picks its checkout with <prefix><repo>",
|
|
1023
1073
|
a.routingLabelPrefix,
|
|
1024
1074
|
);
|
|
@@ -1028,18 +1078,21 @@ const askTrackerAndRepos: AreaAsker = async (ui, a, probes) => {
|
|
|
1028
1078
|
const seed = a.targetRepos[i];
|
|
1029
1079
|
const name = await askValid(
|
|
1030
1080
|
ui,
|
|
1081
|
+
`routing-key.${i + 1}`,
|
|
1031
1082
|
`Routing key for repo ${i + 1} — the "${routingLabelPrefix}<key>" label an issue carries`,
|
|
1032
1083
|
seed?.name ?? "",
|
|
1033
1084
|
(v) => (v.length > 0 ? undefined : "A routing key is required, or no issue can reach this repo."),
|
|
1034
1085
|
);
|
|
1035
1086
|
const cloneUrl = await askValid(
|
|
1036
1087
|
ui,
|
|
1088
|
+
`clone-url.${name}`,
|
|
1037
1089
|
`Clone URL for ${routingLabelPrefix}${name}`,
|
|
1038
1090
|
seed?.cloneUrl ?? "",
|
|
1039
1091
|
(v) => (v.length > 0 ? undefined : "A clone URL is required — the daemon mirrors it before every run."),
|
|
1040
1092
|
);
|
|
1041
1093
|
const defaultBranch = await ask(
|
|
1042
1094
|
ui,
|
|
1095
|
+
`default-branch.${name}`,
|
|
1043
1096
|
`Default branch for ${name} — worktrees are cut from it and PRs target it`,
|
|
1044
1097
|
seed?.defaultBranch ?? SETUP_DEFAULTS.defaultBranch,
|
|
1045
1098
|
);
|
|
@@ -1051,6 +1104,7 @@ const askTrackerAndRepos: AreaAsker = async (ui, a, probes) => {
|
|
|
1051
1104
|
|
|
1052
1105
|
const more = await askYesNo(
|
|
1053
1106
|
ui,
|
|
1107
|
+
`another-repo.${i + 1}`,
|
|
1054
1108
|
"Another repo?",
|
|
1055
1109
|
`${targetRepos.map((r) => r.name).join(", ")} configured. Add another checkout this project routes to?`,
|
|
1056
1110
|
);
|
|
@@ -1121,6 +1175,7 @@ const askCaps: AreaAsker = async (ui, a) => {
|
|
|
1121
1175
|
: "";
|
|
1122
1176
|
const tuneCaps = await askYesNo(
|
|
1123
1177
|
ui,
|
|
1178
|
+
"tune-caps",
|
|
1124
1179
|
"Caps",
|
|
1125
1180
|
`Defaults: ${workersDefault} workers, ` +
|
|
1126
1181
|
`${spendLabel}, ${DEFAULT_CAPS.workerMaxTurns} base / ` +
|
|
@@ -1143,16 +1198,19 @@ const askCaps: AreaAsker = async (ui, a) => {
|
|
|
1143
1198
|
// not silently go unasked.
|
|
1144
1199
|
caps.maxConcurrentWorkers = await askNumber(
|
|
1145
1200
|
ui,
|
|
1201
|
+
"max-concurrent-workers",
|
|
1146
1202
|
"Max concurrent workers",
|
|
1147
1203
|
workersDefault,
|
|
1148
1204
|
);
|
|
1149
1205
|
caps.dailySpendUsd = await askSpendCap(
|
|
1150
1206
|
ui,
|
|
1207
|
+
"daily-spend-usd",
|
|
1151
1208
|
"Spend ceiling per rolling day (USD) — blank = no spend cap",
|
|
1152
1209
|
caps.dailySpendUsd !== undefined ? caps.dailySpendUsd : DEFAULT_CAPS.dailySpendUsd,
|
|
1153
1210
|
);
|
|
1154
1211
|
const workerMaxTurns = await askNumber(
|
|
1155
1212
|
ui,
|
|
1213
|
+
"worker-max-turns",
|
|
1156
1214
|
"Turn ceiling per worker",
|
|
1157
1215
|
caps.workerMaxTurns ?? DEFAULT_CAPS.workerMaxTurns,
|
|
1158
1216
|
);
|
|
@@ -1164,6 +1222,7 @@ const askCaps: AreaAsker = async (ui, a) => {
|
|
|
1164
1222
|
);
|
|
1165
1223
|
const workerMaxTurnsCeiling = await askNumber(
|
|
1166
1224
|
ui,
|
|
1225
|
+
"worker-max-turns-ceiling",
|
|
1167
1226
|
"Maximum turn ceiling for one issue",
|
|
1168
1227
|
turnCeilingFallback,
|
|
1169
1228
|
);
|
|
@@ -1179,16 +1238,19 @@ const askCaps: AreaAsker = async (ui, a) => {
|
|
|
1179
1238
|
}
|
|
1180
1239
|
caps.workerWallClockMs = await askNumber(
|
|
1181
1240
|
ui,
|
|
1241
|
+
"worker-wall-clock-ms",
|
|
1182
1242
|
"Wall-clock ceiling per worker (ms)",
|
|
1183
1243
|
caps.workerWallClockMs ?? DEFAULT_CAPS.workerWallClockMs,
|
|
1184
1244
|
);
|
|
1185
1245
|
caps.maxAttemptsPerIssue = await askNumber(
|
|
1186
1246
|
ui,
|
|
1247
|
+
"max-attempts-per-issue",
|
|
1187
1248
|
"Failed implementation attempts per issue before escalation",
|
|
1188
1249
|
caps.maxAttemptsPerIssue ?? DEFAULT_CAPS.maxAttemptsPerIssue,
|
|
1189
1250
|
);
|
|
1190
1251
|
caps.maxContinuationsPerIssue = await askNumber(
|
|
1191
1252
|
ui,
|
|
1253
|
+
"max-continuations-per-issue",
|
|
1192
1254
|
"Operational continuations per issue before escalation",
|
|
1193
1255
|
caps.maxContinuationsPerIssue ?? DEFAULT_CAPS.maxContinuationsPerIssue,
|
|
1194
1256
|
);
|
|
@@ -1198,7 +1260,12 @@ const askCaps: AreaAsker = async (ui, a) => {
|
|
|
1198
1260
|
/** Outside the caps block: a model is not a ceiling, and an operator who left
|
|
1199
1261
|
* the caps alone may still want workers on a cheaper model. */
|
|
1200
1262
|
const askWorkerModel: AreaAsker = async (ui, a) => {
|
|
1201
|
-
const answered = await ask(
|
|
1263
|
+
const answered = await ask(
|
|
1264
|
+
ui,
|
|
1265
|
+
"worker-model",
|
|
1266
|
+
"Worker model pattern (blank = harness default)",
|
|
1267
|
+
a.workerModel ?? "",
|
|
1268
|
+
);
|
|
1202
1269
|
const next: SetupAnswers = { ...a };
|
|
1203
1270
|
if (answered.trim().length === 0) delete next.workerModel;
|
|
1204
1271
|
else next.workerModel = answered.trim();
|
|
@@ -1216,6 +1283,7 @@ const askOmpSettings: AreaAsker = async (ui, a) => {
|
|
|
1216
1283
|
const seed = a.ompSettings === undefined ? "" : JSON.stringify(a.ompSettings);
|
|
1217
1284
|
const answered = await askValid(
|
|
1218
1285
|
ui,
|
|
1286
|
+
"omp-settings",
|
|
1219
1287
|
"Omp settings overlay for workers (YAML, blank = none — omp's schema, not conductor's)",
|
|
1220
1288
|
seed,
|
|
1221
1289
|
(value) => {
|
|
@@ -1280,12 +1348,18 @@ const askEscalation: AreaAsker = async (ui, a) => {
|
|
|
1280
1348
|
if (telegramChatId === undefined && telegram.pairedOwnerId !== undefined) {
|
|
1281
1349
|
const usePaired = await askYesNo(
|
|
1282
1350
|
ui,
|
|
1351
|
+
"tier2-use-paired-chat",
|
|
1283
1352
|
"Tier-2 escalations",
|
|
1284
1353
|
`omp-telegram is paired with chat ${telegram.pairedOwnerId}. Page it when a run is stuck?`,
|
|
1285
1354
|
);
|
|
1286
1355
|
if (usePaired) telegramChatId = telegram.pairedOwnerId;
|
|
1287
1356
|
} else {
|
|
1288
|
-
const answered = await ask(
|
|
1357
|
+
const answered = await ask(
|
|
1358
|
+
ui,
|
|
1359
|
+
"tier2-chat-id",
|
|
1360
|
+
"Telegram chat id for tier-2 escalations (blank for none)",
|
|
1361
|
+
telegramChatId ?? "",
|
|
1362
|
+
);
|
|
1289
1363
|
telegramChatId = answered.length > 0 ? answered : undefined;
|
|
1290
1364
|
}
|
|
1291
1365
|
} else {
|
|
@@ -1303,6 +1377,7 @@ const askEscalation: AreaAsker = async (ui, a) => {
|
|
|
1303
1377
|
|
|
1304
1378
|
const fallbackToIssueComment = await askYesNo(
|
|
1305
1379
|
ui,
|
|
1380
|
+
"escalation-fallback",
|
|
1306
1381
|
"Escalation fallback",
|
|
1307
1382
|
"Also comment on the issue when a run escalates? Recommended: a chat message you miss is a run nobody sees.",
|
|
1308
1383
|
);
|
|
@@ -1346,6 +1421,7 @@ async function askTelegramTopicId(
|
|
|
1346
1421
|
];
|
|
1347
1422
|
const priorIdx = prior === undefined ? -1 : claimed.findIndex((t) => t.threadId === prior);
|
|
1348
1423
|
const picked = await ui.select("Telegram forum topic for tier-2 pages", options, {
|
|
1424
|
+
key: "tier2-topic",
|
|
1349
1425
|
initialIndex: priorIdx >= 0 ? priorIdx : claimed.length + 1,
|
|
1350
1426
|
});
|
|
1351
1427
|
if (picked === undefined) throw new Cancelled();
|
|
@@ -1358,6 +1434,7 @@ async function askTelegramTopicId(
|
|
|
1358
1434
|
|
|
1359
1435
|
const answered = await ask(
|
|
1360
1436
|
ui,
|
|
1437
|
+
"tier2-topic-id",
|
|
1361
1438
|
"Telegram forum topic id (blank for flat chat)",
|
|
1362
1439
|
prior !== undefined ? String(prior) : "",
|
|
1363
1440
|
);
|
|
@@ -1388,7 +1465,7 @@ const askReporting: AreaAsker = async (ui, a) => {
|
|
|
1388
1465
|
description: "hold non-bypass interruptions outside selected local working hours",
|
|
1389
1466
|
},
|
|
1390
1467
|
],
|
|
1391
|
-
{ initialIndex: a.availability === undefined ? 0 : 1 },
|
|
1468
|
+
{ key: "operator-availability", initialIndex: a.availability === undefined ? 0 : 1 },
|
|
1392
1469
|
);
|
|
1393
1470
|
if (mode === undefined) throw new Cancelled();
|
|
1394
1471
|
|
|
@@ -1420,6 +1497,7 @@ const askReporting: AreaAsker = async (ui, a) => {
|
|
|
1420
1497
|
: a.dailyDigestAt ?? (a.digestCadence === "daily" ? "model-timed" : fallback);
|
|
1421
1498
|
const schedule = await askValid(
|
|
1422
1499
|
ui,
|
|
1500
|
+
"digest-schedule",
|
|
1423
1501
|
'Daily rollup time in that timezone / digest cadence ("per-tick", "model-timed", "off", or 24h HH:MM)',
|
|
1424
1502
|
shown,
|
|
1425
1503
|
(value) =>
|
|
@@ -1444,6 +1522,7 @@ const askReporting: AreaAsker = async (ui, a) => {
|
|
|
1444
1522
|
a.reportingTimezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC";
|
|
1445
1523
|
const reportingTimezone = await askValid(
|
|
1446
1524
|
ui,
|
|
1525
|
+
"reporting-timezone",
|
|
1447
1526
|
"Operator timezone (IANA, for example Europe/London)",
|
|
1448
1527
|
defaultZone,
|
|
1449
1528
|
(value) => {
|
|
@@ -1457,6 +1536,7 @@ const askReporting: AreaAsker = async (ui, a) => {
|
|
|
1457
1536
|
);
|
|
1458
1537
|
const daysText = await askValid(
|
|
1459
1538
|
ui,
|
|
1539
|
+
"working-weekdays",
|
|
1460
1540
|
"Working weekdays (comma-separated: mon,tue,wed,thu,fri,sat,sun)",
|
|
1461
1541
|
a.availability?.days.join(",") ?? "mon,tue,wed,thu,fri",
|
|
1462
1542
|
(value) => {
|
|
@@ -1470,12 +1550,14 @@ const askReporting: AreaAsker = async (ui, a) => {
|
|
|
1470
1550
|
const days = daysText.split(",").map((day) => day.trim().toLowerCase() as Weekday);
|
|
1471
1551
|
const start = await askValid(
|
|
1472
1552
|
ui,
|
|
1553
|
+
"availability-start",
|
|
1473
1554
|
"Availability starts (24h HH:MM)",
|
|
1474
1555
|
a.availability?.start ?? "09:00",
|
|
1475
1556
|
(value) => (/^([01]\d|2[0-3]):[0-5]\d$/.test(value) ? undefined : "Use 24h HH:MM."),
|
|
1476
1557
|
);
|
|
1477
1558
|
const end = await askValid(
|
|
1478
1559
|
ui,
|
|
1560
|
+
"availability-end",
|
|
1479
1561
|
"Availability ends (24h HH:MM)",
|
|
1480
1562
|
a.availability?.end ?? "17:00",
|
|
1481
1563
|
(value) =>
|
|
@@ -1487,6 +1569,7 @@ const askReporting: AreaAsker = async (ui, a) => {
|
|
|
1487
1569
|
);
|
|
1488
1570
|
const bypassText = await askValid(
|
|
1489
1571
|
ui,
|
|
1572
|
+
"quiet-hours-bypass",
|
|
1490
1573
|
`Quiet-hours bypass categories (comma-separated; "none" = none; choices: ${INTERRUPT_CATEGORIES.join(",")})`,
|
|
1491
1574
|
a.availability === undefined ? "fleet-stopped" : a.availability.bypass.join(",") || "none",
|
|
1492
1575
|
(value) => {
|
|
@@ -1634,7 +1717,7 @@ async function chooseAmendArea(ui: WizardUi, prior: ProjectConfig, defaults: Cap
|
|
|
1634
1717
|
description: "full interview for a new project; existing projects stay as they are",
|
|
1635
1718
|
},
|
|
1636
1719
|
],
|
|
1637
|
-
{ initialIndex: 0 },
|
|
1720
|
+
{ key: `existing-project-action.${prior.name}`, initialIndex: 0 },
|
|
1638
1721
|
);
|
|
1639
1722
|
if (mode === undefined) throw new Cancelled();
|
|
1640
1723
|
if (mode === ADD_PROJECT) return { kind: "add-project" };
|
|
@@ -1650,7 +1733,7 @@ async function chooseAmendArea(ui: WizardUi, prior: ProjectConfig, defaults: Cap
|
|
|
1650
1733
|
const picked = await ui.select(
|
|
1651
1734
|
"Which area? Each row shows what it says now",
|
|
1652
1735
|
choices.map((c) => ({ label: c.label, description: c.description })),
|
|
1653
|
-
{ initialIndex: 0 },
|
|
1736
|
+
{ key: `amend-area.${prior.name}`, initialIndex: 0 },
|
|
1654
1737
|
);
|
|
1655
1738
|
if (picked === undefined) throw new Cancelled();
|
|
1656
1739
|
|
|
@@ -1693,6 +1776,7 @@ async function collectAnswers(
|
|
|
1693
1776
|
|
|
1694
1777
|
const projectName = await askValid(
|
|
1695
1778
|
ui,
|
|
1779
|
+
"project-name",
|
|
1696
1780
|
"Project name",
|
|
1697
1781
|
projectArg ?? seed.projectName,
|
|
1698
1782
|
(v) => (v.length > 0 ? undefined : "A name is required — it is how `omp-conductor status --project <name>` finds this project."),
|
|
@@ -1821,7 +1905,7 @@ export async function collectSetup(
|
|
|
1821
1905
|
description: "overwrites this project's config entry on apply; other projects untouched",
|
|
1822
1906
|
},
|
|
1823
1907
|
],
|
|
1824
|
-
{ initialIndex: 0 },
|
|
1908
|
+
{ key: `duplicate-project-action.${answers.projectName}`, initialIndex: 0 },
|
|
1825
1909
|
);
|
|
1826
1910
|
if (mode === undefined) throw new Cancelled();
|
|
1827
1911
|
if (mode.startsWith("Amend")) {
|
|
@@ -1866,7 +1950,7 @@ export async function collectSetup(
|
|
|
1866
1950
|
description: "overwrites this project's config entry on apply; other projects untouched",
|
|
1867
1951
|
},
|
|
1868
1952
|
],
|
|
1869
|
-
{ initialIndex: 0 },
|
|
1953
|
+
{ key: `duplicate-project-action.${answers.projectName}`, initialIndex: 0 },
|
|
1870
1954
|
);
|
|
1871
1955
|
if (mode === undefined) throw new Cancelled();
|
|
1872
1956
|
if (mode.startsWith("Amend")) {
|
|
@@ -1884,6 +1968,7 @@ export async function collectSetup(
|
|
|
1884
1968
|
"Walks every question again and overwrites this project's config entry on apply. " +
|
|
1885
1969
|
"Other projects are left alone. Cancel and pick \"Change one area\" to amend without a full replace, " +
|
|
1886
1970
|
"or \"Add another project\" to create a neighbour.",
|
|
1971
|
+
{ key: `replace-project.${prior.name}` },
|
|
1887
1972
|
);
|
|
1888
1973
|
if (replace !== true) throw new Cancelled();
|
|
1889
1974
|
return { answers: await collectAnswers(ui, prior, projectArg, probes) };
|
|
@@ -1990,6 +2075,7 @@ async function offerCodeGraph(
|
|
|
1990
2075
|
"Clones each index-only checkout as you, installs and enables the reindex timer as root, then seeds one " +
|
|
1991
2076
|
"indexing run so the first fetch happens while you watch. Minutes per repo. " +
|
|
1992
2077
|
"`omp-conductor setup graph` does the same later; `--no-seed` skips the seeding run.",
|
|
2078
|
+
{ key: `offer-code-graph.${project.name}` },
|
|
1993
2079
|
);
|
|
1994
2080
|
if (wantsGraph === true) await graphInstall(project, ui);
|
|
1995
2081
|
else
|
|
@@ -2202,7 +2288,7 @@ export async function setup(
|
|
|
2202
2288
|
amend === undefined ? "Apply this setup?" : `Apply this change to ${AMEND_AREAS[amend.area].name}?`,
|
|
2203
2289
|
REVIEW_CHOICES,
|
|
2204
2290
|
// Bare Enter lands on Review — the one row with no effect.
|
|
2205
|
-
{ initialIndex: 2 },
|
|
2291
|
+
{ key: "setup-review", initialIndex: 2 },
|
|
2206
2292
|
);
|
|
2207
2293
|
if (choice === undefined) {
|
|
2208
2294
|
ui.notify("Setup cancelled — nothing was changed, and the answers were discarded.", "info");
|
|
@@ -2229,7 +2315,7 @@ export async function setup(
|
|
|
2229
2315
|
const pickedArea = await ui.select(
|
|
2230
2316
|
"Edit which area?",
|
|
2231
2317
|
INTERVIEW_AREAS.map((area) => ({ label: area.label })),
|
|
2232
|
-
{ initialIndex: 0 },
|
|
2318
|
+
{ key: "setup-edit-area", initialIndex: 0 },
|
|
2233
2319
|
);
|
|
2234
2320
|
if (pickedArea === undefined) {
|
|
2235
2321
|
ui.notify("Editing cancelled — the answers stand as they were.", "info");
|
|
@@ -2270,12 +2356,16 @@ export async function setup(
|
|
|
2270
2356
|
// last re-derivation: the loop above can only break here on a final "Apply",
|
|
2271
2357
|
// so this one-writer sequence runs exactly once per setup run.
|
|
2272
2358
|
prepareConductor(plan.project.name);
|
|
2273
|
-
const created = await
|
|
2359
|
+
const created = await withProgress("Creating tracker labels", "Tracker labels ready", () =>
|
|
2360
|
+
apply.createLabels(answers.trackerRepo, plan.labels),
|
|
2361
|
+
);
|
|
2274
2362
|
saveConfig(plan.nextConfig);
|
|
2275
2363
|
const briefPath = answers.writeOrchestratorBrief ? writeOrchestratorBrief(answers, prose) : undefined;
|
|
2276
2364
|
const runtimeFiles = writeHostRuntime(plan.runtime);
|
|
2277
2365
|
for (const warning of runtimeFiles.warnings) ui.notify(warning, "warning");
|
|
2278
|
-
const smoke = await
|
|
2366
|
+
const smoke = await withProgress("Running setup smoke", "Setup smoke passed", () =>
|
|
2367
|
+
apply.smoke(plan.project.name),
|
|
2368
|
+
);
|
|
2279
2369
|
let smokeLine =
|
|
2280
2370
|
`paused daemon --once; temporary /healthz on :${smoke.daemon.port}; ` +
|
|
2281
2371
|
`stored status for ${smoke.status.project}`;
|
|
@@ -2392,6 +2482,7 @@ export async function setup(
|
|
|
2392
2482
|
`${plan.runtime.installedAction === "create" ? "Installs" : "Updates"} ${plan.runtime.installedPath} from ` +
|
|
2393
2483
|
`${plan.runtime.service.path}, then enables and restarts it. Needs root, one step at a time, and shows every ` +
|
|
2394
2484
|
"command before it runs. Skipping is fine — `omp-conductor setup host` does exactly this later.",
|
|
2485
|
+
{ key: `install-daemon.${plan.project.name}` },
|
|
2395
2486
|
);
|
|
2396
2487
|
if (install === true)
|
|
2397
2488
|
await apply.hostInstall(
|