tickmarkr 2.2.1 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -9
- package/dist/adapters/catalog-remote.d.ts +1 -4
- package/dist/adapters/catalog-remote.js +52 -42
- package/dist/adapters/catalog.js +5 -3
- package/dist/adapters/claude-code.d.ts +1 -1
- package/dist/adapters/claude-code.js +8 -5
- package/dist/adapters/model-lints.d.ts +9 -5
- package/dist/adapters/model-lints.js +56 -15
- package/dist/adapters/model-windows.js +11 -0
- package/dist/adapters/prompt.js +1 -0
- package/dist/adapters/qwen.d.ts +5 -0
- package/dist/adapters/qwen.js +153 -0
- package/dist/adapters/types.d.ts +21 -1
- package/dist/adapters/types.js +43 -2
- package/dist/cli/commands/approve.js +5 -4
- package/dist/cli/commands/beat.js +7 -4
- package/dist/cli/commands/compile.js +32 -6
- package/dist/cli/commands/doctor.d.ts +9 -4
- package/dist/cli/commands/doctor.js +87 -13
- package/dist/cli/commands/fleet.d.ts +4 -0
- package/dist/cli/commands/fleet.js +53 -14
- package/dist/cli/commands/init.js +36 -21
- package/dist/cli/commands/plan.js +45 -7
- package/dist/cli/commands/report.js +37 -1
- package/dist/cli/commands/status.d.ts +1 -0
- package/dist/cli/commands/status.js +45 -1
- package/dist/cli/commands/verify.d.ts +6 -0
- package/dist/cli/commands/verify.js +145 -25
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/compile/collateral.d.ts +2 -9
- package/dist/compile/collateral.js +17 -18
- package/dist/compile/index.d.ts +4 -1
- package/dist/compile/index.js +41 -7
- package/dist/compile/native.d.ts +4 -2
- package/dist/compile/native.js +58 -9
- package/dist/compile/ownership.js +34 -9
- package/dist/config/config.d.ts +1 -0
- package/dist/config/config.js +52 -6
- package/dist/drivers/herdr.d.ts +1 -0
- package/dist/drivers/herdr.js +11 -1
- package/dist/drivers/index.d.ts +6 -0
- package/dist/drivers/index.js +19 -4
- package/dist/drivers/orca.d.ts +35 -1
- package/dist/drivers/orca.js +260 -20
- package/dist/drivers/subprocess.d.ts +3 -3
- package/dist/drivers/subprocess.js +16 -9
- package/dist/drivers/types.d.ts +12 -0
- package/dist/gates/baseline.d.ts +2 -0
- package/dist/gates/baseline.js +47 -11
- package/dist/gates/llm.d.ts +6 -0
- package/dist/gates/llm.js +25 -9
- package/dist/gates/review.d.ts +7 -3
- package/dist/gates/review.js +61 -22
- package/dist/gates/run-gates.d.ts +5 -2
- package/dist/gates/run-gates.js +50 -26
- package/dist/gates/verdict-cause.d.ts +6 -2
- package/dist/gates/verdict-cause.js +8 -4
- package/dist/route/preference.d.ts +4 -0
- package/dist/route/preference.js +40 -0
- package/dist/route/router.js +15 -2
- package/dist/run/consult.d.ts +1 -0
- package/dist/run/consult.js +39 -8
- package/dist/run/daemon.d.ts +16 -0
- package/dist/run/daemon.js +345 -74
- package/dist/run/git.d.ts +3 -0
- package/dist/run/git.js +40 -5
- package/dist/run/journal.d.ts +15 -2
- package/dist/run/journal.js +73 -12
- package/dist/run/supervision.d.ts +6 -0
- package/dist/run/supervision.js +29 -1
- package/dist/tui/ink/fleet-app.d.ts +4 -0
- package/dist/tui/ink/fleet-app.js +45 -16
- package/dist/tui/ink/init-app.js +4 -4
- package/package.json +59 -1
- package/skills/tickmarkr-overseer/SKILL.md +77 -18
- package/skills/tickmarkr-overseer/scripts/seat-send.sh +88 -18
- package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +36 -2
- package/skills/tickmarkr-overseer/scripts/watch-contamination.sh +36 -15
- package/skills/tickmarkr-overseer/scripts/watch-context.sh +33 -7
- package/skills/tickmarkr-overseer/scripts/watch-pending-input.sh +32 -8
package/dist/run/git.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { ROUTING_ENV_SEAMS } from "../route/router.js";
|
|
3
3
|
export { ROUTING_ENV_SEAMS };
|
|
4
4
|
export declare const FORK_CAP_ENV = "VITEST_MAX_FORKS";
|
|
5
|
+
export declare const SUITE_PARENT_ENV = "TICKMARKR_SUITE_PARENT";
|
|
5
6
|
export declare const DEFAULT_FORK_CAP = "6";
|
|
6
7
|
/**
|
|
7
8
|
* OBS-618: how many PROCESSES one vitest fork can hold at its peak — the fork, a daemon it spawns,
|
|
@@ -89,6 +90,8 @@ export interface ShResult {
|
|
|
89
90
|
stdout: string;
|
|
90
91
|
stderr: string;
|
|
91
92
|
timedOut?: boolean;
|
|
93
|
+
/** The shell exited, but descendants still held its process group and pipes past the grace. */
|
|
94
|
+
reapedGroup?: boolean;
|
|
92
95
|
durationMs?: number;
|
|
93
96
|
/** T7: the capacity THIS child ran under, stamped where its environment was built (see `shell`). */
|
|
94
97
|
capacity?: RunCapacity;
|
package/dist/run/git.js
CHANGED
|
@@ -13,6 +13,7 @@ export { ROUTING_ENV_SEAMS };
|
|
|
13
13
|
// Default to a modest cap through the environment so vitest honors it natively; never pass it
|
|
14
14
|
// as argv (OBS-55) so child test oracles stay intact. The operator's own export wins.
|
|
15
15
|
export const FORK_CAP_ENV = "VITEST_MAX_FORKS";
|
|
16
|
+
export const SUITE_PARENT_ENV = "TICKMARKR_SUITE_PARENT";
|
|
16
17
|
export const DEFAULT_FORK_CAP = "6";
|
|
17
18
|
/**
|
|
18
19
|
* The fork budget belongs to ONE run: how many gate suites can be in flight at once is exactly that
|
|
@@ -137,6 +138,9 @@ export const resetSpawnForTests = () => { spawnChild = undefined; };
|
|
|
137
138
|
const RETRYABLE_SPAWN_CODE = "EAGAIN";
|
|
138
139
|
export const SPAWN_ATTEMPT_LIMIT = 4;
|
|
139
140
|
export const SPAWN_RETRY_BACKOFF_MS = 50;
|
|
141
|
+
// RULING-P99-28: SHIP — every gate user can hit the inherited-pipe hang at this product seam.
|
|
142
|
+
/** Give a normally-exited shell's descendants this long to close their inherited pipes themselves. */
|
|
143
|
+
const SHELL_REAP_GRACE_MS = 2000;
|
|
140
144
|
// stdin "ignore": same class as HARD-05 / SubprocessDriver — never leave an open pipe a child can block on
|
|
141
145
|
// (pi -p / codex exec wait for stdin EOF). timedOut distinguishes SIGKILL-timeout from a real nonzero exit.
|
|
142
146
|
function shell(cmd, cwd, timeoutMs, login) {
|
|
@@ -150,6 +154,9 @@ function shell(cmd, cwd, timeoutMs, login) {
|
|
|
150
154
|
// OBS-110: apply the run's own fork cap only when the operator has not already set one.
|
|
151
155
|
if (!(FORK_CAP_ENV in env))
|
|
152
156
|
env[FORK_CAP_ENV] = resolvedForkCap();
|
|
157
|
+
// OBS-854: descendants can leave the checkout (nested fixture suites do), so cwd alone cannot
|
|
158
|
+
// attribute them. Every daemon shell exports the daemon pid as their durable parentage marker.
|
|
159
|
+
env[SUITE_PARENT_ENV] = String(process.pid);
|
|
153
160
|
// T7: the capacity every result of this shell carries, read HERE — off the environment the child
|
|
154
161
|
// is about to receive, after the precedence above has settled. An operator export is already in
|
|
155
162
|
// `env`, so what gets recorded is the operator's number, which is the case a release was re-taken
|
|
@@ -166,15 +173,25 @@ function shell(cmd, cwd, timeoutMs, login) {
|
|
|
166
173
|
let stdout = "", stderr = "";
|
|
167
174
|
const stdoutDecoder = new StringDecoder("utf8");
|
|
168
175
|
const stderrDecoder = new StringDecoder("utf8");
|
|
169
|
-
let timedOut = false, done = false, started = false, outputSeen = false;
|
|
176
|
+
let timedOut = false, reapedGroup = false, done = false, started = false, outputSeen = false;
|
|
177
|
+
let reapTimer;
|
|
170
178
|
const finish = (code, err) => {
|
|
171
179
|
if (done)
|
|
172
180
|
return;
|
|
173
181
|
done = true;
|
|
174
182
|
clearTimeout(timer);
|
|
183
|
+
clearTimeout(reapTimer);
|
|
175
184
|
stdout += stdoutDecoder.end();
|
|
176
185
|
stderr += stderrDecoder.end();
|
|
177
|
-
resolve({
|
|
186
|
+
resolve({
|
|
187
|
+
code,
|
|
188
|
+
stdout,
|
|
189
|
+
stderr: err ?? stderr,
|
|
190
|
+
timedOut,
|
|
191
|
+
durationMs: Date.now() - startedAt,
|
|
192
|
+
capacity,
|
|
193
|
+
...(reapedGroup ? { reapedGroup: true } : {}),
|
|
194
|
+
});
|
|
178
195
|
};
|
|
179
196
|
const timer = setTimeout(() => {
|
|
180
197
|
timedOut = true;
|
|
@@ -211,9 +228,27 @@ function shell(cmd, cwd, timeoutMs, login) {
|
|
|
211
228
|
finish(127, String(e));
|
|
212
229
|
});
|
|
213
230
|
p.on("close", (code) => finish(code ?? 1));
|
|
214
|
-
// "close" waits for stdio to drain
|
|
215
|
-
|
|
216
|
-
|
|
231
|
+
// "close" waits for stdio to drain. Once bash exits normally, give descendants a bounded grace
|
|
232
|
+
// to exit with it; a survivor still in bash's detached group is then reaped so its inherited pipe
|
|
233
|
+
// cannot hold this promise until the command ceiling. A real timeout wins first and is never
|
|
234
|
+
// reclassified as a grace reap.
|
|
235
|
+
p.on("exit", (code) => {
|
|
236
|
+
if (timedOut) {
|
|
237
|
+
finish(code ?? 1);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
reapTimer = setTimeout(() => {
|
|
241
|
+
if (done)
|
|
242
|
+
return;
|
|
243
|
+
try {
|
|
244
|
+
process.kill(-p.pid, "SIGKILL");
|
|
245
|
+
reapedGroup = true;
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
// The group closed between the shell's exit and the grace boundary; "close" owns finish.
|
|
249
|
+
}
|
|
250
|
+
}, SHELL_REAP_GRACE_MS);
|
|
251
|
+
});
|
|
217
252
|
});
|
|
218
253
|
return (async () => {
|
|
219
254
|
const startedAt = Date.now();
|
package/dist/run/journal.d.ts
CHANGED
|
@@ -116,8 +116,21 @@ export declare function formatPriorFindingEvidence(evidence: PriorFindingEvidenc
|
|
|
116
116
|
export declare function normalizeGateFailure(details: string): string;
|
|
117
117
|
export declare const GATE_FINGERPRINT_CAP = 2;
|
|
118
118
|
export declare function identicalGateFailures(events: JournalEvent[], taskId: string, gate: string, normalized: string): number;
|
|
119
|
-
/**
|
|
119
|
+
/** What each funded repair's next battery actually reached. A repair spends its budget only
|
|
120
|
+
* when that battery reaches one of the gates it was funded to fix; an earlier red is evidence that
|
|
121
|
+
* this repair never got its funded turn, not a charge against the repair ladder. */
|
|
122
|
+
export interface RepairReach {
|
|
123
|
+
repair: number;
|
|
124
|
+
funded: string[];
|
|
125
|
+
reached: string[];
|
|
126
|
+
diedAt?: string;
|
|
127
|
+
charged: boolean;
|
|
128
|
+
}
|
|
129
|
+
export declare function repairReachSinceApproval(events: JournalEvent[], taskId: string): RepairReach[];
|
|
130
|
+
/** Repair slots actually spent this engagement — journal-derived, so a resume inherits it. */
|
|
120
131
|
export declare function repairsSinceApproval(events: JournalEvent[], taskId: string): number;
|
|
132
|
+
/** Recheck releases not yet enacted by a battery (or by a legacy worker launch). */
|
|
133
|
+
export declare function pendingRechecks(events: JournalEvent[]): Set<string>;
|
|
121
134
|
/**
|
|
122
135
|
* Why the last attempt failed, one row per journaled cause, in the daemon's own `source: details`
|
|
123
136
|
* shape. The daemon builds that brief in a loop-local variable, which dies with the process: a resumed
|
|
@@ -242,9 +255,9 @@ export declare const TelemetryRowSchema: z.ZodObject<{
|
|
|
242
255
|
quotaFailover: z.ZodOptional<z.ZodLiteral<true>>;
|
|
243
256
|
overrun: z.ZodOptional<z.ZodLiteral<true>>;
|
|
244
257
|
retryMode: z.ZodOptional<z.ZodEnum<{
|
|
258
|
+
repair: "repair";
|
|
245
259
|
resume: "resume";
|
|
246
260
|
fresh: "fresh";
|
|
247
|
-
repair: "repair";
|
|
248
261
|
}>>;
|
|
249
262
|
signalQuality: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<0.25>, z.ZodLiteral<0.5>, z.ZodLiteral<0.75>, z.ZodLiteral<1>]>>;
|
|
250
263
|
signalBasis: z.ZodOptional<z.ZodEnum<{
|
package/dist/run/journal.js
CHANGED
|
@@ -5,6 +5,7 @@ import { z } from "zod";
|
|
|
5
5
|
import { channelKey, shq, TokenUsageSchema } from "../adapters/types.js";
|
|
6
6
|
import { stateDirName, taskContentDigest, tickmarkrDir } from "../graph/graph.js";
|
|
7
7
|
import { GATE_NAMES, TIERS } from "../graph/schema.js";
|
|
8
|
+
import { channelRouteIdentity } from "../route/preference.js";
|
|
8
9
|
import { buildProfile, classify } from "../route/profile.js";
|
|
9
10
|
import { DecisionEventSchema, trackJournalRows, } from "./protocol.js";
|
|
10
11
|
import { normalizeGateOutcome } from "./outcome.js";
|
|
@@ -72,8 +73,8 @@ export const REVIEW_UPHELD_RELEASE = "review-upheld";
|
|
|
72
73
|
// OBS-203: the third decision a gate-fail park needs. Plain approve WAIVES the failed gate (and every
|
|
73
74
|
// gate before it — daemon.ts remainingGates), which is wrong when the gate failed against a stale task
|
|
74
75
|
// DECLARATION rather than a bad diff: amend the spec's files[], recompile, and the gate now passes
|
|
75
|
-
// honestly. This release
|
|
76
|
-
//
|
|
76
|
+
// honestly. This release runs the whole gate suite over the parked commit before any worker and
|
|
77
|
+
// marks no gate satisfied: green merges directly, red returns to the ladder. Budget semantics match attempt-cap
|
|
77
78
|
// (fresh attempts, tried survives) because the park cost the task its remaining budget.
|
|
78
79
|
export const RECHECK_RELEASE = "recheck";
|
|
79
80
|
// OBS-738: one authority for every recovery surface. The ref is accepted only from the row that
|
|
@@ -547,18 +548,65 @@ export function identicalGateFailures(events, taskId, gate, normalized) {
|
|
|
547
548
|
}
|
|
548
549
|
return n;
|
|
549
550
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
let n = 0;
|
|
551
|
+
export function repairReachSinceApproval(events, taskId) {
|
|
552
|
+
const repairs = [];
|
|
553
553
|
for (const e of events) {
|
|
554
554
|
if (e.taskId !== taskId)
|
|
555
555
|
continue;
|
|
556
|
-
if (e.event === "task-approved")
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
556
|
+
if (e.event === "task-approved") {
|
|
557
|
+
repairs.length = 0;
|
|
558
|
+
}
|
|
559
|
+
else if (e.event === "repair-attempt") {
|
|
560
|
+
const funded = Array.isArray(e.data.gates)
|
|
561
|
+
? e.data.gates.filter((gate) => typeof gate === "string")
|
|
562
|
+
: [];
|
|
563
|
+
repairs.push({
|
|
564
|
+
repair: typeof e.data.repair === "number" ? e.data.repair : repairs.length + 1,
|
|
565
|
+
funded,
|
|
566
|
+
reached: [],
|
|
567
|
+
charged: false,
|
|
568
|
+
launched: false,
|
|
569
|
+
closed: false,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
const repair = repairs.at(-1);
|
|
574
|
+
if (!repair || repair.closed)
|
|
575
|
+
continue;
|
|
576
|
+
if (e.event === "worker-launch") {
|
|
577
|
+
if (repair.launched)
|
|
578
|
+
repair.closed = true;
|
|
579
|
+
else
|
|
580
|
+
repair.launched = true;
|
|
581
|
+
}
|
|
582
|
+
else if (repair.launched && e.event === "gate-result" && typeof e.data.gate === "string") {
|
|
583
|
+
if (!repair.reached.includes(e.data.gate))
|
|
584
|
+
repair.reached.push(e.data.gate);
|
|
585
|
+
if (e.data.pass === false && repair.diedAt === undefined)
|
|
586
|
+
repair.diedAt = e.data.gate;
|
|
587
|
+
if (repair.funded.includes(e.data.gate))
|
|
588
|
+
repair.charged = true;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
560
591
|
}
|
|
561
|
-
return
|
|
592
|
+
return repairs.map(({ launched: _launched, closed: _closed, ...repair }) => repair);
|
|
593
|
+
}
|
|
594
|
+
/** Repair slots actually spent this engagement — journal-derived, so a resume inherits it. */
|
|
595
|
+
export function repairsSinceApproval(events, taskId) {
|
|
596
|
+
return repairReachSinceApproval(events, taskId).filter((repair) => repair.charged).length;
|
|
597
|
+
}
|
|
598
|
+
/** Recheck releases not yet enacted by a battery (or by a legacy worker launch). */
|
|
599
|
+
export function pendingRechecks(events) {
|
|
600
|
+
const pending = new Set();
|
|
601
|
+
for (const e of events) {
|
|
602
|
+
if (!e.taskId)
|
|
603
|
+
continue;
|
|
604
|
+
if (e.event === "task-approved" && e.data.release === RECHECK_RELEASE)
|
|
605
|
+
pending.add(e.taskId);
|
|
606
|
+
else if (e.event === "recheck-battery" || e.event === "worker-launch")
|
|
607
|
+
pending.delete(e.taskId);
|
|
608
|
+
}
|
|
609
|
+
return pending;
|
|
562
610
|
}
|
|
563
611
|
// Both retry decisions below govern exactly ONE dispatch: the next one. So both are read back from the
|
|
564
612
|
// journal at the moment that dispatch is built, never carried in a process variable — a stop between
|
|
@@ -695,8 +743,21 @@ export function pendingRepairFindings(events, taskId) {
|
|
|
695
743
|
* a channel it is no longer using, and a later unrelated failure is not parked under a stale reason.
|
|
696
744
|
*/
|
|
697
745
|
export function activeRetryBan(events, taskId, channel) {
|
|
698
|
-
|
|
699
|
-
|
|
746
|
+
let pending;
|
|
747
|
+
for (const e of events) {
|
|
748
|
+
if (e.taskId !== taskId)
|
|
749
|
+
continue;
|
|
750
|
+
if (e.event === "gate-fingerprint-cap")
|
|
751
|
+
pending = e;
|
|
752
|
+
else if (e.event === DECISION_SPENT
|
|
753
|
+
|| (e.event === "task-approved" && e.data.release === RECHECK_RELEASE))
|
|
754
|
+
pending = undefined;
|
|
755
|
+
}
|
|
756
|
+
return pending && typeof pending.data.channel === "string"
|
|
757
|
+
&& channelRouteIdentity(pending.data.channel) === channelRouteIdentity(channel)
|
|
758
|
+
&& typeof pending.data.gate === "string"
|
|
759
|
+
? pending.data.gate
|
|
760
|
+
: undefined;
|
|
700
761
|
}
|
|
701
762
|
// Fail-closed shape for a dispatched assignment (journal.ts:75-90 posture): a malformed assignment in
|
|
702
763
|
// one dispatch degrades that single task toward today's behavior — counts toward attempts, contributes
|
|
@@ -7,6 +7,12 @@ export declare const SUPERVISION_FUTURE_GRACE_MS = 1000;
|
|
|
7
7
|
export declare const SUPERVISION_DEFAULT_THRESHOLD_PCT = 75;
|
|
8
8
|
export declare const SUPERVISION_TIERS: readonly ["orchestrator", "orchestrator-context", "overseer", "overseer-context", "watch"];
|
|
9
9
|
export type SupervisionTier = (typeof SUPERVISION_TIERS)[number];
|
|
10
|
+
/**
|
|
11
|
+
* Resolve a one-shot supervision writer to the repository state it is claiming to supervise.
|
|
12
|
+
* The nearest state directory wins; reaching a git root without one is a refusal, not permission
|
|
13
|
+
* to create fresh state there or to wander into an enclosing repository. This function is read-only.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveSupervisionRoot(cwd: string): string;
|
|
10
16
|
export declare const SUPERVISION_SEAT_TIERS: readonly ["orchestrator", "orchestrator-context", "overseer", "overseer-context"];
|
|
11
17
|
export type SeatTier = (typeof SUPERVISION_SEAT_TIERS)[number];
|
|
12
18
|
/** Does this tier's record have to name the seat it speaks for? */
|
package/dist/run/supervision.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { stateDirName, tickmarkrDir } from "../graph/graph.js";
|
|
5
5
|
// SUP-01: supervision liveness as FILE STATE, not as a report — lock.ts's proven shape, one file per
|
|
6
6
|
// tier. A watcher beats its OWN file; every other seat derives that tier's state by stat()ing it. So
|
|
@@ -41,6 +41,34 @@ export const SUPERVISION_DEFAULT_THRESHOLD_PCT = 75;
|
|
|
41
41
|
export const SUPERVISION_TIERS = [
|
|
42
42
|
"orchestrator", "orchestrator-context", "overseer", "overseer-context", "watch",
|
|
43
43
|
];
|
|
44
|
+
const pathKind = (path) => {
|
|
45
|
+
try {
|
|
46
|
+
return statSync(path).isDirectory() ? "directory" : "present";
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return "absent";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Resolve a one-shot supervision writer to the repository state it is claiming to supervise.
|
|
54
|
+
* The nearest state directory wins; reaching a git root without one is a refusal, not permission
|
|
55
|
+
* to create fresh state there or to wander into an enclosing repository. This function is read-only.
|
|
56
|
+
*/
|
|
57
|
+
export function resolveSupervisionRoot(cwd) {
|
|
58
|
+
let current = resolve(cwd);
|
|
59
|
+
for (;;) {
|
|
60
|
+
if (pathKind(join(current, stateDirName(current))) === "directory")
|
|
61
|
+
return current;
|
|
62
|
+
if (pathKind(join(current, ".git")) !== "absent") {
|
|
63
|
+
throw new Error(`missing ${stateDirName(current)}/ state dir at repository root ${current}`);
|
|
64
|
+
}
|
|
65
|
+
const parent = dirname(current);
|
|
66
|
+
if (parent === current) {
|
|
67
|
+
throw new Error(`missing ${stateDirName(current)}/ state dir — no tickmarkr repository found from ${resolve(cwd)}`);
|
|
68
|
+
}
|
|
69
|
+
current = parent;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
44
72
|
// The tiers whose records must NAME the seat behind them. A one-shot `tickmarkr beat` records a pid
|
|
45
73
|
// that has already exited by the time anyone reads it, and an instant — nothing a reader can attribute
|
|
46
74
|
// to a seat. Measured 2026-08-26: a consult seat of another tier ran the documented beat loop and the
|
|
@@ -68,6 +68,10 @@ export type FleetModelGroup = {
|
|
|
68
68
|
detectedAt?: string;
|
|
69
69
|
suggestion?: FleetModelSuggestion;
|
|
70
70
|
evidence?: FleetModelEvidence;
|
|
71
|
+
classifyModel?: string;
|
|
72
|
+
variants?: string[];
|
|
73
|
+
foldedModels?: string[];
|
|
74
|
+
score?: number;
|
|
71
75
|
}>;
|
|
72
76
|
};
|
|
73
77
|
export type FleetClassification = {
|
|
@@ -96,7 +96,8 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
|
|
|
96
96
|
};
|
|
97
97
|
const groupRows = (group) => {
|
|
98
98
|
const rows = group.rows.map((row) => {
|
|
99
|
-
const staged = ui.classifications.find((classification) => classification.adapter === group.adapter
|
|
99
|
+
const staged = ui.classifications.find((classification) => classification.adapter === group.adapter
|
|
100
|
+
&& (classification.model === row.model || classification.model === row.classifyModel));
|
|
100
101
|
return {
|
|
101
102
|
adapter: group.adapter,
|
|
102
103
|
model: row.model,
|
|
@@ -105,10 +106,14 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
|
|
|
105
106
|
suggestion: row.suggestion,
|
|
106
107
|
evidence: row.evidence,
|
|
107
108
|
channel: group.channel,
|
|
109
|
+
classifyModel: row.classifyModel,
|
|
110
|
+
variants: row.variants,
|
|
111
|
+
foldedModels: row.foldedModels,
|
|
112
|
+
score: row.score,
|
|
108
113
|
denied: ui.denyModels.has(`${group.adapter}:${row.model}`),
|
|
109
114
|
};
|
|
110
115
|
});
|
|
111
|
-
const known = new Set(rows.
|
|
116
|
+
const known = new Set(rows.flatMap((row) => row.classifyModel ? [row.model, row.classifyModel] : [row.model]));
|
|
112
117
|
for (const staged of ui.classifications) {
|
|
113
118
|
if (staged.adapter === group.adapter && !known.has(staged.model)) {
|
|
114
119
|
rows.push({
|
|
@@ -125,10 +130,21 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
|
|
|
125
130
|
const matches = (label, f) => f === "" || label.toLowerCase().includes(f.toLowerCase());
|
|
126
131
|
// Operator directive 2026-08-13: retired shapes (dated snapshots, previews, non-worker SKUs,
|
|
127
132
|
// legacy families) hide by DEFAULT. `a` reveals them; a CLASSIFIED row is never hidden.
|
|
128
|
-
const modelRows = (f = ui.filter) =>
|
|
129
|
-
|
|
130
|
-
|
|
133
|
+
const modelRows = (f = ui.filter) => {
|
|
134
|
+
const rows = scopedGroups().flatMap(groupRows).filter((row) => matches(`${row.adapter}/${row.model} ${(row.foldedModels ?? []).join(" ")}`, f)
|
|
135
|
+
&& (ui.showAll || row.tier !== undefined || retiredModelReason(row.model) === null));
|
|
136
|
+
const classified = rows.filter((row) => row.tier !== undefined);
|
|
137
|
+
const unclassified = rows.filter((row) => row.tier === undefined).sort((a, b) => Number(!!b.suggestion) - Number(!!a.suggestion)
|
|
138
|
+
|| (b.detectedAt ?? "").localeCompare(a.detectedAt ?? "")
|
|
139
|
+
|| (b.score ?? Number.NEGATIVE_INFINITY) - (a.score ?? Number.NEGATIVE_INFINITY)
|
|
140
|
+
|| `${a.adapter}/${a.model}`.localeCompare(`${b.adapter}/${b.model}`));
|
|
141
|
+
return [...classified, ...unclassified];
|
|
142
|
+
};
|
|
143
|
+
const hiddenModelCount = (f = ui.filter) => scopedGroups().flatMap(groupRows)
|
|
144
|
+
.filter((row) => matches(`${row.adapter}/${row.model} ${(row.foldedModels ?? []).join(" ")}`, f)).length
|
|
131
145
|
- modelRows(f).length;
|
|
146
|
+
const foldedModelCount = (f = ui.filter) => modelRows(f)
|
|
147
|
+
.reduce((count, row) => count + Math.max((row.foldedModels?.length ?? 1) - 1, 0), 0);
|
|
132
148
|
/** rail counts mirror the list's retired-hide (never the text filter) so numbers agree on screen */
|
|
133
149
|
const visibleCount = (scope) => {
|
|
134
150
|
const groups = scope === -1 ? enabledGroups() : (modelGroups[scope] ? [modelGroups[scope]] : []);
|
|
@@ -232,14 +248,17 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
|
|
|
232
248
|
return;
|
|
233
249
|
// OBS-508: catalog evidence prefills the flow — the tier cursor lands on the suggested band and
|
|
234
250
|
// (when the operator keeps that band) the provenance note arrives pre-typed. Free to override.
|
|
235
|
-
const
|
|
251
|
+
const source = group.rows.find((row) => row.model === model);
|
|
252
|
+
const suggestion = source?.suggestion ?? null;
|
|
253
|
+
const classifyModel = source?.classifyModel ?? model;
|
|
236
254
|
const tierAt = suggestion ? Math.max(TIERS.indexOf(suggestion.tier), 0) : 0;
|
|
237
255
|
const firstTouch = !group.rows.some((row) => row.tier !== undefined)
|
|
238
256
|
&& !ui.classifications.some((c) => c.adapter === adapter);
|
|
239
257
|
const base = {
|
|
240
258
|
kind: "classify",
|
|
241
259
|
adapter,
|
|
242
|
-
model,
|
|
260
|
+
model: classifyModel,
|
|
261
|
+
...(classifyModel !== model ? { displayModel: model } : {}),
|
|
243
262
|
channelAt: 0,
|
|
244
263
|
tierAt,
|
|
245
264
|
note: "",
|
|
@@ -966,7 +985,7 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
|
|
|
966
985
|
const group = modelGroups[ui.adapterAt];
|
|
967
986
|
const suggested = rows
|
|
968
987
|
.filter((candidate) => candidate.adapter === group.adapter && !candidate.tier && candidate.suggestion !== undefined)
|
|
969
|
-
.map((candidate) => ({ model: candidate.model, suggestion: candidate.suggestion }));
|
|
988
|
+
.map((candidate) => ({ model: candidate.classifyModel ?? candidate.model, suggestion: candidate.suggestion }));
|
|
970
989
|
if (!suggested.length) {
|
|
971
990
|
ui.notice = "no catalog tier suggestions among the visible unclassified models — evidence comes from the cached catalogs (AA index / API pricing)";
|
|
972
991
|
bump();
|
|
@@ -1101,6 +1120,9 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
|
|
|
1101
1120
|
const parts = [
|
|
1102
1121
|
`${row.adapter}:${row.model}`,
|
|
1103
1122
|
row.tier ?? "unclassified",
|
|
1123
|
+
row.variants?.length ? `variants ${row.variants.join(", ")}` : "",
|
|
1124
|
+
row.classifyModel ? `classify writes ${row.classifyModel}` : "",
|
|
1125
|
+
row.foldedModels?.length ? `${row.foldedModels.length} gateway ids folded: ${row.foldedModels.join(", ")}` : "",
|
|
1104
1126
|
row.evidence?.unauthed !== undefined ? `UNAUTHED — ${row.evidence.unauthed}` : "",
|
|
1105
1127
|
row.evidence?.contextWindow !== undefined ? `${fmtCtx(row.evidence.contextWindow)} ctx` : "",
|
|
1106
1128
|
row.evidence?.outputWindow !== undefined ? `${fmtCtx(row.evidence.outputWindow)} out` : "",
|
|
@@ -1120,7 +1142,8 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
|
|
|
1120
1142
|
const nameW = bodyW - 4 - 11 - 6 - (showPrice ? 12 : 0) - (showProbe ? 7 : 0);
|
|
1121
1143
|
// OBS-531: deep router ids (omp/prime-agent) clip tail-preserving — the LAST segment is the
|
|
1122
1144
|
// distinguishing half; end-clipping rendered ten identical "prime-agent/anthropic/claude-…" rows.
|
|
1123
|
-
const
|
|
1145
|
+
const folded = row.foldedModels?.length ?? 1;
|
|
1146
|
+
const name = clipPathTail(`${row.adapter}/${row.model}${folded > 1 ? ` ×${folded}` : ""}`, nameW);
|
|
1124
1147
|
const prefixLen = Math.min(name.length, row.adapter.length + 1);
|
|
1125
1148
|
return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Pointer, { on: selected }), row.denied
|
|
1126
1149
|
? _jsx(Glyph, { kind: "off" })
|
|
@@ -1174,7 +1197,9 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
|
|
|
1174
1197
|
if (overlay.kind === "classify") {
|
|
1175
1198
|
const subject = overlay.bulk
|
|
1176
1199
|
? `${overlay.adapter} · ${overlay.bulk.rows.length} suggested models`
|
|
1177
|
-
:
|
|
1200
|
+
: overlay.displayModel
|
|
1201
|
+
? `${overlay.adapter}:${overlay.displayModel} · writes ${overlay.model}`
|
|
1202
|
+
: `${overlay.adapter}:${overlay.model}`;
|
|
1178
1203
|
return (_jsxs(OverlayPanel, { title: `classify · ${subject}`, width: bodyW, children: [overlay.stage === "channel" && (_jsxs(_Fragment, { children: [_jsxs(Text, { dimColor: true, children: ["first touch for ", overlay.adapter, " \u2014 how is this CLI billed?"] }), _jsx(Text, { children: " " }), CHANNELS.map((channel, index) => (_jsxs(Text, { children: [_jsx(Pointer, { on: overlay.channelAt === index }), _jsx(Text, { bold: overlay.channelAt === index, children: padCell(channel, 5) }), _jsx(Text, { dimColor: true, children: channel === "sub" ? "flat-rate subscription quota" : "metered API billing" })] }, channel)))] })), overlay.stage === "tier" && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: overlay.suggestion
|
|
1179
1204
|
? `catalog suggests ${overlay.suggestion.tier} — keep it and the provenance note arrives pre-typed`
|
|
1180
1205
|
: "pick the capability band this model routes as" }), _jsx(Text, { children: " " }), TIERS.map((tier, index) => (_jsxs(Text, { children: [_jsx(Pointer, { on: overlay.tierAt === index }), overlay.suggestion?.tier === tier ? _jsx(Glyph, { kind: "on" }) : _jsx(Text, { children: " " }), _jsx(Text, { bold: overlay.tierAt === index, children: ` ${padCell(tier, 9)}` }), _jsx(Text, { dimColor: true, children: tier === "frontier" ? "strongest band — integrity shapes" : tier === "mid" ? "capable daily driver" : "fast + cheap" })] }, tier)))] })), overlay.stage === "note" && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "benchmark provenance (required) \u2014 where does this tier claim come from?" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: INK.brand, children: "> " }), _jsx(Text, { children: clip(overlay.note, bodyW - 6) || "" }), _jsx(Text, { color: INK.brand, children: "\u2588" })] })] }))] }));
|
|
@@ -1220,6 +1245,15 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
|
|
|
1220
1245
|
const selectedSeat = ui.judgeSeat ?? judgeKeepRow;
|
|
1221
1246
|
return (_jsxs(OverlayPanel, { title: "pick \u00B7 judge", width: bodyW, children: [_jsx(Text, { dimColor: true, children: "one seat judges acceptance criteria \u2014 failover stays runtime (GATE-09)" }), _jsx(SearchRow, { filter: ui.filter, active: true }), above > 0 && _jsx(ElisionMark, { count: above, side: "above" }), visible.map((label, index) => (_jsxs(Text, { children: [_jsx(Pointer, { on: start + index === overlay.at }), label === selectedSeat ? _jsx(Glyph, { kind: "on" }) : _jsx(Text, { children: " " }), _jsx(Text, { bold: start + index === overlay.at, children: ` ${clip(label, bodyW - 10)}` })] }, label))), below > 0 && _jsx(ElisionMark, { count: below, side: "below" })] }));
|
|
1222
1247
|
})();
|
|
1248
|
+
const modelVisibilityLine = () => {
|
|
1249
|
+
const hidden = hiddenModelCount();
|
|
1250
|
+
const folded = foldedModelCount();
|
|
1251
|
+
return [
|
|
1252
|
+
...(folded ? [`${folded} same-model gateway id${folded === 1 ? "" : "s"} folded`] : []),
|
|
1253
|
+
...(hidden ? [`${hidden} retired/preview/non-worker hidden — a shows all`] : []),
|
|
1254
|
+
...(ui.showAll ? ["showing retired models — a hides them again"] : []),
|
|
1255
|
+
].join(" · ");
|
|
1256
|
+
};
|
|
1223
1257
|
const listNode = (() => {
|
|
1224
1258
|
if (overlayNode)
|
|
1225
1259
|
return overlayNode;
|
|
@@ -1230,7 +1264,7 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
|
|
|
1230
1264
|
? "All models"
|
|
1231
1265
|
: modelGroups[ui.adapterAt]?.adapter ?? "";
|
|
1232
1266
|
const scopeDenied = ui.adapterAt !== -1 && ui.deny.has(modelGroups[ui.adapterAt]?.adapter ?? "");
|
|
1233
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: scopeLabel }), _jsx(Text, { dimColor: true, children: ` ${rows.length}` })] }), _jsx(SearchRow, { filter: ui.filter, active: ui.searching, hint: "/ to search" }), scopeDenied && _jsx(Text, { color: INK.warn, children: `${modelGroups[ui.adapterAt]?.adapter} is out of the fleet — Space on its rail row adds it back` }), above > 0 && _jsx(ElisionMark, { count: above, side: "above" }), visible.map((row, index) => renderModelRow(row, ui.focus === "list" && start + index === ui.listAt)), below > 0 && _jsx(ElisionMark, { count: below, side: "below", hint: "/ to search" }), rows.length === 0 && !scopeDenied && _jsx(Text, { dimColor: true, children: " no models match" })] }));
|
|
1267
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: scopeLabel }), _jsx(Text, { dimColor: true, children: ` ${rows.length}` })] }), _jsx(SearchRow, { filter: ui.filter, active: ui.searching, hint: "/ to search" }), modelVisibilityLine() && _jsx(Text, { dimColor: true, children: ` ${modelVisibilityLine()}` }), scopeDenied && _jsx(Text, { color: INK.warn, children: `${modelGroups[ui.adapterAt]?.adapter} is out of the fleet — Space on its rail row adds it back` }), above > 0 && _jsx(ElisionMark, { count: above, side: "above" }), visible.map((row, index) => renderModelRow(row, ui.focus === "list" && start + index === ui.listAt)), below > 0 && _jsx(ElisionMark, { count: below, side: "below", hint: "/ to search" }), rows.length === 0 && !scopeDenied && _jsx(Text, { dimColor: true, children: " no models match" })] }));
|
|
1234
1268
|
}
|
|
1235
1269
|
if (ui.view === "shapes") {
|
|
1236
1270
|
const rows = shapeList();
|
|
@@ -1266,11 +1300,6 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
|
|
|
1266
1300
|
: "unclassified — Space/Enter classifies; unclassified models are never routed");
|
|
1267
1301
|
}
|
|
1268
1302
|
}
|
|
1269
|
-
const hidden = hiddenModelCount();
|
|
1270
|
-
if (hidden > 0 && lines.length < 2)
|
|
1271
|
-
lines.push(`${hidden} retired/preview/non-worker hidden — a shows all`);
|
|
1272
|
-
if (ui.showAll && lines.length < 2)
|
|
1273
|
-
lines.push("showing retired models — a hides them again");
|
|
1274
1303
|
return lines;
|
|
1275
1304
|
}
|
|
1276
1305
|
if (ui.view === "shapes") {
|
package/dist/tui/ink/init-app.js
CHANGED
|
@@ -5,13 +5,13 @@ import { ToggleMark } from "./components.js";
|
|
|
5
5
|
import { clip, INK, inkInput, inkOutput, KeyBar, padCell, Pointer } from "./frame.js";
|
|
6
6
|
const DRIVERS = ["auto", "herdr", "subprocess", "orca"];
|
|
7
7
|
// One line per driver, shown for the value currently selected: four environments no longer fit on
|
|
8
|
-
// a single description row at 80 columns (the shipped three already clipped)
|
|
9
|
-
//
|
|
8
|
+
// a single description row at 80 columns (the shipped three already clipped). Auto identifies its
|
|
9
|
+
// innermost host from environment markers only; outside an Orca terminal, Orca is named explicitly.
|
|
10
10
|
const DRIVER_DESC = {
|
|
11
|
-
auto: "auto: herdr when HERDR_ENV=1, else
|
|
11
|
+
auto: "auto: herdr when HERDR_ENV=1, else orca inside an Orca terminal, else subprocess",
|
|
12
12
|
herdr: "herdr: every worker runs in a visible pane you can watch and unblock",
|
|
13
13
|
subprocess: "subprocess: headless child processes — no cockpit, same fail-closed gates",
|
|
14
|
-
orca: "orca: visible terminals in the Orca app —
|
|
14
|
+
orca: "orca: visible terminals in the Orca app — picked by auto inside one; name it elsewhere",
|
|
15
15
|
};
|
|
16
16
|
const VISIBILITY = ["pane", "headless"];
|
|
17
17
|
// Three fields, one toggle, one action — a descriptor array, deliberately not a forms framework.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tickmarkr",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Spec in, verified work out.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -57,5 +57,63 @@
|
|
|
57
57
|
"tsx": "^4.19.0",
|
|
58
58
|
"typescript": "^5.6.0",
|
|
59
59
|
"vitest": "^3.0.0"
|
|
60
|
+
},
|
|
61
|
+
"tickmarkrExport": {
|
|
62
|
+
"publicPaths": {
|
|
63
|
+
"exact": [
|
|
64
|
+
".gitignore",
|
|
65
|
+
".oxlintrc.json",
|
|
66
|
+
"CHANGELOG.md",
|
|
67
|
+
"CODE_OF_CONDUCT.md",
|
|
68
|
+
"CONTRIBUTING.md",
|
|
69
|
+
"FLEET.md",
|
|
70
|
+
"LICENSE",
|
|
71
|
+
"README.md",
|
|
72
|
+
"RELEASING.md",
|
|
73
|
+
"SECURITY.md",
|
|
74
|
+
"package-lock.json",
|
|
75
|
+
"package.json",
|
|
76
|
+
"tickmarkr.spec.md",
|
|
77
|
+
"tsconfig.json",
|
|
78
|
+
"vitest.config.ts",
|
|
79
|
+
".github/pull_request_template.md",
|
|
80
|
+
"scripts/assert-test-file-count.sh",
|
|
81
|
+
"scripts/emit-schema.ts",
|
|
82
|
+
"scripts/probe-rig.mjs",
|
|
83
|
+
"specs/export-selftest.spec.md"
|
|
84
|
+
],
|
|
85
|
+
"prefixes": [
|
|
86
|
+
".github/ISSUE_TEMPLATE/",
|
|
87
|
+
".github/workflows/",
|
|
88
|
+
"assets/",
|
|
89
|
+
"docs/codebase/",
|
|
90
|
+
"fixtures/",
|
|
91
|
+
"schema/",
|
|
92
|
+
"skills/tickmarkr-auto/",
|
|
93
|
+
"skills/tickmarkr-loop/",
|
|
94
|
+
"skills/tickmarkr-overseer/",
|
|
95
|
+
"src/",
|
|
96
|
+
"tests/"
|
|
97
|
+
]
|
|
98
|
+
},
|
|
99
|
+
"excludedPaths": [
|
|
100
|
+
".planning",
|
|
101
|
+
"specs",
|
|
102
|
+
".tickmarkr",
|
|
103
|
+
".overseer",
|
|
104
|
+
".claude",
|
|
105
|
+
"docs",
|
|
106
|
+
"ASSESSMENT-*.md",
|
|
107
|
+
".gitignore",
|
|
108
|
+
"scripts/measure-trailer-width.mjs",
|
|
109
|
+
"scripts/scan-scope-corpus.mjs",
|
|
110
|
+
"scripts/repro-obs96.mjs",
|
|
111
|
+
"CLAUDE.md",
|
|
112
|
+
"scripts/export-public.sh",
|
|
113
|
+
"scripts/verify-export.sh",
|
|
114
|
+
"tests/scripts/verify-export.test.ts",
|
|
115
|
+
".github/workflows/ci.yml",
|
|
116
|
+
"**/*.local.*"
|
|
117
|
+
]
|
|
60
118
|
}
|
|
61
119
|
}
|