tickmarkr 1.90.7 → 1.91.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -362,21 +362,20 @@ export async function init(argv, cwd = process.cwd(), io = {}) {
362
362
  const interactive = input.isTTY === true && output.isTTY === true && !(values.yes ?? false);
363
363
  // Act 1 — preferences. Only when the repo config does not exist yet: init never rewrites an
364
364
  // operator's config; re-tuning is `tickmarkr fleet` (act 3 below still runs and writes overlays).
365
- let wizardQuit = false;
366
365
  if (!repoConfigExists) {
367
366
  if (interactive) {
368
367
  emitBanner();
369
368
  const wizard = await runInitWizard(cwd, input, output);
370
369
  if (wizard === null) {
371
- wizardQuit = true;
372
- notes.push("wizard quit — no repo config written; re-run tickmarkr init (or tickmarkr fleet after a manual config)");
370
+ // Operator field report 2026-08-13: quit means QUIT — no probe, no report wall, no fleet
371
+ // act, one line out. Scaffolding above already happened (spec/global config), and saying
372
+ // so costs one clause; everything else stops here. Esc and ctrl+c both land here.
373
+ return `init: wizard quit — nothing further run (scaffolding kept: ${notes.filter((n) => n.startsWith("wrote")).length} file(s) written above). Re-run tickmarkr init to continue.`;
373
374
  }
374
- else {
375
- writeFileSync(repoConfigPath, configTemplate(wizard.overlay));
376
- notes.push(`wrote ${repoConfigPath}`);
377
- if (wizard.installSkills) {
378
- await installAgentFiles(cwd, values.force ?? false, wizard.installDocs || (values.docs ?? false), notes);
379
- }
375
+ writeFileSync(repoConfigPath, configTemplate(wizard.overlay));
376
+ notes.push(`wrote ${repoConfigPath}`);
377
+ if (wizard.installSkills) {
378
+ await installAgentFiles(cwd, values.force ?? false, wizard.installDocs || (values.docs ?? false), notes);
380
379
  }
381
380
  }
382
381
  else {
@@ -386,17 +385,20 @@ export async function init(argv, cwd = process.cwd(), io = {}) {
386
385
  }
387
386
  // Act 2 — discovery. Doctor is the sensor (fleet never re-probes); runs after preferences so the
388
387
  // probe reads the config the operator just chose. Reuse keeps init fast; --fresh forces the probe.
388
+ // Interactive journeys get the COMPACT surface (status rows + pointer) — the full matrix between
389
+ // two TUIs was the operator's clutter report; non-TTY keeps the complete machine surface.
389
390
  const fresh = values.fresh ?? false;
390
391
  const { reuse, ageMs, health } = initDoctorReuse(cwd, fresh);
391
392
  let doc = reuse && health && ageMs !== null
392
- ? `using probe results from ${formatDoctorAgeForInit(ageMs)} ago — run tickmarkr doctor to refresh (or init --fresh)\n${formatDoctorReport(cwd, loadConfig(cwd), health, allAdapters(), { wrote: false })}`
393
- : await doctor([], cwd, undefined, { banner: false });
394
- // Act 3 — fleet. Interactive only, and never after a wizard quit ("stop" means stop). The
395
- // discovery report prints BETWEEN the acts so the operator reads what the presets screen
396
- // ranks with; the final summary then carries a pointer instead of the same bytes twice.
397
- if (interactive && !wizardQuit) {
393
+ ? interactive
394
+ ? `using probe results from ${formatDoctorAgeForInit(ageMs)} ago — full matrix: tickmarkr doctor · refresh: init --fresh`
395
+ : `using probe results from ${formatDoctorAgeForInit(ageMs)} ago — run tickmarkr doctor to refresh (or init --fresh)\n${formatDoctorReport(cwd, loadConfig(cwd), health, allAdapters(), { wrote: false })}`
396
+ : await doctor([], cwd, undefined, { banner: false, compact: interactive });
397
+ // Act 3 — fleet. The compact discovery surface prints BETWEEN the acts so the operator reads
398
+ // what the presets screen ranks with; the final summary then carries a pointer, not a repeat.
399
+ if (interactive) {
398
400
  output.write(`${doc}\n`);
399
- doc = "discovery report shown above — re-print any time with `tickmarkr doctor`";
401
+ doc = "discovery shown above — full matrix any time with `tickmarkr doctor`";
400
402
  // Same lazy-Ink seam as fleet.ts: load the editor runtime up front so the assembler's
401
403
  // returned props need no startup-input capture window on this path.
402
404
  const { runFleetInkEditor } = await import("../../tui/ink/fleet-app.js");
@@ -697,10 +697,11 @@ acceptance is required on every task (a nested list of observable outcomes).
697
697
  WHAT MAKES A CRITERION REAL:
698
698
  - "test:" must name a real test asserting on recorded state, journal lines, or drawn frames, and its
699
699
  title must match the criterion string verbatim. It also needs a collectable test path in files[].
700
- - A "test:" oracle must name a TOP-LEVEL test case, never one nested under describe(). Vitest prefixes
701
- nested cases with every suite title, while the acceptance gate builds an anchored pattern that
702
- vitest -t applies against the FULL runner-visible name; the verbatim leaf title then selects ZERO
703
- tests and the gate parks even though the suite is green. Measured: two parks in one run.
700
+ - A "test:" oracle's criterion must be the test's OWN title — the leaf — verbatim. Nesting under
701
+ describe() is allowed (OBS-511: the gate matches the criterion as the trailing segment of the
702
+ runner-visible full name), but the leaf title itself must equal the criterion: a shortened,
703
+ decorated, or paraphrased leaf selects ZERO tests and the gate parks even though the suite is green.
704
+ Measured before the widening: two parks across two runs, one full attempt lost in each.
704
705
  - NO criterion may be satisfiable by an absence, a rename, a source-text grep, or an empty collection.
705
706
  "no file references X" is not a criterion — it passes in a repo where the feature was never built.
706
707
  - "goal:" is NEVER verification. Prose in the goal enforces nothing: every obligation needs an
@@ -705,6 +705,11 @@ export function configTemplate(overlay) {
705
705
  # tiers: # model → capability band; extend when new models ship
706
706
  # claude-code: { vendor: anthropic, channel: sub, models: { fable: frontier } }
707
707
  # codex: { models: { gpt-5.2-codex: null } } # null = tombstone: removes a stale seed id
708
+ # tier evidence: \`tickmarkr doctor --refresh-catalog\` fetches the catalogs behind tier suggestions.
709
+ # models.dev and LiveBench need no key. The Artificial Analysis leg runs ONLY when the environment
710
+ # carries ARTIFICIAL_ANALYSIS_API_KEY (free key: artificialanalysis.ai) — unset, AA contributes
711
+ # nothing and suggestions fall back to LiveBench's agentic-coding rank, then price. Not a config
712
+ # key: an API credential belongs in the environment, never in a tracked overlay.
708
713
  # visibility:
709
714
  # llm: headless # headless (default): judge/review/consult run silently | pane: visible agents
710
715
  # keepPanes: run # run (default): ephemeral judge/review/consult panes close when read; a merged task's worker pane closes on done; other worker panes persist until run end | attempt | forever (keep everything for debugging)
@@ -1,5 +1,5 @@
1
1
  // Fleet-overlay mutation, serialization, and diff rendering for the `tickmarkr fleet` write path.
2
- import { isMap, isScalar, isSeq, parseDocument, stringify } from "yaml";
2
+ import { isMap, isScalar, isSeq, parseDocument, stringify, visit } from "yaml";
3
3
  /** Fleet-owned overlay keys — the only config surface `tickmarkr fleet` may write. */
4
4
  export const FLEET_OVERLAY_KEYS = ["routing", "tiers"];
5
5
  function fleetSubset(obj) {
@@ -18,6 +18,9 @@ function sortedUnique(xs) {
18
18
  // private provenance envelope until this module writes the YAML. The envelope never reaches disk.
19
19
  const FIRST_TOUCH_OPEN = "\uE000tickmarkr-fleet-first-touch:";
20
20
  const FIRST_TOUCH_CLOSE = "\uE001";
21
+ // OBS-505: marks a scalar-trailing single-line comment for the two-space inline note style at
22
+ // stringify time; applied and consumed inside renderFleetOverlayWrite, never written to disk.
23
+ const INLINE_COMMENT_SENTINEL = "\uE002";
21
24
  export function fleetFirstTouchProvenance(note, firstTouch) {
22
25
  return `${FIRST_TOUCH_OPEN}${encodeURIComponent(firstTouch.vendor)}:${firstTouch.channel}${FIRST_TOUCH_CLOSE}${note}`;
23
26
  }
@@ -189,10 +192,23 @@ export function renderFleetOverlayWrite(priorBytes, write) {
189
192
  }
190
193
  }
191
194
  }
192
- // Fleet's established note style uses two spaces before `#`; the Document remains the sole
193
- // comment writer while preserving that byte-level convention for old and newly-authored notes.
195
+ // OBS-505: fleet's two-space note style (`tier: mid # note`) applies to INLINE comments only.
196
+ // The previous commentString prefixed " #" onto EVERY comment line, so block comments — the
197
+ // whole init scaffold at column 0, and indented operator essays — each gained a stray leading
198
+ // space, turning a one-key write into a whole-file diff on the one confirmation surface an
199
+ // operator reviews. commentString has no position context, but this writer owns the document:
200
+ // scalar-trailing single-line comments (the only inline form fleet emits) are marked with a
201
+ // private-use sentinel (the FIRST_TOUCH envelope precedent above), everything else renders
202
+ // byte-identical to yaml's own stringifyComment.
203
+ visit(doc, (_key, node) => {
204
+ if (isScalar(node) && typeof node.comment === "string" && !node.comment.includes("\n")) {
205
+ node.comment = `${INLINE_COMMENT_SENTINEL}${node.comment}`;
206
+ }
207
+ });
194
208
  return doc.toString({
195
- commentString: (comment) => comment.replace(/^(?!$)(?: $)?/gm, " #"),
209
+ commentString: (comment) => comment.startsWith(INLINE_COMMENT_SENTINEL)
210
+ ? ` #${comment.slice(1)}`
211
+ : comment.replace(/^(?!$)(?: $)?/gm, "#"),
196
212
  });
197
213
  }
198
214
  /** Build the repo overlay fragment fleet would write for edits since session start. */
@@ -75,10 +75,17 @@ function escapeRegExp(s) {
75
75
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
76
76
  }
77
77
  // v1.89 T6: Vitest applies -t to the runner-visible full name (enclosing describe titles and the test
78
- // title, space-joined). Escaping preserves literal criteria; anchoring makes equality, rather than
79
- // substring containment, the named-test contract.
78
+ // title, space-joined). Escaping preserves literal criteria; the leaf anchor makes trailing-title
79
+ // equality, rather than substring containment, the named-test contract.
80
+ // OBS-511 (runs 1013+1027, one full attempt lost in EACH): two independent workers nested the verbatim
81
+ // criterion under a describe, the old `^…$` full-name equality matched zero tests, and the gate parked a
82
+ // green suite. `(^| )` accepts any describe prefix while keeping the criterion as the complete trailing
83
+ // segment — equality still matches (empty prefix), and a title merely CONTAINING the criterion still
84
+ // selects zero. ponytail: a leaf title that itself ENDS with " <criterion>" also matches — vitest -t sees
85
+ // only the space-joined full name, so true leaf-boundary precision needs the runner's --list JSON
86
+ // (auditAcceptanceCorpus has it at doctor time); upgrade there if this window is ever exploited.
80
87
  export function testFilterPattern(name) {
81
- return `^${escapeRegExp(name)}$`;
88
+ return `(^| )${escapeRegExp(name)}$`;
82
89
  }
83
90
  // OBS-55: when the base command already contains `--`, append -t after forwarded args — a second `--`
84
91
  // makes vitest treat -t as a positional file filter and the name filter is dropped.
@@ -126,8 +133,10 @@ export function auditAcceptanceCorpus(corpusRoot, listedTests) {
126
133
  ? {
127
134
  namedTest: {
128
135
  criterion: item.test,
136
+ // OBS-511: mirror the gate's leaf-anchored suffix rule — the audit's denominator
137
+ // must count exactly the tests the -t filter would select, or doctor and gate disagree.
129
138
  matches: runnerNames
130
- .filter(({ fullName }) => fullName === item.test)
139
+ .filter(({ fullName }) => fullName === item.test || fullName.endsWith(` ${item.test}`))
131
140
  .map(({ listed }) => listed),
132
141
  },
133
142
  }
@@ -1944,7 +1944,7 @@ export async function runDaemon(repoRoot, opts = {}) {
1944
1944
  const promptFile = writePrompt(journal.dir, t, attempt, feedback, nonce);
1945
1945
  // OBS-56: state the non-interactive, one-pass finish contract and the OBS-54 stall budget in every
1946
1946
  // worker prompt, not only consult retry guidance. Prepended so prompt.ts's completion trailer stays last.
1947
- const workerContract = `## Harness contract\n- This harness is non-interactive: make one continuous pass; do not stop for questions or follow-up input.\n- You have a ${taskTimeoutMinutes} minute stall window. Budget the full suite once, then commit and emit the completion trailer before it expires.\n- Each test: acceptance criterion must exist as a vitest test whose title matches the criterion string verbatim.`; // OBS-64
1947
+ const workerContract = `## Harness contract\n- This harness is non-interactive: make one continuous pass; do not stop for questions or follow-up input.\n- You have a ${taskTimeoutMinutes} minute stall window. Budget the full suite once, then commit and emit the completion trailer before it expires.\n- Each test: acceptance criterion must exist as a vitest test whose OWN title (the leaf, not counting enclosing describe titles) is the criterion string verbatim — never shortened, never decorated. Nesting under describe() is allowed.`; // OBS-64; OBS-511: leaf-title rule stated where the worker reads it
1948
1948
  // OBS-47: state the worktree layout contract in the worker prompt (cheap-tier workers were
1949
1949
  // committing/deleting node_modules and tripping the scope gate). The harness re-asserts the link
1950
1950
  // itself before gates regardless of what the worker does with it.
@@ -9,7 +9,20 @@ export declare function TextLines({ lines }: {
9
9
  export declare function ToggleMark({ active }: {
10
10
  active: boolean;
11
11
  }): import("react").JSX.Element;
12
- export declare function FleetListScreen({ title, legend, rows, cursor, details, filter, }: {
12
+ /**
13
+ * v1.90.9 (operator field report: "can't choose models for omp"): a list longer than the
14
+ * terminal rendered EVERY row, so the title, legend, and cursor scrolled off the top — omp's
15
+ * 218 unclassified models left the operator staring at a tail of unselectable rows. The window
16
+ * is cursor-centered and clamped; the markers say exactly what is elided and that the list is
17
+ * still fully reachable (arrows walk it, type-to-search narrows it).
18
+ */
19
+ export declare function windowRows<T>(rows: T[], cursor: number, capacity: number): {
20
+ visible: T[];
21
+ start: number;
22
+ above: number;
23
+ below: number;
24
+ };
25
+ export declare function FleetListScreen({ title, legend, rows, cursor, details, filter, viewRows, }: {
13
26
  title: string;
14
27
  legend: string;
15
28
  rows: FleetListRow[];
@@ -17,6 +30,8 @@ export declare function FleetListScreen({ title, legend, rows, cursor, details,
17
30
  details?: string[];
18
31
  /** active type-to-search string; rendered on the legend line so the operator sees the narrowing */
19
32
  filter?: string;
33
+ /** list viewport capacity; rows beyond it window around the cursor with elision markers */
34
+ viewRows?: number;
20
35
  }): import("react").JSX.Element;
21
36
  export declare function FleetReviewScreen({ title, legend, diff, }: {
22
37
  title: string;
@@ -17,8 +17,22 @@ export function ToggleMark({ active }) {
17
17
  ? _jsx(Text, { color: "ansi256(41)", children: GLYPHS.toggleActive })
18
18
  : _jsx(Text, { dimColor: true, children: GLYPHS.toggleInactive });
19
19
  }
20
- export function FleetListScreen({ title, legend, rows, cursor, details = [], filter, }) {
21
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: title }), _jsx(Text, { dimColor: true, children: filter ? `${legend} · search: ${filter}` : legend }), rows.map((row, index) => (_jsxs(Text, { bold: index === cursor, children: [index === cursor ? `${GLYPHS.pointer} ` : " ", row.content] }, row.id))), _jsx(TextLines, { lines: details })] }));
20
+ /**
21
+ * v1.90.9 (operator field report: "can't choose models for omp"): a list longer than the
22
+ * terminal rendered EVERY row, so the title, legend, and cursor scrolled off the top — omp's
23
+ * 218 unclassified models left the operator staring at a tail of unselectable rows. The window
24
+ * is cursor-centered and clamped; the markers say exactly what is elided and that the list is
25
+ * still fully reachable (arrows walk it, type-to-search narrows it).
26
+ */
27
+ export function windowRows(rows, cursor, capacity) {
28
+ if (capacity <= 0 || rows.length <= capacity)
29
+ return { visible: rows, start: 0, above: 0, below: 0 };
30
+ const start = Math.min(Math.max(cursor - Math.floor(capacity / 2), 0), rows.length - capacity);
31
+ return { visible: rows.slice(start, start + capacity), start, above: start, below: rows.length - start - capacity };
32
+ }
33
+ export function FleetListScreen({ title, legend, rows, cursor, details = [], filter, viewRows, }) {
34
+ const { visible, start, above, below } = windowRows(rows, cursor, viewRows ?? Number.POSITIVE_INFINITY);
35
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: title }), _jsx(Text, { dimColor: true, children: filter ? `${legend} · search: ${filter}` : legend }), above > 0 && _jsxs(Text, { dimColor: true, children: [" \u2026 ", above, " above"] }), visible.map((row, index) => (_jsxs(Text, { bold: start + index === cursor, children: [start + index === cursor ? `${GLYPHS.pointer} ` : " ", row.content] }, row.id))), below > 0 && _jsxs(Text, { dimColor: true, children: [" \u2026 ", below, " below \u2014 type to search"] }), _jsx(TextLines, { lines: details })] }));
22
36
  }
23
37
  export function FleetReviewScreen({ title, legend, diff, }) {
24
38
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: title }), _jsx(Text, { dimColor: true, children: legend }), _jsx(Text, { children: diff })] }));
@@ -42,6 +42,10 @@ type AgentCli = {
42
42
  version: string;
43
43
  authed: boolean;
44
44
  };
45
+ export type FleetModelSuggestion = {
46
+ tier: Tier;
47
+ note: string;
48
+ };
45
49
  export type FleetModelGroup = {
46
50
  adapter: string;
47
51
  vendor?: string;
@@ -49,6 +53,7 @@ export type FleetModelGroup = {
49
53
  model: string;
50
54
  tier?: Tier;
51
55
  detectedAt?: string;
56
+ suggestion?: FleetModelSuggestion;
52
57
  }>;
53
58
  };
54
59
  export type FleetClassification = {
@@ -76,7 +81,7 @@ export type FleetCandidateOption = {
76
81
  };
77
82
  };
78
83
  export declare function formatDoctorAge(ageMs: number | null): string;
79
- export declare function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewOverlay, reloadGuard, entry, initialJudge, judgeSeats, }: {
84
+ export declare function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewOverlay, reloadGuard, entry, initialJudge, judgeSeats, viewRows, }: {
80
85
  ageMs: number | null;
81
86
  agents: AgentCli[];
82
87
  initialDenyAdapters: string[];
@@ -98,6 +103,8 @@ export declare function FleetApp({ ageMs, agents, initialDenyAdapters, initialDe
98
103
  initialJudge?: string;
99
104
  /** discovered adapter:model seats — the judge picker universe */
100
105
  judgeSeats?: string[];
106
+ /** list viewport capacity (terminal rows minus chrome) — long lists window around the cursor */
107
+ viewRows?: number;
101
108
  }): import("react").JSX.Element;
102
109
  export declare function runFleetInkEditor({ ageMs, adapters, health, initialDenyAdapters, initialDenyModels, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewOverlay, reloadGuard, entry, initialJudge, judgeSeats, initialInput, input, output, debug, }: {
103
110
  ageMs: number | null;