@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.
Files changed (161) hide show
  1. package/dist/anthropic-auth.d.ts +53 -0
  2. package/dist/anthropic-auth.d.ts.map +1 -0
  3. package/dist/anthropic-auth.js +89 -0
  4. package/dist/anthropic-auth.js.map +1 -0
  5. package/dist/bin-resolve.d.ts +16 -0
  6. package/dist/bin-resolve.d.ts.map +1 -0
  7. package/dist/bin-resolve.js +35 -0
  8. package/dist/bin-resolve.js.map +1 -0
  9. package/dist/cli.d.ts +3 -0
  10. package/dist/cli.d.ts.map +1 -0
  11. package/dist/cli.js +128 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/config.d.ts +121 -0
  14. package/dist/config.d.ts.map +1 -0
  15. package/dist/config.js +396 -0
  16. package/dist/config.js.map +1 -0
  17. package/dist/cost-pricing.d.ts +130 -0
  18. package/dist/cost-pricing.d.ts.map +1 -0
  19. package/dist/cost-pricing.js +191 -0
  20. package/dist/cost-pricing.js.map +1 -0
  21. package/dist/doctor.d.ts +49 -0
  22. package/dist/doctor.d.ts.map +1 -0
  23. package/dist/doctor.js +776 -0
  24. package/dist/doctor.js.map +1 -0
  25. package/dist/failure-classifier.d.ts +112 -0
  26. package/dist/failure-classifier.d.ts.map +1 -0
  27. package/dist/failure-classifier.js +353 -0
  28. package/dist/failure-classifier.js.map +1 -0
  29. package/dist/gh.d.ts +22 -0
  30. package/dist/gh.d.ts.map +1 -0
  31. package/dist/gh.js +48 -0
  32. package/dist/gh.js.map +1 -0
  33. package/dist/git.d.ts +50 -0
  34. package/dist/git.d.ts.map +1 -0
  35. package/dist/git.js +127 -0
  36. package/dist/git.js.map +1 -0
  37. package/dist/github-client.d.ts +133 -0
  38. package/dist/github-client.d.ts.map +1 -0
  39. package/dist/github-client.js +234 -0
  40. package/dist/github-client.js.map +1 -0
  41. package/dist/keychain.d.ts +21 -0
  42. package/dist/keychain.d.ts.map +1 -0
  43. package/dist/keychain.js +45 -0
  44. package/dist/keychain.js.map +1 -0
  45. package/dist/login.d.ts +12 -0
  46. package/dist/login.d.ts.map +1 -0
  47. package/dist/login.js +133 -0
  48. package/dist/login.js.map +1 -0
  49. package/dist/logout.d.ts +2 -0
  50. package/dist/logout.d.ts.map +1 -0
  51. package/dist/logout.js +31 -0
  52. package/dist/logout.js.map +1 -0
  53. package/dist/mcp-spawn.d.ts +30 -0
  54. package/dist/mcp-spawn.d.ts.map +1 -0
  55. package/dist/mcp-spawn.js +145 -0
  56. package/dist/mcp-spawn.js.map +1 -0
  57. package/dist/messaging.d.ts +49 -0
  58. package/dist/messaging.d.ts.map +1 -0
  59. package/dist/messaging.js +36 -0
  60. package/dist/messaging.js.map +1 -0
  61. package/dist/pkg-version.d.ts +3 -0
  62. package/dist/pkg-version.d.ts.map +1 -0
  63. package/dist/pkg-version.js +20 -0
  64. package/dist/pkg-version.js.map +1 -0
  65. package/dist/profiles/designer-prompt.d.ts +18 -0
  66. package/dist/profiles/designer-prompt.d.ts.map +1 -0
  67. package/dist/profiles/designer-prompt.js +172 -0
  68. package/dist/profiles/designer-prompt.js.map +1 -0
  69. package/dist/profiles/developer-prompt.d.ts +24 -0
  70. package/dist/profiles/developer-prompt.d.ts.map +1 -0
  71. package/dist/profiles/developer-prompt.js +24 -0
  72. package/dist/profiles/developer-prompt.js.map +1 -0
  73. package/dist/profiles/manager-prompt.d.ts +34 -0
  74. package/dist/profiles/manager-prompt.d.ts.map +1 -0
  75. package/dist/profiles/manager-prompt.js +93 -0
  76. package/dist/profiles/manager-prompt.js.map +1 -0
  77. package/dist/profiles/tester-prompt.d.ts +28 -0
  78. package/dist/profiles/tester-prompt.d.ts.map +1 -0
  79. package/dist/profiles/tester-prompt.js +165 -0
  80. package/dist/profiles/tester-prompt.js.map +1 -0
  81. package/dist/prompt.d.ts +38 -0
  82. package/dist/prompt.d.ts.map +1 -0
  83. package/dist/prompt.js +78 -0
  84. package/dist/prompt.js.map +1 -0
  85. package/dist/runtime/cache-dir.d.ts +10 -0
  86. package/dist/runtime/cache-dir.d.ts.map +1 -0
  87. package/dist/runtime/cache-dir.js +42 -0
  88. package/dist/runtime/cache-dir.js.map +1 -0
  89. package/dist/runtime/conflict-resolver.d.ts +65 -0
  90. package/dist/runtime/conflict-resolver.d.ts.map +1 -0
  91. package/dist/runtime/conflict-resolver.js +477 -0
  92. package/dist/runtime/conflict-resolver.js.map +1 -0
  93. package/dist/runtime/expand-args.d.ts +28 -0
  94. package/dist/runtime/expand-args.d.ts.map +1 -0
  95. package/dist/runtime/expand-args.js +50 -0
  96. package/dist/runtime/expand-args.js.map +1 -0
  97. package/dist/runtime/locks.d.ts +21 -0
  98. package/dist/runtime/locks.d.ts.map +1 -0
  99. package/dist/runtime/locks.js +103 -0
  100. package/dist/runtime/locks.js.map +1 -0
  101. package/dist/runtime/provision-mutex.d.ts +37 -0
  102. package/dist/runtime/provision-mutex.d.ts.map +1 -0
  103. package/dist/runtime/provision-mutex.js +67 -0
  104. package/dist/runtime/provision-mutex.js.map +1 -0
  105. package/dist/runtime/quarantine.d.ts +30 -0
  106. package/dist/runtime/quarantine.d.ts.map +1 -0
  107. package/dist/runtime/quarantine.js +53 -0
  108. package/dist/runtime/quarantine.js.map +1 -0
  109. package/dist/runtime/resolution-integrity.d.ts +86 -0
  110. package/dist/runtime/resolution-integrity.d.ts.map +1 -0
  111. package/dist/runtime/resolution-integrity.js +248 -0
  112. package/dist/runtime/resolution-integrity.js.map +1 -0
  113. package/dist/runtime/reviewer.d.ts +81 -0
  114. package/dist/runtime/reviewer.d.ts.map +1 -0
  115. package/dist/runtime/reviewer.js +374 -0
  116. package/dist/runtime/reviewer.js.map +1 -0
  117. package/dist/runtime/rework.d.ts +48 -0
  118. package/dist/runtime/rework.d.ts.map +1 -0
  119. package/dist/runtime/rework.js +136 -0
  120. package/dist/runtime/rework.js.map +1 -0
  121. package/dist/runtime/singleton.d.ts +66 -0
  122. package/dist/runtime/singleton.d.ts.map +1 -0
  123. package/dist/runtime/singleton.js +227 -0
  124. package/dist/runtime/singleton.js.map +1 -0
  125. package/dist/runtime/version-drift.d.ts +31 -0
  126. package/dist/runtime/version-drift.d.ts.map +1 -0
  127. package/dist/runtime/version-drift.js +114 -0
  128. package/dist/runtime/version-drift.js.map +1 -0
  129. package/dist/runtime/worktree.d.ts +74 -0
  130. package/dist/runtime/worktree.d.ts.map +1 -0
  131. package/dist/runtime/worktree.js +211 -0
  132. package/dist/runtime/worktree.js.map +1 -0
  133. package/dist/secrets/inject.d.ts +70 -0
  134. package/dist/secrets/inject.d.ts.map +1 -0
  135. package/dist/secrets/inject.js +102 -0
  136. package/dist/secrets/inject.js.map +1 -0
  137. package/dist/supabase.d.ts +4 -0
  138. package/dist/supabase.d.ts.map +1 -0
  139. package/dist/supabase.js +36 -0
  140. package/dist/supabase.js.map +1 -0
  141. package/dist/task-runner.d.ts +313 -0
  142. package/dist/task-runner.d.ts.map +1 -0
  143. package/dist/task-runner.js +1766 -0
  144. package/dist/task-runner.js.map +1 -0
  145. package/dist/token-provider.d.ts +50 -0
  146. package/dist/token-provider.d.ts.map +1 -0
  147. package/dist/token-provider.js +177 -0
  148. package/dist/token-provider.js.map +1 -0
  149. package/dist/types.d.ts +120 -0
  150. package/dist/types.d.ts.map +1 -0
  151. package/dist/types.js +16 -0
  152. package/dist/types.js.map +1 -0
  153. package/dist/version-check.d.ts +17 -0
  154. package/dist/version-check.d.ts.map +1 -0
  155. package/dist/version-check.js +56 -0
  156. package/dist/version-check.js.map +1 -0
  157. package/dist/watch.d.ts +296 -0
  158. package/dist/watch.d.ts.map +1 -0
  159. package/dist/watch.js +1570 -0
  160. package/dist/watch.js.map +1 -0
  161. 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