tickmarkr 2.5.8 → 2.6.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/dist/adapters/claude-code.js +19 -6
- package/dist/adapters/prompt.js +1 -1
- package/dist/adapters/qwen.js +30 -3
- package/dist/adapters/types.d.ts +3 -0
- package/dist/cli/commands/approve.js +15 -3
- package/dist/cli/commands/beat.d.ts +2 -0
- package/dist/cli/commands/beat.js +28 -29
- package/dist/cli/commands/compile.js +11 -0
- package/dist/cli/commands/fleet.js +61 -53
- package/dist/cli/commands/init.js +1 -1
- package/dist/cli/commands/report.js +11 -1
- package/dist/cli/commands/verify.d.ts +4 -1
- package/dist/cli/commands/verify.js +18 -5
- package/dist/cli/help.d.ts +2 -0
- package/dist/cli/help.js +6 -4
- package/dist/compile/native.js +39 -4
- package/dist/config/config.d.ts +41 -3
- package/dist/config/config.js +48 -17
- package/dist/config/fleet-overlay.js +47 -62
- package/dist/drivers/orca.d.ts +1 -1
- package/dist/drivers/orca.js +21 -4
- package/dist/gates/baseline.d.ts +67 -0
- package/dist/gates/baseline.js +172 -10
- package/dist/gates/cache.d.ts +8 -0
- package/dist/gates/cache.js +26 -13
- package/dist/gates/review.d.ts +8 -7
- package/dist/gates/review.js +83 -45
- package/dist/gates/run-gates.d.ts +4 -1
- package/dist/gates/run-gates.js +32 -12
- package/dist/gates/test-manifest.d.ts +16 -1
- package/dist/gates/test-manifest.js +127 -27
- package/dist/gates/test-reporter.js +4 -0
- package/dist/graph/graph.d.ts +1 -1
- package/dist/graph/graph.js +6 -1
- package/dist/graph/schema.d.ts +2 -0
- package/dist/graph/schema.js +2 -0
- package/dist/route/preference.d.ts +1 -1
- package/dist/route/preference.js +10 -39
- package/dist/run/daemon.d.ts +7 -1
- package/dist/run/daemon.js +237 -53
- package/dist/run/git.d.ts +10 -0
- package/dist/run/git.js +49 -1
- package/dist/run/journal.d.ts +18 -3
- package/dist/run/journal.js +141 -17
- package/dist/run/merge.d.ts +13 -2
- package/dist/run/merge.js +98 -12
- package/dist/run/protocol.d.ts +82 -0
- package/dist/run/protocol.js +35 -0
- package/dist/run/receipt-resolver.d.ts +18 -0
- package/dist/run/receipt-resolver.js +132 -0
- package/dist/run/supervision.d.ts +14 -1
- package/dist/run/supervision.js +122 -24
- package/dist/tui/cockpit/evidence-view.d.ts +10 -1
- package/dist/tui/cockpit/evidence-view.js +37 -5
- package/dist/tui/cockpit/live-runtime.d.ts +4 -0
- package/dist/tui/cockpit/live-runtime.js +37 -3
- package/dist/tui/cockpit/live-store.d.ts +2 -0
- package/dist/tui/ink/fleet-app.d.ts +12 -22
- package/dist/tui/ink/fleet-app.js +520 -131
- package/package.json +1 -1
- package/schema/rungraph.schema.json +7 -0
- package/skills/tickmarkr-overseer/SKILL.md +91 -41
- package/skills/tickmarkr-overseer/scripts/watch-context.sh +14 -2
package/dist/cli/help.js
CHANGED
|
@@ -140,14 +140,16 @@ export const COMMAND_HELP = {
|
|
|
140
140
|
},
|
|
141
141
|
beat: {
|
|
142
142
|
usage: "beat <orchestrator|orchestrator-context|overseer|overseer-context|watch> --seat <identity> [options]",
|
|
143
|
-
description: "Write one supervision beat.
|
|
143
|
+
description: "Write one supervision beat using the recorded arm. Stood-down arms stay DISARMED until explicitly re-armed; stopped beats become STALE.",
|
|
144
144
|
options: {
|
|
145
145
|
"--seat <identity>": "Required: identify the supervising pane or agent.",
|
|
146
|
-
"--arm
|
|
146
|
+
"--new-arm": "Create a new durable arm, allowing beats to resume after stand-down; the hand-off step for a seat taking over a stood-down tier (combine with --arm-id and --pct on a context tier).",
|
|
147
|
+
"--loop": "Create a new durable arm and beat in this process every 10 seconds; exit within one interval after stand-down.",
|
|
148
|
+
"--arm-id <identity>": "Name a newly created arm; with --pct, identify the context observation arm. Never refuses a tick.",
|
|
147
149
|
"--pct <0..100>": "Report current context consumption percentage.",
|
|
148
150
|
"--threshold-pct <0..100>": "Set the context warning threshold (default 75).",
|
|
149
|
-
"--stand-down": "Record an explicit handoff and stand down this tier.",
|
|
150
|
-
}, examples: ["beat overseer --seat supervisor", "beat overseer --seat supervisor --stand-down"],
|
|
151
|
+
"--stand-down": "Record an explicit handoff and stand down this tier. A later one-shot tick on the old arm exits non-zero; context observations still raise and discharge the clear duty.",
|
|
152
|
+
}, examples: ["beat overseer --seat supervisor", "beat overseer --seat supervisor --loop", "beat overseer --seat supervisor --stand-down", "beat overseer --seat supervisor --new-arm"],
|
|
151
153
|
},
|
|
152
154
|
version: {
|
|
153
155
|
usage: "version [--dist]", description: "Print the installed package version. Top-level aliases: --version and -v.",
|
package/dist/compile/native.js
CHANGED
|
@@ -69,8 +69,11 @@ const NESTED_RE = /^\s+- (.+)$/;
|
|
|
69
69
|
// v1.19: a typed acceptance oracle line — "command: ...", "test: ...", or "judge: ...". Anything
|
|
70
70
|
// without one of these prefixes is a plain-string judge criterion (compat path, emits a warning).
|
|
71
71
|
const ORACLE_RE = new RegExp(`^(${ORACLES.join("|")}):\\s*(.*)$`);
|
|
72
|
-
const FIELDS = new Set(["goal", "shape", "deps", "files", "context", "complexity", "humangate", "pin", "floor", "gates", "acceptance", "timeout", "pins"]);
|
|
73
|
-
const listItems = (draft) => draft.list === "acceptance" ? draft.acceptanceRaw
|
|
72
|
+
const FIELDS = new Set(["goal", "shape", "deps", "files", "context", "complexity", "humangate", "pin", "floor", "gates", "acceptance", "timeout", "pins", "outofscope"]);
|
|
73
|
+
const listItems = (draft) => draft.list === "acceptance" ? draft.acceptanceRaw
|
|
74
|
+
: draft.list === "pins" ? draft.pinsRaw
|
|
75
|
+
: draft.list === "outofscope" ? draft.outOfScope
|
|
76
|
+
: draft.gates;
|
|
74
77
|
// v2.5.8 T7: one declared pin item — "literal: <exact text> | glob: <search glob>" (split at the LAST
|
|
75
78
|
// " | glob: " so the text may itself hold a bar) or "fixture: <path set>". A declaration missing the
|
|
76
79
|
// payload its own kind requires refuses the compile naming the task and the item.
|
|
@@ -420,7 +423,7 @@ export function compileNative(file, options = {}) {
|
|
|
420
423
|
for (const [index, line] of content.split("\n").entries()) {
|
|
421
424
|
const heading = line.match(HEAD_RE);
|
|
422
425
|
if (heading) {
|
|
423
|
-
drafts.push({ id: heading[1], title: heading[2].trim(), fields: {}, acceptanceRaw: [], acceptance: [], gates: [], hasGates: false, pinsRaw: [], list: null, itemOpen: false, continuationField: null });
|
|
426
|
+
drafts.push({ id: heading[1], title: heading[2].trim(), fields: {}, acceptanceRaw: [], acceptance: [], gates: [], hasGates: false, pinsRaw: [], outOfScope: [], list: null, itemOpen: false, continuationField: null });
|
|
424
427
|
continue;
|
|
425
428
|
}
|
|
426
429
|
const draft = drafts.at(-1);
|
|
@@ -455,7 +458,7 @@ export function compileNative(file, options = {}) {
|
|
|
455
458
|
invalid(draft.id, field[1], "is unknown");
|
|
456
459
|
const value = field[2].trim();
|
|
457
460
|
draft.itemOpen = false;
|
|
458
|
-
if (name === "acceptance" || name === "gates" || name === "pins") {
|
|
461
|
+
if (name === "acceptance" || name === "gates" || name === "pins" || name === "outofscope") {
|
|
459
462
|
if (value)
|
|
460
463
|
invalid(draft.id, field[1], "must be a nested list");
|
|
461
464
|
draft.list = name;
|
|
@@ -725,6 +728,9 @@ export function compileNative(file, options = {}) {
|
|
|
725
728
|
...(routingHints ? { routingHints } : {}),
|
|
726
729
|
...(draft.hasGates ? { gates: draft.gates } : {}),
|
|
727
730
|
...(draft.pinsRaw.length ? { pins: draft.pinsRaw.map((raw, i) => parsePin(draft.id, raw, i, pinPaths)) } : {}),
|
|
731
|
+
// OBS-1126: an absent list emits no key — a sealed graph's bytes must not move for tasks that
|
|
732
|
+
// never declared a bound.
|
|
733
|
+
...(draft.outOfScope.length ? { outOfScope: draft.outOfScope } : {}),
|
|
728
734
|
};
|
|
729
735
|
});
|
|
730
736
|
const result = validateGraph({
|
|
@@ -841,6 +847,23 @@ acceptance is required on every task (a nested list of observable outcomes).
|
|
|
841
847
|
it carries no literal text)
|
|
842
848
|
LAW: a pins declaration is a LIMITED AUTHORING CONTRACT, not an assertion analyzer —
|
|
843
849
|
tickmarkr holds you to the obligations you DECLARE; it does not discover the pins you forgot.
|
|
850
|
+
THE PIN SWEEP IS BY CALLERS OF EVERY RESHAPED TYPE, NOT BY THE LITERAL (OBS-1127): for each
|
|
851
|
+
type, field or return shape the task changes, enumerate EVERY CALLER and EVERY BRIDGE — the
|
|
852
|
+
command layer that re-serialises it, the live cockpit that renders it, the fixture that
|
|
853
|
+
byte-pins it — and declare each one. A grep for the retired literal finds the sites that
|
|
854
|
+
SPELL the old shape; it misses the sites that CONSUME it under another name. Measured in
|
|
855
|
+
v2.5.9, twice: a deny-scope reshape swept by literal missed the Fleet command bridge that
|
|
856
|
+
re-emitted the old scope set; a pane-status reshape missed the live cockpit caller that
|
|
857
|
+
still read the retired field. Both compiled, both passed the declared pins, both shipped.
|
|
858
|
+
outOfScope: nested list (optional) of bounds the task must NOT cross — carried onto the graph task
|
|
859
|
+
and into its content identity (OBS-1126): a changed list retires prior-run findings.
|
|
860
|
+
It is the DECLARED BOUND the reviewer and the repair worker are HELD TO, stated as EXPLICIT
|
|
861
|
+
EXCLUSIONS ALONGSIDE the authoritative goal and acceptance criteria — it never displaces
|
|
862
|
+
them. The review brief carries the goal (authoritative), the criteria and this list, so a
|
|
863
|
+
finding inside the list is NOT MATERIAL and must not block approval (the reviewer may
|
|
864
|
+
still raise it as minor); the repair worker receives the complete task prompt — goal,
|
|
865
|
+
criteria and this list — not only the finding, so a repair that crosses the list fails
|
|
866
|
+
even when it would silence the finding.
|
|
844
867
|
|
|
845
868
|
HARD BOUNDS — these FAIL the compile, they do not warn:
|
|
846
869
|
- at most 6 acceptance items per task (no exception path)
|
|
@@ -876,6 +899,15 @@ acceptance is required on every task (a nested list of observable outcomes).
|
|
|
876
899
|
A criterion naming a CAPABILITY is satisfiable by a stub whose only caller is its own test, and the
|
|
877
900
|
judge cannot catch it — it reads the DIFF, and a stub's hunks are real. Name the PRODUCTION CALLER
|
|
878
901
|
that must exercise the capability, and the path it runs on; "X is supported" ships as dead code.
|
|
902
|
+
- A CRITERION RUNS END-TO-END THROUGH ITS PRODUCTION CALLER, STATED ONCE. Pin the observable at the
|
|
903
|
+
outermost production entry point — the CLI bridge, the rendered frame, the journal line — and let the
|
|
904
|
+
layers beneath it be exercised by that one claim. A criterion pinned at a helper is satisfied by the
|
|
905
|
+
helper alone while every layer above it is still wrong, and the fix then CLIMBS: each review round
|
|
906
|
+
moves the pin up one layer and burns a round. Measured in v2.5.9 (OBS-1127): a deny-scope criterion
|
|
907
|
+
first pinned the helper, then the production shape, then the Fleet state, then the CLI bridge — four
|
|
908
|
+
review rounds for one claim, "tkr fleet deny lists every scope the schema declares", that stated once
|
|
909
|
+
at the bridge would have failed at round one. Ask: WHAT DOES THE OPERATOR INVOKE, AND WHAT DO THEY
|
|
910
|
+
SEE? Pin that. A criterion satisfied one layer short of the operator is a criterion about plumbing.
|
|
879
911
|
- Enumerating one axis exhaustively is what hides the others. A spec that guards PARTIAL coverage
|
|
880
912
|
site-by-site, member-by-member, can be defeated wholesale by CONDITIONAL coverage, which leaves
|
|
881
913
|
every enumeration satisfied. After you enumerate, ask what a single flag would do to the whole set.
|
|
@@ -922,6 +954,9 @@ acceptance is required on every task (a nested list of observable outcomes).
|
|
|
922
954
|
PRE-SCOPE BY TEXT, ENUMERATE BLOCKERS BY EXECUTION:
|
|
923
955
|
- Before assigning files[], sweep text across the repository tree for names, callers, tests and prose.
|
|
924
956
|
A text sweep produces a candidate list; only running the change enumerates the real blocker set.
|
|
957
|
+
For every type the task RESHAPES, the sweep is by CALLER, not by literal: list each consumer and each
|
|
958
|
+
bridge of the type (command layer, cockpit, fixtures) and put every one in files[] or in a dep's —
|
|
959
|
+
the two v2.5.9 misses above were consumers that never spelled the retired literal (OBS-1127).
|
|
925
960
|
Keep the candidates for scope, then execute the production path and full gates before declaring the
|
|
926
961
|
set closed — this milestone paid a halted run to learn that the two populations are not identical.
|
|
927
962
|
- SPIKE-THE-CONTRACT-THEN-SCOPE trigger question: COULD A TEST THIS TASK DOES NOT OWN BE ASSERTING THE
|
package/dist/config/config.d.ts
CHANGED
|
@@ -62,6 +62,46 @@ export declare const TierEntrySchema: z.ZodObject<{
|
|
|
62
62
|
windows: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
63
63
|
}, z.core.$strip>;
|
|
64
64
|
export type TierEntry = z.infer<typeof TierEntrySchema>;
|
|
65
|
+
export declare const DenyBlockSchema: z.ZodObject<{
|
|
66
|
+
adapters: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
67
|
+
models: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
68
|
+
workers: z.ZodOptional<z.ZodObject<{
|
|
69
|
+
adapters: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
70
|
+
models: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
71
|
+
}, z.core.$strip>>;
|
|
72
|
+
}, z.core.$strip>;
|
|
73
|
+
type DenyKeysOf<T, P extends string> = {
|
|
74
|
+
[K in keyof T & string]: NonNullable<T[K]> extends string[] ? `${P}${Capitalize<K>}` : DenyKeysOf<NonNullable<T[K]>, `${P}${Capitalize<K>}`>;
|
|
75
|
+
}[keyof T & string];
|
|
76
|
+
export type DenyScopeKey = DenyKeysOf<z.infer<typeof DenyBlockSchema>, "deny">;
|
|
77
|
+
export type DenyScope = {
|
|
78
|
+
/** the FleetEditable field: deny + the path segments capitalized (denyWorkersModels) */
|
|
79
|
+
key: DenyScopeKey;
|
|
80
|
+
/** the overlay path: ["routing", "deny", ...segments] */
|
|
81
|
+
path: string[];
|
|
82
|
+
/** the dotted config path the exclusion collector reports (routing.deny.workers.models) */
|
|
83
|
+
dotted: string;
|
|
84
|
+
};
|
|
85
|
+
export declare function denyScopesOf(schema: z.ZodObject<z.ZodRawShape>, segments?: string[]): Array<{
|
|
86
|
+
key: string;
|
|
87
|
+
path: string[];
|
|
88
|
+
dotted: string;
|
|
89
|
+
}>;
|
|
90
|
+
export declare const DENY_SCOPES: readonly DenyScope[];
|
|
91
|
+
/** the staged-set key the fleet browser uses for a scope: its key minus the `deny` prefix (denyWorkersAdapters → workersAdapters) */
|
|
92
|
+
export type StagedDenyKey = DenyScopeKey extends `deny${infer Rest}` ? Uncapitalize<Rest> : never;
|
|
93
|
+
export declare const stagedDenyKeyOf: (scope: DenyScope) => StagedDenyKey;
|
|
94
|
+
/** the FleetApp prop that seeds one scope's staged set (initialDenyWorkersAdapters). Lives here, not in
|
|
95
|
+
* fleet-app.tsx, so the fleet command needs no runtime import of the Ink module (the print path must
|
|
96
|
+
* never load Ink, and chalk reads FORCE_COLOR at import time). */
|
|
97
|
+
export type InitialDenyProps = {
|
|
98
|
+
[K in DenyScopeKey as `initial${Capitalize<K>}`]?: string[];
|
|
99
|
+
};
|
|
100
|
+
export declare const initialDenyPropOf: (scope: DenyScope) => keyof InitialDenyProps;
|
|
101
|
+
/** the authored list at one deny scope of a routing block, undefined when the scope is absent */
|
|
102
|
+
export declare function denyEntriesAt(routing: TickmarkrConfig["routing"], scope: DenyScope): string[] | undefined;
|
|
103
|
+
/** a routing.deny block built from per-scope lists (absent or empty lists are left out) */
|
|
104
|
+
export declare function denyBlockFrom(lists: Partial<Record<DenyScopeKey, string[]>>): NonNullable<TickmarkrConfig["routing"]["deny"]>;
|
|
65
105
|
export declare const ModelPricingSchema: z.ZodObject<{
|
|
66
106
|
inPerMtok: z.ZodNumber;
|
|
67
107
|
outPerMtok: z.ZodNumber;
|
|
@@ -371,11 +411,9 @@ export type FleetTierAssignment = {
|
|
|
371
411
|
tier: Tier;
|
|
372
412
|
provenance?: string;
|
|
373
413
|
};
|
|
374
|
-
export type FleetEditable = {
|
|
414
|
+
export type FleetEditable = Partial<Record<DenyScopeKey, string[]>> & {
|
|
375
415
|
denyAdapters: string[];
|
|
376
416
|
denyModels: string[];
|
|
377
|
-
denyWorkersAdapters?: string[];
|
|
378
|
-
denyWorkersModels?: string[];
|
|
379
417
|
allowOut?: string[];
|
|
380
418
|
tiers: Record<string, Record<string, FleetTierAssignment | null>>;
|
|
381
419
|
map: Record<string, MapEntry>;
|
package/dist/config/config.js
CHANGED
|
@@ -87,9 +87,50 @@ const PrefBlockSchema = z.object({
|
|
|
87
87
|
adapters: z.array(z.string()).optional(),
|
|
88
88
|
models: z.array(z.string()).optional(),
|
|
89
89
|
});
|
|
90
|
-
|
|
90
|
+
// exported so a test can extend the PRODUCTION deny shape and watch the enumeration grow (OBS-1099 add.1)
|
|
91
|
+
export const DenyBlockSchema = PrefBlockSchema.extend({
|
|
91
92
|
workers: PrefBlockSchema.optional(),
|
|
92
93
|
});
|
|
94
|
+
export function denyScopesOf(schema, segments = []) {
|
|
95
|
+
const unwrap = (t) => t instanceof z.ZodOptional ? unwrap(t.unwrap()) : t;
|
|
96
|
+
return Object.entries(schema.shape).flatMap(([name, type]) => {
|
|
97
|
+
const inner = unwrap(type);
|
|
98
|
+
const below = [...segments, name];
|
|
99
|
+
if (inner instanceof z.ZodObject)
|
|
100
|
+
return denyScopesOf(inner, below);
|
|
101
|
+
if (!(inner instanceof z.ZodArray))
|
|
102
|
+
return [];
|
|
103
|
+
const path = ["routing", "deny", ...below];
|
|
104
|
+
return [{ key: `deny${below.map((s) => s[0].toUpperCase() + s.slice(1)).join("")}`, path, dotted: path.join(".") }];
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
export const DENY_SCOPES = denyScopesOf(DenyBlockSchema);
|
|
108
|
+
export const stagedDenyKeyOf = (scope) => (scope.key[4].toLowerCase() + scope.key.slice(5));
|
|
109
|
+
export const initialDenyPropOf = (scope) => `initial${scope.key[0].toUpperCase()}${scope.key.slice(1)}`;
|
|
110
|
+
/** the authored list at one deny scope of a routing block, undefined when the scope is absent */
|
|
111
|
+
export function denyEntriesAt(routing, scope) {
|
|
112
|
+
let cur = routing;
|
|
113
|
+
for (const segment of scope.path.slice(1)) {
|
|
114
|
+
if (cur === undefined || cur === null || typeof cur !== "object")
|
|
115
|
+
return undefined;
|
|
116
|
+
cur = cur[segment];
|
|
117
|
+
}
|
|
118
|
+
return Array.isArray(cur) ? cur : undefined;
|
|
119
|
+
}
|
|
120
|
+
/** a routing.deny block built from per-scope lists (absent or empty lists are left out) */
|
|
121
|
+
export function denyBlockFrom(lists) {
|
|
122
|
+
const block = {};
|
|
123
|
+
for (const scope of DENY_SCOPES) {
|
|
124
|
+
const entries = lists[scope.key];
|
|
125
|
+
if (!entries?.length)
|
|
126
|
+
continue;
|
|
127
|
+
let cur = block;
|
|
128
|
+
for (const segment of scope.path.slice(2, -1))
|
|
129
|
+
cur = (cur[segment] ??= {});
|
|
130
|
+
cur[scope.path[scope.path.length - 1]] = [...entries];
|
|
131
|
+
}
|
|
132
|
+
return block;
|
|
133
|
+
}
|
|
93
134
|
// v1.20 REC-02: operator-maintained price table for the usage/cost report (src/report/cost.ts). Two
|
|
94
135
|
// channel economics, never conflated (spec cost model): API = tokens × per-Mtok rate; sub = flat plan
|
|
95
136
|
// amortized over a windows/month RANGE (+ an API-equivalent counterfactual when the model has a rate).
|
|
@@ -924,8 +965,8 @@ export function fleetEditableFromConfig(cfg, universe) {
|
|
|
924
965
|
tiers[adapter][model] = { tier };
|
|
925
966
|
}
|
|
926
967
|
}
|
|
927
|
-
|
|
928
|
-
const
|
|
968
|
+
// every enumerated deny scope rides verbatim (sorted) under its own key
|
|
969
|
+
const denyLists = Object.fromEntries(DENY_SCOPES.map((scope) => [scope.key, [...(denyEntriesAt(cfg.routing, scope) ?? [])].sort()]));
|
|
929
970
|
let allowOut;
|
|
930
971
|
if (universe !== undefined) {
|
|
931
972
|
const { allow } = cfg.routing;
|
|
@@ -947,13 +988,8 @@ export function fleetEditableFromConfig(cfg, universe) {
|
|
|
947
988
|
allowOut = [...new Set([...adaptersOut, ...modelsOut])].sort();
|
|
948
989
|
}
|
|
949
990
|
return {
|
|
950
|
-
|
|
951
|
-
denyModels,
|
|
991
|
+
...denyLists,
|
|
952
992
|
...(allowOut !== undefined ? { allowOut } : {}),
|
|
953
|
-
// OBS-994: workers-only deny is never folded into the all-seats complement above — it is
|
|
954
|
-
// returned verbatim beside the flat scopes so the fleet browser can show and toggle it.
|
|
955
|
-
denyWorkersAdapters: [...(cfg.routing.deny?.workers?.adapters ?? [])].sort(),
|
|
956
|
-
denyWorkersModels: [...(cfg.routing.deny?.workers?.models ?? [])].sort(),
|
|
957
993
|
tiers,
|
|
958
994
|
map: structuredClone(cfg.routing.map),
|
|
959
995
|
floors: { ...cfg.routing.floors },
|
|
@@ -992,14 +1028,9 @@ export function formatFleetPrint(repoRoot, opts = {}) {
|
|
|
992
1028
|
node.comment = ` ${fleetKeyLayer(repoRoot, dotted, opts)}`;
|
|
993
1029
|
}
|
|
994
1030
|
};
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
annotate(["routing", "deny", "models"]);
|
|
999
|
-
if (effective.routing.deny?.workers?.adapters !== undefined)
|
|
1000
|
-
annotate(["routing", "deny", "workers", "adapters"]);
|
|
1001
|
-
if (effective.routing.deny?.workers?.models !== undefined)
|
|
1002
|
-
annotate(["routing", "deny", "workers", "models"]);
|
|
1031
|
+
for (const scope of DENY_SCOPES)
|
|
1032
|
+
if (denyEntriesAt(effective.routing, scope) !== undefined)
|
|
1033
|
+
annotate(scope.path);
|
|
1003
1034
|
for (const shape of Object.keys(effective.routing.map))
|
|
1004
1035
|
annotate(["routing", "map", shape]);
|
|
1005
1036
|
annotate(["routing", "floors"]);
|
|
@@ -1,6 +1,13 @@
|
|
|
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, universeEntryMatches } from "./config.js";
|
|
3
|
+
import { DENY_SCOPES, universeCovers, universeEntryMatches } from "./config.js";
|
|
4
|
+
// OBS-1099 add.1: the writer ranges over the schema-derived scopes. The flat (all-seats) scopes
|
|
5
|
+
// take part in the membership/allow-form write; every nested scope (routing.deny.workers.*) is a
|
|
6
|
+
// literal deny list written on its own. ponytail: "flat" = directly under routing.deny. Read
|
|
7
|
+
// lazily: config.ts imports this module, so the enumeration is not initialized at load time.
|
|
8
|
+
const flatDenyScopes = () => DENY_SCOPES.filter((scope) => scope.path.length === 3);
|
|
9
|
+
const nestedDenyScopes = () => DENY_SCOPES.filter((scope) => scope.path.length > 3);
|
|
10
|
+
const listsOf = (write, scope) => [write.initial[scope.key] ?? [], write.edited[scope.key] ?? []];
|
|
4
11
|
/** Fleet-owned overlay keys — the only config surface `tickmarkr fleet` may write. */
|
|
5
12
|
export const FLEET_OVERLAY_KEYS = ["routing", "tiers"];
|
|
6
13
|
function fleetSubset(obj) {
|
|
@@ -24,7 +31,7 @@ function allowFormFromExclusions(universe, edited) {
|
|
|
24
31
|
// LEG2-T3 round 2 finding 1: every staged entry excludes what it NAMES — a bare model id every
|
|
25
32
|
// adapter serving it, an identity its alias — never only an adapter id or an adapter:model key.
|
|
26
33
|
// OBS-1046: the staged allow complement is a reason of its own, beside the authored deny lists.
|
|
27
|
-
const entries = [...
|
|
34
|
+
const entries = [...flatDenyScopes().flatMap((scope) => edited[scope.key] ?? []), ...(edited.allowOut ?? [])];
|
|
28
35
|
const adapters = [];
|
|
29
36
|
const models = [];
|
|
30
37
|
let excluded = false;
|
|
@@ -190,9 +197,9 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
190
197
|
// OBS-1046: each flat scope (and the allow complement) is compared on its own — a scope the
|
|
191
198
|
// session never edited keeps its node byte for byte, whatever its sibling did.
|
|
192
199
|
const changed = (before = [], after = []) => sortedUnique(before).join() !== sortedUnique(after).join();
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
if (
|
|
200
|
+
const FLAT_DENY_SCOPES = flatDenyScopes();
|
|
201
|
+
const flatChanged = new Map(FLAT_DENY_SCOPES.map((scope) => [scope.key, changed(...listsOf(write, scope))]));
|
|
202
|
+
if ([...flatChanged.values()].some(Boolean) || changed(initial.allowOut, edited.allowOut)) {
|
|
196
203
|
if (write.universe) {
|
|
197
204
|
// Membership write: the allow form IS the fleet; deny adapters/models scopes are tombstoned
|
|
198
205
|
// so a lower layer can never re-exclude behind the operator's back (workers untouched).
|
|
@@ -230,15 +237,13 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
230
237
|
// LEG2-T3 finding 4: an authored entry the edit admits (staged nowhere any more) must go
|
|
231
238
|
// even from a scope whose own set did not change, or the admitted channel stays excluded
|
|
232
239
|
// behind the preserved bytes.
|
|
233
|
-
const stagedAfter = new Set([...
|
|
234
|
-
const admitted = [...
|
|
240
|
+
const stagedAfter = new Set([...FLAT_DENY_SCOPES.flatMap((scope) => edited[scope.key] ?? []), ...(edited.allowOut ?? [])]);
|
|
241
|
+
const admitted = [...FLAT_DENY_SCOPES.flatMap((scope) => initial[scope.key] ?? []), ...(initial.allowOut ?? [])]
|
|
235
242
|
.filter((entry) => !stagedAfter.has(entry));
|
|
236
|
-
const
|
|
237
|
-
[
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
for (const [scope, before, after, touched] of scopes) {
|
|
241
|
-
const path = ["routing", "deny", scope];
|
|
243
|
+
for (const scope of FLAT_DENY_SCOPES) {
|
|
244
|
+
const [before, after] = listsOf(write, scope);
|
|
245
|
+
const touched = flatChanged.get(scope.key);
|
|
246
|
+
const path = scope.path;
|
|
242
247
|
const authored = authoredEntries(doc, path);
|
|
243
248
|
const stale = authored.some((entry) => admitted.includes(entry));
|
|
244
249
|
if (!touched && !stale)
|
|
@@ -254,28 +259,24 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
254
259
|
}
|
|
255
260
|
}
|
|
256
261
|
else {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
setStringSequencePreservingComments(doc,
|
|
262
|
+
for (const scope of FLAT_DENY_SCOPES) {
|
|
263
|
+
if (!flatChanged.get(scope.key))
|
|
264
|
+
continue;
|
|
265
|
+
const after = edited[scope.key] ?? [];
|
|
266
|
+
setStringSequencePreservingComments(doc, scope.path, after.length ? sortedUnique(after) : null);
|
|
262
267
|
}
|
|
263
268
|
}
|
|
264
269
|
}
|
|
265
270
|
// OBS-994/FL-1: routing.deny.workers is a literal deny list, never a universe-derived
|
|
266
271
|
// membership scope — it never routes through the allow-complement dance above, in either
|
|
267
|
-
// branch. Same tombstone/comment-preserving rules as the flat scopes.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
setStringSequencePreservingComments(doc, ["routing", "deny", "workers", "adapters"], editedWorkersAdapters.length ? sortedUnique(editedWorkersAdapters) : null);
|
|
276
|
-
}
|
|
277
|
-
if (sortedUnique(initialWorkersModels).join() !== sortedUnique(editedWorkersModels).join()) {
|
|
278
|
-
setStringSequencePreservingComments(doc, ["routing", "deny", "workers", "models"], editedWorkersModels.length ? sortedUnique(editedWorkersModels) : null);
|
|
272
|
+
// branch. Same tombstone/comment-preserving rules as the flat scopes. Each nested scope
|
|
273
|
+
// mutates independently — an untouched sibling must not be rewritten (a `null` tombstone over
|
|
274
|
+
// an absent/untouched sibling would mask a lower layer's own workers scope).
|
|
275
|
+
for (const scope of nestedDenyScopes()) {
|
|
276
|
+
const [before, after] = listsOf(write, scope);
|
|
277
|
+
if (!changed(before, after))
|
|
278
|
+
continue;
|
|
279
|
+
setStringSequencePreservingComments(doc, scope.path, after.length ? sortedUnique(after) : null);
|
|
279
280
|
}
|
|
280
281
|
for (const shape of new Set([...Object.keys(initial.map), ...Object.keys(edited.map)])) {
|
|
281
282
|
const before = initial.map[shape];
|
|
@@ -420,8 +421,11 @@ export function fleetRepoOverlayFromDelta(initial, edited, existingRepo = {}, fi
|
|
|
420
421
|
const out = structuredClone(existingRepo);
|
|
421
422
|
const routing = { ...out.routing };
|
|
422
423
|
let routingTouched = false;
|
|
423
|
-
|
|
424
|
-
|
|
424
|
+
// OBS-1099 add.1: the flat scopes (routing.deny.<leaf>) and the allow complement are one
|
|
425
|
+
// membership write; a list or its tombstone lands at every flat scope the enumeration names
|
|
426
|
+
const changedAt = (scope) => sortedUnique(initial[scope.key] ?? []).join() !== sortedUnique(edited[scope.key] ?? []).join();
|
|
427
|
+
const listOrTombstone = (entries) => entries.length ? entries : null;
|
|
428
|
+
const denyChanged = flatDenyScopes().some(changedAt)
|
|
425
429
|
|| sortedUnique(initial.allowOut ?? []).join() !== sortedUnique(edited.allowOut ?? []).join();
|
|
426
430
|
if (denyChanged) {
|
|
427
431
|
if (universe) {
|
|
@@ -434,44 +438,25 @@ export function fleetRepoOverlayFromDelta(initial, edited, existingRepo = {}, fi
|
|
|
434
438
|
}
|
|
435
439
|
else
|
|
436
440
|
delete routing.allow;
|
|
437
|
-
const residualAdapters = residualDeny(universe, edited.denyAdapters);
|
|
438
|
-
const residualModels = residualDeny(universe, edited.denyModels);
|
|
439
441
|
routing.deny = {
|
|
440
442
|
...routing.deny,
|
|
441
|
-
|
|
442
|
-
models: residualModels.length ? residualModels : null,
|
|
443
|
+
...Object.fromEntries(flatDenyScopes().map((scope) => [scope.path[scope.path.length - 1], listOrTombstone(residualDeny(universe, edited[scope.key] ?? []))])),
|
|
443
444
|
};
|
|
444
445
|
}
|
|
445
446
|
else {
|
|
446
|
-
routing.deny =
|
|
447
|
-
adapters: edited.denyAdapters.length ? edited.denyAdapters : null,
|
|
448
|
-
models: edited.denyModels.length ? edited.denyModels : null,
|
|
449
|
-
};
|
|
447
|
+
routing.deny = Object.fromEntries(flatDenyScopes().map((scope) => [scope.path[scope.path.length - 1], listOrTombstone(edited[scope.key] ?? [])]));
|
|
450
448
|
}
|
|
451
449
|
routingTouched = true;
|
|
452
450
|
}
|
|
453
|
-
// OBS-994/FL-1:
|
|
454
|
-
// Each sub-path is included only when it actually changed — an
|
|
455
|
-
// rewritten as a `null` tombstone over whatever the existing repo
|
|
456
|
-
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
if (workersAdaptersChanged || workersModelsChanged) {
|
|
463
|
-
routing.deny = {
|
|
464
|
-
...routing.deny,
|
|
465
|
-
workers: {
|
|
466
|
-
...(routing.deny?.workers),
|
|
467
|
-
...(workersAdaptersChanged
|
|
468
|
-
? { adapters: editedWorkersAdapters.length ? editedWorkersAdapters : null }
|
|
469
|
-
: {}),
|
|
470
|
-
...(workersModelsChanged
|
|
471
|
-
? { models: editedWorkersModels.length ? editedWorkersModels : null }
|
|
472
|
-
: {}),
|
|
473
|
-
},
|
|
474
|
-
};
|
|
451
|
+
// OBS-994/FL-1: a nested scope (routing.deny.workers.*) is a literal list, independent of the
|
|
452
|
+
// universe/allow dance above. Each sub-path is included only when it actually changed — an
|
|
453
|
+
// untouched sibling must not be rewritten as a `null` tombstone over whatever the existing repo
|
|
454
|
+
// overlay already held.
|
|
455
|
+
for (const scope of nestedDenyScopes().filter(changedAt)) {
|
|
456
|
+
let cur = (routing.deny = { ...routing.deny });
|
|
457
|
+
for (const segment of scope.path.slice(2, -1))
|
|
458
|
+
cur = (cur[segment] = { ...cur[segment] });
|
|
459
|
+
cur[scope.path[scope.path.length - 1]] = listOrTombstone(edited[scope.key] ?? []);
|
|
475
460
|
routingTouched = true;
|
|
476
461
|
}
|
|
477
462
|
// pool widened to accept the null tombstone; MapEntry itself never carries null in memory.
|
package/dist/drivers/orca.d.ts
CHANGED
|
@@ -81,7 +81,7 @@ export declare function terminalWorktree(term: Record<string, unknown>): string
|
|
|
81
81
|
* it keeps its resolved spelling: deterministic, and still comparable to another spelling of itself.
|
|
82
82
|
*/
|
|
83
83
|
export declare function canonicalWorktreePath(path: string): string;
|
|
84
|
-
/** The proof line a worker terminal prints first: recovery and focus read it back
|
|
84
|
+
/** The proof line a worker terminal prints first: create, recovery and focus read it back. */
|
|
85
85
|
export declare const CHECKOUT_MARK = "TICKMARKR_CHECKOUT";
|
|
86
86
|
/** The exact bytes the create command prints as its first line. */
|
|
87
87
|
export declare function checkoutProofLine(checkout: string): string;
|
package/dist/drivers/orca.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, linkSync, mkdirSync, readdirSync, readFileSync, readlinkSyn
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { shq } from "../adapters/types.js";
|
|
5
|
-
import { createWorktree, sh } from "../run/git.js";
|
|
5
|
+
import { createWorktree, FORK_CAP_ENV, resolvedForkCap, sh } from "../run/git.js";
|
|
6
6
|
import { Journal, parseRunId } from "../run/journal.js";
|
|
7
7
|
import { stateDirName } from "../graph/graph.js";
|
|
8
8
|
import { readWatchBoard, requestWatchBoardStop, stopWatchBoard, WATCH_OWNER_ENV } from "../run/supervision.js";
|
|
@@ -201,7 +201,7 @@ function enclosesCheckout(reported, checkout) {
|
|
|
201
201
|
const tracked = canonicalWorktreePath(reported);
|
|
202
202
|
return tracked === checkout || checkout.startsWith(`${tracked}/`);
|
|
203
203
|
}
|
|
204
|
-
/** The proof line a worker terminal prints first: recovery and focus read it back
|
|
204
|
+
/** The proof line a worker terminal prints first: create, recovery and focus read it back. */
|
|
205
205
|
export const CHECKOUT_MARK = "TICKMARKR_CHECKOUT";
|
|
206
206
|
// FX-N05: the proof is a FRAMED value — `TICKMARKR_CHECKOUT <byteLength>:<utf8 bytes as hex>;` — so it
|
|
207
207
|
// carries no whitespace or quotes, survives renderer wrapping (hex rows re-join losslessly), and is
|
|
@@ -209,6 +209,7 @@ export const CHECKOUT_MARK = "TICKMARKR_CHECKOUT";
|
|
|
209
209
|
// never decode to this one; `A` versus `A B`, `…--T1` versus `…--T10` are different frames.
|
|
210
210
|
const CHECKOUT_FRAME_RE = /TICKMARKR_CHECKOUT (\d+):([0-9a-f]*)(;?)/g;
|
|
211
211
|
const PROOF_PAGES = 16; // pages read from the oldest cursor before the proof is declared absent
|
|
212
|
+
const CHECKOUT_PROOF_TIMEOUT_MS = 2_000; // allow the wrapper shell to emit its startup proof
|
|
212
213
|
/** The exact bytes the create command prints as its first line. */
|
|
213
214
|
export function checkoutProofLine(checkout) {
|
|
214
215
|
const bytes = Buffer.from(checkout, "utf8");
|
|
@@ -764,9 +765,10 @@ export class OrcaDriver {
|
|
|
764
765
|
// The selector names the TRACKED worktree enclosing this slot's checkout outright — never the
|
|
765
766
|
// UI's active worktree (`active`/`current`) nor the daemon's cwd — and the command itself moves
|
|
766
767
|
// into the checkout, because Orca cannot be asked to place a terminal in a path it does not
|
|
767
|
-
// track (OBS-1004).
|
|
768
|
+
// track (OBS-1004). Shell startup can swallow that prefix; prove it from scrollback below.
|
|
769
|
+
const payload = `export ${FORK_CAP_ENV}=${shq(process.env[FORK_CAP_ENV] ?? resolvedForkCap())}; ${cmd}`;
|
|
768
770
|
const env = await this.call("create", [
|
|
769
|
-
"terminal", "create", "--worktree", `path:${tracked}`, "--title", st.title, "--command", inCheckout(st.cwd,
|
|
771
|
+
"terminal", "create", "--worktree", `path:${tracked}`, "--title", st.title, "--command", inCheckout(st.cwd, payload),
|
|
770
772
|
], this.cliCwd(st));
|
|
771
773
|
const term = requireTerminal("create", env);
|
|
772
774
|
const handle = str(term.handle);
|
|
@@ -788,6 +790,21 @@ export class OrcaDriver {
|
|
|
788
790
|
catch { /* already gone, or no longer provably ours — never a blind retry */ }
|
|
789
791
|
throw this.latched("create", st, `create receipt bound to ${worktree ?? "no worktree"}, not the tracked ${tracked} enclosing ${st.cwd}`, env.raw);
|
|
790
792
|
}
|
|
793
|
+
// A receipt only proves placement on the enclosing tracked checkout. Wait briefly for the
|
|
794
|
+
// startup command's proof before run() can resolve and the daemon can count a worker launch.
|
|
795
|
+
const deadline = this.time.now() + CHECKOUT_PROOF_TIMEOUT_MS;
|
|
796
|
+
let proof = await this.checkoutProven(handle, st.cwd, this.cliCwd(st), env.runtimeId);
|
|
797
|
+
while (!proof.proven && this.time.now() < deadline) {
|
|
798
|
+
await this.time.sleep(POLL_MS);
|
|
799
|
+
proof = await this.checkoutProven(handle, st.cwd, this.cliCwd(st), env.runtimeId);
|
|
800
|
+
}
|
|
801
|
+
if (!proof.proven) {
|
|
802
|
+
try {
|
|
803
|
+
await this.closeTerminal({ ...st, handle, runtimeId: env.runtimeId });
|
|
804
|
+
}
|
|
805
|
+
catch { /* best effort under the answering runtime; the latch prevents another launch */ }
|
|
806
|
+
throw this.latched("create", st, `terminal ${handle} does not prove checkout ${st.cwd} (${proof.reason})`, env.raw);
|
|
807
|
+
}
|
|
791
808
|
st.handle = handle;
|
|
792
809
|
// The handle is bound to the runtime identity that ANSWERED its create.
|
|
793
810
|
st.runtimeId = env.runtimeId;
|
package/dist/gates/baseline.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type EvidenceArtifact, type GateEvidenceReceipt, type ShellReceipt } from "../run/protocol.js";
|
|
1
2
|
import type { TickmarkrConfig } from "../config/config.js";
|
|
2
3
|
import type { AcceptanceItem } from "../graph/schema.js";
|
|
3
4
|
import { type RunCapacity, type ShellOptions, type ShResult } from "../run/git.js";
|
|
@@ -13,8 +14,69 @@ import type { GateResult } from "./types.js";
|
|
|
13
14
|
declare module "./types.js" {
|
|
14
15
|
interface GateResult {
|
|
15
16
|
capacity?: RunCapacity;
|
|
17
|
+
evidenceReceipt?: GateEvidenceReceipt;
|
|
18
|
+
evidenceReceipts?: GateEvidenceReceipt[];
|
|
16
19
|
}
|
|
17
20
|
}
|
|
21
|
+
/** Evidence is observational: the command verdict never depends on this store. */
|
|
22
|
+
export interface GateEvidenceOptions {
|
|
23
|
+
artifactDir?: string;
|
|
24
|
+
runId?: string;
|
|
25
|
+
taskId?: string;
|
|
26
|
+
attempt?: number;
|
|
27
|
+
subjectCommit?: string;
|
|
28
|
+
quotaBytes?: number;
|
|
29
|
+
env?: NodeJS.ProcessEnv;
|
|
30
|
+
/** Injectable storage boundary; mandatory manifest/reporter writes do not use it. */
|
|
31
|
+
write?: (path: string, bytes: Buffer) => void;
|
|
32
|
+
}
|
|
33
|
+
export declare const EVIDENCE_TAIL_BYTES: number;
|
|
34
|
+
export declare const EVIDENCE_RUN_QUOTA_BYTES: number;
|
|
35
|
+
export declare function redactGateOutput(text: string, env: NodeJS.ProcessEnv): string;
|
|
36
|
+
/** Resolve a snapshot reference against actual bytes: eviction never fabricates a live artifact. */
|
|
37
|
+
export declare function resolveEvidenceArtifact(root: string, ref: EvidenceArtifact): EvidenceArtifact;
|
|
38
|
+
export declare function beginGateEvidence(cwd: string, gate: string, command: string, opts?: GateEvidenceOptions, nonce?: string): {
|
|
39
|
+
root: string;
|
|
40
|
+
history: {
|
|
41
|
+
invocationId: string;
|
|
42
|
+
subject: {
|
|
43
|
+
runId: string;
|
|
44
|
+
taskId: string | null;
|
|
45
|
+
attempt: number | null;
|
|
46
|
+
gate: string;
|
|
47
|
+
subjectCommit: string | null;
|
|
48
|
+
};
|
|
49
|
+
termination: {
|
|
50
|
+
kind: "unknown" | "signal" | "not-started" | "exit" | "timeout";
|
|
51
|
+
exitCode: number | null;
|
|
52
|
+
signal: string | null;
|
|
53
|
+
timedOut: boolean | null;
|
|
54
|
+
};
|
|
55
|
+
availability: "available" | "not-started" | "capture-failed" | "killed" | "expired" | "missing";
|
|
56
|
+
redaction: {
|
|
57
|
+
material: boolean;
|
|
58
|
+
};
|
|
59
|
+
stdout: {
|
|
60
|
+
path: string;
|
|
61
|
+
availability: "available" | "not-started" | "capture-failed" | "killed" | "expired" | "missing";
|
|
62
|
+
sha256: string | null;
|
|
63
|
+
retainedBytes: number;
|
|
64
|
+
droppedBytes: number;
|
|
65
|
+
truncated: boolean;
|
|
66
|
+
};
|
|
67
|
+
stderr: {
|
|
68
|
+
path: string;
|
|
69
|
+
availability: "available" | "not-started" | "capture-failed" | "killed" | "expired" | "missing";
|
|
70
|
+
sha256: string | null;
|
|
71
|
+
retainedBytes: number;
|
|
72
|
+
droppedBytes: number;
|
|
73
|
+
truncated: boolean;
|
|
74
|
+
};
|
|
75
|
+
nonce?: string | undefined;
|
|
76
|
+
}[];
|
|
77
|
+
observe(receipt: ShellReceipt): void;
|
|
78
|
+
finish(stdout?: string, stderr?: string): GateEvidenceReceipt;
|
|
79
|
+
};
|
|
18
80
|
export interface BaselineCommand {
|
|
19
81
|
/**
|
|
20
82
|
* Absent when the capture returned no verdict — see `infra`. A pre-v1.90 baseline can also lack it
|
|
@@ -66,6 +128,10 @@ export interface BaselineFileDuration {
|
|
|
66
128
|
durationMs: number;
|
|
67
129
|
}
|
|
68
130
|
export interface Baseline {
|
|
131
|
+
/** No command ran because the dependency inventory could not establish isolation. */
|
|
132
|
+
refusal?: string;
|
|
133
|
+
/** Observational capture receipts stay outside the forgiveness-bearing command entries. */
|
|
134
|
+
evidenceReceipts?: Record<string, GateEvidenceReceipt>;
|
|
69
135
|
commands: Record<string, BaselineCommand>;
|
|
70
136
|
warnings?: BaselineWarning[];
|
|
71
137
|
}
|
|
@@ -158,6 +224,7 @@ export declare const CAPTURE_CEILING_MS = 1800000;
|
|
|
158
224
|
* count is safe to apply as a deficit floor. A null count compares nothing and is safe as-is. */
|
|
159
225
|
export declare function staleFileCountCommands(baseline: Baseline, commands: Record<string, string>, cwd: string): string[];
|
|
160
226
|
export interface BaselineReceiptOptions {
|
|
227
|
+
evidence?: GateEvidenceOptions;
|
|
161
228
|
onReceipt?: ShellOptions["onReceipt"];
|
|
162
229
|
/** Only comparison's build command may carry task-build identity; capture never does. */
|
|
163
230
|
taskBuildAttribution?: ShellOptions["receiptAttribution"];
|