tickmarkr 1.42.0 → 1.43.0
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/brand.d.ts +5 -0
- package/dist/brand.js +11 -0
- package/dist/gates/acceptance.js +6 -6
- package/dist/gates/llm.js +7 -5
- package/dist/gates/review.d.ts +7 -0
- package/dist/gates/review.js +30 -5
- package/dist/run/consult.js +5 -1
- package/dist/run/daemon.js +16 -1
- package/dist/run/git.d.ts +5 -1
- package/dist/run/git.js +36 -9
- package/dist/run/merge.js +2 -2
- package/package.json +1 -1
package/dist/brand.d.ts
CHANGED
|
@@ -2,3 +2,8 @@ export declare const BANNER: string;
|
|
|
2
2
|
/** ANSI-stripped, trailing-space-trimmed twin of BANNER — README hero and other plain surfaces. */
|
|
3
3
|
export declare const PLAIN_BANNER: string;
|
|
4
4
|
export declare function bannerShell(): string;
|
|
5
|
+
export declare const TICKMARKR_EXIT_TRAILER = "printf '\\nTICKMARKR_''EXIT:%s\\n' $?";
|
|
6
|
+
/** OBS-50: visible-pane bootstrap script — banner + agent command + byte-identical exit trailer. */
|
|
7
|
+
export declare function paneDispatchScript(body: string[]): string;
|
|
8
|
+
/** OBS-50: one short herdr pane-run line; bootstrap lives in the script file beside the prompt. */
|
|
9
|
+
export declare function paneDispatchCommand(scriptPath: string): string;
|
package/dist/brand.js
CHANGED
|
@@ -17,3 +17,14 @@ export function bannerShell() {
|
|
|
17
17
|
const printable = BANNER.replaceAll("\x1b", "\\033").replaceAll("\n", "\\n");
|
|
18
18
|
return `printf '%b\\n' '${printable}'`;
|
|
19
19
|
}
|
|
20
|
+
// OBS-50: quote-split exit marker — herdr echoes the typed command into the transcript that
|
|
21
|
+
// waitOutput matches, so the literal must not appear unsplit in the dispatch line.
|
|
22
|
+
export const TICKMARKR_EXIT_TRAILER = `printf '\\nTICKMARKR_''EXIT:%s\\n' $?`;
|
|
23
|
+
/** OBS-50: visible-pane bootstrap script — banner + agent command + byte-identical exit trailer. */
|
|
24
|
+
export function paneDispatchScript(body) {
|
|
25
|
+
return ["export BASH_SILENCE_DEPRECATION_WARNING=1", ...body, TICKMARKR_EXIT_TRAILER].join("\n");
|
|
26
|
+
}
|
|
27
|
+
/** OBS-50: one short herdr pane-run line; bootstrap lives in the script file beside the prompt. */
|
|
28
|
+
export function paneDispatchCommand(scriptPath) {
|
|
29
|
+
return `bash ${JSON.stringify(scriptPath)}`;
|
|
30
|
+
}
|
package/dist/gates/acceptance.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { channelKey, shq } from "../adapters/types.js";
|
|
2
2
|
import { DEFAULT_DIFF_CAP } from "../config/config.js";
|
|
3
3
|
import { renderAcceptanceItem } from "../graph/schema.js";
|
|
4
|
-
import { sh
|
|
4
|
+
import { sh } from "../run/git.js";
|
|
5
|
+
import { checkDiffCap, fetchTaskDiff } from "./review.js";
|
|
5
6
|
import { extractJson, runLlm } from "./llm.js";
|
|
6
7
|
const isCommand = (a) => typeof a === "object" && a.oracle === "command";
|
|
7
8
|
const isTest = (a) => typeof a === "object" && a.oracle === "test";
|
|
@@ -57,12 +58,11 @@ export async function acceptanceGate(task, worktree, baseRef, judge, via, opts =
|
|
|
57
58
|
const warn = onlyJudge
|
|
58
59
|
? "WARNING: only judge oracles — no deterministic command/test oracle guards this task (deterministic preferred, spec §2).\n"
|
|
59
60
|
: "";
|
|
60
|
-
const diff = await
|
|
61
|
+
const { full: diff, forCap } = await fetchTaskDiff(worktree, baseRef);
|
|
61
62
|
const diffCap = opts.diffCap ?? DEFAULT_DIFF_CAP;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
63
|
+
const capFail = checkDiffCap("acceptance", forCap.length, diffCap, warn + detBlock);
|
|
64
|
+
if (capFail)
|
|
65
|
+
return capFail;
|
|
66
66
|
const prompt = `TICKMARKR-JUDGE
|
|
67
67
|
You are a strict acceptance judge. Decide whether the diff satisfies EVERY acceptance criterion.
|
|
68
68
|
Judge only what the diff proves — plausible-but-wrong must fail. Do not award partial credit.
|
package/dist/gates/llm.js
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdtempSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { formatOwnedName, parseOwnedName } from "../drivers/types.js";
|
|
5
|
-
import { bannerShell } from "../brand.js";
|
|
5
|
+
import { bannerShell, paneDispatchCommand, paneDispatchScript } from "../brand.js";
|
|
6
6
|
import { sh } from "../run/git.js";
|
|
7
7
|
export const GATE_PANE_SEP = " · ";
|
|
8
8
|
/** T8: role-first pane name for fleet visibility — judge · T4, review · T3, consult · T2. */
|
|
@@ -38,13 +38,15 @@ export async function runHeadless(adapter, model, prompt, cwd, timeoutMs = 30000
|
|
|
38
38
|
// v1.1 default path: the same headless CLI call, but dispatched through the driver
|
|
39
39
|
// as a visible named agent (herdr pane), with the quote-split completion wrapper.
|
|
40
40
|
export async function runViaDriver(adapter, model, prompt, cwd, via, timeoutMs = 300000) {
|
|
41
|
-
const
|
|
41
|
+
const dir = mkdtempSync(join(tmpdir(), "tickmarkr-llm-"));
|
|
42
|
+
const pf = join(dir, "prompt.md");
|
|
42
43
|
writeFileSync(pf, prompt);
|
|
44
|
+
const scriptPath = join(dir, "dispatch.sh");
|
|
45
|
+
// OBS-50: bootstrap in a script beside the prompt — pane sees one short bash line + banner, not the raw inline command
|
|
46
|
+
writeFileSync(scriptPath, paneDispatchScript([bannerShell(), adapter.headlessCommand(pf, model)]));
|
|
43
47
|
const slot = await via.driver.slot(cwd, rolePaneNameFromPrompt(prompt, via.name), via.label ? { label: via.label } : undefined);
|
|
44
48
|
via.onSlot?.(slot);
|
|
45
|
-
|
|
46
|
-
// extractJson is anchored to JSON braces and the exit marker to TICKMARKR_EXIT:\d, neither matches banner text
|
|
47
|
-
await via.driver.run(slot, `${bannerShell()}; ${adapter.headlessCommand(pf, model)}; printf '\\nTICKMARKR_''EXIT:%s\\n' $?`);
|
|
49
|
+
await via.driver.run(slot, paneDispatchCommand(scriptPath));
|
|
48
50
|
// wait for the digit-suffixed marker (a real $? exit code), never a bare "TICKMARKR_EXIT:" that a
|
|
49
51
|
// self-referential diff under review merely DISPLAYS — same guard the worker path uses (daemon.ts:193).
|
|
50
52
|
// Without it, a judge/review pane echoing tickmarkr's own source false-completes before its verdict lands.
|
package/dist/gates/review.d.ts
CHANGED
|
@@ -7,6 +7,13 @@ export interface ReviewVerdict {
|
|
|
7
7
|
approve: boolean;
|
|
8
8
|
issues: string[];
|
|
9
9
|
}
|
|
10
|
+
export declare function fetchTaskDiff(worktree: string, baseRef: string): Promise<{
|
|
11
|
+
full: string;
|
|
12
|
+
forCap: string;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function checkDiffCap(gate: string, measured: number, cap: number, prefix?: string): GateResult | null;
|
|
15
|
+
export declare function isDiffCapPark(result: GateResult): boolean;
|
|
16
|
+
export declare function diffCapParkReason(results: GateResult[]): string | null;
|
|
10
17
|
export declare function modelId(model: string): string;
|
|
11
18
|
export declare function pickReviewer(author: Assignment, channels: BillingChannel[], exclude?: string[]): BillingChannel | null;
|
|
12
19
|
export declare function reviewGate(task: Task, worktree: string, baseRef: string, author: Assignment, channels: BillingChannel[], adapters: WorkerAdapter[], cfg: TickmarkrConfig, via?: GateVia, excludeReviewers?: string[]): Promise<GateResult>;
|
package/dist/gates/review.js
CHANGED
|
@@ -5,6 +5,32 @@ import { getAdapter } from "../adapters/registry.js";
|
|
|
5
5
|
import { shOk } from "../run/git.js";
|
|
6
6
|
import { marginalCostRank } from "../route/router.js";
|
|
7
7
|
import { extractJson, runLlm } from "./llm.js";
|
|
8
|
+
// OBS-48: cap on zero-context diff bytes (git diff -U0), not context-padded full diff — scattered
|
|
9
|
+
// one-line hunks no longer trip at ~370 diff-bytes per changed line. Full diff still goes to the judge.
|
|
10
|
+
const DIFF_CAP_REMEDY = "split the task, or raise gates.diffCap";
|
|
11
|
+
export async function fetchTaskDiff(worktree, baseRef) {
|
|
12
|
+
const full = await shOk(`git diff '${baseRef}..HEAD'`, worktree);
|
|
13
|
+
const forCap = await shOk(`git diff -U0 '${baseRef}..HEAD'`, worktree);
|
|
14
|
+
return { full, forCap };
|
|
15
|
+
}
|
|
16
|
+
export function checkDiffCap(gate, measured, cap, prefix = "") {
|
|
17
|
+
if (measured <= cap)
|
|
18
|
+
return null;
|
|
19
|
+
return {
|
|
20
|
+
gate,
|
|
21
|
+
pass: false,
|
|
22
|
+
details: prefix + `diff exceeds verifiable cap (${measured} > ${cap}) — ${DIFF_CAP_REMEDY}`,
|
|
23
|
+
// daemon/run-gates: park('human') immediately — the diff cannot shrink by retrying (OBS-48).
|
|
24
|
+
meta: { park: "human" },
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function isDiffCapPark(result) {
|
|
28
|
+
return result.pass === false && result.meta?.park === "human" && /diff exceeds verifiable cap/i.test(result.details);
|
|
29
|
+
}
|
|
30
|
+
// ponytail: single policy hook for callers after runGates — skips the escalation ladder on diff-cap trips.
|
|
31
|
+
export function diffCapParkReason(results) {
|
|
32
|
+
return results.find(isDiffCapPark)?.details ?? null;
|
|
33
|
+
}
|
|
8
34
|
// FLEET-05: canonical model identity = the segment after the last "/". Provider-prefixed ids name ONE
|
|
9
35
|
// base model behind two harnesses (zai-coding-plan/glm-5.2, zai/glm-5.2 → "glm-5.2"); vendor alone (mixed vs
|
|
10
36
|
// zhipu) is not a diversity signal. Suffix-stripping over-excludes only if two genuinely different
|
|
@@ -37,12 +63,11 @@ export async function reviewGate(task, worktree, baseRef, author, channels, adap
|
|
|
37
63
|
? { gate: "review", pass: false, details: "no cross-vendor reviewer available (diversity rule); set review.required:false to waive" }
|
|
38
64
|
: { gate: "review", pass: true, details: "WARNING: no cross-vendor reviewer available — review waived by config" };
|
|
39
65
|
}
|
|
40
|
-
const diff = await
|
|
66
|
+
const { full: diff, forCap } = await fetchTaskDiff(worktree, baseRef);
|
|
41
67
|
const diffCap = cfg.gates.diffCap ?? DEFAULT_DIFF_CAP;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
68
|
+
const capFail = checkDiffCap("review", forCap.length, diffCap);
|
|
69
|
+
if (capFail)
|
|
70
|
+
return capFail;
|
|
46
71
|
const prompt = `TICKMARKR-REVIEW
|
|
47
72
|
You are a skeptical cross-vendor code reviewer. Another agent (vendor: ${author.adapter}) authored this diff.
|
|
48
73
|
Look for correctness bugs, security issues, and acceptance-criteria gaps. Approve only if you would merge it.
|
package/dist/run/consult.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { getAdapter } from "../adapters/registry.js";
|
|
4
|
+
import { bannerShell, paneDispatchCommand, paneDispatchScript } from "../brand.js";
|
|
4
5
|
import { extractJson, gatePaneName } from "../gates/llm.js";
|
|
5
6
|
import { sh } from "./git.js";
|
|
6
7
|
const MAX_RETRY_GUIDANCE_LINES = 10;
|
|
@@ -73,7 +74,10 @@ opts = {}) {
|
|
|
73
74
|
...(opts.runId ? { owned: { role: "consult", taskId: d.taskId, attempt: 0, runId: opts.runId } } : {}),
|
|
74
75
|
});
|
|
75
76
|
opts.onSlot?.(slot);
|
|
76
|
-
|
|
77
|
+
const scriptPath = join(dir, `${d.taskId}-${n}.sh`);
|
|
78
|
+
// OBS-50: visible consult panes get the brand banner; headless path above stays banner-free (machine-parsed stdout)
|
|
79
|
+
writeFileSync(scriptPath, paneDispatchScript([bannerShell(), adapter.headlessCommand(promptFile, cfg.consult.model)]));
|
|
80
|
+
await driver.run(slot, paneDispatchCommand(scriptPath));
|
|
77
81
|
// digit-suffixed marker only: a dossier quoting tickmarkr's own "TICKMARKR_EXIT:" literal must not
|
|
78
82
|
// false-complete the consult pane before its verdict (same guard as daemon.ts:193 / llm.ts).
|
|
79
83
|
await driver.waitOutput(slot, "TICKMARKR_EXIT:\\d", cfg.consult.stallMinutes * 60_000, { regex: true });
|
package/dist/run/daemon.js
CHANGED
|
@@ -11,7 +11,7 @@ import { captureBaseline, detectGateCommands } from "../gates/baseline.js";
|
|
|
11
11
|
import { runGates } from "../gates/run-gates.js";
|
|
12
12
|
import { addEvidence, attributeBlocked, blockedTasks, getTask, loadGraph, pendingTasks, readyTasks, saveGraph, setStatus } from "../graph/graph.js";
|
|
13
13
|
import { consult, renderRetryGuidance } from "./consult.js";
|
|
14
|
-
import { cleanupRunWorktrees, gitHead, sh } from "./git.js";
|
|
14
|
+
import { cleanupRunWorktrees, gitHead, linkNodeModules, sh, WORKTREE_LAYOUT_CONTRACT } from "./git.js";
|
|
15
15
|
import { Journal, loadRoutingProfile, newRunId } from "./journal.js";
|
|
16
16
|
import { acquireRunLock, releaseRunLock } from "./lock.js";
|
|
17
17
|
import { ensureIntegration, integrationBranch, integrationHead, mergeTask, verifyIntegrationTip } from "./merge.js";
|
|
@@ -343,6 +343,11 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
343
343
|
}
|
|
344
344
|
}
|
|
345
345
|
const promptFile = writePrompt(journal.dir, t, attempt, feedback, nonce);
|
|
346
|
+
// OBS-47: state the worktree layout contract in the worker prompt (cheap-tier workers were
|
|
347
|
+
// committing/deleting node_modules and tripping the scope gate). Prepended, not appended, so the
|
|
348
|
+
// completion trailer stays the structural last line (prompt.ts's design). The harness re-asserts
|
|
349
|
+
// the link itself before gates regardless of what the worker does with it.
|
|
350
|
+
writeFileSync(promptFile, `${WORKTREE_LAYOUT_CONTRACT}\n\n${readFileSync(promptFile, "utf8")}`);
|
|
346
351
|
const adapter = getAdapter(assignment.adapter, adapters);
|
|
347
352
|
// VIS-04: workers share one role tab. T2: `owned` names the pane canonically (ownership contract);
|
|
348
353
|
// the legacy name stays the fallback for drivers without owned handling (subprocess spies).
|
|
@@ -556,6 +561,16 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
556
561
|
}
|
|
557
562
|
graph = setStatus(graph, t.id, "gated");
|
|
558
563
|
saveGraph(repoRoot, graph);
|
|
564
|
+
// OBS-47: re-assert the node_modules link BEFORE gates run on any attempt. A worker may have
|
|
565
|
+
// deleted/replaced the symlink provisioned at worktree creation (run-20260717-004803 T5 lost two
|
|
566
|
+
// attempts + a consult to this); restore it harness-side so a prior attempt's environment damage
|
|
567
|
+
// can never fail a later attempt's gates. Gates never trust worker claims — this runs
|
|
568
|
+
// unconditionally, never on worker say-so. Restoration can fail (EPERM/busy); fail closed with a
|
|
569
|
+
// named environmental verdict instead of letting the test gate mask it as a code red.
|
|
570
|
+
if (!linkNodeModules(repoRoot, wt, { force: true })) {
|
|
571
|
+
await park(t, "environmental: node_modules link could not be re-asserted before gates (OBS-47)", "setup", assignment, attempt + 1, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
559
574
|
const onGate = async (e) => {
|
|
560
575
|
if (e.phase === "start")
|
|
561
576
|
return;
|
package/dist/run/git.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export declare function sh(cmd: string, cwd: string, timeoutMs?: number): Promis
|
|
|
8
8
|
export declare function shOk(cmd: string, cwd: string): Promise<string>;
|
|
9
9
|
export declare function gitHead(cwd: string): Promise<string>;
|
|
10
10
|
export declare const sanitizeBranch: (branch: string) => string;
|
|
11
|
+
export declare const WORKTREES_DIR = "worktrees.noindex";
|
|
11
12
|
export declare const worktreePath: (repo: string, branch: string) => string;
|
|
12
13
|
/** OBS-28: remove this run's recorded worktrees; tolerates already-gone paths. */
|
|
13
14
|
export declare function cleanupRunWorktrees(repo: string, branch: string, opts: {
|
|
@@ -16,5 +17,8 @@ export declare function cleanupRunWorktrees(repo: string, branch: string, opts:
|
|
|
16
17
|
}): Promise<void>;
|
|
17
18
|
export declare function resolveIntegrationBranch(_repo: string, branch: string): Promise<string>;
|
|
18
19
|
export declare function createWorktree(repo: string, branch: string, baseRef: string): Promise<string>;
|
|
19
|
-
export declare function linkNodeModules(repo: string, dir: string
|
|
20
|
+
export declare function linkNodeModules(repo: string, dir: string, { force }?: {
|
|
21
|
+
force?: boolean | undefined;
|
|
22
|
+
}): boolean;
|
|
23
|
+
export declare const WORKTREE_LAYOUT_CONTRACT = "## Worktree layout contract (harness-provisioned \u2014 do not modify)\n- node_modules is a symlink into the main repo's node_modules, provisioned by tickmarkr. Never commit, delete, or replace it \u2014 the harness re-asserts this link before gates run, so modifying it cannot help and may fail your attempt.";
|
|
20
24
|
export declare function removeWorktree(repo: string, dir: string): Promise<void>;
|
package/dist/run/git.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { existsSync, symlinkSync } from "node:fs";
|
|
2
|
+
import { existsSync, lstatSync, readlinkSync, rmSync, symlinkSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { shq } from "../adapters/types.js";
|
|
5
5
|
import { tickmarkrDir } from "../graph/graph.js";
|
|
@@ -49,7 +49,12 @@ export async function gitHead(cwd) {
|
|
|
49
49
|
}
|
|
50
50
|
export const sanitizeBranch = (branch) => branch.replace(/[^\w.-]+/g, "-");
|
|
51
51
|
const sanitize = sanitizeBranch;
|
|
52
|
-
|
|
52
|
+
// OBS-49: macOS Spotlight skips any *.noindex directory, so worktree churn during gate bursts
|
|
53
|
+
// stops spawning mdworkers (9 mdworkers measured on an 18-core M5 Max at load-77). Accepted cost:
|
|
54
|
+
// CLI trust stores key on exact worktree paths (OBS-16), so each worktree re-prompts for trust
|
|
55
|
+
// once after this rename — the same one-time per-install cost as the v1.38 state-dir rename.
|
|
56
|
+
export const WORKTREES_DIR = "worktrees.noindex";
|
|
57
|
+
export const worktreePath = (repo, branch) => join(tickmarkrDir(repo), WORKTREES_DIR, sanitize(branch));
|
|
53
58
|
/** OBS-28: remove this run's recorded worktrees; tolerates already-gone paths. */
|
|
54
59
|
export async function cleanupRunWorktrees(repo, branch, opts) {
|
|
55
60
|
if (opts.removeIntegration)
|
|
@@ -69,25 +74,47 @@ const resolveTaskBranch = async (repo, branch) => {
|
|
|
69
74
|
};
|
|
70
75
|
export async function createWorktree(repo, branch, baseRef) {
|
|
71
76
|
branch = await resolveTaskBranch(repo, branch);
|
|
72
|
-
const dir = join(tickmarkrDir(repo),
|
|
77
|
+
const dir = join(tickmarkrDir(repo), WORKTREES_DIR, sanitize(branch));
|
|
73
78
|
if (existsSync(dir))
|
|
74
79
|
await removeWorktree(repo, dir);
|
|
75
80
|
await shOk(`git worktree add -B ${shq(branch)} ${shq(dir)} ${shq(baseRef)}`, repo);
|
|
76
81
|
linkNodeModules(repo, dir);
|
|
77
82
|
return dir;
|
|
78
83
|
}
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
|
|
84
|
+
// OBS-41/OBS-47: the harness provisions node_modules as a symlink into the main repo so devDep-based
|
|
85
|
+
// gates (tsx, vitest) resolve in a bare worktree. Provisioning (createWorktree) calls this LENIENT —
|
|
86
|
+
// create the link only when dest is absent, never clobber (best-effort, never fails worktree creation).
|
|
87
|
+
// The pre-gate re-assert (OBS-47) calls this with force:true — a worker that deleted/replaced the link
|
|
88
|
+
// (real directory, wrong/broken symlink) is restored to the provisioned link. Idempotent: an
|
|
89
|
+
// already-correct link is a no-op. Returns whether dest is the provisioned link: provisioning ignores
|
|
90
|
+
// the result; the pre-gate caller treats false as a named environmental park, never a masked test red.
|
|
91
|
+
export function linkNodeModules(repo, dir, { force = false } = {}) {
|
|
82
92
|
const src = join(repo, "node_modules");
|
|
83
93
|
const dest = join(dir, "node_modules");
|
|
84
|
-
if (!existsSync(src)
|
|
85
|
-
return;
|
|
94
|
+
if (!existsSync(src))
|
|
95
|
+
return true; // nothing provisioned to link — correct state is no link (OBS-27 best-effort)
|
|
86
96
|
try {
|
|
97
|
+
if (lstatSync(dest).isSymbolicLink() && readlinkSync(dest) === src)
|
|
98
|
+
return true; // already the provisioned link
|
|
99
|
+
}
|
|
100
|
+
catch { /* dest absent — fall through to create */ }
|
|
101
|
+
if (!force && existsSync(dest))
|
|
102
|
+
return false; // lenient provisioning (OBS-41): never clobber an existing entry
|
|
103
|
+
try {
|
|
104
|
+
rmSync(dest, { recursive: true, force: true }); // force tolerates absent; clears a wrong link / real dir / file
|
|
87
105
|
symlinkSync(src, dest, "dir");
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return false;
|
|
88
110
|
}
|
|
89
|
-
catch { /* best-effort */ }
|
|
90
111
|
}
|
|
112
|
+
// OBS-47: the worktree layout the harness provisions, stated in the worker prompt so cheap-tier workers
|
|
113
|
+
// stop tripping the scope gate by committing/deleting/replacing node_modules. The harness re-asserts the
|
|
114
|
+
// link itself before gates regardless (gates never trust worker claims) — this contract just keeps the
|
|
115
|
+
// worker from spending an attempt on environment repair.
|
|
116
|
+
export const WORKTREE_LAYOUT_CONTRACT = `## Worktree layout contract (harness-provisioned — do not modify)
|
|
117
|
+
- node_modules is a symlink into the main repo's node_modules, provisioned by tickmarkr. Never commit, delete, or replace it — the harness re-asserts this link before gates run, so modifying it cannot help and may fail your attempt.`;
|
|
91
118
|
export async function removeWorktree(repo, dir) {
|
|
92
119
|
await sh(`git worktree remove --force ${shq(dir)}`, repo); // best-effort; stale dirs are re-added with -B
|
|
93
120
|
await sh(`rm -rf ${shq(dir)}`, repo);
|
package/dist/run/merge.js
CHANGED
|
@@ -3,14 +3,14 @@ import { join } from "node:path";
|
|
|
3
3
|
import { shq } from "../adapters/types.js";
|
|
4
4
|
import { fingerprint } from "../gates/baseline.js";
|
|
5
5
|
import { tickmarkrDir } from "../graph/graph.js";
|
|
6
|
-
import { gitHead, linkNodeModules, resolveIntegrationBranch, sh, shOk } from "./git.js";
|
|
6
|
+
import { gitHead, linkNodeModules, resolveIntegrationBranch, sh, shOk, WORKTREES_DIR } from "./git.js";
|
|
7
7
|
export function integrationBranch(cfg, runId) {
|
|
8
8
|
return `${cfg.integrationBranchPrefix}${runId}`;
|
|
9
9
|
}
|
|
10
10
|
const sanitize = (branch) => branch.replace(/[^\w.-]+/g, "-");
|
|
11
11
|
export async function ensureIntegration(repo, branch, baseRef) {
|
|
12
12
|
branch = await resolveIntegrationBranch(repo, branch);
|
|
13
|
-
const dir = join(tickmarkrDir(repo),
|
|
13
|
+
const dir = join(tickmarkrDir(repo), WORKTREES_DIR, sanitize(branch));
|
|
14
14
|
if (!existsSync(join(dir, ".git"))) {
|
|
15
15
|
const exists = (await sh(`git rev-parse --verify refs/heads/${shq(branch)}`, repo)).code === 0;
|
|
16
16
|
if (exists) {
|