tickmarkr 2.6.5 → 2.6.7

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 (56) hide show
  1. package/README.md +7 -3
  2. package/dist/adapters/claude-code.js +8 -2
  3. package/dist/adapters/codex-commit-check.d.ts +16 -4
  4. package/dist/adapters/codex-commit-check.js +150 -26
  5. package/dist/adapters/codex.d.ts +1 -0
  6. package/dist/adapters/codex.js +20 -8
  7. package/dist/adapters/types.d.ts +4 -1
  8. package/dist/cli/commands/beat.d.ts +6 -1
  9. package/dist/cli/commands/beat.js +123 -76
  10. package/dist/cli/commands/doctor.js +3 -1
  11. package/dist/cli/commands/fleet.js +368 -85
  12. package/dist/cli/commands/init.js +23 -9
  13. package/dist/cli/commands/plan.js +12 -1
  14. package/dist/cli/commands/verify.js +37 -9
  15. package/dist/cli/help.d.ts +1 -0
  16. package/dist/cli/help.js +10 -5
  17. package/dist/cli/index.d.ts +1 -1
  18. package/dist/cli/index.js +1 -1
  19. package/dist/config/fleet-overlay.d.ts +14 -0
  20. package/dist/config/fleet-overlay.js +154 -25
  21. package/dist/drivers/orca.d.ts +17 -3
  22. package/dist/drivers/orca.js +64 -20
  23. package/dist/gates/baseline.d.ts +9 -0
  24. package/dist/gates/baseline.js +14 -3
  25. package/dist/gates/cache.js +1 -1
  26. package/dist/gates/review.js +15 -0
  27. package/dist/gates/run-gates.d.ts +20 -7
  28. package/dist/gates/run-gates.js +345 -210
  29. package/dist/gates/test-manifest.d.ts +11 -0
  30. package/dist/gates/test-manifest.js +151 -111
  31. package/dist/gates/timeout-shaped.d.ts +3 -0
  32. package/dist/gates/timeout-shaped.js +10 -0
  33. package/dist/run/beat-lifecycle.d.ts +154 -0
  34. package/dist/run/beat-lifecycle.js +825 -0
  35. package/dist/run/daemon.js +202 -126
  36. package/dist/run/git-trust.d.ts +27 -0
  37. package/dist/run/git-trust.js +178 -0
  38. package/dist/run/git.js +34 -14
  39. package/dist/run/journal.js +15 -3
  40. package/dist/run/lease.d.ts +23 -0
  41. package/dist/run/lease.js +232 -38
  42. package/dist/run/merge.js +2 -2
  43. package/dist/run/stall.js +20 -1
  44. package/dist/run/supervision.d.ts +8 -0
  45. package/dist/run/supervision.js +24 -4
  46. package/dist/run/verification-job.d.ts +51 -0
  47. package/dist/run/verification-job.js +137 -0
  48. package/dist/tui/ink/fleet-app.d.ts +35 -5
  49. package/dist/tui/ink/fleet-app.js +244 -124
  50. package/package.json +1 -1
  51. package/skills/tickmarkr-auto/SKILL.md +19 -10
  52. package/skills/tickmarkr-loop/SKILL.md +19 -10
  53. package/skills/tickmarkr-overseer/SKILL.md +25 -16
  54. package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +58 -16
  55. package/skills/tickmarkr-overseer/scripts/grade-ci.sh +19 -6
  56. package/skills/tickmarkr-overseer/scripts/watch-parks.sh +121 -64
@@ -49,7 +49,7 @@ const REACH_CHOICES = [
49
49
  { id: "out-workers", label: "out · workers", gloss: "workers skip it; judge/review/consult unaffected" },
50
50
  { id: "out-all", label: "out · all seats", gloss: "no seat routes here" },
51
51
  ];
52
- export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts = {}, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewAdvisories = [], reviewOverlay, reloadGuard, stagedRouting, entry = "probe", initialJudge = "", judgeSeats = [], viewRows = Number.POSITIVE_INFINITY, frameColumns = 100, frameRows, ...initialDenyProps }) {
52
+ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts = {}, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewAdvisories = [], reviewOverlay, reloadGuard, stagedRouting, holderOf, entry = "probe", initialJudge = "", judgeSeats = [], viewRows = Number.POSITIVE_INFINITY, frameColumns = 100, frameRows, ...initialDenyProps }) {
53
53
  const { exit } = useApp();
54
54
  const [, setRevision] = useState(0);
55
55
  // the props are the fleet command's contract, one per schema-enumerated scope (initialDeny…),
@@ -71,6 +71,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
71
71
  notice: "",
72
72
  lastEdit: null,
73
73
  allowOut: new Set(initialAllowOut),
74
+ seededAllowOut: [],
74
75
  ...Object.fromEntries(DENY_SCOPES.map((scope) => [scope.key, new Set(initialDeny[scope.key])])),
75
76
  classifications: [],
76
77
  efforts: structuredClone(initialEfforts),
@@ -139,12 +140,13 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
139
140
  let routingMemo = null;
140
141
  const stagedPolicy = () => {
141
142
  const deny = stagedDeny();
142
- const key = JSON.stringify(deny);
143
+ const stage = stagedMetadata();
144
+ const key = JSON.stringify([deny, stage]);
143
145
  if (routingMemo?.key !== key) {
144
146
  routingMemo = {
145
147
  key,
146
148
  policy: stagedRouting
147
- ? stagedRouting(deny)
149
+ ? stagedRouting(deny, stage)
148
150
  : { ok: true, routing: { deny: denyBlockFrom(denyLists()) } },
149
151
  };
150
152
  }
@@ -163,7 +165,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
163
165
  const reachFor = (adapter, model, identity) => {
164
166
  const policy = stagedPolicy();
165
167
  if (!policy.ok)
166
- return { reach: "unknown", reasons: [`preview unavailable (${policy.error})`], uncoveredAlias: false };
168
+ return { reach: "unknown", reasons: [`preview unavailable (${policy.error})`], uncoveredAlias: false, held: [] };
167
169
  const channel = { adapter, model, ...(identity !== undefined ? { identity } : {}) };
168
170
  const worker = exclusionsOf(channel, policy.routing, "worker");
169
171
  const allSeats = exclusionsOf(channel, policy.routing, "judge");
@@ -175,22 +177,37 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
175
177
  reach,
176
178
  reasons: worker.map(exclusionReason),
177
179
  uncoveredAlias: identity !== undefined && !worker.some((scope) => scope.by === "deny"),
180
+ held: [...new Set(worker.map((scope) => scope.configPath))].flatMap((dotted) => {
181
+ const holder = holderOf?.(dotted);
182
+ return holder === undefined ? [] : [`${dotted} @ ${holder}`];
183
+ }),
178
184
  };
179
185
  };
180
- // OBS-1099 review: a displayed row may fold several discovered gateway ids (foldedModels);
181
- // each folded id is a real channel the policy matches on its own, so every one but a row's
182
- // own id counts as a sibling — an entry covering a folded id is shared, never row-owned
183
- const displayedChannels = () => modelGroups.flatMap((group) => group.rows
184
- .flatMap((other) => (other.foldedModels?.length ? other.foldedModels : [other.model])
185
- .map((model) => ({ adapter: group.adapter, model, identity: other.evidence?.identity }))));
186
+ // T5: the real channels a displayed row stands for — every collapsed variant and catalog-fold
187
+ // constituent fleet derived from the cached doctor list, each with its own identity; a row without
188
+ // them stands for its folded ids, else its reference real id (classifyModel) or its own. Display labels
189
+ // are never channels.
190
+ const membersOf = (adapter, row) => {
191
+ const listed = row.members?.length ? row.members
192
+ : (row.foldedModels?.length ? row.foldedModels : [row.classifyModel ?? row.model]).map((model) => ({ model, identity: row.evidence?.identity }));
193
+ // one channel per real id: a repeated id is one member, so a grouped act takes ONE reason from it
194
+ return listed.filter((member, at) => listed.findIndex((other) => other.model === member.model) === at)
195
+ .map((member) => ({ adapter, model: member.model, ...(member.identity !== undefined ? { identity: member.identity } : {}) }));
196
+ };
197
+ // OBS-1099 review: a displayed row may fold several discovered gateway ids; each real member is a
198
+ // channel the policy matches on its own, so every one but a channel itself counts as a sibling —
199
+ // an entry covering another member is shared, never channel-owned (grouping never privatizes it)
200
+ const displayedChannels = () => modelGroups.flatMap((group) => group.rows.flatMap((other) => membersOf(group.adapter, other)));
186
201
  const siblingsOf = (self) => displayedChannels()
187
202
  .filter((other) => other.adapter !== self.adapter || other.model !== self.model);
188
203
  // Judge c3 (R107): exactly ONE selected entry per act — the channel's adapter:model key when it
189
- // is a candidate, else the first candidate in sorted order; only an entry no sibling matches
190
- const selectOwn = (entries, self) => {
204
+ // is a candidate, else the first candidate in sorted order; only an entry no sibling matches.
205
+ // T5: matched as production matches the list — a deny entry with family matching, an allow-complement
206
+ // entry exactly as routing.allow admits (`fake:C-low` never reaches a bare `fake:C` there)
207
+ const selectOwn = (entries, self, family = true) => {
191
208
  const others = siblingsOf(self);
192
- const candidates = [...entries].filter((entry) => entryMatchesChannel(entry, self, true)
193
- && !others.some((other) => entryMatchesChannel(entry, other, true)));
209
+ const candidates = [...entries].filter((entry) => entryMatchesChannel(entry, self, family)
210
+ && !others.some((other) => entryMatchesChannel(entry, other, family)));
194
211
  const id = `${self.adapter}:${self.model}`;
195
212
  return candidates.includes(id) ? id : candidates.sort()[0];
196
213
  };
@@ -280,11 +297,36 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
280
297
  const shared = isAdapterWide(scope.entry, self) || covers.length > 1;
281
298
  return { entry: scope.entry, scope: scope.configPath, covers, shared, edits: reachEdits(owned) };
282
299
  };
300
+ // T5: a grouped row's reach over EVERY real member — the whole-group state only when every member
301
+ // holds it; otherwise the most-excluded member's reach, marked partial, each member-specific reason
302
+ // prefixed with its real id. A one-member row is exactly its channel's reach.
303
+ const rowReach = (members) => {
304
+ const each = members.map((member) => ({ model: member.model, ...reachFor(member.adapter, member.model, member.identity) }));
305
+ if (each.length === 1) {
306
+ const { model: _model, ...only } = each[0];
307
+ return { ...only, partial: false };
308
+ }
309
+ const folded = foldReach(each);
310
+ const uniform = each.every((channel) => channel.reach === each[0].reach);
311
+ return {
312
+ reach: uniform ? each[0].reach : folded.reach,
313
+ reasons: folded.reasons,
314
+ partial: folded.partial,
315
+ uncoveredAlias: each.some((channel) => channel.uncoveredAlias),
316
+ held: [...new Set(each.flatMap((channel) => channel.held))],
317
+ };
318
+ };
319
+ // T5: the covering entry a grouped row names — a shared one first, so l owns it
320
+ const rowCovering = (members) => {
321
+ const found = members.map(coveringFor).filter((covering) => covering !== undefined);
322
+ return found.find((covering) => covering.shared) ?? found[0];
323
+ };
283
324
  const groupRows = (group) => {
284
325
  const rows = group.rows.map((row) => {
326
+ const members = membersOf(group.adapter, row);
285
327
  const staged = ui.classifications.find((classification) => classification.adapter === group.adapter
286
- && (classification.model === row.model || classification.model === row.classifyModel));
287
- const reach = reachFor(group.adapter, row.model, row.evidence?.identity);
328
+ && (classification.model === row.model || members.some((member) => member.model === classification.model)));
329
+ const reach = rowReach(members);
288
330
  return {
289
331
  adapter: group.adapter,
290
332
  model: row.model,
@@ -296,16 +338,17 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
296
338
  classifyModel: row.classifyModel,
297
339
  variants: row.variants,
298
340
  foldedModels: row.foldedModels,
341
+ ...(row.members ? { members: row.members } : {}),
299
342
  score: row.score,
300
343
  denied: reach.reach !== "in",
301
344
  ...reach,
302
345
  // OBS-1099 item 2 (review round 2): every row is covered the same way — an alias through
303
346
  // its identity, a plain row by a bare-model or adapter entry — so a shared bare-model
304
347
  // entry across adapters has the one owning control too
305
- covering: coveringFor({ adapter: group.adapter, model: row.model, identity: row.evidence?.identity }),
348
+ covering: rowCovering(members),
306
349
  };
307
350
  });
308
- const known = new Set(rows.flatMap((row) => row.classifyModel ? [row.model, row.classifyModel] : [row.model]));
351
+ const known = new Set(group.rows.flatMap((row) => [row.model, ...membersOf(group.adapter, row).map((member) => member.model)]));
309
352
  for (const staged of ui.classifications) {
310
353
  if (staged.adapter === group.adapter && !known.has(staged.model)) {
311
354
  const reach = reachFor(group.adapter, staged.model);
@@ -377,12 +420,13 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
377
420
  // ── state transitions ──────────────────────────────────────────────────────
378
421
  // T8: the previews fold the CURRENT staged metadata — never only what the last review saw
379
422
  const stagedMetadata = () => {
380
- const { classifications, efforts } = editorState();
381
- return { classifications, efforts };
423
+ const { classifications, efforts, seededAllowOut } = editorState();
424
+ return { classifications, efforts, seededAllowOut };
382
425
  };
383
426
  const editorState = () => ({
384
427
  ...denyLists(),
385
428
  allowOut: [...ui.allowOut].sort(),
429
+ seededAllowOut: structuredClone(ui.seededAllowOut),
386
430
  classifications: ui.classifications.map((classification) => classification.vendor && classification.channel
387
431
  ? {
388
432
  ...classification,
@@ -430,9 +474,40 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
430
474
  finish({ kind: "no-changes" });
431
475
  return;
432
476
  }
477
+ if (nextReview.kind === "refused") {
478
+ ui.notice = `w refused — ${nextReview.reason}`;
479
+ bump();
480
+ return;
481
+ }
433
482
  setOverlay({ kind: "review", review: nextReview, scroll: 0 });
434
483
  };
484
+ // T10 (D-925/D-939): a model the current allow form leaves out gets its OWN allow entry staged beside
485
+ // its classification — the ownAllow spelling Space clears — so it can be admitted in this session.
486
+ // Re-staging a classification replaces its seed; a row the allow form admits seeds nothing. A row a
487
+ // flat deny ALSO excludes still seeds, so successive owning lifts (deny, then allow) admit it.
488
+ // T5: one seed per REAL member of a grouped row (collapsed variants, catalog-fold constituents), keyed and
489
+ // spelled by its real id with its own identity — the display label never names a channel.
490
+ const seedAllow = (self, displayModel) => {
491
+ const policy = stagedPolicy();
492
+ if (!policy.ok || !exclusionsOf(self, policy.routing, "judge").some((scope) => scope.by === "allow"))
493
+ return;
494
+ const entry = selectOwn(ui.allowOut, self, false) ?? `${self.adapter}:${self.model}`;
495
+ ui.allowOut = new Set([...ui.allowOut, entry]);
496
+ ui.seededAllowOut = [
497
+ ...ui.seededAllowOut.filter((seed) => seed.adapter !== self.adapter || seed.model !== self.model),
498
+ { adapter: self.adapter, model: self.model, displayModel, ...(self.identity !== undefined ? { identity: self.identity } : {}), entry },
499
+ ];
500
+ };
501
+ // T5: the real members a classification of this browser row stages — a row the browser never listed
502
+ // (n adds one) is its own one member
503
+ const classifiedMembers = (adapter, displayModel) => {
504
+ const row = groupOf(adapter)?.rows.find((candidate) => candidate.model === displayModel);
505
+ return row ? membersOf(adapter, row) : [{ adapter, model: displayModel }];
506
+ };
435
507
  const stageSuggested = (adapter, rows, firstTouch) => {
508
+ for (const row of rows) {
509
+ seedAllow({ adapter, model: row.model, ...(row.identity !== undefined ? { identity: row.identity } : {}) }, row.displayModel);
510
+ }
436
511
  ui.classifications = [
437
512
  ...ui.classifications,
438
513
  ...rows.map((row) => ({
@@ -485,17 +560,23 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
485
560
  }
486
561
  setOverlay({ ...base, stage: "channel", vendor: group.vendor });
487
562
  };
563
+ // T5: the picked tier and provenance land on EVERY real member of the row, each with its own seed —
564
+ // classifyModel stays the reference id the suggestion and the overlay title name
488
565
  const applyClassification = (overlay) => {
489
566
  const answered = overlay.vendor ? ui.channelByAdapter[overlay.adapter] : undefined;
567
+ const displayModel = overlay.displayModel ?? overlay.model;
568
+ const members = classifiedMembers(overlay.adapter, displayModel);
569
+ for (const member of members)
570
+ seedAllow(member, displayModel);
490
571
  ui.classifications = [
491
572
  ...ui.classifications,
492
- {
573
+ ...members.map((member) => ({
493
574
  adapter: overlay.adapter,
494
- model: overlay.model,
575
+ model: member.model,
495
576
  tier: TIERS[overlay.tierAt],
496
577
  note: overlay.note.trim(),
497
578
  ...(overlay.vendor && answered ? { vendor: overlay.vendor, channel: answered } : {}),
498
- },
579
+ })),
499
580
  ];
500
581
  setOverlay(null);
501
582
  };
@@ -523,8 +604,9 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
523
604
  // channel-specific one carries its channel, so a partial exclusion is never intersected away.
524
605
  const adapterReach = (id) => {
525
606
  const group = modelGroups.find((candidate) => candidate.adapter === id);
526
- const channels = (group?.rows ?? []).flatMap((row) => (row.foldedModels?.length ? row.foldedModels : [row.model])
527
- .map((model) => ({ model, ...reachFor(id, model, row.evidence?.identity) })));
607
+ // T5: every real member of every row — a collapsed or folded row's display label is no channel
608
+ const channels = (group?.rows ?? []).flatMap((row) => membersOf(id, row))
609
+ .map((channel) => ({ model: channel.model, ...reachFor(id, channel.model, channel.identity) }));
528
610
  if (channels.length === 0) {
529
611
  // ponytail: a rail entry with no probed channels has only its own literal entries to show
530
612
  const reasons = [
@@ -537,6 +619,11 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
537
619
  : NESTED_ADAPTER_SCOPES.some((scope) => ui[scope.key].has(id)) ? "out-workers" : "in";
538
620
  return { reach, reasons, partial: false };
539
621
  }
622
+ return foldReach(channels);
623
+ };
624
+ // the rail's (and a grouped row's) fold over its real channels — most-excluded reach, partial when they
625
+ // disagree, shared reasons once and channel-specific ones carrying their channel
626
+ function foldReach(channels) {
540
627
  const unknown = channels.find((channel) => channel.reach === "unknown");
541
628
  if (unknown)
542
629
  return { reach: "unknown", reasons: unknown.reasons, partial: false };
@@ -551,7 +638,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
551
638
  .filter((reason) => !shared.includes(reason))
552
639
  .map((reason) => `${channel.model}: ${reason}`));
553
640
  return { reach, reasons: [...shared, ...own], partial };
554
- };
641
+ }
555
642
  const describeReach = (state) => state.partial ? `${state.reach} (partial: not every channel)` : state.reach;
556
643
  // OBS-1099 items 1 and 4: one picker choice changes exactly ONE selected entry in ONE scope —
557
644
  // added, cleared, or re-scoped between the workers list and the flat list — then the row's
@@ -641,90 +728,106 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
641
728
  if (!row)
642
729
  return;
643
730
  const id = `${row.adapter}:${row.model}`;
644
- const self = { adapter: row.adapter, model: row.model, identity: row.evidence?.identity };
645
- // LEG2-T3 round 2 finding 1 / D-233 / D-235 (review round 10): ONE ordered selector —
646
- // ownedDenies — feeds the detail line AND every Space edit, so the entry named is the entry
647
- // edited: a clear takes its head (own key first, then collector order); a demotion takes its
648
- // first flat entry, a promotion its first workers entry; a bare adapter id or a sibling's
649
- // entry is never in it. OBS-1046: an allow exclusion is the LAST reason taken, never together
650
- // with an authored deny.
651
- const edits = reachEdits(ownedDenies(self));
652
- const ownAllow = () => {
653
- const fromAllow = selectOwn(ui.allowOut, self);
654
- return fromAllow === undefined ? undefined : { entry: fromAllow, scope: "routing.allow" };
655
- };
656
- const ownFlat = () => edits.demote ?? ownAllow();
657
- const ownWorkers = () => edits.promote;
658
- const takeFlat = (found) => {
659
- const scope = scopeOf(found.scope);
660
- if (scope)
661
- ui[scope.key] = without(ui[scope.key], found.entry);
662
- else
663
- ui.allowOut = without(ui.allowOut, found.entry);
664
- };
665
- const refuse = (reachLabel) => {
666
- // OBS-1099 item 2: a covered row names the covering entry, every identity it covers, and
667
- // the ONE control that lifts it — the T1 refusal wording stays in front of it
668
- if (row.covering?.shared) {
669
- // the notice clips at the browser width; the detail line and the l overlay list every identity
670
- ui.notice = `${id} stays ${reachLabel} — ${row.covering.scope} (${row.covering.entry}) covers ${row.covering.covers.length} channels — Space edits only this channel's own entries; l lists and lifts it`;
671
- return;
731
+ // T5: a grouped row's act is the same one-reason act on EVERY real member, each decided from the reach
732
+ // it held BEFORE this act — a sibling's fresh entry that also matches a member's recorded identity never
733
+ // turns that member's own addition into "already" (review D-1081) — while every removal still takes only
734
+ // an entry the member owns alone over the staged sets; a member a shared entry keeps out is named and left
735
+ const members = membersOf(row.adapter, row);
736
+ const before = members.map((self) => (members.length === 1
737
+ ? { reach: row.reach, reasons: row.reasons, covering: row.covering }
738
+ : { ...reachFor(self.adapter, self.model, self.identity), covering: coveringFor(self) }));
739
+ const outcomes = members.map((self, at) => {
740
+ const memberId = `${self.adapter}:${self.model}`;
741
+ const { reasons, covering } = before[at];
742
+ // LEG2-T3 round 2 finding 1 / D-233 / D-235 (review round 10): ONE ordered selector —
743
+ // ownedDenies — feeds the detail line AND every Space edit, so the entry named is the entry
744
+ // edited: a clear takes its head (own key first, then collector order); a demotion takes its
745
+ // first flat entry, a promotion its first workers entry; a bare adapter id or a sibling's
746
+ // entry is never in it. OBS-1046: an allow exclusion is the LAST reason taken, never together
747
+ // with an authored deny.
748
+ const edits = reachEdits(ownedDenies(self));
749
+ // review D-1081 add.: the seed this session generated for this member is its own even when a peer
750
+ // member's recorded identity also reaches it (C-high recorded as C-low) — never authored, and lifting
751
+ // it admits only this act's members; a seed any channel outside the row matches stays shared
752
+ const ownSeed = () => ui.seededAllowOut.find((seed) => seed.adapter === self.adapter && seed.model === self.model
753
+ && ui.allowOut.has(seed.entry)
754
+ && !siblingsOf(self).some((other) => !members.some((peer) => peer.adapter === other.adapter && peer.model === other.model)
755
+ && entryMatchesChannel(seed.entry, other, false)))?.entry;
756
+ const ownAllow = () => {
757
+ const fromAllow = selectOwn(ui.allowOut, self, false) ?? ownSeed();
758
+ return fromAllow === undefined ? undefined : { entry: fromAllow, scope: "routing.allow" };
759
+ };
760
+ const ownFlat = () => edits.demote ?? ownAllow();
761
+ const ownWorkers = () => edits.promote;
762
+ const takeFlat = (found) => {
763
+ const scope = scopeOf(found.scope);
764
+ if (scope)
765
+ ui[scope.key] = without(ui[scope.key], found.entry);
766
+ else
767
+ ui.allowOut = without(ui.allowOut, found.entry);
768
+ };
769
+ const refuse = (reachLabel) => {
770
+ // OBS-1099 item 2: a covered row names the covering entry, every identity it covers, and
771
+ // the ONE control that lifts it — the T1 refusal wording stays in front of it
772
+ if (covering?.shared) {
773
+ // the notice clips at the browser width; the detail line and the l overlay list every identity
774
+ return { refused: `${memberId} stays ${reachLabel} — ${covering.scope} (${covering.entry}) covers ${covering.covers.length} channels — Space edits only this channel's own entries; l lists and lifts it` };
775
+ }
776
+ const shared = ui.denyAdapters.has(row.adapter) || ui.allowOut.has(row.adapter) || ui.denyWorkersAdapters.has(row.adapter)
777
+ ? `every ${row.adapter} channel is out together — set the adapter's reach on the rail`
778
+ : "a shared entry covers other channels too";
779
+ return { refused: `${memberId} stays ${reachLabel} — ${reasons.join("; ")} — Space edits only this channel's own entries; ${shared}` };
780
+ };
781
+ const reach = before[at].reach;
782
+ const outAll = reach === "out-all" || reach === "out-allow";
783
+ if (choice === "out-workers" && reach === "in") {
784
+ ui.denyWorkersModels = withEntry(ui.denyWorkersModels, memberId);
785
+ return { edit: `added ${memberId} to routing.deny.workers.models` };
672
786
  }
673
- const shared = ui.denyAdapters.has(row.adapter) || ui.allowOut.has(row.adapter) || ui.denyWorkersAdapters.has(row.adapter)
674
- ? `every ${row.adapter} channel is out together — set the adapter's reach on the rail`
675
- : "a shared entry covers other channels too";
676
- ui.notice = `${id} stays ${reachLabel} — ${row.reasons.join("; ")} — Space edits only this channel's own entries; ${shared}`;
677
- };
678
- const reach = row.reach;
679
- const outAll = reach === "out-all" || reach === "out-allow";
680
- let edit = "";
681
- if (choice === "out-workers" && reach === "in") {
682
- ui.denyWorkersModels = withEntry(ui.denyWorkersModels, id);
683
- edit = `added ${id} to routing.deny.workers.models`;
684
- }
685
- else if (choice === "out-workers" && outAll) {
686
- const found = ownFlat();
687
- if (found === undefined)
688
- return refuse("out");
689
- takeFlat(found);
690
- ui.denyWorkersModels = withEntry(ui.denyWorkersModels, found.entry);
691
- edit = `moved ${found.entry} to routing.deny.workers.models`;
692
- }
693
- else if (choice === "out-all" && reach === "in") {
694
- ui.denyModels = withEntry(ui.denyModels, id);
695
- edit = `added ${id} to routing.deny.models`;
696
- }
697
- else if (choice === "out-all" && reach === "out-workers") {
698
- // OBS-1099 review: a promotion, like a clear, takes only an entry no sibling row matches —
699
- // a shared workers entry (adapter-wide or a bare id another row serves) is refused by name,
700
- // never moved for its siblings and never doubled by a fresh flat deny
701
- const found = ownWorkers();
702
- if (found === undefined)
703
- return refuse("out · workers");
704
- takeFlat(found);
705
- ui.denyModels = withEntry(ui.denyModels, found.entry);
706
- edit = `moved ${found.entry} to routing.deny.models`;
707
- }
708
- else if (choice === "in" && reach !== "in") {
709
- // D-205: in clears ONE row-owned reason from either the flat scopes or the workers scope —
710
- // a shared flat reason (routing.deny.adapters covering every channel) never blocks clearing
711
- // this row's own workers entry; the recomputed reach then names the shared reason that remains
712
- const found = edits.clear ?? (outAll ? ownAllow() : undefined);
713
- if (found !== undefined) {
787
+ else if (choice === "out-workers" && outAll) {
788
+ const found = ownFlat();
789
+ if (found === undefined)
790
+ return refuse("out");
714
791
  takeFlat(found);
715
- edit = `cleared ${found.entry} from ${found.scope}`;
792
+ ui.denyWorkersModels = withEntry(ui.denyWorkersModels, found.entry);
793
+ return { edit: `moved ${found.entry} to routing.deny.workers.models` };
716
794
  }
717
- else {
718
- return refuse(outAll ? "out" : "out · workers");
795
+ else if (choice === "out-all" && reach === "in") {
796
+ ui.denyModels = withEntry(ui.denyModels, memberId);
797
+ return { edit: `added ${memberId} to routing.deny.models` };
719
798
  }
720
- }
721
- else {
722
- ui.notice = `${id} is already ${label}`;
799
+ else if (choice === "out-all" && reach === "out-workers") {
800
+ // OBS-1099 review: a promotion, like a clear, takes only an entry no sibling row matches —
801
+ // a shared workers entry (adapter-wide or a bare id another row serves) is refused by name,
802
+ // never moved for its siblings and never doubled by a fresh flat deny
803
+ const found = ownWorkers();
804
+ if (found === undefined)
805
+ return refuse("out · workers");
806
+ takeFlat(found);
807
+ ui.denyModels = withEntry(ui.denyModels, found.entry);
808
+ return { edit: `moved ${found.entry} to routing.deny.models` };
809
+ }
810
+ else if (choice === "in" && reach !== "in") {
811
+ // D-205: in clears ONE row-owned reason from either the flat scopes or the workers scope —
812
+ // a shared flat reason (routing.deny.adapters covering every channel) never blocks clearing
813
+ // this row's own workers entry; the recomputed reach then names the shared reason that remains
814
+ const found = edits.clear ?? (outAll ? ownAllow() : undefined);
815
+ if (found === undefined)
816
+ return refuse(outAll ? "out" : "out · workers");
817
+ takeFlat(found);
818
+ return { edit: `cleared ${found.entry} from ${found.scope}` };
819
+ }
820
+ return { already: true };
821
+ });
822
+ const made = outcomes.flatMap((outcome) => ("edit" in outcome ? [outcome.edit] : []));
823
+ const refused = outcomes.flatMap((outcome) => ("refused" in outcome ? [outcome.refused] : []));
824
+ if (!made.length) {
825
+ ui.notice = refused[0] ?? `${id} is already ${label}`;
723
826
  return;
724
827
  }
725
- const after = reachFor(row.adapter, row.model, row.evidence?.identity);
726
- const still = reached(after.reach, choice) ? "" : ` — still out: ${after.reasons.join("; ")}`;
727
- ui.lastEdit = { id, text: `space: ${edit}${still}` };
828
+ const after = rowReach(members);
829
+ const still = reached(after.reach, choice) && !after.partial ? "" : ` — still out: ${after.reasons.join("; ")}`;
830
+ ui.lastEdit = { id, text: `space: ${made.join("; ")}${still}` };
728
831
  };
729
832
  // OBS-1065: Enter on a greyed picker row lifts exactly its row-owned covering entry, then the
730
833
  // same picker reopens over the recomputed policy — a shared or adapter-wide entry is refused by
@@ -1551,7 +1654,13 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
1551
1654
  const group = modelGroups[ui.adapterAt];
1552
1655
  const suggested = rows
1553
1656
  .filter((candidate) => candidate.adapter === group.adapter && !candidate.tier && candidate.suggestion !== undefined)
1554
- .map((candidate) => ({ model: candidate.classifyModel ?? candidate.model, suggestion: candidate.suggestion }));
1657
+ // T5: every real member of each suggested row stages, never only its reference id
1658
+ .flatMap((candidate) => membersOf(candidate.adapter, candidate).map((member) => ({
1659
+ model: member.model,
1660
+ displayModel: candidate.model,
1661
+ ...(member.identity !== undefined ? { identity: member.identity } : {}),
1662
+ suggestion: candidate.suggestion,
1663
+ })));
1555
1664
  if (!suggested.length) {
1556
1665
  ui.notice = "no catalog tier suggestions among the visible unclassified models — evidence comes from the cached catalogs (AA index / API pricing)";
1557
1666
  bump();
@@ -1683,7 +1792,11 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
1683
1792
  const reachDetail = (row) => {
1684
1793
  if (row.evidence?.unauthed !== undefined)
1685
1794
  return "unauthed — re-probe with tickmarkr doctor (Space does not toggle reach)";
1686
- const reasons = row.reasons.join("; ");
1795
+ // T10: the reasons, then where each scope is held — the overlay a reviewed w writes
1796
+ const reasons = `${row.reasons.join("; ")}${row.held.length ? ` — held: ${row.held.join("; ")}` : ""}`;
1797
+ // T5: members disagree — never a whole-group state; each member-specific reason names its real id
1798
+ if (row.partial)
1799
+ return `reach: partial — ${row.reach} for some members only — ${reasons} — Space applies one choice to every member`;
1687
1800
  if (row.reach === "out-all")
1688
1801
  return `reach: out · all seats — ${reasons} — Space picks in or out · workers`;
1689
1802
  if (row.reach === "out-allow")
@@ -1694,7 +1807,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
1694
1807
  return `reach: unknown — ${reasons}`;
1695
1808
  return "reach: in — Space picks out · workers or out · all seats";
1696
1809
  };
1697
- const reachCell = (row) => row.reach === "in" || row.reach === "unknown" ? row.reach : `out ${row.reach === "out-workers" ? "workers" : row.reach.slice(4)}`;
1810
+ const reachCell = (row) => row.partial ? "partial" : row.reach === "in" || row.reach === "unknown" ? row.reach : `out ${row.reach === "out-workers" ? "workers" : row.reach.slice(4)}`;
1698
1811
  const identityDetail = (row) => {
1699
1812
  if (row.evidence?.identity === undefined)
1700
1813
  return "";
@@ -1726,11 +1839,16 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
1726
1839
  `${row.adapter}:${row.model}`,
1727
1840
  row.tier ?? "unclassified",
1728
1841
  ui.efforts[row.adapter]?.[row.model] ? `effort ${ui.efforts[row.adapter][row.model]}` : "",
1842
+ // T5: a grouped row names each member doctor recorded unauthed — up front, before the line clips
1843
+ ...(row.evidence?.unauthed === undefined ? (row.members ?? []).filter((member) => member.unauthed !== undefined)
1844
+ .map((member) => `${row.adapter}:${member.model} UNAUTHED — ${member.unauthed} — re-probe with tickmarkr doctor`) : []),
1729
1845
  ui.lastEdit?.id === `${row.adapter}:${row.model}` ? ui.lastEdit.text : "",
1730
1846
  identityDetail(row),
1731
1847
  row.tier ? reachDetail(row) : "",
1732
1848
  row.variants?.length ? `variants ${row.variants.join(", ")}` : "",
1733
- row.classifyModel ? `classify writes ${row.classifyModel}` : "",
1849
+ // T5: a grouped row classifies (and edits) every real member; classifyModel is its reference id
1850
+ row.members?.length ? `classify writes ${row.members.map((member) => member.model).join(", ")}`
1851
+ : row.classifyModel ? `classify writes ${row.classifyModel}` : "",
1734
1852
  row.foldedModels?.length ? `${row.foldedModels.length} gateway ids folded: ${row.foldedModels.join(", ")}` : "",
1735
1853
  row.evidence?.unauthed !== undefined ? `UNAUTHED — ${row.evidence.unauthed}` : "",
1736
1854
  row.evidence?.contextWindow !== undefined ? `${fmtCtx(row.evidence.contextWindow)} ctx` : "",
@@ -1754,15 +1872,17 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
1754
1872
  const folded = row.foldedModels?.length ?? 1;
1755
1873
  const name = clipPathTail(`${row.adapter}/${row.model}${folded > 1 ? ` ×${folded}` : ""}`, nameW);
1756
1874
  const prefixLen = Math.min(name.length, row.adapter.length + 1);
1757
- return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Pointer, { on: selected }), row.reach === "out-all" || row.reach === "out-allow"
1758
- ? _jsx(Glyph, { kind: "off" })
1759
- : row.evidence?.unauthed !== undefined || row.reach === "unknown"
1760
- ? _jsx(Glyph, { kind: "fail" })
1761
- : row.reach === "out-workers"
1762
- ? _jsx(Glyph, { kind: "warn" })
1763
- : row.tier
1764
- ? _jsx(Glyph, { kind: "on" })
1765
- : _jsx(Glyph, { kind: "unknown" }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: row.denied, children: [_jsx(Text, { dimColor: true, children: name.slice(0, prefixLen) }), _jsx(Text, { bold: selected, children: name.slice(prefixLen).padEnd(Math.max(nameW - prefixLen, 0)) })] }), _jsx(Text, { color: tier.color, dimColor: tier.dim, children: padCellStart(tier.text, 11) }), _jsx(Text, { dimColor: row.reach === "in", children: padCellStart(row.tier ? reachCell(row) : "", 12) }), _jsx(Text, { dimColor: true, children: padCellStart(fmtCtx(row.evidence?.contextWindow), 6) }), showPrice && _jsx(Text, { dimColor: priceCell(row) === "sub", children: padCellStart(priceCell(row), 12) }), showProbe && _jsx(Text, { dimColor: true, children: padCellStart(fmtMs(row.evidence?.probeMs), 7) })] }, `${row.adapter}:${row.model}`));
1875
+ return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Pointer, { on: selected }), row.partial
1876
+ ? _jsx(Glyph, { kind: "warn" })
1877
+ : row.reach === "out-all" || row.reach === "out-allow"
1878
+ ? _jsx(Glyph, { kind: "off" })
1879
+ : row.evidence?.unauthed !== undefined || row.reach === "unknown"
1880
+ ? _jsx(Glyph, { kind: "fail" })
1881
+ : row.reach === "out-workers"
1882
+ ? _jsx(Glyph, { kind: "warn" })
1883
+ : row.tier
1884
+ ? _jsx(Glyph, { kind: "on" })
1885
+ : _jsx(Glyph, { kind: "unknown" }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: row.denied, children: [_jsx(Text, { dimColor: true, children: name.slice(0, prefixLen) }), _jsx(Text, { bold: selected, children: name.slice(prefixLen).padEnd(Math.max(nameW - prefixLen, 0)) })] }), _jsx(Text, { color: tier.color, dimColor: tier.dim, children: padCellStart(tier.text, 11) }), _jsx(Text, { dimColor: row.reach === "in", children: padCellStart(row.tier ? reachCell(row) : "", 12) }), _jsx(Text, { dimColor: true, children: padCellStart(fmtCtx(row.evidence?.contextWindow), 6) }), showPrice && _jsx(Text, { dimColor: priceCell(row) === "sub", children: padCellStart(priceCell(row), 12) }), showProbe && _jsx(Text, { dimColor: true, children: padCellStart(fmtMs(row.evidence?.probeMs), 7) })] }, `${row.adapter}:${row.model}`));
1766
1886
  };
1767
1887
  const renderPlainRow = (id, label, selected, rowW = bodyW - 2) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Pointer, { on: selected }), _jsx(Text, { bold: selected, children: clip(label, rowW) })] }, id));
1768
1888
  const renderShapeRow = (row, selected, rowW = bodyW - 2) => {
@@ -1808,8 +1928,8 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
1808
1928
  if (overlay.kind === "classify") {
1809
1929
  const subject = overlay.bulk
1810
1930
  ? `${overlay.adapter} · ${overlay.bulk.rows.length} suggested models`
1811
- : overlay.displayModel
1812
- ? `${overlay.adapter}:${overlay.displayModel} · writes ${overlay.model}`
1931
+ : overlay.displayModel || classifiedMembers(overlay.adapter, overlay.model).length > 1
1932
+ ? `${overlay.adapter}:${overlay.displayModel ?? overlay.model} · writes ${classifiedMembers(overlay.adapter, overlay.displayModel ?? overlay.model).map((member) => member.model).join(", ")}`
1813
1933
  : `${overlay.adapter}:${overlay.model}`;
1814
1934
  return (_jsxs(OverlayPanel, { title: `classify · ${subject}`, width: bodyW, children: [overlay.stage === "channel" && (_jsxs(_Fragment, { children: [_jsxs(Text, { dimColor: true, children: ["first touch for ", overlay.adapter, " \u2014 how is this CLI billed?"] }), _jsx(Text, { children: " " }), CHANNELS.map((channel, index) => (_jsxs(Text, { children: [_jsx(Pointer, { on: overlay.channelAt === index }), _jsx(Text, { bold: overlay.channelAt === index, children: padCell(channel, 5) }), _jsx(Text, { dimColor: true, children: channel === "sub" ? "flat-rate subscription quota" : "metered API billing" })] }, channel)))] })), overlay.stage === "tier" && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: overlay.suggestion
1815
1935
  ? `catalog suggests ${overlay.suggestion.tier} — keep it and the provenance note arrives pre-typed`
@@ -1873,7 +1993,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
1873
1993
  const current = overlay.target.model === undefined
1874
1994
  ? adapterReach(overlay.target.adapter)
1875
1995
  : modelRows().find((row) => row.adapter === overlay.target.adapter && row.model === overlay.target.model) ?? { reach: "unknown", reasons: [] };
1876
- const partial = "partial" in current && current.partial;
1996
+ const partial = "partial" in current && current.partial === true;
1877
1997
  // D-205: a partial rail marks no choice as selected — its channels disagree
1878
1998
  const on = partial || current.reach === "unknown" ? null
1879
1999
  : current.reach === "out-all" || current.reach === "out-allow" ? "out-all" : current.reach;
@@ -1942,8 +2062,8 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
1942
2062
  lines.push(coveringDetail(row));
1943
2063
  if (!row.tier) {
1944
2064
  lines.push(row.suggestion
1945
- ? `catalog suggests ${row.suggestion.tier} — Space/Enter classifies with the note pre-typed · s stages every visible suggestion`
1946
- : "unclassified — Space/Enter classifies; unclassified models are never routed");
2065
+ ? `catalog suggests ${row.suggestion.tier} — Space/Enter classifies with the note pre-typed (t too) · s stages every visible suggestion`
2066
+ : "unclassified — Space/Enter classifies (t too), then Space sets its reach in this session; unclassified models are never routed");
1947
2067
  }
1948
2068
  }
1949
2069
  return lines;
@@ -2060,7 +2180,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
2060
2180
  ? _jsx(Text, { color: INK.warn, children: ` ! ${clip(ui.notice, width - 4)}` })
2061
2181
  : details.map((line) => _jsx(Text, { dimColor: true, children: ` ${clip(line, width - 2)}` }, line)), _jsx(KeyBar, { binds: keyBinds() })] }));
2062
2182
  }
2063
- export async function runFleetInkEditor({ ageMs, adapters, health, initialAllowOut = [], initialEfforts, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewAdvisories = [], reviewOverlay, reloadGuard, stagedRouting, entry = "probe", initialJudge = "", judgeSeats = [], initialInput = [], input, output, debug = false, ...initialDenyProps }) {
2183
+ export async function runFleetInkEditor({ ageMs, adapters, health, initialAllowOut = [], initialEfforts, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewAdvisories = [], reviewOverlay, reloadGuard, stagedRouting, holderOf, entry = "probe", initialJudge = "", judgeSeats = [], initialInput = [], input, output, debug = false, ...initialDenyProps }) {
2064
2184
  const agents = adapters.flatMap((adapter) => {
2065
2185
  const state = health[adapter.id];
2066
2186
  if (!state?.installed)
@@ -2084,7 +2204,7 @@ export async function runFleetInkEditor({ ageMs, adapters, health, initialAllowO
2084
2204
  // rebuilt from output.columns/rows so a pane resize re-renders at the new size. Same root
2085
2205
  // element type on every rerender ⇒ React keeps the component instance, so staged edits,
2086
2206
  // cursor, and overlays all survive the resize.
2087
- const editorElement = () => (_jsx(FleetApp, { ageMs: ageMs, agents: agents, ...initialDenyProps, initialAllowOut: initialAllowOut, initialEfforts: initialEfforts, modelGroups: declaredModelGroups, initialMode: initialMode, modeOptions: modeOptions, initialMap: initialMap, modePreview: modePreview, shapeRows: shapeRows, candidatesForShape: candidatesForShape, preferOptionsForShape: preferOptionsForShape, initialSteering: initialSteering, steeringOptionsFor: steeringOptionsFor, reviewAdvisories: reviewAdvisories, reviewOverlay: reviewOverlay, reloadGuard: reloadGuard, stagedRouting: stagedRouting, entry: entry, initialJudge: initialJudge, judgeSeats: judgeSeats,
2207
+ const editorElement = () => (_jsx(FleetApp, { ageMs: ageMs, agents: agents, ...initialDenyProps, initialAllowOut: initialAllowOut, initialEfforts: initialEfforts, modelGroups: declaredModelGroups, initialMode: initialMode, modeOptions: modeOptions, initialMap: initialMap, modePreview: modePreview, shapeRows: shapeRows, candidatesForShape: candidatesForShape, preferOptionsForShape: preferOptionsForShape, initialSteering: initialSteering, steeringOptionsFor: steeringOptionsFor, reviewAdvisories: reviewAdvisories, reviewOverlay: reviewOverlay, reloadGuard: reloadGuard, stagedRouting: stagedRouting, holderOf: holderOf, entry: entry, initialJudge: initialJudge, judgeSeats: judgeSeats,
2088
2208
  // v1.90.9: cap every list to the terminal minus chrome so the cursor and header can
2089
2209
  // never scroll off-screen (omp's 218-model list did exactly that pre-cap).
2090
2210
  // OBS-523: chrome is ≤12 rows worst case (header, borders, padding, scope+search,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "2.6.5",
3
+ "version": "2.6.7",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",