tickmarkr 1.71.0 → 1.73.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.
Files changed (53) hide show
  1. package/dist/adapters/codex.js +13 -3
  2. package/dist/cli/commands/approve.js +21 -3
  3. package/dist/cli/commands/fleet.d.ts +2 -0
  4. package/dist/cli/commands/fleet.js +212 -449
  5. package/dist/cli/commands/resume.js +10 -2
  6. package/dist/cli/commands/status.d.ts +2 -0
  7. package/dist/cli/commands/status.js +219 -30
  8. package/dist/cli/commands/ui.d.ts +6 -2
  9. package/dist/cli/commands/ui.js +2 -4
  10. package/dist/config/config.d.ts +4 -0
  11. package/dist/config/config.js +6 -1
  12. package/dist/drivers/herdr.d.ts +6 -0
  13. package/dist/drivers/herdr.js +169 -71
  14. package/dist/drivers/subprocess.d.ts +3 -3
  15. package/dist/drivers/subprocess.js +10 -5
  16. package/dist/gates/acceptance.js +41 -17
  17. package/dist/route/preference.d.ts +2 -1
  18. package/dist/route/preference.js +8 -2
  19. package/dist/run/daemon.d.ts +1 -0
  20. package/dist/run/daemon.js +139 -4
  21. package/dist/run/git.d.ts +2 -0
  22. package/dist/run/git.js +55 -0
  23. package/dist/run/journal.d.ts +6 -1
  24. package/dist/run/journal.js +28 -1
  25. package/dist/tui/ink/components.d.ts +38 -0
  26. package/dist/tui/ink/components.js +42 -0
  27. package/dist/tui/ink/fleet-app.d.ts +113 -0
  28. package/dist/tui/ink/fleet-app.js +678 -0
  29. package/dist/tui/ink/studio-app.d.ts +59 -0
  30. package/dist/tui/ink/studio-app.js +320 -0
  31. package/package.json +4 -1
  32. package/dist/tui/app.d.ts +0 -72
  33. package/dist/tui/app.js +0 -334
  34. package/dist/tui/engine.d.ts +0 -52
  35. package/dist/tui/engine.js +0 -92
  36. package/dist/tui/frame.d.ts +0 -22
  37. package/dist/tui/frame.js +0 -56
  38. package/dist/tui/input.d.ts +0 -41
  39. package/dist/tui/input.js +0 -100
  40. package/dist/tui/views/consult-dossier.d.ts +0 -49
  41. package/dist/tui/views/consult-dossier.js +0 -169
  42. package/dist/tui/views/diff-modal.d.ts +0 -9
  43. package/dist/tui/views/diff-modal.js +0 -19
  44. package/dist/tui/views/fleet-view.d.ts +0 -91
  45. package/dist/tui/views/fleet-view.js +0 -337
  46. package/dist/tui/views/preview-view.d.ts +0 -7
  47. package/dist/tui/views/preview-view.js +0 -80
  48. package/dist/tui/views/profile-view.d.ts +0 -15
  49. package/dist/tui/views/profile-view.js +0 -93
  50. package/dist/tui/views/routing-view.d.ts +0 -48
  51. package/dist/tui/views/routing-view.js +0 -234
  52. package/dist/tui/views/runs-view.d.ts +0 -70
  53. package/dist/tui/views/runs-view.js +0 -387
@@ -0,0 +1,678 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { emitKeypressEvents } from "node:readline";
3
+ import { PassThrough } from "node:stream";
4
+ import { render, Text, useApp, useInput } from "ink";
5
+ import { useRef, useState } from "react";
6
+ import { TIERS } from "../../graph/schema.js";
7
+ import { FleetListScreen, FleetReviewScreen, ToggleMark } from "./components.js";
8
+ const STEERING_KEYS = ["review", "consult"];
9
+ const escape = String.fromCharCode(27);
10
+ const inkBookkeepingWrites = new Set(["", `${escape}[?25l`, `${escape}[?25h`, `${escape}[?2026h`, `${escape}[?2026l`]);
11
+ function inkOutput(output) {
12
+ if (typeof output.on === "function" && typeof output.off === "function")
13
+ return output;
14
+ const facade = Object.create(output);
15
+ const write = output.write.bind(output);
16
+ facade.write = ((chunk, ...args) => {
17
+ const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
18
+ if (inkBookkeepingWrites.has(text))
19
+ return true;
20
+ return Reflect.apply(write, output, [chunk, ...args]);
21
+ });
22
+ facade.on = () => facade;
23
+ facade.off = () => facade;
24
+ return facade;
25
+ }
26
+ function inkInput(input, initialInput) {
27
+ const productionInput = typeof input.ref === "function" && typeof input.unref === "function";
28
+ // Isolate Ink's listeners on a bridge so every editor exit can detach the
29
+ // one listener it owns from the operator's terminal. Older injected FleetIO
30
+ // streams can arrive as decoded keypress events, while real TTYs forward raw
31
+ // data and leave decoding entirely to Ink.
32
+ const stream = new PassThrough();
33
+ stream.isTTY = input.isTTY;
34
+ stream.setRawMode = input.setRawMode?.bind(input);
35
+ stream.ref = () => {
36
+ if (productionInput)
37
+ input.ref();
38
+ return stream;
39
+ };
40
+ stream.unref = () => {
41
+ if (productionInput)
42
+ input.unref();
43
+ return stream;
44
+ };
45
+ const queued = [...initialInput];
46
+ let active = true;
47
+ let scheduled;
48
+ const pump = () => {
49
+ scheduled = undefined;
50
+ if (!active)
51
+ return;
52
+ const next = queued.shift();
53
+ if (next === undefined)
54
+ return;
55
+ stream.write(next);
56
+ scheduled = setTimeout(pump, 0);
57
+ };
58
+ const onData = (chunk) => {
59
+ queued.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
60
+ scheduled ??= setTimeout(pump, 0);
61
+ };
62
+ const onKeypress = (sequence, key) => {
63
+ const token = key?.sequence ?? sequence;
64
+ if (token === undefined)
65
+ return;
66
+ queued.push(token);
67
+ scheduled ??= setTimeout(pump, 0);
68
+ };
69
+ if (productionInput) {
70
+ input.on("data", onData);
71
+ }
72
+ else {
73
+ emitKeypressEvents(input);
74
+ input.on("keypress", onKeypress);
75
+ }
76
+ input.resume();
77
+ if (queued.length > 0)
78
+ scheduled = setTimeout(pump, 0);
79
+ return {
80
+ stream: stream,
81
+ stop() {
82
+ active = false;
83
+ if (scheduled)
84
+ clearTimeout(scheduled);
85
+ if (productionInput)
86
+ input.off("data", onData);
87
+ else
88
+ input.off("keypress", onKeypress);
89
+ input.pause();
90
+ stream.end();
91
+ },
92
+ };
93
+ }
94
+ export function formatDoctorAge(ageMs) {
95
+ if (ageMs === null)
96
+ return "no probe data";
97
+ const mins = Math.floor(ageMs / 60_000);
98
+ if (mins < 60)
99
+ return `${mins}m old`;
100
+ return `${Math.floor(mins / 60)}h old`;
101
+ }
102
+ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewOverlay, reloadGuard, }) {
103
+ const { exit } = useApp();
104
+ const [screen, setScreen] = useState("probe");
105
+ const screenRef = useRef(screen);
106
+ const [cursor, setCursor] = useState(0);
107
+ const cursorRef = useRef(cursor);
108
+ const denyRef = useRef(undefined);
109
+ denyRef.current ??= new Set(initialDenyAdapters);
110
+ const [denyAdapters, setDenyAdapters] = useState(() => new Set(initialDenyAdapters));
111
+ const denyModelsRef = useRef(undefined);
112
+ denyModelsRef.current ??= new Set(initialDenyModels);
113
+ const [denyModels, setDenyModels] = useState(() => new Set(initialDenyModels));
114
+ const classificationsRef = useRef([]);
115
+ const [, setClassificationRevision] = useState(0);
116
+ const modelGroupRef = useRef(0);
117
+ const [modelGroup, setModelGroup] = useState(0);
118
+ const modelCursorRef = useRef(0);
119
+ const [modelCursor, setModelCursor] = useState(0);
120
+ const tierCursorRef = useRef(0);
121
+ const [tierCursor, setTierCursor] = useState(0);
122
+ const pendingClassificationRef = useRef(null);
123
+ const noteRef = useRef("");
124
+ const [note, setNote] = useState("");
125
+ const [notice, setNotice] = useState("");
126
+ const modeCursorRef = useRef(undefined);
127
+ modeCursorRef.current ??= Math.max(modeOptions.findIndex((option) => option.id === initialMode), 0);
128
+ const [modeCursor, setModeCursor] = useState(modeCursorRef.current);
129
+ const selectedModeRef = useRef(initialMode);
130
+ const mapRef = useRef(undefined);
131
+ mapRef.current ??= structuredClone(initialMap);
132
+ const [map, setMap] = useState(() => structuredClone(initialMap));
133
+ const shapeCursorRef = useRef(0);
134
+ const [shapeCursor, setShapeCursor] = useState(0);
135
+ const candidateCursorRef = useRef(0);
136
+ const [candidateCursor, setCandidateCursor] = useState(0);
137
+ const candidatesRef = useRef([]);
138
+ const preferCursorRef = useRef(0);
139
+ const [preferCursor, setPreferCursor] = useState(0);
140
+ const preferRowsRef = useRef([]);
141
+ const preferChainRef = useRef([]);
142
+ const [, setPreferRevision] = useState(0);
143
+ const steeringRef = useRef(undefined);
144
+ steeringRef.current ??= structuredClone(initialSteering);
145
+ const [steering, setSteering] = useState(() => structuredClone(initialSteering));
146
+ const steeringCursorRef = useRef(0);
147
+ const [steeringCursor, setSteeringCursor] = useState(0);
148
+ const steeringPickerRef = useRef("review");
149
+ const steeringRowsRef = useRef([]);
150
+ const steeringChainRef = useRef([]);
151
+ const steeringPickerCursorRef = useRef(0);
152
+ const [steeringPickerCursor, setSteeringPickerCursor] = useState(0);
153
+ const [, setSteeringPickerRevision] = useState(0);
154
+ const reviewRef = useRef(null);
155
+ const [review, setReview] = useState(null);
156
+ const doneRef = useRef(false);
157
+ const enabledModelGroups = () => modelGroups.filter((group) => !denyRef.current?.has(group.adapter));
158
+ const stagedMap = () => mapRef.current;
159
+ const currentModelRows = () => {
160
+ const group = enabledModelGroups()[modelGroupRef.current];
161
+ if (!group)
162
+ return [];
163
+ return group.rows.map((row) => {
164
+ const staged = classificationsRef.current.find((classification) => classification.adapter === group.adapter && classification.model === row.model);
165
+ return staged ? { ...row, tier: staged.tier } : row;
166
+ });
167
+ };
168
+ const editorState = () => ({
169
+ denyAdapters: [...(denyRef.current ?? [])].sort(),
170
+ denyModels: [...(denyModelsRef.current ?? [])].sort(),
171
+ classifications: classificationsRef.current,
172
+ selectedMode: selectedModeRef.current,
173
+ map: stagedMap(),
174
+ steering: structuredClone(steeringRef.current),
175
+ });
176
+ const finish = (result) => {
177
+ if (doneRef.current)
178
+ return;
179
+ doneRef.current = true;
180
+ exit(result);
181
+ };
182
+ const finishEditor = () => {
183
+ showScreen("steering");
184
+ };
185
+ const showScreen = (next) => {
186
+ screenRef.current = next;
187
+ setScreen(next);
188
+ };
189
+ useInput((input, key) => {
190
+ if (doneRef.current)
191
+ return;
192
+ if (key.escape && (screenRef.current === "candidates" || screenRef.current === "shape-prefer")) {
193
+ showScreen("shapes");
194
+ return;
195
+ }
196
+ if (key.escape && screenRef.current === "steering-prefer") {
197
+ showScreen("steering");
198
+ return;
199
+ }
200
+ if (key.escape || (screenRef.current !== "provenance" && input === "q") || (key.ctrl && input === "c")) {
201
+ finish({ kind: "quit" });
202
+ return;
203
+ }
204
+ if (screenRef.current === "probe") {
205
+ if (input === "r") {
206
+ finish({ kind: "refresh" });
207
+ }
208
+ else if (key.return) {
209
+ showScreen("agents");
210
+ }
211
+ return;
212
+ }
213
+ if (screenRef.current === "agents") {
214
+ if (key.downArrow || input === "j") {
215
+ const next = Math.min(cursorRef.current + 1, Math.max(agents.length - 1, 0));
216
+ cursorRef.current = next;
217
+ setCursor(next);
218
+ return;
219
+ }
220
+ if (key.upArrow || input === "k") {
221
+ const next = Math.max(cursorRef.current - 1, 0);
222
+ cursorRef.current = next;
223
+ setCursor(next);
224
+ return;
225
+ }
226
+ if (input === " " && agents.length > 0) {
227
+ const next = new Set(denyRef.current);
228
+ const id = agents[cursorRef.current].id;
229
+ if (next.has(id))
230
+ next.delete(id);
231
+ else
232
+ next.add(id);
233
+ denyRef.current = next;
234
+ setDenyAdapters(next);
235
+ return;
236
+ }
237
+ if (key.return) {
238
+ if (enabledModelGroups().length === 0)
239
+ showScreen("modes");
240
+ else
241
+ showScreen("models");
242
+ }
243
+ return;
244
+ }
245
+ if (screenRef.current === "models") {
246
+ const rows = currentModelRows();
247
+ if (key.downArrow || input === "j") {
248
+ const next = Math.min(modelCursorRef.current + 1, Math.max(rows.length - 1, 0));
249
+ modelCursorRef.current = next;
250
+ setModelCursor(next);
251
+ setNotice("");
252
+ return;
253
+ }
254
+ if (key.upArrow || input === "k") {
255
+ const next = Math.max(modelCursorRef.current - 1, 0);
256
+ modelCursorRef.current = next;
257
+ setModelCursor(next);
258
+ setNotice("");
259
+ return;
260
+ }
261
+ const group = enabledModelGroups()[modelGroupRef.current];
262
+ const row = rows[modelCursorRef.current];
263
+ if (input === " " && group && row?.tier) {
264
+ const id = `${group.adapter}:${row.model}`;
265
+ const next = new Set(denyModelsRef.current);
266
+ if (next.has(id))
267
+ next.delete(id);
268
+ else
269
+ next.add(id);
270
+ denyModelsRef.current = next;
271
+ setDenyModels(next);
272
+ return;
273
+ }
274
+ if (input === "t" && group && row) {
275
+ if (row.tier) {
276
+ setNotice("fleet: tier reassignment on classified models is not supported in v1 — edit config directly");
277
+ return;
278
+ }
279
+ tierCursorRef.current = 0;
280
+ setTierCursor(0);
281
+ pendingClassificationRef.current = {
282
+ adapter: group.adapter,
283
+ model: row.model,
284
+ tier: TIERS[0],
285
+ };
286
+ showScreen("tiers");
287
+ return;
288
+ }
289
+ if (key.return) {
290
+ if (modelGroupRef.current + 1 < enabledModelGroups().length) {
291
+ const next = modelGroupRef.current + 1;
292
+ modelGroupRef.current = next;
293
+ setModelGroup(next);
294
+ modelCursorRef.current = 0;
295
+ setModelCursor(0);
296
+ }
297
+ else {
298
+ showScreen("modes");
299
+ }
300
+ }
301
+ return;
302
+ }
303
+ if (screenRef.current === "tiers") {
304
+ if (key.downArrow || input === "j") {
305
+ const next = Math.min(tierCursorRef.current + 1, TIERS.length - 1);
306
+ tierCursorRef.current = next;
307
+ setTierCursor(next);
308
+ return;
309
+ }
310
+ if (key.upArrow || input === "k") {
311
+ const next = Math.max(tierCursorRef.current - 1, 0);
312
+ tierCursorRef.current = next;
313
+ setTierCursor(next);
314
+ return;
315
+ }
316
+ if (key.return && pendingClassificationRef.current) {
317
+ pendingClassificationRef.current.tier = TIERS[tierCursorRef.current];
318
+ noteRef.current = "";
319
+ setNote("");
320
+ setNotice("");
321
+ showScreen("provenance");
322
+ }
323
+ return;
324
+ }
325
+ if (screenRef.current === "provenance") {
326
+ if (key.return) {
327
+ if (!noteRef.current.trim()) {
328
+ setNotice("fleet: a typed benchmark-provenance note is required");
329
+ return;
330
+ }
331
+ const pending = pendingClassificationRef.current;
332
+ if (pending) {
333
+ const next = [...classificationsRef.current, { ...pending, note: noteRef.current.trim() }];
334
+ classificationsRef.current = next;
335
+ setClassificationRevision((revision) => revision + 1);
336
+ }
337
+ pendingClassificationRef.current = null;
338
+ setNotice("");
339
+ showScreen("models");
340
+ return;
341
+ }
342
+ if (key.backspace || key.delete) {
343
+ const next = noteRef.current.slice(0, -1);
344
+ noteRef.current = next;
345
+ setNote(next);
346
+ return;
347
+ }
348
+ if (input && !key.ctrl && !key.meta) {
349
+ const next = noteRef.current + input;
350
+ noteRef.current = next;
351
+ setNote(next);
352
+ }
353
+ }
354
+ if (screenRef.current === "modes") {
355
+ const currentModeCursor = modeCursorRef.current ?? 0;
356
+ if (key.downArrow || input === "j") {
357
+ const next = Math.min(currentModeCursor + 1, Math.max(modeOptions.length - 1, 0));
358
+ modeCursorRef.current = next;
359
+ setModeCursor(next);
360
+ return;
361
+ }
362
+ if (key.upArrow || input === "k") {
363
+ const next = Math.max(currentModeCursor - 1, 0);
364
+ modeCursorRef.current = next;
365
+ setModeCursor(next);
366
+ return;
367
+ }
368
+ if (key.return && modeOptions[currentModeCursor]) {
369
+ selectedModeRef.current = modeOptions[currentModeCursor].id;
370
+ showScreen("shapes");
371
+ }
372
+ return;
373
+ }
374
+ if (screenRef.current === "shapes") {
375
+ const rows = shapeRows(selectedModeRef.current, stagedMap());
376
+ if (key.downArrow || input === "j") {
377
+ const next = Math.min(shapeCursorRef.current + 1, Math.max(rows.length - 1, 0));
378
+ shapeCursorRef.current = next;
379
+ setShapeCursor(next);
380
+ return;
381
+ }
382
+ if (key.upArrow || input === "k") {
383
+ const next = Math.max(shapeCursorRef.current - 1, 0);
384
+ shapeCursorRef.current = next;
385
+ setShapeCursor(next);
386
+ return;
387
+ }
388
+ const shape = rows[shapeCursorRef.current]?.id;
389
+ if (input === "a" && shape) {
390
+ const nextEntry = { ...stagedMap()[shape] };
391
+ delete nextEntry.pin;
392
+ const nextMap = { ...stagedMap(), [shape]: nextEntry };
393
+ mapRef.current = nextMap;
394
+ setMap(nextMap);
395
+ return;
396
+ }
397
+ if (input === "p" && shape) {
398
+ candidatesRef.current = candidatesForShape(shape, selectedModeRef.current, stagedMap());
399
+ candidateCursorRef.current = 0;
400
+ setCandidateCursor(0);
401
+ showScreen("candidates");
402
+ return;
403
+ }
404
+ if (input === "f" && shape) {
405
+ const current = stagedMap()[shape]?.prefer ?? [];
406
+ preferRowsRef.current = preferOptionsForShape(shape, current);
407
+ preferChainRef.current = current.slice();
408
+ preferCursorRef.current = 0;
409
+ setPreferCursor(0);
410
+ showScreen("shape-prefer");
411
+ return;
412
+ }
413
+ if (key.return)
414
+ finishEditor();
415
+ return;
416
+ }
417
+ if (screenRef.current === "candidates") {
418
+ if (key.downArrow || input === "j") {
419
+ const next = Math.min(candidateCursorRef.current + 1, Math.max(candidatesRef.current.length - 1, 0));
420
+ candidateCursorRef.current = next;
421
+ setCandidateCursor(next);
422
+ return;
423
+ }
424
+ if (key.upArrow || input === "k") {
425
+ const next = Math.max(candidateCursorRef.current - 1, 0);
426
+ candidateCursorRef.current = next;
427
+ setCandidateCursor(next);
428
+ return;
429
+ }
430
+ if (key.return && candidatesRef.current[candidateCursorRef.current]) {
431
+ const shape = shapeRows(selectedModeRef.current, stagedMap())[shapeCursorRef.current]?.id;
432
+ if (shape) {
433
+ const picked = candidatesRef.current[candidateCursorRef.current];
434
+ const nextMap = { ...stagedMap(), [shape]: { pin: picked.pin } };
435
+ mapRef.current = nextMap;
436
+ setMap(nextMap);
437
+ }
438
+ showScreen("shapes");
439
+ }
440
+ return;
441
+ }
442
+ if (screenRef.current === "shape-prefer") {
443
+ if (key.downArrow || input === "j") {
444
+ const next = Math.min(preferCursorRef.current + 1, Math.max(preferRowsRef.current.length - 1, 0));
445
+ preferCursorRef.current = next;
446
+ setPreferCursor(next);
447
+ return;
448
+ }
449
+ if (key.upArrow || input === "k") {
450
+ const next = Math.max(preferCursorRef.current - 1, 0);
451
+ preferCursorRef.current = next;
452
+ setPreferCursor(next);
453
+ return;
454
+ }
455
+ if (input === " " && preferRowsRef.current[preferCursorRef.current]) {
456
+ const option = preferRowsRef.current[preferCursorRef.current];
457
+ const at = preferChainRef.current.indexOf(option);
458
+ if (at === -1)
459
+ preferChainRef.current.push(option);
460
+ else
461
+ preferChainRef.current.splice(at, 1);
462
+ setPreferRevision((revision) => revision + 1);
463
+ return;
464
+ }
465
+ if (key.return) {
466
+ const shape = shapeRows(selectedModeRef.current, stagedMap())[shapeCursorRef.current]?.id;
467
+ if (shape) {
468
+ const nextEntry = { ...stagedMap()[shape] };
469
+ if (preferChainRef.current.length)
470
+ nextEntry.prefer = preferChainRef.current.slice();
471
+ else
472
+ delete nextEntry.prefer;
473
+ const nextMap = { ...stagedMap(), [shape]: nextEntry };
474
+ mapRef.current = nextMap;
475
+ setMap(nextMap);
476
+ }
477
+ showScreen("shapes");
478
+ }
479
+ return;
480
+ }
481
+ if (screenRef.current === "steering") {
482
+ if (key.downArrow || input === "j") {
483
+ const next = Math.min(steeringCursorRef.current + 1, STEERING_KEYS.length - 1);
484
+ steeringCursorRef.current = next;
485
+ setSteeringCursor(next);
486
+ return;
487
+ }
488
+ if (key.upArrow || input === "k") {
489
+ const next = Math.max(steeringCursorRef.current - 1, 0);
490
+ steeringCursorRef.current = next;
491
+ setSteeringCursor(next);
492
+ return;
493
+ }
494
+ if (input === "f") {
495
+ const which = STEERING_KEYS[steeringCursorRef.current];
496
+ const current = steeringRef.current?.[which] ?? [];
497
+ steeringPickerRef.current = which;
498
+ steeringRowsRef.current = steeringOptionsFor(which, current);
499
+ steeringChainRef.current = current.slice();
500
+ steeringPickerCursorRef.current = 0;
501
+ setSteeringPickerCursor(0);
502
+ setNotice("");
503
+ showScreen("steering-prefer");
504
+ return;
505
+ }
506
+ if (key.return) {
507
+ const nextReview = reviewOverlay(editorState());
508
+ if (nextReview.kind === "empty") {
509
+ finish({ kind: "no-changes" });
510
+ return;
511
+ }
512
+ reviewRef.current = nextReview;
513
+ setReview(nextReview);
514
+ setNotice("");
515
+ showScreen("review");
516
+ }
517
+ return;
518
+ }
519
+ if (screenRef.current === "steering-prefer") {
520
+ if (key.downArrow || input === "j") {
521
+ const next = Math.min(steeringPickerCursorRef.current + 1, Math.max(steeringRowsRef.current.length - 1, 0));
522
+ steeringPickerCursorRef.current = next;
523
+ setSteeringPickerCursor(next);
524
+ return;
525
+ }
526
+ if (key.upArrow || input === "k") {
527
+ const next = Math.max(steeringPickerCursorRef.current - 1, 0);
528
+ steeringPickerCursorRef.current = next;
529
+ setSteeringPickerCursor(next);
530
+ return;
531
+ }
532
+ if (input === " " && steeringRowsRef.current[steeringPickerCursorRef.current]) {
533
+ const option = steeringRowsRef.current[steeringPickerCursorRef.current];
534
+ const at = steeringChainRef.current.indexOf(option);
535
+ if (at === -1)
536
+ steeringChainRef.current.push(option);
537
+ else
538
+ steeringChainRef.current.splice(at, 1);
539
+ setSteeringPickerRevision((revision) => revision + 1);
540
+ return;
541
+ }
542
+ if (key.return) {
543
+ const which = steeringPickerRef.current;
544
+ const next = {
545
+ ...steeringRef.current,
546
+ [which]: steeringChainRef.current.length ? steeringChainRef.current.slice() : undefined,
547
+ };
548
+ steeringRef.current = next;
549
+ setSteering(next);
550
+ setNotice("");
551
+ showScreen("steering");
552
+ }
553
+ return;
554
+ }
555
+ if (screenRef.current === "review") {
556
+ if (input === "n") {
557
+ finish({ kind: "discard" });
558
+ return;
559
+ }
560
+ if (input === "y" && reviewRef.current) {
561
+ const loadError = reloadGuard(reviewRef.current.after);
562
+ if (loadError) {
563
+ setNotice(`fleet: config loader rejects the proposed overlay — ${loadError}`);
564
+ reviewRef.current = null;
565
+ setReview(null);
566
+ showScreen("steering");
567
+ return;
568
+ }
569
+ finish({ kind: "write", review: reviewRef.current });
570
+ }
571
+ }
572
+ });
573
+ if (screen === "probe") {
574
+ return (_jsx(FleetListScreen, { title: "step 1/6 \u00B7 probe data", legend: "enter continue \u00B7 r refresh via doctor \u00B7 esc/q quit", rows: [{
575
+ id: "doctor",
576
+ content: _jsx(Text, { children: `probe data: ${formatDoctorAge(ageMs)} (.tickmarkr/doctor.json)` }),
577
+ }], cursor: 0 }));
578
+ }
579
+ if (screen === "agents") {
580
+ const rows = agents.map((agent) => {
581
+ const active = !denyAdapters.has(agent.id);
582
+ return {
583
+ id: agent.id,
584
+ content: (_jsxs(_Fragment, { children: [_jsx(ToggleMark, { active: active }), _jsx(Text, { children: ` ${agent.id} ${agent.version} ${agent.authed ? "authed" : "unauthed"}` })] })),
585
+ };
586
+ });
587
+ return (_jsx(FleetListScreen, { title: "step 2/6 \u00B7 agent CLIs", legend: "\u2191\u2193/jk move \u00B7 space toggle \u00B7 enter next \u00B7 esc/q quit", rows: rows, cursor: cursor }));
588
+ }
589
+ const group = enabledModelGroups()[modelGroup];
590
+ const modelRows = currentModelRows();
591
+ if (screen === "models") {
592
+ const rows = modelRows.map((row) => {
593
+ const denied = group ? denyModels.has(`${group.adapter}:${row.model}`) : false;
594
+ return {
595
+ id: row.model,
596
+ content: row.tier ? (_jsxs(_Fragment, { children: [_jsx(ToggleMark, { active: !denied }), _jsx(Text, { children: ` ${row.model} ${row.tier} ${denied ? "denied" : "allowed"}` })] })) : _jsx(Text, { children: `( ) ${row.model} ??? unclassified${row.detectedAt ? ` (${row.detectedAt})` : ""}` }),
597
+ };
598
+ });
599
+ return (_jsx(FleetListScreen, { title: `step 3/6 · models · ${group?.adapter ?? ""}`, legend: "\u2191\u2193/jk move \u00B7 space toggle \u00B7 t tier \u00B7 enter next \u00B7 esc/q quit", rows: rows, cursor: modelCursor, details: notice ? [notice] : [] }));
600
+ }
601
+ if (screen === "tiers") {
602
+ return (_jsx(FleetListScreen, { title: `pick · tier · ${pendingClassificationRef.current?.adapter}:${pendingClassificationRef.current?.model}`, legend: "\u2191\u2193/jk move \u00B7 enter select \u00B7 esc/q quit", rows: TIERS.map((tier) => ({ id: tier, content: _jsx(Text, { children: tier }) })), cursor: tierCursor }));
603
+ }
604
+ if (screen === "provenance")
605
+ return (_jsx(FleetListScreen, { title: `benchmark provenance · ${pendingClassificationRef.current?.adapter}:${pendingClassificationRef.current?.model}`, legend: "type note \u00B7 enter apply \u00B7 esc cancel", rows: [{ id: "note", content: _jsx(Text, { children: note || "benchmark provenance note (required):" }) }], cursor: 0, details: notice ? [notice] : [] }));
606
+ if (screen === "modes") {
607
+ return (_jsx(FleetListScreen, { title: "step 4/6 \u00B7 routing mode", legend: "\u2191\u2193/jk move \u00B7 enter select \u00B7 esc/q quit", rows: modeOptions.map((option) => ({
608
+ id: option.id,
609
+ content: (_jsxs(_Fragment, { children: [option.id === initialMode ? _jsx(ToggleMark, { active: true }) : _jsx(Text, { children: " " }), _jsx(Text, { children: ` ${option.id.padEnd(11)} ${option.gloss}` })] })),
610
+ })), cursor: modeCursor, details: modePreview(modeOptions[modeCursor].id, map) }));
611
+ }
612
+ const renderedShapes = shapeRows(selectedModeRef.current, map);
613
+ if (screen === "shapes") {
614
+ return (_jsx(FleetListScreen, { title: "step 5/6 \u00B7 shape routing", legend: "\u2191\u2193/jk move \u00B7 a auto \u00B7 p pin \u00B7 f prefer \u00B7 enter next \u00B7 esc/q quit", rows: renderedShapes.map((row) => ({ id: row.id, content: _jsx(Text, { children: row.label }) })), cursor: shapeCursor }));
615
+ }
616
+ const shape = renderedShapes[shapeCursor]?.id;
617
+ if (screen === "candidates") {
618
+ return (_jsx(FleetListScreen, { title: `pick · ${shape}`, legend: "\u2191\u2193/jk move \u00B7 enter pin \u00B7 esc cancel \u00B7 q quit", rows: candidatesRef.current.map((candidate) => ({
619
+ id: candidate.id,
620
+ content: _jsx(Text, { children: candidate.label }),
621
+ })), cursor: candidateCursor }));
622
+ }
623
+ if (screen === "shape-prefer")
624
+ return (_jsx(FleetListScreen, { title: `pick · ${shape}.prefer`, legend: "\u2191\u2193/jk move \u00B7 space add/drop \u00B7 enter apply (empty clears) \u00B7 esc cancel \u00B7 q quit", rows: preferRowsRef.current.map((option) => {
625
+ const at = preferChainRef.current.indexOf(option);
626
+ return { id: option, content: _jsx(Text, { children: `${at === -1 ? "·" : String(at + 1)} ${option}` }) };
627
+ }), cursor: preferCursor }));
628
+ if (screen === "steering") {
629
+ return (_jsx(FleetListScreen, { title: "step 6/6 \u00B7 steering", legend: "\u2191\u2193/jk move \u00B7 f prefer \u00B7 enter review \u00B7 esc/q quit", rows: STEERING_KEYS.map((key) => ({
630
+ id: key,
631
+ content: _jsx(Text, { children: `${key}.prefer → ${steering[key]?.join(", ") ?? "(none)"}` }),
632
+ })), cursor: steeringCursor, details: notice ? [notice] : [] }));
633
+ }
634
+ if (screen === "steering-prefer") {
635
+ const which = steeringPickerRef.current;
636
+ return (_jsx(FleetListScreen, { title: `pick · ${which}.prefer`, legend: "\u2191\u2193/jk move \u00B7 space add/drop \u00B7 enter apply (empty clears) \u00B7 esc cancel \u00B7 q quit", rows: steeringRowsRef.current.map((option) => {
637
+ const at = steeringChainRef.current.indexOf(option);
638
+ return { id: option, content: _jsx(Text, { children: `${at === -1 ? "·" : String(at + 1)} ${option}` }) };
639
+ }), cursor: steeringPickerCursor }));
640
+ }
641
+ return (_jsx(FleetReviewScreen, { title: "review \u00B7 overlay diff", legend: "y write \u00B7 n discard \u00B7 esc/q quit", diff: review?.diff ?? "" }));
642
+ }
643
+ export async function runFleetInkEditor({ ageMs, adapters, health, initialDenyAdapters, initialDenyModels, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewOverlay, reloadGuard, initialInput = [], input, output, debug = false, }) {
644
+ const agents = adapters.flatMap((adapter) => {
645
+ const state = health[adapter.id];
646
+ if (!state?.installed)
647
+ return [];
648
+ return [{
649
+ id: adapter.id,
650
+ version: state.version ?? "installed",
651
+ authed: state.authed,
652
+ }];
653
+ });
654
+ const bridgedInput = inkInput(input, initialInput);
655
+ const legacyOutput = typeof output.on !== "function" || typeof output.off !== "function";
656
+ const app = render(_jsx(FleetApp, { ageMs: ageMs, agents: agents, initialDenyAdapters: initialDenyAdapters, initialDenyModels: initialDenyModels, modelGroups: modelGroups, initialMode: initialMode, modeOptions: modeOptions, initialMap: initialMap, modePreview: modePreview, shapeRows: shapeRows, candidatesForShape: candidatesForShape, preferOptionsForShape: preferOptionsForShape, initialSteering: initialSteering, steeringOptionsFor: steeringOptionsFor, reviewOverlay: reviewOverlay, reloadGuard: reloadGuard }), {
657
+ // FleetIO's injected stream predates Ink and did not require ref/unref.
658
+ // Real terminal streams pass through unchanged; the compatibility facade
659
+ // adds only those lifecycle methods and leaves input decoding to Ink.
660
+ stdin: bridgedInput.stream,
661
+ stdout: inkOutput(output),
662
+ exitOnCtrlC: false,
663
+ patchConsole: false,
664
+ // Legacy FleetIO outputs collected one complete frame per keypress before the Ink
665
+ // migration. Disable Ink's render throttling only for that injected facade so a
666
+ // cold-start byte sequence cannot collapse intermediate compatibility frames.
667
+ debug: debug || legacyOutput,
668
+ });
669
+ let result;
670
+ try {
671
+ result = await app.waitUntilExit();
672
+ return result;
673
+ }
674
+ finally {
675
+ app.unmount();
676
+ bridgedInput.stop();
677
+ }
678
+ }