tickmarkr 2.5.3 → 2.5.5
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/registry.js +6 -1
- package/dist/adapters/types.d.ts +3 -0
- package/dist/cli/commands/approve.d.ts +1 -0
- package/dist/cli/commands/approve.js +54 -6
- package/dist/cli/commands/doctor.d.ts +4 -0
- package/dist/cli/commands/doctor.js +44 -29
- package/dist/cli/commands/fleet.js +195 -33
- package/dist/cli/commands/init.js +196 -6
- package/dist/cli/commands/plan.js +144 -3
- package/dist/cli/commands/resume.js +6 -2
- package/dist/cli/commands/run.js +21 -2
- package/dist/cli/commands/verify.js +1 -1
- package/dist/cli/help.d.ts +2 -0
- package/dist/cli/help.js +3 -1
- package/dist/compile/collateral.d.ts +2 -0
- package/dist/compile/collateral.js +50 -9
- package/dist/compile/ownership.d.ts +7 -0
- package/dist/compile/ownership.js +59 -22
- package/dist/config/config.d.ts +23 -8
- package/dist/config/config.js +42 -28
- package/dist/config/fleet-overlay.d.ts +3 -9
- package/dist/config/fleet-overlay.js +68 -11
- package/dist/config/fleet-why.d.ts +7 -0
- package/dist/config/fleet-why.js +5 -0
- package/dist/drivers/herdr.d.ts +5 -0
- package/dist/drivers/herdr.js +14 -3
- package/dist/drivers/index.d.ts +15 -1
- package/dist/drivers/index.js +38 -10
- package/dist/drivers/orca.d.ts +99 -10
- package/dist/drivers/orca.js +586 -97
- package/dist/drivers/types.d.ts +1 -0
- package/dist/gates/baseline.d.ts +3 -0
- package/dist/gates/baseline.js +2 -1
- package/dist/gates/cache.d.ts +100 -0
- package/dist/gates/cache.js +389 -0
- package/dist/gates/review.d.ts +35 -2
- package/dist/gates/review.js +64 -16
- package/dist/gates/run-gates.d.ts +5 -0
- package/dist/gates/run-gates.js +134 -18
- package/dist/gates/test-manifest.d.ts +99 -0
- package/dist/gates/test-manifest.js +389 -0
- package/dist/gates/test-reporter.d.ts +4 -0
- package/dist/gates/test-reporter.js +49 -0
- package/dist/route/preference.d.ts +22 -1
- package/dist/route/preference.js +123 -25
- package/dist/route/router.d.ts +13 -0
- package/dist/route/router.js +95 -17
- package/dist/run/daemon.d.ts +13 -0
- package/dist/run/daemon.js +398 -96
- package/dist/run/git.d.ts +35 -1
- package/dist/run/git.js +111 -10
- package/dist/run/journal.d.ts +14 -2
- package/dist/run/journal.js +78 -10
- package/dist/run/lease.d.ts +14 -0
- package/dist/run/lease.js +87 -0
- package/dist/run/merge.d.ts +2 -0
- package/dist/run/merge.js +91 -3
- package/dist/run/operator-state.d.ts +11 -0
- package/dist/run/operator-state.js +17 -3
- package/dist/tui/cockpit/board.d.ts +96 -0
- package/dist/tui/cockpit/board.js +346 -0
- package/dist/tui/cockpit/decision-actions.js +2 -0
- package/dist/tui/cockpit/layout.d.ts +5 -1
- package/dist/tui/cockpit/layout.js +8 -3
- package/dist/tui/cockpit/live-runtime.js +83 -31
- package/dist/tui/cockpit/run-view.d.ts +7 -5
- package/dist/tui/cockpit/run-view.js +12 -11
- package/dist/tui/ink/fleet-app.d.ts +41 -27
- package/dist/tui/ink/fleet-app.js +204 -31
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/SKILL.md +180 -99
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
-
import { extname, join, relative } from "node:path";
|
|
2
|
+
import { extname, join, posix, relative } from "node:path";
|
|
3
3
|
import { filesGlob } from "../graph/files-glob.js";
|
|
4
4
|
import { criticalPathHits, DEFAULT_CONFIG, DEFAULT_REVIEW_CRITICAL_PATHS, effectiveReviewPolicy, loadConfig, } from "../config/config.js";
|
|
5
5
|
import { renderAcceptanceItem } from "../graph/schema.js";
|
|
@@ -156,20 +156,61 @@ export function classifyScopeOffenders(taskId, hard, predicted) {
|
|
|
156
156
|
const missed = hard.filter((f) => !named.has(f));
|
|
157
157
|
return { authoring: hard.length > 0 && missed.length === 0, predicted: hit, missed, repair: filesRepair(taskId, hit) };
|
|
158
158
|
}
|
|
159
|
+
const moduleKey = (path) => path.replace(/\\/g, "/").replace(/\.(?:[cm]?[jt]sx?)$/, "");
|
|
160
|
+
function directImportSpecifiers(text) {
|
|
161
|
+
// Comments cannot create an edge. Keep strings intact because they are the import target.
|
|
162
|
+
const source = text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
|
|
163
|
+
const specifiers = new Set();
|
|
164
|
+
for (const match of source.matchAll(/\bimport\s+(?:type\s+)?(?:[\w$*{},\s]+?\s+from\s+)?["']([^"']+)["']/g)) {
|
|
165
|
+
specifiers.add(match[1]);
|
|
166
|
+
}
|
|
167
|
+
for (const match of source.matchAll(/\bimport\s*\(\s*["']([^"']+)["']\s*\)/g)) {
|
|
168
|
+
specifiers.add(match[1]);
|
|
169
|
+
}
|
|
170
|
+
return [...specifiers];
|
|
171
|
+
}
|
|
172
|
+
function directlyImports(testPath, testText, sourcePath) {
|
|
173
|
+
const target = moduleKey(sourcePath);
|
|
174
|
+
const testDir = posix.dirname(testPath.replace(/\\/g, "/"));
|
|
175
|
+
return directImportSpecifiers(testText).some((specifier) => {
|
|
176
|
+
const imported = specifier.startsWith(".")
|
|
177
|
+
? posix.normalize(posix.join(testDir, specifier))
|
|
178
|
+
: specifier.startsWith("src/") ? specifier : "";
|
|
179
|
+
return imported !== "" && moduleKey(imported) === target;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
159
182
|
/**
|
|
160
183
|
* Return human-readable scope-lint lines for plan output (no `!` prefix — plan owns that).
|
|
161
184
|
* Each line names the task id and at least one missing collateral test path.
|
|
185
|
+
* OBS-971: uncapped — every predicted path is listed, with direct importers of an owned source
|
|
186
|
+
* sorted first, followed by the total count. The runtime map (collateralHits) remains unchanged.
|
|
162
187
|
*/
|
|
163
188
|
export function collateralLints(tasks, repoRoot) {
|
|
164
189
|
const lines = [];
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
190
|
+
const hitsMap = collateralHits(tasks, repoRoot);
|
|
191
|
+
const read = makeReader(repoRoot);
|
|
192
|
+
const tasksById = new Map(tasks.map((t) => [t.id, t]));
|
|
193
|
+
for (const [id, hits] of hitsMap) {
|
|
194
|
+
const t = tasksById.get(id);
|
|
195
|
+
const files = (t?.files ?? []).map((f) => f.replace(/^\.\//, ""));
|
|
196
|
+
const ownedSources = files.filter(isSrcPath);
|
|
197
|
+
const direct = [];
|
|
198
|
+
const indirect = [];
|
|
199
|
+
for (const h of hits) {
|
|
200
|
+
const text = read(h) ?? "";
|
|
201
|
+
const isDirect = ownedSources.some((src) => directlyImports(h, text, src));
|
|
202
|
+
if (isDirect) {
|
|
203
|
+
direct.push(h);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
indirect.push(h);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
direct.sort((a, b) => a.localeCompare(b));
|
|
210
|
+
indirect.sort((a, b) => a.localeCompare(b));
|
|
211
|
+
const ordered = [...direct, ...indirect];
|
|
212
|
+
const listed = ordered.join(", ");
|
|
213
|
+
const tail = hits.length > MAX_HITS_PER_TASK ? ` (${hits.length} total)` : "";
|
|
173
214
|
lines.push(`${id}: likely collateral tests not in files[]: ${listed}${tail}`);
|
|
174
215
|
}
|
|
175
216
|
return lines;
|
|
@@ -31,6 +31,13 @@ export type OwnershipFinding = {
|
|
|
31
31
|
ownerTaskId: string;
|
|
32
32
|
path: string;
|
|
33
33
|
detail: string;
|
|
34
|
+
} | {
|
|
35
|
+
code: "unowned-source-of-owned-test";
|
|
36
|
+
taskId: string;
|
|
37
|
+
test: string;
|
|
38
|
+
source: string;
|
|
39
|
+
corroboration?: OwnershipCorroboration;
|
|
40
|
+
detail: string;
|
|
34
41
|
};
|
|
35
42
|
export declare const SHAPE_ORACLES: readonly ["tests/run/narration.test.ts", "tests/cli/brand-surfaces.test.ts", "tests/run/notify-identity.test.ts", "tests/run/outcome-projections.test.ts", "tests/cockpit/setup.test.ts"];
|
|
36
43
|
export declare const SHAPE_ORACLE_SOURCES: readonly ["src/run/daemon.ts", "src/cli/commands/run.ts"];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, readdirSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
2
|
import { basename, extname, join, posix } from "node:path";
|
|
3
3
|
import { filesGlob } from "../graph/files-glob.js";
|
|
4
4
|
import { collateralHits } from "./collateral.js";
|
|
@@ -17,21 +17,24 @@ export const SHAPE_ORACLE_MAP = {
|
|
|
17
17
|
"src/run/daemon.ts": SHAPE_ORACLES,
|
|
18
18
|
"src/cli/commands/run.ts": SHAPE_ORACLES,
|
|
19
19
|
};
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
function touchesSource(files, source) {
|
|
20
|
+
// A files[] entry touches a mapped shape-oracle source when it names it literally or by stem, or when
|
|
21
|
+
// the entry's glob — anchored or broad — matches the source AND the source exists in the repository
|
|
22
|
+
// being compiled: "src/run/**" owning this repository's daemon owns the five oracles, while the
|
|
23
|
+
// shipped sample PRD's "src/**" task in a repository holding no mapped source compiles clean.
|
|
24
|
+
// Anchored globs like "src/run/daemon.*" target the source specifically and touch it regardless.
|
|
25
|
+
function touchesSource(files, source, repoRoot) {
|
|
27
26
|
const stem = source.replace(/\.(?:[cm]?[jt]sx?)$/, "");
|
|
28
27
|
return files.some((entry) => {
|
|
29
28
|
if (entry === source || entry === stem)
|
|
30
29
|
return true;
|
|
31
30
|
const special = entry.search(/[*?{[]/);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
if (special === -1)
|
|
32
|
+
return false;
|
|
33
|
+
if (!filesGlob([entry])(source))
|
|
34
|
+
return false;
|
|
35
|
+
if (entry.slice(0, special) === `${stem}.`)
|
|
36
|
+
return true;
|
|
37
|
+
return repoRoot !== undefined && existsSync(join(repoRoot, source));
|
|
35
38
|
});
|
|
36
39
|
}
|
|
37
40
|
const normalize = (path) => path.replace(/^\.\//, "").split("\\").join("/");
|
|
@@ -133,20 +136,24 @@ function mentionsCommandEntry(text) {
|
|
|
133
136
|
return /(?:^|\/)src\/cli\/index\.(?:ts|js)\b/.test(text)
|
|
134
137
|
|| /["'`]src["'`]\s*,\s*["'`]cli["'`]\s*,\s*["'`]index\.(?:ts|js)["'`]/.test(text);
|
|
135
138
|
}
|
|
136
|
-
function
|
|
137
|
-
// A .test.ts-shaped collateral fixture is not by itself a dedicated test. Requiring a runner leaf
|
|
138
|
-
// keeps import-only scan fixtures advisory while every executable subject in the measured union stays.
|
|
139
|
+
function corroborateSource(test, sourcePath) {
|
|
139
140
|
const executable = test.text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
|
|
140
141
|
if (!/\b(?:test|it)(?:\.(?:concurrent|each|fails|only|skip|todo))*\s*\(/.test(executable))
|
|
141
142
|
return undefined;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return { kind: "direct-import", source: match.source };
|
|
145
|
-
}
|
|
143
|
+
if (directlyImports(test, sourcePath))
|
|
144
|
+
return { kind: "direct-import", source: sourcePath };
|
|
146
145
|
if (invokesChildProcessSpawn(test.text) && mentionsCommandEntry(test.text)) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
if (/^src\/cli\/commands\/[^/]+\.(?:[cm]?[jt]sx?)$/.test(sourcePath)) {
|
|
147
|
+
return { kind: "command-entry-spawn", source: sourcePath, entry: "src/cli/index.ts" };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
function corroboration(test, matches) {
|
|
153
|
+
for (const match of matches) {
|
|
154
|
+
const evidence = corroborateSource(test, match.source);
|
|
155
|
+
if (evidence)
|
|
156
|
+
return evidence;
|
|
150
157
|
}
|
|
151
158
|
return undefined;
|
|
152
159
|
}
|
|
@@ -239,7 +246,7 @@ export function ownershipFindings(tasks, repoRoot) {
|
|
|
239
246
|
}
|
|
240
247
|
for (const entry of indexed) {
|
|
241
248
|
for (const [source, oracles] of Object.entries(SHAPE_ORACLE_MAP)) {
|
|
242
|
-
if (!touchesSource(entry.files, source))
|
|
249
|
+
if (!touchesSource(entry.files, source, repoRoot))
|
|
243
250
|
continue;
|
|
244
251
|
for (const oracle of oracles) {
|
|
245
252
|
if (!entry.owns(oracle)) {
|
|
@@ -254,6 +261,35 @@ export function ownershipFindings(tasks, repoRoot) {
|
|
|
254
261
|
}
|
|
255
262
|
}
|
|
256
263
|
}
|
|
264
|
+
for (const test of sources) {
|
|
265
|
+
const testOwners = owners(test.path);
|
|
266
|
+
if (testOwners.length === 0)
|
|
267
|
+
continue;
|
|
268
|
+
const testStem = basename(test.path).replace(/\.test\.ts$/, "");
|
|
269
|
+
for (const sourcePath of allSources) {
|
|
270
|
+
if (owners(sourcePath).length > 0)
|
|
271
|
+
continue;
|
|
272
|
+
const sourceStem = basename(sourcePath, extname(sourcePath));
|
|
273
|
+
if (testStem !== sourceStem && !testStem.startsWith(`${sourceStem}-`))
|
|
274
|
+
continue;
|
|
275
|
+
const evidence = corroborateSource(test, sourcePath);
|
|
276
|
+
if (!evidence)
|
|
277
|
+
continue;
|
|
278
|
+
for (const owner of testOwners) {
|
|
279
|
+
findings.push({
|
|
280
|
+
code: "unowned-source-of-owned-test",
|
|
281
|
+
taskId: owner.task.id,
|
|
282
|
+
test: test.path,
|
|
283
|
+
source: sourcePath,
|
|
284
|
+
corroboration: evidence,
|
|
285
|
+
detail: `${test.path} owned by ${owner.task.id} is a dedicated test of ${sourcePath} but no task owns ${sourcePath}`
|
|
286
|
+
+ (evidence.kind === "direct-import" ? `; it imports ${evidence.source} directly`
|
|
287
|
+
: evidence.kind === "command-entry-spawn"
|
|
288
|
+
? `; it spawns ${evidence.entry} to exercise ${evidence.source}` : ""),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
257
293
|
for (const source of sources) {
|
|
258
294
|
for (const owner of owners(source.path)) {
|
|
259
295
|
for (const path of repositoryPaths(source.text)) {
|
|
@@ -290,6 +326,7 @@ export function ownershipFindings(tasks, repoRoot) {
|
|
|
290
326
|
case "unowned-shape-oracle": return f.oracle;
|
|
291
327
|
case "test-path-outside-allowlist": return f.test;
|
|
292
328
|
case "unordered-context-write": return f.path;
|
|
329
|
+
case "unowned-source-of-owned-test": return f.test;
|
|
293
330
|
}
|
|
294
331
|
};
|
|
295
332
|
return findings.sort((a, b) => {
|
package/dist/config/config.d.ts
CHANGED
|
@@ -175,6 +175,10 @@ export declare const TickmarkrConfigSchema: z.ZodObject<{
|
|
|
175
175
|
on: "on";
|
|
176
176
|
off: "off";
|
|
177
177
|
}>;
|
|
178
|
+
escalateTier: z.ZodDefault<z.ZodEnum<{
|
|
179
|
+
on: "on";
|
|
180
|
+
off: "off";
|
|
181
|
+
}>>;
|
|
178
182
|
learnedTuning: z.ZodOptional<z.ZodObject<{
|
|
179
183
|
halfLifeRuns: z.ZodOptional<z.ZodNumber>;
|
|
180
184
|
availWeight: z.ZodOptional<z.ZodNumber>;
|
|
@@ -275,6 +279,11 @@ export declare const TickmarkrConfigSchema: z.ZodObject<{
|
|
|
275
279
|
"judge-only": "judge-only";
|
|
276
280
|
}>>;
|
|
277
281
|
criticalPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
282
|
+
floor: z.ZodDefault<z.ZodUnion<readonly [z.ZodLiteral<"worker">, z.ZodEnum<{
|
|
283
|
+
cheap: "cheap";
|
|
284
|
+
mid: "mid";
|
|
285
|
+
frontier: "frontier";
|
|
286
|
+
}>]>>;
|
|
278
287
|
}, z.core.$strip>;
|
|
279
288
|
consult: z.ZodObject<{
|
|
280
289
|
adapter: z.ZodString;
|
|
@@ -355,24 +364,30 @@ export type FleetTierAssignment = {
|
|
|
355
364
|
export type FleetEditable = {
|
|
356
365
|
denyAdapters: string[];
|
|
357
366
|
denyModels: string[];
|
|
367
|
+
denyWorkersAdapters?: string[];
|
|
368
|
+
denyWorkersModels?: string[];
|
|
358
369
|
tiers: Record<string, Record<string, FleetTierAssignment | null>>;
|
|
359
370
|
map: Record<string, MapEntry>;
|
|
360
371
|
floors: Record<string, Tier>;
|
|
361
372
|
};
|
|
362
373
|
export declare function repoOverlayPath(repoRoot: string): string;
|
|
363
374
|
export declare function readOverlayFile(path: string): Record<string, unknown>;
|
|
375
|
+
/** A discovered fleet universe row: the served models of one adapter and, per model, the resolved
|
|
376
|
+
* identity doctor recorded for a floating alias (LEG2-T3 round 2 finding 3). */
|
|
377
|
+
export type FleetUniverseRow = {
|
|
378
|
+
adapter: string;
|
|
379
|
+
models: string[];
|
|
380
|
+
identities?: Record<string, string>;
|
|
381
|
+
};
|
|
382
|
+
/** Does a deny/allow entry name this universe channel? Grammar A1 (adapter id, bare model id,
|
|
383
|
+
* adapter:model) plus the recorded identity (bare or adapter:identity) — the collector's authority. */
|
|
384
|
+
export declare function universeEntryMatches(row: FleetUniverseRow, model: string, entry: string): boolean;
|
|
364
385
|
/** Grammar A1 membership test: does any discovered universe row cover this deny/allow entry
|
|
365
386
|
* (adapter id, bare model id, or adapter:model)? Uncovered entries name channels the probe
|
|
366
387
|
* did not serve this session (failed auth, rate limit, retired sku) — they are NOT editable
|
|
367
388
|
* membership state and must survive a membership write verbatim, never be re-derived away. */
|
|
368
|
-
export declare function universeCovers(universe:
|
|
369
|
-
|
|
370
|
-
models: string[];
|
|
371
|
-
}[], entry: string): boolean;
|
|
372
|
-
export declare function fleetEditableFromConfig(cfg: TickmarkrConfig, universe?: {
|
|
373
|
-
adapter: string;
|
|
374
|
-
models: string[];
|
|
375
|
-
}[]): FleetEditable;
|
|
389
|
+
export declare function universeCovers(universe: FleetUniverseRow[], entry: string): boolean;
|
|
390
|
+
export declare function fleetEditableFromConfig(cfg: TickmarkrConfig, universe?: FleetUniverseRow[]): FleetEditable;
|
|
376
391
|
/** Resolve which layer last set a dotted fleet path (defaults < global < repo). */
|
|
377
392
|
export declare function fleetKeyLayer(repoRoot: string, dotted: string, opts?: {
|
|
378
393
|
globalDir?: string;
|
package/dist/config/config.js
CHANGED
|
@@ -302,6 +302,8 @@ export const TickmarkrConfigSchema = z.object({
|
|
|
302
302
|
}),
|
|
303
303
|
floors: z.record(z.string(), TierEnum),
|
|
304
304
|
learned: z.enum(["on", "off"]), // v1.6 ROUTE-09 kill switch; a typo (offf) fails loud via safeParse
|
|
305
|
+
// OBS-986 (ES-1): climb one tier on request when untried channels exist in higher tiers.
|
|
306
|
+
escalateTier: z.enum(["on", "off"]).default("on"),
|
|
305
307
|
// v1.9 ROUTE-15 — optional overrides for profile.ts HALF_LIFE_RUNS/AVAIL_WEIGHT; absent ⇒ byte-identical defaults.
|
|
306
308
|
// SIBLING of learned (not nested): routing.learned is the on/off enum switch.
|
|
307
309
|
learnedTuning: z.object({
|
|
@@ -366,6 +368,11 @@ export const TickmarkrConfigSchema = z.object({
|
|
|
366
368
|
// R3: globs no task may skip review on (input/demux/lifecycle, gates/run/drivers/adapters, anything
|
|
367
369
|
// reaching a shell). A judge-only task intersecting one of these fails COMPILE, never silently skips.
|
|
368
370
|
criticalPaths: z.array(z.string()).optional(),
|
|
371
|
+
// RF-1 (OBS-922 add.2/3): review.floor — `worker` (default) names no tier and leaves the seat
|
|
372
|
+
// ranking to the author's routed seat; a tier is folded in as max(task floor, this) by reviewGate
|
|
373
|
+
// and its retry — it can raise a seat and never lower one. Not in the template: the cockpit
|
|
374
|
+
// fixture pins the fresh-install bytes (see d0e89a9a).
|
|
375
|
+
floor: z.union([z.literal("worker"), TierEnum]).default("worker"),
|
|
369
376
|
}),
|
|
370
377
|
// v1.54 T1: prefer — ranked consult seat failover. Entries MUST be adapter:model (unlike
|
|
371
378
|
// review.prefer's adapter|adapter:model grammar): a consult seat has no channel to inherit a
|
|
@@ -445,6 +452,7 @@ export const DEFAULT_CONFIG = {
|
|
|
445
452
|
// a workspace that has accumulated ≥MIN_SAMPLES warm telemetry per cell. Preview any workspace's effect
|
|
446
453
|
// first with `tickmarkr plan` / `tickmarkr report`; flip to "off" to pin exact static routing (the kill switch stands).
|
|
447
454
|
learned: "on",
|
|
455
|
+
escalateTier: "on",
|
|
448
456
|
allowUnverifiedModels: false,
|
|
449
457
|
},
|
|
450
458
|
// Seed table (spec §13). New models = edit this (or your config.yaml), never code.
|
|
@@ -590,7 +598,7 @@ export const DEFAULT_CONFIG = {
|
|
|
590
598
|
judge: { adapter: "claude-code", model: "fable" },
|
|
591
599
|
// R3: no `policy` floor — the neutral floor leaves the compiler's per-task assignment standing, so
|
|
592
600
|
// the path-keyed rule is reachable out of the box rather than raised to full by construction.
|
|
593
|
-
review: { complexityThreshold: 7, timeoutMs: 900_000, required: true, criticalPaths: [...DEFAULT_REVIEW_CRITICAL_PATHS] },
|
|
601
|
+
review: { complexityThreshold: 7, timeoutMs: 900_000, required: true, floor: "worker", criticalPaths: [...DEFAULT_REVIEW_CRITICAL_PATHS] },
|
|
594
602
|
consult: { adapter: "claude-code", model: "fable", stallMinutes: 15 },
|
|
595
603
|
// v1.4: gate LLM calls (judge/review/consult) run headless by default; pane opts back into visible agents.
|
|
596
604
|
// v1.2: workers are the real agent TUI in the pane; "print" restores the -p-rendered-in-pane path.
|
|
@@ -784,6 +792,7 @@ export function configTemplate(overlay) {
|
|
|
784
792
|
# floors: # tier authority — advisory minimum bands; 'tickmarkr plan' lints violations
|
|
785
793
|
# migration: frontier
|
|
786
794
|
# learned: on # default ON (ROUTE-14); cold profile = exact v1.5 static routing, warms per workspace. Set 'off' to pin static routing; preview with 'tickmarkr plan'
|
|
795
|
+
# escalateTier: on # on | off (default on): climb one tier on request when untried channels exist in higher tiers
|
|
787
796
|
# learnedTuning: { halfLifeRuns: 5, availWeight: 0.05 } # optional; defaults byte-identical
|
|
788
797
|
# explore: { mode: on, excludeShapes: [], excludeComplexityAtOrAbove: null, cap: 5 } # optional; absent ⇒ byte-identical
|
|
789
798
|
# sla: { implement: 15 } # optional per-shape minutes — advisory plan lint only; absent ⇒ no lint
|
|
@@ -817,7 +826,9 @@ export function configTemplate(overlay) {
|
|
|
817
826
|
# test: npm test
|
|
818
827
|
# byShape:
|
|
819
828
|
# docs: { acceptance: false, review: false } # baseline, evidence, and scope are mandatory
|
|
820
|
-
# review: { complexityThreshold: 7, timeoutMs: 900000, required: true, prefer: [codex:gpt-5.6-sol, kimi] }
|
|
829
|
+
# review: { complexityThreshold: 7, timeoutMs: 900000, required: true, floor: worker, prefer: [codex:gpt-5.6-sol, kimi] }
|
|
830
|
+
# # floor: worker (default) | cheap | mid | frontier — the reviewer seats at or above
|
|
831
|
+
# # max(author tier, task floor, this tier, prior reviewer); a tier here only raises it
|
|
821
832
|
# # prefer: ordered reviewer seat preference (adapter | adapter:model); ranks
|
|
822
833
|
# # diversity-eligible channels only — never admits a same-vendor/same-model reviewer
|
|
823
834
|
# consult: { adapter: claude-code, model: fable, stallMinutes: 15, prefer: [codex:gpt-5.6-sol, kimi:kimi-code/k3] }
|
|
@@ -876,6 +887,14 @@ export function readOverlayFile(path) {
|
|
|
876
887
|
throw new ConfigError(`invalid overlay at ${path}`);
|
|
877
888
|
return raw;
|
|
878
889
|
}
|
|
890
|
+
/** Does a deny/allow entry name this universe channel? Grammar A1 (adapter id, bare model id,
|
|
891
|
+
* adapter:model) plus the recorded identity (bare or adapter:identity) — the collector's authority. */
|
|
892
|
+
export function universeEntryMatches(row, model, entry) {
|
|
893
|
+
if (entry === row.adapter || entry === model || entry === `${row.adapter}:${model}`)
|
|
894
|
+
return true;
|
|
895
|
+
const identity = row.identities?.[model];
|
|
896
|
+
return identity !== undefined && (entry === identity || entry === `${row.adapter}:${identity}`);
|
|
897
|
+
}
|
|
879
898
|
/** Grammar A1 membership test: does any discovered universe row cover this deny/allow entry
|
|
880
899
|
* (adapter id, bare model id, or adapter:model)? Uncovered entries name channels the probe
|
|
881
900
|
* did not serve this session (failed auth, rate limit, retired sku) — they are NOT editable
|
|
@@ -884,9 +903,11 @@ export function universeCovers(universe, entry) {
|
|
|
884
903
|
return universe.some((row) => entry === row.adapter || row.models.some((m) => entry === m || entry === `${row.adapter}:${m}`));
|
|
885
904
|
}
|
|
886
905
|
// v1.92 fleet membership: with a discovered `universe` (classified models only) the deny sets become
|
|
887
|
-
// the EFFECTIVE out-of-fleet
|
|
888
|
-
// routing.deny adapters/models
|
|
889
|
-
//
|
|
906
|
+
// the EFFECTIVE out-of-fleet set — the routing.allow complement (fail-closed) as adapter ids and
|
|
907
|
+
// adapter:model keys, UNION every authored routing.deny adapters/models entry VERBATIM in its own list
|
|
908
|
+
// (never workers). LEG2-T3 round 2 finding 2: authored entries are never collapsed into a membership
|
|
909
|
+
// key, so one Space press clears one reason; finding 3: allow admission honours recorded identity.
|
|
910
|
+
// Absent universe ⇒ deny arrays verbatim, so non-UI callers stay byte-identical.
|
|
890
911
|
export function fleetEditableFromConfig(cfg, universe) {
|
|
891
912
|
const tiers = {};
|
|
892
913
|
for (const [adapter, entry] of Object.entries(cfg.tiers)) {
|
|
@@ -898,43 +919,32 @@ export function fleetEditableFromConfig(cfg, universe) {
|
|
|
898
919
|
let denyAdapters = [...(cfg.routing.deny?.adapters ?? [])].sort();
|
|
899
920
|
let denyModels = [...(cfg.routing.deny?.models ?? [])].sort();
|
|
900
921
|
if (universe !== undefined) {
|
|
901
|
-
const { allow
|
|
902
|
-
// Grammar A1 (disallowedBy precedent): entries match adapter id, bare model id, or adapter:model.
|
|
903
|
-
// Inlined rather than imported — route/preference.ts imports this module.
|
|
904
|
-
const denyEntries = [...(deny?.adapters ?? []), ...(deny?.models ?? [])];
|
|
922
|
+
const { allow } = cfg.routing;
|
|
905
923
|
const allowEntries = allow ? [...(allow.adapters ?? []), ...(allow.models ?? [])] : null;
|
|
906
|
-
const
|
|
907
|
-
const matches = (e) => e === adapter || e === model || e === `${adapter}:${model}`;
|
|
908
|
-
if (denyEntries.some(matches))
|
|
909
|
-
return true;
|
|
910
|
-
return allowEntries !== null && !allowEntries.some(matches);
|
|
911
|
-
};
|
|
924
|
+
const notAdmitted = (row, model) => allowEntries !== null && !allowEntries.some((entry) => universeEntryMatches(row, model, entry));
|
|
912
925
|
const adaptersOut = [];
|
|
913
926
|
const modelsOut = [];
|
|
914
927
|
for (const row of universe) {
|
|
915
|
-
const out = row.models.filter((m) =>
|
|
916
|
-
if (row.models.length ? out.length === row.models.length :
|
|
928
|
+
const out = row.models.filter((m) => notAdmitted(row, m));
|
|
929
|
+
if (row.models.length ? out.length === row.models.length : notAdmitted(row, "")) {
|
|
917
930
|
adaptersOut.push(row.adapter);
|
|
918
931
|
}
|
|
919
932
|
else {
|
|
920
933
|
modelsOut.push(...out.map((m) => `${row.adapter}:${m}`));
|
|
921
934
|
}
|
|
922
935
|
}
|
|
923
|
-
// OBS-517: deny entries the probe universe
|
|
924
|
-
//
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
...adaptersOut,
|
|
928
|
-
...(deny?.adapters ?? []).filter((e) => !universeCovers(universe, e)),
|
|
929
|
-
])].sort();
|
|
930
|
-
denyModels = [...new Set([
|
|
931
|
-
...modelsOut,
|
|
932
|
-
...(deny?.models ?? []).filter((e) => !universeCovers(universe, e)),
|
|
933
|
-
])].sort();
|
|
936
|
+
// OBS-517: authored deny entries — covered by the probe universe or not — ride verbatim, so a
|
|
937
|
+
// transient probe failure can never un-deny one on the next write (fail-open on the ban list).
|
|
938
|
+
denyAdapters = [...new Set([...adaptersOut, ...denyAdapters])].sort();
|
|
939
|
+
denyModels = [...new Set([...modelsOut, ...denyModels])].sort();
|
|
934
940
|
}
|
|
935
941
|
return {
|
|
936
942
|
denyAdapters,
|
|
937
943
|
denyModels,
|
|
944
|
+
// OBS-994: workers-only deny is never folded into the all-seats complement above — it is
|
|
945
|
+
// returned verbatim beside the flat scopes so the fleet browser can show and toggle it.
|
|
946
|
+
denyWorkersAdapters: [...(cfg.routing.deny?.workers?.adapters ?? [])].sort(),
|
|
947
|
+
denyWorkersModels: [...(cfg.routing.deny?.workers?.models ?? [])].sort(),
|
|
938
948
|
tiers,
|
|
939
949
|
map: structuredClone(cfg.routing.map),
|
|
940
950
|
floors: { ...cfg.routing.floors },
|
|
@@ -977,6 +987,10 @@ export function formatFleetPrint(repoRoot, opts = {}) {
|
|
|
977
987
|
annotate(["routing", "deny", "adapters"]);
|
|
978
988
|
if (effective.routing.deny?.models !== undefined)
|
|
979
989
|
annotate(["routing", "deny", "models"]);
|
|
990
|
+
if (effective.routing.deny?.workers?.adapters !== undefined)
|
|
991
|
+
annotate(["routing", "deny", "workers", "adapters"]);
|
|
992
|
+
if (effective.routing.deny?.workers?.models !== undefined)
|
|
993
|
+
annotate(["routing", "deny", "workers", "models"]);
|
|
980
994
|
for (const shape of Object.keys(effective.routing.map))
|
|
981
995
|
annotate(["routing", "map", shape]);
|
|
982
996
|
annotate(["routing", "floors"]);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type FleetEditable, type RoutingMode } from "./config.js";
|
|
1
|
+
import { type FleetEditable, type FleetUniverseRow, type RoutingMode } from "./config.js";
|
|
2
2
|
/** Fleet-owned overlay keys — the only config surface `tickmarkr fleet` may write. */
|
|
3
3
|
export declare const FLEET_OVERLAY_KEYS: readonly ["routing", "tiers"];
|
|
4
4
|
export type FleetOverlayWrite = {
|
|
@@ -19,10 +19,7 @@ export type FleetOverlayWrite = {
|
|
|
19
19
|
consult?: string[];
|
|
20
20
|
};
|
|
21
21
|
};
|
|
22
|
-
universe?:
|
|
23
|
-
adapter: string;
|
|
24
|
-
models: string[];
|
|
25
|
-
}[];
|
|
22
|
+
universe?: FleetUniverseRow[];
|
|
26
23
|
};
|
|
27
24
|
export type FleetFirstTouch = {
|
|
28
25
|
vendor: string;
|
|
@@ -33,10 +30,7 @@ export declare function fleetFirstTouchProvenance(note: string, firstTouch: Flee
|
|
|
33
30
|
* keys, ordering, scalar style, and comments; fresh provenance is written directly on its tier node. */
|
|
34
31
|
export declare function renderFleetOverlayWrite(priorBytes: string, write: FleetOverlayWrite): string;
|
|
35
32
|
/** Build the repo overlay fragment fleet would write for edits since session start. */
|
|
36
|
-
export declare function fleetRepoOverlayFromDelta(initial: FleetEditable, edited: FleetEditable, existingRepo?: Record<string, unknown>, firstTouches?: Readonly<Record<string, FleetFirstTouch>>, universe?:
|
|
37
|
-
adapter: string;
|
|
38
|
-
models: string[];
|
|
39
|
-
}[]): Record<string, unknown>;
|
|
33
|
+
export declare function fleetRepoOverlayFromDelta(initial: FleetEditable, edited: FleetEditable, existingRepo?: Record<string, unknown>, firstTouches?: Readonly<Record<string, FleetFirstTouch>>, universe?: FleetUniverseRow[]): Record<string, unknown>;
|
|
40
34
|
export declare function repoOverlayYaml(overlay: Record<string, unknown>): string;
|
|
41
35
|
export declare function serializeFleetOverlay(overlay: Record<string, unknown>): string;
|
|
42
36
|
export declare function unifiedYamlDiff(before: string, after: string, label?: string): string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Fleet-overlay mutation, serialization, and diff rendering for the `tickmarkr fleet` write path.
|
|
2
2
|
import { isMap, isScalar, isSeq, parseDocument, stringify, visit } from "yaml";
|
|
3
|
-
import { universeCovers } from "./config.js";
|
|
3
|
+
import { universeCovers, universeEntryMatches } from "./config.js";
|
|
4
4
|
/** Fleet-owned overlay keys — the only config surface `tickmarkr fleet` may write. */
|
|
5
5
|
export const FLEET_OVERLAY_KEYS = ["routing", "tiers"];
|
|
6
6
|
function fleetSubset(obj) {
|
|
@@ -21,17 +21,18 @@ function allowFormFromExclusions(universe, edited) {
|
|
|
21
21
|
if (!universe.length) {
|
|
22
22
|
throw new Error("fleet write: universe is empty — no classified models to compute routing.allow from; classify models in `tickmarkr fleet` first");
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
// LEG2-T3 round 2 finding 1: every staged entry excludes what it NAMES — a bare model id every
|
|
25
|
+
// adapter serving it, an identity its alias — never only an adapter id or an adapter:model key.
|
|
26
|
+
const entries = [...edited.denyAdapters, ...edited.denyModels];
|
|
26
27
|
const adapters = [];
|
|
27
28
|
const models = [];
|
|
28
29
|
let excluded = false;
|
|
29
30
|
for (const row of universe) {
|
|
30
|
-
if (
|
|
31
|
+
if (entries.includes(row.adapter)) {
|
|
31
32
|
excluded = true;
|
|
32
33
|
continue;
|
|
33
34
|
}
|
|
34
|
-
const inFleet = row.models.filter((m) => !
|
|
35
|
+
const inFleet = row.models.filter((m) => !entries.some((entry) => universeEntryMatches(row, m, entry)));
|
|
35
36
|
if (inFleet.length === row.models.length) {
|
|
36
37
|
adapters.push(row.adapter);
|
|
37
38
|
}
|
|
@@ -49,6 +50,21 @@ function allowFormFromExclusions(universe, edited) {
|
|
|
49
50
|
function residualDeny(universe, entries) {
|
|
50
51
|
return sortedUnique(entries.filter((entry) => !universeCovers(universe, entry)));
|
|
51
52
|
}
|
|
53
|
+
// LEG2-T3 round 2 finding 2: the flat deny list a membership write leaves behind. Every entry the
|
|
54
|
+
// repo overlay authored in THIS list that is still staged stays verbatim, in its authored order and
|
|
55
|
+
// with its comments — one cleared reason never takes an independent one with it, and an untouched
|
|
56
|
+
// list keeps its node. A staged entry the allow form cannot express as a membership key (outside the
|
|
57
|
+
// probe universe, or a bare-model/identity spelling) is written verbatim too; canonical keys the
|
|
58
|
+
// session added ride the allow form alone.
|
|
59
|
+
function flatDenyAfterWrite(doc, path, after, universe) {
|
|
60
|
+
const node = doc.getIn(path, true);
|
|
61
|
+
const authored = isSeq(node) ? node.items.flatMap((item) => (isScalar(item) ? [String(item.value)] : [])) : [];
|
|
62
|
+
const canonical = (entry) => universe.some((row) => entry === row.adapter || row.models.some((m) => entry === `${row.adapter}:${m}`));
|
|
63
|
+
const kept = authored.filter((entry) => after.includes(entry));
|
|
64
|
+
const verbatim = sortedUnique(after.filter((entry) => !kept.includes(entry)
|
|
65
|
+
&& (!universeCovers(universe, entry) || !canonical(entry))));
|
|
66
|
+
return [...new Set([...kept, ...verbatim])];
|
|
67
|
+
}
|
|
52
68
|
// fleet.ts deliberately remains the sole overlay builder and writer. Its established classification
|
|
53
69
|
// seam copies only `tier` and `note` into FleetEditable, so first-touch entry metadata rides inside a
|
|
54
70
|
// private provenance envelope until this module writes the YAML. The envelope never reaches disk.
|
|
@@ -199,18 +215,35 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
199
215
|
// Whole fleet in: no restriction to express — the allow block goes away entirely.
|
|
200
216
|
deleteAt(doc, ["routing", "allow"]);
|
|
201
217
|
}
|
|
202
|
-
//
|
|
203
|
-
// re-exclude behind the
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
218
|
+
// Authored and non-canonical entries stay in deny (LEG2-T3 finding 4, round 2 finding 2); a
|
|
219
|
+
// list left with nothing is tombstoned so a lower layer can never re-exclude behind the
|
|
220
|
+
// operator's back (workers untouched).
|
|
221
|
+
for (const [scope, after] of [["adapters", edited.denyAdapters], ["models", edited.denyModels]]) {
|
|
222
|
+
const path = ["routing", "deny", scope];
|
|
223
|
+
const remaining = flatDenyAfterWrite(doc, path, after, write.universe);
|
|
224
|
+
setStringSequencePreservingComments(doc, path, remaining.length ? remaining : null);
|
|
225
|
+
}
|
|
208
226
|
}
|
|
209
227
|
else {
|
|
210
228
|
setStringSequencePreservingComments(doc, ["routing", "deny", "adapters"], edited.denyAdapters.length ? sortedUnique(edited.denyAdapters) : null);
|
|
211
229
|
setStringSequencePreservingComments(doc, ["routing", "deny", "models"], edited.denyModels.length ? sortedUnique(edited.denyModels) : null);
|
|
212
230
|
}
|
|
213
231
|
}
|
|
232
|
+
// OBS-994/FL-1: routing.deny.workers is a literal deny list, never a universe-derived
|
|
233
|
+
// membership scope — it never routes through the allow-complement dance above, in either
|
|
234
|
+
// branch. Same tombstone/comment-preserving rules as the flat scopes.
|
|
235
|
+
const initialWorkersAdapters = initial.denyWorkersAdapters ?? [];
|
|
236
|
+
const editedWorkersAdapters = edited.denyWorkersAdapters ?? [];
|
|
237
|
+
const initialWorkersModels = initial.denyWorkersModels ?? [];
|
|
238
|
+
const editedWorkersModels = edited.denyWorkersModels ?? [];
|
|
239
|
+
// Each sub-path mutates independently — an untouched sibling must not be rewritten (a `null`
|
|
240
|
+
// tombstone over an absent/untouched sibling would mask a lower layer's own workers scope).
|
|
241
|
+
if (sortedUnique(initialWorkersAdapters).join() !== sortedUnique(editedWorkersAdapters).join()) {
|
|
242
|
+
setStringSequencePreservingComments(doc, ["routing", "deny", "workers", "adapters"], editedWorkersAdapters.length ? sortedUnique(editedWorkersAdapters) : null);
|
|
243
|
+
}
|
|
244
|
+
if (sortedUnique(initialWorkersModels).join() !== sortedUnique(editedWorkersModels).join()) {
|
|
245
|
+
setStringSequencePreservingComments(doc, ["routing", "deny", "workers", "models"], editedWorkersModels.length ? sortedUnique(editedWorkersModels) : null);
|
|
246
|
+
}
|
|
214
247
|
for (const shape of new Set([...Object.keys(initial.map), ...Object.keys(edited.map)])) {
|
|
215
248
|
const before = initial.map[shape];
|
|
216
249
|
const after = edited.map[shape];
|
|
@@ -379,6 +412,30 @@ export function fleetRepoOverlayFromDelta(initial, edited, existingRepo = {}, fi
|
|
|
379
412
|
}
|
|
380
413
|
routingTouched = true;
|
|
381
414
|
}
|
|
415
|
+
// OBS-994/FL-1: workers deny is a literal list, independent of the universe/allow dance above.
|
|
416
|
+
// Each sub-path is included only when it actually changed — an untouched sibling must not be
|
|
417
|
+
// rewritten as a `null` tombstone over whatever the existing repo overlay already held.
|
|
418
|
+
const initialWorkersAdapters = initial.denyWorkersAdapters ?? [];
|
|
419
|
+
const editedWorkersAdapters = edited.denyWorkersAdapters ?? [];
|
|
420
|
+
const initialWorkersModels = initial.denyWorkersModels ?? [];
|
|
421
|
+
const editedWorkersModels = edited.denyWorkersModels ?? [];
|
|
422
|
+
const workersAdaptersChanged = sortedUnique(initialWorkersAdapters).join() !== sortedUnique(editedWorkersAdapters).join();
|
|
423
|
+
const workersModelsChanged = sortedUnique(initialWorkersModels).join() !== sortedUnique(editedWorkersModels).join();
|
|
424
|
+
if (workersAdaptersChanged || workersModelsChanged) {
|
|
425
|
+
routing.deny = {
|
|
426
|
+
...routing.deny,
|
|
427
|
+
workers: {
|
|
428
|
+
...(routing.deny?.workers),
|
|
429
|
+
...(workersAdaptersChanged
|
|
430
|
+
? { adapters: editedWorkersAdapters.length ? editedWorkersAdapters : null }
|
|
431
|
+
: {}),
|
|
432
|
+
...(workersModelsChanged
|
|
433
|
+
? { models: editedWorkersModels.length ? editedWorkersModels : null }
|
|
434
|
+
: {}),
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
routingTouched = true;
|
|
438
|
+
}
|
|
382
439
|
// pool widened to accept the null tombstone; MapEntry itself never carries null in memory.
|
|
383
440
|
const mapDelta = {};
|
|
384
441
|
for (const shape of new Set([...Object.keys(initial.map), ...Object.keys(edited.map)])) {
|
|
@@ -21,3 +21,10 @@ export type FleetWhyOptions = {
|
|
|
21
21
|
export declare function projectFleetWhy<Id extends string>(values: readonly FleetWhyValue<Id>[], options: FleetWhyOptions): FleetWhyRow<Id>[];
|
|
22
22
|
/** Plain line-mode twin of the Shapes rows; labels are projected, never reconstructed here. */
|
|
23
23
|
export declare function renderFleetWhy(rows: readonly FleetWhyRow[]): string;
|
|
24
|
+
/** LEG2-T3: one exclusion-collector scope as the reason a fleet row shows — its config path, then
|
|
25
|
+
* the entry that matched (a deny) or the fact the allowlist does not admit the channel. */
|
|
26
|
+
export declare function exclusionReason(scope: {
|
|
27
|
+
by: "deny" | "allow";
|
|
28
|
+
configPath: string;
|
|
29
|
+
entry: string;
|
|
30
|
+
}): string;
|
package/dist/config/fleet-why.js
CHANGED
|
@@ -40,3 +40,8 @@ export function projectFleetWhy(values, options) {
|
|
|
40
40
|
export function renderFleetWhy(rows) {
|
|
41
41
|
return ["tickmarkr fleet --why — effective shape routing", ...rows.map((row) => row.label)].join("\n");
|
|
42
42
|
}
|
|
43
|
+
/** LEG2-T3: one exclusion-collector scope as the reason a fleet row shows — its config path, then
|
|
44
|
+
* the entry that matched (a deny) or the fact the allowlist does not admit the channel. */
|
|
45
|
+
export function exclusionReason(scope) {
|
|
46
|
+
return scope.by === "allow" ? `${scope.configPath} (not admitted)` : `${scope.configPath} (${scope.entry})`;
|
|
47
|
+
}
|
package/dist/drivers/herdr.d.ts
CHANGED
|
@@ -128,6 +128,11 @@ export declare class HerdrDriver implements ExecutorDriver {
|
|
|
128
128
|
close(slot: Slot): Promise<void>;
|
|
129
129
|
private closeGrouped;
|
|
130
130
|
private watchPanes;
|
|
131
|
+
/** WB-1 (OBS-988): the daemon proved this run's own board lost. Forget the cached slot FIRST — even a
|
|
132
|
+
* retire that fails must never let the narrator answer with the ghost again — then take the pane
|
|
133
|
+
* back on ownership alone: the dead UI can never acknowledge, so the stop request is left for a
|
|
134
|
+
* merely stuck one to find. Serialized with the narrator so no split races the close. */
|
|
135
|
+
retireLostWatch(slot: Slot): Promise<void>;
|
|
131
136
|
/** Name collisions never confer repository ownership. Unknown boards stay protected. */
|
|
132
137
|
private retireWatch;
|
|
133
138
|
focus(target: FocusTarget): Promise<FocusResult>;
|