tickmarkr 2.7.9 → 2.8.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/codex.d.ts +1 -1
- package/dist/adapters/codex.js +18 -6
- package/dist/adapters/fake.js +42 -4
- package/dist/cli/commands/approve.js +33 -8
- package/dist/cli/commands/doctor.js +11 -2
- package/dist/cli/commands/verify.js +32 -12
- package/dist/gates/acceptance.d.ts +1 -1
- package/dist/gates/acceptance.js +75 -68
- package/dist/gates/baseline.d.ts +5 -1
- package/dist/gates/baseline.js +31 -6
- package/dist/gates/operation-facts.d.ts +47 -0
- package/dist/gates/operation-facts.js +84 -0
- package/dist/gates/run-gates.js +81 -12
- package/dist/run/daemon.d.ts +77 -13
- package/dist/run/daemon.js +348 -94
- package/dist/run/git.d.ts +21 -4
- package/dist/run/git.js +26 -9
- package/dist/run/launchservices-check.d.ts +23 -1
- package/dist/run/launchservices-check.js +88 -3
- package/dist/run/merge.d.ts +114 -7
- package/dist/run/merge.js +146 -44
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/SKILL.md +8 -0
package/dist/adapters/codex.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export declare function readCodexModelsCache(path?: string): {
|
|
|
5
5
|
retirements?: Record<string, ModelRetirement | null>;
|
|
6
6
|
hidden?: string[];
|
|
7
7
|
};
|
|
8
|
-
export declare const CODEX_GIT_GRANT = "-c \"sandbox_workspace_write.writable_roots=[$(tkr_common=$(git rev-parse --path-format=absolute --git-common-dir)
|
|
8
|
+
export declare const CODEX_GIT_GRANT = "-c \"sandbox_workspace_write.writable_roots=[$(tkr_common=$(git rev-parse --path-format=absolute --git-common-dir && echo /) && tkr_gitdir=$(git rev-parse --absolute-git-dir && echo /) && tkr_common=${tkr_common%?/} && tkr_gitdir=${tkr_gitdir%?/} && { tkr_head=$(git symbolic-ref -q HEAD); [ $? -le 1 ]; } && case $tkr_head in (refs/heads/*) tkr_leaf=${tkr_head%/*};; (*) tkr_leaf=;; esac && [ \"$(printf %s \"$tkr_common$tkr_gitdir$tkr_leaf\" | LC_ALL=C tr -d '\\0-\\37\\177\"\\\\')\" = \"$tkr_common$tkr_gitdir$tkr_leaf\" ] && { [ \"$tkr_gitdir\" = \"$tkr_common\" ] || printf '\"%s\",' \"$tkr_gitdir\"; } && printf '\"%s/objects\"' \"$tkr_common\" && { [ -z \"$tkr_leaf\" ] || printf ',\"%s/%s\",\"%s/logs/%s\"' \"$tkr_common\" \"$tkr_leaf\" \"$tkr_common\" \"$tkr_leaf\"; })]\"";
|
|
9
9
|
export declare const CODEX_TRUST_DIALOG: TrustDialog;
|
|
10
10
|
export declare const CODEX_INPUT_BOX: import("./types.js").InputBox;
|
|
11
11
|
export declare function seedCodexTrust(repoRoot: string, configPath?: string): TrustVerdict;
|
package/dist/adapters/codex.js
CHANGED
|
@@ -106,13 +106,25 @@ function codexRetirement(upgrade) {
|
|
|
106
106
|
// worktree cwd, for Codex WORKERS only: v2.6.7 T4 (D-1046) — invoke() (the headless worker form, including
|
|
107
107
|
// the interactive size fallback) and interactiveCommand carry it; headlessCommand, which judge, review,
|
|
108
108
|
// consult and the model probe launch, carries no writable_roots at all (those seats never commit).
|
|
109
|
-
// v2.6.6 T9 (K, D-912): the grant is
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
109
|
+
// v2.6.6 T9 (K, D-912) → v2.8.0 T1 (rows 17/18, D-910): the grant is a LEAF of what a linked-worktree branch
|
|
110
|
+
// commit writes, never the shared hooks/config (the shipped [common] and D-910's [common, gitdir] let a seat
|
|
111
|
+
// write <common>/hooks and <common>/config, which unsandboxed git later runs) and never whole common/refs and
|
|
112
|
+
// common/logs (which let a seat move main, any branch or tag, or the stash). The closed root table, in order:
|
|
113
|
+
// - the worktree's own gitdir, only when it differs from the common dir (an ordinary checkout gets none);
|
|
114
|
+
// - <common>/objects;
|
|
115
|
+
// - only when HEAD is a symbolic ref refs/heads/<B>: <common>/refs/heads/<D> and <common>/logs/refs/heads/<D>,
|
|
116
|
+
// <D> being <B> without its last slash-separated segment (refs/heads itself when <B> holds no slash);
|
|
117
|
+
// a detached HEAD gets no refs or reflog root.
|
|
118
|
+
// A directory outside any repository, any failed git read, or any root holding a character of the closed
|
|
119
|
+
// forbidden set (U+0000 to U+001F, U+007F DEL, the double quote, the backslash) yields the EMPTY array `[]`:
|
|
120
|
+
// no git write root, so a commit fails closed — never a partial array, an escaped string or codex's
|
|
121
|
+
// literal-string fallback. Every emitted root is therefore a TOML basic string that parses back to the exact
|
|
122
|
+
// path (git's own trailing newline is stripped through a sentinel, never a path's). No common-root fallback;
|
|
123
|
+
// packed-refs is not granted (packed-ref ops fail closed); stash, tags, notes and branches outside the leaf
|
|
124
|
+
// are denied. Declared: sibling refs in the same leaf (other task branches, the run's integration branch
|
|
125
|
+
// under the default prefix) stay writable until 2.8.1's integration identity.
|
|
114
126
|
// doctor's probe (codex-commit-check.ts) executes these exact bytes; it never rebuilds the grant.
|
|
115
|
-
export const CODEX_GIT_GRANT = `-c "sandbox_workspace_write.writable_roots=[$(tkr_common=$(git rev-parse --path-format=absolute --git-common-dir)
|
|
127
|
+
export const CODEX_GIT_GRANT = `-c "sandbox_workspace_write.writable_roots=[$(tkr_common=$(git rev-parse --path-format=absolute --git-common-dir && echo /) && tkr_gitdir=$(git rev-parse --absolute-git-dir && echo /) && tkr_common=\${tkr_common%?/} && tkr_gitdir=\${tkr_gitdir%?/} && { tkr_head=$(git symbolic-ref -q HEAD); [ $? -le 1 ]; } && case $tkr_head in (refs/heads/*) tkr_leaf=\${tkr_head%/*};; (*) tkr_leaf=;; esac && [ "$(printf %s "$tkr_common$tkr_gitdir$tkr_leaf" | LC_ALL=C tr -d '\\0-\\37\\177"\\\\')" = "$tkr_common$tkr_gitdir$tkr_leaf" ] && { [ "$tkr_gitdir" = "$tkr_common" ] || printf '"%s",' "$tkr_gitdir"; } && printf '"%s/objects"' "$tkr_common" && { [ -z "$tkr_leaf" ] || printf ',"%s/%s","%s/logs/%s"' "$tkr_common" "$tkr_leaf" "$tkr_common" "$tkr_leaf"; })]"`;
|
|
116
128
|
// OBS-125: codex 0.144.x gates enabled hooks behind PER-PROJECT-PATH hook trust — a NEW gate distinct
|
|
117
129
|
// from the directory trust seedCodexTrust seeds. Every worker runs in a fresh ephemeral worktree, an
|
|
118
130
|
// untrusted project, so codex re-prompts "Hooks need review" and the interactive worker stalls forever
|
package/dist/adapters/fake.js
CHANGED
|
@@ -2,6 +2,40 @@ import { readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { parseWorkerResult } from "./prompt.js";
|
|
4
4
|
import { OPENCODE_BUSY_FRAME_MARKERS, shq, TokenUsageSchema } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* S23: the first citation of the gate's nonce-framed index — from the LAST line exactly `CITABLE_LINES_BEGIN <nonce>`
|
|
7
|
+
* to the next line exactly `CITABLE_LINES_END <nonce>`. Every entry must be a JSON object with a string path and a
|
|
8
|
+
* non-empty array of non-negative safe-integer lines before any is used; otherwise (no block, no END, an empty or
|
|
9
|
+
* invalid entry anywhere) there is no citation. Never throws.
|
|
10
|
+
*/
|
|
11
|
+
function firstCitation(prompt, nonce) {
|
|
12
|
+
if (!nonce)
|
|
13
|
+
return undefined;
|
|
14
|
+
const lines = prompt.split("\n");
|
|
15
|
+
const begin = lines.lastIndexOf(`CITABLE_LINES_BEGIN ${nonce}`);
|
|
16
|
+
const end = begin < 0 ? -1 : lines.indexOf(`CITABLE_LINES_END ${nonce}`, begin + 1);
|
|
17
|
+
if (end < 0)
|
|
18
|
+
return undefined;
|
|
19
|
+
const entries = [];
|
|
20
|
+
for (const line of lines.slice(begin + 1, end)) {
|
|
21
|
+
let entry;
|
|
22
|
+
try {
|
|
23
|
+
entry = JSON.parse(line);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
29
|
+
return undefined;
|
|
30
|
+
const { path, lines: cited } = entry;
|
|
31
|
+
if (typeof path !== "string" || !Array.isArray(cited) || cited.length === 0
|
|
32
|
+
|| !cited.every((n) => Number.isSafeInteger(n) && n >= 0))
|
|
33
|
+
return undefined;
|
|
34
|
+
entries.push({ path, lines: cited });
|
|
35
|
+
}
|
|
36
|
+
const first = entries[0];
|
|
37
|
+
return first ? { path: first.path, line: first.lines[0] } : undefined;
|
|
38
|
+
}
|
|
5
39
|
// ponytail: deterministic scripted adapter — the whole integration suite runs on it, zero tokens.
|
|
6
40
|
export class FakeAdapter {
|
|
7
41
|
scriptPath;
|
|
@@ -77,13 +111,15 @@ export class FakeAdapter {
|
|
|
77
111
|
val = { ...verdict, criteria: items.map((criterion) => ({ criterion, met: true, reason: "scripted fake pass" })) };
|
|
78
112
|
}
|
|
79
113
|
}
|
|
114
|
+
// S23: a scripted row without evidence cites the first line of the first entry of the gate's own citable
|
|
115
|
+
// index — never a string. An invalid or unterminated index yields no evidence (the gate then fails closed).
|
|
80
116
|
if (key === "judge" && isJudge && val && typeof val === "object" && !Array.isArray(val)) {
|
|
81
117
|
const verdict = val;
|
|
82
|
-
const
|
|
83
|
-
if (
|
|
118
|
+
const evidence = firstCitation(prompt, nonce);
|
|
119
|
+
if (evidence && Array.isArray(verdict.criteria)) {
|
|
84
120
|
val = {
|
|
85
121
|
...verdict,
|
|
86
|
-
criteria: verdict.criteria.map((row) => row && typeof row === "object" && !("evidence" in row) ? { ...row, evidence
|
|
122
|
+
criteria: verdict.criteria.map((row) => row && typeof row === "object" && !("evidence" in row) ? { ...row, evidence } : row),
|
|
87
123
|
};
|
|
88
124
|
}
|
|
89
125
|
}
|
|
@@ -152,10 +188,12 @@ export class FakeAdapter {
|
|
|
152
188
|
}
|
|
153
189
|
// the run nonce lives in the prompt writePrompt handed us; echo a matching trailer so
|
|
154
190
|
// parseWorkerResult(output, nonce) succeeds exactly as before the nonce existed
|
|
191
|
+
// S23: the verdict nonce is the LAST whole `VERDICT_NONCE: <hex>` line — the gate writes its own after the goal,
|
|
192
|
+
// criteria and diff, so a nonce line quoted in any of them never binds.
|
|
155
193
|
nonceFor(promptFile) {
|
|
156
194
|
try {
|
|
157
195
|
const prompt = readFileSync(promptFile, "utf8");
|
|
158
|
-
return
|
|
196
|
+
return prompt.split("\n").map((line) => /^VERDICT_NONCE: ([0-9a-f]+)$/.exec(line)?.[1]).filter(Boolean).at(-1)
|
|
159
197
|
?? /TICKMARKR_RESULT_([0-9a-z]+)/.exec(prompt)?.[1]
|
|
160
198
|
?? "";
|
|
161
199
|
}
|
|
@@ -5,6 +5,7 @@ import { allAdapters } from "../../adapters/registry.js";
|
|
|
5
5
|
import { channelKey } from "../../adapters/types.js";
|
|
6
6
|
import { loadConfig } from "../../config/config.js";
|
|
7
7
|
import { integrationBranch } from "../../run/merge.js";
|
|
8
|
+
import { integrationAnchor } from "../../run/daemon.js";
|
|
8
9
|
import { separabilityErrors, surfaceErrors, taskBudgetErrors } from "../../compile/collateral.js";
|
|
9
10
|
import { GATE_NAMES } from "../../graph/schema.js";
|
|
10
11
|
import { applyScopeAmendments, bindingToken, captureOwedCheck, engagementComparable, engagementReleased, failedGateBeforePark, parseBindingToken, physicalLine, ATTEMPT_CAP_RELEASE, GATE_SATISFIED_RELEASE, Journal, RECHECK_RELEASE, REVIEW_UPHELD_RELEASE } from "../../run/journal.js";
|
|
@@ -168,6 +169,11 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
168
169
|
const serialization = await acquireApprovalSerialization(cwd, runId);
|
|
169
170
|
try {
|
|
170
171
|
const journal = Journal.open(cwd, runId);
|
|
172
|
+
// 2.8.1 T3 (ANL-023 §7.3): a marked run's recheck counts against its journal anchor; an unreadable one admits nothing.
|
|
173
|
+
const anchor = integrationAnchor(journal.read());
|
|
174
|
+
if (recheck && anchor && "unreadable" in anchor) {
|
|
175
|
+
throw new Error(`refusing to recheck ${taskId} in ${runId}: its journal names no readable published tip (${anchor.unreadable})`);
|
|
176
|
+
}
|
|
171
177
|
const status = journal.replayStatuses().get(taskId);
|
|
172
178
|
if (status === undefined) {
|
|
173
179
|
throw new Error(`task ${taskId} has no events in run ${runId} — unknown task or never dispatched`);
|
|
@@ -308,12 +314,12 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
308
314
|
// daemon's boundary never observes the decision without its obligation (or a second write).
|
|
309
315
|
// The seats a review discharge may resolve to are this repository's declared channels, recorded on
|
|
310
316
|
// the row so every later fold reads the same table whatever process or checkout it runs in.
|
|
311
|
-
let
|
|
317
|
+
let byName = "unknown";
|
|
312
318
|
let declared;
|
|
313
319
|
let owedTask;
|
|
314
320
|
try {
|
|
315
321
|
const cfg = loadConfig(cwd);
|
|
316
|
-
|
|
322
|
+
byName = integrationBranch(cfg, runId);
|
|
317
323
|
declared = allAdapters().flatMap((a) => a.channels(cfg)).map((c) => ({ key: channelKey(c), vendor: c.vendor }));
|
|
318
324
|
}
|
|
319
325
|
catch { /* an unreadable config binds no range and declares no seat: unknown */ }
|
|
@@ -321,11 +327,15 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
321
327
|
owedTask = loadGraph(cwd).tasks.find((t) => t.id === taskId);
|
|
322
328
|
}
|
|
323
329
|
catch { /* recorded as unknown criteria and scope */ }
|
|
324
|
-
const
|
|
330
|
+
const integration = integrationBase(journal.read(), () => byName);
|
|
331
|
+
const owed = captureOwedCheck({
|
|
325
332
|
cwd, runId, taskId, gate: failedGate, events, acceptance: owedTask?.acceptance, files: owedTask?.files, declared,
|
|
326
333
|
cause: reason ?? `operator waived failed ${failedGate} gate`, // the row's own park field binds the park
|
|
327
|
-
taskRef: `${
|
|
334
|
+
taskRef: `${integration.branch}--${taskId}`, integrationRef: integration.base,
|
|
328
335
|
});
|
|
336
|
+
// An unreadable anchor still records the waive, its obligation unknown: the debt is never silently known.
|
|
337
|
+
const obligation = integration.unreadable === undefined ? owed
|
|
338
|
+
: { ...owed, known: false, unknownReason: `its journal names no readable published tip (${integration.unreadable})` };
|
|
329
339
|
journal.append("task-approved", taskId, {
|
|
330
340
|
by,
|
|
331
341
|
...(reason ? { reason } : {}),
|
|
@@ -370,6 +380,20 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
370
380
|
serialization.release();
|
|
371
381
|
}
|
|
372
382
|
}
|
|
383
|
+
/**
|
|
384
|
+
* 2.8.1 T3 (ANL-023 §7.3): the integration branch and base a decision reads. A marked run (run-start
|
|
385
|
+
* `integrationAnchor: "journal"`) takes its branch from run-start, never the current config, and its base is the
|
|
386
|
+
* journal anchor A (`unreadable` when the journal names none); an unmarked run reads the branch by name, as before.
|
|
387
|
+
*/
|
|
388
|
+
function integrationBase(events, byName) {
|
|
389
|
+
const anchor = integrationAnchor(events);
|
|
390
|
+
if (!anchor) {
|
|
391
|
+
const branch = byName();
|
|
392
|
+
return { branch, base: branch };
|
|
393
|
+
}
|
|
394
|
+
const branch = String(events.find((e) => e.event === "run-start").data.branch);
|
|
395
|
+
return "anchor" in anchor ? { branch, base: anchor.anchor } : { branch, base: "unknown", unreadable: anchor.unreadable };
|
|
396
|
+
}
|
|
373
397
|
/** The row a decision on this task answers right now: its newest failure when failed, else its newest park. */
|
|
374
398
|
function openDecision(journal, taskId) {
|
|
375
399
|
const { events, sourceIndexes } = readJournalEvents(journal);
|
|
@@ -391,11 +415,12 @@ function requireBound(runId, taskId, bound, open, what) {
|
|
|
391
415
|
* OBS-1022: what a failed task has landed. A preserved snapshot is authoritative only when it contains
|
|
392
416
|
* the current task-branch tip; an older preservation can remain in the journal after a later retry has
|
|
393
417
|
* advanced/rebuilt the branch. Missing task branches may still recover from a preserved ref. The selected
|
|
394
|
-
* ref is counted against the run's integration
|
|
418
|
+
* ref is counted against the run's integration base (integrationBase); unreadable refs count as zero and fail closed.
|
|
419
|
+
* `base` names the branch the text prints; the count reads `integration.base`.
|
|
395
420
|
*/
|
|
396
421
|
function failedTaskCommitsAhead(cwd, journal, runId, taskId) {
|
|
397
|
-
const
|
|
398
|
-
const base =
|
|
422
|
+
const integration = integrationBase(journal.read(), () => integrationBranch(loadConfig(cwd), runId));
|
|
423
|
+
const base = integration.branch;
|
|
399
424
|
const taskRef = `${base}--${taskId}`;
|
|
400
425
|
const preserved = [...journal.read()].reverse().find((e) => e.taskId === taskId && e.event === "worktree-preserved" && typeof e.data.ref === "string");
|
|
401
426
|
const preservedRef = preserved?.data.ref;
|
|
@@ -416,7 +441,7 @@ function failedTaskCommitsAhead(cwd, journal, runId, taskId) {
|
|
|
416
441
|
}
|
|
417
442
|
}
|
|
418
443
|
try {
|
|
419
|
-
const out = execFileSync("git", ["rev-list", "--count", `${base}..${ref}`], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
444
|
+
const out = execFileSync("git", ["rev-list", "--count", `${integration.base}..${ref}`], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
420
445
|
return { ref, base, count: Number.parseInt(out.trim(), 10) || 0 };
|
|
421
446
|
}
|
|
422
447
|
catch {
|
|
@@ -22,7 +22,7 @@ import { sh } from "../../run/git.js";
|
|
|
22
22
|
import { auditNamedTestOracles, listVitestTests } from "../../gates/acceptance.js";
|
|
23
23
|
import { readReviewNoVerdictHistory, reviewNoVerdictRows } from "../../run/journal.js";
|
|
24
24
|
import { checkModelDiscovery } from "../../adapters/model-discovery-check.js";
|
|
25
|
-
import { recordLaunchServices } from "../../run/launchservices-check.js";
|
|
25
|
+
import { LAUNCHSERVICES_PORT_ADVISORY, probeLaunchServicesPorts, recordLaunchServices } from "../../run/launchservices-check.js";
|
|
26
26
|
/** Where a newer `table_<date>.csv` is discovered — the deployed site builds filenames by
|
|
27
27
|
* concatenation and publishes no index, so the release listing is the only enumerable surface. */
|
|
28
28
|
export const LIVEBENCH_RELEASES_URL = "https://api.github.com/repos/LiveBench/livebench.github.io/contents/public";
|
|
@@ -670,7 +670,8 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
670
670
|
const discovery = await checkModelDiscovery({ cwd, cfg, adapters, health, catalog, resolved: resolvedIdentitySnapshot, mayRefresh: !opts.catalog && !catalogRefreshLine && catalogRefreshAllowed, fetcher: opts.catalogFetcher, now: opts.catalogNow });
|
|
671
671
|
catalog = discovery.catalog;
|
|
672
672
|
aliasDriftFindings.push(...discovery.findings);
|
|
673
|
-
const
|
|
673
|
+
const portProbe = await probeLaunchServicesPorts();
|
|
674
|
+
const launchServices = await recordLaunchServices(readDoctor(cwd), health, portProbe.observation);
|
|
674
675
|
writeDoctor(cwd, launchServices.health);
|
|
675
676
|
const rows = adapters.map((a) => {
|
|
676
677
|
const h = health[a.id];
|
|
@@ -680,6 +681,14 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
680
681
|
const healthy = h.installed && (a.id !== kimi.id || h.authed) && !(a.id === codex.id && h.codexCommit === "escape");
|
|
681
682
|
return alignedStatusRow(healthy ? "pass" : "fail", a.id, state);
|
|
682
683
|
});
|
|
684
|
+
const ports = portProbe.ports;
|
|
685
|
+
const portLine = ports.status === "non-darwin" ? "launchservicesd ports: not applicable"
|
|
686
|
+
: ports.status === "read" ? `launchservicesd pid ${ports.pid}: ${ports.count} ports`
|
|
687
|
+
: `launchservicesd pid ${ports.status === "unknown" ? ports.pid ?? "unknown" : "unknown"}: ports unknown`;
|
|
688
|
+
if (ports.status !== "skipped") {
|
|
689
|
+
rows.push(alignedStatusRow(ports.status === "read" && ports.count < LAUNCHSERVICES_PORT_ADVISORY || ports.status === "non-darwin"
|
|
690
|
+
? "pass" : "warn", "launchservicesd", portLine));
|
|
691
|
+
}
|
|
683
692
|
const reviewDemotions = recentReviewDemotions(cwd);
|
|
684
693
|
if (reviewDemotions.length) {
|
|
685
694
|
rows.push(legend("recent review-seat demotions:"));
|
|
@@ -13,11 +13,11 @@ import { runGates } from "../../gates/run-gates.js";
|
|
|
13
13
|
import { getTask, loadGraph } from "../../graph/graph.js";
|
|
14
14
|
import { GATE_NAMES } from "../../graph/schema.js";
|
|
15
15
|
import { executionSignal } from "../../run/execution-budget.js";
|
|
16
|
-
import { addDetachedWorktree, linkNodeModules, removeWorktree, shGit, shGitOk } from "../../run/git.js";
|
|
16
|
+
import { addDetachedWorktree, linkNodeModules, reflogWitness, removeWorktree, shGit, shGitOk } from "../../run/git.js";
|
|
17
17
|
import { foldOwedChecks, integrationMapped, owedCriteria, Journal, OWED_DISCHARGE_EVENT } from "../../run/journal.js";
|
|
18
18
|
import { describeLeaseWait, withRepositoryLease } from "../../run/lease.js";
|
|
19
19
|
import { isPidLive } from "../../run/lock.js";
|
|
20
|
-
import { recordLaunchServices } from "../../run/launchservices-check.js";
|
|
20
|
+
import { LAUNCHSERVICES_PORT_STOP, probeLaunchServicesPorts, recordLaunchServices } from "../../run/launchservices-check.js";
|
|
21
21
|
/**
|
|
22
22
|
* tickmarkr verify — the gate battery as a standalone command (OPERATING-MODEL-2026-08-11 item 3).
|
|
23
23
|
*
|
|
@@ -184,6 +184,14 @@ export async function verify(argv, cwd = process.cwd(), options = {}) {
|
|
|
184
184
|
},
|
|
185
185
|
allowPositionals: false,
|
|
186
186
|
});
|
|
187
|
+
const portProbe = await probeLaunchServicesPorts({ requireTop: true });
|
|
188
|
+
const ports = portProbe.ports;
|
|
189
|
+
if (ports.status === "read" || ports.status === "unknown") {
|
|
190
|
+
console.error(`verify: ports ${ports.status === "read" ? ports.count : "unknown"}`);
|
|
191
|
+
}
|
|
192
|
+
if (ports.status === "read" && ports.count >= LAUNCHSERVICES_PORT_STOP) {
|
|
193
|
+
return { out: `verify refused: launchservicesd has ${ports.count} ports (threshold ${LAUNCHSERVICES_PORT_STOP}); log out or restart to reset launchservicesd before verifying`, code: 1 };
|
|
194
|
+
}
|
|
187
195
|
const stateRoot = await verifyStateRoot(cwd);
|
|
188
196
|
const fileRoot = (file) => existsSync(join(cwd, ".tickmarkr", file)) ? cwd : stateRoot;
|
|
189
197
|
if (resolve(stateRoot) !== resolve(cwd)) {
|
|
@@ -192,16 +200,24 @@ export async function verify(argv, cwd = process.cwd(), options = {}) {
|
|
|
192
200
|
}
|
|
193
201
|
const cfg = loadConfig(fileRoot("config.yaml"));
|
|
194
202
|
const head = (await shGitOk("git rev-parse HEAD", cwd)).trim();
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
203
|
+
// The latch reads HEAD's reflog through git as a validated witness, refuses before capture when it
|
|
204
|
+
// is absent or invalid, and compares it line for line at every subject check.
|
|
205
|
+
const witnessFault = (witness, commit) => {
|
|
206
|
+
if (witness.code !== 0)
|
|
207
|
+
return `exit ${witness.code}`;
|
|
208
|
+
const newest = witness.lines[0];
|
|
209
|
+
if (newest === undefined)
|
|
210
|
+
return "no reflog";
|
|
211
|
+
if (newest !== commit)
|
|
212
|
+
return `newest is ${newest.slice(0, 12)}`;
|
|
213
|
+
return undefined;
|
|
214
|
+
};
|
|
215
|
+
const capturedWitness = await reflogWitness(cwd, "HEAD");
|
|
216
|
+
const fault = witnessFault(capturedWitness, head);
|
|
217
|
+
if (fault) {
|
|
218
|
+
throw new Error(`verify refused: HEAD's reflog cannot witness the candidate (${fault}) — record HEAD moves in this checkout, then verify again`);
|
|
200
219
|
}
|
|
201
|
-
|
|
202
|
-
return "";
|
|
203
|
-
} };
|
|
204
|
-
const headLogAtCapture = headLog();
|
|
220
|
+
const headReflogAtCapture = capturedWitness.lines.join("\n");
|
|
205
221
|
const baseTip = (await shGitOk(`git rev-parse '${values.base}'`, cwd).catch(() => {
|
|
206
222
|
throw new Error(`--base ${values.base} is not a resolvable ref — pass --base <ref> naming the branch this diff targets`);
|
|
207
223
|
})).trim();
|
|
@@ -493,9 +509,13 @@ export async function verify(argv, cwd = process.cwd(), options = {}) {
|
|
|
493
509
|
let subjectLatch;
|
|
494
510
|
const subjectChange = async () => {
|
|
495
511
|
const now = await shGitOk("git rev-parse HEAD", cwd).then((out) => out.trim(), () => undefined);
|
|
512
|
+
// The latch reads HEAD's reflog through git as a validated witness, refuses before capture when it
|
|
513
|
+
// is absent or invalid, and compares it line for line at every subject check.
|
|
514
|
+
const witness = now === head ? await reflogWitness(cwd, "HEAD") : undefined;
|
|
515
|
+
const reflogMoved = witness !== undefined && (witness.code !== 0 || witness.lines.join("\n") !== headReflogAtCapture || witness.lines[0] !== head);
|
|
496
516
|
const why = now === undefined ? "HEAD is unreadable"
|
|
497
517
|
: now !== head ? `HEAD moved from ${head.slice(0, 12)} to ${now.slice(0, 12)}`
|
|
498
|
-
:
|
|
518
|
+
: reflogMoved ? `HEAD moved and was restored to ${head.slice(0, 12)} (its reflog recorded the move)`
|
|
499
519
|
: await candidateDirt();
|
|
500
520
|
if (why)
|
|
501
521
|
subjectLatch ??= why;
|