tickmarkr 2.6.1 → 2.6.3
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 +16 -3
- package/dist/adapters/catalog-remote.js +89 -47
- package/dist/adapters/claude-code.js +9 -6
- package/dist/adapters/codex.js +7 -4
- package/dist/adapters/prompt.d.ts +1 -0
- package/dist/adapters/prompt.js +14 -6
- package/dist/adapters/registry.js +3 -3
- package/dist/adapters/types.d.ts +12 -4
- package/dist/adapters/types.js +6 -0
- package/dist/cli/commands/approve.d.ts +11 -4
- package/dist/cli/commands/approve.js +82 -27
- package/dist/cli/commands/compile.js +13 -3
- package/dist/cli/commands/doctor.d.ts +8 -2
- package/dist/cli/commands/doctor.js +11 -3
- package/dist/cli/commands/fleet.js +87 -11
- package/dist/cli/commands/plan.js +13 -8
- package/dist/cli/commands/report.d.ts +2 -1
- package/dist/cli/commands/report.js +74 -8
- package/dist/cli/commands/resume.js +4 -2
- package/dist/cli/commands/status.js +43 -20
- package/dist/cli/help.d.ts +2 -0
- package/dist/cli/help.js +9 -2
- package/dist/compile/native.js +7 -0
- package/dist/config/config.d.ts +35 -2
- package/dist/config/config.js +86 -10
- package/dist/config/fleet-overlay.d.ts +13 -2
- package/dist/config/fleet-overlay.js +60 -0
- package/dist/drivers/herdr.d.ts +12 -0
- package/dist/drivers/herdr.js +51 -0
- package/dist/drivers/orca.d.ts +35 -2
- package/dist/drivers/orca.js +222 -67
- package/dist/drivers/types.d.ts +2 -0
- package/dist/drivers/types.js +2 -2
- package/dist/eval/canary.d.ts +2 -1
- package/dist/eval/canary.js +2 -2
- package/dist/eval/dispatch.js +1 -0
- package/dist/gates/acceptance.d.ts +9 -1
- package/dist/gates/acceptance.js +31 -4
- package/dist/gates/baseline.d.ts +32 -2
- package/dist/gates/baseline.js +111 -24
- package/dist/gates/cache.d.ts +8 -0
- package/dist/gates/cache.js +12 -2
- package/dist/gates/llm.d.ts +11 -4
- package/dist/gates/llm.js +40 -21
- package/dist/gates/review.d.ts +14 -1
- package/dist/gates/review.js +160 -34
- package/dist/gates/run-gates.d.ts +56 -4
- package/dist/gates/run-gates.js +358 -58
- package/dist/gates/test-manifest.d.ts +45 -1
- package/dist/gates/test-manifest.js +78 -12
- package/dist/graph/schema.d.ts +2 -0
- package/dist/graph/schema.js +2 -0
- package/dist/plan/scope.js +2 -2
- package/dist/route/preference.d.ts +20 -2
- package/dist/route/preference.js +48 -13
- package/dist/route/router.d.ts +12 -1
- package/dist/route/router.js +56 -24
- package/dist/run/consult.d.ts +15 -1
- package/dist/run/consult.js +18 -7
- package/dist/run/daemon.d.ts +38 -2
- package/dist/run/daemon.js +895 -192
- package/dist/run/git.d.ts +8 -0
- package/dist/run/git.js +14 -0
- package/dist/run/interactive-seed.d.ts +4 -0
- package/dist/run/interactive-seed.js +35 -9
- package/dist/run/journal.d.ts +152 -3
- package/dist/run/journal.js +551 -50
- package/dist/run/lease.d.ts +13 -0
- package/dist/run/lease.js +45 -0
- package/dist/run/merge.d.ts +3 -1
- package/dist/run/merge.js +3 -2
- package/dist/run/operator-summary.d.ts +3 -0
- package/dist/run/operator-summary.js +3 -1
- package/dist/run/protocol.d.ts +46 -1
- package/dist/run/protocol.js +14 -2
- package/dist/run/receipt-resolver.d.ts +22 -0
- package/dist/run/receipt-resolver.js +40 -1
- package/dist/run/repair-selection.d.ts +11 -1
- package/dist/run/repair-selection.js +17 -9
- package/dist/run/supervision.d.ts +7 -1
- package/dist/run/supervision.js +5 -2
- package/dist/run/wall-budget.d.ts +48 -0
- package/dist/run/wall-budget.js +280 -0
- package/dist/tui/cockpit/board.js +3 -3
- package/dist/tui/cockpit/decision-actions.d.ts +8 -5
- package/dist/tui/cockpit/decision-actions.js +55 -32
- package/dist/tui/cockpit/derive.js +13 -2
- package/dist/tui/cockpit/live-runtime.d.ts +10 -0
- package/dist/tui/cockpit/live-runtime.js +50 -3
- package/dist/tui/cockpit/run-cockpit.d.ts +3 -0
- package/dist/tui/cockpit/run-cockpit.js +27 -2
- package/dist/tui/cockpit/run-view.d.ts +9 -2
- package/dist/tui/cockpit/run-view.js +66 -9
- package/dist/tui/cockpit/setup-cockpit.d.ts +6 -0
- package/dist/tui/cockpit/setup-cockpit.js +10 -3
- package/dist/tui/ink/fleet-app.d.ts +15 -3
- package/dist/tui/ink/fleet-app.js +91 -22
- package/package.json +3 -1
- package/schema/config.schema.json +825 -0
- package/skills/tickmarkr-loop/SKILL.md +15 -3
- package/skills/tickmarkr-overseer/SKILL.md +42 -0
- package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +91 -0
- package/skills/tickmarkr-overseer/scripts/context-statusline.sh +81 -0
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +36 -34
- package/skills/tickmarkr-overseer/scripts/watch-journal.sh +6 -4
package/dist/config/config.js
CHANGED
|
@@ -2,11 +2,11 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { filesGlob } from "../graph/files-glob.js";
|
|
5
|
-
import { Document, parse } from "yaml";
|
|
5
|
+
import { Document, isScalar, parse } from "yaml";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { CITED_MODEL_WINDOWS } from "../adapters/model-windows.js";
|
|
8
8
|
import { stateDirName } from "../graph/graph.js";
|
|
9
|
-
import { SHAPES, TIERS } from "../graph/schema.js";
|
|
9
|
+
import { EFFORTS, SHAPES, TIERS } from "../graph/schema.js";
|
|
10
10
|
const TierEnum = z.enum(TIERS, {
|
|
11
11
|
error: (iss) => `Invalid option: expected one of "cheap"|"mid"|"frontier" (got ${JSON.stringify(iss.input)})`,
|
|
12
12
|
});
|
|
@@ -51,21 +51,32 @@ export const MapEntrySchema = z.object({
|
|
|
51
51
|
escalate: z.boolean().optional(),
|
|
52
52
|
});
|
|
53
53
|
const DeclaredVendorSchema = z.string().trim().min(1, "vendor must be nonempty");
|
|
54
|
-
|
|
54
|
+
// OBS-1182: effort is launch metadata only these CLIs accept (claude --effort, codex -c
|
|
55
|
+
// model_reasoning_effort=). Every other adapter's override refuses the key outright, so config
|
|
56
|
+
// load and the emitted JSON schema both reject an effort that could never be launched.
|
|
57
|
+
export const EFFORT_ADAPTERS = ["claude-code", "codex"];
|
|
58
|
+
const EffortEnum = z.enum(EFFORTS, {
|
|
59
|
+
error: (iss) => `Invalid effort: expected one of "low"|"medium"|"high" (got ${JSON.stringify(iss.input)})`,
|
|
60
|
+
});
|
|
61
|
+
const NoEffort = z.never({
|
|
62
|
+
error: () => `effort is launch metadata only ${EFFORT_ADAPTERS.join(" and ")} accept — this adapter's CLI takes no effort level`,
|
|
63
|
+
});
|
|
64
|
+
const modelOverrideSchema = (effort) => z
|
|
55
65
|
.object({
|
|
56
66
|
vendor: DeclaredVendorSchema.optional(),
|
|
57
67
|
channel: z.enum(["sub", "api"]).optional(),
|
|
68
|
+
effort: effort.optional(),
|
|
58
69
|
})
|
|
59
|
-
.refine((override) => override.vendor !== undefined || override.channel !== undefined, {
|
|
60
|
-
message: "a model override must declare vendor, channel, or
|
|
70
|
+
.refine((override) => override.vendor !== undefined || override.channel !== undefined || override.effort !== undefined, {
|
|
71
|
+
message: "a model override must declare vendor, channel, effort, or a combination",
|
|
61
72
|
});
|
|
62
|
-
|
|
73
|
+
const tierEntrySchema = (effort) => z.object({
|
|
63
74
|
vendor: DeclaredVendorSchema.nullable(),
|
|
64
75
|
channel: z.enum(["sub", "api"]),
|
|
65
76
|
models: z.record(z.string(), TierEnum),
|
|
66
77
|
// T19: keep model values scalar for existing consumers. Optional sibling metadata resolves before
|
|
67
78
|
// parent metadata in channelsFromConfig; neither provider prefixes nor adapter ids manufacture it.
|
|
68
|
-
modelOverrides: z.record(z.string(),
|
|
79
|
+
modelOverrides: z.record(z.string(), modelOverrideSchema(effort)).optional(),
|
|
69
80
|
// v1.47 T3: optional per-model context-window sizes (tokens). Absent block ⇒ no doctor column, no plan lint.
|
|
70
81
|
windows: z.record(z.string(), z.number().int().positive()).optional(),
|
|
71
82
|
}).superRefine((entry, ctx) => {
|
|
@@ -81,6 +92,13 @@ export const TierEntrySchema = z.object({
|
|
|
81
92
|
});
|
|
82
93
|
}
|
|
83
94
|
});
|
|
95
|
+
export const TierEntrySchema = tierEntrySchema(EffortEnum);
|
|
96
|
+
// Per-adapter object + catchall rather than a record, so the emitted JSON schema carries the same
|
|
97
|
+
// adapter restriction as the loader; typed as the record every consumer already reads.
|
|
98
|
+
// ponytail: catchall parses the EFFORT_ADAPTERS keys first — the seed table already lists them first.
|
|
99
|
+
const TiersSchema = z
|
|
100
|
+
.object(Object.fromEntries(EFFORT_ADAPTERS.map((id) => [id, TierEntrySchema.optional()])))
|
|
101
|
+
.catchall(tierEntrySchema(NoEffort));
|
|
84
102
|
// v1.10 FLEET-06: optional routing.allow/deny fleet preference; absent blocks ⇒ byte-identical routing/discovery.
|
|
85
103
|
// deny wins on conflict; presence of allow (even {} / empty arrays) activates allowlist (fail-closed).
|
|
86
104
|
const PrefBlockSchema = z.object({
|
|
@@ -307,7 +325,8 @@ const ShapeGateParticipationSchema = z
|
|
|
307
325
|
});
|
|
308
326
|
export const ExecutionPolicySchema = z.object({
|
|
309
327
|
boundedInfrastructure: z.boolean().default(false),
|
|
310
|
-
|
|
328
|
+
// OBS-1199: absent inherits the effective default (on); an explicit false stays off.
|
|
329
|
+
repairSelection: z.boolean().optional(),
|
|
311
330
|
taskExecutionLimitMs: z.number().int().positive(),
|
|
312
331
|
}).strict();
|
|
313
332
|
export const TickmarkrConfigSchema = z.object({
|
|
@@ -372,7 +391,7 @@ export const TickmarkrConfigSchema = z.object({
|
|
|
372
391
|
allow: PrefBlockSchema.optional(),
|
|
373
392
|
deny: DenyBlockSchema.optional(),
|
|
374
393
|
}),
|
|
375
|
-
tiers:
|
|
394
|
+
tiers: TiersSchema,
|
|
376
395
|
pricing: z.record(z.string(), z.number()),
|
|
377
396
|
// v1.20 REC-02: optional detailed price table for cost estimation. Distinct from `pricing` above
|
|
378
397
|
// (the coarse per-task tier estimate `tickmarkr plan` shows) — this one drives the usage/cost report.
|
|
@@ -390,6 +409,12 @@ export const TickmarkrConfigSchema = z.object({
|
|
|
390
409
|
tipTest: z.string(),
|
|
391
410
|
lint: z.string(),
|
|
392
411
|
diffCap: z.number().int().positive(),
|
|
412
|
+
// OBS-1140: per-run gate-evidence byte quota; the oldest artifacts are evicted (and tombstoned)
|
|
413
|
+
// past it. Zero retains zero artifact bytes. Evidence retention never changes a gate's pass bit.
|
|
414
|
+
evidenceQuotaBytes: z.number().int().nonnegative(),
|
|
415
|
+
// OBS-1199: repair rounds screen known failing files plus affected tests before the full suite
|
|
416
|
+
// that still decides every merge. On unless false here or in executionPolicy, on every driver.
|
|
417
|
+
repairSelection: z.boolean(),
|
|
393
418
|
byShape: z.partialRecord(z.enum(SHAPES), ShapeGateParticipationSchema).optional(),
|
|
394
419
|
}).partial(),
|
|
395
420
|
scope: z.object({
|
|
@@ -470,6 +495,7 @@ function assertSeededModelWindows() {
|
|
|
470
495
|
}
|
|
471
496
|
}
|
|
472
497
|
}
|
|
498
|
+
export const DEFAULT_EVIDENCE_QUOTA_BYTES = 8 * 1024 * 1024;
|
|
473
499
|
export const DEFAULT_CONFIG = {
|
|
474
500
|
concurrency: 3,
|
|
475
501
|
driver: "auto",
|
|
@@ -642,7 +668,7 @@ export const DEFAULT_CONFIG = {
|
|
|
642
668
|
},
|
|
643
669
|
},
|
|
644
670
|
pricing: { cheap: 0.1, mid: 0.5, frontier: 2.5 },
|
|
645
|
-
gates: { diffCap: DEFAULT_DIFF_CAP },
|
|
671
|
+
gates: { diffCap: DEFAULT_DIFF_CAP, evidenceQuotaBytes: DEFAULT_EVIDENCE_QUOTA_BYTES },
|
|
646
672
|
judge: { adapter: "claude-code", model: "fable" },
|
|
647
673
|
// R3: no `policy` floor — the neutral floor leaves the compiler's per-task assignment standing, so
|
|
648
674
|
// the path-keyed rule is reachable out of the box rather than raised to full by construction.
|
|
@@ -798,6 +824,44 @@ export function loadConfigWithMode(repoRoot, opts = {}) {
|
|
|
798
824
|
assertSeededModelWindows();
|
|
799
825
|
return { cfg: r.data, mode: resolveRoutingMode(r.data, [globalCfg, repoCfg]) };
|
|
800
826
|
}
|
|
827
|
+
/** OBS-1182: tiers.<adapter>.modelOverrides as the layers under the repo overlay (defaults + global)
|
|
828
|
+
* merge them. Read raw, never schema-validated: a lower layer may only validate beside repo fields
|
|
829
|
+
* (global `vendor: null` completed by a repo vendor) and its override metadata is still inherited.
|
|
830
|
+
* OBS-1188: an unparseable global file, or a non-map where this path expects a map, is an error —
|
|
831
|
+
* never an empty lower layer, which would silently drop the metadata a write must re-mask. */
|
|
832
|
+
export function lowerLayerModelOverrides(opts = {}) {
|
|
833
|
+
const isMap = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
834
|
+
const path = join(opts.globalDir ?? globalConfigDir(), "config.yaml");
|
|
835
|
+
let global;
|
|
836
|
+
try {
|
|
837
|
+
global = readYaml(path);
|
|
838
|
+
}
|
|
839
|
+
catch (error) {
|
|
840
|
+
return { ok: false, error: `lower config layer ${path} is malformed YAML: ${error.message.replace(/\s+/g, " ").trim()}` };
|
|
841
|
+
}
|
|
842
|
+
const malformed = (at) => ({ ok: false, error: `lower config layer ${path} is malformed: ${at} is not a map` });
|
|
843
|
+
// deepMerge replaces a map with any non-map the global declares, so the merged shape is the global's
|
|
844
|
+
const merged = deepMerge(structuredClone(DEFAULT_CONFIG), global);
|
|
845
|
+
if (!isMap(merged))
|
|
846
|
+
return malformed("the document");
|
|
847
|
+
const tiers = merged.tiers ?? {}; // a global `tiers: null` tombstone leaves no lower tiers
|
|
848
|
+
if (!isMap(tiers))
|
|
849
|
+
return malformed("tiers");
|
|
850
|
+
const overrides = {};
|
|
851
|
+
for (const [adapter, entry] of Object.entries(tiers)) {
|
|
852
|
+
if (!isMap(entry))
|
|
853
|
+
return malformed(`tiers.${adapter}`);
|
|
854
|
+
const models = entry.modelOverrides ?? {};
|
|
855
|
+
if (!isMap(models))
|
|
856
|
+
return malformed(`tiers.${adapter}.modelOverrides`);
|
|
857
|
+
for (const [model, override] of Object.entries(models)) {
|
|
858
|
+
if (!isMap(override))
|
|
859
|
+
return malformed(`tiers.${adapter}.modelOverrides.${model}`);
|
|
860
|
+
}
|
|
861
|
+
overrides[adapter] = models;
|
|
862
|
+
}
|
|
863
|
+
return { ok: true, overrides };
|
|
864
|
+
}
|
|
801
865
|
export function loadConfig(repoRoot, opts = {}) {
|
|
802
866
|
return loadConfigWithMode(repoRoot, opts).cfg;
|
|
803
867
|
}
|
|
@@ -959,10 +1023,14 @@ export function universeCovers(universe, entry) {
|
|
|
959
1023
|
// Absent universe ⇒ deny arrays verbatim and no allowOut, so non-UI callers stay byte-identical.
|
|
960
1024
|
export function fleetEditableFromConfig(cfg, universe) {
|
|
961
1025
|
const tiers = {};
|
|
1026
|
+
const efforts = {};
|
|
962
1027
|
for (const [adapter, entry] of Object.entries(cfg.tiers)) {
|
|
963
1028
|
tiers[adapter] = {};
|
|
964
1029
|
for (const [model, tier] of Object.entries(entry.models)) {
|
|
965
1030
|
tiers[adapter][model] = { tier };
|
|
1031
|
+
const effort = entry.modelOverrides?.[model]?.effort;
|
|
1032
|
+
if (effort)
|
|
1033
|
+
(efforts[adapter] ??= {})[model] = effort;
|
|
966
1034
|
}
|
|
967
1035
|
}
|
|
968
1036
|
// every enumerated deny scope rides verbatim (sorted) under its own key
|
|
@@ -991,6 +1059,7 @@ export function fleetEditableFromConfig(cfg, universe) {
|
|
|
991
1059
|
...denyLists,
|
|
992
1060
|
...(allowOut !== undefined ? { allowOut } : {}),
|
|
993
1061
|
tiers,
|
|
1062
|
+
...(Object.keys(efforts).length ? { efforts } : {}),
|
|
994
1063
|
map: structuredClone(cfg.routing.map),
|
|
995
1064
|
floors: { ...cfg.routing.floors },
|
|
996
1065
|
};
|
|
@@ -1037,6 +1106,13 @@ export function formatFleetPrint(repoRoot, opts = {}) {
|
|
|
1037
1106
|
for (const [adapter, entry] of Object.entries(effective.tiers)) {
|
|
1038
1107
|
for (const model of Object.keys(entry.models))
|
|
1039
1108
|
annotate(["tiers", adapter, "models", model]);
|
|
1109
|
+
// OBS-1182: the effort value prints in place; its note names the layer that set it and the edit
|
|
1110
|
+
for (const [model, override] of Object.entries(entry.modelOverrides ?? {})) {
|
|
1111
|
+
const path = ["tiers", adapter, "modelOverrides", model, "effort"];
|
|
1112
|
+
const node = doc.getIn(path, true);
|
|
1113
|
+
if (override.effort && isScalar(node))
|
|
1114
|
+
node.comment = ` ${fleetKeyLayer(repoRoot, path.join("."), opts)} — fleet e edits`;
|
|
1115
|
+
}
|
|
1040
1116
|
}
|
|
1041
1117
|
return String(doc);
|
|
1042
1118
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { type FleetEditable, type FleetUniverseRow, type RoutingMode } from "./config.js";
|
|
1
|
+
import { type FleetEditable, type FleetUniverseRow, type LowerLayerModelOverrides, 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
|
+
type FleetOverlayWriteFields = {
|
|
5
5
|
initial: FleetEditable;
|
|
6
6
|
edited: FleetEditable;
|
|
7
7
|
mode?: RoutingMode;
|
|
@@ -21,6 +21,16 @@ export type FleetOverlayWrite = {
|
|
|
21
21
|
};
|
|
22
22
|
universe?: FleetUniverseRow[];
|
|
23
23
|
};
|
|
24
|
+
type EffortFree = FleetEditable & {
|
|
25
|
+
efforts?: undefined;
|
|
26
|
+
};
|
|
27
|
+
export type FleetOverlayWrite = FleetOverlayWriteFields & ({
|
|
28
|
+
lowerOverrides: LowerLayerModelOverrides;
|
|
29
|
+
} | {
|
|
30
|
+
initial: EffortFree;
|
|
31
|
+
edited: EffortFree;
|
|
32
|
+
lowerOverrides?: undefined;
|
|
33
|
+
});
|
|
24
34
|
export type FleetFirstTouch = {
|
|
25
35
|
vendor: string;
|
|
26
36
|
channel: "sub" | "api";
|
|
@@ -35,3 +45,4 @@ export declare function repoOverlayYaml(overlay: Record<string, unknown>): strin
|
|
|
35
45
|
export declare function serializeFleetOverlay(overlay: Record<string, unknown>): string;
|
|
36
46
|
export declare function unifiedYamlDiff(before: string, after: string, label?: string): string;
|
|
37
47
|
export declare function fleetEditableEquals(a: FleetEditable, b: FleetEditable): boolean;
|
|
48
|
+
export {};
|
|
@@ -360,6 +360,66 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
360
360
|
setScalarPreservingComment(doc, path, after.tier, unpackFleetProvenance(after.provenance).provenance);
|
|
361
361
|
}
|
|
362
362
|
}
|
|
363
|
+
// OBS-1182: effort lands in tiers.<adapter>.modelOverrides.<model>.effort — the scalar tier and
|
|
364
|
+
// every sibling override key (vendor, channel) keep their bytes.
|
|
365
|
+
const efforts = (e) => e.efforts ?? {};
|
|
366
|
+
for (const adapter of new Set([...Object.keys(efforts(initial)), ...Object.keys(efforts(edited))])) {
|
|
367
|
+
for (const model of new Set([...Object.keys(efforts(initial)[adapter] ?? {}), ...Object.keys(efforts(edited)[adapter] ?? {})])) {
|
|
368
|
+
const after = efforts(edited)[adapter]?.[model];
|
|
369
|
+
if (efforts(initial)[adapter]?.[model] === after)
|
|
370
|
+
continue;
|
|
371
|
+
// OBS-1188: the type already demands lower state here; this refuses a cast/JS caller before the
|
|
372
|
+
// bytes are returned, so a blind effort write never reaches disk.
|
|
373
|
+
const lowerAll = write.lowerOverrides;
|
|
374
|
+
if (!lowerAll) {
|
|
375
|
+
throw new Error(`fleet write: the effort edit on ${adapter}:${model} needs the lower config layers' model overrides — refusing to write it blind`);
|
|
376
|
+
}
|
|
377
|
+
const override = ["tiers", adapter, "modelOverrides", model];
|
|
378
|
+
if (after !== undefined) {
|
|
379
|
+
// Setting beneath a tombstone (the override, or modelOverrides above it) lifts it, and
|
|
380
|
+
// deepMerge would restore what it masked: re-mask each lower-layer key it covered — sibling
|
|
381
|
+
// models, then this override's own keys.
|
|
382
|
+
// ponytail: tiers.<adapter> itself tombstoned is unreachable here — its models, and so a tier, are masked too.
|
|
383
|
+
const tombstoned = (depth) => {
|
|
384
|
+
const node = doc.getIn(override.slice(0, depth), true);
|
|
385
|
+
return node !== undefined && !isMap(node);
|
|
386
|
+
};
|
|
387
|
+
const lowerModels = lowerAll[adapter] ?? {};
|
|
388
|
+
const remask = [];
|
|
389
|
+
if (tombstoned(3))
|
|
390
|
+
for (const other of Object.keys(lowerModels))
|
|
391
|
+
if (other !== model)
|
|
392
|
+
remask.push([...override.slice(0, 3), other]);
|
|
393
|
+
if (tombstoned(3) || tombstoned(4)) {
|
|
394
|
+
for (const key of Object.keys(lowerModels[model] ?? {}))
|
|
395
|
+
if (key !== "effort")
|
|
396
|
+
remask.push([...override, key]);
|
|
397
|
+
}
|
|
398
|
+
setScalarPreservingComment(doc, [...override, "effort"], after);
|
|
399
|
+
for (const path of remask)
|
|
400
|
+
setScalarPreservingComment(doc, path, null);
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
// Clearing means no effort in the MERGED config: drop the repo key, then mask an effort the
|
|
404
|
+
// lower layers still declare.
|
|
405
|
+
deleteAt(doc, [...override, "effort"]);
|
|
406
|
+
const lower = lowerAll[adapter]?.[model];
|
|
407
|
+
const maskEffort = lower?.effort !== undefined;
|
|
408
|
+
const repoNode = doc.getIn(override, true);
|
|
409
|
+
const { effort: _masked, ...rest } = { ...lower, ...(isMap(repoNode) ? repoNode.toJSON() : {}) };
|
|
410
|
+
if (Object.values(rest).some((v) => v !== null && v !== undefined)) {
|
|
411
|
+
if (maskEffort)
|
|
412
|
+
setScalarPreservingComment(doc, [...override, "effort"], null);
|
|
413
|
+
}
|
|
414
|
+
else if (maskEffort || (isMap(repoNode) && repoNode.items.length > 0)) {
|
|
415
|
+
// Tombstone-only siblings must still suppress lower metadata when effort is cleared,
|
|
416
|
+
// even without inherited effort. Their merged {} would fail the nonempty refinement.
|
|
417
|
+
setScalarPreservingComment(doc, override, null);
|
|
418
|
+
}
|
|
419
|
+
for (const depth of [4, 3, 2, 1])
|
|
420
|
+
deleteEmptyMap(doc, override.slice(0, depth));
|
|
421
|
+
}
|
|
422
|
+
}
|
|
363
423
|
if (write.mode !== undefined) {
|
|
364
424
|
setScalarPreservingComment(doc, ["routing", "mode"], write.mode);
|
|
365
425
|
}
|
package/dist/drivers/herdr.d.ts
CHANGED
|
@@ -62,6 +62,8 @@ export declare class HerdrDriver implements ExecutorDriver {
|
|
|
62
62
|
private deliverySerial;
|
|
63
63
|
private dispatchLeases;
|
|
64
64
|
private deliveredPanes;
|
|
65
|
+
/** OBS-1203: slots bound by adopt() — their reads are harvest evidence, so a failed read throws. */
|
|
66
|
+
private adoptedSlots;
|
|
65
67
|
private inputBoxes;
|
|
66
68
|
private bootstraps;
|
|
67
69
|
private journalRoots;
|
|
@@ -93,6 +95,16 @@ export declare class HerdrDriver implements ExecutorDriver {
|
|
|
93
95
|
private paneId;
|
|
94
96
|
private paneWidth;
|
|
95
97
|
slot(cwd: string, name: string, opts?: SlotOpts): Promise<Slot>;
|
|
98
|
+
/**
|
|
99
|
+
* OBS-1203: bind THIS instance, READ-ONLY, to the pane an earlier daemon created for an owned slot,
|
|
100
|
+
* so a resumed daemon can read the interrupted attempt's trailer. slot() is never a substitute: it
|
|
101
|
+
* allocates a tab, reclaimStaleLabel closes the very pane holding the evidence, and it holds a
|
|
102
|
+
* dispatch lease only run() releases. Binding needs ownership evidence, never a label alone: exactly
|
|
103
|
+
* one pane carries the FULL owned label, herdr reports that pane's cwd as the journaled task
|
|
104
|
+
* checkout, and that path is a git checkout whose own top level it is. Zero, ambiguous or foreign
|
|
105
|
+
* candidates throw — the caller's decline. `pane list` and `git rev-parse` are the only calls made.
|
|
106
|
+
*/
|
|
107
|
+
adopt(slot: Slot): Promise<Slot>;
|
|
96
108
|
private tabSlot;
|
|
97
109
|
private reclaimStaleLabel;
|
|
98
110
|
private staleLabelPanes;
|
package/dist/drivers/herdr.js
CHANGED
|
@@ -7,6 +7,7 @@ import { consumePaneLaunchIntent, PANE_IDENTITY_ENV, paneIdentityLine } from "..
|
|
|
7
7
|
import { createWorktree, sh } from "../run/git.js";
|
|
8
8
|
import { Journal } from "../run/journal.js";
|
|
9
9
|
import { readSupervision, readWatchBoard, requestWatchBoardStop, reserveWatchBoard, stopWatchBoard, WATCH_OWNER_ENV } from "../run/supervision.js";
|
|
10
|
+
import { canonicalWorktreePath } from "./orca.js";
|
|
10
11
|
import { herdrSealShellPrefix } from "./subprocess.js";
|
|
11
12
|
import { canonicalizeLegacyName, formatOwnedName, panesToClose, parseOwnedName } from "./types.js";
|
|
12
13
|
// VIS-09 P43-03: adopted safety floor from 43-MEASUREMENT.md (narrowest safe 53 → floor 108).
|
|
@@ -153,6 +154,8 @@ export class HerdrDriver {
|
|
|
153
154
|
deliverySerial = Promise.resolve();
|
|
154
155
|
dispatchLeases = new WeakMap();
|
|
155
156
|
deliveredPanes = new WeakMap();
|
|
157
|
+
/** OBS-1203: slots bound by adopt() — their reads are harvest evidence, so a failed read throws. */
|
|
158
|
+
adoptedSlots = new WeakSet();
|
|
156
159
|
inputBoxes = new WeakMap();
|
|
157
160
|
// OBS-253: the adapter-declared bootstrap this slot has already delivered. A fresh pane is a bare
|
|
158
161
|
// shell, so it is the only thing that can put a TUI back under a typed turn that has to move.
|
|
@@ -452,6 +455,49 @@ export class HerdrDriver {
|
|
|
452
455
|
// refcount, no groupSerial, no degrade latch — dedicated tabs have no shared state to guard (SUP-01).
|
|
453
456
|
return slot; // label undefined → defaults to name (today's behavior)
|
|
454
457
|
}
|
|
458
|
+
/**
|
|
459
|
+
* OBS-1203: bind THIS instance, READ-ONLY, to the pane an earlier daemon created for an owned slot,
|
|
460
|
+
* so a resumed daemon can read the interrupted attempt's trailer. slot() is never a substitute: it
|
|
461
|
+
* allocates a tab, reclaimStaleLabel closes the very pane holding the evidence, and it holds a
|
|
462
|
+
* dispatch lease only run() releases. Binding needs ownership evidence, never a label alone: exactly
|
|
463
|
+
* one pane carries the FULL owned label, herdr reports that pane's cwd as the journaled task
|
|
464
|
+
* checkout, and that path is a git checkout whose own top level it is. Zero, ambiguous or foreign
|
|
465
|
+
* candidates throw — the caller's decline. `pane list` and `git rev-parse` are the only calls made.
|
|
466
|
+
*/
|
|
467
|
+
async adopt(slot) {
|
|
468
|
+
if (!parseOwnedName(slot.name))
|
|
469
|
+
throw new Error(`herdr adopt: slot ${slot.name} carries no owned label — nothing to adopt`);
|
|
470
|
+
const listed = await this.herdr("pane list");
|
|
471
|
+
let panes;
|
|
472
|
+
try {
|
|
473
|
+
const parsed = JSON.parse(listed.stdout).result?.panes;
|
|
474
|
+
if (listed.code !== 0 || !Array.isArray(parsed))
|
|
475
|
+
throw new Error("no panes array");
|
|
476
|
+
panes = parsed;
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
throw new Error(`herdr adopt: pane list unreadable (exit ${listed.code}) — cannot prove ownership of ${slot.name}`);
|
|
480
|
+
}
|
|
481
|
+
const owned = panes.filter((p) => p.label === slot.name && typeof p.pane_id === "string" && p.pane_id);
|
|
482
|
+
if (owned.length !== 1) {
|
|
483
|
+
throw new Error(`herdr adopt: ${owned.length === 0 ? "no pane carries" : `${owned.length} panes carry`} the owned label ${slot.name}`);
|
|
484
|
+
}
|
|
485
|
+
const pane = owned[0];
|
|
486
|
+
const checkout = canonicalWorktreePath(slot.cwd);
|
|
487
|
+
if (typeof pane.cwd !== "string" || canonicalWorktreePath(pane.cwd) !== checkout) {
|
|
488
|
+
throw new Error(`herdr adopt: pane ${pane.pane_id} labelled ${slot.name} sits in ${pane.cwd ?? "an unreported cwd"}, not the task checkout ${checkout} — foreign`);
|
|
489
|
+
}
|
|
490
|
+
const top = await sh("git rev-parse --show-toplevel", checkout);
|
|
491
|
+
if (top.code !== 0 || canonicalWorktreePath(top.stdout.trim()) !== checkout) {
|
|
492
|
+
throw new Error(`herdr adopt: ${checkout} is not its own git checkout — no checkout proof for ${slot.name}`);
|
|
493
|
+
}
|
|
494
|
+
const adopted = { id: pane.pane_id, name: slot.name, cwd: slot.cwd,
|
|
495
|
+
...(pane.tab_id === undefined ? {} : { tabId: pane.tab_id }), ...(slot.group === undefined ? {} : { group: slot.group }) };
|
|
496
|
+
// Reads go to the pane ownership was proven on, never to a later re-resolution of the label.
|
|
497
|
+
this.deliveredPanes.set(adopted, pane.pane_id);
|
|
498
|
+
this.adoptedSlots.add(adopted);
|
|
499
|
+
return adopted;
|
|
500
|
+
}
|
|
455
501
|
// today's per-slot tab path, plus the VIS-04 orphan reap
|
|
456
502
|
// label defaults to the slot name; group tabs pass the STAGE name instead — a first-member label
|
|
457
503
|
// outlives its member once keepPanes reaps it (run-20260709-104447: the codex pane sat in a tab
|
|
@@ -1099,6 +1145,11 @@ export class HerdrDriver {
|
|
|
1099
1145
|
async read(slot, lines) {
|
|
1100
1146
|
const pane = await this.paneId(slot);
|
|
1101
1147
|
const r = await this.herdr(`pane read ${shq(pane)} --source recent-unwrapped --lines ${lines}`, slot.cwd);
|
|
1148
|
+
// OBS-1203 review: an adopted pane's read decides a resume harvest; a failed read proves neither a
|
|
1149
|
+
// trailer nor its absence, so it throws (the caller's decline) rather than read as an empty pane.
|
|
1150
|
+
// ponytail: other slots keep the historical empty-on-failure read; widen once seed/consult callers catch.
|
|
1151
|
+
if (r.code !== 0 && this.adoptedSlots.has(slot))
|
|
1152
|
+
throw new Error(`herdr pane read ${pane} failed (exit ${r.code}): ${r.stderr || r.stdout}`);
|
|
1102
1153
|
return r.stdout;
|
|
1103
1154
|
}
|
|
1104
1155
|
// v1.22 T5 / OBS-19: raw keystroke into the pane TUI (trust-dialog auto-answer). Resolves the pane
|
package/dist/drivers/orca.d.ts
CHANGED
|
@@ -123,12 +123,13 @@ export declare function mapAgentState(term: Record<string, unknown>, tuiIdle: bo
|
|
|
123
123
|
* A retired record is replaced by a new reservation (CAS on the tombstone bytes) only once its pane
|
|
124
124
|
* is proven gone. Everything else — a reservation or claim with no bound pane, a failed cleanup, a
|
|
125
125
|
* record another driver holds with a live observer — refuses and keeps the record exactly as it is.
|
|
126
|
+
* One reservation or claim is HELD rather than in flight: its placer is this process after an
|
|
127
|
+
* indeterminate split receipt (OBS-1172). It is reconciled again on every call, never split over.
|
|
126
128
|
*/
|
|
127
129
|
/** `placer` is the pid of the narrator that wrote the reservation: a reserved or claimed record is a
|
|
128
130
|
* normal intermediate state while that pid lives (another driver awaiting listing or bind) and a
|
|
129
131
|
* crash to recover only once it is dead. */
|
|
130
132
|
type BoardRecord = WatchBoardOwner & {
|
|
131
|
-
retired?: true;
|
|
132
133
|
runtimeId?: string;
|
|
133
134
|
placer?: number;
|
|
134
135
|
};
|
|
@@ -198,6 +199,16 @@ export declare class OrcaDriver implements ExecutorDriver {
|
|
|
198
199
|
/** Explicit runtime probe. Also T3's doctor probe. */
|
|
199
200
|
probeRuntime(cwd?: string): Promise<string>;
|
|
200
201
|
slot(cwd: string, name: string, opts?: SlotOpts): Promise<Slot>;
|
|
202
|
+
/**
|
|
203
|
+
* OBS-1109: bind THIS instance to a terminal an earlier daemon created for an OWNED slot, so a
|
|
204
|
+
* resumed daemon can read the interrupted attempt's pane. Binding needs ownership evidence, never
|
|
205
|
+
* a title or handle alone: exactly one tab on the tracked worktree carries the FULL journaled owned
|
|
206
|
+
* title, it resolves to exactly one terminal, and that terminal's scrollback proves the exact task
|
|
207
|
+
* checkout `slot.cwd` (the frame only tickmarkr's own create command prints). Zero, ambiguous or
|
|
208
|
+
* foreign candidates leave the slot unbound: the throw is the caller's decline. The journaled
|
|
209
|
+
* nonce is then checked by the caller against the trailer this binding reads.
|
|
210
|
+
*/
|
|
211
|
+
adopt(slot: Slot): Promise<Slot>;
|
|
201
212
|
/** Where to invoke the CLI for this slot's calls (see OrcaSlotState.dir). */
|
|
202
213
|
private cliCwd;
|
|
203
214
|
private state;
|
|
@@ -263,7 +274,15 @@ export declare class OrcaDriver implements ExecutorDriver {
|
|
|
263
274
|
*/
|
|
264
275
|
private screenBehindBlindStream;
|
|
265
276
|
/** A rendered-frame liveness read. `--screen` and `--cursor` are mutually exclusive in Orca. */
|
|
266
|
-
private
|
|
277
|
+
private screenFrame;
|
|
278
|
+
/**
|
|
279
|
+
* OBS-1205: the public rendered-screen read a slot carries (Slot.readScreen). Kimi draws its trust
|
|
280
|
+
* modal with cursor addressing, so the stream holds the box rules and no `Trust` text while the
|
|
281
|
+
* painted frame shows `❯ Trust this folder`. Same ownership discipline as every read — bound
|
|
282
|
+
* runtime, addressed handle, running status — and a frame the runtime could not render is
|
|
283
|
+
* unreadable, never an empty screen a caller could read as "no modal".
|
|
284
|
+
*/
|
|
285
|
+
readScreen(slot: Slot): Promise<string>;
|
|
267
286
|
/** A single UNPAGED tail read — exactly what the caller asked for and nothing more. Markers split
|
|
268
287
|
* across cursor pages are not reassembled here; that is waitOutput's job. */
|
|
269
288
|
read(slot: Slot, lines: number): Promise<string>;
|
|
@@ -290,6 +309,15 @@ export declare class OrcaDriver implements ExecutorDriver {
|
|
|
290
309
|
sendKey(slot: Slot, key: string): Promise<void>;
|
|
291
310
|
nudge(slot: Slot, message: string): Promise<boolean>;
|
|
292
311
|
narrator(cwd: string, command: string, runId?: string): Promise<Slot>;
|
|
312
|
+
/** OBS-1172: what an indeterminate split left behind, reconciled from the owner record and Orca's
|
|
313
|
+
* terminal table — never from the receipt that did not arrive. The reservation's token reaches no
|
|
314
|
+
* process but the split's own command, so a claim proves that split started a board; the owned
|
|
315
|
+
* record (this driver, the watch title) is ours by that same token. The claimant must be live (pid
|
|
316
|
+
* and arm presence), and it must name the terminal it runs in — a listed pane in the launching tab.
|
|
317
|
+
* A claim that names none proves a board started, not WHICH pane hosts it: a sole neighbouring pane
|
|
318
|
+
* (row titles are shell-controlled, never ownership keys) is not adopted, so the claim stays held.
|
|
319
|
+
* No claim and no such pane: the split started nothing the table shows. Anything else is held. */
|
|
320
|
+
private settleIndeterminate;
|
|
293
321
|
private boardPath;
|
|
294
322
|
/** A recorded pane (a receipt's handle bound to the split envelope's runtime, never a guess) is
|
|
295
323
|
* gone when that runtime no longer lists it, or when a handle-bound close receipt names it.
|
|
@@ -303,6 +331,11 @@ export declare class OrcaDriver implements ExecutorDriver {
|
|
|
303
331
|
* handle-bound close (timeout keeps the tombstone and the pane); a dead one never acknowledges,
|
|
304
332
|
* so it is only asked. */
|
|
305
333
|
private retireAndClose;
|
|
334
|
+
/** Stop a retired board's observer before its pane is closed. A live observer acknowledges on
|
|
335
|
+
* injected time; a dead one never can, so it is only asked. OBS-1131: an acknowledgement timeout
|
|
336
|
+
* is not proof of death — while the pid lives the owned pane stays protected — but a pid that died
|
|
337
|
+
* during the wait is, and its pane is closed instead of leaked. */
|
|
338
|
+
private stopObserver;
|
|
306
339
|
/** WB-1 seam: the daemon reports this board lost. "Lost" can be a stale beat or missing presence
|
|
307
340
|
* under a still-live owner pid, so it is not proof of a dead observer — retirement keeps close's
|
|
308
341
|
* acknowledgement discipline (Leg-2 T9 P1). */
|