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.
- package/dist/adapters/claude-code.js +8 -2
- package/dist/adapters/codex-commit-check.d.ts +3 -2
- package/dist/adapters/codex-commit-check.js +37 -10
- package/dist/adapters/codex.js +10 -4
- package/dist/cli/commands/beat.d.ts +7 -2
- package/dist/cli/commands/beat.js +144 -77
- package/dist/cli/commands/fleet.js +66 -21
- package/dist/cli/commands/init.js +18 -7
- package/dist/cli/commands/plan.js +16 -18
- package/dist/cli/commands/verify.js +4 -2
- package/dist/cli/help.d.ts +1 -0
- package/dist/cli/help.js +14 -6
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/drivers/herdr.js +2 -2
- package/dist/drivers/orca.d.ts +12 -4
- package/dist/drivers/orca.js +200 -39
- package/dist/gates/baseline.d.ts +9 -0
- package/dist/gates/baseline.js +14 -3
- package/dist/gates/cache.js +1 -1
- package/dist/gates/evidence.js +3 -3
- package/dist/gates/review.d.ts +1 -1
- package/dist/gates/review.js +77 -13
- package/dist/gates/run-gates.d.ts +21 -6
- package/dist/gates/run-gates.js +439 -224
- package/dist/gates/test-manifest.d.ts +42 -0
- package/dist/gates/test-manifest.js +203 -118
- package/dist/run/beat-lifecycle.d.ts +239 -0
- package/dist/run/beat-lifecycle.js +1782 -0
- package/dist/run/daemon.d.ts +4 -0
- package/dist/run/daemon.js +129 -40
- package/dist/run/git-trust.d.ts +102 -0
- package/dist/run/git-trust.js +334 -0
- package/dist/run/git.d.ts +24 -3
- package/dist/run/git.js +169 -30
- package/dist/run/journal.js +15 -3
- package/dist/run/lease.d.ts +23 -0
- package/dist/run/lease.js +232 -38
- package/dist/run/merge.js +4 -4
- package/dist/run/stall.js +20 -1
- package/dist/run/supervision.d.ts +8 -0
- package/dist/run/supervision.js +24 -4
- package/dist/run/verification-job.d.ts +53 -0
- package/dist/run/verification-job.js +159 -0
- package/dist/tui/ink/fleet-app.d.ts +16 -2
- package/dist/tui/ink/fleet-app.js +265 -141
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +80 -27
- package/skills/tickmarkr-loop/SKILL.md +87 -27
- package/skills/tickmarkr-overseer/SKILL.md +125 -47
- package/skills/tickmarkr-overseer/scripts/watch-journal.sh +9 -7
package/dist/run/git.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
|
|
514
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
702
|
-
|
|
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
|
|
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";
|
package/dist/run/journal.js
CHANGED
|
@@ -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
|
-
|
|
2402
|
-
|
|
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 = [];
|
package/dist/run/lease.d.ts
CHANGED
|
@@ -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>;
|