tickmarkr 2.6.7 → 2.6.8

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/run/git.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { CommandReceiptAttribution, ShellReceipt } from "./protocol.js";
2
2
  import { spawn } from "node:child_process";
3
3
  import { ROUTING_ENV_SEAMS } from "../route/router.js";
4
+ import { type OwnGitCapability } from "./git-trust.js";
4
5
  export { ROUTING_ENV_SEAMS };
5
6
  export declare const FORK_CAP_ENV = "VITEST_MAX_FORKS";
6
7
  export declare const SUITE_PARENT_ENV = "TICKMARKR_SUITE_PARENT";
@@ -168,12 +169,32 @@ export interface ShellOptions {
168
169
  /** Called once per invocation (1-based), including each pre-spawn retry. */
169
170
  receiptAttribution?: (invocation: number) => CommandReceiptAttribution;
170
171
  }
171
- /** Shared command seam, including invocation-bound manifested runners. */
172
+ /** Shared command seam, including invocation-bound manifested runners. A PAYLOAD entry: its children keep the
173
+ * operator's git config (hooks, fsmonitor) and get no checkout pin — ownership follows the entry, never `login`
174
+ * or the command text. tickmarkr's own git goes through shGit/shGitOk. */
172
175
  export declare function shell(cmd: string, cwd: string, timeoutMs: number, login?: boolean, options?: ShellOptions): Promise<ShResult>;
176
+ /**
177
+ * v2.6.8 T2: what an OWN-GIT caller declares at its call site — never inferred from the command text. Every own-git
178
+ * child in a linked checkout keeps the path pin and the ref-store pin; `createsWorktree` is the worktree capability the
179
+ * branch-creating call sites declare, and addDetachedWorktree is the detached one (see OwnGitCapability and ownGitPin).
180
+ */
181
+ export type OwnGitOptions = OwnGitCapability;
173
182
  export declare function sh(cmd: string, cwd: string, timeoutMs?: number, options?: ShellOptions): Promise<ShResult>;
174
- export declare function shGit(cmd: string, cwd: string, timeoutMs?: number): Promise<ShResult>;
183
+ export declare function shGit(cmd: string, cwd: string, timeoutMs?: number, own?: OwnGitOptions): Promise<ShResult>;
175
184
  export declare function shOk(cmd: string, cwd: string): Promise<string>;
176
- export declare function shGitOk(cmd: string, cwd: string): Promise<string>;
185
+ export declare function shGitOk(cmd: string, cwd: string, own?: OwnGitOptions): Promise<string>;
186
+ /**
187
+ * v2.6.8 T2: the DETACHED worktree capability, declared by calling it (the daemon's baseline recapture and standalone
188
+ * verify's base worktree). Under the ref-store pin git stubs a detached add's new HEAD as `ref: refs/heads/.invalid`
189
+ * and checks out nothing, and no own-git step ever runs unpinned instead: the commit-ish is resolved to an object id in
190
+ * `cwd`'s checkout (HEAD and branch names read the trusted store), the checkout is added `--no-checkout --detach` at
191
+ * that id (its ref store pinned to the trusted common directory, see DetachedAddCapability), and then — pinned to the
192
+ * NEW checkout's own authority — its HEAD is written as that id, git's `refs/heads/.invalid` stub ref is deleted and the
193
+ * checkout is populated.
194
+ * A step raced by a commondir change is refused by name before the next one runs, and what the add created is rolled
195
+ * back through the trusted common directory derived here once (dropWorktree), even while that commondir stays hostile.
196
+ */
197
+ export declare function addDetachedWorktree(cwd: string, dir: string, commitish: string, timeoutMs?: number): Promise<ShResult>;
177
198
  export declare function npmDependencyManifestChanged(cwd: string, baselineRef: string): Promise<boolean>;
178
199
  export declare function npmDependencyInstallCommand(cwd: string): string;
179
200
  export declare function gitHead(cwd: string): Promise<string>;
package/dist/run/git.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { observeVerificationReceipt, observeVerificationSpawn, verificationJobEnvironment } from "./verification-job.js";
2
- import { executionSignal } from "./execution-budget.js";
2
+ import { executionSignal, withoutExecutionBudget } from "./execution-budget.js";
3
3
  import { commandLeaseEnvironment, repositoryLeaseEnvironment, registerRepositoryChild, withCommandLease } from "./lease.js";
4
4
  import { AsyncLocalStorage } from "node:async_hooks";
5
5
  import { execFile, execFileSync, spawn } from "node:child_process";
@@ -11,7 +11,7 @@ import { shq } from "../adapters/types.js";
11
11
  import { tickmarkrDir } from "../graph/graph.js";
12
12
  import { ROUTING_ENV_SEAMS } from "../route/router.js";
13
13
  import { titleEnvironment } from "./title-environment.js";
14
- import { assertGitTrust, protectedGitEnv, trustedCommonDir } from "./git-trust.js";
14
+ import { assertGitTrust, GitTrustRefusal, ownGitPin, protectedGitEnv, trustedCommonDir } from "./git-trust.js";
15
15
  export { ROUTING_ENV_SEAMS };
16
16
  // OBS-110: gate/baseline/tip-verify children are vitest suites; without a fork cap, concurrent
17
17
  // full-suite gate runs fork a worker-per-core pool per worktree and saturate the operator box.
@@ -259,8 +259,13 @@ export const SPAWN_RETRY_BACKOFF_MS = 50;
259
259
  // RULING-P99-28: SHIP — every gate user can hit the inherited-pipe hang at this product seam.
260
260
  /** Give a normally-exited shell's descendants this long to close their inherited pipes themselves. */
261
261
  const SHELL_REAP_GRACE_MS = 2000;
262
- /** Shared command seam, including invocation-bound manifested runners. */
262
+ /** Shared command seam, including invocation-bound manifested runners. A PAYLOAD entry: its children keep the
263
+ * operator's git config (hooks, fsmonitor) and get no checkout pin — ownership follows the entry, never `login`
264
+ * or the command text. tickmarkr's own git goes through shGit/shGitOk. */
263
265
  export function shell(cmd, cwd, timeoutMs, login = false, options = {}) {
266
+ return shellEntry(cmd, cwd, timeoutMs, login, options, undefined);
267
+ }
268
+ function shellEntry(cmd, cwd, timeoutMs, login, options, ownGit) {
264
269
  const inherited = executionSignal();
265
270
  const signal = inherited && options.signal ? AbortSignal.any([inherited, options.signal]) : inherited ?? options.signal;
266
271
  let attribution;
@@ -305,22 +310,24 @@ export function shell(cmd, cwd, timeoutMs, login = false, options = {}) {
305
310
  };
306
311
  begin(1);
307
312
  checkAbort();
308
- return withCommandLease(cmd, () => executeShell(cmd, cwd, timeoutMs, login, { ...options, signal }, { begin, emit, checkAbort }))
313
+ return withCommandLease(cmd, () => executeShell(cmd, cwd, timeoutMs, login, { ...options, signal }, { begin, emit, checkAbort }, ownGit))
309
314
  .catch((error) => {
310
315
  if (signal?.aborted)
311
316
  emit({ outcome: "cancelled", exitCode: null, signal: null });
312
317
  throw error;
313
318
  });
314
319
  }
315
- function executeShell(cmd, cwd, timeoutMs, login, options, observation) {
320
+ function executeShell(cmd, cwd, timeoutMs, login, options, observation, ownGit) {
316
321
  observation.checkAbort();
317
322
  // OBS-74: scrub tickmarkr's own routing env seams from every child — a daemon carrying
318
323
  // TICKMARKR_QUALITY leaked it into baseline/gate/tip-verify children, turning a dogfood
319
324
  // repo's route() tests red inside the gates. Scrub a copy at this one choke point so
320
325
  // children are hermetic by construction; the daemon's own process.env stays unchanged.
321
- // v2.6.7 T4 (E1): git children run with core.fsmonitor=false and an inert hooksPath whatever the
322
- // repository or inherited GIT_CONFIG_* say; every other parent, capability and explicit value survives.
323
- const env = protectedGitEnv(verificationJobEnvironment(repositoryLeaseEnvironment(commandLeaseEnvironment(options.env ?? process.env))));
326
+ // v2.6.7 T4 (E1) / v2.6.8 T2: tickmarkr's OWN git children (shGit/shGitOk) run with core.fsmonitor=false and
327
+ // an inert hooksPath whatever the repository or inherited GIT_CONFIG_* say; payload children keep the
328
+ // operator's config. Every other parent, capability and explicit value survives.
329
+ const payloadEnv = verificationJobEnvironment(repositoryLeaseEnvironment(commandLeaseEnvironment(options.env ?? process.env)));
330
+ const env = ownGit ? protectedGitEnv(payloadEnv) : payloadEnv;
324
331
  for (const k of ROUTING_ENV_SEAMS)
325
332
  delete env[k];
326
333
  // A run freezes the operator override and cores at startup; admission can lower the round cap.
@@ -345,6 +352,8 @@ function executeShell(cmd, cwd, timeoutMs, login, options, observation) {
345
352
  // under. `Number` of an unparseable export is NaN, which every reader treats as malformed and
346
353
  // therefore fails closed — the honest direction when the cap in play cannot be stated.
347
354
  const capacity = { forkCap: Number(env[FORK_CAP_ENV]), cores: resolvedCapacity().cores };
355
+ // v2.6.8 T2: the pin's post-exit recheck for the attempt that actually ran its child, and its ref view's release.
356
+ let recheck, release, unview, store;
348
357
  const attempt = () => new Promise((resolve) => {
349
358
  const startedAt = Date.now();
350
359
  // detached: bash gets its own process group so a timeout can kill the whole tree —
@@ -353,15 +362,24 @@ function executeShell(cmd, cwd, timeoutMs, login, options, observation) {
353
362
  let p;
354
363
  // v2.6.7 T4: the trust check runs before EVERY spawn attempt (retries included) and caches nothing — a
355
364
  // seat can rewrite its gitdir's commondir/config.worktree between any two calls. A refusal names the file.
365
+ // v2.6.8 T2 (P3): an own-git attempt in a linked checkout also pins its child (config, objects and the ref store)
366
+ // to the authority that check derived, and its result is refused when commondir changed before the child exited;
367
+ // payloads get no pin. The command text never selects a pin.
368
+ let childEnv = env;
369
+ recheck = undefined;
370
+ unview = undefined;
371
+ store = undefined;
356
372
  try {
357
- assertGitTrust(cwd);
373
+ const pin = ownGit ? ownGitPin(cwd, env, ownGit) : (assertGitTrust(cwd), undefined);
374
+ if (pin)
375
+ ({ env: childEnv, recheck, release, unview, store } = pin);
358
376
  }
359
377
  catch (error) {
360
378
  observation.emit({ outcome: "spawn-failed", exitCode: null, signal: null, error: String(error) });
361
379
  throw error;
362
380
  }
363
381
  try {
364
- p = (spawnChild ?? spawn)("bash", [login ? "-lc" : "-c", cmd], { cwd, env, stdio: ["ignore", "pipe", "pipe"], detached: true });
382
+ p = (spawnChild ?? spawn)("bash", [login ? "-lc" : "-c", cmd], { cwd, env: childEnv, stdio: ["ignore", "pipe", "pipe"], detached: true });
365
383
  }
366
384
  catch (error) {
367
385
  observation.emit({ outcome: "spawn-failed", exitCode: null, signal: null, error: String(error) });
@@ -521,9 +539,29 @@ function executeShell(cmd, cwd, timeoutMs, login, options, observation) {
521
539
  if (n > 1)
522
540
  observation.begin(n);
523
541
  observation.checkAbort();
524
- const r = await attempt();
525
- if (!("refused" in r))
526
- return r;
542
+ let r;
543
+ try {
544
+ r = await attempt();
545
+ }
546
+ finally {
547
+ release?.();
548
+ release = undefined;
549
+ }
550
+ if (!("refused" in r)) {
551
+ try {
552
+ recheck?.();
553
+ }
554
+ catch (error) {
555
+ // v2.6.8 T2: a raced worktree add already registered what it created in the trusted store; it is removed through
556
+ // the trusted common directory this attempt derived, even while the caller's commondir stays hostile and even
557
+ // when the execution budget expired meanwhile; a failed rollback rides on the refusal, never swallowed
558
+ const rollback = ownGit && "rollback" in ownGit ? ownGit.rollback : undefined;
559
+ if (rollback && store)
560
+ await dropWorktree(store, rollback, timeoutMs).catch((failure) => { throw rollbackFailed(error, failure); });
561
+ throw error;
562
+ }
563
+ return unview ? { ...r, stdout: unview(r.stdout), stderr: unview(r.stderr) } : r;
564
+ }
527
565
  // Bounded, and the bound is what makes a persisting shortage a REPORTED failure rather than a
528
566
  // wedged daemon: past it the caller gets the refusal's own text under exit 127, as before.
529
567
  if (n >= SPAWN_ATTEMPT_LIMIT) {
@@ -537,8 +575,10 @@ export function sh(cmd, cwd, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS, options = {})
537
575
  return shell(cmd, cwd, timeoutMs, true, options);
538
576
  }
539
577
  // Git plumbing never needs an operator profile; skip login-shell startup and its side effects.
540
- export function shGit(cmd, cwd, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS) {
541
- return shell(cmd, cwd, timeoutMs, false);
578
+ // v2.6.8 T2: the explicit OWN-GIT entry — forced inert hooks/fsmonitor and, in a linked checkout, the trusted-path and
579
+ // ref-store pin.
580
+ export function shGit(cmd, cwd, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS, own = {}) {
581
+ return shellEntry(cmd, cwd, timeoutMs, false, {}, own);
542
582
  }
543
583
  export async function shOk(cmd, cwd) {
544
584
  const r = await sh(cmd, cwd);
@@ -546,12 +586,88 @@ export async function shOk(cmd, cwd) {
546
586
  throw new Error(`command failed (${r.code}): ${cmd}\n${r.stderr || r.stdout}`);
547
587
  return r.stdout;
548
588
  }
549
- export async function shGitOk(cmd, cwd) {
550
- const r = await shGit(cmd, cwd);
589
+ export async function shGitOk(cmd, cwd, own = {}) {
590
+ const r = await shGit(cmd, cwd, DEFAULT_SHELL_TIMEOUT_MS, own);
551
591
  if (r.code !== 0)
552
592
  throw new Error(`command failed (${r.code}): ${cmd}\n${r.stderr || r.stdout}`);
553
593
  return r.stdout;
554
594
  }
595
+ /**
596
+ * v2.6.8 T2: the DETACHED worktree capability, declared by calling it (the daemon's baseline recapture and standalone
597
+ * verify's base worktree). Under the ref-store pin git stubs a detached add's new HEAD as `ref: refs/heads/.invalid`
598
+ * and checks out nothing, and no own-git step ever runs unpinned instead: the commit-ish is resolved to an object id in
599
+ * `cwd`'s checkout (HEAD and branch names read the trusted store), the checkout is added `--no-checkout --detach` at
600
+ * that id (its ref store pinned to the trusted common directory, see DetachedAddCapability), and then — pinned to the
601
+ * NEW checkout's own authority — its HEAD is written as that id, git's `refs/heads/.invalid` stub ref is deleted and the
602
+ * checkout is populated.
603
+ * A step raced by a commondir change is refused by name before the next one runs, and what the add created is rolled
604
+ * back through the trusted common directory derived here once (dropWorktree), even while that commondir stays hostile.
605
+ */
606
+ export async function addDetachedWorktree(cwd, dir, commitish, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS) {
607
+ // the authority this operation keeps, rollback included: derived once from the trusted layout, never a commondir
608
+ const store = trustedCommonDir(cwd);
609
+ const resolved = await shGit(`git rev-parse --verify --end-of-options ${shq(`${commitish}^{commit}`)}`, cwd, timeoutMs);
610
+ if (resolved.code !== 0)
611
+ return resolved;
612
+ const oid = resolved.stdout.trim();
613
+ // under a ref payload git also resolves its new-HEAD stub into a shared refs/heads/.invalid; it is deleted again
614
+ const rollback = { dir, branch: { name: ".invalid" } };
615
+ const added = await shellEntry(`git worktree add --no-checkout --detach ${shq(dir)} ${shq(oid)}`, cwd, timeoutMs, false, {}, { createsWorktree: "detached", rollback });
616
+ if (added.code !== 0)
617
+ return added;
618
+ let repaired;
619
+ try {
620
+ repaired = await shGit(`git update-ref --no-deref HEAD ${shq(oid)} && git update-ref -d refs/heads/.invalid && git reset -q --hard --no-recurse-submodules`, dir, timeoutMs);
621
+ }
622
+ catch (error) {
623
+ await dropWorktree(store, rollback, timeoutMs).catch((failure) => { throw rollbackFailed(error, failure); });
624
+ throw error;
625
+ }
626
+ if (repaired.code !== 0)
627
+ await dropWorktree(store, rollback, timeoutMs);
628
+ return repaired;
629
+ }
630
+ /** A failed rollback is never swallowed: it rides on the error the operation already throws (a refusal stays one). */
631
+ function rollbackFailed(error, failure) {
632
+ const why = `; its rollback failed, so what it created may remain: ${failure instanceof Error ? failure.message : String(failure)}`;
633
+ return error instanceof GitTrustRefusal ? new GitTrustRefusal(error.path, `${error.detail}${why}`, { cause: failure })
634
+ : new AggregateError([error, failure], `${error instanceof Error ? error.message : String(error)}${why}`);
635
+ }
636
+ /**
637
+ * Removes a checkout `dir` and its registration — and, when named, a branch the add created (deleted) or moved
638
+ * (restored to `prior`) — through `store`, a trusted common directory derived from the layout (TrustedStoreCapability):
639
+ * nothing here reads the caller's checkout, so it completes while that checkout's commondir is hostile. Cleanup only, so
640
+ * it runs outside any execution budget (an expired one would refuse every step and leave what the add created), each
641
+ * step bounded by `timeoutMs`; every step is tried, and any that failed is thrown, never swallowed.
642
+ */
643
+ async function dropWorktree(store, { dir, branch }, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS) {
644
+ const failed = await withoutExecutionBudget(async () => {
645
+ const run = async (cmd) => {
646
+ try {
647
+ const r = await shellEntry(cmd, store, timeoutMs, false, {}, { store });
648
+ return r.code === 0 ? undefined : `${cmd} (${r.code}): ${(r.stderr || r.stdout).trim()}`;
649
+ }
650
+ catch (error) {
651
+ return `${cmd}: ${String(error)}`;
652
+ }
653
+ };
654
+ await run(`git worktree remove --force ${shq(dir)}`); // best-effort; the steps below decide
655
+ const steps = [`rm -rf ${shq(dir)}`, "git worktree prune"];
656
+ if (branch) {
657
+ const ref = shq(`refs/heads/${branch.name}`);
658
+ steps.push(branch.prior ? `git update-ref ${ref} ${shq(branch.prior)}` : `git update-ref -d ${ref}`);
659
+ }
660
+ const out = [];
661
+ for (const cmd of steps) {
662
+ const f = await run(cmd);
663
+ if (f)
664
+ out.push(f);
665
+ }
666
+ return out;
667
+ });
668
+ if (failed.length)
669
+ throw new Error(`cleanup of ${dir} through ${store} failed: ${failed.join("; ")}`);
670
+ }
555
671
  const NPM_DEPENDENCY_MANIFESTS = ["package.json", "package-lock.json", "npm-shrinkwrap.json"];
556
672
  // OBS-126: compare the whole attempt (committed, staged, unstaged, or newly added manifest) with
557
673
  // the task's integration-tip baseline. A committed package.json change is invisible to `git status`,
@@ -705,7 +821,10 @@ export async function createWorktree(repo, branch, baseRef) {
705
821
  const dir = join(tickmarkrDir(repo), WORKTREES_DIR, sanitize(branch));
706
822
  if (existsSync(dir))
707
823
  await removeWorktree(repo, dir);
708
- await shGitOk(`git worktree add -B ${shq(branch)} ${shq(dir)} ${shq(baseRef)}`, repo);
824
+ // the worktree capability: a branch add, whose symbolic HEAD keeps the ref-store pin (see OwnGitCapability); a raced
825
+ // add is rolled back, its branch restored to the value read here or deleted when it is new
826
+ const prior = (await shGit(`git rev-parse --verify -q ${shq(`refs/heads/${branch}`)}`, repo)).stdout.trim() || undefined;
827
+ await shGitOk(`git worktree add -B ${shq(branch)} ${shq(dir)} ${shq(baseRef)}`, repo, { createsWorktree: "branch", rollback: { dir, branch: { name: branch, prior } } });
709
828
  linkNodeModules(repo, dir);
710
829
  return dir;
711
830
  }
@@ -906,10 +1025,10 @@ export async function changeRepresented(cwd, commit, dest = "HEAD") {
906
1025
  const tree = await shGit(`git rev-parse ${shq(`${dest}^{tree}`)}`, cwd);
907
1026
  return tree.code === 0 && merged.stdout.split("\n")[0].trim() === tree.stdout.trim();
908
1027
  }
1028
+ // v2.6.8 T2: through the trusted common directory derived once from `repo`'s layout, never `repo`'s mutable commondir,
1029
+ // so a checkout is removed even while that commondir stays hostile (see dropWorktree)
909
1030
  export async function removeWorktree(repo, dir) {
910
- await shGit(`git worktree remove --force ${shq(dir)}`, repo); // best-effort; stale dirs are re-added with -B
911
- await shGit(`rm -rf ${shq(dir)}`, repo);
912
- await shGit("git worktree prune", repo);
1031
+ await dropWorktree(trustedCommonDir(repo), { dir });
913
1032
  }
914
1033
  export const REFS_PREFLIGHT_PREFIX = "refs/tickmarkr/preflight";
915
1034
  export const REFS_PROBE_REMEDY = "run from the main repository or a full clone; a sandbox that denies writes under that path cannot host a run";
package/dist/run/merge.js CHANGED
@@ -39,10 +39,10 @@ export async function ensureIntegration(repo, branch, baseRef) {
39
39
  if (!existsSync(join(dir, ".git"))) {
40
40
  const exists = (await shGit(`git rev-parse --verify refs/heads/${shq(branch)}`, repo)).code === 0;
41
41
  if (exists) {
42
- await shGitOk(`git worktree add ${shq(dir)} ${shq(branch)}`, repo);
42
+ await shGitOk(`git worktree add ${shq(dir)} ${shq(branch)}`, repo, { createsWorktree: "branch", rollback: { dir } });
43
43
  }
44
44
  else {
45
- await shGitOk(`git worktree add -b ${shq(branch)} ${shq(dir)} ${shq(baseRef)}`, repo);
45
+ await shGitOk(`git worktree add -b ${shq(branch)} ${shq(dir)} ${shq(baseRef)}`, repo, { createsWorktree: "branch", rollback: { dir, branch: { name: branch } } });
46
46
  }
47
47
  }
48
48
  linkNodeModules(repo, dir);
@@ -38,6 +38,8 @@ export interface VerificationJobReport {
38
38
  phases: VerificationPhase[];
39
39
  }
40
40
  export declare const VERIFICATION_JOB_TOKEN_ENV = "TICKMARKR_VERIFICATION_JOB_TOKEN";
41
+ /** v2.6.8 T2: a child carries only THIS process's local job id; outside a local job an inherited (foreign) token
42
+ * is dropped, never forwarded — a nested verify mints and stamps its own. */
41
43
  export declare function verificationJobEnvironment(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
42
44
  export declare const setVerificationJobClockForTests: (now: () => number) => void;
43
45
  export declare const resetVerificationJobClockForTests: () => void;
@@ -1,15 +1,21 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
- import { execFileSync } from "node:child_process";
3
2
  import { randomUUID } from "node:crypto";
4
3
  import { renameSync, rmSync, writeFileSync } from "node:fs";
5
- import { assertGitTrust, protectedGitEnv } from "./git-trust.js";
4
+ import { shGit } from "./git.js";
6
5
  import { executionSignal } from "./execution-budget.js";
7
- import { hasRepositoryLeaseOwnership, REPOSITORY_LEASE_TOKEN_ENV, reentrantHolder, repositoryLeasePath, reapRepositoryChildren, withCommandLease, withFreshCommandLease, withRepositoryLease } from "./lease.js";
6
+ import { hasRepositoryLeaseOwnership, readHolder, REPOSITORY_LEASE_TOKEN_ENV, reentrantHolder, repositoryLeasePath, reapRepositoryChildren, withCommandLease, withFreshCommandLease, withRepositoryLease } from "./lease.js";
8
7
  const jobs = new AsyncLocalStorage();
9
8
  export const VERIFICATION_JOB_TOKEN_ENV = "TICKMARKR_VERIFICATION_JOB_TOKEN";
9
+ /** v2.6.8 T2: a child carries only THIS process's local job id; outside a local job an inherited (foreign) token
10
+ * is dropped, never forwarded — a nested verify mints and stamps its own. */
10
11
  export function verificationJobEnvironment(env) {
11
12
  const job = jobs.getStore();
12
- return job ? { ...env, [VERIFICATION_JOB_TOKEN_ENV]: job.report.id } : { ...env };
13
+ const child = { ...env };
14
+ if (job)
15
+ child[VERIFICATION_JOB_TOKEN_ENV] = job.report.id;
16
+ else
17
+ delete child[VERIFICATION_JOB_TOKEN_ENV];
18
+ return child;
13
19
  }
14
20
  let clock = () => Date.now();
15
21
  export const setVerificationJobClockForTests = (now) => { clock = now; };
@@ -60,9 +66,11 @@ export async function withVerificationJob(command, cwd, reportPath, evidence, ru
60
66
  const signal = executionSignal();
61
67
  let commit = evidence?.subjectCommit ?? null;
62
68
  if (!commit) {
69
+ // v2.6.8 T2: the job-identity HEAD read is tickmarkr's own git — pinned in a linked checkout, refused when raced
63
70
  try {
64
- assertGitTrust(cwd);
65
- commit = execFileSync("git", ["rev-parse", "HEAD"], { cwd, env: protectedGitEnv(process.env), encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
71
+ const r = await shGit("git rev-parse HEAD", cwd);
72
+ if (r.code === 0)
73
+ commit = r.stdout.trim();
66
74
  }
67
75
  catch { /* unknown remains explicit */ }
68
76
  }
@@ -91,9 +99,10 @@ export async function withVerificationJob(command, cwd, reportPath, evidence, ru
91
99
  report.reservationReused = hasRepositoryLeaseOwnership() && !jobs.getStore();
92
100
  // A process descendant may share its ancestor's scheduler capability only after the file
93
101
  // generation and strict ancestry have been verified. A same-pid sibling always queues fresh.
94
- const inherited = !hasRepositoryLeaseOwnership()
95
- && await reentrantHolder(await repositoryLeasePath(cwd), process.env[REPOSITORY_LEASE_TOKEN_ENV]);
102
+ const leasePath = await repositoryLeasePath(cwd);
103
+ const inherited = !hasRepositoryLeaseOwnership() && await reentrantHolder(leasePath, process.env[REPOSITORY_LEASE_TOKEN_ENV]);
96
104
  const reserveCommand = report.reservationReused || inherited ? withCommandLease : withFreshCommandLease;
105
+ let owner; // the live holder the reservation's in-process owner reaps at release
97
106
  outcome = await reserveCommand(command, () => withRepositoryLease(cwd, () => {
98
107
  signal?.throwIfAborted();
99
108
  report.admittedAt = clock();
@@ -104,14 +113,27 @@ export async function withVerificationJob(command, cwd, reportPath, evidence, ru
104
113
  return await run();
105
114
  }
106
115
  finally {
107
- await reapRepositoryChildren({ pid: process.pid, cwd, at: report.queuedAt, token: report.id }, 20, VERIFICATION_JOB_TOKEN_ENV);
116
+ try {
117
+ await reapRepositoryChildren({ pid: process.pid, cwd, at: report.queuedAt, token: report.id }, 20, VERIFICATION_JOB_TOKEN_ENV);
118
+ }
119
+ finally {
120
+ // v2.6.8 T2: a nested verify records its shells' birth-checked groups in the reservation FILE, never in the
121
+ // in-process owner's holder, which its release reaps. Carry them back to that owner — this job's own
122
+ // reservation or a reused enclosing one — the same in-memory merge registerRepositoryChild makes, even
123
+ // when the job census above failed (its error still propagates).
124
+ const recorded = owner?.pid === process.pid ? readHolder(leasePath) : undefined;
125
+ if (owner && recorded?.token === owner.token) {
126
+ owner.roots = [...new Set([...(owner.roots ?? []), ...(recorded.roots ?? [])])];
127
+ owner.rootBirths = { ...owner.rootBirths, ...recorded.rootBirths };
128
+ }
129
+ }
108
130
  report.reapedAt = clock();
109
131
  job.persist();
110
132
  }
111
133
  }));
112
134
  }, { isolated: true, protectOrphans: true, independent: !!jobs.getStore(), signal, pollMs: 20,
113
135
  onWait: holder => { report.waitingOn = { ...holder }; job.persist(); },
114
- onAdmission: holder => { report.repositoryAdmittedAt = clock(); report.reservation = { ...holder }; job.persist(); },
136
+ onAdmission: holder => { owner = holder; report.repositoryAdmittedAt = clock(); report.reservation = { ...holder }; job.persist(); },
115
137
  onRelease: () => { report.reapedAt ??= clock(); report.repositoryReleasedAt = clock(); job.persist(); } }));
116
138
  report.releasedAt = clock();
117
139
  report.state = signal?.aborted ? "cancelled" : outcome.pass ? "completed" : "failed";
@@ -157,7 +157,7 @@ export type FleetStagedRouting = {
157
157
  ok: false;
158
158
  error: string;
159
159
  };
160
- export declare function FleetApp({ ageMs, agents, initialAllowOut, initialEfforts, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewAdvisories, reviewOverlay, reloadGuard, stagedRouting, holderOf, entry, initialJudge, judgeSeats, viewRows, frameColumns, frameRows, ...initialDenyProps }: InitialDenyProps & {
160
+ export declare function FleetApp({ ageMs, agents, initialAllowOut, initialEfforts, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewAdvisories, reviewOverlay, layerStamp, reloadGuard, stagedRouting, holderOf, entry, initialJudge, judgeSeats, viewRows, frameColumns, frameRows, ...initialDenyProps }: InitialDenyProps & {
161
161
  ageMs: number | null;
162
162
  agents: AgentCli[];
163
163
  /** OBS-1046: adapter ids and adapter:model keys routing.allow leaves out of the fleet */
@@ -181,6 +181,9 @@ export declare function FleetApp({ ageMs, agents, initialAllowOut, initialEffort
181
181
  /** OBS-1052(3): advisory review no-verdict history lines — display only, never a routing input */
182
182
  reviewAdvisories?: string[];
183
183
  reviewOverlay: (state: FleetEditorState) => FleetOverlayReview;
184
+ /** B-KEYS r2 (D-1446): the loaded overlay layers' identity (fleet: both overlay files' bytes) — the rows
185
+ * derive from them, so a layer written during the session re-derives every row; read once per render */
186
+ layerStamp?: () => string;
184
187
  reloadGuard: (bytes: string) => string | null;
185
188
  /** the routing policy the staged deny sets load as (fleet: the config loader over the candidate
186
189
  * bytes); absent ⇒ the four staged sets alone, with no allowlist */
@@ -199,7 +202,7 @@ export declare function FleetApp({ ageMs, agents, initialAllowOut, initialEffort
199
202
  /** terminal height — set only on a real TTY so the footer sticks to the bottom */
200
203
  frameRows?: number;
201
204
  }): import("react").JSX.Element;
202
- export declare function runFleetInkEditor({ ageMs, adapters, health, initialAllowOut, initialEfforts, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewAdvisories, reviewOverlay, reloadGuard, stagedRouting, holderOf, entry, initialJudge, judgeSeats, initialInput, input, output, debug, ...initialDenyProps }: InitialDenyProps & {
205
+ export declare function runFleetInkEditor({ ageMs, adapters, health, initialAllowOut, initialEfforts, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewAdvisories, reviewOverlay, layerStamp, reloadGuard, stagedRouting, holderOf, entry, initialJudge, judgeSeats, initialInput, input, output, debug, ...initialDenyProps }: InitialDenyProps & {
203
206
  ageMs: number | null;
204
207
  adapters: WorkerAdapter[];
205
208
  health: Record<string, AuthHealth>;
@@ -224,6 +227,8 @@ export declare function runFleetInkEditor({ ageMs, adapters, health, initialAllo
224
227
  /** OBS-1052(3): advisory review no-verdict history lines — display only, never a routing input */
225
228
  reviewAdvisories?: string[];
226
229
  reviewOverlay: (state: FleetEditorState) => FleetOverlayReview;
230
+ /** B-KEYS r2: the loaded overlay layers' identity (fleet: both overlay files' bytes), read once per render */
231
+ layerStamp?: () => string;
227
232
  reloadGuard: (bytes: string) => string | null;
228
233
  /** the routing policy the staged deny sets load as (fleet: the config loader over the candidate
229
234
  * bytes); absent ⇒ the four staged sets alone, with no allowlist */
@@ -49,7 +49,7 @@ const REACH_CHOICES = [
49
49
  { id: "out-workers", label: "out · workers", gloss: "workers skip it; judge/review/consult unaffected" },
50
50
  { id: "out-all", label: "out · all seats", gloss: "no seat routes here" },
51
51
  ];
52
- export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts = {}, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewAdvisories = [], reviewOverlay, reloadGuard, stagedRouting, holderOf, entry = "probe", initialJudge = "", judgeSeats = [], viewRows = Number.POSITIVE_INFINITY, frameColumns = 100, frameRows, ...initialDenyProps }) {
52
+ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts = {}, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewAdvisories = [], reviewOverlay, layerStamp, reloadGuard, stagedRouting, holderOf, entry = "probe", initialJudge = "", judgeSeats = [], viewRows = Number.POSITIVE_INFINITY, frameColumns = 100, frameRows, ...initialDenyProps }) {
53
53
  const { exit } = useApp();
54
54
  const [, setRevision] = useState(0);
55
55
  // the props are the fleet command's contract, one per schema-enumerated scope (initialDeny…),
@@ -137,21 +137,51 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
137
137
  // the STAGED policy (fleet loads the candidate bytes; see stagedRouting) and the recorded
138
138
  // identity — never literal membership of the staged sets, which missed identity, bare-model and
139
139
  // family entries and could not tell an allowlist exclusion from a deny.
140
- let routingMemo = null;
140
+ // B-KEYS (D-1338): the staged CONTENT every row and the staged policy derive from — the deny sets,
141
+ // the allow complement, classifications, efforts and seeds, read raw off ui (the provenance a
142
+ // review folds is a pure function of them). Computed per call, never per render: a handler
143
+ // mutates ui and reads rows in the same tick, before bump.
144
+ const stagedKey = () => JSON.stringify([
145
+ DENY_SCOPES.map((scope) => [...ui[scope.key]].sort()),
146
+ [...ui.allowOut].sort(),
147
+ ui.classifications,
148
+ ui.efforts,
149
+ ui.seededAllowOut,
150
+ ]);
151
+ // ponytail: both memos live in refs keyed on stagedKey — a render-local memo reset on every keypress,
152
+ // so each cursor move reloaded the policy and re-derived ~800 rows' reach (B-KEYS). A cursor move,
153
+ // a filter, showAll or an overlay never recompute a row; any staged edit changes the key and
154
+ // recomputes every group. Ceiling: one JSON.stringify of the staged sets per lookup (µs); a dirty
155
+ // counter bumped at every mutation site if a fleet ever outgrows it.
156
+ const routingMemo = useRef(null);
141
157
  const stagedPolicy = () => {
158
+ const key = memoKey();
159
+ const memo = routingMemo.current;
160
+ if (memo?.key === key)
161
+ return memo.policy;
142
162
  const deny = stagedDeny();
143
163
  const stage = stagedMetadata();
144
- const key = JSON.stringify([deny, stage]);
145
- if (routingMemo?.key !== key) {
146
- routingMemo = {
147
- key,
148
- policy: stagedRouting
149
- ? stagedRouting(deny, stage)
150
- : { ok: true, routing: { deny: denyBlockFrom(denyLists()) } },
151
- };
152
- }
153
- return routingMemo.policy;
164
+ const policy = stagedRouting
165
+ ? stagedRouting(deny, stage)
166
+ : { ok: true, routing: { deny: denyBlockFrom(denyLists()) } };
167
+ routingMemo.current = { key, policy };
168
+ return policy;
154
169
  };
170
+ const rowsMemo = useRef({ key: "", byAdapter: new Map() });
171
+ // B-KEYS r2 (D-1446): staged content alone does not identify a row — the rows also derive from the loaded
172
+ // overlay layers (stagedRouting and holderOf read both files) and the props they read. Once per render a
173
+ // generation moves when the layer bytes or one of those props changes, and both memos key on it beside the
174
+ // staged content: a layer written mid-session shows on the next frame, as on the base, while a cursor move
175
+ // over unchanged layers still derives nothing. Cost: one read of each overlay per frame (the B2 previewCfg key).
176
+ const layers = useRef({ stamp: "", inputs: [], gen: 0 });
177
+ {
178
+ const stamp = layerStamp?.() ?? "";
179
+ const inputs = [modelGroups, stagedRouting, holderOf];
180
+ if (stamp !== layers.current.stamp || inputs.some((input, at) => input !== layers.current.inputs[at])) {
181
+ layers.current = { stamp, inputs, gen: layers.current.gen + 1 };
182
+ }
183
+ }
184
+ const memoKey = () => `${layers.current.gen}\u0000${stagedKey()}`;
155
185
  // OBS-1099 add.1: the browser's deny discovery ranges over the schema-enumerated scopes (the
156
186
  // route collector names its four paths by hand, so a scope the schema gains would never render
157
187
  // or lift here). A flat scope reaches every seat; a nested one only the seat its block names
@@ -321,7 +351,19 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
321
351
  const found = members.map(coveringFor).filter((covering) => covering !== undefined);
322
352
  return found.find((covering) => covering.shared) ?? found[0];
323
353
  };
354
+ // B-KEYS: one derivation per group per staged state — the memo above hands every later pass
355
+ // (visibleCount per rail row, modelRows, the hidden and folded counts, a handler's lookup) the
356
+ // same rows; the returned array is read, never mutated, by every caller.
324
357
  const groupRows = (group) => {
358
+ const key = memoKey();
359
+ if (rowsMemo.current.key !== key)
360
+ rowsMemo.current = { key, byAdapter: new Map() };
361
+ const memo = rowsMemo.current.byAdapter;
362
+ const rows = memo.get(group.adapter) ?? deriveGroupRows(group);
363
+ memo.set(group.adapter, rows);
364
+ return rows;
365
+ };
366
+ const deriveGroupRows = (group) => {
325
367
  const rows = group.rows.map((row) => {
326
368
  const members = membersOf(group.adapter, row);
327
369
  const staged = ui.classifications.find((classification) => classification.adapter === group.adapter
@@ -2180,7 +2222,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
2180
2222
  ? _jsx(Text, { color: INK.warn, children: ` ! ${clip(ui.notice, width - 4)}` })
2181
2223
  : details.map((line) => _jsx(Text, { dimColor: true, children: ` ${clip(line, width - 2)}` }, line)), _jsx(KeyBar, { binds: keyBinds() })] }));
2182
2224
  }
2183
- export async function runFleetInkEditor({ ageMs, adapters, health, initialAllowOut = [], initialEfforts, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewAdvisories = [], reviewOverlay, reloadGuard, stagedRouting, holderOf, entry = "probe", initialJudge = "", judgeSeats = [], initialInput = [], input, output, debug = false, ...initialDenyProps }) {
2225
+ export async function runFleetInkEditor({ ageMs, adapters, health, initialAllowOut = [], initialEfforts, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewAdvisories = [], reviewOverlay, layerStamp, reloadGuard, stagedRouting, holderOf, entry = "probe", initialJudge = "", judgeSeats = [], initialInput = [], input, output, debug = false, ...initialDenyProps }) {
2184
2226
  const agents = adapters.flatMap((adapter) => {
2185
2227
  const state = health[adapter.id];
2186
2228
  if (!state?.installed)
@@ -2204,7 +2246,7 @@ export async function runFleetInkEditor({ ageMs, adapters, health, initialAllowO
2204
2246
  // rebuilt from output.columns/rows so a pane resize re-renders at the new size. Same root
2205
2247
  // element type on every rerender ⇒ React keeps the component instance, so staged edits,
2206
2248
  // cursor, and overlays all survive the resize.
2207
- const editorElement = () => (_jsx(FleetApp, { ageMs: ageMs, agents: agents, ...initialDenyProps, initialAllowOut: initialAllowOut, initialEfforts: initialEfforts, modelGroups: declaredModelGroups, initialMode: initialMode, modeOptions: modeOptions, initialMap: initialMap, modePreview: modePreview, shapeRows: shapeRows, candidatesForShape: candidatesForShape, preferOptionsForShape: preferOptionsForShape, initialSteering: initialSteering, steeringOptionsFor: steeringOptionsFor, reviewAdvisories: reviewAdvisories, reviewOverlay: reviewOverlay, reloadGuard: reloadGuard, stagedRouting: stagedRouting, holderOf: holderOf, entry: entry, initialJudge: initialJudge, judgeSeats: judgeSeats,
2249
+ const editorElement = () => (_jsx(FleetApp, { ageMs: ageMs, agents: agents, ...initialDenyProps, initialAllowOut: initialAllowOut, initialEfforts: initialEfforts, modelGroups: declaredModelGroups, initialMode: initialMode, modeOptions: modeOptions, initialMap: initialMap, modePreview: modePreview, shapeRows: shapeRows, candidatesForShape: candidatesForShape, preferOptionsForShape: preferOptionsForShape, initialSteering: initialSteering, steeringOptionsFor: steeringOptionsFor, reviewAdvisories: reviewAdvisories, reviewOverlay: reviewOverlay, layerStamp: layerStamp, reloadGuard: reloadGuard, stagedRouting: stagedRouting, holderOf: holderOf, entry: entry, initialJudge: initialJudge, judgeSeats: judgeSeats,
2208
2250
  // v1.90.9: cap every list to the terminal minus chrome so the cursor and header can
2209
2251
  // never scroll off-screen (omp's 218-model list did exactly that pre-cap).
2210
2252
  // OBS-523: chrome is ≤12 rows worst case (header, borders, padding, scope+search,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "2.6.7",
3
+ "version": "2.6.8",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",