tickmarkr 2.6.4 → 2.6.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 (73) hide show
  1. package/README.md +7 -3
  2. package/dist/adapters/catalog-remote.d.ts +23 -0
  3. package/dist/adapters/catalog-remote.js +38 -0
  4. package/dist/adapters/claude-code.d.ts +1 -1
  5. package/dist/adapters/claude-code.js +4 -1
  6. package/dist/adapters/codex-commit-check.d.ts +36 -0
  7. package/dist/adapters/codex-commit-check.js +205 -0
  8. package/dist/adapters/codex.d.ts +1 -0
  9. package/dist/adapters/codex.js +12 -6
  10. package/dist/adapters/model-discovery-check.d.ts +33 -0
  11. package/dist/adapters/model-discovery-check.js +123 -0
  12. package/dist/adapters/model-lints.d.ts +1 -1
  13. package/dist/adapters/model-lints.js +4 -0
  14. package/dist/adapters/prompt.js +3 -2
  15. package/dist/adapters/types.d.ts +10 -0
  16. package/dist/cli/commands/approve.js +10 -4
  17. package/dist/cli/commands/doctor.d.ts +2 -0
  18. package/dist/cli/commands/doctor.js +17 -3
  19. package/dist/cli/commands/fleet.d.ts +10 -1
  20. package/dist/cli/commands/fleet.js +363 -106
  21. package/dist/cli/commands/init.js +10 -3
  22. package/dist/cli/commands/plan.js +76 -4
  23. package/dist/cli/commands/verify.d.ts +1 -1
  24. package/dist/cli/commands/verify.js +65 -15
  25. package/dist/config/fleet-overlay.d.ts +16 -1
  26. package/dist/config/fleet-overlay.js +230 -28
  27. package/dist/drivers/orca.d.ts +34 -2
  28. package/dist/drivers/orca.js +206 -32
  29. package/dist/gates/cache.d.ts +4 -0
  30. package/dist/gates/cache.js +10 -2
  31. package/dist/gates/llm.d.ts +4 -1
  32. package/dist/gates/llm.js +6 -1
  33. package/dist/gates/review.js +8 -3
  34. package/dist/gates/run-gates.d.ts +5 -3
  35. package/dist/gates/run-gates.js +112 -56
  36. package/dist/gates/test-manifest.d.ts +3 -0
  37. package/dist/gates/test-manifest.js +1 -1
  38. package/dist/gates/timeout-shaped.d.ts +3 -0
  39. package/dist/gates/timeout-shaped.js +10 -0
  40. package/dist/run/daemon.js +326 -130
  41. package/dist/run/git.js +9 -0
  42. package/dist/run/journal.d.ts +6 -2
  43. package/dist/run/journal.js +22 -7
  44. package/dist/run/launchservices-check.d.ts +56 -0
  45. package/dist/run/launchservices-check.js +117 -0
  46. package/dist/run/operator-state.d.ts +22 -2
  47. package/dist/run/operator-state.js +20 -3
  48. package/dist/run/title-environment.d.ts +45 -0
  49. package/dist/run/title-environment.js +127 -0
  50. package/dist/run/title-preload.cjs +38 -0
  51. package/dist/run/title-preload.d.cts +1 -0
  52. package/dist/tui/cockpit/board.js +12 -6
  53. package/dist/tui/cockpit/keys.d.ts +8 -0
  54. package/dist/tui/cockpit/keys.js +4 -1
  55. package/dist/tui/cockpit/layout.d.ts +2 -2
  56. package/dist/tui/cockpit/layout.js +2 -2
  57. package/dist/tui/cockpit/live-runtime.js +46 -20
  58. package/dist/tui/cockpit/live-store.d.ts +24 -1
  59. package/dist/tui/cockpit/live-store.js +99 -8
  60. package/dist/tui/cockpit/log-view.d.ts +28 -0
  61. package/dist/tui/cockpit/log-view.js +60 -0
  62. package/dist/tui/cockpit/run-view.js +4 -0
  63. package/dist/tui/cockpit/shell.d.ts +2 -1
  64. package/dist/tui/cockpit/shell.js +6 -4
  65. package/dist/tui/ink/fleet-app.d.ts +37 -10
  66. package/dist/tui/ink/fleet-app.js +65 -21
  67. package/package.json +1 -1
  68. package/skills/tickmarkr-auto/SKILL.md +32 -2
  69. package/skills/tickmarkr-loop/SKILL.md +46 -2
  70. package/skills/tickmarkr-overseer/SKILL.md +83 -8
  71. package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +58 -16
  72. package/skills/tickmarkr-overseer/scripts/grade-ci.sh +19 -6
  73. package/skills/tickmarkr-overseer/scripts/watch-parks.sh +121 -64
@@ -0,0 +1,60 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { narrationRow } from "../../cli/commands/run.js";
4
+ import { STORE_LIMITS } from "./live-store.js";
5
+ /** The board uses the daemon's TTY narration, including its suppression and sanitization. */
6
+ export const renderLogLines = (events, runId, columns) => events.map(event => narrationRow(event, runId, columns)).filter((line) => line !== null);
7
+ const narrate = (rows, source) => rows.flatMap(row => row.event
8
+ ? renderLogLines([row.event], source.runId, source.columns).map(text => ({ line: row.line, text })) : []);
9
+ /** Demand-page backwards to fill the viewport, including events outside the store's retained tail.
10
+ * Only the requested visible rows survive; suppressed events never consume viewport rows. */
11
+ export function readLogPage(source, count, end = source.journal.lines) {
12
+ let cursor = Math.min(end, source.journal.lines);
13
+ let rows = [];
14
+ try {
15
+ while (cursor > 0 && rows.length < count) {
16
+ const first = Math.max(1, cursor - STORE_LIMITS.history + 1);
17
+ const retained = source.journal.history.filter(row => row.line >= first && row.line <= cursor);
18
+ const page = retained.length === cursor - first + 1 ? retained
19
+ : source.page(first, cursor - first + 1, source.journal.generation);
20
+ rows = [...narrate(page, source), ...rows].slice(-count);
21
+ cursor = first - 1;
22
+ }
23
+ return { rows };
24
+ }
25
+ catch (error) {
26
+ return { rows: [], error: error instanceof Error ? error.message : String(error) };
27
+ }
28
+ }
29
+ /** Move by visible narration rows. Undefined means follow the journal tail again. */
30
+ export function moveLogEnd(source, end, delta) {
31
+ const cursor = Math.min(end ?? source.journal.lines, source.journal.lines);
32
+ if (delta < 0) {
33
+ const rows = readLogPage(source, -delta + 1, cursor).rows;
34
+ return rows[Math.max(0, rows.length - 1 + delta)]?.line ?? end;
35
+ }
36
+ let first = cursor + 1, remaining = delta;
37
+ while (first <= source.journal.lines) {
38
+ const last = Math.min(source.journal.lines, first + STORE_LIMITS.history - 1);
39
+ let rows;
40
+ try {
41
+ rows = narrate(source.page(first, last - first + 1, source.journal.generation), source);
42
+ }
43
+ catch {
44
+ return end;
45
+ } // A stale journal observation must never terminate the board's key handler.
46
+ if (rows.length >= remaining) {
47
+ const line = rows[remaining - 1].line;
48
+ return line === source.journal.lines ? undefined : line;
49
+ }
50
+ remaining -= rows.length;
51
+ first = last + 1;
52
+ }
53
+ return undefined;
54
+ }
55
+ export function LogView({ page }) {
56
+ return _jsx(Box, { flexDirection: "column", children: page.error
57
+ ? _jsxs(Text, { children: ["Log unavailable: ", page.error] })
58
+ : page.rows.length ? page.rows.map(row => _jsx(Text, { wrap: "truncate-end", children: row.text }, row.line))
59
+ : _jsx(Text, { children: "No narrated events" }) });
60
+ }
@@ -98,6 +98,10 @@ export function runGateCells(task, evidence, rows = []) {
98
98
  else if (cell.state === "queued") {
99
99
  labels.push(`queued — ${row?.event?.event ?? "wait"}${typeof data.count === "number" ? ` (${data.count} suites)` : ""}`);
100
100
  }
101
+ else if (cell.state === "unknown" && row?.event?.event !== "gate-result") {
102
+ // I1: a start or wait row settled at a terminal boundary — no verdict ever measured it.
103
+ labels.push(`unknown — ${row?.event?.event ?? "start"} never finished`);
104
+ }
101
105
  else if (data.disabled === true) {
102
106
  labels.push("disabled by policy");
103
107
  }
@@ -2,13 +2,14 @@ import { type ReactNode } from "react";
2
2
  import { type ShellPlan } from "./layout.js";
3
3
  import { SHELL_BINDINGS } from "./keys.js";
4
4
  import type { LiveStoreSnapshot } from "./live-store.js";
5
- export type ShellView = "home" | "run" | "evidence";
5
+ export type ShellView = "home" | "run" | "evidence" | "log";
6
6
  export type ShellFocus = ShellPlan["focus"][number];
7
7
  export interface ShellState {
8
8
  view: ShellView;
9
9
  focus: ShellFocus;
10
10
  scroll: number;
11
11
  help: boolean;
12
+ logEnd?: number;
12
13
  evidenceSection?: number;
13
14
  canOpen?: boolean;
14
15
  shortcutColumns?: number;
@@ -13,7 +13,8 @@ export const shellHelp = (bindings = SHELL_BINDINGS) => [
13
13
  "Esc closes deepest overlay; it never quits.",
14
14
  "Text: q1? is literal; Enter applies; Esc cancels.",
15
15
  "Confirm: y alone confirms; n/Esc cancels.",
16
- "Tab now moves focus; view numbers are 1/4/5.",
16
+ "Tab now moves focus; view numbers are 1/4/5/7.",
17
+ "Log: arrows/page keys scroll; f toggles tail follow.",
17
18
  "Fleet: tickmarkr fleet", "Plan: tickmarkr plan", "Health: tickmarkr doctor",
18
19
  ];
19
20
  /** Read the Yoga geometry that actually painted, including clipped/scrolled leaves.
@@ -65,10 +66,11 @@ export function CockpitShell({ snapshot, state, columns, rows, version, children
65
66
  const overlay = state.editor !== undefined ? [`${state.editorKind === "export" ? "Export destination" : "Filter"}: ${state.editor}`, "Enter Apply | Esc Cancel"] : state.overlay ?? (state.help ? shellHelp(bindings) : undefined);
66
67
  const totalRows = overlay?.length ?? contentRows;
67
68
  const displayOffset = overlay ? Math.min(state.overlayOffset, Math.max(0, totalRows - plan.bodyRows)) : scroll;
68
- const position = `${totalRows ? displayOffset + 1 : 0}–${Math.min(totalRows, displayOffset + plan.bodyRows)}/${totalRows} | ${Math.max(0, totalRows - plan.bodyRows)} hidden`;
69
+ const logMode = state.view === "log" ? `${state.logEnd === undefined ? "FOLLOW" : "PAUSED"} | ` : "";
70
+ const position = `${logMode}${totalRows ? displayOffset + 1 : 0}–${Math.min(totalRows, displayOffset + plan.bodyRows)}/${totalRows} | ${Math.max(0, totalRows - plan.bodyRows)} hidden`;
69
71
  const keybar = bindings.filter(b => ["focus", "open", "actions", "help", "quit"].includes(b.action) && (columns >= 60 || b.action !== "focus")).map(b => `${b.key} ${columns < 60 && b.action === "actions" ? "Act" : b.label}`).join(" | ");
70
72
  const status = columns < 60 ? `${op.label} | tip ${op.currentTip.toUpperCase()}` : `${op.label} | MERGED ${op.merged}/${op.planned ?? "?"} | CURRENT TIP ${op.currentTip.toUpperCase()} | ${snapshot.freshness}`;
71
73
  return _jsx(ShellPresentation.Provider, { value: true, children: _jsx(ShellTheme.Provider, { value: { mode: colour, palette }, children: _jsxs(Box, { ref: root, flexDirection: "column", width: columns, height: rows, flexShrink: 0, backgroundColor: ink(palette.surface), children: [line(`tickmarkr ${version} | ${state.view.toUpperCase()} | ${snapshot.journal.source.split("/").at(-2) ?? "run identity unknown"}`), line(columns < 60
72
- ? `1H 4R 5E | ${position}`
73
- : `1 Home 4 Run 5 Evidence | ${state.focus} | ${position}`), line("─".repeat(columns), columns, palette.chrome), _jsxs(Box, { height: plan.bodyRows, flexShrink: 0, flexDirection: "row", overflow: "hidden", children: [_jsx(Box, { width: 1, flexDirection: "column", children: Array.from({ length: plan.bodyRows }, (_, i) => _jsx(Text, { color: ink(palette.chrome), children: "\u2502" }, i)) }), plan.rail > 0 && _jsxs(_Fragment, { children: [_jsx(Box, { width: plan.rail, flexDirection: "column", children: ["1 Home", "4 Run", "5 Evidence", "", "CLI:", "fleet", "plan", "doctor"].map((t, i) => _jsx(Text, { color: ink(palette.chrome), children: fitCells(t, plan.rail) }, i)) }), _jsx(Box, { width: 1, flexDirection: "column", children: Array.from({ length: plan.bodyRows }, (_, i) => _jsx(Text, { color: ink(palette.chrome), children: "\u2502" }, i)) })] }), _jsxs(Box, { width: plan.bodyColumns, height: plan.bodyRows, flexDirection: "column", overflow: "hidden", flexShrink: 0, children: [overlay?.slice(displayOffset, displayOffset + plan.bodyRows).map((t, i) => _jsx(Text, { wrap: "truncate-end", color: ink(palette.text), children: fitCells(t, plan.bodyColumns) }, i)), _jsx(Box, { ref: leaf, display: overlay ? "none" : "flex", flexDirection: "column", flexShrink: 0, marginTop: -scroll, children: children })] }), plan.shortcuts > 0 && _jsxs(_Fragment, { children: [_jsx(Box, { width: 1, flexDirection: "column", children: Array.from({ length: plan.bodyRows }, (_, i) => _jsx(Text, { color: ink(palette.chrome), children: "\u2502" }, i)) }), _jsx(Box, { width: plan.shortcuts, flexDirection: "column", children: bindings.map(b => _jsx(Text, { color: ink(palette.chrome), wrap: "truncate-end", children: fitCells(`${b.key} ${b.label}`, plan.shortcuts) }, b.key)) })] }), _jsx(Box, { width: 1, flexDirection: "column", children: Array.from({ length: plan.bodyRows }, (_, i) => _jsx(Text, { color: ink(palette.chrome), children: "\u2502" }, i)) })] }), line("─".repeat(columns), columns, palette.chrome), line(status), line(keybar), line("─".repeat(columns), columns, palette.chrome)] }) }) });
74
+ ? `1H 4R 5E 7L | ${position}`
75
+ : `1 Home 4 Run 5 Evidence 7 Log | ${state.focus} | ${position}`), line("─".repeat(columns), columns, palette.chrome), _jsxs(Box, { height: plan.bodyRows, flexShrink: 0, flexDirection: "row", overflow: "hidden", children: [_jsx(Box, { width: 1, flexDirection: "column", children: Array.from({ length: plan.bodyRows }, (_, i) => _jsx(Text, { color: ink(palette.chrome), children: "\u2502" }, i)) }), plan.rail > 0 && _jsxs(_Fragment, { children: [_jsx(Box, { width: plan.rail, flexDirection: "column", children: ["1 Home", "4 Run", "5 Evidence", "7 Log", "", "CLI:", "fleet", "plan", "doctor"].map((t, i) => _jsx(Text, { color: ink(palette.chrome), children: fitCells(t, plan.rail) }, i)) }), _jsx(Box, { width: 1, flexDirection: "column", children: Array.from({ length: plan.bodyRows }, (_, i) => _jsx(Text, { color: ink(palette.chrome), children: "\u2502" }, i)) })] }), _jsxs(Box, { width: plan.bodyColumns, height: plan.bodyRows, flexDirection: "column", overflow: "hidden", flexShrink: 0, children: [overlay?.slice(displayOffset, displayOffset + plan.bodyRows).map((t, i) => _jsx(Text, { wrap: "truncate-end", color: ink(palette.text), children: fitCells(t, plan.bodyColumns) }, i)), _jsx(Box, { ref: leaf, display: overlay ? "none" : "flex", flexDirection: "column", flexShrink: 0, marginTop: -scroll, children: children })] }), plan.shortcuts > 0 && _jsxs(_Fragment, { children: [_jsx(Box, { width: 1, flexDirection: "column", children: Array.from({ length: plan.bodyRows }, (_, i) => _jsx(Text, { color: ink(palette.chrome), children: "\u2502" }, i)) }), _jsx(Box, { width: plan.shortcuts, flexDirection: "column", children: bindings.map(b => _jsx(Text, { color: ink(palette.chrome), wrap: "truncate-end", children: fitCells(`${b.key} ${b.label}`, plan.shortcuts) }, b.key)) })] }), _jsx(Box, { width: 1, flexDirection: "column", children: Array.from({ length: plan.bodyRows }, (_, i) => _jsx(Text, { color: ink(palette.chrome), children: "\u2502" }, i)) })] }), line("─".repeat(columns), columns, palette.chrome), line(status), line(keybar), line("─".repeat(columns), columns, palette.chrome)] }) }) });
74
76
  }
@@ -2,9 +2,21 @@ import { type AuthHealth, type WorkerAdapter } from "../../adapters/types.js";
2
2
  import { type DenyScopeKey, type InitialDenyProps, type MapEntry, type RoutingMode, type StagedDenyKey, type Tier, type TickmarkrConfig } from "../../config/config.js";
3
3
  import { type Effort, type Shape } from "../../graph/schema.js";
4
4
  export type FleetSteeringKey = "review" | "consult";
5
+ /** T10 (D-939): the routing.allow entry a classification staged this session excluded — the row's own
6
+ * entry in its ownAllow spelling, added to the staged complement so Space can admit it in-session.
7
+ * `model` is the classified id (the tiers key), `displayModel` the browser row it was staged from. */
8
+ export type FleetSeededAllow = {
9
+ adapter: string;
10
+ model: string;
11
+ displayModel: string;
12
+ identity?: string;
13
+ entry: string;
14
+ };
5
15
  export type FleetEditorState = Record<DenyScopeKey, string[]> & {
6
16
  /** OBS-1046: the staged routing.allow complement, distinct from the authored deny lists above */
7
17
  allowOut?: string[];
18
+ /** T10: one seed per staged classification the current allow form left out */
19
+ seededAllowOut?: FleetSeededAllow[];
8
20
  classifications: FleetClassification[];
9
21
  /** OBS-1182: staged launch effort per adapter → model; absent = the CLI's own default */
10
22
  efforts?: Record<string, Record<string, Effort>>;
@@ -16,8 +28,19 @@ export type FleetEditorState = Record<DenyScopeKey, string[]> & {
16
28
  };
17
29
  steering: Record<FleetSteeringKey, string[] | undefined>;
18
30
  };
31
+ /** T8: the staged classifications and efforts every preview folds — the same metadata a review stages,
32
+ * so a preview ranks what w would write before the first review and after an escaped one. */
33
+ export type FleetStagedMetadata = Pick<FleetEditorState, "classifications" | "efforts" | "seededAllowOut">;
34
+ /** OBS-525: auto reverts the shape's WHOLE routing declaration — pool included, not just the pin.
35
+ * prefer survives: it biases auto routing and is valid without pin/pool. */
36
+ export declare function autoMapEntry(entry: MapEntry | undefined): MapEntry;
19
37
  export type FleetOverlayReview = {
20
38
  kind: "empty";
39
+ }
40
+ /** T10: a staged batch the one-destination review cannot write (mixed destinations, a stray seed) */
41
+ | {
42
+ kind: "refused";
43
+ reason: string;
21
44
  } | {
22
45
  kind: "diff";
23
46
  before: string;
@@ -125,7 +148,7 @@ export type FleetStagedRouting = {
125
148
  ok: false;
126
149
  error: string;
127
150
  };
128
- export declare function FleetApp({ ageMs, agents, initialAllowOut, initialEfforts, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewAdvisories, reviewOverlay, reloadGuard, stagedRouting, entry, initialJudge, judgeSeats, viewRows, frameColumns, frameRows, ...initialDenyProps }: InitialDenyProps & {
151
+ export declare function FleetApp({ ageMs, agents, initialAllowOut, initialEfforts, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewAdvisories, reviewOverlay, reloadGuard, stagedRouting, holderOf, entry, initialJudge, judgeSeats, viewRows, frameColumns, frameRows, ...initialDenyProps }: InitialDenyProps & {
129
152
  ageMs: number | null;
130
153
  agents: AgentCli[];
131
154
  /** OBS-1046: adapter ids and adapter:model keys routing.allow leaves out of the fleet */
@@ -136,9 +159,9 @@ export declare function FleetApp({ ageMs, agents, initialAllowOut, initialEffort
136
159
  initialMode: RoutingMode;
137
160
  modeOptions: FleetModeOption[];
138
161
  initialMap: Record<string, MapEntry>;
139
- modePreview: (mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny) => string[];
140
- shapeRows: (mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny) => FleetShapeRow[];
141
- candidatesForShape: (shape: Shape, mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny) => {
162
+ modePreview: (mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny, stage?: FleetStagedMetadata) => string[];
163
+ shapeRows: (mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny, stage?: FleetStagedMetadata) => FleetShapeRow[];
164
+ candidatesForShape: (shape: Shape, mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny, stage?: FleetStagedMetadata) => {
142
165
  rows: FleetCandidateOption[];
143
166
  excludedNote?: string;
144
167
  ledger?: string[];
@@ -152,7 +175,9 @@ export declare function FleetApp({ ageMs, agents, initialAllowOut, initialEffort
152
175
  reloadGuard: (bytes: string) => string | null;
153
176
  /** the routing policy the staged deny sets load as (fleet: the config loader over the candidate
154
177
  * bytes); absent ⇒ the four staged sets alone, with no allowlist */
155
- stagedRouting?: (deny: FleetStagedDeny) => FleetStagedRouting;
178
+ stagedRouting?: (deny: FleetStagedDeny, stage?: FleetStagedMetadata) => FleetStagedRouting;
179
+ /** T10: the overlay `path:line` holding a dotted config scope in this repository, undefined = defaults */
180
+ holderOf?: (dotted: string) => string | undefined;
156
181
  entry?: "presets" | "probe";
157
182
  /** resolved config judge as "adapter:model" — shown on the (keep default) picker row */
158
183
  initialJudge?: string;
@@ -165,7 +190,7 @@ export declare function FleetApp({ ageMs, agents, initialAllowOut, initialEffort
165
190
  /** terminal height — set only on a real TTY so the footer sticks to the bottom */
166
191
  frameRows?: number;
167
192
  }): import("react").JSX.Element;
168
- export declare function runFleetInkEditor({ ageMs, adapters, health, initialAllowOut, initialEfforts, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewAdvisories, reviewOverlay, reloadGuard, stagedRouting, entry, initialJudge, judgeSeats, initialInput, input, output, debug, ...initialDenyProps }: InitialDenyProps & {
193
+ export declare function runFleetInkEditor({ ageMs, adapters, health, initialAllowOut, initialEfforts, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewAdvisories, reviewOverlay, reloadGuard, stagedRouting, holderOf, entry, initialJudge, judgeSeats, initialInput, input, output, debug, ...initialDenyProps }: InitialDenyProps & {
169
194
  ageMs: number | null;
170
195
  adapters: WorkerAdapter[];
171
196
  health: Record<string, AuthHealth>;
@@ -177,9 +202,9 @@ export declare function runFleetInkEditor({ ageMs, adapters, health, initialAllo
177
202
  initialMode: RoutingMode;
178
203
  modeOptions: FleetModeOption[];
179
204
  initialMap: Record<string, MapEntry>;
180
- modePreview: (mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny) => string[];
181
- shapeRows: (mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny) => FleetShapeRow[];
182
- candidatesForShape: (shape: Shape, mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny) => {
205
+ modePreview: (mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny, stage?: FleetStagedMetadata) => string[];
206
+ shapeRows: (mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny, stage?: FleetStagedMetadata) => FleetShapeRow[];
207
+ candidatesForShape: (shape: Shape, mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny, stage?: FleetStagedMetadata) => {
183
208
  rows: FleetCandidateOption[];
184
209
  excludedNote?: string;
185
210
  ledger?: string[];
@@ -193,7 +218,9 @@ export declare function runFleetInkEditor({ ageMs, adapters, health, initialAllo
193
218
  reloadGuard: (bytes: string) => string | null;
194
219
  /** the routing policy the staged deny sets load as (fleet: the config loader over the candidate
195
220
  * bytes); absent ⇒ the four staged sets alone, with no allowlist */
196
- stagedRouting?: (deny: FleetStagedDeny) => FleetStagedRouting;
221
+ stagedRouting?: (deny: FleetStagedDeny, stage?: FleetStagedMetadata) => FleetStagedRouting;
222
+ /** T10: the overlay `path:line` holding a dotted config scope in this repository, undefined = defaults */
223
+ holderOf?: (dotted: string) => string | undefined;
197
224
  /** "presets" = init's entry: Esc in the browser is HOME to the preset overlay, not quit */
198
225
  entry?: "presets" | "probe";
199
226
  initialJudge?: string;
@@ -12,6 +12,12 @@ import { windowRows } from "./components.js";
12
12
  import { wrapCells } from "../cockpit/width.js";
13
13
  import { clip, clipPathTail, ElisionMark, enterAltScreen, fmtCtx, fmtMs, fmtUsdPair, Glyph, INK, inkInput, inkOutput, KeyBar, OverlayPanel, padCell, padCellStart, Pointer, SearchRow, } from "./frame.js";
14
14
  const STEERING_KEYS = ["review", "consult"];
15
+ /** OBS-525: auto reverts the shape's WHOLE routing declaration — pool included, not just the pin.
16
+ * prefer survives: it biases auto routing and is valid without pin/pool. */
17
+ export function autoMapEntry(entry) {
18
+ const { pin: _pin, pool: _pool, ...rest } = entry ?? {};
19
+ return rest;
20
+ }
15
21
  /** every scope's staged list, empty — the "every policy scope open" probe the picker lifts against */
16
22
  export const openStagedDeny = () => ({
17
23
  ...Object.fromEntries(DENY_SCOPES.map((scope) => [stagedDenyKeyOf(scope), []])),
@@ -43,7 +49,7 @@ const REACH_CHOICES = [
43
49
  { id: "out-workers", label: "out · workers", gloss: "workers skip it; judge/review/consult unaffected" },
44
50
  { id: "out-all", label: "out · all seats", gloss: "no seat routes here" },
45
51
  ];
46
- 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 }) {
47
53
  const { exit } = useApp();
48
54
  const [, setRevision] = useState(0);
49
55
  // the props are the fleet command's contract, one per schema-enumerated scope (initialDeny…),
@@ -65,6 +71,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
65
71
  notice: "",
66
72
  lastEdit: null,
67
73
  allowOut: new Set(initialAllowOut),
74
+ seededAllowOut: [],
68
75
  ...Object.fromEntries(DENY_SCOPES.map((scope) => [scope.key, new Set(initialDeny[scope.key])])),
69
76
  classifications: [],
70
77
  efforts: structuredClone(initialEfforts),
@@ -133,12 +140,13 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
133
140
  let routingMemo = null;
134
141
  const stagedPolicy = () => {
135
142
  const deny = stagedDeny();
136
- const key = JSON.stringify(deny);
143
+ const stage = stagedMetadata();
144
+ const key = JSON.stringify([deny, stage]);
137
145
  if (routingMemo?.key !== key) {
138
146
  routingMemo = {
139
147
  key,
140
148
  policy: stagedRouting
141
- ? stagedRouting(deny)
149
+ ? stagedRouting(deny, stage)
142
150
  : { ok: true, routing: { deny: denyBlockFrom(denyLists()) } },
143
151
  };
144
152
  }
@@ -157,7 +165,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
157
165
  const reachFor = (adapter, model, identity) => {
158
166
  const policy = stagedPolicy();
159
167
  if (!policy.ok)
160
- return { reach: "unknown", reasons: [`preview unavailable (${policy.error})`], uncoveredAlias: false };
168
+ return { reach: "unknown", reasons: [`preview unavailable (${policy.error})`], uncoveredAlias: false, held: [] };
161
169
  const channel = { adapter, model, ...(identity !== undefined ? { identity } : {}) };
162
170
  const worker = exclusionsOf(channel, policy.routing, "worker");
163
171
  const allSeats = exclusionsOf(channel, policy.routing, "judge");
@@ -169,6 +177,10 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
169
177
  reach,
170
178
  reasons: worker.map(exclusionReason),
171
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
+ }),
172
184
  };
173
185
  };
174
186
  // OBS-1099 review: a displayed row may fold several discovered gateway ids (foldedModels);
@@ -346,7 +358,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
346
358
  ...Object.fromEntries(DENY_SCOPES.map((scope) => [stagedDenyKeyOf(scope), [...ui[scope.key]].sort()])),
347
359
  allowOut: [...ui.allowOut].sort(),
348
360
  });
349
- const shapeList = () => shapeRows(ui.selectedMode, ui.map, stagedDeny());
361
+ const shapeList = () => shapeRows(ui.selectedMode, ui.map, stagedDeny(), stagedMetadata());
350
362
  const steeringList = () => [
351
363
  ...STEERING_KEYS.map((key) => ({
352
364
  id: key,
@@ -369,9 +381,15 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
369
381
  ...modelGroups.map((_, index) => ({ kind: "adapter", index })),
370
382
  ];
371
383
  // ── state transitions ──────────────────────────────────────────────────────
384
+ // T8: the previews fold the CURRENT staged metadata — never only what the last review saw
385
+ const stagedMetadata = () => {
386
+ const { classifications, efforts, seededAllowOut } = editorState();
387
+ return { classifications, efforts, seededAllowOut };
388
+ };
372
389
  const editorState = () => ({
373
390
  ...denyLists(),
374
391
  allowOut: [...ui.allowOut].sort(),
392
+ seededAllowOut: structuredClone(ui.seededAllowOut),
375
393
  classifications: ui.classifications.map((classification) => classification.vendor && classification.channel
376
394
  ? {
377
395
  ...classification,
@@ -419,9 +437,38 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
419
437
  finish({ kind: "no-changes" });
420
438
  return;
421
439
  }
440
+ if (nextReview.kind === "refused") {
441
+ ui.notice = `w refused — ${nextReview.reason}`;
442
+ bump();
443
+ return;
444
+ }
422
445
  setOverlay({ kind: "review", review: nextReview, scroll: 0 });
423
446
  };
447
+ // T10 (D-925/D-939): a model the current allow form leaves out gets its OWN allow entry staged beside
448
+ // its classification — the ownAllow spelling Space clears — so it can be admitted in this session.
449
+ // Re-staging a classification replaces its seed; a row the allow form admits seeds nothing. A row a
450
+ // flat deny ALSO excludes still seeds, so successive owning lifts (deny, then allow) admit it.
451
+ const seedAllow = (adapter, model, displayModel) => {
452
+ // ponytail: a collapsed variant row's allow entry would name no channel (its display name is not the real
453
+ // model), so it is not seeded for same-session admission — classify, save, then admit next session;
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 } : {}) };
459
+ const policy = stagedPolicy();
460
+ if (!policy.ok || !exclusionsOf(self, policy.routing, "judge").some((scope) => scope.by === "allow"))
461
+ return;
462
+ const entry = selectOwn(ui.allowOut, self) ?? `${adapter}:${displayModel}`;
463
+ ui.allowOut = new Set([...ui.allowOut, entry]);
464
+ ui.seededAllowOut = [
465
+ ...ui.seededAllowOut.filter((seed) => seed.adapter !== adapter || seed.model !== model),
466
+ { adapter, model, displayModel, ...(identity !== undefined ? { identity } : {}), entry },
467
+ ];
468
+ };
424
469
  const stageSuggested = (adapter, rows, firstTouch) => {
470
+ for (const row of rows)
471
+ seedAllow(adapter, row.model, row.displayModel);
425
472
  ui.classifications = [
426
473
  ...ui.classifications,
427
474
  ...rows.map((row) => ({
@@ -476,6 +523,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
476
523
  };
477
524
  const applyClassification = (overlay) => {
478
525
  const answered = overlay.vendor ? ui.channelByAdapter[overlay.adapter] : undefined;
526
+ seedAllow(overlay.adapter, overlay.model, overlay.displayModel ?? overlay.model);
479
527
  ui.classifications = [
480
528
  ...ui.classifications,
481
529
  {
@@ -733,7 +781,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
733
781
  // channel must be OFFERED at or above the floor once every policy scope is open — a failed
734
782
  // model probe (dropped by discovery), a pin or a pool keep it out of that list and a floor
735
783
  // marks it belowFloor, so nothing here lifts it.
736
- const eligible = candidatesForShape(overlay.shape, ui.selectedMode, ui.map, openStagedDeny()).rows
784
+ const eligible = candidatesForShape(overlay.shape, ui.selectedMode, ui.map, openStagedDeny(), stagedMetadata()).rows
737
785
  .some((row) => row.id === id && !row.belowFloor);
738
786
  if (!eligible) {
739
787
  // the ledger line spells its reasons; drop the row label and the reach segment for the notice
@@ -756,7 +804,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
756
804
  const text = `lift: cleared ${found.entry} from ${found.scope}${after.reach === "in" ? "" : ` — still out: ${after.reasons.join("; ")}`}`;
757
805
  ui.lastEdit = { id, text };
758
806
  ui.notice = text;
759
- const picked = candidatesForShape(overlay.shape, ui.selectedMode, ui.map, stagedDeny());
807
+ const picked = candidatesForShape(overlay.shape, ui.selectedMode, ui.map, stagedDeny(), stagedMetadata());
760
808
  ui.overlay = { ...overlay, rows: picked.rows, excludedNote: picked.excludedNote, ledger: picked.ledger ?? [], at: 0 };
761
809
  bump();
762
810
  };
@@ -1540,7 +1588,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
1540
1588
  const group = modelGroups[ui.adapterAt];
1541
1589
  const suggested = rows
1542
1590
  .filter((candidate) => candidate.adapter === group.adapter && !candidate.tier && candidate.suggestion !== undefined)
1543
- .map((candidate) => ({ model: candidate.classifyModel ?? candidate.model, suggestion: candidate.suggestion }));
1591
+ .map((candidate) => ({ model: candidate.classifyModel ?? candidate.model, displayModel: candidate.model, suggestion: candidate.suggestion }));
1544
1592
  if (!suggested.length) {
1545
1593
  ui.notice = "no catalog tier suggestions among the visible unclassified models — evidence comes from the cached catalogs (AA index / API pricing)";
1546
1594
  bump();
@@ -1590,17 +1638,12 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
1590
1638
  if (!shape)
1591
1639
  return;
1592
1640
  if (hotkey === "a") {
1593
- // OBS-525: auto reverts the shape's WHOLE routing declaration — pool included, not just
1594
- // the pin. prefer survives: it biases auto routing and is valid without pin/pool.
1595
- const nextEntry = { ...ui.map[shape] };
1596
- delete nextEntry.pin;
1597
- delete nextEntry.pool;
1598
- ui.map = { ...ui.map, [shape]: nextEntry };
1641
+ ui.map = { ...ui.map, [shape]: autoMapEntry(ui.map[shape]) };
1599
1642
  bump();
1600
1643
  return;
1601
1644
  }
1602
1645
  if (key.return || hotkey === "p") {
1603
- const picked = candidatesForShape(shape, ui.selectedMode, ui.map, stagedDeny());
1646
+ const picked = candidatesForShape(shape, ui.selectedMode, ui.map, stagedDeny(), stagedMetadata());
1604
1647
  setOverlay({
1605
1648
  kind: "candidates",
1606
1649
  shape,
@@ -1677,7 +1720,8 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
1677
1720
  const reachDetail = (row) => {
1678
1721
  if (row.evidence?.unauthed !== undefined)
1679
1722
  return "unauthed — re-probe with tickmarkr doctor (Space does not toggle reach)";
1680
- const reasons = row.reasons.join("; ");
1723
+ // T10: the reasons, then where each scope is held — the overlay a reviewed w writes
1724
+ const reasons = `${row.reasons.join("; ")}${row.held.length ? ` — held: ${row.held.join("; ")}` : ""}`;
1681
1725
  if (row.reach === "out-all")
1682
1726
  return `reach: out · all seats — ${reasons} — Space picks in or out · workers`;
1683
1727
  if (row.reach === "out-allow")
@@ -1791,7 +1835,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
1791
1835
  if (!overlay)
1792
1836
  return null;
1793
1837
  if (overlay.kind === "presets") {
1794
- const details = modeOptions[overlay.at] ? modePreview(modeOptions[overlay.at].id, ui.map, stagedDeny()) : [];
1838
+ const details = modeOptions[overlay.at] ? modePreview(modeOptions[overlay.at].id, ui.map, stagedDeny(), stagedMetadata()) : [];
1795
1839
  return (_jsxs(OverlayPanel, { title: overlay.home ? "routing preset" : "routing mode", width: bodyW, children: [_jsx(Text, { dimColor: true, children: overlay.home ? "a preset routes every shape — custom opens the browser" : "floors move with the mode; the browser edits the rest" }), _jsx(Text, { children: " " }), modeOptions.map((option, index) => (_jsxs(Text, { children: [_jsx(Pointer, { on: overlay.at === index }), option.id === ui.selectedMode ? _jsx(Glyph, { kind: "on" }) : _jsx(Text, { children: " " }), _jsx(Text, { bold: overlay.at === index, children: ` ${padCell(option.id, 12)}` }), _jsx(Text, { dimColor: true, children: clip(option.gloss, bodyW - 20) })] }, option.id))), overlay.home && (_jsxs(Text, { children: [_jsx(Pointer, { on: overlay.at === modeOptions.length }), _jsx(Text, { children: " " }), _jsx(Text, { bold: overlay.at === modeOptions.length, children: ` ${padCell("custom", 12)}` }), _jsx(Text, { dimColor: true, children: clip("open the fleet browser (models · shapes · steering)", bodyW - 20) })] })), _jsx(Text, { children: " " }), details.map((line) => _jsx(Text, { dimColor: true, children: line }, line))] }));
1796
1840
  }
1797
1841
  if (overlay.kind === "review") {
@@ -1936,8 +1980,8 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
1936
1980
  lines.push(coveringDetail(row));
1937
1981
  if (!row.tier) {
1938
1982
  lines.push(row.suggestion
1939
- ? `catalog suggests ${row.suggestion.tier} — Space/Enter classifies with the note pre-typed · s stages every visible suggestion`
1940
- : "unclassified — Space/Enter classifies; unclassified models are never routed");
1983
+ ? `catalog suggests ${row.suggestion.tier} — Space/Enter classifies with the note pre-typed (t too) · s stages every visible suggestion`
1984
+ : "unclassified — Space/Enter classifies (t too), then Space sets its reach in this session; unclassified models are never routed");
1941
1985
  }
1942
1986
  }
1943
1987
  return lines;
@@ -2054,7 +2098,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
2054
2098
  ? _jsx(Text, { color: INK.warn, children: ` ! ${clip(ui.notice, width - 4)}` })
2055
2099
  : details.map((line) => _jsx(Text, { dimColor: true, children: ` ${clip(line, width - 2)}` }, line)), _jsx(KeyBar, { binds: keyBinds() })] }));
2056
2100
  }
2057
- 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 }) {
2101
+ 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 }) {
2058
2102
  const agents = adapters.flatMap((adapter) => {
2059
2103
  const state = health[adapter.id];
2060
2104
  if (!state?.installed)
@@ -2078,7 +2122,7 @@ export async function runFleetInkEditor({ ageMs, adapters, health, initialAllowO
2078
2122
  // rebuilt from output.columns/rows so a pane resize re-renders at the new size. Same root
2079
2123
  // element type on every rerender ⇒ React keeps the component instance, so staged edits,
2080
2124
  // cursor, and overlays all survive the resize.
2081
- 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,
2125
+ 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,
2082
2126
  // v1.90.9: cap every list to the terminal minus chrome so the cursor and header can
2083
2127
  // never scroll off-screen (omp's 218-model list did exactly that pre-cap).
2084
2128
  // 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.4",
3
+ "version": "2.6.6",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -12,7 +12,7 @@ Use the canonical loop skill's [cockpit, parked decisions and printed twins](../
12
12
  for each target: delivered views are **1 Home, 4 Run, 5 Evidence**; Fleet/Bootstrap and
13
13
  Plan/Health remain follow-ons with CLI entries. The recorded 1/3 merged, human T2, blocked
14
14
  T3 case stays PARTIAL despite tip pass. Review the Run confirmation, read back the appended
15
- `task-approved` receipt, and explicitly `tickmarkr resume <runId>` when no live owner exists;
15
+ `task-approved` receipt, and explicitly launch `tickmarkr resume <runId>` with the host-owned split form below when no live owner exists;
16
16
  approval records permission and never dispatches work itself. Non-TTY readers keep `status`,
17
17
  `report` and default-watch line output, with `--watch --plain` as the TTY fallback.
18
18
  Keep `name: tickmarkr-auto` and the installed links to canonical `skills/` intact.
@@ -92,7 +92,37 @@ After sending, **confirm delivery** by reading the target pane and verifying the
92
92
  1. **Prepare** — confirm the target list. Run the [binary preflight](#binary-preflight-before-compile-or-run). Check `git status`, confirm no tickmarkr run is active, and work from a non-main branch.
93
93
  2. **Compile** — run `tickmarkr compile <spec-or-directory>`. Fix source-spec defects instead of editing the generated graph.
94
94
  3. **Plan** — run `tickmarkr plan`. Review routes, capability-floor warnings, and human gates before execution.
95
- 4. **Run** — run `tickmarkr run`. A watch ending the seat's turn is no watch: keep a **blocking journal consumer** alive for the run's terminal events — the shipped watcher below, or a foreground `until grep` on the run's terminal events — and ensure it is re-armed at most every twenty minutes. Never rely on a `Monitor`-only wake. Watch the run journal rather than polling agents, using the shipped watcher — `.claude/skills/tickmarkr-overseer/scripts/watch-journal.sh <state-dir>/runs 20 28800` — which takes a line baseline at arm time, then wakes ONCE on `run-end`, `task-human`, `task-failed` or `consult-verdict` and grades the run-end summary against every execution clause for you; the debt clause is yours — read CURRENT `tickmarkr status <runId>` (step 5). Re-arm after every wake. ⛔ Never `tail -F | grep -m1` (run-end is the journal's last line, so tail never notices the broken pipe and the watcher hangs forever) and never a pane-level done wait (it fires on every agent turn end, not mission end). ⚠ A bare whole-file `grep -q '"event":"run-end"'` is the trap the watcher exists to avoid: on a resume it matches the PREVIOUS run's run-end and returns instantly, so a re-armed watcher reads as coverage that does not exist. Resolve blocked interactions in the relevant agent session.
95
+ 4. **Run** — launch `tickmarkr run` using the [host-owned split form](#host-owned-daemon-and-detached-beats), and start detached beats with their run-end stop items. A watch ending the seat's turn is no watch: keep a **blocking journal consumer** alive for the run's terminal events — the shipped watcher below, or a foreground `until grep` on the run's terminal events — and ensure it is re-armed at most every twenty minutes. Never rely on a `Monitor`-only wake. Watch the run journal rather than polling agents, using the shipped watcher — `.claude/skills/tickmarkr-overseer/scripts/watch-journal.sh <state-dir>/runs 20 28800` — which takes a line baseline at arm time, then wakes ONCE on `run-end`, `task-human`, `task-failed` or `consult-verdict` and grades the run-end summary against every execution clause for you; the debt clause is yours — read CURRENT `tickmarkr status <runId>` (step 5). Re-arm after every wake. ⛔ Never `tail -F | grep -m1` (run-end is the journal's last line, so tail never notices the broken pipe and the watcher hangs forever) and never a pane-level done wait (it fires on every agent turn end, not mission end). ⚠ A bare whole-file `grep -q '"event":"run-end"'` is the trap the watcher exists to avoid: on a resume it matches the PREVIOUS run's run-end and returns instantly, so a re-armed watcher reads as coverage that does not exist. Resolve blocked interactions in the relevant agent session.
96
96
  5. **Verify and consolidate** — continue only after a green run. A run is green when the run-end event exists in the journal, the tip verify is not "failed", the summary's `failed`, `human`, `blocked` and `pending` buckets are all empty, and CURRENT `tickmarkr status <runId>` reads its owed checks outstanding empty AND known (`outstanding 0`) — a run with a parked task is partial, not green. Empty execution buckets alone are not green either (D-660): every bucket empty and the tip passed, but an operator waived one review, leaves one accepted-risk review check owed — status reads `outstanding 1 (T7 review)` and `run`/`resume` still exit 0 on execution alone, so the run is execution complete, not green. It turns green only when a `tickmarkr verify --record <runId>` discharge moves CURRENT status to `outstanding 0`, including a discharge landing after run-end — the historical run-end record keeps the old count, so never read debt from it; `outstanding unknown` is never green. Tickmarkr consolidates accepted work on `tickmarkr/<runId>` and never signs off to the main branch. A human controls any later release merge.
97
97
  6. **Record** — `tickmarkr report <runId> --md` prints Markdown to stdout; redirect explicitly beside the spec (for example `tickmarkr report <runId> --md > feature.record.md`) and commit the execution record when the repository tracks those records.
98
98
  7. **Continue** — move to the next requested target. If a target fails or is parked, stop with the journal evidence rather than silently skipping it.
99
+
100
+ ## Host-owned daemon and detached beats
101
+
102
+ Launch the daemon as a SPLIT of the orchestrator's own pane in the ORCH tab, owned by the
103
+ host PTY. Never launch it as an agent harness background task or in a separate tab. The daemon
104
+ self-places its board from that split, keeping both daemon and board in the ORCH tab.
105
+
106
+ - **On Orca (`TERM_PROGRAM=Orca` and non-empty `ORCA_TERMINAL_HANDLE`)**:
107
+ `orca terminal split --terminal "$ORCA_TERMINAL_HANDLE" --direction vertical --command "tickmarkr run"`.
108
+ - **On herdr (`HERDR_ENV=1`)**: use `herdr pane split` on the orchestrator's own pane, then
109
+ `herdr pane run <new> "tickmarkr run"` in the returned split.
110
+
111
+ For `resume`, use the same host-owned split form with `tickmarkr resume <runId>` in place of
112
+ `tickmarkr run`. After either launch, read this repository's lock pid and walk its ppid chain
113
+ in the process table to the host PTY; verify no agent session is an ancestor. If an agent is
114
+ an ancestor, stop that launch and relaunch through the host split before continuing.
115
+
116
+ Start `tickmarkr beat <tier> --seat <seat> --loop` as a DETACHED background process, one per
117
+ (tier, seat), from the repository root. Use setsid plus nohup (or the host's equivalent detached
118
+ session facility), redirect output to a file, and record a pidfile under the repository's state
119
+ dir. Verify ppid 1 and no agent-session ancestor. Never put beats in a harness background task
120
+ or a visible tab. The lifecycle must be idempotent: start checks the pidfile against the live
121
+ process payload and returns an "already running" no-op for the same (repo, tier, seat); status
122
+ checks that payload and beat freshness; stop sends `--stand-down` for the same tier and seat,
123
+ verifies the loop exits within one interval, then removes its pidfile. A stale or reused pid
124
+ must never be killed without matching its live payload. Include a run-end stop item for every
125
+ beat started, also on failure, park or handoff; verify DISARMED and remove retired beat files.
126
+
127
+ Keep only watchers that must WAKE the seat in the harness. Re-arm them on each wake and at
128
+ their harness cap; daemon and beat lifetimes must not depend on that cap.
@@ -88,10 +88,40 @@ When spawning consultants (agents gathering synthesis input for decisions like S
88
88
  1. **Prepare** — start from the requested spec. Run the [binary preflight](#binary-preflight-before-compile-or-run). Check `git status`, confirm no tickmarkr run is active, and work from a non-main branch.
89
89
  2. **Compile** — run `tickmarkr compile <spec>`. Correct compilation errors in the spec, never in the generated graph.
90
90
  3. **Plan** — run `tickmarkr plan`. Review the routing table, capability-floor warnings, and every human gate, including work that each gate blocks.
91
- 4. **Run** — run `tickmarkr run`. A watch ending the seat's turn is no watch: keep a **blocking journal consumer** alive for the run's terminal events — the shipped watcher below, or a foreground `until grep` on the run's terminal events — and ensure it is re-armed at most every twenty minutes. Never rely on a `Monitor`-only wake. Watch the run journal rather than polling agents, using the shipped watcher — `.claude/skills/tickmarkr-overseer/scripts/watch-journal.sh <state-dir>/runs 20 28800` — which takes a line baseline at arm time, then wakes ONCE on `run-end`, `task-human`, `task-failed` or `consult-verdict` and grades the run-end summary against every execution clause for you; the debt clause is yours — read CURRENT `tickmarkr status <runId>` (step 5). Re-arm after every wake. ⛔ Never `tail -F | grep -m1` (run-end is the journal's last line, so tail never notices the broken pipe and the watcher hangs forever) and never a pane-level done wait (it fires on every agent turn end, not mission end). ⚠ A bare whole-file `grep -q '"event":"run-end"'` is the trap the watcher exists to avoid: on a resume it matches the PREVIOUS run's run-end and returns instantly, so a re-armed watcher reads as coverage that does not exist. Resolve blocked interactions in the agent session; do not turn them into proxy questions.
91
+ 4. **Run** — launch `tickmarkr run` using the [host-owned split form](#host-owned-daemon-and-detached-beats), and start detached beats with their run-end stop items. A watch ending the seat's turn is no watch: keep a **blocking journal consumer** alive for the run's terminal events — the shipped watcher below, or a foreground `until grep` on the run's terminal events — and ensure it is re-armed at most every twenty minutes. Never rely on a `Monitor`-only wake. Watch the run journal rather than polling agents, using the shipped watcher — `.claude/skills/tickmarkr-overseer/scripts/watch-journal.sh <state-dir>/runs 20 28800` — which takes a line baseline at arm time, then wakes ONCE on `run-end`, `task-human`, `task-failed` or `consult-verdict` and grades the run-end summary against every execution clause for you; the debt clause is yours — read CURRENT `tickmarkr status <runId>` (step 5). Re-arm after every wake. ⛔ Never `tail -F | grep -m1` (run-end is the journal's last line, so tail never notices the broken pipe and the watcher hangs forever) and never a pane-level done wait (it fires on every agent turn end, not mission end). ⚠ A bare whole-file `grep -q '"event":"run-end"'` is the trap the watcher exists to avoid: on a resume it matches the PREVIOUS run's run-end and returns instantly, so a re-armed watcher reads as coverage that does not exist. Resolve blocked interactions in the agent session; do not turn them into proxy questions.
92
92
  5. **Verify and consolidate** — accept only a green run. A run is green when the run-end event exists in the journal, the tip verify is not "failed", the summary's `failed`, `human`, `blocked` and `pending` buckets are all empty, and CURRENT `tickmarkr status <runId>` reads its owed checks outstanding empty AND known (`outstanding 0`) — a run with a parked task is partial, not green. Empty execution buckets alone are not green either (D-660): every bucket empty and the tip passed, but an operator waived one review, leaves one accepted-risk review check owed — status reads `outstanding 1 (T7 review)` and `run`/`resume` still exit 0 on execution alone, so the run is execution complete, not green. It turns green only when a `tickmarkr verify --record <runId>` discharge moves CURRENT status to `outstanding 0`, including a discharge landing after run-end — the historical run-end record keeps the old count, so never read debt from it; `outstanding unknown` is never green. Tickmarkr consolidates accepted task work on `tickmarkr/<runId>`; it never signs off to the main branch. A human may later merge that integration branch through the repository's normal release process.
93
93
  6. **Record** — `tickmarkr report <runId> --md` prints Markdown to stdout. Redirect it explicitly beside the source spec (for example `tickmarkr report <runId> --md > feature.record.md`) and commit the execution record when the repository tracks those records. Then [stand down](#stand-down-mission-end-and-retirement).
94
94
 
95
+ ## Host-owned daemon and detached beats
96
+
97
+ Launch the daemon as a SPLIT of the orchestrator's own pane in the ORCH tab, owned by the
98
+ host PTY. Never launch it as an agent harness background task or in a separate tab. The daemon
99
+ self-places its board from that split, keeping both daemon and board in the ORCH tab.
100
+
101
+ - **On Orca (`TERM_PROGRAM=Orca` and non-empty `ORCA_TERMINAL_HANDLE`)**:
102
+ `orca terminal split --terminal "$ORCA_TERMINAL_HANDLE" --direction vertical --command "tickmarkr run"`.
103
+ - **On herdr (`HERDR_ENV=1`)**: use `herdr pane split` on the orchestrator's own pane, then
104
+ `herdr pane run <new> "tickmarkr run"` in the returned split.
105
+
106
+ For `resume`, use the same host-owned split form with `tickmarkr resume <runId>` in place of
107
+ `tickmarkr run`. After either launch, read this repository's lock pid and walk its ppid chain
108
+ in the process table to the host PTY; verify no agent session is an ancestor. If an agent is
109
+ an ancestor, stop that launch and relaunch through the host split before continuing.
110
+
111
+ Start `tickmarkr beat <tier> --seat <seat> --loop` as a DETACHED background process, one per
112
+ (tier, seat), from the repository root. Use setsid plus nohup (or the host's equivalent detached
113
+ session facility), redirect output to a file, and record a pidfile under the repository's state
114
+ dir. Verify ppid 1 and no agent-session ancestor. Never put beats in a harness background task
115
+ or a visible tab. The lifecycle must be idempotent: start checks the pidfile against the live
116
+ process payload and returns an "already running" no-op for the same (repo, tier, seat); status
117
+ checks that payload and beat freshness; stop sends `--stand-down` for the same tier and seat,
118
+ verifies the loop exits within one interval, then removes its pidfile. A stale or reused pid
119
+ must never be killed without matching its live payload. Include a run-end stop item for every
120
+ beat started, also on failure, park or handoff; verify DISARMED and remove retired beat files.
121
+
122
+ Keep only watchers that must WAKE the seat in the harness. Re-arm them on each wake and at
123
+ their harness cap; daemon and beat lifetimes must not depend on that cap.
124
+
95
125
  ## Cockpit, parked decisions and printed twins
96
126
 
97
127
  The shipped cockpit is **1 Home, 4 Run, 5 Evidence**: `tickmarkr ui [runId]` defaults to
@@ -107,7 +137,7 @@ run/task, original park `#L`, actor/reason, exact argv, consequence and enactor.
107
137
  confirms; `n`/Esc cancel; Enter never confirms. Read the receipt's appended `task-approved`
108
138
  line and actor/reason back from the journal. Approval records permission, never dispatch,
109
139
  a passed gate or task completion. With no live owner it says **approved; resume required**:
110
- exit the observer and run `tickmarkr resume <runId>` explicitly. A matching live daemon
140
+ exit the observer and launch `tickmarkr resume <runId>` explicitly with the same host-owned split form above. A matching live daemon
111
141
  enacts at its next task boundary; if a different live run owns the repository lock, wait
112
142
  for that run to end before resuming this one.
113
143
 
@@ -177,6 +207,20 @@ repair and catalog refresh; default doctor and `--fix` still probe. `scope <inte
177
207
  --preview` performs no probes, model calls or writes; authoring requires confirmation or
178
208
  `--yes`. These actions remain CLI entries until their follow-on views ship.
179
209
 
210
+ **macOS host failure: a launchservicesd restart (D-787/D-789).** If GUI apps suddenly read "The
211
+ application … is not open anymore" during a local suite, launchservicesd crashed and respawned with an
212
+ empty app registry. That was measured on 2026-09-30: `0x1000600b` Mach port exhaustion under node
213
+ `process.title` check-ins, crash at 17:15:00Z and respawn at 17:15:09Z. `tickmarkr doctor` and standalone
214
+ `tickmarkr verify` record the daemon's pid/start through the separate `src/run/launchservices-check.ts`
215
+ module: one bounded `/bin/ps` read with a 5000 ms kill ceiling. doctor's wiring is five lines in
216
+ `src/cli/commands/doctor.ts`. The identity lives on the first `doctor.json` record, and one advisory
217
+ prints and persists when it changed. Unknown evidence keeps the prior identity, and verdicts and retries
218
+ never change. Confirm the identity with `/bin/ps -o pid=,lstart= -p "$(pgrep -x launchservicesd)"`: it
219
+ is young beside Dock and Finder. Confirm the log with `/usr/bin/log show --start '<that lstart minus two
220
+ minutes>'`, looking for `0x1000600b` then `Successfully spawned launchservicesd`. Type the full path: a bare
221
+ `log` is a zsh builtin that prints nothing. Then quit and relaunch the affected apps, or log out /
222
+ restart once the run is stood down; a restart ends agent sessions and wipes `/private/tmp`.
223
+
180
224
  Keep this skill's canonical identity `tickmarkr-loop`; sibling skills link to this
181
225
  walkthrough. In this repository `skills/` is canonical and installed `.claude/skills/`
182
226
  files resolve to it; change the canonical source, never create a second help skill.