@cluesmith/codev 3.3.3 → 3.3.5

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 (244) hide show
  1. package/dashboard-dist/assets/index-BuflRUi6.js +220 -0
  2. package/dashboard-dist/assets/index-BuflRUi6.js.map +1 -0
  3. package/dashboard-dist/assets/index-r3bZzLmX.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 +15 -3
  14. package/dist/agent-farm/commands/inbox.js.map +1 -1
  15. package/dist/agent-farm/commands/send.d.ts +3 -2
  16. package/dist/agent-farm/commands/send.d.ts.map +1 -1
  17. package/dist/agent-farm/commands/send.js +40 -6
  18. package/dist/agent-farm/commands/send.js.map +1 -1
  19. package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
  20. package/dist/agent-farm/commands/spawn-worktree.js +28 -9
  21. package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
  22. package/dist/agent-farm/commands/tower.d.ts +21 -9
  23. package/dist/agent-farm/commands/tower.d.ts.map +1 -1
  24. package/dist/agent-farm/commands/tower.js +90 -30
  25. package/dist/agent-farm/commands/tower.js.map +1 -1
  26. package/dist/agent-farm/db/tower-owner.d.ts +168 -0
  27. package/dist/agent-farm/db/tower-owner.d.ts.map +1 -0
  28. package/dist/agent-farm/db/tower-owner.js +369 -0
  29. package/dist/agent-farm/db/tower-owner.js.map +1 -0
  30. package/dist/agent-farm/db/types.d.ts +30 -5
  31. package/dist/agent-farm/db/types.d.ts.map +1 -1
  32. package/dist/agent-farm/db/types.js.map +1 -1
  33. package/dist/agent-farm/lib/ide-record.d.ts +65 -0
  34. package/dist/agent-farm/lib/ide-record.d.ts.map +1 -0
  35. package/dist/agent-farm/lib/ide-record.js +118 -0
  36. package/dist/agent-farm/lib/ide-record.js.map +1 -0
  37. package/dist/agent-farm/lib/tunnel-client.d.ts +22 -0
  38. package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -1
  39. package/dist/agent-farm/lib/tunnel-client.js +53 -5
  40. package/dist/agent-farm/lib/tunnel-client.js.map +1 -1
  41. package/dist/agent-farm/servers/decisions/file.d.ts +50 -0
  42. package/dist/agent-farm/servers/decisions/file.d.ts.map +1 -0
  43. package/dist/agent-farm/servers/decisions/file.js +146 -0
  44. package/dist/agent-farm/servers/decisions/file.js.map +1 -0
  45. package/dist/agent-farm/servers/decisions/grammar.d.ts +36 -0
  46. package/dist/agent-farm/servers/decisions/grammar.d.ts.map +1 -0
  47. package/dist/agent-farm/servers/decisions/grammar.js +109 -0
  48. package/dist/agent-farm/servers/decisions/grammar.js.map +1 -0
  49. package/dist/agent-farm/servers/decisions/notice.d.ts +11 -0
  50. package/dist/agent-farm/servers/decisions/notice.d.ts.map +1 -0
  51. package/dist/agent-farm/servers/decisions/notice.js +18 -0
  52. package/dist/agent-farm/servers/decisions/notice.js.map +1 -0
  53. package/dist/agent-farm/servers/decisions/parse.d.ts +36 -0
  54. package/dist/agent-farm/servers/decisions/parse.d.ts.map +1 -0
  55. package/dist/agent-farm/servers/decisions/parse.js +308 -0
  56. package/dist/agent-farm/servers/decisions/parse.js.map +1 -0
  57. package/dist/agent-farm/servers/decisions/routes.d.ts +17 -0
  58. package/dist/agent-farm/servers/decisions/routes.d.ts.map +1 -0
  59. package/dist/agent-farm/servers/decisions/routes.js +160 -0
  60. package/dist/agent-farm/servers/decisions/routes.js.map +1 -0
  61. package/dist/agent-farm/servers/decisions/splice.d.ts +26 -0
  62. package/dist/agent-farm/servers/decisions/splice.d.ts.map +1 -0
  63. package/dist/agent-farm/servers/decisions/splice.js +120 -0
  64. package/dist/agent-farm/servers/decisions/splice.js.map +1 -0
  65. package/dist/agent-farm/servers/decisions/stamp.d.ts +4 -0
  66. package/dist/agent-farm/servers/decisions/stamp.d.ts.map +1 -0
  67. package/dist/agent-farm/servers/decisions/stamp.js +171 -0
  68. package/dist/agent-farm/servers/decisions/stamp.js.map +1 -0
  69. package/dist/agent-farm/servers/gate-profiles.d.ts +23 -2
  70. package/dist/agent-farm/servers/gate-profiles.d.ts.map +1 -1
  71. package/dist/agent-farm/servers/gate-profiles.js +43 -2
  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 +213 -4
  82. package/dist/agent-farm/servers/mailbox-delivery.d.ts.map +1 -1
  83. package/dist/agent-farm/servers/mailbox-delivery.js +489 -68
  84. package/dist/agent-farm/servers/mailbox-delivery.js.map +1 -1
  85. package/dist/agent-farm/servers/mailbox-wiring.d.ts +18 -4
  86. package/dist/agent-farm/servers/mailbox-wiring.d.ts.map +1 -1
  87. package/dist/agent-farm/servers/mailbox-wiring.js +94 -21
  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 +32 -15
  94. package/dist/agent-farm/servers/overview.d.ts.map +1 -1
  95. package/dist/agent-farm/servers/overview.js +181 -103
  96. package/dist/agent-farm/servers/overview.js.map +1 -1
  97. package/dist/agent-farm/servers/render-gate.d.ts +53 -0
  98. package/dist/agent-farm/servers/render-gate.d.ts.map +1 -1
  99. package/dist/agent-farm/servers/render-gate.js +135 -24
  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 +4 -1
  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 +6 -7
  110. package/dist/agent-farm/servers/tower-instances.js.map +1 -1
  111. package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
  112. package/dist/agent-farm/servers/tower-routes.js +116 -18
  113. package/dist/agent-farm/servers/tower-routes.js.map +1 -1
  114. package/dist/agent-farm/servers/tower-server.js +64 -7
  115. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  116. package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
  117. package/dist/agent-farm/servers/tower-terminals.js +53 -8
  118. package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
  119. package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -1
  120. package/dist/agent-farm/servers/tower-websocket.js +18 -1
  121. package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
  122. package/dist/agent-farm/utils/message-format.d.ts +8 -0
  123. package/dist/agent-farm/utils/message-format.d.ts.map +1 -1
  124. package/dist/agent-farm/utils/message-format.js +21 -6
  125. package/dist/agent-farm/utils/message-format.js.map +1 -1
  126. package/dist/agent-farm/utils/port.d.ts +22 -0
  127. package/dist/agent-farm/utils/port.d.ts.map +1 -0
  128. package/dist/agent-farm/utils/port.js +35 -0
  129. package/dist/agent-farm/utils/port.js.map +1 -0
  130. package/dist/agent-farm/utils/server-utils.d.ts +26 -0
  131. package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
  132. package/dist/agent-farm/utils/server-utils.js +36 -0
  133. package/dist/agent-farm/utils/server-utils.js.map +1 -1
  134. package/dist/cli.js +2 -2
  135. package/dist/cli.js.map +1 -1
  136. package/dist/commands/consult/index.d.ts +20 -1
  137. package/dist/commands/consult/index.d.ts.map +1 -1
  138. package/dist/commands/consult/index.js +117 -3
  139. package/dist/commands/consult/index.js.map +1 -1
  140. package/dist/commands/consult/tree-tripwire.d.ts +177 -0
  141. package/dist/commands/consult/tree-tripwire.d.ts.map +1 -0
  142. package/dist/commands/consult/tree-tripwire.js +356 -0
  143. package/dist/commands/consult/tree-tripwire.js.map +1 -0
  144. package/dist/commands/doctor.d.ts +69 -0
  145. package/dist/commands/doctor.d.ts.map +1 -1
  146. package/dist/commands/doctor.js +163 -0
  147. package/dist/commands/doctor.js.map +1 -1
  148. package/dist/commands/generate-image.d.ts +5 -0
  149. package/dist/commands/generate-image.d.ts.map +1 -1
  150. package/dist/commands/generate-image.js +225 -4
  151. package/dist/commands/generate-image.js.map +1 -1
  152. package/dist/commands/porch/checks.d.ts +7 -1
  153. package/dist/commands/porch/checks.d.ts.map +1 -1
  154. package/dist/commands/porch/checks.js +77 -14
  155. package/dist/commands/porch/checks.js.map +1 -1
  156. package/dist/commands/porch/index.d.ts +38 -1
  157. package/dist/commands/porch/index.d.ts.map +1 -1
  158. package/dist/commands/porch/index.js +79 -43
  159. package/dist/commands/porch/index.js.map +1 -1
  160. package/dist/commands/porch/protocol.d.ts +4 -3
  161. package/dist/commands/porch/protocol.d.ts.map +1 -1
  162. package/dist/commands/porch/protocol.js +32 -5
  163. package/dist/commands/porch/protocol.js.map +1 -1
  164. package/dist/commands/porch/types.d.ts +2 -0
  165. package/dist/commands/porch/types.d.ts.map +1 -1
  166. package/dist/lib/agent-env.d.ts +95 -0
  167. package/dist/lib/agent-env.d.ts.map +1 -0
  168. package/dist/lib/agent-env.js +137 -0
  169. package/dist/lib/agent-env.js.map +1 -0
  170. package/dist/lib/config.d.ts +8 -0
  171. package/dist/lib/config.d.ts.map +1 -1
  172. package/dist/lib/config.js.map +1 -1
  173. package/dist/lib/forge-contracts.d.ts +10 -0
  174. package/dist/lib/forge-contracts.d.ts.map +1 -1
  175. package/dist/lib/forge-contracts.js.map +1 -1
  176. package/dist/lib/forge-rate-limit.d.ts +45 -0
  177. package/dist/lib/forge-rate-limit.d.ts.map +1 -0
  178. package/dist/lib/forge-rate-limit.js +89 -0
  179. package/dist/lib/forge-rate-limit.js.map +1 -0
  180. package/dist/lib/forge.d.ts +28 -0
  181. package/dist/lib/forge.d.ts.map +1 -1
  182. package/dist/lib/forge.js +63 -10
  183. package/dist/lib/forge.js.map +1 -1
  184. package/dist/lib/github.d.ts +13 -1
  185. package/dist/lib/github.d.ts.map +1 -1
  186. package/dist/lib/github.js +58 -15
  187. package/dist/lib/github.js.map +1 -1
  188. package/dist/lib/test-env.d.ts +17 -0
  189. package/dist/lib/test-env.d.ts.map +1 -1
  190. package/dist/lib/test-env.js +20 -0
  191. package/dist/lib/test-env.js.map +1 -1
  192. package/dist/terminal/pty-session.d.ts +122 -4
  193. package/dist/terminal/pty-session.d.ts.map +1 -1
  194. package/dist/terminal/pty-session.js +142 -7
  195. package/dist/terminal/pty-session.js.map +1 -1
  196. package/dist/terminal/session-manager.d.ts +23 -2
  197. package/dist/terminal/session-manager.d.ts.map +1 -1
  198. package/dist/terminal/session-manager.js +115 -36
  199. package/dist/terminal/session-manager.js.map +1 -1
  200. package/dist/terminal/session-screen.d.ts +34 -5
  201. package/dist/terminal/session-screen.d.ts.map +1 -1
  202. package/dist/terminal/session-screen.js +52 -6
  203. package/dist/terminal/session-screen.js.map +1 -1
  204. package/dist/terminal/terminal-replies.d.ts +102 -0
  205. package/dist/terminal/terminal-replies.d.ts.map +1 -0
  206. package/dist/terminal/terminal-replies.js +181 -0
  207. package/dist/terminal/terminal-replies.js.map +1 -0
  208. package/package.json +4 -4
  209. package/scripts/forge/github/pr-list.sh +6 -2
  210. package/scripts/forge/github/pr-search.sh +4 -2
  211. package/scripts/forge/gitlab/issue-search.sh +1 -0
  212. package/scripts/forge/gitlab/pr-search.sh +35 -2
  213. package/scripts/forge/linear/auth-status.sh +1 -0
  214. package/scripts/forge/linear/issue-comment.sh +1 -0
  215. package/scripts/forge/linear/issue-list.sh +1 -0
  216. package/scripts/forge/linear/issue-search.sh +1 -0
  217. package/scripts/forge/linear/issue-view.sh +1 -0
  218. package/scripts/forge/linear/recently-closed.sh +1 -0
  219. package/scripts/forge/linear/user-identity.sh +1 -0
  220. package/skeleton/.claude/skills/afx/SKILL.md +15 -0
  221. package/skeleton/.claude/skills/arch-init/SKILL.md +56 -9
  222. package/skeleton/.claude/skills/arch-save/SKILL.md +100 -9
  223. package/skeleton/.claude/skills/consult/SKILL.md +9 -0
  224. package/skeleton/.claude/skills/generate-image/SKILL.md +19 -2
  225. package/skeleton/.claude/skills/porch/SKILL.md +18 -0
  226. package/skeleton/.codex/skills/afx/SKILL.md +15 -0
  227. package/skeleton/.codex/skills/arch-init/SKILL.md +56 -9
  228. package/skeleton/.codex/skills/arch-save/SKILL.md +100 -9
  229. package/skeleton/.codex/skills/consult/SKILL.md +9 -0
  230. package/skeleton/.codex/skills/generate-image/SKILL.md +19 -2
  231. package/skeleton/.codex/skills/porch/SKILL.md +18 -0
  232. package/skeleton/protocol-schema.json +6 -0
  233. package/skeleton/protocols/maintain/protocol.md +5 -0
  234. package/skeleton/protocols/pir/prompts/review.md +0 -1
  235. package/skeleton/protocols/protocol-schema.json +6 -0
  236. package/skeleton/resources/commands/agent-farm.md +71 -2
  237. package/skeleton/resources/commands/consult.md +9 -2
  238. package/skeleton/roles/consultant.md +32 -0
  239. package/skeleton/templates/AGENTS.md +1 -1
  240. package/skeleton/templates/CLAUDE.md +1 -1
  241. package/skeleton/templates/cheatsheet.md +1 -1
  242. package/dashboard-dist/assets/index-CUIjS_Vb.js +0 -199
  243. package/dashboard-dist/assets/index-CUIjS_Vb.js.map +0 -1
  244. package/dashboard-dist/assets/index-DnYLy5C_.css +0 -32
@@ -0,0 +1,177 @@
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
+ /**
41
+ * Where the repository itself is pointing.
42
+ *
43
+ * The working tree is only half the state a lane can move. `git commit` takes a
44
+ * dirty tree clean, and `git checkout` between two refs with identical content
45
+ * changes nothing a file comparison can see — in both cases the snapshot of
46
+ * *paths* is a poor witness, and in the clean-ref-switch case it is no witness
47
+ * at all. The reviewer is then reviewing a different commit than the builder
48
+ * thinks, which is worse than an edited file because it leaves no local trace.
49
+ *
50
+ * Either field is `null` when git cannot answer — most usefully in a repository
51
+ * with no commits yet, where `rev-parse HEAD` fails and the tree is still worth
52
+ * watching.
53
+ */
54
+ export interface RepositoryIdentity {
55
+ /** HEAD's object id. */
56
+ head: string | null;
57
+ /** Branch name, or `HEAD` when detached. */
58
+ branch: string | null;
59
+ }
60
+ /**
61
+ * A point-in-time view of every path git considers changed, plus where the
62
+ * repository was pointing when it was taken.
63
+ *
64
+ * `available: false` means the tripwire could not run at all — most often
65
+ * because the workspace is not a git repository. Callers say so once on stderr
66
+ * rather than pretending the tree was clean.
67
+ */
68
+ export interface TreeSnapshot {
69
+ available: boolean;
70
+ /** Reason the snapshot is unavailable; only set when `available` is false. */
71
+ reason?: string;
72
+ /** Repo-relative path → `"<status code>:<content hash>"`. */
73
+ entries: Map<string, string>;
74
+ /** HEAD and branch at the moment of the snapshot. */
75
+ identity: RepositoryIdentity;
76
+ }
77
+ /**
78
+ * Snapshot the working tree of `workspaceRoot`.
79
+ *
80
+ * `-z` (NUL-separated) rather than plain `--porcelain` because git C-quotes
81
+ * paths containing spaces or non-ASCII in the human-readable form, and a quoted
82
+ * path cannot be handed back to `fs.readFileSync`. `--untracked-files=all` so
83
+ * a newly created file is listed individually instead of being collapsed into
84
+ * a single `?? dir/` entry.
85
+ *
86
+ * Records HEAD and the branch alongside the paths, so a lane that commits or
87
+ * switches refs is caught even when no file ends up looking different.
88
+ */
89
+ export declare function snapshotTree(workspaceRoot: string): TreeSnapshot;
90
+ /**
91
+ * Resolve a lane's `--output` to a repo-relative path, or null if it lands
92
+ * outside the workspace.
93
+ *
94
+ * `--output` arrives exactly as the user typed it, which may be relative
95
+ * (`consult -o review.txt`), so a raw `startsWith(workspaceRoot)` test misses it
96
+ * and the lane ends up reporting its own review file. Resolving first also kills
97
+ * the opposite error: a plain prefix test counts `/repo-2/x` as living inside
98
+ * `/repo`.
99
+ *
100
+ * The resolution base is **`cwd`, not `workspaceRoot`**, because that is where
101
+ * the file actually lands: every lane writes with a bare
102
+ * `fs.writeFileSync(outputPath, …)`, and `consult` never chdirs. Resolving
103
+ * against the workspace root instead would agree with reality only when the two
104
+ * happen to coincide — run `consult -o review.txt` from a subdirectory and the
105
+ * file is written to `<subdir>/review.txt` while the exclusion names
106
+ * `review.txt`, so the lane reports its own output as a mutation.
107
+ */
108
+ export declare function relativeOutputPath(workspaceRoot: string, outputPath?: string, cwd?: string): string | null;
109
+ /**
110
+ * Repo-relative paths that differ between two snapshots, sorted.
111
+ *
112
+ * `ignore` takes the extra paths this particular run owns — the lane's review
113
+ * output file, whose location depends on the project and phase. A tripwire that
114
+ * cries wolf about its own output would be trained away within a week.
115
+ */
116
+ export declare function diffTreeSnapshots(before: TreeSnapshot, after: TreeSnapshot, ignore?: string[]): string[];
117
+ /** One field of the repository's identity that moved during a review. */
118
+ export interface IdentityChange {
119
+ field: 'HEAD' | 'branch';
120
+ before: string;
121
+ after: string;
122
+ }
123
+ /**
124
+ * Repository identity fields that differ between two snapshots.
125
+ *
126
+ * A field that was unreadable at either end is skipped rather than reported as
127
+ * a change: "we could not tell" and "it moved" are different claims, and only
128
+ * one of them is evidence.
129
+ */
130
+ export declare function diffRepositoryIdentity(before: TreeSnapshot, after: TreeSnapshot): IdentityChange[];
131
+ /**
132
+ * Render a string safe to print to a terminal.
133
+ *
134
+ * Git path names are bytes, and almost anything that is not `/` or NUL is legal
135
+ * in one — including newlines and ESC. Printing such a name raw lets it forge
136
+ * the rest of the banner: a file called `\n - harmless.ts` adds a line to the
137
+ * list, and one containing a CSI sequence can repaint or erase what is above it.
138
+ * The warning exists to be trusted, so nothing it prints gets to move the
139
+ * cursor. C0, DEL and C1 are escaped; ordinary Unicode is left alone.
140
+ */
141
+ export declare function escapeControlChars(value: string): string;
142
+ /**
143
+ * The warning for a repository that moved under the reviewer.
144
+ *
145
+ * Separate from the changed-files banner because it is a different fact with a
146
+ * different remedy: no file need look any different, and `git status` will not
147
+ * show you what happened — `git reflog` will.
148
+ */
149
+ export declare function formatRepositoryMovedWarning(model: string, changes: IdentityChange[]): string;
150
+ /**
151
+ * The warning text, shared by the stderr banner and the review-output footer.
152
+ *
153
+ * Deliberately says the tree changed *while the lane ran*, not that the lane
154
+ * changed it. Under `cmap` all three lanes run as concurrent processes in one
155
+ * worktree, so each one sees the others' writes; asserting a culprit we cannot
156
+ * identify would be a claim the evidence does not support.
157
+ */
158
+ export declare function formatTreeChangeWarning(model: string, files: string[]): string;
159
+ /**
160
+ * The warning for a post-review snapshot that could not be taken at all.
161
+ *
162
+ * Distinct from "the tree changed" because the failure is different in kind: we
163
+ * know something happened, and we cannot say what. Reported rather than
164
+ * swallowed — a comparison that silently returns "no changes" when it could not
165
+ * look is the failure mode this whole module exists to avoid.
166
+ */
167
+ export declare function formatSnapshotLostWarning(model: string, reason?: string): string;
168
+ /**
169
+ * Append the warning to a lane's review output so it survives into the file
170
+ * porch and the builder actually read — stderr scrolls past, review files don't.
171
+ *
172
+ * Written as a plain paragraph with no `VERDICT:` line, so `parseVerdict()`
173
+ * (which scans last→first for that prefix) still finds the reviewer's own
174
+ * verdict above it.
175
+ */
176
+ export declare function appendTreeChangeWarningToOutput(outputPath: string | undefined, warning: string): void;
177
+ //# sourceMappingURL=tree-tripwire.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree-tripwire.d.ts","sourceRoot":"","sources":["../../../src/commands/consult/tree-tripwire.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAOH;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,kBAAkB;IACjC,wBAAwB;IACxB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,4CAA4C;IAC5C,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,OAAO,CAAC;IACnB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6DAA6D;IAC7D,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,qDAAqD;IACrD,QAAQ,EAAE,kBAAkB,CAAC;CAC9B;AAmDD;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,aAAa,EAAE,MAAM,GAAG,YAAY,CAmDhE;AAiCD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAChC,aAAa,EAAE,MAAM,EACrB,UAAU,CAAC,EAAE,MAAM,EACnB,GAAG,GAAE,MAAsB,GAC1B,MAAM,GAAG,IAAI,CAMf;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,YAAY,EACnB,MAAM,GAAE,MAAM,EAAO,GACpB,MAAM,EAAE,CAqBV;AAED,yEAAyE;AACzE,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,GAAG,QAAQ,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,YAAY,GAAG,cAAc,EAAE,CAiBlG;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUxD;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,GAAG,MAAM,CAU7F;AAKD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAe9E;AAED;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAQhF;AAED;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,CAC7C,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,EAAE,MAAM,GACd,IAAI,CAQN"}
@@ -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"}