@tokenfactory/acc-runner 0.12.0 → 0.16.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/README.md +54 -0
- package/dist/cli.js +5 -0
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts +19 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +126 -3
- package/dist/doctor.js.map +1 -1
- package/dist/failure-classifier.d.ts +58 -7
- package/dist/failure-classifier.d.ts.map +1 -1
- package/dist/failure-classifier.js +172 -6
- package/dist/failure-classifier.js.map +1 -1
- package/dist/git.d.ts +15 -0
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +19 -0
- package/dist/git.js.map +1 -1
- package/dist/github-client.d.ts +133 -0
- package/dist/github-client.d.ts.map +1 -0
- package/dist/github-client.js +234 -0
- package/dist/github-client.js.map +1 -0
- package/dist/runtime/cache-dir.d.ts +2 -0
- package/dist/runtime/cache-dir.d.ts.map +1 -0
- package/dist/runtime/cache-dir.js +15 -0
- package/dist/runtime/cache-dir.js.map +1 -0
- package/dist/runtime/quarantine.d.ts +10 -0
- package/dist/runtime/quarantine.d.ts.map +1 -1
- package/dist/runtime/quarantine.js.map +1 -1
- package/dist/runtime/resolution-integrity.d.ts +86 -0
- package/dist/runtime/resolution-integrity.d.ts.map +1 -0
- package/dist/runtime/resolution-integrity.js +248 -0
- package/dist/runtime/resolution-integrity.js.map +1 -0
- package/dist/runtime/reviewer.d.ts +8 -1
- package/dist/runtime/reviewer.d.ts.map +1 -1
- package/dist/runtime/reviewer.js +18 -0
- package/dist/runtime/reviewer.js.map +1 -1
- package/dist/runtime/singleton.d.ts +47 -0
- package/dist/runtime/singleton.d.ts.map +1 -0
- package/dist/runtime/singleton.js +200 -0
- package/dist/runtime/singleton.js.map +1 -0
- package/dist/runtime/version-drift.d.ts +31 -0
- package/dist/runtime/version-drift.d.ts.map +1 -0
- package/dist/runtime/version-drift.js +114 -0
- package/dist/runtime/version-drift.js.map +1 -0
- package/dist/task-runner.d.ts +40 -2
- package/dist/task-runner.d.ts.map +1 -1
- package/dist/task-runner.js +290 -50
- package/dist/task-runner.js.map +1 -1
- package/dist/watch.d.ts +19 -0
- package/dist/watch.d.ts.map +1 -1
- package/dist/watch.js +254 -6
- package/dist/watch.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.53 T-53-4 — runner-side, budget-aware GitHub REST client.
|
|
3
|
+
*
|
|
4
|
+
* A port of the api/ side's `api/_lib/github-client.ts` (T-52-4) into the
|
|
5
|
+
* runner package. The runner CANNOT import api/ code (separate build, no
|
|
6
|
+
* shared module graph), so the pattern is reproduced here. The three
|
|
7
|
+
* behaviours match the server client 1:1:
|
|
8
|
+
*
|
|
9
|
+
* 1. Quota tracking — x-ratelimit-remaining / x-ratelimit-reset per
|
|
10
|
+
* resource (core, search, graphql, …) from every response, surfaced
|
|
11
|
+
* via getQuotaSnapshot() for `acc-runner doctor`.
|
|
12
|
+
*
|
|
13
|
+
* 2. Secondary-limit backoff — a 429, or a 403 carrying retry-after /
|
|
14
|
+
* x-ratelimit-remaining: 0, is a rate-limit response, not an answer.
|
|
15
|
+
* The client sleeps once (retry-after if given, else an exponential
|
|
16
|
+
* base, both jittered, capped at MAX_BACKOFF_MS) and retries once. A
|
|
17
|
+
* second rate-limit response — or a retry-after beyond the cap —
|
|
18
|
+
* surfaces as a typed RateLimitedError.
|
|
19
|
+
*
|
|
20
|
+
* 3. Per-task-execution soft budget — runTask arms a budget at the top
|
|
21
|
+
* of a task (startTaskGithubBudget) and disarms it in its finally
|
|
22
|
+
* (endTaskGithubBudget). Once the budget is spent, every further
|
|
23
|
+
* githubFetch throws GithubBudgetExceededError WITHOUT hitting the
|
|
24
|
+
* network. The caller treats this as a CLEAN, RETRYABLE infra failure
|
|
25
|
+
* — NOT a code failure: it never reaches classifyClaudeExit, so the
|
|
26
|
+
* runner is never quarantined for exhausting its GitHub budget.
|
|
27
|
+
*
|
|
28
|
+
* ── Coverage note (READ THIS) ──────────────────────────────────────────
|
|
29
|
+
* As of v0.53 the runner makes ALL of its GitHub calls through the `gh`
|
|
30
|
+
* CLI (see gh.ts, runtime/conflict-resolver.ts, runtime/rework.ts,
|
|
31
|
+
* runtime/reviewer.ts). Those `gh`/`gh api` invocations are SEPARATE
|
|
32
|
+
* processes with their own rate-limit handling and are out of scope for
|
|
33
|
+
* this client — they remain UNCOVERED by the budget/backoff here. This
|
|
34
|
+
* module is the sanctioned wrapper for any future *direct* REST call the
|
|
35
|
+
* runner adds (`fetch` against api.github.com); routing such calls through
|
|
36
|
+
* githubFetch is what keeps them budget- and rate-limit-aware.
|
|
37
|
+
*
|
|
38
|
+
* Reference: api/_lib/github-client.ts, planning/ACC_WORKFLOW_GAP_ANALYSIS.md §G13.
|
|
39
|
+
*/
|
|
40
|
+
/** Soft per-task-execution call budget. */
|
|
41
|
+
export const DEFAULT_TASK_GITHUB_BUDGET = 50;
|
|
42
|
+
/**
|
|
43
|
+
* Longest single backoff sleep. A secondary-limit retry-after can be 60s+;
|
|
44
|
+
* sleeping that long inside a task run wastes wall-clock the operator is
|
|
45
|
+
* paying for, so anything above the cap skips the retry and throws
|
|
46
|
+
* RateLimitedError instead.
|
|
47
|
+
*/
|
|
48
|
+
export const MAX_BACKOFF_MS = 15_000;
|
|
49
|
+
/** Base for the exponential backoff when GitHub sends no retry-after. */
|
|
50
|
+
export const BASE_BACKOFF_MS = 2_000;
|
|
51
|
+
/**
|
|
52
|
+
* Typed rate-limit failure: GitHub said "back off" twice in a row (or
|
|
53
|
+
* asked for a longer wait than we can afford). A rate-limit storm is then
|
|
54
|
+
* diagnosable instead of looking like random github_http_403 noise. This
|
|
55
|
+
* is a RETRYABLE INFRA failure — see isRetryableGithubError.
|
|
56
|
+
*/
|
|
57
|
+
export class RateLimitedError extends Error {
|
|
58
|
+
status;
|
|
59
|
+
resource;
|
|
60
|
+
/** ISO timestamp the quota window resets at, when GitHub said. */
|
|
61
|
+
reset_at;
|
|
62
|
+
retry_after_s;
|
|
63
|
+
constructor(opts) {
|
|
64
|
+
super(`github rate limited (${opts.status})` +
|
|
65
|
+
(opts.resource ? ` on ${opts.resource}` : "") +
|
|
66
|
+
(opts.reset_at ? `, resets ${opts.reset_at}` : ""));
|
|
67
|
+
this.name = "RateLimitedError";
|
|
68
|
+
this.status = opts.status;
|
|
69
|
+
this.resource = opts.resource;
|
|
70
|
+
this.reset_at = opts.reset_at;
|
|
71
|
+
this.retry_after_s = opts.retry_after_s;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Soft-budget exhaustion. NOT a code failure: the task simply did as much
|
|
76
|
+
* direct GitHub work as one task execution is allowed. The caller should
|
|
77
|
+
* surface it as a clean, RETRYABLE infra failure and NEVER feed it to the
|
|
78
|
+
* claude-exit classifier (which would quarantine the runner).
|
|
79
|
+
*/
|
|
80
|
+
export class GithubBudgetExceededError extends Error {
|
|
81
|
+
used;
|
|
82
|
+
budget;
|
|
83
|
+
constructor(used, budget) {
|
|
84
|
+
super(`github task budget exhausted (${used}/${budget} calls)`);
|
|
85
|
+
this.name = "GithubBudgetExceededError";
|
|
86
|
+
this.used = used;
|
|
87
|
+
this.budget = budget;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* v0.53 T-53-4: both client failure modes are retryable infra conditions,
|
|
92
|
+
* not task/code failures. The contract the runner relies on: an error for
|
|
93
|
+
* which this returns true must surface as `phase: "github"` / retryable and
|
|
94
|
+
* MUST NOT set `quarantine_cause` — i.e. it is classifier-neutral and never
|
|
95
|
+
* benches the runner.
|
|
96
|
+
*/
|
|
97
|
+
export function isRetryableGithubError(err) {
|
|
98
|
+
return err instanceof RateLimitedError || err instanceof GithubBudgetExceededError;
|
|
99
|
+
}
|
|
100
|
+
// ── Module state (per process) ───────────────────────────────────────────────
|
|
101
|
+
const quotaByResource = new Map();
|
|
102
|
+
let usedThisTask = 0;
|
|
103
|
+
let taskBudget = null;
|
|
104
|
+
/**
|
|
105
|
+
* Begin a budgeted task execution: resets the per-task counter and arms the
|
|
106
|
+
* soft budget. runTask calls this at the top of a task; the explicit reset
|
|
107
|
+
* also re-baselines a long-lived watch process between tasks.
|
|
108
|
+
*/
|
|
109
|
+
export function startTaskGithubBudget(budget = DEFAULT_TASK_GITHUB_BUDGET) {
|
|
110
|
+
usedThisTask = 0;
|
|
111
|
+
taskBudget = budget;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* End a budgeted task execution: disarms the soft budget so any direct REST
|
|
115
|
+
* call made OUTSIDE a task run (e.g. doctor) is unbudgeted. Idempotent.
|
|
116
|
+
*/
|
|
117
|
+
export function endTaskGithubBudget() {
|
|
118
|
+
taskBudget = null;
|
|
119
|
+
}
|
|
120
|
+
export function getQuotaSnapshot() {
|
|
121
|
+
let min = null;
|
|
122
|
+
const resources = {};
|
|
123
|
+
for (const [name, q] of quotaByResource) {
|
|
124
|
+
resources[name] = { ...q };
|
|
125
|
+
if (q.remaining !== null &&
|
|
126
|
+
(min === null || min.remaining === null || q.remaining < min.remaining)) {
|
|
127
|
+
min = q;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
remaining: min?.remaining ?? null,
|
|
132
|
+
reset_at: min?.reset_at ?? null,
|
|
133
|
+
used_this_task: usedThisTask,
|
|
134
|
+
resources,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** Test seam: wipe quota/budget state between unit tests. */
|
|
138
|
+
export function resetGithubClientStateForTests() {
|
|
139
|
+
quotaByResource.clear();
|
|
140
|
+
usedThisTask = 0;
|
|
141
|
+
taskBudget = null;
|
|
142
|
+
}
|
|
143
|
+
function recordRateLimitHeaders(res) {
|
|
144
|
+
const remainingRaw = res.headers.get("x-ratelimit-remaining");
|
|
145
|
+
const resetRaw = res.headers.get("x-ratelimit-reset");
|
|
146
|
+
if (remainingRaw === null && resetRaw === null)
|
|
147
|
+
return;
|
|
148
|
+
const resource = res.headers.get("x-ratelimit-resource") ?? "core";
|
|
149
|
+
const remaining = remainingRaw !== null && remainingRaw !== "" ? Number(remainingRaw) : NaN;
|
|
150
|
+
const resetEpochS = resetRaw !== null && resetRaw !== "" ? Number(resetRaw) : NaN;
|
|
151
|
+
quotaByResource.set(resource, {
|
|
152
|
+
remaining: Number.isFinite(remaining) ? remaining : null,
|
|
153
|
+
reset_at: Number.isFinite(resetEpochS)
|
|
154
|
+
? new Date(resetEpochS * 1000).toISOString()
|
|
155
|
+
: null,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* A 429 is always "back off". A 403 is only a rate limit when it carries
|
|
160
|
+
* retry-after (secondary limit) or x-ratelimit-remaining: 0 (primary limit
|
|
161
|
+
* exhausted) — a plain 403 is a permissions error and passes through.
|
|
162
|
+
*/
|
|
163
|
+
function isRateLimitResponse(res) {
|
|
164
|
+
if (res.status === 429)
|
|
165
|
+
return true;
|
|
166
|
+
if (res.status !== 403)
|
|
167
|
+
return false;
|
|
168
|
+
if (res.headers.get("retry-after") !== null)
|
|
169
|
+
return true;
|
|
170
|
+
return res.headers.get("x-ratelimit-remaining") === "0";
|
|
171
|
+
}
|
|
172
|
+
function toRateLimitedError(res) {
|
|
173
|
+
const retryAfterRaw = res.headers.get("retry-after");
|
|
174
|
+
const retryAfter = retryAfterRaw !== null ? Number(retryAfterRaw) : NaN;
|
|
175
|
+
const resetRaw = res.headers.get("x-ratelimit-reset");
|
|
176
|
+
const resetEpochS = resetRaw !== null ? Number(resetRaw) : NaN;
|
|
177
|
+
return new RateLimitedError({
|
|
178
|
+
status: res.status,
|
|
179
|
+
resource: res.headers.get("x-ratelimit-resource"),
|
|
180
|
+
reset_at: Number.isFinite(resetEpochS)
|
|
181
|
+
? new Date(resetEpochS * 1000).toISOString()
|
|
182
|
+
: null,
|
|
183
|
+
retry_after_s: Number.isFinite(retryAfter) ? retryAfter : null,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
function defaultSleep(ms) {
|
|
187
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Authenticated GitHub REST fetch. The caller supplies whatever token is
|
|
191
|
+
* appropriate (the runner uses the operator's own gh/git credentials for
|
|
192
|
+
* CLI calls; a direct REST consumer would pass a token it already holds).
|
|
193
|
+
* An empty token sends an anonymous request rather than `Bearer <empty>`.
|
|
194
|
+
*/
|
|
195
|
+
export async function githubFetch(url, token, init = {}, deps = {}) {
|
|
196
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
197
|
+
const sleep = deps.sleep ?? defaultSleep;
|
|
198
|
+
const random = deps.random ?? Math.random;
|
|
199
|
+
const headers = new Headers(init.headers);
|
|
200
|
+
if (token)
|
|
201
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
202
|
+
if (!headers.has("Accept")) {
|
|
203
|
+
headers.set("Accept", "application/vnd.github+json");
|
|
204
|
+
}
|
|
205
|
+
headers.set("X-GitHub-Api-Version", "2022-11-28");
|
|
206
|
+
headers.set("User-Agent", "acc-runner");
|
|
207
|
+
const doFetch = async () => {
|
|
208
|
+
if (taskBudget !== null && usedThisTask >= taskBudget) {
|
|
209
|
+
throw new GithubBudgetExceededError(usedThisTask, taskBudget);
|
|
210
|
+
}
|
|
211
|
+
usedThisTask++;
|
|
212
|
+
const res = await fetchImpl(url, { ...init, headers });
|
|
213
|
+
recordRateLimitHeaders(res);
|
|
214
|
+
return res;
|
|
215
|
+
};
|
|
216
|
+
const first = await doFetch();
|
|
217
|
+
if (!isRateLimitResponse(first))
|
|
218
|
+
return first;
|
|
219
|
+
// Rate limited: sleep jittered-exponential once, retry once.
|
|
220
|
+
const retryAfterRaw = first.headers.get("retry-after");
|
|
221
|
+
const retryAfterS = retryAfterRaw !== null ? Number(retryAfterRaw) : NaN;
|
|
222
|
+
const baseMs = Number.isFinite(retryAfterS) ? retryAfterS * 1000 : BASE_BACKOFF_MS;
|
|
223
|
+
if (baseMs > MAX_BACKOFF_MS) {
|
|
224
|
+
// GitHub asked for a longer pause than we want to burn mid-task —
|
|
225
|
+
// surface the typed error now instead of sleeping into the run.
|
|
226
|
+
throw toRateLimitedError(first);
|
|
227
|
+
}
|
|
228
|
+
await sleep(Math.round(baseMs * (1 + random() * 0.5)));
|
|
229
|
+
const second = await doFetch();
|
|
230
|
+
if (!isRateLimitResponse(second))
|
|
231
|
+
return second;
|
|
232
|
+
throw toRateLimitedError(second);
|
|
233
|
+
}
|
|
234
|
+
//# sourceMappingURL=github-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github-client.js","sourceRoot":"","sources":["../src/github-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-dir.d.ts","sourceRoot":"","sources":["../../src/runtime/cache-dir.ts"],"names":[],"mappings":"AAWA,wBAAgB,QAAQ,IAAI,MAAM,CAKjC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.14.0 (T-54-2): single source of truth for the runner cache root.
|
|
3
|
+
*
|
|
4
|
+
* `quarantine.ts` already honoured ACC_RUNNER_CACHE_DIR inline; the
|
|
5
|
+
* singleton-lock and last-task-env modules added in this release reuse the
|
|
6
|
+
* same resolution so tests can point all cache state at a tmpdir with one
|
|
7
|
+
* env var. Defaults to ~/.cache/acc-runner (unchanged from prior releases).
|
|
8
|
+
*/
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
export function cacheDir() {
|
|
12
|
+
return (process.env.ACC_RUNNER_CACHE_DIR?.trim() ||
|
|
13
|
+
path.join(os.homedir(), ".cache", "acc-runner"));
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=cache-dir.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-dir.js","sourceRoot":"","sources":["../../src/runtime/cache-dir.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,MAAM,UAAU,QAAQ;IACtB,OAAO,CACL,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,IAAI,EAAE;QACxC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,YAAY,CAAC,CAChD,CAAC;AACJ,CAAC"}
|
|
@@ -4,6 +4,16 @@ export interface QuarantineState {
|
|
|
4
4
|
classifiedAt: string;
|
|
5
5
|
taskId: string;
|
|
6
6
|
detail: string;
|
|
7
|
+
/**
|
|
8
|
+
* v0.53 T-53-4: how many consecutive instant-empty (heuristic) claude
|
|
9
|
+
* exits triggered the quarantine. Cache-schema additive — older readers
|
|
10
|
+
* ignore it; the keychain entry schema and the runner protocol message
|
|
11
|
+
* shapes are untouched. Definitive machine failures (dyld, exit 127,
|
|
12
|
+
* usage/auth patterns) record 1; a heuristic env_broken records 2 (the
|
|
13
|
+
* task-runner only quarantines after the retry also exits instantly).
|
|
14
|
+
* Optional so a state written by a pre-0.53 runner still parses.
|
|
15
|
+
*/
|
|
16
|
+
consecutive?: number;
|
|
7
17
|
}
|
|
8
18
|
/** Path to the quarantine sentinel file. Configurable via ACC_RUNNER_CACHE_DIR for tests. */
|
|
9
19
|
export declare function quarantineFile(): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quarantine.d.ts","sourceRoot":"","sources":["../../src/runtime/quarantine.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAEhE,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,eAAe,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"quarantine.d.ts","sourceRoot":"","sources":["../../src/runtime/quarantine.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAEhE,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,eAAe,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,6FAA6F;AAC7F,wBAAgB,cAAc,IAAI,MAAM,CAMvC;AAED,0EAA0E;AAC1E,wBAAsB,aAAa,IAAI,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAQrE;AAED,oEAAoE;AACpE,wBAAsB,aAAa,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAIzE;AAED,yEAAyE;AACzE,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAMrD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quarantine.js","sourceRoot":"","sources":["../../src/runtime/quarantine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"quarantine.js","sourceRoot":"","sources":["../../src/runtime/quarantine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAoB7B,6FAA6F;AAC7F,MAAM,UAAU,cAAc;IAC5B,OAAO,IAAI,CAAC,IAAI,CACd,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,IAAI,EAAE;QACtC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,YAAY,CAAC,EACjD,iBAAiB,CAClB,CAAC;AACJ,CAAC;AAED,0EAA0E;AAC1E,MAAM,CAAC,KAAK,UAAU,aAAa;IACjC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,cAAc,EAAE,EAAE,MAAM,CAAC,CAAC;QACxD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAoB,CAAC;IAC5C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QAClE,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC;AAED,oEAAoE;AACpE,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,KAAsB;IACxD,MAAM,IAAI,GAAG,cAAc,EAAE,CAAC;IAC9B,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACxD,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;AACnE,CAAC;AAED,yEAAyE;AACzE,MAAM,CAAC,KAAK,UAAU,eAAe;IACnC,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC,CAAC;IACpC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;YAAE,MAAM,GAAG,CAAC;IAClE,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export type IntegrityAssertion = "conflict_markers" | "typecheck" | "version_regression" | "changelog_missing";
|
|
2
|
+
export interface IntegrityFailure {
|
|
3
|
+
assertion: IntegrityAssertion;
|
|
4
|
+
/** Human-readable, log-safe (no secrets) explanation of the failure. */
|
|
5
|
+
detail: string;
|
|
6
|
+
/** Offending tracked files, when the assertion is file-scoped. */
|
|
7
|
+
files?: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface ResolutionIntegrityResult {
|
|
10
|
+
ok: boolean;
|
|
11
|
+
/** Assertions that actually ran (version checks skip when nothing changed). */
|
|
12
|
+
checked: IntegrityAssertion[];
|
|
13
|
+
failures: IntegrityFailure[];
|
|
14
|
+
}
|
|
15
|
+
/** A package.json / CHANGELOG.md pair to version-check, relative to workdir. */
|
|
16
|
+
export interface VersionTarget {
|
|
17
|
+
packageJsonPath: string;
|
|
18
|
+
changelogPath: string;
|
|
19
|
+
}
|
|
20
|
+
export interface AssertResolutionOptions {
|
|
21
|
+
/**
|
|
22
|
+
* Base ref the resolution should not regress against (e.g. the integration
|
|
23
|
+
* branch). When omitted, or when the base file can't be read, the version
|
|
24
|
+
* assertion is skipped — never false-failed.
|
|
25
|
+
*/
|
|
26
|
+
baseRef?: string;
|
|
27
|
+
/** Typecheck command (defaults to `pnpm typecheck`). */
|
|
28
|
+
typecheckCmd?: string;
|
|
29
|
+
/**
|
|
30
|
+
* package.json / CHANGELOG.md pairs to version-check. Defaults to the repo
|
|
31
|
+
* root pair. The runner passes the acc-runner package pair (the #496 site).
|
|
32
|
+
*/
|
|
33
|
+
versionTargets?: VersionTarget[];
|
|
34
|
+
}
|
|
35
|
+
export interface ResolutionIntegrityDeps {
|
|
36
|
+
/** Tracked files still carrying a conflict marker. Default: `git grep`. */
|
|
37
|
+
findConflictMarkers?: (workdir: string) => Promise<string[]>;
|
|
38
|
+
/** Run the typecheck command. Default: spawn `pnpm typecheck` in workdir. */
|
|
39
|
+
runTypecheck?: (workdir: string, cmd: string) => Promise<{
|
|
40
|
+
ok: boolean;
|
|
41
|
+
output: string;
|
|
42
|
+
}>;
|
|
43
|
+
/** Resolved (post-resolution, on-disk) version for a target, or null. */
|
|
44
|
+
readResolvedVersion?: (workdir: string, target: VersionTarget) => Promise<string | null>;
|
|
45
|
+
/** Base-ref version for a target, or null when unreadable (→ skip check). */
|
|
46
|
+
readBaseVersion?: (workdir: string, baseRef: string, target: VersionTarget) => Promise<string | null>;
|
|
47
|
+
/** CHANGELOG.md text for a target, or null when absent. */
|
|
48
|
+
readChangelog?: (workdir: string, target: VersionTarget) => Promise<string | null>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Conflict-marker line shape: 7 of the same marker char at the start of a
|
|
52
|
+
* line. `<<<<<<<` and `>>>>>>>` are unambiguous; the `=======` divider is
|
|
53
|
+
* matched only as a whole line so a 7-equals markdown rule plus trailing text
|
|
54
|
+
* does not trip it.
|
|
55
|
+
*/
|
|
56
|
+
export declare const CONFLICT_MARKER_RE: RegExp;
|
|
57
|
+
export declare function textHasConflictMarker(text: string): boolean;
|
|
58
|
+
/** Semver compare (a-b sign); tolerant of pre-release/build suffixes. */
|
|
59
|
+
export declare function cmpSemver(a: string, b: string): number;
|
|
60
|
+
/**
|
|
61
|
+
* True when `changelog` carries an entry for `version`. The version must
|
|
62
|
+
* appear bounded by non-version characters so 0.14.0 does not match inside
|
|
63
|
+
* 0.14.00 or 10.14.0.
|
|
64
|
+
*/
|
|
65
|
+
export declare function changelogMentionsVersion(changelog: string, version: string): boolean;
|
|
66
|
+
export declare const DEFAULT_VERSION_TARGET: VersionTarget;
|
|
67
|
+
/**
|
|
68
|
+
* Assert that a conflict resolution in `workdir` is shippable. Returns a
|
|
69
|
+
* structured result; `ok === false` means the caller MUST reject the
|
|
70
|
+
* resolution (do not push, do not mark resolved).
|
|
71
|
+
*/
|
|
72
|
+
export declare function assertResolutionIntegrity(workdir: string, opts?: AssertResolutionOptions, deps?: ResolutionIntegrityDeps): Promise<ResolutionIntegrityResult>;
|
|
73
|
+
/**
|
|
74
|
+
* True when a task that reaches the worktree/push path is a conflict
|
|
75
|
+
* resolution and must clear the integrity guardrail. `conflict_resolution`
|
|
76
|
+
* task types are handled mechanically upstream and never reach the push path,
|
|
77
|
+
* so the live signal here is the title an operator/manager files when the
|
|
78
|
+
* mechanical resolver bails and a Claude session takes over the merge.
|
|
79
|
+
*/
|
|
80
|
+
export declare function isConflictResolutionTask(task: {
|
|
81
|
+
type?: string | null;
|
|
82
|
+
title?: string | null;
|
|
83
|
+
}): boolean;
|
|
84
|
+
/** One-line, log-safe summary of which assertions failed. */
|
|
85
|
+
export declare function summariseFailures(failures: IntegrityFailure[]): string;
|
|
86
|
+
//# sourceMappingURL=resolution-integrity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolution-integrity.d.ts","sourceRoot":"","sources":["../../src/runtime/resolution-integrity.ts"],"names":[],"mappings":"AAsCA,MAAM,MAAM,kBAAkB,GAC1B,kBAAkB,GAClB,WAAW,GACX,oBAAoB,GACpB,mBAAmB,CAAC;AAExB,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,kBAAkB,CAAC;IAC9B,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,OAAO,CAAC;IACZ,+EAA+E;IAC/E,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,QAAQ,EAAE,gBAAgB,EAAE,CAAC;CAC9B;AAED,gFAAgF;AAChF,MAAM,WAAW,aAAa;IAC5B,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wDAAwD;IACxD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,cAAc,CAAC,EAAE,aAAa,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,uBAAuB;IACtC,2EAA2E;IAC3E,mBAAmB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC7D,6EAA6E;IAC7E,YAAY,CAAC,EAAE,CACb,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,KACR,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9C,yEAAyE;IACzE,mBAAmB,CAAC,EAAE,CACpB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,aAAa,KAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC5B,6EAA6E;IAC7E,eAAe,CAAC,EAAE,CAChB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,aAAa,KAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC5B,2DAA2D;IAC3D,aAAa,CAAC,EAAE,CACd,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,aAAa,KAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC7B;AAMD;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,QAAsC,CAAC;AAEtE,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED,yEAAyE;AACzE,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAQtD;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,OAAO,CAGT;AAED,eAAO,MAAM,sBAAsB,EAAE,aAGpC,CAAC;AAqGF;;;;GAIG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,MAAM,EACf,IAAI,GAAE,uBAA4B,EAClC,IAAI,GAAE,uBAA4B,GACjC,OAAO,CAAC,yBAAyB,CAAC,CAgFpC;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE;IAC7C,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,GAAG,OAAO,CAIV;AAED,6DAA6D;AAC7D,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,MAAM,CAEtE"}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.57 T-57-2: Conflict-resolution integrity guardrail.
|
|
3
|
+
*
|
|
4
|
+
* A reusable post-resolution assertion the conflict-resolution path MUST pass
|
|
5
|
+
* before a resolution is declared complete / pushed. It exists because agent
|
|
6
|
+
* conflict resolutions in v0.55–v0.56 repeatedly shipped broken merges that
|
|
7
|
+
* were still marked "resolved":
|
|
8
|
+
* - spliced blocks left stale variable names (task-runner.ts) → CI typecheck
|
|
9
|
+
* failure that the runner never re-checked;
|
|
10
|
+
* - duplicate declarations (health.ts) → same;
|
|
11
|
+
* - a silently dropped version bump (#496 reverted package.json
|
|
12
|
+
* 0.14.0 → 0.13.0) → a version REGRESSION nobody noticed.
|
|
13
|
+
*
|
|
14
|
+
* The guardrail re-verifies the resolution from the runner's own vantage point
|
|
15
|
+
* (it never trusts the agent's self-reported gate results):
|
|
16
|
+
* 1. ZERO git conflict markers remain in any tracked file
|
|
17
|
+
* (<<<<<<< / ======= / >>>>>>>).
|
|
18
|
+
* 2. `pnpm typecheck` passes.
|
|
19
|
+
* 3. If a package.json "version" changed in the resolution, it only moved
|
|
20
|
+
* FORWARD (semver-greater than the base, never a regression) AND
|
|
21
|
+
* CHANGELOG.md carries a matching entry for the new version.
|
|
22
|
+
*
|
|
23
|
+
* On ANY failure the caller MUST reject the resolution: do not push, do not
|
|
24
|
+
* mark the task resolved; keep it actionable and emit a diagnostic naming the
|
|
25
|
+
* failed assertion. The default implementations shell out to `git` / `pnpm`,
|
|
26
|
+
* but every I/O boundary is an injectable dep so the three incident replays
|
|
27
|
+
* (version regression, stale-var typecheck fail, marker-left-behind) can be
|
|
28
|
+
* driven without a real worktree.
|
|
29
|
+
*/
|
|
30
|
+
import path from "node:path";
|
|
31
|
+
import fs from "node:fs/promises";
|
|
32
|
+
import { execa } from "execa";
|
|
33
|
+
import { buildEnv } from "../bin-resolve.js";
|
|
34
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
35
|
+
// Pure helpers (exported for direct unit coverage)
|
|
36
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
37
|
+
/**
|
|
38
|
+
* Conflict-marker line shape: 7 of the same marker char at the start of a
|
|
39
|
+
* line. `<<<<<<<` and `>>>>>>>` are unambiguous; the `=======` divider is
|
|
40
|
+
* matched only as a whole line so a 7-equals markdown rule plus trailing text
|
|
41
|
+
* does not trip it.
|
|
42
|
+
*/
|
|
43
|
+
export const CONFLICT_MARKER_RE = /^(?:<{7}|>{7})(?:$|\s)|^={7}\s*$/m;
|
|
44
|
+
export function textHasConflictMarker(text) {
|
|
45
|
+
return CONFLICT_MARKER_RE.test(text);
|
|
46
|
+
}
|
|
47
|
+
/** Semver compare (a-b sign); tolerant of pre-release/build suffixes. */
|
|
48
|
+
export function cmpSemver(a, b) {
|
|
49
|
+
const parts = (s) => s.split(/[.+-]/).map((x) => Number(x) || 0);
|
|
50
|
+
const [pa, pb] = [parts(a), parts(b)];
|
|
51
|
+
const len = Math.max(pa.length, pb.length);
|
|
52
|
+
for (let i = 0; i < len; i++) {
|
|
53
|
+
if ((pa[i] ?? 0) !== (pb[i] ?? 0))
|
|
54
|
+
return (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
55
|
+
}
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* True when `changelog` carries an entry for `version`. The version must
|
|
60
|
+
* appear bounded by non-version characters so 0.14.0 does not match inside
|
|
61
|
+
* 0.14.00 or 10.14.0.
|
|
62
|
+
*/
|
|
63
|
+
export function changelogMentionsVersion(changelog, version) {
|
|
64
|
+
const escaped = version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
65
|
+
return new RegExp(`(^|[^0-9.])${escaped}([^0-9.]|$)`, "m").test(changelog);
|
|
66
|
+
}
|
|
67
|
+
export const DEFAULT_VERSION_TARGET = {
|
|
68
|
+
packageJsonPath: "package.json",
|
|
69
|
+
changelogPath: "CHANGELOG.md",
|
|
70
|
+
};
|
|
71
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
72
|
+
// Default I/O implementations
|
|
73
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
74
|
+
async function defaultFindConflictMarkers(workdir) {
|
|
75
|
+
// `git grep` only scans tracked files. -I skips binaries, -l lists names.
|
|
76
|
+
// -E with the three marker forms; `=======` constrained to a lone line via
|
|
77
|
+
// the `\$` anchors git's POSIX-ERE understands.
|
|
78
|
+
const patterns = ["^<<<<<<<", "^>>>>>>>", "^=======$"];
|
|
79
|
+
const args = ["grep", "-I", "-l", "-E"];
|
|
80
|
+
for (let i = 0; i < patterns.length; i++) {
|
|
81
|
+
args.push(i === 0 ? "-e" : "-e", patterns[i]);
|
|
82
|
+
}
|
|
83
|
+
// `--` guards against a pattern being read as a pathspec.
|
|
84
|
+
args.push("--");
|
|
85
|
+
const { stdout, exitCode } = await execa("git", args, {
|
|
86
|
+
cwd: workdir,
|
|
87
|
+
env: buildEnv(),
|
|
88
|
+
reject: false,
|
|
89
|
+
});
|
|
90
|
+
// git grep exits 1 when there are no matches — that's the clean case.
|
|
91
|
+
if (exitCode === 1)
|
|
92
|
+
return [];
|
|
93
|
+
return stdout
|
|
94
|
+
.split(/\r?\n/)
|
|
95
|
+
.map((l) => l.trim())
|
|
96
|
+
.filter((l) => l.length > 0);
|
|
97
|
+
}
|
|
98
|
+
async function defaultRunTypecheck(workdir, cmd) {
|
|
99
|
+
const result = await execa(cmd, {
|
|
100
|
+
cwd: workdir,
|
|
101
|
+
env: buildEnv(),
|
|
102
|
+
shell: true,
|
|
103
|
+
reject: false,
|
|
104
|
+
});
|
|
105
|
+
const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim();
|
|
106
|
+
return { ok: result.exitCode === 0, output };
|
|
107
|
+
}
|
|
108
|
+
async function readJsonVersion(text) {
|
|
109
|
+
if (text === null)
|
|
110
|
+
return null;
|
|
111
|
+
try {
|
|
112
|
+
const v = JSON.parse(text).version;
|
|
113
|
+
return typeof v === "string" ? v : null;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function defaultReadResolvedVersion(workdir, target) {
|
|
120
|
+
try {
|
|
121
|
+
const text = await fs.readFile(path.join(workdir, target.packageJsonPath), "utf8");
|
|
122
|
+
return readJsonVersion(text);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function defaultReadBaseVersion(workdir, baseRef, target) {
|
|
129
|
+
try {
|
|
130
|
+
const { stdout, exitCode } = await execa("git", ["show", `${baseRef}:${target.packageJsonPath}`], { cwd: workdir, env: buildEnv(), reject: false });
|
|
131
|
+
if (exitCode !== 0)
|
|
132
|
+
return null;
|
|
133
|
+
return readJsonVersion(stdout);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function defaultReadChangelog(workdir, target) {
|
|
140
|
+
try {
|
|
141
|
+
return await fs.readFile(path.join(workdir, target.changelogPath), "utf8");
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
148
|
+
// The guardrail
|
|
149
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
150
|
+
const OUTPUT_TAIL = 2000;
|
|
151
|
+
/**
|
|
152
|
+
* Assert that a conflict resolution in `workdir` is shippable. Returns a
|
|
153
|
+
* structured result; `ok === false` means the caller MUST reject the
|
|
154
|
+
* resolution (do not push, do not mark resolved).
|
|
155
|
+
*/
|
|
156
|
+
export async function assertResolutionIntegrity(workdir, opts = {}, deps = {}) {
|
|
157
|
+
const findConflictMarkers = deps.findConflictMarkers ?? defaultFindConflictMarkers;
|
|
158
|
+
const runTypecheck = deps.runTypecheck ?? defaultRunTypecheck;
|
|
159
|
+
const readResolvedVersion = deps.readResolvedVersion ?? defaultReadResolvedVersion;
|
|
160
|
+
const readBaseVersion = deps.readBaseVersion ?? defaultReadBaseVersion;
|
|
161
|
+
const readChangelog = deps.readChangelog ?? defaultReadChangelog;
|
|
162
|
+
const typecheckCmd = opts.typecheckCmd?.trim() || "pnpm typecheck";
|
|
163
|
+
const targets = opts.versionTargets?.length
|
|
164
|
+
? opts.versionTargets
|
|
165
|
+
: [DEFAULT_VERSION_TARGET];
|
|
166
|
+
const checked = [];
|
|
167
|
+
const failures = [];
|
|
168
|
+
// ── 1. Conflict markers ────────────────────────────────────────────────────
|
|
169
|
+
checked.push("conflict_markers");
|
|
170
|
+
const markerFiles = await findConflictMarkers(workdir);
|
|
171
|
+
if (markerFiles.length > 0) {
|
|
172
|
+
failures.push({
|
|
173
|
+
assertion: "conflict_markers",
|
|
174
|
+
detail: `unresolved conflict markers remain in ${markerFiles.length} file(s): ${markerFiles.join(", ")}`,
|
|
175
|
+
files: markerFiles,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
// ── 2. Typecheck ────────────────────────────────────────────────────────────
|
|
179
|
+
checked.push("typecheck");
|
|
180
|
+
const tc = await runTypecheck(workdir, typecheckCmd);
|
|
181
|
+
if (!tc.ok) {
|
|
182
|
+
failures.push({
|
|
183
|
+
assertion: "typecheck",
|
|
184
|
+
detail: `\`${typecheckCmd}\` failed:\n${tc.output.slice(-OUTPUT_TAIL)}`,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
// ── 3. Version-forward + CHANGELOG ──────────────────────────────────────────
|
|
188
|
+
// Only meaningful when we have a base to compare against.
|
|
189
|
+
if (opts.baseRef) {
|
|
190
|
+
let ranVersionCheck = false;
|
|
191
|
+
let ranChangelogCheck = false;
|
|
192
|
+
for (const target of targets) {
|
|
193
|
+
const baseVersion = await readBaseVersion(workdir, opts.baseRef, target);
|
|
194
|
+
const resolvedVersion = await readResolvedVersion(workdir, target);
|
|
195
|
+
// Skip silently when either side is unreadable or the file is new on
|
|
196
|
+
// this side — there is nothing to regress.
|
|
197
|
+
if (baseVersion === null || resolvedVersion === null)
|
|
198
|
+
continue;
|
|
199
|
+
if (baseVersion === resolvedVersion)
|
|
200
|
+
continue; // no version change → no checks
|
|
201
|
+
ranVersionCheck = true;
|
|
202
|
+
if (cmpSemver(resolvedVersion, baseVersion) <= 0) {
|
|
203
|
+
failures.push({
|
|
204
|
+
assertion: "version_regression",
|
|
205
|
+
detail: `${target.packageJsonPath} version regressed ${baseVersion} → ${resolvedVersion} ` +
|
|
206
|
+
`(must move forward, never backward)`,
|
|
207
|
+
files: [target.packageJsonPath],
|
|
208
|
+
});
|
|
209
|
+
// A regressed version can't have a forward CHANGELOG entry to require.
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
// Version moved forward → a matching CHANGELOG entry is mandatory.
|
|
213
|
+
ranChangelogCheck = true;
|
|
214
|
+
const changelog = await readChangelog(workdir, target);
|
|
215
|
+
if (changelog === null || !changelogMentionsVersion(changelog, resolvedVersion)) {
|
|
216
|
+
failures.push({
|
|
217
|
+
assertion: "changelog_missing",
|
|
218
|
+
detail: `${target.packageJsonPath} bumped to ${resolvedVersion} but ` +
|
|
219
|
+
`${target.changelogPath} has no entry for it`,
|
|
220
|
+
files: [target.changelogPath],
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (ranVersionCheck)
|
|
225
|
+
checked.push("version_regression");
|
|
226
|
+
if (ranChangelogCheck)
|
|
227
|
+
checked.push("changelog_missing");
|
|
228
|
+
}
|
|
229
|
+
return { ok: failures.length === 0, checked, failures };
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* True when a task that reaches the worktree/push path is a conflict
|
|
233
|
+
* resolution and must clear the integrity guardrail. `conflict_resolution`
|
|
234
|
+
* task types are handled mechanically upstream and never reach the push path,
|
|
235
|
+
* so the live signal here is the title an operator/manager files when the
|
|
236
|
+
* mechanical resolver bails and a Claude session takes over the merge.
|
|
237
|
+
*/
|
|
238
|
+
export function isConflictResolutionTask(task) {
|
|
239
|
+
if (task.type === "conflict_resolution")
|
|
240
|
+
return true;
|
|
241
|
+
const title = (task.title ?? "").toLowerCase();
|
|
242
|
+
return /merge conflict|resolve\s+conflict|conflict\s+resolution/.test(title);
|
|
243
|
+
}
|
|
244
|
+
/** One-line, log-safe summary of which assertions failed. */
|
|
245
|
+
export function summariseFailures(failures) {
|
|
246
|
+
return failures.map((f) => f.assertion).join(", ");
|
|
247
|
+
}
|
|
248
|
+
//# sourceMappingURL=resolution-integrity.js.map
|