tickmarkr 2.6.6 → 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.
Files changed (51) hide show
  1. package/dist/adapters/claude-code.js +8 -2
  2. package/dist/adapters/codex-commit-check.d.ts +3 -2
  3. package/dist/adapters/codex-commit-check.js +37 -10
  4. package/dist/adapters/codex.js +10 -4
  5. package/dist/cli/commands/beat.d.ts +7 -2
  6. package/dist/cli/commands/beat.js +144 -77
  7. package/dist/cli/commands/fleet.js +66 -21
  8. package/dist/cli/commands/init.js +18 -7
  9. package/dist/cli/commands/plan.js +16 -18
  10. package/dist/cli/commands/verify.js +4 -2
  11. package/dist/cli/help.d.ts +1 -0
  12. package/dist/cli/help.js +14 -6
  13. package/dist/cli/index.d.ts +1 -1
  14. package/dist/cli/index.js +1 -1
  15. package/dist/drivers/herdr.js +2 -2
  16. package/dist/drivers/orca.d.ts +12 -4
  17. package/dist/drivers/orca.js +200 -39
  18. package/dist/gates/baseline.d.ts +9 -0
  19. package/dist/gates/baseline.js +14 -3
  20. package/dist/gates/cache.js +1 -1
  21. package/dist/gates/evidence.js +3 -3
  22. package/dist/gates/review.d.ts +1 -1
  23. package/dist/gates/review.js +77 -13
  24. package/dist/gates/run-gates.d.ts +21 -6
  25. package/dist/gates/run-gates.js +439 -224
  26. package/dist/gates/test-manifest.d.ts +42 -0
  27. package/dist/gates/test-manifest.js +203 -118
  28. package/dist/run/beat-lifecycle.d.ts +239 -0
  29. package/dist/run/beat-lifecycle.js +1782 -0
  30. package/dist/run/daemon.d.ts +4 -0
  31. package/dist/run/daemon.js +129 -40
  32. package/dist/run/git-trust.d.ts +102 -0
  33. package/dist/run/git-trust.js +334 -0
  34. package/dist/run/git.d.ts +24 -3
  35. package/dist/run/git.js +169 -30
  36. package/dist/run/journal.js +15 -3
  37. package/dist/run/lease.d.ts +23 -0
  38. package/dist/run/lease.js +232 -38
  39. package/dist/run/merge.js +4 -4
  40. package/dist/run/stall.js +20 -1
  41. package/dist/run/supervision.d.ts +8 -0
  42. package/dist/run/supervision.js +24 -4
  43. package/dist/run/verification-job.d.ts +53 -0
  44. package/dist/run/verification-job.js +159 -0
  45. package/dist/tui/ink/fleet-app.d.ts +16 -2
  46. package/dist/tui/ink/fleet-app.js +265 -141
  47. package/package.json +1 -1
  48. package/skills/tickmarkr-auto/SKILL.md +80 -27
  49. package/skills/tickmarkr-loop/SKILL.md +87 -27
  50. package/skills/tickmarkr-overseer/SKILL.md +125 -47
  51. package/skills/tickmarkr-overseer/scripts/watch-journal.sh +9 -7
package/dist/run/git.js CHANGED
@@ -1,5 +1,6 @@
1
- import { executionSignal } from "./execution-budget.js";
2
- import { commandLeaseEnvironment, withCommandLease } from "./lease.js";
1
+ import { observeVerificationReceipt, observeVerificationSpawn, verificationJobEnvironment } from "./verification-job.js";
2
+ import { executionSignal, withoutExecutionBudget } from "./execution-budget.js";
3
+ import { commandLeaseEnvironment, repositoryLeaseEnvironment, registerRepositoryChild, withCommandLease } from "./lease.js";
3
4
  import { AsyncLocalStorage } from "node:async_hooks";
4
5
  import { execFile, execFileSync, spawn } from "node:child_process";
5
6
  import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
@@ -10,6 +11,7 @@ import { shq } from "../adapters/types.js";
10
11
  import { tickmarkrDir } from "../graph/graph.js";
11
12
  import { ROUTING_ENV_SEAMS } from "../route/router.js";
12
13
  import { titleEnvironment } from "./title-environment.js";
14
+ import { assertGitTrust, GitTrustRefusal, ownGitPin, protectedGitEnv, trustedCommonDir } from "./git-trust.js";
13
15
  export { ROUTING_ENV_SEAMS };
14
16
  // OBS-110: gate/baseline/tip-verify children are vitest suites; without a fork cap, concurrent
15
17
  // full-suite gate runs fork a worker-per-core pool per worktree and saturate the operator box.
@@ -257,8 +259,13 @@ export const SPAWN_RETRY_BACKOFF_MS = 50;
257
259
  // RULING-P99-28: SHIP — every gate user can hit the inherited-pipe hang at this product seam.
258
260
  /** Give a normally-exited shell's descendants this long to close their inherited pipes themselves. */
259
261
  const SHELL_REAP_GRACE_MS = 2000;
260
- /** 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. */
261
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) {
262
269
  const inherited = executionSignal();
263
270
  const signal = inherited && options.signal ? AbortSignal.any([inherited, options.signal]) : inherited ?? options.signal;
264
271
  let attribution;
@@ -289,6 +296,7 @@ export function shell(cmd, cwd, timeoutMs, login = false, options = {}) {
289
296
  activeShells.delete(activePid);
290
297
  activePid = undefined;
291
298
  }
299
+ observeVerificationReceipt({ ...receipt, confirmedStart });
292
300
  // Observational callbacks must not change process cleanup, retries or cancellation.
293
301
  try {
294
302
  options.onReceipt?.({ ...receipt, confirmedStart, ...(attribution ? { attribution: { ...attribution } } : {}) });
@@ -302,20 +310,24 @@ export function shell(cmd, cwd, timeoutMs, login = false, options = {}) {
302
310
  };
303
311
  begin(1);
304
312
  checkAbort();
305
- 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))
306
314
  .catch((error) => {
307
315
  if (signal?.aborted)
308
316
  emit({ outcome: "cancelled", exitCode: null, signal: null });
309
317
  throw error;
310
318
  });
311
319
  }
312
- function executeShell(cmd, cwd, timeoutMs, login, options, observation) {
320
+ function executeShell(cmd, cwd, timeoutMs, login, options, observation, ownGit) {
313
321
  observation.checkAbort();
314
322
  // OBS-74: scrub tickmarkr's own routing env seams from every child — a daemon carrying
315
323
  // TICKMARKR_QUALITY leaked it into baseline/gate/tip-verify children, turning a dogfood
316
324
  // repo's route() tests red inside the gates. Scrub a copy at this one choke point so
317
325
  // children are hermetic by construction; the daemon's own process.env stays unchanged.
318
- const env = 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;
319
331
  for (const k of ROUTING_ENV_SEAMS)
320
332
  delete env[k];
321
333
  // A run freezes the operator override and cores at startup; admission can lower the round cap.
@@ -340,14 +352,34 @@ function executeShell(cmd, cwd, timeoutMs, login, options, observation) {
340
352
  // under. `Number` of an unparseable export is NaN, which every reader treats as malformed and
341
353
  // therefore fails closed — the honest direction when the cap in play cannot be stated.
342
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;
343
357
  const attempt = () => new Promise((resolve) => {
344
358
  const startedAt = Date.now();
345
359
  // detached: bash gets its own process group so a timeout can kill the whole tree —
346
360
  // SIGKILLing bash alone orphans grandchildren (codex/pi) that hold the stdio pipes
347
361
  // open, so "close" never fires and the promise wedges forever (v1.33.1 init hang).
348
362
  let p;
363
+ // v2.6.7 T4: the trust check runs before EVERY spawn attempt (retries included) and caches nothing — a
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;
372
+ try {
373
+ const pin = ownGit ? ownGitPin(cwd, env, ownGit) : (assertGitTrust(cwd), undefined);
374
+ if (pin)
375
+ ({ env: childEnv, recheck, release, unview, store } = pin);
376
+ }
377
+ catch (error) {
378
+ observation.emit({ outcome: "spawn-failed", exitCode: null, signal: null, error: String(error) });
379
+ throw error;
380
+ }
349
381
  try {
350
- 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 });
351
383
  }
352
384
  catch (error) {
353
385
  observation.emit({ outcome: "spawn-failed", exitCode: null, signal: null, error: String(error) });
@@ -428,6 +460,19 @@ function executeShell(cmd, cwd, timeoutMs, login, options, observation) {
428
460
  boundDrain();
429
461
  };
430
462
  options.signal?.addEventListener("abort", abort, { once: true });
463
+ try {
464
+ registerRepositoryChild(p.pid);
465
+ }
466
+ catch (error) {
467
+ try {
468
+ process.kill(-p.pid, "SIGKILL");
469
+ }
470
+ catch {
471
+ p.kill("SIGKILL");
472
+ }
473
+ spawnError = String(error);
474
+ }
475
+ observeVerificationSpawn(p.pid);
431
476
  options.onSpawn?.(p.pid);
432
477
  if (options.signal?.aborted)
433
478
  abort();
@@ -494,9 +539,29 @@ function executeShell(cmd, cwd, timeoutMs, login, options, observation) {
494
539
  if (n > 1)
495
540
  observation.begin(n);
496
541
  observation.checkAbort();
497
- const r = await attempt();
498
- if (!("refused" in r))
499
- 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
+ }
500
565
  // Bounded, and the bound is what makes a persisting shortage a REPORTED failure rather than a
501
566
  // wedged daemon: past it the caller gets the refusal's own text under exit 127, as before.
502
567
  if (n >= SPAWN_ATTEMPT_LIMIT) {
@@ -510,8 +575,10 @@ export function sh(cmd, cwd, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS, options = {})
510
575
  return shell(cmd, cwd, timeoutMs, true, options);
511
576
  }
512
577
  // Git plumbing never needs an operator profile; skip login-shell startup and its side effects.
513
- export function shGit(cmd, cwd, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS) {
514
- 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);
515
582
  }
516
583
  export async function shOk(cmd, cwd) {
517
584
  const r = await sh(cmd, cwd);
@@ -519,12 +586,88 @@ export async function shOk(cmd, cwd) {
519
586
  throw new Error(`command failed (${r.code}): ${cmd}\n${r.stderr || r.stdout}`);
520
587
  return r.stdout;
521
588
  }
522
- export async function shGitOk(cmd, cwd) {
523
- 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);
524
591
  if (r.code !== 0)
525
592
  throw new Error(`command failed (${r.code}): ${cmd}\n${r.stderr || r.stdout}`);
526
593
  return r.stdout;
527
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
+ }
528
671
  const NPM_DEPENDENCY_MANIFESTS = ["package.json", "package-lock.json", "npm-shrinkwrap.json"];
529
672
  // OBS-126: compare the whole attempt (committed, staged, unstaged, or newly added manifest) with
530
673
  // the task's integration-tip baseline. A committed package.json change is invisible to `git status`,
@@ -678,7 +821,10 @@ export async function createWorktree(repo, branch, baseRef) {
678
821
  const dir = join(tickmarkrDir(repo), WORKTREES_DIR, sanitize(branch));
679
822
  if (existsSync(dir))
680
823
  await removeWorktree(repo, dir);
681
- 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 } } });
682
828
  linkNodeModules(repo, dir);
683
829
  return dir;
684
830
  }
@@ -693,21 +839,14 @@ export async function createWorktree(repo, branch, baseRef) {
693
839
  // match the provisioned SYMLINK, so a worker staging with `git add -A` commits the link and burns an
694
840
  // attempt on the scope gate. Write `node_modules` (no slash: matches the link too) into the exclude
695
841
  // file git actually consults for this worktree. Per-worktree `info/` is ignored in linked worktrees
696
- // (gitrepository-layout redirects it), so resolve through the `.git` gitfile + `commondir` indirection
697
- // to the common git dir. Local git metadata only — the target repository's .gitignore is never edited.
842
+ // (gitrepository-layout redirects it), so write the common git dir's exclude. Local git metadata only — the target repository's .gitignore is never edited.
698
843
  // Idempotent (appends only when no entry exists) and best-effort, like the link itself.
844
+ // v2.6.7 T4 (W1): the common dir is the independently derived one (trustedCommonDir), never what a
845
+ // seat-writable commondir names — a redirected commondir never receives the write.
699
846
  function excludeNodeModules(dir) {
700
847
  try {
701
- let gitDir = join(dir, ".git");
702
- if (lstatSync(gitDir).isFile()) { // linked worktree: .git is a gitfile naming the real git dir
703
- const m = /^gitdir:\s*(.+?)\s*$/m.exec(readFileSync(gitDir, "utf8"));
704
- if (!m)
705
- return;
706
- gitDir = resolve(dir, m[1]);
707
- }
708
- const commondir = join(gitDir, "commondir");
709
- if (existsSync(commondir))
710
- gitDir = resolve(gitDir, readFileSync(commondir, "utf8").trim());
848
+ lstatSync(join(dir, ".git")); // only the checkout itself, never an enclosing repository
849
+ const gitDir = trustedCommonDir(dir);
711
850
  const exclude = join(gitDir, "info", "exclude");
712
851
  const current = existsSync(exclude) ? readFileSync(exclude, "utf8") : "";
713
852
  if (/^node_modules$/m.test(current))
@@ -886,10 +1025,10 @@ export async function changeRepresented(cwd, commit, dest = "HEAD") {
886
1025
  const tree = await shGit(`git rev-parse ${shq(`${dest}^{tree}`)}`, cwd);
887
1026
  return tree.code === 0 && merged.stdout.split("\n")[0].trim() === tree.stdout.trim();
888
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)
889
1030
  export async function removeWorktree(repo, dir) {
890
- await shGit(`git worktree remove --force ${shq(dir)}`, repo); // best-effort; stale dirs are re-added with -B
891
- await shGit(`rm -rf ${shq(dir)}`, repo);
892
- await shGit("git worktree prune", repo);
1031
+ await dropWorktree(trustedCommonDir(repo), { dir });
893
1032
  }
894
1033
  export const REFS_PREFLIGHT_PREFIX = "refs/tickmarkr/preflight";
895
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";
@@ -11,6 +11,7 @@ import { channelRouteIdentity, modelProvider } from "../route/preference.js";
11
11
  import { buildProfile, classify } from "../route/profile.js";
12
12
  import { DecisionEventSchema, trackJournalRows, } from "./protocol.js";
13
13
  import { PRESERVE_COMMIT_SUBJECT, PRESERVE_PRODUCER_TRAILER } from "./git.js";
14
+ import { assertGitTrust, protectedGitEnv } from "./git-trust.js";
14
15
  import { normalizeGateOutcome } from "./outcome.js";
15
16
  import { redactSecrets } from "./redact.js";
16
17
  export function phaseForGate(gate) {
@@ -2398,9 +2399,17 @@ export class Journal {
2398
2399
  // "unknown" — never zero — for a waiver without a well-formed obligation or a proof that no longer holds.
2399
2400
  export const OWED_CHECK_VERSION = 1;
2400
2401
  export const OWED_DISCHARGE_EVENT = "owed-check-discharged";
2401
- const owedGit = (cwd, args, input) => execFileSync("git", args, {
2402
- cwd, encoding: "utf8", maxBuffer: 64 * 1024 * 1024, ...(input === undefined ? { stdio: ["ignore", "pipe", "pipe"] } : { input }),
2403
- });
2402
+ // v2.6.7 T4: every owed-identity git call runs the uncached trust check and the forced child config, and its
2403
+ // diff-producing verbs never run an external diff or textconv driver; a refusal throws, so the fold is unknown.
2404
+ const NO_DRIVERS = new Set(["diff", "show", "log"]);
2405
+ const owedGit = (cwd, args, input) => {
2406
+ assertGitTrust(cwd);
2407
+ const argv = NO_DRIVERS.has(args[0]) ? [args[0], "--no-ext-diff", "--no-textconv", ...args.slice(1)] : args;
2408
+ return execFileSync("git", argv, {
2409
+ cwd, env: protectedGitEnv(process.env), encoding: "utf8", maxBuffer: 64 * 1024 * 1024,
2410
+ ...(input === undefined ? { stdio: ["ignore", "pipe", "pipe"] } : { input }),
2411
+ });
2412
+ };
2404
2413
  const sha256 = (text) => createHash("sha256").update(text).digest("hex");
2405
2414
  export const owedCriteria = (acceptance) => sha256(JSON.stringify(acceptance));
2406
2415
  /** The daemon's canonical gate subject for base..head (daemon.ts gateCommitSubject), byte for byte. */
@@ -2700,6 +2709,9 @@ export function foldOwedChecks(events, cwd, memo) {
2700
2709
  }
2701
2710
  }
2702
2711
  function foldOwedChecksOrThrow(events, cwd, memo) {
2712
+ // v2.6.7 T4 (G2): checkout trust is re-derived at the fold boundary, before a memoised proof can answer —
2713
+ // a memo hit runs no git, so hostile metadata would otherwise ride a clean fold's cached proof to known debt.
2714
+ assertGitTrust(cwd);
2703
2715
  const accepted = new Map();
2704
2716
  const discharged = new Set();
2705
2717
  const unknown = [];
@@ -27,6 +27,13 @@ export interface RepositoryLeaseHolder {
27
27
  cwd: string;
28
28
  at: number;
29
29
  token: string;
30
+ /** Exact owner birth and token-bound execution roots; retained across owner death. */
31
+ identity?: string;
32
+ protectOrphans?: true;
33
+ /** The owner settled; only its protected tree can still block reclamation. */
34
+ ownerReleased?: true;
35
+ roots?: number[];
36
+ rootBirths?: Record<string, string>;
30
37
  }
31
38
  export interface RepositoryLeaseOptions {
32
39
  pollMs?: number;
@@ -35,6 +42,13 @@ export interface RepositoryLeaseOptions {
35
42
  onWait?: (holder: RepositoryLeaseHolder) => void;
36
43
  /** The token a descendant inherited; defaults to REPOSITORY_LEASE_TOKEN_ENV. */
37
44
  inherited?: string;
45
+ /** Manifest jobs bridge capabilities through explicit child environments, never process.env. */
46
+ isolated?: boolean;
47
+ protectOrphans?: boolean;
48
+ onAdmission?: (holder: RepositoryLeaseHolder) => void;
49
+ onRelease?: () => void;
50
+ /** A new job inside an existing job cannot borrow that job's async capability. */
51
+ independent?: boolean;
38
52
  }
39
53
  /** OBS-880/1071: the file lease's token, exported to the holder's descendants for the span it is held
40
54
  * so a nested runner (a gate's suite under `tickmarkr verify`, a mutation child inside a leased suite)
@@ -68,4 +82,13 @@ export declare const readHolder: (path: string) => RepositoryLeaseHolder | undef
68
82
  export declare function tryReserve(path: string, mine: RepositoryLeaseHolder): boolean;
69
83
  /** Exposed for recovery regressions; the standalone verify uses the combined critical section. */
70
84
  export declare const reclaimDead: (path: string, identity: string, pollMs?: number) => Promise<boolean>;
85
+ export declare function protectedProcesses(holder: RepositoryLeaseHolder, tokenEnv?: string, ownedOnly?: boolean): Promise<number[]>;
86
+ export declare const repositoryLeaseEnvironment: (env: NodeJS.ProcessEnv) => NodeJS.ProcessEnv;
87
+ /** Called synchronously at spawn, before any user callback, and only on an owned generation. */
88
+ export declare function registerRepositoryChild(pid: number | undefined): void;
89
+ export declare const withFreshCommandLease: <T>(command: string, run: () => Promise<T>) => Promise<T>;
90
+ /** The standalone wrapper uses the same command-then-repository order when a scheduler exists.
91
+ * Without one (CLI/global setup), this remains exactly the outer file reservation API. */
71
92
  export declare function withRepositoryLease<T>(cwd: string, run: () => Promise<T>, opts?: RepositoryLeaseOptions): Promise<T>;
93
+ export declare const hasRepositoryLeaseOwnership: () => boolean;
94
+ export declare function reapRepositoryChildren(holder: RepositoryLeaseHolder, pollMs: number, tokenEnv?: string): Promise<void>;