@cluesmith/codev 3.3.2 → 3.3.4

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 (257) hide show
  1. package/dashboard-dist/assets/{index-D3XaKpZW.js → index-DZf6pNPE.js} +41 -41
  2. package/dashboard-dist/assets/index-DZf6pNPE.js.map +1 -0
  3. package/dashboard-dist/assets/index-DnYLy5C_.css +32 -0
  4. package/dashboard-dist/index.html +2 -2
  5. package/dist/agent-farm/cli.d.ts.map +1 -1
  6. package/dist/agent-farm/cli.js +48 -0
  7. package/dist/agent-farm/cli.js.map +1 -1
  8. package/dist/agent-farm/commands/ide.d.ts +20 -0
  9. package/dist/agent-farm/commands/ide.d.ts.map +1 -0
  10. package/dist/agent-farm/commands/ide.js +138 -0
  11. package/dist/agent-farm/commands/ide.js.map +1 -0
  12. package/dist/agent-farm/commands/inbox.d.ts.map +1 -1
  13. package/dist/agent-farm/commands/inbox.js +79 -10
  14. package/dist/agent-farm/commands/inbox.js.map +1 -1
  15. package/dist/agent-farm/commands/interrupt.d.ts +4 -1
  16. package/dist/agent-farm/commands/interrupt.d.ts.map +1 -1
  17. package/dist/agent-farm/commands/interrupt.js +6 -3
  18. package/dist/agent-farm/commands/interrupt.js.map +1 -1
  19. package/dist/agent-farm/commands/reset.d.ts +4 -1
  20. package/dist/agent-farm/commands/reset.d.ts.map +1 -1
  21. package/dist/agent-farm/commands/reset.js +6 -3
  22. package/dist/agent-farm/commands/reset.js.map +1 -1
  23. package/dist/agent-farm/commands/send.d.ts +28 -0
  24. package/dist/agent-farm/commands/send.d.ts.map +1 -1
  25. package/dist/agent-farm/commands/send.js +79 -6
  26. package/dist/agent-farm/commands/send.js.map +1 -1
  27. package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
  28. package/dist/agent-farm/commands/spawn-worktree.js +16 -7
  29. package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
  30. package/dist/agent-farm/commands/tower.d.ts +21 -9
  31. package/dist/agent-farm/commands/tower.d.ts.map +1 -1
  32. package/dist/agent-farm/commands/tower.js +90 -30
  33. package/dist/agent-farm/commands/tower.js.map +1 -1
  34. package/dist/agent-farm/db/index.d.ts +3 -1
  35. package/dist/agent-farm/db/index.d.ts.map +1 -1
  36. package/dist/agent-farm/db/index.js +8 -443
  37. package/dist/agent-farm/db/index.js.map +1 -1
  38. package/dist/agent-farm/db/mailbox.d.ts +47 -7
  39. package/dist/agent-farm/db/mailbox.d.ts.map +1 -1
  40. package/dist/agent-farm/db/mailbox.js +56 -12
  41. package/dist/agent-farm/db/mailbox.js.map +1 -1
  42. package/dist/agent-farm/db/migrations.d.ts +57 -0
  43. package/dist/agent-farm/db/migrations.d.ts.map +1 -0
  44. package/dist/agent-farm/db/migrations.js +530 -0
  45. package/dist/agent-farm/db/migrations.js.map +1 -0
  46. package/dist/agent-farm/db/schema.d.ts +1 -1
  47. package/dist/agent-farm/db/schema.d.ts.map +1 -1
  48. package/dist/agent-farm/db/schema.js +1 -0
  49. package/dist/agent-farm/db/schema.js.map +1 -1
  50. package/dist/agent-farm/db/tower-owner.d.ts +168 -0
  51. package/dist/agent-farm/db/tower-owner.d.ts.map +1 -0
  52. package/dist/agent-farm/db/tower-owner.js +369 -0
  53. package/dist/agent-farm/db/tower-owner.js.map +1 -0
  54. package/dist/agent-farm/db/types.d.ts +47 -0
  55. package/dist/agent-farm/db/types.d.ts.map +1 -1
  56. package/dist/agent-farm/db/types.js.map +1 -1
  57. package/dist/agent-farm/lib/ide-record.d.ts +65 -0
  58. package/dist/agent-farm/lib/ide-record.d.ts.map +1 -0
  59. package/dist/agent-farm/lib/ide-record.js +118 -0
  60. package/dist/agent-farm/lib/ide-record.js.map +1 -0
  61. package/dist/agent-farm/lib/tunnel-client.d.ts +22 -0
  62. package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -1
  63. package/dist/agent-farm/lib/tunnel-client.js +96 -19
  64. package/dist/agent-farm/lib/tunnel-client.js.map +1 -1
  65. package/dist/agent-farm/servers/cron-delivery.d.ts +7 -1
  66. package/dist/agent-farm/servers/cron-delivery.d.ts.map +1 -1
  67. package/dist/agent-farm/servers/cron-delivery.js +2 -1
  68. package/dist/agent-farm/servers/cron-delivery.js.map +1 -1
  69. package/dist/agent-farm/servers/gate-profiles.d.ts +44 -2
  70. package/dist/agent-farm/servers/gate-profiles.d.ts.map +1 -1
  71. package/dist/agent-farm/servers/gate-profiles.js +79 -6
  72. package/dist/agent-farm/servers/gate-profiles.js.map +1 -1
  73. package/dist/agent-farm/servers/ide-forward.d.ts +62 -0
  74. package/dist/agent-farm/servers/ide-forward.d.ts.map +1 -0
  75. package/dist/agent-farm/servers/ide-forward.js +274 -0
  76. package/dist/agent-farm/servers/ide-forward.js.map +1 -0
  77. package/dist/agent-farm/servers/ide-server.d.ts +68 -0
  78. package/dist/agent-farm/servers/ide-server.d.ts.map +1 -0
  79. package/dist/agent-farm/servers/ide-server.js +281 -0
  80. package/dist/agent-farm/servers/ide-server.js.map +1 -0
  81. package/dist/agent-farm/servers/mailbox-delivery.d.ts +296 -12
  82. package/dist/agent-farm/servers/mailbox-delivery.d.ts.map +1 -1
  83. package/dist/agent-farm/servers/mailbox-delivery.js +566 -69
  84. package/dist/agent-farm/servers/mailbox-delivery.js.map +1 -1
  85. package/dist/agent-farm/servers/mailbox-wiring.d.ts +66 -19
  86. package/dist/agent-farm/servers/mailbox-wiring.d.ts.map +1 -1
  87. package/dist/agent-farm/servers/mailbox-wiring.js +204 -37
  88. package/dist/agent-farm/servers/mailbox-wiring.js.map +1 -1
  89. package/dist/agent-farm/servers/message-write.d.ts +138 -11
  90. package/dist/agent-farm/servers/message-write.d.ts.map +1 -1
  91. package/dist/agent-farm/servers/message-write.js +144 -35
  92. package/dist/agent-farm/servers/message-write.js.map +1 -1
  93. package/dist/agent-farm/servers/overview.d.ts +28 -12
  94. package/dist/agent-farm/servers/overview.d.ts.map +1 -1
  95. package/dist/agent-farm/servers/overview.js +130 -79
  96. package/dist/agent-farm/servers/overview.js.map +1 -1
  97. package/dist/agent-farm/servers/render-gate.d.ts +77 -1
  98. package/dist/agent-farm/servers/render-gate.d.ts.map +1 -1
  99. package/dist/agent-farm/servers/render-gate.js +174 -25
  100. package/dist/agent-farm/servers/render-gate.js.map +1 -1
  101. package/dist/agent-farm/servers/session-submit.d.ts +7 -4
  102. package/dist/agent-farm/servers/session-submit.d.ts.map +1 -1
  103. package/dist/agent-farm/servers/session-submit.js +7 -4
  104. package/dist/agent-farm/servers/session-submit.js.map +1 -1
  105. package/dist/agent-farm/servers/tower-cron.d.ts.map +1 -1
  106. package/dist/agent-farm/servers/tower-cron.js +7 -3
  107. package/dist/agent-farm/servers/tower-cron.js.map +1 -1
  108. package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
  109. package/dist/agent-farm/servers/tower-instances.js +17 -10
  110. package/dist/agent-farm/servers/tower-instances.js.map +1 -1
  111. package/dist/agent-farm/servers/tower-routes.d.ts +32 -0
  112. package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
  113. package/dist/agent-farm/servers/tower-routes.js +186 -26
  114. package/dist/agent-farm/servers/tower-routes.js.map +1 -1
  115. package/dist/agent-farm/servers/tower-server.js +79 -9
  116. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  117. package/dist/agent-farm/servers/tower-terminals.d.ts +21 -0
  118. package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
  119. package/dist/agent-farm/servers/tower-terminals.js +106 -13
  120. package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
  121. package/dist/agent-farm/servers/tower-utils.d.ts +35 -0
  122. package/dist/agent-farm/servers/tower-utils.d.ts.map +1 -1
  123. package/dist/agent-farm/servers/tower-utils.js +39 -0
  124. package/dist/agent-farm/servers/tower-utils.js.map +1 -1
  125. package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -1
  126. package/dist/agent-farm/servers/tower-websocket.js +26 -2
  127. package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
  128. package/dist/agent-farm/utils/message-format.d.ts +78 -2
  129. package/dist/agent-farm/utils/message-format.d.ts.map +1 -1
  130. package/dist/agent-farm/utils/message-format.js +107 -11
  131. package/dist/agent-farm/utils/message-format.js.map +1 -1
  132. package/dist/agent-farm/utils/port.d.ts +22 -0
  133. package/dist/agent-farm/utils/port.d.ts.map +1 -0
  134. package/dist/agent-farm/utils/port.js +35 -0
  135. package/dist/agent-farm/utils/port.js.map +1 -0
  136. package/dist/agent-farm/utils/server-utils.d.ts +26 -0
  137. package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
  138. package/dist/agent-farm/utils/server-utils.js +36 -0
  139. package/dist/agent-farm/utils/server-utils.js.map +1 -1
  140. package/dist/cli.d.ts.map +1 -1
  141. package/dist/cli.js +3 -1
  142. package/dist/cli.js.map +1 -1
  143. package/dist/commands/consult/index.d.ts.map +1 -1
  144. package/dist/commands/consult/index.js +61 -1
  145. package/dist/commands/consult/index.js.map +1 -1
  146. package/dist/commands/consult/tree-tripwire.d.ts +177 -0
  147. package/dist/commands/consult/tree-tripwire.d.ts.map +1 -0
  148. package/dist/commands/consult/tree-tripwire.js +356 -0
  149. package/dist/commands/consult/tree-tripwire.js.map +1 -0
  150. package/dist/commands/doctor.d.ts +69 -0
  151. package/dist/commands/doctor.d.ts.map +1 -1
  152. package/dist/commands/doctor.js +165 -2
  153. package/dist/commands/doctor.js.map +1 -1
  154. package/dist/commands/generate-image.d.ts +25 -1
  155. package/dist/commands/generate-image.d.ts.map +1 -1
  156. package/dist/commands/generate-image.js +410 -2
  157. package/dist/commands/generate-image.js.map +1 -1
  158. package/dist/commands/porch/prompts.d.ts.map +1 -1
  159. package/dist/commands/porch/prompts.js +18 -2
  160. package/dist/commands/porch/prompts.js.map +1 -1
  161. package/dist/lib/agent-env.d.ts +86 -0
  162. package/dist/lib/agent-env.d.ts.map +1 -0
  163. package/dist/lib/agent-env.js +126 -0
  164. package/dist/lib/agent-env.js.map +1 -0
  165. package/dist/lib/forge-contracts.d.ts +76 -5
  166. package/dist/lib/forge-contracts.d.ts.map +1 -1
  167. package/dist/lib/forge-contracts.js.map +1 -1
  168. package/dist/lib/forge-rate-limit.d.ts +45 -0
  169. package/dist/lib/forge-rate-limit.d.ts.map +1 -0
  170. package/dist/lib/forge-rate-limit.js +89 -0
  171. package/dist/lib/forge-rate-limit.js.map +1 -0
  172. package/dist/lib/forge.d.ts +29 -1
  173. package/dist/lib/forge.d.ts.map +1 -1
  174. package/dist/lib/forge.js +96 -21
  175. package/dist/lib/forge.js.map +1 -1
  176. package/dist/lib/github.d.ts +13 -1
  177. package/dist/lib/github.d.ts.map +1 -1
  178. package/dist/lib/github.js +58 -15
  179. package/dist/lib/github.js.map +1 -1
  180. package/dist/lib/test-env.d.ts +17 -0
  181. package/dist/lib/test-env.d.ts.map +1 -1
  182. package/dist/lib/test-env.js +20 -0
  183. package/dist/lib/test-env.js.map +1 -1
  184. package/dist/terminal/pty-manager.d.ts +10 -1
  185. package/dist/terminal/pty-manager.d.ts.map +1 -1
  186. package/dist/terminal/pty-manager.js +38 -9
  187. package/dist/terminal/pty-manager.js.map +1 -1
  188. package/dist/terminal/pty-session.d.ts +174 -8
  189. package/dist/terminal/pty-session.d.ts.map +1 -1
  190. package/dist/terminal/pty-session.js +299 -25
  191. package/dist/terminal/pty-session.js.map +1 -1
  192. package/dist/terminal/session-manager.d.ts +23 -2
  193. package/dist/terminal/session-manager.d.ts.map +1 -1
  194. package/dist/terminal/session-manager.js +115 -36
  195. package/dist/terminal/session-manager.js.map +1 -1
  196. package/dist/terminal/session-screen.d.ts +34 -5
  197. package/dist/terminal/session-screen.d.ts.map +1 -1
  198. package/dist/terminal/session-screen.js +52 -6
  199. package/dist/terminal/session-screen.js.map +1 -1
  200. package/dist/terminal/shellper-client.d.ts +73 -0
  201. package/dist/terminal/shellper-client.d.ts.map +1 -1
  202. package/dist/terminal/shellper-client.js +151 -0
  203. package/dist/terminal/shellper-client.js.map +1 -1
  204. package/dist/terminal/shellper-main.js +20 -3
  205. package/dist/terminal/shellper-main.js.map +1 -1
  206. package/dist/terminal/shellper-process.d.ts +2 -0
  207. package/dist/terminal/shellper-process.d.ts.map +1 -1
  208. package/dist/terminal/shellper-process.js +17 -1
  209. package/dist/terminal/shellper-process.js.map +1 -1
  210. package/dist/terminal/shellper-protocol.d.ts +28 -0
  211. package/dist/terminal/shellper-protocol.d.ts.map +1 -1
  212. package/dist/terminal/shellper-protocol.js.map +1 -1
  213. package/dist/terminal/terminal-replies.d.ts +102 -0
  214. package/dist/terminal/terminal-replies.d.ts.map +1 -0
  215. package/dist/terminal/terminal-replies.js +181 -0
  216. package/dist/terminal/terminal-replies.js.map +1 -0
  217. package/package.json +4 -4
  218. package/scripts/forge/gitea/_lib.sh +214 -0
  219. package/scripts/forge/gitea/issue-comment.sh +8 -1
  220. package/scripts/forge/gitea/issue-view.sh +95 -4
  221. package/scripts/forge/gitea/pr-create.sh +155 -0
  222. package/scripts/forge/gitea/pr-exists.sh +32 -4
  223. package/scripts/forge/gitea/pr-list.sh +40 -31
  224. package/scripts/forge/gitea/pr-view.sh +65 -4
  225. package/scripts/forge/gitea/recently-merged.sh +91 -21
  226. package/scripts/forge/gitea/user-identity.sh +30 -1
  227. package/scripts/forge/github/issue-view.sh +5 -2
  228. package/scripts/forge/github/pr-create.sh +47 -0
  229. package/scripts/forge/github/pr-list.sh +6 -2
  230. package/scripts/forge/github/pr-search.sh +6 -2
  231. package/scripts/forge/gitlab/issue-search.sh +1 -0
  232. package/scripts/forge/gitlab/issue-view.sh +24 -4
  233. package/scripts/forge/gitlab/pr-create.sh +53 -0
  234. package/scripts/forge/gitlab/pr-search.sh +37 -2
  235. package/scripts/forge/linear/auth-status.sh +1 -0
  236. package/scripts/forge/linear/issue-comment.sh +1 -0
  237. package/scripts/forge/linear/issue-list.sh +1 -0
  238. package/scripts/forge/linear/issue-search.sh +1 -0
  239. package/scripts/forge/linear/issue-view.sh +14 -2
  240. package/scripts/forge/linear/recently-closed.sh +1 -0
  241. package/scripts/forge/linear/user-identity.sh +1 -0
  242. package/skeleton/.claude/skills/generate-image/SKILL.md +39 -1
  243. package/skeleton/.codex/skills/generate-image/SKILL.md +39 -1
  244. package/skeleton/protocols/air/prompts/pr.md +6 -1
  245. package/skeleton/protocols/aspir/prompts/review.md +6 -1
  246. package/skeleton/protocols/bugfix/prompts/pr.md +6 -1
  247. package/skeleton/protocols/bugfix/protocol.md +2 -1
  248. package/skeleton/protocols/maintain/prompts/review.md +6 -1
  249. package/skeleton/protocols/pir/prompts/review.md +10 -7
  250. package/skeleton/protocols/spir/prompts/review.md +6 -1
  251. package/skeleton/resources/commands/agent-farm.md +28 -3
  252. package/skeleton/roles/consultant.md +32 -0
  253. package/skeleton/templates/AGENTS.md +1 -1
  254. package/skeleton/templates/CLAUDE.md +1 -1
  255. package/templates/tower.html +16 -1
  256. package/dashboard-dist/assets/index-BZRTEMtH.css +0 -32
  257. package/dashboard-dist/assets/index-D3XaKpZW.js.map +0 -1
@@ -0,0 +1,356 @@
1
+ /**
2
+ * Working-tree tripwire for consultation lanes (#1649).
3
+ *
4
+ * A reviewer is supposed to read the tree, never write to it. `consultant.md`
5
+ * now says so explicitly, but instruction is a probabilistic control: the claude
6
+ * lane runs with `permissionMode: 'bypassPermissions'`, so `Edit`/`Write`/`Bash`
7
+ * remain *reachable* even though nothing asks for them. In #1649 a review lane
8
+ * reverted three guards in a builder's worktree to check whether the tests were
9
+ * vacuous, and the only reason anyone found out is that the builder happened to
10
+ * diff against HEAD afterwards.
11
+ *
12
+ * This module is the detection half. It snapshots the tree before a lane runs
13
+ * and again after, and reports what moved. It does not block anything — its job
14
+ * is to turn a silent mutation into a loud one.
15
+ *
16
+ * ## Why not just diff `git status --porcelain` output
17
+ *
18
+ * Because the interesting case is a tree that was *already dirty*. A builder
19
+ * mid-phase has uncommitted work; porcelain reports ` M src/foo.ts` before the
20
+ * review and ` M src/foo.ts` after it, identical strings, while the file's
21
+ * contents changed underneath. So the snapshot pairs each path porcelain
22
+ * reports with a hash of its contents on disk.
23
+ *
24
+ * ## What it cannot see
25
+ *
26
+ * Files git ignores. That is deliberate — `.consult/` and `codev/projects/​*​/*.txt`
27
+ * are ignored, which is exactly why consult's own review output does not trip
28
+ * its own wire — but it does mean a lane writing into an ignored path goes
29
+ * unnoticed. Naming the tree under review is the goal here, not sandboxing.
30
+ *
31
+ * Edits *inside* a submodule, for the same reason: `git status` reports the
32
+ * submodule as one directory entry, so a changed file within it moves nothing
33
+ * this module can see.
34
+ *
35
+ * And an edit the lane restores byte-for-byte, which by construction leaves a
36
+ * before/after comparison nothing to compare. `consultant.md` is the only
37
+ * control for that case; there is a test asserting this silence, so the limit is
38
+ * pinned rather than assumed.
39
+ */
40
+ import * as fs from 'node:fs';
41
+ import * as path from 'node:path';
42
+ import * as crypto from 'node:crypto';
43
+ import { execFileSync } from 'node:child_process';
44
+ const NO_IDENTITY = { head: null, branch: null };
45
+ const UNAVAILABLE = (reason) => ({
46
+ available: false,
47
+ reason,
48
+ entries: new Map(),
49
+ identity: NO_IDENTITY,
50
+ });
51
+ /**
52
+ * Read HEAD and the branch name in one `git rev-parse`.
53
+ *
54
+ * Failure is not fatal to the snapshot. A freshly `git init`ed repository has no
55
+ * HEAD to resolve, and the file-level half of the tripwire works there perfectly
56
+ * well; refusing to watch the tree because the repo has no commits would trade a
57
+ * real check for a missing one.
58
+ */
59
+ function readIdentity(workspaceRoot) {
60
+ try {
61
+ const raw = execFileSync('git', ['rev-parse', 'HEAD', '--abbrev-ref', 'HEAD'], {
62
+ cwd: workspaceRoot,
63
+ encoding: 'utf-8',
64
+ stdio: ['ignore', 'pipe', 'pipe'],
65
+ });
66
+ if (typeof raw !== 'string')
67
+ return NO_IDENTITY;
68
+ const [head, branch] = raw.trim().split('\n');
69
+ return { head: head || null, branch: branch || null };
70
+ }
71
+ catch {
72
+ return NO_IDENTITY;
73
+ }
74
+ }
75
+ /**
76
+ * Hash a working-tree file, or mark it absent.
77
+ *
78
+ * Absent is a real state, not an error: a path can appear in porcelain output
79
+ * as a staged deletion, and a lane that *deletes* a file is precisely what we
80
+ * are watching for.
81
+ */
82
+ function hashPath(absolute) {
83
+ try {
84
+ const stat = fs.lstatSync(absolute);
85
+ if (!stat.isFile())
86
+ return `<${stat.isDirectory() ? 'dir' : 'special'}>`;
87
+ return crypto.createHash('sha256').update(fs.readFileSync(absolute)).digest('hex');
88
+ }
89
+ catch {
90
+ return '<absent>';
91
+ }
92
+ }
93
+ /**
94
+ * Snapshot the working tree of `workspaceRoot`.
95
+ *
96
+ * `-z` (NUL-separated) rather than plain `--porcelain` because git C-quotes
97
+ * paths containing spaces or non-ASCII in the human-readable form, and a quoted
98
+ * path cannot be handed back to `fs.readFileSync`. `--untracked-files=all` so
99
+ * a newly created file is listed individually instead of being collapsed into
100
+ * a single `?? dir/` entry.
101
+ *
102
+ * Records HEAD and the branch alongside the paths, so a lane that commits or
103
+ * switches refs is caught even when no file ends up looking different.
104
+ */
105
+ export function snapshotTree(workspaceRoot) {
106
+ // Typed `unknown` rather than `string` so the guard below is a real check and
107
+ // not a comparison TypeScript has already narrowed away.
108
+ let raw;
109
+ try {
110
+ raw = execFileSync('git', ['status', '--porcelain', '-z', '--untracked-files=all'], {
111
+ cwd: workspaceRoot,
112
+ encoding: 'utf-8',
113
+ maxBuffer: 32 * 1024 * 1024,
114
+ // Capture git's stderr instead of inheriting it. Otherwise a workspace
115
+ // that is not a repository prints a bare `fatal: not a git repository`
116
+ // over the top of the message we actually mean to show.
117
+ stdio: ['ignore', 'pipe', 'pipe'],
118
+ });
119
+ }
120
+ catch (err) {
121
+ return UNAVAILABLE(err instanceof Error ? err.message.split('\n')[0] : String(err));
122
+ }
123
+ // The tripwire is a diagnostic wrapped around every lane, so it must never be
124
+ // the thing that breaks a consultation. If `git status` came back as anything
125
+ // other than text — a stubbed `execFileSync` under a test, a Buffer if the
126
+ // encoding option is ever dropped — report the snapshot as unavailable rather
127
+ // than throwing out of the wrapper and taking the review down with it.
128
+ if (typeof raw !== 'string') {
129
+ return UNAVAILABLE(`git status returned ${typeof raw}, not text`);
130
+ }
131
+ const entries = new Map();
132
+ // Each record is `XY <path>`; rename/copy records are followed by a second
133
+ // NUL-terminated field holding the ORIGINAL path, which must be consumed so
134
+ // it is not mistaken for the next record's status code.
135
+ const fields = raw.split('\0').filter((f) => f.length > 0);
136
+ for (let i = 0; i < fields.length; i++) {
137
+ const field = fields[i];
138
+ const status = field.slice(0, 2);
139
+ const rel = field.slice(3);
140
+ if (!rel)
141
+ continue;
142
+ entries.set(rel, `${status}:${hashPath(path.join(workspaceRoot, rel))}`);
143
+ if (status[0] === 'R' || status[0] === 'C') {
144
+ const origin = fields[++i];
145
+ if (origin !== undefined) {
146
+ entries.set(origin, `${status}-from:${hashPath(path.join(workspaceRoot, origin))}`);
147
+ }
148
+ }
149
+ }
150
+ return { available: true, entries, identity: readIdentity(workspaceRoot) };
151
+ }
152
+ /**
153
+ * Paths `consult` writes itself on every run, and which therefore are not
154
+ * evidence of anything.
155
+ *
156
+ * `.consult/history.log` is appended by `logQuery()` after the lane returns, so
157
+ * it lands inside the window this module measures. In this repo `.consult/` is
158
+ * gitignored and never surfaces — which is exactly why it was not obvious. A
159
+ * live run against a scratch repo without that entry reported the log file as a
160
+ * mutation on the very first try. An adopter's `.gitignore` is not something to
161
+ * depend on for correctness here.
162
+ */
163
+ const CONSULT_OWNED_PREFIXES = ['.consult/'];
164
+ /**
165
+ * Review files written by consultation lanes, which are not evidence either.
166
+ *
167
+ * `computePersistentOutputPath()` here and `getReviewFilePath()` in porch share
168
+ * one naming convention — `<project>/<id>-<phase>-iter<N>-<lane>.txt` under
169
+ * `codev/projects/` — and porch's verify step runs all three lanes in parallel
170
+ * against the same worktree. Each lane can only exclude its *own* output path,
171
+ * so without this every cmap round would have each lane reporting its two
172
+ * siblings' review files as mutations.
173
+ *
174
+ * Invisible in this repo, again, because `.gitignore` here carries
175
+ * `codev/projects/*​/*.txt` — but `CODEV_GITIGNORE_ENTRIES` does not ship that
176
+ * rule, so every adopter would have got a red banner on every cmap round. Same
177
+ * shape as the `.consult/history.log` false positive, and the reason this is a
178
+ * rule in code rather than a line in someone's `.gitignore`.
179
+ */
180
+ const LANE_REVIEW_FILE = /^codev\/projects\/[^/]+\/[^/]+-iter\d+-[^/]+\.txt$/;
181
+ /**
182
+ * Resolve a lane's `--output` to a repo-relative path, or null if it lands
183
+ * outside the workspace.
184
+ *
185
+ * `--output` arrives exactly as the user typed it, which may be relative
186
+ * (`consult -o review.txt`), so a raw `startsWith(workspaceRoot)` test misses it
187
+ * and the lane ends up reporting its own review file. Resolving first also kills
188
+ * the opposite error: a plain prefix test counts `/repo-2/x` as living inside
189
+ * `/repo`.
190
+ *
191
+ * The resolution base is **`cwd`, not `workspaceRoot`**, because that is where
192
+ * the file actually lands: every lane writes with a bare
193
+ * `fs.writeFileSync(outputPath, …)`, and `consult` never chdirs. Resolving
194
+ * against the workspace root instead would agree with reality only when the two
195
+ * happen to coincide — run `consult -o review.txt` from a subdirectory and the
196
+ * file is written to `<subdir>/review.txt` while the exclusion names
197
+ * `review.txt`, so the lane reports its own output as a mutation.
198
+ */
199
+ export function relativeOutputPath(workspaceRoot, outputPath, cwd = process.cwd()) {
200
+ if (!outputPath)
201
+ return null;
202
+ const rel = path.relative(workspaceRoot, path.resolve(cwd, outputPath));
203
+ if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel))
204
+ return null;
205
+ // Snapshot keys come from git, which always uses forward slashes.
206
+ return rel.split(path.sep).join('/');
207
+ }
208
+ /**
209
+ * Repo-relative paths that differ between two snapshots, sorted.
210
+ *
211
+ * `ignore` takes the extra paths this particular run owns — the lane's review
212
+ * output file, whose location depends on the project and phase. A tripwire that
213
+ * cries wolf about its own output would be trained away within a week.
214
+ */
215
+ export function diffTreeSnapshots(before, after, ignore = []) {
216
+ if (!before.available || !after.available)
217
+ return [];
218
+ const ignored = new Set(ignore);
219
+ const isOurs = (rel) => ignored.has(rel) ||
220
+ CONSULT_OWNED_PREFIXES.some(prefix => rel.startsWith(prefix)) ||
221
+ LANE_REVIEW_FILE.test(rel);
222
+ const changed = new Set();
223
+ for (const [rel, sig] of after.entries) {
224
+ if (isOurs(rel))
225
+ continue;
226
+ if (before.entries.get(rel) !== sig)
227
+ changed.add(rel);
228
+ }
229
+ for (const rel of before.entries.keys()) {
230
+ if (isOurs(rel))
231
+ continue;
232
+ if (!after.entries.has(rel))
233
+ changed.add(rel);
234
+ }
235
+ return [...changed].sort();
236
+ }
237
+ /**
238
+ * Repository identity fields that differ between two snapshots.
239
+ *
240
+ * A field that was unreadable at either end is skipped rather than reported as
241
+ * a change: "we could not tell" and "it moved" are different claims, and only
242
+ * one of them is evidence.
243
+ */
244
+ export function diffRepositoryIdentity(before, after) {
245
+ if (!before.available || !after.available)
246
+ return [];
247
+ const changes = [];
248
+ const fields = [
249
+ ['HEAD', 'head'],
250
+ ['branch', 'branch'],
251
+ ];
252
+ for (const [label, key] of fields) {
253
+ const a = before.identity[key];
254
+ const b = after.identity[key];
255
+ if (a === null || b === null)
256
+ continue;
257
+ if (a !== b)
258
+ changes.push({ field: label, before: a, after: b });
259
+ }
260
+ return changes;
261
+ }
262
+ /**
263
+ * Render a string safe to print to a terminal.
264
+ *
265
+ * Git path names are bytes, and almost anything that is not `/` or NUL is legal
266
+ * in one — including newlines and ESC. Printing such a name raw lets it forge
267
+ * the rest of the banner: a file called `\n - harmless.ts` adds a line to the
268
+ * list, and one containing a CSI sequence can repaint or erase what is above it.
269
+ * The warning exists to be trusted, so nothing it prints gets to move the
270
+ * cursor. C0, DEL and C1 are escaped; ordinary Unicode is left alone.
271
+ */
272
+ export function escapeControlChars(value) {
273
+ const named = {
274
+ '\n': '\\n',
275
+ '\r': '\\r',
276
+ '\t': '\\t',
277
+ };
278
+ // eslint-disable-next-line no-control-regex
279
+ return value.replace(/[\x00-\x1f\x7f-\x9f]/g, ch => named[ch] ?? `\\x${ch.charCodeAt(0).toString(16).padStart(2, '0')}`);
280
+ }
281
+ /**
282
+ * The warning for a repository that moved under the reviewer.
283
+ *
284
+ * Separate from the changed-files banner because it is a different fact with a
285
+ * different remedy: no file need look any different, and `git status` will not
286
+ * show you what happened — `git reflog` will.
287
+ */
288
+ export function formatRepositoryMovedWarning(model, changes) {
289
+ return [
290
+ `REPOSITORY MOVED during the ${model} review (#1649).`,
291
+ 'A consultant reviews the commit in front of it; it must never commit, or switch refs.',
292
+ ...changes.map(c => ` - ${c.field}: ${escapeControlChars(c.before)} -> ${escapeControlChars(c.after)}`),
293
+ 'The files on disk may look untouched and still not be the ones this review describes.',
294
+ 'Check `git reflog` and `git status` before trusting this review or shipping the branch.',
295
+ ].join('\n');
296
+ }
297
+ /** Cap the file list so a lane that touched hundreds of paths stays readable. */
298
+ const MAX_LISTED = 20;
299
+ /**
300
+ * The warning text, shared by the stderr banner and the review-output footer.
301
+ *
302
+ * Deliberately says the tree changed *while the lane ran*, not that the lane
303
+ * changed it. Under `cmap` all three lanes run as concurrent processes in one
304
+ * worktree, so each one sees the others' writes; asserting a culprit we cannot
305
+ * identify would be a claim the evidence does not support.
306
+ */
307
+ export function formatTreeChangeWarning(model, files) {
308
+ const listed = files.slice(0, MAX_LISTED);
309
+ const rest = files.length - listed.length;
310
+ const lines = [
311
+ `WORKING TREE CHANGED during the ${model} review (#1649).`,
312
+ `A consultant reviews the tree; it must never write to it. ${files.length} path${files.length === 1 ? '' : 's'} changed while this lane was running:`,
313
+ ...listed.map(f => ` - ${escapeControlChars(f)}`),
314
+ ];
315
+ if (rest > 0)
316
+ lines.push(` … and ${rest} more`);
317
+ lines.push('Check `git status` and `git diff` before trusting this review or shipping the branch.', 'Note: under cmap the lanes run concurrently in one worktree, so another lane — or you —', 'may be the writer. This warning names what moved, not who moved it.');
318
+ return lines.join('\n');
319
+ }
320
+ /**
321
+ * The warning for a post-review snapshot that could not be taken at all.
322
+ *
323
+ * Distinct from "the tree changed" because the failure is different in kind: we
324
+ * know something happened, and we cannot say what. Reported rather than
325
+ * swallowed — a comparison that silently returns "no changes" when it could not
326
+ * look is the failure mode this whole module exists to avoid.
327
+ */
328
+ export function formatSnapshotLostWarning(model, reason) {
329
+ return [
330
+ `WORKING TREE UNREADABLE after the ${model} review (#1649).`,
331
+ 'The tree was readable before this lane ran and `git status` fails now, so the tripwire',
332
+ 'cannot say whether anything changed. This is not the same as "nothing changed".',
333
+ `Reason: ${escapeControlChars(reason ?? 'unknown')}`,
334
+ 'Check `git status` and `git diff` before trusting this review or shipping the branch.',
335
+ ].join('\n');
336
+ }
337
+ /**
338
+ * Append the warning to a lane's review output so it survives into the file
339
+ * porch and the builder actually read — stderr scrolls past, review files don't.
340
+ *
341
+ * Written as a plain paragraph with no `VERDICT:` line, so `parseVerdict()`
342
+ * (which scans last→first for that prefix) still finds the reviewer's own
343
+ * verdict above it.
344
+ */
345
+ export function appendTreeChangeWarningToOutput(outputPath, warning) {
346
+ if (!outputPath || !fs.existsSync(outputPath))
347
+ return;
348
+ try {
349
+ fs.appendFileSync(outputPath, `\n\n---\n\n## ⚠️ ${warning}\n`);
350
+ }
351
+ catch {
352
+ // The stderr banner already carried the warning; failing to annotate the
353
+ // file must not turn a successful review into a failed command.
354
+ }
355
+ }
356
+ //# sourceMappingURL=tree-tripwire.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree-tripwire.js","sourceRoot":"","sources":["../../../src/commands/consult/tree-tripwire.ts"],"names":[],"mappings":"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"}
@@ -31,6 +31,75 @@ export interface SessionLogCheck {
31
31
  * hermetic; this function is where the logic lives and is pinned by tests.
32
32
  */
33
33
  export declare function checkSessionLogs(logDir: string, env?: NodeJS.ProcessEnv): SessionLogCheck;
34
+ export interface TowerEnvCheck {
35
+ status: 'ok' | 'warn' | 'skipped';
36
+ /** Claude Code session markers found on the running Tower daemon. */
37
+ markers: string[];
38
+ /** Running shellper daemons whose own env still carries markers. */
39
+ contaminatedSessions: number;
40
+ /** Human-readable one-liner. No colour codes. */
41
+ summary: string;
42
+ /** Present only when `status === 'warn'`. */
43
+ recommendation?: string;
44
+ }
45
+ /**
46
+ * Remediation for a Tower daemon that inherited the markers (#1219).
47
+ *
48
+ * A plain restart is enough for the *daemon*, but deliberately not for what it
49
+ * already spawned: `towerStop` leaves shellpers running on purpose
50
+ * (`commands/tower.ts` — they are detached so terminals survive a restart), so
51
+ * their agent processes keep the markers until each is restarted. Saying only
52
+ * "restart Tower" would be a half-truth that reads as a full fix.
53
+ */
54
+ export declare const TOWER_ENV_RESTART_HINT: string;
55
+ /**
56
+ * Remediation when Tower itself is clean but sessions it spawned earlier are not.
57
+ *
58
+ * The force-kill flag is named but not recommended outright: it ends every
59
+ * running agent on the machine, which is a bigger hammer than most cases need.
60
+ */
61
+ export declare const TOWER_ENV_SESSION_HINT: string;
62
+ /**
63
+ * Decide what the "Tower environment" doctor section should say (#1219).
64
+ *
65
+ * A Tower started from inside a Claude Code session inherits that session's
66
+ * markers and hands them to every agent it spawns, which turns transcript
67
+ * saving — and therefore resume — off for all of them. Newly started Towers
68
+ * scrub themselves, but a *long-running* daemon started before this fix (or by
69
+ * something other than `afx tower start`) can still be carrying them, and that
70
+ * is invisible until a crash-recovery attempt fails.
71
+ *
72
+ * The readers are injected so the decision is unit-testable without a live
73
+ * daemon; `doctor()` passes the real `ps eww` ones. A `null` Tower env means no
74
+ * Tower is running (or its env could not be read) — reported as `skipped`, never
75
+ * a warning, since "no Tower" is not a fault.
76
+ *
77
+ * `readSessionEnvs` covers what a daemon-only check would miss (CMAP review):
78
+ * shellpers are detached and survive `afx tower stop` by design, so after a
79
+ * plain restart Tower reads clean while the agents it spawned earlier are still
80
+ * carrying the markers — and still unresumable.
81
+ */
82
+ export declare function checkTowerEnv(readTowerEnv: () => Record<string, string> | null, readSessionEnvs?: () => Array<Record<string, string>>): TowerEnvCheck;
83
+ /**
84
+ * PIDs of the running shellper daemons — the processes that outlive Tower and
85
+ * so can still be carrying markers after Tower itself has been restarted clean.
86
+ */
87
+ export declare function findShellperPids(): number[];
88
+ /**
89
+ * Read a running process's environment via `ps eww`.
90
+ *
91
+ * `ps eww -o command=` prints the argv followed by the environment on one line,
92
+ * and `ps ww -o command=` prints the argv alone — so subtracting the second from
93
+ * the first leaves the environment with no argv tokens to misparse. Returns
94
+ * `null` when the process is gone or the two readings disagree (a race), rather
95
+ * than guessing. macOS has no `/proc`, so `ps` is the portable reader here.
96
+ *
97
+ * `ps` separates environment entries with spaces and does not quote them, so a
98
+ * *value* containing a space is truncated at that space. Names are unaffected,
99
+ * and this check only ever asks which variables are present — but that is why
100
+ * the result is not a general-purpose environment reader.
101
+ */
102
+ export declare function readProcessEnv(pid: number): Record<string, string> | null;
34
103
  /**
35
104
  * Main doctor function
36
105
  */
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA2DH;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,QAAyB,CAAC;AAE7D,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,IAAI,GAAG,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,aAAa,EAAE,MAAM,CAAC;IACtB,6CAA6C;IAC7C,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,eAAe,CAgCjB;AA8gBD;;GAEG;AACH,wBAAsB,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,CAigB9C"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA8DH;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,QAAyB,CAAC;AAE7D,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,IAAI,GAAG,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,aAAa,EAAE,MAAM,CAAC;IACtB,6CAA6C;IAC7C,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,eAAe,CAgCjB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,IAAI,GAAG,MAAM,GAAG,SAAS,CAAC;IAClC,qEAAqE;IACrE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,oEAAoE;IACpE,oBAAoB,EAAE,MAAM,CAAC;IAC7B,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,6CAA6C;IAC7C,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,QAE6C,CAAC;AAEjF;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,QAE2C,CAAC;AAE/E;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,aAAa,CAC3B,YAAY,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,EACjD,eAAe,GAAE,MAAM,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAY,GAC9D,aAAa,CAwCf;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,EAAE,CAO3C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAoBzE;AAuhBD;;GAEG;AACH,wBAAsB,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,CA4hB9C"}
@@ -19,6 +19,9 @@ import { findMigrationBackups, formatMigrationBackup, formatBytes, totalBytes, S
19
19
  import { resolveAgyBin, AGY_OAUTH_MARKERS } from './consult/index.js';
20
20
  import { checkCachedAgyAuth, recordAgyAuthState } from './consult/agy-auth-cache.js';
21
21
  import { AGENT_FARM_DIR } from '@cluesmith/codev-core/constants';
22
+ import { findClaudeSessionMarkers } from '../lib/agent-env.js';
23
+ import { getProcessesOnPort } from '../agent-farm/utils/port.js';
24
+ import { DEFAULT_TOWER_PORT } from '@cluesmith/codev-sdk/constants';
22
25
  import { measureSessionLogs, resolveLogRetentionDays, formatBytes as formatLogBytes, } from '../agent-farm/servers/session-log-sweep.js';
23
26
  const __filename = fileURLToPath(import.meta.url);
24
27
  const __dirname = dirname(__filename);
@@ -68,6 +71,133 @@ export function checkSessionLogs(logDir, env = process.env) {
68
71
  : 'restart Tower to run the retention sweep (afx tower stop && afx tower start)',
69
72
  };
70
73
  }
74
+ /**
75
+ * Remediation for a Tower daemon that inherited the markers (#1219).
76
+ *
77
+ * A plain restart is enough for the *daemon*, but deliberately not for what it
78
+ * already spawned: `towerStop` leaves shellpers running on purpose
79
+ * (`commands/tower.ts` — they are detached so terminals survive a restart), so
80
+ * their agent processes keep the markers until each is restarted. Saying only
81
+ * "restart Tower" would be a half-truth that reads as a full fix.
82
+ */
83
+ export const TOWER_ENV_RESTART_HINT = 'restart Tower (afx tower stop && afx tower start), then restart each affected agent — '
84
+ + 'a plain restart leaves existing shellper sessions running with the markers';
85
+ /**
86
+ * Remediation when Tower itself is clean but sessions it spawned earlier are not.
87
+ *
88
+ * The force-kill flag is named but not recommended outright: it ends every
89
+ * running agent on the machine, which is a bigger hammer than most cases need.
90
+ */
91
+ export const TOWER_ENV_SESSION_HINT = 'restart each affected agent to clear them (or, to end every running session at once, '
92
+ + 'afx tower stop --force-kill-all-child-processes — this kills all agents)';
93
+ /**
94
+ * Decide what the "Tower environment" doctor section should say (#1219).
95
+ *
96
+ * A Tower started from inside a Claude Code session inherits that session's
97
+ * markers and hands them to every agent it spawns, which turns transcript
98
+ * saving — and therefore resume — off for all of them. Newly started Towers
99
+ * scrub themselves, but a *long-running* daemon started before this fix (or by
100
+ * something other than `afx tower start`) can still be carrying them, and that
101
+ * is invisible until a crash-recovery attempt fails.
102
+ *
103
+ * The readers are injected so the decision is unit-testable without a live
104
+ * daemon; `doctor()` passes the real `ps eww` ones. A `null` Tower env means no
105
+ * Tower is running (or its env could not be read) — reported as `skipped`, never
106
+ * a warning, since "no Tower" is not a fault.
107
+ *
108
+ * `readSessionEnvs` covers what a daemon-only check would miss (CMAP review):
109
+ * shellpers are detached and survive `afx tower stop` by design, so after a
110
+ * plain restart Tower reads clean while the agents it spawned earlier are still
111
+ * carrying the markers — and still unresumable.
112
+ */
113
+ export function checkTowerEnv(readTowerEnv, readSessionEnvs = () => []) {
114
+ const env = readTowerEnv();
115
+ const markers = env ? findClaudeSessionMarkers(env) : [];
116
+ const contaminatedSessions = readSessionEnvs()
117
+ .filter((sessionEnv) => findClaudeSessionMarkers(sessionEnv).length > 0)
118
+ .length;
119
+ if (markers.length > 0) {
120
+ const sessionNote = contaminatedSessions > 0
121
+ ? `; ${contaminatedSessions} running session(s) already carry them`
122
+ : '';
123
+ return {
124
+ status: 'warn',
125
+ markers,
126
+ contaminatedSessions,
127
+ summary: `Tower inherited ${markers.length} Claude Code session marker(s): ${markers.join(', ')}${sessionNote}`,
128
+ recommendation: TOWER_ENV_RESTART_HINT,
129
+ };
130
+ }
131
+ // Sessions spawned before the restart are still contaminated — the case a
132
+ // daemon-only check would call "ok" while agents stay unresumable. Shellpers
133
+ // outlive Tower, so this holds whether or not a Tower is running now; only
134
+ // the wording changes, since "Tower is clean" is not true of an absent Tower.
135
+ if (contaminatedSessions > 0) {
136
+ const lead = env ? 'Tower is clean, but' : 'No Tower running, but';
137
+ return {
138
+ status: 'warn',
139
+ markers,
140
+ contaminatedSessions,
141
+ summary: `${lead} ${contaminatedSessions} running session(s) still carry Claude Code session markers`,
142
+ recommendation: TOWER_ENV_SESSION_HINT,
143
+ };
144
+ }
145
+ if (!env) {
146
+ return { status: 'skipped', markers: [], contaminatedSessions: 0, summary: 'Tower not running' };
147
+ }
148
+ return { status: 'ok', markers, contaminatedSessions, summary: 'Tower environment is clean' };
149
+ }
150
+ /**
151
+ * PIDs of the running shellper daemons — the processes that outlive Tower and
152
+ * so can still be carrying markers after Tower itself has been restarted clean.
153
+ */
154
+ export function findShellperPids() {
155
+ const out = runCommand('pgrep', ['-f', 'shellper-main.js']);
156
+ if (!out)
157
+ return [];
158
+ return out
159
+ .split('\n')
160
+ .map((line) => parseInt(line.trim(), 10))
161
+ .filter((pid) => !isNaN(pid));
162
+ }
163
+ /**
164
+ * Read a running process's environment via `ps eww`.
165
+ *
166
+ * `ps eww -o command=` prints the argv followed by the environment on one line,
167
+ * and `ps ww -o command=` prints the argv alone — so subtracting the second from
168
+ * the first leaves the environment with no argv tokens to misparse. Returns
169
+ * `null` when the process is gone or the two readings disagree (a race), rather
170
+ * than guessing. macOS has no `/proc`, so `ps` is the portable reader here.
171
+ *
172
+ * `ps` separates environment entries with spaces and does not quote them, so a
173
+ * *value* containing a space is truncated at that space. Names are unaffected,
174
+ * and this check only ever asks which variables are present — but that is why
175
+ * the result is not a general-purpose environment reader.
176
+ */
177
+ export function readProcessEnv(pid) {
178
+ // `ww` in both: without it `ps` truncates to the terminal width when stdout is
179
+ // a tty, and a truncated argv would break the prefix subtraction below.
180
+ const command = runCommand('ps', ['ww', '-o', 'command=', '-p', String(pid)]);
181
+ const withEnv = runCommand('ps', ['eww', '-o', 'command=', '-p', String(pid)]);
182
+ if (command === null || withEnv === null)
183
+ return null;
184
+ if (!withEnv.startsWith(command))
185
+ return null;
186
+ const env = {};
187
+ for (const token of withEnv.slice(command.length).trim().split(' ')) {
188
+ const eq = token.indexOf('=');
189
+ if (eq <= 0)
190
+ continue;
191
+ env[token.slice(0, eq)] = token.slice(eq + 1);
192
+ }
193
+ // No pairs parsed means `ps` showed us no environment at all — another user's
194
+ // process, or a platform where `e` is not honoured. Returning `{}` here would
195
+ // read downstream as "an environment with no markers", i.e. a clean bill of
196
+ // health for a process we never actually inspected. Say "unknown" instead.
197
+ if (Object.keys(env).length === 0)
198
+ return null;
199
+ return env;
200
+ }
71
201
  /**
72
202
  * Compare semantic versions: returns true if v1 >= v2
73
203
  */
@@ -283,6 +413,14 @@ async function verifyClaudeViaSDK() {
283
413
  const session = claudeQuery({
284
414
  prompt: 'Reply OK',
285
415
  options: {
416
+ // The #1649 audit: this is the second `bypassPermissions` call site in the
417
+ // package, so it runs with the same unrestricted tool access the consult
418
+ // lane does. It is an auth probe, not a review — one turn, no tools needed
419
+ // — but it had nothing telling it so, and it runs in whatever directory the
420
+ // user typed `codev doctor` in. Say it outright, as consultant.md now does.
421
+ systemPrompt: 'You are a connectivity probe for `codev doctor`. Reply with the requested ' +
422
+ 'text and nothing else. Do not read, create, or modify any file, and do not ' +
423
+ 'run any command — there is no task here beyond answering.',
286
424
  allowedTools: [],
287
425
  maxTurns: 1,
288
426
  permissionMode: 'bypassPermissions',
@@ -919,7 +1057,7 @@ export async function doctor() {
919
1057
  }
920
1058
  console.log('');
921
1059
  }
922
- // Full forge concept reporting: all 15 concepts with resolution source and executable check
1060
+ // Full forge concept reporting: all 18 concepts with resolution source and executable check
923
1061
  const forgeConfig = loadForgeConfig(workspaceRoot);
924
1062
  const provider = forgeConfig?.provider ?? 'github';
925
1063
  console.log(chalk.bold('Forge Concepts') + ` (provider: ${provider})`);
@@ -935,7 +1073,7 @@ export async function doctor() {
935
1073
  }
936
1074
  }
937
1075
  }
938
- // Report all 15 concepts with source and executable availability
1076
+ // Report all 18 concepts with source and executable availability
939
1077
  const resolutions = resolveAllConcepts(forgeConfig);
940
1078
  const missingExecs = new Set();
941
1079
  for (const r of resolutions) {
@@ -1009,6 +1147,31 @@ export async function doctor() {
1009
1147
  console.log(` ${chalk.green('✓')} ${sessionLogs.summary} ${chalk.blue(`(${sessionLogs.retentionNote})`)}`);
1010
1148
  }
1011
1149
  console.log('');
1150
+ // Tower environment hygiene (#1219). A Tower that inherited a Claude Code
1151
+ // session's markers spawns agents whose transcripts are never saved, so they
1152
+ // cannot be resumed after a crash — silent until recovery time.
1153
+ const towerPids = getProcessesOnPort(DEFAULT_TOWER_PORT);
1154
+ const towerEnv = checkTowerEnv(() => (towerPids.length > 0 ? readProcessEnv(towerPids[0]) : null), () => findShellperPids()
1155
+ .map(readProcessEnv)
1156
+ .filter((e) => e !== null));
1157
+ console.log(chalk.bold('Tower Environment') + ` (port ${DEFAULT_TOWER_PORT})`);
1158
+ console.log('');
1159
+ if (towerEnv.status === 'warn') {
1160
+ console.log(` ${chalk.yellow('⚠')} ${towerEnv.summary}`);
1161
+ warnings++;
1162
+ warningDetails.push({
1163
+ name: 'Tower environment',
1164
+ issue: towerEnv.summary,
1165
+ recommendation: towerEnv.recommendation,
1166
+ });
1167
+ }
1168
+ else if (towerEnv.status === 'skipped') {
1169
+ console.log(` ${chalk.blue('-')} ${towerEnv.summary}`);
1170
+ }
1171
+ else {
1172
+ console.log(` ${chalk.green('✓')} ${towerEnv.summary}`);
1173
+ }
1174
+ console.log('');
1012
1175
  // Summary
1013
1176
  console.log('============================================');
1014
1177
  if (errors > 0) {