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
|
@@ -101,6 +101,14 @@ export const CLAUDE_INPUT_BOX = declareInputBox("claude-code", {
|
|
|
101
101
|
export function claudeSlug(real) {
|
|
102
102
|
return real.replace(/[^A-Za-z0-9]/g, "-");
|
|
103
103
|
}
|
|
104
|
+
// Shared resolution for context and resume identity: never scan a neighbouring session.
|
|
105
|
+
function sessionTranscriptPath(session) {
|
|
106
|
+
const real = realpathSync(session.cwd);
|
|
107
|
+
const sid = session.id.replace(/\.jsonl$/i, "");
|
|
108
|
+
if (!sid || sid.includes("/") || sid.includes("\\") || sid.includes(".."))
|
|
109
|
+
return null;
|
|
110
|
+
return join(homedir(), ".claude", "projects", claudeSlug(real), `${sid}.jsonl`);
|
|
111
|
+
}
|
|
104
112
|
// newest-first by mtime, bounded — mtime picks WHICH files to scan, never a record's cursor.
|
|
105
113
|
// Shared by collectUsage (spend) and readClaudeAliasIdentity (OBS-145): same store, same bounds.
|
|
106
114
|
function newestSessionFiles(dir) {
|
|
@@ -318,19 +326,24 @@ export const claudeCode = {
|
|
|
318
326
|
return undefined; // missing dir / any throw ⇒ fail open
|
|
319
327
|
}
|
|
320
328
|
},
|
|
329
|
+
readSessionTranscript(session) {
|
|
330
|
+
try {
|
|
331
|
+
const file = sessionTranscriptPath(session);
|
|
332
|
+
return file ? { bytes: readFileSync(file).byteLength } : null;
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
},
|
|
321
338
|
// v1.23 T1: last-turn context fill from ~/.claude/projects/<slug>/<sessionId>.jsonl ONLY.
|
|
322
339
|
// tokens = input_tokens + cache_creation_input_tokens + cache_read_input_tokens of the LAST
|
|
323
340
|
// assistant usage record (not a sum over turns — ctx-watch.sh class; overseer wake signal).
|
|
324
341
|
// Disk read only: no claude spawn, no pane, no network. null = unknown.
|
|
325
342
|
contextUsage(session) {
|
|
326
343
|
try {
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
// session id is a filename stem (herdr agent_session.value); refuse path traversal.
|
|
330
|
-
const sid = session.id.replace(/\.jsonl$/i, "");
|
|
331
|
-
if (!sid || sid.includes("/") || sid.includes("\\") || sid.includes(".."))
|
|
344
|
+
const file = sessionTranscriptPath(session);
|
|
345
|
+
if (!file)
|
|
332
346
|
return null;
|
|
333
|
-
const file = join(homedir(), ".claude", "projects", slug, `${sid}.jsonl`);
|
|
334
347
|
let text;
|
|
335
348
|
try {
|
|
336
349
|
text = readFileSync(file, "utf8").slice(0, MAX_SESSION_BYTES);
|
package/dist/adapters/prompt.js
CHANGED
|
@@ -18,7 +18,7 @@ Goal: ${task.goal}
|
|
|
18
18
|
|
|
19
19
|
## Acceptance criteria (you will be judged against these, verbatim)
|
|
20
20
|
${list(task.acceptance.map(renderAcceptanceItem))}
|
|
21
|
-
${task.files.length ? `\n## File scope — touch ONLY paths matching:\n${list(task.files)}\n` : ""}${task.context.length ? `\n## Context (read these first)\n${list(task.context)}\n` : ""}
|
|
21
|
+
${task.files.length ? `\n## File scope — touch ONLY paths matching:\n${list(task.files)}\n` : ""}${task.context.length ? `\n## Context (read these first)\n${list(task.context)}\n` : ""}${task.outOfScope?.length ? `\n## Out of scope\nStay within the task’s declared bounds; do not pursue these items:\n${list(task.outOfScope)}\n` : ""}
|
|
22
22
|
## Rules
|
|
23
23
|
- Work only inside the current directory (your isolated worktree). Never push. Never switch branches.
|
|
24
24
|
- Make small atomic git commits as you go (git add + git commit, conventional messages).
|
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);
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -188,6 +188,9 @@ export interface WorkerAdapter {
|
|
|
188
188
|
listModelsFetchedAt?(): string | undefined;
|
|
189
189
|
collectUsage?(cwd: string, sinceMs: number): TokenUsage | undefined;
|
|
190
190
|
contextUsage?(session: SessionRef): ContextUsage | null;
|
|
191
|
+
readSessionTranscript?(session: SessionRef): {
|
|
192
|
+
bytes: number;
|
|
193
|
+
} | null;
|
|
191
194
|
trust?(repoRoot: string): TrustVerdict;
|
|
192
195
|
trustDialog: TrustDialog;
|
|
193
196
|
inputBox?: InputBox;
|
|
@@ -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
|
}
|
|
@@ -115,7 +115,7 @@ A run is green only when ALL of these hold: the run-end event exists in the jour
|
|
|
115
115
|
|
|
116
116
|
### Verified handoffs
|
|
117
117
|
|
|
118
|
-
When relaying missions between agents, never use bare send-text (\`herdr agent send\` / pane send-text) — it omits Enter. Use \`herdr pane run <pane> "<message>"\` or \`herdr notification show "<message>"\`. Confirm delivery by reading the TARGET composer afterward; if the sent text still sits unsubmitted after ~15 s, send-keys enter to that pane once and read again (OBS-975);
|
|
118
|
+
When relaying missions between agents, never use bare send-text (\`herdr agent send\` / pane send-text) — it omits Enter. Use \`herdr pane run <pane> "<message>"\` or \`herdr notification show "<message>"\`. Confirm delivery by reading the TARGET composer afterward; if the sent text still sits unsubmitted after ~15 s, retry the Enter only when that composer read shows the staged text and no permission prompt or numbered choice holds focus — an Enter sent onto an active prompt approves it or picks a choice instead of submitting the message (OBS-1119). Under that guard, send-keys enter to that pane once and read again (OBS-975); when a prompt or choice holds focus instead, resolve it first and re-read before retrying. Never report "relayed" without read-back.
|
|
119
119
|
|
|
120
120
|
### Orient before you act — this block may be the ONLY guidance your host loaded
|
|
121
121
|
|
|
@@ -11,6 +11,7 @@ import { estimateCosts } from "../../report/cost.js";
|
|
|
11
11
|
import { assignmentChannel, buildOperatorRecord, formatChannelMoney, formatChannelTokens, formatTokenUsage as fields, totalTokens as total, } from "../../report/operator-record.js";
|
|
12
12
|
import { cellsOf, cellSummary } from "../../route/profile.js";
|
|
13
13
|
import { Journal, loadRoutingProfile } from "../../run/journal.js";
|
|
14
|
+
import { formatTipProof, runEndTipProof } from "../../run/daemon.js";
|
|
14
15
|
import { deriveRunCockpitData } from "../../tui/cockpit/derive.js";
|
|
15
16
|
const n = (x) => x.toLocaleString("en-US"); // explicit locale — CI/darwin flake guard
|
|
16
17
|
const EM = "—";
|
|
@@ -169,6 +170,15 @@ const VERIFICATION_READING = {
|
|
|
169
170
|
failed: "FAILED — the run did not verify its own tip",
|
|
170
171
|
absent: "absent — no tip verification recorded: neither passed nor failed",
|
|
171
172
|
};
|
|
173
|
+
function verificationReading(runId, events) {
|
|
174
|
+
const state = verificationOf(runId, events);
|
|
175
|
+
const proof = runEndTipProof(closedCycle(events));
|
|
176
|
+
if ((state === "passed" || state === "cached") && proof.gates?.length) {
|
|
177
|
+
const allCarried = proof.gates.every(({ kind }) => kind === "reused");
|
|
178
|
+
return `${allCarried ? "cached" : "passed"} — ${formatTipProof(proof)}`;
|
|
179
|
+
}
|
|
180
|
+
return VERIFICATION_READING[state];
|
|
181
|
+
}
|
|
172
182
|
// VIS-07 / REC-01: derived only from the run journal, telemetry, and local configuration.
|
|
173
183
|
export function renderMarkdownRecord(runId, events, prices = [], rows = []) {
|
|
174
184
|
const runStart = events.find((e) => e.event === "run-start");
|
|
@@ -205,7 +215,7 @@ export function renderMarkdownRecord(runId, events, prices = [], rows = []) {
|
|
|
205
215
|
`- **done:** ${count("done")}`,
|
|
206
216
|
`- **failed:** ${count("failed")}`,
|
|
207
217
|
`- **human:** ${count("human")}`,
|
|
208
|
-
`- **verification:** ${
|
|
218
|
+
`- **verification:** ${verificationReading(runId, events)}`,
|
|
209
219
|
"",
|
|
210
220
|
"## Usage & efficiency",
|
|
211
221
|
"",
|
|
@@ -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({
|
|
@@ -307,6 +307,8 @@ export async function verify(argv, cwd = process.cwd()) {
|
|
|
307
307
|
try {
|
|
308
308
|
linkNodeModules(cwd, baseDir, { force: true });
|
|
309
309
|
baseline = await captureBaseline(baseDir, commands);
|
|
310
|
+
if (baseline.refusal)
|
|
311
|
+
return { out: baseline.refusal, code: 1 };
|
|
310
312
|
const missing = verdictlessCommands(baseline, commands);
|
|
311
313
|
if (missing.length) {
|
|
312
314
|
writeFileSync(verdictlessMarker, JSON.stringify({ base: mergeBase, commands: missing }) + "\n");
|
|
@@ -328,6 +330,7 @@ export async function verify(argv, cwd = process.cwd()) {
|
|
|
328
330
|
worktree: cwd, baseRef: mergeBase,
|
|
329
331
|
result: { ok: true, summary: "standalone verify — no worker claims to trust", deviations: [], raw: "" },
|
|
330
332
|
author, commands, baseline, channels, ...(judgeChannels ? { judgeChannels } : {}), adapters, cfg,
|
|
333
|
+
evidence: options.evidence,
|
|
331
334
|
verificationScope: "standalone", artifactDir, stateDir: join(stateRoot, ".tickmarkr"),
|
|
332
335
|
onGate: (e) => {
|
|
333
336
|
if (e.phase === "start")
|
|
@@ -345,17 +348,27 @@ export async function verify(argv, cwd = process.cwd()) {
|
|
|
345
348
|
return match ? [{ classification: match[1], note: match[2], reviewer: result.meta?.reviewer }] : [];
|
|
346
349
|
}));
|
|
347
350
|
const artifactPath = join(artifactDir, "verify-results.json");
|
|
348
|
-
|
|
349
|
-
|
|
351
|
+
const artifactBytes = Buffer.from(JSON.stringify({ base: baseTip, head, mergeBase, green, gateRows: results, reviewFindings }, null, 2) + "\n");
|
|
352
|
+
let artifactSha256 = null;
|
|
353
|
+
let artifactAvailability = "capture-failed";
|
|
354
|
+
try {
|
|
355
|
+
writeFileSync(artifactPath, artifactBytes, { flag: "wx" });
|
|
356
|
+
artifactSha256 = createHash("sha256").update(artifactBytes).digest("hex");
|
|
357
|
+
artifactAvailability = "available";
|
|
358
|
+
console.error(`verify: artifacts written to ${artifactPath}`);
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
console.error(`verify: evidence capture failed for ${artifactPath}`);
|
|
362
|
+
}
|
|
350
363
|
const review = reviewRows.at(-1);
|
|
351
364
|
if (recordJournal && review) {
|
|
352
365
|
recordJournal.append("review-leg2", values.task ?? "VERIFY", {
|
|
353
|
-
base: baseTip, head, mergeBase, author: channelKey(author), artifactPath,
|
|
366
|
+
base: baseTip, head, mergeBase, author: channelKey(author), artifactPath, artifactSha256, artifactAvailability,
|
|
354
367
|
...review,
|
|
355
368
|
});
|
|
356
369
|
}
|
|
357
370
|
if (values.json) {
|
|
358
|
-
return { out: JSON.stringify({ base: baseTip, head, mergeBase, green, artifactPath, results }, null, 2), code: green ? 0 : 2 };
|
|
371
|
+
return { out: JSON.stringify({ base: baseTip, head, mergeBase, green, artifactPath, artifactSha256, artifactAvailability, results }, null, 2), code: green ? 0 : 2 };
|
|
359
372
|
}
|
|
360
373
|
const lines = results.map((r) => `${r.pass ? "PASS" : "FAIL"} ${r.gate}\n${r.details.split("\n").map((l) => ` ${l}`).join("\n")}`);
|
|
361
374
|
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;
|