tickmarkr 2.7.9 → 2.8.1
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/codex.d.ts +1 -1
- package/dist/adapters/codex.js +18 -6
- package/dist/adapters/fake.js +42 -4
- package/dist/cli/commands/approve.js +33 -8
- package/dist/cli/commands/doctor.js +11 -2
- package/dist/cli/commands/verify.js +32 -12
- package/dist/gates/acceptance.d.ts +1 -1
- package/dist/gates/acceptance.js +75 -68
- package/dist/gates/baseline.d.ts +5 -1
- package/dist/gates/baseline.js +31 -6
- package/dist/gates/operation-facts.d.ts +47 -0
- package/dist/gates/operation-facts.js +84 -0
- package/dist/gates/run-gates.js +81 -12
- package/dist/run/daemon.d.ts +77 -13
- package/dist/run/daemon.js +348 -94
- package/dist/run/git.d.ts +21 -4
- package/dist/run/git.js +26 -9
- package/dist/run/launchservices-check.d.ts +23 -1
- package/dist/run/launchservices-check.js +88 -3
- package/dist/run/merge.d.ts +114 -7
- package/dist/run/merge.js +146 -44
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/SKILL.md +8 -0
package/dist/run/merge.js
CHANGED
|
@@ -4,8 +4,7 @@ import { shq } from "../adapters/types.js";
|
|
|
4
4
|
import { baselineEntryRefusal, beginGateEvidence, ceilingKillResult, classifyFreshRunnerOutput, classifyRunnerOutput, fileCountDeficit, waitForCalmWindow, effectiveCeilingMs, classifiedFileDurations, fingerprint, freshFailures, } from "../gates/baseline.js";
|
|
5
5
|
import { evaluateManifestedTest, isVitestTestCommand } from "../gates/test-manifest.js";
|
|
6
6
|
import { computeVerificationIdentity, formatReusedDetails, getVerdictStore, getWorktreeTree, resolveStateDir, isInfraResult, } from "../gates/cache.js";
|
|
7
|
-
import {
|
|
8
|
-
import { dependencyLinkRefusal, describeCapacity, gitHead, linkNodeModules, resolveIntegrationBranch, resolvedCapacity, sameCapacity, sh, shGit, shGitOk, WORKTREES_DIR } from "./git.js";
|
|
7
|
+
import { dependencyLinkRefusal, describeCapacity, resolveIntegrationBranch, resolvedCapacity, sameCapacity, sh, shGit } from "./git.js";
|
|
9
8
|
import { executionSignal } from "./execution-budget.js";
|
|
10
9
|
// Diagnostic convenience logs are observational, just like receipt persistence.
|
|
11
10
|
function writeTipLog(path, text) {
|
|
@@ -32,42 +31,131 @@ export function reusedTipEvidence(source) {
|
|
|
32
31
|
export function integrationBranch(cfg, runId) {
|
|
33
32
|
return `${cfg.integrationBranchPrefix}${runId}`;
|
|
34
33
|
}
|
|
35
|
-
|
|
34
|
+
/**
|
|
35
|
+
* 2.8.1 T2 (ANL-023 §7.2): creates the integration ref create-only at `baseRef` (the empty old value means "must not
|
|
36
|
+
* exist") and returns the published tip E. The run keeps no checkout of the branch: every later publication is a
|
|
37
|
+
* compare-and-swap of the ref from `repo` (mergeTask). An existing or uncreatable ref throws — the fatal path's text.
|
|
38
|
+
*/
|
|
36
39
|
export async function ensureIntegration(repo, branch, baseRef) {
|
|
37
40
|
branch = await resolveIntegrationBranch(repo, branch);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
// ponytail: no --create-reflog (ANL-023 §7.2 names it): the repository's own core.logAllRefUpdates decides, because
|
|
42
|
+
// nothing reads this reflog (X10) and run-end-integration's E-P10 builds its no-reflog legacy branch through a fresh
|
|
43
|
+
// run with logging disabled. Add the flag back once that fixture builds its legacy branch by hand.
|
|
44
|
+
const r = await shGit(`git update-ref --no-deref -m ${shq("tickmarkr: run start")} ${shq(`refs/heads/${branch}`)} ${shq(baseRef)} ""`, repo);
|
|
45
|
+
if (r.code !== 0)
|
|
46
|
+
throw new Error(`integration branch ${branch} already exists or cannot be created: ${(r.stderr || r.stdout).trim()}`);
|
|
47
|
+
return baseRef;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* R(ref): the integration ref read from `repo` through the own-git runner, never its reflog. The exact ref only —
|
|
51
|
+
* a descendant ref (`refs/heads/<branch>/x`) is not this ref, and a ref that exists but names no commit is unreadable.
|
|
52
|
+
*/
|
|
53
|
+
export async function readIntegrationRef(repo, branch) {
|
|
54
|
+
const ref = `refs/heads/${branch}`;
|
|
55
|
+
const unreadable = (cmd, r) => ({ kind: "unreadable", read: `${cmd} exited ${r.code}${(r.stderr || r.stdout).trim() ? `: ${(r.stderr || r.stdout).trim()}` : ""}` });
|
|
56
|
+
const symbolic = await shGit(`git symbolic-ref -q ${shq(ref)}`, repo);
|
|
57
|
+
if (symbolic.code !== 0 && symbolic.code !== 1)
|
|
58
|
+
return unreadable("git symbolic-ref", symbolic);
|
|
59
|
+
const commit = await shGit(`git rev-parse --verify -q ${shq(`${ref}^{commit}`)}`, repo);
|
|
60
|
+
if (commit.code !== 0 && commit.code !== 1)
|
|
61
|
+
return unreadable("git rev-parse", commit);
|
|
62
|
+
const found = commit.code === 0 ? commit.stdout.trim() : undefined;
|
|
63
|
+
if (symbolic.code === 0)
|
|
64
|
+
return { kind: "symbolic", target: symbolic.stdout.trim(), ...(found ? { commit: found } : {}) };
|
|
65
|
+
if (found)
|
|
66
|
+
return { kind: "regular", commit: found };
|
|
67
|
+
const exists = await shGit(`git show-ref --verify -q ${shq(ref)}`, repo);
|
|
68
|
+
return exists.code === 1 ? { kind: "missing" } : unreadable("git show-ref", exists);
|
|
50
69
|
}
|
|
51
|
-
|
|
52
|
-
|
|
70
|
+
/** I(x): holds iff the ref is regular at x; otherwise the leg, with the state the repair text is chosen by. */
|
|
71
|
+
export async function checkIntegrationIdentity(repo, branch, expected) {
|
|
72
|
+
const state = await readIntegrationRef(repo, branch);
|
|
73
|
+
if (state.kind === "regular" && state.commit === expected)
|
|
74
|
+
return { ok: true, state };
|
|
75
|
+
return { ok: false, leg: state.kind === "regular" ? "value" : state.kind, state };
|
|
53
76
|
}
|
|
54
|
-
|
|
55
|
-
|
|
77
|
+
/**
|
|
78
|
+
* The repair text (shared by run end's failure row, the merge park and the resume refusal): backticked commands run
|
|
79
|
+
* from the repository root in order, stopping at the first that fails. A found commit is preserved create-only under
|
|
80
|
+
* refs/tickmarkr/found/<runId>/<F> before a no-deref compare-and-swap restores the ref; a symbolic found ref or one
|
|
81
|
+
* already holding another commit stops the sequence before the restore. Dangling and unreadable print no command.
|
|
82
|
+
*/
|
|
83
|
+
export function integrationRepairText(state, branch, runId, expected) {
|
|
84
|
+
const ref = `refs/heads/${branch}`;
|
|
85
|
+
const byHand = `inspect ${ref} and restore it to ${expected} by hand`;
|
|
86
|
+
const found = state.kind === "regular" ? state.commit : state.kind === "symbolic" ? state.commit : undefined;
|
|
87
|
+
if (found) {
|
|
88
|
+
const keep = `refs/tickmarkr/found/${runId}/${found}`;
|
|
89
|
+
return `\`test -z "$(git symbolic-ref -q ${keep})"\` then \`test "$(git rev-parse -q --verify ${keep})" = ${found} || git update-ref --no-deref ${keep} ${found} ""\` then \`git update-ref --no-deref ${ref} ${expected} ${found}\``;
|
|
90
|
+
}
|
|
91
|
+
if (state.kind === "missing")
|
|
92
|
+
return `\`git update-ref --no-deref ${ref} ${expected} ""\``;
|
|
93
|
+
if (state.kind === "symbolic")
|
|
94
|
+
return `symbolic to ${state.target}, which names no commit; ${byHand}`;
|
|
95
|
+
return state.kind === "unreadable" ? `${ref} is unreadable (${state.read}); ${byHand}` : byHand;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 2.8.1 T2 (ANL-023 §7.2): publishes `gated` onto the integration ref by compare-and-swap from the stored tip
|
|
99
|
+
* `expected`, all git from `repo` — no checkout. (1) the task-tip check; (2) I(expected); (3) ancestry; (4) merge-tree;
|
|
100
|
+
* (5) commit-tree; (6) `update-ref --no-deref` from `expected`; (7) `onPublished(M)` in the same synchronous
|
|
101
|
+
* continuation as (6)'s result, so the caller's rows land in publication order. `merged` is the task's recorded merge
|
|
102
|
+
* commits, newest first: a gated commit already in the history is done only through its own recorded merge.
|
|
103
|
+
* `ancestorOnly` (a replay that ran no gates): a gated commit not in E is refused `infra`, never published.
|
|
104
|
+
*/
|
|
105
|
+
export async function mergeTask(repo, { branch, expected, merged = [], ancestorOnly = false }, taskBranch, message, gated, onPublished, seam) {
|
|
106
|
+
const tip = await shGit(`git rev-parse --verify ${shq(`refs/heads/${taskBranch}`)}`, repo);
|
|
56
107
|
if (tip.code !== 0)
|
|
57
108
|
return { ok: false, conflict: tip.stderr || tip.stdout };
|
|
58
109
|
const branchTip = tip.stdout.trim();
|
|
59
|
-
if (branchTip !==
|
|
60
|
-
return { ok: false, tipMoved: { gatedCommit, branchTip } };
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
110
|
+
if (branchTip !== gated)
|
|
111
|
+
return { ok: false, tipMoved: { gatedCommit: gated, branchTip } };
|
|
112
|
+
const moved = (leg, state, stage) => ({
|
|
113
|
+
ok: false, state, integrationMoved: { branch, expected, leg, stage,
|
|
114
|
+
found: (state.kind === "regular" || state.kind === "symbolic" ? state.commit : undefined) ?? null,
|
|
115
|
+
...(state.kind === "symbolic" ? { target: state.target } : {}) },
|
|
116
|
+
});
|
|
117
|
+
const infra = (step, r) => ({ ok: false, infra: { step, message: (r.stderr || r.stdout).trim() || `exit ${r.code}` } });
|
|
118
|
+
const run = async (step, cmd) => (await seam?.(step)) || shGit(cmd, repo);
|
|
119
|
+
const held = await checkIntegrationIdentity(repo, branch, expected);
|
|
120
|
+
if (!held.ok)
|
|
121
|
+
return moved(held.leg, held.state, "check");
|
|
122
|
+
const ancestry = await run("ancestry", `git merge-base --is-ancestor ${shq(gated)} ${shq(expected)}`);
|
|
123
|
+
if (ancestry.code === 0) {
|
|
124
|
+
// Ancestor or equal tip: only the task's own recorded merge (second parent = gated) makes it done. A resume replay
|
|
125
|
+
// hands the task's own gated commit here, never a tip recreated on E (runDaemon's resume gate replay).
|
|
126
|
+
for (const commit of merged) {
|
|
127
|
+
const second = await shGit(`git rev-parse --verify -q ${shq(`${commit}^2`)}`, repo);
|
|
128
|
+
if (second.code !== 0)
|
|
129
|
+
break; // a failed read (no second parent, no such object) is never passed over: fail closed
|
|
130
|
+
if (second.stdout.trim() === gated)
|
|
131
|
+
return { ok: true, alreadyMerged: commit };
|
|
132
|
+
}
|
|
133
|
+
return { ok: false, alreadyIntegrated: { gated, tip: expected } };
|
|
134
|
+
}
|
|
135
|
+
if (ancestry.code !== 1)
|
|
136
|
+
return infra("ancestry", ancestry);
|
|
137
|
+
// A caller that skipped the gates on its own ancestry proof (a resume replay) never reaches construction.
|
|
138
|
+
if (ancestorOnly)
|
|
139
|
+
return infra("ancestry", { code: 1, stdout: "", stderr: `${gated} is not in ${expected}; an ungated replay publishes nothing` });
|
|
140
|
+
const tree = await run("merge-tree", `git merge-tree --write-tree ${shq(expected)} ${shq(gated)}`);
|
|
141
|
+
if (tree.code === 1)
|
|
142
|
+
return { ok: false, conflict: (tree.stdout || tree.stderr).trim() };
|
|
143
|
+
if (tree.code !== 0)
|
|
144
|
+
return infra("merge-tree", tree);
|
|
145
|
+
const made = await run("commit-tree", `git commit-tree ${shq(tree.stdout.split("\n")[0].trim())} -p ${shq(expected)} -p ${shq(gated)} -m ${shq(message)}`);
|
|
146
|
+
if (made.code !== 0)
|
|
147
|
+
return infra("commit-tree", made);
|
|
148
|
+
const commit = made.stdout.trim();
|
|
149
|
+
await seam?.("publish");
|
|
150
|
+
const published = await shGit(`git update-ref --no-deref -m ${shq(message)} ${shq(`refs/heads/${branch}`)} ${shq(commit)} ${shq(expected)}`, repo);
|
|
151
|
+
if (published.code === 0) {
|
|
152
|
+
onPublished(commit); // no await between the compare-and-swap's result and the caller's rows
|
|
153
|
+
return { ok: true, commit, from: expected };
|
|
154
|
+
}
|
|
155
|
+
const state = await readIntegrationRef(repo, branch);
|
|
156
|
+
if (state.kind === "regular" && state.commit === expected)
|
|
157
|
+
return infra("publish", published);
|
|
158
|
+
return moved(state.kind === "regular" ? "value" : state.kind, state, "publish");
|
|
71
159
|
}
|
|
72
160
|
// OBS-34 ruled strict exit-code verify; Q121s (TRIAL T-OBS-1) narrows it: a red whose failure
|
|
73
161
|
// fingerprints are ALL recorded in the run's baseline is forgiven with the battery's own math
|
|
@@ -75,7 +163,7 @@ export async function mergeTask(intWt, taskBranch, message, gatedCommit) {
|
|
|
75
163
|
// terminus unreachable by construction and misattributed the red to the last merged task.
|
|
76
164
|
// Fail-closed edges kept: no baseline → strict; baseline green for that gate → strict; any fresh
|
|
77
165
|
// fingerprint, or output with no recognizable failure shape, → failed.
|
|
78
|
-
export async function verifyIntegrationTip(intWt, commands, runDir, baseline, evidenceOptions = {}) {
|
|
166
|
+
export async function verifyIntegrationTip(intWt, commands, runDir, baseline, evidenceOptions = {}, { deferVerdicts = false } = {}) {
|
|
79
167
|
const results = [];
|
|
80
168
|
let runStartCommands;
|
|
81
169
|
let journalFound = false;
|
|
@@ -216,7 +304,9 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
|
|
|
216
304
|
// Persistent reuse requires the run's durable journal. Without it there is no
|
|
217
305
|
// recorded cycle provenance to reconcile with the caller's capacity checks.
|
|
218
306
|
const hit = journalFound ? store.get(id) : undefined;
|
|
219
|
-
|
|
307
|
+
// S19m4: a forgiven test green written before the tip read kill text never proves the suite completed.
|
|
308
|
+
const killTextUnread = gate === "test" && hit?.meta?.forgiven === true && hit.meta.killTextRead !== true;
|
|
309
|
+
if (hit && id && hit.pass && !isInfraResult(hit) && !killTextUnread) {
|
|
220
310
|
results.push({
|
|
221
311
|
gate,
|
|
222
312
|
cmd,
|
|
@@ -339,7 +429,7 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
|
|
|
339
429
|
});
|
|
340
430
|
continue;
|
|
341
431
|
}
|
|
342
|
-
const verdict = freshFailures(entry, stripped);
|
|
432
|
+
const verdict = freshFailures(entry, stripped, gate === "test");
|
|
343
433
|
const { failing, unreadable } = verdict;
|
|
344
434
|
// S22: a missing or unreadable entry forgives nothing; the red names it rather than a bare exit code.
|
|
345
435
|
const refusal = baselineEntryRefusal(gate, verdict);
|
|
@@ -366,8 +456,12 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
|
|
|
366
456
|
// often the previous session's — so a capture taken under a different resolved capacity forgives
|
|
367
457
|
// nothing at the tip either. An absent capacity is a pre-T7 baseline and keeps today's verdict.
|
|
368
458
|
const comparable = sameCapacity(entry?.capacity, r.capacity);
|
|
369
|
-
const
|
|
459
|
+
const forgivable = r.code !== 0 && baselineRed && failing.length === 0 && !unreadable && cause !== "infra"
|
|
370
460
|
&& comparable;
|
|
461
|
+
// S19m4 (battery parity): kill text beside forgivable failures means the suite never completed — red, infra.
|
|
462
|
+
const killLine = forgivable && !greenTeardown && !deficit ? verdict.killLine : undefined;
|
|
463
|
+
const forgiven = forgivable && killLine === undefined;
|
|
464
|
+
const rowCause = killLine !== undefined ? "infra" : cause;
|
|
371
465
|
const pass = !deficit && (r.code === 0 || greenTeardown || forgiven);
|
|
372
466
|
if (!pass)
|
|
373
467
|
writeTipLog(artifact, raw);
|
|
@@ -378,18 +472,20 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
|
|
|
378
472
|
evidenceReceipt, evidenceReceipts,
|
|
379
473
|
exitCode: r.code,
|
|
380
474
|
fingerprints: r.code !== 0 && !greenTeardown ? fingerprint(stripped) : [],
|
|
381
|
-
details: `${
|
|
475
|
+
details: `${rowCause === "infra" ? "infra; " : ""}${rerun ? `${rerun}: ` : ""}` + (deficit?.replace(/^infra; /, "") ?? (r.code === 0 ? "exit 0"
|
|
382
476
|
: greenTeardown ? `exit ${r.code} after a green suite summary; only the runner's teardown fingerprint followed it`
|
|
383
477
|
: forgiven ? `exit ${r.code} but only baseline-recorded failures (forgiven vs baseline)`
|
|
384
|
-
:
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
478
|
+
: killLine !== undefined ? `exit ${r.code}; every failure is baseline-recorded, but the runner printed kill text — `
|
|
479
|
+
+ `it never completed a suite, so nothing is forgiven: ${killLine}`
|
|
480
|
+
: !comparable && baselineRed && failing.length === 0
|
|
481
|
+
? `exit ${r.code}; every failure is baseline-recorded, but that capture ran under `
|
|
482
|
+
+ `${describeCapacity(entry?.capacity)} and this verification ran under ${describeCapacity(r.capacity)} `
|
|
483
|
+
+ `— forgiveness across a changed capacity is not evidence`
|
|
484
|
+
: `exit ${r.code}`))
|
|
389
485
|
// S22: every red here names a missing or unreadable entry beside its own cause, deficit included
|
|
390
486
|
+ (!pass && refusal ? `; ${refusal}` : ""),
|
|
391
487
|
...(forgiven && !deficit ? { forgiven: true, ...(baseline?.provenance ? { baselineProvenance: baseline.provenance } : {}) } : {}),
|
|
392
|
-
...(
|
|
488
|
+
...(rowCause ? { cause: rowCause } : {}),
|
|
393
489
|
...(pass ? {} : { artifact }),
|
|
394
490
|
};
|
|
395
491
|
results.push(tipResult);
|
|
@@ -406,14 +502,20 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
|
|
|
406
502
|
}
|
|
407
503
|
const id = pending.get(result.gate);
|
|
408
504
|
if (id && id.tree === completedTree && !isInfraResult(result)) {
|
|
409
|
-
|
|
505
|
+
const verdict = { ...result, meta: {
|
|
410
506
|
fingerprints: result.fingerprints, forgiven: result.forgiven, baselineProvenance: result.baselineProvenance,
|
|
507
|
+
...(result.forgiven && result.gate === "test" ? { killTextRead: true } : {}),
|
|
411
508
|
reportPath: result.reportPath, spawnedCommand: result.spawnedCommand,
|
|
412
509
|
artifact: result.artifact,
|
|
413
510
|
evidenceReceipt: result.evidenceReceipt, evidenceReceipts: result.evidenceReceipts,
|
|
414
511
|
evidenceAbsence: result.evidenceAbsence, originRunRoot: result.originRunRoot,
|
|
415
512
|
nonce: result.nonce, stdoutPath: result.stdoutPath, stderrPath: result.stderrPath,
|
|
416
|
-
} }
|
|
513
|
+
} };
|
|
514
|
+
// 2.8.1 T1: a deferring caller (run end) writes the verdict itself, only after its closing identity check.
|
|
515
|
+
if (deferVerdicts)
|
|
516
|
+
result.deferredVerdict = { id, verdict };
|
|
517
|
+
else
|
|
518
|
+
store.set(id, verdict);
|
|
417
519
|
}
|
|
418
520
|
}
|
|
419
521
|
return results;
|
package/package.json
CHANGED
|
@@ -503,6 +503,14 @@ journal tail to decide what happens next, or sweeping orphans — you have taken
|
|
|
503
503
|
- **the INTEGRATION subject** — the integration-branch commit that worktree was created from, named by
|
|
504
504
|
sha from the journal's `task-dispatch` row or `tickmarkr status`; never "the run", and never the
|
|
505
505
|
branch name alone, which moves while you are reading it;
|
|
506
|
+
- **the operator view of that branch** — a live run has NO integration checkout: the daemon publishes
|
|
507
|
+
each merge onto `tickmarkr/<runId>` by compare-and-swap from the repository root. View it through a
|
|
508
|
+
detached snapshot (`git worktree add --detach <dir> tickmarkr/<runId>`), refreshed by removing it
|
|
509
|
+
(`git worktree remove <dir>`) and re-adding it; check the branch out attached only with the daemon
|
|
510
|
+
stopped (no live `graph.lock`) — an attached checkout taken while the run is live goes stale at the next
|
|
511
|
+
publication, and any write to the ref parks the next merge infra. Run `tickmarkr verify` and `report`
|
|
512
|
+
from the main checkout or a snapshot;
|
|
513
|
+
a legacy attached view from a 2.8.0 run is stale from the first publication;
|
|
506
514
|
- **and, where the fix is a FILE the tree must hold** — a new test, a fixture, a schema — its presence
|
|
507
515
|
at that commit: `git -C <task-worktree> cat-file -e <task-head>:<path>`. A fix that lives only in the
|
|
508
516
|
daemon's `dist` cannot make a worker's missing oracle appear, and a worker red on that missing file is
|