audit-tools 0.32.20 → 0.32.22

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 (195) hide show
  1. package/dist/audit/clarification/blastRadius.d.ts +23 -0
  2. package/dist/audit/clarification/blastRadius.d.ts.map +1 -0
  3. package/dist/audit/clarification/blastRadius.js +65 -0
  4. package/dist/audit/clarification/blastRadius.js.map +1 -0
  5. package/dist/audit/clarification/dials.d.ts +28 -0
  6. package/dist/audit/clarification/dials.d.ts.map +1 -0
  7. package/dist/audit/clarification/dials.js +37 -0
  8. package/dist/audit/clarification/dials.js.map +1 -0
  9. package/dist/audit/clarification/partition.d.ts +25 -0
  10. package/dist/audit/clarification/partition.d.ts.map +1 -0
  11. package/dist/audit/clarification/partition.js +77 -0
  12. package/dist/audit/clarification/partition.js.map +1 -0
  13. package/dist/audit/clarification/riskGate.d.ts +24 -0
  14. package/dist/audit/clarification/riskGate.d.ts.map +1 -0
  15. package/dist/audit/clarification/riskGate.js +38 -0
  16. package/dist/audit/clarification/riskGate.js.map +1 -0
  17. package/dist/audit/clarification/voiQueue.d.ts +17 -0
  18. package/dist/audit/clarification/voiQueue.d.ts.map +1 -0
  19. package/dist/audit/clarification/voiQueue.js +37 -0
  20. package/dist/audit/clarification/voiQueue.js.map +1 -0
  21. package/dist/audit/cli/auditStep.d.ts +2 -0
  22. package/dist/audit/cli/auditStep.d.ts.map +1 -1
  23. package/dist/audit/cli/auditStep.js +9 -1
  24. package/dist/audit/cli/auditStep.js.map +1 -1
  25. package/dist/audit/cli/charterClarificationPrompt.d.ts +19 -0
  26. package/dist/audit/cli/charterClarificationPrompt.d.ts.map +1 -0
  27. package/dist/audit/cli/charterClarificationPrompt.js +60 -0
  28. package/dist/audit/cli/charterClarificationPrompt.js.map +1 -0
  29. package/dist/audit/cli/nextStepCommand.d.ts.map +1 -1
  30. package/dist/audit/cli/nextStepCommand.js +74 -0
  31. package/dist/audit/cli/nextStepCommand.js.map +1 -1
  32. package/dist/audit/cli/nextStepHelpers.d.ts +62 -0
  33. package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
  34. package/dist/audit/cli/nextStepHelpers.js +129 -0
  35. package/dist/audit/cli/nextStepHelpers.js.map +1 -1
  36. package/dist/audit/cli/steps.d.ts +4 -4
  37. package/dist/audit/cli/steps.d.ts.map +1 -1
  38. package/dist/audit/cli/steps.js +2 -0
  39. package/dist/audit/cli/steps.js.map +1 -1
  40. package/dist/audit/io/artifacts.d.ts +6 -0
  41. package/dist/audit/io/artifacts.d.ts.map +1 -1
  42. package/dist/audit/io/artifacts.js +2 -0
  43. package/dist/audit/io/artifacts.js.map +1 -1
  44. package/dist/audit/orchestrator/advance.d.ts +23 -0
  45. package/dist/audit/orchestrator/advance.d.ts.map +1 -1
  46. package/dist/audit/orchestrator/advance.js +122 -4
  47. package/dist/audit/orchestrator/advance.js.map +1 -1
  48. package/dist/audit/orchestrator/advanceTypes.d.ts +22 -1
  49. package/dist/audit/orchestrator/advanceTypes.d.ts.map +1 -1
  50. package/dist/audit/orchestrator/charterClarificationExecutor.d.ts +28 -0
  51. package/dist/audit/orchestrator/charterClarificationExecutor.d.ts.map +1 -0
  52. package/dist/audit/orchestrator/charterClarificationExecutor.js +130 -0
  53. package/dist/audit/orchestrator/charterClarificationExecutor.js.map +1 -0
  54. package/dist/audit/orchestrator/dependencyMap.d.ts +2 -0
  55. package/dist/audit/orchestrator/dependencyMap.d.ts.map +1 -1
  56. package/dist/audit/orchestrator/dependencyMap.js +24 -0
  57. package/dist/audit/orchestrator/dependencyMap.js.map +1 -1
  58. package/dist/audit/orchestrator/designReviewPrompt.d.ts +17 -0
  59. package/dist/audit/orchestrator/designReviewPrompt.d.ts.map +1 -1
  60. package/dist/audit/orchestrator/designReviewPrompt.js +50 -0
  61. package/dist/audit/orchestrator/designReviewPrompt.js.map +1 -1
  62. package/dist/audit/orchestrator/executorRunners.d.ts.map +1 -1
  63. package/dist/audit/orchestrator/executorRunners.js +4 -0
  64. package/dist/audit/orchestrator/executorRunners.js.map +1 -1
  65. package/dist/audit/orchestrator/executors.d.ts.map +1 -1
  66. package/dist/audit/orchestrator/executors.js +20 -0
  67. package/dist/audit/orchestrator/executors.js.map +1 -1
  68. package/dist/audit/orchestrator/nextStep.d.ts +11 -1
  69. package/dist/audit/orchestrator/nextStep.d.ts.map +1 -1
  70. package/dist/audit/orchestrator/nextStep.js +6 -2
  71. package/dist/audit/orchestrator/nextStep.js.map +1 -1
  72. package/dist/audit/orchestrator/staleness.d.ts +27 -1
  73. package/dist/audit/orchestrator/staleness.d.ts.map +1 -1
  74. package/dist/audit/orchestrator/staleness.js +33 -16
  75. package/dist/audit/orchestrator/staleness.js.map +1 -1
  76. package/dist/audit/orchestrator/state.d.ts +12 -1
  77. package/dist/audit/orchestrator/state.d.ts.map +1 -1
  78. package/dist/audit/orchestrator/state.js +41 -2
  79. package/dist/audit/orchestrator/state.js.map +1 -1
  80. package/dist/audit/orchestrator/synthesisExecutors.d.ts.map +1 -1
  81. package/dist/audit/orchestrator/synthesisExecutors.js +1 -0
  82. package/dist/audit/orchestrator/synthesisExecutors.js.map +1 -1
  83. package/dist/audit/orchestrator/systemicChallengeExecutor.d.ts +25 -0
  84. package/dist/audit/orchestrator/systemicChallengeExecutor.d.ts.map +1 -0
  85. package/dist/audit/orchestrator/systemicChallengeExecutor.js +107 -0
  86. package/dist/audit/orchestrator/systemicChallengeExecutor.js.map +1 -0
  87. package/dist/audit/reporting/mergeFindings.d.ts +2 -1
  88. package/dist/audit/reporting/mergeFindings.d.ts.map +1 -1
  89. package/dist/audit/reporting/mergeFindings.js +6 -1
  90. package/dist/audit/reporting/mergeFindings.js.map +1 -1
  91. package/dist/audit/reporting/synthesis.d.ts +2 -0
  92. package/dist/audit/reporting/synthesis.d.ts.map +1 -1
  93. package/dist/audit/reporting/synthesis.js +1 -1
  94. package/dist/audit/reporting/synthesis.js.map +1 -1
  95. package/dist/audit/systemic/aggregateMetricsDigest.d.ts +11 -0
  96. package/dist/audit/systemic/aggregateMetricsDigest.d.ts.map +1 -0
  97. package/dist/audit/systemic/aggregateMetricsDigest.js +84 -0
  98. package/dist/audit/systemic/aggregateMetricsDigest.js.map +1 -0
  99. package/dist/audit/systemic/metricsDigestTypes.d.ts +35 -0
  100. package/dist/audit/systemic/metricsDigestTypes.d.ts.map +1 -0
  101. package/dist/audit/systemic/metricsDigestTypes.js +10 -0
  102. package/dist/audit/systemic/metricsDigestTypes.js.map +1 -0
  103. package/dist/audit/systemic/secondOrderAdversaryPrompt.d.ts +16 -0
  104. package/dist/audit/systemic/secondOrderAdversaryPrompt.d.ts.map +1 -0
  105. package/dist/audit/systemic/secondOrderAdversaryPrompt.js +85 -0
  106. package/dist/audit/systemic/secondOrderAdversaryPrompt.js.map +1 -0
  107. package/dist/audit/systemic/systemicChallengeLoop.d.ts +48 -0
  108. package/dist/audit/systemic/systemicChallengeLoop.d.ts.map +1 -0
  109. package/dist/audit/systemic/systemicChallengeLoop.js +107 -0
  110. package/dist/audit/systemic/systemicChallengeLoop.js.map +1 -0
  111. package/dist/audit/types/charterClarification.d.ts +52 -0
  112. package/dist/audit/types/charterClarification.d.ts.map +1 -0
  113. package/dist/audit/types/charterClarification.js +2 -0
  114. package/dist/audit/types/charterClarification.js.map +1 -0
  115. package/dist/audit/types/systemicChallenge.d.ts +69 -0
  116. package/dist/audit/types/systemicChallenge.d.ts.map +1 -0
  117. package/dist/audit/types/systemicChallenge.js +2 -0
  118. package/dist/audit/types/systemicChallenge.js.map +1 -0
  119. package/dist/remediate/steps/dispatch/acceptNode.d.ts +178 -0
  120. package/dist/remediate/steps/dispatch/acceptNode.d.ts.map +1 -0
  121. package/dist/remediate/steps/dispatch/acceptNode.js +351 -0
  122. package/dist/remediate/steps/dispatch/acceptNode.js.map +1 -0
  123. package/dist/remediate/steps/dispatch/common.d.ts +123 -0
  124. package/dist/remediate/steps/dispatch/common.d.ts.map +1 -0
  125. package/dist/remediate/steps/dispatch/common.js +236 -0
  126. package/dist/remediate/steps/dispatch/common.js.map +1 -0
  127. package/dist/remediate/steps/dispatch/dagNodeFields.d.ts +37 -0
  128. package/dist/remediate/steps/dispatch/dagNodeFields.d.ts.map +1 -0
  129. package/dist/remediate/steps/dispatch/dagNodeFields.js +19 -0
  130. package/dist/remediate/steps/dispatch/dagNodeFields.js.map +1 -0
  131. package/dist/remediate/steps/dispatch/implementPrompt.d.ts +70 -0
  132. package/dist/remediate/steps/dispatch/implementPrompt.d.ts.map +1 -0
  133. package/dist/remediate/steps/dispatch/implementPrompt.js +577 -0
  134. package/dist/remediate/steps/dispatch/implementPrompt.js.map +1 -0
  135. package/dist/remediate/steps/dispatch/marshal.d.ts +32 -0
  136. package/dist/remediate/steps/dispatch/marshal.d.ts.map +1 -0
  137. package/dist/remediate/steps/dispatch/marshal.js +935 -0
  138. package/dist/remediate/steps/dispatch/marshal.js.map +1 -0
  139. package/dist/remediate/steps/dispatch/verifyCommands.d.ts +97 -0
  140. package/dist/remediate/steps/dispatch/verifyCommands.d.ts.map +1 -0
  141. package/dist/remediate/steps/dispatch/verifyCommands.js +204 -0
  142. package/dist/remediate/steps/dispatch/verifyCommands.js.map +1 -0
  143. package/dist/remediate/steps/dispatch/waveScheduling.d.ts +90 -0
  144. package/dist/remediate/steps/dispatch/waveScheduling.d.ts.map +1 -0
  145. package/dist/remediate/steps/dispatch/waveScheduling.js +352 -0
  146. package/dist/remediate/steps/dispatch/waveScheduling.js.map +1 -0
  147. package/dist/remediate/steps/dispatch/worktreeLifecycle.d.ts +189 -0
  148. package/dist/remediate/steps/dispatch/worktreeLifecycle.d.ts.map +1 -0
  149. package/dist/remediate/steps/dispatch/worktreeLifecycle.js +643 -0
  150. package/dist/remediate/steps/dispatch/worktreeLifecycle.js.map +1 -0
  151. package/dist/remediate/steps/dispatch/writeScope.d.ts +151 -0
  152. package/dist/remediate/steps/dispatch/writeScope.d.ts.map +1 -0
  153. package/dist/remediate/steps/dispatch/writeScope.js +376 -0
  154. package/dist/remediate/steps/dispatch/writeScope.js.map +1 -0
  155. package/dist/remediate/steps/dispatch.d.ts +16 -871
  156. package/dist/remediate/steps/dispatch.d.ts.map +1 -1
  157. package/dist/remediate/steps/dispatch.js +18 -3623
  158. package/dist/remediate/steps/dispatch.js.map +1 -1
  159. package/dist/shared/decompose/charterClarification.d.ts +80 -0
  160. package/dist/shared/decompose/charterClarification.d.ts.map +1 -0
  161. package/dist/shared/decompose/charterClarification.js +105 -0
  162. package/dist/shared/decompose/charterClarification.js.map +1 -0
  163. package/dist/shared/decompose/systemicChallenge.d.ts +326 -0
  164. package/dist/shared/decompose/systemicChallenge.d.ts.map +1 -0
  165. package/dist/shared/decompose/systemicChallenge.js +30 -0
  166. package/dist/shared/decompose/systemicChallenge.js.map +1 -0
  167. package/dist/shared/index.d.ts +10 -5
  168. package/dist/shared/index.d.ts.map +1 -1
  169. package/dist/shared/index.js +6 -4
  170. package/dist/shared/index.js.map +1 -1
  171. package/dist/shared/opencodePermissions.d.ts +73 -0
  172. package/dist/shared/opencodePermissions.d.ts.map +1 -1
  173. package/dist/shared/opencodePermissions.js +172 -0
  174. package/dist/shared/opencodePermissions.js.map +1 -1
  175. package/dist/shared/tooling/exec.d.ts +10 -1
  176. package/dist/shared/tooling/exec.d.ts.map +1 -1
  177. package/dist/shared/tooling/exec.js +13 -1
  178. package/dist/shared/tooling/exec.js.map +1 -1
  179. package/dist/shared/types/charter.d.ts +89 -0
  180. package/dist/shared/types/charter.d.ts.map +1 -1
  181. package/dist/shared/types/charter.js +68 -0
  182. package/dist/shared/types/charter.js.map +1 -1
  183. package/dist/shared/types/intentCheckpoint.d.ts +15 -0
  184. package/dist/shared/types/intentCheckpoint.d.ts.map +1 -1
  185. package/dist/shared/types/intentCheckpoint.js +11 -0
  186. package/dist/shared/types/intentCheckpoint.js.map +1 -1
  187. package/dist/shared/validation/charterGate.d.ts +20 -1
  188. package/dist/shared/validation/charterGate.d.ts.map +1 -1
  189. package/dist/shared/validation/charterGate.js +23 -0
  190. package/dist/shared/validation/charterGate.js.map +1 -1
  191. package/opencode.json +36 -20
  192. package/package.json +1 -1
  193. package/scripts/postinstall.mjs +2 -1
  194. package/wrapper/audit-code-wrapper-opencode.mjs +38 -12
  195. package/wrapper/remediate-code-wrapper-opencode.mjs +38 -12
@@ -0,0 +1,643 @@
1
+ import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
2
+ import { join, relative, dirname, isAbsolute } from "node:path";
3
+ import { spawnSyncHidden } from "audit-tools/shared";
4
+ import { runShellCommand } from "../../utils/commands.js";
5
+ import { toRepoRelative, refSafeSegment, gitTopLevel, canonicalPathKey, gitBranchExists, gitEditedFilesForBranch, } from "./common.js";
6
+ /**
7
+ * Create an isolated git worktree on a fresh branch at HEAD. Throws on non-zero exit.
8
+ *
9
+ * Refuses when `root` is not ITSELF a git top-level: a bare `git worktree add`
10
+ * with `cwd: root` walks UP to the nearest enclosing repo and silently creates
11
+ * the worktree/branch in that ancestor (observed polluting the monorepo with
12
+ * leaked `remediate-*` branches during the rolling_engine flip). The resolved
13
+ * top-level must equal the target root, or we refuse rather than escape.
14
+ */
15
+ export function createWorktree(root, worktreePath, branchName) {
16
+ const top = gitTopLevel(root);
17
+ if (top === null) {
18
+ throw new Error(`Refusing to create a worktree: ${root} is not inside a git repository ` +
19
+ `(git rev-parse --show-toplevel failed). Rolling dispatch requires the target root to be a git repo.`);
20
+ }
21
+ if (canonicalPathKey(top) !== canonicalPathKey(root)) {
22
+ throw new Error(`Refusing to create a worktree: the git top-level for ${root} is ${top}, not the target root ` +
23
+ `itself, so 'git worktree add' would escape to an ancestor repo. Initialize a git repo at the ` +
24
+ `target root before rolling dispatch.`);
25
+ }
26
+ const result = spawnSyncHidden("git", ["worktree", "add", "-b", branchName, worktreePath, "HEAD"], { cwd: root, encoding: "utf8", shell: false });
27
+ if (result.status !== 0) {
28
+ const stderr = (result.stderr ?? "").trim();
29
+ const stdout = (result.stdout ?? "").trim();
30
+ throw new Error(`git worktree add failed (exit ${result.status ?? "unknown"}):\n${stderr || stdout}`);
31
+ }
32
+ // Link the main checkout's (gitignored → absent-in-worktree) node_modules so
33
+ // per-node verify can resolve deps. Folded into creation, not left to each
34
+ // caller to remember — a fresh worktree without it silently fails verify with
35
+ // missing-module errors. Best-effort + idempotent (see ensureWorktreeNodeModules).
36
+ ensureWorktreeNodeModules(root, worktreePath);
37
+ }
38
+ /**
39
+ * Materialize into a fresh worktree any of the node's declared target paths that
40
+ * exist in the main tree but are absent from the worktree — i.e. git-untracked or
41
+ * gitignored files that `git worktree add HEAD` does not bring over. Without this
42
+ * a node whose scope names an untracked config file (the dogfood hit
43
+ * `opencode.json` and an uncommitted `.gemini/commands/*.toml`) cannot see its own
44
+ * target, so the edit silently no-ops. The "absent in worktree" test is the
45
+ * discriminator: a tracked path is already materialized from HEAD, so only the
46
+ * genuinely-missing untracked/ignored declarations are copied — a tracked-but-dirty
47
+ * file keeps its clean-from-HEAD worktree content and is never clobbered. Paths are
48
+ * repo-relative (the declared scope contract); absolute/escaping paths are skipped.
49
+ * Best-effort: a copy failure must not abort the dispatch (logged, not thrown).
50
+ */
51
+ export function seedUntrackedDeclaredPaths(root, worktreeRoot, declaredPaths) {
52
+ for (const rel of new Set(declaredPaths)) {
53
+ if (!rel || isAbsolute(rel))
54
+ continue;
55
+ // Reject paths that escape the root (defence-in-depth; declared scope is
56
+ // repo-relative and never `..`-prefixed in practice).
57
+ const dst = join(worktreeRoot, rel);
58
+ const src = join(root, rel);
59
+ if (relative(worktreeRoot, dst).startsWith(".."))
60
+ continue;
61
+ if (!existsSync(src) || existsSync(dst))
62
+ continue;
63
+ try {
64
+ mkdirSync(dirname(dst), { recursive: true });
65
+ cpSync(src, dst, { recursive: true });
66
+ }
67
+ catch (err) {
68
+ process.stderr.write(`[remediate-code] worktree seed: could not copy untracked declared path ${rel}: ${err.message}\n`);
69
+ }
70
+ }
71
+ }
72
+ /** Remove a git worktree. Best-effort: logs but does not throw on failure. */
73
+ export function removeWorktree(root, worktreePath) {
74
+ // Already-absent worktree = silent no-op: no spawn, no stderr, no throw. A
75
+ // path that still exists but fails git-remove for another reason is surfaced
76
+ // exactly as before. We do NOT match on the 'not a working tree' stderr text.
77
+ if (!existsSync(worktreePath))
78
+ return;
79
+ const result = spawnSyncHidden("git", ["worktree", "remove", "--force", worktreePath], { cwd: root, encoding: "utf8", shell: false });
80
+ if (result.status !== 0) {
81
+ const stderr = (result.stderr ?? "").trim();
82
+ process.stderr.write(`[remediate-code] worktree remove failed (exit ${result.status ?? "unknown"}): ${stderr}\n`);
83
+ }
84
+ }
85
+ /**
86
+ * Fully reset a node's isolated worktree + branch so a fresh `createWorktree -b`
87
+ * can run, even when a prior attempt left either behind. This is the idempotent
88
+ * cleanup the in-process driver needs across a `rate_limited` re-queue: the
89
+ * engine re-enters the dispatcher for the SAME block while its branch (and maybe
90
+ * a stale worktree admin entry) still exist, and `git worktree add -b <branch>`
91
+ * would otherwise fail with "branch already exists". Removing the worktree,
92
+ * pruning stale admin records, then force-deleting the branch makes every
93
+ * (re-)dispatch start clean from HEAD. All steps are best-effort (a missing
94
+ * worktree/branch is the expected first-attempt case, not an error). Any partial
95
+ * edits from a throttled prior attempt are intentionally discarded — the
96
+ * re-dispatch redoes the node from HEAD.
97
+ */
98
+ export function resetNodeWorktreeAndBranch(root, worktreePath, branchName) {
99
+ removeWorktree(root, worktreePath);
100
+ // Prune stale worktree admin entries (e.g. a dir deleted out from under git),
101
+ // otherwise `git worktree add` can refuse a path it still thinks is registered.
102
+ spawnSyncHidden("git", ["worktree", "prune"], { cwd: root, shell: false });
103
+ // Force-delete the leftover branch from a prior attempt so `-b` recreates it.
104
+ spawnSyncHidden("git", ["branch", "-D", branchName], { cwd: root, shell: false });
105
+ // Force-remove a leftover worktree DIRECTORY: when a prior attempt's worktree
106
+ // became an orphaned dir (registered admin entry gone but files remain),
107
+ // `git worktree remove` no-ops ("is not a working tree") and `git worktree add`
108
+ // then refuses because the path already exists. Deleting the dir makes the
109
+ // re-create succeed. Best-effort.
110
+ if (existsSync(worktreePath)) {
111
+ rmSync(worktreePath, { recursive: true, force: true });
112
+ }
113
+ }
114
+ /**
115
+ * Run each targeted command in the worktree directory. Returns pass/fail and
116
+ * combined output.
117
+ *
118
+ * `targeted_commands` are opaque host-authored command *strings* (e.g.
119
+ * `npm run build`, `grep -c '/packages/' .gitignore`, anything with pipes,
120
+ * quotes, or redirections), NOT pre-tokenized argv. They are run through the
121
+ * platform shell (`runShellCommand` → `spawnSync(..., { shell: true })`, the
122
+ * same path `close.ts` uses for `test_command`/`e2e_command`) so the shell — not
123
+ * a word-split + `spawnSync(shell:false)` — resolves the verb. That is what
124
+ * makes this OS-agnostic: on win32 `cmd.exe` natively execs `.cmd` shims (npm,
125
+ * npx, …) and resolves PATH commands; on darwin/linux `/bin/sh` does. The prior
126
+ * argv path ENOENT'd the *spawn itself* for any verb that wasn't a bare
127
+ * executable (e.g. `grep` on Windows), turning a correct fix into a phantom
128
+ * contract failure that burned the retry budget.
129
+ */
130
+ export function verifyNodeInWorktree(worktreePath, targetedCommands) {
131
+ const outputs = [];
132
+ for (const cmd of targetedCommands) {
133
+ const r = runShellCommand(cmd, {
134
+ cwd: worktreePath,
135
+ encoding: "utf8",
136
+ });
137
+ if (r.error) {
138
+ // Shell itself failed to spawn — surface it as a verify failure with the
139
+ // error text rather than a silent status.
140
+ outputs.push(`$ ${cmd}\n${r.error.message}`);
141
+ return { passed: false, output: outputs.join("\n---\n") };
142
+ }
143
+ const combined = [r.stdout ?? "", r.stderr ?? ""].filter(Boolean).join("\n");
144
+ outputs.push(`$ ${cmd}\n${combined}`);
145
+ if (r.status !== 0) {
146
+ return { passed: false, output: outputs.join("\n---\n") };
147
+ }
148
+ }
149
+ return { passed: true, output: outputs.join("\n---\n") };
150
+ }
151
+ /** Merge the worktree branch into the current HEAD via cherry-pick. On failure, removes the worktree and returns the error. */
152
+ export function mergeWorktree(root, worktreePath, branchName) {
153
+ // Get the tip commit of the worktree branch
154
+ const revResult = spawnSyncHidden("git", ["rev-parse", branchName], {
155
+ cwd: root,
156
+ encoding: "utf8",
157
+ shell: false,
158
+ });
159
+ if (revResult.status !== 0) {
160
+ const errMsg = (revResult.stderr ?? "").trim();
161
+ removeWorktree(root, worktreePath);
162
+ return { success: false, error: `Failed to resolve worktree branch ${branchName}: ${errMsg}` };
163
+ }
164
+ const worktreeTip = revResult.stdout.trim();
165
+ const mergeResult = spawnSyncHidden("git", ["cherry-pick", worktreeTip], {
166
+ cwd: root,
167
+ encoding: "utf8",
168
+ shell: false,
169
+ });
170
+ if (mergeResult.status !== 0) {
171
+ const stderr = (mergeResult.stderr ?? "").trim();
172
+ // Abort the cherry-pick so the main tree stays clean
173
+ spawnSyncHidden("git", ["cherry-pick", "--abort"], { cwd: root, shell: false });
174
+ removeWorktree(root, worktreePath);
175
+ return { success: false, error: `cherry-pick failed: ${stderr}` };
176
+ }
177
+ removeWorktree(root, worktreePath);
178
+ return { success: true };
179
+ }
180
+ /**
181
+ * Repo-relative tracked paths in the MAIN checkout that have uncommitted changes
182
+ * AND collide with a path the node's branch edits. A pre-existing dirty tracked
183
+ * file the cherry-pick would touch makes `git cherry-pick` abort with the opaque
184
+ * "Your local changes to the following files would be overwritten by merge" — a
185
+ * condition the node itself cannot fix and that identically re-fails every
186
+ * auto-retry (observed: a docs-only node routed to human triage over unrelated
187
+ * uncommitted WIP on the same file). Detected here so `acceptNodeWorktree` can
188
+ * surface the actionable cause (which file, commit-or-stash) instead of the raw
189
+ * git error. Best-effort: an unavailable diff / status probe returns `[]` (fall
190
+ * through to the normal cherry-pick, which reproduces the original behaviour).
191
+ */
192
+ export function dirtyMainTreeCollisions(root, branch) {
193
+ const edited = gitEditedFilesForBranch(root, branch);
194
+ if (!edited.available || edited.files.size === 0)
195
+ return [];
196
+ const status = spawnSyncHidden("git", ["status", "--porcelain", "--", ...edited.files], { cwd: root, encoding: "utf8", shell: false });
197
+ if (status.error || status.status !== 0)
198
+ return [];
199
+ const dirty = [];
200
+ for (const line of (status.stdout ?? "").split(/\r?\n/)) {
201
+ // Porcelain v1: two status chars + a space, then the path. Rename entries
202
+ // ("R old -> new") keep the destination after the arrow.
203
+ const raw = line.slice(3).trim();
204
+ if (raw.length === 0)
205
+ continue;
206
+ const arrow = raw.lastIndexOf(" -> ");
207
+ const p = (arrow >= 0 ? raw.slice(arrow + 4) : raw).replace(/\\/g, "/");
208
+ if (p.length > 0)
209
+ dirty.push(p);
210
+ }
211
+ return dirty;
212
+ }
213
+ /**
214
+ * Rebase a node's worktree branch onto the main checkout's current HEAD (the
215
+ * remediation branch tip) so a sibling that merged AFTER this worktree was created
216
+ * is folded in before this node verifies and merges. Additive edits to a shared
217
+ * file merge automatically (git's per-commit 3-way); a true hunk conflict is a
218
+ * genuine seam that aborts cleanly so the node routes to triage instead of landing
219
+ * a broken merge. The branch is checked out in the worktree, so the rebase runs
220
+ * there. A no-op (branch already on HEAD — the common, no-sibling-merged case)
221
+ * succeeds. Leaves the branch on its pre-rebase commit on abort (so the failed
222
+ * node's work can still be quarantined).
223
+ */
224
+ export function rebaseBranchOntoHead(root, worktreePath, branch) {
225
+ const head = spawnSyncHidden("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8", shell: false });
226
+ if (head.error || head.status !== 0) {
227
+ const detail = (head.stderr ?? head.error?.message ?? "git rev-parse failed").toString().trim();
228
+ return { ok: false, error: `could not resolve remediation HEAD for rebase: ${detail}` };
229
+ }
230
+ const target = head.stdout.trim();
231
+ const rebase = spawnSyncHidden("git", ["rebase", target], {
232
+ cwd: worktreePath,
233
+ encoding: "utf8",
234
+ shell: false,
235
+ });
236
+ if (rebase.error || rebase.status !== 0) {
237
+ const detail = [rebase.stdout ?? "", rebase.stderr ?? "", rebase.error?.message ?? ""]
238
+ .filter(Boolean)
239
+ .join("\n")
240
+ .trim();
241
+ // Leave a clean tree: abort the in-progress rebase before the worktree is dropped.
242
+ spawnSyncHidden("git", ["rebase", "--abort"], { cwd: worktreePath, shell: false });
243
+ return {
244
+ ok: false,
245
+ error: `rebase onto the current remediation HEAD conflicted (a real seam — two ` +
246
+ `nodes edited the same lines): ${detail}`,
247
+ };
248
+ }
249
+ return { ok: true };
250
+ }
251
+ /** Worktree path for a remediation block. */
252
+ export function worktreePath(root, blockId, runId) {
253
+ return join(root, ".audit-tools", "worktrees", `remediate-${blockId}-${runId}`);
254
+ }
255
+ export function remediationBranchName(runId) {
256
+ return `remediation/${refSafeSegment(runId, "run")}`;
257
+ }
258
+ /**
259
+ * Lock path for the base-mutating accept critical section (INV-2/CE-001), keyed on
260
+ * the base repo root + run's remediation branch. DISTINCT from the per-run
261
+ * `rolling-session.lock` (`withFileLock` is non-reentrant — an exclusive `wx`
262
+ * create — so the base lock MUST be a different path than the session lock
263
+ * `advanceHostRolling` already holds, or the nested acquire would self-deadlock).
264
+ * Both rolling drivers (host-subagent + in-process) serialize the rebase →
265
+ * cherry-pick → cross-package check → reset sequence through this single lock.
266
+ */
267
+ export function baseBranchLockPath(root, runId) {
268
+ return join(root, ".audit-tools", "remediation", "runs", refSafeSegment(runId, "run"), "base-branch.lock");
269
+ }
270
+ /** Durable ref under which a failed-but-committed node's commit is preserved. */
271
+ export function quarantineRef(runId, blockId) {
272
+ return `refs/remediation-quarantine/${refSafeSegment(runId, "run")}/${refSafeSegment(blockId, "node")}`;
273
+ }
274
+ /**
275
+ * Preserve a failed-but-committed node's work so it can never be lost. A node that
276
+ * committed real edits to its worktree branch but then failed verify / the
277
+ * write-scope gate / the cherry-pick is about to have its worktree removed and (on
278
+ * the next re-dispatch) its branch force-deleted — orphaning the commit. The dogfood
279
+ * lost a verified fix exactly this way (the worktree was pruned before recovery).
280
+ * Point a durable ref at the branch tip: a ref under refs/remediation-quarantine/
281
+ * survives `git branch -D` and `git worktree prune`, so the work stays reachable for
282
+ * a manual `git cherry-pick`. Best-effort; returns the ref + commit, or null.
283
+ */
284
+ export function quarantineFailedNodeCommit(root, branch, runId, blockId) {
285
+ const rev = spawnSyncHidden("git", ["rev-parse", "--verify", "--quiet", `${branch}^{commit}`], {
286
+ cwd: root,
287
+ encoding: "utf8",
288
+ shell: false,
289
+ });
290
+ if (rev.status !== 0)
291
+ return null;
292
+ const commit = (rev.stdout ?? "").trim();
293
+ const ref = quarantineRef(runId, blockId);
294
+ const upd = spawnSyncHidden("git", ["update-ref", ref, commit], {
295
+ cwd: root,
296
+ encoding: "utf8",
297
+ shell: false,
298
+ });
299
+ if (upd.status !== 0) {
300
+ process.stderr.write(`[remediate-code] could not quarantine ${branch}: ${(upd.stderr ?? "").trim()}\n`);
301
+ return null;
302
+ }
303
+ process.stderr.write(`[remediate-code] preserved failed node ${blockId} commit ${commit.slice(0, 8)} at ${ref} for recovery\n`);
304
+ return { ref, commit };
305
+ }
306
+ /**
307
+ * Preserve a failed-but-UNCOMMITTED node's worktree edits before the worktree is
308
+ * removed. The verify / scope / merge-fail paths quarantine an already-committed
309
+ * branch tip; this is the missing twin for the commit-REFUSAL path: when
310
+ * `commitWorktree` fails loudly (e.g. a generated artifact landed under the write
311
+ * scope — the genuine CE-003 fail-loud), the worker's real source edits are still
312
+ * sitting uncommitted in the worktree and would be destroyed by `removeWorktree`.
313
+ * Stage everything and land a preservation commit on the node's ISOLATED branch
314
+ * (never cherry-picked into main — it exists only so a durable quarantine ref can
315
+ * point at the otherwise-lost work), then quarantine that commit. A guard that
316
+ * destroys good work is worse than the bug it guards. Best-effort; returns the
317
+ * quarantine ref + commit, or null when there was nothing to preserve / it failed.
318
+ */
319
+ export function quarantineUncommittedWorktreeEdits(root, worktreeRoot, branch, runId, blockId) {
320
+ // Stage tracked modifications + new (non-ignored) source files. `git add -A`
321
+ // honours .gitignore, so the incidental ignored churn (node_modules/dist) and
322
+ // the offending generated artifact stay out — what we preserve is the worker's
323
+ // real source work.
324
+ const add = spawnSyncHidden("git", ["add", "-A"], {
325
+ cwd: worktreeRoot,
326
+ encoding: "utf8",
327
+ shell: false,
328
+ });
329
+ if (add.status !== 0)
330
+ return null;
331
+ // Nothing staged → no uncommitted edits to preserve.
332
+ const staged = spawnSyncHidden("git", ["diff", "--cached", "--quiet"], {
333
+ cwd: worktreeRoot,
334
+ shell: false,
335
+ });
336
+ if (staged.status === 0)
337
+ return null;
338
+ const commit = spawnSyncHidden("git", ["commit", "-m", `remediate-quarantine ${blockId} (${runId}) — fail-loud preserve`], { cwd: worktreeRoot, encoding: "utf8", shell: false });
339
+ if (commit.status !== 0) {
340
+ process.stderr.write(`[remediate-code] could not preserve uncommitted edits for ${blockId}: ${(commit.stderr ?? "").trim()}\n`);
341
+ return null;
342
+ }
343
+ return quarantineFailedNodeCommit(root, branch, runId, blockId);
344
+ }
345
+ /** Clear a node's quarantine ref (e.g. once a later re-dispatch landed successfully). Best-effort. */
346
+ export function clearQuarantinedCommit(root, runId, blockId) {
347
+ spawnSyncHidden("git", ["update-ref", "-d", quarantineRef(runId, blockId)], {
348
+ cwd: root,
349
+ encoding: "utf8",
350
+ shell: false,
351
+ });
352
+ }
353
+ /** Quarantined failed-node commits still preserved for a run, for recovery surfacing in the report. */
354
+ export function listQuarantinedCommits(root, runId) {
355
+ const prefix = `refs/remediation-quarantine/${refSafeSegment(runId, "run")}/`;
356
+ const res = spawnSyncHidden("git", ["for-each-ref", "--format=%(refname) %(objectname)", prefix], {
357
+ cwd: root,
358
+ encoding: "utf8",
359
+ shell: false,
360
+ });
361
+ if (res.status !== 0 || !res.stdout)
362
+ return [];
363
+ const out = [];
364
+ for (const line of res.stdout.split("\n")) {
365
+ const [ref, commit] = line.trim().split(/\s+/);
366
+ if (!ref || !commit)
367
+ continue;
368
+ out.push({ block: ref.slice(prefix.length), ref, commit });
369
+ }
370
+ return out;
371
+ }
372
+ /**
373
+ * Ensure the main checkout is on the dedicated remediation branch BEFORE any node
374
+ * commit is cherry-picked, so accepted work lands there and the user's base branch
375
+ * is NEVER modified — the run leaves a feature branch for review (it does not merge
376
+ * back). Idempotent across waves: creates the branch from the current HEAD (the base)
377
+ * the first time, checks it out on later waves. Best-effort on a non-git root (the
378
+ * worktree dispatch flow can't run there anyway): returns null without throwing so
379
+ * non-git callers/tests are unaffected. Returns the branch name on success.
380
+ */
381
+ /** Sidecar recording the base branch the run was launched from (B5). */
382
+ export function remediationBaseBranchPath(artifactsDir) {
383
+ return join(artifactsDir, "remediation-base-branch.json");
384
+ }
385
+ /**
386
+ * The base branch this run was launched from, recorded when the remediation
387
+ * branch was first created. `null` when unrecorded (e.g. a detached HEAD at
388
+ * launch, or a branch created by a prior run) — the opt-in merge-to-base
389
+ * closing action degrades to "merge manually" rather than guessing a target.
390
+ */
391
+ export function readRemediationBaseBranch(artifactsDir) {
392
+ try {
393
+ const parsed = JSON.parse(readFileSync(remediationBaseBranchPath(artifactsDir), "utf8"));
394
+ return typeof parsed?.base_branch === "string" && parsed.base_branch.length > 0
395
+ ? parsed.base_branch
396
+ : null;
397
+ }
398
+ catch {
399
+ return null;
400
+ }
401
+ }
402
+ export function ensureRemediationBranchCheckedOut(root, runId, artifactsDir) {
403
+ const top = gitTopLevel(root);
404
+ if (top === null || canonicalPathKey(top) !== canonicalPathKey(root))
405
+ return null;
406
+ const branch = remediationBranchName(runId);
407
+ const current = spawnSyncHidden("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
408
+ cwd: root,
409
+ encoding: "utf8",
410
+ shell: false,
411
+ });
412
+ if (current.status === 0 && (current.stdout ?? "").trim() === branch)
413
+ return branch;
414
+ const branchExisted = gitBranchExists(root, branch);
415
+ const args = branchExisted ? ["checkout", branch] : ["checkout", "-b", branch];
416
+ const co = spawnSyncHidden("git", args, { cwd: root, encoding: "utf8", shell: false });
417
+ if (co.status !== 0) {
418
+ process.stderr.write(`[remediate-code] could not switch to remediation branch ${branch}: ${(co.stderr ?? "").trim()}\n`);
419
+ return null;
420
+ }
421
+ // First creation: record the branch we came from as the merge-back target (B5).
422
+ // Only a real branch name — a detached HEAD reports "HEAD" and is skipped.
423
+ if (!branchExisted && artifactsDir) {
424
+ const base = current.status === 0 ? (current.stdout ?? "").trim() : "";
425
+ if (base && base !== "HEAD" && base !== branch) {
426
+ try {
427
+ writeFileSync(remediationBaseBranchPath(artifactsDir), JSON.stringify({ base_branch: base }, null, 2));
428
+ }
429
+ catch {
430
+ // best-effort: an unrecorded base degrades to manual merge, never throws
431
+ }
432
+ }
433
+ }
434
+ process.stderr.write(`[remediate-code] remediation changes land on branch ${branch} (base branch left untouched)\n`);
435
+ return branch;
436
+ }
437
+ /**
438
+ * Stage and commit all of a worktree's edits onto its branch. The TOOL owns this
439
+ * commit (never the worker/host) so that the branch has a real commit for two
440
+ * downstream invariants: `gitEditedFilesForBranch` (the write-scope ground truth,
441
+ * `HEAD...<branch>`) and `mergeWorktree`'s cherry-pick both operate on the worker's
442
+ * changes rather than an empty diff against HEAD. Gitignored paths (node_modules,
443
+ * .audit-tools artifacts, the result file written to the main artifacts dir) are
444
+ * excluded by `git add -A` honoring .gitignore, so the commit captures exactly the
445
+ * source edits. Returns `committed:false` (not an error) when the worker made no
446
+ * tracked edits — there is then nothing to verify or merge.
447
+ */
448
+ /**
449
+ * Source-file extensions a worker is allowed to CREATE as a brand-new untracked
450
+ * file that `.gitignore` happens to shadow. A new source file (a .ts the build
451
+ * compiles, a .js shim, a source .json fixture/config) is real work that MUST land
452
+ * even though `git add -A` honours `.gitignore` and would silently drop it
453
+ * (CE-003/CE-004 — the dogfood lost a gitignored friction-dir file exactly this
454
+ * way). A new
455
+ * file with any OTHER suffix under write scope is a GENERATED artifact (`.tsbuildinfo`,
456
+ * an emitted `.d.ts`, a coverage dump, …) — committing it would land a stale build
457
+ * output, so those FAIL LOUDLY rather than commit a half-change.
458
+ */
459
+ const SOURCE_NEW_FILE_EXTENSIONS = new Set([
460
+ ".ts",
461
+ ".tsx",
462
+ ".mts",
463
+ ".cts",
464
+ ".js",
465
+ ".jsx",
466
+ ".mjs",
467
+ ".cjs",
468
+ ".json",
469
+ ]);
470
+ /**
471
+ * A new untracked-ignored file is a SOURCE file (force-add it) iff its extension
472
+ * is in {@link SOURCE_NEW_FILE_EXTENSIONS} AND it is not a generated declaration
473
+ * (`.d.ts` / `.d.mts` / `.d.cts`) — a generated `.d.ts` has a `.ts` suffix but is
474
+ * build output, so it must be excluded from the source set.
475
+ */
476
+ function isSourceNewFile(rel) {
477
+ const lower = rel.toLowerCase();
478
+ if (/\.d\.[cm]?ts$/.test(lower))
479
+ return false;
480
+ const dot = lower.lastIndexOf(".");
481
+ if (dot < 0)
482
+ return false;
483
+ return SOURCE_NEW_FILE_EXTENSIONS.has(lower.slice(dot));
484
+ }
485
+ /**
486
+ * Whether a repo-relative path falls UNDER one of the block's declared write
487
+ * paths. A declared path matches a new file when it is the file itself, a
488
+ * directory prefix of it (declared `src/x` matches `src/x/new.ts`), or a glob
489
+ * whose pre-wildcard directory prefix contains it (a leading-wildcard glob like
490
+ * a friction-dir pattern matches `src/a/friction/new.ts`). Declared paths and the
491
+ * candidate are normalized to
492
+ * repo-relative forward-slash form so the comparison is OS-agnostic.
493
+ */
494
+ function isUnderWritePaths(rel, declaredWritePaths, root) {
495
+ const target = toRepoRelative(rel, root);
496
+ for (const raw of declaredWritePaths) {
497
+ const declared = toRepoRelative(raw, root);
498
+ if (declared === target)
499
+ return true;
500
+ // Glob: reduce a wildcard pattern to the leading literal dir segment(s)
501
+ // before the first wildcard and treat that as a containing prefix.
502
+ const wildcard = declared.search(/[*?[]/);
503
+ const literalPrefix = (wildcard >= 0 ? declared.slice(0, wildcard) : declared)
504
+ .replace(/\/+$/, "");
505
+ if (literalPrefix.length === 0) {
506
+ // A leading-wildcard glob (a friction-dir style pattern) — match on the
507
+ // trailing literal segment appearing anywhere in the candidate's path.
508
+ const trailing = declared.replace(/^[*?/]+/, "").replace(/\/+$/, "");
509
+ if (trailing.length > 0 && (target === trailing || target.includes(`/${trailing}/`) || target.includes(`${trailing}/`))) {
510
+ return true;
511
+ }
512
+ continue;
513
+ }
514
+ if (target === literalPrefix || target.startsWith(`${literalPrefix}/`))
515
+ return true;
516
+ }
517
+ return false;
518
+ }
519
+ /**
520
+ * Force-add worker-created NEW files that `.gitignore` shadows so genuine new
521
+ * source work lands in the commit. `git add -A` (and any diff) honour `.gitignore`
522
+ * and can ONLY be enumerated via `git ls-files --others --ignored
523
+ * --exclude-standard`. Only files UNDER the block's declared write scope are
524
+ * considered: a source-extension one is `git add -f`'d so it lands; a non-source
525
+ * one (generated artifact under a source dir) FAILS LOUDLY rather than committing
526
+ * build output. An untracked-ignored file OUTSIDE the write scope is incidental
527
+ * churn (node_modules created by running npm in the worktree, dist/, .audit-tools/,
528
+ * another module's output) — `git add -A` already skips it, so it is skipped here
529
+ * too (it must NOT trip a fail-loud; doing so falsely rejected every node whose
530
+ * worker ran npm in its worktree). `declaredWritePaths` undefined → the lifecycle
531
+ * unit-test path with no scope: skip force-add entirely (legacy behaviour).
532
+ */
533
+ function forceAddNewSourceFiles(worktreeRoot, declaredWritePaths) {
534
+ if (declaredWritePaths === undefined)
535
+ return { ok: true };
536
+ const ls = spawnSyncHidden("git", ["ls-files", "--others", "--ignored", "--exclude-standard"], { cwd: worktreeRoot, encoding: "utf8", shell: false });
537
+ if (ls.error || ls.status !== 0) {
538
+ return {
539
+ ok: false,
540
+ error: `git ls-files (untracked-ignored enumeration) failed: ${(ls.stderr ?? ls.error?.message ?? "").toString().trim()}`,
541
+ };
542
+ }
543
+ const newFiles = (ls.stdout ?? "")
544
+ .split(/\r?\n/)
545
+ .map((l) => l.trim().replace(/\\/g, "/"))
546
+ .filter((l) => l.length > 0);
547
+ const toForceAdd = [];
548
+ for (const rel of newFiles) {
549
+ // Only files UNDER the node's declared write scope are candidates. An
550
+ // untracked-ignored file OUTSIDE the write scope is incidental churn the
551
+ // worker did not author as part of this change — node_modules (e.g.
552
+ // `node_modules/.bin/esbuild` created by running npm/vitest in the worktree),
553
+ // `dist/`, `.audit-tools/`, or another module's output. `git add -A` already
554
+ // skips all of it; it must NOT trip a fail-loud (doing so falsely rejected
555
+ // every node whose worker ran npm in its worktree). Skip it.
556
+ if (!isUnderWritePaths(rel, declaredWritePaths, worktreeRoot))
557
+ continue;
558
+ if (isSourceNewFile(rel)) {
559
+ toForceAdd.push(rel);
560
+ continue;
561
+ }
562
+ // Under the declared write scope but NOT a source extension: a generated
563
+ // artifact (e.g. a `.tsbuildinfo` / generated `.d.ts`) a worker dropped under
564
+ // a source dir. This is the genuine CE-003 case — fail loudly rather than
565
+ // committing build output.
566
+ return {
567
+ ok: false,
568
+ error: `Worker created a new non-source (generated) file under its write scope: ${rel}. ` +
569
+ `Only source-extension new files are committed; a generated artifact must not land. ` +
570
+ `Refusing to commit a half-change.`,
571
+ };
572
+ }
573
+ for (const rel of toForceAdd) {
574
+ const add = spawnSyncHidden("git", ["add", "-f", "--", rel], {
575
+ cwd: worktreeRoot,
576
+ encoding: "utf8",
577
+ shell: false,
578
+ });
579
+ if (add.status !== 0) {
580
+ return { ok: false, error: `git add -f ${rel} failed: ${(add.stderr ?? "").trim()}` };
581
+ }
582
+ }
583
+ return { ok: true };
584
+ }
585
+ export function commitWorktree(worktreeRoot, message, declaredWritePaths) {
586
+ // Force-add worker-created new SOURCE files that `.gitignore` shadows (and fail
587
+ // loudly on a generated-artifact / out-of-scope new file) BEFORE `git add -A`,
588
+ // which on its own silently drops every untracked-ignored path (CE-003/CE-004).
589
+ const forced = forceAddNewSourceFiles(worktreeRoot, declaredWritePaths);
590
+ if (!forced.ok) {
591
+ return { committed: false, error: forced.error };
592
+ }
593
+ const add = spawnSyncHidden("git", ["add", "-A"], {
594
+ cwd: worktreeRoot,
595
+ encoding: "utf8",
596
+ shell: false,
597
+ });
598
+ if (add.status !== 0) {
599
+ return { committed: false, error: `git add failed: ${(add.stderr ?? "").trim()}` };
600
+ }
601
+ // `git diff --cached --quiet` exits 0 when nothing is staged → no worker edits.
602
+ const staged = spawnSyncHidden("git", ["diff", "--cached", "--quiet"], {
603
+ cwd: worktreeRoot,
604
+ shell: false,
605
+ });
606
+ if (staged.status === 0) {
607
+ return { committed: false };
608
+ }
609
+ const commit = spawnSyncHidden("git", ["commit", "-m", message], {
610
+ cwd: worktreeRoot,
611
+ encoding: "utf8",
612
+ shell: false,
613
+ });
614
+ if (commit.status !== 0) {
615
+ return { committed: false, error: `git commit failed: ${(commit.stderr ?? "").trim()}` };
616
+ }
617
+ return { committed: true };
618
+ }
619
+ /**
620
+ * Make the main checkout's installed `node_modules` available to a worktree. A
621
+ * fresh `git worktree add` checks out only tracked files, and `node_modules` is
622
+ * gitignored, so per-node verify commands (`npm run check`, focused tests) would
623
+ * otherwise fail with missing dependencies. Best-effort junction/symlink to the
624
+ * main root's `node_modules`; on failure it logs and the verify step surfaces the
625
+ * missing-deps error rather than crashing the dispatch. NOTE: workspace package
626
+ * symlinks inside `node_modules/@audit-tools/*` point back into the MAIN checkout,
627
+ * so cross-package runtime resolution sees the main tree — the authoritative
628
+ * cross-package re-check is the central post-merge build/gate, not this fast
629
+ * per-node verify (which gates obvious breakage early).
630
+ */
631
+ export function ensureWorktreeNodeModules(mainRoot, worktreeRoot) {
632
+ const target = join(mainRoot, "node_modules");
633
+ const link = join(worktreeRoot, "node_modules");
634
+ if (!existsSync(target) || existsSync(link))
635
+ return;
636
+ try {
637
+ symlinkSync(target, link, "junction");
638
+ }
639
+ catch (err) {
640
+ process.stderr.write(`[remediate-code] worktree node_modules link failed (${worktreeRoot}): ${String(err)}\n`);
641
+ }
642
+ }
643
+ //# sourceMappingURL=worktreeLifecycle.js.map