omp-conductor 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +549 -234
- package/package.json +8 -5
- package/schema/config.schema.json +609 -0
- package/src/availability.ts +165 -0
- package/src/board.ts +19 -32
- package/src/brief-upgrade.ts +1 -1
- package/src/briefs/orchestrator.md +72 -31
- package/src/briefs/policy.md +48 -36
- package/src/briefs/probes/gates.md +51 -0
- package/src/briefs/probes/project-context.md +59 -0
- package/src/briefs/probes/release-procedure.md +81 -0
- package/src/cli.ts +356 -212
- package/src/config-schema.ts +352 -0
- package/src/config.ts +1037 -679
- package/src/confinement.ts +54 -0
- package/src/daemon.ts +644 -390
- package/src/diff-flags.ts +73 -4
- package/src/digest-schedule.ts +92 -24
- package/src/escalate.ts +89 -22
- package/src/fleet.ts +351 -46
- package/src/generate-schema.ts +21 -0
- package/src/graph.ts +3 -3
- package/src/host.ts +16 -0
- package/src/omp.ts +21 -1
- package/src/orchestrator-tick.ts +732 -56
- package/src/privileged.ts +264 -0
- package/src/reports.ts +203 -6
- package/src/session-host.ts +3 -0
- package/src/setup-host.ts +209 -24
- package/src/setup-install.ts +320 -0
- package/src/setup-probe.ts +412 -0
- package/src/setup-wizard.ts +1946 -0
- package/src/setup.ts +457 -53
- package/src/store.ts +610 -98
- package/src/tracker/github.ts +43 -5
- package/src/types.ts +153 -14
- package/src/upgrade.ts +44 -10
- package/src/verbs/actions.ts +131 -13
- package/src/verbs/server.ts +40 -18
- package/src/wizard-ui.ts +249 -0
- package/src/worker.ts +24 -7
- package/skills/conductor-onboarding/SKILL.md +0 -748
- package/skills/conductor-update/SKILL.md +0 -51
- package/src/plugin.ts +0 -1495
package/src/setup.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The onboarding core behind
|
|
2
|
+
* The onboarding core behind `omp-conductor setup`.
|
|
3
3
|
*
|
|
4
4
|
* Everything here is headless and synchronously testable: the plugin owns the
|
|
5
5
|
* dialogs, this module owns the decisions. That split is the point — a wizard
|
|
@@ -52,22 +52,73 @@ import {
|
|
|
52
52
|
DEFAULT_REPORT_SCOPE,
|
|
53
53
|
DENIED_RELEASE_GRANTS,
|
|
54
54
|
RELEASE_SHAPES,
|
|
55
|
+
WEEKDAYS,
|
|
55
56
|
type BaseFreshness,
|
|
56
57
|
type BehindBaseAction,
|
|
57
58
|
type Caps,
|
|
58
59
|
type ConductorConfig,
|
|
60
|
+
type DigestCadence,
|
|
59
61
|
type DraftPolicy,
|
|
60
62
|
type OrchestratorMode,
|
|
63
|
+
type InterruptCategory,
|
|
61
64
|
type ProjectConfig,
|
|
62
65
|
type ProjectPolicy,
|
|
63
66
|
type ReleaseRequirement,
|
|
64
|
-
type ReportScope,
|
|
65
67
|
type ReportScopeChoice,
|
|
66
68
|
type ReportingPolicy,
|
|
69
|
+
type Weekday,
|
|
70
|
+
type WeeklyAvailability,
|
|
67
71
|
type RepoTarget,
|
|
68
72
|
type ResolvedGrants,
|
|
69
73
|
} from "./types.ts";
|
|
70
74
|
|
|
75
|
+
/**
|
|
76
|
+
* What the onboarding skill interviewed for and no probe can supply.
|
|
77
|
+
*
|
|
78
|
+
* The skill is explicit that reading a repo answers none of this: "a tracker
|
|
79
|
+
* shows what is open, never what matters" (`SKILL.md:337-341`), and a release
|
|
80
|
+
* boundary is a sentence only the operator can say. Every field here fills a
|
|
81
|
+
* stub the shipped `policy.md` already carries, which is what makes deleting the
|
|
82
|
+
* skill (#309) a move rather than a loss.
|
|
83
|
+
*
|
|
84
|
+
* Every field is optional because the object is what has been *collected so
|
|
85
|
+
* far*: an amend that never reaches these prompts carries none of them, and
|
|
86
|
+
* `judgment` itself is absent on a project re-read from disk, since none of this
|
|
87
|
+
* lives in `config.json`.
|
|
88
|
+
*
|
|
89
|
+
* `boundary` and everything under it is asked only when the orchestrator holds
|
|
90
|
+
* some release shape: a humans-release fleet has no boundary to draw, and asking
|
|
91
|
+
* for one would invite a procedure the config does not grant.
|
|
92
|
+
*/
|
|
93
|
+
export interface OperatorJudgment {
|
|
94
|
+
/** Where the roadmap lives and what is being shipped now. Always *asked*;
|
|
95
|
+
* absent means the interview has not reached it (or the operator left it blank). */
|
|
96
|
+
roadmap?: string;
|
|
97
|
+
/** Where the orchestrator's leg ends, in one sentence. */
|
|
98
|
+
boundary?: string;
|
|
99
|
+
/** What may be released, and from which branch. */
|
|
100
|
+
releaseWhat?: string;
|
|
101
|
+
/** Batched how, after which *named* checks are green. */
|
|
102
|
+
releaseWhen?: string;
|
|
103
|
+
/** The proof that must be held first — results read, not an impression. */
|
|
104
|
+
releaseProof?: string;
|
|
105
|
+
/** What must still be asked, every time. */
|
|
106
|
+
releaseAsk?: string;
|
|
107
|
+
/** What stays permanently forbidden. */
|
|
108
|
+
releaseForbidden?: string;
|
|
109
|
+
/** The batching unit in the operator's own vocabulary. */
|
|
110
|
+
worthCutting?: string;
|
|
111
|
+
/** Who owns the rollback. A named person moves the boundary earlier. */
|
|
112
|
+
rollbackOwner?: string;
|
|
113
|
+
/**
|
|
114
|
+
* Whether the operator accepted the boundary-moves-earlier consequence of
|
|
115
|
+
* naming a person as the rollback owner. Recorded rather than enforced: the
|
|
116
|
+
* wizard states the consequence and offers to move the boundary, and an
|
|
117
|
+
* operator who declines has made a choice, not a mistake.
|
|
118
|
+
*/
|
|
119
|
+
rollbackMovesBoundary?: boolean;
|
|
120
|
+
}
|
|
121
|
+
|
|
71
122
|
/**
|
|
72
123
|
* Every decision the wizard needs, in one plain object. Collected by the UI,
|
|
73
124
|
* consumed by `buildConfig`, so the prompt order can change without touching
|
|
@@ -89,11 +140,24 @@ export interface SetupAnswers {
|
|
|
89
140
|
*/
|
|
90
141
|
workerModel?: string;
|
|
91
142
|
telegramChatId?: string;
|
|
143
|
+
/** Forum topic for tier-2 Telegram pages; absent keeps flat-chat 0.13 behaviour. */
|
|
144
|
+
telegramTopicId?: number;
|
|
92
145
|
fallbackToIssueComment: boolean;
|
|
93
146
|
/** How loud the supervising orchestrator session should be. */
|
|
94
147
|
reportScope: ReportScopeChoice;
|
|
95
|
-
/**
|
|
96
|
-
|
|
148
|
+
/** One IANA zone shared by the configured window and its daily digest. */
|
|
149
|
+
reportingTimezone?: string;
|
|
150
|
+
/** Daily digest wall-clock (`HH:MM`) when an explicit daily policy is chosen. */
|
|
151
|
+
dailyDigestAt?: string;
|
|
152
|
+
/** Digest cadence when preserving or explicitly editing a reporting policy. */
|
|
153
|
+
digestCadence?: DigestCadence;
|
|
154
|
+
/** Weekly window without its shared timezone; absent keeps 24-hour interrupts. */
|
|
155
|
+
availability?: Omit<WeeklyAvailability, "timezone">;
|
|
156
|
+
/**
|
|
157
|
+
* Exact policy read from disk. Unrelated amend areas carry this through
|
|
158
|
+
* untouched; the reporting interview deletes it before applying new answers.
|
|
159
|
+
*/
|
|
160
|
+
preservedReporting?: ReportingPolicy;
|
|
97
161
|
/**
|
|
98
162
|
* Mechanical gate for release/deploy-shaped tool calls, per shape. Always
|
|
99
163
|
* complete: the wizard asks about every shape, so an answer object can never
|
|
@@ -113,6 +177,17 @@ export interface SetupAnswers {
|
|
|
113
177
|
* prompt that asked it to the step that acts on it.
|
|
114
178
|
*/
|
|
115
179
|
writeOrchestratorBrief: boolean;
|
|
180
|
+
/**
|
|
181
|
+
* The operator's own judgment, asked in the interview because no amount of
|
|
182
|
+
* repo reading produces it — and because `src/briefs/policy.md` ships stubs
|
|
183
|
+
* waiting for exactly these words (`:25-28`, `:53-56`).
|
|
184
|
+
*
|
|
185
|
+
* Not config: none of it is a key the daemon reads. It exists to reach
|
|
186
|
+
* `POLICY.md` — written directly, and offered to a prose probe as stated input
|
|
187
|
+
* the probe may not invent. It rides on the answers object because the wizard
|
|
188
|
+
* has to carry it from the prompt that asked it to the step that writes it.
|
|
189
|
+
*/
|
|
190
|
+
judgment?: OperatorJudgment;
|
|
116
191
|
/**
|
|
117
192
|
* Who lands green PRs and who cuts releases. Both default to the human — see
|
|
118
193
|
* {@link DEFAULT_AUTHORITY} — and the answer is what the rendered brief's
|
|
@@ -138,6 +213,16 @@ export interface SetupAnswers {
|
|
|
138
213
|
* the same question four times to arrive at four siblings.
|
|
139
214
|
*/
|
|
140
215
|
graphRoot?: string;
|
|
216
|
+
/**
|
|
217
|
+
* Where this project's worktrees live. Absent means {@link defaultWorkspaceRoot}
|
|
218
|
+
* under the state dir (the first-project layout). An added project seeds
|
|
219
|
+
* `~/.omp/conductor/projects/<name>/worktrees` instead; an amend carries the
|
|
220
|
+
* path already on disk so a re-run never migrates a neighbour.
|
|
221
|
+
*/
|
|
222
|
+
workspaceRoot?: string;
|
|
223
|
+
/** Bare mirrors for this project; same seeding rules as {@link workspaceRoot}. */
|
|
224
|
+
mirrorRoot?: string;
|
|
225
|
+
|
|
141
226
|
}
|
|
142
227
|
|
|
143
228
|
/** What `gh auth status` says the active token may do. */
|
|
@@ -189,7 +274,7 @@ export const REPORT_SCOPE_CHOICES: readonly {
|
|
|
189
274
|
scope: "decisions",
|
|
190
275
|
label: "Decisions interrupt, rest batches",
|
|
191
276
|
description:
|
|
192
|
-
"tier-2 decisions and fleet-stopping conditions immediately; every other material event ships with the
|
|
277
|
+
"tier-2 decisions and fleet-stopping conditions immediately; every other material event ships with the configured digest",
|
|
193
278
|
},
|
|
194
279
|
{
|
|
195
280
|
scope: "material",
|
|
@@ -510,6 +595,88 @@ export async function createMissingLabels(trackerRepo: string, plan: LabelPlan[]
|
|
|
510
595
|
return created;
|
|
511
596
|
}
|
|
512
597
|
|
|
598
|
+
const QUIET_INTERRUPT_ON: InterruptCategory[] = [
|
|
599
|
+
"tier2",
|
|
600
|
+
"fleet-stopped",
|
|
601
|
+
"confirmed-failure",
|
|
602
|
+
];
|
|
603
|
+
|
|
604
|
+
function interruptCategories(scope: ReportScopeChoice): InterruptCategory[] {
|
|
605
|
+
return scope === "quiet"
|
|
606
|
+
? [...QUIET_INTERRUPT_ON]
|
|
607
|
+
: [...(SCOPE_PRESETS[scope] ?? SCOPE_PRESETS[DEFAULT_REPORT_SCOPE]).interruptOn];
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** Copy policy arrays so a setup amend cannot mutate the config it was seeded from. */
|
|
611
|
+
function cloneReportingPolicy(policy: ReportingPolicy): ReportingPolicy {
|
|
612
|
+
return {
|
|
613
|
+
interruptOn: [...policy.interruptOn],
|
|
614
|
+
digest: { ...policy.digest },
|
|
615
|
+
...(policy.availability === undefined
|
|
616
|
+
? {}
|
|
617
|
+
: {
|
|
618
|
+
availability: {
|
|
619
|
+
...policy.availability,
|
|
620
|
+
days: [...policy.availability.days],
|
|
621
|
+
bypass: [...policy.availability.bypass],
|
|
622
|
+
},
|
|
623
|
+
}),
|
|
624
|
+
...(policy.scopePreset === undefined ? {} : { scopePreset: policy.scopePreset }),
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/** Turn the wizard's local notification answers into the runtime policy. */
|
|
629
|
+
function reportingFromAnswers(a: SetupAnswers): ReportingPolicy {
|
|
630
|
+
if (a.preservedReporting !== undefined) return cloneReportingPolicy(a.preservedReporting);
|
|
631
|
+
|
|
632
|
+
const digest = (fallback: DigestCadence): ReportingPolicy["digest"] => {
|
|
633
|
+
const cadence = a.digestCadence ?? fallback;
|
|
634
|
+
if (cadence !== "daily") return { cadence };
|
|
635
|
+
const at = a.dailyDigestAt?.trim();
|
|
636
|
+
const timezone = a.reportingTimezone?.trim();
|
|
637
|
+
return {
|
|
638
|
+
cadence,
|
|
639
|
+
...(at === undefined || at.length === 0 ? {} : { at }),
|
|
640
|
+
...(timezone === undefined || timezone.length === 0 ? {} : { timezone }),
|
|
641
|
+
};
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
const timezone = a.reportingTimezone?.trim();
|
|
645
|
+
if (a.availability !== undefined) {
|
|
646
|
+
if (timezone === undefined || timezone.length === 0) {
|
|
647
|
+
throw new Error("operator availability requires an IANA reporting timezone");
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
interruptOn: interruptCategories(a.reportScope),
|
|
651
|
+
digest: digest("daily"),
|
|
652
|
+
availability: {
|
|
653
|
+
timezone,
|
|
654
|
+
days: [...a.availability.days],
|
|
655
|
+
start: a.availability.start,
|
|
656
|
+
end: a.availability.end,
|
|
657
|
+
bypass: [...a.availability.bypass],
|
|
658
|
+
},
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
if (a.reportScope === "quiet") {
|
|
662
|
+
return {
|
|
663
|
+
interruptOn: [...QUIET_INTERRUPT_ON],
|
|
664
|
+
digest: digest("daily"),
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
const preset = SCOPE_PRESETS[a.reportScope] ?? SCOPE_PRESETS[DEFAULT_REPORT_SCOPE];
|
|
668
|
+
const resolvedDigest = digest(preset.digest.cadence);
|
|
669
|
+
const presetDigest =
|
|
670
|
+
resolvedDigest.cadence === preset.digest.cadence &&
|
|
671
|
+
resolvedDigest.at === preset.digest.at &&
|
|
672
|
+
resolvedDigest.timezone === preset.digest.timezone;
|
|
673
|
+
return {
|
|
674
|
+
interruptOn: [...preset.interruptOn],
|
|
675
|
+
digest: resolvedDigest,
|
|
676
|
+
...(presetDigest ? { scopePreset: preset.scopePreset } : {}),
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
513
680
|
/**
|
|
514
681
|
* The single `ProjectConfig` the answers describe.
|
|
515
682
|
*
|
|
@@ -545,6 +712,13 @@ export function buildProject(a: SetupAnswers): ProjectConfig {
|
|
|
545
712
|
if (a.telegramChatId !== undefined && a.telegramChatId.trim().length > 0) {
|
|
546
713
|
escalation.telegramChatId = a.telegramChatId.trim();
|
|
547
714
|
}
|
|
715
|
+
if (
|
|
716
|
+
a.telegramTopicId !== undefined &&
|
|
717
|
+
Number.isFinite(a.telegramTopicId) &&
|
|
718
|
+
Number.isSafeInteger(a.telegramTopicId)
|
|
719
|
+
) {
|
|
720
|
+
escalation.telegramTopicId = a.telegramTopicId;
|
|
721
|
+
}
|
|
548
722
|
|
|
549
723
|
const caps: Partial<Caps> = {};
|
|
550
724
|
// Drops `undefined` members so an unanswered cap is absent from the JSON
|
|
@@ -576,28 +750,16 @@ export function buildProject(a: SetupAnswers): ProjectConfig {
|
|
|
576
750
|
// and a release require without anyone having to know a default (#129).
|
|
577
751
|
policy: clonePolicy(a.policy),
|
|
578
752
|
// Written out even when it is the default, so an operator amending the
|
|
579
|
-
// volume has a line in the file to point at.
|
|
580
|
-
//
|
|
581
|
-
//
|
|
582
|
-
|
|
583
|
-
reporting:
|
|
584
|
-
a.reportScope === "quiet"
|
|
585
|
-
? {
|
|
586
|
-
interruptOn: ["tier2", "fleet-stopped", "confirmed-failure"],
|
|
587
|
-
digest: {
|
|
588
|
-
cadence: "daily",
|
|
589
|
-
...(a.quietDigestAt !== undefined && a.quietDigestAt.length > 0 ? { at: a.quietDigestAt } : {}),
|
|
590
|
-
},
|
|
591
|
-
}
|
|
592
|
-
: {
|
|
593
|
-
interruptOn: [...(SCOPE_PRESETS[a.reportScope] ?? SCOPE_PRESETS[DEFAULT_REPORT_SCOPE]).interruptOn],
|
|
594
|
-
digest: { ...(SCOPE_PRESETS[a.reportScope] ?? SCOPE_PRESETS[DEFAULT_REPORT_SCOPE]).digest },
|
|
595
|
-
scopePreset: (SCOPE_PRESETS[a.reportScope] ?? SCOPE_PRESETS[DEFAULT_REPORT_SCOPE]).scopePreset,
|
|
596
|
-
},
|
|
753
|
+
// volume has a line in the file to point at. Opting into availability makes
|
|
754
|
+
// the schedule explicit and daily; omitting it preserves the preset's
|
|
755
|
+
// legacy 24-hour behaviour (#273).
|
|
756
|
+
reporting: reportingFromAnswers(a),
|
|
597
757
|
// Both under the state dir so one `rm -rf ~/.omp/conductor` is a complete
|
|
598
758
|
// uninstall, and neither can land in a repo the daemon then tries to commit.
|
|
599
|
-
|
|
600
|
-
|
|
759
|
+
// An explicit answer wins (added projects seed under projects/<name>/; amends
|
|
760
|
+
// carry the path already on disk). Absence keeps the first-project flat layout.
|
|
761
|
+
workspaceRoot: a.workspaceRoot?.trim() || defaultWorkspaceRoot(),
|
|
762
|
+
mirrorRoot: a.mirrorRoot?.trim() || defaultMirrorRoot(),
|
|
601
763
|
};
|
|
602
764
|
}
|
|
603
765
|
|
|
@@ -612,6 +774,16 @@ export function buildProject(a: SetupAnswers): ProjectConfig {
|
|
|
612
774
|
export function buildConfig(a: SetupAnswers, existing?: ConductorConfig): ConductorConfig {
|
|
613
775
|
const project = buildProject(a);
|
|
614
776
|
const previous = existing?.projects ?? [];
|
|
777
|
+
const collision = previous.find(
|
|
778
|
+
(p) => p.name !== project.name && p.workspaceRoot === project.workspaceRoot,
|
|
779
|
+
);
|
|
780
|
+
if (collision !== undefined) {
|
|
781
|
+
throw new Error(
|
|
782
|
+
`workspaceRoot ${project.workspaceRoot} collides between projects ` +
|
|
783
|
+
`"${collision.name}" and "${project.name}" — two fleet sessions cannot share a cwd ` +
|
|
784
|
+
`(one tick config would clobber the other). Pick a different workspaceRoot for one of them.`,
|
|
785
|
+
);
|
|
786
|
+
}
|
|
615
787
|
|
|
616
788
|
return {
|
|
617
789
|
version: CONFIG_VERSION,
|
|
@@ -636,7 +808,14 @@ export function buildConfig(a: SetupAnswers, existing?: ConductorConfig): Conduc
|
|
|
636
808
|
* default at each prompt, which is how one prompt comes to disagree with the
|
|
637
809
|
* config key it writes.
|
|
638
810
|
*/
|
|
639
|
-
export function defaultAnswers(projectName: string): SetupAnswers {
|
|
811
|
+
export function defaultAnswers(projectName: string, opts: { added?: boolean } = {}): SetupAnswers {
|
|
812
|
+
// Added projects live under projects/<name>/ so two fleets never share a cwd
|
|
813
|
+
// with the first project's flat worktrees/mirrors. The first project keeps
|
|
814
|
+
// those flat paths; buildProject falls back to defaultWorkspaceRoot() when
|
|
815
|
+
// these are absent.
|
|
816
|
+
const scopedBase = opts.added === true && projectName.trim().length > 0
|
|
817
|
+
? join(stateDir(), "projects", projectName.trim())
|
|
818
|
+
: undefined;
|
|
640
819
|
return {
|
|
641
820
|
projectName,
|
|
642
821
|
// Empty rather than a plausible guess: both are required, and a pre-filled
|
|
@@ -654,6 +833,12 @@ export function defaultAnswers(projectName: string): SetupAnswers {
|
|
|
654
833
|
orchestratorMode: SETUP_DEFAULTS.orchestratorMode,
|
|
655
834
|
reportScope: SETUP_DEFAULT_REPORT_SCOPE,
|
|
656
835
|
writeOrchestratorBrief: false,
|
|
836
|
+
...(scopedBase === undefined
|
|
837
|
+
? {}
|
|
838
|
+
: {
|
|
839
|
+
workspaceRoot: defaultWorkspaceRoot(scopedBase),
|
|
840
|
+
mirrorRoot: defaultMirrorRoot(scopedBase),
|
|
841
|
+
}),
|
|
657
842
|
};
|
|
658
843
|
}
|
|
659
844
|
|
|
@@ -667,19 +852,60 @@ export function defaultAnswers(projectName: string): SetupAnswers {
|
|
|
667
852
|
* changing an unrelated one. `buildProject(answersFromProject(p))` is pinned to
|
|
668
853
|
* `p` by a test for exactly that reason.
|
|
669
854
|
*
|
|
670
|
-
*
|
|
855
|
+
* Four fields cannot be a straight copy:
|
|
671
856
|
*
|
|
672
857
|
* - `writeOrchestratorBrief` is a decision rather than a value, and it starts
|
|
673
858
|
* `false` so an amend that never visits the brief area leaves that file alone.
|
|
674
|
-
* - `reportScope`
|
|
675
|
-
*
|
|
676
|
-
*
|
|
859
|
+
* - `reportScope` recognizes both preset metadata and the explicit quiet
|
|
860
|
+
* category set, because an availability schedule intentionally has no preset.
|
|
861
|
+
* - The reporting timezone is shared by availability and the daily digest in
|
|
862
|
+
* the wizard even though runtime config stores it in both places.
|
|
677
863
|
* - `graphRoot` is one answer for a whole project while the config stores one
|
|
678
864
|
* path per repo, so it comes back from whichever repo already has one. Repos
|
|
679
865
|
* that disagree — only a hand-edit can produce that — widen to all of them on
|
|
680
866
|
* the next write exactly as a full re-run would, and the plan summary names
|
|
681
867
|
* every clone before anything is written.
|
|
682
868
|
*/
|
|
869
|
+
function sameInterruptCategories(
|
|
870
|
+
actual: readonly InterruptCategory[],
|
|
871
|
+
expected: readonly InterruptCategory[],
|
|
872
|
+
): boolean {
|
|
873
|
+
return (
|
|
874
|
+
actual.length === expected.length &&
|
|
875
|
+
actual.every((category) => expected.includes(category))
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
export function reportScopeFromPolicy(policy: ReportingPolicy | undefined): ReportScopeChoice {
|
|
880
|
+
if (policy?.scopePreset !== undefined) return policy.scopePreset;
|
|
881
|
+
const actual = policy?.interruptOn ?? SCOPE_PRESETS[DEFAULT_REPORT_SCOPE].interruptOn;
|
|
882
|
+
if (sameInterruptCategories(actual, QUIET_INTERRUPT_ON)) return "quiet";
|
|
883
|
+
for (const scope of ["material", "decisions", "escalations"] as const) {
|
|
884
|
+
if (sameInterruptCategories(actual, SCOPE_PRESETS[scope].interruptOn)) return scope;
|
|
885
|
+
}
|
|
886
|
+
return DEFAULT_REPORT_SCOPE;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function reportScopeDescriptionForPolicy(policy: ReportingPolicy | undefined): string {
|
|
890
|
+
const scope = reportScopeFromPolicy(policy);
|
|
891
|
+
const choice = REPORT_SCOPE_CHOICES.find((candidate) => candidate.scope === scope);
|
|
892
|
+
const expectedCadence =
|
|
893
|
+
scope === "quiet" ? "daily" : SCOPE_PRESETS[scope].digest.cadence;
|
|
894
|
+
const configured = policy?.digest ?? SCOPE_PRESETS[DEFAULT_REPORT_SCOPE].digest;
|
|
895
|
+
if (configured.cadence === expectedCadence) {
|
|
896
|
+
return choice?.description ?? "unknown scope";
|
|
897
|
+
}
|
|
898
|
+
const digest =
|
|
899
|
+
configured.cadence === "none"
|
|
900
|
+
? "disabled"
|
|
901
|
+
: configured.cadence === "per-tick"
|
|
902
|
+
? "every tick"
|
|
903
|
+
: `daily at ${configured.at ?? "(model-timed)"}${
|
|
904
|
+
configured.timezone === undefined ? "" : ` ${configured.timezone}`
|
|
905
|
+
}`;
|
|
906
|
+
return `${choice?.label ?? scope}; configured digest ${digest}`;
|
|
907
|
+
}
|
|
908
|
+
|
|
683
909
|
export function answersFromProject(p: ProjectConfig): SetupAnswers {
|
|
684
910
|
const answers: SetupAnswers = {
|
|
685
911
|
projectName: p.name,
|
|
@@ -699,16 +925,37 @@ export function answersFromProject(p: ProjectConfig): SetupAnswers {
|
|
|
699
925
|
releaseGrants: resolveReleaseGrants(p),
|
|
700
926
|
policy: resolvePolicy(p),
|
|
701
927
|
orchestratorMode: p.escalation.orchestrator,
|
|
702
|
-
reportScope: p.reporting
|
|
928
|
+
reportScope: reportScopeFromPolicy(p.reporting),
|
|
703
929
|
writeOrchestratorBrief: false,
|
|
704
930
|
};
|
|
931
|
+
if (p.reporting !== undefined) {
|
|
932
|
+
answers.preservedReporting = cloneReportingPolicy(p.reporting);
|
|
933
|
+
answers.digestCadence = p.reporting.digest.cadence;
|
|
934
|
+
}
|
|
705
935
|
|
|
706
936
|
// Set only when present, never as an explicit `undefined`: an absent key is
|
|
707
937
|
// what keeps the rewritten config identical to the one that was read.
|
|
708
938
|
if (p.workerModel !== undefined) answers.workerModel = p.workerModel;
|
|
709
939
|
if (p.escalation.telegramChatId !== undefined) answers.telegramChatId = p.escalation.telegramChatId;
|
|
940
|
+
if (p.escalation.telegramTopicId !== undefined) answers.telegramTopicId = p.escalation.telegramTopicId;
|
|
941
|
+
if (p.reporting?.digest.at !== undefined) answers.dailyDigestAt = p.reporting.digest.at;
|
|
942
|
+
if (p.reporting?.digest.timezone !== undefined) {
|
|
943
|
+
answers.reportingTimezone = p.reporting.digest.timezone;
|
|
944
|
+
}
|
|
945
|
+
if (p.reporting?.availability !== undefined) {
|
|
946
|
+
answers.reportingTimezone = p.reporting.availability.timezone;
|
|
947
|
+
answers.availability = {
|
|
948
|
+
days: [...p.reporting.availability.days],
|
|
949
|
+
start: p.reporting.availability.start,
|
|
950
|
+
end: p.reporting.availability.end,
|
|
951
|
+
bypass: [...p.reporting.availability.bypass],
|
|
952
|
+
};
|
|
953
|
+
}
|
|
710
954
|
const graphed = graphRepos(p)[0];
|
|
711
955
|
if (graphed !== undefined) answers.graphRoot = dirname(graphed.graphProject);
|
|
956
|
+
if (p.workspaceRoot.length > 0) answers.workspaceRoot = p.workspaceRoot;
|
|
957
|
+
if (p.mirrorRoot.length > 0) answers.mirrorRoot = p.mirrorRoot;
|
|
958
|
+
|
|
712
959
|
|
|
713
960
|
return answers;
|
|
714
961
|
}
|
|
@@ -736,7 +983,6 @@ function briefVarsForProject(p: ProjectConfig): Record<string, string> {
|
|
|
736
983
|
QUEUE_LABEL: p.queueLabel,
|
|
737
984
|
RELEASES_DEFAULT: RELEASES_DEFAULTS[`${p.authority.merge}/${p.authority.release}`],
|
|
738
985
|
MERGE_DUTY: MERGE_DUTY[p.authority.merge],
|
|
739
|
-
REPORT_SCOPE: p.reporting?.scopePreset ?? DEFAULT_REPORT_SCOPE,
|
|
740
986
|
POLICY_SOURCE: policySourceLine(p),
|
|
741
987
|
};
|
|
742
988
|
}
|
|
@@ -754,7 +1000,7 @@ function briefVarsForProject(p: ProjectConfig): Record<string, string> {
|
|
|
754
1000
|
function policySourceLine(p: ProjectConfig): string {
|
|
755
1001
|
return (
|
|
756
1002
|
`They are in \`${configPath()}\` under \`projects[] "${p.name}" policy\`, and they change with ` +
|
|
757
|
-
|
|
1003
|
+
`\`omp-conductor setup policy\` ("${AMEND_AREAS.policy.name}"). Never by hand in this file.`
|
|
758
1004
|
);
|
|
759
1005
|
}
|
|
760
1006
|
|
|
@@ -803,8 +1049,48 @@ export function orchestratorBriefPath(a: SetupAnswers): string {
|
|
|
803
1049
|
}
|
|
804
1050
|
|
|
805
1051
|
/** Wizard-time render, via the project the answers describe. */
|
|
806
|
-
export function renderOrchestratorBrief(a: SetupAnswers): string {
|
|
807
|
-
|
|
1052
|
+
export function renderOrchestratorBrief(a: SetupAnswers, prose: ProbedProse = {}): string {
|
|
1053
|
+
// Rendered through the same judgment fill the writer applies, so "what setup
|
|
1054
|
+
// would write" and "what setup writes" cannot diverge — the preview an operator
|
|
1055
|
+
// consents to is the file they get.
|
|
1056
|
+
const project = buildProject(a);
|
|
1057
|
+
return composeOrchestrator(renderFloorForProject(project), renderPolicy(a, prose));
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/** `POLICY.md` for these answers: the project's own template plus the interview. */
|
|
1061
|
+
export function renderPolicy(a: SetupAnswers, prose: ProbedProse = {}): string {
|
|
1062
|
+
return fillJudgment(renderPolicyForProject(buildProject(a)), a.judgment, prose);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* The body of `## Project context`.
|
|
1067
|
+
*
|
|
1068
|
+
* **One writer, not two.** Confirmed prose is the whole section: the probe was
|
|
1069
|
+
* handed the roadmap answer as a stated input and told to carry it as its fourth
|
|
1070
|
+
* item, so synthesising a roadmap line here as well would print the operator's
|
|
1071
|
+
* priority twice — once out of the product/repos/grooming order the section is
|
|
1072
|
+
* written to be read in.
|
|
1073
|
+
*
|
|
1074
|
+
* Without prose — `--no-ai`, a declined preview, an unreachable peer — the roadmap
|
|
1075
|
+
* answer is still the one part of this section the interview *did* collect, so it
|
|
1076
|
+
* is written above a note naming what is left.
|
|
1077
|
+
*/
|
|
1078
|
+
function contextSection(roadmap: string | undefined, context: string | undefined): string {
|
|
1079
|
+
const written = context?.trim();
|
|
1080
|
+
if (written !== undefined && written.length > 0) {
|
|
1081
|
+
// The heading came from the template; a probe that repeated it would leave two.
|
|
1082
|
+
return written.replace(/^##\s*Project context[^\n]*\n+/, "");
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
const stated = roadmap?.trim();
|
|
1086
|
+
return [
|
|
1087
|
+
...(stated === undefined || stated.length === 0
|
|
1088
|
+
? []
|
|
1089
|
+
: [`**Roadmap and current priority.** ${stated}`, "", "Judge grooming priority against that, not against recency.", ""]),
|
|
1090
|
+
"Still to fill in, by hand or by re-running `omp-conductor setup brief`: the",
|
|
1091
|
+
"product in a paragraph, a map of which repo owns what, and the grooming guidance",
|
|
1092
|
+
"Duty 2 needs to spot issues that would collide.",
|
|
1093
|
+
].join("\n");
|
|
808
1094
|
}
|
|
809
1095
|
|
|
810
1096
|
/**
|
|
@@ -816,17 +1102,109 @@ export function renderOrchestratorBrief(a: SetupAnswers): string {
|
|
|
816
1102
|
* that dialog's answer un-actionable — an operator who says "yes, overwrite it"
|
|
817
1103
|
* must get an overwrite.
|
|
818
1104
|
*/
|
|
819
|
-
export function writeOrchestratorBrief(a: SetupAnswers): string {
|
|
1105
|
+
export function writeOrchestratorBrief(a: SetupAnswers, prose: ProbedProse = {}): string {
|
|
820
1106
|
const project = buildProject(a);
|
|
821
1107
|
const policyPath = policyPathForProject(project);
|
|
822
1108
|
const orchestratorPath = briefPathForProject(project);
|
|
823
1109
|
mkdirSync(dirname(orchestratorPath), { recursive: true });
|
|
824
|
-
const policy =
|
|
1110
|
+
const policy = renderPolicy(a, prose);
|
|
825
1111
|
writeFileSync(policyPath, policy);
|
|
826
1112
|
writeFileSync(orchestratorPath, composeOrchestrator(renderFloorForProject(project), policy));
|
|
827
1113
|
return orchestratorPath;
|
|
828
1114
|
}
|
|
829
1115
|
|
|
1116
|
+
/** The stub `policy.md` ships where the operator's own priority pointer belongs. */
|
|
1117
|
+
const CONTEXT_STUB =
|
|
1118
|
+
"Empty until setup fills it in: the product in a paragraph, a map of which repo\n" +
|
|
1119
|
+
"owns what, the grooming guidance Duty 2 needs to judge priority and spot issues\n" +
|
|
1120
|
+
"that would collide, and where the roadmap lives. `omp-conductor setup` asks for\n" +
|
|
1121
|
+
"the last of those and proposes the rest.";
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* Prose a probe read out of the repos and the **operator confirmed**.
|
|
1125
|
+
*
|
|
1126
|
+
* Separate from {@link SetupAnswers} on purpose: these are paragraphs of the
|
|
1127
|
+
* operator's brief, not configuration. Nothing here is ever written unconfirmed,
|
|
1128
|
+
* and every field is optional — a declined preview, an unreachable peer and
|
|
1129
|
+
* `--no-ai` all arrive here as the same absence.
|
|
1130
|
+
*/
|
|
1131
|
+
export interface ProbedProse {
|
|
1132
|
+
/** The product, the repo map and the grooming guidance Duty 2 needs. */
|
|
1133
|
+
projectContext?: string;
|
|
1134
|
+
/** This repo's real release machinery, expressing the operator's stated intent. */
|
|
1135
|
+
releaseProcedure?: string;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* Writes the interview's answers, and any confirmed probe prose, into the two
|
|
1140
|
+
* `POLICY.md` stubs that were waiting for them.
|
|
1141
|
+
*
|
|
1142
|
+
* Text substitution rather than a template variable, because these are the
|
|
1143
|
+
* *operator's* words in the operator's file: the conductor renders `POLICY.md`
|
|
1144
|
+
* once and never reads it back, so anything here has to land as prose they can
|
|
1145
|
+
* then edit freely.
|
|
1146
|
+
*
|
|
1147
|
+
* Absent answers leave the shipped guidance in place — a stub that still explains
|
|
1148
|
+
* what belongs there is more use than a heading with nothing under it. What no
|
|
1149
|
+
* outcome leaves behind is a pointer at the onboarding skill #309 deleted: the
|
|
1150
|
+
* template no longer carries one, and `setup.test.ts` pins its absence, because a
|
|
1151
|
+
* stub telling an operator to load something that does not ship is exactly the
|
|
1152
|
+
* capability loss #307 and #309 were sequenced to avoid.
|
|
1153
|
+
*/
|
|
1154
|
+
export function fillJudgment(
|
|
1155
|
+
policy: string,
|
|
1156
|
+
judgment: OperatorJudgment | undefined,
|
|
1157
|
+
prose: ProbedProse = {},
|
|
1158
|
+
): string {
|
|
1159
|
+
const j = judgment ?? {};
|
|
1160
|
+
let out = policy;
|
|
1161
|
+
|
|
1162
|
+
const procedure = prose.releaseProcedure?.trim();
|
|
1163
|
+
const releases = [...releaseLines(j), ...(procedure === undefined || procedure.length === 0 ? [] : ["", procedure])];
|
|
1164
|
+
if (releases.length > 0) {
|
|
1165
|
+
// Appended under the four bullets rather than replacing them: the bullets say
|
|
1166
|
+
// what a good answer contains, and keeping them beside the answer is what
|
|
1167
|
+
// makes a later edit self-checking.
|
|
1168
|
+
out = out.replace(
|
|
1169
|
+
" forbidden everywhere, always.\n",
|
|
1170
|
+
` forbidden everywhere, always.\n\n${releases.join("\n")}\n`,
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
out = out.replace(CONTEXT_STUB, contextSection(j.roadmap, prose.projectContext));
|
|
1175
|
+
|
|
1176
|
+
return out;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/** The operator's release intent, as the brief's own bullet shape. */
|
|
1180
|
+
function releaseLines(j: OperatorJudgment): string[] {
|
|
1181
|
+
const lines: string[] = [];
|
|
1182
|
+
const add = (label: string, value: string | undefined): void => {
|
|
1183
|
+
if (value !== undefined && value.trim().length > 0) lines.push(`- **${label}** ${value.trim()}`);
|
|
1184
|
+
};
|
|
1185
|
+
add("Your leg ends:", j.boundary);
|
|
1186
|
+
add("What may be released:", j.releaseWhat);
|
|
1187
|
+
add("When:", j.releaseWhen);
|
|
1188
|
+
add("Proof held first:", j.releaseProof);
|
|
1189
|
+
add("Always ask about:", j.releaseAsk);
|
|
1190
|
+
add("Permanently forbidden:", j.releaseForbidden);
|
|
1191
|
+
add("A release worth cutting:", j.worthCutting);
|
|
1192
|
+
add("Rollback owner:", j.rollbackOwner);
|
|
1193
|
+
if (lines.length === 0) return lines;
|
|
1194
|
+
return [
|
|
1195
|
+
"Answered during setup — yours to edit:",
|
|
1196
|
+
"",
|
|
1197
|
+
...lines,
|
|
1198
|
+
...(j.rollbackMovesBoundary === true
|
|
1199
|
+
? [
|
|
1200
|
+
"",
|
|
1201
|
+
"The rollback owner is a person, so the boundary above ends *before* the",
|
|
1202
|
+
"irreversible step: prepare and verify it, then hand it over.",
|
|
1203
|
+
]
|
|
1204
|
+
: []),
|
|
1205
|
+
];
|
|
1206
|
+
}
|
|
1207
|
+
|
|
830
1208
|
/**
|
|
831
1209
|
* Recompose `ORCHESTRATOR.md` from the package floor + live `POLICY.md`.
|
|
832
1210
|
*
|
|
@@ -931,6 +1309,12 @@ function pairedOwner(accessPath: string): string | undefined {
|
|
|
931
1309
|
return undefined;
|
|
932
1310
|
}
|
|
933
1311
|
|
|
1312
|
+
function describeAvailabilityDays(days: readonly Weekday[]): string {
|
|
1313
|
+
return days.length === WEEKDAYS.length && WEEKDAYS.every((day) => days.includes(day))
|
|
1314
|
+
? "every day"
|
|
1315
|
+
: days.join(",");
|
|
1316
|
+
}
|
|
1317
|
+
|
|
934
1318
|
/**
|
|
935
1319
|
* Everything that would change, as plain text, with no side effects at all.
|
|
936
1320
|
*
|
|
@@ -1009,8 +1393,8 @@ export function summarisePlan(
|
|
|
1009
1393
|
for (const r of graphed) lines.push(` ${r.name} ${r.graphProject}`);
|
|
1010
1394
|
lines.push(
|
|
1011
1395
|
" conductor's own index-only clones — nothing human edits them, and",
|
|
1012
|
-
" nothing here creates them. Run `omp-conductor graph
|
|
1013
|
-
" setup: it
|
|
1396
|
+
" nothing here creates them. Run `omp-conductor setup graph` after",
|
|
1397
|
+
" setup: it clones, installs the timer and seeds the first index.",
|
|
1014
1398
|
);
|
|
1015
1399
|
}
|
|
1016
1400
|
|
|
@@ -1025,8 +1409,12 @@ export function summarisePlan(
|
|
|
1025
1409
|
|
|
1026
1410
|
lines.push("", "escalation");
|
|
1027
1411
|
if (a.telegramChatId !== undefined && a.telegramChatId.trim().length > 0) {
|
|
1412
|
+
const topic =
|
|
1413
|
+
a.telegramTopicId !== undefined && Number.isSafeInteger(a.telegramTopicId)
|
|
1414
|
+
? ` topic ${a.telegramTopicId}`
|
|
1415
|
+
: " flat chat";
|
|
1028
1416
|
lines.push(
|
|
1029
|
-
` tier 2 Telegram chat ${a.telegramChatId.trim()}` +
|
|
1417
|
+
` tier 2 Telegram chat ${a.telegramChatId.trim()}${topic}` +
|
|
1030
1418
|
(tg.hasToken ? "" : " — WARNING: no bot token found, this will not send"),
|
|
1031
1419
|
` via omp-telegram at ${tg.stateDir}`,
|
|
1032
1420
|
);
|
|
@@ -1083,12 +1471,27 @@ export function summarisePlan(
|
|
|
1083
1471
|
` environments ${a.policy.release.environments.join(", ") || "none declared — every deploy target is refused"}`,
|
|
1084
1472
|
);
|
|
1085
1473
|
|
|
1086
|
-
const
|
|
1474
|
+
const reporting = project.reporting as ReportingPolicy;
|
|
1087
1475
|
const briefPath = orchestratorBriefPath(a);
|
|
1088
1476
|
lines.push(
|
|
1089
1477
|
"",
|
|
1090
1478
|
"reporting",
|
|
1091
|
-
` scope ${a.reportScope} — ${
|
|
1479
|
+
` scope ${a.reportScope} — ${reportScopeDescriptionForPolicy(reporting)}`,
|
|
1480
|
+
);
|
|
1481
|
+
if (reporting.availability === undefined) {
|
|
1482
|
+
lines.push(" availability continuous — interrupt categories may page 24 hours a day");
|
|
1483
|
+
} else {
|
|
1484
|
+
lines.push(
|
|
1485
|
+
` availability ${describeAvailabilityDays(reporting.availability.days)} ` +
|
|
1486
|
+
`${reporting.availability.start}-${reporting.availability.end} ${reporting.availability.timezone}`,
|
|
1487
|
+
` bypass ${reporting.availability.bypass.join(", ") || "none"}`,
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
lines.push(
|
|
1491
|
+
` digest ${reporting.digest.cadence}` +
|
|
1492
|
+
(reporting.digest.cadence === "daily"
|
|
1493
|
+
? ` at ${reporting.digest.at ?? "(model-timed)"}${reporting.digest.timezone ? ` ${reporting.digest.timezone}` : ""}`
|
|
1494
|
+
: ""),
|
|
1092
1495
|
);
|
|
1093
1496
|
if (a.writeOrchestratorBrief) {
|
|
1094
1497
|
const policyPath = briefPath.replace(/ORCHESTRATOR\.md$/, "POLICY.md");
|
|
@@ -1133,16 +1536,16 @@ export function formatGates(gates: readonly { cmd: string; cwd: string }[]): str
|
|
|
1133
1536
|
* The wizard's questions, grouped as the areas a re-run can amend one of, in the
|
|
1134
1537
|
* order the full interview asks them.
|
|
1135
1538
|
*
|
|
1136
|
-
* Data rather than a switch so the menu, the
|
|
1137
|
-
*
|
|
1138
|
-
* added area fails to compile until it has a name, a current
|
|
1139
|
-
* questions.
|
|
1539
|
+
* Data rather than a switch so the menu, the CLI's positional area vocabulary,
|
|
1540
|
+
* ./setup-wizard.ts's `AREA_ASKERS` table and the amend summary all enumerate the
|
|
1541
|
+
* same nine areas: an added area fails to compile until it has a name, a current
|
|
1542
|
+
* value and a set of questions.
|
|
1140
1543
|
*/
|
|
1141
1544
|
export const AMEND_AREA_IDS = [
|
|
1142
1545
|
"tracker",
|
|
1143
1546
|
"gates",
|
|
1144
1547
|
"caps",
|
|
1145
|
-
"graph",
|
|
1548
|
+
"code-graph",
|
|
1146
1549
|
"authority",
|
|
1147
1550
|
"policy",
|
|
1148
1551
|
"escalation",
|
|
@@ -1209,7 +1612,7 @@ export const AMEND_AREAS: {
|
|
|
1209
1612
|
);
|
|
1210
1613
|
},
|
|
1211
1614
|
},
|
|
1212
|
-
graph: {
|
|
1615
|
+
"code-graph": {
|
|
1213
1616
|
name: "code graph",
|
|
1214
1617
|
asks: "whether workers query a code-graph index, and the root its one-clone-per-repo lives under",
|
|
1215
1618
|
describe: (p) => {
|
|
@@ -1253,12 +1656,14 @@ export const AMEND_AREAS: {
|
|
|
1253
1656
|
},
|
|
1254
1657
|
escalation: {
|
|
1255
1658
|
name: "escalation & triage",
|
|
1256
|
-
asks: "the tier-2 Telegram chat, whether escalations also comment, and where the orchestrator session lives",
|
|
1659
|
+
asks: "the tier-2 Telegram chat, optional forum topic, whether escalations also comment, and where the orchestrator session lives",
|
|
1257
1660
|
describe: (p) =>
|
|
1258
1661
|
[
|
|
1259
1662
|
p.escalation.telegramChatId === undefined
|
|
1260
1663
|
? "tier 2 by issue comment only"
|
|
1261
|
-
:
|
|
1664
|
+
: p.escalation.telegramTopicId === undefined
|
|
1665
|
+
? `tier 2 pages Telegram ${p.escalation.telegramChatId} (flat chat)`
|
|
1666
|
+
: `tier 2 pages Telegram ${p.escalation.telegramChatId} topic ${p.escalation.telegramTopicId}`,
|
|
1262
1667
|
p.escalation.fallbackToIssueComment ? "comments too" : "no comment fallback",
|
|
1263
1668
|
`triage ${p.escalation.orchestrator}`,
|
|
1264
1669
|
].join(", "),
|
|
@@ -1267,9 +1672,8 @@ export const AMEND_AREAS: {
|
|
|
1267
1672
|
name: "reporting scope",
|
|
1268
1673
|
asks: "how much the orchestrator says unprompted",
|
|
1269
1674
|
describe: (p) => {
|
|
1270
|
-
const scope = p.reporting
|
|
1271
|
-
|
|
1272
|
-
return `${scope} — ${choice?.description ?? "unknown scope"}`;
|
|
1675
|
+
const scope = reportScopeFromPolicy(p.reporting);
|
|
1676
|
+
return `${scope} — ${reportScopeDescriptionForPolicy(p.reporting)}`;
|
|
1273
1677
|
},
|
|
1274
1678
|
},
|
|
1275
1679
|
brief: {
|