tickmarkr 1.90.8 → 1.92.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 +12 -9
- package/dist/adapters/catalog-remote.d.ts +11 -1
- package/dist/adapters/catalog-remote.js +169 -19
- package/dist/adapters/catalog.js +59 -0
- package/dist/adapters/model-lints.d.ts +37 -2
- package/dist/adapters/model-lints.js +180 -36
- package/dist/adapters/registry.js +32 -6
- package/dist/brand.d.ts +2 -0
- package/dist/brand.js +13 -0
- package/dist/cli/commands/doctor.d.ts +12 -0
- package/dist/cli/commands/doctor.js +90 -20
- package/dist/cli/commands/fleet-picker.js +1 -1
- package/dist/cli/commands/fleet.d.ts +4 -4
- package/dist/cli/commands/fleet.js +198 -37
- package/dist/cli/commands/init.js +6 -4
- package/dist/compile/native.js +5 -4
- package/dist/config/config.d.ts +34 -1
- package/dist/config/config.js +101 -5
- package/dist/config/fleet-overlay.d.ts +9 -2
- package/dist/config/fleet-overlay.js +159 -14
- package/dist/gates/acceptance.js +13 -4
- package/dist/route/preference.d.ts +1 -1
- package/dist/route/preference.js +12 -2
- package/dist/route/router.js +31 -2
- package/dist/run/daemon.js +1 -1
- package/dist/tui/ink/components.d.ts +16 -1
- package/dist/tui/ink/components.js +16 -2
- package/dist/tui/ink/fleet-app.d.ts +57 -8
- package/dist/tui/ink/fleet-app.js +1228 -727
- package/dist/tui/ink/frame.d.ts +62 -0
- package/dist/tui/ink/frame.js +204 -0
- package/dist/tui/ink/init-app.d.ts +2 -1
- package/dist/tui/ink/init-app.js +28 -100
- package/package.json +1 -1
package/dist/compile/native.js
CHANGED
|
@@ -697,10 +697,11 @@ acceptance is required on every task (a nested list of observable outcomes).
|
|
|
697
697
|
WHAT MAKES A CRITERION REAL:
|
|
698
698
|
- "test:" must name a real test asserting on recorded state, journal lines, or drawn frames, and its
|
|
699
699
|
title must match the criterion string verbatim. It also needs a collectable test path in files[].
|
|
700
|
-
- A "test:" oracle must
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
tests and the gate parks even though the suite is green.
|
|
700
|
+
- A "test:" oracle's criterion must be the test's OWN title — the leaf — verbatim. Nesting under
|
|
701
|
+
describe() is allowed (OBS-511: the gate matches the criterion as the trailing segment of the
|
|
702
|
+
runner-visible full name), but the leaf title itself must equal the criterion: a shortened,
|
|
703
|
+
decorated, or paraphrased leaf selects ZERO tests and the gate parks even though the suite is green.
|
|
704
|
+
Measured before the widening: two parks across two runs, one full attempt lost in each.
|
|
704
705
|
- NO criterion may be satisfiable by an absence, a rename, a source-text grep, or an empty collection.
|
|
705
706
|
"no file references X" is not a criterion — it passes in a repo where the feature was never built.
|
|
706
707
|
- "goal:" is NEVER verification. Prose in the goal enforces nothing: every obligation needs an
|
package/dist/config/config.d.ts
CHANGED
|
@@ -16,11 +16,26 @@ declare const ModeEnum: z.ZodEnum<{
|
|
|
16
16
|
}>;
|
|
17
17
|
export type RoutingMode = z.infer<typeof ModeEnum>;
|
|
18
18
|
export declare const INTEGRITY_FLOOR_SHAPES: readonly ["plan", "spec", "migration", "ui"];
|
|
19
|
+
export declare const PoolSchema: z.ZodObject<{
|
|
20
|
+
mode: z.ZodEnum<{
|
|
21
|
+
any: "any";
|
|
22
|
+
ordered: "ordered";
|
|
23
|
+
}>;
|
|
24
|
+
channels: z.ZodArray<z.ZodString>;
|
|
25
|
+
}, z.core.$strip>;
|
|
26
|
+
export type PoolEntry = z.infer<typeof PoolSchema>;
|
|
19
27
|
export declare const MapEntrySchema: z.ZodObject<{
|
|
20
28
|
pin: z.ZodOptional<z.ZodObject<{
|
|
21
29
|
via: z.ZodString;
|
|
22
30
|
model: z.ZodString;
|
|
23
31
|
}, z.core.$strip>>;
|
|
32
|
+
pool: z.ZodOptional<z.ZodObject<{
|
|
33
|
+
mode: z.ZodEnum<{
|
|
34
|
+
any: "any";
|
|
35
|
+
ordered: "ordered";
|
|
36
|
+
}>;
|
|
37
|
+
channels: z.ZodArray<z.ZodString>;
|
|
38
|
+
}, z.core.$strip>>;
|
|
24
39
|
tier: z.ZodOptional<z.ZodPipe<z.ZodOptional<z.ZodNull>, z.ZodTransform<undefined, null | undefined>>>;
|
|
25
40
|
prefer: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
26
41
|
escalate: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -139,6 +154,13 @@ export declare const TickmarkrConfigSchema: z.ZodObject<{
|
|
|
139
154
|
via: z.ZodString;
|
|
140
155
|
model: z.ZodString;
|
|
141
156
|
}, z.core.$strip>>;
|
|
157
|
+
pool: z.ZodOptional<z.ZodObject<{
|
|
158
|
+
mode: z.ZodEnum<{
|
|
159
|
+
any: "any";
|
|
160
|
+
ordered: "ordered";
|
|
161
|
+
}>;
|
|
162
|
+
channels: z.ZodArray<z.ZodString>;
|
|
163
|
+
}, z.core.$strip>>;
|
|
142
164
|
tier: z.ZodOptional<z.ZodPipe<z.ZodOptional<z.ZodNull>, z.ZodTransform<undefined, null | undefined>>>;
|
|
143
165
|
prefer: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
144
166
|
escalate: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -331,7 +353,18 @@ export type FleetEditable = {
|
|
|
331
353
|
};
|
|
332
354
|
export declare function repoOverlayPath(repoRoot: string): string;
|
|
333
355
|
export declare function readOverlayFile(path: string): Record<string, unknown>;
|
|
334
|
-
|
|
356
|
+
/** Grammar A1 membership test: does any discovered universe row cover this deny/allow entry
|
|
357
|
+
* (adapter id, bare model id, or adapter:model)? Uncovered entries name channels the probe
|
|
358
|
+
* did not serve this session (failed auth, rate limit, retired sku) — they are NOT editable
|
|
359
|
+
* membership state and must survive a membership write verbatim, never be re-derived away. */
|
|
360
|
+
export declare function universeCovers(universe: {
|
|
361
|
+
adapter: string;
|
|
362
|
+
models: string[];
|
|
363
|
+
}[], entry: string): boolean;
|
|
364
|
+
export declare function fleetEditableFromConfig(cfg: TickmarkrConfig, universe?: {
|
|
365
|
+
adapter: string;
|
|
366
|
+
models: string[];
|
|
367
|
+
}[]): FleetEditable;
|
|
335
368
|
/** Resolve which layer last set a dotted fleet path (defaults < global < repo). */
|
|
336
369
|
export declare function fleetKeyLayer(repoRoot: string, dotted: string, opts?: {
|
|
337
370
|
globalDir?: string;
|
package/dist/config/config.js
CHANGED
|
@@ -24,6 +24,13 @@ const ModeEnum = z.enum(ROUTING_MODES, {
|
|
|
24
24
|
// Integrity set (weak-oracle shapes, OBS-68..70 class): no mode may resolve these below frontier.
|
|
25
25
|
// Only an explicit operator floor line can — and it draws a standing plan lint every load.
|
|
26
26
|
export const INTEGRITY_FLOOR_SHAPES = ["plan", "spec", "migration", "ui"];
|
|
27
|
+
// v1.92: a pool is a shape's OWN closed candidate set — mode "any" lets the economy engine pick
|
|
28
|
+
// inside it (cost, then tier); mode "ordered" walks the declaration order, first live wins. An
|
|
29
|
+
// exhausted pool fails loud at plan (pin precedent, fail-closed).
|
|
30
|
+
export const PoolSchema = z.object({
|
|
31
|
+
mode: z.enum(["any", "ordered"]),
|
|
32
|
+
channels: z.array(z.string().regex(/^[^:\s]+:\S.*$/, "pool channels are adapter:model")).min(1),
|
|
33
|
+
});
|
|
27
34
|
// v1.52 T5: tier is no longer a map-entry band authority — routing.floors is the only tier
|
|
28
35
|
// source left (v1.51 T3 deprecated this field with a promise to remove it here). Only the legacy
|
|
29
36
|
// `tier: null` tombstone still parses (and normalizes to absent); any real value fails config
|
|
@@ -32,6 +39,7 @@ export const INTEGRITY_FLOOR_SHAPES = ["plan", "spec", "migration", "ui"];
|
|
|
32
39
|
// outright) so a map entry can never carry a real tier value again, structurally.
|
|
33
40
|
export const MapEntrySchema = z.object({
|
|
34
41
|
pin: z.object({ via: z.string(), model: z.string() }).optional(),
|
|
42
|
+
pool: PoolSchema.optional(),
|
|
35
43
|
tier: z
|
|
36
44
|
.null({
|
|
37
45
|
error: (iss) => `routing.map entry tier ${JSON.stringify(iss.input)} is no longer a band authority — move this value to routing.floors.<shape> (only "tier: null" tombstones still parse)`,
|
|
@@ -272,7 +280,26 @@ export const TickmarkrConfigSchema = z.object({
|
|
|
272
280
|
routing: z.object({
|
|
273
281
|
// v1.51 T1: preset expanded into floors by loadConfig — absent ⇒ risk-based ⇒ byte-identical routing.
|
|
274
282
|
mode: ModeEnum.optional(),
|
|
275
|
-
|
|
283
|
+
// v1.92: pool exclusivity — the record-level superRefine (not MapEntrySchema's) because the
|
|
284
|
+
// error must name the shape. Fail-closed: both keys present fails config load.
|
|
285
|
+
map: z.record(z.string(), MapEntrySchema).superRefine((map, ctx) => {
|
|
286
|
+
for (const [shape, entry] of Object.entries(map)) {
|
|
287
|
+
if (!entry.pool)
|
|
288
|
+
continue;
|
|
289
|
+
if (entry.pin) {
|
|
290
|
+
ctx.addIssue({
|
|
291
|
+
code: "custom", path: [shape, "pool"],
|
|
292
|
+
message: `routing.map.${shape}: pin and pool are one declaration — a pin is pool {mode: ordered, channels:[one]}; keep one`,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
if (entry.prefer) {
|
|
296
|
+
ctx.addIssue({
|
|
297
|
+
code: "custom", path: [shape, "pool"],
|
|
298
|
+
message: `routing.map.${shape}: pool already decides the candidate set — prefer is the OPEN bias; keep one`,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}),
|
|
276
303
|
floors: z.record(z.string(), TierEnum),
|
|
277
304
|
learned: z.enum(["on", "off"]), // v1.6 ROUTE-09 kill switch; a typo (offf) fails loud via safeParse
|
|
278
305
|
// v1.9 ROUTE-15 — optional overrides for profile.ts HALF_LIFE_RUNS/AVAIL_WEIGHT; absent ⇒ byte-identical defaults.
|
|
@@ -529,6 +556,19 @@ function deepMerge(base, over, path = []) {
|
|
|
529
556
|
return over;
|
|
530
557
|
}
|
|
531
558
|
const out = { ...base };
|
|
559
|
+
// v1.92: pin/pool/prefer form ONE routing declaration slot per shape — a higher layer that
|
|
560
|
+
// declares any of them overrides the lower layer's whole slot atomically, so a seed prefer can
|
|
561
|
+
// never merge beside an overlay pool into a same-shape conflict the author never wrote (the
|
|
562
|
+
// map-entry exclusivity refine still rejects duplicates written in ONE document). Tombstones
|
|
563
|
+
// (explicit null) keep their precise per-key semantics via the loop below.
|
|
564
|
+
if (path.length === 3 && path[0] === "routing" && path[1] === "map") {
|
|
565
|
+
const declared = over;
|
|
566
|
+
if (declared.pin != null || declared.pool != null || declared.prefer != null) {
|
|
567
|
+
delete out.pin;
|
|
568
|
+
delete out.pool;
|
|
569
|
+
delete out.prefer;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
532
572
|
for (const [k, v] of Object.entries(over)) {
|
|
533
573
|
if (v === null) {
|
|
534
574
|
// T19: null is a declared multi-vendor state only at tiers.<adapter>.vendor. Every other null
|
|
@@ -686,13 +726,16 @@ export function configTemplate(overlay) {
|
|
|
686
726
|
# implement: { prefer: [cursor-agent, codex] }
|
|
687
727
|
# migration: { pin: { via: claude-code, model: fable } }
|
|
688
728
|
# tests: { tier: null } # tombstone: null removes a legacy map tier (a real tier value now fails config load — move the band to routing.floors.tests)
|
|
729
|
+
# implement: { pool: { mode: any, channels: [codex:gpt-5.6-luna, kimi:kimi-code/k3] } }
|
|
730
|
+
# # pool: the shape's OWN candidate set — any: economy engine picks inside it;
|
|
731
|
+
# # ordered: first live entry wins. Exhausted pool fails loud at plan (pin precedent).
|
|
689
732
|
# floors: # tier authority — advisory minimum bands; 'tickmarkr plan' lints violations
|
|
690
733
|
# migration: frontier
|
|
691
734
|
# 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'
|
|
692
735
|
# learnedTuning: { halfLifeRuns: 5, availWeight: 0.05 } # optional; defaults byte-identical
|
|
693
736
|
# explore: { mode: on, excludeShapes: [], excludeComplexityAtOrAbove: null, cap: 5 } # optional; absent ⇒ byte-identical
|
|
694
737
|
# sla: { implement: 15 } # optional per-shape minutes — advisory plan lint only; absent ⇒ no lint
|
|
695
|
-
# allow: { adapters: [claude-code, codex] } #
|
|
738
|
+
# allow: { adapters: [claude-code, codex] } # the FLEET: presence activates fail-closed; the fleet editor writes this form
|
|
696
739
|
# deny: # optional fleet denylist; deny beats allow on conflict
|
|
697
740
|
# models:
|
|
698
741
|
# - pi:zai/glm-5.2 # OBS-57: pi passes run-start probe but hangs at finish without TICKMARKR_RESULT — remove after no-trailer demotion ships (v1.46 provider-outage taxonomy)
|
|
@@ -705,6 +748,11 @@ export function configTemplate(overlay) {
|
|
|
705
748
|
# tiers: # model → capability band; extend when new models ship
|
|
706
749
|
# claude-code: { vendor: anthropic, channel: sub, models: { fable: frontier } }
|
|
707
750
|
# codex: { models: { gpt-5.2-codex: null } } # null = tombstone: removes a stale seed id
|
|
751
|
+
# tier evidence: \`tickmarkr doctor --refresh-catalog\` fetches the catalogs behind tier suggestions.
|
|
752
|
+
# models.dev and LiveBench need no key. The Artificial Analysis leg runs ONLY when the environment
|
|
753
|
+
# carries ARTIFICIAL_ANALYSIS_API_KEY (free key: artificialanalysis.ai) — unset, AA contributes
|
|
754
|
+
# nothing and suggestions fall back to LiveBench's agentic-coding rank, then price. Not a config
|
|
755
|
+
# key: an API credential belongs in the environment, never in a tracked overlay.
|
|
708
756
|
# visibility:
|
|
709
757
|
# llm: headless # headless (default): judge/review/consult run silently | pane: visible agents
|
|
710
758
|
# keepPanes: run # run (default): ephemeral judge/review/consult panes close when read; a merged task's worker pane closes on done; other worker panes persist until run end | attempt | forever (keep everything for debugging)
|
|
@@ -776,7 +824,18 @@ export function readOverlayFile(path) {
|
|
|
776
824
|
throw new ConfigError(`invalid overlay at ${path}`);
|
|
777
825
|
return raw;
|
|
778
826
|
}
|
|
779
|
-
|
|
827
|
+
/** Grammar A1 membership test: does any discovered universe row cover this deny/allow entry
|
|
828
|
+
* (adapter id, bare model id, or adapter:model)? Uncovered entries name channels the probe
|
|
829
|
+
* did not serve this session (failed auth, rate limit, retired sku) — they are NOT editable
|
|
830
|
+
* membership state and must survive a membership write verbatim, never be re-derived away. */
|
|
831
|
+
export function universeCovers(universe, entry) {
|
|
832
|
+
return universe.some((row) => entry === row.adapter || row.models.some((m) => entry === m || entry === `${row.adapter}:${m}`));
|
|
833
|
+
}
|
|
834
|
+
// v1.92 fleet membership: with a discovered `universe` (classified models only) the deny sets become
|
|
835
|
+
// the EFFECTIVE out-of-fleet complement — anything routing.allow does not admit (fail-closed) UNION
|
|
836
|
+
// routing.deny adapters/models scopes (never workers). Absent universe ⇒ deny arrays verbatim, so
|
|
837
|
+
// non-UI callers stay byte-identical.
|
|
838
|
+
export function fleetEditableFromConfig(cfg, universe) {
|
|
780
839
|
const tiers = {};
|
|
781
840
|
for (const [adapter, entry] of Object.entries(cfg.tiers)) {
|
|
782
841
|
tiers[adapter] = {};
|
|
@@ -784,9 +843,46 @@ export function fleetEditableFromConfig(cfg) {
|
|
|
784
843
|
tiers[adapter][model] = { tier };
|
|
785
844
|
}
|
|
786
845
|
}
|
|
846
|
+
let denyAdapters = [...(cfg.routing.deny?.adapters ?? [])].sort();
|
|
847
|
+
let denyModels = [...(cfg.routing.deny?.models ?? [])].sort();
|
|
848
|
+
if (universe !== undefined) {
|
|
849
|
+
const { allow, deny } = cfg.routing;
|
|
850
|
+
// Grammar A1 (disallowedBy precedent): entries match adapter id, bare model id, or adapter:model.
|
|
851
|
+
// Inlined rather than imported — route/preference.ts imports this module.
|
|
852
|
+
const denyEntries = [...(deny?.adapters ?? []), ...(deny?.models ?? [])];
|
|
853
|
+
const allowEntries = allow ? [...(allow.adapters ?? []), ...(allow.models ?? [])] : null;
|
|
854
|
+
const outOfFleet = (adapter, model) => {
|
|
855
|
+
const matches = (e) => e === adapter || e === model || e === `${adapter}:${model}`;
|
|
856
|
+
if (denyEntries.some(matches))
|
|
857
|
+
return true;
|
|
858
|
+
return allowEntries !== null && !allowEntries.some(matches);
|
|
859
|
+
};
|
|
860
|
+
const adaptersOut = [];
|
|
861
|
+
const modelsOut = [];
|
|
862
|
+
for (const row of universe) {
|
|
863
|
+
const out = row.models.filter((m) => outOfFleet(row.adapter, m));
|
|
864
|
+
if (row.models.length ? out.length === row.models.length : outOfFleet(row.adapter, "")) {
|
|
865
|
+
adaptersOut.push(row.adapter);
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
modelsOut.push(...out.map((m) => `${row.adapter}:${m}`));
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
// OBS-517: deny entries the probe universe does not cover (failed/rate-limited probes drop the
|
|
872
|
+
// channel from discoverChannels) are preserved VERBATIM — deriving membership purely from the
|
|
873
|
+
// universe silently un-denied them on the next write (fail-open on the operator's own ban list).
|
|
874
|
+
denyAdapters = [...new Set([
|
|
875
|
+
...adaptersOut,
|
|
876
|
+
...(deny?.adapters ?? []).filter((e) => !universeCovers(universe, e)),
|
|
877
|
+
])].sort();
|
|
878
|
+
denyModels = [...new Set([
|
|
879
|
+
...modelsOut,
|
|
880
|
+
...(deny?.models ?? []).filter((e) => !universeCovers(universe, e)),
|
|
881
|
+
])].sort();
|
|
882
|
+
}
|
|
787
883
|
return {
|
|
788
|
-
denyAdapters
|
|
789
|
-
denyModels
|
|
884
|
+
denyAdapters,
|
|
885
|
+
denyModels,
|
|
790
886
|
tiers,
|
|
791
887
|
map: structuredClone(cfg.routing.map),
|
|
792
888
|
floors: { ...cfg.routing.floors },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type FleetEditable, 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,6 +19,10 @@ export type FleetOverlayWrite = {
|
|
|
19
19
|
consult?: string[];
|
|
20
20
|
};
|
|
21
21
|
};
|
|
22
|
+
universe?: {
|
|
23
|
+
adapter: string;
|
|
24
|
+
models: string[];
|
|
25
|
+
}[];
|
|
22
26
|
};
|
|
23
27
|
export type FleetFirstTouch = {
|
|
24
28
|
vendor: string;
|
|
@@ -29,7 +33,10 @@ export declare function fleetFirstTouchProvenance(note: string, firstTouch: Flee
|
|
|
29
33
|
* keys, ordering, scalar style, and comments; fresh provenance is written directly on its tier node. */
|
|
30
34
|
export declare function renderFleetOverlayWrite(priorBytes: string, write: FleetOverlayWrite): string;
|
|
31
35
|
/** Build the repo overlay fragment fleet would write for edits since session start. */
|
|
32
|
-
export declare function fleetRepoOverlayFromDelta(initial: FleetEditable, edited: FleetEditable, existingRepo?: Record<string, unknown>, firstTouches?: Readonly<Record<string, FleetFirstTouch
|
|
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
40
|
export declare function repoOverlayYaml(overlay: Record<string, unknown>): string;
|
|
34
41
|
export declare function serializeFleetOverlay(overlay: Record<string, unknown>): string;
|
|
35
42
|
export declare function unifiedYamlDiff(before: string, after: string, label?: string): string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Fleet-overlay mutation, serialization, and diff rendering for the `tickmarkr fleet` write path.
|
|
2
|
-
import { isMap, isScalar, isSeq, parseDocument, stringify } from "yaml";
|
|
2
|
+
import { isMap, isScalar, isSeq, parseDocument, stringify, visit } from "yaml";
|
|
3
|
+
import { universeCovers } from "./config.js";
|
|
3
4
|
/** Fleet-owned overlay keys — the only config surface `tickmarkr fleet` may write. */
|
|
4
5
|
export const FLEET_OVERLAY_KEYS = ["routing", "tiers"];
|
|
5
6
|
function fleetSubset(obj) {
|
|
@@ -13,11 +14,49 @@ function fleetSubset(obj) {
|
|
|
13
14
|
function sortedUnique(xs) {
|
|
14
15
|
return [...new Set(xs)].sort();
|
|
15
16
|
}
|
|
17
|
+
// The minimal routing.allow form for an exclusion-set membership write: whole adapter ids for
|
|
18
|
+
// fully-in adapters, adapter:model keys for partially-in ones, nothing for fully-out ones.
|
|
19
|
+
// `excluded` false ⇔ the whole universe is in fleet ⇒ the allow block is removed, not written.
|
|
20
|
+
function allowFormFromExclusions(universe, edited) {
|
|
21
|
+
if (!universe.length) {
|
|
22
|
+
throw new Error("fleet write: universe is empty — no classified models to compute routing.allow from; classify models in `tickmarkr fleet` first");
|
|
23
|
+
}
|
|
24
|
+
const denyAdapters = new Set(edited.denyAdapters);
|
|
25
|
+
const denyModels = new Set(edited.denyModels);
|
|
26
|
+
const adapters = [];
|
|
27
|
+
const models = [];
|
|
28
|
+
let excluded = false;
|
|
29
|
+
for (const row of universe) {
|
|
30
|
+
if (denyAdapters.has(row.adapter)) {
|
|
31
|
+
excluded = true;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const inFleet = row.models.filter((m) => !denyModels.has(`${row.adapter}:${m}`));
|
|
35
|
+
if (inFleet.length === row.models.length) {
|
|
36
|
+
adapters.push(row.adapter);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
excluded = true;
|
|
40
|
+
models.push(...inFleet.map((m) => `${row.adapter}:${m}`));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { adapters: sortedUnique(adapters), models: sortedUnique(models), excluded };
|
|
44
|
+
}
|
|
45
|
+
// OBS-517: deny entries the discovered universe cannot express through the allow form — the probe
|
|
46
|
+
// dropped their channel (failed auth, rate limit, retired sku) so the allow complement never names
|
|
47
|
+
// them. They must be written back into routing.deny verbatim (deny beats allow at routing time),
|
|
48
|
+
// or a transient probe failure permanently erases a deliberate operator exclusion.
|
|
49
|
+
function residualDeny(universe, entries) {
|
|
50
|
+
return sortedUnique(entries.filter((entry) => !universeCovers(universe, entry)));
|
|
51
|
+
}
|
|
16
52
|
// fleet.ts deliberately remains the sole overlay builder and writer. Its established classification
|
|
17
53
|
// seam copies only `tier` and `note` into FleetEditable, so first-touch entry metadata rides inside a
|
|
18
54
|
// private provenance envelope until this module writes the YAML. The envelope never reaches disk.
|
|
19
55
|
const FIRST_TOUCH_OPEN = "\uE000tickmarkr-fleet-first-touch:";
|
|
20
56
|
const FIRST_TOUCH_CLOSE = "\uE001";
|
|
57
|
+
// OBS-505: marks a scalar-trailing single-line comment for the two-space inline note style at
|
|
58
|
+
// stringify time; applied and consumed inside renderFleetOverlayWrite, never written to disk.
|
|
59
|
+
const INLINE_COMMENT_SENTINEL = "\uE002";
|
|
21
60
|
export function fleetFirstTouchProvenance(note, firstTouch) {
|
|
22
61
|
return `${FIRST_TOUCH_OPEN}${encodeURIComponent(firstTouch.vendor)}:${firstTouch.channel}${FIRST_TOUCH_CLOSE}${note}`;
|
|
23
62
|
}
|
|
@@ -103,8 +142,47 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
103
142
|
const denyChanged = sortedUnique(initial.denyAdapters).join() !== sortedUnique(edited.denyAdapters).join()
|
|
104
143
|
|| sortedUnique(initial.denyModels).join() !== sortedUnique(edited.denyModels).join();
|
|
105
144
|
if (denyChanged) {
|
|
106
|
-
|
|
107
|
-
|
|
145
|
+
if (write.universe) {
|
|
146
|
+
// Membership write: the allow form IS the fleet; deny adapters/models scopes are tombstoned
|
|
147
|
+
// so a lower layer can never re-exclude behind the operator's back (workers untouched).
|
|
148
|
+
const form = allowFormFromExclusions(write.universe, edited);
|
|
149
|
+
if (form.excluded) {
|
|
150
|
+
const allowNode = doc.getIn(["routing", "allow"], true);
|
|
151
|
+
if (allowNode !== undefined && !isMap(allowNode))
|
|
152
|
+
doc.deleteIn(["routing", "allow"]);
|
|
153
|
+
// deleteIn throws on an empty document — only delete keys that exist.
|
|
154
|
+
if (form.adapters.length) {
|
|
155
|
+
setStringSequencePreservingComments(doc, ["routing", "allow", "adapters"], form.adapters);
|
|
156
|
+
}
|
|
157
|
+
else if (doc.getIn(["routing", "allow", "adapters"]) !== undefined) {
|
|
158
|
+
doc.deleteIn(["routing", "allow", "adapters"]);
|
|
159
|
+
}
|
|
160
|
+
if (form.models.length) {
|
|
161
|
+
setStringSequencePreservingComments(doc, ["routing", "allow", "models"], form.models);
|
|
162
|
+
}
|
|
163
|
+
else if (doc.getIn(["routing", "allow", "models"]) !== undefined) {
|
|
164
|
+
doc.deleteIn(["routing", "allow", "models"]);
|
|
165
|
+
}
|
|
166
|
+
// Whole fleet out: allow stays present but empty — fail-closed, nothing admitted.
|
|
167
|
+
if (doc.getIn(["routing", "allow"]) === undefined) {
|
|
168
|
+
doc.setIn(["routing", "allow"], doc.createNode({}));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else if (doc.getIn(["routing", "allow"]) !== undefined) {
|
|
172
|
+
// Whole fleet in: no restriction to express — the allow block goes away entirely.
|
|
173
|
+
doc.deleteIn(["routing", "allow"]);
|
|
174
|
+
}
|
|
175
|
+
// Residuals stay in deny; covered scopes are tombstoned so a lower layer can never
|
|
176
|
+
// re-exclude behind the operator's back (workers untouched).
|
|
177
|
+
const residualAdapters = residualDeny(write.universe, edited.denyAdapters);
|
|
178
|
+
const residualModels = residualDeny(write.universe, edited.denyModels);
|
|
179
|
+
setStringSequencePreservingComments(doc, ["routing", "deny", "adapters"], residualAdapters.length ? residualAdapters : null);
|
|
180
|
+
setStringSequencePreservingComments(doc, ["routing", "deny", "models"], residualModels.length ? residualModels : null);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
setStringSequencePreservingComments(doc, ["routing", "deny", "adapters"], edited.denyAdapters.length ? sortedUnique(edited.denyAdapters) : null);
|
|
184
|
+
setStringSequencePreservingComments(doc, ["routing", "deny", "models"], edited.denyModels.length ? sortedUnique(edited.denyModels) : null);
|
|
185
|
+
}
|
|
108
186
|
}
|
|
109
187
|
for (const shape of new Set([...Object.keys(initial.map), ...Object.keys(edited.map)])) {
|
|
110
188
|
const before = initial.map[shape];
|
|
@@ -115,9 +193,33 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
115
193
|
else
|
|
116
194
|
doc.setIn(["routing", "map", shape, "pin"], doc.createNode(after.pin));
|
|
117
195
|
}
|
|
196
|
+
if (JSON.stringify(before?.pool) !== JSON.stringify(after?.pool)) {
|
|
197
|
+
const path = ["routing", "map", shape, "pool"];
|
|
198
|
+
if (after?.pool === undefined) {
|
|
199
|
+
// pool: null tombstone — masks a lower-layer pool instead of inheriting it again.
|
|
200
|
+
setStringSequencePreservingComments(doc, path, null);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
const existing = doc.getIn(path, true);
|
|
204
|
+
if (existing !== undefined && !isMap(existing))
|
|
205
|
+
doc.deleteIn(path);
|
|
206
|
+
setScalarPreservingComment(doc, [...path, "mode"], after.pool.mode);
|
|
207
|
+
// Channel order is semantic (ordered walks it; any breaks ties by it) — dedupe, never sort.
|
|
208
|
+
setStringSequencePreservingComments(doc, [...path, "channels"], [...new Set(after.pool.channels)]);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
118
211
|
if (JSON.stringify(before?.prefer) !== JSON.stringify(after?.prefer)) {
|
|
119
|
-
|
|
120
|
-
|
|
212
|
+
if (after?.prefer === undefined && (after?.pool !== undefined || after?.pin !== undefined)) {
|
|
213
|
+
// the pin/pool declaration owns the whole slot at merge (deepMerge drops the lower
|
|
214
|
+
// layer's pin/pool/prefer atomically) — an [] mask here would re-declare prefer BESIDE
|
|
215
|
+
// the pool in one document and fail the exclusivity refine; delete the key instead
|
|
216
|
+
doc.deleteIn(["routing", "map", shape, "prefer"]);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
// Clearing a resolved list with no replacing declaration must mask the lower layer
|
|
220
|
+
// with [], not delete the key and inherit it again.
|
|
221
|
+
setStringSequencePreservingComments(doc, ["routing", "map", shape, "prefer"], after?.prefer ?? []);
|
|
222
|
+
}
|
|
121
223
|
}
|
|
122
224
|
if (before?.escalate !== after?.escalate) {
|
|
123
225
|
if (after?.escalate === undefined)
|
|
@@ -189,14 +291,31 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
189
291
|
}
|
|
190
292
|
}
|
|
191
293
|
}
|
|
192
|
-
//
|
|
193
|
-
//
|
|
294
|
+
// OBS-505: fleet's two-space note style (`tier: mid # note`) applies to INLINE comments only.
|
|
295
|
+
// The previous commentString prefixed " #" onto EVERY comment line, so block comments — the
|
|
296
|
+
// whole init scaffold at column 0, and indented operator essays — each gained a stray leading
|
|
297
|
+
// space, turning a one-key write into a whole-file diff on the one confirmation surface an
|
|
298
|
+
// operator reviews. commentString has no position context, but this writer owns the document:
|
|
299
|
+
// scalar-trailing single-line comments (the only inline form fleet emits) are marked with a
|
|
300
|
+
// private-use sentinel (the FIRST_TOUCH envelope precedent above), everything else renders
|
|
301
|
+
// byte-identical to yaml's own stringifyComment.
|
|
302
|
+
visit(doc, (_key, node) => {
|
|
303
|
+
if (isScalar(node) && typeof node.comment === "string" && !node.comment.includes("\n")) {
|
|
304
|
+
node.comment = `${INLINE_COMMENT_SENTINEL}${node.comment}`;
|
|
305
|
+
}
|
|
306
|
+
});
|
|
194
307
|
return doc.toString({
|
|
195
|
-
commentString: (comment) => comment.
|
|
308
|
+
commentString: (comment) => comment.startsWith(INLINE_COMMENT_SENTINEL)
|
|
309
|
+
? ` #${comment.slice(1)}`
|
|
310
|
+
: comment.replace(/^(?!$)(?: $)?/gm, "#"),
|
|
311
|
+
// OBS-518: yaml's default pads flow collections (`[kimi]` → `[ kimi ]`), churning untouched
|
|
312
|
+
// lines on the one confirmation surface an operator reviews. Hand-written overlays use the
|
|
313
|
+
// unpadded form; emit it.
|
|
314
|
+
flowCollectionPadding: false,
|
|
196
315
|
});
|
|
197
316
|
}
|
|
198
317
|
/** Build the repo overlay fragment fleet would write for edits since session start. */
|
|
199
|
-
export function fleetRepoOverlayFromDelta(initial, edited, existingRepo = {}, firstTouches = {}) {
|
|
318
|
+
export function fleetRepoOverlayFromDelta(initial, edited, existingRepo = {}, firstTouches = {}, universe) {
|
|
200
319
|
if (fleetEditableEquals(initial, edited))
|
|
201
320
|
return {};
|
|
202
321
|
const out = structuredClone(existingRepo);
|
|
@@ -205,19 +324,45 @@ export function fleetRepoOverlayFromDelta(initial, edited, existingRepo = {}, fi
|
|
|
205
324
|
const denyChanged = sortedUnique(initial.denyAdapters).join() !== sortedUnique(edited.denyAdapters).join()
|
|
206
325
|
|| sortedUnique(initial.denyModels).join() !== sortedUnique(edited.denyModels).join();
|
|
207
326
|
if (denyChanged) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
327
|
+
if (universe) {
|
|
328
|
+
const form = allowFormFromExclusions(universe, edited);
|
|
329
|
+
if (form.excluded) {
|
|
330
|
+
routing.allow = {
|
|
331
|
+
...(form.adapters.length ? { adapters: form.adapters } : {}),
|
|
332
|
+
...(form.models.length ? { models: form.models } : {}),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
else
|
|
336
|
+
delete routing.allow;
|
|
337
|
+
const residualAdapters = residualDeny(universe, edited.denyAdapters);
|
|
338
|
+
const residualModels = residualDeny(universe, edited.denyModels);
|
|
339
|
+
routing.deny = {
|
|
340
|
+
...routing.deny,
|
|
341
|
+
adapters: residualAdapters.length ? residualAdapters : null,
|
|
342
|
+
models: residualModels.length ? residualModels : null,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
routing.deny = {
|
|
347
|
+
adapters: edited.denyAdapters.length ? edited.denyAdapters : null,
|
|
348
|
+
models: edited.denyModels.length ? edited.denyModels : null,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
212
351
|
routingTouched = true;
|
|
213
352
|
}
|
|
353
|
+
// pool widened to accept the null tombstone; MapEntry itself never carries null in memory.
|
|
214
354
|
const mapDelta = {};
|
|
215
355
|
for (const shape of new Set([...Object.keys(initial.map), ...Object.keys(edited.map)])) {
|
|
216
356
|
if (JSON.stringify(initial.map[shape]) !== JSON.stringify(edited.map[shape])) {
|
|
217
357
|
const next = { ...edited.map[shape] };
|
|
218
|
-
if (initial.map[shape]?.prefer !== undefined && edited.map[shape]?.prefer === undefined
|
|
358
|
+
if (initial.map[shape]?.prefer !== undefined && edited.map[shape]?.prefer === undefined
|
|
359
|
+
&& edited.map[shape]?.pool === undefined && edited.map[shape]?.pin === undefined) {
|
|
219
360
|
next.prefer = [];
|
|
220
361
|
}
|
|
362
|
+
// pool: null tombstone — a removed pool must mask the lower layer, never inherit it again.
|
|
363
|
+
if (initial.map[shape]?.pool !== undefined && edited.map[shape]?.pool === undefined) {
|
|
364
|
+
next.pool = null;
|
|
365
|
+
}
|
|
221
366
|
mapDelta[shape] = next;
|
|
222
367
|
}
|
|
223
368
|
}
|
package/dist/gates/acceptance.js
CHANGED
|
@@ -75,10 +75,17 @@ function escapeRegExp(s) {
|
|
|
75
75
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
76
76
|
}
|
|
77
77
|
// v1.89 T6: Vitest applies -t to the runner-visible full name (enclosing describe titles and the test
|
|
78
|
-
// title, space-joined). Escaping preserves literal criteria;
|
|
79
|
-
// substring containment, the named-test contract.
|
|
78
|
+
// title, space-joined). Escaping preserves literal criteria; the leaf anchor makes trailing-title
|
|
79
|
+
// equality, rather than substring containment, the named-test contract.
|
|
80
|
+
// OBS-511 (runs 1013+1027, one full attempt lost in EACH): two independent workers nested the verbatim
|
|
81
|
+
// criterion under a describe, the old `^…$` full-name equality matched zero tests, and the gate parked a
|
|
82
|
+
// green suite. `(^| )` accepts any describe prefix while keeping the criterion as the complete trailing
|
|
83
|
+
// segment — equality still matches (empty prefix), and a title merely CONTAINING the criterion still
|
|
84
|
+
// selects zero. ponytail: a leaf title that itself ENDS with " <criterion>" also matches — vitest -t sees
|
|
85
|
+
// only the space-joined full name, so true leaf-boundary precision needs the runner's --list JSON
|
|
86
|
+
// (auditAcceptanceCorpus has it at doctor time); upgrade there if this window is ever exploited.
|
|
80
87
|
export function testFilterPattern(name) {
|
|
81
|
-
return
|
|
88
|
+
return `(^| )${escapeRegExp(name)}$`;
|
|
82
89
|
}
|
|
83
90
|
// OBS-55: when the base command already contains `--`, append -t after forwarded args — a second `--`
|
|
84
91
|
// makes vitest treat -t as a positional file filter and the name filter is dropped.
|
|
@@ -126,8 +133,10 @@ export function auditAcceptanceCorpus(corpusRoot, listedTests) {
|
|
|
126
133
|
? {
|
|
127
134
|
namedTest: {
|
|
128
135
|
criterion: item.test,
|
|
136
|
+
// OBS-511: mirror the gate's leaf-anchored suffix rule — the audit's denominator
|
|
137
|
+
// must count exactly the tests the -t filter would select, or doctor and gate disagree.
|
|
129
138
|
matches: runnerNames
|
|
130
|
-
.filter(({ fullName }) => fullName === item.test)
|
|
139
|
+
.filter(({ fullName }) => fullName === item.test || fullName.endsWith(` ${item.test}`))
|
|
131
140
|
.map(({ listed }) => listed),
|
|
132
141
|
},
|
|
133
142
|
}
|
|
@@ -27,7 +27,7 @@ export declare function disallowedBy(c: {
|
|
|
27
27
|
model: string;
|
|
28
28
|
}, routing: TickmarkrConfig["routing"], role?: PreferenceRole): Disallowed | null;
|
|
29
29
|
export interface DenyPreferCollision {
|
|
30
|
-
kind: "prefer" | "pin";
|
|
30
|
+
kind: "prefer" | "pin" | "pool";
|
|
31
31
|
shape: string;
|
|
32
32
|
detail: string;
|
|
33
33
|
disallowed: Disallowed;
|
package/dist/route/preference.js
CHANGED
|
@@ -113,11 +113,21 @@ export function denyPreferCollisions(cfg, shapes) {
|
|
|
113
113
|
disallowed: preferEntryDenied(prefer[0], cfg),
|
|
114
114
|
});
|
|
115
115
|
}
|
|
116
|
+
const pool = entry.pool;
|
|
117
|
+
if (pool) {
|
|
118
|
+
const denied = pool.channels.map((p) => {
|
|
119
|
+
const i = p.indexOf(":");
|
|
120
|
+
return disallowedBy({ adapter: p.slice(0, i), model: p.slice(i + 1) }, cfg.routing);
|
|
121
|
+
});
|
|
122
|
+
// only a FULLY dead pool collides — a partial deny still leaves live members to route
|
|
123
|
+
if (denied.every((d) => d !== null)) {
|
|
124
|
+
out.push({ kind: "pool", shape, detail: pool.channels.join(" > "), disallowed: denied[0] });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
116
127
|
}
|
|
117
128
|
return out;
|
|
118
129
|
}
|
|
119
130
|
export function denyPreferCollisionLine({ kind, shape, detail, disallowed }) {
|
|
120
|
-
const surface = kind === "pin" ? `routing.map.${shape}.pin` : `routing.map.${shape}.prefer`;
|
|
121
131
|
const verb = kind === "pin" ? "is disallowed" : "fully disallowed";
|
|
122
|
-
return `deny∩prefer:
|
|
132
|
+
return `deny∩prefer: routing.map.${shape}.${kind} ${detail} ${verb} by routing.${disallowed.by} (${disallowed.entry}) — remove the ${disallowed.by} entry or adjust ${kind}`;
|
|
123
133
|
}
|
package/dist/route/router.js
CHANGED
|
@@ -160,9 +160,9 @@ export function route(task, cfg, channels, profile, preferCtx, exclude, exploreC
|
|
|
160
160
|
const mapPinFloor = taskFloor && (!advisoryFloor || TIER_RANK[taskFloor] >= TIER_RANK[advisoryFloor])
|
|
161
161
|
? { tier: taskFloor, source: "task" }
|
|
162
162
|
: advisoryFloor ? { tier: advisoryFloor, source: "config" } : undefined;
|
|
163
|
-
const lintMapPinFloor = (tier) => {
|
|
163
|
+
const lintMapPinFloor = (tier, what = "pin") => {
|
|
164
164
|
if (mapPinFloor && TIER_RANK[tier] < TIER_RANK[mapPinFloor.tier]) {
|
|
165
|
-
lints.push(`${task.id} (${task.shape}): map
|
|
165
|
+
lints.push(`${task.id} (${task.shape}): map ${what} routes ${tier}, below ${mapPinFloor.source} floor ${mapPinFloor.tier} — map ${what}s are supreme`);
|
|
166
166
|
}
|
|
167
167
|
};
|
|
168
168
|
const source = task.routingHints?.source;
|
|
@@ -190,6 +190,35 @@ export function route(task, cfg, channels, profile, preferCtx, exclude, exploreC
|
|
|
190
190
|
maybeSlaLint(lints, task, profile, slaMinutes, c);
|
|
191
191
|
return { assignment: toAssignment(c), ladder: ladderFor(task, entry), lints, provenance: `${degraded}pin ${entry.pin.via}:${entry.pin.model} (config routing.map)` };
|
|
192
192
|
}
|
|
193
|
+
// map pool: operator-authored closed candidate set (v1.92) — sits between the pin (a
|
|
194
|
+
// single-channel pool) and the open floor/auto path. Exhaustion stays fail-loud (pin precedent).
|
|
195
|
+
if (entry?.pool) {
|
|
196
|
+
const pool = entry.pool;
|
|
197
|
+
if (prefActive) {
|
|
198
|
+
for (const p of pool.channels) {
|
|
199
|
+
const i = p.indexOf(":");
|
|
200
|
+
const d = disallowedBy({ adapter: p.slice(0, i), model: p.slice(i + 1) }, cfg.routing);
|
|
201
|
+
if (d) {
|
|
202
|
+
throw new RoutingError(`${task.id}: pool entry ${p} is disallowed by routing.${d.by} (${d.entry}) — remove the ${d.by} entry or drop it from the pool`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// live preserves pool declaration order — ordered mode and any-mode stable-sort ties depend on it
|
|
207
|
+
const live = pool.channels
|
|
208
|
+
.map((id) => channels.find((c) => channelKey(c) === id))
|
|
209
|
+
.filter((c) => c !== undefined);
|
|
210
|
+
if (!live.length) {
|
|
211
|
+
throw new RoutingError(`${task.id}: routing.map.${task.shape}.pool has no live channel — declared: ${pool.channels.join(", ")}; doctor found: ${channels.map(channelKey).join(", ") || "(nothing)"}`);
|
|
212
|
+
}
|
|
213
|
+
// ponytail: no learned/explore/spread inside pools in v1 — a pool pick is fully static; the
|
|
214
|
+
// upgrade path is feeding the pool-filtered set through the learned path below.
|
|
215
|
+
const chosen = pool.mode === "ordered"
|
|
216
|
+
? live[0]
|
|
217
|
+
: [...live].sort((a, b) => marginalCostRank(a) - marginalCostRank(b) || TIER_RANK[a.tier] - TIER_RANK[b.tier])[0];
|
|
218
|
+
lintMapPinFloor(chosen.tier, "pool");
|
|
219
|
+
maybeSlaLint(lints, task, profile, slaMinutes, chosen);
|
|
220
|
+
return { assignment: toAssignment(chosen), ladder: ladderFor(task, entry), lints, provenance: `${degraded}pool ${pool.mode} ${channelKey(chosen)} (config routing.map)` };
|
|
221
|
+
}
|
|
193
222
|
const baseTier = floor ?? "cheap";
|
|
194
223
|
// D-04: a task floor is hard only on floor/auto paths; the map-pin branch above stays supreme.
|
|
195
224
|
const minTier = taskFloor && TIER_RANK[taskFloor] > TIER_RANK[baseTier] ? taskFloor : baseTier;
|
package/dist/run/daemon.js
CHANGED
|
@@ -1944,7 +1944,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1944
1944
|
const promptFile = writePrompt(journal.dir, t, attempt, feedback, nonce);
|
|
1945
1945
|
// OBS-56: state the non-interactive, one-pass finish contract and the OBS-54 stall budget in every
|
|
1946
1946
|
// worker prompt, not only consult retry guidance. Prepended so prompt.ts's completion trailer stays last.
|
|
1947
|
-
const workerContract = `## Harness contract\n- This harness is non-interactive: make one continuous pass; do not stop for questions or follow-up input.\n- You have a ${taskTimeoutMinutes} minute stall window. Budget the full suite once, then commit and emit the completion trailer before it expires.\n- Each test: acceptance criterion must exist as a vitest test whose title
|
|
1947
|
+
const workerContract = `## Harness contract\n- This harness is non-interactive: make one continuous pass; do not stop for questions or follow-up input.\n- You have a ${taskTimeoutMinutes} minute stall window. Budget the full suite once, then commit and emit the completion trailer before it expires.\n- Each test: acceptance criterion must exist as a vitest test whose OWN title (the leaf, not counting enclosing describe titles) is the criterion string verbatim — never shortened, never decorated. Nesting under describe() is allowed.`; // OBS-64; OBS-511: leaf-title rule stated where the worker reads it
|
|
1948
1948
|
// OBS-47: state the worktree layout contract in the worker prompt (cheap-tier workers were
|
|
1949
1949
|
// committing/deleting node_modules and tripping the scope gate). The harness re-asserts the link
|
|
1950
1950
|
// itself before gates regardless of what the worker does with it.
|