@specship/specship-darwin-arm64 0.29.0 → 0.30.0

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 (289) hide show
  1. package/bin/specship +1 -1
  2. package/lib/dist/.claude-plugin/plugin.json +15 -3
  3. package/lib/dist/activation/cheatsheet.d.ts +10 -7
  4. package/lib/dist/activation/cheatsheet.d.ts.map +1 -1
  5. package/lib/dist/activation/cheatsheet.js +21 -5
  6. package/lib/dist/activation/cheatsheet.js.map +1 -1
  7. package/lib/dist/activation/hook-output.d.ts +75 -2
  8. package/lib/dist/activation/hook-output.d.ts.map +1 -1
  9. package/lib/dist/activation/hook-output.js +114 -3
  10. package/lib/dist/activation/hook-output.js.map +1 -1
  11. package/lib/dist/activation/search-intercept.d.ts +20 -7
  12. package/lib/dist/activation/search-intercept.d.ts.map +1 -1
  13. package/lib/dist/activation/search-intercept.js +27 -5
  14. package/lib/dist/activation/search-intercept.js.map +1 -1
  15. package/lib/dist/activation/steering.d.ts +7 -4
  16. package/lib/dist/activation/steering.d.ts.map +1 -1
  17. package/lib/dist/activation/steering.js +10 -8
  18. package/lib/dist/activation/steering.js.map +1 -1
  19. package/lib/dist/agents/detect.d.ts +32 -0
  20. package/lib/dist/agents/detect.d.ts.map +1 -0
  21. package/lib/dist/agents/detect.js +54 -0
  22. package/lib/dist/agents/detect.js.map +1 -0
  23. package/lib/dist/agents/recent-project.d.ts +18 -0
  24. package/lib/dist/agents/recent-project.d.ts.map +1 -0
  25. package/lib/dist/agents/recent-project.js +92 -0
  26. package/lib/dist/agents/recent-project.js.map +1 -0
  27. package/lib/dist/analytics/canonical-tools.d.ts +23 -0
  28. package/lib/dist/analytics/canonical-tools.d.ts.map +1 -0
  29. package/lib/dist/analytics/canonical-tools.js +36 -0
  30. package/lib/dist/analytics/canonical-tools.js.map +1 -0
  31. package/lib/dist/analytics/canonical-tools.json +16 -0
  32. package/lib/dist/analytics/specship-impact.d.ts +3 -2
  33. package/lib/dist/analytics/specship-impact.d.ts.map +1 -1
  34. package/lib/dist/analytics/specship-impact.js +6 -4
  35. package/lib/dist/analytics/specship-impact.js.map +1 -1
  36. package/lib/dist/bin/specship.js +215 -170
  37. package/lib/dist/bin/specship.js.map +1 -1
  38. package/lib/dist/bin/uninstall.js +2 -2
  39. package/lib/dist/bin/uninstall.js.map +1 -1
  40. package/lib/dist/claudemd/audit.d.ts +12 -3
  41. package/lib/dist/claudemd/audit.d.ts.map +1 -1
  42. package/lib/dist/claudemd/audit.js +65 -22
  43. package/lib/dist/claudemd/audit.js.map +1 -1
  44. package/lib/dist/commands/specship/spec.md +9 -0
  45. package/lib/dist/db/migrations.d.ts +1 -1
  46. package/lib/dist/db/migrations.d.ts.map +1 -1
  47. package/lib/dist/db/migrations.js +18 -4
  48. package/lib/dist/db/migrations.js.map +1 -1
  49. package/lib/dist/db/schema.sql +8 -0
  50. package/lib/dist/db/spec-queries.d.ts +13 -1
  51. package/lib/dist/db/spec-queries.d.ts.map +1 -1
  52. package/lib/dist/db/spec-queries.js +21 -0
  53. package/lib/dist/db/spec-queries.js.map +1 -1
  54. package/lib/dist/enforce/changed-files.d.ts +7 -0
  55. package/lib/dist/enforce/changed-files.d.ts.map +1 -1
  56. package/lib/dist/enforce/changed-files.js +17 -0
  57. package/lib/dist/enforce/changed-files.js.map +1 -1
  58. package/lib/dist/enforce/enforce.d.ts +35 -1
  59. package/lib/dist/enforce/enforce.d.ts.map +1 -1
  60. package/lib/dist/enforce/enforce.js +66 -2
  61. package/lib/dist/enforce/enforce.js.map +1 -1
  62. package/lib/dist/extraction/index.d.ts +50 -4
  63. package/lib/dist/extraction/index.d.ts.map +1 -1
  64. package/lib/dist/extraction/index.js +105 -13
  65. package/lib/dist/extraction/index.js.map +1 -1
  66. package/lib/dist/extraction/parse-warnings.d.ts +16 -0
  67. package/lib/dist/extraction/parse-warnings.d.ts.map +1 -0
  68. package/lib/dist/extraction/parse-warnings.js +30 -0
  69. package/lib/dist/extraction/parse-warnings.js.map +1 -0
  70. package/lib/dist/extraction/tree-sitter.d.ts +27 -0
  71. package/lib/dist/extraction/tree-sitter.d.ts.map +1 -1
  72. package/lib/dist/extraction/tree-sitter.js +168 -1
  73. package/lib/dist/extraction/tree-sitter.js.map +1 -1
  74. package/lib/dist/health/smoke-check.js +1 -1
  75. package/lib/dist/health/smoke-check.js.map +1 -1
  76. package/lib/dist/hooks/hooks.json +16 -1
  77. package/lib/dist/index.d.ts +63 -3
  78. package/lib/dist/index.d.ts.map +1 -1
  79. package/lib/dist/index.js +235 -9
  80. package/lib/dist/index.js.map +1 -1
  81. package/lib/dist/installer/agent-record.d.ts +80 -0
  82. package/lib/dist/installer/agent-record.d.ts.map +1 -0
  83. package/lib/dist/installer/agent-record.js +351 -0
  84. package/lib/dist/installer/agent-record.js.map +1 -0
  85. package/lib/dist/installer/auto-migrate.d.ts +56 -0
  86. package/lib/dist/installer/auto-migrate.d.ts.map +1 -0
  87. package/lib/dist/installer/auto-migrate.js +328 -0
  88. package/lib/dist/installer/auto-migrate.js.map +1 -0
  89. package/lib/dist/installer/index.d.ts +37 -8
  90. package/lib/dist/installer/index.d.ts.map +1 -1
  91. package/lib/dist/installer/index.js +156 -46
  92. package/lib/dist/installer/index.js.map +1 -1
  93. package/lib/dist/installer/install-defaults.d.ts +4 -3
  94. package/lib/dist/installer/install-defaults.d.ts.map +1 -1
  95. package/lib/dist/installer/install-defaults.js +6 -5
  96. package/lib/dist/installer/install-defaults.js.map +1 -1
  97. package/lib/dist/installer/{config-writer.d.ts → targets/claude-config-writer.d.ts} +1 -1
  98. package/lib/dist/installer/targets/claude-config-writer.d.ts.map +1 -0
  99. package/lib/dist/installer/{config-writer.js → targets/claude-config-writer.js} +7 -6
  100. package/lib/dist/installer/targets/claude-config-writer.js.map +1 -0
  101. package/lib/dist/installer/targets/claude-projects.d.ts +9 -0
  102. package/lib/dist/installer/targets/claude-projects.d.ts.map +1 -0
  103. package/lib/dist/installer/targets/claude-projects.js +151 -0
  104. package/lib/dist/installer/targets/claude-projects.js.map +1 -0
  105. package/lib/dist/installer/targets/claude.d.ts +28 -1
  106. package/lib/dist/installer/targets/claude.d.ts.map +1 -1
  107. package/lib/dist/installer/targets/claude.js +39 -20
  108. package/lib/dist/installer/targets/claude.js.map +1 -1
  109. package/lib/dist/installer/targets/feature-parity.d.ts +28 -0
  110. package/lib/dist/installer/targets/feature-parity.d.ts.map +1 -0
  111. package/lib/dist/installer/targets/feature-parity.js +32 -0
  112. package/lib/dist/installer/targets/feature-parity.js.map +1 -0
  113. package/lib/dist/installer/{gemini-commands.d.ts → targets/gemini-commands.d.ts} +7 -3
  114. package/lib/dist/installer/targets/gemini-commands.d.ts.map +1 -0
  115. package/lib/dist/installer/{gemini-commands.js → targets/gemini-commands.js} +35 -14
  116. package/lib/dist/installer/targets/gemini-commands.js.map +1 -0
  117. package/lib/dist/installer/targets/gemini-projects.d.ts +11 -0
  118. package/lib/dist/installer/targets/gemini-projects.d.ts.map +1 -0
  119. package/lib/dist/installer/targets/gemini-projects.js +84 -0
  120. package/lib/dist/installer/targets/gemini-projects.js.map +1 -0
  121. package/lib/dist/installer/targets/gemini.d.ts +94 -8
  122. package/lib/dist/installer/targets/gemini.d.ts.map +1 -1
  123. package/lib/dist/installer/targets/gemini.js +392 -33
  124. package/lib/dist/installer/targets/gemini.js.map +1 -1
  125. package/lib/dist/installer/targets/registry.d.ts +8 -1
  126. package/lib/dist/installer/targets/registry.d.ts.map +1 -1
  127. package/lib/dist/installer/targets/registry.js +14 -5
  128. package/lib/dist/installer/targets/registry.js.map +1 -1
  129. package/lib/dist/installer/targets/shared.d.ts +44 -1
  130. package/lib/dist/installer/targets/shared.d.ts.map +1 -1
  131. package/lib/dist/installer/targets/shared.js +130 -3
  132. package/lib/dist/installer/targets/shared.js.map +1 -1
  133. package/lib/dist/installer/targets/types.d.ts +23 -1
  134. package/lib/dist/installer/targets/types.d.ts.map +1 -1
  135. package/lib/dist/jira/auto-publish.d.ts +22 -2
  136. package/lib/dist/jira/auto-publish.d.ts.map +1 -1
  137. package/lib/dist/jira/auto-publish.js +54 -9
  138. package/lib/dist/jira/auto-publish.js.map +1 -1
  139. package/lib/dist/jira/board-first.d.ts +15 -2
  140. package/lib/dist/jira/board-first.d.ts.map +1 -1
  141. package/lib/dist/jira/board-first.js +16 -0
  142. package/lib/dist/jira/board-first.js.map +1 -1
  143. package/lib/dist/jira/ci-publish.d.ts +72 -0
  144. package/lib/dist/jira/ci-publish.d.ts.map +1 -0
  145. package/lib/dist/jira/ci-publish.js +162 -0
  146. package/lib/dist/jira/ci-publish.js.map +1 -0
  147. package/lib/dist/jira/publish.d.ts +23 -0
  148. package/lib/dist/jira/publish.d.ts.map +1 -1
  149. package/lib/dist/jira/publish.js +0 -0
  150. package/lib/dist/jira/publish.js.map +1 -1
  151. package/lib/dist/jira/regression-pack.d.ts.map +1 -1
  152. package/lib/dist/jira/regression-pack.js +7 -1
  153. package/lib/dist/jira/regression-pack.js.map +1 -1
  154. package/lib/dist/jira/repo-config.d.ts.map +1 -1
  155. package/lib/dist/jira/repo-config.js +8 -0
  156. package/lib/dist/jira/repo-config.js.map +1 -1
  157. package/lib/dist/jira/types.d.ts +8 -0
  158. package/lib/dist/jira/types.d.ts.map +1 -1
  159. package/lib/dist/jira/types.js.map +1 -1
  160. package/lib/dist/knowledge/enforce.d.ts.map +1 -1
  161. package/lib/dist/knowledge/enforce.js +6 -1
  162. package/lib/dist/knowledge/enforce.js.map +1 -1
  163. package/lib/dist/mcp/jira-tools.d.ts +5 -0
  164. package/lib/dist/mcp/jira-tools.d.ts.map +1 -1
  165. package/lib/dist/mcp/jira-tools.js +59 -4
  166. package/lib/dist/mcp/jira-tools.js.map +1 -1
  167. package/lib/dist/mcp/proxy.d.ts.map +1 -1
  168. package/lib/dist/mcp/proxy.js +2 -1
  169. package/lib/dist/mcp/proxy.js.map +1 -1
  170. package/lib/dist/mcp/server-instructions.d.ts +16 -10
  171. package/lib/dist/mcp/server-instructions.d.ts.map +1 -1
  172. package/lib/dist/mcp/server-instructions.js +69 -20
  173. package/lib/dist/mcp/server-instructions.js.map +1 -1
  174. package/lib/dist/mcp/session.d.ts +11 -0
  175. package/lib/dist/mcp/session.d.ts.map +1 -1
  176. package/lib/dist/mcp/session.js +43 -1
  177. package/lib/dist/mcp/session.js.map +1 -1
  178. package/lib/dist/mcp/tools.d.ts.map +1 -1
  179. package/lib/dist/mcp/tools.js +8 -0
  180. package/lib/dist/mcp/tools.js.map +1 -1
  181. package/lib/dist/reflect/apply.d.ts +2 -2
  182. package/lib/dist/reflect/apply.d.ts.map +1 -1
  183. package/lib/dist/reflect/apply.js +6 -5
  184. package/lib/dist/reflect/apply.js.map +1 -1
  185. package/lib/dist/reflect/miner.d.ts.map +1 -1
  186. package/lib/dist/reflect/miner.js +28 -15
  187. package/lib/dist/reflect/miner.js.map +1 -1
  188. package/lib/dist/reflect/session-outcomes.d.ts.map +1 -1
  189. package/lib/dist/reflect/session-outcomes.js +8 -3
  190. package/lib/dist/reflect/session-outcomes.js.map +1 -1
  191. package/lib/dist/reflect/targets.d.ts.map +1 -1
  192. package/lib/dist/reflect/targets.js +25 -5
  193. package/lib/dist/reflect/targets.js.map +1 -1
  194. package/lib/dist/reflect/types.d.ts +7 -0
  195. package/lib/dist/reflect/types.d.ts.map +1 -1
  196. package/lib/dist/resolution/spec-link-resolver.d.ts +14 -2
  197. package/lib/dist/resolution/spec-link-resolver.d.ts.map +1 -1
  198. package/lib/dist/resolution/spec-link-resolver.js +13 -3
  199. package/lib/dist/resolution/spec-link-resolver.js.map +1 -1
  200. package/lib/dist/server/canonical-tools.js +50 -0
  201. package/lib/dist/server/cli.js +2 -2
  202. package/lib/dist/server/host-profiles.js +39 -0
  203. package/lib/dist/server/ingest/index.js +2 -1
  204. package/lib/dist/server/ingest/ingestor.js +195 -76
  205. package/lib/dist/server/ingest/project-paths.js +8 -2
  206. package/lib/dist/server/ingest/recorded-projects.js +43 -0
  207. package/lib/dist/server/ingest/specship-classify.js +7 -2
  208. package/lib/dist/server/ingest/transcripts/claude-jsonl.js +54 -0
  209. package/lib/dist/server/ingest/transcripts/claude.js +64 -0
  210. package/lib/dist/server/ingest/transcripts/gemini.js +469 -0
  211. package/lib/dist/server/ingest/transcripts/index.js +14 -0
  212. package/lib/dist/server/ingest/{parser.js → transcripts/shared.js} +3 -49
  213. package/lib/dist/server/ingest/transcripts/types.js +4 -0
  214. package/lib/dist/server/ingest/watcher.js +31 -30
  215. package/lib/dist/server/routes/claude.js +27 -10
  216. package/lib/dist/server/routes/graph.js +64 -3
  217. package/lib/dist/server/routes/mcp.js +57 -15
  218. package/lib/dist/server/routes/memory.js +54 -35
  219. package/lib/dist/server/routes/projects.js +78 -7
  220. package/lib/dist/server/routes/spec.js +125 -0
  221. package/lib/dist/server/server.js +8 -2
  222. package/lib/dist/skills/spec-author/SKILL.md +1 -1
  223. package/lib/dist/skills/spec-author/references/gap-questions.md +1 -1
  224. package/lib/dist/spec-lint/lint.d.ts +26 -1
  225. package/lib/dist/spec-lint/lint.d.ts.map +1 -1
  226. package/lib/dist/spec-lint/lint.js +96 -7
  227. package/lib/dist/spec-lint/lint.js.map +1 -1
  228. package/lib/dist/sync/worktree.d.ts +48 -0
  229. package/lib/dist/sync/worktree.d.ts.map +1 -1
  230. package/lib/dist/sync/worktree.js +109 -0
  231. package/lib/dist/sync/worktree.js.map +1 -1
  232. package/lib/dist/ui/assets/bricolage-latin-C23fTOVi.woff2 +0 -0
  233. package/lib/dist/ui/assets/bricolage-latin-ext-B82pxkPf.woff2 +0 -0
  234. package/lib/dist/ui/assets/index-54vorjyq.css +1 -0
  235. package/lib/dist/ui/assets/index-BZQonCVb.js +52 -0
  236. package/lib/dist/ui/assets/space-grotesk-latin-DPT1xrvW.woff2 +0 -0
  237. package/lib/dist/ui/assets/space-grotesk-latin-ext-wJGaI8U0.woff2 +0 -0
  238. package/lib/dist/ui/index.html +2 -2
  239. package/lib/dist/update/updater.d.ts +6 -0
  240. package/lib/dist/update/updater.d.ts.map +1 -1
  241. package/lib/dist/update/updater.js +13 -1
  242. package/lib/dist/update/updater.js.map +1 -1
  243. package/lib/dist/workflows/agents/claude.d.ts +9 -0
  244. package/lib/dist/workflows/agents/claude.d.ts.map +1 -1
  245. package/lib/dist/workflows/agents/claude.js +22 -3
  246. package/lib/dist/workflows/agents/claude.js.map +1 -1
  247. package/lib/dist/workflows/agents/gemini-version.d.ts +18 -0
  248. package/lib/dist/workflows/agents/gemini-version.d.ts.map +1 -0
  249. package/lib/dist/workflows/agents/gemini-version.js +81 -0
  250. package/lib/dist/workflows/agents/gemini-version.js.map +1 -0
  251. package/lib/dist/workflows/agents/gemini.d.ts +1 -1
  252. package/lib/dist/workflows/agents/gemini.d.ts.map +1 -1
  253. package/lib/dist/workflows/agents/gemini.js +12 -2
  254. package/lib/dist/workflows/agents/gemini.js.map +1 -1
  255. package/lib/dist/workflows/agents/index.d.ts +2 -0
  256. package/lib/dist/workflows/agents/index.d.ts.map +1 -1
  257. package/lib/dist/workflows/agents/index.js +5 -0
  258. package/lib/dist/workflows/agents/index.js.map +1 -1
  259. package/lib/dist/workflows/agents/types.d.ts +13 -0
  260. package/lib/dist/workflows/agents/types.d.ts.map +1 -1
  261. package/lib/dist/workflows/agents/types.js.map +1 -1
  262. package/lib/dist/workflows/condition-evaluator.d.ts +8 -0
  263. package/lib/dist/workflows/condition-evaluator.d.ts.map +1 -1
  264. package/lib/dist/workflows/condition-evaluator.js +7 -0
  265. package/lib/dist/workflows/condition-evaluator.js.map +1 -1
  266. package/lib/dist/workflows/defaults/claude-design-implement.yaml +11 -11
  267. package/lib/dist/workflows/defaults/spec-author.yaml +9 -9
  268. package/lib/dist/workflows/defaults/spec-fix.yaml +8 -8
  269. package/lib/dist/workflows/defaults/spec-implement.yaml +38 -23
  270. package/lib/dist/workflows/defaults/spec-relink.yaml +5 -5
  271. package/lib/dist/workflows/defaults/spec-verify.yaml +3 -3
  272. package/lib/dist/workflows/executor.d.ts +7 -0
  273. package/lib/dist/workflows/executor.d.ts.map +1 -1
  274. package/lib/dist/workflows/executor.js +22 -5
  275. package/lib/dist/workflows/executor.js.map +1 -1
  276. package/lib/node_modules/.package-lock.json +1 -1
  277. package/lib/package.json +3 -2
  278. package/package.json +1 -1
  279. package/lib/dist/installer/config-writer.d.ts.map +0 -1
  280. package/lib/dist/installer/config-writer.js.map +0 -1
  281. package/lib/dist/installer/gemini-commands.d.ts.map +0 -1
  282. package/lib/dist/installer/gemini-commands.js.map +0 -1
  283. package/lib/dist/ui/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  284. package/lib/dist/ui/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  285. package/lib/dist/ui/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  286. package/lib/dist/ui/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  287. package/lib/dist/ui/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  288. package/lib/dist/ui/assets/index-BzuUWUAE.js +0 -50
  289. package/lib/dist/ui/assets/index-ezwX2EfG.css +0 -1
@@ -61,6 +61,7 @@ const path = __importStar(require("path"));
61
61
  const fs = __importStar(require("fs"));
62
62
  const os = __importStar(require("os"));
63
63
  const directory_1 = require("../directory");
64
+ const hook_output_1 = require("../activation/hook-output");
64
65
  const worktree_1 = require("../sync/worktree");
65
66
  const shimmer_progress_1 = require("../ui/shimmer-progress");
66
67
  const glyphs_1 = require("../ui/glyphs");
@@ -69,6 +70,7 @@ const updater_1 = require("../update/updater");
69
70
  const resolve_latest_1 = require("../update/resolve-latest");
70
71
  const run_installer_1 = require("../update/run-installer");
71
72
  const wasm_runtime_flags_1 = require("../extraction/wasm-runtime-flags");
73
+ const parse_warnings_1 = require("../extraction/parse-warnings");
72
74
  // Lazy-load heavy modules (SpecShip, runInstaller) to keep CLI startup fast.
73
75
  async function loadSpecShip() {
74
76
  try {
@@ -118,133 +120,6 @@ async function loadServerPackage() {
118
120
  * The JSONL ingest watcher now ships inside `@specship/specship-server`
119
121
  * itself — `createServer({ ingest: true })` starts it. No separate package.
120
122
  */
121
- /**
122
- * Claude Code's slug encoding (every non-alphanumeric char → '-') is lossy,
123
- * so decoding by mapping '-' back to '/' mangles any path containing
124
- * hyphens/dots/underscores. Recover real paths from `~/.claude.json` (its
125
- * `projects` object is keyed by real absolute paths), then from a `"cwd"`
126
- * value in the slug's newest transcript, before falling back to the lossy
127
- * decode. Mirrors `server/src/ingest/project-paths.ts`
128
- * (REQ-SLUGRES-004) — the server package depends on this one, so the CLI
129
- * can't import it.
130
- */
131
- function buildClaudeSlugIndex() {
132
- const bySlug = new Map();
133
- try {
134
- const raw = fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8');
135
- const parsed = JSON.parse(raw);
136
- for (const realPath of Object.keys(parsed.projects ?? {})) {
137
- bySlug.set(realPath.replace(/[^A-Za-z0-9]/g, '-'), realPath);
138
- }
139
- }
140
- catch { /* missing or malformed — sniff/fallback below still work */ }
141
- return bySlug;
142
- }
143
- function sniffClaudeSlugCwd(claudeRoot, slug) {
144
- const dir = path.join(claudeRoot, slug);
145
- let newest = null;
146
- try {
147
- for (const f of fs.readdirSync(dir, { withFileTypes: true })) {
148
- if (!f.isFile() || !f.name.toLowerCase().endsWith('.jsonl'))
149
- continue;
150
- const st = fs.statSync(path.join(dir, f.name));
151
- if (!newest || st.mtimeMs > newest.mtimeMs)
152
- newest = { file: f.name, mtimeMs: st.mtimeMs };
153
- }
154
- }
155
- catch {
156
- return null;
157
- }
158
- if (!newest)
159
- return null;
160
- try {
161
- const fd = fs.openSync(path.join(dir, newest.file), 'r');
162
- let chunk;
163
- try {
164
- const buf = Buffer.alloc(64 * 1024);
165
- const read = fs.readSync(fd, buf, 0, buf.length, 0);
166
- chunk = buf.toString('utf8', 0, read);
167
- }
168
- finally {
169
- fs.closeSync(fd);
170
- }
171
- const m = /"cwd"\s*:\s*"((?:[^"\\]|\\.)*)"/.exec(chunk);
172
- if (!m)
173
- return null;
174
- let cwd = JSON.parse(`"${m[1]}"`);
175
- // Accept the cwd or an ancestor of it — sessions may record a subdir.
176
- for (;;) {
177
- if (cwd.replace(/[^A-Za-z0-9]/g, '-') === slug)
178
- return cwd;
179
- const parent = path.dirname(cwd);
180
- if (parent === cwd)
181
- return null;
182
- cwd = parent;
183
- }
184
- }
185
- catch {
186
- return null;
187
- }
188
- }
189
- /**
190
- * Auto-pick a default project for `specship desktop` when the user
191
- * didn't pass `-p` and the cwd isn't initialized either. Walks
192
- * `~/.claude/projects/*` (Claude Code's per-project transcript dirs),
193
- * resolves each slug back to a real path, and returns the most-recently-
194
- * touched one that's still on disk AND has been `specship init`'d.
195
- *
196
- * Returns null when nothing qualifies — the server boots projectless and
197
- * the desktop UI prompts the user to pick.
198
- */
199
- async function pickRecentInitializedProject() {
200
- const claudeRoot = path.join(os.homedir(), '.claude', 'projects');
201
- let entries;
202
- try {
203
- entries = fs.readdirSync(claudeRoot, { withFileTypes: true });
204
- }
205
- catch {
206
- return null;
207
- }
208
- const slugIndex = buildClaudeSlugIndex();
209
- const candidates = [];
210
- for (const ent of entries) {
211
- if (!ent.isDirectory())
212
- continue;
213
- const slug = ent.name;
214
- const decoded = slugIndex.get(slug) ??
215
- sniffClaudeSlugCwd(claudeRoot, slug) ??
216
- (slug.startsWith('-') ? '/' + slug.slice(1).replace(/-/g, '/') : slug);
217
- try {
218
- const st = fs.statSync(decoded);
219
- if (!st.isDirectory())
220
- continue;
221
- }
222
- catch {
223
- continue;
224
- }
225
- if (!(0, directory_1.isInitialized)(decoded))
226
- continue;
227
- // Use the newest .jsonl mtime in the slug dir as the activity signal.
228
- let lastTouched = 0;
229
- try {
230
- const files = fs.readdirSync(path.join(claudeRoot, slug), { withFileTypes: true });
231
- for (const f of files) {
232
- if (!f.isFile() || !f.name.toLowerCase().endsWith('.jsonl'))
233
- continue;
234
- try {
235
- const st = fs.statSync(path.join(claudeRoot, slug, f.name));
236
- if (st.mtimeMs > lastTouched)
237
- lastTouched = st.mtimeMs;
238
- }
239
- catch { /* ignore */ }
240
- }
241
- }
242
- catch { /* ignore */ }
243
- candidates.push({ decoded, lastTouched });
244
- }
245
- candidates.sort((a, b) => b.lastTouched - a.lastTouched);
246
- return candidates[0]?.decoded ?? null;
247
- }
248
123
  // Dynamic import helper — tsc compiles import() to require() in CJS mode,
249
124
  // which fails for ESM-only packages. This bypasses the transformation.
250
125
  // eslint-disable-next-line @typescript-eslint/no-implied-eval
@@ -333,9 +208,79 @@ function main() {
333
208
  .name('specship')
334
209
  .description('Code intelligence and knowledge graph for any codebase')
335
210
  .version(packageJson.version);
211
+ /**
212
+ * Commands that run on every agent turn / tool call (the installed hooks, the
213
+ * status line) or as the long-lived MCP server. They must stay exactly as
214
+ * fast as before, so the installed-agents record is never checked on them.
215
+ */
216
+ const AGENT_RECORD_SKIP_COMMANDS = new Set([
217
+ 'serve',
218
+ 'cheatsheet',
219
+ 'spec-nudge',
220
+ 'steer-nudge',
221
+ 'search-intercept',
222
+ 'statusline',
223
+ 'sync',
224
+ 'sync-if-dirty',
225
+ 'mark-dirty',
226
+ 'auto-sync',
227
+ 'hook',
228
+ ]);
229
+ // REQ-AGENTSWITCH-006.A2: any other command rebuilds a missing/unreadable
230
+ // installed-agents record from the agents' configs. A present record costs one
231
+ // file read and no write. Silent: the record never blocks or fails a command.
232
+ /**
233
+ * Commands that never auto-set-up a newly used agent (REQ-AGENTSWITCH-008):
234
+ * the install commands do it explicitly, and the MCP server detects its
235
+ * agent from the client name in its own session instead.
236
+ */
237
+ const AUTO_MIGRATE_SKIP_COMMANDS = new Set(['install', 'uninstall', 'serve', 'update']);
238
+ program.hook('preAction', (_root, actionCommand) => {
239
+ let top = actionCommand;
240
+ while (top.parent && top.parent !== program)
241
+ top = top.parent;
242
+ // REQ-AGENTSWITCH-008: a `specship` command in a supported agent's shell —
243
+ // typed by the user, run by a hook, or by the git hook a commit from that
244
+ // shell fires — sets SpecShip up for that agent if the record doesn't list
245
+ // it here. Unknown agent → no file access; recorded agent → one small read.
246
+ // The notice goes to stderr so hook stdout stays the host's JSON.
247
+ if (!AUTO_MIGRATE_SKIP_COMMANDS.has(top.name())) {
248
+ try {
249
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
250
+ const { agentFromEnv } = require('../agents/detect');
251
+ const agent = agentFromEnv();
252
+ if (agent !== 'unknown') {
253
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
254
+ const { maybeAutoMigrate, migrationLine } = require('../installer/auto-migrate');
255
+ const line = migrationLine(maybeAutoMigrate(agent));
256
+ if (line)
257
+ process.stderr.write(line + '\n');
258
+ }
259
+ }
260
+ catch {
261
+ /* advisory — never blocks or fails a command */
262
+ }
263
+ }
264
+ if (AGENT_RECORD_SKIP_COMMANDS.has(top.name()))
265
+ return;
266
+ try {
267
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
268
+ const { ensureAgentRecord } = require('../installer/agent-record');
269
+ ensureAgentRecord(process.cwd());
270
+ }
271
+ catch {
272
+ /* advisory */
273
+ }
274
+ });
336
275
  // =============================================================================
337
276
  // Helper Functions
338
277
  // =============================================================================
278
+ /** Every supported agent's display name, from the target registry (REQ-AGENTSWITCH-012). */
279
+ function agentNames() {
280
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
281
+ const { allTargets } = require('../installer/targets/registry');
282
+ return allTargets().map((t) => t.displayName).join(', ');
283
+ }
339
284
  /**
340
285
  * Resolve project path from argument or current directory
341
286
  * Walks up parent directories to find nearest initialized SpecShip project
@@ -496,7 +441,7 @@ function main() {
496
441
  if (!sp)
497
442
  return;
498
443
  console.log();
499
- console.log(chalk.bold('Try this first — ask Claude:'));
444
+ console.log(chalk.bold('Try this first — ask your coding agent:'));
500
445
  console.log(' ' + chalk.cyan(sp.prompt));
501
446
  console.log(chalk.dim(" (it'll explore the index instead of reading files)"));
502
447
  }
@@ -540,6 +485,10 @@ function main() {
540
485
  else {
541
486
  clack.log.warn('No files found to index');
542
487
  }
488
+ // Parse-error regions that may have swallowed declarations (REQ-HTMLLEX-003).
489
+ const parseWarning = (0, parse_warnings_1.formatParseWarningSummary)(result.filesWithParseWarnings);
490
+ if (parseWarning)
491
+ clack.log.warn(parseWarning);
543
492
  if (hasErrors) {
544
493
  const errorsByCode = new Map();
545
494
  for (const err of result.errors) {
@@ -854,10 +803,12 @@ function main() {
854
803
  // CLAUDE.md governance findings ride the same SessionStart line
855
804
  // (CLAUDEMD-DOC, REQ-CLAUDEMD-003) — silent at zero.
856
805
  try {
857
- const { readClaudeMdAudit } = require('../claudemd/audit');
806
+ const { readClaudeMdAudit, auditMemoryFiles } = require('../claudemd/audit');
858
807
  const audit = readClaudeMdAudit(projectPath);
859
808
  if (audit && audit.findings.length > 0) {
860
- console.log(`⚠ ${audit.findings.length} CLAUDE.md finding(s) — review with /specship:check claudemd`);
809
+ // Name the memory file(s) the findings are about (REQ-AGENTSWITCH-003).
810
+ const files = auditMemoryFiles(audit).join(' and ');
811
+ console.log(`⚠ ${audit.findings.length} ${files} finding(s) — review with /specship:check claudemd`);
861
812
  }
862
813
  }
863
814
  catch {
@@ -997,6 +948,10 @@ function main() {
997
948
  parts.push(`${specWarnings} warning${specWarnings === 1 ? '' : 's'}`);
998
949
  clack.log.warn(`Spec parse: ${parts.join(', ')}`);
999
950
  }
951
+ // Parse-error regions that may have swallowed declarations (REQ-HTMLLEX-003).
952
+ const parseWarning = (0, parse_warnings_1.formatParseWarningSummary)(result.filesWithParseWarnings);
953
+ if (parseWarning)
954
+ clack.log.warn(parseWarning);
1000
955
  pushDriftNotices(result);
1001
956
  await pushJiraDriftComments(cg, result);
1002
957
  await pushJiraAutoPublish(result);
@@ -1026,6 +981,10 @@ function main() {
1026
981
  // working tree (e.g. a nested worktree borrowing the main checkout's index).
1027
982
  const startPath = path.resolve(pathArg || process.cwd());
1028
983
  const worktreeMismatch = (0, worktree_1.detectWorktreeIndexMismatch)(startPath, projectPath);
984
+ // The agent whose shell ran this command, from its launcher env vars
985
+ // (REQ-AGENTSWITCH-007). Report-only: detection never changes anything.
986
+ const { agentFromEnv, agentDisplayName } = await Promise.resolve().then(() => __importStar(require('../agents/detect')));
987
+ const currentAgent = agentFromEnv(process.env);
1029
988
  try {
1030
989
  if (!(0, directory_1.isInitialized)(projectPath)) {
1031
990
  if (options.json) {
@@ -1035,11 +994,13 @@ function main() {
1035
994
  projectPath,
1036
995
  indexPath: (0, directory_1.getSpecShipDir)(projectPath),
1037
996
  lastIndexed: null,
997
+ currentAgent,
1038
998
  }));
1039
999
  return;
1040
1000
  }
1041
1001
  console.log(chalk.bold('\nSpecShip Status\n'));
1042
1002
  info(`Project: ${projectPath}`);
1003
+ info(`Agent: ${agentDisplayName(currentAgent)}`);
1043
1004
  warn('Not initialized');
1044
1005
  info('Run "specship init" to initialize');
1045
1006
  return;
@@ -1050,6 +1011,9 @@ function main() {
1050
1011
  const changes = cg.getChangedFiles();
1051
1012
  const backend = cg.getBackend();
1052
1013
  const journalMode = cg.getJournalMode();
1014
+ // Recorded index revision vs live HEAD (REQ-MULTIDEV-004). Report-only:
1015
+ // nothing here syncs because HEAD moved.
1016
+ const freshness = cg.getIndexFreshness();
1053
1017
  // JSON output mode
1054
1018
  if (options.json) {
1055
1019
  const lastIndexedMs = cg.getLastIndexedAt();
@@ -1075,6 +1039,8 @@ function main() {
1075
1039
  worktreeMismatch: worktreeMismatch
1076
1040
  ? { worktreeRoot: worktreeMismatch.worktreeRoot, indexRoot: worktreeMismatch.indexRoot }
1077
1041
  : null,
1042
+ ...(0, worktree_1.indexFreshnessJsonFields)(freshness),
1043
+ currentAgent,
1078
1044
  }));
1079
1045
  cg.destroy();
1080
1046
  return;
@@ -1082,9 +1048,16 @@ function main() {
1082
1048
  console.log(chalk.bold('\nSpecShip Status\n'));
1083
1049
  // Project info
1084
1050
  console.log(chalk.cyan('Project:'), projectPath);
1051
+ console.log(chalk.cyan('Agent: '), agentDisplayName(currentAgent));
1085
1052
  if (worktreeMismatch) {
1086
1053
  warn((0, worktree_1.worktreeMismatchWarning)(worktreeMismatch));
1087
1054
  }
1055
+ if (freshness) {
1056
+ console.log(chalk.cyan('Indexed:'), (0, worktree_1.formatRevision)(freshness.indexedBranch, freshness.indexedCommit));
1057
+ console.log(chalk.cyan('HEAD: '), (0, worktree_1.formatRevision)(freshness.headBranch, freshness.headCommit));
1058
+ if (freshness.stale)
1059
+ warn((0, worktree_1.indexStaleNotice)(freshness));
1060
+ }
1088
1061
  console.log();
1089
1062
  // Index stats
1090
1063
  console.log(chalk.bold('Index Statistics:'));
@@ -1163,7 +1136,7 @@ function main() {
1163
1136
  */
1164
1137
  program
1165
1138
  .command('statusline')
1166
- .description('Print a SpecShip status-line segment (reads Claude Code JSON on stdin)')
1139
+ .description('Print a SpecShip status-line segment (Claude-only surface — reads the status-line JSON the agent pipes on stdin)')
1167
1140
  .action(async () => {
1168
1141
  const { buildSegment } = await Promise.resolve().then(() => __importStar(require('../statusline/index')));
1169
1142
  let raw = '';
@@ -1548,7 +1521,7 @@ function main() {
1548
1521
  .command('capture')
1549
1522
  .description('Capture a lesson/anti-pattern as a human-gated memory rule (lesson text on stdin).')
1550
1523
  .requiredOption('--title <title>', 'short title for the lesson')
1551
- .option('--target <target>', 'where apply would write it: "memory" (portable ~/.claude note, default) or "claude-md" (project CLAUDE.md)', 'memory')
1524
+ .option('--target <target>', 'where apply would write it: "memory" (portable user memory, default) or "claude-md" (the project memory file of the active agent)', 'memory')
1552
1525
  .option('--path <projectRoot>', 'project root (default: cwd)')
1553
1526
  .option('-j, --json', 'output the created proposal as JSON')
1554
1527
  .action(async (options) => {
@@ -1578,7 +1551,7 @@ function main() {
1578
1551
  }
1579
1552
  else {
1580
1553
  info(`Captured lesson as proposal ${p.contentHash.slice(0, 12)} (${p.state}): ${p.title}`);
1581
- info(`Target: ${p.targetKind === 'claude_md' ? 'project CLAUDE.md' : 'portable ~/.claude memory note'} — nothing written yet.`);
1554
+ info(`Target: ${memoryTargetLabel(p, projectPath)} — nothing written yet.`);
1582
1555
  info('Review with `specship reflect`; manage applied memory rules with `specship memory list`.');
1583
1556
  }
1584
1557
  cg.destroy();
@@ -1588,6 +1561,20 @@ function main() {
1588
1561
  process.exit(1);
1589
1562
  }
1590
1563
  });
1564
+ /**
1565
+ * Where a memory_rule proposal writes, named from its own target path so each
1566
+ * agent's memory file shows (REQ-AGENTSWITCH-003).
1567
+ */
1568
+ function memoryTargetLabel(p, projectRoot) {
1569
+ if (p.targetKind !== 'claude_md') {
1570
+ const dir = path.dirname(p.targetPath);
1571
+ const home = os.homedir();
1572
+ return `portable memory note in ${dir.startsWith(home) ? '~' + dir.slice(home.length) : dir}`;
1573
+ }
1574
+ const file = path.basename(p.targetPath);
1575
+ const inProject = !path.relative(path.resolve(projectRoot), p.targetPath).startsWith('..');
1576
+ return inProject ? `project ${file}` : `portable user ${file}`;
1577
+ }
1591
1578
  // Body text of a memory_rule proposal's payload (the note or the CLAUDE.md block).
1592
1579
  function memoryRuleBody(p) {
1593
1580
  if (p.payload.kind === 'memory_note')
@@ -1623,7 +1610,7 @@ function main() {
1623
1610
  return;
1624
1611
  }
1625
1612
  for (const p of items) {
1626
- const target = p.targetKind === 'claude_md' ? 'CLAUDE.md' : 'memory note';
1613
+ const target = p.targetKind === 'claude_md' ? path.basename(p.targetPath) : 'memory note';
1627
1614
  success(`${p.contentHash.slice(0, 12)} [${target}] ${p.title}`);
1628
1615
  }
1629
1616
  cg.destroy();
@@ -2260,7 +2247,7 @@ function main() {
2260
2247
  .option('-j, --json', 'Output as JSON')
2261
2248
  .option('--strict', 'Treat every check as gating for this run only (nothing read from or written to config)')
2262
2249
  .option('--enable-gate <checks...>', 'Persist gating for the named checks into specship.config.json, then run the gate')
2263
- .option('--since <ref>', 'Evaluate only requirements whose links or spec files touch files changed since <ref> (drift + behaviour; fitness and maintainability stay repo-global)')
2250
+ .option('--since <ref>', 'Evaluate only requirements whose links or spec files touch files changed since <ref> (drift + behaviour; fitness and maintainability stay repo-global), and report links that resolved at <ref> but this branch broke (branchBroken)')
2264
2251
  .action(async (pathArg, options) => {
2265
2252
  const projectPath = resolveProjectPath(pathArg);
2266
2253
  try {
@@ -2300,6 +2287,8 @@ function main() {
2300
2287
  }
2301
2288
  const { default: SpecShip } = await loadSpecShip();
2302
2289
  const cg = await SpecShip.open(projectPath);
2290
+ // The branch-broken check parses link targets at the ref (REQ-MULTIDEV-002).
2291
+ await cg.prepareEnforce(scope);
2303
2292
  // --strict gates everything for this run only (REQ-ENFORCE-004.A2).
2304
2293
  const r = options.strict
2305
2294
  ? cg.getEnforce(enforce.strictEnforceConfig(), scope)
@@ -2312,7 +2301,8 @@ function main() {
2312
2301
  console.log(chalk.bold('\nEnforcement gate\n'));
2313
2302
  if (scope) {
2314
2303
  console.log(chalk.dim(` scoped to ${scope.changedFiles.length} file(s) changed since ${scope.since}` +
2315
- ' — drift and behaviour are link-scoped; fitness and maintainability are repo-global analyses and ran unscoped.\n'));
2304
+ ' — drift and behaviour are link-scoped; branchBroken compares against that ref;' +
2305
+ ' fitness, maintainability and undeclaredLinks are repo-global and ran unscoped.\n'));
2316
2306
  }
2317
2307
  for (const c of r.checks) {
2318
2308
  const tag = c.gating ? chalk.dim('[gating]') : chalk.dim('[advisory]');
@@ -2390,7 +2380,7 @@ function main() {
2390
2380
  root = process.cwd();
2391
2381
  }
2392
2382
  else {
2393
- root = await pickRecentInitializedProject();
2383
+ root = await (await Promise.resolve().then(() => __importStar(require('../agents/recent-project')))).pickRecentInitializedProject();
2394
2384
  }
2395
2385
  const port = options.port ? parseInt(options.port, 10) : 4242;
2396
2386
  const host = options.host ?? '127.0.0.1';
@@ -2431,7 +2421,7 @@ function main() {
2431
2421
  console.error(chalk.green((0, glyphs_1.getGlyphs)().ok) + ` Dashboard: ${handle.url}/`);
2432
2422
  }
2433
2423
  if (options.ingest !== false) {
2434
- console.error(chalk.dim(' Claude Code transcript ingest watcher active'));
2424
+ console.error(chalk.dim(` Transcript ingest watcher active (${agentNames()})`));
2435
2425
  }
2436
2426
  if (options.mcp) {
2437
2427
  if (!root) {
@@ -2533,7 +2523,7 @@ function main() {
2533
2523
  // Use stderr so stdout stays clean for any piped/stdio usage.
2534
2524
  console.error(chalk.bold('\nSpecShip MCP Server\n'));
2535
2525
  console.error(chalk.blue((0, glyphs_1.getGlyphs)().info) + ' Use --mcp flag to start the MCP server');
2536
- console.error('\nTo use with Claude Code, add to your MCP configuration:');
2526
+ console.error('\nTo use it from your coding agent, add to its MCP configuration (or run `specship install --print-config --target <agent>`):');
2537
2527
  console.error(chalk.dim(`
2538
2528
  {
2539
2529
  "mcpServers": {
@@ -2570,8 +2560,8 @@ function main() {
2570
2560
  .option('-p, --path <path>', 'Project path (default: cwd, else your most recent initialized project)')
2571
2561
  .option('--port <n>', 'HTTP port (default 4242)')
2572
2562
  .option('--host <h>', 'HTTP bind host (default 127.0.0.1)')
2573
- .option('--ingest', 'Enable the Claude Code JSONL transcript watcher', true)
2574
- .option('--no-ingest', 'Disable the Claude Code JSONL transcript watcher')
2563
+ .option('--ingest', 'Enable the agent transcript watcher (every supported agent)', true)
2564
+ .option('--no-ingest', 'Disable the agent transcript watcher')
2575
2565
  .option('--web-dir <path>', 'Path to a built desktop SPA (index.html lives here); auto-detected by default from the bundled ui/dist')
2576
2566
  .option('--no-web', 'Run headless (API only, no dashboard SPA)')
2577
2567
  .option('--no-watch', 'Disable the file watcher (no auto-sync; useful on slow filesystems like WSL2 /mnt drives)')
@@ -2975,7 +2965,7 @@ function main() {
2975
2965
  */
2976
2966
  program
2977
2967
  .command('install')
2978
- .description('Install specship MCP server into Claude Code')
2968
+ .description('Install the specship MCP server into your coding agent (choose one with --target)')
2979
2969
  .option('-l, --location <where>', 'Install location: "global" or "local". Default: prompt (local)')
2980
2970
  .option('-y, --yes', 'Non-interactive: defaults to --location=local (the only question an interactive install asks)')
2981
2971
  .option('--no-permissions', 'Skip writing the auto-allow permissions list (written by default)')
@@ -2984,30 +2974,47 @@ function main() {
2984
2974
  .option('--path <repo>', 'Target repo to wire and initialize (default: current directory). Project-local files and the .specship index land there')
2985
2975
  .option('--with-jira', 'Enable the optional JIRA integration (talks to your Atlassian instance; off by default — the core install is 100% local)')
2986
2976
  .option('--with-designer', 'Enable the optional Designer integration (EXPERIMENTAL — drives claude.ai/design via a debug Chrome session and may break without notice; off by default)')
2987
- .option('--statusline', '(default) Wire the SpecShip status-line segment into Claude; never overwrites an existing status line')
2977
+ .option('--statusline', '(default; Claude-only surface) Wire the SpecShip status-line segment; never overwrites an existing status line')
2988
2978
  .option('--skip-statusline', 'Do not add the status-line segment (it is added by default)')
2989
2979
  .option('--skip-index', 'Do not index the current project (it is indexed by default)')
2990
- .option('--print-config', 'Print the MCP config snippet and exit (no file writes). Claude Code by default; `--target gemini` prints the Gemini CLI settings snippet')
2980
+ .option('--print-config', 'Print the target agent\'s MCP config snippet and exit (no file writes); pick the agent with --target')
2991
2981
  // -t/--target SELECTS the agents to wire (GEMINI-TARGET-DOC, REQ-GEMINI-002)
2992
2982
  // and also picks the snippet --print-config prints. The legacy spellings
2993
2983
  // ("claude" / "auto" / "all" / "none") keep their Claude-only meanings, so
2994
2984
  // existing invocations — including our own offline-install scripts — behave
2995
2985
  // exactly as before.
2996
2986
  .option('-t, --target <ids>', 'Agents to wire, comma-separated: "claude" (default) | "gemini" | "claude,gemini". Also selects the snippet `--print-config` prints. Legacy: "auto"/"all" = claude, "none" = skip')
2997
- .option('--trust', 'Gemini only (REQ-GEMINI-009): write ~/.gemini/policies/specship.toml auto-allowing SpecShip\'s read-only tools. Opt-in — an admin policy still wins and run_shell_command is unaffected')
2987
+ .option('--trust', `Gemini only (REQ-GEMINI-009): write ~/${(0, hook_output_1.hostTraitsFor)('gemini').configDir}/policies/specship.toml auto-allowing SpecShip's read-only tools. Opt-in — an admin policy still wins and run_shell_command is unaffected`)
2988
+ // REQ-AGENTSWITCH-006.A3: reinstall for every recorded agent. Run by
2989
+ // `specship update` (through the new binary) and the bundle installer; exits
2990
+ // 3 when nothing is recorded so the bundle installer can fall back.
2991
+ .addOption(new commander_1.Option('--replay-recorded', 'Reinstall for every agent in the installed-agents record').hideHelp())
2998
2992
  .action(async (opts) => {
2993
+ if (opts.replayRecorded) {
2994
+ const { reinstallRecordedAgents } = await Promise.resolve().then(() => __importStar(require('../installer')));
2995
+ const reports = reinstallRecordedAgents();
2996
+ if (reports.length === 0) {
2997
+ info('No agents recorded — nothing to reinstall.');
2998
+ process.exit(3);
2999
+ }
3000
+ for (const r of reports) {
3001
+ if (r.status === 'reinstalled')
3002
+ success(r.detail);
3003
+ else
3004
+ warn(r.detail);
3005
+ }
3006
+ if (reports.some((r) => r.status === 'failed'))
3007
+ process.exit(1);
3008
+ return;
3009
+ }
2999
3010
  if (opts.printConfig) {
3000
3011
  const loc = (opts.location === 'local' ? 'local' : 'global');
3001
- // Only `gemini` selects the non-default snippet; every other value
3002
- // (including the vestigial "auto" / "all" / "none") stays Claude, so
3003
- // existing invocations print exactly what they printed before.
3004
- if (opts.target === 'gemini') {
3005
- const { geminiTarget } = await Promise.resolve().then(() => __importStar(require('../installer/targets/gemini')));
3006
- process.stdout.write(geminiTarget.printConfig(loc));
3007
- return;
3008
- }
3009
- const { claudeTarget } = await Promise.resolve().then(() => __importStar(require('../installer/targets/claude')));
3010
- process.stdout.write(claudeTarget.printConfig(loc));
3012
+ // A registered target id prints that target's snippet; any other value
3013
+ // (including the vestigial "auto" / "all" / "none") prints the default
3014
+ // host's, so existing invocations print exactly what they printed before.
3015
+ const { getTarget } = await Promise.resolve().then(() => __importStar(require('../installer/targets/registry')));
3016
+ const target = getTarget(String(opts.target ?? '')) ?? getTarget(hook_output_1.DEFAULT_HOST_TRAITS.id);
3017
+ process.stdout.write(target.printConfig(loc));
3011
3018
  return;
3012
3019
  }
3013
3020
  // Target repo (INSTALL-SCOPE-DOC, REQ-SCOPE-002): the project-local
@@ -3260,7 +3267,12 @@ function main() {
3260
3267
  }
3261
3268
  }
3262
3269
  catch { /* store unavailable → omit the line */ }
3263
- const payload = buildCheatsheetPayload(process.env, { conventions, openProposals });
3270
+ // SessionStart is sent by every host, so the launcher env identifies the
3271
+ // agent; unknown → today's text (REQ-AGENTSWITCH-002).
3272
+ const { agentFromEnv, UNKNOWN_AGENT } = await Promise.resolve().then(() => __importStar(require('../agents/detect')));
3273
+ const agent = agentFromEnv(process.env);
3274
+ const traits = agent === UNKNOWN_AGENT ? undefined : (0, hook_output_1.hostTraitsFor)(agent);
3275
+ const payload = buildCheatsheetPayload(process.env, { conventions, openProposals }, traits);
3264
3276
  if (payload)
3265
3277
  process.stdout.write(JSON.stringify(payload) + '\n');
3266
3278
  process.exit(0);
@@ -3276,7 +3288,7 @@ function main() {
3276
3288
  */
3277
3289
  program
3278
3290
  .command('spec-nudge')
3279
- .description('Internal hook: nudge toward spec-author on feature/bug intent (UserPromptSubmit)')
3291
+ .description(`Internal hook: nudge toward spec-author on feature/bug intent (${hook_output_1.DEFAULT_HOST_TRAITS.fallbackPromptEvent})`)
3280
3292
  .action(async () => {
3281
3293
  const shouldNudge = (prompt) => {
3282
3294
  const text = (prompt || '').trim();
@@ -3319,7 +3331,7 @@ function main() {
3319
3331
  */
3320
3332
  program
3321
3333
  .command('steer-nudge')
3322
- .description('Internal hook: steer flow/structure questions to specship_explore (UserPromptSubmit)')
3334
+ .description(`Internal hook: steer flow/structure questions to specship_explore (${hook_output_1.DEFAULT_HOST_TRAITS.fallbackPromptEvent})`)
3323
3335
  .action(async () => {
3324
3336
  // Prefer the hook payload's cwd (Claude Code / Gemini CLI both send it
3325
3337
  // on stdin) so the check targets the project the prompt belongs to, not
@@ -3350,7 +3362,7 @@ function main() {
3350
3362
  catch { /* telemetry must never break the hook */ }
3351
3363
  }
3352
3364
  const { buildSteeringNudge } = await Promise.resolve().then(() => __importStar(require('../activation/steering')));
3353
- const additionalContext = buildSteeringNudge(cwd, process.env, undefined, traits.toolPrefix);
3365
+ const additionalContext = buildSteeringNudge(cwd, traits, process.env);
3354
3366
  if (additionalContext) {
3355
3367
  process.stdout.write(buildHookAdditionalContext(additionalContext, payload.hookEventName, traits.fallbackPromptEvent));
3356
3368
  }
@@ -3394,10 +3406,6 @@ function main() {
3394
3406
  }
3395
3407
  }
3396
3408
  catch { /* not JSON — strict path stays off, advisory still works via payload */ }
3397
- const DENY_TOOLS = new Set(traits.denyEligibleTools);
3398
- const shellCommand = traits.shellTool !== null && toolName === traits.shellTool && typeof toolInput.command === 'string'
3399
- ? toolInput.command
3400
- : null;
3401
3409
  const { resolveSetting } = await Promise.resolve().then(() => __importStar(require('../config/runtime-settings')));
3402
3410
  const strict = traits.supportsBeforeToolDeny &&
3403
3411
  resolveSetting('SPECSHIP_GEMINI_STRICT', cwd, process.env, os.homedir()) === '1';
@@ -3409,11 +3417,10 @@ function main() {
3409
3417
  cwd,
3410
3418
  sessionId: payload.sessionId,
3411
3419
  transcriptPath: payload.transcriptPath,
3412
- specshipToolMarkers: traits.specshipToolMarkers,
3413
- transcriptFormat: traits.transcriptFormat,
3420
+ traits,
3421
+ toolName,
3422
+ toolInput,
3414
3423
  strict,
3415
- denyEligible: DENY_TOOLS.has(toolName),
3416
- shellCommand,
3417
3424
  searchPattern,
3418
3425
  });
3419
3426
  if (result) {
@@ -3443,12 +3450,28 @@ function main() {
3443
3450
  .description('Completely remove SpecShip (wiring, indexes, ~/.specship data, and the binary)')
3444
3451
  .option('-y, --yes', 'Skip the confirmation prompt (also: --force)')
3445
3452
  .option('-f, --force', 'Alias for --yes')
3446
- .option('--keep-data', 'Only unwire from Claude Code; keep the indexes, ~/.specship data, and the binary')
3453
+ .option('--keep-data', 'Only unwire from the selected agent(s); keep the indexes, ~/.specship data, and the binary')
3447
3454
  .option('-l, --location <where>', 'With --keep-data: which wiring to remove ("global" or "local")')
3448
- // vestigial — kept so existing `--target claude` invocations keep working.
3449
- .option('-t, --target <ids>', '(vestigial) accepted: "claude" | "auto" | "all" | "none"')
3455
+ .option('--local', 'Only unwire this project (same as --keep-data --location local); keeps indexes, data and the binary')
3456
+ .option('--global', 'Only unwire the user-wide install (same as --keep-data --location global); keeps indexes, data and the binary')
3457
+ .option('-t, --target <ids>', 'Agents to unwire: "claude" | "gemini" | "claude,gemini". With --keep-data/--local/--global the default is claude; a full purge with no --target unwires every agent')
3450
3458
  .action(async (opts) => {
3451
3459
  const { runUninstaller } = await Promise.resolve().then(() => __importStar(require('../installer')));
3460
+ // `--local` / `--global` (REQ-AGENTSWITCH-008): the undo command an
3461
+ // automatic agent setup prints — wiring-only, never the full purge.
3462
+ if (opts.local && opts.global) {
3463
+ error('Use only one of --local and --global.');
3464
+ process.exit(1);
3465
+ }
3466
+ if (opts.local || opts.global) {
3467
+ const where = opts.local ? 'local' : 'global';
3468
+ if (opts.location && opts.location !== where) {
3469
+ error(`--${where} conflicts with --location ${opts.location}.`);
3470
+ process.exit(1);
3471
+ }
3472
+ opts.location = where;
3473
+ opts.keepData = true;
3474
+ }
3452
3475
  if (opts.location && opts.location !== 'global' && opts.location !== 'local') {
3453
3476
  error(`--location must be "global" or "local" (got "${opts.location}").`);
3454
3477
  process.exit(1);
@@ -4368,6 +4391,9 @@ function main() {
4368
4391
  detect: () => (0, updater_1.detectInstallMethod)(__dirname, installDir),
4369
4392
  resolveLatest: (method) => (0, resolve_latest_1.resolveLatestVersion)(method),
4370
4393
  runInstaller: (method) => (0, run_installer_1.runInstaller)(method, { installDir, binDir }),
4394
+ // REQ-AGENTSWITCH-006.A3: the old code is still loaded in this
4395
+ // process, so the reinstall runs through the NEW binary.
4396
+ reinstallAgents: () => replayRecordedAgents((0, updater_1.detectInstallMethod)(__dirname, installDir), binDir),
4371
4397
  installDir,
4372
4398
  binDir,
4373
4399
  }, { check: options.check });
@@ -4380,6 +4406,25 @@ function main() {
4380
4406
  if (result.exitCode !== 0)
4381
4407
  process.exit(result.exitCode);
4382
4408
  });
4409
+ /**
4410
+ * Run `specship install --replay-recorded` on the freshly installed binary and
4411
+ * return its output lines. Exit 3 means nothing was recorded.
4412
+ */
4413
+ function replayRecordedAgents(method, binDir) {
4414
+ const bundleLink = path.join(binDir, 'specship');
4415
+ const bin = method === 'bundle' && fs.existsSync(bundleLink) ? bundleLink : 'specship';
4416
+ return new Promise((resolve, reject) => {
4417
+ const { execFile } = require('child_process');
4418
+ execFile(bin, ['install', '--replay-recorded'], { encoding: 'utf8', shell: process.platform === 'win32', windowsHide: true }, (err, stdout) => {
4419
+ const lines = String(stdout ?? '').split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
4420
+ const code = err?.code;
4421
+ if (!err || code === 3)
4422
+ resolve(lines);
4423
+ else
4424
+ reject(new Error(lines.at(-1) ?? err.message));
4425
+ });
4426
+ });
4427
+ }
4383
4428
  /**
4384
4429
  * specship jira — connect SpecShip to a JIRA instance (REQ-JIRA-001).
4385
4430
  *