tickmarkr 2.5.0 → 2.5.2
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/fake.d.ts +1 -0
- package/dist/adapters/fake.js +2 -1
- package/dist/adapters/opencode.d.ts +1 -0
- package/dist/adapters/opencode.js +4 -1
- package/dist/adapters/types.d.ts +14 -0
- package/dist/adapters/types.js +25 -0
- package/dist/cli/commands/approve.js +1 -1
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.js +46 -6
- package/dist/cli/commands/plan.js +58 -6
- package/dist/cli/commands/report.js +3 -1
- package/dist/cli/commands/run.d.ts +5 -0
- package/dist/cli/commands/run.js +17 -1
- package/dist/compile/native.js +3 -0
- package/dist/config/config.d.ts +6 -0
- package/dist/config/config.js +6 -0
- package/dist/drivers/subprocess.d.ts +2 -3
- package/dist/drivers/subprocess.js +2 -3
- package/dist/gates/baseline.d.ts +9 -0
- package/dist/gates/baseline.js +85 -29
- package/dist/gates/llm.d.ts +5 -0
- package/dist/gates/llm.js +188 -3
- package/dist/gates/review.d.ts +6 -3
- package/dist/gates/review.js +95 -42
- package/dist/gates/run-gates.d.ts +3 -0
- package/dist/gates/run-gates.js +14 -4
- package/dist/run/daemon.d.ts +8 -4
- package/dist/run/daemon.js +3150 -2839
- package/dist/run/git.d.ts +4 -2
- package/dist/run/git.js +11 -23
- package/dist/run/merge.d.ts +1 -1
- package/dist/run/merge.js +126 -18
- package/dist/tui/ink/init-app.d.ts +4 -0
- package/dist/tui/ink/init-app.js +18 -7
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/scripts/watch-context.sh +16 -2
package/dist/adapters/fake.d.ts
CHANGED
package/dist/adapters/fake.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { parseWorkerResult } from "./prompt.js";
|
|
4
|
-
import { shq, TokenUsageSchema } from "./types.js";
|
|
4
|
+
import { OPENCODE_BUSY_FRAME_MARKERS, shq, TokenUsageSchema } from "./types.js";
|
|
5
5
|
// ponytail: deterministic scripted adapter — the whole integration suite runs on it, zero tokens.
|
|
6
6
|
export class FakeAdapter {
|
|
7
7
|
scriptPath;
|
|
8
8
|
id = "fake";
|
|
9
9
|
vendor = "fake-a";
|
|
10
|
+
busyFrameMarkers = OPENCODE_BUSY_FRAME_MARKERS;
|
|
10
11
|
// v1.89 T1 / OBS-414: the scripted adapter runs `bash -c` — it has no TUI, so no prompt of any
|
|
11
12
|
// kind can render. Daemon tests that exercise the auto-answer seam assign a captured declaration
|
|
12
13
|
// to this field explicitly; that assignment is the falsifier for the claim made here.
|
|
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { probeVersion } from "./claude-code.js";
|
|
6
6
|
import { parseWorkerResult } from "./prompt.js";
|
|
7
|
-
import { channelsFromConfig, MODEL_ID_RE, shq, TokenUsageSchema } from "./types.js";
|
|
7
|
+
import { channelsFromConfig, MODEL_ID_RE, OPENCODE_BUSY_FRAME_MARKERS, shq, TokenUsageSchema } from "./types.js";
|
|
8
8
|
// v1.5 MODEL-01: pure parser for `opencode models` (one provider/model id per line, no header).
|
|
9
9
|
// Live-verified 2026-07-10, opencode 1.17.15. Lines are already the seed id format; drop blanks.
|
|
10
10
|
export function parseOpencodeModels(raw) {
|
|
@@ -13,9 +13,12 @@ export function parseOpencodeModels(raw) {
|
|
|
13
13
|
.filter((l) => l.length > 0)
|
|
14
14
|
.filter((id) => MODEL_ID_RE.test(id));
|
|
15
15
|
}
|
|
16
|
+
// Keep the adapter export available without making the fake adapter import probe code.
|
|
17
|
+
export { OPENCODE_BUSY_FRAME_MARKERS } from "./types.js";
|
|
16
18
|
export const opencode = {
|
|
17
19
|
id: "opencode",
|
|
18
20
|
vendor: "mixed",
|
|
21
|
+
busyFrameMarkers: OPENCODE_BUSY_FRAME_MARKERS,
|
|
19
22
|
probeCwd: "neutral",
|
|
20
23
|
probe: async () => probeVersion("opencode"),
|
|
21
24
|
channels: (cfg) => channelsFromConfig("opencode", cfg),
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -150,6 +150,19 @@ export declare function matchesInputBox(paneText: string, inputBox: InputBox): b
|
|
|
150
150
|
export declare function matchesEmptyInputBox(paneText: string, inputBox: InputBox): boolean;
|
|
151
151
|
export declare function matchesOccupiedInputBox(paneText: string, inputBox: InputBox): boolean;
|
|
152
152
|
export declare function missingInputStateDeclarations(inputBox: InputBox | undefined): string[];
|
|
153
|
+
export interface BusyFrameMarker {
|
|
154
|
+
name: string;
|
|
155
|
+
match(paneText: string): boolean;
|
|
156
|
+
}
|
|
157
|
+
export declare const OPENCODE_BUSY_FRAME_MARKERS: readonly BusyFrameMarker[];
|
|
158
|
+
export declare class SettledTrailerTracker {
|
|
159
|
+
private readonly markers;
|
|
160
|
+
private idleSamples;
|
|
161
|
+
readonly busyMarkersSeen: Set<string>;
|
|
162
|
+
settled: boolean;
|
|
163
|
+
constructor(markers: readonly BusyFrameMarker[]);
|
|
164
|
+
sample(paneText: string, trailer: boolean): boolean;
|
|
165
|
+
}
|
|
153
166
|
export interface WorkerAdapter {
|
|
154
167
|
id: string;
|
|
155
168
|
vendor: string;
|
|
@@ -174,6 +187,7 @@ export interface WorkerAdapter {
|
|
|
174
187
|
trust?(repoRoot: string): TrustVerdict;
|
|
175
188
|
trustDialog: TrustDialog;
|
|
176
189
|
inputBox?: InputBox;
|
|
190
|
+
busyFrameMarkers?: readonly BusyFrameMarker[];
|
|
177
191
|
hardcodedFlags?: {
|
|
178
192
|
binary: string;
|
|
179
193
|
flags: string[];
|
package/dist/adapters/types.js
CHANGED
|
@@ -207,6 +207,31 @@ export function missingInputStateDeclarations(inputBox) {
|
|
|
207
207
|
missing.push("occupiedMatch");
|
|
208
208
|
return missing;
|
|
209
209
|
}
|
|
210
|
+
// OBS-957 capture retained under fixtures/opencode (the captured UI identifies as omp).
|
|
211
|
+
// Anchor activity glyphs to chrome/tool rows so a quoted spinner in a summary is harmless.
|
|
212
|
+
export const OPENCODE_BUSY_FRAME_MARKERS = [
|
|
213
|
+
{ name: "unchecked TODO", match: (text) => /(?:^|\n)[^\n]*[├└]─[^\n]*☐/.test(text) || /(?:^|\n)\s*[-*] \[ \]/.test(text) },
|
|
214
|
+
{ name: "in-flight tool", match: (text) => /(?:^|\n)\s*⎋ [^\n]+/.test(text) },
|
|
215
|
+
{ name: "spinner", match: (text) => /(?:^|\n)(?:[╭│][─ ]*|\s*)[⠁-⣿](?:\s|$)/.test(text) },
|
|
216
|
+
];
|
|
217
|
+
export class SettledTrailerTracker {
|
|
218
|
+
markers;
|
|
219
|
+
idleSamples = 0;
|
|
220
|
+
busyMarkersSeen = new Set();
|
|
221
|
+
settled = false;
|
|
222
|
+
constructor(markers) {
|
|
223
|
+
this.markers = markers;
|
|
224
|
+
}
|
|
225
|
+
sample(paneText, trailer) {
|
|
226
|
+
const busy = this.markers.filter((marker) => marker.match(paneText));
|
|
227
|
+
if (trailer)
|
|
228
|
+
for (const marker of busy)
|
|
229
|
+
this.busyMarkersSeen.add(marker.name);
|
|
230
|
+
this.idleSamples = trailer && busy.length === 0 ? this.idleSamples + 1 : 0;
|
|
231
|
+
this.settled = this.idleSamples >= 2;
|
|
232
|
+
return this.settled;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
210
235
|
export function channelsFromConfig(adapterId, cfg) {
|
|
211
236
|
const e = cfg.tiers[adapterId];
|
|
212
237
|
if (!e)
|
|
@@ -114,7 +114,7 @@ export function approvalRunOwner(cwd, runId) {
|
|
|
114
114
|
/** The one sentence that says who enacts this release and what it buys. */
|
|
115
115
|
export function approvalEnactment(token, run) {
|
|
116
116
|
if (run.live) {
|
|
117
|
-
return `the live daemon enacts this at its next task boundary — it will ${APPROVAL_ENACTS[token]}`;
|
|
117
|
+
return `the live daemon enacts this at its next task boundary — it will ${APPROVAL_ENACTS[token]}, including in the approval window or by cancelling an in-progress tip verify`;
|
|
118
118
|
}
|
|
119
119
|
if (run.blockingRunId) {
|
|
120
120
|
return `release recorded; live run \`${run.blockingRunId}\` holds the repository lock, so resume \`${run.runId}\` after it ends to ${APPROVAL_ENACTS[token]}`;
|
|
@@ -162,6 +162,7 @@ export declare function doctorProbePreflight(cwd?: string, cfg?: {
|
|
|
162
162
|
gates: {
|
|
163
163
|
build?: string | undefined;
|
|
164
164
|
test?: string | undefined;
|
|
165
|
+
tipTest?: string | undefined;
|
|
165
166
|
lint?: string | undefined;
|
|
166
167
|
diffCap?: number | undefined;
|
|
167
168
|
byShape?: Partial<Record<"plan" | "spec" | "implement" | "tests" | "docs" | "migration" | "ui" | "refactor" | "chore", {
|
|
@@ -278,6 +279,7 @@ export declare function cachedDoctorDiagnostics(cwd?: string, adapters?: WorkerA
|
|
|
278
279
|
gates: {
|
|
279
280
|
build?: string | undefined;
|
|
280
281
|
test?: string | undefined;
|
|
282
|
+
tipTest?: string | undefined;
|
|
281
283
|
lint?: string | undefined;
|
|
282
284
|
diffCap?: number | undefined;
|
|
283
285
|
byShape?: Partial<Record<"plan" | "spec" | "implement" | "tests" | "docs" | "migration" | "ui" | "refactor" | "chore", {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export type DetectedHost = "herdr" | "orca" | "subprocess";
|
|
2
|
+
export declare function detectedHost(env?: NodeJS.ProcessEnv): DetectedHost;
|
|
1
3
|
/** Test seam mirroring FleetIO: production callers omit it and get the process streams. */
|
|
2
4
|
export type InitIO = {
|
|
3
5
|
input?: NodeJS.ReadStream;
|
|
@@ -4,6 +4,7 @@ import { join, relative } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
6
|
import { parseArgs } from "node:util";
|
|
7
|
+
import { parse as parseYaml } from "yaml";
|
|
7
8
|
import { allAdapters, formatDoctorAgeForInit, formatDoctorReport, initDoctorReuse } from "../../adapters/registry.js";
|
|
8
9
|
import { configTemplate, DEFAULT_CONFIG, globalConfigDir, loadConfig } from "../../config/config.js";
|
|
9
10
|
import { LEGACY_PREFIX, specTemplate } from "../../compile/native.js";
|
|
@@ -113,7 +114,7 @@ A run is green only when ALL of these hold: the run-end event exists in the jour
|
|
|
113
114
|
|
|
114
115
|
### Verified handoffs
|
|
115
116
|
|
|
116
|
-
When relaying missions between agents, never use bare send-text (\`herdr agent send\` / pane send-text) — it omits Enter. Use \`herdr pane run <pane> "<message>"\` or \`herdr notification show "<message>"\`. Confirm delivery by reading the
|
|
117
|
+
When relaying missions between agents, never use bare send-text (\`herdr agent send\` / pane send-text) — it omits Enter. Use \`herdr pane run <pane> "<message>"\` or \`herdr notification show "<message>"\`. Confirm delivery by reading the TARGET composer afterward; if the sent text still sits unsubmitted after ~15 s, send-keys enter to that pane once and read again (OBS-975); never report "relayed" without read-back.
|
|
117
118
|
|
|
118
119
|
### Orient before you act — this block may be the ONLY guidance your host loaded
|
|
119
120
|
|
|
@@ -218,7 +219,14 @@ const describeDrift = (d) => [
|
|
|
218
219
|
// should not be asked again just because a copy drifted. Staleness is a different question with a
|
|
219
220
|
// different answer (`--agent --force`), and it is reported by name on every init path below.
|
|
220
221
|
const skillsInstalled = (cwd) => hostTargets(cwd).every((t) => AGENT_SKILLS.every((s) => existsSync(join(t.skillsDir, s, "SKILL.md"))));
|
|
221
|
-
|
|
222
|
+
export function detectedHost(env = process.env) {
|
|
223
|
+
if (env.HERDR_ENV === "1")
|
|
224
|
+
return "herdr";
|
|
225
|
+
if (orcaHostDetected(env))
|
|
226
|
+
return "orca";
|
|
227
|
+
return "subprocess";
|
|
228
|
+
}
|
|
229
|
+
const wizardDriverDefault = () => "auto";
|
|
222
230
|
function packageName(cwd) {
|
|
223
231
|
try {
|
|
224
232
|
const parsed = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
|
|
@@ -323,6 +331,7 @@ async function runInitWizard(cwd, input, output) {
|
|
|
323
331
|
visibilityLlm: DEFAULT_CONFIG.visibility.llm,
|
|
324
332
|
offerSkills: !skillsInstalled(cwd),
|
|
325
333
|
skillsDefault: existsSync(join(cwd, ".claude", "skills")) && !skillsInstalled(cwd),
|
|
334
|
+
detectedHost: detectedHost(),
|
|
326
335
|
},
|
|
327
336
|
input,
|
|
328
337
|
output,
|
|
@@ -368,8 +377,37 @@ export async function init(argv, cwd = process.cwd(), io = {}) {
|
|
|
368
377
|
}
|
|
369
378
|
const repoConfigPath = join(tickmarkrDir(cwd), "config.yaml");
|
|
370
379
|
const repoConfigExists = existsSync(repoConfigPath);
|
|
371
|
-
if (repoConfigExists)
|
|
372
|
-
|
|
380
|
+
if (repoConfigExists) {
|
|
381
|
+
if (values.force) {
|
|
382
|
+
const currentRaw = readFileSync(repoConfigPath, "utf8");
|
|
383
|
+
let parsedDriver;
|
|
384
|
+
try {
|
|
385
|
+
const parsed = parseYaml(currentRaw);
|
|
386
|
+
if (typeof parsed?.driver === "string")
|
|
387
|
+
parsedDriver = parsed.driver;
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
// malformed: fall back to regex
|
|
391
|
+
}
|
|
392
|
+
if (!parsedDriver) {
|
|
393
|
+
const m = currentRaw.match(/^\s*driver:\s*([^\s#]+)/m);
|
|
394
|
+
if (m)
|
|
395
|
+
parsedDriver = m[1];
|
|
396
|
+
}
|
|
397
|
+
const host = detectedHost();
|
|
398
|
+
if (parsedDriver && parsedDriver === host) {
|
|
399
|
+
const rewritten = currentRaw.replace(new RegExp(`^(\\s*driver:\\s*)${parsedDriver}\\b.*$`, "m"), `$1auto # auto — resolves to ${host} here`);
|
|
400
|
+
writeFileSync(repoConfigPath, rewritten);
|
|
401
|
+
notes.push(`rewrote driver: ${parsedDriver} to auto in ${repoConfigPath}`);
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
notes.push(`kept existing ${repoConfigPath}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
notes.push(`kept existing ${repoConfigPath}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
373
411
|
const specPath = join(cwd, SCAFFOLD_SPEC);
|
|
374
412
|
if (existsSync(specPath)) {
|
|
375
413
|
notes.push(`kept existing ${specPath}`);
|
|
@@ -417,7 +455,9 @@ export async function init(argv, cwd = process.cwd(), io = {}) {
|
|
|
417
455
|
// so costs one clause; everything else stops here. Esc and ctrl+c both land here.
|
|
418
456
|
return `init: wizard quit — nothing further run (scaffolding kept: ${notes.filter((n) => n.startsWith("wrote")).length} file(s) written above). Re-run tickmarkr init to continue.`;
|
|
419
457
|
}
|
|
420
|
-
|
|
458
|
+
const host = detectedHost();
|
|
459
|
+
const renderedConfig = configTemplate(wizard.overlay).replace(/^driver: auto$/m, `driver: auto # auto — resolves to ${host} here`);
|
|
460
|
+
writeFileSync(repoConfigPath, renderedConfig);
|
|
421
461
|
notes.push(`wrote ${repoConfigPath}`);
|
|
422
462
|
if (wizard.installSkills) {
|
|
423
463
|
await installAgentFiles(cwd, values.force ?? false, wizard.installDocs || (values.docs ?? false), notes);
|
|
@@ -462,7 +502,7 @@ export async function init(argv, cwd = process.cwd(), io = {}) {
|
|
|
462
502
|
const next = nextSteps(cwd, SCAFFOLD_SPEC);
|
|
463
503
|
if (!visual())
|
|
464
504
|
return `${notes.join("\n")}\n${doc}\n${next}\n${ENVIRONMENTS_FOOTER}`;
|
|
465
|
-
const noteRows = notes.map((note) => ` ${statusRow(/^(?:wrote|overwrote|appended)/.test(note) ? "pass" : note.startsWith("skipped") ? "warn" : "neutral", note)}`);
|
|
505
|
+
const noteRows = notes.map((note) => ` ${statusRow(/^(?:wrote|overwrote|appended|rewrote)/.test(note) ? "pass" : note.startsWith("skipped") ? "warn" : "neutral", note)}`);
|
|
466
506
|
const footerRows = ENVIRONMENTS_FOOTER.split("\n").slice(1).map((line) => {
|
|
467
507
|
const separator = line.indexOf(" — ");
|
|
468
508
|
return kvRow(line.slice(2, separator), line.slice(separator + 1), 12);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
4
|
import { allAdapters, doctorAgeMs, modelAuthExclusions, probeAll, readDoctor, rolePools, servableExclusions, servabilityLine } from "../../adapters/registry.js";
|
|
@@ -7,7 +7,7 @@ import { GLYPHS, dim, rule, title, warn } from "../../brand.js";
|
|
|
7
7
|
import { collateralLints, sourceScopeLints } from "../../compile/collateral.js";
|
|
8
8
|
import { classifyContextPath } from "../../compile/native.js";
|
|
9
9
|
import { DEFAULT_CONFIG, effectiveReviewPolicy, overlayPreferShapes, ROUTING_MODES, TIER_RANK } from "../../config/config.js";
|
|
10
|
-
import { loadGraph } from "../../graph/graph.js";
|
|
10
|
+
import { graphDefinitionHash, loadGraph, stateDirName } from "../../graph/graph.js";
|
|
11
11
|
import { renderAcceptanceItem } from "../../graph/schema.js";
|
|
12
12
|
import { resolveRunMode } from "../../run/daemon.js";
|
|
13
13
|
import { disallowedBy, excludedChannels, exclusionLine, routingEntrySeatLines } from "../../route/preference.js";
|
|
@@ -15,7 +15,7 @@ import { staffLedEvidence } from "../../route/profile.js";
|
|
|
15
15
|
import { route, RoutingError } from "../../route/router.js";
|
|
16
16
|
import { auditNamedTestOracles, listVitestTests } from "../../gates/acceptance.js";
|
|
17
17
|
import { modelId, modelProvider, pickReviewer } from "../../gates/review.js";
|
|
18
|
-
import { Journal, loadRoutingProfile } from "../../run/journal.js";
|
|
18
|
+
import { Journal, loadRoutingProfile, recordedGraphDefinitionHash } from "../../run/journal.js";
|
|
19
19
|
import { harnessLine, resolveHarness } from "../harness.js";
|
|
20
20
|
import { channelKey, shq } from "../../adapters/types.js";
|
|
21
21
|
import { shGit } from "../../run/git.js";
|
|
@@ -40,7 +40,10 @@ const stylizePlan = (out) => {
|
|
|
40
40
|
const fleetCanCrossVendorReview = (channels) => {
|
|
41
41
|
for (let i = 0; i < channels.length; i++)
|
|
42
42
|
for (let j = 0; j < channels.length; j++)
|
|
43
|
-
if (i !== j
|
|
43
|
+
if (i !== j
|
|
44
|
+
&& channels[i].vendor !== channels[j].vendor
|
|
45
|
+
&& modelProvider(channels[i].model, channels[i].vendor) !== modelProvider(channels[j].model, channels[j].vendor)
|
|
46
|
+
&& modelId(channels[i].model) !== modelId(channels[j].model))
|
|
44
47
|
return true;
|
|
45
48
|
return false;
|
|
46
49
|
};
|
|
@@ -121,6 +124,43 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
121
124
|
throw new Error(`--mode must be one of ${ROUTING_MODES.join(" | ")} (got ${values.mode})`);
|
|
122
125
|
}
|
|
123
126
|
const g = loadGraph(cwd);
|
|
127
|
+
const gHash = graphDefinitionHash(g);
|
|
128
|
+
const runsRoot = join(cwd, stateDirName(cwd), "runs");
|
|
129
|
+
let matchingEvents;
|
|
130
|
+
let matchingRunId;
|
|
131
|
+
if (existsSync(runsRoot)) {
|
|
132
|
+
const locked = runLockRunId(cwd);
|
|
133
|
+
const candidates = readdirSync(runsRoot)
|
|
134
|
+
.filter((d) => d.startsWith("run-") && existsSync(join(runsRoot, d, "journal.jsonl")))
|
|
135
|
+
.sort();
|
|
136
|
+
const ordered = locked && candidates.includes(locked)
|
|
137
|
+
? [locked, ...candidates.filter((id) => id !== locked).reverse()]
|
|
138
|
+
: candidates.reverse();
|
|
139
|
+
for (const runId of ordered) {
|
|
140
|
+
try {
|
|
141
|
+
const events = Journal.open(cwd, runId).read();
|
|
142
|
+
if (recordedGraphDefinitionHash(events) === gHash) {
|
|
143
|
+
matchingEvents = events;
|
|
144
|
+
matchingRunId = runId;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// ignore unreadable or torn journals
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const hasRun = matchingEvents !== undefined;
|
|
154
|
+
const reviewHistory = matchingEvents
|
|
155
|
+
? matchingEvents
|
|
156
|
+
.filter((event) => event.event === "gate-result" && event.data.gate === "review" && typeof event.data.reviewer === "string")
|
|
157
|
+
.map((event) => event.data.reviewer)
|
|
158
|
+
: [];
|
|
159
|
+
// OBS-947 repair: a resumed run re-dispatches only tasks whose replayed status is pending (or that the
|
|
160
|
+
// journal never mentions). done, failed and human are all skipped alike, so rendering their preview
|
|
161
|
+
// picks must not advance the shared reviewHistory — a phantom draw wraps the LRU and makes a later
|
|
162
|
+
// pending task print the ranked-first seat while the journal's actual next draw is a later channel.
|
|
163
|
+
const taskStatuses = matchingRunId ? Journal.open(cwd, matchingRunId).replayStatuses() : new Map();
|
|
124
164
|
const oracleRefusals = new Map();
|
|
125
165
|
const oracleAdvisories = new Map();
|
|
126
166
|
if (g.tasks.some((task) => task.acceptance.some((item) => typeof item === "object" && item.oracle === "test"))) {
|
|
@@ -306,10 +346,22 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
306
346
|
const policy = effectiveReviewPolicy(task.files, cfg.review);
|
|
307
347
|
if (policy === "judge-only")
|
|
308
348
|
return { line: "none — reviewPolicy judge-only: declared leaf paths", cost: 0 };
|
|
309
|
-
|
|
349
|
+
let rotationSeat = 1;
|
|
350
|
+
let rotationCount = 0;
|
|
351
|
+
const reviewer = pickReviewer(author, pools.review, [], cfg.review.prefer ?? [], task.routingHints?.floor, reviewHistory, (seat, count) => { rotationSeat = seat; rotationCount = count; });
|
|
310
352
|
if (!reviewer)
|
|
311
353
|
return { line: "none — no cross-vendor reviewer available", cost: 0 };
|
|
312
|
-
|
|
354
|
+
// Advance the simulated rotation only for tasks the run would actually dispatch next — the same
|
|
355
|
+
// rule as readyTasks (src/graph/graph.ts): pending, with every dependency already done. A pending
|
|
356
|
+
// task behind a parked/failed/pending dependency cannot run, so it consumes no draw (RULING-229-14).
|
|
357
|
+
const isPending = (id) => { const s = taskStatuses.get(id); return s === undefined || s === "pending"; };
|
|
358
|
+
const ready = isPending(task.id) && task.deps.every((d) => taskStatuses.get(d) === "done");
|
|
359
|
+
if (hasRun && ready)
|
|
360
|
+
reviewHistory.push(channelKey(reviewer));
|
|
361
|
+
const rotationNote = rotationCount > 1
|
|
362
|
+
? (hasRun ? ` (seat ${rotationSeat} of ${rotationCount})` : ` (seat 1 of ${rotationCount} — rotation applies at run time)`)
|
|
363
|
+
: "";
|
|
364
|
+
return { line: `${channelKey(reviewer)}${rotationNote} — reviewPolicy full`, cost: seatCost(reviewer) };
|
|
313
365
|
};
|
|
314
366
|
const routed = [];
|
|
315
367
|
for (const t of g.tasks) {
|
|
@@ -268,7 +268,9 @@ export function renderMarkdownRecord(runId, events, prices = [], rows = []) {
|
|
|
268
268
|
pass = "pass (same-provider — independence not established)";
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
|
-
|
|
271
|
+
const resolved = gate === "review" && g.data.pass === true && Array.isArray(g.data.resolved)
|
|
272
|
+
? g.data.resolved.filter((id) => typeof id === "string") : [];
|
|
273
|
+
lines.push(` - ${gate}: ${pass} — ${firstLine(g.data.details)}${resolved.length ? `; resolved: ${resolved.join(", ")}` : ""}`);
|
|
272
274
|
}
|
|
273
275
|
for (const row of leg2) {
|
|
274
276
|
const pass = row.data.pass === true ? "pass" : row.data.pass === false ? "fail" : EM;
|
|
@@ -4,6 +4,11 @@ import { type JournalEvent } from "../../run/journal.js";
|
|
|
4
4
|
* it is a phase counter, while a phase-start CARRYING a gate is the gate start the rail draws. */
|
|
5
5
|
export declare const TTY_NOISE_EVENTS: readonly ["worker-contact", "worker-status"];
|
|
6
6
|
type RailTone = "pass" | "fail" | "attention" | "active" | "neutral";
|
|
7
|
+
/** Approval-close lifecycle labels extend the established closed rail vocabulary. */
|
|
8
|
+
export declare const APPROVAL_RAIL_ROWS: Record<string, {
|
|
9
|
+
label: string;
|
|
10
|
+
tone: RailTone;
|
|
11
|
+
}>;
|
|
7
12
|
/** Closed retained set: the short operator label and the row's default tone. Labels are the rail's
|
|
8
13
|
* own vocabulary — a raw journal event name is what this surface exists to stop printing. A `pass`
|
|
9
14
|
* or `ok` datum on the event overrides the default tone, so one gate row can read either way.
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -33,6 +33,12 @@ const RAIL_TONES = {
|
|
|
33
33
|
active: { glyph: GLYPHS.pointer, paint: LIVE.running },
|
|
34
34
|
neutral: { glyph: GLYPHS.neutral, paint: LIVE.chrome },
|
|
35
35
|
};
|
|
36
|
+
/** Approval-close lifecycle labels extend the established closed rail vocabulary. */
|
|
37
|
+
export const APPROVAL_RAIL_ROWS = {
|
|
38
|
+
"approval-window-start": { label: "approval window", tone: "attention" },
|
|
39
|
+
"approval-window-expired": { label: "approval window expired", tone: "attention" },
|
|
40
|
+
"tip-verify-cancelled": { label: "tip verify cancelled", tone: "attention" },
|
|
41
|
+
};
|
|
36
42
|
/** Closed retained set: the short operator label and the row's default tone. Labels are the rail's
|
|
37
43
|
* own vocabulary — a raw journal event name is what this surface exists to stop printing. A `pass`
|
|
38
44
|
* or `ok` datum on the event overrides the default tone, so one gate row can read either way.
|
|
@@ -116,8 +122,13 @@ export const RAIL_ROWS = {
|
|
|
116
122
|
// gate starts and verdicts
|
|
117
123
|
"phase-start": { label: "gate start", tone: "active" },
|
|
118
124
|
"gate-result": { label: "gate", tone: "pass" },
|
|
125
|
+
"baseline-wait": { label: "baseline wait", tone: "active" },
|
|
126
|
+
"suite-budget": { label: "suite budget", tone: "attention" },
|
|
119
127
|
"gate-reused": { label: "gate reused", tone: "neutral" },
|
|
120
128
|
"judge-retry": { label: "judge retry", tone: "attention" },
|
|
129
|
+
"review-no-verdict": { label: "review unavailable", tone: "attention" },
|
|
130
|
+
"review-pool-demotion": { label: "review seat demoted", tone: "attention" },
|
|
131
|
+
"review-infra-retry": { label: "review infra retry", tone: "attention" },
|
|
121
132
|
"review-retry": { label: "review retry", tone: "attention" },
|
|
122
133
|
// repairs and escalations
|
|
123
134
|
"repair-dispatch": { label: "repair", tone: "attention" },
|
|
@@ -304,6 +315,11 @@ const RAIL_PROJECTION = {
|
|
|
304
315
|
? `gated ${d.gatedCommit.slice(0, 12)}, tip ${d.branchTip.slice(0, 12)}`
|
|
305
316
|
: undefined,
|
|
306
317
|
"trust-auto-answer": (d) => typeof d.adapter === "string" ? `${d.adapter}${typeof d.phase === "string" ? ` ${d.phase}` : ""}` : undefined,
|
|
318
|
+
// SB-1: the census the ceiling released beside, and the budget the round ran under versus the one it
|
|
319
|
+
// would have had on an empty census — none of `{count, occupancyCap, conservativeCap}` is on the ladder
|
|
320
|
+
"suite-budget": (d) => typeof d.count === "number" && typeof d.conservativeCap === "number" && typeof d.occupancyCap === "number"
|
|
321
|
+
? `beside ${d.count}, cap ${d.conservativeCap} not ${d.occupancyCap}`
|
|
322
|
+
: undefined,
|
|
307
323
|
};
|
|
308
324
|
// The row's text: the formatter's detail first, then the salient fields that detail could not carry.
|
|
309
325
|
// Appending (never prefixing) keeps the operator's prose at the front of the row, so the clip a
|
|
@@ -330,7 +346,7 @@ const clipCells = (text, cells) => cells <= 0 ? "" : cellWidth(text) <= cells ?
|
|
|
330
346
|
export function narrationRow(event, runId, columns = process.stdout.columns ?? 80) {
|
|
331
347
|
if (TTY_NOISE_EVENTS.includes(event.event))
|
|
332
348
|
return null;
|
|
333
|
-
const row = RAIL_ROWS[event.event];
|
|
349
|
+
const row = APPROVAL_RAIL_ROWS[event.event] ?? RAIL_ROWS[event.event];
|
|
334
350
|
if (!row)
|
|
335
351
|
return null;
|
|
336
352
|
if (event.event === "phase-start" && typeof event.data.gate !== "string")
|
package/dist/compile/native.js
CHANGED
|
@@ -847,6 +847,9 @@ acceptance is required on every task (a nested list of observable outcomes).
|
|
|
847
847
|
hard value anywhere in the domain, the criterion asserts a universal that may be FALSE ABOUT THE
|
|
848
848
|
WORLD — bound it or say where it stops holding, rather than demanding a value that does not exist.
|
|
849
849
|
|
|
850
|
+
- A millisecond ceiling in a test is a BUDGET for the SLOWEST RUNNER.
|
|
851
|
+
Any ceiling under one second must carry a SLOWEST-RUNNER note and a member that OVERRUNS it.
|
|
852
|
+
|
|
850
853
|
PICK THE CRITERION FORM FROM WHO COULD BE WRONG:
|
|
851
854
|
- When the WORKER could be wrong because it can choose the value, use "test:" and pin the exact
|
|
852
855
|
literal it could otherwise choose; an example selected by its implementer proves only itself.
|
package/dist/config/config.d.ts
CHANGED
|
@@ -240,6 +240,7 @@ export declare const TickmarkrConfigSchema: z.ZodObject<{
|
|
|
240
240
|
gates: z.ZodObject<{
|
|
241
241
|
build: z.ZodOptional<z.ZodString>;
|
|
242
242
|
test: z.ZodOptional<z.ZodString>;
|
|
243
|
+
tipTest: z.ZodOptional<z.ZodString>;
|
|
243
244
|
lint: z.ZodOptional<z.ZodString>;
|
|
244
245
|
diffCap: z.ZodOptional<z.ZodNumber>;
|
|
245
246
|
byShape: z.ZodOptional<z.ZodOptional<z.ZodRecord<z.ZodEnum<{
|
|
@@ -339,6 +340,11 @@ export type InitConfigOverlay = {
|
|
|
339
340
|
llm?: TickmarkrConfig["visibility"]["llm"];
|
|
340
341
|
};
|
|
341
342
|
};
|
|
343
|
+
/**
|
|
344
|
+
* Default config overlay template.
|
|
345
|
+
* Seam: gates.test defines the per-task gate and baseline test command;
|
|
346
|
+
* gates.tipTest optionally defines the integration tip verify test command (defaults to gates.test).
|
|
347
|
+
*/
|
|
342
348
|
export declare function configTemplate(overlay?: InitConfigOverlay): string;
|
|
343
349
|
export { FLEET_OVERLAY_KEYS, fleetEditableEquals, fleetRepoOverlayFromDelta, renderFleetOverlayWrite, repoOverlayYaml, serializeFleetOverlay, unifiedYamlDiff, } from "./fleet-overlay.js";
|
|
344
350
|
export type { FleetOverlayWrite } from "./fleet-overlay.js";
|
package/dist/config/config.js
CHANGED
|
@@ -337,6 +337,7 @@ export const TickmarkrConfigSchema = z.object({
|
|
|
337
337
|
gates: z.object({
|
|
338
338
|
build: z.string(),
|
|
339
339
|
test: z.string(),
|
|
340
|
+
tipTest: z.string(),
|
|
340
341
|
lint: z.string(),
|
|
341
342
|
diffCap: z.number().int().positive(),
|
|
342
343
|
byShape: z.partialRecord(z.enum(SHAPES), ShapeGateParticipationSchema).optional(),
|
|
@@ -756,6 +757,11 @@ export function overlayBytesLoadError(repoRoot, bytes, opts = {}) {
|
|
|
756
757
|
return e.message;
|
|
757
758
|
}
|
|
758
759
|
}
|
|
760
|
+
/**
|
|
761
|
+
* Default config overlay template.
|
|
762
|
+
* Seam: gates.test defines the per-task gate and baseline test command;
|
|
763
|
+
* gates.tipTest optionally defines the integration tip verify test command (defaults to gates.test).
|
|
764
|
+
*/
|
|
759
765
|
export function configTemplate(overlay) {
|
|
760
766
|
const base = `# tickmarkr config overlay — merges over built-in defaults (repo beats global beats defaults)
|
|
761
767
|
# concurrency: 3
|
|
@@ -3,9 +3,8 @@ export declare const MAX_BUF: number;
|
|
|
3
3
|
export declare const HERDR_CONTROL_VARS: readonly ["HERDR_ENV", "HERDR_SOCKET_PATH", "ORCA_TERMINAL_HANDLE", "ORCA_PANE_KEY", "ORCA_TAB_ID"];
|
|
4
4
|
/**
|
|
5
5
|
* Copy of worker env with the fork cap applied and host control-plane vars stripped.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* than a flat constant. The operator's own export still wins.
|
|
6
|
+
* Workers retain the run's concurrency-derived cap (resolvedForkCap), independently of
|
|
7
|
+
* verification's suite-window budget. The operator's own export still wins.
|
|
9
8
|
*/
|
|
10
9
|
export declare function sealHerdrEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
11
10
|
/** Pane/login-shell form of the same host-neutral worker env seal (herdr seed + daemon setup). */
|
|
@@ -22,9 +22,8 @@ export const HERDR_CONTROL_VARS = [
|
|
|
22
22
|
];
|
|
23
23
|
/**
|
|
24
24
|
* Copy of worker env with the fork cap applied and host control-plane vars stripped.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* than a flat constant. The operator's own export still wins.
|
|
25
|
+
* Workers retain the run's concurrency-derived cap (resolvedForkCap), independently of
|
|
26
|
+
* verification's suite-window budget. The operator's own export still wins.
|
|
28
27
|
*/
|
|
29
28
|
export function sealHerdrEnv(env = process.env) {
|
|
30
29
|
const out = { ...env };
|
package/dist/gates/baseline.d.ts
CHANGED
|
@@ -31,6 +31,8 @@ export interface BaselineCommand {
|
|
|
31
31
|
durationMs?: number;
|
|
32
32
|
/** Sum of the per-file durations named by the runner; null when its output names none. */
|
|
33
33
|
fileDurationSumMs?: number | null;
|
|
34
|
+
/** Total files reported by the runner summary; null when unavailable. */
|
|
35
|
+
fileCount?: number | null;
|
|
34
36
|
/** fileDurationSumMs / durationMs — average implied file concurrency, not a configured fork count. */
|
|
35
37
|
impliedParallelism?: number | null;
|
|
36
38
|
/** The slowest per-file entry named by the runner; null when per-file timing is unavailable. */
|
|
@@ -158,6 +160,7 @@ export declare function detectVacuousOracles(cwd: string, tasks: ReadonlyArray<{
|
|
|
158
160
|
}>): Promise<VacuousOracleWarning[]>;
|
|
159
161
|
export declare function compareToBaseline(cwd: string, commands: Record<string, string>, baseline: Baseline, enabled: string[], opts?: {
|
|
160
162
|
rerunOf?: HostStarvedRerun;
|
|
163
|
+
infraRerun?: HostStarvedRerun;
|
|
161
164
|
}): Promise<GateResult[]>;
|
|
162
165
|
export interface HostStarvedRerun {
|
|
163
166
|
durationMs: number;
|
|
@@ -178,4 +181,10 @@ interface CalmWindow {
|
|
|
178
181
|
}
|
|
179
182
|
export declare function setCalmWindowForTests(over: Partial<CalmWindow>): void;
|
|
180
183
|
export declare function resetCalmWindowForTests(): void;
|
|
184
|
+
export declare function waitForCalmWindow(): Promise<number>;
|
|
185
|
+
/** Summary totals survive digit-normalized fingerprints and exclude test-owned echoed output. */
|
|
186
|
+
export declare function runnerFileCount(raw: string): number | null;
|
|
187
|
+
export declare function fileCountDeficit(entry: BaselineCommand | undefined, raw: string): string | undefined;
|
|
188
|
+
/** Both readers classify fresh evidence first, retaining the whole-output guard against infra forgiveness. */
|
|
189
|
+
export declare function classifyFreshRunnerOutput(entry: BaselineCommand | undefined, raw: string, code: number): FailureClassification | undefined;
|
|
181
190
|
export {};
|