tickmarkr 1.90.5 → 1.90.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/doctor.js +25 -3
- package/dist/cli/commands/fleet.d.ts +18 -0
- package/dist/cli/commands/fleet.js +88 -56
- package/dist/cli/commands/init.d.ts +6 -1
- package/dist/cli/commands/init.js +73 -47
- package/dist/cli/commands/run.js +3 -2
- package/dist/cli/commands/verify.d.ts +1 -0
- package/dist/cli/commands/verify.js +12 -2
- package/dist/config/fleet-overlay.d.ts +4 -0
- package/dist/config/fleet-overlay.js +4 -0
- package/dist/gates/baseline.d.ts +15 -0
- package/dist/gates/baseline.js +88 -5
- package/dist/run/consult.js +17 -2
- package/dist/run/daemon.js +25 -0
- package/dist/tui/ink/components.d.ts +3 -1
- package/dist/tui/ink/components.js +2 -2
- package/dist/tui/ink/fleet-app.d.ts +15 -2
- package/dist/tui/ink/fleet-app.js +169 -41
- package/dist/tui/ink/init-app.d.ts +34 -0
- package/dist/tui/ink/init-app.js +272 -0
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import { writeFileSync } from "node:fs";
|
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { detectPackageManager } from "../../gates/baseline.js";
|
|
5
|
+
import { detectPackageManager, turboContinueFindings } from "../../gates/baseline.js";
|
|
6
6
|
import { version } from "./version.js";
|
|
7
7
|
import { allAdapters, binaryShadowWarnings, detectCandidateClis, flagDriftWarnings, modelAliasExclusions, modelAliasLine, probeAll, probeModels, resolveShellBinary, servableExclusions, servabilityLine, writeDoctor } from "../../adapters/registry.js";
|
|
8
8
|
import { CLAUDE_ALIAS_IDENTITY_STAMPS, claudeCode, resolveClaudeAliasIdentity } from "../../adapters/claude-code.js";
|
|
@@ -212,6 +212,8 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
212
212
|
? `tickmarkr doctor --refresh-catalog: model catalog refreshed${refreshed.warning ? `; ${refreshed.warning}` : ""}`
|
|
213
213
|
: `tickmarkr doctor --refresh-catalog: catalog refresh unavailable — ${refreshed.warning ?? "unknown failure"}; retained ${refreshed.catalog.source} catalog`;
|
|
214
214
|
}
|
|
215
|
+
// Operator directive 2026-08-12 (declutter): long per-model lists render only when asked for.
|
|
216
|
+
const listAllModels = _argv.includes("--models");
|
|
215
217
|
const cfg = loadConfig(cwd);
|
|
216
218
|
// T17: synchronous/cache-only by operator ruling. The only network-bearing catalog function is the
|
|
217
219
|
// separately invoked refreshCatalogCommand; ordinary doctor never calls it.
|
|
@@ -300,6 +302,11 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
300
302
|
const pmRow = packageManagerFinding(cwd);
|
|
301
303
|
if (pmRow)
|
|
302
304
|
rows.push(alignedStatusRow(pmRow.verdict, "package-manager", pmRow.detail));
|
|
305
|
+
// GATE-FIX-4 §3 (dossier, 2026-08-13): an early-abort runner leaves later packages unverified
|
|
306
|
+
// yet baseline-forgiven — surface it BEFORE a run pays for it.
|
|
307
|
+
for (const f of turboContinueFindings(cwd, cfg)) {
|
|
308
|
+
rows.push(alignedStatusRow("warn", `gates.${f.gate}`, f.detail));
|
|
309
|
+
}
|
|
303
310
|
// Q142s: tickmarkr's own binary gets the shadow check every adapter binary already had.
|
|
304
311
|
const shadowRow = selfShadowFinding(await version(), cwd);
|
|
305
312
|
if (shadowRow)
|
|
@@ -436,9 +443,24 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
436
443
|
// Keep the historical count as an index, but it is no longer the whole report: every model gets
|
|
437
444
|
// a cache-backed evidence or explicit uncovered line. Neither branch changes cfg, health, or route().
|
|
438
445
|
rows.push(` ${dim(`(${unclassified.length} more listed, unclassified)`)}`);
|
|
439
|
-
|
|
440
|
-
|
|
446
|
+
const advisories = unclassified.map((model) => catalogModelAdvisory(cfg, catalog, a.id, model, resolvedCatalogModel(a.id, model)));
|
|
447
|
+
// Q128s exhibit #4 / operator directive 2026-08-12: ~600 per-model advisory rows buried the six
|
|
448
|
+
// decision-grade flags on a real machine. Default view keeps ONLY rows that ask the operator for a
|
|
449
|
+
// decision (tier suggestions); the no-decision bulk (uncovered + covered evidence-only) compresses
|
|
450
|
+
// to one counted line per adapter. `doctor --models` restores every row — asked-for, not ambient.
|
|
451
|
+
const decisive = advisories.filter((adv) => adv.suggestion);
|
|
452
|
+
const bulk = advisories.filter((adv) => !adv.suggestion);
|
|
453
|
+
const shown = listAllModels ? advisories : decisive;
|
|
454
|
+
for (const advisory of shown)
|
|
441
455
|
rows.push(` ${dim(`catalog · ${advisory.display}`)}`);
|
|
456
|
+
if (!listAllModels && bulk.length) {
|
|
457
|
+
const uncovered = bulk.filter((adv) => adv.coverage === "uncovered").length;
|
|
458
|
+
const evidenceOnly = bulk.length - uncovered;
|
|
459
|
+
const parts = [
|
|
460
|
+
...(uncovered ? [`${uncovered} uncovered by ${catalog.source === "cache" ? "cached catalogs" : "vendored catalog"}`] : []),
|
|
461
|
+
...(evidenceOnly ? [`${evidenceOnly} covered without a tier suggestion`] : []),
|
|
462
|
+
];
|
|
463
|
+
rows.push(` ${dim(`catalog · ${parts.join(" · ")} — tickmarkr doctor --models lists each`)}`);
|
|
442
464
|
}
|
|
443
465
|
}
|
|
444
466
|
return rows;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { WorkerAdapter } from "../../adapters/types.js";
|
|
2
|
+
import type { FleetEditorResult } from "../../tui/ink/fleet-app.js";
|
|
3
|
+
type FleetEditorProps = Parameters<typeof import("../../tui/ink/fleet-app.js").runFleetInkEditor>[0];
|
|
2
4
|
export type FleetInput = NodeJS.ReadableStream & {
|
|
3
5
|
isTTY?: boolean;
|
|
4
6
|
setRawMode?: (mode: boolean) => unknown;
|
|
@@ -26,3 +28,19 @@ export declare function fleet(argv: string[], cwd?: string, adapters?: WorkerAda
|
|
|
26
28
|
out: string;
|
|
27
29
|
code: number;
|
|
28
30
|
}>;
|
|
31
|
+
/** Everything between the doctor-reuse gate and the Ink render, packaged for reuse: `tickmarkr init`
|
|
32
|
+
* runs the same editor (preset-first) without re-owning the assembly or the write funnel. `commit`
|
|
33
|
+
* remains the single config actuator — it maps editor results to the command's exact result strings
|
|
34
|
+
* and performs the one guarded overlay write. */
|
|
35
|
+
export declare function assembleFleetEditor(cwd: string, adapters: WorkerAdapter[], io: FleetIO, opts: {
|
|
36
|
+
globalDir: string;
|
|
37
|
+
entry?: "presets" | "probe";
|
|
38
|
+
fresh?: boolean;
|
|
39
|
+
}): Promise<{
|
|
40
|
+
props: FleetEditorProps;
|
|
41
|
+
commit: (result: FleetEditorResult) => string;
|
|
42
|
+
renderWhy: () => string;
|
|
43
|
+
} | {
|
|
44
|
+
unavailable: string;
|
|
45
|
+
}>;
|
|
46
|
+
export {};
|
|
@@ -104,12 +104,57 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
104
104
|
}
|
|
105
105
|
if (!why && !interactive)
|
|
106
106
|
return { out: NON_TTY_MSG, code: 1 };
|
|
107
|
-
const
|
|
108
|
-
|
|
107
|
+
const assembled = await assembleFleetEditor(cwd, adapters, io, {
|
|
108
|
+
globalDir,
|
|
109
|
+
fresh: values.fresh ?? false,
|
|
110
|
+
});
|
|
111
|
+
if ("unavailable" in assembled)
|
|
112
|
+
return { out: assembled.unavailable, code: 1 };
|
|
113
|
+
if (why)
|
|
114
|
+
return assembled.renderWhy();
|
|
115
|
+
// Keep Ink out of print, non-TTY, and missing-probe paths: the component runtime belongs
|
|
116
|
+
// exclusively to the interactive editor. The capture window brackets only the dynamic Ink
|
|
117
|
+
// import — keys typed while the module loads land in props.initialInput, never get dropped.
|
|
118
|
+
const initialInput = [];
|
|
119
|
+
const productionInput = input;
|
|
120
|
+
const captureStartupInput = typeof productionInput.ref === "function"
|
|
121
|
+
&& typeof productionInput.unref === "function";
|
|
122
|
+
const onStartupInput = (chunk) => {
|
|
123
|
+
initialInput.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
|
124
|
+
};
|
|
125
|
+
if (captureStartupInput) {
|
|
126
|
+
input.on("data", onStartupInput);
|
|
127
|
+
input.resume();
|
|
128
|
+
}
|
|
129
|
+
let runFleetInkEditor;
|
|
130
|
+
try {
|
|
131
|
+
({ runFleetInkEditor } = await import("../../tui/ink/fleet-app.js"));
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (captureStartupInput) {
|
|
135
|
+
input.off("data", onStartupInput);
|
|
136
|
+
input.pause();
|
|
137
|
+
}
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
if (captureStartupInput) {
|
|
141
|
+
input.off("data", onStartupInput);
|
|
142
|
+
input.pause();
|
|
143
|
+
}
|
|
144
|
+
assembled.props.initialInput = initialInput;
|
|
145
|
+
const result = await runFleetInkEditor(assembled.props);
|
|
146
|
+
return assembled.commit(result);
|
|
147
|
+
}
|
|
148
|
+
/** Everything between the doctor-reuse gate and the Ink render, packaged for reuse: `tickmarkr init`
|
|
149
|
+
* runs the same editor (preset-first) without re-owning the assembly or the write funnel. `commit`
|
|
150
|
+
* remains the single config actuator — it maps editor results to the command's exact result strings
|
|
151
|
+
* and performs the one guarded overlay write. */
|
|
152
|
+
export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
153
|
+
const globalDir = opts.globalDir;
|
|
154
|
+
const { reuse, health: cached } = initDoctorReuse(cwd, opts.fresh ?? false);
|
|
109
155
|
if (!reuse || !cached) {
|
|
110
156
|
return {
|
|
111
|
-
|
|
112
|
-
code: 1,
|
|
157
|
+
unavailable: "tickmarkr fleet: probe data missing or stale — run `tickmarkr doctor` first (fleet never re-probes; doctor is the sensor)",
|
|
113
158
|
};
|
|
114
159
|
}
|
|
115
160
|
const rm = resolveRunMode(cwd, { globalDir });
|
|
@@ -231,8 +276,6 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
231
276
|
return projectFleetWhy(values, { repoRoot: cwd, globalDir });
|
|
232
277
|
};
|
|
233
278
|
const routedShapeRows = (mode, map) => projectedShapeRows(mode, map).map(({ id, label }) => ({ id, label }));
|
|
234
|
-
if (why)
|
|
235
|
-
return renderFleetWhy(projectedShapeRows(rm.mode.mode, editable.map));
|
|
236
279
|
const candidatesForShape = (shape, mode, map) => shapeCandidates(previewTask(shape), previewCfg(mode, map), channels, profile).map((candidate) => ({
|
|
237
280
|
id: `${candidate.assignment.adapter}:${candidate.assignment.model}`,
|
|
238
281
|
label: candidateRow(candidate, cfg.pricing),
|
|
@@ -255,6 +298,9 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
255
298
|
const discovered = which === "review" ? [...reviewAdapters, ...seats] : seats;
|
|
256
299
|
return [...discovered, ...current.filter((entry) => !discovered.includes(entry))];
|
|
257
300
|
};
|
|
301
|
+
// The judge is one seat by schema (config.judge: { adapter, model }); the editor offers the same
|
|
302
|
+
// discovered seats universe plus a keep-default row, and a write is staged only on a real change.
|
|
303
|
+
const initialJudge = `${cfg.judge.adapter}:${cfg.judge.model}`;
|
|
258
304
|
let pendingWrite = null;
|
|
259
305
|
const reviewOverlay = (state) => {
|
|
260
306
|
const staged = structuredClone(initial);
|
|
@@ -275,7 +321,10 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
275
321
|
const path = repoOverlayPath(cwd);
|
|
276
322
|
const modeChanged = state.selectedMode !== rm.mode.mode;
|
|
277
323
|
const steeringChanged = ["review", "consult"].some((key) => JSON.stringify(state.steering[key]) !== JSON.stringify(initialSteering[key]));
|
|
278
|
-
|
|
324
|
+
const judgeSeat = state.judgeSeat;
|
|
325
|
+
const judgeChanged = judgeSeat !== undefined
|
|
326
|
+
&& `${judgeSeat.adapter}:${judgeSeat.model}` !== initialJudge;
|
|
327
|
+
if (!modeChanged && !steeringChanged && !judgeChanged && fleetEditableEquals(initial, staged)) {
|
|
279
328
|
pendingWrite = null;
|
|
280
329
|
return { kind: "empty" };
|
|
281
330
|
}
|
|
@@ -283,6 +332,7 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
283
332
|
initial,
|
|
284
333
|
edited: staged,
|
|
285
334
|
...(modeChanged ? { mode: state.selectedMode } : {}),
|
|
335
|
+
...(judgeChanged && judgeSeat ? { judge: judgeSeat } : {}),
|
|
286
336
|
steering: { initial: initialSteering, edited: state.steering },
|
|
287
337
|
};
|
|
288
338
|
const after = renderFleetOverlayWrite(before, write);
|
|
@@ -301,35 +351,7 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
301
351
|
};
|
|
302
352
|
const reloadGuard = io.reloadGuard
|
|
303
353
|
?? ((bytes) => overlayBytesLoadError(cwd, bytes, { globalDir }));
|
|
304
|
-
|
|
305
|
-
// exclusively to the interactive editor.
|
|
306
|
-
const initialInput = [];
|
|
307
|
-
const productionInput = input;
|
|
308
|
-
const captureStartupInput = typeof productionInput.ref === "function"
|
|
309
|
-
&& typeof productionInput.unref === "function";
|
|
310
|
-
const onStartupInput = (chunk) => {
|
|
311
|
-
initialInput.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
|
312
|
-
};
|
|
313
|
-
if (captureStartupInput) {
|
|
314
|
-
input.on("data", onStartupInput);
|
|
315
|
-
input.resume();
|
|
316
|
-
}
|
|
317
|
-
let runFleetInkEditor;
|
|
318
|
-
try {
|
|
319
|
-
({ runFleetInkEditor } = await import("../../tui/ink/fleet-app.js"));
|
|
320
|
-
}
|
|
321
|
-
catch (error) {
|
|
322
|
-
if (captureStartupInput) {
|
|
323
|
-
input.off("data", onStartupInput);
|
|
324
|
-
input.pause();
|
|
325
|
-
}
|
|
326
|
-
throw error;
|
|
327
|
-
}
|
|
328
|
-
if (captureStartupInput) {
|
|
329
|
-
input.off("data", onStartupInput);
|
|
330
|
-
input.pause();
|
|
331
|
-
}
|
|
332
|
-
const result = await runFleetInkEditor({
|
|
354
|
+
const props = {
|
|
333
355
|
ageMs: doctorAgeMs(cwd),
|
|
334
356
|
adapters,
|
|
335
357
|
health,
|
|
@@ -350,25 +372,35 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
350
372
|
steeringOptionsFor,
|
|
351
373
|
reviewOverlay,
|
|
352
374
|
reloadGuard,
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
375
|
+
entry: opts.entry,
|
|
376
|
+
initialJudge,
|
|
377
|
+
judgeSeats: seats,
|
|
378
|
+
initialInput: [],
|
|
379
|
+
input: (io.input ?? process.stdin),
|
|
380
|
+
output: (io.output ?? process.stdout),
|
|
356
381
|
debug: io.debug,
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
382
|
+
};
|
|
383
|
+
const commit = (result) => {
|
|
384
|
+
if (result.kind === "quit")
|
|
385
|
+
return QUIT;
|
|
386
|
+
if (result.kind === "refresh") {
|
|
387
|
+
return "fleet: run `tickmarkr doctor` to refresh probe data, then re-run `tickmarkr fleet` (doctor is the sensor; fleet never re-probes)";
|
|
388
|
+
}
|
|
389
|
+
if (result.kind === "no-changes")
|
|
390
|
+
return "fleet: no overlay changes (empty diff)";
|
|
391
|
+
if (result.kind === "discard")
|
|
392
|
+
return "fleet: discarded overlay changes";
|
|
393
|
+
// The command remains the single config actuator. Every interactive edit reaches this
|
|
394
|
+
// one write only after the component-rendered diff confirm and the production reload guard.
|
|
395
|
+
const write = pendingWrite;
|
|
396
|
+
if (!write)
|
|
397
|
+
throw new Error("fleet write reached confirmation without a staged overlay mutation");
|
|
398
|
+
writeFleetOverlay(result.review.path, (prior) => renderFleetOverlayWrite(prior, write));
|
|
399
|
+
return `fleet: wrote ${result.review.path}`;
|
|
400
|
+
};
|
|
401
|
+
return {
|
|
402
|
+
props,
|
|
403
|
+
commit,
|
|
404
|
+
renderWhy: () => renderFleetWhy(projectedShapeRows(rm.mode.mode, editable.map)),
|
|
405
|
+
};
|
|
374
406
|
}
|
|
@@ -1 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
/** Test seam mirroring FleetIO: production callers omit it and get the process streams. */
|
|
2
|
+
export type InitIO = {
|
|
3
|
+
input?: NodeJS.ReadStream;
|
|
4
|
+
output?: NodeJS.WriteStream;
|
|
5
|
+
};
|
|
6
|
+
export declare function init(argv: string[], cwd?: string, io?: InitIO): Promise<string>;
|
|
@@ -10,6 +10,7 @@ import { BANNER, kvRow, legend, rule, statusRow, title } from "../../brand.js";
|
|
|
10
10
|
import { tickmarkrDir } from "../../graph/graph.js";
|
|
11
11
|
import { Journal } from "../../run/journal.js";
|
|
12
12
|
import { doctor } from "./doctor.js";
|
|
13
|
+
import { assembleFleetEditor } from "./fleet.js";
|
|
13
14
|
const SCAFFOLD_SPEC = "tickmarkr.spec.md";
|
|
14
15
|
// Operator-approved (2026-07-17) environments footer — three rows; no npm install for herdr
|
|
15
16
|
// (npm package "herdr" is a reserved 0.0.0 placeholder as of that date).
|
|
@@ -214,6 +215,11 @@ async function installAgentFiles(cwd, force, docs, notes) {
|
|
|
214
215
|
const confirm = async (question) => {
|
|
215
216
|
if (!interactive)
|
|
216
217
|
return false;
|
|
218
|
+
// Post-wizard hardening: an Ink act that ran earlier in this process leaves stdin paused and
|
|
219
|
+
// unref'd on unmount, and readline revives neither — the process exits mid-question (found by
|
|
220
|
+
// the consolidated-init smoke). Revive explicitly before asking.
|
|
221
|
+
process.stdin.resume();
|
|
222
|
+
process.stdin.ref();
|
|
217
223
|
prompt ??= createInterface({ input: process.stdin, output: process.stdout });
|
|
218
224
|
return /^(?:y|yes)$/i.test((await prompt.question(`${question} [y/N] `)).trim());
|
|
219
225
|
};
|
|
@@ -268,43 +274,31 @@ async function installAgentFiles(cwd, force, docs, notes) {
|
|
|
268
274
|
prompt?.close();
|
|
269
275
|
}
|
|
270
276
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const llmRaw = await askDefault(rl, "visibility.llm (pane|headless)", llmDef);
|
|
295
|
-
const llm = llmRaw === "pane" || llmRaw === "headless" ? llmRaw : llmDef;
|
|
296
|
-
let installSkills = false;
|
|
297
|
-
if (!skillsInstalled(cwd)) {
|
|
298
|
-
const skillsDef = existsSync(join(cwd, ".claude", "skills")) && !skillsInstalled(cwd);
|
|
299
|
-
installSkills = await askYesNo(rl, "Install agent skills (tickmarkr-loop/tickmarkr-auto/tickmarkr-overseer)?", skillsDef);
|
|
300
|
-
}
|
|
301
|
-
return { overlay: { driver, concurrency, visibility: { llm } }, installSkills };
|
|
302
|
-
}
|
|
303
|
-
finally {
|
|
304
|
-
rl.close();
|
|
305
|
-
}
|
|
277
|
+
// The consolidated wizard (operator directive 2026-08-12): init walks three acts in one
|
|
278
|
+
// command — preferences (Ink form) → discovery (doctor, the sensor) → fleet (presets or the
|
|
279
|
+
// full editor). The readline asks this replaced live on in git history; the Ink act keeps the
|
|
280
|
+
// same four questions and the same defaults, so `--yes` and non-TTY paths are unchanged.
|
|
281
|
+
async function runInitWizard(cwd, input, output) {
|
|
282
|
+
// Dynamic import, deliberately (same seam as fleet.ts): the Ink/React runtime must stay out of
|
|
283
|
+
// the non-TTY and --yes paths — init runs in CI and agent shells where importing Ink at module
|
|
284
|
+
// load would drag a renderer into paths that never draw.
|
|
285
|
+
const { runInitWizardApp } = await import("../../tui/ink/init-app.js");
|
|
286
|
+
const result = await runInitWizardApp({
|
|
287
|
+
fields: {
|
|
288
|
+
driver: wizardDriverDefault(),
|
|
289
|
+
concurrency: DEFAULT_CONFIG.concurrency,
|
|
290
|
+
visibilityLlm: DEFAULT_CONFIG.visibility.llm,
|
|
291
|
+
offerSkills: !skillsInstalled(cwd),
|
|
292
|
+
skillsDefault: existsSync(join(cwd, ".claude", "skills")) && !skillsInstalled(cwd),
|
|
293
|
+
},
|
|
294
|
+
input,
|
|
295
|
+
output,
|
|
296
|
+
});
|
|
297
|
+
if (result.kind === "quit")
|
|
298
|
+
return null;
|
|
299
|
+
return { overlay: result.overlay, installSkills: result.installSkills, installDocs: result.installDocs };
|
|
306
300
|
}
|
|
307
|
-
export async function init(argv, cwd = process.cwd()) {
|
|
301
|
+
export async function init(argv, cwd = process.cwd(), io = {}) {
|
|
308
302
|
let bannerEmitted = false;
|
|
309
303
|
const emitBanner = () => {
|
|
310
304
|
if (visual() && !bannerEmitted) {
|
|
@@ -363,26 +357,58 @@ export async function init(argv, cwd = process.cwd()) {
|
|
|
363
357
|
notes.push(`stale ${join(d.dir, "SKILL.md")} — ${describeDrift(d)}; run tickmarkr init --agent --force to refresh it`);
|
|
364
358
|
}
|
|
365
359
|
}
|
|
366
|
-
const
|
|
367
|
-
const
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
360
|
+
const input = io.input ?? process.stdin;
|
|
361
|
+
const output = io.output ?? process.stdout;
|
|
362
|
+
const interactive = input.isTTY === true && output.isTTY === true && !(values.yes ?? false);
|
|
363
|
+
// Act 1 — preferences. Only when the repo config does not exist yet: init never rewrites an
|
|
364
|
+
// operator's config; re-tuning is `tickmarkr fleet` (act 3 below still runs and writes overlays).
|
|
365
|
+
let wizardQuit = false;
|
|
372
366
|
if (!repoConfigExists) {
|
|
373
367
|
if (interactive) {
|
|
374
368
|
emitBanner();
|
|
375
|
-
const wizard = await runInitWizard(cwd);
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
369
|
+
const wizard = await runInitWizard(cwd, input, output);
|
|
370
|
+
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)");
|
|
373
|
+
}
|
|
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
|
+
}
|
|
380
|
+
}
|
|
380
381
|
}
|
|
381
382
|
else {
|
|
382
383
|
writeFileSync(repoConfigPath, configTemplate());
|
|
383
384
|
notes.push(`wrote ${repoConfigPath}`);
|
|
384
385
|
}
|
|
385
386
|
}
|
|
387
|
+
// Act 2 — discovery. Doctor is the sensor (fleet never re-probes); runs after preferences so the
|
|
388
|
+
// probe reads the config the operator just chose. Reuse keeps init fast; --fresh forces the probe.
|
|
389
|
+
const fresh = values.fresh ?? false;
|
|
390
|
+
const { reuse, ageMs, health } = initDoctorReuse(cwd, fresh);
|
|
391
|
+
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) {
|
|
398
|
+
output.write(`${doc}\n`);
|
|
399
|
+
doc = "discovery report shown above — re-print any time with `tickmarkr doctor`";
|
|
400
|
+
// Same lazy-Ink seam as fleet.ts: load the editor runtime up front so the assembler's
|
|
401
|
+
// returned props need no startup-input capture window on this path.
|
|
402
|
+
const { runFleetInkEditor } = await import("../../tui/ink/fleet-app.js");
|
|
403
|
+
const editor = await assembleFleetEditor(cwd, allAdapters(), { input, output }, { globalDir: gdir, entry: "presets" });
|
|
404
|
+
if ("unavailable" in editor) {
|
|
405
|
+
notes.push(`fleet: ${editor.unavailable}`);
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
const result = await runFleetInkEditor(editor.props);
|
|
409
|
+
notes.push(editor.commit(result));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
386
412
|
if (values.agent)
|
|
387
413
|
await installAgentFiles(cwd, values.force ?? false, values.docs ?? false, notes);
|
|
388
414
|
const next = nextSteps(cwd, SCAFFOLD_SPEC);
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -67,8 +67,9 @@ export async function run(argv, cwd = process.cwd()) {
|
|
|
67
67
|
console.warn(`tickmarkr: !! ${conflict}`);
|
|
68
68
|
}
|
|
69
69
|
// OBS-107 (v1.67 T5): advisory only — a stale compiled graph with prior terminal statuses and no
|
|
70
|
-
// live daemon will otherwise
|
|
71
|
-
// is
|
|
70
|
+
// live daemon will otherwise confuse the operator. The recompile remedy is named; dispatch
|
|
71
|
+
// proceeds unchanged when anything is still dispatchable. When NOTHING is (all-terminal graph),
|
|
72
|
+
// runDaemon's no-op refusal (GATE-FIX-4 defect 4) turns the old zero-work "finished" into an error.
|
|
72
73
|
const TERMINAL_STATUSES = ["done", "failed", "human"];
|
|
73
74
|
if (graph.tasks.some((t) => TERMINAL_STATUSES.includes(t.status)) && !isRunLockLive(cwd)) {
|
|
74
75
|
console.warn("tickmarkr: compiled graph carries terminal statuses from a prior run and no daemon is active — "
|
|
@@ -3,6 +3,7 @@ import { type AcceptanceItem } from "../../graph/schema.js";
|
|
|
3
3
|
export declare function parseCriteria(text: string): AcceptanceItem[];
|
|
4
4
|
export declare const HUMAN_CHANNEL: BillingChannel;
|
|
5
5
|
export declare const HUMAN_AUTHOR: Assignment;
|
|
6
|
+
export declare function verifyStateDir(cwd: string): string;
|
|
6
7
|
export declare function verify(argv: string[], cwd?: string): Promise<{
|
|
7
8
|
out: string;
|
|
8
9
|
code: number;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
@@ -42,6 +42,16 @@ export function parseCriteria(text) {
|
|
|
42
42
|
// verify models the author as a "human" vendor channel — resolvable, excludes nothing real.
|
|
43
43
|
export const HUMAN_CHANNEL = { adapter: "human", vendor: "human", model: "human", channel: "sub", tier: "frontier" };
|
|
44
44
|
export const HUMAN_AUTHOR = { adapter: "human", model: "human", channel: "sub", tier: "frontier" };
|
|
45
|
+
// GATE-FIX-4 defect 1 (false-RED on macOS): os.tmpdir() returns /var/folders/…, a symlink into
|
|
46
|
+
// /private/var — so a baseline captured under the repo path and a head battery run under the tmp
|
|
47
|
+
// path disagree on every path-bearing fingerprint, and verify reds a green diff. graph.ts's
|
|
48
|
+
// saveGraph carries the standing precedent ("never os.tmpdir()" — rename(2) atomicity there, path
|
|
49
|
+
// identity here): tmpdir is fine for verify's disposable state, but only through realpathSync so
|
|
50
|
+
// every path verify hands to gates is already canonical. Exported for the unit test that pins the
|
|
51
|
+
// realpath (CI cannot rely on the macOS symlink).
|
|
52
|
+
export function verifyStateDir(cwd) {
|
|
53
|
+
return join(realpathSync(tmpdir()), "tickmarkr-verify", createHash("sha256").update(cwd).digest("hex").slice(0, 12));
|
|
54
|
+
}
|
|
45
55
|
export async function verify(argv, cwd = process.cwd()) {
|
|
46
56
|
const { values } = parseArgs({
|
|
47
57
|
args: argv,
|
|
@@ -100,7 +110,7 @@ export async function verify(argv, cwd = process.cwd()) {
|
|
|
100
110
|
// ALL verify state (cache, base worktree, artifacts) lives OUTSIDE the repo: verify gates the repo
|
|
101
111
|
// root itself, so any file it wrote there would trip the battery's own dirty-worktree refusal.
|
|
102
112
|
// ponytail: tmpdir means the baseline cache dies on reboot/cleanup — worst case is one re-capture.
|
|
103
|
-
const stateDir =
|
|
113
|
+
const stateDir = verifyStateDir(cwd);
|
|
104
114
|
mkdirSync(stateDir, { recursive: true });
|
|
105
115
|
let baseline;
|
|
106
116
|
const cachePath = join(stateDir, `baseline-${mergeBase.slice(0, 12)}.json`);
|
|
@@ -170,6 +170,10 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
170
170
|
if (write.mode !== undefined) {
|
|
171
171
|
setScalarPreservingComment(doc, ["routing", "mode"], write.mode);
|
|
172
172
|
}
|
|
173
|
+
if (write.judge) {
|
|
174
|
+
setScalarPreservingComment(doc, ["judge", "adapter"], write.judge.adapter);
|
|
175
|
+
setScalarPreservingComment(doc, ["judge", "model"], write.judge.model);
|
|
176
|
+
}
|
|
173
177
|
if (write.steering) {
|
|
174
178
|
for (const key of ["review", "consult"]) {
|
|
175
179
|
const before = write.steering.initial[key];
|
package/dist/gates/baseline.d.ts
CHANGED
|
@@ -47,6 +47,21 @@ export type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
|
|
|
47
47
|
*/
|
|
48
48
|
export declare function detectPackageManager(repoRoot: string): PackageManager;
|
|
49
49
|
export declare function detectGateCommands(repoRoot: string, cfg: TickmarkrConfig): Record<string, string>;
|
|
50
|
+
/**
|
|
51
|
+
* Dossier GATE-FIX-4 §3 (2026-08-13): turbo schedules per-package tasks and ABORTS the whole
|
|
52
|
+
* run at the first failing package, so every package after the abort goes unverified — and the
|
|
53
|
+
* baseline gate then forgives the truncated output as "matching". The class appeared in three
|
|
54
|
+
* gates and four syntaxes on one repo (turbo's scheduler, `&&`, a JS for-loop process.exit, and
|
|
55
|
+
* turbo nested inside two of them). This preflight names only what it can see textually: the
|
|
56
|
+
* resolved gate command, and — for the synthesized `<pm> run <script>` form — that script's own
|
|
57
|
+
* body. The remedy deliberately demands a forwarding check first: the same repo's lint wrapper
|
|
58
|
+
* branched on argv.length, so a blind `-- --continue` silently replaced the gate with a
|
|
59
|
+
* different command (the trap their overseer caught by reading, not by symmetry).
|
|
60
|
+
*/
|
|
61
|
+
export declare function turboContinueFindings(repoRoot: string, cfg: TickmarkrConfig): Array<{
|
|
62
|
+
gate: string;
|
|
63
|
+
detail: string;
|
|
64
|
+
}>;
|
|
50
65
|
/**
|
|
51
66
|
* The ceiling a battery runs under: whichever is larger of the shipped constant and the slack applied
|
|
52
67
|
* to what this command actually measured. A pre-v1.90 baseline (no measurement) gets exactly the
|