tickmarkr 2.6.5 → 2.6.7
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/README.md +7 -3
- package/dist/adapters/claude-code.js +8 -2
- package/dist/adapters/codex-commit-check.d.ts +16 -4
- package/dist/adapters/codex-commit-check.js +150 -26
- package/dist/adapters/codex.d.ts +1 -0
- package/dist/adapters/codex.js +20 -8
- package/dist/adapters/types.d.ts +4 -1
- package/dist/cli/commands/beat.d.ts +6 -1
- package/dist/cli/commands/beat.js +123 -76
- package/dist/cli/commands/doctor.js +3 -1
- package/dist/cli/commands/fleet.js +368 -85
- package/dist/cli/commands/init.js +23 -9
- package/dist/cli/commands/plan.js +12 -1
- package/dist/cli/commands/verify.js +37 -9
- package/dist/cli/help.d.ts +1 -0
- package/dist/cli/help.js +10 -5
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/config/fleet-overlay.d.ts +14 -0
- package/dist/config/fleet-overlay.js +154 -25
- package/dist/drivers/orca.d.ts +17 -3
- package/dist/drivers/orca.js +64 -20
- 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/review.js +15 -0
- package/dist/gates/run-gates.d.ts +20 -7
- package/dist/gates/run-gates.js +345 -210
- package/dist/gates/test-manifest.d.ts +11 -0
- package/dist/gates/test-manifest.js +151 -111
- package/dist/gates/timeout-shaped.d.ts +3 -0
- package/dist/gates/timeout-shaped.js +10 -0
- package/dist/run/beat-lifecycle.d.ts +154 -0
- package/dist/run/beat-lifecycle.js +825 -0
- package/dist/run/daemon.js +202 -126
- package/dist/run/git-trust.d.ts +27 -0
- package/dist/run/git-trust.js +178 -0
- package/dist/run/git.js +34 -14
- 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 +2 -2
- 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 +51 -0
- package/dist/run/verification-job.js +137 -0
- package/dist/tui/ink/fleet-app.d.ts +35 -5
- package/dist/tui/ink/fleet-app.js +244 -124
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +19 -10
- package/skills/tickmarkr-loop/SKILL.md +19 -10
- package/skills/tickmarkr-overseer/SKILL.md +25 -16
- package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +58 -16
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +19 -6
- package/skills/tickmarkr-overseer/scripts/watch-parks.sh +121 -64
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
// v2.6.7 T4 (GHSA-2): a seat-run linked worktree's gitdir is seat-writable (the Codex worker grant), so its
|
|
5
|
+
// `commondir` and `config.worktree` can redirect every later unsandboxed host git in that checkout to a
|
|
6
|
+
// hostile common directory or config program. Authority is derived afresh on every call from the seat-denied
|
|
7
|
+
// worktree gitfile and the trusted ordinary common directory's own bytes — never persisted enrollment, never
|
|
8
|
+
// the mutable commondir/config.worktree, never ambient GIT_DIR/GIT_COMMON_DIR. No verdict is cached: a check
|
|
9
|
+
// passed once proves nothing about the next spawn. This module must not import git.ts, lease.ts or journal.ts.
|
|
10
|
+
/** A covered git invocation refused before spawn; `path` names the offending metadata. */
|
|
11
|
+
export class GitTrustRefusal extends Error {
|
|
12
|
+
path;
|
|
13
|
+
constructor(path, detail) {
|
|
14
|
+
super(`git trust refusal: ${path}: ${detail}`);
|
|
15
|
+
this.name = "GitTrustRefusal";
|
|
16
|
+
this.path = path;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** The nearest `.git` entry at or above cwd, or undefined outside any checkout. */
|
|
20
|
+
const dotGitOf = (cwd) => {
|
|
21
|
+
let dir;
|
|
22
|
+
try {
|
|
23
|
+
dir = realpathSync.native(cwd);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
for (;;) {
|
|
29
|
+
const candidate = join(dir, ".git");
|
|
30
|
+
try {
|
|
31
|
+
lstatSync(candidate);
|
|
32
|
+
return candidate;
|
|
33
|
+
}
|
|
34
|
+
catch { /* keep walking */ }
|
|
35
|
+
const up = dirname(dir);
|
|
36
|
+
if (up === dir)
|
|
37
|
+
return undefined;
|
|
38
|
+
dir = up;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const present = (path) => {
|
|
42
|
+
try {
|
|
43
|
+
lstatSync(path);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* A path exactly as git reads it from a gitfile or commondir (setup.c read_gitfile_gently /
|
|
52
|
+
* get_common_dir_noenv): raw bytes with ONLY trailing CR/LF stripped — never trimmed, so a leading or trailing
|
|
53
|
+
* space stays a path component git follows — relative to `base` unless it starts with "/", then a PHYSICAL
|
|
54
|
+
* realpath (symlinks resolved before `..`, as git's strbuf_realpath and realpath(3) do; fs.realpathSync
|
|
55
|
+
* normalizes `..` lexically first). Throws when the path does not resolve.
|
|
56
|
+
*/
|
|
57
|
+
const resolveAsGit = (base, bytes) => {
|
|
58
|
+
let end = bytes.length;
|
|
59
|
+
while (end && (bytes[end - 1] === 0x0a || bytes[end - 1] === 0x0d))
|
|
60
|
+
end--;
|
|
61
|
+
const named = bytes.subarray(0, end);
|
|
62
|
+
return realpathSync.native(named[0] === 0x2f ? named : Buffer.concat([Buffer.from(`${base}/`), named]), { encoding: "buffer" });
|
|
63
|
+
};
|
|
64
|
+
function authorityOf(dotGit) {
|
|
65
|
+
let directory;
|
|
66
|
+
try {
|
|
67
|
+
directory = statSync(dotGit).isDirectory();
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
throw new GitTrustRefusal(dotGit, `unreadable checkout metadata (${String(e)})`);
|
|
71
|
+
}
|
|
72
|
+
if (directory) {
|
|
73
|
+
const gitdir = realpathSync.native(dotGit);
|
|
74
|
+
return { gitdir, common: gitdir, linked: false };
|
|
75
|
+
}
|
|
76
|
+
// git accepts a gitfile only when its first bytes are exactly "gitdir: " and the rest names the path
|
|
77
|
+
let named;
|
|
78
|
+
try {
|
|
79
|
+
const bytes = readFileSync(dotGit);
|
|
80
|
+
if (bytes.subarray(0, 8).toString("latin1") === "gitdir: ")
|
|
81
|
+
named = bytes.subarray(8);
|
|
82
|
+
}
|
|
83
|
+
catch { /* refused below */ }
|
|
84
|
+
if (!named || !/[^\r\n]/.test(named.toString("latin1")))
|
|
85
|
+
throw new GitTrustRefusal(dotGit, "not a readable gitfile");
|
|
86
|
+
let gitdir;
|
|
87
|
+
try {
|
|
88
|
+
gitdir = resolveAsGit(dirname(dotGit), named).toString();
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
throw new GitTrustRefusal(dotGit, `names a missing gitdir ${named.toString().trimEnd()}`);
|
|
92
|
+
}
|
|
93
|
+
// Supported linked layout: <common>/worktrees/<name>, where <common> is itself an ordinary git directory.
|
|
94
|
+
// The layout decides, never commondir's presence: a seat can delete its own commondir (and plant standalone
|
|
95
|
+
// repository metadata beside it), which must not turn its writable gitdir into its own trusted common dir.
|
|
96
|
+
const common = basename(dirname(gitdir)) === "worktrees" ? dirname(dirname(gitdir)) : undefined;
|
|
97
|
+
if (common !== undefined && present(join(common, "HEAD")) && !present(join(common, "commondir")))
|
|
98
|
+
return { gitdir, common, linked: true };
|
|
99
|
+
// Outside that layout, a gitdir without commondir is an ordinary separate git directory (submodule, --separate-git-dir).
|
|
100
|
+
if (!present(join(gitdir, "commondir")))
|
|
101
|
+
return { gitdir, common: gitdir, linked: false };
|
|
102
|
+
if (common === undefined)
|
|
103
|
+
throw new GitTrustRefusal(dotGit, `names ${gitdir}, outside a supported <common>/worktrees/<name> layout`);
|
|
104
|
+
throw new GitTrustRefusal(common, "the linked worktree's common directory is not an ordinary git directory");
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The canonical, independently expected common git directory for the checkout enclosing `cwd` — never what
|
|
108
|
+
* a mutable commondir says. Throws GitTrustRefusal naming the authority path when it cannot be established.
|
|
109
|
+
*/
|
|
110
|
+
export function trustedCommonDir(cwd) {
|
|
111
|
+
const dotGit = dotGitOf(cwd);
|
|
112
|
+
if (!dotGit)
|
|
113
|
+
throw new GitTrustRefusal(cwd, "no checkout encloses this directory");
|
|
114
|
+
return authorityOf(dotGit).common;
|
|
115
|
+
}
|
|
116
|
+
/** Read failures and unparseable values never become clean authority. */
|
|
117
|
+
function worktreeConfigEnabled(common) {
|
|
118
|
+
const config = join(common, "config");
|
|
119
|
+
if (!present(config))
|
|
120
|
+
throw new GitTrustRefusal(config, "the trusted common config is missing");
|
|
121
|
+
try {
|
|
122
|
+
return execFileSync("git", ["config", "--file", config, "--type=bool", "--get", "extensions.worktreeConfig"], {
|
|
123
|
+
cwd: "/", encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 15_000,
|
|
124
|
+
}).trim() === "true";
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
const e = error;
|
|
128
|
+
if (e.status === 1 && !e.stderr?.trim())
|
|
129
|
+
return false; // the key is absent
|
|
130
|
+
throw new GitTrustRefusal(config, `cannot read extensions.worktreeConfig (${(e.stderr ?? String(error)).trim().split("\n")[0]})`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The uncached pre-spawn check for a covered git invocation at `cwd`. Outside any checkout it is a no-op
|
|
135
|
+
* (unrelated shell commands stay compatible). Inside one it refuses, naming the file, when the commondir git
|
|
136
|
+
* would follow resolves anywhere but the expected common directory, or when the trusted common config enables
|
|
137
|
+
* extensions.worktreeConfig and the linked gitdir holds a config.worktree.
|
|
138
|
+
*/
|
|
139
|
+
export function assertGitTrust(cwd) {
|
|
140
|
+
const dotGit = dotGitOf(cwd);
|
|
141
|
+
if (!dotGit)
|
|
142
|
+
return;
|
|
143
|
+
const { gitdir, common, linked } = authorityOf(dotGit);
|
|
144
|
+
const commondir = join(gitdir, "commondir");
|
|
145
|
+
if (linked && !present(commondir))
|
|
146
|
+
throw new GitTrustRefusal(commondir, `is missing, so git would treat the seat-writable gitdir as its own repository; expected ${common}`);
|
|
147
|
+
if (present(commondir)) {
|
|
148
|
+
// read as git reads it (resolveAsGit), never trimmed or lexically normalized: either would let a seat-written
|
|
149
|
+
// path that names the real common directory to us lead git somewhere else
|
|
150
|
+
let actual;
|
|
151
|
+
try {
|
|
152
|
+
actual = resolveAsGit(gitdir, readFileSync(commondir));
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
throw new GitTrustRefusal(commondir, `names no readable directory; expected ${common}`);
|
|
156
|
+
}
|
|
157
|
+
if (!actual.equals(Buffer.from(common)))
|
|
158
|
+
throw new GitTrustRefusal(commondir, `resolves to ${actual.toString()}, not the expected common directory ${common}`);
|
|
159
|
+
}
|
|
160
|
+
const worktreeConfig = join(gitdir, "config.worktree");
|
|
161
|
+
if (linked && present(worktreeConfig) && worktreeConfigEnabled(common)) {
|
|
162
|
+
throw new GitTrustRefusal(worktreeConfig, `extensions.worktreeConfig is enabled in ${join(common, "config")}, so git would read this seat-writable file`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** The inert hooks directory every protected git child receives (no hook can exist beneath it). */
|
|
166
|
+
export const INERT_HOOKS_PATH = "/dev/null";
|
|
167
|
+
const FORCED_GIT_CONFIG = `'core.fsmonitor=false' 'core.hooksPath=${INERT_HOOKS_PATH}'`;
|
|
168
|
+
/**
|
|
169
|
+
* A copy of `env` whose git children run with core.fsmonitor=false and an inert core.hooksPath, despite repo
|
|
170
|
+
* config and inherited GIT_CONFIG_COUNT/GIT_CONFIG_PARAMETERS: git applies GIT_CONFIG_PARAMETERS after
|
|
171
|
+
* GIT_CONFIG_COUNT and the last value wins, so the forced pair is appended last. Every other value survives.
|
|
172
|
+
* Named keys only — this does not neutralize arbitrary executable drivers. The pair is inherited by every
|
|
173
|
+
* descendant of the protected child (a suite that needs real hooks must clear it for its own git).
|
|
174
|
+
*/
|
|
175
|
+
export function protectedGitEnv(env) {
|
|
176
|
+
const prior = env.GIT_CONFIG_PARAMETERS?.trim();
|
|
177
|
+
return { ...env, GIT_CONFIG_PARAMETERS: prior ? `${prior} ${FORCED_GIT_CONFIG}` : FORCED_GIT_CONFIG };
|
|
178
|
+
}
|
package/dist/run/git.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { observeVerificationReceipt, observeVerificationSpawn, verificationJobEnvironment } from "./verification-job.js";
|
|
1
2
|
import { executionSignal } from "./execution-budget.js";
|
|
2
|
-
import { commandLeaseEnvironment, withCommandLease } from "./lease.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, 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.
|
|
@@ -289,6 +291,7 @@ export function shell(cmd, cwd, timeoutMs, login = false, options = {}) {
|
|
|
289
291
|
activeShells.delete(activePid);
|
|
290
292
|
activePid = undefined;
|
|
291
293
|
}
|
|
294
|
+
observeVerificationReceipt({ ...receipt, confirmedStart });
|
|
292
295
|
// Observational callbacks must not change process cleanup, retries or cancellation.
|
|
293
296
|
try {
|
|
294
297
|
options.onReceipt?.({ ...receipt, confirmedStart, ...(attribution ? { attribution: { ...attribution } } : {}) });
|
|
@@ -315,7 +318,9 @@ function executeShell(cmd, cwd, timeoutMs, login, options, observation) {
|
|
|
315
318
|
// TICKMARKR_QUALITY leaked it into baseline/gate/tip-verify children, turning a dogfood
|
|
316
319
|
// repo's route() tests red inside the gates. Scrub a copy at this one choke point so
|
|
317
320
|
// children are hermetic by construction; the daemon's own process.env stays unchanged.
|
|
318
|
-
|
|
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))));
|
|
319
324
|
for (const k of ROUTING_ENV_SEAMS)
|
|
320
325
|
delete env[k];
|
|
321
326
|
// A run freezes the operator override and cores at startup; admission can lower the round cap.
|
|
@@ -346,6 +351,15 @@ function executeShell(cmd, cwd, timeoutMs, login, options, observation) {
|
|
|
346
351
|
// SIGKILLing bash alone orphans grandchildren (codex/pi) that hold the stdio pipes
|
|
347
352
|
// open, so "close" never fires and the promise wedges forever (v1.33.1 init hang).
|
|
348
353
|
let p;
|
|
354
|
+
// v2.6.7 T4: the trust check runs before EVERY spawn attempt (retries included) and caches nothing — a
|
|
355
|
+
// seat can rewrite its gitdir's commondir/config.worktree between any two calls. A refusal names the file.
|
|
356
|
+
try {
|
|
357
|
+
assertGitTrust(cwd);
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
observation.emit({ outcome: "spawn-failed", exitCode: null, signal: null, error: String(error) });
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
349
363
|
try {
|
|
350
364
|
p = (spawnChild ?? spawn)("bash", [login ? "-lc" : "-c", cmd], { cwd, env, stdio: ["ignore", "pipe", "pipe"], detached: true });
|
|
351
365
|
}
|
|
@@ -428,6 +442,19 @@ function executeShell(cmd, cwd, timeoutMs, login, options, observation) {
|
|
|
428
442
|
boundDrain();
|
|
429
443
|
};
|
|
430
444
|
options.signal?.addEventListener("abort", abort, { once: true });
|
|
445
|
+
try {
|
|
446
|
+
registerRepositoryChild(p.pid);
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
try {
|
|
450
|
+
process.kill(-p.pid, "SIGKILL");
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
p.kill("SIGKILL");
|
|
454
|
+
}
|
|
455
|
+
spawnError = String(error);
|
|
456
|
+
}
|
|
457
|
+
observeVerificationSpawn(p.pid);
|
|
431
458
|
options.onSpawn?.(p.pid);
|
|
432
459
|
if (options.signal?.aborted)
|
|
433
460
|
abort();
|
|
@@ -693,21 +720,14 @@ export async function createWorktree(repo, branch, baseRef) {
|
|
|
693
720
|
// match the provisioned SYMLINK, so a worker staging with `git add -A` commits the link and burns an
|
|
694
721
|
// attempt on the scope gate. Write `node_modules` (no slash: matches the link too) into the exclude
|
|
695
722
|
// 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.
|
|
723
|
+
// (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
724
|
// Idempotent (appends only when no entry exists) and best-effort, like the link itself.
|
|
725
|
+
// v2.6.7 T4 (W1): the common dir is the independently derived one (trustedCommonDir), never what a
|
|
726
|
+
// seat-writable commondir names — a redirected commondir never receives the write.
|
|
699
727
|
function excludeNodeModules(dir) {
|
|
700
728
|
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());
|
|
729
|
+
lstatSync(join(dir, ".git")); // only the checkout itself, never an enclosing repository
|
|
730
|
+
const gitDir = trustedCommonDir(dir);
|
|
711
731
|
const exclude = join(gitDir, "info", "exclude");
|
|
712
732
|
const current = existsSync(exclude) ? readFileSync(exclude, "utf8") : "";
|
|
713
733
|
if (/^node_modules$/m.test(current))
|
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>;
|