tickmarkr 2.8.4 → 2.8.6
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 +3 -2
- package/dist/adapters/codex-commit-check.d.ts +4 -3
- package/dist/adapters/codex-commit-check.js +44 -29
- package/dist/adapters/codex.d.ts +2 -2
- package/dist/adapters/codex.js +38 -32
- package/dist/adapters/cursor-agent.js +2 -1
- package/dist/adapters/grok.js +2 -1
- package/dist/adapters/kimi.js +11 -9
- package/dist/adapters/model-lints.js +4 -3
- package/dist/adapters/opencode.js +2 -1
- package/dist/adapters/pi.js +3 -2
- package/dist/adapters/qwen.js +2 -1
- package/dist/adapters/registry.js +2 -1
- package/dist/adapters/types.d.ts +10 -1
- package/dist/adapters/types.js +18 -3
- package/dist/cli/commands/approve.js +9 -28
- package/dist/cli/commands/doctor.d.ts +49 -5
- package/dist/cli/commands/doctor.js +956 -94
- package/dist/cli/commands/fleet.js +11 -3
- package/dist/cli/commands/init.js +27 -19
- package/dist/cli/commands/status.js +9 -6
- package/dist/cli/commands/verify.js +12 -3
- package/dist/compile/gsd.js +4 -3
- package/dist/compile/index.js +4 -1
- package/dist/compile/native.js +8 -6
- package/dist/config/config.d.ts +15 -0
- package/dist/config/config.js +67 -5
- package/dist/drivers/host-env.d.ts +3 -0
- package/dist/drivers/host-env.js +47 -0
- package/dist/drivers/orca.js +3 -1
- package/dist/drivers/subprocess.d.ts +1 -1
- package/dist/drivers/subprocess.js +4 -3
- package/dist/gates/baseline.d.ts +1 -1
- package/dist/gates/baseline.js +4 -3
- package/dist/gates/cache.js +2 -4
- package/dist/graph/schema.js +3 -1
- package/dist/run/daemon.js +54 -4
- package/dist/run/git-trust.d.ts +11 -0
- package/dist/run/git-trust.js +1 -1
- package/dist/run/git.d.ts +13 -1
- package/dist/run/git.js +34 -14
- package/dist/run/journal.d.ts +15 -1
- package/dist/run/journal.js +83 -17
- package/dist/run/lock.d.ts +5 -0
- package/dist/run/lock.js +202 -9
- package/dist/run/protocol.d.ts +1 -1
- package/dist/tui/cockpit/live-store.js +12 -2
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +65 -21
- package/skills/tickmarkr-loop/SKILL.md +65 -21
- package/skills/tickmarkr-overseer/SKILL.md +88 -32
package/dist/gates/cache.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { execSync } from "node:child_process";
|
|
3
2
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
3
|
import { dirname, join, relative, resolve } from "node:path";
|
|
5
4
|
import { tmpdir } from "node:os";
|
|
6
5
|
import { checkoutIncarnation, describeCapacity, inventoryDependencyLinks, resolvedCapacity, shGit, verificationProtocol } from "../run/git.js";
|
|
7
6
|
import { shq } from "../adapters/types.js";
|
|
7
|
+
import { trustedCommonDir } from "../run/git-trust.js";
|
|
8
8
|
import { titleEnvironment } from "../run/title-environment.js";
|
|
9
9
|
export const DEFAULT_VERDICT_CACHE_BOUND = 128;
|
|
10
10
|
let testCacheBound;
|
|
@@ -269,9 +269,7 @@ export function resolveStateDir(worktree, artifactDir) {
|
|
|
269
269
|
return join(worktree, ".tickmarkr");
|
|
270
270
|
}
|
|
271
271
|
try {
|
|
272
|
-
const
|
|
273
|
-
const resolved = resolve(worktree, commonDir);
|
|
274
|
-
const commonRoot = realpathSync(dirname(resolved));
|
|
272
|
+
const commonRoot = realpathSync(dirname(trustedCommonDir(worktree)));
|
|
275
273
|
if (existsSync(join(commonRoot, ".tickmarkr"))) {
|
|
276
274
|
return join(commonRoot, ".tickmarkr");
|
|
277
275
|
}
|
package/dist/graph/schema.js
CHANGED
|
@@ -118,7 +118,9 @@ export const RunGraphSchema = z
|
|
|
118
118
|
source: z.enum(SPEC_SOURCES),
|
|
119
119
|
paths: z.array(z.string()),
|
|
120
120
|
hash: z.string(),
|
|
121
|
-
// Q11
|
|
121
|
+
// Q11 / queue row 220: an author-declared ref, proven at run and resume start against the run's
|
|
122
|
+
// starting commit (HEAD for a run, the recorded run-start base for a resume) by ancestry or patch-id;
|
|
123
|
+
// drifted or unresolvable, the run or resume is refused before it writes anything. Never rebased onto.
|
|
122
124
|
base: z.string().min(1).optional(),
|
|
123
125
|
}),
|
|
124
126
|
tasks: z.array(TaskSchema).min(1),
|
package/dist/run/daemon.js
CHANGED
|
@@ -34,7 +34,7 @@ import { executionSignal, remainingExecutionMs, withExecutionBudget, withoutExec
|
|
|
34
34
|
import { repairSelectionDecision, repairSelectionEnabled } from "./repair-selection.js";
|
|
35
35
|
import { failureDisposition, reserveInfrastructureRetry } from "./recovery.js";
|
|
36
36
|
import { runEnvironment } from "./environment.js";
|
|
37
|
-
import { addDetachedWorktree, changeRepresented, cleanupRunWorktrees, deriveForkCap, FORK_CAP_ENV, gitHead, linkNodeModules, npmDependencyInstallCommand, npmDependencyManifestChanged, PRESERVE_COMMIT_SUBJECT, PRESERVE_PRODUCER_TRAILER, preserveWorktree, producerFields, reflogWitness, removeWorktree, resolvedCapacity, runWithForkBudget, runWithVerificationBudget, sameCapacity, sameVerification, sh, shell, shGit, SUITE_PARENT_ENV, verificationProtocol, WORKTREE_LAYOUT_CONTRACT, WORKTREES_DIR, worktreePath } from "./git.js";
|
|
37
|
+
import { addDetachedWorktree, changeRepresented, cleanupRunWorktrees, declaredBaseContainment, deriveForkCap, FORK_CAP_ENV, gitHead, linkNodeModules, npmDependencyInstallCommand, npmDependencyManifestChanged, PRESERVE_COMMIT_SUBJECT, PRESERVE_PRODUCER_TRAILER, preserveWorktree, producerFields, reflogWitness, removeWorktree, resolvedCapacity, runWithForkBudget, runWithVerificationBudget, sameCapacity, sameVerification, sh, shell, shGit, SUITE_PARENT_ENV, verificationProtocol, WORKTREE_LAYOUT_CONTRACT, WORKTREES_DIR, worktreePath } from "./git.js";
|
|
38
38
|
import { MASK } from "./redact.js";
|
|
39
39
|
import { runInteractiveSeed } from "./interactive-seed.js";
|
|
40
40
|
import { classifyRepairDisposition, resolveScopeHints } from "./repair-disposition.js";
|
|
@@ -1145,6 +1145,48 @@ export function integrationAnchor(events) {
|
|
|
1145
1145
|
return { unreadable: `the newest merge row's from is ${show(merge.data.from)}, not an object id` };
|
|
1146
1146
|
return { anchor: merge.data.commit };
|
|
1147
1147
|
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Queue row 220 (D-2111): prove the spec's declared base against the commit this run starts from — the
|
|
1150
|
+
* checkout HEAD for a fresh run (recorded as its run-start baseRef), the recorded run-start baseRef for a
|
|
1151
|
+
* resume, never the checkout's current HEAD: a resume never re-bases, and no run rebases onto the declared
|
|
1152
|
+
* ref. Drift and an unresolvable answer throw; runDaemon calls this before it writes anything.
|
|
1153
|
+
*/
|
|
1154
|
+
async function proveDeclaredBase(repoRoot, runId, ref, resume) {
|
|
1155
|
+
const action = resume ? `resume ${runId}` : "run";
|
|
1156
|
+
const remedy = `Remedy: check out a branch that contains ${ref}, or correct base: in the spec and recompile.`;
|
|
1157
|
+
const unresolvable = (against, why) => new Error(`refusing to ${action}: declared base ${ref} is unresolvable against ${against} — ${why}. ${remedy}`);
|
|
1158
|
+
let start;
|
|
1159
|
+
let against;
|
|
1160
|
+
if (resume) {
|
|
1161
|
+
const recorded = Journal.open(repoRoot, runId).read().find((e) => e.event === "run-start")?.data.baseRef;
|
|
1162
|
+
if (typeof recorded !== "string" || !recorded)
|
|
1163
|
+
throw unresolvable("the recorded run-start base", "the journal's run-start records no baseRef");
|
|
1164
|
+
start = recorded;
|
|
1165
|
+
against = `the recorded run-start base ${short(start)}`;
|
|
1166
|
+
}
|
|
1167
|
+
else {
|
|
1168
|
+
const head = await shGit("git rev-parse --verify HEAD^{commit}", repoRoot);
|
|
1169
|
+
if (head.code !== 0)
|
|
1170
|
+
throw unresolvable("HEAD", `git said for HEAD: ${(head.stderr || head.stdout).trim()}`);
|
|
1171
|
+
start = head.stdout.trim();
|
|
1172
|
+
against = `HEAD ${short(start)}`;
|
|
1173
|
+
}
|
|
1174
|
+
const probe = await declaredBaseContainment(repoRoot, ref, start);
|
|
1175
|
+
if (probe.result === "contained")
|
|
1176
|
+
return { ref, via: probe.via, start };
|
|
1177
|
+
if (probe.result === "unresolvable")
|
|
1178
|
+
throw unresolvable(against, `git said for ${probe.ref}: ${probe.evidence}`);
|
|
1179
|
+
const lines = await Promise.all(probe.missing.map(async (m) => {
|
|
1180
|
+
const subject = await shGit(`git log -1 --format=%s ${shq(m.commit)}`, repoRoot);
|
|
1181
|
+
// a drift refusal must name the subject: an unreadable one refuses as unresolvable, quoting git
|
|
1182
|
+
if (subject.code !== 0)
|
|
1183
|
+
throw unresolvable(against, `git said for missing commit ${short(m.commit)}: ${(subject.stderr || subject.stdout).trim()}`);
|
|
1184
|
+
return ` ${short(m.commit)} ${subject.stdout.trim()}${m.merge ? " (merge)" : ""}`
|
|
1185
|
+
+ ` — ${m.paths.length > 0 ? m.paths.join(", ") : "no paths"}`;
|
|
1186
|
+
}));
|
|
1187
|
+
throw new Error(`refusing to ${action}: declared base ${ref} has drifted from ${against} — `
|
|
1188
|
+
+ `${probe.missing.length} declared commit(s) missing:\n${lines.join("\n")}\n${remedy}`);
|
|
1189
|
+
}
|
|
1148
1190
|
/** The ref legs' details: the leg, both commits, the symbolic target and the repair text. */
|
|
1149
1191
|
export function identityDetails(leg, state, branch, runId, expected) {
|
|
1150
1192
|
const found = state.kind === "regular" || state.kind === "symbolic" ? state.commit : undefined;
|
|
@@ -2105,6 +2147,12 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2105
2147
|
let baselineFailed = false;
|
|
2106
2148
|
try {
|
|
2107
2149
|
graph = loadGraph(repoRoot);
|
|
2150
|
+
// Queue row 220 (D-2111): a declared base is proven HERE — before any journal row (lock-reclaimed and
|
|
2151
|
+
// exit-cause included), the resume anchor checks, a worktree, the integration ref or baseline capture —
|
|
2152
|
+
// so a refusal leaves nothing; the finally releases the lock and repository claim. No base: unchanged.
|
|
2153
|
+
const declaredBase = graph.spec.base === undefined ? undefined
|
|
2154
|
+
: await proveDeclaredBase(repoRoot, runId, graph.spec.base, opts.resume === true);
|
|
2155
|
+
const declaredBaseRow = declaredBase ? { declaredBase: { ref: declaredBase.ref, via: declaredBase.via } } : {};
|
|
2108
2156
|
// One bounded snapshot supplies every dispatch. On resume the current journal still participates
|
|
2109
2157
|
// in the fold so its task-done/ordinary-approval events can retire older evidence, but findings it
|
|
2110
2158
|
// produced are suppressed: same-run feedback already replays those bytes and must not deliver them
|
|
@@ -2943,6 +2991,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2943
2991
|
...(replayedExclusions.size > 0 ? { excludedChannels: [...replayedExclusions].sort() } : {}),
|
|
2944
2992
|
...(opts.retryFailed ? { retryFailed: true } : {}),
|
|
2945
2993
|
...(opts.graphChanged ? { graphChanged: true } : {}), // OBS-1073: the release is the engagement's
|
|
2994
|
+
...declaredBaseRow,
|
|
2946
2995
|
});
|
|
2947
2996
|
runStarted = true;
|
|
2948
2997
|
await placeBoard();
|
|
@@ -2993,7 +3042,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2993
3042
|
}
|
|
2994
3043
|
}
|
|
2995
3044
|
else {
|
|
2996
|
-
baseRef = await gitHead(repoRoot);
|
|
3045
|
+
baseRef = declaredBase?.start ?? await gitHead(repoRoot); // row 220: the proven commit is the run's base
|
|
2997
3046
|
journal.append("baseline-start", undefined, { baseRef, commands, capacity: resolvedCapacity() });
|
|
2998
3047
|
await placeBoard();
|
|
2999
3048
|
baselinePending = true;
|
|
@@ -3034,6 +3083,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3034
3083
|
branch, graphDefinitionHash: graphDefinitionHash(graph),
|
|
3035
3084
|
integrationAnchor: "journal", // 2.8.1 T2: this run creates its branch at baseRef and journals every publication with `from`
|
|
3036
3085
|
...(prior ? { supersedes: prior.runId } : {}),
|
|
3086
|
+
...declaredBaseRow,
|
|
3037
3087
|
}); // graphDefinitionHash: T3 engagement identity (status+resume share it); pid: v1.13 (VIS-11) liveness; mode/modeSource: v1.51 T2; supersedes: v1.53 T5
|
|
3038
3088
|
runStarted = true;
|
|
3039
3089
|
// v1.53 T5: mark the prior run AFTER this run's run-start exists, so the prior journal never
|
|
@@ -5186,7 +5236,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
5186
5236
|
const icmd = retryMode === "resume"
|
|
5187
5237
|
? adapter.resumeCommand(sessionId, promptFile, assignment.model, assignment.effort)
|
|
5188
5238
|
: cfg.visibility.worker === "interactive" && driver.interactive
|
|
5189
|
-
? adapter.interactiveCommand(promptFile, assignment.model, assignment.effort)
|
|
5239
|
+
? adapter.interactiveCommand(promptFile, assignment.model, assignment.effort, { cwd: wt, branch: taskBranch, repoRoot })
|
|
5190
5240
|
: null;
|
|
5191
5241
|
// v1.69 T6: adapters that declare interactiveSeed launch the real TUI and inject the prompt as a
|
|
5192
5242
|
// user turn; they do NOT need the argv-seeding surface that interactiveCommand represents.
|
|
@@ -5203,7 +5253,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
5203
5253
|
// timing can never interleave a `$(…)` with what follows it (the codex corruption class).
|
|
5204
5254
|
const workerCmd = interactive
|
|
5205
5255
|
? (hasSeed ? ":" : icmd)
|
|
5206
|
-
: adapter.invoke(t, wt, assignment, { promptFile }).command;
|
|
5256
|
+
: adapter.invoke(t, wt, assignment, { promptFile, branch: taskBranch, repoRoot }).command;
|
|
5207
5257
|
const dispatchScript = promptFile.replace(/\.md$/, ".sh");
|
|
5208
5258
|
// Approval can reset the attempt counter while the old pane remains retained.
|
|
5209
5259
|
// Its ownership claim must survive a new engagement reusing the script path.
|
package/dist/run/git-trust.d.ts
CHANGED
|
@@ -4,11 +4,21 @@ export declare class GitTrustRefusal extends Error {
|
|
|
4
4
|
readonly detail: string;
|
|
5
5
|
constructor(path: string, detail: string, options?: ErrorOptions);
|
|
6
6
|
}
|
|
7
|
+
interface Authority {
|
|
8
|
+
gitdir: string;
|
|
9
|
+
common: string;
|
|
10
|
+
linked: boolean;
|
|
11
|
+
}
|
|
7
12
|
/**
|
|
8
13
|
* The canonical, independently expected common git directory for the checkout enclosing `cwd` — never what
|
|
9
14
|
* a mutable commondir says. Throws GitTrustRefusal naming the authority path when it cannot be established.
|
|
10
15
|
*/
|
|
11
16
|
export declare function trustedCommonDir(cwd: string): string;
|
|
17
|
+
/** The checked authority for `cwd` (see assertGitTrust), with the canonical worktree root the gitfile sits in. */
|
|
18
|
+
export declare function checkedAuthority(cwd: string): (Authority & {
|
|
19
|
+
worktree: string;
|
|
20
|
+
stamp?: string;
|
|
21
|
+
}) | undefined;
|
|
12
22
|
/**
|
|
13
23
|
* The uncached pre-spawn check for a covered git invocation at `cwd`. Outside any checkout it is a no-op
|
|
14
24
|
* (unrelated shell commands stay compatible). Inside one it refuses, naming the file, when the commondir git
|
|
@@ -119,3 +129,4 @@ export declare const INERT_HOOKS_PATH = "/dev/null";
|
|
|
119
129
|
* children (shGit/shGitOk) receive it; payload shells (gate commands, suites) keep the operator's git config.
|
|
120
130
|
*/
|
|
121
131
|
export declare function protectedGitEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
132
|
+
export {};
|
package/dist/run/git-trust.js
CHANGED
|
@@ -151,7 +151,7 @@ catch {
|
|
|
151
151
|
return false;
|
|
152
152
|
} };
|
|
153
153
|
/** The checked authority for `cwd` (see assertGitTrust), with the canonical worktree root the gitfile sits in. */
|
|
154
|
-
function checkedAuthority(cwd) {
|
|
154
|
+
export function checkedAuthority(cwd) {
|
|
155
155
|
const dotGit = dotGitOf(cwd);
|
|
156
156
|
if (!dotGit)
|
|
157
157
|
return undefined;
|
package/dist/run/git.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CommandReceiptAttribution, ShellReceipt } from "./protocol.js";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
2
|
+
import { spawn, type SpawnSyncReturns } from "node:child_process";
|
|
3
3
|
import { ROUTING_ENV_SEAMS } from "../route/router.js";
|
|
4
4
|
import { type OwnGitCapability } from "./git-trust.js";
|
|
5
5
|
export { ROUTING_ENV_SEAMS };
|
|
@@ -187,6 +187,18 @@ export declare function sh(cmd: string, cwd: string, timeoutMs?: number, options
|
|
|
187
187
|
export declare function shGit(cmd: string, cwd: string, timeoutMs?: number, own?: OwnGitOptions, options?: Pick<ShellOptions, "bytes">): Promise<ShResult>;
|
|
188
188
|
export declare function shOk(cmd: string, cwd: string): Promise<string>;
|
|
189
189
|
export declare function shGitOk(cmd: string, cwd: string, own?: OwnGitOptions): Promise<string>;
|
|
190
|
+
/**
|
|
191
|
+
* Queue row 2 item b (D-1042, D-2112, D-2114): the SYNCHRONOUS own-git entry for operator commands — argv, no shell.
|
|
192
|
+
* It applies what shGit's own-git entry applies, from the same sources: the uncached trust check and, in a linked
|
|
193
|
+
* checkout, ownGitPin's pin, post-exit recheck and ref-view release, each derived from `dir` — the directory git
|
|
194
|
+
* targets (a site's -C directory), never the caller's cwd — and protectedGitEnv's forced core.fsmonitor=false and
|
|
195
|
+
* inert core.hooksPath appended last. A trust refusal throws GitTrustRefusal naming the path (before the spawn, or
|
|
196
|
+
* after it for a commondir changed while the child ran); every other outcome is spawnSync's own result, so a caller's
|
|
197
|
+
* fail-open answer for an ordinary git failure is unchanged.
|
|
198
|
+
*/
|
|
199
|
+
export declare function gitSync(dir: string, args: readonly string[], { env }?: {
|
|
200
|
+
env?: NodeJS.ProcessEnv;
|
|
201
|
+
}): SpawnSyncReturns<string>;
|
|
190
202
|
/**
|
|
191
203
|
* v2.6.8 T2: the DETACHED worktree capability, declared by calling it (the daemon's baseline recapture and standalone
|
|
192
204
|
* verify's base worktree). Under the ref-store pin git stubs a detached add's new HEAD as `ref: refs/heads/.invalid`
|
package/dist/run/git.js
CHANGED
|
@@ -2,14 +2,15 @@ import { observeVerificationReceipt, observeVerificationSpawn, verificationJobEn
|
|
|
2
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
|
-
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
6
|
-
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
|
|
6
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmdirSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { availableParallelism, homedir, tmpdir } from "node:os";
|
|
8
8
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
9
9
|
import { StringDecoder } from "node:string_decoder";
|
|
10
10
|
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
|
+
import { HOST_SESSION_VARS } from "../drivers/host-env.js";
|
|
13
14
|
import { titleEnvironment } from "./title-environment.js";
|
|
14
15
|
import { assertGitTrust, GitTrustRefusal, ownGitPin, protectedGitEnv, trustedCommonDir } from "./git-trust.js";
|
|
15
16
|
export { ROUTING_ENV_SEAMS };
|
|
@@ -330,6 +331,9 @@ function executeShell(cmd, cwd, timeoutMs, login, options, observation, ownGit)
|
|
|
330
331
|
const env = ownGit ? protectedGitEnv(payloadEnv) : payloadEnv;
|
|
331
332
|
for (const k of ROUTING_ENV_SEAMS)
|
|
332
333
|
delete env[k];
|
|
334
|
+
// Queue row 193 (D-2102 add.1): no gate or agent child inherits the launching agent session's host variables.
|
|
335
|
+
for (const k of HOST_SESSION_VARS)
|
|
336
|
+
delete env[k];
|
|
333
337
|
// A run freezes the operator override and cores at startup; admission can lower the round cap.
|
|
334
338
|
env[FORK_CAP_ENV] = verificationBudget.getStore()
|
|
335
339
|
? String(resolvedCapacity().forkCap)
|
|
@@ -596,6 +600,27 @@ export async function shGitOk(cmd, cwd, own = {}) {
|
|
|
596
600
|
throw new Error(`command failed (${r.code}): ${cmd}\n${r.stderr || r.stdout}`);
|
|
597
601
|
return r.stdout;
|
|
598
602
|
}
|
|
603
|
+
/**
|
|
604
|
+
* Queue row 2 item b (D-1042, D-2112, D-2114): the SYNCHRONOUS own-git entry for operator commands — argv, no shell.
|
|
605
|
+
* It applies what shGit's own-git entry applies, from the same sources: the uncached trust check and, in a linked
|
|
606
|
+
* checkout, ownGitPin's pin, post-exit recheck and ref-view release, each derived from `dir` — the directory git
|
|
607
|
+
* targets (a site's -C directory), never the caller's cwd — and protectedGitEnv's forced core.fsmonitor=false and
|
|
608
|
+
* inert core.hooksPath appended last. A trust refusal throws GitTrustRefusal naming the path (before the spawn, or
|
|
609
|
+
* after it for a commondir changed while the child ran); every other outcome is spawnSync's own result, so a caller's
|
|
610
|
+
* fail-open answer for an ordinary git failure is unchanged.
|
|
611
|
+
*/
|
|
612
|
+
export function gitSync(dir, args, { env = process.env } = {}) {
|
|
613
|
+
const forced = protectedGitEnv(env);
|
|
614
|
+
const pin = ownGitPin(dir, forced);
|
|
615
|
+
try {
|
|
616
|
+
const r = spawnSync("git", args, { cwd: dir, env: pin?.env ?? forced, encoding: "utf8", maxBuffer: 1 << 28, stdio: ["ignore", "pipe", "pipe"] });
|
|
617
|
+
pin?.recheck();
|
|
618
|
+
return pin && typeof r.stdout === "string" ? { ...r, stdout: pin.unview(r.stdout), stderr: pin.unview(r.stderr) } : r;
|
|
619
|
+
}
|
|
620
|
+
finally {
|
|
621
|
+
pin?.release();
|
|
622
|
+
}
|
|
623
|
+
}
|
|
599
624
|
/**
|
|
600
625
|
* v2.6.8 T2: the DETACHED worktree capability, declared by calling it (the daemon's baseline recapture and standalone
|
|
601
626
|
* verify's base worktree). Under the ref-store pin git stubs a detached add's new HEAD as `ref: refs/heads/.invalid`
|
|
@@ -1089,19 +1114,14 @@ export async function probeRefsWritable(cwd = process.cwd()) {
|
|
|
1089
1114
|
const error = (delRes.stderr || delRes.stdout).trim() || `git update-ref -d failed (${delRes.code})`;
|
|
1090
1115
|
return { ok: false, path: refsDir, refsDir, error };
|
|
1091
1116
|
}
|
|
1092
|
-
//
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
const tmDir = join(refsDir, "tickmarkr");
|
|
1099
|
-
if (existsSync(tmDir) && readdirSync(tmDir).length === 0) {
|
|
1100
|
-
rmSync(tmDir, { recursive: true, force: true });
|
|
1117
|
+
// Queue row 194 (D-2102): remove only EMPTY directories git left behind, non-recursively — rmdir(2)
|
|
1118
|
+
// refuses (ENOTEMPTY) a ref another process writes under refs/tickmarkr at this moment, where an
|
|
1119
|
+
// emptiness check followed by a recursive remove deleted it. Best-effort: a refusal leaves the tree.
|
|
1120
|
+
for (const dir of [join(refsDir, "tickmarkr", "preflight"), join(refsDir, "tickmarkr")]) {
|
|
1121
|
+
try {
|
|
1122
|
+
rmdirSync(dir);
|
|
1101
1123
|
}
|
|
1102
|
-
|
|
1103
|
-
catch {
|
|
1104
|
-
// best-effort cleanup of empty dirs
|
|
1124
|
+
catch { /* absent, or no longer empty */ }
|
|
1105
1125
|
}
|
|
1106
1126
|
return { ok: true, path: refsDir, refsDir };
|
|
1107
1127
|
}
|
package/dist/run/journal.d.ts
CHANGED
|
@@ -42,7 +42,14 @@ export declare const bindingToken: (binding: DecisionBinding) => string;
|
|
|
42
42
|
export declare function parseBindingToken(token: string): DecisionBinding | undefined;
|
|
43
43
|
/** The binding a task-approved row recorded under `park` or `failure`, when well formed. */
|
|
44
44
|
export declare function recordedBinding(value: unknown): DecisionBinding | undefined;
|
|
45
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* The newest failed gate before a park row — the gate a waive of that park would satisfy.
|
|
47
|
+
* Queue row 225 (D-2111): a gate-fail park with no task-dispatch, gate-result (passing or failing) or
|
|
48
|
+
* unvoided task-approved row of its task since its previous task-human — a resume's re-park at the same
|
|
49
|
+
* review round cap — inherits that park's failed gate, the walk continuing past it. A task-approved row
|
|
50
|
+
* an approval-refused row of the task voids before the newer park (its lines name it, or it names none)
|
|
51
|
+
* does not end the walk; refusals are read here directly, never through foldDecisions (which calls this).
|
|
52
|
+
*/
|
|
46
53
|
export declare function failedGateBeforePark(events: readonly JournalEvent[], taskId: string, parkIndex: number): GateName | undefined;
|
|
47
54
|
/** The physical journal line of `events[i]` (see SOURCE_LINE). */
|
|
48
55
|
export declare const physicalLine: (events: readonly JournalEvent[], i: number) => number;
|
|
@@ -96,6 +103,13 @@ export interface PreservedRef {
|
|
|
96
103
|
export declare function preservedRefsByTask(events: JournalEvent[]): Map<string, PreservedRef[]>;
|
|
97
104
|
export declare function reviewRoundsSinceApproval(events: JournalEvent[], taskId: string, rounds?: (decided: JournalEvent[]) => JournalEvent[]): number;
|
|
98
105
|
export declare function upheldFeedbackByTask(events: JournalEvent[]): Map<string, string>;
|
|
106
|
+
/**
|
|
107
|
+
* Queue row 225 (D-2111): a restore names upheld feedback only when it is FUNDED — the task carries an
|
|
108
|
+
* active uphold brief AND an effective task-approved row newer than its newest task-human. A restore
|
|
109
|
+
* that only re-parks at the same cap announces nothing, so a spent uphold is never re-announced.
|
|
110
|
+
* `decided` is already read through the decision fold (effectiveEvents).
|
|
111
|
+
*/
|
|
112
|
+
export declare function restoresUpheldFeedback(decided: JournalEvent[], taskId: string): boolean;
|
|
99
113
|
export interface StructuredFinding {
|
|
100
114
|
class: string;
|
|
101
115
|
path: string;
|
package/dist/run/journal.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
|
|
4
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmdirSync } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { channelKey, shq, TokenUsageSchema } from "../adapters/types.js";
|
|
@@ -11,7 +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
|
+
import { assertGitTrust, protectedGitEnv, trustedCommonDir } from "./git-trust.js";
|
|
15
15
|
import { normalizeGateOutcome } from "./outcome.js";
|
|
16
16
|
import { redactSecrets } from "./redact.js";
|
|
17
17
|
export function phaseForGate(gate) {
|
|
@@ -97,17 +97,44 @@ export function recordedBinding(value) {
|
|
|
97
97
|
const { line, ts } = value;
|
|
98
98
|
return typeof line === "number" && Number.isSafeInteger(line) && line > 0 && typeof ts === "string" ? { line, ts } : undefined;
|
|
99
99
|
}
|
|
100
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* The newest failed gate before a park row — the gate a waive of that park would satisfy.
|
|
102
|
+
* Queue row 225 (D-2111): a gate-fail park with no task-dispatch, gate-result (passing or failing) or
|
|
103
|
+
* unvoided task-approved row of its task since its previous task-human — a resume's re-park at the same
|
|
104
|
+
* review round cap — inherits that park's failed gate, the walk continuing past it. A task-approved row
|
|
105
|
+
* an approval-refused row of the task voids before the newer park (its lines name it, or it names none)
|
|
106
|
+
* does not end the walk; refusals are read here directly, never through foldDecisions (which calls this).
|
|
107
|
+
*/
|
|
101
108
|
export function failedGateBeforePark(events, taskId, parkIndex) {
|
|
109
|
+
let inherits = events[parkIndex]?.data.kind === "gate-fail";
|
|
110
|
+
let refused = new Set();
|
|
102
111
|
for (let i = parkIndex - 1; i >= 0; i -= 1) {
|
|
103
112
|
const event = events[i];
|
|
104
113
|
if (event.taskId !== taskId)
|
|
105
114
|
continue;
|
|
106
|
-
if (event.event === "task-
|
|
115
|
+
if (event.event === "task-human") {
|
|
116
|
+
// Only a gate-fail predecessor has a failed gate to hand down; any other kind ends the walk as today.
|
|
117
|
+
if (!inherits || event.data.kind !== "gate-fail")
|
|
118
|
+
return undefined;
|
|
119
|
+
// The walk now reads the previous park's own span, under that park's own refusals.
|
|
120
|
+
refused = new Set();
|
|
121
|
+
}
|
|
122
|
+
else if (event.event === APPROVAL_REFUSED) {
|
|
123
|
+
if (refused !== "all")
|
|
124
|
+
refused = Array.isArray(event.data.lines) ? new Set([...refused, ...event.data.lines]) : "all";
|
|
125
|
+
}
|
|
126
|
+
else if (event.event === "task-approved") {
|
|
127
|
+
if (!inherits || (refused !== "all" && !refused.has(physicalLine(events, i))))
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
else if (event.event === "task-dispatch") {
|
|
107
131
|
return undefined;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
132
|
+
}
|
|
133
|
+
else if (event.event === "gate-result") {
|
|
134
|
+
if (event.data.pass === false && typeof event.data.gate === "string" && GATE_NAMES.includes(event.data.gate)) {
|
|
135
|
+
return event.data.gate;
|
|
136
|
+
}
|
|
137
|
+
inherits = false; // a passing gate-result keeps the park's own span
|
|
111
138
|
}
|
|
112
139
|
}
|
|
113
140
|
return undefined;
|
|
@@ -321,6 +348,16 @@ export function upheldFeedbackByTask(events) {
|
|
|
321
348
|
}
|
|
322
349
|
return upheld;
|
|
323
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* Queue row 225 (D-2111): a restore names upheld feedback only when it is FUNDED — the task carries an
|
|
353
|
+
* active uphold brief AND an effective task-approved row newer than its newest task-human. A restore
|
|
354
|
+
* that only re-parks at the same cap announces nothing, so a spent uphold is never re-announced.
|
|
355
|
+
* `decided` is already read through the decision fold (effectiveEvents).
|
|
356
|
+
*/
|
|
357
|
+
export function restoresUpheldFeedback(decided, taskId) {
|
|
358
|
+
const newest = (event) => decided.reduce((at, e, i) => e.taskId === taskId && e.event === event ? i : at, -1);
|
|
359
|
+
return newest("task-approved") > newest("task-human") && upheldFeedbackByTask(decided).has(taskId);
|
|
360
|
+
}
|
|
324
361
|
const CONSULT_ACTIONS = ["retry", "reroute", "decompose", "human"];
|
|
325
362
|
const consultAction = (action) => typeof action === "string" && CONSULT_ACTIONS.includes(action)
|
|
326
363
|
? action
|
|
@@ -1651,30 +1688,59 @@ export function engagementComparable(events, loadedHash) {
|
|
|
1651
1688
|
return recorded === loadedHash ? { comparable: true, recorded } : { comparable: false, reason: "mismatch", recorded };
|
|
1652
1689
|
}
|
|
1653
1690
|
const RUN_SEQUENCE_WIDTH = 16;
|
|
1691
|
+
const TICKET = /^\d{16}$/;
|
|
1692
|
+
const ticketsIn = (dir) => {
|
|
1693
|
+
try {
|
|
1694
|
+
return readdirSync(dir).filter((name) => TICKET.test(name));
|
|
1695
|
+
}
|
|
1696
|
+
catch (error) {
|
|
1697
|
+
if (error.code === "ENOENT")
|
|
1698
|
+
return [];
|
|
1699
|
+
throw error;
|
|
1700
|
+
}
|
|
1701
|
+
};
|
|
1702
|
+
const highest = (tickets) => tickets.reduce((max, name) => (BigInt(name) > max ? BigInt(name) : max), 0n);
|
|
1654
1703
|
// The daemon mints before acquiring graph.lock, and each CLI invocation has fresh module state. Claim
|
|
1655
|
-
// a
|
|
1656
|
-
//
|
|
1704
|
+
// a ticket with mkdir's atomic EEXIST boundary: a losing process advances and retries. Queue row 194
|
|
1705
|
+
// (D-2102): the sequence lives at the trusted common git dir, shared by the main checkout and every
|
|
1706
|
+
// linked worktree (a trust refusal refuses — never a per-checkout fallback), and its first ticket sits
|
|
1707
|
+
// above this checkout's own pre-change sequence. Pruning keeps it bounded, and the durable high-water
|
|
1708
|
+
// mark is the highest ticket itself: a prune removes a ticket only after seeing a higher one, so the
|
|
1709
|
+
// highest ever claimed survives. A stale reader can recreate a pruned ticket, but then sees that higher
|
|
1710
|
+
// one and claims again above it — only a ticket that is the highest after its own mkdir is issued.
|
|
1657
1711
|
function claimRunSequence(repoRoot = process.cwd()) {
|
|
1658
|
-
const dir = join(
|
|
1712
|
+
const dir = join(trustedCommonDir(repoRoot), "tickmarkr-run-id-sequence");
|
|
1659
1713
|
mkdirSync(dir, { recursive: true });
|
|
1660
|
-
const
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
return value > max ? value : max;
|
|
1664
|
-
}, 0n) + 1n;
|
|
1714
|
+
const own = highest(ticketsIn(join(repoRoot, stateDirName(repoRoot), "run-id-sequence")));
|
|
1715
|
+
const shared = highest(ticketsIn(dir));
|
|
1716
|
+
let candidate = (own > shared ? own : shared) + 1n;
|
|
1665
1717
|
while (true) {
|
|
1666
1718
|
const suffix = candidate.toString().padStart(RUN_SEQUENCE_WIDTH, "0");
|
|
1667
1719
|
if (suffix.length > RUN_SEQUENCE_WIDTH)
|
|
1668
1720
|
throw new Error("run id sequence exhausted");
|
|
1669
1721
|
try {
|
|
1670
1722
|
mkdirSync(join(dir, suffix));
|
|
1671
|
-
return suffix;
|
|
1672
1723
|
}
|
|
1673
1724
|
catch (error) {
|
|
1674
1725
|
if (error.code !== "EEXIST")
|
|
1675
1726
|
throw error;
|
|
1676
1727
|
candidate += 1n;
|
|
1728
|
+
continue;
|
|
1729
|
+
}
|
|
1730
|
+
const tickets = ticketsIn(dir);
|
|
1731
|
+
const top = highest(tickets);
|
|
1732
|
+
if (top > candidate) {
|
|
1733
|
+
candidate = top + 1n;
|
|
1734
|
+
continue;
|
|
1735
|
+
}
|
|
1736
|
+
for (const name of tickets) {
|
|
1737
|
+
if (BigInt(name) < candidate)
|
|
1738
|
+
try {
|
|
1739
|
+
rmdirSync(join(dir, name));
|
|
1740
|
+
}
|
|
1741
|
+
catch { /* a concurrent prune took it */ }
|
|
1677
1742
|
}
|
|
1743
|
+
return suffix;
|
|
1678
1744
|
}
|
|
1679
1745
|
}
|
|
1680
1746
|
export function newRunId(now = new Date()) {
|
|
@@ -1979,7 +2045,7 @@ export class Journal {
|
|
|
1979
2045
|
const folded = !decisionRow && data.fatal === true ? [...priorEvents, { ts: new Date().toISOString(), event, data }] : priorEvents;
|
|
1980
2046
|
return decisionRow ? reduced : { ...reduced, owedChecks: foldOwedChecks(folded, join(this.dir, "..", "..", "..")) };
|
|
1981
2047
|
})()
|
|
1982
|
-
: event === "resume-restore" && rowTaskId &&
|
|
2048
|
+
: event === "resume-restore" && rowTaskId && restoresUpheldFeedback(effectiveEvents(priorEvents), rowTaskId)
|
|
1983
2049
|
? {
|
|
1984
2050
|
...inputData,
|
|
1985
2051
|
upheldFeedbackRestoredFor: rowTaskId,
|
package/dist/run/lock.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export declare const STALE_MS = 60000;
|
|
|
3
3
|
export interface Inspection {
|
|
4
4
|
pid?: number;
|
|
5
5
|
runId?: string;
|
|
6
|
+
startedAt?: number;
|
|
7
|
+
checkout?: string;
|
|
6
8
|
garbage: boolean;
|
|
7
9
|
dead: boolean;
|
|
8
10
|
expired: boolean;
|
|
@@ -15,6 +17,9 @@ export type RunLineEvent = {
|
|
|
15
17
|
};
|
|
16
18
|
export declare function shouldRefuse(i: Pick<Inspection, "garbage" | "dead">): boolean;
|
|
17
19
|
export declare function isPidLive(pid: number): boolean;
|
|
20
|
+
/** The repository run claim for the checkout enclosing `repoRoot`; undefined when no checkout encloses it
|
|
21
|
+
* (graph.lock alone guards there, as before). Any other git trust refusal propagates: fail closed. */
|
|
22
|
+
export declare function repositoryClaimPath(repoRoot: string): string | undefined;
|
|
18
23
|
export declare function acquireRunLock(repoRoot: string, runId: string): {
|
|
19
24
|
reclaimed?: {
|
|
20
25
|
pid: number;
|