@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,136 @@
1
+ /**
2
+ * v0.51 T-51-1 (G2): rework task support — sentinel parsing + prompt.
3
+ *
4
+ * A `rework` task is created by acc.create_rework_task (migration 0172)
5
+ * when a reviewer rejects a PR with confidence < 0.9. Unlike
6
+ * conflict_resolution tasks (resolved via the GitHub API, no Claude),
7
+ * rework tasks run the normal Claude session — but against the PR's
8
+ * EXISTING head branch: task-runner.ts provisions the worktree from
9
+ * `origin/<branch>` instead of forking from the integration branch,
10
+ * pushes back to the same branch (same PR), and never opens a new PR.
11
+ *
12
+ * The task description carries the reviewer's verdict in a sentinel the
13
+ * server embeds (mirrors the acc-conflict sentinel from T-48-5):
14
+ * <!-- acc-rework: {"pr_number":N,"repo":"o/r","branch":"...",
15
+ * "origin_task_id":"T-...","verdict":"reject","confidence":0.72,
16
+ * "comments":["..."],"cycle":1} -->
17
+ */
18
+ import { execa } from "execa";
19
+ import { buildEnv } from "../bin-resolve.js";
20
+ /** Mirrors the bound enforced inside acc.create_rework_task. */
21
+ export const MAX_REWORK_CYCLES = 3;
22
+ // Lazy body match so reviewer comments containing "}" don't truncate the
23
+ // JSON (the acc-conflict regex's [^}]* would). Comments never contain
24
+ // "-->" — the RPC builds them from reviewer reasons.
25
+ const REWORK_META_RE = /<!--\s*acc-rework:\s*(\{[\s\S]*?\})\s*-->/;
26
+ function coerce(parsed) {
27
+ if (typeof parsed.pr_number !== "number" ||
28
+ typeof parsed.repo !== "string" ||
29
+ !parsed.repo.includes("/")) {
30
+ return null;
31
+ }
32
+ return {
33
+ pr_number: parsed.pr_number,
34
+ repo: parsed.repo,
35
+ branch: typeof parsed.branch === "string" ? parsed.branch : "",
36
+ origin_task_id: typeof parsed.origin_task_id === "string" ? parsed.origin_task_id : "",
37
+ verdict: typeof parsed.verdict === "string" ? parsed.verdict : "reject",
38
+ confidence: typeof parsed.confidence === "number" ? parsed.confidence : 0,
39
+ comments: Array.isArray(parsed.comments)
40
+ ? parsed.comments.filter((c) => typeof c === "string")
41
+ : [],
42
+ cycle: typeof parsed.cycle === "number" ? parsed.cycle : 1,
43
+ };
44
+ }
45
+ /**
46
+ * Extract rework metadata from the task description. Falls back to the
47
+ * task row's own pr_number/repo/branch when the sentinel is absent or
48
+ * malformed (same degradation path as parseConflictMeta).
49
+ */
50
+ export function parseReworkMeta(description, fallback) {
51
+ if (description) {
52
+ const match = REWORK_META_RE.exec(description);
53
+ if (match) {
54
+ try {
55
+ const meta = coerce(JSON.parse(match[1]));
56
+ if (meta)
57
+ return meta;
58
+ }
59
+ catch {
60
+ // fall through to task-row fallback
61
+ }
62
+ }
63
+ }
64
+ const prNumber = fallback?.pr_number;
65
+ const repo = fallback?.repo;
66
+ if (typeof prNumber === "number" && typeof repo === "string" && repo.includes("/")) {
67
+ return {
68
+ pr_number: prNumber,
69
+ repo,
70
+ branch: fallback?.branch ?? "",
71
+ origin_task_id: "",
72
+ verdict: "reject",
73
+ confidence: 0,
74
+ comments: [],
75
+ cycle: 1,
76
+ };
77
+ }
78
+ return null;
79
+ }
80
+ /**
81
+ * Resolve the PR's head branch via gh when the sentinel carried an empty
82
+ * branch (e.g. the origin task row had no branch back-written yet).
83
+ */
84
+ export async function fetchPrHeadRef(repo, prNumber) {
85
+ const { stdout } = await execa("gh", ["pr", "view", String(prNumber), "-R", repo, "--json", "headRefName", "--jq", ".headRefName"], { env: buildEnv() });
86
+ return stdout.trim();
87
+ }
88
+ /**
89
+ * Rework prompt. Replaces renderTaskPrompt for rework tasks: the protocol
90
+ * header changes from "open a PR" to "update the existing PR's branch",
91
+ * and the reviewer's verdict + comments become the work spec. The runner
92
+ * pushes after Claude exits, so the session only needs to commit.
93
+ */
94
+ export function renderReworkPrompt(args) {
95
+ const { task, meta, branch } = args;
96
+ const lines = [];
97
+ lines.push(`**Branch:** ${branch} (EXISTING — already checked out; do NOT create a new branch)`);
98
+ lines.push(`**PR:** #${meta.pr_number} on ${meta.repo} (EXISTING — do NOT open a new PR)`);
99
+ lines.push("");
100
+ lines.push(`# Task ${task.id}: ${task.title}`);
101
+ lines.push("");
102
+ lines.push("A reviewer rejected this PR. You are on the PR's existing branch with " +
103
+ "its current contents. Address every reviewer comment below with " +
104
+ "minimal, focused changes, then COMMIT your work. The runner pushes " +
105
+ "the branch after you exit — do not push and do not open a PR.");
106
+ lines.push("");
107
+ lines.push(`## Reviewer verdict (rework cycle ${meta.cycle}/${MAX_REWORK_CYCLES})`);
108
+ lines.push(`${meta.verdict} (confidence ${meta.confidence})`);
109
+ lines.push("");
110
+ lines.push("## Reviewer comments");
111
+ lines.push(...(meta.comments.length
112
+ ? meta.comments.map((c) => `- ${c}`)
113
+ : ["- (none provided — re-read the task description and PR diff for context)"]));
114
+ lines.push("");
115
+ if (task.description) {
116
+ lines.push("## Task description");
117
+ lines.push(task.description);
118
+ lines.push("");
119
+ }
120
+ lines.push("## Forbidden files (DO NOT TOUCH)");
121
+ lines.push(...(task.forbidden.length ? task.forbidden.map((f) => `- ${f}`) : ["- (none)"]));
122
+ lines.push("");
123
+ lines.push("## Gates");
124
+ lines.push(`Run \`${task.typecheck_cmd?.trim() || "pnpm typecheck && pnpm lint && pnpm build"}\` ` +
125
+ "after your edits. All gates must pass before you commit. If a gate " +
126
+ "fails and you cannot fix it, say so explicitly in your final output " +
127
+ "and do not commit broken code.");
128
+ lines.push("");
129
+ lines.push("## Required output");
130
+ lines.push("- Summary of what you changed per reviewer comment");
131
+ lines.push("- Any comments you could NOT address, and why");
132
+ lines.push("- Gate results (typecheck/lint/build)");
133
+ lines.push("");
134
+ return lines.join("\n");
135
+ }
136
+ //# sourceMappingURL=rework.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rework.js","sourceRoot":"","sources":["../../src/runtime/rework.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ export interface SingletonLockInfo {
2
+ pid: number;
3
+ /** Canonical (realpath-resolved where possible) repo path this runner serves. */
4
+ repoPath: string;
5
+ /**
6
+ * v1.02-A: the runner identity (ACC_RUNNER_ID) this lock is keyed to, or
7
+ * "" for the legacy repo-path-only key. Recorded so doctor and the loud
8
+ * duplicate banner can name the holder's identity.
9
+ */
10
+ runnerId: string;
11
+ acquiredAt: string;
12
+ version: string;
13
+ }
14
+ /**
15
+ * v1.02-A: the runner identity that keys the singleton lock. ACC_RUNNER_ID
16
+ * when set, else "" (legacy repo-path-only key). Never the minted acc-eph id.
17
+ */
18
+ export declare function singletonRunnerId(env?: NodeJS.ProcessEnv): string;
19
+ export interface SingletonLock {
20
+ lockFile: string;
21
+ repoPath: string;
22
+ /** Remove the lock iff we still own it. Safe to call more than once. */
23
+ release(): Promise<void>;
24
+ }
25
+ export declare class SingletonLockHeldError extends Error {
26
+ readonly holder: SingletonLockInfo;
27
+ readonly lockFile: string;
28
+ constructor(holder: SingletonLockInfo, lockFile: string);
29
+ }
30
+ /**
31
+ * Resolve a repo path to a stable canonical form so two spellings of the
32
+ * same directory (trailing slash, symlink, `./`) map to one lock file.
33
+ * realpath fails if the path does not exist yet — fall back to resolve().
34
+ */
35
+ export declare function canonicalRepoPath(repoPath: string): string;
36
+ /**
37
+ * `<cache>/runner.<hash>.lock`. The hash is derived from the canonical repo
38
+ * path AND the runner identity (v1.02-A), so two distinct ACC_RUNNER_IDs on
39
+ * one repo get distinct lock files and coexist. An empty `runnerId` keeps the
40
+ * pre-v1.02 hash (repo path alone) so a runner without ACC_RUNNER_ID still
41
+ * detects a pre-upgrade holder for the same repo.
42
+ */
43
+ export declare function singletonLockPath(repoPath: string, runnerId?: string): string;
44
+ /**
45
+ * Acquire the single-instance lock for `repoPath` under `runnerId`. Throws
46
+ * SingletonLockHeldError when a *live* process already holds the lock for the
47
+ * same (canonical repo path, runner identity) pair. A stale lock (dead pid,
48
+ * our own pid, or a corrupt file) is reclaimed with a stderr breadcrumb and
49
+ * then re-acquired. A different `runnerId` keys a different lock file, so two
50
+ * distinct identities on one repo never contend (v1.02-A).
51
+ */
52
+ export declare function acquireSingletonLock(repoPath: string, pid?: number, runnerId?: string): Promise<SingletonLock>;
53
+ /**
54
+ * Inspect the lock file for `repoPath` and return its holder iff a *live*
55
+ * process other than `selfPid` owns it. Used by `acc-runner doctor` to
56
+ * report a duplicate runner without trying to acquire the lock.
57
+ */
58
+ export declare function findLiveLockHolder(repoPath: string, selfPid?: number, runnerId?: string): Promise<SingletonLockInfo | null>;
59
+ /**
60
+ * pgrep-style scan for other `acc-runner watch` processes. Best-effort and
61
+ * offline of the lock file — used by doctor to catch a runner that predates
62
+ * the singleton lock (no lock file) or one whose lock was hand-deleted.
63
+ * Returns foreign pids; never throws (returns [] on any failure).
64
+ */
65
+ export declare function listForeignRunnerPids(selfPid?: number): Promise<number[]>;
66
+ //# sourceMappingURL=singleton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"singleton.d.ts","sourceRoot":"","sources":["../../src/runtime/singleton.ts"],"names":[],"mappings":"AAsCA,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,iFAAiF;IACjF,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAE9E;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,wEAAwE;IACxE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,qBAAa,sBAAuB,SAAQ,KAAK;aAE7B,MAAM,EAAE,iBAAiB;aACzB,QAAQ,EAAE,MAAM;gBADhB,MAAM,EAAE,iBAAiB,EACzB,QAAQ,EAAE,MAAM;CAQnC;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAO1D;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,QAAQ,GAAE,MAA4B,GACrC,MAAM,CAKR;AA4CD;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAChB,GAAG,GAAE,MAAoB,EACzB,QAAQ,GAAE,MAA4B,GACrC,OAAO,CAAC,aAAa,CAAC,CA+CxB;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,MAAoB,EAC7B,QAAQ,GAAE,MAA4B,GACrC,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAMnC;AAED;;;;;GAKG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,GAAE,MAAoB,GAC5B,OAAO,CAAC,MAAM,EAAE,CAAC,CAqBnB"}
@@ -0,0 +1,227 @@
1
+ /**
2
+ * v0.14.0 (T-54-2, GA-3): single-instance runner lock.
3
+ *
4
+ * On 2026-06-05 two `acc-runner watch` processes came up on one machine
5
+ * sharing the same repo path. They collided on per-task worktree locks and
6
+ * raced git refs, double-failing an entire batch in under a second
7
+ * (CURRENT_CYCLE.md v0.53 incident (b)). No startup guard existed.
8
+ *
9
+ * This module gives any task-executing entrypoint (today: `watch`) an
10
+ * exclusive lock keyed by the *canonical repo path* it serves. Acquisition
11
+ * uses O_EXCL (`fs.openSync(file, "wx")`) so the create-or-fail is atomic;
12
+ * an existing lock whose pid is dead is reclaimed with a log line, while a
13
+ * live holder for the SAME repo path is rejected loudly.
14
+ *
15
+ * Different repo paths coexist (future multi-repo): the lock file name is
16
+ * derived from a hash of the canonical repo path, so one machine can run
17
+ * one runner per repo without contention. The conceptual name in the spec
18
+ * is `<cache>/runner.lock`; the per-repo hash suffix is what makes
19
+ * coexistence possible.
20
+ *
21
+ * v1.02-A — concurrent runners per host. The lock is now keyed by
22
+ * (canonical repo path + runner identity), not the repo path alone, so two
23
+ * DISTINCT ACC_RUNNER_IDs can serve the same repo on one machine at the same
24
+ * time (each gets its own cache dir via `cacheDir()`, so no worktree/ref
25
+ * race). A runner with NO ACC_RUNNER_ID keeps the legacy repo-path-only key
26
+ * — two un-identified runners on one repo still collide, which is the
27
+ * original duplicate-runner footgun the lock exists to catch. The identity
28
+ * deliberately reads ACC_RUNNER_ID directly (never the minted acc-eph id,
29
+ * which is random per process and would make the lock never collide).
30
+ */
31
+ import fs from "node:fs";
32
+ import fsp from "node:fs/promises";
33
+ import path from "node:path";
34
+ import crypto from "node:crypto";
35
+ import { cacheDir } from "./cache-dir.js";
36
+ import { isProcessAlive } from "./locks.js";
37
+ import { PACKAGE_VERSION } from "../pkg-version.js";
38
+ /**
39
+ * v1.02-A: the runner identity that keys the singleton lock. ACC_RUNNER_ID
40
+ * when set, else "" (legacy repo-path-only key). Never the minted acc-eph id.
41
+ */
42
+ export function singletonRunnerId(env = process.env) {
43
+ return env.ACC_RUNNER_ID?.trim() || "";
44
+ }
45
+ export class SingletonLockHeldError extends Error {
46
+ holder;
47
+ lockFile;
48
+ constructor(holder, lockFile) {
49
+ super(`another acc-runner (pid ${holder.pid}) is already serving repo ${holder.repoPath}` +
50
+ (holder.runnerId ? ` as ${holder.runnerId}` : ""));
51
+ this.holder = holder;
52
+ this.lockFile = lockFile;
53
+ this.name = "SingletonLockHeldError";
54
+ }
55
+ }
56
+ /**
57
+ * Resolve a repo path to a stable canonical form so two spellings of the
58
+ * same directory (trailing slash, symlink, `./`) map to one lock file.
59
+ * realpath fails if the path does not exist yet — fall back to resolve().
60
+ */
61
+ export function canonicalRepoPath(repoPath) {
62
+ const resolved = path.resolve(repoPath);
63
+ try {
64
+ return fs.realpathSync(resolved);
65
+ }
66
+ catch {
67
+ return resolved;
68
+ }
69
+ }
70
+ /**
71
+ * `<cache>/runner.<hash>.lock`. The hash is derived from the canonical repo
72
+ * path AND the runner identity (v1.02-A), so two distinct ACC_RUNNER_IDs on
73
+ * one repo get distinct lock files and coexist. An empty `runnerId` keeps the
74
+ * pre-v1.02 hash (repo path alone) so a runner without ACC_RUNNER_ID still
75
+ * detects a pre-upgrade holder for the same repo.
76
+ */
77
+ export function singletonLockPath(repoPath, runnerId = singletonRunnerId()) {
78
+ const canonical = canonicalRepoPath(repoPath);
79
+ const key = runnerId ? `${canonical}\0${runnerId}` : canonical;
80
+ const hash = crypto.createHash("sha1").update(key).digest("hex").slice(0, 12);
81
+ return path.join(cacheDir(), `runner.${hash}.lock`);
82
+ }
83
+ async function readLockInfo(file) {
84
+ let raw;
85
+ try {
86
+ raw = await fsp.readFile(file, "utf8");
87
+ }
88
+ catch (err) {
89
+ if (err.code === "ENOENT")
90
+ return null;
91
+ throw err;
92
+ }
93
+ try {
94
+ const parsed = JSON.parse(raw);
95
+ if (typeof parsed.pid !== "number" || !Number.isFinite(parsed.pid))
96
+ return null;
97
+ return {
98
+ pid: parsed.pid,
99
+ repoPath: typeof parsed.repoPath === "string" ? parsed.repoPath : "<unknown>",
100
+ // Absent in pre-v1.02 lock files → "" (legacy repo-path-only key).
101
+ runnerId: typeof parsed.runnerId === "string" ? parsed.runnerId : "",
102
+ acquiredAt: typeof parsed.acquiredAt === "string" ? parsed.acquiredAt : "<unknown>",
103
+ version: typeof parsed.version === "string" ? parsed.version : "<unknown>",
104
+ };
105
+ }
106
+ catch {
107
+ // Corrupt/partial lock file — treat as reclaimable rather than fatal.
108
+ return null;
109
+ }
110
+ }
111
+ function makeLock(file, repoPath, pid) {
112
+ return {
113
+ lockFile: file,
114
+ repoPath,
115
+ async release() {
116
+ try {
117
+ const holder = await readLockInfo(file);
118
+ // Only unlink if we still own it — never stomp a lock another
119
+ // process reclaimed after we exited uncleanly.
120
+ if (holder && holder.pid === pid)
121
+ await fsp.unlink(file);
122
+ }
123
+ catch (err) {
124
+ if (err.code !== "ENOENT")
125
+ throw err;
126
+ }
127
+ },
128
+ };
129
+ }
130
+ /**
131
+ * Acquire the single-instance lock for `repoPath` under `runnerId`. Throws
132
+ * SingletonLockHeldError when a *live* process already holds the lock for the
133
+ * same (canonical repo path, runner identity) pair. A stale lock (dead pid,
134
+ * our own pid, or a corrupt file) is reclaimed with a stderr breadcrumb and
135
+ * then re-acquired. A different `runnerId` keys a different lock file, so two
136
+ * distinct identities on one repo never contend (v1.02-A).
137
+ */
138
+ export async function acquireSingletonLock(repoPath, pid = process.pid, runnerId = singletonRunnerId()) {
139
+ const canonical = canonicalRepoPath(repoPath);
140
+ const file = singletonLockPath(repoPath, runnerId);
141
+ await fsp.mkdir(path.dirname(file), { recursive: true });
142
+ const payload = `${JSON.stringify({
143
+ pid,
144
+ repoPath: canonical,
145
+ runnerId,
146
+ acquiredAt: new Date().toISOString(),
147
+ version: PACKAGE_VERSION,
148
+ }, null, 2)}\n`;
149
+ // Two attempts: the second only runs after we reclaim a stale lock, which
150
+ // closes the (tiny) window between the failed create and the unlink.
151
+ for (let attempt = 0; attempt < 2; attempt++) {
152
+ try {
153
+ // "wx" === O_CREAT | O_EXCL | O_WRONLY: atomic create-or-fail.
154
+ const fd = fs.openSync(file, "wx");
155
+ try {
156
+ fs.writeSync(fd, payload);
157
+ }
158
+ finally {
159
+ fs.closeSync(fd);
160
+ }
161
+ return makeLock(file, canonical, pid);
162
+ }
163
+ catch (err) {
164
+ if (err.code !== "EEXIST")
165
+ throw err;
166
+ const holder = await readLockInfo(file);
167
+ if (holder && holder.pid !== pid && isProcessAlive(holder.pid)) {
168
+ throw new SingletonLockHeldError(holder, file);
169
+ }
170
+ // Stale: dead holder, our own leftover pid, or unparseable. Reclaim.
171
+ process.stderr.write(`[acc-runner] reclaiming stale runner lock ${file} ` +
172
+ `(held by pid ${holder?.pid ?? "?"}, not alive)\n`);
173
+ try {
174
+ fs.unlinkSync(file);
175
+ }
176
+ catch (unlinkErr) {
177
+ if (unlinkErr.code !== "ENOENT")
178
+ throw unlinkErr;
179
+ }
180
+ }
181
+ }
182
+ throw new Error(`could not acquire singleton lock at ${file}`);
183
+ }
184
+ /**
185
+ * Inspect the lock file for `repoPath` and return its holder iff a *live*
186
+ * process other than `selfPid` owns it. Used by `acc-runner doctor` to
187
+ * report a duplicate runner without trying to acquire the lock.
188
+ */
189
+ export async function findLiveLockHolder(repoPath, selfPid = process.pid, runnerId = singletonRunnerId()) {
190
+ const holder = await readLockInfo(singletonLockPath(repoPath, runnerId));
191
+ if (holder && holder.pid !== selfPid && isProcessAlive(holder.pid)) {
192
+ return holder;
193
+ }
194
+ return null;
195
+ }
196
+ /**
197
+ * pgrep-style scan for other `acc-runner watch` processes. Best-effort and
198
+ * offline of the lock file — used by doctor to catch a runner that predates
199
+ * the singleton lock (no lock file) or one whose lock was hand-deleted.
200
+ * Returns foreign pids; never throws (returns [] on any failure).
201
+ */
202
+ export async function listForeignRunnerPids(selfPid = process.pid) {
203
+ try {
204
+ const { execa } = await import("execa");
205
+ const { stdout } = await execa("ps", ["-axo", "pid=,args="]);
206
+ const pids = [];
207
+ for (const line of stdout.split(/\r?\n/)) {
208
+ const m = line.trim().match(/^(\d+)\s+(.*)$/);
209
+ if (!m)
210
+ continue;
211
+ const pid = Number.parseInt(m[1], 10);
212
+ const args = m[2];
213
+ if (pid === selfPid)
214
+ continue;
215
+ // Match the runner's watch command without matching this doctor run
216
+ // (which is `acc-runner doctor`) or a grep of the word.
217
+ if (/acc-runner\b.*\bwatch\b/.test(args) || /\bacc-runner-watch\b/.test(args)) {
218
+ pids.push(pid);
219
+ }
220
+ }
221
+ return pids;
222
+ }
223
+ catch {
224
+ return [];
225
+ }
226
+ }
227
+ //# sourceMappingURL=singleton.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"singleton.js","sourceRoot":"","sources":["../../src/runtime/singleton.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ export declare const NPM_PACKAGE = "@tokenfactory/acc-runner";
2
+ export interface TaskEnvRecord {
3
+ /** `claude --version` first line at the last successful task, or null. */
4
+ claudeVersion: string | null;
5
+ recordedAt: string;
6
+ }
7
+ export declare function taskEnvFile(): string;
8
+ export interface NpmLatestDeps {
9
+ /** Fetch the latest published version string, or null on failure. */
10
+ fetchLatest?: () => Promise<string | null>;
11
+ /** Now, ms since epoch — injectable for cache-freshness tests. */
12
+ now?: number;
13
+ }
14
+ /**
15
+ * Latest published version, via a cached registry lookup. Returns the
16
+ * cached value while it is fresh; refetches once it goes stale. On a
17
+ * network failure with no usable cache, returns null so the caller skips
18
+ * the drift check silently (offline-safe).
19
+ */
20
+ export declare function npmLatestVersion(deps?: NpmLatestDeps): Promise<string | null>;
21
+ /**
22
+ * First line of `claude --version`, or null when the binary is missing or
23
+ * errors. Shared by the watch success-recorder and doctor's drift check so
24
+ * both compare the exact same string.
25
+ */
26
+ export declare function getClaudeVersion(): Promise<string | null>;
27
+ /** Persist the claude CLI version seen at a successful task. Best-effort. */
28
+ export declare function recordTaskClaudeVersion(claudeVersion: string | null): Promise<void>;
29
+ /** Read the last recorded task-env, or null when no task has succeeded yet. */
30
+ export declare function readTaskEnv(): Promise<TaskEnvRecord | null>;
31
+ //# sourceMappingURL=version-drift.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version-drift.d.ts","sourceRoot":"","sources":["../../src/runtime/version-drift.ts"],"names":[],"mappings":"AAwBA,eAAO,MAAM,WAAW,6BAA6B,CAAC;AAQtD,MAAM,WAAW,aAAa;IAC5B,0EAA0E;IAC1E,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;CACpB;AAMD,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAgBD,MAAM,WAAW,aAAa;IAC5B,qEAAqE;IACrE,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC3C,kEAAkE;IAClE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAiBD;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAuBxB;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAO/D;AAED,6EAA6E;AAC7E,wBAAsB,uBAAuB,CAC3C,aAAa,EAAE,MAAM,GAAG,IAAI,GAC3B,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,+EAA+E;AAC/E,wBAAsB,WAAW,IAAI,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAEjE"}
@@ -0,0 +1,114 @@
1
+ /**
2
+ * v0.14.0 (T-54-2, GA-6 runner-side): version-drift detection inputs.
3
+ *
4
+ * Two silent drifts double-failed the v0.53 batch's blast radius: this
5
+ * machine ran acc-runner 0.11.0 while 0.12.0 was on npm, and the claude
6
+ * CLI auto-updated mid-cycle without anyone noticing. `acc-runner doctor`
7
+ * now surfaces both as WARNs. This module owns the two data sources:
8
+ *
9
+ * 1. npmLatestVersion() — one *cached* registry lookup (default 6h TTL),
10
+ * offline-safe (returns null on any network failure so doctor skips
11
+ * the check silently rather than failing).
12
+ * 2. recordTaskClaudeVersion() / readTaskEnv() — the claude CLI version
13
+ * observed at the last successful task, persisted in the cache so
14
+ * doctor can flag "claude changed under you since the last green run".
15
+ *
16
+ * Both use additive cache files under the runner cache dir — no existing
17
+ * cache schema is touched.
18
+ */
19
+ import fs from "node:fs/promises";
20
+ import path from "node:path";
21
+ import { execa } from "execa";
22
+ import { buildEnv } from "../bin-resolve.js";
23
+ import { cacheDir } from "./cache-dir.js";
24
+ export const NPM_PACKAGE = "@tokenfactory/acc-runner";
25
+ const REGISTRY_TTL_MS = 6 * 60 * 60 * 1000; // 6h — "one cached lookup", not per-run.
26
+ function registryCacheFile() {
27
+ return path.join(cacheDir(), "npm-latest.json");
28
+ }
29
+ export function taskEnvFile() {
30
+ return path.join(cacheDir(), "last-task-env.json");
31
+ }
32
+ async function readJson(file) {
33
+ try {
34
+ return JSON.parse(await fs.readFile(file, "utf8"));
35
+ }
36
+ catch (err) {
37
+ if (err.code === "ENOENT")
38
+ return null;
39
+ return null; // corrupt cache is non-fatal — treat as a miss
40
+ }
41
+ }
42
+ async function writeJson(file, value) {
43
+ await fs.mkdir(path.dirname(file), { recursive: true });
44
+ await fs.writeFile(file, `${JSON.stringify(value, null, 2)}\n`, "utf8");
45
+ }
46
+ /** Default network fetch of the registry "latest" dist-tag. */
47
+ async function fetchLatestFromRegistry() {
48
+ try {
49
+ const res = await fetch(`https://registry.npmjs.org/${NPM_PACKAGE}/latest`, { headers: { Accept: "application/json" } });
50
+ if (!res.ok)
51
+ return null;
52
+ const body = (await res.json());
53
+ return typeof body.version === "string" ? body.version : null;
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
59
+ /**
60
+ * Latest published version, via a cached registry lookup. Returns the
61
+ * cached value while it is fresh; refetches once it goes stale. On a
62
+ * network failure with no usable cache, returns null so the caller skips
63
+ * the drift check silently (offline-safe).
64
+ */
65
+ export async function npmLatestVersion(deps = {}) {
66
+ const now = deps.now ?? Date.now();
67
+ const fetchLatest = deps.fetchLatest ?? fetchLatestFromRegistry;
68
+ const file = registryCacheFile();
69
+ const cached = await readJson(file);
70
+ if (cached?.latest) {
71
+ const age = now - Date.parse(cached.checkedAt);
72
+ if (Number.isFinite(age) && age >= 0 && age < REGISTRY_TTL_MS) {
73
+ return cached.latest;
74
+ }
75
+ }
76
+ const latest = await fetchLatest();
77
+ if (!latest) {
78
+ // Offline / registry down: fall back to a stale cache if we have one.
79
+ return cached?.latest ?? null;
80
+ }
81
+ await writeJson(file, {
82
+ latest,
83
+ checkedAt: new Date(now).toISOString(),
84
+ }).catch(() => {
85
+ /* cache write is best-effort */
86
+ });
87
+ return latest;
88
+ }
89
+ /**
90
+ * First line of `claude --version`, or null when the binary is missing or
91
+ * errors. Shared by the watch success-recorder and doctor's drift check so
92
+ * both compare the exact same string.
93
+ */
94
+ export async function getClaudeVersion() {
95
+ try {
96
+ const { stdout } = await execa("claude", ["--version"], { env: buildEnv() });
97
+ return stdout.split(/\r?\n/)[0]?.trim() || null;
98
+ }
99
+ catch {
100
+ return null;
101
+ }
102
+ }
103
+ /** Persist the claude CLI version seen at a successful task. Best-effort. */
104
+ export async function recordTaskClaudeVersion(claudeVersion) {
105
+ await writeJson(taskEnvFile(), {
106
+ claudeVersion,
107
+ recordedAt: new Date().toISOString(),
108
+ });
109
+ }
110
+ /** Read the last recorded task-env, or null when no task has succeeded yet. */
111
+ export async function readTaskEnv() {
112
+ return readJson(taskEnvFile());
113
+ }
114
+ //# sourceMappingURL=version-drift.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version-drift.js","sourceRoot":"","sources":["../../src/runtime/version-drift.ts"],"names":[],"mappings":"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"}