tickmarkr 2.5.8 → 2.5.9
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/qwen.js +30 -3
- 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/verify.d.ts +4 -1
- package/dist/cli/commands/verify.js +16 -5
- package/dist/cli/help.d.ts +2 -0
- package/dist/cli/help.js +6 -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/gates/baseline.d.ts +65 -0
- package/dist/gates/baseline.js +163 -9
- package/dist/gates/review.d.ts +6 -4
- package/dist/gates/review.js +62 -21
- package/dist/gates/run-gates.d.ts +4 -1
- package/dist/gates/run-gates.js +21 -11
- package/dist/gates/test-manifest.d.ts +11 -1
- package/dist/gates/test-manifest.js +41 -21
- package/dist/run/daemon.js +84 -41
- package/dist/run/journal.d.ts +17 -2
- package/dist/run/journal.js +99 -11
- package/dist/run/merge.d.ts +13 -2
- package/dist/run/merge.js +74 -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/ink/fleet-app.d.ts +12 -22
- package/dist/tui/ink/fleet-app.js +520 -131
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/SKILL.md +61 -36
- package/skills/tickmarkr-overseer/scripts/watch-context.sh +14 -2
package/dist/adapters/qwen.js
CHANGED
|
@@ -21,7 +21,7 @@ function decodeQwenEvents(events) {
|
|
|
21
21
|
continue;
|
|
22
22
|
if ("type" in event && event.type === "assistant" && "message" in event) {
|
|
23
23
|
const message = event.message;
|
|
24
|
-
if (message && typeof message === "object" && "content" in message && Array.isArray(message.content)) {
|
|
24
|
+
if (message && typeof message === "object" && (!("role" in message) || message.role === "assistant") && "content" in message && Array.isArray(message.content)) {
|
|
25
25
|
for (const content of message.content) {
|
|
26
26
|
if (!content || typeof content !== "object" || !(("type" in content) && content.type === "text"))
|
|
27
27
|
continue;
|
|
@@ -64,7 +64,7 @@ function decodeQwenEvents(events) {
|
|
|
64
64
|
failed = true;
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
-
const assistantText = text.join("
|
|
67
|
+
const assistantText = text.join("");
|
|
68
68
|
const apiError = assistantText.match(/\[API Error:[^\n]*/)?.[0];
|
|
69
69
|
if (apiError)
|
|
70
70
|
failed = true;
|
|
@@ -95,6 +95,33 @@ function eventArray(raw) {
|
|
|
95
95
|
}
|
|
96
96
|
return undefined;
|
|
97
97
|
}
|
|
98
|
+
// OBS-1085: a pane capture can begin inside an event's string. Do not repair that
|
|
99
|
+
// event or scan its prose for trailers. A candidate must decode all the way to
|
|
100
|
+
// the surviving array close: this prevents a nested object or an escaped tool
|
|
101
|
+
// echo from masquerading as a top-level event. Keep terminal failures, but only
|
|
102
|
+
// assistant events can contribute completion text. No partial tail is accepted.
|
|
103
|
+
function eventSuffix(raw) {
|
|
104
|
+
// Shell prompts and stderr after the stream may contain `]`; use the event
|
|
105
|
+
// array's object-plus-close boundary, just as eventArray does.
|
|
106
|
+
const close = raw.lastIndexOf("}]");
|
|
107
|
+
if (close === -1)
|
|
108
|
+
return undefined;
|
|
109
|
+
const end = close + 1;
|
|
110
|
+
const boundaries = /,\s*(?=\{)/g;
|
|
111
|
+
for (const boundary of raw.matchAll(boundaries)) {
|
|
112
|
+
const start = boundary.index + boundary[0].length;
|
|
113
|
+
if (start >= end)
|
|
114
|
+
break;
|
|
115
|
+
try {
|
|
116
|
+
const events = JSON.parse(`[${raw.slice(start, end + 1)}`);
|
|
117
|
+
if (!Array.isArray(events) || !events.every((event) => event !== null && typeof event === "object" && "type" in event && typeof event.type === "string"))
|
|
118
|
+
continue;
|
|
119
|
+
return events;
|
|
120
|
+
}
|
|
121
|
+
catch { /* still inside the clipped event; try the next boundary */ }
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
98
125
|
// Decode qwen's JSON envelope before scanning only decoded assistant text for the worker trailer.
|
|
99
126
|
export function parseQwenResult(raw, nonce) {
|
|
100
127
|
const malformed = {
|
|
@@ -104,7 +131,7 @@ export function parseQwenResult(raw, nonce) {
|
|
|
104
131
|
raw,
|
|
105
132
|
cause: raw.trim() ? "malformed-verdict" : "empty-output",
|
|
106
133
|
};
|
|
107
|
-
const parsed = eventArray(raw);
|
|
134
|
+
const parsed = eventArray(raw) ?? eventSuffix(raw);
|
|
108
135
|
if (!parsed)
|
|
109
136
|
return malformed;
|
|
110
137
|
const decoded = decodeQwenEvents(parsed);
|
|
@@ -88,10 +88,16 @@ export function permittedDecisionVerbs(park) {
|
|
|
88
88
|
}
|
|
89
89
|
if (park.kind === "infra")
|
|
90
90
|
return ["approve", "recheck"];
|
|
91
|
+
// OBS-1084: an authoring park whose newest gate row is a red tool gate is a runner report (a load
|
|
92
|
+
// flake in an unowned suite), so the battery re-runs on the preserved ref with no worker.
|
|
93
|
+
if (park.kind === "authoring" && isToolGate(park.failedGate))
|
|
94
|
+
return ["approve", "recheck"];
|
|
91
95
|
if (park.kind === "diff-cap")
|
|
92
96
|
return ["recheck"]; // OBS-1007: a cap trip is re-gated under a raised cap, never re-bought
|
|
93
97
|
return ["approve"];
|
|
94
98
|
}
|
|
99
|
+
const TOOL_GATES = ["build", "test", "lint"];
|
|
100
|
+
const isToolGate = (gate) => gate !== undefined && TOOL_GATES.includes(gate);
|
|
95
101
|
/** The release marker this command appends for a verb on a park — the fact a read-back must match. */
|
|
96
102
|
export function releaseForDecision(verb, park) {
|
|
97
103
|
if (verb === "waive")
|
|
@@ -170,7 +176,8 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
170
176
|
const capPark = park?.kind === ATTEMPT_CAP_RELEASE;
|
|
171
177
|
const gateFailPark = park?.kind === "gate-fail";
|
|
172
178
|
const infraPark = park?.kind === "infra" || park?.kind === "diff-cap";
|
|
173
|
-
const
|
|
179
|
+
const authoringRunnerPark = park?.kind === "authoring" && isToolGate(park.failedGate);
|
|
180
|
+
const failedGate = gateFailPark || authoringRunnerPark ? park?.failedGate : undefined;
|
|
174
181
|
if ((park?.kind === "scope-request" && !decisions) || files !== undefined) {
|
|
175
182
|
if (park?.kind !== "scope-request")
|
|
176
183
|
throw new Error("--files requires a scope-request park");
|
|
@@ -203,6 +210,7 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
203
210
|
}
|
|
204
211
|
journal.append("task-approved", taskId, {
|
|
205
212
|
by, ...(reason ? { reason } : {}), via: "cli", release: "scope-request",
|
|
213
|
+
...(reviewRoundCeiling === undefined ? {} : { reviewRoundCeiling }), // OBS-1083
|
|
206
214
|
amendment: { from, to: graphDefinitionHash(amended), beforeFiles: task.files, files: amendedFiles, parkLine: park.line, definition: taskDefinitionFingerprint(task) },
|
|
207
215
|
});
|
|
208
216
|
// Do not write graph.json from this process while the daemon owns it: its sweep materializes
|
|
@@ -231,14 +239,18 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
231
239
|
return disposition(cwd, runId, "fund-fixed-attempt", `upheld the reviewer for ${taskId} in ${runId} — by ${by}`, serialization.contended);
|
|
232
240
|
}
|
|
233
241
|
if (recheck) {
|
|
234
|
-
if ((!gateFailPark || !failedGate) && !infraPark) {
|
|
235
|
-
throw new Error(`--recheck applies to a gate-fail, infra
|
|
242
|
+
if ((!gateFailPark || !failedGate) && !infraPark && !authoringRunnerPark) {
|
|
243
|
+
throw new Error(`--recheck applies to a gate-fail, infra, diff-cap or red-tool-gate authoring park; ${taskId}'s newest park is ${String(lastHuman?.data.kind ?? "none")} with failed gate ${failedGate ?? "none"} — refusing`);
|
|
236
244
|
}
|
|
237
245
|
journal.append("task-approved", taskId, {
|
|
238
246
|
by,
|
|
239
247
|
...(reason ? { reason } : {}),
|
|
240
248
|
via: "cli",
|
|
241
249
|
release: RECHECK_RELEASE,
|
|
250
|
+
// OBS-1084: failedGate names the runner report for every recheck. Authoring rechecks additionally
|
|
251
|
+
// carry gate so their durable release row identifies the red tool gate that made recheck admissible.
|
|
252
|
+
...(failedGate ? { failedGate } : {}),
|
|
253
|
+
...(authoringRunnerPark && failedGate ? { gate: failedGate } : {}),
|
|
242
254
|
...(reviewRoundCeiling === undefined ? {} : { reviewRoundCeiling }),
|
|
243
255
|
});
|
|
244
256
|
return disposition(cwd, runId, "re-dispatch", `re-checking ${taskId} in ${runId} — by ${by}; ${failedGate ? `failed gate ${failedGate}` : `${park.kind} park`}; no gate marked satisfied`, serialization.contended);
|
|
@@ -1,23 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { dirname } from "node:path";
|
|
3
4
|
import { tickmarkrDir } from "../../graph/graph.js";
|
|
4
|
-
import { SUPERVISION_BEAT_MS, SUPERVISION_DEFAULT_THRESHOLD_PCT, SUPERVISION_STALE_MS, SUPERVISION_TIERS, beatSupervision, resolveSupervisionRoot, supervisionStandDownPath, } from "../../run/supervision.js";
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// ONE-SHOT AND ERROR-PROPAGATING, deliberately not `armSupervision`. That helper is the right shape for
|
|
12
|
-
// a long-lived node process and the wrong shape here twice over. Its recurring interval would outlive
|
|
13
|
-
// the invocation — in a long-lived dispatcher a later `--stand-down` would disarm only its own timer
|
|
14
|
-
// while an earlier one kept writing, flipping DISARMED back to ARMED ten seconds later: the exact
|
|
15
|
-
// fail-open this instrument exists to catch. And it swallows write failures by design (an unwritten
|
|
16
|
-
// beat ages out, which is the truth for a watcher that must not be crashed by its own instrument) —
|
|
17
|
-
// but a CLI that swallows them PRINTS a state nothing recorded, so `beat` says ARMED while `status`
|
|
18
|
-
// reads UNREADABLE. Here the writes are unguarded: a failure leaves the process with a non-zero exit
|
|
19
|
-
// and a message instead of a claim. The LOOP belongs to the caller, which is what makes the beat
|
|
20
|
-
// evidence: stop calling it and the tier ages into STALE on its own.
|
|
5
|
+
import { SUPERVISION_BEAT_MS, SUPERVISION_DEFAULT_THRESHOLD_PCT, SUPERVISION_STALE_MS, SUPERVISION_TIERS, beatSupervision, newSupervisionArm, resolveSupervisionRoot, supervisionStandDownPath, } from "../../run/supervision.js";
|
|
6
|
+
// One-shot ticks reuse a durable arm; only --new-arm (or --loop) acknowledges a stand-down.
|
|
7
|
+
// The loop runs in this process, so its recorded pid belongs to the actual repeating writer.
|
|
8
|
+
// Writes are unguarded and a refused ONE-SHOT tick THROWS: the shipped watcher gates on the exit
|
|
9
|
+
// code with stdout discarded, so a refusal that exits 0 leaves a live watcher believing it is armed
|
|
10
|
+
// while status reads DISARMED. Only the loop returns on stand-down — exiting is its designed end.
|
|
21
11
|
const VALUE_OPTIONS = ["--seat", "--arm-id", "--pct", "--threshold-pct"];
|
|
22
12
|
export async function beat(argv, cwd = process.cwd()) {
|
|
23
13
|
const standDown = argv.includes("--stand-down");
|
|
@@ -28,11 +18,11 @@ export async function beat(argv, cwd = process.cwd()) {
|
|
|
28
18
|
const named = argv.find((a, i) => !a.startsWith("--") && !VALUE_OPTIONS.includes(argv[i - 1] ?? ""));
|
|
29
19
|
if (!isTier(named)) {
|
|
30
20
|
throw new Error(`usage: tickmarkr beat <${SUPERVISION_TIERS.join("|")}> --seat <identity> ` +
|
|
31
|
-
`[--arm-id <identity> --pct <0..100> --threshold-pct <0..100>] [--stand-down] — ` +
|
|
21
|
+
`[--new-arm] [--loop] [--arm-id <identity> --pct <0..100> --threshold-pct <0..100>] [--stand-down] — ` +
|
|
32
22
|
`got ${named ? `\`${named}\`` : "no tier"}`);
|
|
33
23
|
}
|
|
34
24
|
// SUP-05: NO SEAT, NO BEAT — and the refusal comes before any write, so a refused invocation leaves
|
|
35
|
-
// the tier exactly as it found it.
|
|
25
|
+
// the tier exactly as it found it. In one-shot mode the exitedWriterPid it records has
|
|
36
26
|
// already exited by the time anyone reads the record, so tier + writer + instant is a beat nobody can
|
|
37
27
|
// attribute to a seat. Measured 2026-08-26: a consult seat ran the documented loop verbatim and the
|
|
38
28
|
// board read that tier ARMED with no seat of that tier having armed anything, and a seatless ARMED
|
|
@@ -46,12 +36,21 @@ export async function beat(argv, cwd = process.cwd()) {
|
|
|
46
36
|
const repoRoot = resolveSupervisionRoot(cwd);
|
|
47
37
|
if (standDown)
|
|
48
38
|
return standDownTier(repoRoot, named, seat);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
39
|
+
const loop = argv.includes("--loop");
|
|
40
|
+
const arm = argv.includes("--new-arm") || loop ? newSupervisionArm(repoRoot, named, armId) : undefined;
|
|
41
|
+
// Context arm ids retain their existing obligation semantics; they do not implicitly re-arm liveness.
|
|
42
|
+
const observation = pct === undefined ? undefined : { armId: armId ?? arm?.armId ?? seat, pct, thresholdPct };
|
|
43
|
+
do {
|
|
44
|
+
if (!beatSupervision(repoRoot, named, seat, observation, { arm, armId, loop })) {
|
|
45
|
+
const refusal = `${named} DISARMED — stood down; use --new-arm to resume beating`;
|
|
46
|
+
if (loop)
|
|
47
|
+
return refusal;
|
|
48
|
+
throw new Error(refusal);
|
|
49
|
+
}
|
|
50
|
+
if (!loop)
|
|
51
|
+
break;
|
|
52
|
+
await new Promise((resolve) => setTimeout(resolve, SUPERVISION_BEAT_MS));
|
|
53
|
+
} while (loop);
|
|
55
54
|
return `${named} ARMED as ${seat} — beat again every ${SUPERVISION_BEAT_MS / 1_000}s; the tier reads STALE ${SUPERVISION_STALE_MS / 1_000}s after the last beat`;
|
|
56
55
|
}
|
|
57
56
|
/** `--seat <identity>` or `--seat=<identity>`; blank and missing are the same answer — none. */
|
|
@@ -84,15 +83,15 @@ function percentageOf(argv, name) {
|
|
|
84
83
|
// so the tier reads DISARMED rather than ageing out as a death. Published atomically — written aside,
|
|
85
84
|
// renamed over — because a torn marker is rejected by the reader, and a rejected stand-down reports a
|
|
86
85
|
// deliberate hand-off as a death.
|
|
87
|
-
function standDownTier(repoRoot, tier, seat) {
|
|
86
|
+
export function standDownTier(repoRoot, tier, seat) {
|
|
88
87
|
tickmarkrDir(repoRoot); // the write path DOES create — markers land inside the gitignored state dir
|
|
89
88
|
const p = supervisionStandDownPath(repoRoot, tier);
|
|
90
|
-
const tmp = `${p}.${
|
|
89
|
+
const tmp = `${p}.${randomUUID()}.tmp`;
|
|
91
90
|
mkdirSync(dirname(p), { recursive: true });
|
|
92
91
|
// The marker names the seat for the same reason the beat does: "someone stood this tier down" is not
|
|
93
92
|
// a hand-off anyone can act on, and on a seat tier the reader rejects an anonymous one outright.
|
|
94
93
|
writeFileSync(tmp, JSON.stringify({
|
|
95
|
-
tier, seat, exitedWriterPid: process.pid, disarmedAt: new Date().toISOString(),
|
|
94
|
+
tier, seat, standDownId: randomUUID(), exitedWriterPid: process.pid, disarmedAt: new Date().toISOString(),
|
|
96
95
|
}) + "\n");
|
|
97
96
|
renameSync(tmp, p);
|
|
98
97
|
return `${tier} DISARMED — ${seat} handed off; status reads it stood down, not dead`;
|
|
@@ -2,6 +2,8 @@ import { isAbsolute, join } from "node:path";
|
|
|
2
2
|
import { parseArgs } from "node:util";
|
|
3
3
|
import { collateralLints, sourceScopeFindings, sourceScopeLints, } from "../../compile/collateral.js";
|
|
4
4
|
import { CompileError } from "../../compile/common.js";
|
|
5
|
+
import { loadConfig } from "../../config/config.js";
|
|
6
|
+
import { denyPreferCollisionLine, denyPreferCollisions } from "../../route/preference.js";
|
|
5
7
|
import { compileSource } from "../../compile/index.js";
|
|
6
8
|
import { retiredLiteralErrors } from "../../compile/retired-literals.js";
|
|
7
9
|
import { clearCompileRefusal, saveCompileRefusal, saveGraph, stateDirName } from "../../graph/graph.js";
|
|
@@ -82,6 +84,15 @@ export async function compile(argv, cwd = process.cwd(), harnessFrom = process.a
|
|
|
82
84
|
if (uncovered.length > 0) {
|
|
83
85
|
throw new CompileError(`${src} has uncovered declared pin obligations:\n${uncovered.map((line) => ` - ${line}`).join("\n")}${diagnostics}`);
|
|
84
86
|
}
|
|
87
|
+
// v2.5.9 T11 (OBS-1086): a shape whose pool the config denies in full is unroutable for every task
|
|
88
|
+
// that uses it; plan already said so, compile now refuses the seal before any state write so a dry
|
|
89
|
+
// run reaches the same refusal. Pool only: a partial deny leaves live members, and pin/prefer
|
|
90
|
+
// collisions on the default map stay doctor's and resume's business. Scoped to the graph's shapes.
|
|
91
|
+
const deadPools = denyPreferCollisions(loadConfig(cwd), g.tasks.map((task) => task.shape))
|
|
92
|
+
.filter((collision) => collision.kind === "pool");
|
|
93
|
+
if (deadPools.length > 0) {
|
|
94
|
+
throw new CompileError(`${src} routes a task shape to a fully denied pool:\n${deadPools.map((collision) => ` - ${denyPreferCollisionLine(collision)}`).join("\n")}${diagnostics}`);
|
|
95
|
+
}
|
|
85
96
|
// One bounded read supplies both cross-run surfaces: unresolved findings below and merge facts for
|
|
86
97
|
// the ancestry check. Neither fact mutates the compiled graph; status and every readiness predicate
|
|
87
98
|
// remain the source compiler's answer.
|
|
@@ -5,7 +5,7 @@ import { allAdapters, discoverChannels, doctorAgeMs, initDoctorReuse, modelAuthE
|
|
|
5
5
|
import { catalogModelAdvisory, catalogTierRanking, declaredModelWindow, fleetUnclassifiedModels } from "../../adapters/model-lints.js";
|
|
6
6
|
import { CATALOG_REFRESH_TIMEOUT_MS, formatCatalogRefreshLegs, readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog-remote.js";
|
|
7
7
|
import { CLAUDE_ALIAS_IDENTITY_STAMPS, readClaudeAliasIdentity } from "../../adapters/claude-code.js";
|
|
8
|
-
import { fleetEditableFromConfig, fleetEditableEquals, formatFleetPrint, globalConfigDir, loadConfigWithMode, overlayBytesLoadError, readOverlayFile, renderFleetOverlayWrite, repoOverlayPath, ROUTING_MODES, TIER_RANK, unifiedYamlDiff, } from "../../config/config.js";
|
|
8
|
+
import { fleetEditableFromConfig, fleetEditableEquals, formatFleetPrint, globalConfigDir, DENY_SCOPES, initialDenyPropOf, loadConfigWithMode, overlayBytesLoadError, readOverlayFile, stagedDenyKeyOf, renderFleetOverlayWrite, repoOverlayPath, ROUTING_MODES, TIER_RANK, unifiedYamlDiff, } from "../../config/config.js";
|
|
9
9
|
import { exclusionReason, projectFleetWhy, renderFleetWhy } from "../../config/fleet-why.js";
|
|
10
10
|
import { SHAPES, TIERS } from "../../graph/schema.js";
|
|
11
11
|
import { doctor } from "./doctor.js";
|
|
@@ -320,14 +320,12 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
320
320
|
]));
|
|
321
321
|
const channels = discoverChannels(cfg, adapters, health);
|
|
322
322
|
const profile = loadRoutingProfile(cwd, cfg, { preview: true });
|
|
323
|
+
const stagedDenyOf = (editable) => Object.fromEntries(DENY_SCOPES.map((scope) => [stagedDenyKeyOf(scope), editable[scope.key] ?? []]));
|
|
323
324
|
const stagedEditable = (map, deny) => ({
|
|
324
325
|
...structuredClone(initial),
|
|
325
|
-
|
|
326
|
-
denyModels: deny.models,
|
|
326
|
+
...Object.fromEntries(DENY_SCOPES.map((scope) => [scope.key, deny[stagedDenyKeyOf(scope)]])),
|
|
327
327
|
// OBS-1046: the allow complement is staged beside the deny lists, never folded into them
|
|
328
328
|
allowOut: deny.allowOut ?? initial.allowOut,
|
|
329
|
-
denyWorkersAdapters: deny.workersAdapters,
|
|
330
|
-
denyWorkersModels: deny.workersModels,
|
|
331
329
|
map,
|
|
332
330
|
});
|
|
333
331
|
const candidateMemo = new Map();
|
|
@@ -573,39 +571,57 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
573
571
|
const floor = cfgPreview.routing.floors[shape];
|
|
574
572
|
const collisions = denyPreferCollisions(cfgPreview, [shape]);
|
|
575
573
|
const discovered = new Set(previewChannels.map((c) => `${c.adapter}:${c.model}`));
|
|
576
|
-
// discovery already dropped a channel whose model probe failed — it
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
574
|
+
// T2 review (D-225): discovery already dropped a channel whose model probe failed — it is
|
|
575
|
+
// rebuilt as a structured channel from its adapter's own declaration and walks the SAME
|
|
576
|
+
// reach + collector path as every other row, the auth reason appended LAST; never a bare string
|
|
577
|
+
const withIdentity = (adapter, declared) => {
|
|
578
|
+
const identity = health[adapter]?.modelAuth?.[declared.model]?.identity ?? health[adapter]?.modelIdentities?.[declared.model];
|
|
579
|
+
return identity ? { ...declared, identity } : declared;
|
|
580
|
+
};
|
|
581
|
+
const failedProbes = unauthedModels.filter(({ key }) => !discovered.has(key)).flatMap(({ key, adapter, reason }) => {
|
|
582
|
+
const declared = adapters.find((a) => a.id === adapter)?.channels(cfg).find((c) => `${c.adapter}:${c.model}` === key);
|
|
583
|
+
return declared ? [{ ...withIdentity(adapter, declared), authReason: `unauthed (${reason}) — re-probe with tickmarkr doctor` }] : [];
|
|
584
|
+
});
|
|
585
|
+
// T2 review round 6: an installed-but-unauthed CLI is dropped by discovery AND by the model
|
|
586
|
+
// probe list, so its configured channels are rebuilt from the adapter's own declaration too —
|
|
587
|
+
// each greys with its reach and covering entry, the CLI auth reason last; never one bare adapter line
|
|
588
|
+
const unauthedCliChannels = unauthedClis.flatMap((adapter) => (adapters.find((a) => a.id === adapter)?.channels(cfg) ?? [])
|
|
589
|
+
.map((declared) => ({ ...withIdentity(adapter, declared), authReason: `${adapter} CLI unauthed — re-probe with tickmarkr doctor` })));
|
|
590
|
+
// D-225: the ledger is the EXCLUDED half of a closed partition — a channel the picker offers
|
|
591
|
+
// never greys beside itself; its pin/pool/floor/prefer captions stay in excludedNote
|
|
592
|
+
const ledgerChannels = [...previewChannels.map((c) => ({ ...c, authReason: undefined })), ...failedProbes, ...unauthedCliChannels]
|
|
593
|
+
.filter((c) => !offered.has(`${c.adapter}:${c.model}`));
|
|
594
|
+
return ledgerChannels.flatMap((c) => {
|
|
595
|
+
const key = `${c.adapter}:${c.model}`;
|
|
596
|
+
const reasons = exclusionCollector(c, cfgPreview.routing, "worker").map(exclusionReason);
|
|
597
|
+
// OBS-1065: a greyed picker row carries its reach beside its reasons, like the models view
|
|
598
|
+
const allSeats = exclusionCollector(c, cfgPreview.routing, "judge");
|
|
599
|
+
const reach = allSeats.some((scope) => scope.by === "deny") ? "out all"
|
|
600
|
+
: allSeats.length ? "out allow" : reasons.length ? "out workers" : "in";
|
|
601
|
+
if (!c.authReason && health[c.adapter]?.modelAuth?.[c.model]?.authed === false)
|
|
602
|
+
reasons.push("unauthed — re-probe with tickmarkr doctor");
|
|
603
|
+
if (entry?.pin && key !== `${entry.pin.via}:${entry.pin.model}`) {
|
|
604
|
+
reasons.push(`not routing.map.${shape}.pin (${entry.pin.via}:${entry.pin.model}) — not manageable here`);
|
|
605
|
+
}
|
|
606
|
+
if (entry?.pool && !entry.pool.channels.includes(key)) {
|
|
607
|
+
reasons.push(`outside routing.map.${shape}.pool — not manageable here`);
|
|
608
|
+
}
|
|
609
|
+
if (floor && TIER_RANK[c.tier] < TIER_RANK[floor]) {
|
|
610
|
+
reasons.push(`below routing.floors.${shape} (${floor}) — not manageable here`);
|
|
611
|
+
}
|
|
612
|
+
if (entry?.prefer?.some((p) => entryMatchesChannel(p, c))) {
|
|
613
|
+
reasons.push(`task hint: routing.map.${shape}.prefer names it — not manageable here`);
|
|
614
|
+
}
|
|
615
|
+
for (const collision of collisions) {
|
|
616
|
+
const named = collision.detail.split(" > ").some((p) => entryMatchesChannel(p, c));
|
|
617
|
+
if (named || offered.size === 0)
|
|
618
|
+
reasons.push(denyPreferCollisionLine(collision));
|
|
619
|
+
}
|
|
620
|
+
if (c.authReason)
|
|
621
|
+
reasons.push(c.authReason);
|
|
622
|
+
// labelled like the models view's row (adapter/model), so a ledger row never reads as a candidate
|
|
623
|
+
return reasons.length ? [`${c.adapter}/${c.model} — reach: ${reach} — ${reasons.join("; ")}`] : [];
|
|
624
|
+
});
|
|
609
625
|
};
|
|
610
626
|
const candidatesForShape = (shape, mode, map, deny) => {
|
|
611
627
|
const preview = previewCfg(mode, map, deny);
|
|
@@ -616,6 +632,8 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
616
632
|
id: `${candidate.assignment.adapter}:${candidate.assignment.model}`,
|
|
617
633
|
label: candidateRow(candidate, cfg.pricing),
|
|
618
634
|
pin: { via: candidate.assignment.adapter, model: candidate.assignment.model },
|
|
635
|
+
// OBS-1065: a picker lift reads eligibility from here, not the ledger strings
|
|
636
|
+
belowFloor: candidate.belowFloor,
|
|
619
637
|
}));
|
|
620
638
|
const offered = new Set(rows.map((row) => row.id));
|
|
621
639
|
const notes = [excludedNoteFor(cfgPreview), ...shapeExclusionCaptions(shape, map, cfgPreview)].filter((note) => note !== undefined);
|
|
@@ -644,12 +662,10 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
644
662
|
let pendingWrite = null;
|
|
645
663
|
const reviewOverlay = (state) => {
|
|
646
664
|
const staged = structuredClone(initial);
|
|
647
|
-
|
|
648
|
-
|
|
665
|
+
// OBS-994/FL-1, OBS-1099 add.1: every schema-enumerated deny scope rides the same review/write funnel
|
|
666
|
+
for (const scope of DENY_SCOPES)
|
|
667
|
+
staged[scope.key] = state[scope.key];
|
|
649
668
|
staged.allowOut = state.allowOut ?? initial.allowOut;
|
|
650
|
-
// OBS-994/FL-1: the workers-only deny scope rides the same review/write funnel as the flat one.
|
|
651
|
-
staged.denyWorkersAdapters = state.denyWorkersAdapters;
|
|
652
|
-
staged.denyWorkersModels = state.denyWorkersModels;
|
|
653
669
|
staged.map = state.map;
|
|
654
670
|
const today = new Date().toISOString().slice(0, 10);
|
|
655
671
|
for (const classification of state.classifications) {
|
|
@@ -700,11 +716,8 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
700
716
|
ageMs: doctorAgeMs(cwd),
|
|
701
717
|
adapters,
|
|
702
718
|
health,
|
|
703
|
-
|
|
704
|
-
initialDenyModels: editable.denyModels,
|
|
719
|
+
...Object.fromEntries(DENY_SCOPES.map((scope) => [initialDenyPropOf(scope), editable[scope.key] ?? []])),
|
|
705
720
|
initialAllowOut: editable.allowOut,
|
|
706
|
-
initialDenyWorkersAdapters: editable.denyWorkersAdapters,
|
|
707
|
-
initialDenyWorkersModels: editable.denyWorkersModels,
|
|
708
721
|
modelGroups,
|
|
709
722
|
initialMode: rm.mode.mode,
|
|
710
723
|
modeOptions: ROUTING_MODES.map((mode) => ({ id: mode, gloss: MODE_GLOSS[mode] })),
|
|
@@ -768,11 +781,6 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
768
781
|
return {
|
|
769
782
|
props,
|
|
770
783
|
commit,
|
|
771
|
-
renderWhy: () => renderFleetWhy(projectedShapeRows(rm.mode.mode, editable.map,
|
|
772
|
-
adapters: editable.denyAdapters,
|
|
773
|
-
models: editable.denyModels,
|
|
774
|
-
workersAdapters: editable.denyWorkersAdapters ?? [],
|
|
775
|
-
workersModels: editable.denyWorkersModels ?? [],
|
|
776
|
-
})),
|
|
784
|
+
renderWhy: () => renderFleetWhy(projectedShapeRows(rm.mode.mode, editable.map, stagedDenyOf(editable))),
|
|
777
785
|
};
|
|
778
786
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Assignment, type BillingChannel } from "../../adapters/types.js";
|
|
2
|
+
import { type GateEvidenceOptions } from "../../gates/baseline.js";
|
|
2
3
|
import { type AcceptanceItem } from "../../graph/schema.js";
|
|
3
4
|
export declare function parseCriteria(text: string): AcceptanceItem[];
|
|
4
5
|
export declare const HUMAN_CHANNEL: BillingChannel;
|
|
@@ -10,7 +11,9 @@ export declare function verifyStateRoot(cwd: string): Promise<string>;
|
|
|
10
11
|
export declare function baselineCachePath(cwd: string, baseSha: string, commands: Record<string, string>): string;
|
|
11
12
|
export declare function excludeAuthorProvider(channels: BillingChannel[], author: BillingChannel): BillingChannel[];
|
|
12
13
|
export declare const VERIFY_HELP = "usage: tickmarkr verify [--base <ref>] [--criteria <file> | --task <id>] [--json]\nThe final verdict and JSON result are written to stdout; progress and diagnostics are written to stderr.\nDo not merge stdout and stderr (for example with 2>&1): doing so corrupts the verdict stream.";
|
|
13
|
-
export declare function verify(argv: string[], cwd?: string
|
|
14
|
+
export declare function verify(argv: string[], cwd?: string, options?: {
|
|
15
|
+
evidence?: GateEvidenceOptions;
|
|
16
|
+
}): Promise<{
|
|
14
17
|
out: string;
|
|
15
18
|
code: number;
|
|
16
19
|
}>;
|
|
@@ -130,7 +130,7 @@ async function warnWideTaskRange(cwd, stateRoot, taskId, mergeBase) {
|
|
|
130
130
|
export const VERIFY_HELP = `usage: tickmarkr verify [--base <ref>] [--criteria <file> | --task <id>] [--json]
|
|
131
131
|
The final verdict and JSON result are written to stdout; progress and diagnostics are written to stderr.
|
|
132
132
|
Do not merge stdout and stderr (for example with 2>&1): doing so corrupts the verdict stream.`;
|
|
133
|
-
export async function verify(argv, cwd = process.cwd()) {
|
|
133
|
+
export async function verify(argv, cwd = process.cwd(), options = {}) {
|
|
134
134
|
if (argv.some((arg) => arg === "--help" || arg === "-h"))
|
|
135
135
|
return { out: VERIFY_HELP, code: 0 };
|
|
136
136
|
const { values } = parseArgs({
|
|
@@ -328,6 +328,7 @@ export async function verify(argv, cwd = process.cwd()) {
|
|
|
328
328
|
worktree: cwd, baseRef: mergeBase,
|
|
329
329
|
result: { ok: true, summary: "standalone verify — no worker claims to trust", deviations: [], raw: "" },
|
|
330
330
|
author, commands, baseline, channels, ...(judgeChannels ? { judgeChannels } : {}), adapters, cfg,
|
|
331
|
+
evidence: options.evidence,
|
|
331
332
|
verificationScope: "standalone", artifactDir, stateDir: join(stateRoot, ".tickmarkr"),
|
|
332
333
|
onGate: (e) => {
|
|
333
334
|
if (e.phase === "start")
|
|
@@ -345,17 +346,27 @@ export async function verify(argv, cwd = process.cwd()) {
|
|
|
345
346
|
return match ? [{ classification: match[1], note: match[2], reviewer: result.meta?.reviewer }] : [];
|
|
346
347
|
}));
|
|
347
348
|
const artifactPath = join(artifactDir, "verify-results.json");
|
|
348
|
-
|
|
349
|
-
|
|
349
|
+
const artifactBytes = Buffer.from(JSON.stringify({ base: baseTip, head, mergeBase, green, gateRows: results, reviewFindings }, null, 2) + "\n");
|
|
350
|
+
let artifactSha256 = null;
|
|
351
|
+
let artifactAvailability = "capture-failed";
|
|
352
|
+
try {
|
|
353
|
+
writeFileSync(artifactPath, artifactBytes, { flag: "wx" });
|
|
354
|
+
artifactSha256 = createHash("sha256").update(artifactBytes).digest("hex");
|
|
355
|
+
artifactAvailability = "available";
|
|
356
|
+
console.error(`verify: artifacts written to ${artifactPath}`);
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
console.error(`verify: evidence capture failed for ${artifactPath}`);
|
|
360
|
+
}
|
|
350
361
|
const review = reviewRows.at(-1);
|
|
351
362
|
if (recordJournal && review) {
|
|
352
363
|
recordJournal.append("review-leg2", values.task ?? "VERIFY", {
|
|
353
|
-
base: baseTip, head, mergeBase, author: channelKey(author), artifactPath,
|
|
364
|
+
base: baseTip, head, mergeBase, author: channelKey(author), artifactPath, artifactSha256, artifactAvailability,
|
|
354
365
|
...review,
|
|
355
366
|
});
|
|
356
367
|
}
|
|
357
368
|
if (values.json) {
|
|
358
|
-
return { out: JSON.stringify({ base: baseTip, head, mergeBase, green, artifactPath, results }, null, 2), code: green ? 0 : 2 };
|
|
369
|
+
return { out: JSON.stringify({ base: baseTip, head, mergeBase, green, artifactPath, artifactSha256, artifactAvailability, results }, null, 2), code: green ? 0 : 2 };
|
|
359
370
|
}
|
|
360
371
|
const lines = results.map((r) => `${r.pass ? "PASS" : "FAIL"} ${r.gate}\n${r.details.split("\n").map((l) => ` ${l}`).join("\n")}`);
|
|
361
372
|
const verdict = green
|
package/dist/cli/help.d.ts
CHANGED
|
@@ -176,6 +176,8 @@ export declare const COMMAND_HELP: {
|
|
|
176
176
|
description: string;
|
|
177
177
|
options: {
|
|
178
178
|
"--seat <identity>": string;
|
|
179
|
+
"--new-arm": string;
|
|
180
|
+
"--loop": string;
|
|
179
181
|
"--arm-id <identity>": string;
|
|
180
182
|
"--pct <0..100>": string;
|
|
181
183
|
"--threshold-pct <0..100>": string;
|
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/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>;
|