tickmarkr 2.4.2 → 2.5.1
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/README.md +110 -24
- package/dist/adapters/fake.d.ts +1 -0
- package/dist/adapters/fake.js +2 -1
- package/dist/adapters/opencode.d.ts +1 -0
- package/dist/adapters/opencode.js +4 -1
- package/dist/adapters/types.d.ts +14 -0
- package/dist/adapters/types.js +25 -0
- package/dist/cli/commands/approve.d.ts +42 -0
- package/dist/cli/commands/approve.js +80 -13
- package/dist/cli/commands/doctor.d.ts +234 -0
- package/dist/cli/commands/doctor.js +139 -5
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.js +45 -5
- package/dist/cli/commands/plan.js +58 -6
- package/dist/cli/commands/report.js +18 -45
- package/dist/cli/commands/run.js +3 -0
- package/dist/cli/commands/scope.js +36 -6
- package/dist/cli/commands/stats.d.ts +2 -0
- package/dist/cli/commands/stats.js +42 -23
- package/dist/cli/commands/status.js +20 -4
- package/dist/cli/commands/ui.js +42 -53
- package/dist/cli/commands/unlock.d.ts +1 -1
- package/dist/cli/commands/unlock.js +59 -9
- package/dist/cli/help.d.ts +219 -0
- package/dist/cli/help.js +212 -0
- package/dist/cli/index.d.ts +42 -2
- package/dist/cli/index.js +23 -8
- package/dist/drivers/herdr.d.ts +7 -2
- package/dist/drivers/herdr.js +78 -48
- package/dist/drivers/orca.d.ts +3 -2
- package/dist/drivers/orca.js +43 -4
- package/dist/drivers/subprocess.d.ts +1 -0
- package/dist/drivers/subprocess.js +3 -0
- package/dist/drivers/types.d.ts +14 -0
- package/dist/gates/artifact-manifest.d.ts +50 -0
- package/dist/gates/artifact-manifest.js +23 -0
- package/dist/gates/llm.d.ts +5 -0
- package/dist/gates/llm.js +188 -3
- package/dist/gates/review.d.ts +6 -3
- package/dist/gates/review.js +79 -33
- package/dist/gates/run-gates.d.ts +3 -0
- package/dist/gates/run-gates.js +14 -4
- package/dist/plan/scope.d.ts +25 -0
- package/dist/plan/scope.js +92 -12
- package/dist/report/operator-record.d.ts +49 -0
- package/dist/report/operator-record.js +137 -0
- package/dist/run/daemon.d.ts +5 -5
- package/dist/run/daemon.js +166 -44
- package/dist/run/lock.d.ts +59 -2
- package/dist/run/lock.js +184 -26
- package/dist/run/operator-state.d.ts +86 -0
- package/dist/run/operator-state.js +165 -0
- package/dist/run/supervision.d.ts +32 -0
- package/dist/run/supervision.js +138 -17
- package/dist/tui/cockpit/capture.d.ts +19 -0
- package/dist/tui/cockpit/capture.js +89 -1
- package/dist/tui/cockpit/components.d.ts +15 -1
- package/dist/tui/cockpit/components.js +79 -9
- package/dist/tui/cockpit/decision-actions.d.ts +147 -0
- package/dist/tui/cockpit/decision-actions.js +315 -0
- package/dist/tui/cockpit/derive.d.ts +1 -1
- package/dist/tui/cockpit/derive.js +2 -0
- package/dist/tui/cockpit/evidence-view.d.ts +119 -0
- package/dist/tui/cockpit/evidence-view.js +210 -0
- package/dist/tui/cockpit/home-view.d.ts +88 -0
- package/dist/tui/cockpit/home-view.js +240 -0
- package/dist/tui/cockpit/keys.d.ts +125 -0
- package/dist/tui/cockpit/keys.js +31 -0
- package/dist/tui/cockpit/layout.d.ts +14 -0
- package/dist/tui/cockpit/layout.js +15 -0
- package/dist/tui/cockpit/live-runtime.d.ts +46 -0
- package/dist/tui/cockpit/live-runtime.js +683 -0
- package/dist/tui/cockpit/live-store.d.ts +289 -0
- package/dist/tui/cockpit/live-store.js +308 -0
- package/dist/tui/cockpit/live.d.ts +21 -1
- package/dist/tui/cockpit/live.js +12 -1
- package/dist/tui/cockpit/run-view.d.ts +100 -0
- package/dist/tui/cockpit/run-view.js +202 -0
- package/dist/tui/cockpit/shell.d.ts +50 -0
- package/dist/tui/cockpit/shell.js +74 -0
- package/dist/tui/cockpit/theme.d.ts +27 -0
- package/dist/tui/cockpit/theme.js +21 -0
- package/dist/tui/ink/init-app.d.ts +4 -0
- package/dist/tui/ink/init-app.js +18 -7
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +10 -1
- package/skills/tickmarkr-loop/SKILL.md +72 -1
- package/skills/tickmarkr-overseer/SKILL.md +12 -0
- package/skills/tickmarkr-overseer/scripts/watch-context.sh +13 -2
|
@@ -96,5 +96,239 @@ export declare function selfShadowFinding(ownVersion: string, cwd?: string, reso
|
|
|
96
96
|
* check) reads it and bumps the constant.
|
|
97
97
|
*/
|
|
98
98
|
export declare function liveBenchStalenessFinding(now: Date): string | undefined;
|
|
99
|
+
/**
|
|
100
|
+
* A no-call disclosure for the action that normal doctor performs. This is deliberately built
|
|
101
|
+
* from config alone: opening the disclosure must not turn a cache reader into a model purchase.
|
|
102
|
+
*/
|
|
103
|
+
export declare function doctorProbePreflight(cwd?: string, cfg?: {
|
|
104
|
+
concurrency: number;
|
|
105
|
+
driver: "auto" | "herdr" | "subprocess" | "orca";
|
|
106
|
+
integrationBranchPrefix: string;
|
|
107
|
+
taskTimeoutMinutes: number;
|
|
108
|
+
contextWarnTokens: number;
|
|
109
|
+
routing: {
|
|
110
|
+
map: Record<string, {
|
|
111
|
+
pin?: {
|
|
112
|
+
via: string;
|
|
113
|
+
model: string;
|
|
114
|
+
} | undefined;
|
|
115
|
+
pool?: {
|
|
116
|
+
mode: "any" | "ordered";
|
|
117
|
+
channels: string[];
|
|
118
|
+
} | undefined;
|
|
119
|
+
tier?: undefined;
|
|
120
|
+
prefer?: string[] | undefined;
|
|
121
|
+
escalate?: boolean | undefined;
|
|
122
|
+
}>;
|
|
123
|
+
floors: Record<string, "cheap" | "mid" | "frontier">;
|
|
124
|
+
learned: "on" | "off";
|
|
125
|
+
allowUnverifiedModels: boolean;
|
|
126
|
+
mode?: "partner-led" | "risk-based" | "staff-led" | undefined;
|
|
127
|
+
learnedTuning?: {
|
|
128
|
+
halfLifeRuns?: number | undefined;
|
|
129
|
+
availWeight?: number | undefined;
|
|
130
|
+
} | undefined;
|
|
131
|
+
explore?: {
|
|
132
|
+
mode?: "on" | "off" | undefined;
|
|
133
|
+
excludeShapes?: string[] | undefined;
|
|
134
|
+
excludeComplexityAtOrAbove?: number | null | undefined;
|
|
135
|
+
cap?: number | undefined;
|
|
136
|
+
} | undefined;
|
|
137
|
+
sla?: Record<string, number> | undefined;
|
|
138
|
+
allow?: {
|
|
139
|
+
adapters?: string[] | undefined;
|
|
140
|
+
models?: string[] | undefined;
|
|
141
|
+
} | undefined;
|
|
142
|
+
deny?: {
|
|
143
|
+
adapters?: string[] | undefined;
|
|
144
|
+
models?: string[] | undefined;
|
|
145
|
+
workers?: {
|
|
146
|
+
adapters?: string[] | undefined;
|
|
147
|
+
models?: string[] | undefined;
|
|
148
|
+
} | undefined;
|
|
149
|
+
} | undefined;
|
|
150
|
+
};
|
|
151
|
+
tiers: Record<string, {
|
|
152
|
+
vendor: string | null;
|
|
153
|
+
channel: "sub" | "api";
|
|
154
|
+
models: Record<string, "cheap" | "mid" | "frontier">;
|
|
155
|
+
modelOverrides?: Record<string, {
|
|
156
|
+
vendor?: string | undefined;
|
|
157
|
+
channel?: "sub" | "api" | undefined;
|
|
158
|
+
}> | undefined;
|
|
159
|
+
windows?: Record<string, number> | undefined;
|
|
160
|
+
}>;
|
|
161
|
+
pricing: Record<string, number>;
|
|
162
|
+
gates: {
|
|
163
|
+
build?: string | undefined;
|
|
164
|
+
test?: string | undefined;
|
|
165
|
+
lint?: string | undefined;
|
|
166
|
+
diffCap?: number | undefined;
|
|
167
|
+
byShape?: Partial<Record<"plan" | "spec" | "implement" | "tests" | "docs" | "migration" | "ui" | "refactor" | "chore", {
|
|
168
|
+
[x: string]: unknown;
|
|
169
|
+
acceptance?: boolean | undefined;
|
|
170
|
+
review?: boolean | undefined;
|
|
171
|
+
}>> | undefined;
|
|
172
|
+
};
|
|
173
|
+
judge: {
|
|
174
|
+
adapter: string;
|
|
175
|
+
model: string;
|
|
176
|
+
};
|
|
177
|
+
review: {
|
|
178
|
+
complexityThreshold: number;
|
|
179
|
+
timeoutMs: number;
|
|
180
|
+
required: boolean;
|
|
181
|
+
prefer?: string[] | undefined;
|
|
182
|
+
policy?: "full" | "judge-only" | undefined;
|
|
183
|
+
criticalPaths?: string[] | undefined;
|
|
184
|
+
};
|
|
185
|
+
consult: {
|
|
186
|
+
adapter: string;
|
|
187
|
+
model: string;
|
|
188
|
+
stallMinutes: number;
|
|
189
|
+
prefer?: string[] | undefined;
|
|
190
|
+
};
|
|
191
|
+
visibility: {
|
|
192
|
+
llm: "pane" | "headless";
|
|
193
|
+
keepPanes: "run" | "attempt" | "forever";
|
|
194
|
+
worker: "interactive" | "print";
|
|
195
|
+
workersPerTab: number;
|
|
196
|
+
};
|
|
197
|
+
setup?: string | undefined;
|
|
198
|
+
cost?: {
|
|
199
|
+
models?: Record<string, {
|
|
200
|
+
inPerMtok: number;
|
|
201
|
+
outPerMtok: number;
|
|
202
|
+
cacheReadPerMtok?: number | undefined;
|
|
203
|
+
rateDate?: string | undefined;
|
|
204
|
+
}> | undefined;
|
|
205
|
+
subs?: Record<string, {
|
|
206
|
+
planMonthly: number;
|
|
207
|
+
windowsPerMonthLow: number;
|
|
208
|
+
windowsPerMonthHigh: number;
|
|
209
|
+
}> | undefined;
|
|
210
|
+
} | undefined;
|
|
211
|
+
scope?: {
|
|
212
|
+
allowDeviations?: string[] | undefined;
|
|
213
|
+
} | undefined;
|
|
214
|
+
}, adapters?: WorkerAdapter[]): string;
|
|
215
|
+
/**
|
|
216
|
+
* Health surfaces use this cache-only reader. It intentionally receives adapters only to render
|
|
217
|
+
* their configured identities; it never invokes adapter.probe(), listModels(), or a model command.
|
|
218
|
+
*/
|
|
219
|
+
export declare function cachedDoctorDiagnostics(cwd?: string, adapters?: WorkerAdapter[], cfg?: {
|
|
220
|
+
concurrency: number;
|
|
221
|
+
driver: "auto" | "herdr" | "subprocess" | "orca";
|
|
222
|
+
integrationBranchPrefix: string;
|
|
223
|
+
taskTimeoutMinutes: number;
|
|
224
|
+
contextWarnTokens: number;
|
|
225
|
+
routing: {
|
|
226
|
+
map: Record<string, {
|
|
227
|
+
pin?: {
|
|
228
|
+
via: string;
|
|
229
|
+
model: string;
|
|
230
|
+
} | undefined;
|
|
231
|
+
pool?: {
|
|
232
|
+
mode: "any" | "ordered";
|
|
233
|
+
channels: string[];
|
|
234
|
+
} | undefined;
|
|
235
|
+
tier?: undefined;
|
|
236
|
+
prefer?: string[] | undefined;
|
|
237
|
+
escalate?: boolean | undefined;
|
|
238
|
+
}>;
|
|
239
|
+
floors: Record<string, "cheap" | "mid" | "frontier">;
|
|
240
|
+
learned: "on" | "off";
|
|
241
|
+
allowUnverifiedModels: boolean;
|
|
242
|
+
mode?: "partner-led" | "risk-based" | "staff-led" | undefined;
|
|
243
|
+
learnedTuning?: {
|
|
244
|
+
halfLifeRuns?: number | undefined;
|
|
245
|
+
availWeight?: number | undefined;
|
|
246
|
+
} | undefined;
|
|
247
|
+
explore?: {
|
|
248
|
+
mode?: "on" | "off" | undefined;
|
|
249
|
+
excludeShapes?: string[] | undefined;
|
|
250
|
+
excludeComplexityAtOrAbove?: number | null | undefined;
|
|
251
|
+
cap?: number | undefined;
|
|
252
|
+
} | undefined;
|
|
253
|
+
sla?: Record<string, number> | undefined;
|
|
254
|
+
allow?: {
|
|
255
|
+
adapters?: string[] | undefined;
|
|
256
|
+
models?: string[] | undefined;
|
|
257
|
+
} | undefined;
|
|
258
|
+
deny?: {
|
|
259
|
+
adapters?: string[] | undefined;
|
|
260
|
+
models?: string[] | undefined;
|
|
261
|
+
workers?: {
|
|
262
|
+
adapters?: string[] | undefined;
|
|
263
|
+
models?: string[] | undefined;
|
|
264
|
+
} | undefined;
|
|
265
|
+
} | undefined;
|
|
266
|
+
};
|
|
267
|
+
tiers: Record<string, {
|
|
268
|
+
vendor: string | null;
|
|
269
|
+
channel: "sub" | "api";
|
|
270
|
+
models: Record<string, "cheap" | "mid" | "frontier">;
|
|
271
|
+
modelOverrides?: Record<string, {
|
|
272
|
+
vendor?: string | undefined;
|
|
273
|
+
channel?: "sub" | "api" | undefined;
|
|
274
|
+
}> | undefined;
|
|
275
|
+
windows?: Record<string, number> | undefined;
|
|
276
|
+
}>;
|
|
277
|
+
pricing: Record<string, number>;
|
|
278
|
+
gates: {
|
|
279
|
+
build?: string | undefined;
|
|
280
|
+
test?: string | undefined;
|
|
281
|
+
lint?: string | undefined;
|
|
282
|
+
diffCap?: number | undefined;
|
|
283
|
+
byShape?: Partial<Record<"plan" | "spec" | "implement" | "tests" | "docs" | "migration" | "ui" | "refactor" | "chore", {
|
|
284
|
+
[x: string]: unknown;
|
|
285
|
+
acceptance?: boolean | undefined;
|
|
286
|
+
review?: boolean | undefined;
|
|
287
|
+
}>> | undefined;
|
|
288
|
+
};
|
|
289
|
+
judge: {
|
|
290
|
+
adapter: string;
|
|
291
|
+
model: string;
|
|
292
|
+
};
|
|
293
|
+
review: {
|
|
294
|
+
complexityThreshold: number;
|
|
295
|
+
timeoutMs: number;
|
|
296
|
+
required: boolean;
|
|
297
|
+
prefer?: string[] | undefined;
|
|
298
|
+
policy?: "full" | "judge-only" | undefined;
|
|
299
|
+
criticalPaths?: string[] | undefined;
|
|
300
|
+
};
|
|
301
|
+
consult: {
|
|
302
|
+
adapter: string;
|
|
303
|
+
model: string;
|
|
304
|
+
stallMinutes: number;
|
|
305
|
+
prefer?: string[] | undefined;
|
|
306
|
+
};
|
|
307
|
+
visibility: {
|
|
308
|
+
llm: "pane" | "headless";
|
|
309
|
+
keepPanes: "run" | "attempt" | "forever";
|
|
310
|
+
worker: "interactive" | "print";
|
|
311
|
+
workersPerTab: number;
|
|
312
|
+
};
|
|
313
|
+
setup?: string | undefined;
|
|
314
|
+
cost?: {
|
|
315
|
+
models?: Record<string, {
|
|
316
|
+
inPerMtok: number;
|
|
317
|
+
outPerMtok: number;
|
|
318
|
+
cacheReadPerMtok?: number | undefined;
|
|
319
|
+
rateDate?: string | undefined;
|
|
320
|
+
}> | undefined;
|
|
321
|
+
subs?: Record<string, {
|
|
322
|
+
planMonthly: number;
|
|
323
|
+
windowsPerMonthLow: number;
|
|
324
|
+
windowsPerMonthHigh: number;
|
|
325
|
+
}> | undefined;
|
|
326
|
+
} | undefined;
|
|
327
|
+
scope?: {
|
|
328
|
+
allowDeviations?: string[] | undefined;
|
|
329
|
+
} | undefined;
|
|
330
|
+
}): string;
|
|
331
|
+
/** Repair is an actuator, but never a reason to run the paid diagnostic sensor. */
|
|
332
|
+
export declare function doctorFixOnly(cwd?: string): string;
|
|
99
333
|
export declare function doctor(_argv: string[], cwd?: string, adapters?: WorkerAdapter[], opts?: DoctorOpts): Promise<string>;
|
|
100
334
|
export {};
|
|
@@ -4,7 +4,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { detectPackageManager, turboContinueFindings } from "../../gates/baseline.js";
|
|
6
6
|
import { version } from "./version.js";
|
|
7
|
-
import { allAdapters, binaryShadowWarnings, detectCandidateClis, flagDriftWarnings, modelAliasExclusions, modelAliasLine, probeAll, probeModels, resolveShellBinary, servableExclusions, servabilityLine, writeDoctor } from "../../adapters/registry.js";
|
|
7
|
+
import { allAdapters, binaryShadowWarnings, detectCandidateClis, doctorAgeMs, flagDriftWarnings, modelAliasExclusions, modelAliasLine, probeAll, probeModels, readDoctor, resolveShellBinary, servableExclusions, servabilityLine, writeDoctor } from "../../adapters/registry.js";
|
|
8
8
|
import { CLAUDE_ALIAS_IDENTITY_STAMPS, claudeCode, resolveClaudeAliasIdentity } from "../../adapters/claude-code.js";
|
|
9
9
|
import { shq } from "../../adapters/types.js";
|
|
10
10
|
import { BANNER, compactTokens, dim, fail, kvRow, legend, ok, rule, statusRow, title } from "../../brand.js";
|
|
@@ -347,8 +347,142 @@ export function liveBenchStalenessFinding(now) {
|
|
|
347
347
|
return undefined;
|
|
348
348
|
return `LiveBench table pinned at ${LIVEBENCH_TABLE_DATE} is ${days} days old (>${LIVEBENCH_TABLE_MAX_AGE_DAYS}d) — list ${LIVEBENCH_RELEASES_URL} for a newer table_<date>.csv and bump LIVEBENCH_TABLE_DATE (advisory — tickmarkr never fetches that listing)`;
|
|
349
349
|
}
|
|
350
|
+
function configuredModels(cfg, adapters) {
|
|
351
|
+
const allowed = adapters ? new Set(adapters.map((adapter) => adapter.id)) : undefined;
|
|
352
|
+
return Object.entries(cfg.tiers).flatMap(([adapter, tier]) => allowed && !allowed.has(adapter) ? [] :
|
|
353
|
+
Object.keys(tier.models ?? {}).map((model) => ({ adapter, model })));
|
|
354
|
+
}
|
|
355
|
+
function cacheAge(ageMs) {
|
|
356
|
+
return ageMs === null ? "unavailable" : `${Math.floor(ageMs / 60_000)}m`;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* A no-call disclosure for the action that normal doctor performs. This is deliberately built
|
|
360
|
+
* from config alone: opening the disclosure must not turn a cache reader into a model purchase.
|
|
361
|
+
*/
|
|
362
|
+
export function doctorProbePreflight(cwd = process.cwd(), cfg = loadConfig(cwd), adapters = allAdapters()) {
|
|
363
|
+
const models = configuredModels(cfg, adapters);
|
|
364
|
+
const scope = models.length
|
|
365
|
+
? models.map(({ adapter, model }) => `${adapter}:${model}`).join(", ")
|
|
366
|
+
: "none (no configured models)";
|
|
367
|
+
const state = stateDirName(cwd);
|
|
368
|
+
return [
|
|
369
|
+
"tickmarkr doctor — probe preflight:",
|
|
370
|
+
`configured model-call scope: ${models.length} model${models.length === 1 ? "" : "s"} — ${scope}`,
|
|
371
|
+
`cache policy: explicit probing replaces ${state}/doctor.json; each installed configured model receives one bounded call, with a serial retry only after a first failure (a prior timeout skips that retry)`,
|
|
372
|
+
`affected destinations: ${state}/doctor.json (adapter and model verdict cache); temporary probe prompts are removed`,
|
|
373
|
+
"catalog-only alternative: tickmarkr doctor --refresh-catalog refreshes the catalog cache without adapter or model probes",
|
|
374
|
+
].join("\n");
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Health surfaces use this cache-only reader. It intentionally receives adapters only to render
|
|
378
|
+
* their configured identities; it never invokes adapter.probe(), listModels(), or a model command.
|
|
379
|
+
*/
|
|
380
|
+
export function cachedDoctorDiagnostics(cwd = process.cwd(), adapters = allAdapters(), cfg = loadConfig(cwd)) {
|
|
381
|
+
const health = readDoctor(cwd);
|
|
382
|
+
const age = doctorAgeMs(cwd);
|
|
383
|
+
const configured = configuredModels(cfg, adapters);
|
|
384
|
+
const state = stateDirName(cwd);
|
|
385
|
+
const knownAdapters = new Set(adapters.map((adapter) => adapter.id));
|
|
386
|
+
const rows = configured.map(({ adapter, model }) => {
|
|
387
|
+
const cachedAdapter = health?.[adapter];
|
|
388
|
+
const cachedModel = cachedAdapter?.modelAuth?.[model];
|
|
389
|
+
const status = !health
|
|
390
|
+
? "unknown (cache unavailable)"
|
|
391
|
+
: !knownAdapters.has(adapter)
|
|
392
|
+
? "unknown (adapter unavailable to this reader)"
|
|
393
|
+
: !cachedAdapter
|
|
394
|
+
? "unknown (adapter absent from cache)"
|
|
395
|
+
: !cachedAdapter.installed
|
|
396
|
+
? "unavailable (adapter not installed)"
|
|
397
|
+
: !cachedAdapter.authed
|
|
398
|
+
? "unavailable (adapter not authenticated)"
|
|
399
|
+
: !cachedModel
|
|
400
|
+
? "unknown (never probed)"
|
|
401
|
+
: cachedModel.authed
|
|
402
|
+
? `passed (cached ${cachedModel.probedAt})`
|
|
403
|
+
: `failed (cached ${cachedModel.reason ?? "probe failed"})`;
|
|
404
|
+
return ` ${adapter}:${model} ${status}`;
|
|
405
|
+
});
|
|
406
|
+
return [
|
|
407
|
+
"tickmarkr doctor — cached diagnostics:",
|
|
408
|
+
`cache source: ${health ? `${state}/doctor.json` : "unavailable"}`,
|
|
409
|
+
`cache age: ${cacheAge(age)}`,
|
|
410
|
+
"cache policy: read-only; adapter and model probes were not called",
|
|
411
|
+
...(rows.length ? ["configured model status:", ...rows] : ["configured model status: none (no configured models)"]),
|
|
412
|
+
].join("\n");
|
|
413
|
+
}
|
|
414
|
+
const RUNNER_CONFIG_PATHS = [
|
|
415
|
+
"jest.config.js",
|
|
416
|
+
"jest.config.cjs",
|
|
417
|
+
"jest.config.mjs",
|
|
418
|
+
"jest.config.ts",
|
|
419
|
+
"jest.config.json",
|
|
420
|
+
"vitest.config.ts",
|
|
421
|
+
"vitest.config.js",
|
|
422
|
+
"vitest.config.mts",
|
|
423
|
+
"vitest.config.mjs",
|
|
424
|
+
"vitest.config.cts",
|
|
425
|
+
];
|
|
426
|
+
function runnerConfigSnapshot(cwd) {
|
|
427
|
+
return new Map(RUNNER_CONFIG_PATHS.flatMap((path) => {
|
|
428
|
+
const absolute = join(cwd, path);
|
|
429
|
+
return existsSync(absolute) ? [[path, readFileSync(absolute, "utf8")]] : [];
|
|
430
|
+
}));
|
|
431
|
+
}
|
|
432
|
+
function actualTextDiff(path, before, after) {
|
|
433
|
+
const oldLines = before.split("\n");
|
|
434
|
+
const newLines = after.split("\n");
|
|
435
|
+
let start = 0;
|
|
436
|
+
while (start < oldLines.length && start < newLines.length && oldLines[start] === newLines[start])
|
|
437
|
+
start += 1;
|
|
438
|
+
let oldEnd = oldLines.length;
|
|
439
|
+
let newEnd = newLines.length;
|
|
440
|
+
while (oldEnd > start && newEnd > start && oldLines[oldEnd - 1] === newLines[newEnd - 1]) {
|
|
441
|
+
oldEnd -= 1;
|
|
442
|
+
newEnd -= 1;
|
|
443
|
+
}
|
|
444
|
+
return [
|
|
445
|
+
`--- ${path} (before)`,
|
|
446
|
+
`+++ ${path} (after)`,
|
|
447
|
+
...oldLines.slice(start, oldEnd).map((line) => `-${line}`),
|
|
448
|
+
...newLines.slice(start, newEnd).map((line) => `+${line}`),
|
|
449
|
+
].join("\n");
|
|
450
|
+
}
|
|
451
|
+
/** Repair is an actuator, but never a reason to run the paid diagnostic sensor. */
|
|
452
|
+
export function doctorFixOnly(cwd = process.cwd()) {
|
|
453
|
+
const before = runnerConfigSnapshot(cwd);
|
|
454
|
+
const finding = runnerIgnoreFinding(cwd);
|
|
455
|
+
const repair = finding?.verdict === "warn"
|
|
456
|
+
? applyRunnerIgnore(cwd)
|
|
457
|
+
: { action: "none", detail: finding?.detail ?? "no recognized test runner" };
|
|
458
|
+
const verified = runnerIgnoreFinding(cwd);
|
|
459
|
+
const diffs = [...before].flatMap(([path, original]) => {
|
|
460
|
+
const current = readFileSync(join(cwd, path), "utf8");
|
|
461
|
+
return current === original ? [] : [actualTextDiff(path, original, current)];
|
|
462
|
+
});
|
|
463
|
+
const result = repair.action === "wrote" && verified?.verdict !== "pass"
|
|
464
|
+
? "invalid repair result (verification did not pass)"
|
|
465
|
+
: `${repair.action} — ${repair.detail}`;
|
|
466
|
+
return [
|
|
467
|
+
"tickmarkr doctor --fix-only — repair-only:",
|
|
468
|
+
"model probes: skipped (repair-only; adapters were not called)",
|
|
469
|
+
"catalog: untouched",
|
|
470
|
+
`repair result: ${result}`,
|
|
471
|
+
`repair verification: ${verified?.verdict ?? "unavailable"}`,
|
|
472
|
+
"repair diff:",
|
|
473
|
+
...(diffs.length ? diffs : ["(no runner-config bytes changed)"]),
|
|
474
|
+
].join("\n");
|
|
475
|
+
}
|
|
350
476
|
export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(), opts = {}) {
|
|
351
|
-
if (_argv.
|
|
477
|
+
if (_argv.includes("--fix-only"))
|
|
478
|
+
return doctorFixOnly(cwd);
|
|
479
|
+
if (_argv.includes("--cached") || _argv.includes("--cached-only")) {
|
|
480
|
+
return cachedDoctorDiagnostics(cwd, adapters);
|
|
481
|
+
}
|
|
482
|
+
if (_argv.includes("--probe-preflight") || _argv.includes("--preflight")) {
|
|
483
|
+
return doctorProbePreflight(cwd, loadConfig(cwd), adapters);
|
|
484
|
+
}
|
|
485
|
+
if (_argv.length === 1 && (_argv[0] === "--refresh-catalog" || _argv[0] === "--catalog-only")) {
|
|
352
486
|
const refreshed = await refreshCatalogCommand({ repoRoot: cwd, fetcher: opts.catalogFetcher, now: opts.catalogNow });
|
|
353
487
|
return refreshed.updated
|
|
354
488
|
? `tickmarkr doctor --refresh-catalog: model catalog refreshed — ${formatCatalogRefreshLegs(refreshed.legs)}${refreshed.warning ? `; ${refreshed.warning}` : ""}`
|
|
@@ -378,9 +512,9 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
378
512
|
// banner at START — the logo greets the operator before the ~60s probe wait, never trailing it (operator report 2026-07-17)
|
|
379
513
|
if (opts.banner !== false && visual())
|
|
380
514
|
process.stdout.write(BANNER);
|
|
381
|
-
// stderr, live:
|
|
382
|
-
//
|
|
383
|
-
console.error(
|
|
515
|
+
// stderr, live: disclose the paid boundary before the first adapter/model call. The separate
|
|
516
|
+
// --probe-preflight route exposes these same bytes without crossing that boundary.
|
|
517
|
+
console.error(`${doctorProbePreflight(cwd, cfg, adapters)}\nprobing installed agent CLIs — one short LLM call per configured model, may take a minute...`);
|
|
384
518
|
const probeProgressTTY = process.stderr.isTTY === true;
|
|
385
519
|
const health = await probeAll(adapters, { cwd });
|
|
386
520
|
const kimiAdapter = adapters.find((a) => a.id === kimi.id);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export type DetectedHost = "herdr" | "orca" | "subprocess";
|
|
2
|
+
export declare function detectedHost(env?: NodeJS.ProcessEnv): DetectedHost;
|
|
1
3
|
/** Test seam mirroring FleetIO: production callers omit it and get the process streams. */
|
|
2
4
|
export type InitIO = {
|
|
3
5
|
input?: NodeJS.ReadStream;
|
|
@@ -4,6 +4,7 @@ import { join, relative } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
6
|
import { parseArgs } from "node:util";
|
|
7
|
+
import { parse as parseYaml } from "yaml";
|
|
7
8
|
import { allAdapters, formatDoctorAgeForInit, formatDoctorReport, initDoctorReuse } from "../../adapters/registry.js";
|
|
8
9
|
import { configTemplate, DEFAULT_CONFIG, globalConfigDir, loadConfig } from "../../config/config.js";
|
|
9
10
|
import { LEGACY_PREFIX, specTemplate } from "../../compile/native.js";
|
|
@@ -218,7 +219,14 @@ const describeDrift = (d) => [
|
|
|
218
219
|
// should not be asked again just because a copy drifted. Staleness is a different question with a
|
|
219
220
|
// different answer (`--agent --force`), and it is reported by name on every init path below.
|
|
220
221
|
const skillsInstalled = (cwd) => hostTargets(cwd).every((t) => AGENT_SKILLS.every((s) => existsSync(join(t.skillsDir, s, "SKILL.md"))));
|
|
221
|
-
|
|
222
|
+
export function detectedHost(env = process.env) {
|
|
223
|
+
if (env.HERDR_ENV === "1")
|
|
224
|
+
return "herdr";
|
|
225
|
+
if (orcaHostDetected(env))
|
|
226
|
+
return "orca";
|
|
227
|
+
return "subprocess";
|
|
228
|
+
}
|
|
229
|
+
const wizardDriverDefault = () => "auto";
|
|
222
230
|
function packageName(cwd) {
|
|
223
231
|
try {
|
|
224
232
|
const parsed = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
|
|
@@ -323,6 +331,7 @@ async function runInitWizard(cwd, input, output) {
|
|
|
323
331
|
visibilityLlm: DEFAULT_CONFIG.visibility.llm,
|
|
324
332
|
offerSkills: !skillsInstalled(cwd),
|
|
325
333
|
skillsDefault: existsSync(join(cwd, ".claude", "skills")) && !skillsInstalled(cwd),
|
|
334
|
+
detectedHost: detectedHost(),
|
|
326
335
|
},
|
|
327
336
|
input,
|
|
328
337
|
output,
|
|
@@ -368,8 +377,37 @@ export async function init(argv, cwd = process.cwd(), io = {}) {
|
|
|
368
377
|
}
|
|
369
378
|
const repoConfigPath = join(tickmarkrDir(cwd), "config.yaml");
|
|
370
379
|
const repoConfigExists = existsSync(repoConfigPath);
|
|
371
|
-
if (repoConfigExists)
|
|
372
|
-
|
|
380
|
+
if (repoConfigExists) {
|
|
381
|
+
if (values.force) {
|
|
382
|
+
const currentRaw = readFileSync(repoConfigPath, "utf8");
|
|
383
|
+
let parsedDriver;
|
|
384
|
+
try {
|
|
385
|
+
const parsed = parseYaml(currentRaw);
|
|
386
|
+
if (typeof parsed?.driver === "string")
|
|
387
|
+
parsedDriver = parsed.driver;
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
// malformed: fall back to regex
|
|
391
|
+
}
|
|
392
|
+
if (!parsedDriver) {
|
|
393
|
+
const m = currentRaw.match(/^\s*driver:\s*([^\s#]+)/m);
|
|
394
|
+
if (m)
|
|
395
|
+
parsedDriver = m[1];
|
|
396
|
+
}
|
|
397
|
+
const host = detectedHost();
|
|
398
|
+
if (parsedDriver && parsedDriver === host) {
|
|
399
|
+
const rewritten = currentRaw.replace(new RegExp(`^(\\s*driver:\\s*)${parsedDriver}\\b.*$`, "m"), `$1auto # auto — resolves to ${host} here`);
|
|
400
|
+
writeFileSync(repoConfigPath, rewritten);
|
|
401
|
+
notes.push(`rewrote driver: ${parsedDriver} to auto in ${repoConfigPath}`);
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
notes.push(`kept existing ${repoConfigPath}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
notes.push(`kept existing ${repoConfigPath}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
373
411
|
const specPath = join(cwd, SCAFFOLD_SPEC);
|
|
374
412
|
if (existsSync(specPath)) {
|
|
375
413
|
notes.push(`kept existing ${specPath}`);
|
|
@@ -417,7 +455,9 @@ export async function init(argv, cwd = process.cwd(), io = {}) {
|
|
|
417
455
|
// so costs one clause; everything else stops here. Esc and ctrl+c both land here.
|
|
418
456
|
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.`;
|
|
419
457
|
}
|
|
420
|
-
|
|
458
|
+
const host = detectedHost();
|
|
459
|
+
const renderedConfig = configTemplate(wizard.overlay).replace(/^driver: auto$/m, `driver: auto # auto — resolves to ${host} here`);
|
|
460
|
+
writeFileSync(repoConfigPath, renderedConfig);
|
|
421
461
|
notes.push(`wrote ${repoConfigPath}`);
|
|
422
462
|
if (wizard.installSkills) {
|
|
423
463
|
await installAgentFiles(cwd, values.force ?? false, wizard.installDocs || (values.docs ?? false), notes);
|
|
@@ -462,7 +502,7 @@ export async function init(argv, cwd = process.cwd(), io = {}) {
|
|
|
462
502
|
const next = nextSteps(cwd, SCAFFOLD_SPEC);
|
|
463
503
|
if (!visual())
|
|
464
504
|
return `${notes.join("\n")}\n${doc}\n${next}\n${ENVIRONMENTS_FOOTER}`;
|
|
465
|
-
const noteRows = notes.map((note) => ` ${statusRow(/^(?:wrote|overwrote|appended)/.test(note) ? "pass" : note.startsWith("skipped") ? "warn" : "neutral", note)}`);
|
|
505
|
+
const noteRows = notes.map((note) => ` ${statusRow(/^(?:wrote|overwrote|appended|rewrote)/.test(note) ? "pass" : note.startsWith("skipped") ? "warn" : "neutral", note)}`);
|
|
466
506
|
const footerRows = ENVIRONMENTS_FOOTER.split("\n").slice(1).map((line) => {
|
|
467
507
|
const separator = line.indexOf(" — ");
|
|
468
508
|
return kvRow(line.slice(2, separator), line.slice(separator + 1), 12);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
4
|
import { allAdapters, doctorAgeMs, modelAuthExclusions, probeAll, readDoctor, rolePools, servableExclusions, servabilityLine } from "../../adapters/registry.js";
|
|
@@ -7,7 +7,7 @@ import { GLYPHS, dim, rule, title, warn } from "../../brand.js";
|
|
|
7
7
|
import { collateralLints, sourceScopeLints } from "../../compile/collateral.js";
|
|
8
8
|
import { classifyContextPath } from "../../compile/native.js";
|
|
9
9
|
import { DEFAULT_CONFIG, effectiveReviewPolicy, overlayPreferShapes, ROUTING_MODES, TIER_RANK } from "../../config/config.js";
|
|
10
|
-
import { loadGraph } from "../../graph/graph.js";
|
|
10
|
+
import { graphDefinitionHash, loadGraph, stateDirName } from "../../graph/graph.js";
|
|
11
11
|
import { renderAcceptanceItem } from "../../graph/schema.js";
|
|
12
12
|
import { resolveRunMode } from "../../run/daemon.js";
|
|
13
13
|
import { disallowedBy, excludedChannels, exclusionLine, routingEntrySeatLines } from "../../route/preference.js";
|
|
@@ -15,7 +15,7 @@ import { staffLedEvidence } from "../../route/profile.js";
|
|
|
15
15
|
import { route, RoutingError } from "../../route/router.js";
|
|
16
16
|
import { auditNamedTestOracles, listVitestTests } from "../../gates/acceptance.js";
|
|
17
17
|
import { modelId, modelProvider, pickReviewer } from "../../gates/review.js";
|
|
18
|
-
import { Journal, loadRoutingProfile } from "../../run/journal.js";
|
|
18
|
+
import { Journal, loadRoutingProfile, recordedGraphDefinitionHash } from "../../run/journal.js";
|
|
19
19
|
import { harnessLine, resolveHarness } from "../harness.js";
|
|
20
20
|
import { channelKey, shq } from "../../adapters/types.js";
|
|
21
21
|
import { shGit } from "../../run/git.js";
|
|
@@ -40,7 +40,10 @@ const stylizePlan = (out) => {
|
|
|
40
40
|
const fleetCanCrossVendorReview = (channels) => {
|
|
41
41
|
for (let i = 0; i < channels.length; i++)
|
|
42
42
|
for (let j = 0; j < channels.length; j++)
|
|
43
|
-
if (i !== j
|
|
43
|
+
if (i !== j
|
|
44
|
+
&& channels[i].vendor !== channels[j].vendor
|
|
45
|
+
&& modelProvider(channels[i].model, channels[i].vendor) !== modelProvider(channels[j].model, channels[j].vendor)
|
|
46
|
+
&& modelId(channels[i].model) !== modelId(channels[j].model))
|
|
44
47
|
return true;
|
|
45
48
|
return false;
|
|
46
49
|
};
|
|
@@ -121,6 +124,43 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
121
124
|
throw new Error(`--mode must be one of ${ROUTING_MODES.join(" | ")} (got ${values.mode})`);
|
|
122
125
|
}
|
|
123
126
|
const g = loadGraph(cwd);
|
|
127
|
+
const gHash = graphDefinitionHash(g);
|
|
128
|
+
const runsRoot = join(cwd, stateDirName(cwd), "runs");
|
|
129
|
+
let matchingEvents;
|
|
130
|
+
let matchingRunId;
|
|
131
|
+
if (existsSync(runsRoot)) {
|
|
132
|
+
const locked = runLockRunId(cwd);
|
|
133
|
+
const candidates = readdirSync(runsRoot)
|
|
134
|
+
.filter((d) => d.startsWith("run-") && existsSync(join(runsRoot, d, "journal.jsonl")))
|
|
135
|
+
.sort();
|
|
136
|
+
const ordered = locked && candidates.includes(locked)
|
|
137
|
+
? [locked, ...candidates.filter((id) => id !== locked).reverse()]
|
|
138
|
+
: candidates.reverse();
|
|
139
|
+
for (const runId of ordered) {
|
|
140
|
+
try {
|
|
141
|
+
const events = Journal.open(cwd, runId).read();
|
|
142
|
+
if (recordedGraphDefinitionHash(events) === gHash) {
|
|
143
|
+
matchingEvents = events;
|
|
144
|
+
matchingRunId = runId;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// ignore unreadable or torn journals
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const hasRun = matchingEvents !== undefined;
|
|
154
|
+
const reviewHistory = matchingEvents
|
|
155
|
+
? matchingEvents
|
|
156
|
+
.filter((event) => event.event === "gate-result" && event.data.gate === "review" && typeof event.data.reviewer === "string")
|
|
157
|
+
.map((event) => event.data.reviewer)
|
|
158
|
+
: [];
|
|
159
|
+
// OBS-947 repair: a resumed run re-dispatches only tasks whose replayed status is pending (or that the
|
|
160
|
+
// journal never mentions). done, failed and human are all skipped alike, so rendering their preview
|
|
161
|
+
// picks must not advance the shared reviewHistory — a phantom draw wraps the LRU and makes a later
|
|
162
|
+
// pending task print the ranked-first seat while the journal's actual next draw is a later channel.
|
|
163
|
+
const taskStatuses = matchingRunId ? Journal.open(cwd, matchingRunId).replayStatuses() : new Map();
|
|
124
164
|
const oracleRefusals = new Map();
|
|
125
165
|
const oracleAdvisories = new Map();
|
|
126
166
|
if (g.tasks.some((task) => task.acceptance.some((item) => typeof item === "object" && item.oracle === "test"))) {
|
|
@@ -306,10 +346,22 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
306
346
|
const policy = effectiveReviewPolicy(task.files, cfg.review);
|
|
307
347
|
if (policy === "judge-only")
|
|
308
348
|
return { line: "none — reviewPolicy judge-only: declared leaf paths", cost: 0 };
|
|
309
|
-
|
|
349
|
+
let rotationSeat = 1;
|
|
350
|
+
let rotationCount = 0;
|
|
351
|
+
const reviewer = pickReviewer(author, pools.review, [], cfg.review.prefer ?? [], task.routingHints?.floor, reviewHistory, (seat, count) => { rotationSeat = seat; rotationCount = count; });
|
|
310
352
|
if (!reviewer)
|
|
311
353
|
return { line: "none — no cross-vendor reviewer available", cost: 0 };
|
|
312
|
-
|
|
354
|
+
// Advance the simulated rotation only for tasks the run would actually dispatch next — the same
|
|
355
|
+
// rule as readyTasks (src/graph/graph.ts): pending, with every dependency already done. A pending
|
|
356
|
+
// task behind a parked/failed/pending dependency cannot run, so it consumes no draw (RULING-229-14).
|
|
357
|
+
const isPending = (id) => { const s = taskStatuses.get(id); return s === undefined || s === "pending"; };
|
|
358
|
+
const ready = isPending(task.id) && task.deps.every((d) => taskStatuses.get(d) === "done");
|
|
359
|
+
if (hasRun && ready)
|
|
360
|
+
reviewHistory.push(channelKey(reviewer));
|
|
361
|
+
const rotationNote = rotationCount > 1
|
|
362
|
+
? (hasRun ? ` (seat ${rotationSeat} of ${rotationCount})` : ` (seat 1 of ${rotationCount} — rotation applies at run time)`)
|
|
363
|
+
: "";
|
|
364
|
+
return { line: `${channelKey(reviewer)}${rotationNote} — reviewPolicy full`, cost: seatCost(reviewer) };
|
|
313
365
|
};
|
|
314
366
|
const routed = [];
|
|
315
367
|
for (const t of g.tasks) {
|