@tokenfactory/acc-runner 0.27.0 → 0.27.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/anthropic-auth.d.ts +53 -0
- package/dist/anthropic-auth.d.ts.map +1 -0
- package/dist/anthropic-auth.js +89 -0
- package/dist/anthropic-auth.js.map +1 -0
- package/dist/bin-resolve.d.ts +16 -0
- package/dist/bin-resolve.d.ts.map +1 -0
- package/dist/bin-resolve.js +35 -0
- package/dist/bin-resolve.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +128 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +121 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +396 -0
- package/dist/config.js.map +1 -0
- package/dist/cost-pricing.d.ts +130 -0
- package/dist/cost-pricing.d.ts.map +1 -0
- package/dist/cost-pricing.js +191 -0
- package/dist/cost-pricing.js.map +1 -0
- package/dist/doctor.d.ts +49 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +776 -0
- package/dist/doctor.js.map +1 -0
- package/dist/failure-classifier.d.ts +112 -0
- package/dist/failure-classifier.d.ts.map +1 -0
- package/dist/failure-classifier.js +353 -0
- package/dist/failure-classifier.js.map +1 -0
- package/dist/gh.d.ts +22 -0
- package/dist/gh.d.ts.map +1 -0
- package/dist/gh.js +48 -0
- package/dist/gh.js.map +1 -0
- package/dist/git.d.ts +50 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +127 -0
- package/dist/git.js.map +1 -0
- 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/keychain.d.ts +21 -0
- package/dist/keychain.d.ts.map +1 -0
- package/dist/keychain.js +45 -0
- package/dist/keychain.js.map +1 -0
- package/dist/login.d.ts +12 -0
- package/dist/login.d.ts.map +1 -0
- package/dist/login.js +133 -0
- package/dist/login.js.map +1 -0
- package/dist/logout.d.ts +2 -0
- package/dist/logout.d.ts.map +1 -0
- package/dist/logout.js +31 -0
- package/dist/logout.js.map +1 -0
- package/dist/mcp-spawn.d.ts +30 -0
- package/dist/mcp-spawn.d.ts.map +1 -0
- package/dist/mcp-spawn.js +145 -0
- package/dist/mcp-spawn.js.map +1 -0
- package/dist/messaging.d.ts +49 -0
- package/dist/messaging.d.ts.map +1 -0
- package/dist/messaging.js +36 -0
- package/dist/messaging.js.map +1 -0
- package/dist/pkg-version.d.ts +3 -0
- package/dist/pkg-version.d.ts.map +1 -0
- package/dist/pkg-version.js +20 -0
- package/dist/pkg-version.js.map +1 -0
- package/dist/profiles/designer-prompt.d.ts +18 -0
- package/dist/profiles/designer-prompt.d.ts.map +1 -0
- package/dist/profiles/designer-prompt.js +172 -0
- package/dist/profiles/designer-prompt.js.map +1 -0
- package/dist/profiles/developer-prompt.d.ts +24 -0
- package/dist/profiles/developer-prompt.d.ts.map +1 -0
- package/dist/profiles/developer-prompt.js +24 -0
- package/dist/profiles/developer-prompt.js.map +1 -0
- package/dist/profiles/manager-prompt.d.ts +34 -0
- package/dist/profiles/manager-prompt.d.ts.map +1 -0
- package/dist/profiles/manager-prompt.js +93 -0
- package/dist/profiles/manager-prompt.js.map +1 -0
- package/dist/profiles/tester-prompt.d.ts +28 -0
- package/dist/profiles/tester-prompt.d.ts.map +1 -0
- package/dist/profiles/tester-prompt.js +165 -0
- package/dist/profiles/tester-prompt.js.map +1 -0
- package/dist/prompt.d.ts +38 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +78 -0
- package/dist/prompt.js.map +1 -0
- package/dist/runtime/cache-dir.d.ts +10 -0
- package/dist/runtime/cache-dir.d.ts.map +1 -0
- package/dist/runtime/cache-dir.js +42 -0
- package/dist/runtime/cache-dir.js.map +1 -0
- package/dist/runtime/conflict-resolver.d.ts +65 -0
- package/dist/runtime/conflict-resolver.d.ts.map +1 -0
- package/dist/runtime/conflict-resolver.js +477 -0
- package/dist/runtime/conflict-resolver.js.map +1 -0
- package/dist/runtime/expand-args.d.ts +28 -0
- package/dist/runtime/expand-args.d.ts.map +1 -0
- package/dist/runtime/expand-args.js +50 -0
- package/dist/runtime/expand-args.js.map +1 -0
- package/dist/runtime/locks.d.ts +21 -0
- package/dist/runtime/locks.d.ts.map +1 -0
- package/dist/runtime/locks.js +103 -0
- package/dist/runtime/locks.js.map +1 -0
- package/dist/runtime/provision-mutex.d.ts +37 -0
- package/dist/runtime/provision-mutex.d.ts.map +1 -0
- package/dist/runtime/provision-mutex.js +67 -0
- package/dist/runtime/provision-mutex.js.map +1 -0
- package/dist/runtime/quarantine.d.ts +30 -0
- package/dist/runtime/quarantine.d.ts.map +1 -0
- package/dist/runtime/quarantine.js +53 -0
- package/dist/runtime/quarantine.js.map +1 -0
- 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 +81 -0
- package/dist/runtime/reviewer.d.ts.map +1 -0
- package/dist/runtime/reviewer.js +374 -0
- package/dist/runtime/reviewer.js.map +1 -0
- package/dist/runtime/rework.d.ts +48 -0
- package/dist/runtime/rework.d.ts.map +1 -0
- package/dist/runtime/rework.js +136 -0
- package/dist/runtime/rework.js.map +1 -0
- package/dist/runtime/singleton.d.ts +66 -0
- package/dist/runtime/singleton.d.ts.map +1 -0
- package/dist/runtime/singleton.js +227 -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/runtime/worktree.d.ts +74 -0
- package/dist/runtime/worktree.d.ts.map +1 -0
- package/dist/runtime/worktree.js +211 -0
- package/dist/runtime/worktree.js.map +1 -0
- package/dist/secrets/inject.d.ts +70 -0
- package/dist/secrets/inject.d.ts.map +1 -0
- package/dist/secrets/inject.js +102 -0
- package/dist/secrets/inject.js.map +1 -0
- package/dist/supabase.d.ts +4 -0
- package/dist/supabase.d.ts.map +1 -0
- package/dist/supabase.js +36 -0
- package/dist/supabase.js.map +1 -0
- package/dist/task-runner.d.ts +313 -0
- package/dist/task-runner.d.ts.map +1 -0
- package/dist/task-runner.js +1766 -0
- package/dist/task-runner.js.map +1 -0
- package/dist/token-provider.d.ts +50 -0
- package/dist/token-provider.d.ts.map +1 -0
- package/dist/token-provider.js +177 -0
- package/dist/token-provider.js.map +1 -0
- package/dist/types.d.ts +120 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/version-check.d.ts +17 -0
- package/dist/version-check.d.ts.map +1 -0
- package/dist/version-check.js +56 -0
- package/dist/version-check.js.map +1 -0
- package/dist/watch.d.ts +296 -0
- package/dist/watch.d.ts.map +1 -0
- package/dist/watch.js +1570 -0
- package/dist/watch.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local PID lockfile at `<cache>/locks/<task_id>.pid` so two same-machine
|
|
3
|
+
* runner processes can't both enter the same task's worktree. Held for the
|
|
4
|
+
* task lifetime, released in a finally.
|
|
5
|
+
*
|
|
6
|
+
* v1.00-B: the cache root resolves through `cacheDir()` (which honours
|
|
7
|
+
* ACC_RUNNER_CACHE_DIR), not a hardcoded ~/.cache path. This is what lets
|
|
8
|
+
* two ephemeral runners on one host stay isolated — distinct
|
|
9
|
+
* ACC_RUNNER_CACHE_DIR values give each its own task-lock namespace, so
|
|
10
|
+
* they never collide on a shared `locks/` dir.
|
|
11
|
+
*/
|
|
12
|
+
import fs from "node:fs/promises";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { cacheDir } from "./cache-dir.js";
|
|
15
|
+
// Lazy so tests can override ACC_RUNNER_CACHE_DIR / HOME between cases.
|
|
16
|
+
export function locksDir() {
|
|
17
|
+
return path.join(cacheDir(), "locks");
|
|
18
|
+
}
|
|
19
|
+
export class TaskLockHeldError extends Error {
|
|
20
|
+
taskId;
|
|
21
|
+
heldByPid;
|
|
22
|
+
constructor(taskId, heldByPid) {
|
|
23
|
+
super(`task ${taskId} lock held by pid ${heldByPid}`);
|
|
24
|
+
this.taskId = taskId;
|
|
25
|
+
this.heldByPid = heldByPid;
|
|
26
|
+
this.name = "TaskLockHeldError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function lockPath(taskId) {
|
|
30
|
+
return path.join(locksDir(), `${taskId}.pid`);
|
|
31
|
+
}
|
|
32
|
+
export function isProcessAlive(pid) {
|
|
33
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
34
|
+
return false;
|
|
35
|
+
try {
|
|
36
|
+
process.kill(pid, 0);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
// EPERM = exists but unsignalable — still alive.
|
|
41
|
+
return err.code === "EPERM";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function readPid(file) {
|
|
45
|
+
try {
|
|
46
|
+
const text = await fs.readFile(file, "utf8");
|
|
47
|
+
const pid = Number.parseInt(text.trim(), 10);
|
|
48
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
if (err.code === "ENOENT")
|
|
52
|
+
return null;
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export async function acquireTaskLock(taskId, pid = process.pid) {
|
|
57
|
+
await fs.mkdir(locksDir(), { recursive: true });
|
|
58
|
+
const file = lockPath(taskId);
|
|
59
|
+
const existing = await readPid(file);
|
|
60
|
+
if (existing !== null && existing !== pid && isProcessAlive(existing)) {
|
|
61
|
+
throw new TaskLockHeldError(taskId, existing);
|
|
62
|
+
}
|
|
63
|
+
await fs.writeFile(file, `${pid}\n`, "utf8");
|
|
64
|
+
return {
|
|
65
|
+
taskId,
|
|
66
|
+
pidFile: file,
|
|
67
|
+
async release() {
|
|
68
|
+
try {
|
|
69
|
+
// Only delete if we still own it — avoids stomping a recovered lock.
|
|
70
|
+
const current = await readPid(file);
|
|
71
|
+
if (current === pid)
|
|
72
|
+
await fs.unlink(file);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if (err.code !== "ENOENT")
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export async function listStaleLocks() {
|
|
82
|
+
let entries;
|
|
83
|
+
try {
|
|
84
|
+
entries = await fs.readdir(locksDir());
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
if (err.code === "ENOENT")
|
|
88
|
+
return [];
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
const stale = [];
|
|
92
|
+
for (const name of entries) {
|
|
93
|
+
if (!name.endsWith(".pid"))
|
|
94
|
+
continue;
|
|
95
|
+
const file = path.join(locksDir(), name);
|
|
96
|
+
const pid = await readPid(file);
|
|
97
|
+
if (pid === null || !isProcessAlive(pid)) {
|
|
98
|
+
stale.push({ taskId: name.slice(0, -4), pidFile: file, pid });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return stale;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=locks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locks.js","sourceRoot":"","sources":["../../src/runtime/locks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.21 T-66-1 — in-process provisioning mutex.
|
|
3
|
+
*
|
|
4
|
+
* The runner runs up to `concurrencyLimit` tasks at once, but the per-task
|
|
5
|
+
* worktree-provisioning critical section (`git fetch` + `git worktree
|
|
6
|
+
* remove`/prune + `git worktree add`) all targets the SAME shared clone.
|
|
7
|
+
* Two of those running concurrently collide on git's repo-global locks
|
|
8
|
+
* (`.git/index.lock`, `HEAD.lock`, `packed-refs.lock`, `.git/worktrees/…`),
|
|
9
|
+
* so the loser throws a git error in the pre-Claude phase and the task dies
|
|
10
|
+
* (this is how T-65-2 failed in ~5s beside T-65-1; see
|
|
11
|
+
* planning/T65-2_CONCURRENT_DISPATCH_ROOTCAUSE.md).
|
|
12
|
+
*
|
|
13
|
+
* `withProvisionLock` serializes ONLY that critical section, keyed by the
|
|
14
|
+
* shared clone path. The long phases (the Claude run and the push) stay
|
|
15
|
+
* concurrent up to `concurrencyLimit`, so the throughput benefit is
|
|
16
|
+
* preserved — only the few seconds of git plumbing serialize. Serializing
|
|
17
|
+
* the section also makes the v0.37-A stale-lock guard (`clearGitLocks`)
|
|
18
|
+
* safe: it can no longer delete a `.git/*.lock` that a concurrent provision
|
|
19
|
+
* is actively holding, because no concurrent provision against the same
|
|
20
|
+
* clone can be in flight.
|
|
21
|
+
*
|
|
22
|
+
* The lock is a per-key promise chain: each caller appends its critical
|
|
23
|
+
* section to the tail of the chain for its key and awaits the prior tail.
|
|
24
|
+
* The chain advances in a `finally`, so a thrown critical section releases
|
|
25
|
+
* the lock for the next waiter (no deadlock). Keys whose chain has drained
|
|
26
|
+
* are dropped from the map so it can't grow unbounded.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Run `fn` while holding the provisioning lock for `key` (the shared clone
|
|
30
|
+
* path). Callers for the same key run strictly one-at-a-time, in arrival
|
|
31
|
+
* order; callers for different keys never block each other. The resolved
|
|
32
|
+
* value (or thrown error) of `fn` is passed straight through to the caller.
|
|
33
|
+
*/
|
|
34
|
+
export declare function withProvisionLock<T>(key: string, fn: () => Promise<T>): Promise<T>;
|
|
35
|
+
/** Test/diagnostic helper: number of clone keys with a live or pending chain. */
|
|
36
|
+
export declare function activeProvisionKeys(): number;
|
|
37
|
+
//# sourceMappingURL=provision-mutex.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provision-mutex.d.ts","sourceRoot":"","sources":["../../src/runtime/provision-mutex.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAMH;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,GAAG,EAAE,MAAM,EACX,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CA2BZ;AAED,iFAAiF;AACjF,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.21 T-66-1 — in-process provisioning mutex.
|
|
3
|
+
*
|
|
4
|
+
* The runner runs up to `concurrencyLimit` tasks at once, but the per-task
|
|
5
|
+
* worktree-provisioning critical section (`git fetch` + `git worktree
|
|
6
|
+
* remove`/prune + `git worktree add`) all targets the SAME shared clone.
|
|
7
|
+
* Two of those running concurrently collide on git's repo-global locks
|
|
8
|
+
* (`.git/index.lock`, `HEAD.lock`, `packed-refs.lock`, `.git/worktrees/…`),
|
|
9
|
+
* so the loser throws a git error in the pre-Claude phase and the task dies
|
|
10
|
+
* (this is how T-65-2 failed in ~5s beside T-65-1; see
|
|
11
|
+
* planning/T65-2_CONCURRENT_DISPATCH_ROOTCAUSE.md).
|
|
12
|
+
*
|
|
13
|
+
* `withProvisionLock` serializes ONLY that critical section, keyed by the
|
|
14
|
+
* shared clone path. The long phases (the Claude run and the push) stay
|
|
15
|
+
* concurrent up to `concurrencyLimit`, so the throughput benefit is
|
|
16
|
+
* preserved — only the few seconds of git plumbing serialize. Serializing
|
|
17
|
+
* the section also makes the v0.37-A stale-lock guard (`clearGitLocks`)
|
|
18
|
+
* safe: it can no longer delete a `.git/*.lock` that a concurrent provision
|
|
19
|
+
* is actively holding, because no concurrent provision against the same
|
|
20
|
+
* clone can be in flight.
|
|
21
|
+
*
|
|
22
|
+
* The lock is a per-key promise chain: each caller appends its critical
|
|
23
|
+
* section to the tail of the chain for its key and awaits the prior tail.
|
|
24
|
+
* The chain advances in a `finally`, so a thrown critical section releases
|
|
25
|
+
* the lock for the next waiter (no deadlock). Keys whose chain has drained
|
|
26
|
+
* are dropped from the map so it can't grow unbounded.
|
|
27
|
+
*/
|
|
28
|
+
// Per-key tail of the run queue. Resolves when the current holder's critical
|
|
29
|
+
// section (and its release bookkeeping) has settled.
|
|
30
|
+
const chains = new Map();
|
|
31
|
+
/**
|
|
32
|
+
* Run `fn` while holding the provisioning lock for `key` (the shared clone
|
|
33
|
+
* path). Callers for the same key run strictly one-at-a-time, in arrival
|
|
34
|
+
* order; callers for different keys never block each other. The resolved
|
|
35
|
+
* value (or thrown error) of `fn` is passed straight through to the caller.
|
|
36
|
+
*/
|
|
37
|
+
export async function withProvisionLock(key, fn) {
|
|
38
|
+
// The predecessor we must wait behind (resolved/rejected — we don't care
|
|
39
|
+
// which; we only need it to have finished touching the clone).
|
|
40
|
+
const prior = chains.get(key) ?? Promise.resolve();
|
|
41
|
+
// Our gate resolves when WE are done, letting the next waiter proceed.
|
|
42
|
+
let release;
|
|
43
|
+
const ours = new Promise((resolve) => {
|
|
44
|
+
release = resolve;
|
|
45
|
+
});
|
|
46
|
+
// Append ourselves to the tail BEFORE awaiting, so a third caller queues
|
|
47
|
+
// behind us, not behind our predecessor.
|
|
48
|
+
chains.set(key, ours);
|
|
49
|
+
// Wait for the predecessor to finish. `.catch` so a predecessor that threw
|
|
50
|
+
// never rejects our wait — its failure is its own caller's problem.
|
|
51
|
+
await prior.catch(() => undefined);
|
|
52
|
+
try {
|
|
53
|
+
return await fn();
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
release();
|
|
57
|
+
// Drop the key once our gate is the current tail (no one queued behind
|
|
58
|
+
// us), so the map doesn't accumulate dead entries over the runner's life.
|
|
59
|
+
if (chains.get(key) === ours)
|
|
60
|
+
chains.delete(key);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Test/diagnostic helper: number of clone keys with a live or pending chain. */
|
|
64
|
+
export function activeProvisionKeys() {
|
|
65
|
+
return chains.size;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=provision-mutex.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provision-mutex.js","sourceRoot":"","sources":["../../src/runtime/provision-mutex.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,6EAA6E;AAC7E,qDAAqD;AACrD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAyB,CAAC;AAEhD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,GAAW,EACX,EAAoB;IAEpB,yEAAyE;IACzE,+DAA+D;IAC/D,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;IAEnD,uEAAuE;IACvE,IAAI,OAAoB,CAAC;IACzB,MAAM,IAAI,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QACzC,OAAO,GAAG,OAAO,CAAC;IACpB,CAAC,CAAC,CAAC;IAEH,yEAAyE;IACzE,yCAAyC;IACzC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAEtB,2EAA2E;IAC3E,oEAAoE;IACpE,MAAM,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IAEnC,IAAI,CAAC;QACH,OAAO,MAAM,EAAE,EAAE,CAAC;IACpB,CAAC;YAAS,CAAC;QACT,OAAO,EAAE,CAAC;QACV,uEAAuE;QACvE,0EAA0E;QAC1E,IAAI,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,IAAI;YAAE,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACnD,CAAC;AACH,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,mBAAmB;IACjC,OAAO,MAAM,CAAC,IAAI,CAAC;AACrB,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ClaudeExitClass } from "../failure-classifier.js";
|
|
2
|
+
export interface QuarantineState {
|
|
3
|
+
cause: ClaudeExitClass;
|
|
4
|
+
classifiedAt: string;
|
|
5
|
+
taskId: string;
|
|
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;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Path to the quarantine sentinel file. Resolves through the shared
|
|
20
|
+
* `cacheDir()` (honours ACC_RUNNER_CACHE_DIR), so a per-runner cache dir
|
|
21
|
+
* gives each ephemeral runner its own quarantine state.
|
|
22
|
+
*/
|
|
23
|
+
export declare function quarantineFile(): string;
|
|
24
|
+
/** Returns the current quarantine state, or null when not quarantined. */
|
|
25
|
+
export declare function getQuarantine(): Promise<QuarantineState | null>;
|
|
26
|
+
/** Write the quarantine sentinel. Creates parent dirs if needed. */
|
|
27
|
+
export declare function setQuarantine(state: QuarantineState): Promise<void>;
|
|
28
|
+
/** Remove the quarantine sentinel. Safe to call when not quarantined. */
|
|
29
|
+
export declare function clearQuarantine(): Promise<void>;
|
|
30
|
+
//# sourceMappingURL=quarantine.d.ts.map
|
|
@@ -0,0 +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;IACf;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAEvC;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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.48: per-runner quarantine state. Written to
|
|
3
|
+
* ~/.cache/acc-runner/quarantine.json by the watch loop when the failure
|
|
4
|
+
* classifier flags a machine-level failure (usage_limit | auth_expired |
|
|
5
|
+
* env_broken). The presence of this file blocks pump() from claiming
|
|
6
|
+
* new tasks so no further work is burned.
|
|
7
|
+
*
|
|
8
|
+
* Clearing the quarantine (after the operator resolves the underlying issue):
|
|
9
|
+
* - Delete the file manually, OR
|
|
10
|
+
* - Run `acc-runner quarantine clear` (wired in cli.ts).
|
|
11
|
+
*
|
|
12
|
+
* `acc-runner doctor` surfaces the quarantine state and its cause.
|
|
13
|
+
*/
|
|
14
|
+
import fs from "node:fs/promises";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { cacheDir } from "./cache-dir.js";
|
|
17
|
+
/**
|
|
18
|
+
* Path to the quarantine sentinel file. Resolves through the shared
|
|
19
|
+
* `cacheDir()` (honours ACC_RUNNER_CACHE_DIR), so a per-runner cache dir
|
|
20
|
+
* gives each ephemeral runner its own quarantine state.
|
|
21
|
+
*/
|
|
22
|
+
export function quarantineFile() {
|
|
23
|
+
return path.join(cacheDir(), "quarantine.json");
|
|
24
|
+
}
|
|
25
|
+
/** Returns the current quarantine state, or null when not quarantined. */
|
|
26
|
+
export async function getQuarantine() {
|
|
27
|
+
try {
|
|
28
|
+
const raw = await fs.readFile(quarantineFile(), "utf8");
|
|
29
|
+
return JSON.parse(raw);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
if (err.code === "ENOENT")
|
|
33
|
+
return null;
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Write the quarantine sentinel. Creates parent dirs if needed. */
|
|
38
|
+
export async function setQuarantine(state) {
|
|
39
|
+
const file = quarantineFile();
|
|
40
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
41
|
+
await fs.writeFile(file, JSON.stringify(state, null, 2), "utf8");
|
|
42
|
+
}
|
|
43
|
+
/** Remove the quarantine sentinel. Safe to call when not quarantined. */
|
|
44
|
+
export async function clearQuarantine() {
|
|
45
|
+
try {
|
|
46
|
+
await fs.unlink(quarantineFile());
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
if (err.code !== "ENOENT")
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=quarantine.js.map
|
|
@@ -0,0 +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,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAoB1C;;;;GAIG;AACH,MAAM,UAAU,cAAc;IAC5B,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,iBAAiB,CAAC,CAAC;AAClD,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
|