tickmarkr 2.6.6 → 2.6.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/adapters/claude-code.js +8 -2
- package/dist/adapters/codex-commit-check.d.ts +3 -2
- package/dist/adapters/codex-commit-check.js +37 -10
- package/dist/adapters/codex.js +10 -4
- package/dist/cli/commands/beat.d.ts +6 -1
- package/dist/cli/commands/beat.js +123 -76
- package/dist/cli/commands/fleet.js +64 -21
- package/dist/cli/commands/init.js +18 -7
- package/dist/cli/help.d.ts +1 -0
- package/dist/cli/help.js +10 -5
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/gates/baseline.d.ts +9 -0
- package/dist/gates/baseline.js +14 -3
- package/dist/gates/cache.js +1 -1
- package/dist/gates/review.js +15 -0
- package/dist/gates/run-gates.d.ts +18 -6
- package/dist/gates/run-gates.js +324 -204
- package/dist/gates/test-manifest.d.ts +8 -0
- package/dist/gates/test-manifest.js +151 -111
- package/dist/run/beat-lifecycle.d.ts +154 -0
- package/dist/run/beat-lifecycle.js +825 -0
- package/dist/run/daemon.js +18 -7
- package/dist/run/git-trust.d.ts +27 -0
- package/dist/run/git-trust.js +178 -0
- package/dist/run/git.js +34 -14
- package/dist/run/journal.js +15 -3
- package/dist/run/lease.d.ts +23 -0
- package/dist/run/lease.js +232 -38
- package/dist/run/merge.js +2 -2
- package/dist/run/stall.js +20 -1
- package/dist/run/supervision.d.ts +8 -0
- package/dist/run/supervision.js +24 -4
- package/dist/run/verification-job.d.ts +51 -0
- package/dist/run/verification-job.js +137 -0
- package/dist/tui/ink/fleet-app.d.ts +9 -0
- package/dist/tui/ink/fleet-app.js +209 -127
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +19 -10
- package/skills/tickmarkr-loop/SKILL.md +19 -10
- package/skills/tickmarkr-overseer/SKILL.md +25 -16
|
@@ -281,8 +281,15 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
281
281
|
: readClaudeAliasIdentity(cwd, model) ?? CLAUDE_ALIAS_IDENTITY_STAMPS[model];
|
|
282
282
|
// One ranking universe for the whole screen: every unclassified row bands fleet-relatively
|
|
283
283
|
// against the same set, so a suggestion never depends on which adapter group renders first.
|
|
284
|
+
// T5: a displayed row's real members — every id doctor actually served among its collapsed variants
|
|
285
|
+
// (the bare id only when served, never the display label alone) — folded constituents append theirs.
|
|
286
|
+
// Each real id is ONE member even when doctor's list repeats it, so one act takes one reason from it.
|
|
284
287
|
const detectedRows = fleetUnclassifiedModels(cfg, health, adapters)
|
|
285
|
-
.map((row) => ({
|
|
288
|
+
.map((row) => ({
|
|
289
|
+
...row,
|
|
290
|
+
resolvedModel: resolvedCatalogModel(row.adapter, row.model),
|
|
291
|
+
members: [...new Set([row.model, ...(row.variants ?? [])])].filter((id) => health[row.adapter]?.models?.includes(id)),
|
|
292
|
+
}));
|
|
286
293
|
const catalogRanking = catalogTierRanking(cfg, catalog, detectedRows, resolvedCatalogModel);
|
|
287
294
|
const foldedRows = [];
|
|
288
295
|
const folds = new Map();
|
|
@@ -296,6 +303,8 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
296
303
|
if (first) {
|
|
297
304
|
first.foldedModels ??= [first.model];
|
|
298
305
|
first.foldedModels.push(row.model);
|
|
306
|
+
// T5: a collapsed constituent keeps every variant when it is catalog-folded
|
|
307
|
+
first.members = [...new Set([...first.members, ...row.members])];
|
|
299
308
|
}
|
|
300
309
|
else {
|
|
301
310
|
foldedRows.push(next);
|
|
@@ -347,9 +356,26 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
347
356
|
...unclassified
|
|
348
357
|
.filter((row) => !editable.tiers[adapter.id]?.[row.model])
|
|
349
358
|
.map((row) => {
|
|
350
|
-
const
|
|
359
|
+
const found = rowEvidence(adapter.id, row.classifyModel ?? row.model, row.advisory.coverage === "covered" ? row.advisory.evidence : undefined);
|
|
360
|
+
// T5: each member keeps its own recorded identity and auth verdict; a group is unauthed as a
|
|
361
|
+
// row only when every member is — one member's failed probe never blocks the others' reach
|
|
362
|
+
const grouped = row.members.length > 1 || (row.members.length === 1 && row.members[0] !== row.model);
|
|
363
|
+
const members = grouped ? row.members.map((model) => {
|
|
364
|
+
const identity = resolvedModelIdentity(adapter.id, model);
|
|
365
|
+
const probed = health[adapter.id]?.modelAuth?.[model];
|
|
366
|
+
return {
|
|
367
|
+
model,
|
|
368
|
+
...(identity !== undefined ? { identity } : {}),
|
|
369
|
+
...(probed?.authed === false ? { unauthed: probed.reason ?? "probe failed" } : {}),
|
|
370
|
+
};
|
|
371
|
+
}) : undefined;
|
|
372
|
+
const { unauthed: _unauthed, ...authedEvidence } = found ?? {};
|
|
373
|
+
const evidence = members?.some((member) => member.unauthed === undefined) && found?.unauthed !== undefined
|
|
374
|
+
? (Object.keys(authedEvidence).length ? authedEvidence : undefined)
|
|
375
|
+
: found;
|
|
351
376
|
return {
|
|
352
377
|
model: row.model,
|
|
378
|
+
...(members ? { members } : {}),
|
|
353
379
|
detectedAt: row.detectedAt,
|
|
354
380
|
...(row.classifyModel ? { classifyModel: row.classifyModel } : {}),
|
|
355
381
|
...(row.variants ? { variants: row.variants } : {}),
|
|
@@ -474,15 +500,18 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
474
500
|
? { ...initial, allowOut: [...new Set([...(initial.allowOut ?? []), ...seeds.map((seed) => seed.entry)])].sort() }
|
|
475
501
|
: initial);
|
|
476
502
|
const FLAT_SCOPES = DENY_SCOPES.filter((scope) => scope.path.length === 3);
|
|
477
|
-
const seedChannel = (seed) => ({ adapter: seed.adapter, model: seed.
|
|
503
|
+
const seedChannel = (seed) => ({ adapter: seed.adapter, model: seed.model, ...(seed.identity !== undefined ? { identity: seed.identity } : {}) });
|
|
478
504
|
// an allow-complement entry excludes a seeded channel by the allow form's own matching (adapter id,
|
|
479
505
|
// model, adapter:model, recorded identity) — the seed's spelling is one such entry, never the only one
|
|
480
506
|
const seedRow = (seed) => ({
|
|
481
507
|
adapter: seed.adapter,
|
|
482
|
-
models: [seed.
|
|
483
|
-
...(seed.identity !== undefined ? { identities: { [seed.
|
|
508
|
+
models: [seed.model],
|
|
509
|
+
...(seed.identity !== undefined ? { identities: { [seed.model]: seed.identity } } : {}),
|
|
484
510
|
});
|
|
485
|
-
const allowExcludes = (staged, seed) => (staged.allowOut ?? []).some((entry) => universeEntryMatches(seedRow(seed), seed.
|
|
511
|
+
const allowExcludes = (staged, seed) => (staged.allowOut ?? []).some((entry) => universeEntryMatches(seedRow(seed), seed.model, entry));
|
|
512
|
+
// T5: the real channels a displayed row stands for (fleet's own derivation above); an ungrouped row is one
|
|
513
|
+
const rowMembers = (row) => ("members" in row && row.members)
|
|
514
|
+
|| [{ model: ("classifyModel" in row && row.classifyModel) || row.model, ...(row.evidence?.identity !== undefined ? { identity: row.evidence.identity } : {}) }];
|
|
486
515
|
// membership keeps the channel out: its allow complement or a flat (all-seats) deny entry
|
|
487
516
|
const excludedIn = (staged, seed) => allowExcludes(staged, seed)
|
|
488
517
|
|| FLAT_SCOPES.some((scope) => (staged[scope.key] ?? []).some((entry) => entryMatchesChannel(entry, seedChannel(seed), true)));
|
|
@@ -573,12 +602,9 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
573
602
|
// own flat-deny lift: a row the baseline membership kept out that the staged one admits; a lone or
|
|
574
603
|
// unrelated tier stays user
|
|
575
604
|
const own = admitted.some((seed) => seed.adapter === adapter && seed.model === model)
|
|
576
|
-
|| (modelGroups.find((group) => group.adapter === adapter)?.rows ?? [])
|
|
577
|
-
.filter((
|
|
578
|
-
.some((
|
|
579
|
-
const channel = { adapter, displayModel: row.model, ...(row.evidence?.identity !== undefined ? { identity: row.evidence.identity } : {}) };
|
|
580
|
-
return excludedIn(baseline, channel) && !excludedIn(staged, channel);
|
|
581
|
-
});
|
|
605
|
+
|| (modelGroups.find((group) => group.adapter === adapter)?.rows ?? []).flatMap(rowMembers)
|
|
606
|
+
.filter((member) => member.model === model)
|
|
607
|
+
.some((member) => excludedIn(baseline, { adapter, ...member }) && !excludedIn(staged, { adapter, ...member }));
|
|
582
608
|
edits.push({ name: `tiers.${adapter}.models.${model}`, destination: own ? family() : "user" });
|
|
583
609
|
}
|
|
584
610
|
}
|
|
@@ -607,17 +633,22 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
607
633
|
// the browser never showed, or whose spelling names no channel, is refused.
|
|
608
634
|
const editingUniverse = (candidate, seeds) => {
|
|
609
635
|
const target = universeOf(candidateChannels(candidate));
|
|
636
|
+
// T5 review: the seed's OWN channel must be in the universe — its entry matching another member's
|
|
637
|
+
// recorded identity (C-high recorded as C-low) never stands for an unauthed or unprobed C-low
|
|
610
638
|
const covers = (seed) => target.some((row) => row.adapter === seed.adapter
|
|
611
|
-
&& row.models.
|
|
639
|
+
&& row.models.includes(seed.model) && universeEntryMatches(row, seed.model, seed.entry));
|
|
612
640
|
for (const seed of seeds) {
|
|
613
641
|
if (covers(seed))
|
|
614
642
|
continue;
|
|
615
|
-
|
|
643
|
+
// T5: a seed names a real member of the row it was staged from — never that row's display label
|
|
644
|
+
const shown = modelGroups.some((group) => group.adapter === seed.adapter
|
|
645
|
+
&& group.rows.some((row) => row.model === seed.displayModel && rowMembers(row).some((member) => member.model === seed.model)));
|
|
616
646
|
const row = target.find((candidateRow) => candidateRow.adapter === seed.adapter);
|
|
617
647
|
if (shown && row) {
|
|
618
|
-
row.models
|
|
648
|
+
if (!row.models.includes(seed.model))
|
|
649
|
+
row.models = [...row.models, seed.model];
|
|
619
650
|
if (seed.identity !== undefined)
|
|
620
|
-
row.identities = { ...row.identities, [seed.
|
|
651
|
+
row.identities = { ...row.identities, [seed.model]: seed.identity };
|
|
621
652
|
}
|
|
622
653
|
if (!covers(seed))
|
|
623
654
|
throw new ConfigError(`the seeded allow entry ${seed.entry} names no channel of the discovered fleet — re-probe with tickmarkr doctor, then classify it again`);
|
|
@@ -1282,24 +1313,36 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
1282
1313
|
// and it was not configured at probe time), so it stays unroutable — invisible in every
|
|
1283
1314
|
// picker — until the next probe. Name the step, or the classify flow reads as broken.
|
|
1284
1315
|
const unprobed = [];
|
|
1316
|
+
// T5: a grouped classification admits every member's real id, yet a member doctor recorded unauthed
|
|
1317
|
+
// stays unroutable — named, never implied routable by its group's admission
|
|
1318
|
+
const unauthed = [];
|
|
1285
1319
|
for (const [adapter, models] of Object.entries(write.edited.tiers)) {
|
|
1286
1320
|
for (const [model, assigned] of Object.entries(models)) {
|
|
1287
1321
|
if (assigned === null || assigned === undefined)
|
|
1288
1322
|
continue;
|
|
1289
1323
|
if (JSON.stringify(write.initial.tiers[adapter]?.[model]) === JSON.stringify(assigned))
|
|
1290
1324
|
continue;
|
|
1291
|
-
|
|
1325
|
+
const probed = health[adapter]?.modelAuth?.[model];
|
|
1326
|
+
if (probed === undefined)
|
|
1292
1327
|
unprobed.push(`${adapter}:${model}`);
|
|
1328
|
+
else if (probed.authed === false)
|
|
1329
|
+
unauthed.push(`${adapter}:${model}`);
|
|
1293
1330
|
}
|
|
1294
1331
|
}
|
|
1295
1332
|
// B2: a repo-shadowed save says what still applies here — never claimed applied
|
|
1296
1333
|
const head = [`fleet: wrote ${pending.path}`, ...(pending.destination === "user"
|
|
1297
1334
|
? repoShadowNotes(result.review.before, result.review.after).map((note) => `fleet: ${note}`)
|
|
1298
1335
|
: [])].join("\n");
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1336
|
+
const listed = (ids) => ids.slice(0, 3).join(", ") + (ids.length > 3 ? `, +${ids.length - 3} more` : "");
|
|
1337
|
+
return [
|
|
1338
|
+
head,
|
|
1339
|
+
...(unprobed.length
|
|
1340
|
+
? [`fleet: ${unprobed.length} newly classified model(s) have no probe verdict yet (${listed(unprobed)}) — run \`tickmarkr fleet --fresh\` to probe them; unverified models stay unroutable`]
|
|
1341
|
+
: []),
|
|
1342
|
+
...(unauthed.length
|
|
1343
|
+
? [`fleet: ${unauthed.length} newly classified model(s) are unauthed (${listed(unauthed)}) — re-probe with tickmarkr doctor; unauthed models stay unroutable`]
|
|
1344
|
+
: []),
|
|
1345
|
+
].join("\n");
|
|
1303
1346
|
};
|
|
1304
1347
|
return {
|
|
1305
1348
|
props,
|
|
@@ -130,13 +130,24 @@ you were handed the whole picture — list the repository root, open every guida
|
|
|
130
130
|
predate a project rename, so **search by CONCEPT, not by the current product name.** A memory nobody opens
|
|
131
131
|
is worse than none: every seat assumes the lesson is recorded somewhere and no seat looks.
|
|
132
132
|
- **The gates are the product.** Seven, defined in \`src/graph/schema.ts\`:
|
|
133
|
-
\`build test lint evidence scope acceptance review\`. **That is DECLARATION order, not execution order
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
133
|
+
\`build test lint evidence scope acceptance review\`. **That is DECLARATION order, not execution order.**
|
|
134
|
+
The cheap checks run first and stop at their first red: \`build\`, \`lint\`, then \`evidence\` and \`scope\`.
|
|
135
|
+
A fresh or semantic-repair candidate then runs \`acceptance\` ‖ \`review\` CONCURRENTLY (\`run-gates.ts\`,
|
|
136
|
+
*"judge ‖ review"*) before any test payload: a decisive semantic red ends the round with zero diagnostic
|
|
137
|
+
or full test-gate runs (a required acceptance oracle still executes); otherwise ONE full verification job
|
|
138
|
+
follows, with no selected screen. Only an attributed behavioral test-red repair may buy one diagnostic
|
|
139
|
+
first — every required failing file plus conservatively selected tests reaching the diff — admitted only
|
|
140
|
+
on comparable harness timing with ratio <= 0.15 AND estimated cost <= 60000 ms; its behavioral red stops
|
|
141
|
+
the round before semantics, otherwise semantics precede the full job. Disabled, unsupported, over-cap or
|
|
142
|
+
unknown-cost selection skips the diagnostic instead of running a full suite before semantics, and a
|
|
143
|
+
killed diagnostic is never green. A seatless or no-verdict review lets acceptance finish and publish and
|
|
144
|
+
the full job run in-round, and stays unsatisfied until review-only recovery; a seatless acceptance
|
|
145
|
+
cancels its sibling, records the unrun test (\`testOwed\`) and every enabled gate still owed (an unrecorded
|
|
146
|
+
review included), cannot merge and parks infra; an actual cancellation or throw awaits cleanup and leaves
|
|
147
|
+
no mergeable partial set. Test-only verification and an explicit full recheck keep their full scope with
|
|
148
|
+
no invented semantics and no diagnostic. Nothing merges until the FULL suite has passed on the exact
|
|
149
|
+
merge-candidate commit; an exact full green answers only after identity, environment, cleanliness and
|
|
150
|
+
current complete-manifest checks. The first five are MANDATORY; only \`acceptance\` and
|
|
140
151
|
\`review\` may be omitted per task. Implementations are in \`src/gates/\` — \`baseline.ts\` (build/test/lint,
|
|
141
152
|
diffed against a recorded baseline so pre-existing failures are forgiven), \`evidence.ts\`, \`scope.ts\`,
|
|
142
153
|
\`acceptance.ts\` (its judge reads the DIFF and every criterion must cite a changed hunk), \`review.ts\`
|
package/dist/cli/help.d.ts
CHANGED
package/dist/cli/help.js
CHANGED
|
@@ -148,17 +148,22 @@ export const COMMAND_HELP = {
|
|
|
148
148
|
],
|
|
149
149
|
},
|
|
150
150
|
beat: {
|
|
151
|
-
usage: "beat <orchestrator|orchestrator-context|overseer|overseer-context|watch> --seat <identity> [options]",
|
|
152
|
-
description: "
|
|
151
|
+
usage: "beat <start|stop|status> <orchestrator|orchestrator-context|overseer|overseer-context|watch> --seat <identity> | beat <tier> --seat <identity> [options]",
|
|
152
|
+
description: "start launches one detached --loop beat per tier and exits 0 only after reading back that child's own advancing beat; stop retires only that recorded generation and reads back DISARMED; status is read-only and exits nonzero for STALE, MISMATCH or UNREADABLE. The legacy <tier> form writes one supervision beat using the recorded arm: stood-down arms stay DISARMED until explicitly re-armed; stopped beats become STALE.",
|
|
153
153
|
options: {
|
|
154
|
-
"--seat <identity>": "Required: identify the supervising pane or agent.",
|
|
154
|
+
"--seat <identity>": "Required for start, stop and every write: identify the supervising pane or agent; optional for status.",
|
|
155
155
|
"--new-arm": "Create a new durable arm, allowing beats to resume after stand-down; the hand-off step for a seat taking over a stood-down tier (combine with --arm-id and --pct on a context tier).",
|
|
156
|
-
"--loop": "Create a new durable arm and beat in this process every 10 seconds; exit within one interval after stand-down.",
|
|
156
|
+
"--loop": "Create a new durable arm and beat in this process every 10 seconds; exit nonzero with its reason within one interval after stand-down, supersession or a refused write.",
|
|
157
157
|
"--arm-id <identity>": "Name a newly created arm; with --pct, identify the context observation arm. Never refuses a tick.",
|
|
158
158
|
"--pct <0..100>": "Report current context consumption percentage.",
|
|
159
159
|
"--threshold-pct <0..100>": "Set the context warning threshold (default 75).",
|
|
160
160
|
"--stand-down": "Record an explicit handoff and stand down this tier. A later one-shot tick on the old arm exits non-zero; context observations still raise and discharge the clear duty.",
|
|
161
|
-
|
|
161
|
+
"--status": "Legacy spelling of beat status <tier>: the same read-only result, writing nothing.",
|
|
162
|
+
}, examples: [
|
|
163
|
+
"beat start overseer --seat supervisor", "beat status overseer --seat supervisor", "beat stop overseer --seat supervisor",
|
|
164
|
+
"beat overseer --seat supervisor", "beat overseer --seat supervisor --loop", "beat overseer --seat supervisor --stand-down",
|
|
165
|
+
"beat overseer --seat supervisor --new-arm", "beat overseer --status",
|
|
166
|
+
],
|
|
162
167
|
},
|
|
163
168
|
version: {
|
|
164
169
|
usage: "version [--dist]", description: "Print the installed package version. Top-level aliases: --version and -v.",
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -45,7 +45,7 @@ export declare const COMMANDS: {
|
|
|
45
45
|
eval: typeof evalCommand;
|
|
46
46
|
};
|
|
47
47
|
export type RegisteredCommand = keyof typeof COMMANDS;
|
|
48
|
-
export declare const USAGE = "tickmarkr \u2014 spec-driven orchestration harness for AI coding agents\nusage: tickmarkr <command>\n init guided setup + doctor; init --agent [--force] [--docs] adds agent skills/docs\n doctor re-probe adapters, herdr, auth; print capability matrix (--fix writes the test-runner ignore when a safe edit exists)\n fleet interactive fleet editor (fleet --print for CI drift checks)\n compile <src> spec \u2192 .tickmarkr/graph.json (fails without acceptance criteria)\n scope <intent> preview locally with --preview; draft after confirmation or --yes (--force to overwrite)\n plan dry-run routing table + cost estimate + floor lints\n eval discover and validate fixtures, seed isolated temp repos, then clean them up\n run execute the graph (--concurrency N --driver auto|herdr|subprocess|orca --route-strict; orca runs only when named)\n status live run state (--watch --events: JSON documents on stdout, keepalives on stderr; 2>&1 corrupts the stream)\n stats all-run channel delivery, red, rescue, author and reviewer statistics\n verify run the gate battery standalone against merge-base(--base, HEAD)..HEAD \u2014 verdict/JSON on stdout, progress on stderr; 2>&1 corrupts the verdict stream (--base main --criteria <file> | --task <id> [--files <glob>] [--author adapter:model] [--no-review] [--json])\n resume <id> continue a run from its journal\n report <id> cost/quality report (--md writes Markdown to stdout; redirect to save a record)\n profile show learned routing profile (profile reset = forget history via cursor, keeps telemetry)\n ui open Home, Run or Evidence (--view home|run|evidence; --setup <id> opens Run Parks)\n unlock remove a stale/garbage run lock (refuses if the holder is alive)\n beat <tier>
|
|
48
|
+
export declare const USAGE = "tickmarkr \u2014 spec-driven orchestration harness for AI coding agents\nusage: tickmarkr <command>\n init guided setup + doctor; init --agent [--force] [--docs] adds agent skills/docs\n doctor re-probe adapters, herdr, auth; print capability matrix (--fix writes the test-runner ignore when a safe edit exists)\n fleet interactive fleet editor (fleet --print for CI drift checks)\n compile <src> spec \u2192 .tickmarkr/graph.json (fails without acceptance criteria)\n scope <intent> preview locally with --preview; draft after confirmation or --yes (--force to overwrite)\n plan dry-run routing table + cost estimate + floor lints\n eval discover and validate fixtures, seed isolated temp repos, then clean them up\n run execute the graph (--concurrency N --driver auto|herdr|subprocess|orca --route-strict; orca runs only when named)\n status live run state (--watch --events: JSON documents on stdout, keepalives on stderr; 2>&1 corrupts the stream)\n stats all-run channel delivery, red, rescue, author and reviewer statistics\n verify run the gate battery standalone against merge-base(--base, HEAD)..HEAD \u2014 verdict/JSON on stdout, progress on stderr; 2>&1 corrupts the verdict stream (--base main --criteria <file> | --task <id> [--files <glob>] [--author adapter:model] [--no-review] [--json])\n resume <id> continue a run from its journal\n report <id> cost/quality report (--md writes Markdown to stdout; redirect to save a record)\n profile show learned routing profile (profile reset = forget history via cursor, keeps telemetry)\n ui open Home, Run or Evidence (--view home|run|evidence; --setup <id> opens Run Parks)\n unlock remove a stale/garbage run lock (refuses if the holder is alive)\n beat <tier> supervision beats for orchestrator|orchestrator-context|overseer|overseer-context|watch, --seat <identity> required: beat start|stop|status <tier> owns one detached loop per tier (status is read-only); the legacy beat <tier> records one beat (--stand-down to hand off), and status reads the tier STALE once the beats stop\n approve <id> <task> release a park (--uphold sides with the reviewer and funds a fixed attempt; --by <name> --reason <text>); takes effect on resume\n version print the installed version (--dist adds build location and fingerprint)\n\nUse tickmarkr <command> --help (or -h) for all options and examples.\nUse tickmarkr profile <operation> --help for nested profile help.";
|
|
49
49
|
export declare function dispatch(cmd: string | undefined, argv: string[], commands?: CommandMap): Promise<{
|
|
50
50
|
out: string;
|
|
51
51
|
code: number;
|
package/dist/cli/index.js
CHANGED
|
@@ -46,7 +46,7 @@ usage: tickmarkr <command>
|
|
|
46
46
|
profile show learned routing profile (profile reset = forget history via cursor, keeps telemetry)
|
|
47
47
|
ui open Home, Run or Evidence (--view home|run|evidence; --setup <id> opens Run Parks)
|
|
48
48
|
unlock remove a stale/garbage run lock (refuses if the holder is alive)
|
|
49
|
-
beat <tier>
|
|
49
|
+
beat <tier> supervision beats for orchestrator|orchestrator-context|overseer|overseer-context|watch, --seat <identity> required: beat start|stop|status <tier> owns one detached loop per tier (status is read-only); the legacy beat <tier> records one beat (--stand-down to hand off), and status reads the tier STALE once the beats stop
|
|
50
50
|
approve <id> <task> release a park (--uphold sides with the reviewer and funds a fixed attempt; --by <name> --reason <text>); takes effect on resume
|
|
51
51
|
version print the installed version (--dist adds build location and fingerprint)
|
|
52
52
|
|
package/dist/gates/baseline.d.ts
CHANGED
|
@@ -125,6 +125,9 @@ export interface BaselineCommand {
|
|
|
125
125
|
/** VL-1: every per-file duration the runner named, so a later per-file hang budget (test-manifest.ts)
|
|
126
126
|
* can be derived per file rather than from one slowest-file number; null when unavailable. */
|
|
127
127
|
fileDurations?: BaselineFileDuration[] | null;
|
|
128
|
+
/** v2.6.7 close: set when fileTiming classified each duration's outcome (`failed` on a red file). A red capture
|
|
129
|
+
* without it (an older binary, a cached or hand-supplied baseline) never says which files were red. */
|
|
130
|
+
fileOutcomes?: true;
|
|
128
131
|
/** The ceiling that measurement implies, persisted so every later battery uses the same number. */
|
|
129
132
|
ceilingMs?: number;
|
|
130
133
|
/**
|
|
@@ -145,6 +148,8 @@ export interface BaselineCommand {
|
|
|
145
148
|
export interface BaselineFileDuration {
|
|
146
149
|
file: string;
|
|
147
150
|
durationMs: number;
|
|
151
|
+
/** The file was RED in this capture (× or ❯): its duration ended early and is no measure of its green runtime. */
|
|
152
|
+
failed?: true;
|
|
148
153
|
}
|
|
149
154
|
/**
|
|
150
155
|
* OBS-1123: WHICH capture a forgiveness rests on — the commit it measured and when it was published.
|
|
@@ -175,6 +180,10 @@ export type FailureClassification = "regression" | "infra";
|
|
|
175
180
|
* unreadable-runner case the existing fail-closed path already owns.
|
|
176
181
|
*/
|
|
177
182
|
export declare function classifyFailureOutput(output: string): FailureClassification | undefined;
|
|
183
|
+
/** The per-file timings a consumer may read as green runtimes. A red capture that never classified its files
|
|
184
|
+
* (no fileOutcomes) marks every file failed, so each is budgeted and costed as untimed — never as the early-ending
|
|
185
|
+
* time of a run that may have been red for it. */
|
|
186
|
+
export declare function classifiedFileDurations(entry: BaselineCommand | undefined): BaselineFileDuration[] | null | undefined;
|
|
178
187
|
export declare const UNRECOGNIZED_FAILURE = "<unrecognized failure output>";
|
|
179
188
|
export declare function fingerprint(output: string): string[];
|
|
180
189
|
/**
|
package/dist/gates/baseline.js
CHANGED
|
@@ -430,7 +430,7 @@ const normalizeLine = (l) => l.replace(/\d+/g, "#").replace(/\s+/g, " ").trim();
|
|
|
430
430
|
// turbo-prefixed, but a test-name timing has no parenthesized file tally and therefore cannot enter
|
|
431
431
|
// this measurement. These are observations of runner output, not a promise that every runner exposes
|
|
432
432
|
// them — callers record null, never zero, when no line matches.
|
|
433
|
-
const FILE_DURATION_RE = /^\s*(?:(?:[\w@./-]+:\s*)*)[✓✔×❯]\s+(?:\|[^|\r\n]+\|\s+)?(\S+)\s+\([^\r\n)]*\)\s+(\d+(?:\.\d+)?)\s*(ms|s|m)\b/;
|
|
433
|
+
const FILE_DURATION_RE = /^\s*(?:(?:[\w@./-]+:\s*)*)([✓✔×❯])\s+(?:\|[^|\r\n]+\|\s+)?(\S+)\s+\([^\r\n)]*\)\s+(\d+(?:\.\d+)?)\s*(ms|s|m)\b/;
|
|
434
434
|
const durationUnitMs = (unit) => unit === "m" ? 60_000 : unit === "s" ? 1_000 : 1;
|
|
435
435
|
function fileTiming(output, wallClockMs) {
|
|
436
436
|
const files = [];
|
|
@@ -438,9 +438,10 @@ function fileTiming(output, wallClockMs) {
|
|
|
438
438
|
const match = FILE_DURATION_RE.exec(line.replace(ANSI_RE, ""));
|
|
439
439
|
if (!match)
|
|
440
440
|
continue;
|
|
441
|
-
const durationMs = Number(match[
|
|
441
|
+
const durationMs = Number(match[3]) * durationUnitMs(match[4]);
|
|
442
|
+
// vitest marks a red file ❯ (suiteFail) and a red test × (taskFail)
|
|
442
443
|
if (Number.isFinite(durationMs))
|
|
443
|
-
files.push({ file: match[
|
|
444
|
+
files.push({ file: match[2], durationMs, ...(match[1] === "×" || match[1] === "❯" ? { failed: true } : {}) });
|
|
444
445
|
}
|
|
445
446
|
if (!files.length)
|
|
446
447
|
return { fileDurationSumMs: null, impliedParallelism: null, longestFile: null, fileDurations: null };
|
|
@@ -451,8 +452,18 @@ function fileTiming(output, wallClockMs) {
|
|
|
451
452
|
impliedParallelism: wallClockMs > 0 ? fileDurationSumMs / wallClockMs : null,
|
|
452
453
|
longestFile,
|
|
453
454
|
fileDurations: files,
|
|
455
|
+
fileOutcomes: true,
|
|
454
456
|
};
|
|
455
457
|
}
|
|
458
|
+
/** The per-file timings a consumer may read as green runtimes. A red capture that never classified its files
|
|
459
|
+
* (no fileOutcomes) marks every file failed, so each is budgeted and costed as untimed — never as the early-ending
|
|
460
|
+
* time of a run that may have been red for it. */
|
|
461
|
+
export function classifiedFileDurations(entry) {
|
|
462
|
+
const files = entry?.fileDurations;
|
|
463
|
+
if (!Array.isArray(files) || entry?.fileOutcomes || entry?.exitCode === 0)
|
|
464
|
+
return files;
|
|
465
|
+
return files.map((f) => ({ ...f, failed: true }));
|
|
466
|
+
}
|
|
456
467
|
// A failing command whose output holds no shape any runner here names. The marker is content-free and
|
|
457
468
|
// constant: downstream consumers (tip verify journals fingerprint counts) still see that the command
|
|
458
469
|
// failed, while no line of the output — narration, status strip, stack trace — can enter the set. Being
|
package/dist/gates/cache.js
CHANGED
|
@@ -53,7 +53,7 @@ export function baselineIdentity(baseline) {
|
|
|
53
53
|
return "none";
|
|
54
54
|
// Measurements control execution budgets, not which failures the baseline forgives.
|
|
55
55
|
const commands = Object.fromEntries(Object.entries(baseline.commands).map(([gate, entry]) => {
|
|
56
|
-
const { durationMs: _duration, fileDurationSumMs: _sum, impliedParallelism: _parallelism, longestFile: _longest, fileDurations: _files, ceilingMs: _ceiling, ...evidence } = entry;
|
|
56
|
+
const { durationMs: _duration, fileDurationSumMs: _sum, impliedParallelism: _parallelism, longestFile: _longest, fileDurations: _files, fileOutcomes: _outcomes, ceilingMs: _ceiling, ...evidence } = entry;
|
|
57
57
|
return [gate, evidence];
|
|
58
58
|
}));
|
|
59
59
|
return createHash("sha256").update(canonicalJson(commands)).digest("hex").slice(0, 16);
|
package/dist/gates/review.js
CHANGED
|
@@ -535,6 +535,13 @@ carriedAuthors = [], operatorContext) {
|
|
|
535
535
|
? `You may run at most the task's own test files explicitly named in files[]; these are the only suites you may run: ${ownTestFiles.map((file) => `\`${file}\``).join(", ")}.`
|
|
536
536
|
: "No suite may be run: files[] names no explicit test file owned by this task.";
|
|
537
537
|
const responseRequirement = reviewResponseRequirement(nonce);
|
|
538
|
+
// D-966 (T3 E): a reviewer that stopped at the first example of a class drip-fed it one round at a
|
|
539
|
+
// time — T5's rounds carried 2→1→1→1→2→1 findings and T10's 3→2→1→1→1, each round's repair fixing
|
|
540
|
+
// the case it was shown while its same-class siblings waited for the next review. The closed note
|
|
541
|
+
// tuple asks for every counterexample ACTUALLY FOUND in scope at once, plus the boundary searched and
|
|
542
|
+
// what stays uncertain, so the repair sees the class and the next round knows what was covered. It
|
|
543
|
+
// rides the existing note text (details → repair brief unchanged) and never asks for an exhaustive
|
|
544
|
+
// search the reviewer cannot honestly make; uncertainty alone stays out of the material bar.
|
|
538
545
|
const prompt = `TICKMARKR-REVIEW
|
|
539
546
|
${responseRequirement}
|
|
540
547
|
|
|
@@ -580,6 +587,14 @@ acceptance criterion it violates (or to the regression this diff introduces), st
|
|
|
580
587
|
and marks its evidence executed (you ran the reproducer), static (you traced it by reading) or blocked (it
|
|
581
588
|
could not run here). Blocked evidence never turns a finding into a pass, and a worker's own case table or
|
|
582
589
|
enumeration never resolves a finding: judge the diff itself.
|
|
590
|
+
Report the whole class in this one review, never only its first example. Each material finding's "note" is one
|
|
591
|
+
line carrying the closed tuple "rule: <the goal clause, criterion or regressed behaviour violated> | found:
|
|
592
|
+
<every counterexample you ACTUALLY FOUND inside the declared scope, each as path:line input → consequence,
|
|
593
|
+
joined by "; "> | searched: <the boundary you actually searched: files, symbols, call sites> | uncertain:
|
|
594
|
+
<what lies outside that boundary or could not be established inside it>". Found one counterexample, list one;
|
|
595
|
+
found several, list every one in the same finding. Never list a case you did not find. Your search is bounded:
|
|
596
|
+
never claim it covered every input, caller or path beyond the boundary you name, and remaining uncertainty
|
|
597
|
+
alone is never a material finding — record it under uncertain, or as a deferred minor with its rationale.
|
|
583
598
|
|
|
584
599
|
Respond with ONLY this JSON:
|
|
585
600
|
{"nonce": "${nonce}", "approve": true|false, "resolved": [], "reraised": [], "findings": [{"note": "...", "severity": "material"|"minor", "defer": false, "rationale": ""}], "comments": [{"path": "path/to/file", "line": 42, "body": "actionable feedback", "finding": 1}]}
|
|
@@ -98,14 +98,26 @@ export interface GateContext {
|
|
|
98
98
|
* a command that already has `--` takes them directly. Every path is quoted — config flows into a shell.
|
|
99
99
|
*/
|
|
100
100
|
export declare function testCommandForFiles(testCmd: string, files: string[]): string;
|
|
101
|
-
/**
|
|
102
|
-
export declare const
|
|
101
|
+
/** v2.6.7 T1 (closed order table): an attributed test-red diagnostic is admitted only when BOTH hold. */
|
|
102
|
+
export declare const DIAGNOSTIC_MAX_RATIO = 0.15;
|
|
103
|
+
export declare const DIAGNOSTIC_MAX_ESTIMATE_MS = 60000;
|
|
104
|
+
export interface DiagnosticAdmission {
|
|
105
|
+
admitted: boolean;
|
|
106
|
+
reason: "diagnostic-admitted" | "diagnostic-unknown-cost" | "diagnostic-capacity-mismatch" | "diagnostic-cost-ratio" | "diagnostic-cost-estimate";
|
|
107
|
+
costRatio?: number;
|
|
108
|
+
estimatedMs?: number;
|
|
109
|
+
}
|
|
103
110
|
/**
|
|
104
|
-
* The
|
|
105
|
-
*
|
|
106
|
-
*
|
|
111
|
+
* The diagnostic's admission evidence, from the per-file durations the harness measured at baseline
|
|
112
|
+
* capture — never a worker's timing. Unknown (any selected file unmeasured, any measurement that is not a
|
|
113
|
+
* finite number > 0 ms, an infra baseline, or no positive total) skips: an expensive or unknown screen must
|
|
114
|
+
* never become a full suite before semantics.
|
|
115
|
+
* So does timing recorded under a malformed or different capacity (git.ts sameCapacity): it divided the
|
|
116
|
+
* machine by another number, so it is not comparable evidence about this one.
|
|
117
|
+
* `estimatedMs` is the serial sum of those files: admission evidence, not a kill ceiling and not a
|
|
118
|
+
* claim about the parallel-plus-serial wall time the runner will actually take.
|
|
107
119
|
*/
|
|
108
|
-
export declare function
|
|
120
|
+
export declare function diagnosticAdmission(baseline: Baseline, selected: readonly string[]): DiagnosticAdmission;
|
|
109
121
|
/** A retry base no runner invocation parses. evaluateManifestedTest builds its stranded single-fork
|
|
110
122
|
* retry from the base it is handed, and one it cannot parse throws before any spawn — so this base
|
|
111
123
|
* disables that inner recovery: a worker-RPC-stranded re-observation comes back infra (the caller
|