tickmarkr 2.6.0 → 2.6.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/dist/adapters/prompt.d.ts +2 -1
- package/dist/adapters/prompt.js +10 -0
- package/dist/adapters/types.d.ts +1 -0
- package/dist/adapters/types.js +10 -0
- package/dist/cli/commands/fleet.js +26 -1
- package/dist/cli/commands/plan.js +7 -3
- package/dist/cli/commands/status.js +13 -3
- package/dist/cli/help.d.ts +2 -0
- package/dist/cli/help.js +2 -0
- package/dist/drivers/orca.js +6 -1
- package/dist/gates/cache.d.ts +3 -1
- package/dist/gates/cache.js +10 -3
- package/dist/gates/llm.js +4 -1
- package/dist/gates/review.js +7 -11
- package/dist/gates/run-gates.d.ts +4 -0
- package/dist/gates/run-gates.js +7 -4
- package/dist/gates/test-manifest.d.ts +12 -0
- package/dist/gates/test-manifest.js +26 -6
- package/dist/graph/graph.d.ts +6 -2
- package/dist/graph/graph.js +15 -4
- package/dist/route/role-pick.d.ts +16 -0
- package/dist/route/role-pick.js +15 -0
- package/dist/route/router.d.ts +14 -0
- package/dist/route/router.js +9 -1
- package/dist/run/consult.js +5 -9
- package/dist/run/daemon.d.ts +9 -0
- package/dist/run/daemon.js +395 -59
- package/dist/run/git.d.ts +36 -1
- package/dist/run/git.js +89 -5
- package/dist/run/host-health.d.ts +20 -0
- package/dist/run/host-health.js +64 -0
- package/dist/run/journal.js +9 -0
- package/dist/run/operator-state.d.ts +24 -2
- package/dist/run/operator-state.js +41 -5
- package/dist/run/stall.d.ts +38 -2
- package/dist/run/stall.js +276 -6
- package/dist/tui/cockpit/board.d.ts +1 -1
- package/dist/tui/cockpit/board.js +27 -19
- package/dist/tui/cockpit/derive.d.ts +2 -0
- package/dist/tui/cockpit/derive.js +4 -0
- package/dist/tui/cockpit/live-store.d.ts +1 -0
- package/dist/tui/cockpit/live-store.js +31 -8
- package/dist/tui/cockpit/run-cockpit.js +2 -1
- package/dist/tui/cockpit/run-view.d.ts +2 -4
- package/dist/tui/cockpit/run-view.js +9 -8
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/SKILL.md +43 -6
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Task } from "../graph/schema.js";
|
|
2
2
|
import { type VerdictUnparseableCause } from "../gates/verdict-cause.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type WorkerResult } from "./types.js";
|
|
4
4
|
export declare function buildTaskPrompt(task: Task, feedback?: string, nonce?: string): string;
|
|
5
5
|
export declare function writePrompt(dir: string, task: Task, attempt: number, feedback?: string, nonce?: string): string;
|
|
6
6
|
export declare function trailerPattern(nonce: string): string;
|
|
@@ -11,4 +11,5 @@ export type ClassifiedWorkerResult = WorkerResult & {
|
|
|
11
11
|
};
|
|
12
12
|
export declare function parseWorkerResult(raw: string, nonce: string): ClassifiedWorkerResult;
|
|
13
13
|
export type DeadChannelReason = "auth-required" | "setup-required" | "provider-outage" | "timeout";
|
|
14
|
+
export declare function classifyTransientCapacity(result: WorkerResult): RegExpExecArray | null;
|
|
14
15
|
export declare function classifyDeadChannel(result: WorkerResult): DeadChannelReason | undefined;
|
package/dist/adapters/prompt.js
CHANGED
|
@@ -2,6 +2,7 @@ import { copyFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { renderAcceptanceItem } from "../graph/schema.js";
|
|
4
4
|
import { classifyVerdictCause } from "../gates/verdict-cause.js";
|
|
5
|
+
import { CAPACITY_RE } from "./types.js";
|
|
5
6
|
// v1.87 T5: the scope rule states only what the runtime enforces, and no more. What it enforces:
|
|
6
7
|
// `scope.allowDeviations` comes from the operator's config, is snapshotted and frozen when a gate
|
|
7
8
|
// round starts (run-gates.ts `allowDeviations`), and is never written back — so a worker has no
|
|
@@ -133,6 +134,15 @@ const AUTH_RE = /not logged in|please (?:log ?in|sign in)|please run [^\n]{0,30}
|
|
|
133
134
|
const SETUP_RE = /command not found|not recognized as an internal or external command|spawn \S+ ENOENT|missing required config|workspace trust (?:required|not granted)/i;
|
|
134
135
|
const OUTAGE_RE = /unable to reach the model provider|cannot reach the model provider|model provider.{0,40}(?:unavailable|unreachable)|service (?:is )?temporarily unavailable|upstream connect error|overloaded_error/i;
|
|
135
136
|
const TIMEOUT_RE = /\bETIMEDOUT\b|request timed out|connection timed out|deadline exceeded|timed out waiting for/i;
|
|
137
|
+
// OBS-1161: transient capacity is the fourth classification beside quota and the typed dead-channel
|
|
138
|
+
// reasons, and it obeys the same boundary rule: a parsed trailer — ok:true OR ok:false — is the worker
|
|
139
|
+
// speaking, so a verdict that QUOTES the capacity phrase stays ordinary work evidence. The caller
|
|
140
|
+
// hands in the chrome-filtered banner rows as `raw`, exactly as it does for classifyDeadChannel.
|
|
141
|
+
export function classifyTransientCapacity(result) {
|
|
142
|
+
if (result.ok || (result.summary !== NO_TRAILER_SUMMARY && result.summary !== UNPARSEABLE_TRAILER_SUMMARY))
|
|
143
|
+
return null;
|
|
144
|
+
return CAPACITY_RE.exec(result.raw);
|
|
145
|
+
}
|
|
136
146
|
export function classifyDeadChannel(result) {
|
|
137
147
|
// A parsed trailer — ok:true OR ok:false — is the worker speaking: genuine work outcomes walk
|
|
138
148
|
// the normal gate/ladder path even when their transcript mentions auth/outage/timeout text.
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -210,5 +210,6 @@ export declare const PROMPT_ARGV_CEILING_LINUX = 120000;
|
|
|
210
210
|
export declare const PROMPT_ARGV_CEILING_DEFAULT = 900000;
|
|
211
211
|
export declare function promptArgvCeiling(platform?: string): number;
|
|
212
212
|
export declare function promptFitsArgv(promptFile: string, platform?: string): boolean;
|
|
213
|
+
export declare const CAPACITY_RE: RegExp;
|
|
213
214
|
export declare const QUOTA_RE: RegExp;
|
|
214
215
|
export declare const MODEL_ID_RE: RegExp;
|
package/dist/adapters/types.js
CHANGED
|
@@ -294,6 +294,16 @@ export function promptFitsArgv(promptFile, platform = process.platform) {
|
|
|
294
294
|
// ZAI coding-plan exhaustion text: "Insufficient balance or no resource package. Please recharge."
|
|
295
295
|
// Anchor the distinctive full phrase, not the two-word "insufficient balance" fragment — that fires
|
|
296
296
|
// on ordinary billing/wallet task output the harness edits (research Pitfall 3, 2026-07-10).
|
|
297
|
+
// OBS-1161: transient provider capacity ("Selected model is at capacity") is NOT quota — the seat
|
|
298
|
+
// comes back within minutes, so it takes bounded same-seat backoff before a same-floor failover and
|
|
299
|
+
// never permanent demotion. Kept apart from QUOTA_RE so permanent quota policy stays untouched, and
|
|
300
|
+
// anchored on the full phrase (the Pitfall-3 lesson: a bare "capacity" fires on ordinary work text).
|
|
301
|
+
// A banner is the CLI speaking, so the phrase must OPEN its row — after at most an error glyph
|
|
302
|
+
// (codex paints "■ Selected model is at capacity. …") or an "error:" label. Anything else in front —
|
|
303
|
+
// a `>` quote, a quotation mark, a sentence — is the transcript QUOTING it: work evidence, never capacity.
|
|
304
|
+
// ponytail: row-start grammar, not per-adapter fixtures — the ceiling is a worker whose own output
|
|
305
|
+
// opens a row with the bare phrase inside the banner tail; widen the glyph set only from a captured frame.
|
|
306
|
+
export const CAPACITY_RE = /^[ \t■⚠✗✘×!]*(?:(?:stream )?error:[ \t]*)?(?:(?:selected |the )?model is (?:currently |temporarily )?at capacity|(?:provider|service|server) is (?:currently )?at capacity)/im;
|
|
297
307
|
export const QUOTA_RE = /rate.?limit|quota|usage limit|out of credits|insufficient credit|insufficient balance or no resource|\b429\b/i;
|
|
298
308
|
// v1.5 MODEL-01: charset gate for detected model ids (research Pitfall 4, verified 2026-07-10).
|
|
299
309
|
// Ids come from CLI stdout / another program's JSON (models_cache.json) and are echoed into
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
|
-
import { allAdapters, discoverChannels, doctorAgeMs, initDoctorReuse, modelAuthExclusions } from "../../adapters/registry.js";
|
|
4
|
+
import { allAdapters, discoverChannels, doctorAgeMs, initDoctorReuse, modelAuthExclusions, PREFERENCE_ROLES } from "../../adapters/registry.js";
|
|
5
5
|
import { catalogModelAdvisory, catalogTierRanking, declaredModelWindow, fleetUnclassifiedModels } from "../../adapters/model-lints.js";
|
|
6
6
|
import { CATALOG_REFRESH_TIMEOUT_MS, formatCatalogRefreshLegs, readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog-remote.js";
|
|
7
7
|
import { CLAUDE_ALIAS_IDENTITY_STAMPS, readClaudeAliasIdentity } from "../../adapters/claude-code.js";
|
|
@@ -11,6 +11,7 @@ import { SHAPES, TIERS } from "../../graph/schema.js";
|
|
|
11
11
|
import { doctor } from "./doctor.js";
|
|
12
12
|
import { candidateRow, costSignal, shapeCandidates } from "./fleet-picker.js";
|
|
13
13
|
import { route } from "../../route/router.js";
|
|
14
|
+
import { pickRole } from "../../route/role-pick.js";
|
|
14
15
|
import { denyPreferCollisionLine, denyPreferCollisions, disallowedBy, entryMatchesChannel, exclusionCollector } from "../../route/preference.js";
|
|
15
16
|
import { resolveRunMode } from "../../run/daemon.js";
|
|
16
17
|
import { loadRoutingProfile } from "../../run/journal.js";
|
|
@@ -85,6 +86,8 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
85
86
|
const { values } = parseArgs({
|
|
86
87
|
args: argv,
|
|
87
88
|
options: {
|
|
89
|
+
pick: { type: "string" },
|
|
90
|
+
"exclude-vendor": { type: "string", multiple: true },
|
|
88
91
|
print: { type: "boolean" },
|
|
89
92
|
why: { type: "boolean" },
|
|
90
93
|
"global-dir": { type: "string" },
|
|
@@ -92,6 +95,28 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
92
95
|
},
|
|
93
96
|
});
|
|
94
97
|
const globalDir = values["global-dir"] ?? globalConfigDir();
|
|
98
|
+
if (values.pick !== undefined) {
|
|
99
|
+
const role = PREFERENCE_ROLES.find((role) => role === values.pick);
|
|
100
|
+
const refuse = (reason) => ({ out: `tickmarkr fleet --pick ${values.pick}: ${reason}`, code: 1 });
|
|
101
|
+
if (!role)
|
|
102
|
+
return refuse("unsupported-role — expected worker, judge, review or consult; no role prefer resolved");
|
|
103
|
+
if (values.print || values.why || values.fresh)
|
|
104
|
+
return refuse("incompatible options — --pick cannot use --print, --why or --fresh");
|
|
105
|
+
const { cfg } = resolveRunMode(cwd, { globalDir });
|
|
106
|
+
const { reuse, health } = initDoctorReuse(cwd, false);
|
|
107
|
+
const selection = pickRole(role, cfg, adapters, reuse && health ? health : {}, {
|
|
108
|
+
excludeVendors: new Set(values["exclude-vendor"] ?? []),
|
|
109
|
+
});
|
|
110
|
+
if (!selection.ok) {
|
|
111
|
+
return refuse(selection.reason === "missing-prefer" ? `${role}.prefer: missing-prefer`
|
|
112
|
+
: !reuse || !health ? `${role}.prefer: probe data missing or stale — run tickmarkr doctor first`
|
|
113
|
+
: `${role}.prefer: ${selection.reason}`);
|
|
114
|
+
}
|
|
115
|
+
const { adapter, model, vendor, channel } = selection.channel;
|
|
116
|
+
return JSON.stringify({ role, adapter, model, vendor, channel });
|
|
117
|
+
}
|
|
118
|
+
if (values["exclude-vendor"]?.length)
|
|
119
|
+
return { out: "tickmarkr fleet: --exclude-vendor requires --pick <role>", code: 1 };
|
|
95
120
|
const print = values.print ?? false;
|
|
96
121
|
const why = values.why ?? false;
|
|
97
122
|
const input = io.input ?? process.stdin;
|
|
@@ -7,10 +7,10 @@ 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 { chainDepth, dispatchWaves, graphDefinitionHash, loadGraph, stateDirName } from "../../graph/graph.js";
|
|
10
|
+
import { batteryPriority, chainDepth, dispatchWaves, graphDefinitionHash, loadGraph, stateDirName } from "../../graph/graph.js";
|
|
11
11
|
import { filesGlob } from "../../graph/files-glob.js";
|
|
12
12
|
import { renderAcceptanceItem } from "../../graph/schema.js";
|
|
13
|
-
import { resolveRunMode } from "../../run/daemon.js";
|
|
13
|
+
import { pendingDaemonApprovalActions, resolveRunMode } from "../../run/daemon.js";
|
|
14
14
|
import { disallowedBy, excludedChannels, exclusionLine, routingEntrySeatLines } from "../../route/preference.js";
|
|
15
15
|
import { decayWeight, HALF_LIFE_RUNS, staffLedEvidence } from "../../route/profile.js";
|
|
16
16
|
import { route, RoutingError } from "../../route/router.js";
|
|
@@ -372,7 +372,11 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
372
372
|
// OBS-1018: depth and wave come from the same functions the daemon admits by, so the plan shows the
|
|
373
373
|
// critical path the run will actually follow at this concurrency.
|
|
374
374
|
const depths = chainDepth(g);
|
|
375
|
-
|
|
375
|
+
// OBS-1158: waves simulate the graph the daemon would admit on resume — the journal's replayed
|
|
376
|
+
// statuses (an approval releases its park to pending) and its pending recheck approvals ranked
|
|
377
|
+
// through the same seam the daemon admits by.
|
|
378
|
+
const projected = { ...g, tasks: g.tasks.map((t) => ({ ...t, status: taskStatuses.get(t.id) ?? t.status })) };
|
|
379
|
+
const waves = dispatchWaves(projected, cfg.concurrency, batteryPriority(pendingDaemonApprovalActions(matchingEvents ?? []).values()));
|
|
376
380
|
const byId = new Map(g.tasks.map((t) => [t.id, t]));
|
|
377
381
|
const chainLine = (t) => {
|
|
378
382
|
const wave = waves.get(t.id);
|
|
@@ -8,6 +8,7 @@ import { formatOwnedName, parseOwnedName, } from "../../drivers/types.js";
|
|
|
8
8
|
import { blockedTasks, graphDefinitionHash, loadGraph, stateDirName } from "../../graph/graph.js";
|
|
9
9
|
import { GATE_NAMES } from "../../graph/schema.js";
|
|
10
10
|
import { projectActivity } from "../../run/activity.js";
|
|
11
|
+
import { authorsNote, doneAuthors } from "../../run/operator-state.js";
|
|
11
12
|
import { projectOperatorSummary } from "../../run/operator-summary.js";
|
|
12
13
|
import { trackJournalRows } from "../../run/protocol.js";
|
|
13
14
|
import { newestPark, permittedDecisionVerbs } from "./approve.js";
|
|
@@ -988,6 +989,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
988
989
|
const comparable = record?.comparable ?? false;
|
|
989
990
|
const rehashAt = record?.rehashAt;
|
|
990
991
|
const assignments = new Map();
|
|
992
|
+
const authorsByTask = new Map();
|
|
991
993
|
const contexts = new Map();
|
|
992
994
|
// v1.53 T5: this run is dead — a newer run replaced it
|
|
993
995
|
const supersededBy = [...events].reverse()
|
|
@@ -1000,6 +1002,11 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1000
1002
|
assignments.set(e.taskId, `${a.adapter}:${a.model}`);
|
|
1001
1003
|
}
|
|
1002
1004
|
}
|
|
1005
|
+
if (e.event === "task-done" && e.taskId) {
|
|
1006
|
+
const authors = doneAuthors(e.data);
|
|
1007
|
+
if (authors)
|
|
1008
|
+
authorsByTask.set(e.taskId, authors);
|
|
1009
|
+
}
|
|
1003
1010
|
if (e.event === "context-sample" && e.taskId && typeof e.data.tokens === "number" && Number.isFinite(e.data.tokens)) {
|
|
1004
1011
|
contexts.set(e.taskId, e.data.tokens); // last write wins
|
|
1005
1012
|
}
|
|
@@ -1106,12 +1113,14 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1106
1113
|
const label = isStarved ? " starved" : phrase ? ` ${phrase}` : "";
|
|
1107
1114
|
const channel = journalRowsOnly ? folded?.actor ?? "-" : assignments.get(t.id) ?? "-";
|
|
1108
1115
|
const ctx = contexts.get(t.id);
|
|
1109
|
-
|
|
1116
|
+
// Merged authors are their own fact beside the last dispatch: a carry-only seat authored nothing.
|
|
1117
|
+
const authors = authorsNote(authorsByTask.get(t.id), channel);
|
|
1118
|
+
const assignCol = [channel, ctx !== undefined ? `ctx ${ctx}` : "", authors ?? ""].filter(Boolean).join(divider);
|
|
1110
1119
|
const gates = comparable
|
|
1111
1120
|
? gateSnapshot(t, events, rehashAt)
|
|
1112
1121
|
: { states: defaultGateStates(t), priorGraph: false };
|
|
1113
1122
|
const pane = panes.get(t.id);
|
|
1114
|
-
return { t, st, merged, failureKind, redTier, label, assignCol, isStarved, phrase, channel, ctx, livePhase, pane, summary, ...gates };
|
|
1123
|
+
return { t, st, merged, failureKind, redTier, label, assignCol, isStarved, phrase, channel, ctx, authors, livePhase, pane, summary, ...gates };
|
|
1115
1124
|
});
|
|
1116
1125
|
if (!unicode) {
|
|
1117
1126
|
// Keep the machine task-title columns intact; projection details occupy their own line.
|
|
@@ -1215,7 +1224,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1215
1224
|
}).join("");
|
|
1216
1225
|
const gateHeader = GATE_NAMES.map((gate) => fitCells(gate.slice(0, 2), 3)).join("");
|
|
1217
1226
|
const noteFor = (cell) => {
|
|
1218
|
-
const { t, st, merged, failureKind, redTier, states, priorGraph, isStarved, phrase, ctx, livePhase, pane } = cell;
|
|
1227
|
+
const { t, st, merged, failureKind, redTier, states, priorGraph, isStarved, phrase, ctx, livePhase, pane, authors } = cell;
|
|
1219
1228
|
const effort = effortByTask.get(t.id);
|
|
1220
1229
|
const blocking = graphTaskStatus(st, t.status) === "done" ? [] : dependents.get(t.id) ?? [];
|
|
1221
1230
|
const failed = failedGates(states);
|
|
@@ -1242,6 +1251,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1242
1251
|
blocking.length ? `blocks ${blocking.join(", ")}` : "",
|
|
1243
1252
|
pane ? `pane ${pane}` : "",
|
|
1244
1253
|
ctx !== undefined ? `ctx ${ctx}` : "",
|
|
1254
|
+
authors ?? "",
|
|
1245
1255
|
].filter(Boolean).join(" · ");
|
|
1246
1256
|
};
|
|
1247
1257
|
const staleWorker = (cell) => cell.livePhase?.phase === "worker"
|
package/dist/cli/help.d.ts
CHANGED
package/dist/cli/help.js
CHANGED
|
@@ -30,6 +30,8 @@ export const COMMAND_HELP = {
|
|
|
30
30
|
fleet: {
|
|
31
31
|
usage: "fleet [options]", description: "Edit fleet configuration interactively, or print it for scripts.",
|
|
32
32
|
options: {
|
|
33
|
+
"--pick <role>": "Resolve one role preference as JSON without launching a seat (review or consult; roles without prefer refuse).",
|
|
34
|
+
"--exclude-vendor <vendor>": "Exclude a vendor from --pick; repeat for multiple vendors.",
|
|
33
35
|
"--print": "Print fleet configuration without opening the editor.",
|
|
34
36
|
"--why": "Include routing eligibility explanations in printed output.",
|
|
35
37
|
"--global-dir <path>": "Use this global configuration directory.",
|
package/dist/drivers/orca.js
CHANGED
|
@@ -246,7 +246,12 @@ export function provesCheckout(text, checkout) {
|
|
|
246
246
|
* subshell inside it all start in the checkout and its exit status is the payload's (FX-N02).
|
|
247
247
|
*/
|
|
248
248
|
export function checkoutPrefix(checkout) {
|
|
249
|
-
|
|
249
|
+
// OBS-1168: printf BUILDS the marker from two arguments, so the typed command never carries
|
|
250
|
+
// `TICKMARKR_CHECKOUT ` verbatim. The shell's echo of the typed line is repainted and truncated in
|
|
251
|
+
// the scrollback, which used to leave an INCOMPLETE frame beside the complete printed one and made
|
|
252
|
+
// provesCheckout refuse a correct launch. The printed bytes are exactly checkoutProofLine(checkout).
|
|
253
|
+
const head = CHECKOUT_MARK.slice(0, CHECKOUT_MARK.indexOf("_"));
|
|
254
|
+
return `cd ${shq(checkout)} && printf '%s%s\\n' ${head} ${shq(checkoutProofLine(checkout).slice(head.length))} && sh -c `;
|
|
250
255
|
}
|
|
251
256
|
/** The command a terminal on the tracked worktree runs so that it executes INSIDE the checkout. */
|
|
252
257
|
export function inCheckout(checkout, cmd) {
|
package/dist/gates/cache.d.ts
CHANGED
|
@@ -36,9 +36,11 @@ export interface VerificationIdentity {
|
|
|
36
36
|
gate?: string;
|
|
37
37
|
/** which verifier produced it: a battery gate row or an integration-tip row (own report, own forgiveness rule) */
|
|
38
38
|
scope?: VerificationScope;
|
|
39
|
-
/** Checkout location is diagnostic metadata;
|
|
39
|
+
/** Checkout location is diagnostic metadata; build outputs additionally require an incarnation. */
|
|
40
40
|
worktree?: string;
|
|
41
41
|
tree: string;
|
|
42
|
+
/** Build success also promises local outputs, which die with the checkout. */
|
|
43
|
+
checkoutIncarnation?: string;
|
|
42
44
|
command: string;
|
|
43
45
|
baseline: string;
|
|
44
46
|
environment: string;
|
package/dist/gates/cache.js
CHANGED
|
@@ -3,7 +3,7 @@ import { execSync } from "node:child_process";
|
|
|
3
3
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join, relative, resolve } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
|
-
import { describeCapacity, inventoryDependencyLinks, resolvedCapacity, shGit, verificationProtocol } from "../run/git.js";
|
|
6
|
+
import { checkoutIncarnation, describeCapacity, inventoryDependencyLinks, resolvedCapacity, shGit, verificationProtocol } from "../run/git.js";
|
|
7
7
|
import { shq } from "../adapters/types.js";
|
|
8
8
|
export const DEFAULT_VERDICT_CACHE_BOUND = 128;
|
|
9
9
|
let testCacheBound;
|
|
@@ -114,6 +114,9 @@ export async function computeVerificationIdentity(params) {
|
|
|
114
114
|
const tree = params.tree ?? (await getWorktreeTree(params.worktree));
|
|
115
115
|
if (!tree)
|
|
116
116
|
return undefined;
|
|
117
|
+
const incarnation = params.gate === "build" ? checkoutIncarnation(params.worktree) : undefined;
|
|
118
|
+
if (params.gate === "build" && !incarnation)
|
|
119
|
+
return undefined;
|
|
117
120
|
const baseline = baselineIdentity(params.baseline);
|
|
118
121
|
const env = environmentFingerprint({
|
|
119
122
|
worktree: params.worktree,
|
|
@@ -129,6 +132,7 @@ export async function computeVerificationIdentity(params) {
|
|
|
129
132
|
scope: params.scope ?? "battery",
|
|
130
133
|
worktree: realpathSync(params.worktree),
|
|
131
134
|
tree,
|
|
135
|
+
...(incarnation ? { checkoutIncarnation: incarnation } : {}),
|
|
132
136
|
command: params.command,
|
|
133
137
|
baseline,
|
|
134
138
|
environment: env.fingerprint,
|
|
@@ -138,8 +142,10 @@ export async function computeVerificationIdentity(params) {
|
|
|
138
142
|
export function verificationIdentityKey(id) {
|
|
139
143
|
const cmdHash = createHash("sha256").update(id.command).digest("hex").slice(0, 16);
|
|
140
144
|
const gate = id.gate ?? "gate";
|
|
141
|
-
//
|
|
142
|
-
|
|
145
|
+
// Lint/test remain content-addressed. Builds also promise outputs in this physical checkout.
|
|
146
|
+
// Scope continues to separate each verifier's evidence policy.
|
|
147
|
+
const workingTree = createHash("sha256").update(id.tree)
|
|
148
|
+
.update(id.gate === "build" ? `\0checkout:${id.checkoutIncarnation ?? "unbound"}` : "").digest("hex");
|
|
143
149
|
return `${id.scope ?? "battery"}-${gate}-${workingTree}-${cmdHash}-${id.baseline}-${id.environment}`;
|
|
144
150
|
}
|
|
145
151
|
export function formatReusedDetails(originalDetails, id) {
|
|
@@ -179,6 +185,7 @@ export function reusedIdentity(id) {
|
|
|
179
185
|
gate: id.gate ?? "gate",
|
|
180
186
|
scope: id.scope ?? "battery",
|
|
181
187
|
tree: id.tree,
|
|
188
|
+
...(id.checkoutIncarnation ? { checkoutIncarnation: id.checkoutIncarnation } : {}),
|
|
182
189
|
...(id.worktree ? { worktree: id.worktree } : {}),
|
|
183
190
|
command: id.command,
|
|
184
191
|
baseline: id.baseline,
|
package/dist/gates/llm.js
CHANGED
|
@@ -403,7 +403,10 @@ async function runViaDriverDetailed(adapter, model, prompt, cwd, via, timeoutMs
|
|
|
403
403
|
// That seat was killed by its configured timeout, not an early launch reroute.
|
|
404
404
|
if (now - startedAt >= timeoutMs)
|
|
405
405
|
break;
|
|
406
|
-
|
|
406
|
+
// OBS-1177: the beat is armed for an INTERACTIVE (pane) driver only. On the subprocess driver
|
|
407
|
+
// a headless `claude -p` buffers every byte until it exits, so 0 seat bytes at 30 s is not a
|
|
408
|
+
// dead launch; that seat keeps its full ceiling, which still bounds it (fail-closed by timeout).
|
|
409
|
+
if (reviewing && via.driver.interactive && !firstLivenessObserved && now - startedAt >= REVIEW_FIRST_LIVENESS_MS) {
|
|
407
410
|
firstLivenessObserved = true;
|
|
408
411
|
// RS-2: the beat reads seat-authored bytes ALONE. CPU evidence never holds a preamble-only
|
|
409
412
|
// capture open to the ceiling; a seat that has not written one byte of its own is re-routed.
|
package/dist/gates/review.js
CHANGED
|
@@ -8,7 +8,7 @@ import { getAdapter } from "../adapters/registry.js";
|
|
|
8
8
|
import { shOk } from "../run/git.js";
|
|
9
9
|
import { carryReviewFindings, observedReviewFingerprints, reviewFingerprintMatches, structuredFindings, UNIDENTIFIED } from "../run/journal.js";
|
|
10
10
|
import { redactSecrets } from "../run/redact.js";
|
|
11
|
-
import {
|
|
11
|
+
import { rankPreferredChannels, reviewPreferenceTieBreak } from "../route/role-pick.js";
|
|
12
12
|
import { modelProvider } from "../route/preference.js";
|
|
13
13
|
import { resolveStateDir } from "./cache.js";
|
|
14
14
|
import { appendAnchoredReview, COMPLETION_FAKING_CHECKLIST, extractVerdictJson, generateVerdictNonce, parseAnchoredComments, runLlmDetailed, verdictNonceLine } from "./llm.js";
|
|
@@ -227,13 +227,6 @@ export function isReviewClosureMismatch(v, priorIds) {
|
|
|
227
227
|
const priors = priorIds instanceof Set ? priorIds : new Set(priorIds);
|
|
228
228
|
return ids.some((id) => matchClosureId(id, priors) === undefined);
|
|
229
229
|
}
|
|
230
|
-
// v1.53 T2: same entry grammar as routing.map.prefer (router.ts preferIndex — router is out of this
|
|
231
|
-
// module's dependency direction for a private fn, so the 3 lines live here too): `adapter` matches
|
|
232
|
-
// every channel of that adapter, `adapter:model` exactly one; unmatched channels sort after all entries.
|
|
233
|
-
function reviewPreferIndex(c, prefer) {
|
|
234
|
-
const i = prefer.findIndex((p) => p === c.adapter || p === channelKey(c));
|
|
235
|
-
return i === -1 ? prefer.length : i;
|
|
236
|
-
}
|
|
237
230
|
/**
|
|
238
231
|
* RF-1 (OBS-922 add.2/3): the tier a reviewer must meet is the maximum of the author's tier, the
|
|
239
232
|
* task-declared floor, a configured `review.floor` tier and, on a second round or a retry, the prior
|
|
@@ -290,7 +283,7 @@ excludeVendors = new Set(), authors = [channelKey(author)]) {
|
|
|
290
283
|
return null;
|
|
291
284
|
// RF-1: every caller inherits the author-tier floor — a reviewer is never seated below its author.
|
|
292
285
|
const effectiveFloor = resolveReviewerFloor(author.tier, floor).floor;
|
|
293
|
-
const
|
|
286
|
+
const eligible = channels
|
|
294
287
|
// Three independent axes: different vendor, different resolved provider identity (OBS-946: on initial pick
|
|
295
288
|
// as well as failover, so an aggregator channel stamped "mixed" never seats the author's own provider),
|
|
296
289
|
// and different base-model identity (ADDED TO the vendor rule, never replacing it). The diversity
|
|
@@ -300,8 +293,11 @@ excludeVendors = new Set(), authors = [channelKey(author)]) {
|
|
|
300
293
|
&& modelId(c.model) !== modelId(a.model))
|
|
301
294
|
&& !exclude.includes(channelKey(c))
|
|
302
295
|
&& !excludeVendors.has(c.vendor)
|
|
303
|
-
&& TIER_RANK[c.tier] >= TIER_RANK[effectiveFloor])
|
|
304
|
-
|
|
296
|
+
&& TIER_RANK[c.tier] >= TIER_RANK[effectiveFloor]);
|
|
297
|
+
const ranked = rankPreferredChannels(eligible, prefer, {
|
|
298
|
+
includeUnpreferred: true,
|
|
299
|
+
tieBreak: reviewPreferenceTieBreak,
|
|
300
|
+
});
|
|
305
301
|
const reviewer = [...ranked].sort((a, b) => Number(demoted.has(channelKey(a))) - Number(demoted.has(channelKey(b)))
|
|
306
302
|
|| history.lastIndexOf(channelKey(a)) - history.lastIndexOf(channelKey(b))
|
|
307
303
|
|| ranked.indexOf(a) - ranked.indexOf(b))[0] ?? null;
|
|
@@ -7,6 +7,7 @@ import { type GateVia } from "./llm.js";
|
|
|
7
7
|
import { type PriorReviewer } from "./review.js";
|
|
8
8
|
import type { GateResult } from "./types.js";
|
|
9
9
|
import { type VerificationRetryCause } from "../run/recovery.js";
|
|
10
|
+
import { type PreserveProducer } from "../run/git.js";
|
|
10
11
|
import { type StructuredFinding } from "../run/journal.js";
|
|
11
12
|
import { type VerificationScope } from "./cache.js";
|
|
12
13
|
export type LoadProvider = () => number;
|
|
@@ -71,6 +72,9 @@ export interface GateContext {
|
|
|
71
72
|
/** Explicit worker funding requires fresh red measurements, never a gate waiver. */
|
|
72
73
|
cachedRedBypass?: "operator-rerun";
|
|
73
74
|
carriedAuthors?: readonly string[];
|
|
75
|
+
/** The attempt whose worker last wrote the gated checkout; a dirty-tree refusal stamps it on the
|
|
76
|
+
* preserve commit and its row. Absent (standalone verify, gate-only restores) preserves as "unknown". */
|
|
77
|
+
producer?: PreserveProducer;
|
|
74
78
|
reviewHistory?: string[];
|
|
75
79
|
priorReviewers?: PriorReviewer[];
|
|
76
80
|
artifactDir?: string;
|
package/dist/gates/run-gates.js
CHANGED
|
@@ -17,7 +17,7 @@ import { scopeGate } from "./scope.js";
|
|
|
17
17
|
import { evaluateManifestedTest, isVitestTestCommand } from "./test-manifest.js";
|
|
18
18
|
import { executionSignal } from "../run/execution-budget.js";
|
|
19
19
|
import { failureDisposition } from "../run/recovery.js";
|
|
20
|
-
import { dependencyLinkRefusal, preserveWorktree, shGit, resolvedCapacity, verificationProtocol } from "../run/git.js";
|
|
20
|
+
import { dependencyLinkRefusal, preserveWorktree, producerFields, shGit, resolvedCapacity, verificationProtocol } from "../run/git.js";
|
|
21
21
|
import { withJudgeInvocationEvidence } from "../run/journal.js";
|
|
22
22
|
import { computeVerificationIdentity, verificationIdentityKey, formatReusedRow, getVerdictStore, isInfraResult, resolveStateDir, reusedIdentity, } from "./cache.js";
|
|
23
23
|
const productionLoadProvider = () => loadavg()[0] ?? 0;
|
|
@@ -223,6 +223,7 @@ async function runVitestManifestGate(worktree, cmd, baseline, selected, artifact
|
|
|
223
223
|
overallCeilingMs: effectiveCeilingMs(entry),
|
|
224
224
|
artifactDir,
|
|
225
225
|
evidence: retry.evidence,
|
|
226
|
+
retryBaseCommand: retry.retryBaseCommand, // OBS-1166: the un-narrowed command for a selected screen's stranded retry
|
|
226
227
|
});
|
|
227
228
|
const reportPath = outcome.reportPath;
|
|
228
229
|
const evidence = { evidenceReceipt: outcome.evidenceReceipt, evidenceReceipts: outcome.evidenceReceipts };
|
|
@@ -482,7 +483,7 @@ export async function runGates(task, ctx) {
|
|
|
482
483
|
let preservedRef;
|
|
483
484
|
let preservationError;
|
|
484
485
|
try {
|
|
485
|
-
preservedRef = await preserveWorktree(ctx.worktree);
|
|
486
|
+
preservedRef = await preserveWorktree(ctx.worktree, ctx.producer);
|
|
486
487
|
}
|
|
487
488
|
catch (error) {
|
|
488
489
|
// Never masks the refusal, but never pretends a snapshot exists either — surfaced below.
|
|
@@ -537,6 +538,7 @@ export async function runGates(task, ctx) {
|
|
|
537
538
|
dirtyWorktree: true,
|
|
538
539
|
ref: preservedRef,
|
|
539
540
|
preservedRef,
|
|
541
|
+
...producerFields(ctx.producer),
|
|
540
542
|
paths: dirtyPaths,
|
|
541
543
|
files: dirtyPaths,
|
|
542
544
|
path: primaryFile,
|
|
@@ -583,7 +585,7 @@ export async function runGates(task, ctx) {
|
|
|
583
585
|
let preservedRef;
|
|
584
586
|
let preservationError;
|
|
585
587
|
try {
|
|
586
|
-
preservedRef = await preserveWorktree(ctx.worktree);
|
|
588
|
+
preservedRef = await preserveWorktree(ctx.worktree, ctx.producer);
|
|
587
589
|
}
|
|
588
590
|
catch (error) {
|
|
589
591
|
preservationError = error instanceof Error ? error.message : String(error);
|
|
@@ -614,6 +616,7 @@ export async function runGates(task, ctx) {
|
|
|
614
616
|
dirtyAtRoundEnd: true,
|
|
615
617
|
ref: preservedRef,
|
|
616
618
|
preservedRef,
|
|
619
|
+
...producerFields(ctx.producer),
|
|
617
620
|
paths: dirtyPaths,
|
|
618
621
|
files: dirtyPaths,
|
|
619
622
|
path: primaryFile,
|
|
@@ -677,7 +680,7 @@ export async function runGates(task, ctx) {
|
|
|
677
680
|
// other scripted test command keeps today's exit-code contract byte-identically.
|
|
678
681
|
const useManifest = g === "test" && commands.test !== undefined && isVitestTestCommand(commands.test, ctx.worktree);
|
|
679
682
|
r = useManifest
|
|
680
|
-
? await measure(g, () => runVitestManifestGate(ctx.worktree, commands.test, ctx.baseline, selected, ctx.artifactDir, { ...retryOptions(identity), evidence }))
|
|
683
|
+
? await measure(g, () => runVitestManifestGate(ctx.worktree, commands.test, ctx.baseline, selected, ctx.artifactDir, { ...retryOptions(identity), evidence, ...(selected ? { retryBaseCommand: ctx.commands.test } : {}) }))
|
|
681
684
|
: (await measure(g, () => compareToBaseline(ctx.worktree, commands, ctx.baseline, [g], { ...retryOptions(identity), evidence, ...(g === "build" ? { onReceipt: buildReceipt, taskBuildAttribution: beginBuild } : {}), ...(g === "test" && selected ? { selected } : {}) })))[0];
|
|
682
685
|
}
|
|
683
686
|
finally {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type GateEvidenceOptions, type BaselineFileDuration } from "./baseline.js";
|
|
2
2
|
import type { GateEvidenceReceipt } from "../run/protocol.js";
|
|
3
|
+
export declare const VITEST_CACHE_ENV = "TICKMARKR_VITEST_CACHE_DIR";
|
|
4
|
+
export declare function worktreeVitestCache(cwd: string, inherited?: string): string;
|
|
3
5
|
export declare function isVitestTestCommand(cmd: string, cwd: string): boolean;
|
|
4
6
|
/** One identity for every path this module compares: repo-relative, forward-slash. `vitest list
|
|
5
7
|
* --json` and `TestModule.moduleId` both hand back an absolute filesystem path already resolved
|
|
@@ -144,6 +146,13 @@ export declare function discoverTestManifest(cmd: string, cwd: string, opts: {
|
|
|
144
146
|
* runner cannot list (a null compares nothing — it never manufactures a deficit). The suite is not
|
|
145
147
|
* run here; the capture already ran it once. */
|
|
146
148
|
export declare function manifestFileCount(cmd: string, cwd: string): Promise<number | null>;
|
|
149
|
+
/** The stranded single-fork retry: positional filters are substring matches that OR with any filter
|
|
150
|
+
* the command already carries, and under a `projects` config the CLI `--exclude` never subtracts such a
|
|
151
|
+
* selection (OBS-1166: a selected screen's retry rediscovered the whole selection and refused). So the
|
|
152
|
+
* retry is built from the UN-narrowed base command, its own `--` rule, the stranded files as the only
|
|
153
|
+
* positional filters, and an `--exclude` of every completed file; the caller then requires discovery
|
|
154
|
+
* to prove the exact retry set before launch. */
|
|
155
|
+
export declare function singleForkRetryCommand(base: string, cwd: string, stranded: readonly string[], completed: readonly string[]): string;
|
|
147
156
|
/** One configured runner execution, and its own collection under the same arguments and environment.
|
|
148
157
|
* The installed runner is trusted (R28 add.1 option B); the nonce catches stale artifacts, not forgery. */
|
|
149
158
|
export declare function evaluateManifestedTest(cmd: string, cwd: string, opts: {
|
|
@@ -152,4 +161,7 @@ export declare function evaluateManifestedTest(cmd: string, cwd: string, opts: {
|
|
|
152
161
|
overallCeilingMs?: number;
|
|
153
162
|
artifactDir?: string;
|
|
154
163
|
evidence?: GateEvidenceOptions;
|
|
164
|
+
/** OBS-1166: the configured command BEFORE a selected screen narrowed it, so a stranded retry's
|
|
165
|
+
* only positional filters are the stranded files. Absent (a full suite), `cmd` is that command. */
|
|
166
|
+
retryBaseCommand?: string;
|
|
155
167
|
}): Promise<ManifestGateOutcome>;
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
-
import { existsSync, mkdtempSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { isAbsolute, join, relative, sep } from "node:path";
|
|
4
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { TEST_REPORTER_SOURCE } from "./test-reporter.js";
|
|
6
6
|
import { shq } from "../adapters/types.js";
|
|
7
7
|
import { beginGateEvidence, redactGateOutput } from "./baseline.js";
|
|
8
8
|
import { FORK_CAP_ENV, ROUTING_ENV_SEAMS, SUITE_PARENT_ENV, shell, resolvedCapacity, verificationProtocol } from "../run/git.js";
|
|
9
|
+
// Outside the shared dependency symlink and ignored by the shipped repository.
|
|
10
|
+
export const VITEST_CACHE_ENV = "TICKMARKR_VITEST_CACHE_DIR";
|
|
11
|
+
export function worktreeVitestCache(cwd, inherited) {
|
|
12
|
+
const local = resolve(cwd, ".vitest-cache");
|
|
13
|
+
const candidate = inherited ? resolve(cwd, inherited) : local;
|
|
14
|
+
const within = relative(local, candidate);
|
|
15
|
+
return within === "" || (!isAbsolute(within) && within !== ".." && !within.startsWith(`..${sep}`))
|
|
16
|
+
? candidate : local;
|
|
17
|
+
}
|
|
9
18
|
/**
|
|
10
19
|
* VL-1 (OBS-985 lineage): a test gate's completion must be the runner's OWN report, never a stdout
|
|
11
20
|
* count. `fileCountDeficit` (baseline.ts) reads a summary LINE — a selected screen's smaller count
|
|
@@ -387,6 +396,7 @@ export function runManifestedTest(cmd, cwd, opts) {
|
|
|
387
396
|
* verdict measured with `pretest` hooks is never compared to one without. */
|
|
388
397
|
function manifestEnvironment(cwd) {
|
|
389
398
|
const env = { ...process.env, PATH: `${join(cwd, "node_modules/.bin")}:${process.env.PATH ?? ""}`,
|
|
399
|
+
[VITEST_CACHE_ENV]: worktreeVitestCache(cwd),
|
|
390
400
|
[FORK_CAP_ENV]: String(resolvedCapacity().forkCap), [SUITE_PARENT_ENV]: String(process.pid) };
|
|
391
401
|
const verification = verificationProtocol(env, cwd);
|
|
392
402
|
for (const key of ROUTING_ENV_SEAMS)
|
|
@@ -436,6 +446,9 @@ export async function manifestFileCount(cmd, cwd) {
|
|
|
436
446
|
catch {
|
|
437
447
|
return null;
|
|
438
448
|
}
|
|
449
|
+
finally {
|
|
450
|
+
rmSync(dir, { recursive: true, force: true });
|
|
451
|
+
} // OBS-1155: the listing directory is the capture's alone, listed or not
|
|
439
452
|
}
|
|
440
453
|
/** Vitest's forks pool awaits the parallel phase, then throws before the single-fork phase
|
|
441
454
|
* on any rejected worker. Recover only that exact, fully accounted-for boundary. */
|
|
@@ -474,6 +487,16 @@ function strandedSingleForkFiles(files, nonce, run) {
|
|
|
474
487
|
return;
|
|
475
488
|
return single;
|
|
476
489
|
}
|
|
490
|
+
/** The stranded single-fork retry: positional filters are substring matches that OR with any filter
|
|
491
|
+
* the command already carries, and under a `projects` config the CLI `--exclude` never subtracts such a
|
|
492
|
+
* selection (OBS-1166: a selected screen's retry rediscovered the whole selection and refused). So the
|
|
493
|
+
* retry is built from the UN-narrowed base command, its own `--` rule, the stranded files as the only
|
|
494
|
+
* positional filters, and an `--exclude` of every completed file; the caller then requires discovery
|
|
495
|
+
* to prove the exact retry set before launch. */
|
|
496
|
+
export function singleForkRetryCommand(base, cwd, stranded, completed) {
|
|
497
|
+
const excluded = completed.map(f => `--exclude=${shq(f.replace(/[\\*?[\]{}()!+@]/g, "\\$&"))}`).join(" ");
|
|
498
|
+
return `${base}${runnerInvocation(base, cwd).separator} ${stranded.map(f => shq(join(cwd, f))).join(" ")} ${excluded}`;
|
|
499
|
+
}
|
|
477
500
|
/** One configured runner execution, and its own collection under the same arguments and environment.
|
|
478
501
|
* The installed runner is trusted (R28 add.1 option B); the nonce catches stale artifacts, not forgery. */
|
|
479
502
|
export async function evaluateManifestedTest(cmd, cwd, opts) {
|
|
@@ -518,10 +541,7 @@ export async function evaluateManifestedTest(cmd, cwd, opts) {
|
|
|
518
541
|
recovery = { firstNonce: nonce, firstReportPath: reportPath, retryNonce, files: stranded };
|
|
519
542
|
nonce = retryNonce;
|
|
520
543
|
reportPath = join(dir, `test-manifest-report-${nonce}.json`);
|
|
521
|
-
|
|
522
|
-
// completed file as well, then require discovery to prove the exact retry set before launch.
|
|
523
|
-
const excluded = files.filter(f => !stranded.includes(f)).map(f => `--exclude=${shq(f.replace(/[\\*?[\]{}()!+@]/g, "\\$&"))}`).join(" ");
|
|
524
|
-
const retryCommand = `${cmd}${invocation.separator} ${stranded.map(f => shq(join(cwd, f))).join(" ")} ${excluded}`;
|
|
544
|
+
const retryCommand = singleForkRetryCommand(opts.retryBaseCommand ?? cmd, cwd, stranded, files.filter(f => !stranded.includes(f)));
|
|
525
545
|
const listed = await discoverTestManifest(retryCommand, cwd, { dir, nonce, env,
|
|
526
546
|
overallCeilingMs: opts.overallCeilingMs, evidence: { ...opts.evidence, artifactDir: opts.evidence?.artifactDir ?? dir } });
|
|
527
547
|
evidenceReceipts.push(...listed.evidenceReceipts);
|
package/dist/graph/graph.d.ts
CHANGED
|
@@ -36,8 +36,12 @@ export declare function addEvidence(g: RunGraph, id: string, patch: {
|
|
|
36
36
|
gateResults?: unknown[];
|
|
37
37
|
}): RunGraph;
|
|
38
38
|
export declare function chainDepth(g: RunGraph): Map<string, number>;
|
|
39
|
-
export declare function readyTasks(g: RunGraph): Task[];
|
|
40
|
-
export declare function
|
|
39
|
+
export declare function readyTasks(g: RunGraph, prioritized?: ReadonlySet<string>): Task[];
|
|
40
|
+
export declare function batteryPriority(actions: Iterable<{
|
|
41
|
+
taskId: string;
|
|
42
|
+
authority: string;
|
|
43
|
+
}>): Set<string>;
|
|
44
|
+
export declare function dispatchWaves(g: RunGraph, concurrency: number, prioritized?: ReadonlySet<string>): Map<string, number>;
|
|
41
45
|
export declare function isComplete(g: RunGraph): boolean;
|
|
42
46
|
export declare function isStalled(g: RunGraph): boolean;
|
|
43
47
|
export declare function closureReaches(g: RunGraph, taskId: string, pred: (t: Task) => boolean): boolean;
|
package/dist/graph/graph.js
CHANGED
|
@@ -191,21 +191,32 @@ export function chainDepth(g) {
|
|
|
191
191
|
// OBS-1018: admission is critical-path order — deepest chain root first, ties in declaration order.
|
|
192
192
|
// The daemon's dispatch loop slices this list unchanged. Stated limit: resume-restore of previously
|
|
193
193
|
// in-flight attempts keeps its own precedence (src/run/daemon.ts); only fresh admission is ordered here.
|
|
194
|
-
|
|
194
|
+
// OBS-1158: `prioritized` names tasks holding a pending battery (recheck) approval. Among READY tasks
|
|
195
|
+
// of equal depth an approved recheck precedes fresh work; deeper fresh work keeps its rank, and a
|
|
196
|
+
// recheck that is not ready (dependency-blocked) is not admitted at all, so it gains nothing here.
|
|
197
|
+
export function readyTasks(g, prioritized = NO_PRIORITY) {
|
|
195
198
|
const done = new Set(g.tasks.filter((t) => t.status === "done").map((t) => t.id));
|
|
196
199
|
const depth = chainDepth(g);
|
|
200
|
+
const rank = (t) => (prioritized.has(t.id) ? 1 : 0);
|
|
197
201
|
return g.tasks
|
|
198
202
|
.filter((t) => t.status === "pending" && t.deps.every((d) => done.has(d)))
|
|
199
|
-
.sort((a, b) => depth.get(b.id) - depth.get(a.id)); // Array#sort is stable:
|
|
203
|
+
.sort((a, b) => depth.get(b.id) - depth.get(a.id) || rank(b) - rank(a)); // Array#sort is stable: remaining ties keep declaration order
|
|
204
|
+
}
|
|
205
|
+
const NO_PRIORITY = new Set();
|
|
206
|
+
// OBS-1158: the one seam plan and the daemon share for battery priority — journal approval actions
|
|
207
|
+
// in (src/run/journal.ts pendingApprovalActions), the ids whose pending authority is `battery` out.
|
|
208
|
+
// Waivers, worker-funding approvals and inert releases never qualify; a consumed approval is absent.
|
|
209
|
+
export function batteryPriority(actions) {
|
|
210
|
+
return new Set([...actions].filter((a) => a.authority === "battery").map((a) => a.taskId));
|
|
200
211
|
}
|
|
201
212
|
// OBS-1018: the dispatch wave each pending task would enter at `concurrency` slots if every wave
|
|
202
213
|
// took one tick — computed by draining readyTasks, so plan and daemon can never disagree on order.
|
|
203
214
|
// Tasks already past pending carry no wave.
|
|
204
|
-
export function dispatchWaves(g, concurrency) {
|
|
215
|
+
export function dispatchWaves(g, concurrency, prioritized = NO_PRIORITY) {
|
|
205
216
|
const waves = new Map();
|
|
206
217
|
let sim = g;
|
|
207
218
|
for (let wave = 1;; wave++) {
|
|
208
|
-
const batch = readyTasks(sim).slice(0, Math.max(1, concurrency));
|
|
219
|
+
const batch = readyTasks(sim, prioritized).slice(0, Math.max(1, concurrency));
|
|
209
220
|
if (!batch.length)
|
|
210
221
|
return waves;
|
|
211
222
|
for (const t of batch) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type AuthHealth, type BillingChannel, type WorkerAdapter } from "../adapters/types.js";
|
|
2
|
+
import { type TickmarkrConfig } from "../config/config.js";
|
|
3
|
+
import type { PreferenceRole } from "./preference.js";
|
|
4
|
+
export { preferIndex, rankPreferredChannels, reviewPreferenceTieBreak } from "./router.js";
|
|
5
|
+
export type RolePick = {
|
|
6
|
+
ok: true;
|
|
7
|
+
channel: BillingChannel;
|
|
8
|
+
} | {
|
|
9
|
+
ok: false;
|
|
10
|
+
reason: "missing-prefer" | "no-eligible-preferred-channel";
|
|
11
|
+
};
|
|
12
|
+
/** Strict role selection: no task, author, pin, or default seat is invented. */
|
|
13
|
+
export declare function pickRole(role: PreferenceRole, cfg: TickmarkrConfig, adapters: WorkerAdapter[], health: Record<string, AuthHealth>, opts?: {
|
|
14
|
+
excludeVendors?: ReadonlySet<string>;
|
|
15
|
+
eligible?: (channel: BillingChannel) => boolean;
|
|
16
|
+
}): RolePick;
|