tickmarkr 2.6.6 → 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.
- package/dist/adapters/claude-code.js +8 -2
- package/dist/adapters/codex-commit-check.d.ts +3 -2
- package/dist/adapters/codex-commit-check.js +37 -10
- package/dist/adapters/codex.js +10 -4
- package/dist/cli/commands/beat.d.ts +6 -1
- package/dist/cli/commands/beat.js +123 -76
- package/dist/cli/commands/fleet.js +64 -21
- package/dist/cli/commands/init.js +18 -7
- package/dist/cli/help.d.ts +1 -0
- package/dist/cli/help.js +10 -5
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/gates/baseline.d.ts +9 -0
- package/dist/gates/baseline.js +14 -3
- package/dist/gates/cache.js +1 -1
- package/dist/gates/review.js +15 -0
- package/dist/gates/run-gates.d.ts +18 -6
- package/dist/gates/run-gates.js +324 -204
- package/dist/gates/test-manifest.d.ts +8 -0
- package/dist/gates/test-manifest.js +151 -111
- package/dist/run/beat-lifecycle.d.ts +154 -0
- package/dist/run/beat-lifecycle.js +825 -0
- package/dist/run/daemon.js +18 -7
- package/dist/run/git-trust.d.ts +27 -0
- package/dist/run/git-trust.js +178 -0
- package/dist/run/git.js +34 -14
- package/dist/run/journal.js +15 -3
- package/dist/run/lease.d.ts +23 -0
- package/dist/run/lease.js +232 -38
- package/dist/run/merge.js +2 -2
- package/dist/run/stall.js +20 -1
- package/dist/run/supervision.d.ts +8 -0
- package/dist/run/supervision.js +24 -4
- package/dist/run/verification-job.d.ts +51 -0
- package/dist/run/verification-job.js +137 -0
- package/dist/tui/ink/fleet-app.d.ts +9 -0
- package/dist/tui/ink/fleet-app.js +209 -127
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +19 -10
- package/skills/tickmarkr-loop/SKILL.md +19 -10
- package/skills/tickmarkr-overseer/SKILL.md +25 -16
|
@@ -183,20 +183,31 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
183
183
|
}),
|
|
184
184
|
};
|
|
185
185
|
};
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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)));
|
|
192
201
|
const siblingsOf = (self) => displayedChannels()
|
|
193
202
|
.filter((other) => other.adapter !== self.adapter || other.model !== self.model);
|
|
194
203
|
// Judge c3 (R107): exactly ONE selected entry per act — the channel's adapter:model key when it
|
|
195
|
-
// is a candidate, else the first candidate in sorted order; only an entry no sibling matches
|
|
196
|
-
|
|
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) => {
|
|
197
208
|
const others = siblingsOf(self);
|
|
198
|
-
const candidates = [...entries].filter((entry) => entryMatchesChannel(entry, self,
|
|
199
|
-
&& !others.some((other) => entryMatchesChannel(entry, other,
|
|
209
|
+
const candidates = [...entries].filter((entry) => entryMatchesChannel(entry, self, family)
|
|
210
|
+
&& !others.some((other) => entryMatchesChannel(entry, other, family)));
|
|
200
211
|
const id = `${self.adapter}:${self.model}`;
|
|
201
212
|
return candidates.includes(id) ? id : candidates.sort()[0];
|
|
202
213
|
};
|
|
@@ -286,11 +297,36 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
286
297
|
const shared = isAdapterWide(scope.entry, self) || covers.length > 1;
|
|
287
298
|
return { entry: scope.entry, scope: scope.configPath, covers, shared, edits: reachEdits(owned) };
|
|
288
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
|
+
};
|
|
289
324
|
const groupRows = (group) => {
|
|
290
325
|
const rows = group.rows.map((row) => {
|
|
326
|
+
const members = membersOf(group.adapter, row);
|
|
291
327
|
const staged = ui.classifications.find((classification) => classification.adapter === group.adapter
|
|
292
|
-
&& (classification.model === row.model ||
|
|
293
|
-
const reach =
|
|
328
|
+
&& (classification.model === row.model || members.some((member) => member.model === classification.model)));
|
|
329
|
+
const reach = rowReach(members);
|
|
294
330
|
return {
|
|
295
331
|
adapter: group.adapter,
|
|
296
332
|
model: row.model,
|
|
@@ -302,16 +338,17 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
302
338
|
classifyModel: row.classifyModel,
|
|
303
339
|
variants: row.variants,
|
|
304
340
|
foldedModels: row.foldedModels,
|
|
341
|
+
...(row.members ? { members: row.members } : {}),
|
|
305
342
|
score: row.score,
|
|
306
343
|
denied: reach.reach !== "in",
|
|
307
344
|
...reach,
|
|
308
345
|
// OBS-1099 item 2 (review round 2): every row is covered the same way — an alias through
|
|
309
346
|
// its identity, a plain row by a bare-model or adapter entry — so a shared bare-model
|
|
310
347
|
// entry across adapters has the one owning control too
|
|
311
|
-
covering:
|
|
348
|
+
covering: rowCovering(members),
|
|
312
349
|
};
|
|
313
350
|
});
|
|
314
|
-
const known = new Set(rows.flatMap((row) =>
|
|
351
|
+
const known = new Set(group.rows.flatMap((row) => [row.model, ...membersOf(group.adapter, row).map((member) => member.model)]));
|
|
315
352
|
for (const staged of ui.classifications) {
|
|
316
353
|
if (staged.adapter === group.adapter && !known.has(staged.model)) {
|
|
317
354
|
const reach = reachFor(group.adapter, staged.model);
|
|
@@ -448,27 +485,29 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
448
485
|
// its classification — the ownAllow spelling Space clears — so it can be admitted in this session.
|
|
449
486
|
// Re-staging a classification replaces its seed; a row the allow form admits seeds nothing. A row a
|
|
450
487
|
// flat deny ALSO excludes still seeds, so successive owning lifts (deny, then allow) admit it.
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
// full grouped-row membership (collapse + catalog fold) is 2.6.7 (T10 owed review F1, D-1016)
|
|
455
|
-
if (model !== displayModel)
|
|
456
|
-
return;
|
|
457
|
-
const identity = groupOf(adapter)?.rows.find((row) => row.model === displayModel)?.evidence?.identity;
|
|
458
|
-
const self = { adapter, model: displayModel, ...(identity !== undefined ? { identity } : {}) };
|
|
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) => {
|
|
459
491
|
const policy = stagedPolicy();
|
|
460
492
|
if (!policy.ok || !exclusionsOf(self, policy.routing, "judge").some((scope) => scope.by === "allow"))
|
|
461
493
|
return;
|
|
462
|
-
const entry = selectOwn(ui.allowOut, self) ?? `${adapter}:${
|
|
494
|
+
const entry = selectOwn(ui.allowOut, self, false) ?? `${self.adapter}:${self.model}`;
|
|
463
495
|
ui.allowOut = new Set([...ui.allowOut, entry]);
|
|
464
496
|
ui.seededAllowOut = [
|
|
465
|
-
...ui.seededAllowOut.filter((seed) => seed.adapter !== adapter || seed.model !== model),
|
|
466
|
-
{ adapter, model, displayModel, ...(identity !== undefined ? { identity } : {}), entry },
|
|
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 },
|
|
467
499
|
];
|
|
468
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
|
+
};
|
|
469
507
|
const stageSuggested = (adapter, rows, firstTouch) => {
|
|
470
|
-
for (const row of rows)
|
|
471
|
-
seedAllow(adapter, row.model, row.displayModel);
|
|
508
|
+
for (const row of rows) {
|
|
509
|
+
seedAllow({ adapter, model: row.model, ...(row.identity !== undefined ? { identity: row.identity } : {}) }, row.displayModel);
|
|
510
|
+
}
|
|
472
511
|
ui.classifications = [
|
|
473
512
|
...ui.classifications,
|
|
474
513
|
...rows.map((row) => ({
|
|
@@ -521,18 +560,23 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
521
560
|
}
|
|
522
561
|
setOverlay({ ...base, stage: "channel", vendor: group.vendor });
|
|
523
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
|
|
524
565
|
const applyClassification = (overlay) => {
|
|
525
566
|
const answered = overlay.vendor ? ui.channelByAdapter[overlay.adapter] : undefined;
|
|
526
|
-
|
|
567
|
+
const displayModel = overlay.displayModel ?? overlay.model;
|
|
568
|
+
const members = classifiedMembers(overlay.adapter, displayModel);
|
|
569
|
+
for (const member of members)
|
|
570
|
+
seedAllow(member, displayModel);
|
|
527
571
|
ui.classifications = [
|
|
528
572
|
...ui.classifications,
|
|
529
|
-
{
|
|
573
|
+
...members.map((member) => ({
|
|
530
574
|
adapter: overlay.adapter,
|
|
531
|
-
model:
|
|
575
|
+
model: member.model,
|
|
532
576
|
tier: TIERS[overlay.tierAt],
|
|
533
577
|
note: overlay.note.trim(),
|
|
534
578
|
...(overlay.vendor && answered ? { vendor: overlay.vendor, channel: answered } : {}),
|
|
535
|
-
},
|
|
579
|
+
})),
|
|
536
580
|
];
|
|
537
581
|
setOverlay(null);
|
|
538
582
|
};
|
|
@@ -560,8 +604,9 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
560
604
|
// channel-specific one carries its channel, so a partial exclusion is never intersected away.
|
|
561
605
|
const adapterReach = (id) => {
|
|
562
606
|
const group = modelGroups.find((candidate) => candidate.adapter === id);
|
|
563
|
-
|
|
564
|
-
|
|
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) }));
|
|
565
610
|
if (channels.length === 0) {
|
|
566
611
|
// ponytail: a rail entry with no probed channels has only its own literal entries to show
|
|
567
612
|
const reasons = [
|
|
@@ -574,6 +619,11 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
574
619
|
: NESTED_ADAPTER_SCOPES.some((scope) => ui[scope.key].has(id)) ? "out-workers" : "in";
|
|
575
620
|
return { reach, reasons, partial: false };
|
|
576
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) {
|
|
577
627
|
const unknown = channels.find((channel) => channel.reach === "unknown");
|
|
578
628
|
if (unknown)
|
|
579
629
|
return { reach: "unknown", reasons: unknown.reasons, partial: false };
|
|
@@ -588,7 +638,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
588
638
|
.filter((reason) => !shared.includes(reason))
|
|
589
639
|
.map((reason) => `${channel.model}: ${reason}`));
|
|
590
640
|
return { reach, reasons: [...shared, ...own], partial };
|
|
591
|
-
}
|
|
641
|
+
}
|
|
592
642
|
const describeReach = (state) => state.partial ? `${state.reach} (partial: not every channel)` : state.reach;
|
|
593
643
|
// OBS-1099 items 1 and 4: one picker choice changes exactly ONE selected entry in ONE scope —
|
|
594
644
|
// added, cleared, or re-scoped between the workers list and the flat list — then the row's
|
|
@@ -678,90 +728,106 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
678
728
|
if (!row)
|
|
679
729
|
return;
|
|
680
730
|
const id = `${row.adapter}:${row.model}`;
|
|
681
|
-
|
|
682
|
-
//
|
|
683
|
-
//
|
|
684
|
-
//
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
const
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
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` };
|
|
709
786
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
};
|
|
715
|
-
const reach = row.reach;
|
|
716
|
-
const outAll = reach === "out-all" || reach === "out-allow";
|
|
717
|
-
let edit = "";
|
|
718
|
-
if (choice === "out-workers" && reach === "in") {
|
|
719
|
-
ui.denyWorkersModels = withEntry(ui.denyWorkersModels, id);
|
|
720
|
-
edit = `added ${id} to routing.deny.workers.models`;
|
|
721
|
-
}
|
|
722
|
-
else if (choice === "out-workers" && outAll) {
|
|
723
|
-
const found = ownFlat();
|
|
724
|
-
if (found === undefined)
|
|
725
|
-
return refuse("out");
|
|
726
|
-
takeFlat(found);
|
|
727
|
-
ui.denyWorkersModels = withEntry(ui.denyWorkersModels, found.entry);
|
|
728
|
-
edit = `moved ${found.entry} to routing.deny.workers.models`;
|
|
729
|
-
}
|
|
730
|
-
else if (choice === "out-all" && reach === "in") {
|
|
731
|
-
ui.denyModels = withEntry(ui.denyModels, id);
|
|
732
|
-
edit = `added ${id} to routing.deny.models`;
|
|
733
|
-
}
|
|
734
|
-
else if (choice === "out-all" && reach === "out-workers") {
|
|
735
|
-
// OBS-1099 review: a promotion, like a clear, takes only an entry no sibling row matches —
|
|
736
|
-
// a shared workers entry (adapter-wide or a bare id another row serves) is refused by name,
|
|
737
|
-
// never moved for its siblings and never doubled by a fresh flat deny
|
|
738
|
-
const found = ownWorkers();
|
|
739
|
-
if (found === undefined)
|
|
740
|
-
return refuse("out · workers");
|
|
741
|
-
takeFlat(found);
|
|
742
|
-
ui.denyModels = withEntry(ui.denyModels, found.entry);
|
|
743
|
-
edit = `moved ${found.entry} to routing.deny.models`;
|
|
744
|
-
}
|
|
745
|
-
else if (choice === "in" && reach !== "in") {
|
|
746
|
-
// D-205: in clears ONE row-owned reason from either the flat scopes or the workers scope —
|
|
747
|
-
// a shared flat reason (routing.deny.adapters covering every channel) never blocks clearing
|
|
748
|
-
// this row's own workers entry; the recomputed reach then names the shared reason that remains
|
|
749
|
-
const found = edits.clear ?? (outAll ? ownAllow() : undefined);
|
|
750
|
-
if (found !== undefined) {
|
|
787
|
+
else if (choice === "out-workers" && outAll) {
|
|
788
|
+
const found = ownFlat();
|
|
789
|
+
if (found === undefined)
|
|
790
|
+
return refuse("out");
|
|
751
791
|
takeFlat(found);
|
|
752
|
-
|
|
792
|
+
ui.denyWorkersModels = withEntry(ui.denyWorkersModels, found.entry);
|
|
793
|
+
return { edit: `moved ${found.entry} to routing.deny.workers.models` };
|
|
753
794
|
}
|
|
754
|
-
else {
|
|
755
|
-
|
|
795
|
+
else if (choice === "out-all" && reach === "in") {
|
|
796
|
+
ui.denyModels = withEntry(ui.denyModels, memberId);
|
|
797
|
+
return { edit: `added ${memberId} to routing.deny.models` };
|
|
756
798
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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}`;
|
|
760
826
|
return;
|
|
761
827
|
}
|
|
762
|
-
const after =
|
|
763
|
-
const still = reached(after.reach, choice) ? "" : ` — still out: ${after.reasons.join("; ")}`;
|
|
764
|
-
ui.lastEdit = { id, text: `space: ${
|
|
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}` };
|
|
765
831
|
};
|
|
766
832
|
// OBS-1065: Enter on a greyed picker row lifts exactly its row-owned covering entry, then the
|
|
767
833
|
// same picker reopens over the recomputed policy — a shared or adapter-wide entry is refused by
|
|
@@ -1588,7 +1654,13 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
1588
1654
|
const group = modelGroups[ui.adapterAt];
|
|
1589
1655
|
const suggested = rows
|
|
1590
1656
|
.filter((candidate) => candidate.adapter === group.adapter && !candidate.tier && candidate.suggestion !== undefined)
|
|
1591
|
-
|
|
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
|
+
})));
|
|
1592
1664
|
if (!suggested.length) {
|
|
1593
1665
|
ui.notice = "no catalog tier suggestions among the visible unclassified models — evidence comes from the cached catalogs (AA index / API pricing)";
|
|
1594
1666
|
bump();
|
|
@@ -1722,6 +1794,9 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
1722
1794
|
return "unauthed — re-probe with tickmarkr doctor (Space does not toggle reach)";
|
|
1723
1795
|
// T10: the reasons, then where each scope is held — the overlay a reviewed w writes
|
|
1724
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`;
|
|
1725
1800
|
if (row.reach === "out-all")
|
|
1726
1801
|
return `reach: out · all seats — ${reasons} — Space picks in or out · workers`;
|
|
1727
1802
|
if (row.reach === "out-allow")
|
|
@@ -1732,7 +1807,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
1732
1807
|
return `reach: unknown — ${reasons}`;
|
|
1733
1808
|
return "reach: in — Space picks out · workers or out · all seats";
|
|
1734
1809
|
};
|
|
1735
|
-
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)}`;
|
|
1736
1811
|
const identityDetail = (row) => {
|
|
1737
1812
|
if (row.evidence?.identity === undefined)
|
|
1738
1813
|
return "";
|
|
@@ -1764,11 +1839,16 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
1764
1839
|
`${row.adapter}:${row.model}`,
|
|
1765
1840
|
row.tier ?? "unclassified",
|
|
1766
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`) : []),
|
|
1767
1845
|
ui.lastEdit?.id === `${row.adapter}:${row.model}` ? ui.lastEdit.text : "",
|
|
1768
1846
|
identityDetail(row),
|
|
1769
1847
|
row.tier ? reachDetail(row) : "",
|
|
1770
1848
|
row.variants?.length ? `variants ${row.variants.join(", ")}` : "",
|
|
1771
|
-
row
|
|
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}` : "",
|
|
1772
1852
|
row.foldedModels?.length ? `${row.foldedModels.length} gateway ids folded: ${row.foldedModels.join(", ")}` : "",
|
|
1773
1853
|
row.evidence?.unauthed !== undefined ? `UNAUTHED — ${row.evidence.unauthed}` : "",
|
|
1774
1854
|
row.evidence?.contextWindow !== undefined ? `${fmtCtx(row.evidence.contextWindow)} ctx` : "",
|
|
@@ -1792,15 +1872,17 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
1792
1872
|
const folded = row.foldedModels?.length ?? 1;
|
|
1793
1873
|
const name = clipPathTail(`${row.adapter}/${row.model}${folded > 1 ? ` ×${folded}` : ""}`, nameW);
|
|
1794
1874
|
const prefixLen = Math.min(name.length, row.adapter.length + 1);
|
|
1795
|
-
return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Pointer, { on: selected }), row.
|
|
1796
|
-
? _jsx(Glyph, { kind: "
|
|
1797
|
-
: row.
|
|
1798
|
-
? _jsx(Glyph, { kind: "
|
|
1799
|
-
: row.reach === "
|
|
1800
|
-
? _jsx(Glyph, { kind: "
|
|
1801
|
-
: row.
|
|
1802
|
-
? _jsx(Glyph, { kind: "
|
|
1803
|
-
:
|
|
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}`));
|
|
1804
1886
|
};
|
|
1805
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));
|
|
1806
1888
|
const renderShapeRow = (row, selected, rowW = bodyW - 2) => {
|
|
@@ -1846,8 +1928,8 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
1846
1928
|
if (overlay.kind === "classify") {
|
|
1847
1929
|
const subject = overlay.bulk
|
|
1848
1930
|
? `${overlay.adapter} · ${overlay.bulk.rows.length} suggested models`
|
|
1849
|
-
: overlay.displayModel
|
|
1850
|
-
? `${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(", ")}`
|
|
1851
1933
|
: `${overlay.adapter}:${overlay.model}`;
|
|
1852
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
|
|
1853
1935
|
? `catalog suggests ${overlay.suggestion.tier} — keep it and the provenance note arrives pre-typed`
|
|
@@ -1911,7 +1993,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
1911
1993
|
const current = overlay.target.model === undefined
|
|
1912
1994
|
? adapterReach(overlay.target.adapter)
|
|
1913
1995
|
: modelRows().find((row) => row.adapter === overlay.target.adapter && row.model === overlay.target.model) ?? { reach: "unknown", reasons: [] };
|
|
1914
|
-
const partial = "partial" in current && current.partial;
|
|
1996
|
+
const partial = "partial" in current && current.partial === true;
|
|
1915
1997
|
// D-205: a partial rail marks no choice as selected — its channels disagree
|
|
1916
1998
|
const on = partial || current.reach === "unknown" ? null
|
|
1917
1999
|
: current.reach === "out-all" || current.reach === "out-allow" ? "out-all" : current.reach;
|
package/package.json
CHANGED
|
@@ -113,16 +113,25 @@ For `resume`, use the same host-owned split form with `tickmarkr resume <runId>`
|
|
|
113
113
|
in the process table to the host PTY; verify no agent session is an ancestor. If an agent is
|
|
114
114
|
an ancestor, stop that launch and relaunch through the host split before continuing.
|
|
115
115
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
116
|
+
Detached beats are product-owned, one per (tier, seat), from the repository root, through the
|
|
117
|
+
shipped lifecycle verbs — never a hand-rolled setsid plus nohup wrapper, pidfile or shell loop:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
cd <repo> && tickmarkr beat start <tier> --seat <seat>
|
|
121
|
+
cd <repo> && tickmarkr beat status <tier> --seat <seat>
|
|
122
|
+
cd <repo> && tickmarkr beat stop <tier> --seat <seat>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`start` launches the `--loop` child in its own session, logs to `<state-dir>/supervision/<tier>.log`,
|
|
126
|
+
records its pid, birth, argv and cwd, and exits 0 only after it reads back that exact child's own
|
|
127
|
+
advancing beat. A repeat start for the same (tier, seat) is an "already running" no-op; a stale,
|
|
128
|
+
dead, foreign, reused-pid, unreadable or busy tier is a nonzero refusal that changes nothing.
|
|
129
|
+
Verify ppid 1 is never proof on its own — an orphan of a dying session shows it too; `status`
|
|
130
|
+
is the check: it reads the recorded process identity and beat freshness, writes nothing, and exits
|
|
131
|
+
nonzero for STALE, MISMATCH or UNREADABLE. `stop` signals only that recorded pid after its
|
|
132
|
+
generation and identity still match, and reads back DISARMED. Never put beats in a harness
|
|
133
|
+
background task or a visible tab. Include a run-end `beat stop` item for every beat started, also
|
|
134
|
+
on failure, park or handoff, and require its DISARMED read-back.
|
|
126
135
|
|
|
127
136
|
Keep only watchers that must WAKE the seat in the harness. Re-arm them on each wake and at
|
|
128
137
|
their harness cap; daemon and beat lifetimes must not depend on that cap.
|
|
@@ -108,16 +108,25 @@ For `resume`, use the same host-owned split form with `tickmarkr resume <runId>`
|
|
|
108
108
|
in the process table to the host PTY; verify no agent session is an ancestor. If an agent is
|
|
109
109
|
an ancestor, stop that launch and relaunch through the host split before continuing.
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
111
|
+
Detached beats are product-owned, one per (tier, seat), from the repository root, through the
|
|
112
|
+
shipped lifecycle verbs — never a hand-rolled setsid plus nohup wrapper, pidfile or shell loop:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
cd <repo> && tickmarkr beat start <tier> --seat <seat>
|
|
116
|
+
cd <repo> && tickmarkr beat status <tier> --seat <seat>
|
|
117
|
+
cd <repo> && tickmarkr beat stop <tier> --seat <seat>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`start` launches the `--loop` child in its own session, logs to `<state-dir>/supervision/<tier>.log`,
|
|
121
|
+
records its pid, birth, argv and cwd, and exits 0 only after it reads back that exact child's own
|
|
122
|
+
advancing beat. A repeat start for the same (tier, seat) is an "already running" no-op; a stale,
|
|
123
|
+
dead, foreign, reused-pid, unreadable or busy tier is a nonzero refusal that changes nothing.
|
|
124
|
+
Verify ppid 1 is never proof on its own — an orphan of a dying session shows it too; `status`
|
|
125
|
+
is the check: it reads the recorded process identity and beat freshness, writes nothing, and exits
|
|
126
|
+
nonzero for STALE, MISMATCH or UNREADABLE. `stop` signals only that recorded pid after its
|
|
127
|
+
generation and identity still match, and reads back DISARMED. Never put beats in a harness
|
|
128
|
+
background task or a visible tab. Include a run-end `beat stop` item for every beat started, also
|
|
129
|
+
on failure, park or handoff, and require its DISARMED read-back.
|
|
121
130
|
|
|
122
131
|
Keep only watchers that must WAKE the seat in the harness. Re-arm them on each wake and at
|
|
123
132
|
their harness cap; daemon and beat lifetimes must not depend on that cap.
|