tickmarkr 2.8.0 → 2.8.2
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.js +3 -2
- package/dist/cli/commands/approve.js +33 -8
- package/dist/cli/commands/verify.js +23 -11
- package/dist/gates/baseline.js +3 -3
- package/dist/gates/run-gates.js +61 -1
- package/dist/run/daemon.d.ts +77 -13
- package/dist/run/daemon.js +350 -95
- package/dist/run/git.d.ts +16 -3
- package/dist/run/git.js +19 -6
- package/dist/run/merge.d.ts +114 -7
- package/dist/run/merge.js +127 -34
- package/dist/run/wall-budget.js +6 -1
- package/package.json +1 -2
- package/skills/tickmarkr-overseer/SKILL.md +42 -20
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +162 -166
- package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +0 -133
package/dist/run/git.d.ts
CHANGED
|
@@ -198,16 +198,29 @@ export declare function shGitOk(cmd: string, cwd: string, own?: OwnGitOptions):
|
|
|
198
198
|
* A step raced by a commondir change is refused by name before the next one runs, and what the add created is rolled
|
|
199
199
|
* back through the trusted common directory derived here once (dropWorktree), even while that commondir stays hostile.
|
|
200
200
|
*/
|
|
201
|
-
export declare function addDetachedWorktree(cwd: string, dir: string, commitish: string, timeoutMs?: number
|
|
201
|
+
export declare function addDetachedWorktree(cwd: string, dir: string, commitish: string, timeoutMs?: number, { logHead }?: {
|
|
202
|
+
logHead?: boolean | undefined;
|
|
203
|
+
}): Promise<ShResult>;
|
|
204
|
+
/**
|
|
205
|
+
* 2.8.1 T1 (ANL-023 §7.1): the reflog witness W(dir, ref) — read-only, the newest entry first. It is valid for a commit
|
|
206
|
+
* x only when the exit is 0, at least one line came back and the first is x: exit 0 with no lines is git's answer for
|
|
207
|
+
* a missing or unreadable log, so an empty witness is a refusal, never an accepted empty.
|
|
208
|
+
*/
|
|
209
|
+
export declare function reflogWitness(dir: string, ref: string): Promise<{
|
|
210
|
+
code: number;
|
|
211
|
+
lines: string[];
|
|
212
|
+
}>;
|
|
202
213
|
export declare function npmDependencyManifestChanged(cwd: string, baselineRef: string): Promise<boolean>;
|
|
203
214
|
export declare function npmDependencyInstallCommand(cwd: string): string;
|
|
204
215
|
export declare function gitHead(cwd: string): Promise<string>;
|
|
205
216
|
export declare const sanitizeBranch: (branch: string) => string;
|
|
206
217
|
export declare const WORKTREES_DIR = "worktrees.noindex";
|
|
207
218
|
export declare const worktreePath: (repo: string, branch: string) => string;
|
|
208
|
-
/**
|
|
219
|
+
/**
|
|
220
|
+
* OBS-28: remove this run's recorded task worktrees; tolerates already-gone paths. 2.8.1 T2: a run keeps no
|
|
221
|
+
* integration checkout, and a 2.8.0 run's legacy one is never removed here (it may hold the only view of a merge).
|
|
222
|
+
*/
|
|
209
223
|
export declare function cleanupRunWorktrees(repo: string, branch: string, opts: {
|
|
210
|
-
removeIntegration: boolean;
|
|
211
224
|
removeTaskIds: string[];
|
|
212
225
|
}): Promise<void>;
|
|
213
226
|
export declare function resolveIntegrationBranch(_repo: string, branch: string): Promise<string>;
|
package/dist/run/git.js
CHANGED
|
@@ -607,7 +607,10 @@ export async function shGitOk(cmd, cwd, own = {}) {
|
|
|
607
607
|
* A step raced by a commondir change is refused by name before the next one runs, and what the add created is rolled
|
|
608
608
|
* back through the trusted common directory derived here once (dropWorktree), even while that commondir stays hostile.
|
|
609
609
|
*/
|
|
610
|
-
export async function addDetachedWorktree(cwd, dir, commitish, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS) {
|
|
610
|
+
export async function addDetachedWorktree(cwd, dir, commitish, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS, { logHead = false } = {}) {
|
|
611
|
+
// 2.8.1 T1 (ANL-023 §7.1): `logHead` writes the new checkout's HEAD reflog whatever the repository's
|
|
612
|
+
// core.logAllRefUpdates says, so a later HEAD move by any command is appended to an established witness.
|
|
613
|
+
const git = logHead ? "git -c core.logAllRefUpdates=always" : "git";
|
|
611
614
|
// the authority this operation keeps, rollback included: derived once from the trusted layout, never a commondir
|
|
612
615
|
const store = trustedCommonDir(cwd);
|
|
613
616
|
const resolved = await shGit(`git rev-parse --verify --end-of-options ${shq(`${commitish}^{commit}`)}`, cwd, timeoutMs);
|
|
@@ -616,12 +619,12 @@ export async function addDetachedWorktree(cwd, dir, commitish, timeoutMs = DEFAU
|
|
|
616
619
|
const oid = resolved.stdout.trim();
|
|
617
620
|
// under a ref payload git also resolves its new-HEAD stub into a shared refs/heads/.invalid; it is deleted again
|
|
618
621
|
const rollback = { dir, branch: { name: ".invalid" } };
|
|
619
|
-
const added = await shellEntry(
|
|
622
|
+
const added = await shellEntry(`${git} worktree add --no-checkout --detach ${shq(dir)} ${shq(oid)}`, cwd, timeoutMs, false, {}, { createsWorktree: "detached", rollback });
|
|
620
623
|
if (added.code !== 0)
|
|
621
624
|
return added;
|
|
622
625
|
let repaired;
|
|
623
626
|
try {
|
|
624
|
-
repaired = await shGit(
|
|
627
|
+
repaired = await shGit(`${git} update-ref --no-deref HEAD ${shq(oid)} && ${git} update-ref -d refs/heads/.invalid && ${git} reset -q --hard --no-recurse-submodules`, dir, timeoutMs);
|
|
625
628
|
}
|
|
626
629
|
catch (error) {
|
|
627
630
|
await dropWorktree(store, rollback, timeoutMs).catch((failure) => { throw rollbackFailed(error, failure); });
|
|
@@ -631,6 +634,15 @@ export async function addDetachedWorktree(cwd, dir, commitish, timeoutMs = DEFAU
|
|
|
631
634
|
await dropWorktree(store, rollback, timeoutMs);
|
|
632
635
|
return repaired;
|
|
633
636
|
}
|
|
637
|
+
/**
|
|
638
|
+
* 2.8.1 T1 (ANL-023 §7.1): the reflog witness W(dir, ref) — read-only, the newest entry first. It is valid for a commit
|
|
639
|
+
* x only when the exit is 0, at least one line came back and the first is x: exit 0 with no lines is git's answer for
|
|
640
|
+
* a missing or unreadable log, so an empty witness is a refusal, never an accepted empty.
|
|
641
|
+
*/
|
|
642
|
+
export async function reflogWitness(dir, ref) {
|
|
643
|
+
const r = await shGit(`git reflog show --no-abbrev --format=%H ${shq(ref)}`, dir);
|
|
644
|
+
return { code: r.code, lines: r.stdout.split("\n").map((line) => line.trim()).filter(Boolean) };
|
|
645
|
+
}
|
|
634
646
|
/** A failed rollback is never swallowed: it rides on the error the operation already throws (a refusal stays one). */
|
|
635
647
|
function rollbackFailed(error, failure) {
|
|
636
648
|
const why = `; its rollback failed, so what it created may remain: ${failure instanceof Error ? failure.message : String(failure)}`;
|
|
@@ -738,10 +750,11 @@ const sanitize = sanitizeBranch;
|
|
|
738
750
|
// once after this rename — the same one-time per-install cost as the v1.38 state-dir rename.
|
|
739
751
|
export const WORKTREES_DIR = "worktrees.noindex";
|
|
740
752
|
export const worktreePath = (repo, branch) => join(tickmarkrDir(repo), WORKTREES_DIR, sanitize(branch));
|
|
741
|
-
/**
|
|
753
|
+
/**
|
|
754
|
+
* OBS-28: remove this run's recorded task worktrees; tolerates already-gone paths. 2.8.1 T2: a run keeps no
|
|
755
|
+
* integration checkout, and a 2.8.0 run's legacy one is never removed here (it may hold the only view of a merge).
|
|
756
|
+
*/
|
|
742
757
|
export async function cleanupRunWorktrees(repo, branch, opts) {
|
|
743
|
-
if (opts.removeIntegration)
|
|
744
|
-
await removeWorktree(repo, worktreePath(repo, branch));
|
|
745
758
|
for (const id of opts.removeTaskIds)
|
|
746
759
|
await removeWorktree(repo, worktreePath(repo, `${branch}--${id}`));
|
|
747
760
|
}
|
package/dist/run/merge.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TickmarkrConfig } from "../config/config.js";
|
|
2
2
|
import { type Baseline, type BaselineProvenance, type GateEvidenceOptions, type FailureClassification } from "../gates/baseline.js";
|
|
3
|
+
import { type VerificationIdentity } from "../gates/cache.js";
|
|
3
4
|
import type { GateEvidenceReceipt } from "./protocol.js";
|
|
4
5
|
export interface TipVerifyResult {
|
|
5
6
|
evidenceReceipt?: GateEvidenceReceipt;
|
|
@@ -30,18 +31,124 @@ export interface TipVerifyResult {
|
|
|
30
31
|
* for a kill, the shared runner classifier for everything else. `infra` means nothing was verified.
|
|
31
32
|
*/
|
|
32
33
|
cause?: FailureClassification;
|
|
34
|
+
/** 2.8.1 T1: with `deferVerdicts`, the verdict-store write this result would have made, left to the caller. */
|
|
35
|
+
deferredVerdict?: {
|
|
36
|
+
id: VerificationIdentity;
|
|
37
|
+
verdict: TipVerifyResult & {
|
|
38
|
+
meta: Record<string, unknown>;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
33
41
|
}
|
|
34
42
|
/** Carry execution evidence without minting an invocation or rebasing its artifact paths. */
|
|
35
43
|
export declare function reusedTipEvidence(source: Partial<TipVerifyResult>): Partial<TipVerifyResult>;
|
|
36
44
|
export declare function integrationBranch(cfg: TickmarkrConfig, runId: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* 2.8.1 T2 (ANL-023 §7.2): creates the integration ref create-only at `baseRef` (the empty old value means "must not
|
|
47
|
+
* exist") and returns the published tip E. The run keeps no checkout of the branch: every later publication is a
|
|
48
|
+
* compare-and-swap of the ref from `repo` (mergeTask). An existing or uncreatable ref throws — the fatal path's text.
|
|
49
|
+
*/
|
|
37
50
|
export declare function ensureIntegration(repo: string, branch: string, baseRef: string): Promise<string>;
|
|
38
|
-
|
|
39
|
-
export
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
51
|
+
/** 2.8.1 T1 (ANL-023 §7.1): the closed states of the integration ref, as R(ref) reads them. */
|
|
52
|
+
export type IntegrationRefState = {
|
|
53
|
+
kind: "regular";
|
|
54
|
+
commit: string;
|
|
55
|
+
} | {
|
|
56
|
+
kind: "missing";
|
|
57
|
+
} | {
|
|
58
|
+
kind: "symbolic";
|
|
59
|
+
target: string;
|
|
60
|
+
commit?: string;
|
|
61
|
+
} | {
|
|
62
|
+
kind: "unreadable";
|
|
63
|
+
read: string;
|
|
64
|
+
};
|
|
65
|
+
export type IntegrationLeg = "symbolic" | "missing" | "value" | "unreadable";
|
|
66
|
+
/**
|
|
67
|
+
* R(ref): the integration ref read from `repo` through the own-git runner, never its reflog. The exact ref only —
|
|
68
|
+
* a descendant ref (`refs/heads/<branch>/x`) is not this ref, and a ref that exists but names no commit is unreadable.
|
|
69
|
+
*/
|
|
70
|
+
export declare function readIntegrationRef(repo: string, branch: string): Promise<IntegrationRefState>;
|
|
71
|
+
/** I(x): holds iff the ref is regular at x; otherwise the leg, with the state the repair text is chosen by. */
|
|
72
|
+
export declare function checkIntegrationIdentity(repo: string, branch: string, expected: string): Promise<{
|
|
73
|
+
ok: true;
|
|
74
|
+
state: IntegrationRefState;
|
|
75
|
+
} | {
|
|
76
|
+
ok: false;
|
|
77
|
+
leg: IntegrationLeg;
|
|
78
|
+
state: IntegrationRefState;
|
|
79
|
+
}>;
|
|
80
|
+
/**
|
|
81
|
+
* The repair text (shared by run end's failure row, the merge park and the resume refusal): backticked commands run
|
|
82
|
+
* from the repository root in order, stopping at the first that fails. A found commit is preserved create-only under
|
|
83
|
+
* refs/tickmarkr/found/<runId>/<F> before a no-deref compare-and-swap restores the ref; a symbolic found ref or one
|
|
84
|
+
* already holding another commit stops the sequence before the restore. Dangling and unreadable print no command.
|
|
85
|
+
*/
|
|
86
|
+
export declare function integrationRepairText(state: IntegrationRefState, branch: string, runId: string, expected: string): string;
|
|
87
|
+
type GitResult = {
|
|
88
|
+
code: number;
|
|
89
|
+
stdout: string;
|
|
90
|
+
stderr: string;
|
|
91
|
+
};
|
|
92
|
+
/** The merge's git steps after the identity check, in order; `publish` is the compare-and-swap. */
|
|
93
|
+
export type MergeStep = "ancestry" | "merge-tree" | "commit-tree" | "publish";
|
|
94
|
+
/** Test-only: runs before each step; a returned result replaces that step's git result (never `publish`'s). */
|
|
95
|
+
export type MergeSeam = (step: MergeStep) => Promise<GitResult | void> | GitResult | void;
|
|
96
|
+
export interface IntegrationMove {
|
|
97
|
+
branch: string;
|
|
98
|
+
expected: string;
|
|
99
|
+
found: string | null;
|
|
100
|
+
leg: IntegrationLeg;
|
|
101
|
+
target?: string;
|
|
102
|
+
stage: "check" | "publish";
|
|
103
|
+
}
|
|
104
|
+
export type MergeOutcome = {
|
|
105
|
+
ok: true;
|
|
106
|
+
commit: string;
|
|
107
|
+
from: string;
|
|
108
|
+
} | {
|
|
109
|
+
ok: true;
|
|
110
|
+
alreadyMerged: string;
|
|
111
|
+
} | {
|
|
112
|
+
ok: false;
|
|
113
|
+
tipMoved: {
|
|
43
114
|
gatedCommit: string;
|
|
44
115
|
branchTip: string;
|
|
45
116
|
};
|
|
46
|
-
}
|
|
47
|
-
|
|
117
|
+
} | {
|
|
118
|
+
ok: false;
|
|
119
|
+
conflict: string;
|
|
120
|
+
} | {
|
|
121
|
+
ok: false;
|
|
122
|
+
integrationMoved: IntegrationMove;
|
|
123
|
+
state: IntegrationRefState;
|
|
124
|
+
} | {
|
|
125
|
+
ok: false;
|
|
126
|
+
alreadyIntegrated: {
|
|
127
|
+
gated: string;
|
|
128
|
+
tip: string;
|
|
129
|
+
};
|
|
130
|
+
} | {
|
|
131
|
+
ok: false;
|
|
132
|
+
infra: {
|
|
133
|
+
step: MergeStep;
|
|
134
|
+
message: string;
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* 2.8.1 T2 (ANL-023 §7.2): publishes `gated` onto the integration ref by compare-and-swap from the stored tip
|
|
139
|
+
* `expected`, all git from `repo` — no checkout. (1) the task-tip check; (2) I(expected); (3) ancestry; (4) merge-tree;
|
|
140
|
+
* (5) commit-tree; (6) `update-ref --no-deref` from `expected`; (7) `onPublished(M)` in the same synchronous
|
|
141
|
+
* continuation as (6)'s result, so the caller's rows land in publication order. `merged` is the task's recorded merge
|
|
142
|
+
* commits, newest first: a gated commit already in the history is done only through its own recorded merge.
|
|
143
|
+
* `ancestorOnly` (a replay that ran no gates): a gated commit not in E is refused `infra`, never published.
|
|
144
|
+
*/
|
|
145
|
+
export declare function mergeTask(repo: string, { branch, expected, merged, ancestorOnly }: {
|
|
146
|
+
branch: string;
|
|
147
|
+
expected: string;
|
|
148
|
+
merged?: readonly string[];
|
|
149
|
+
ancestorOnly?: boolean;
|
|
150
|
+
}, taskBranch: string, message: string, gated: string, onPublished: (commit: string) => void, seam?: MergeSeam): Promise<MergeOutcome>;
|
|
151
|
+
export declare function verifyIntegrationTip(intWt: string, commands: Record<string, string>, runDir: string, baseline?: Baseline, evidenceOptions?: GateEvidenceOptions, { deferVerdicts }?: {
|
|
152
|
+
deferVerdicts?: boolean;
|
|
153
|
+
}): Promise<TipVerifyResult[]>;
|
|
154
|
+
export {};
|
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;
|
|
@@ -414,7 +502,7 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
|
|
|
414
502
|
}
|
|
415
503
|
const id = pending.get(result.gate);
|
|
416
504
|
if (id && id.tree === completedTree && !isInfraResult(result)) {
|
|
417
|
-
|
|
505
|
+
const verdict = { ...result, meta: {
|
|
418
506
|
fingerprints: result.fingerprints, forgiven: result.forgiven, baselineProvenance: result.baselineProvenance,
|
|
419
507
|
...(result.forgiven && result.gate === "test" ? { killTextRead: true } : {}),
|
|
420
508
|
reportPath: result.reportPath, spawnedCommand: result.spawnedCommand,
|
|
@@ -422,7 +510,12 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
|
|
|
422
510
|
evidenceReceipt: result.evidenceReceipt, evidenceReceipts: result.evidenceReceipts,
|
|
423
511
|
evidenceAbsence: result.evidenceAbsence, originRunRoot: result.originRunRoot,
|
|
424
512
|
nonce: result.nonce, stdoutPath: result.stdoutPath, stderrPath: result.stderrPath,
|
|
425
|
-
} }
|
|
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);
|
|
426
519
|
}
|
|
427
520
|
}
|
|
428
521
|
return results;
|
package/dist/run/wall-budget.js
CHANGED
|
@@ -74,7 +74,12 @@ export function wallBudget(events) {
|
|
|
74
74
|
return;
|
|
75
75
|
span("queue", opened, ts);
|
|
76
76
|
// One open wait per owner, closed in journal order: the list stays sorted and non-overlapping.
|
|
77
|
-
|
|
77
|
+
// Appended in place: a copy per close made a run with n waits cost O(n^2) (ANL-022 §3 T1 #5).
|
|
78
|
+
const list = queued.get(key);
|
|
79
|
+
if (list)
|
|
80
|
+
list.push([opened, ts]);
|
|
81
|
+
else
|
|
82
|
+
queued.set(key, [[opened, ts]]);
|
|
78
83
|
waits.delete(key);
|
|
79
84
|
};
|
|
80
85
|
const service = (gate, task, a, b) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tickmarkr",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.2",
|
|
4
4
|
"description": "Spec in, verified work out.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -77,7 +77,6 @@
|
|
|
77
77
|
"tsconfig.json",
|
|
78
78
|
"vitest.config.ts",
|
|
79
79
|
".github/pull_request_template.md",
|
|
80
|
-
"scripts/assert-test-file-count.sh",
|
|
81
80
|
"scripts/emit-schema.ts",
|
|
82
81
|
"scripts/probe-rig.mjs",
|
|
83
82
|
"scripts/run-ci-vitest.sh",
|
|
@@ -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
|
|
@@ -683,12 +691,13 @@ subject of a release proof is never *"the run"* and never *"the fix leg"* — it
|
|
|
683
691
|
the one commit that carries the run's work AND every leg merged into it. Name that commit's sha in the
|
|
684
692
|
proof, in the same act as the grade, and the proof is bound to that sha alone. **When the subject moves — a
|
|
685
693
|
later merge, a new integration tip, an amended or re-cut commit, a fresh export, a re-tag — the proof is
|
|
686
|
-
VOID, and the new subject is graded from scratch**,
|
|
687
|
-
the merged tree and not from either parent. Two greens assembled into one verdict are two
|
|
688
|
-
trees that never contained each other: the union's own conflict resolutions, its ordering
|
|
689
|
-
collected
|
|
690
|
-
the
|
|
691
|
-
pointed at bytes: **a proof that names no commit cannot
|
|
694
|
+
VOID, and the new subject is graded from scratch**, its tracked test set included, since that set is
|
|
695
|
+
recomputed from the merged tree and not from either parent. Two greens assembled into one verdict are two
|
|
696
|
+
claims about two trees that never contained each other: the union's own conflict resolutions, its ordering
|
|
697
|
+
and its collected test set are exactly what neither green observed — which is why `RELEASING.md` step 4
|
|
698
|
+
hands the grader the mirror's `HEAD` and the grader refuses a run of any other head before a single record
|
|
699
|
+
is downloaded. This is the void-condition duty pointed at bytes: **a proof that names no commit cannot
|
|
700
|
+
notice its subject leaving.**
|
|
692
701
|
|
|
693
702
|
**THE RECIPROCAL DUTY, and it is yours because you write both documents:** when you issue a ruling that
|
|
694
703
|
re-scopes, narrows, splits or re-owns anything, **the ruling must name every sealed document its subject
|
|
@@ -1051,7 +1060,8 @@ they are left implicit:
|
|
|
1051
1060
|
test exists — the acceptance oracle runs the sentence as vitest `-t` and a paraphrase matches zero tests.
|
|
1052
1061
|
- Every fix-leg verify carries `--author <the seat that wrote it>`; without it the author is HUMAN, every
|
|
1053
1062
|
vendor stays eligible, and a same-vendor approval is not the cross-vendor review the criterion names.
|
|
1054
|
-
- A grader change is proved on a
|
|
1063
|
+
- A grader change is proved on a recorded run — GitHub's structured run record and the proof groups its
|
|
1064
|
+
jobs uploaded — never on a synthetic job log: the grader reads no log text at all.
|
|
1055
1065
|
- A public-CI red is classified in a throttled Linux container first (`docker run --cpus=2 node:22`),
|
|
1056
1066
|
by measurement: starvation on the base tree is not a defect on the branch.
|
|
1057
1067
|
|
|
@@ -1595,19 +1605,31 @@ Create specialist seats using `orca terminal create --worktree path:<repo> --com
|
|
|
1595
1605
|
- **RUN BOTH RELEASE PROOFS BEFORE STANDING DOWN.** The first is the exported-tree suite, which
|
|
1596
1606
|
maintainers run from the private repo with its `verify:export` script — that tooling is deliberately
|
|
1597
1607
|
not part of this package, so the command does not exist in a public checkout; it must install,
|
|
1598
|
-
build, lint, and only then run the suite in the exported tree
|
|
1599
|
-
|
|
1600
|
-
smoke
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
`
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1608
|
+
build, lint, and only then run the suite in the exported tree, and only its zero exit writes the export
|
|
1609
|
+
proof that `export-public.sh --onto` reads before it writes the mirror commit. Then the installed-Orca
|
|
1610
|
+
smoke must exercise its live leg, decided from its own record and never from Vitest's exit status:
|
|
1611
|
+
`TICKMARKR_CI_RECORD=/tmp/tickmarkr-orca-smoke.record.json TICKMARKR_E2E=1 npx --no-install vitest run tests/e2e/orca-smoke.e2e.test.ts`,
|
|
1612
|
+
then
|
|
1613
|
+
`npx --no-install tsx scripts/ci/cli.ts verdict --require-test 'tests/e2e/orca-smoke.e2e.test.ts::e2e: orca driver smoke > live: the real-orca terminal leg — vitest-skipped without the gate or a reachable runtime' /tmp/tickmarkr-orca-smoke.record.json`
|
|
1614
|
+
must exit 0. The forced-stall proof (RELEASING.md step 6) is decided the same way: its record from
|
|
1615
|
+
`TICKMARKR_CI_RECORD=/tmp/tickmarkr-forced-stall.record.json TICKMARKR_TEST_FORCE_STALL=1 npx --no-install vitest run tests/e2e/forced-stall.e2e.test.ts -t 'the enabled forced-stall release proof makes the integration tip verifier recover a real RPC-stranded serial file as parallel exit1 then serial-only exit0 under distinct nonces, so a helper-only green or a full-suite retry fails'`,
|
|
1616
|
+
then
|
|
1617
|
+
`npx --no-install tsx scripts/ci/cli.ts verdict --require-test 'tests/e2e/forced-stall.e2e.test.ts::the enabled forced-stall release proof makes the integration tip verifier recover a real RPC-stranded serial file as parallel exit1 then serial-only exit0 under distinct nonces, so a helper-only green or a full-suite retry fails' /tmp/tickmarkr-forced-stall.record.json`
|
|
1618
|
+
must exit 0. A skipped smoke or stall leaf is not proof: Vitest exits 0 when the named leaf skips (the gate
|
|
1619
|
+
unset, Orca unreachable, a drifted `-t` title), and the verdict reads it RED. Record both results before
|
|
1620
|
+
declaring the release complete.
|
|
1621
|
+
|
|
1622
|
+
- **GRADE PUBLIC CI FROM ITS RUN RECORD AND PROOF GROUPS AT RUN-END ONLY (law 48).** A badge is not
|
|
1623
|
+
the grade, and an in-progress run is never graded. At run-end execute
|
|
1624
|
+
`scripts/grade-ci.sh <run-id> <mirror HEAD sha> <mirror clone> <tag>` and require GREEN from every job
|
|
1625
|
+
of the declared inventory it prints first — serial `test` and `test-macos`; a missing, duplicated or
|
|
1626
|
+
unknown job reads UNREADABLE, never a skip, and a sharded run reads UNREADABLE until its proof-group
|
|
1627
|
+
selection ships (ANL-022 §7.4). The grader is bound to the head: a run of any other commit reads
|
|
1628
|
+
UNREADABLE before anything is downloaded. It reads GitHub's structured step outcomes and conclusions and
|
|
1629
|
+
recomputes each job's uploaded proof group (`proof-<job>`: the Vitest records and the floors sidecar)
|
|
1630
|
+
against the test files that commit tracks in the mirror clone — there is no count to derive, and no job
|
|
1631
|
+
log text is read, so a log gh labels `UNKNOWN STEP` or a test that prints timeout text cannot move the
|
|
1632
|
+
grade. Its UNREADABLE state is a hard stop, not a green default.
|
|
1611
1633
|
|
|
1612
1634
|
- **RE-READ THE REGISTRY BEFORE RULING (law 51).** A successful Release run and the first
|
|
1613
1635
|
`npm view` are separate observations: registry propagation can briefly return the prior version.
|