tickmarkr 1.44.0 → 1.46.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/adapters/fake.js +2 -2
- package/dist/adapters/registry.js +12 -3
- package/dist/brand.js +2 -1
- package/dist/cli/commands/approve.js +6 -7
- package/dist/cli/commands/compile.js +10 -6
- package/dist/cli/commands/init.js +55 -3
- package/dist/cli/commands/profile.js +129 -7
- package/dist/cli/commands/resume.js +5 -1
- package/dist/cli/commands/status.js +6 -4
- package/dist/compile/collateral.js +4 -2
- package/dist/drivers/herdr.d.ts +1 -0
- package/dist/drivers/herdr.js +12 -2
- package/dist/gates/acceptance.d.ts +2 -0
- package/dist/gates/acceptance.js +98 -27
- package/dist/gates/llm.d.ts +8 -0
- package/dist/gates/llm.js +92 -8
- package/dist/gates/review.js +6 -3
- package/dist/gates/scope.js +2 -2
- package/dist/graph/graph.d.ts +1 -0
- package/dist/graph/graph.js +12 -0
- package/dist/plan/prompt.js +1 -1
- package/dist/route/profile.d.ts +22 -0
- package/dist/route/profile.js +42 -10
- package/dist/route/router.d.ts +2 -2
- package/dist/route/router.js +10 -2
- package/dist/run/consult.d.ts +6 -1
- package/dist/run/consult.js +54 -10
- package/dist/run/daemon.d.ts +1 -0
- package/dist/run/daemon.js +165 -27
- package/dist/run/git.d.ts +2 -0
- package/dist/run/git.js +20 -7
- package/dist/run/journal.d.ts +53 -4
- package/dist/run/journal.js +137 -16
- package/dist/run/lock.js +3 -3
- package/dist/run/merge.js +8 -8
- package/package.json +1 -1
package/dist/run/journal.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { type Assignment } from "../adapters/types.js";
|
|
3
3
|
import type { TickmarkrConfig } from "../config/config.js";
|
|
4
4
|
import { type TaskStatus } from "../graph/schema.js";
|
|
5
|
-
import { type RoutingProfile } from "../route/profile.js";
|
|
5
|
+
import { type ProfileDiscount, type RoutingProfile } from "../route/profile.js";
|
|
6
6
|
export interface JournalEvent {
|
|
7
7
|
ts: string;
|
|
8
8
|
event: string;
|
|
@@ -16,10 +16,21 @@ export interface ResumeState {
|
|
|
16
16
|
lastAssignment?: Assignment;
|
|
17
17
|
}
|
|
18
18
|
export declare const ATTEMPT_CAP_RELEASE: "attempt-cap";
|
|
19
|
-
export declare const PARK_KINDS: readonly ["ladder-exhausted", "attempt-cap", "gate-fail", "quota", "reroute-exhausted", "setup", "stall", "merge-conflict"];
|
|
19
|
+
export declare const PARK_KINDS: readonly ["human-gate", "ladder-exhausted", "attempt-cap", "gate-fail", "quota", "reroute-exhausted", "setup", "stall", "merge-conflict", "tip-moved"];
|
|
20
20
|
export type ParkKind = (typeof PARK_KINDS)[number];
|
|
21
21
|
export declare const RETRY_MODES: readonly ["resume", "fresh"];
|
|
22
22
|
export type RetryMode = (typeof RETRY_MODES)[number];
|
|
23
|
+
export declare const WORKER_RESULT_CAUSES: readonly ["provider-death", "stall-timeout", "malformed-trailer", "clean-exit-no-trailer"];
|
|
24
|
+
export type WorkerResultCause = (typeof WORKER_RESULT_CAUSES)[number];
|
|
25
|
+
/** OBS-53: classify worker-result failures so retries and routing see the true signal, not one lumped bucket. */
|
|
26
|
+
export declare function classifyWorkerResultCause(opts: {
|
|
27
|
+
output: string;
|
|
28
|
+
ok: boolean;
|
|
29
|
+
finished: boolean;
|
|
30
|
+
exitCode: number | null;
|
|
31
|
+
summary: string;
|
|
32
|
+
timedOut: boolean;
|
|
33
|
+
}): WorkerResultCause | undefined;
|
|
23
34
|
export declare const TelemetryRowSchema: z.ZodObject<{
|
|
24
35
|
taskId: z.ZodString;
|
|
25
36
|
shape: z.ZodString;
|
|
@@ -42,9 +53,11 @@ export declare const TelemetryRowSchema: z.ZodObject<{
|
|
|
42
53
|
"attempt-cap": "attempt-cap";
|
|
43
54
|
"gate-fail": "gate-fail";
|
|
44
55
|
quota: "quota";
|
|
56
|
+
"human-gate": "human-gate";
|
|
45
57
|
"reroute-exhausted": "reroute-exhausted";
|
|
46
58
|
stall: "stall";
|
|
47
59
|
"merge-conflict": "merge-conflict";
|
|
60
|
+
"tip-moved": "tip-moved";
|
|
48
61
|
}>>;
|
|
49
62
|
tokens: z.ZodCatch<z.ZodOptional<z.ZodObject<{
|
|
50
63
|
input: z.ZodNumber;
|
|
@@ -60,11 +73,44 @@ export declare const TelemetryRowSchema: z.ZodObject<{
|
|
|
60
73
|
resume: "resume";
|
|
61
74
|
fresh: "fresh";
|
|
62
75
|
}>>;
|
|
76
|
+
signalQuality: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<0.25>, z.ZodLiteral<0.5>, z.ZodLiteral<0.75>, z.ZodLiteral<1>]>>;
|
|
77
|
+
signalBasis: z.ZodOptional<z.ZodEnum<{
|
|
78
|
+
proved: "proved";
|
|
79
|
+
"review-agree": "review-agree";
|
|
80
|
+
"judge-only": "judge-only";
|
|
81
|
+
legacy: "legacy";
|
|
82
|
+
vacuous: "vacuous";
|
|
83
|
+
skipped: "skipped";
|
|
84
|
+
}>>;
|
|
63
85
|
}, z.core.$strip>;
|
|
64
86
|
export type TelemetryRow = z.infer<typeof TelemetryRowSchema>;
|
|
65
|
-
export declare
|
|
66
|
-
export
|
|
87
|
+
export declare const SIGNAL_BASIS: readonly ["proved", "review-agree", "judge-only", "legacy", "vacuous", "skipped"];
|
|
88
|
+
export type SignalBasis = (typeof SIGNAL_BASIS)[number];
|
|
89
|
+
export declare const SIGNAL_QUALITY: Record<SignalBasis, 0 | 0.25 | 0.5 | 0.75 | 1>;
|
|
90
|
+
export declare function signalQualityFromBasis(basis: SignalBasis): 0 | 0.25 | 0.5 | 0.75 | 1;
|
|
91
|
+
export declare function deriveSignalBasis(gate: string, pass: boolean, details: string, meta?: Record<string, unknown>): SignalBasis;
|
|
92
|
+
export declare function gateResultJournalData(gate: string, pass: boolean, details: string, meta?: Record<string, unknown>): {
|
|
93
|
+
gate: string;
|
|
94
|
+
pass: boolean;
|
|
95
|
+
details: string;
|
|
96
|
+
signalBasis: SignalBasis;
|
|
97
|
+
signalQuality: number;
|
|
98
|
+
} & Record<string, unknown>;
|
|
99
|
+
export declare function recordedGraphDefinitionHash(events: JournalEvent[]): string | undefined;
|
|
100
|
+
export type EngagementCompare = {
|
|
101
|
+
comparable: true;
|
|
102
|
+
recorded: string;
|
|
103
|
+
} | {
|
|
104
|
+
comparable: false;
|
|
105
|
+
reason: "mismatch";
|
|
106
|
+
recorded: string;
|
|
107
|
+
} | {
|
|
108
|
+
comparable: false;
|
|
109
|
+
reason: "unbound";
|
|
110
|
+
};
|
|
111
|
+
export declare function engagementComparable(events: JournalEvent[], loadedHash: string): EngagementCompare;
|
|
67
112
|
export declare function newRunId(now?: Date): string;
|
|
113
|
+
export declare function parseRunId(runId: string): string;
|
|
68
114
|
export declare function readAllTelemetry(repoRoot: string, lastK: number, opts?: {
|
|
69
115
|
after?: string;
|
|
70
116
|
}): (TelemetryRow & {
|
|
@@ -72,6 +118,9 @@ export declare function readAllTelemetry(repoRoot: string, lastK: number, opts?:
|
|
|
72
118
|
})[];
|
|
73
119
|
export declare const RUNS_WINDOW = 50;
|
|
74
120
|
export declare function readProfileCursor(repoRoot: string): string | undefined;
|
|
121
|
+
export declare function profileDiscountsPath(repoRoot: string): string;
|
|
122
|
+
export declare function readProfileDiscounts(repoRoot: string): ProfileDiscount[];
|
|
123
|
+
export declare function appendProfileDiscount(repoRoot: string, discount: ProfileDiscount): void;
|
|
75
124
|
export declare function loadRoutingProfile(repoRoot: string, cfg: TickmarkrConfig, opts?: {
|
|
76
125
|
preview?: boolean;
|
|
77
126
|
}): RoutingProfile | undefined;
|
package/dist/run/journal.js
CHANGED
|
@@ -2,7 +2,7 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { channelKey, TokenUsageSchema } from "../adapters/types.js";
|
|
5
|
-
import { stateDirName } from "../graph/graph.js";
|
|
5
|
+
import { stateDirName, tickmarkrDir } from "../graph/graph.js";
|
|
6
6
|
import { TIERS } from "../graph/schema.js";
|
|
7
7
|
import { buildProfile } from "../route/profile.js";
|
|
8
8
|
export function formatJournalNarration({ event, taskId, data }) {
|
|
@@ -38,9 +38,28 @@ const DispatchAssignmentSchema = z.object({
|
|
|
38
38
|
channel: z.enum(["sub", "api"]),
|
|
39
39
|
tier: z.enum(TIERS),
|
|
40
40
|
});
|
|
41
|
-
export const PARK_KINDS = ["ladder-exhausted", "attempt-cap", "gate-fail", "quota",
|
|
42
|
-
"reroute-exhausted", "setup", "stall", "merge-conflict"];
|
|
41
|
+
export const PARK_KINDS = ["human-gate", "ladder-exhausted", "attempt-cap", "gate-fail", "quota",
|
|
42
|
+
"reroute-exhausted", "setup", "stall", "merge-conflict", "tip-moved"];
|
|
43
43
|
export const RETRY_MODES = ["resume", "fresh"];
|
|
44
|
+
export const WORKER_RESULT_CAUSES = ["provider-death", "stall-timeout", "malformed-trailer", "clean-exit-no-trailer"];
|
|
45
|
+
// OBS-53: provider-outage signatures in dead worker output ("Unable to reach the model provider" and kin).
|
|
46
|
+
const PROVIDER_OUTAGE_RE = /Unable to reach the model provider|cannot reach the model provider|model provider.*(?:unavailable|unreachable)/i;
|
|
47
|
+
/** OBS-53: classify worker-result failures so retries and routing see the true signal, not one lumped bucket. */
|
|
48
|
+
export function classifyWorkerResultCause(opts) {
|
|
49
|
+
if (opts.ok && opts.finished)
|
|
50
|
+
return undefined;
|
|
51
|
+
if (PROVIDER_OUTAGE_RE.test(opts.output))
|
|
52
|
+
return "provider-death";
|
|
53
|
+
if (opts.summary === "unparseable TICKMARKR_RESULT trailer")
|
|
54
|
+
return "malformed-trailer";
|
|
55
|
+
if (!opts.finished && opts.timedOut)
|
|
56
|
+
return "stall-timeout";
|
|
57
|
+
if (!opts.finished && opts.exitCode !== null)
|
|
58
|
+
return "clean-exit-no-trailer";
|
|
59
|
+
if (!opts.finished)
|
|
60
|
+
return "stall-timeout";
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
44
63
|
export const TelemetryRowSchema = z.object({
|
|
45
64
|
// v1.5 core — required; every old row has all eight (daemon.ts writes all eight)
|
|
46
65
|
taskId: z.string(), shape: z.string(), adapter: z.string(), model: z.string(), channel: z.string(),
|
|
@@ -71,23 +90,88 @@ export const TelemetryRowSchema = z.object({
|
|
|
71
90
|
overrun: z.literal(true).optional(),
|
|
72
91
|
// v1.29 additive: mode of the attempt represented by this row. Absent on old telemetry.
|
|
73
92
|
retryMode: z.enum(RETRY_MODES).optional(),
|
|
93
|
+
// v1.46 additive (T5): gate-signal quality for routing hygiene — OPTIONAL, absent = legacy (0.25 at fold).
|
|
94
|
+
signalQuality: z.union([z.literal(0), z.literal(0.25), z.literal(0.5), z.literal(0.75), z.literal(1)]).optional(),
|
|
95
|
+
signalBasis: z.enum(["proved", "review-agree", "judge-only", "legacy", "vacuous", "skipped"]).optional(),
|
|
74
96
|
});
|
|
75
|
-
//
|
|
76
|
-
|
|
97
|
+
// v1.46 T5 (Sol signal telemetry): gate-result rows carry explicit signalQuality so future defect windows
|
|
98
|
+
// are identifiable without forensics. Basis is the provenance claim; quality is the dyadic h-fold weight.
|
|
99
|
+
export const SIGNAL_BASIS = ["proved", "review-agree", "judge-only", "legacy", "vacuous", "skipped"];
|
|
100
|
+
export const SIGNAL_QUALITY = {
|
|
101
|
+
proved: 1,
|
|
102
|
+
"review-agree": 0.75,
|
|
103
|
+
"judge-only": 0.5,
|
|
104
|
+
legacy: 0.25,
|
|
105
|
+
vacuous: 0,
|
|
106
|
+
skipped: 0,
|
|
107
|
+
};
|
|
108
|
+
export function signalQualityFromBasis(basis) {
|
|
109
|
+
return SIGNAL_QUALITY[basis];
|
|
110
|
+
}
|
|
111
|
+
export function deriveSignalBasis(gate, pass, details, meta = {}) {
|
|
112
|
+
if (meta.skipped === true)
|
|
113
|
+
return "skipped";
|
|
114
|
+
if (meta.unparseable === true)
|
|
115
|
+
return "vacuous";
|
|
116
|
+
if (gate === "acceptance")
|
|
117
|
+
return "judge-only";
|
|
118
|
+
if (gate === "review")
|
|
119
|
+
return pass ? "review-agree" : "vacuous";
|
|
120
|
+
if (gate === "test" || gate === "build") {
|
|
121
|
+
if (!pass)
|
|
122
|
+
return "vacuous";
|
|
123
|
+
if (/no test files|0 tests|tests\s+0\s/i.test(details))
|
|
124
|
+
return "vacuous";
|
|
125
|
+
return "proved";
|
|
126
|
+
}
|
|
127
|
+
return pass ? "proved" : "vacuous";
|
|
128
|
+
}
|
|
129
|
+
// The canonical gate-result journal payload — daemon should spread this into append("gate-result", …).
|
|
130
|
+
export function gateResultJournalData(gate, pass, details, meta = {}) {
|
|
131
|
+
const signalBasis = deriveSignalBasis(gate, pass, details, meta);
|
|
132
|
+
return { gate, pass, details, ...meta, signalBasis, signalQuality: signalQualityFromBasis(signalBasis) };
|
|
133
|
+
}
|
|
134
|
+
// T3 (Sol #2 / Fable F2): one canonical engagement identity, shared by status AND resume. The run-start
|
|
135
|
+
// event records graphDefinitionHash (over compiled task definitions only — see graph.graphDefinitionHash);
|
|
136
|
+
// this is the single field both consumers read, and the single comparator below is the single place the
|
|
137
|
+
// journal↔graph join is decided. unbound (no recorded definition hash, e.g. a pre-v1.44 journal) and
|
|
138
|
+
// mismatch are both not-comparable — status renders the notice either way; resume refuses either way and
|
|
139
|
+
// distinguishes the reason only for its message and the --graph-changed release event.
|
|
140
|
+
export function recordedGraphDefinitionHash(events) {
|
|
77
141
|
for (const e of events) {
|
|
78
|
-
if (e.event === "run-start" && typeof e.data.
|
|
79
|
-
return e.data.
|
|
142
|
+
if (e.event === "run-start" && typeof e.data.graphDefinitionHash === "string")
|
|
143
|
+
return e.data.graphDefinitionHash;
|
|
80
144
|
}
|
|
81
145
|
return undefined;
|
|
82
146
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
147
|
+
// THE shared comparator (criterion: status and resume decide through one comparator). status reads
|
|
148
|
+
// .comparable; resume reads .comparable plus .reason/.recorded for its refusal message and the release.
|
|
149
|
+
export function engagementComparable(events, loadedHash) {
|
|
150
|
+
const recorded = recordedGraphDefinitionHash(events);
|
|
151
|
+
if (recorded === undefined)
|
|
152
|
+
return { comparable: false, reason: "unbound" };
|
|
153
|
+
return recorded === loadedHash ? { comparable: true, recorded } : { comparable: false, reason: "mismatch", recorded };
|
|
86
154
|
}
|
|
87
155
|
export function newRunId(now = new Date()) {
|
|
88
156
|
const p = (n, w = 2) => String(n).padStart(w, "0");
|
|
89
157
|
return `run-${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}-${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
|
|
90
158
|
}
|
|
159
|
+
// Sol #4: one strict parser for every journal open/create path — generated run-… ids plus test
|
|
160
|
+
// suffix chars only; forbid path separators, dot-segments, and empty ids.
|
|
161
|
+
export function parseRunId(runId) {
|
|
162
|
+
const id = runId.trim();
|
|
163
|
+
if (!id)
|
|
164
|
+
throw new Error("invalid run id: empty");
|
|
165
|
+
if (id.includes("/") || id.includes("\\"))
|
|
166
|
+
throw new Error(`invalid run id: ${runId}`);
|
|
167
|
+
for (const seg of id.split(/[/\\]/)) {
|
|
168
|
+
if (seg === "." || seg === "..")
|
|
169
|
+
throw new Error(`invalid run id: ${runId}`);
|
|
170
|
+
}
|
|
171
|
+
if (!/^run-[A-Za-z0-9][A-Za-z0-9_-]*$/.test(id))
|
|
172
|
+
throw new Error(`invalid run id: ${runId}`);
|
|
173
|
+
return id;
|
|
174
|
+
}
|
|
91
175
|
const runsDir = (repoRoot) => join(repoRoot, stateDirName(repoRoot), "runs");
|
|
92
176
|
// One JSONL reader for every append-only log: skip blanks, drop any line that
|
|
93
177
|
// won't parse, keeping everything before it intact.
|
|
@@ -143,6 +227,38 @@ export function readProfileCursor(repoRoot) {
|
|
|
143
227
|
return undefined;
|
|
144
228
|
return readFileSync(path, "utf8").trim() || undefined;
|
|
145
229
|
}
|
|
230
|
+
// v1.46 T5 evidence hygiene state — one line per mark: `<runId> [<taskId>] <weight> # <reason>`.
|
|
231
|
+
// Follows the profile-since precedent: state file in .tickmarkr/, never config, never git.
|
|
232
|
+
const PROFILE_DISCOUNTS_RE = /^(run-[A-Za-z0-9][A-Za-z0-9_-]*)(?:\s+(\S+))?\s+(0|0\.5)\s+#\s+(.+)$/;
|
|
233
|
+
export function profileDiscountsPath(repoRoot) {
|
|
234
|
+
return join(repoRoot, stateDirName(repoRoot), "profile-discounts");
|
|
235
|
+
}
|
|
236
|
+
export function readProfileDiscounts(repoRoot) {
|
|
237
|
+
const path = profileDiscountsPath(repoRoot);
|
|
238
|
+
if (!existsSync(path))
|
|
239
|
+
return [];
|
|
240
|
+
const out = [];
|
|
241
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
242
|
+
const trimmed = line.trim();
|
|
243
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
244
|
+
continue;
|
|
245
|
+
const m = PROFILE_DISCOUNTS_RE.exec(trimmed);
|
|
246
|
+
if (!m)
|
|
247
|
+
continue;
|
|
248
|
+
out.push({
|
|
249
|
+
runId: m[1],
|
|
250
|
+
...(m[2] ? { taskId: m[2] } : {}),
|
|
251
|
+
weight: m[3] === "0" ? 0 : 0.5,
|
|
252
|
+
reason: m[4].trim(),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
export function appendProfileDiscount(repoRoot, discount) {
|
|
258
|
+
tickmarkrDir(repoRoot);
|
|
259
|
+
const line = `${discount.runId}${discount.taskId ? ` ${discount.taskId}` : ""} ${discount.weight} # ${discount.reason}\n`;
|
|
260
|
+
appendFileSync(profileDiscountsPath(repoRoot), line);
|
|
261
|
+
}
|
|
146
262
|
// The one shared profile builder (criterion 4: plan and daemon share ONE code path).
|
|
147
263
|
// preview:true bypasses the routing.learned:off short-circuit so `tickmarkr plan` can render the
|
|
148
264
|
// trust-ramp preview while the daemon (no preview) stays inert (VALIDATION 13-01-11).
|
|
@@ -151,7 +267,8 @@ export function loadRoutingProfile(repoRoot, cfg, opts = {}) {
|
|
|
151
267
|
return undefined; // never built, never passed
|
|
152
268
|
const rows = readAllTelemetry(repoRoot, RUNS_WINDOW, { after: readProfileCursor(repoRoot) });
|
|
153
269
|
// ROUTE-15: halfLifeRuns threads from config as a pure param; undefined ⇒ module default (byte-identical).
|
|
154
|
-
|
|
270
|
+
const discounts = readProfileDiscounts(repoRoot);
|
|
271
|
+
return rows.length ? buildProfile(rows, { halfLifeRuns: cfg.routing.learnedTuning?.halfLifeRuns, discounts }) : undefined; // cold ⇒ undefined ⇒ v1.5 dead-code path
|
|
155
272
|
}
|
|
156
273
|
export class Journal {
|
|
157
274
|
dir;
|
|
@@ -163,15 +280,19 @@ export class Journal {
|
|
|
163
280
|
this.narrate = narrate;
|
|
164
281
|
}
|
|
165
282
|
static create(repoRoot, runId, narrate) {
|
|
166
|
-
const
|
|
283
|
+
const id = parseRunId(runId);
|
|
284
|
+
const dir = join(runsDir(repoRoot), id);
|
|
285
|
+
if (existsSync(join(dir, "journal.jsonl")))
|
|
286
|
+
throw new Error(`journal already exists for ${id}`);
|
|
167
287
|
mkdirSync(dir, { recursive: true });
|
|
168
|
-
return new Journal(dir,
|
|
288
|
+
return new Journal(dir, id, narrate);
|
|
169
289
|
}
|
|
170
290
|
static open(repoRoot, runId, narrate) {
|
|
171
|
-
const
|
|
291
|
+
const id = parseRunId(runId);
|
|
292
|
+
const dir = join(runsDir(repoRoot), id);
|
|
172
293
|
if (!existsSync(join(dir, "journal.jsonl")))
|
|
173
|
-
throw new Error(`no journal for ${
|
|
174
|
-
return new Journal(dir,
|
|
294
|
+
throw new Error(`no journal for ${id} at ${dir}`);
|
|
295
|
+
return new Journal(dir, id, narrate);
|
|
175
296
|
}
|
|
176
297
|
// withJournal: journal.jsonl appears at first append, after Journal.create mkdirs — a caller that
|
|
177
298
|
// will Journal.open the result (status, report) must fall back to the newest run that is actually
|
package/dist/run/lock.js
CHANGED
|
@@ -141,9 +141,9 @@ export function releaseRunLock(repoRoot) {
|
|
|
141
141
|
unlinkIfOurs(lockPath(repoRoot)); // never delete a reclaiming successor's lock — only ours
|
|
142
142
|
heldPath = undefined;
|
|
143
143
|
}
|
|
144
|
-
// Read-only
|
|
145
|
-
//
|
|
146
|
-
//
|
|
144
|
+
// Read-only predicate: true iff a lock exists that the decision table would REFUSE on (alive,
|
|
145
|
+
// EPERM, or ANY garbage). A provably-dead holder (ESRCH) reads not-live (LOCK-02/OBS-05). Never
|
|
146
|
+
// mutates the lock. compile now acquires via acquireRunLock; this remains for drift oracles/tests.
|
|
147
147
|
export function isRunLockLive(repoRoot) {
|
|
148
148
|
try {
|
|
149
149
|
return shouldRefuse(inspect(lockPath(repoRoot)));
|
package/dist/run/merge.js
CHANGED
|
@@ -3,7 +3,7 @@ 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,
|
|
6
|
+
import { gitHead, linkNodeModules, resolveIntegrationBranch, sh, shGit, shGitOk, WORKTREES_DIR } from "./git.js";
|
|
7
7
|
export function integrationBranch(cfg, runId) {
|
|
8
8
|
return `${cfg.integrationBranchPrefix}${runId}`;
|
|
9
9
|
}
|
|
@@ -12,12 +12,12 @@ export async function ensureIntegration(repo, branch, baseRef) {
|
|
|
12
12
|
branch = await resolveIntegrationBranch(repo, branch);
|
|
13
13
|
const dir = join(tickmarkrDir(repo), WORKTREES_DIR, sanitize(branch));
|
|
14
14
|
if (!existsSync(join(dir, ".git"))) {
|
|
15
|
-
const exists = (await
|
|
15
|
+
const exists = (await shGit(`git rev-parse --verify refs/heads/${shq(branch)}`, repo)).code === 0;
|
|
16
16
|
if (exists) {
|
|
17
|
-
await
|
|
17
|
+
await shGitOk(`git worktree add ${shq(dir)} ${shq(branch)}`, repo);
|
|
18
18
|
}
|
|
19
19
|
else {
|
|
20
|
-
await
|
|
20
|
+
await shGitOk(`git worktree add -b ${shq(branch)} ${shq(dir)} ${shq(baseRef)}`, repo);
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
linkNodeModules(repo, dir);
|
|
@@ -27,21 +27,21 @@ export function integrationHead(intWt) {
|
|
|
27
27
|
return gitHead(intWt);
|
|
28
28
|
}
|
|
29
29
|
export async function mergeTask(intWt, taskBranch, message, gatedCommit) {
|
|
30
|
-
const tip = await
|
|
30
|
+
const tip = await shGit(`git rev-parse --verify ${shq(`refs/heads/${taskBranch}`)}`, intWt);
|
|
31
31
|
if (tip.code !== 0)
|
|
32
32
|
return { ok: false, conflict: tip.stderr || tip.stdout };
|
|
33
33
|
const branchTip = tip.stdout.trim();
|
|
34
34
|
if (branchTip !== gatedCommit)
|
|
35
35
|
return { ok: false, tipMoved: { gatedCommit, branchTip } };
|
|
36
36
|
// Merge the verified hash, not the mutable branch name: a move after the comparison cannot land ungated content.
|
|
37
|
-
const r = await
|
|
37
|
+
const r = await shGit(`git merge --no-ff ${shq(gatedCommit)} -m ${shq(message)}`, intWt);
|
|
38
38
|
if (r.code === 0)
|
|
39
39
|
return { ok: true };
|
|
40
|
-
const conflict = (await
|
|
40
|
+
const conflict = (await shGit("git status --porcelain", intWt)).stdout
|
|
41
41
|
.split("\n")
|
|
42
42
|
.filter((l) => l.startsWith("UU") || l.startsWith("AA"))
|
|
43
43
|
.join("\n") || r.stderr || r.stdout;
|
|
44
|
-
await
|
|
44
|
+
await shGit("git merge --abort", intWt);
|
|
45
45
|
return { ok: false, conflict };
|
|
46
46
|
}
|
|
47
47
|
// OBS-34: strict exit-code verify on the integration tip — no baseline forgiveness.
|