tickmarkr 2.5.9 → 2.6.1
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.d.ts +2 -1
- package/dist/adapters/prompt.js +11 -1
- package/dist/adapters/types.d.ts +4 -0
- package/dist/adapters/types.js +10 -0
- package/dist/cli/commands/fleet.js +26 -1
- package/dist/cli/commands/init.js +1 -1
- package/dist/cli/commands/plan.js +7 -3
- package/dist/cli/commands/report.js +11 -1
- package/dist/cli/commands/status.js +13 -3
- package/dist/cli/commands/verify.js +2 -0
- package/dist/cli/help.d.ts +2 -0
- package/dist/cli/help.js +2 -0
- package/dist/compile/native.js +39 -4
- package/dist/drivers/orca.d.ts +1 -1
- package/dist/drivers/orca.js +27 -5
- package/dist/gates/baseline.d.ts +2 -0
- package/dist/gates/baseline.js +9 -1
- package/dist/gates/cache.d.ts +11 -1
- package/dist/gates/cache.js +35 -15
- package/dist/gates/llm.js +4 -1
- package/dist/gates/review.d.ts +2 -3
- package/dist/gates/review.js +28 -35
- package/dist/gates/run-gates.d.ts +4 -0
- package/dist/gates/run-gates.js +17 -4
- package/dist/gates/test-manifest.d.ts +17 -0
- package/dist/gates/test-manifest.js +109 -9
- package/dist/gates/test-reporter.js +4 -0
- package/dist/graph/graph.d.ts +7 -3
- package/dist/graph/graph.js +21 -5
- 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/route/role-pick.d.ts +16 -0
- package/dist/route/role-pick.js +15 -0
- package/dist/route/router.d.ts +14 -0
- package/dist/route/router.js +9 -1
- package/dist/run/consult.js +5 -9
- package/dist/run/daemon.d.ts +16 -1
- package/dist/run/daemon.js +554 -77
- package/dist/run/git.d.ts +46 -1
- package/dist/run/git.js +138 -6
- package/dist/run/host-health.d.ts +20 -0
- package/dist/run/host-health.js +64 -0
- package/dist/run/journal.d.ts +1 -1
- package/dist/run/journal.js +51 -6
- package/dist/run/merge.d.ts +1 -1
- package/dist/run/merge.js +30 -6
- package/dist/run/operator-state.d.ts +24 -2
- package/dist/run/operator-state.js +41 -5
- package/dist/run/stall.d.ts +38 -2
- package/dist/run/stall.js +276 -6
- package/dist/tui/cockpit/board.d.ts +1 -1
- package/dist/tui/cockpit/board.js +27 -19
- package/dist/tui/cockpit/derive.d.ts +2 -0
- package/dist/tui/cockpit/derive.js +4 -0
- 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 +3 -0
- package/dist/tui/cockpit/live-store.js +31 -8
- package/dist/tui/cockpit/run-cockpit.js +2 -1
- package/dist/tui/cockpit/run-view.d.ts +2 -4
- package/dist/tui/cockpit/run-view.js +9 -8
- package/package.json +1 -1
- package/schema/rungraph.schema.json +7 -0
- package/skills/tickmarkr-overseer/SKILL.md +76 -14
|
@@ -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);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Task } from "../graph/schema.js";
|
|
2
2
|
import { type VerdictUnparseableCause } from "../gates/verdict-cause.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type WorkerResult } from "./types.js";
|
|
4
4
|
export declare function buildTaskPrompt(task: Task, feedback?: string, nonce?: string): string;
|
|
5
5
|
export declare function writePrompt(dir: string, task: Task, attempt: number, feedback?: string, nonce?: string): string;
|
|
6
6
|
export declare function trailerPattern(nonce: string): string;
|
|
@@ -11,4 +11,5 @@ export type ClassifiedWorkerResult = WorkerResult & {
|
|
|
11
11
|
};
|
|
12
12
|
export declare function parseWorkerResult(raw: string, nonce: string): ClassifiedWorkerResult;
|
|
13
13
|
export type DeadChannelReason = "auth-required" | "setup-required" | "provider-outage" | "timeout";
|
|
14
|
+
export declare function classifyTransientCapacity(result: WorkerResult): RegExpExecArray | null;
|
|
14
15
|
export declare function classifyDeadChannel(result: WorkerResult): DeadChannelReason | undefined;
|
package/dist/adapters/prompt.js
CHANGED
|
@@ -2,6 +2,7 @@ import { copyFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { renderAcceptanceItem } from "../graph/schema.js";
|
|
4
4
|
import { classifyVerdictCause } from "../gates/verdict-cause.js";
|
|
5
|
+
import { CAPACITY_RE } from "./types.js";
|
|
5
6
|
// v1.87 T5: the scope rule states only what the runtime enforces, and no more. What it enforces:
|
|
6
7
|
// `scope.allowDeviations` comes from the operator's config, is snapshotted and frozen when a gate
|
|
7
8
|
// round starts (run-gates.ts `allowDeviations`), and is never written back — so a worker has no
|
|
@@ -18,7 +19,7 @@ Goal: ${task.goal}
|
|
|
18
19
|
|
|
19
20
|
## Acceptance criteria (you will be judged against these, verbatim)
|
|
20
21
|
${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` : ""}
|
|
22
|
+
${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
23
|
## Rules
|
|
23
24
|
- Work only inside the current directory (your isolated worktree). Never push. Never switch branches.
|
|
24
25
|
- Make small atomic git commits as you go (git add + git commit, conventional messages).
|
|
@@ -133,6 +134,15 @@ const AUTH_RE = /not logged in|please (?:log ?in|sign in)|please run [^\n]{0,30}
|
|
|
133
134
|
const SETUP_RE = /command not found|not recognized as an internal or external command|spawn \S+ ENOENT|missing required config|workspace trust (?:required|not granted)/i;
|
|
134
135
|
const OUTAGE_RE = /unable to reach the model provider|cannot reach the model provider|model provider.{0,40}(?:unavailable|unreachable)|service (?:is )?temporarily unavailable|upstream connect error|overloaded_error/i;
|
|
135
136
|
const TIMEOUT_RE = /\bETIMEDOUT\b|request timed out|connection timed out|deadline exceeded|timed out waiting for/i;
|
|
137
|
+
// OBS-1161: transient capacity is the fourth classification beside quota and the typed dead-channel
|
|
138
|
+
// reasons, and it obeys the same boundary rule: a parsed trailer — ok:true OR ok:false — is the worker
|
|
139
|
+
// speaking, so a verdict that QUOTES the capacity phrase stays ordinary work evidence. The caller
|
|
140
|
+
// hands in the chrome-filtered banner rows as `raw`, exactly as it does for classifyDeadChannel.
|
|
141
|
+
export function classifyTransientCapacity(result) {
|
|
142
|
+
if (result.ok || (result.summary !== NO_TRAILER_SUMMARY && result.summary !== UNPARSEABLE_TRAILER_SUMMARY))
|
|
143
|
+
return null;
|
|
144
|
+
return CAPACITY_RE.exec(result.raw);
|
|
145
|
+
}
|
|
136
146
|
export function classifyDeadChannel(result) {
|
|
137
147
|
// A parsed trailer — ok:true OR ok:false — is the worker speaking: genuine work outcomes walk
|
|
138
148
|
// the normal gate/ladder path even when their transcript mentions auth/outage/timeout text.
|
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;
|
|
@@ -207,5 +210,6 @@ export declare const PROMPT_ARGV_CEILING_LINUX = 120000;
|
|
|
207
210
|
export declare const PROMPT_ARGV_CEILING_DEFAULT = 900000;
|
|
208
211
|
export declare function promptArgvCeiling(platform?: string): number;
|
|
209
212
|
export declare function promptFitsArgv(promptFile: string, platform?: string): boolean;
|
|
213
|
+
export declare const CAPACITY_RE: RegExp;
|
|
210
214
|
export declare const QUOTA_RE: RegExp;
|
|
211
215
|
export declare const MODEL_ID_RE: RegExp;
|
package/dist/adapters/types.js
CHANGED
|
@@ -294,6 +294,16 @@ export function promptFitsArgv(promptFile, platform = process.platform) {
|
|
|
294
294
|
// ZAI coding-plan exhaustion text: "Insufficient balance or no resource package. Please recharge."
|
|
295
295
|
// Anchor the distinctive full phrase, not the two-word "insufficient balance" fragment — that fires
|
|
296
296
|
// on ordinary billing/wallet task output the harness edits (research Pitfall 3, 2026-07-10).
|
|
297
|
+
// OBS-1161: transient provider capacity ("Selected model is at capacity") is NOT quota — the seat
|
|
298
|
+
// comes back within minutes, so it takes bounded same-seat backoff before a same-floor failover and
|
|
299
|
+
// never permanent demotion. Kept apart from QUOTA_RE so permanent quota policy stays untouched, and
|
|
300
|
+
// anchored on the full phrase (the Pitfall-3 lesson: a bare "capacity" fires on ordinary work text).
|
|
301
|
+
// A banner is the CLI speaking, so the phrase must OPEN its row — after at most an error glyph
|
|
302
|
+
// (codex paints "■ Selected model is at capacity. …") or an "error:" label. Anything else in front —
|
|
303
|
+
// a `>` quote, a quotation mark, a sentence — is the transcript QUOTING it: work evidence, never capacity.
|
|
304
|
+
// ponytail: row-start grammar, not per-adapter fixtures — the ceiling is a worker whose own output
|
|
305
|
+
// opens a row with the bare phrase inside the banner tail; widen the glyph set only from a captured frame.
|
|
306
|
+
export const CAPACITY_RE = /^[ \t■⚠✗✘×!]*(?:(?:stream )?error:[ \t]*)?(?:(?:selected |the )?model is (?:currently |temporarily )?at capacity|(?:provider|service|server) is (?:currently )?at capacity)/im;
|
|
297
307
|
export const QUOTA_RE = /rate.?limit|quota|usage limit|out of credits|insufficient credit|insufficient balance or no resource|\b429\b/i;
|
|
298
308
|
// v1.5 MODEL-01: charset gate for detected model ids (research Pitfall 4, verified 2026-07-10).
|
|
299
309
|
// Ids come from CLI stdout / another program's JSON (models_cache.json) and are echoed into
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
|
-
import { allAdapters, discoverChannels, doctorAgeMs, initDoctorReuse, modelAuthExclusions } from "../../adapters/registry.js";
|
|
4
|
+
import { allAdapters, discoverChannels, doctorAgeMs, initDoctorReuse, modelAuthExclusions, PREFERENCE_ROLES } from "../../adapters/registry.js";
|
|
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";
|
|
@@ -11,6 +11,7 @@ import { SHAPES, TIERS } from "../../graph/schema.js";
|
|
|
11
11
|
import { doctor } from "./doctor.js";
|
|
12
12
|
import { candidateRow, costSignal, shapeCandidates } from "./fleet-picker.js";
|
|
13
13
|
import { route } from "../../route/router.js";
|
|
14
|
+
import { pickRole } from "../../route/role-pick.js";
|
|
14
15
|
import { denyPreferCollisionLine, denyPreferCollisions, disallowedBy, entryMatchesChannel, exclusionCollector } from "../../route/preference.js";
|
|
15
16
|
import { resolveRunMode } from "../../run/daemon.js";
|
|
16
17
|
import { loadRoutingProfile } from "../../run/journal.js";
|
|
@@ -85,6 +86,8 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
85
86
|
const { values } = parseArgs({
|
|
86
87
|
args: argv,
|
|
87
88
|
options: {
|
|
89
|
+
pick: { type: "string" },
|
|
90
|
+
"exclude-vendor": { type: "string", multiple: true },
|
|
88
91
|
print: { type: "boolean" },
|
|
89
92
|
why: { type: "boolean" },
|
|
90
93
|
"global-dir": { type: "string" },
|
|
@@ -92,6 +95,28 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
92
95
|
},
|
|
93
96
|
});
|
|
94
97
|
const globalDir = values["global-dir"] ?? globalConfigDir();
|
|
98
|
+
if (values.pick !== undefined) {
|
|
99
|
+
const role = PREFERENCE_ROLES.find((role) => role === values.pick);
|
|
100
|
+
const refuse = (reason) => ({ out: `tickmarkr fleet --pick ${values.pick}: ${reason}`, code: 1 });
|
|
101
|
+
if (!role)
|
|
102
|
+
return refuse("unsupported-role — expected worker, judge, review or consult; no role prefer resolved");
|
|
103
|
+
if (values.print || values.why || values.fresh)
|
|
104
|
+
return refuse("incompatible options — --pick cannot use --print, --why or --fresh");
|
|
105
|
+
const { cfg } = resolveRunMode(cwd, { globalDir });
|
|
106
|
+
const { reuse, health } = initDoctorReuse(cwd, false);
|
|
107
|
+
const selection = pickRole(role, cfg, adapters, reuse && health ? health : {}, {
|
|
108
|
+
excludeVendors: new Set(values["exclude-vendor"] ?? []),
|
|
109
|
+
});
|
|
110
|
+
if (!selection.ok) {
|
|
111
|
+
return refuse(selection.reason === "missing-prefer" ? `${role}.prefer: missing-prefer`
|
|
112
|
+
: !reuse || !health ? `${role}.prefer: probe data missing or stale — run tickmarkr doctor first`
|
|
113
|
+
: `${role}.prefer: ${selection.reason}`);
|
|
114
|
+
}
|
|
115
|
+
const { adapter, model, vendor, channel } = selection.channel;
|
|
116
|
+
return JSON.stringify({ role, adapter, model, vendor, channel });
|
|
117
|
+
}
|
|
118
|
+
if (values["exclude-vendor"]?.length)
|
|
119
|
+
return { out: "tickmarkr fleet: --exclude-vendor requires --pick <role>", code: 1 };
|
|
95
120
|
const print = values.print ?? false;
|
|
96
121
|
const why = values.why ?? false;
|
|
97
122
|
const input = io.input ?? process.stdin;
|
|
@@ -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
|
|
|
@@ -7,10 +7,10 @@ import { GLYPHS, dim, rule, title, warn } from "../../brand.js";
|
|
|
7
7
|
import { collateralLints, sourceScopeLints } from "../../compile/collateral.js";
|
|
8
8
|
import { classifyContextPath } from "../../compile/native.js";
|
|
9
9
|
import { DEFAULT_CONFIG, effectiveReviewPolicy, overlayPreferShapes, ROUTING_MODES, TIER_RANK } from "../../config/config.js";
|
|
10
|
-
import { chainDepth, dispatchWaves, graphDefinitionHash, loadGraph, stateDirName } from "../../graph/graph.js";
|
|
10
|
+
import { batteryPriority, chainDepth, dispatchWaves, graphDefinitionHash, loadGraph, stateDirName } from "../../graph/graph.js";
|
|
11
11
|
import { filesGlob } from "../../graph/files-glob.js";
|
|
12
12
|
import { renderAcceptanceItem } from "../../graph/schema.js";
|
|
13
|
-
import { resolveRunMode } from "../../run/daemon.js";
|
|
13
|
+
import { pendingDaemonApprovalActions, resolveRunMode } from "../../run/daemon.js";
|
|
14
14
|
import { disallowedBy, excludedChannels, exclusionLine, routingEntrySeatLines } from "../../route/preference.js";
|
|
15
15
|
import { decayWeight, HALF_LIFE_RUNS, staffLedEvidence } from "../../route/profile.js";
|
|
16
16
|
import { route, RoutingError } from "../../route/router.js";
|
|
@@ -372,7 +372,11 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
372
372
|
// OBS-1018: depth and wave come from the same functions the daemon admits by, so the plan shows the
|
|
373
373
|
// critical path the run will actually follow at this concurrency.
|
|
374
374
|
const depths = chainDepth(g);
|
|
375
|
-
|
|
375
|
+
// OBS-1158: waves simulate the graph the daemon would admit on resume — the journal's replayed
|
|
376
|
+
// statuses (an approval releases its park to pending) and its pending recheck approvals ranked
|
|
377
|
+
// through the same seam the daemon admits by.
|
|
378
|
+
const projected = { ...g, tasks: g.tasks.map((t) => ({ ...t, status: taskStatuses.get(t.id) ?? t.status })) };
|
|
379
|
+
const waves = dispatchWaves(projected, cfg.concurrency, batteryPriority(pendingDaemonApprovalActions(matchingEvents ?? []).values()));
|
|
376
380
|
const byId = new Map(g.tasks.map((t) => [t.id, t]));
|
|
377
381
|
const chainLine = (t) => {
|
|
378
382
|
const wave = waves.get(t.id);
|
|
@@ -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
|
"",
|
|
@@ -8,6 +8,7 @@ import { formatOwnedName, parseOwnedName, } from "../../drivers/types.js";
|
|
|
8
8
|
import { blockedTasks, graphDefinitionHash, loadGraph, stateDirName } from "../../graph/graph.js";
|
|
9
9
|
import { GATE_NAMES } from "../../graph/schema.js";
|
|
10
10
|
import { projectActivity } from "../../run/activity.js";
|
|
11
|
+
import { authorsNote, doneAuthors } from "../../run/operator-state.js";
|
|
11
12
|
import { projectOperatorSummary } from "../../run/operator-summary.js";
|
|
12
13
|
import { trackJournalRows } from "../../run/protocol.js";
|
|
13
14
|
import { newestPark, permittedDecisionVerbs } from "./approve.js";
|
|
@@ -988,6 +989,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
988
989
|
const comparable = record?.comparable ?? false;
|
|
989
990
|
const rehashAt = record?.rehashAt;
|
|
990
991
|
const assignments = new Map();
|
|
992
|
+
const authorsByTask = new Map();
|
|
991
993
|
const contexts = new Map();
|
|
992
994
|
// v1.53 T5: this run is dead — a newer run replaced it
|
|
993
995
|
const supersededBy = [...events].reverse()
|
|
@@ -1000,6 +1002,11 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1000
1002
|
assignments.set(e.taskId, `${a.adapter}:${a.model}`);
|
|
1001
1003
|
}
|
|
1002
1004
|
}
|
|
1005
|
+
if (e.event === "task-done" && e.taskId) {
|
|
1006
|
+
const authors = doneAuthors(e.data);
|
|
1007
|
+
if (authors)
|
|
1008
|
+
authorsByTask.set(e.taskId, authors);
|
|
1009
|
+
}
|
|
1003
1010
|
if (e.event === "context-sample" && e.taskId && typeof e.data.tokens === "number" && Number.isFinite(e.data.tokens)) {
|
|
1004
1011
|
contexts.set(e.taskId, e.data.tokens); // last write wins
|
|
1005
1012
|
}
|
|
@@ -1106,12 +1113,14 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1106
1113
|
const label = isStarved ? " starved" : phrase ? ` ${phrase}` : "";
|
|
1107
1114
|
const channel = journalRowsOnly ? folded?.actor ?? "-" : assignments.get(t.id) ?? "-";
|
|
1108
1115
|
const ctx = contexts.get(t.id);
|
|
1109
|
-
|
|
1116
|
+
// Merged authors are their own fact beside the last dispatch: a carry-only seat authored nothing.
|
|
1117
|
+
const authors = authorsNote(authorsByTask.get(t.id), channel);
|
|
1118
|
+
const assignCol = [channel, ctx !== undefined ? `ctx ${ctx}` : "", authors ?? ""].filter(Boolean).join(divider);
|
|
1110
1119
|
const gates = comparable
|
|
1111
1120
|
? gateSnapshot(t, events, rehashAt)
|
|
1112
1121
|
: { states: defaultGateStates(t), priorGraph: false };
|
|
1113
1122
|
const pane = panes.get(t.id);
|
|
1114
|
-
return { t, st, merged, failureKind, redTier, label, assignCol, isStarved, phrase, channel, ctx, livePhase, pane, summary, ...gates };
|
|
1123
|
+
return { t, st, merged, failureKind, redTier, label, assignCol, isStarved, phrase, channel, ctx, authors, livePhase, pane, summary, ...gates };
|
|
1115
1124
|
});
|
|
1116
1125
|
if (!unicode) {
|
|
1117
1126
|
// Keep the machine task-title columns intact; projection details occupy their own line.
|
|
@@ -1215,7 +1224,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1215
1224
|
}).join("");
|
|
1216
1225
|
const gateHeader = GATE_NAMES.map((gate) => fitCells(gate.slice(0, 2), 3)).join("");
|
|
1217
1226
|
const noteFor = (cell) => {
|
|
1218
|
-
const { t, st, merged, failureKind, redTier, states, priorGraph, isStarved, phrase, ctx, livePhase, pane } = cell;
|
|
1227
|
+
const { t, st, merged, failureKind, redTier, states, priorGraph, isStarved, phrase, ctx, livePhase, pane, authors } = cell;
|
|
1219
1228
|
const effort = effortByTask.get(t.id);
|
|
1220
1229
|
const blocking = graphTaskStatus(st, t.status) === "done" ? [] : dependents.get(t.id) ?? [];
|
|
1221
1230
|
const failed = failedGates(states);
|
|
@@ -1242,6 +1251,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1242
1251
|
blocking.length ? `blocks ${blocking.join(", ")}` : "",
|
|
1243
1252
|
pane ? `pane ${pane}` : "",
|
|
1244
1253
|
ctx !== undefined ? `ctx ${ctx}` : "",
|
|
1254
|
+
authors ?? "",
|
|
1245
1255
|
].filter(Boolean).join(" · ");
|
|
1246
1256
|
};
|
|
1247
1257
|
const staleWorker = (cell) => cell.livePhase?.phase === "worker"
|
|
@@ -307,6 +307,8 @@ export async function verify(argv, cwd = process.cwd(), options = {}) {
|
|
|
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");
|
package/dist/cli/help.d.ts
CHANGED
package/dist/cli/help.js
CHANGED
|
@@ -30,6 +30,8 @@ export const COMMAND_HELP = {
|
|
|
30
30
|
fleet: {
|
|
31
31
|
usage: "fleet [options]", description: "Edit fleet configuration interactively, or print it for scripts.",
|
|
32
32
|
options: {
|
|
33
|
+
"--pick <role>": "Resolve one role preference as JSON without launching a seat (review or consult; roles without prefer refuse).",
|
|
34
|
+
"--exclude-vendor <vendor>": "Exclude a vendor from --pick; repeat for multiple vendors.",
|
|
33
35
|
"--print": "Print fleet configuration without opening the editor.",
|
|
34
36
|
"--why": "Include routing eligibility explanations in printed output.",
|
|
35
37
|
"--global-dir <path>": "Use this global configuration directory.",
|
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/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");
|
|
@@ -245,7 +246,12 @@ export function provesCheckout(text, checkout) {
|
|
|
245
246
|
* subshell inside it all start in the checkout and its exit status is the payload's (FX-N02).
|
|
246
247
|
*/
|
|
247
248
|
export function checkoutPrefix(checkout) {
|
|
248
|
-
|
|
249
|
+
// OBS-1168: printf BUILDS the marker from two arguments, so the typed command never carries
|
|
250
|
+
// `TICKMARKR_CHECKOUT ` verbatim. The shell's echo of the typed line is repainted and truncated in
|
|
251
|
+
// the scrollback, which used to leave an INCOMPLETE frame beside the complete printed one and made
|
|
252
|
+
// provesCheckout refuse a correct launch. The printed bytes are exactly checkoutProofLine(checkout).
|
|
253
|
+
const head = CHECKOUT_MARK.slice(0, CHECKOUT_MARK.indexOf("_"));
|
|
254
|
+
return `cd ${shq(checkout)} && printf '%s%s\\n' ${head} ${shq(checkoutProofLine(checkout).slice(head.length))} && sh -c `;
|
|
249
255
|
}
|
|
250
256
|
/** The command a terminal on the tracked worktree runs so that it executes INSIDE the checkout. */
|
|
251
257
|
export function inCheckout(checkout, cmd) {
|
|
@@ -764,9 +770,10 @@ export class OrcaDriver {
|
|
|
764
770
|
// The selector names the TRACKED worktree enclosing this slot's checkout outright — never the
|
|
765
771
|
// UI's active worktree (`active`/`current`) nor the daemon's cwd — and the command itself moves
|
|
766
772
|
// into the checkout, because Orca cannot be asked to place a terminal in a path it does not
|
|
767
|
-
// track (OBS-1004).
|
|
773
|
+
// track (OBS-1004). Shell startup can swallow that prefix; prove it from scrollback below.
|
|
774
|
+
const payload = `export ${FORK_CAP_ENV}=${shq(process.env[FORK_CAP_ENV] ?? resolvedForkCap())}; ${cmd}`;
|
|
768
775
|
const env = await this.call("create", [
|
|
769
|
-
"terminal", "create", "--worktree", `path:${tracked}`, "--title", st.title, "--command", inCheckout(st.cwd,
|
|
776
|
+
"terminal", "create", "--worktree", `path:${tracked}`, "--title", st.title, "--command", inCheckout(st.cwd, payload),
|
|
770
777
|
], this.cliCwd(st));
|
|
771
778
|
const term = requireTerminal("create", env);
|
|
772
779
|
const handle = str(term.handle);
|
|
@@ -788,6 +795,21 @@ export class OrcaDriver {
|
|
|
788
795
|
catch { /* already gone, or no longer provably ours — never a blind retry */ }
|
|
789
796
|
throw this.latched("create", st, `create receipt bound to ${worktree ?? "no worktree"}, not the tracked ${tracked} enclosing ${st.cwd}`, env.raw);
|
|
790
797
|
}
|
|
798
|
+
// A receipt only proves placement on the enclosing tracked checkout. Wait briefly for the
|
|
799
|
+
// startup command's proof before run() can resolve and the daemon can count a worker launch.
|
|
800
|
+
const deadline = this.time.now() + CHECKOUT_PROOF_TIMEOUT_MS;
|
|
801
|
+
let proof = await this.checkoutProven(handle, st.cwd, this.cliCwd(st), env.runtimeId);
|
|
802
|
+
while (!proof.proven && this.time.now() < deadline) {
|
|
803
|
+
await this.time.sleep(POLL_MS);
|
|
804
|
+
proof = await this.checkoutProven(handle, st.cwd, this.cliCwd(st), env.runtimeId);
|
|
805
|
+
}
|
|
806
|
+
if (!proof.proven) {
|
|
807
|
+
try {
|
|
808
|
+
await this.closeTerminal({ ...st, handle, runtimeId: env.runtimeId });
|
|
809
|
+
}
|
|
810
|
+
catch { /* best effort under the answering runtime; the latch prevents another launch */ }
|
|
811
|
+
throw this.latched("create", st, `terminal ${handle} does not prove checkout ${st.cwd} (${proof.reason})`, env.raw);
|
|
812
|
+
}
|
|
791
813
|
st.handle = handle;
|
|
792
814
|
// The handle is bound to the runtime identity that ANSWERED its create.
|
|
793
815
|
st.runtimeId = env.runtimeId;
|
package/dist/gates/baseline.d.ts
CHANGED
|
@@ -128,6 +128,8 @@ export interface BaselineFileDuration {
|
|
|
128
128
|
durationMs: number;
|
|
129
129
|
}
|
|
130
130
|
export interface Baseline {
|
|
131
|
+
/** No command ran because the dependency inventory could not establish isolation. */
|
|
132
|
+
refusal?: string;
|
|
131
133
|
/** Observational capture receipts stay outside the forgiveness-bearing command entries. */
|
|
132
134
|
evidenceReceipts?: Record<string, GateEvidenceReceipt>;
|
|
133
135
|
commands: Record<string, BaselineCommand>;
|
package/dist/gates/baseline.js
CHANGED
|
@@ -3,7 +3,7 @@ import { execFileSync } from "node:child_process";
|
|
|
3
3
|
import { existsSync, readFileSync, mkdirSync, readdirSync, statSync, unlinkSync, writeFileSync, rmdirSync } from "node:fs";
|
|
4
4
|
import { availableParallelism, loadavg, tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
import { DEFAULT_SHELL_TIMEOUT_MS, describeCapacity, sameCapacity, sh } from "../run/git.js";
|
|
6
|
+
import { dependencyLinkRefusal, DEFAULT_SHELL_TIMEOUT_MS, describeCapacity, sameCapacity, sh } from "../run/git.js";
|
|
7
7
|
import { executionSignal } from "../run/execution-budget.js";
|
|
8
8
|
import { isVitestTestCommand, manifestFileCount } from "./test-manifest.js";
|
|
9
9
|
export const EVIDENCE_TAIL_BYTES = 16 * 1024;
|
|
@@ -620,6 +620,14 @@ export function staleFileCountCommands(baseline, commands, cwd) {
|
|
|
620
620
|
}
|
|
621
621
|
export async function captureBaseline(cwd, commands, opts = {}) {
|
|
622
622
|
const base = { commands: {} };
|
|
623
|
+
const refusal = dependencyLinkRefusal(cwd);
|
|
624
|
+
if (refusal) {
|
|
625
|
+
base.refusal = refusal;
|
|
626
|
+
for (const name of Object.keys(commands)) {
|
|
627
|
+
base.commands[name] = { infra: true, invalidCause: "infra", fingerprints: [], invalidatingLines: [refusal] };
|
|
628
|
+
}
|
|
629
|
+
return base;
|
|
630
|
+
}
|
|
623
631
|
for (const [name, cmd] of Object.entries(commands)) {
|
|
624
632
|
if (name === "tipTest" && commands.test !== undefined && cmd === commands.test) {
|
|
625
633
|
continue;
|
package/dist/gates/cache.d.ts
CHANGED
|
@@ -36,9 +36,11 @@ export interface VerificationIdentity {
|
|
|
36
36
|
gate?: string;
|
|
37
37
|
/** which verifier produced it: a battery gate row or an integration-tip row (own report, own forgiveness rule) */
|
|
38
38
|
scope?: VerificationScope;
|
|
39
|
-
/** Checkout location is diagnostic metadata;
|
|
39
|
+
/** Checkout location is diagnostic metadata; build outputs additionally require an incarnation. */
|
|
40
40
|
worktree?: string;
|
|
41
41
|
tree: string;
|
|
42
|
+
/** Build success also promises local outputs, which die with the checkout. */
|
|
43
|
+
checkoutIncarnation?: string;
|
|
42
44
|
command: string;
|
|
43
45
|
baseline: string;
|
|
44
46
|
environment: string;
|
|
@@ -59,7 +61,15 @@ export declare function computeVerificationIdentity(params: {
|
|
|
59
61
|
}): Promise<VerificationIdentity | undefined>;
|
|
60
62
|
export declare function verificationIdentityKey(id: VerificationIdentity): string;
|
|
61
63
|
export declare function formatReusedDetails(originalDetails: string, id: VerificationIdentity): string;
|
|
64
|
+
declare module "./types.js" {
|
|
65
|
+
interface GateResult {
|
|
66
|
+
originRunRoot?: string;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
62
69
|
export interface CachedVerdict {
|
|
70
|
+
evidenceReceipt?: GateResult["evidenceReceipt"];
|
|
71
|
+
evidenceReceipts?: GateResult["evidenceReceipts"];
|
|
72
|
+
originRunRoot?: string;
|
|
63
73
|
gate: string;
|
|
64
74
|
pass: boolean;
|
|
65
75
|
details: string;
|