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.
- package/README.md +7 -3
- package/dist/adapters/catalog-remote.d.ts +23 -0
- package/dist/adapters/catalog-remote.js +38 -0
- package/dist/adapters/claude-code.d.ts +1 -1
- package/dist/adapters/claude-code.js +4 -1
- package/dist/adapters/codex-commit-check.d.ts +36 -0
- package/dist/adapters/codex-commit-check.js +205 -0
- package/dist/adapters/codex.d.ts +1 -0
- package/dist/adapters/codex.js +12 -6
- package/dist/adapters/model-discovery-check.d.ts +33 -0
- package/dist/adapters/model-discovery-check.js +123 -0
- package/dist/adapters/model-lints.d.ts +1 -1
- package/dist/adapters/model-lints.js +4 -0
- package/dist/adapters/prompt.js +3 -2
- package/dist/adapters/types.d.ts +10 -0
- package/dist/cli/commands/approve.js +10 -4
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.js +17 -3
- package/dist/cli/commands/fleet.d.ts +10 -1
- package/dist/cli/commands/fleet.js +363 -106
- package/dist/cli/commands/init.js +10 -3
- package/dist/cli/commands/plan.js +76 -4
- package/dist/cli/commands/verify.d.ts +1 -1
- package/dist/cli/commands/verify.js +65 -15
- package/dist/config/fleet-overlay.d.ts +16 -1
- package/dist/config/fleet-overlay.js +230 -28
- package/dist/drivers/orca.d.ts +34 -2
- package/dist/drivers/orca.js +206 -32
- package/dist/gates/cache.d.ts +4 -0
- package/dist/gates/cache.js +10 -2
- package/dist/gates/llm.d.ts +4 -1
- package/dist/gates/llm.js +6 -1
- package/dist/gates/review.js +8 -3
- package/dist/gates/run-gates.d.ts +5 -3
- package/dist/gates/run-gates.js +112 -56
- package/dist/gates/test-manifest.d.ts +3 -0
- package/dist/gates/test-manifest.js +1 -1
- package/dist/gates/timeout-shaped.d.ts +3 -0
- package/dist/gates/timeout-shaped.js +10 -0
- package/dist/run/daemon.js +326 -130
- package/dist/run/git.js +9 -0
- package/dist/run/journal.d.ts +6 -2
- package/dist/run/journal.js +22 -7
- package/dist/run/launchservices-check.d.ts +56 -0
- package/dist/run/launchservices-check.js +117 -0
- package/dist/run/operator-state.d.ts +22 -2
- package/dist/run/operator-state.js +20 -3
- package/dist/run/title-environment.d.ts +45 -0
- package/dist/run/title-environment.js +127 -0
- package/dist/run/title-preload.cjs +38 -0
- package/dist/run/title-preload.d.cts +1 -0
- package/dist/tui/cockpit/board.js +12 -6
- package/dist/tui/cockpit/keys.d.ts +8 -0
- package/dist/tui/cockpit/keys.js +4 -1
- package/dist/tui/cockpit/layout.d.ts +2 -2
- package/dist/tui/cockpit/layout.js +2 -2
- package/dist/tui/cockpit/live-runtime.js +46 -20
- package/dist/tui/cockpit/live-store.d.ts +24 -1
- package/dist/tui/cockpit/live-store.js +99 -8
- package/dist/tui/cockpit/log-view.d.ts +28 -0
- package/dist/tui/cockpit/log-view.js +60 -0
- package/dist/tui/cockpit/run-view.js +4 -0
- package/dist/tui/cockpit/shell.d.ts +2 -1
- package/dist/tui/cockpit/shell.js +6 -4
- package/dist/tui/ink/fleet-app.d.ts +37 -10
- package/dist/tui/ink/fleet-app.js +65 -21
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +32 -2
- package/skills/tickmarkr-loop/SKILL.md +46 -2
- package/skills/tickmarkr-overseer/SKILL.md +83 -8
- package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +58 -16
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +19 -6
- package/skills/tickmarkr-overseer/scripts/watch-parks.sh +121 -64
|
@@ -132,7 +132,11 @@ you were handed the whole picture — list the repository root, open every guida
|
|
|
132
132
|
- **The gates are the product.** Seven, defined in \`src/graph/schema.ts\`:
|
|
133
133
|
\`build test lint evidence scope acceptance review\`. **That is DECLARATION order, not execution order** —
|
|
134
134
|
the first five run as a battery that stops at its first red, then \`acceptance\` and \`review\` run
|
|
135
|
-
CONCURRENTLY (\`run-gates.ts:39\`, *"judge ‖ review"*).
|
|
135
|
+
CONCURRENTLY (\`run-gates.ts:39\`, *"judge ‖ review"*). One repair reorders them: a repair the REVIEW sent
|
|
136
|
+
back (carried \`review:material\`, test selection on, no required repair test) runs build, lint, evidence and
|
|
137
|
+
scope, then \`acceptance\` ‖ \`review\`, then its test screen, so a review that re-reds an unchanged subject
|
|
138
|
+
starts no test at all; a test-red repair keeps the battery first. Either way nothing merges until the FULL
|
|
139
|
+
suite has passed on the merge-candidate commit. The first five are MANDATORY; only \`acceptance\` and
|
|
136
140
|
\`review\` may be omitted per task. Implementations are in \`src/gates/\` — \`baseline.ts\` (build/test/lint,
|
|
137
141
|
diffed against a recorded baseline so pre-existing failures are forgiven), \`evidence.ts\`, \`scope.ts\`,
|
|
138
142
|
\`acceptance.ts\` (its judge reads the DIFF and every criterion must cite a changed hunk), \`review.ts\`
|
|
@@ -635,7 +639,8 @@ export async function init(argv, cwd = process.cwd(), io = {}) {
|
|
|
635
639
|
const interactive = input.isTTY === true && output.isTTY === true && !(values.yes ?? false);
|
|
636
640
|
// Act 1 — preferences. Only when the repo config does not exist yet: init never rewrites an
|
|
637
641
|
// operator's config; re-tuning is `tickmarkr fleet` (act 3 below still runs). Act 1 writes only
|
|
638
|
-
// project execution preferences; model choices are act 3's, saved
|
|
642
|
+
// project execution preferences; model choices are act 3's, saved by Fleet's one destination rule:
|
|
643
|
+
// the USER overlay (B2), except a membership edit the repo overlay holds (T10).
|
|
639
644
|
if (!repoConfigExists) {
|
|
640
645
|
if (interactive) {
|
|
641
646
|
emitBanner();
|
|
@@ -673,7 +678,9 @@ export async function init(argv, cwd = process.cwd(), io = {}) {
|
|
|
673
678
|
// Act 3 — fleet. The compact discovery surface prints BETWEEN the acts so the operator reads
|
|
674
679
|
// what the fleet browser (and its presets overlay, raised on the first Shapes entry) ranks
|
|
675
680
|
// with; the final summary then carries a pointer, not a repeat. Its confirmation is the shared
|
|
676
|
-
// Fleet actuator's:
|
|
681
|
+
// Fleet actuator's: a preset (routing.mode) lands in the user overlay (gdir), and so does every
|
|
682
|
+
// choice except a routing.allow/deny membership edit this repo overlay holds — T10 writes that one
|
|
683
|
+
// where it is effective, and the review names the destination before y.
|
|
677
684
|
if (interactive) {
|
|
678
685
|
output.write(`${doc}\n`);
|
|
679
686
|
doc = "discovery shown above — full matrix any time with `tickmarkr doctor`";
|
|
@@ -2,6 +2,7 @@ import { existsSync, readdirSync, readFileSync } 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";
|
|
5
|
+
import { aliasIdentityHeadlines } from "../../adapters/model-discovery-check.js";
|
|
5
6
|
import { formatModelAuthLine, contextWindowLints, modelLints, preferEntryLints, ttyVisual } from "../../adapters/model-lints.js";
|
|
6
7
|
import { GLYPHS, dim, rule, title, warn } from "../../brand.js";
|
|
7
8
|
import { collateralLints, sourceScopeLints } from "../../compile/collateral.js";
|
|
@@ -15,9 +16,11 @@ import { disallowedBy, excludedChannels, exclusionLine, observedSeat, routingEnt
|
|
|
15
16
|
import { decayWeight, HALF_LIFE_RUNS, staffLedEvidence } from "../../route/profile.js";
|
|
16
17
|
import { resolvedFloor, route, RoutingError } from "../../route/router.js";
|
|
17
18
|
import { auditNamedTestOracles, listVitestTests } from "../../gates/acceptance.js";
|
|
18
|
-
import { modelId, modelProvider, pickReviewer } from "../../gates/review.js";
|
|
19
|
+
import { carriedAuthorVendors, gateReviewerFloor, modelId, modelProvider, pickReviewer } from "../../gates/review.js";
|
|
19
20
|
import { Journal, loadRoutingProfile, readProfileCursor, recordedGraphDefinitionHash, RUNS_WINDOW } from "../../run/journal.js";
|
|
20
21
|
import { harnessLine, resolveHarness } from "../harness.js";
|
|
22
|
+
import { codexCommitHeadline, codexEscapeRoleLine } from "../../adapters/codex-commit-check.js";
|
|
23
|
+
import { codex } from "../../adapters/codex.js";
|
|
21
24
|
import { channelKey, shq } from "../../adapters/types.js";
|
|
22
25
|
import { shGit } from "../../run/git.js";
|
|
23
26
|
import { runLockRunId, runStatusLine } from "../../run/lock.js";
|
|
@@ -135,6 +138,13 @@ async function taskInputFindings(tasks, cwd) {
|
|
|
135
138
|
}
|
|
136
139
|
return findings;
|
|
137
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* v2.6.5 T9 (D-862): a task the run never dispatched gates under the fresh route's author, so plan can
|
|
143
|
+
* mirror its review eligibility exactly. Once dispatched, resume state only the daemon decides (a restored
|
|
144
|
+
* or failed-over author, a tier climb, carried commits, a parked recheck) picks the author — fail closed.
|
|
145
|
+
* ponytail: unknown, not exact; call production's own resume functions once daemon.ts can export them (POOL).
|
|
146
|
+
*/
|
|
147
|
+
const resumeDecidesAuthor = (events, taskId) => events.some((row) => row.taskId === taskId && row.event === "task-dispatch");
|
|
138
148
|
// v1.89 T4: harnessFrom is the resolver's INPUT — a caller (the byte-pinned goldens) fixes the location
|
|
139
149
|
// and keeps this machine's absolute paths out of a fixture. The default is the INVOKED entrypoint,
|
|
140
150
|
// `process.argv[1]`: the bin symlink a global install puts on PATH, which resolves to dist/cli/index.js.
|
|
@@ -183,6 +193,26 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
183
193
|
.filter((event) => event.event === "gate-result" && event.data.gate === "review" && typeof event.data.reviewer === "string")
|
|
184
194
|
.map((event) => event.data.reviewer)
|
|
185
195
|
: [];
|
|
196
|
+
// daemon.ts replayedReviewerExclusions: a journaled review-pool-demotion is excluded from every review
|
|
197
|
+
// pick of the resumed run this plan previews (reviewer role only).
|
|
198
|
+
const replayedReviewerExclusions = [...new Set((matchingEvents ?? [])
|
|
199
|
+
.filter((event) => event.event === "review-pool-demotion" && typeof event.data.reviewer === "string")
|
|
200
|
+
.map((event) => event.data.reviewer))];
|
|
201
|
+
const demotedReviewers = new Set(replayedReviewerExclusions);
|
|
202
|
+
// daemon.ts demotedChannels: the resumed run's worker exclusions (journal replayExcludedChannels).
|
|
203
|
+
const runJournal = matchingRunId ? Journal.open(cwd, matchingRunId) : undefined;
|
|
204
|
+
const demotedWorkers = runJournal?.replayExcludedChannels() ?? new Set();
|
|
205
|
+
// run-gates.ts retired (OBS-1025 add.2): seats with two no-verdicts this run are out of every pick, tallied
|
|
206
|
+
// as daemon.ts seeds reviewNoVerdicts — noVerdict rows only, and a checkout-proof timeout never strikes.
|
|
207
|
+
const noVerdicts = new Map();
|
|
208
|
+
for (const event of matchingEvents ?? []) {
|
|
209
|
+
if (event.event !== "review-no-verdict" || event.data.noVerdict !== true || typeof event.data.reviewer !== "string")
|
|
210
|
+
continue;
|
|
211
|
+
if (event.data.cause === "seat-launch-failed" && event.data.launchCause === "checkout-proof-timeout")
|
|
212
|
+
continue;
|
|
213
|
+
noVerdicts.set(event.data.reviewer, (noVerdicts.get(event.data.reviewer) ?? 0) + 1);
|
|
214
|
+
}
|
|
215
|
+
const retiredReviewers = [...noVerdicts].filter(([, n]) => n >= 2).map(([seat]) => seat);
|
|
186
216
|
// OBS-947 repair: a resumed run re-dispatches only tasks whose replayed status is pending (or that the
|
|
187
217
|
// journal never mentions). done, failed and human are all skipped alike, so rendering their preview
|
|
188
218
|
// picks must not advance the shared reviewHistory — a phantom draw wraps the LRU and makes a later
|
|
@@ -243,9 +273,23 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
243
273
|
if (judgeDeny?.by === "deny") {
|
|
244
274
|
lines.push(`REFUSAL OBS-576: judge seat ${cfg.judge.adapter}:${cfg.judge.model} is removed by flat routing.deny (${judgeDeny.entry}) — move worker-only policy to routing.deny.workers`, "");
|
|
245
275
|
}
|
|
276
|
+
// v2.6.5 T9 (H): alias drift is read from the identity doctor persisted, never re-probed here.
|
|
277
|
+
const aliasHeadlines = aliasIdentityHeadlines(cfg, health);
|
|
278
|
+
if (aliasHeadlines.length)
|
|
279
|
+
lines.push(...aliasHeadlines, "");
|
|
246
280
|
const entrySeats = routingEntrySeatLines(cfg);
|
|
247
281
|
if (entrySeats.length)
|
|
248
282
|
lines.push(...entrySeats, "");
|
|
283
|
+
// v2.6.6 T9 (K): Codex judge/review/consult seats launch with the worker grant (llm.ts, consult.ts), so a
|
|
284
|
+
// doctor-probed escape blocks them too; worker rows carry their own headline below.
|
|
285
|
+
const codexRoles = [
|
|
286
|
+
...(cfg.judge.adapter === codex.id ? ["judge"] : []),
|
|
287
|
+
...(pools.review.some((c) => c.adapter === codex.id) ? ["review"] : []),
|
|
288
|
+
...(cfg.consult.adapter === codex.id || pools.consult.some((c) => c.adapter === codex.id) ? ["consult"] : []),
|
|
289
|
+
];
|
|
290
|
+
const codexEscape = codexEscapeRoleLine(codexRoles, adapters, health);
|
|
291
|
+
if (codexEscape)
|
|
292
|
+
lines.push(codexEscape, "");
|
|
249
293
|
// OBS-1185: the floor route() resolved for THIS task — a task hint over the configured/mode floor.
|
|
250
294
|
const derivation = (t) => {
|
|
251
295
|
const floor = resolvedFloor(t, cfg);
|
|
@@ -359,9 +403,36 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
359
403
|
return { line: "none — reviewPolicy judge-only: declared leaf paths", cost: 0 };
|
|
360
404
|
let rotationSeat = 1;
|
|
361
405
|
let rotationCount = 0;
|
|
362
|
-
|
|
406
|
+
// Dispatched before: no count, no single-seat claim, no previewed reviewer (and so no rotation draw this
|
|
407
|
+
// plan could name). Never a guess.
|
|
408
|
+
if (resumeDecidesAuthor(matchingEvents ?? [], task.id)) {
|
|
409
|
+
lints.push(`${task.id}: eligible cross-vendor review seat count unknown — resume state decides the author`);
|
|
410
|
+
return { line: "unknown — resume state decides the author", cost: 0 };
|
|
411
|
+
}
|
|
412
|
+
// Never dispatched, daemon.ts subjectAuthors is just the fresh author; its vendor stays off the review.
|
|
413
|
+
const authors = [channelKey(author)];
|
|
414
|
+
const authorVendors = carriedAuthorVendors(pools.review, authors);
|
|
415
|
+
// Production's inputs as the task's first review round sees them: excludeReviewers = daemon badReviewers =
|
|
416
|
+
// replayedReviewerExclusions. run-gates.ts:1303 floors on THIS task's journaled reviewers at their dispatched
|
|
417
|
+
// tiers (daemon taskReviewers) ∪ excludeReviewers, so a replayed exclusion RAISES the floor; :1305 picks
|
|
418
|
+
// with excludeReviewers ∪ retired. prefer only reorders (the seat count below passes none).
|
|
419
|
+
const priorReviewers = [...(matchingEvents ?? []).filter((e) => e.taskId === task.id && typeof e.data.reviewer === "string"
|
|
420
|
+
&& ((e.event === "gate-result" && e.data.gate === "review") || e.event === "review-no-verdict"))
|
|
421
|
+
.map((e) => ({ reviewer: e.data.reviewer, tier: e.data.reviewerTier })), ...replayedReviewerExclusions];
|
|
422
|
+
const reviewFloor = gateReviewerFloor(task, cfg, author, pools.review, priorReviewers).floor;
|
|
423
|
+
const exclusions = [...replayedReviewerExclusions, ...retiredReviewers];
|
|
424
|
+
const reviewer = pickReviewer(author, pools.review, exclusions, cfg.review.prefer ?? [], reviewFloor, reviewHistory, (seat, count) => { rotationSeat = seat; rotationCount = count; }, demotedReviewers, authorVendors, authors);
|
|
363
425
|
if (!reviewer)
|
|
364
426
|
return { line: "none — no cross-vendor reviewer available", cost: 0 };
|
|
427
|
+
// v2.6.5 T9 (H): one flaked or demoted seat away from no reviewer. Enumerate the production filter
|
|
428
|
+
// (pickReviewer at the gate's floor) and count served identities, so two aliases of one model count once.
|
|
429
|
+
const seats = [];
|
|
430
|
+
for (let s; (s = pickReviewer(author, pools.review, [...exclusions, ...seats.map(channelKey)], [], reviewFloor, [], undefined, demotedReviewers, authorVendors, authors));)
|
|
431
|
+
seats.push(s);
|
|
432
|
+
const identities = new Set(seats.map((c) => c.identity ?? modelId(c.model)));
|
|
433
|
+
if (identities.size === 1) {
|
|
434
|
+
lints.push(`${task.id}: exactly one eligible cross-vendor review seat remains — identity ${[...identities][0]} via ${seats.map(channelKey).join(", ")}; one flake or demotion leaves no reviewer`);
|
|
435
|
+
}
|
|
365
436
|
// Advance the simulated rotation only for tasks the run would actually dispatch next — the same
|
|
366
437
|
// rule as readyTasks (src/graph/graph.ts): pending, with every dependency already done. A pending
|
|
367
438
|
// task behind a parked/failed/pending dependency cannot run, so it consumes no draw (RULING-229-14).
|
|
@@ -402,7 +473,8 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
402
473
|
continue;
|
|
403
474
|
}
|
|
404
475
|
try {
|
|
405
|
-
|
|
476
|
+
// daemon.ts:3322: the resumed run routes around its replayed worker exclusions.
|
|
477
|
+
const r = route(t, cfg, channels, dispatchProfile, undefined, demotedWorkers);
|
|
406
478
|
routed.push({ taskId: t.id, adapter: r.assignment.adapter, model: r.assignment.model });
|
|
407
479
|
for (const l of r.lints) {
|
|
408
480
|
const suf = exclusionReason(l);
|
|
@@ -412,7 +484,7 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
412
484
|
const judge = judgeSeat(t);
|
|
413
485
|
const review = reviewSeat(t, r.assignment);
|
|
414
486
|
cost += est + judge.cost + review.cost;
|
|
415
|
-
lines.push(` ${t.id.padEnd(6)} ${t.shape.padEnd(10)} c${String(t.complexity).padEnd(3)}→ ${r.assignment.adapter}:${r.assignment.model} [${r.assignment.channel}/${r.assignment.tier}]${t.timeoutMinutes !== undefined ? ` (timeout ${t.timeoutMinutes}m)` : ""}${t.humanGate ? " (human gate)" : ""}${est ? ` ~$${est.toFixed(2)}` : ""} — ${r.provenance}`, ` judge: ${judge.line}${judge.cost ? ` ~$${judge.cost.toFixed(2)}` : ""}`, ` review: ${review.line}${review.cost ? ` ~$${review.cost.toFixed(2)}` : ""}`, chainLine(t));
|
|
487
|
+
lines.push(` ${t.id.padEnd(6)} ${t.shape.padEnd(10)} c${String(t.complexity).padEnd(3)}→ ${r.assignment.adapter}:${r.assignment.model} [${r.assignment.channel}/${r.assignment.tier}]${t.timeoutMinutes !== undefined ? ` (timeout ${t.timeoutMinutes}m)` : ""}${t.humanGate ? " (human gate)" : ""}${est ? ` ~$${est.toFixed(2)}` : ""} — ${r.provenance}`, ...[codexCommitHeadline(r.assignment.adapter, adapters, health)].filter((l) => l !== null), ` judge: ${judge.line}${judge.cost ? ` ~$${judge.cost.toFixed(2)}` : ""}`, ` review: ${review.line}${review.cost ? ` ~$${review.cost.toFixed(2)}` : ""}`, chainLine(t));
|
|
416
488
|
const d = derivation(t);
|
|
417
489
|
if (d)
|
|
418
490
|
lines.push(d);
|
|
@@ -7,7 +7,7 @@ export declare const HUMAN_AUTHOR: Assignment;
|
|
|
7
7
|
export declare function verifyStateDir(cwd: string): string;
|
|
8
8
|
/** Linked worktrees share operational state with the repository that owns the common git dir. */
|
|
9
9
|
export declare function verifyStateRoot(cwd: string): Promise<string>;
|
|
10
|
-
/**
|
|
10
|
+
/** Repository content plus the effective runner inputs the daemon hashes; never a checkout path. */
|
|
11
11
|
export declare function baselineCachePath(cwd: string, baseSha: string, commands: Record<string, string>): string;
|
|
12
12
|
export declare function excludeAuthorProvider(channels: BillingChannel[], author: BillingChannel): BillingChannel[];
|
|
13
13
|
export declare const VERIFY_HELP = "usage: tickmarkr verify [--base <ref>] [--criteria <file> | --task <id>] [--json]\nThe final verdict and JSON result are written to stdout; progress and diagnostics are written to stderr.\nDo not merge stdout and stderr (for example with 2>&1): doing so corrupts the verdict stream.";
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, utimesSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
|
-
import { allAdapters, probeAll, readDoctor, rolePools } from "../../adapters/registry.js";
|
|
6
|
+
import { allAdapters, probeAll, readDoctor, rolePools, writeDoctor } from "../../adapters/registry.js";
|
|
7
7
|
import { channelKey } from "../../adapters/types.js";
|
|
8
8
|
import { loadConfig } from "../../config/config.js";
|
|
9
9
|
import { captureBaseline, detectGateCommands, staleFileCountCommands } from "../../gates/baseline.js";
|
|
10
|
-
import { getVerdictStore } from "../../gates/cache.js";
|
|
10
|
+
import { getVerdictStore, runnerInputsHash } from "../../gates/cache.js";
|
|
11
11
|
import { modelProvider } from "../../gates/review.js";
|
|
12
12
|
import { runGates } from "../../gates/run-gates.js";
|
|
13
13
|
import { getTask, loadGraph } from "../../graph/graph.js";
|
|
@@ -17,6 +17,7 @@ import { linkNodeModules, removeWorktree, shGit, shGitOk } from "../../run/git.j
|
|
|
17
17
|
import { foldOwedChecks, integrationMapped, owedCriteria, Journal, OWED_DISCHARGE_EVENT } from "../../run/journal.js";
|
|
18
18
|
import { withRepositoryLease } from "../../run/lease.js";
|
|
19
19
|
import { isPidLive } from "../../run/lock.js";
|
|
20
|
+
import { recordLaunchServices } from "../../run/launchservices-check.js";
|
|
20
21
|
/**
|
|
21
22
|
* tickmarkr verify — the gate battery as a standalone command (OPERATING-MODEL-2026-08-11 item 3).
|
|
22
23
|
*
|
|
@@ -86,7 +87,7 @@ const hashParts = (parts) => {
|
|
|
86
87
|
hash.update(part);
|
|
87
88
|
return hash.digest("hex").slice(0, 12);
|
|
88
89
|
};
|
|
89
|
-
/**
|
|
90
|
+
/** Repository content plus the effective runner inputs the daemon hashes; never a checkout path. */
|
|
90
91
|
export function baselineCachePath(cwd, baseSha, commands) {
|
|
91
92
|
const lockParts = [];
|
|
92
93
|
for (const file of LOCKFILES) {
|
|
@@ -94,13 +95,44 @@ export function baselineCachePath(cwd, baseSha, commands) {
|
|
|
94
95
|
lockParts.push(file, existsSync(path) ? readFileSync(path) : "<absent>");
|
|
95
96
|
}
|
|
96
97
|
const commandParts = Object.entries(commands).sort(([a], [b]) => a.localeCompare(b)).map(([name, command]) => `${name}\0${command}\0`);
|
|
97
|
-
return join(realpathSync(tmpdir()), "tickmarkr-verify", "cache", `baseline-${baseSha.slice(0, 12)}-${hashParts(lockParts)}-${hashParts(commandParts)}.json`);
|
|
98
|
+
return join(realpathSync(tmpdir()), "tickmarkr-verify", "cache", `baseline-${baseSha.slice(0, 12)}-${hashParts(lockParts)}-${hashParts(commandParts)}-${runnerInputsHash()}.json`);
|
|
98
99
|
}
|
|
99
100
|
const verdictlessCommands = (baseline, commands) => Object.keys(commands).filter((name) => baseline.commands[name]?.exitCode === undefined);
|
|
100
101
|
export function excludeAuthorProvider(channels, author) {
|
|
101
102
|
const provider = modelProvider(author.model, author.vendor);
|
|
102
103
|
return [author, ...channels.filter((candidate) => candidate !== author && modelProvider(candidate.model, candidate.vendor) !== provider)];
|
|
103
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* G (D-874): `--author` names who wrote the diff so review can EXCLUDE it — it never asks for a seat. So
|
|
107
|
+
* it resolves against installed identities (an installed adapter's doctor-listed or configured model),
|
|
108
|
+
* not against the review pool, whose role allow/deny, model-auth and tier gates say nothing about
|
|
109
|
+
* authorship. A pool member resolves to itself, unchanged. Anything else is an author-only identity: its
|
|
110
|
+
* vendor is the configured declaration (the model's override, else the adapter entry's; the adapter's own
|
|
111
|
+
* only when config has no entry), never inferred, and an unseeded model carries no tier of its own — it
|
|
112
|
+
* holds the human sentinel's frontier floor, so the reviewer floor is never lowered. Same-vendor, so it
|
|
113
|
+
* can never be seated. An identity nothing declares refuses by name.
|
|
114
|
+
*/
|
|
115
|
+
function authorIdentity(claim, cfg, adapters, health, pool) {
|
|
116
|
+
const [adapter = "", ...rest] = claim.split(":");
|
|
117
|
+
const model = rest.join(":");
|
|
118
|
+
const member = pool.find((c) => c.adapter === adapter && c.model === model);
|
|
119
|
+
if (member)
|
|
120
|
+
return member;
|
|
121
|
+
const a = adapters.find((x) => x.id === adapter);
|
|
122
|
+
const h = health[adapter];
|
|
123
|
+
const seeded = a?.channels(cfg).find((c) => c.model === model);
|
|
124
|
+
const entry = cfg.tiers[adapter];
|
|
125
|
+
const vendor = seeded?.vendor ?? (entry ? entry.modelOverrides?.[model]?.vendor ?? entry.vendor : a?.vendor);
|
|
126
|
+
const why = !a ? `no registered adapter "${adapter}"`
|
|
127
|
+
: !h?.installed ? `doctor does not record ${adapter} as installed`
|
|
128
|
+
: !seeded && !h.models?.includes(model) ? `${adapter} neither lists "${model}" in doctor nor configures it`
|
|
129
|
+
: !vendor ? `no vendor is declared for ${adapter}:${model}`
|
|
130
|
+
: undefined;
|
|
131
|
+
if (why) {
|
|
132
|
+
throw new Error(`--author ${claim} does not name a discoverable author identity (${why}) — name an installed adapter's doctor-listed or configured model as <adapter>:<model>, or human`);
|
|
133
|
+
}
|
|
134
|
+
return seeded ?? { adapter, vendor: vendor, model, channel: entry?.modelOverrides?.[model]?.channel ?? entry?.channel ?? "sub", tier: "frontier" };
|
|
135
|
+
}
|
|
104
136
|
function recordedMerges(repoRoot) {
|
|
105
137
|
const runs = join(repoRoot, ".tickmarkr", "runs");
|
|
106
138
|
if (!existsSync(runs))
|
|
@@ -287,6 +319,9 @@ export async function verify(argv, cwd = process.cwd(), options = {}) {
|
|
|
287
319
|
const pools = rolePools(cfg, adapters, health);
|
|
288
320
|
judgeChannels = pools.judge;
|
|
289
321
|
channels = pools.review;
|
|
322
|
+
// Resolved before either branch so an undiscoverable claim refuses here, ahead of any capture —
|
|
323
|
+
// even where recorded authors bind and the claim is otherwise ignored.
|
|
324
|
+
const claimed = values.author && values.author !== "human" ? authorIdentity(values.author, cfg, adapters, health, channels) : undefined;
|
|
290
325
|
if (owed.length) {
|
|
291
326
|
// The run's lifetime patch authors bind the reviewer's exclusions; --author never overrides
|
|
292
327
|
// them. An author the pool cannot resolve leaves review unseatable, which fails closed.
|
|
@@ -297,15 +332,9 @@ export async function verify(argv, cwd = process.cwd(), options = {}) {
|
|
|
297
332
|
if (first)
|
|
298
333
|
author = { adapter: first.adapter, model: first.model, channel: first.channel, tier: first.tier };
|
|
299
334
|
}
|
|
300
|
-
else if (
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const c = channels.find((ch) => ch.adapter === adapter && ch.model === model);
|
|
304
|
-
if (!c) {
|
|
305
|
-
throw new Error(`--author ${values.author} does not name a discoverable review channel — one of: ${channels.map(channelKey).join(", ") || "(none)"}`);
|
|
306
|
-
}
|
|
307
|
-
author = { adapter: c.adapter, model: c.model, channel: c.channel, tier: c.tier };
|
|
308
|
-
channels = excludeAuthorProvider(channels, c);
|
|
335
|
+
else if (claimed) {
|
|
336
|
+
author = { adapter: claimed.adapter, model: claimed.model, channel: claimed.channel, tier: claimed.tier };
|
|
337
|
+
channels = excludeAuthorProvider(channels, claimed);
|
|
309
338
|
}
|
|
310
339
|
else {
|
|
311
340
|
channels = [...channels, HUMAN_CHANNEL];
|
|
@@ -378,7 +407,7 @@ export async function verify(argv, cwd = process.cwd(), options = {}) {
|
|
|
378
407
|
console.error(`verify: reusing cached baseline for ${mergeBase.slice(0, 12)} (${names.join(", ")}: ${cachePath})`);
|
|
379
408
|
return cached;
|
|
380
409
|
}
|
|
381
|
-
console.error(`verify: capturing baseline at merge-base ${mergeBase.slice(0, 12)} for ${names.join(", ")} (cached by base, lockfile and
|
|
410
|
+
console.error(`verify: capturing baseline at merge-base ${mergeBase.slice(0, 12)} for ${names.join(", ")} (cached by base, lockfile, command and effective runner hashes at ${cachePath})`);
|
|
382
411
|
// Unleased captures of the SAME checkout overlap, so each owns a unique base worktree and removes
|
|
383
412
|
// only its own; the pid in the name lets a later verify sweep what a killed one left behind.
|
|
384
413
|
for (const entry of readdirSync(stateDir)) {
|
|
@@ -577,6 +606,27 @@ export async function verify(argv, cwd = process.cwd(), options = {}) {
|
|
|
577
606
|
...review,
|
|
578
607
|
});
|
|
579
608
|
}
|
|
609
|
+
// A1 (D-787/D-789): one bounded launchservicesd identity read AFTER the verdict is final, so a suite
|
|
610
|
+
// that restarted the daemon is caught and the verdict, exit code and retries never depend on it.
|
|
611
|
+
// It rides the doctor.json verify read; with none there it creates no state file.
|
|
612
|
+
const doctorRoot = fileRoot("doctor.json");
|
|
613
|
+
const priorDoctor = readDoctor(doctorRoot);
|
|
614
|
+
const launchServices = priorDoctor ? await recordLaunchServices(priorDoctor, priorDoctor) : undefined;
|
|
615
|
+
if (launchServices && launchServices.health !== priorDoctor) {
|
|
616
|
+
// Evidence only, not a probe: keep doctor.json's mtime (the auth-cache freshness signal, doctorAgeMs),
|
|
617
|
+
// and a failed write is an advisory diagnostic — the verdict below is returned regardless.
|
|
618
|
+
const doctorFile = join(doctorRoot, ".tickmarkr", "doctor.json");
|
|
619
|
+
try {
|
|
620
|
+
const { atime, mtime } = statSync(doctorFile);
|
|
621
|
+
writeDoctor(doctorRoot, launchServices.health);
|
|
622
|
+
utimesSync(doctorFile, atime, mtime);
|
|
623
|
+
}
|
|
624
|
+
catch (e) {
|
|
625
|
+
console.error(`verify: launchservicesd evidence not persisted (${e instanceof Error ? e.message : String(e)}) (advisory — verdict unchanged)`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (launchServices?.line)
|
|
629
|
+
console.error(`verify: ${launchServices.line}`);
|
|
580
630
|
if (values.json) {
|
|
581
631
|
return { out: JSON.stringify({ base: baseTip, head, mergeBase, green, artifactPath, artifactSha256, artifactAvailability, results,
|
|
582
632
|
...(owedAfter && owedLines.length ? { owed: { debt: owedAfter.debt, discharged: dischargeRefusal ? [] : owed.map((o) => o.id), ...(dischargeRefusal ? { refused: dischargeRefusal } : {}) } } : {}),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type FleetEditable, type FleetUniverseRow, type LowerLayerModelOverrides, type RoutingMode } from "./config.js";
|
|
1
|
+
import { type FleetEditable, type FleetUniverseRow, type LowerLayerModelOverrides, type MapEntry, type RoutingMode } from "./config.js";
|
|
2
2
|
/** Fleet-owned overlay keys — the only config surface `tickmarkr fleet` may write. */
|
|
3
3
|
export declare const FLEET_OVERLAY_KEYS: readonly ["routing", "tiers"];
|
|
4
4
|
type FleetOverlayWriteFields = {
|
|
@@ -20,6 +20,15 @@ type FleetOverlayWriteFields = {
|
|
|
20
20
|
};
|
|
21
21
|
};
|
|
22
22
|
universe?: FleetUniverseRow[];
|
|
23
|
+
lowerMap?: Record<string, MapEntry>;
|
|
24
|
+
preservedAllow?: {
|
|
25
|
+
adapters?: string[];
|
|
26
|
+
models?: string[];
|
|
27
|
+
};
|
|
28
|
+
lowerAllow?: {
|
|
29
|
+
adapters?: string[];
|
|
30
|
+
models?: string[];
|
|
31
|
+
};
|
|
23
32
|
};
|
|
24
33
|
type EffortFree = FleetEditable & {
|
|
25
34
|
efforts?: undefined;
|
|
@@ -39,6 +48,12 @@ export declare function fleetFirstTouchProvenance(note: string, firstTouch: Flee
|
|
|
39
48
|
/** Apply only fields fleet authored to the parsed YAML document. Untouched nodes retain their
|
|
40
49
|
* keys, ordering, scalar style, and comments; fresh provenance is written directly on its tier node. */
|
|
41
50
|
export declare function renderFleetOverlayWrite(priorBytes: string, write: FleetOverlayWrite): string;
|
|
51
|
+
/** T10: the 1-based line of the key that holds `path` in these overlay bytes — the path's own key, the
|
|
52
|
+
* key whose `null` masks it from above, or a `<<` merge key on it (D-987: the write there refuses) — else
|
|
53
|
+
* undefined (the overlay neither declares nor masks it). */
|
|
54
|
+
export declare function overlayHoldingLine(bytes: string, path: readonly string[]): number | undefined;
|
|
55
|
+
/** T10: does `path` hold a mapping in these overlay bytes — neither absent nor a `null` mask? */
|
|
56
|
+
export declare function overlayHoldsMap(bytes: string, path: readonly string[]): boolean;
|
|
42
57
|
/** Build the repo overlay fragment fleet would write for edits since session start. */
|
|
43
58
|
export declare function fleetRepoOverlayFromDelta(initial: FleetEditable, edited: FleetEditable, existingRepo?: Record<string, unknown>, firstTouches?: Readonly<Record<string, FleetFirstTouch>>, universe?: FleetUniverseRow[]): Record<string, unknown>;
|
|
44
59
|
export declare function repoOverlayYaml(overlay: Record<string, unknown>): string;
|