tickmarkr 2.5.5 → 2.5.6

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 (48) hide show
  1. package/README.md +3 -1
  2. package/dist/cli/commands/approve.js +64 -3
  3. package/dist/cli/commands/doctor.d.ts +10 -0
  4. package/dist/cli/commands/fleet.js +4 -0
  5. package/dist/cli/commands/plan.js +20 -4
  6. package/dist/cli/commands/verify.js +102 -81
  7. package/dist/config/config.d.ts +11 -0
  8. package/dist/config/config.js +21 -12
  9. package/dist/config/fleet-overlay.js +55 -17
  10. package/dist/drivers/index.js +2 -1
  11. package/dist/drivers/orca.d.ts +21 -1
  12. package/dist/drivers/orca.js +209 -27
  13. package/dist/gates/baseline.d.ts +14 -3
  14. package/dist/gates/baseline.js +61 -14
  15. package/dist/gates/cache.d.ts +14 -13
  16. package/dist/gates/cache.js +17 -5
  17. package/dist/gates/llm.d.ts +3 -0
  18. package/dist/gates/llm.js +11 -0
  19. package/dist/gates/review.d.ts +28 -3
  20. package/dist/gates/review.js +106 -14
  21. package/dist/gates/run-gates.d.ts +6 -1
  22. package/dist/gates/run-gates.js +299 -101
  23. package/dist/gates/test-manifest.d.ts +30 -1
  24. package/dist/gates/test-manifest.js +113 -39
  25. package/dist/gates/test-reporter.js +14 -6
  26. package/dist/graph/graph.d.ts +2 -0
  27. package/dist/graph/graph.js +45 -1
  28. package/dist/run/consult.js +5 -4
  29. package/dist/run/daemon.d.ts +2 -0
  30. package/dist/run/daemon.js +480 -99
  31. package/dist/run/execution-budget.d.ts +25 -0
  32. package/dist/run/execution-budget.js +142 -0
  33. package/dist/run/git.d.ts +40 -0
  34. package/dist/run/git.js +89 -4
  35. package/dist/run/journal.d.ts +3 -1
  36. package/dist/run/journal.js +38 -16
  37. package/dist/run/lease.d.ts +44 -0
  38. package/dist/run/lease.js +226 -3
  39. package/dist/run/recovery.d.ts +8 -0
  40. package/dist/run/recovery.js +25 -0
  41. package/dist/run/repair-selection.d.ts +12 -0
  42. package/dist/run/repair-selection.js +56 -0
  43. package/dist/run/stall.d.ts +6 -1
  44. package/dist/run/stall.js +60 -3
  45. package/dist/tui/ink/fleet-app.d.ts +10 -2
  46. package/dist/tui/ink/fleet-app.js +33 -15
  47. package/package.json +2 -2
  48. package/skills/tickmarkr-overseer/scripts/grade-ci.sh +29 -2
@@ -25,7 +25,7 @@ function splitSeat(seat) {
25
25
  const at = seat.indexOf(":");
26
26
  return { adapter: seat.slice(0, at), model: seat.slice(at + 1) };
27
27
  }
28
- export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels, initialDenyWorkersAdapters = [], initialDenyWorkersModels = [], modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewOverlay, reloadGuard, stagedRouting, entry = "probe", initialJudge = "", judgeSeats = [], viewRows = Number.POSITIVE_INFINITY, frameColumns = 100, frameRows, }) {
28
+ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels, initialAllowOut = [], initialDenyWorkersAdapters = [], initialDenyWorkersModels = [], modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewOverlay, reloadGuard, stagedRouting, entry = "probe", initialJudge = "", judgeSeats = [], viewRows = Number.POSITIVE_INFINITY, frameColumns = 100, frameRows, }) {
29
29
  const { exit } = useApp();
30
30
  const [, setRevision] = useState(0);
31
31
  const uiRef = useRef(null);
@@ -45,6 +45,7 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
45
45
  lastEdit: null,
46
46
  deny: new Set(initialDenyAdapters),
47
47
  denyModels: new Set(initialDenyModels),
48
+ allowOut: new Set(initialAllowOut),
48
49
  denyWorkersAdapters: new Set(initialDenyWorkersAdapters),
49
50
  denyWorkersModels: new Set(initialDenyWorkersModels),
50
51
  classifications: [],
@@ -64,6 +65,7 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
64
65
  // chip, and the empty-`w` notice all read it. Counts staged EDITS, not diff hunks.
65
66
  const initialDenySet = new Set(initialDenyAdapters);
66
67
  const initialDenyModelSet = new Set(initialDenyModels);
68
+ const initialAllowOutSet = new Set(initialAllowOut);
67
69
  const initialDenyWorkersAdapterSet = new Set(initialDenyWorkersAdapters);
68
70
  const initialDenyWorkersModelSet = new Set(initialDenyWorkersModels);
69
71
  const stagedCount = () => {
@@ -82,6 +84,9 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
82
84
  for (const model of initialDenyModelSet)
83
85
  if (!ui.denyModels.has(model))
84
86
  n += 1;
87
+ for (const key of initialAllowOutSet)
88
+ if (!ui.allowOut.has(key))
89
+ n += 1;
85
90
  for (const adapter of ui.denyWorkersAdapters)
86
91
  if (!initialDenyWorkersAdapterSet.has(adapter))
87
92
  n += 1;
@@ -218,6 +223,7 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
218
223
  models: [...ui.denyModels].sort(),
219
224
  workersAdapters: [...ui.denyWorkersAdapters].sort(),
220
225
  workersModels: [...ui.denyWorkersModels].sort(),
226
+ allowOut: [...ui.allowOut].sort(),
221
227
  });
222
228
  const shapeList = () => shapeRows(ui.selectedMode, ui.map, stagedDeny());
223
229
  const steeringList = () => [
@@ -245,6 +251,7 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
245
251
  const editorState = () => ({
246
252
  denyAdapters: [...ui.deny].sort(),
247
253
  denyModels: [...ui.denyModels].sort(),
254
+ allowOut: [...ui.allowOut].sort(),
248
255
  denyWorkersAdapters: [...ui.denyWorkersAdapters].sort(),
249
256
  denyWorkersModels: [...ui.denyWorkersModels].sort(),
250
257
  classifications: ui.classifications.map((classification) => classification.vendor && classification.channel
@@ -940,12 +947,18 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
940
947
  if (agents.find((agent) => agent.id === id)?.authed === false) {
941
948
  ui.notice = `${id} is not authed — Space only toggles fleet membership; re-auth the ${id} CLI, then run tickmarkr doctor`;
942
949
  }
943
- const next = new Set(ui.deny);
944
- if (next.has(id))
945
- next.delete(id);
946
- else
947
- next.add(id);
948
- ui.deny = next;
950
+ // OBS-1046: an adapter routing.allow leaves out comes back in by clearing THAT reason
951
+ if (ui.allowOut.has(id)) {
952
+ ui.allowOut = new Set([...ui.allowOut].filter((key) => key !== id));
953
+ }
954
+ else {
955
+ const next = new Set(ui.deny);
956
+ if (next.has(id))
957
+ next.delete(id);
958
+ else
959
+ next.add(id);
960
+ ui.deny = next;
961
+ }
949
962
  clampList();
950
963
  bump();
951
964
  return;
@@ -1048,12 +1061,14 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
1048
1061
  ui.lastEdit = { id, text: `space: moved ${entry} to routing.deny.models` };
1049
1062
  }
1050
1063
  else {
1051
- // round 2 finding 2: either flat list may carry this channel's own reason
1064
+ // round 2 finding 2: either flat list may carry this channel's own reason; OBS-1046: an
1065
+ // allow exclusion is the LAST reason cleared, and never together with an authored deny
1052
1066
  const fromModels = select(ui.denyModels, true);
1053
1067
  const fromAdapters = fromModels === undefined ? select(ui.deny, true) : undefined;
1054
- const entry = fromModels ?? fromAdapters;
1068
+ const fromAllow = fromModels === undefined && fromAdapters === undefined ? select(ui.allowOut, true) : undefined;
1069
+ const entry = fromModels ?? fromAdapters ?? fromAllow;
1055
1070
  if (entry === undefined) {
1056
- const shared = ui.deny.has(row.adapter)
1071
+ const shared = ui.deny.has(row.adapter) || ui.allowOut.has(row.adapter)
1057
1072
  ? `every ${row.adapter} channel is out together — take the adapter back in on the rail`
1058
1073
  : "a shared entry covers other channels too";
1059
1074
  ui.notice = `${id} stays out — ${row.reasons.join("; ")} — Space edits only this channel's own entries; ${shared}`;
@@ -1062,9 +1077,12 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
1062
1077
  }
1063
1078
  if (fromModels !== undefined)
1064
1079
  ui.denyModels = new Set([...ui.denyModels].filter((staged) => staged !== entry));
1065
- else
1080
+ else if (fromAdapters !== undefined)
1066
1081
  ui.deny = new Set([...ui.deny].filter((staged) => staged !== entry));
1067
- ui.lastEdit = { id, text: `space: cleared ${entry} from routing.deny.${fromModels !== undefined ? "models" : "adapters"}` };
1082
+ else
1083
+ ui.allowOut = new Set([...ui.allowOut].filter((staged) => staged !== entry));
1084
+ const scope = fromModels !== undefined ? "routing.deny.models" : fromAdapters !== undefined ? "routing.deny.adapters" : "routing.allow";
1085
+ ui.lastEdit = { id, text: `space: cleared ${entry} from ${scope}` };
1068
1086
  }
1069
1087
  bump();
1070
1088
  return;
@@ -1333,7 +1351,7 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
1333
1351
  const railAdapter = (index, railIndex) => {
1334
1352
  const group = modelGroups[index];
1335
1353
  const agent = agents.find((candidate) => candidate.id === group.adapter);
1336
- const denied = ui.deny.has(group.adapter);
1354
+ const denied = ui.deny.has(group.adapter) || ui.allowOut.has(group.adapter);
1337
1355
  const active = ui.view === "models" && ui.adapterAt === index;
1338
1356
  const selected = ui.focus === "rail" && ui.railAt === railIndex;
1339
1357
  return (_jsxs(Text, { children: [selected ? _jsx(Text, { color: INK.brand, children: "❯ " }) : _jsx(Text, { children: " " }), denied ? _jsx(Glyph, { kind: "off" }) : agent?.authed === false ? _jsx(Glyph, { kind: "fail" }) : _jsx(Glyph, { kind: "on" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: active, color: active ? INK.brand : undefined, dimColor: denied, children: padCell(group.adapter, 12) }), _jsx(Text, { dimColor: true, children: padCellStart(String(visibleCount(index)), 4) })] }, group.adapter));
@@ -1578,7 +1596,7 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
1578
1596
  ? _jsx(Text, { color: INK.warn, children: ` ! ${clip(ui.notice, width - 4)}` })
1579
1597
  : details.map((line) => _jsx(Text, { dimColor: true, children: ` ${clip(line, width - 2)}` }, line)), _jsx(KeyBar, { binds: keyBinds() })] }));
1580
1598
  }
1581
- export async function runFleetInkEditor({ ageMs, adapters, health, initialDenyAdapters, initialDenyModels, initialDenyWorkersAdapters = [], initialDenyWorkersModels = [], modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewOverlay, reloadGuard, stagedRouting, entry = "probe", initialJudge = "", judgeSeats = [], initialInput = [], input, output, debug = false, }) {
1599
+ export async function runFleetInkEditor({ ageMs, adapters, health, initialDenyAdapters, initialDenyModels, initialAllowOut = [], initialDenyWorkersAdapters = [], initialDenyWorkersModels = [], modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewOverlay, reloadGuard, stagedRouting, entry = "probe", initialJudge = "", judgeSeats = [], initialInput = [], input, output, debug = false, }) {
1582
1600
  const agents = adapters.flatMap((adapter) => {
1583
1601
  const state = health[adapter.id];
1584
1602
  if (!state?.installed)
@@ -1602,7 +1620,7 @@ export async function runFleetInkEditor({ ageMs, adapters, health, initialDenyAd
1602
1620
  // rebuilt from output.columns/rows so a pane resize re-renders at the new size. Same root
1603
1621
  // element type on every rerender ⇒ React keeps the component instance, so staged edits,
1604
1622
  // cursor, and overlays all survive the resize.
1605
- const editorElement = () => (_jsx(FleetApp, { ageMs: ageMs, agents: agents, initialDenyAdapters: initialDenyAdapters, initialDenyModels: initialDenyModels, initialDenyWorkersAdapters: initialDenyWorkersAdapters, initialDenyWorkersModels: initialDenyWorkersModels, modelGroups: declaredModelGroups, initialMode: initialMode, modeOptions: modeOptions, initialMap: initialMap, modePreview: modePreview, shapeRows: shapeRows, candidatesForShape: candidatesForShape, preferOptionsForShape: preferOptionsForShape, initialSteering: initialSteering, steeringOptionsFor: steeringOptionsFor, reviewOverlay: reviewOverlay, reloadGuard: reloadGuard, stagedRouting: stagedRouting, entry: entry, initialJudge: initialJudge, judgeSeats: judgeSeats,
1623
+ const editorElement = () => (_jsx(FleetApp, { ageMs: ageMs, agents: agents, initialDenyAdapters: initialDenyAdapters, initialDenyModels: initialDenyModels, initialAllowOut: initialAllowOut, initialDenyWorkersAdapters: initialDenyWorkersAdapters, initialDenyWorkersModels: initialDenyWorkersModels, modelGroups: declaredModelGroups, initialMode: initialMode, modeOptions: modeOptions, initialMap: initialMap, modePreview: modePreview, shapeRows: shapeRows, candidatesForShape: candidatesForShape, preferOptionsForShape: preferOptionsForShape, initialSteering: initialSteering, steeringOptionsFor: steeringOptionsFor, reviewOverlay: reviewOverlay, reloadGuard: reloadGuard, stagedRouting: stagedRouting, entry: entry, initialJudge: initialJudge, judgeSeats: judgeSeats,
1606
1624
  // v1.90.9: cap every list to the terminal minus chrome so the cursor and header can
1607
1625
  // never scroll off-screen (omp's 218-model list did exactly that pre-cap).
1608
1626
  // 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.5.5",
3
+ "version": "2.5.6",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "homepage": "https://github.com/alzahrani-khalid/tickmarkr",
19
19
  "engines": {
20
- "node": ">=20"
20
+ "node": ">=20.3"
21
21
  },
22
22
  "bin": {
23
23
  "tickmarkr": "dist/cli/index.js",
@@ -50,14 +50,41 @@ while IFS=$'\t' read -r id name status conclusion; do
50
50
  skipped=$(printf '%s\n' "$files" | grep -oE '[0-9]+ skipped' | awk '{s+=$1} END{print s+0}')
51
51
  failed=$(printf '%s\n' "$files" | grep -oE '[0-9]+ failed' | awk '{s+=$1} END{print s+0}')
52
52
  timedout=$(grep -cE 'Test timed out|Error: Hook timed out' "$log" || true)
53
+ # Vitest reports errors thrown outside any test (unhandled rejections, worker crashes) in an
54
+ # "Unhandled Errors" block that leaves the file summary green and the count oracle satisfied;
55
+ # its own tally line names how many. Any unhandled error is RED, never a green suite.
56
+ unhandled=$(grep -oE 'Vitest caught [0-9]+ unhandled error' "$log" | grep -oE '[0-9]+' | awk '{s+=$1} END{print s+0}')
57
+ if [ "$unhandled" -eq 0 ]; then
58
+ unhandled=$(grep -cE 'Unhandled Errors' "$log" || true)
59
+ fi
60
+ # OBS-1058 (RULING-231-17 precedent, v2.5.5 close R115 add.7): a vitest-worker RPC timeout
61
+ # ("Timeout calling \"onTaskUpdate\"") is the starved runner talking, not a test — every green
62
+ # release run has carried one. Only a timeout that is ITSELF an unhandled entry counts: the first
63
+ # payload line under a per-error header, nothing later. The header is Vitest's own structural line —
64
+ # a run of ⎯ on each side of "Unhandled Error", ANSI stripped (both the raw ESC byte and gh's `^[` rendering of it), nothing else on the line — so prose that
65
+ # merely contains the words never opens a window — and the payload must be Vitest's whole line
66
+ # (`Error: [vitest-worker]: Timeout calling "<method>"`), so an assertion quoting it stays RED. Per-error headers are honoured only inside Vitest's own block —
67
+ # after its structural "Unhandled Errors" line and "Vitest caught N" tally, until the file summary — and
68
+ # the count never exceeds N, so a test that prints a header and a timeout to stdout forges nothing. A stray diagnostic elsewhere in the log never
69
+ # offsets a real unhandled error; anything else under a header stays RED.
70
+ esc=$(printf '\033')
71
+ rpc=$(awk -v esc="$esc" '{ line = $0; gsub(/\^\[\[[0-9;]*m/, "", line); gsub(esc "\\[[0-9;]*m", "", line); sub(/^[^\t]*\t[^\t]*\t[0-9T:.-]+Z[ ]?/, "", line) }
72
+ line ~ /^(⎯)+ Unhandled Errors (⎯)+[ \t]*$/ { opening = 1; next }
73
+ opening && line !~ /[^ \t]/ { next }
74
+ opening { opening = 0; if (line ~ /^Vitest caught [0-9]+ unhandled error/) { armed = 1; cap = line; sub(/^Vitest caught /, "", cap); sub(/ .*$/, "", cap) } }
75
+ armed && line ~ /^[ \t]*Test Files / { armed = 0 }
76
+ armed && line ~ /^(⎯)+ Unhandled Error (⎯)+[ \t]*$/ { pending = 1; next }
77
+ pending && line ~ /[^ \t]/ { pending = 0; if (line ~ /^Error: \[vitest-worker\]: Timeout calling "[A-Za-z]+"[ \t]*$/) n++ }
78
+ END { if (n > cap + 0) n = cap + 0; print n + 0 }' "$log")
79
+ if [ "$rpc" -gt 0 ] && [ "$unhandled" -ge "$rpc" ]; then unhandled=$((unhandled - rpc)); fi
53
80
  errors=$(grep -oE '##\[error\].*' "$log" | sort | uniq -c | sed 's/^ *//' | tr '\n' ';')
54
81
 
55
- echo "$name: oracle=[${oracle:-MISSING}] files=[$(printf '%s' "$files" | tr '\n' '|')] passed=$passed skipped=$skipped failed=$failed timedout=$timedout errors=[$errors]"
82
+ echo "$name: oracle=[${oracle:-MISSING}] files=[$(printf '%s' "$files" | tr '\n' '|')] passed=$passed skipped=$skipped failed=$failed timedout=$timedout unhandled=$unhandled runner_rpc_timeouts=${rpc:-0} errors=[$errors]"
56
83
  if [ -z "$oracle" ] || [ -z "$files" ]; then
57
84
  echo "$name: UNREADABLE"
58
85
  mark_unreadable
59
86
  elif [ "$oracle" = "COUNT_ORACLE GREEN expected=$expected actual=$expected" ] \
60
- && [ "$failed" -eq 0 ] && [ "$timedout" -eq 0 ] \
87
+ && [ "$failed" -eq 0 ] && [ "$timedout" -eq 0 ] && [ "$unhandled" -eq 0 ] \
61
88
  && [ $((passed + skipped)) -eq "$expected" ]; then
62
89
  echo "$name: GREEN"
63
90
  else