infinity-harness 2.6.6 → 2.7.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/CHANGELOG.md +66 -0
- package/README.md +68 -15
- package/extensions/infinity-harness/index.ts +391 -18
- package/package.json +1 -1
- package/src/core/config.ts +1 -1
- package/src/core/settings.ts +10 -3
- package/src/core/types.ts +16 -0
- package/src/exec/piWorker.ts +707 -0
- package/src/intake.ts +4 -1
- package/src/loop.ts +35 -34
- package/src/remote.ts +26 -6
- package/src/scheduler.ts +10 -3
- package/src/supervisor.ts +955 -0
- package/src/ui/dashboard.ts +127 -0
- package/src/ui/widget.ts +134 -0
- package/src/ui/wizard.ts +43 -7
package/src/ui/dashboard.ts
CHANGED
|
@@ -68,8 +68,30 @@ export type DashboardState = {
|
|
|
68
68
|
* reads, so a level turned off in one is off in the other.
|
|
69
69
|
*/
|
|
70
70
|
display?: DisplayPolicy | null;
|
|
71
|
+
/** Where the work is happening. */
|
|
72
|
+
engine?: "background" | "main-session" | null;
|
|
73
|
+
/** The background pi sessions doing the work, and what each is on. */
|
|
74
|
+
workers?: DashWorker[] | null;
|
|
75
|
+
/** The tail of the background log. */
|
|
76
|
+
activity?: DashActivity[] | null;
|
|
71
77
|
};
|
|
72
78
|
|
|
79
|
+
export type DashWorker = {
|
|
80
|
+
name: string;
|
|
81
|
+
unitLabel: string;
|
|
82
|
+
level: string;
|
|
83
|
+
model: string;
|
|
84
|
+
servedModel?: string | null;
|
|
85
|
+
difficulty?: string | null;
|
|
86
|
+
state: string;
|
|
87
|
+
doing?: string | null;
|
|
88
|
+
turns?: number;
|
|
89
|
+
tokens?: { inputTokens: number; outputTokens: number } | null;
|
|
90
|
+
contextRatio?: number | null;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export type DashActivity = { at: string; level: string; worker: string | null; text: string };
|
|
94
|
+
|
|
73
95
|
// ── escaping ────────────────────────────────────────────────────────────────
|
|
74
96
|
|
|
75
97
|
const HTML_ESCAPES: Record<string, string> = {
|
|
@@ -799,6 +821,50 @@ body{
|
|
|
799
821
|
color:var(--faint);font-weight:600;margin-bottom:6px;
|
|
800
822
|
}
|
|
801
823
|
|
|
824
|
+
|
|
825
|
+
/* -- background sessions --------------------------------------------------- */
|
|
826
|
+
/*
|
|
827
|
+
* The run's work happens in other pi processes now, so this panel is the only
|
|
828
|
+
* place it is visible. It is deliberately the loudest thing under the meter:
|
|
829
|
+
* a live dot, the model each session is on, and a reverse-chronological log.
|
|
830
|
+
*/
|
|
831
|
+
.card.background h2{margin:0 0 4px;font-size:14px;letter-spacing:.01em}
|
|
832
|
+
.bg-note{margin:0 0 12px;color:var(--muted);font-size:13px}
|
|
833
|
+
.bg-workers{list-style:none;margin:0;padding:0;display:grid;gap:8px}
|
|
834
|
+
.bg-worker{
|
|
835
|
+
display:grid;grid-template-columns:auto auto 1fr;gap:4px 10px;align-items:baseline;
|
|
836
|
+
padding:10px 12px;border:1px solid var(--border);border-radius:10px;background:var(--surface-2);
|
|
837
|
+
}
|
|
838
|
+
.bg-dot{
|
|
839
|
+
width:8px;height:8px;border-radius:50%;background:var(--muted);
|
|
840
|
+
align-self:center;grid-row:1;
|
|
841
|
+
}
|
|
842
|
+
.bg-worker.is-working .bg-dot,.bg-worker.is-starting .bg-dot{
|
|
843
|
+
background:var(--c-accent);box-shadow:0 0 0 3px var(--ring);animation:bgpulse 1.6s ease-in-out infinite;
|
|
844
|
+
}
|
|
845
|
+
.bg-worker.is-failed .bg-dot{background:var(--t-blocked,#C0392B)}
|
|
846
|
+
@keyframes bgpulse{0%,100%{opacity:1}50%{opacity:.35}}
|
|
847
|
+
@media (prefers-reduced-motion:reduce){.bg-worker .bg-dot{animation:none}}
|
|
848
|
+
.bg-name{font-weight:650}
|
|
849
|
+
.bg-model{
|
|
850
|
+
font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;
|
|
851
|
+
color:var(--c-accent);overflow-wrap:anywhere;
|
|
852
|
+
}
|
|
853
|
+
.bg-meta{grid-column:2/-1;color:var(--muted);font-size:12.5px}
|
|
854
|
+
.bg-doing{grid-column:2/-1;font-size:13px;overflow-wrap:anywhere}
|
|
855
|
+
.bg-log{
|
|
856
|
+
list-style:none;margin:12px 0 0;padding:10px 0 0;border-top:1px solid var(--border);
|
|
857
|
+
max-height:260px;overflow:auto;display:grid;gap:2px;
|
|
858
|
+
}
|
|
859
|
+
.bg-line{display:grid;grid-template-columns:44px 34px 1fr;gap:8px;font-size:12.5px;align-items:baseline}
|
|
860
|
+
.bg-time{color:var(--faint);font-variant-numeric:tabular-nums}
|
|
861
|
+
.bg-who{color:var(--c-accent);font-weight:600}
|
|
862
|
+
.bg-text{color:var(--muted);overflow-wrap:anywhere}
|
|
863
|
+
.bg-line.lvl-work .bg-text{color:var(--text)}
|
|
864
|
+
.bg-line.lvl-good .bg-text{color:var(--t-success,#177245)}
|
|
865
|
+
.bg-line.lvl-warn .bg-text{color:var(--t-rework,#8E44AD)}
|
|
866
|
+
.bg-line.lvl-error .bg-text{color:var(--t-blocked,#C0392B)}
|
|
867
|
+
|
|
802
868
|
/* -- masthead ------------------------------------------------------------- */
|
|
803
869
|
.masthead{
|
|
804
870
|
position:sticky;top:0;z-index:20;
|
|
@@ -1210,6 +1276,66 @@ function formatTimestamp(iso: string): string {
|
|
|
1210
1276
|
return d.toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z");
|
|
1211
1277
|
}
|
|
1212
1278
|
|
|
1279
|
+
/**
|
|
1280
|
+
* The background sessions, and the log of what they have done.
|
|
1281
|
+
*
|
|
1282
|
+
* When the work moved out of the human's terminal this became the only place
|
|
1283
|
+
* a run is visible at all — so it says, per session, which unit it owns, what
|
|
1284
|
+
* model is answering for it, and the last thing it did. The model is on the
|
|
1285
|
+
* card because "routed to the difficult tier" is a claim, and a claim you
|
|
1286
|
+
* cannot see is one nobody believes.
|
|
1287
|
+
*/
|
|
1288
|
+
export function renderBackground(
|
|
1289
|
+
engine: DashboardState["engine"],
|
|
1290
|
+
workers: readonly DashWorker[],
|
|
1291
|
+
activity: readonly DashActivity[],
|
|
1292
|
+
): string {
|
|
1293
|
+
if (!engine && workers.length === 0 && activity.length === 0) return "";
|
|
1294
|
+
const head =
|
|
1295
|
+
engine === "main-session"
|
|
1296
|
+
? `<p class="bg-note">Work runs in the pi session you started the harness from.</p>`
|
|
1297
|
+
: `<p class="bg-note">Work runs in separate background pi sessions, each on the model its difficulty tier names.</p>`;
|
|
1298
|
+
const cards = workers.length
|
|
1299
|
+
? workers
|
|
1300
|
+
.map((w) => {
|
|
1301
|
+
const tokens = w.tokens ? w.tokens.inputTokens + w.tokens.outputTokens : 0;
|
|
1302
|
+
const ctx = typeof w.contextRatio === "number" ? `${Math.round(w.contextRatio * 100)}% ctx` : "";
|
|
1303
|
+
const bits = [
|
|
1304
|
+
`${w.level} · ${w.unitLabel}`,
|
|
1305
|
+
w.difficulty ? `${w.difficulty} tier` : "",
|
|
1306
|
+
`${w.turns ?? 0} turn${(w.turns ?? 0) === 1 ? "" : "s"}`,
|
|
1307
|
+
tokens ? `${tokens.toLocaleString("en-US")} tokens` : "",
|
|
1308
|
+
ctx,
|
|
1309
|
+
].filter(Boolean);
|
|
1310
|
+
return (
|
|
1311
|
+
`<li class="bg-worker is-${esc(w.state)}">` +
|
|
1312
|
+
`<span class="bg-dot" aria-hidden="true"></span>` +
|
|
1313
|
+
`<span class="bg-name">${esc(w.name)}</span>` +
|
|
1314
|
+
`<span class="bg-model">${esc(w.servedModel || w.model || "pi default")}</span>` +
|
|
1315
|
+
`<span class="bg-meta">${esc(bits.join(" · "))}</span>` +
|
|
1316
|
+
(w.doing ? `<span class="bg-doing">${esc(w.doing)}</span>` : "") +
|
|
1317
|
+
`</li>`
|
|
1318
|
+
);
|
|
1319
|
+
})
|
|
1320
|
+
.join("")
|
|
1321
|
+
: `<li class="bg-worker is-idle"><span class="bg-dot" aria-hidden="true"></span><span class="bg-name">idle</span><span class="bg-meta">no background session running</span></li>`;
|
|
1322
|
+
const log = activity.length
|
|
1323
|
+
? `<ol class="bg-log">` +
|
|
1324
|
+
activity
|
|
1325
|
+
.slice(-40)
|
|
1326
|
+
.reverse()
|
|
1327
|
+
.map(
|
|
1328
|
+
(l) =>
|
|
1329
|
+
`<li class="bg-line lvl-${esc(l.level)}"><span class="bg-time">${esc(l.at.slice(11, 16))}</span>` +
|
|
1330
|
+
(l.worker ? `<span class="bg-who">${esc(l.worker)}</span>` : `<span class="bg-who"></span>`) +
|
|
1331
|
+
`<span class="bg-text">${esc(l.text)}</span></li>`,
|
|
1332
|
+
)
|
|
1333
|
+
.join("") +
|
|
1334
|
+
`</ol>`
|
|
1335
|
+
: "";
|
|
1336
|
+
return `<section class="card background"><h2>Background</h2>${head}<ul class="bg-workers">${cards}</ul>${log}</section>`;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1213
1339
|
export function renderDashboard(state: DashboardState): string {
|
|
1214
1340
|
const list = normalizeForRender(state.list ?? { version: "0", baseRevision: 0, features: [] });
|
|
1215
1341
|
const features = list.features ?? [];
|
|
@@ -1349,6 +1475,7 @@ ${
|
|
|
1349
1475
|
: ""
|
|
1350
1476
|
}
|
|
1351
1477
|
${display.progress ? renderProgress(counts, progress.tasksTotal, progress.featuresDone, progress.featuresTotal) : ""}
|
|
1478
|
+
${renderBackground(state.engine ?? null, state.workers ?? [], state.activity ?? [])}
|
|
1352
1479
|
${renderGate(gate)}
|
|
1353
1480
|
${goalTabsHtml}${phaseTabsHtml}
|
|
1354
1481
|
${body}
|
package/src/ui/widget.ts
CHANGED
|
@@ -86,6 +86,27 @@ export type WidgetState = {
|
|
|
86
86
|
* there too — configuring how you read a plan once, rather than twice.
|
|
87
87
|
*/
|
|
88
88
|
display?: DisplayPolicy | null;
|
|
89
|
+
/**
|
|
90
|
+
* Where the work is actually happening.
|
|
91
|
+
*
|
|
92
|
+
* `background` means the run is being driven by separate pi sessions and
|
|
93
|
+
* this one is a control panel. It is the single most important thing the
|
|
94
|
+
* widget can say, because it is the difference between "my model is being
|
|
95
|
+
* spent on this" and "it is not".
|
|
96
|
+
*/
|
|
97
|
+
engine?: "background" | "main-session" | null;
|
|
98
|
+
/**
|
|
99
|
+
* The background sessions doing the work right now.
|
|
100
|
+
*
|
|
101
|
+
* This is the answer to "what is happening?" when the answer is no longer
|
|
102
|
+
* visible in the transcript — which, once the work moved out of this
|
|
103
|
+
* session, is always.
|
|
104
|
+
*/
|
|
105
|
+
workers?: WorkerLine[] | null;
|
|
106
|
+
/** The tail of the background log: what the workers have been doing. */
|
|
107
|
+
activity?: ActivityLine[] | null;
|
|
108
|
+
/** Rows of background log to draw. 0 hides the section. */
|
|
109
|
+
activityRows?: number;
|
|
89
110
|
/**
|
|
90
111
|
* What the human asked for, before a plan exists to hold a goal.
|
|
91
112
|
*
|
|
@@ -96,6 +117,26 @@ export type WidgetState = {
|
|
|
96
117
|
intake?: string | null;
|
|
97
118
|
};
|
|
98
119
|
|
|
120
|
+
/** One background pi session, as the widget draws it. */
|
|
121
|
+
export type WorkerLine = {
|
|
122
|
+
name: string;
|
|
123
|
+
unit: string;
|
|
124
|
+
level: string;
|
|
125
|
+
model: string;
|
|
126
|
+
difficulty?: string | null;
|
|
127
|
+
state: "starting" | "working" | "idle" | "closed" | "failed";
|
|
128
|
+
doing?: string | null;
|
|
129
|
+
tokens?: number;
|
|
130
|
+
contextRatio?: number | null;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export type ActivityLine = {
|
|
134
|
+
at: string;
|
|
135
|
+
level: "info" | "work" | "warn" | "error" | "good";
|
|
136
|
+
worker: string | null;
|
|
137
|
+
text: string;
|
|
138
|
+
};
|
|
139
|
+
|
|
99
140
|
export type WidgetView = {
|
|
100
141
|
/** First visible row, or null to follow the active task. */
|
|
101
142
|
scroll: number | null;
|
|
@@ -378,6 +419,78 @@ export function rowWindow(
|
|
|
378
419
|
* Returns plain strings so the host can hand them straight to
|
|
379
420
|
* `ctx.ui.setWidget`. Colour is embedded as ANSI when the styler is colouring.
|
|
380
421
|
*/
|
|
422
|
+
|
|
423
|
+
/** Rows of background log drawn by default. Enough to see the shape of a minute. */
|
|
424
|
+
export const ACTIVITY_ROWS = 5;
|
|
425
|
+
|
|
426
|
+
const ACTIVITY_ROLE: Record<ActivityLine["level"], Role> = {
|
|
427
|
+
info: "muted",
|
|
428
|
+
work: "text",
|
|
429
|
+
warn: "rework",
|
|
430
|
+
error: "blocked",
|
|
431
|
+
good: "success",
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const WORKER_ROLE: Record<WorkerLine["state"], Role> = {
|
|
435
|
+
starting: "active",
|
|
436
|
+
working: "active",
|
|
437
|
+
idle: "muted",
|
|
438
|
+
closed: "muted",
|
|
439
|
+
failed: "blocked",
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
/** `HH:MM` in local time — a full ISO stamp is six columns of nothing. */
|
|
443
|
+
function clock(iso: string): string {
|
|
444
|
+
const d = new Date(iso);
|
|
445
|
+
if (Number.isNaN(d.getTime())) return "--:--";
|
|
446
|
+
return String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* The background sessions, one line each.
|
|
451
|
+
*
|
|
452
|
+
* The model is on the line on purpose. The whole point of routing work out of
|
|
453
|
+
* the human's session is that a different model does it, and a claim you
|
|
454
|
+
* cannot see is a claim nobody believes — least of all after the version
|
|
455
|
+
* where it silently did not happen.
|
|
456
|
+
*/
|
|
457
|
+
export function renderWorkers(workers: readonly WorkerLine[], inner: number, g: GlyphSet, s: Styler): string[] {
|
|
458
|
+
const out: string[] = [];
|
|
459
|
+
for (const w of workers) {
|
|
460
|
+
const role = WORKER_ROLE[w.state] ?? "muted";
|
|
461
|
+
const dot = s.fg(role, w.state === "working" || w.state === "starting" ? g.inProgress : g.pending);
|
|
462
|
+
const head =
|
|
463
|
+
dot +
|
|
464
|
+
" " +
|
|
465
|
+
s.bold(s.fg(role, w.name)) +
|
|
466
|
+
s.fg("rule", " · ") +
|
|
467
|
+
s.fg("text", w.unit) +
|
|
468
|
+
(w.difficulty ? s.fg("muted", " (" + w.difficulty + ")") : "");
|
|
469
|
+
const model = s.fg("accent", w.model || "pi default");
|
|
470
|
+
const ctx =
|
|
471
|
+
typeof w.contextRatio === "number" ? s.fg(w.contextRatio > 0.75 ? "rework" : "muted", " " + Math.round(w.contextRatio * 100) + "%") : "";
|
|
472
|
+
const right = model + ctx;
|
|
473
|
+
const gap = Math.max(1, inner - width(head) - width(right));
|
|
474
|
+
out.push(truncate(head + " ".repeat(gap) + right, inner));
|
|
475
|
+
if (w.doing) out.push(truncate(" " + s.fg("muted", g.arrow + " " + w.doing), inner));
|
|
476
|
+
}
|
|
477
|
+
return out;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** The background log: the only place a human sees what the workers did. */
|
|
481
|
+
export function renderActivity(lines: readonly ActivityLine[], rows: number, inner: number, s: Styler): string[] {
|
|
482
|
+
const tail = lines.slice(-Math.max(0, rows));
|
|
483
|
+
return tail.map((l) =>
|
|
484
|
+
truncate(
|
|
485
|
+
s.fg("rule", clock(l.at)) +
|
|
486
|
+
" " +
|
|
487
|
+
(l.worker ? s.fg("accent", l.worker) + " " : "") +
|
|
488
|
+
s.fg(ACTIVITY_ROLE[l.level] ?? "muted", l.text),
|
|
489
|
+
inner,
|
|
490
|
+
),
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
381
494
|
export function renderWidget(state: WidgetState, options: WidgetOptions = {}): string[] {
|
|
382
495
|
const total = options.width ?? DEFAULT_WIDTH;
|
|
383
496
|
const s = options.styler ?? createStyler();
|
|
@@ -535,6 +648,27 @@ export function renderWidget(state: WidgetState, options: WidgetOptions = {}): s
|
|
|
535
648
|
}
|
|
536
649
|
}
|
|
537
650
|
|
|
651
|
+
// -- background sessions --------------------------------------------------
|
|
652
|
+
//
|
|
653
|
+
// Placed above the rail because, on a run driven by workers, this is the
|
|
654
|
+
// part of the widget that changes second by second. A human glancing at the
|
|
655
|
+
// terminal is checking that something is alive.
|
|
656
|
+
const workers = state.workers ?? [];
|
|
657
|
+
if (workers.length) {
|
|
658
|
+
push();
|
|
659
|
+
push(s.fg("rule", "background") + s.fg("rule", " " + g.rail.repeat(Math.max(1, inner - 12))));
|
|
660
|
+
for (const line of renderWorkers(workers, inner, g, s)) push(line);
|
|
661
|
+
} else if (state.engine === "background" && state.list.features.length > 0) {
|
|
662
|
+
push();
|
|
663
|
+
push(truncate(s.fg("muted", "background · no worker running — /infinity:run starts one"), inner));
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const activityRows = state.activityRows ?? (state.activity?.length ? ACTIVITY_ROWS : 0);
|
|
667
|
+
if (activityRows > 0 && state.activity?.length) {
|
|
668
|
+
push();
|
|
669
|
+
for (const line of renderActivity(state.activity, view.expanded ? activityRows * 3 : activityRows, inner, s)) push(line);
|
|
670
|
+
}
|
|
671
|
+
|
|
538
672
|
// -- phase rail -----------------------------------------------------------
|
|
539
673
|
if (display.rail) {
|
|
540
674
|
push();
|
package/src/ui/wizard.ts
CHANGED
|
@@ -109,14 +109,49 @@ async function pickThinkingLevel(prompt: Prompter, title: string): Promise<Think
|
|
|
109
109
|
return picked as ThinkingLevel;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
/**
|
|
113
|
+
* What a difficulty tier actually buys, at the level the human just chose.
|
|
114
|
+
*
|
|
115
|
+
* Difficulty is scored per task, but the *model* changes only where the
|
|
116
|
+
* session changes — that is what makes it a model boundary at all. So at
|
|
117
|
+
* feature-level handoff a feature runs on its hardest task's model and the
|
|
118
|
+
* easy tasks inside it get the hard model too. Saying that here, in the
|
|
119
|
+
* question, is the difference between a setting that behaves surprisingly and
|
|
120
|
+
* one the human chose with their eyes open.
|
|
121
|
+
*/
|
|
122
|
+
export function tierScopeNote(handoff: string): string {
|
|
123
|
+
switch (handoff) {
|
|
124
|
+
case "off":
|
|
125
|
+
case "goal":
|
|
126
|
+
return "one session for the whole run, so the run takes the hardest tier in the plan";
|
|
127
|
+
case "phase":
|
|
128
|
+
return "one session per phase, so every task in a phase takes that phase's hardest tier";
|
|
129
|
+
case "sprint":
|
|
130
|
+
return "one session per sprint, so every task in a sprint takes that sprint's hardest tier";
|
|
131
|
+
case "feature":
|
|
132
|
+
return "one session per feature, so every task in a feature takes that feature's hardest tier";
|
|
133
|
+
case "subtask":
|
|
134
|
+
return "one session per subtask, so each subtask can take its own tier";
|
|
135
|
+
default:
|
|
136
|
+
return "one session per task, so each task takes its own tier";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function pickModelsStep(
|
|
141
|
+
prompt: Prompter,
|
|
142
|
+
modelsFn?: WizardOptions["models"],
|
|
143
|
+
handoff: string = "task",
|
|
144
|
+
): Promise<{ router: NonNullable<IntakeAnswers["router"]> } | undefined> {
|
|
113
145
|
const models = modelsFn ? (await modelsFn()) ?? [] : [];
|
|
114
146
|
// First ask whether routing is even wanted — most runs don't need it, and
|
|
115
147
|
// skipping the 8 follow-up questions keeps the wizard short. Old tests that
|
|
116
148
|
// don't know about this step get routing off by default so they keep passing.
|
|
117
149
|
const ROUTE_ON = "yes — pick models per tier";
|
|
118
150
|
const ROUTE_OFF = "no — use pi's current model for everything";
|
|
119
|
-
const enablePick = await prompt.select(
|
|
151
|
+
const enablePick = await prompt.select(
|
|
152
|
+
`Route work by difficulty to different models? (${tierScopeNote(handoff)})`,
|
|
153
|
+
[ROUTE_ON, ROUTE_OFF],
|
|
154
|
+
);
|
|
120
155
|
if (enablePick === undefined) {
|
|
121
156
|
// No answer scripted (e.g. an older test) → treat as "off" so the
|
|
122
157
|
// wizard doesn't look cancelled to callers that only scripted four steps.
|
|
@@ -178,10 +213,11 @@ export async function runIntakeWizard(options: WizardOptions): Promise<WizardRes
|
|
|
178
213
|
const handoffLabels = handoffOptions.map((o) => line(o.label, o.help));
|
|
179
214
|
const handoffPick = await prompt.select(HANDOFF_QUESTION.title, handoffLabels);
|
|
180
215
|
if (handoffPick === undefined) return { cancelled: true };
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
216
|
+
// Every level the question offers, not the three this cast used to allow:
|
|
217
|
+
// picking "every feature" and getting the task-level narrowing is how a
|
|
218
|
+
// setting silently becomes a different setting.
|
|
219
|
+
const handoff = (handoffOptions[handoffLabels.indexOf(handoffPick)]?.value ??
|
|
220
|
+
"phase") as import("../core/types.ts").HandoffGranularity;
|
|
185
221
|
|
|
186
222
|
// -- 3b. research depth (only when research is in the pipeline) ----------
|
|
187
223
|
let researchDepth: import("../intake.ts").ResearchDepth | undefined;
|
|
@@ -201,7 +237,7 @@ export async function runIntakeWizard(options: WizardOptions): Promise<WizardRes
|
|
|
201
237
|
}
|
|
202
238
|
|
|
203
239
|
// -- 4. models ----------------------------------------------------------
|
|
204
|
-
const modelsAnswer = await pickModelsStep(prompt, options.models);
|
|
240
|
+
const modelsAnswer = await pickModelsStep(prompt, options.models, handoff);
|
|
205
241
|
if (modelsAnswer === undefined) return { cancelled: true };
|
|
206
242
|
|
|
207
243
|
// -- 5. execution (parallelism) -----------------------------------------
|