@nemus-cli/nemus 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (320) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/LICENSE +21 -0
  3. package/README.md +275 -0
  4. package/assets/README.md +53 -0
  5. package/assets/banner.png +0 -0
  6. package/assets/banner.svg +66 -0
  7. package/assets/favicon-32.png +0 -0
  8. package/assets/favicon-64.png +0 -0
  9. package/assets/icon-256.png +0 -0
  10. package/assets/icon.svg +23 -0
  11. package/assets/logo-256.png +0 -0
  12. package/assets/logo-512.png +0 -0
  13. package/assets/logo-wordmark.svg +35 -0
  14. package/assets/logo.svg +32 -0
  15. package/bin/workspace.js +130 -0
  16. package/dist/cli/ai-prompt.js +421 -0
  17. package/dist/cli/dashboard/AgentList.js +61 -0
  18. package/dist/cli/dashboard/DashboardSidebar.js +280 -0
  19. package/dist/cli/dashboard/HelpFooter.js +73 -0
  20. package/dist/cli/dashboard/StatusBadge.js +50 -0
  21. package/dist/cli/dashboard/index.js +11 -0
  22. package/dist/cli/dashboard/types.js +2 -0
  23. package/dist/cli/tui.js +48 -0
  24. package/dist/commands/ai-prompt.js +22 -0
  25. package/dist/commands/analyze-deps.js +138 -0
  26. package/dist/commands/archive.js +113 -0
  27. package/dist/commands/branch/create.js +119 -0
  28. package/dist/commands/branch/index.js +104 -0
  29. package/dist/commands/branch/merge.js +90 -0
  30. package/dist/commands/branch/rebase.js +84 -0
  31. package/dist/commands/branch/switch.js +165 -0
  32. package/dist/commands/cache/index.js +80 -0
  33. package/dist/commands/cache/manager.js +198 -0
  34. package/dist/commands/cleanup.js +153 -0
  35. package/dist/commands/configure-claude.js +55 -0
  36. package/dist/commands/configure.js +231 -0
  37. package/dist/commands/create.js +204 -0
  38. package/dist/commands/dashboard/hook-handler.js +162 -0
  39. package/dist/commands/dashboard/index.js +258 -0
  40. package/dist/commands/dashboard/launcher.js +40 -0
  41. package/dist/commands/dashboard/session-picker.js +106 -0
  42. package/dist/commands/dashboard/sidebar.js +39 -0
  43. package/dist/commands/dashboard/workspace-picker.js +95 -0
  44. package/dist/commands/delete.js +194 -0
  45. package/dist/commands/deprecated-aliases.js +44 -0
  46. package/dist/commands/diff.js +129 -0
  47. package/dist/commands/doctor.js +141 -0
  48. package/dist/commands/generate-docs.js +77 -0
  49. package/dist/commands/ghq-status.js +72 -0
  50. package/dist/commands/go.js +154 -0
  51. package/dist/commands/history.js +124 -0
  52. package/dist/commands/list.js +191 -0
  53. package/dist/commands/mcp/index.js +69 -0
  54. package/dist/commands/migrate.js +296 -0
  55. package/dist/commands/remove-repo.js +217 -0
  56. package/dist/commands/report-bug.js +112 -0
  57. package/dist/commands/run.js +147 -0
  58. package/dist/commands/save-context.js +171 -0
  59. package/dist/commands/sessions.js +129 -0
  60. package/dist/commands/status.js +138 -0
  61. package/dist/commands/suite/create.js +212 -0
  62. package/dist/commands/suite/delete.js +64 -0
  63. package/dist/commands/suite/export.js +126 -0
  64. package/dist/commands/suite/import.js +128 -0
  65. package/dist/commands/suite/index.js +94 -0
  66. package/dist/commands/suite/list.js +49 -0
  67. package/dist/commands/suite/use.js +227 -0
  68. package/dist/commands/sync.js +117 -0
  69. package/dist/commands/update.js +203 -0
  70. package/dist/mcp/install.js +550 -0
  71. package/dist/mcp/server.js +398 -0
  72. package/dist/mcp/tools.js +922 -0
  73. package/dist/pi-extensions/permission-sync.ts.template +41 -0
  74. package/dist/pi-extensions/permissions-gate.ts.template +69 -0
  75. package/dist/pi-extensions/pr-fix-trigger.ts.template +124 -0
  76. package/dist/pi-extensions/review-reminder.ts.template +58 -0
  77. package/dist/pi-extensions/workspace-input-status.ts.template +919 -0
  78. package/dist/program.js +136 -0
  79. package/dist/scripts/workspace-table.py +394 -0
  80. package/dist/types/dashboard.js +11 -0
  81. package/dist/types/index.js +2 -0
  82. package/dist/utils/agent-config.js +263 -0
  83. package/dist/utils/agent-state.js +178 -0
  84. package/dist/utils/banner.js +24 -0
  85. package/dist/utils/branch-operations.js +232 -0
  86. package/dist/utils/bug-report.js +355 -0
  87. package/dist/utils/cache.js +168 -0
  88. package/dist/utils/claude-integration.js +617 -0
  89. package/dist/utils/claude-sessions.js +260 -0
  90. package/dist/utils/cleanup-operations.js +120 -0
  91. package/dist/utils/colors.js +26 -0
  92. package/dist/utils/command-helpers.js +44 -0
  93. package/dist/utils/config.js +150 -0
  94. package/dist/utils/context-file.js +33 -0
  95. package/dist/utils/dashboard-hooks.js +172 -0
  96. package/dist/utils/dependency-analyzer.js +234 -0
  97. package/dist/utils/diff-operations.js +80 -0
  98. package/dist/utils/doc-generator.js +184 -0
  99. package/dist/utils/ghq-integration.js +332 -0
  100. package/dist/utils/git-operations.js +237 -0
  101. package/dist/utils/git-status.js +187 -0
  102. package/dist/utils/github.js +68 -0
  103. package/dist/utils/health-checks.js +345 -0
  104. package/dist/utils/history.js +150 -0
  105. package/dist/utils/hooks.js +100 -0
  106. package/dist/utils/logger.js +40 -0
  107. package/dist/utils/permission-sync.js +681 -0
  108. package/dist/utils/pi-extensions.js +224 -0
  109. package/dist/utils/progress.js +92 -0
  110. package/dist/utils/prompts.js +279 -0
  111. package/dist/utils/repo-resolver.js +260 -0
  112. package/dist/utils/retry.js +65 -0
  113. package/dist/utils/run-operations.js +30 -0
  114. package/dist/utils/suite.js +159 -0
  115. package/dist/utils/sync-operations.js +107 -0
  116. package/dist/utils/tmux-dashboard.js +184 -0
  117. package/dist/utils/validation.js +153 -0
  118. package/dist/utils/version-check.js +121 -0
  119. package/dist/utils/workspace-meta.js +185 -0
  120. package/install-shell-integration.sh +532 -0
  121. package/install.sh +401 -0
  122. package/package.json +90 -0
  123. package/remove-shell-block.awk +44 -0
  124. package/scripts/postinstall.js +85 -0
  125. package/skills/analyze-deps.md +23 -0
  126. package/skills/archive-workspace.md +26 -0
  127. package/skills/branch-create.md +18 -0
  128. package/skills/create-workspace.md +39 -0
  129. package/skills/delete-workspace.md +37 -0
  130. package/skills/list-org-repos.md +26 -0
  131. package/skills/list-suites.md +24 -0
  132. package/skills/list-workspaces.md +27 -0
  133. package/skills/refresh-workspace-docs.md +89 -0
  134. package/skills/remove-repo.md +25 -0
  135. package/skills/run-command.md +23 -0
  136. package/skills/save-context.md +68 -0
  137. package/skills/search-repos.md +31 -0
  138. package/skills/snapshot-list.md +18 -0
  139. package/skills/snapshot-save.md +12 -0
  140. package/skills/switch-branch.md +18 -0
  141. package/skills/update-cache.md +21 -0
  142. package/skills/update-workspace.md +66 -0
  143. package/skills/workspace-cleanup.md +18 -0
  144. package/skills/workspace-diff.md +23 -0
  145. package/skills/workspace-doctor.md +23 -0
  146. package/skills/workspace-info.md +45 -0
  147. package/skills/workspace-manager/SKILL.md +201 -0
  148. package/skills/workspace-manager/references/ai-prompt.md +44 -0
  149. package/skills/workspace-manager/references/analyze-deps.md +45 -0
  150. package/skills/workspace-manager/references/archive-workspace.md +38 -0
  151. package/skills/workspace-manager/references/branch-create.md +31 -0
  152. package/skills/workspace-manager/references/branch-merge.md +22 -0
  153. package/skills/workspace-manager/references/branch-rebase.md +22 -0
  154. package/skills/workspace-manager/references/cache-clear.md +18 -0
  155. package/skills/workspace-manager/references/cache-info.md +22 -0
  156. package/skills/workspace-manager/references/configure-claude.md +31 -0
  157. package/skills/workspace-manager/references/configure.md +35 -0
  158. package/skills/workspace-manager/references/create-workspace.md +45 -0
  159. package/skills/workspace-manager/references/dashboard.md +61 -0
  160. package/skills/workspace-manager/references/delete-workspace.md +33 -0
  161. package/skills/workspace-manager/references/generate-docs.md +36 -0
  162. package/skills/workspace-manager/references/ghq-status.md +28 -0
  163. package/skills/workspace-manager/references/go.md +33 -0
  164. package/skills/workspace-manager/references/history.md +41 -0
  165. package/skills/workspace-manager/references/list-org-repos.md +26 -0
  166. package/skills/workspace-manager/references/list-suites.md +21 -0
  167. package/skills/workspace-manager/references/list-workspaces.md +27 -0
  168. package/skills/workspace-manager/references/mcp.md +49 -0
  169. package/skills/workspace-manager/references/remove-repo.md +26 -0
  170. package/skills/workspace-manager/references/run-command.md +44 -0
  171. package/skills/workspace-manager/references/search-repos.md +24 -0
  172. package/skills/workspace-manager/references/sessions.md +27 -0
  173. package/skills/workspace-manager/references/suite-create.md +22 -0
  174. package/skills/workspace-manager/references/suite-delete.md +21 -0
  175. package/skills/workspace-manager/references/suite-export.md +19 -0
  176. package/skills/workspace-manager/references/suite-import.md +19 -0
  177. package/skills/workspace-manager/references/suite-use.md +27 -0
  178. package/skills/workspace-manager/references/switch-branch.md +25 -0
  179. package/skills/workspace-manager/references/update-cache.md +22 -0
  180. package/skills/workspace-manager/references/update-workspace.md +50 -0
  181. package/skills/workspace-manager/references/workspace-cleanup.md +33 -0
  182. package/skills/workspace-manager/references/workspace-diff.md +27 -0
  183. package/skills/workspace-manager/references/workspace-doctor.md +27 -0
  184. package/skills/workspace-manager/references/workspace-info.md +33 -0
  185. package/skills/workspace-manager/references/workspace-status.md +43 -0
  186. package/skills/workspace-manager/references/workspace-sync.md +26 -0
  187. package/skills/workspace-status.md +26 -0
  188. package/skills/workspace-sync.md +23 -0
  189. package/src/cli/ai-prompt.test.ts +667 -0
  190. package/src/cli/ai-prompt.ts +421 -0
  191. package/src/cli/dashboard/AgentList.tsx +44 -0
  192. package/src/cli/dashboard/DashboardSidebar.tsx +291 -0
  193. package/src/cli/dashboard/HelpFooter.tsx +22 -0
  194. package/src/cli/dashboard/StatusBadge.tsx +23 -0
  195. package/src/cli/dashboard/index.ts +5 -0
  196. package/src/cli/dashboard/types.ts +16 -0
  197. package/src/cli/tui.tsx +51 -0
  198. package/src/cli/workspace-cli.test.ts +30 -0
  199. package/src/commands/ai-prompt.ts +21 -0
  200. package/src/commands/analyze-deps.ts +119 -0
  201. package/src/commands/archive.ts +114 -0
  202. package/src/commands/branch/create.ts +94 -0
  203. package/src/commands/branch/index.ts +74 -0
  204. package/src/commands/branch/merge.ts +61 -0
  205. package/src/commands/branch/rebase.ts +54 -0
  206. package/src/commands/branch/switch.ts +150 -0
  207. package/src/commands/cache/index.ts +51 -0
  208. package/src/commands/cache/manager.ts +168 -0
  209. package/src/commands/cleanup.ts +132 -0
  210. package/src/commands/configure-claude.ts +55 -0
  211. package/src/commands/configure.ts +204 -0
  212. package/src/commands/create.ts +196 -0
  213. package/src/commands/dashboard/hook-handler.ts +138 -0
  214. package/src/commands/dashboard/index.ts +248 -0
  215. package/src/commands/dashboard/launcher.ts +42 -0
  216. package/src/commands/dashboard/session-picker.ts +73 -0
  217. package/src/commands/dashboard/sidebar.js +39 -0
  218. package/src/commands/dashboard/workspace-picker.ts +62 -0
  219. package/src/commands/delete.test.ts +250 -0
  220. package/src/commands/delete.ts +171 -0
  221. package/src/commands/deprecated-aliases.ts +55 -0
  222. package/src/commands/diff.ts +106 -0
  223. package/src/commands/doctor.ts +110 -0
  224. package/src/commands/generate-docs.ts +45 -0
  225. package/src/commands/ghq-status.ts +70 -0
  226. package/src/commands/go.ts +125 -0
  227. package/src/commands/history.ts +136 -0
  228. package/src/commands/list.test.ts +219 -0
  229. package/src/commands/list.ts +176 -0
  230. package/src/commands/mcp/index.ts +39 -0
  231. package/src/commands/migrate.ts +277 -0
  232. package/src/commands/remove-repo.ts +194 -0
  233. package/src/commands/report-bug.ts +88 -0
  234. package/src/commands/run.ts +125 -0
  235. package/src/commands/save-context.test.ts +113 -0
  236. package/src/commands/save-context.ts +141 -0
  237. package/src/commands/sessions.ts +99 -0
  238. package/src/commands/status.ts +111 -0
  239. package/src/commands/suite/create.ts +240 -0
  240. package/src/commands/suite/delete.ts +65 -0
  241. package/src/commands/suite/export.ts +97 -0
  242. package/src/commands/suite/import.ts +100 -0
  243. package/src/commands/suite/index.ts +66 -0
  244. package/src/commands/suite/list.ts +54 -0
  245. package/src/commands/suite/use.ts +232 -0
  246. package/src/commands/sync.ts +97 -0
  247. package/src/commands/update.ts +197 -0
  248. package/src/mcp/install.ts +529 -0
  249. package/src/mcp/server.ts +579 -0
  250. package/src/mcp/tools.test.ts +545 -0
  251. package/src/mcp/tools.ts +1019 -0
  252. package/src/mcp/update-workspace.test.ts +220 -0
  253. package/src/pi-extensions/permission-sync.ts.template +41 -0
  254. package/src/pi-extensions/permissions-gate.ts.template +69 -0
  255. package/src/pi-extensions/pr-fix-trigger.ts.template +124 -0
  256. package/src/pi-extensions/review-reminder.ts.template +58 -0
  257. package/src/pi-extensions/workspace-input-status.ts.template +919 -0
  258. package/src/program.ts +112 -0
  259. package/src/scripts/workspace-table.py +394 -0
  260. package/src/types/dashboard.ts +29 -0
  261. package/src/types/index.ts +109 -0
  262. package/src/types/ink.d.ts +171 -0
  263. package/src/utils/agent-config.test.ts +303 -0
  264. package/src/utils/agent-config.ts +264 -0
  265. package/src/utils/agent-state.test.ts +166 -0
  266. package/src/utils/agent-state.ts +135 -0
  267. package/src/utils/banner.ts +24 -0
  268. package/src/utils/branch-operations.ts +237 -0
  269. package/src/utils/bug-report.test.ts +174 -0
  270. package/src/utils/bug-report.ts +355 -0
  271. package/src/utils/cache.ts +150 -0
  272. package/src/utils/claude-integration.test.ts +381 -0
  273. package/src/utils/claude-integration.ts +649 -0
  274. package/src/utils/claude-sessions.test.ts +262 -0
  275. package/src/utils/claude-sessions.ts +251 -0
  276. package/src/utils/cleanup-operations.ts +94 -0
  277. package/src/utils/colors.ts +25 -0
  278. package/src/utils/command-helpers.ts +46 -0
  279. package/src/utils/config.test.ts +203 -0
  280. package/src/utils/config.ts +133 -0
  281. package/src/utils/context-file.ts +36 -0
  282. package/src/utils/dashboard-hooks.test.ts +168 -0
  283. package/src/utils/dashboard-hooks.ts +163 -0
  284. package/src/utils/dependency-analyzer.ts +243 -0
  285. package/src/utils/diff-operations.ts +59 -0
  286. package/src/utils/doc-generator.ts +187 -0
  287. package/src/utils/ghq-integration.test.ts +195 -0
  288. package/src/utils/ghq-integration.ts +317 -0
  289. package/src/utils/git-operations.test.ts +265 -0
  290. package/src/utils/git-operations.ts +233 -0
  291. package/src/utils/git-status.ts +171 -0
  292. package/src/utils/github.ts +74 -0
  293. package/src/utils/health-checks.ts +359 -0
  294. package/src/utils/history.ts +140 -0
  295. package/src/utils/hooks.ts +92 -0
  296. package/src/utils/logger.ts +37 -0
  297. package/src/utils/permission-sync.test.ts +593 -0
  298. package/src/utils/permission-sync.ts +720 -0
  299. package/src/utils/pi-extensions.test.ts +182 -0
  300. package/src/utils/pi-extensions.ts +226 -0
  301. package/src/utils/progress.ts +73 -0
  302. package/src/utils/prompts.test.ts +169 -0
  303. package/src/utils/prompts.ts +296 -0
  304. package/src/utils/repo-resolver.test.ts +313 -0
  305. package/src/utils/repo-resolver.ts +294 -0
  306. package/src/utils/retry.test.ts +124 -0
  307. package/src/utils/retry.ts +85 -0
  308. package/src/utils/run-operations.ts +38 -0
  309. package/src/utils/suite.test.ts +256 -0
  310. package/src/utils/suite.ts +144 -0
  311. package/src/utils/sync-operations.ts +87 -0
  312. package/src/utils/tmux-dashboard.ts +193 -0
  313. package/src/utils/validation.test.ts +139 -0
  314. package/src/utils/validation.ts +128 -0
  315. package/src/utils/version-check.test.ts +108 -0
  316. package/src/utils/version-check.ts +96 -0
  317. package/src/utils/workspace-meta.ts +173 -0
  318. package/sync-permissions.sh +137 -0
  319. package/tsconfig.json +20 -0
  320. package/uninstall.sh +162 -0
@@ -0,0 +1,381 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+ vi.mock('fs/promises', () => ({
4
+ readFile: vi.fn(),
5
+ writeFile: vi.fn().mockResolvedValue(undefined),
6
+ readdir: vi.fn().mockRejectedValue(new Error('ENOENT')),
7
+ }));
8
+
9
+ vi.mock('./logger', () => ({
10
+ logSuccess: vi.fn(),
11
+ logWarning: vi.fn(),
12
+ }));
13
+
14
+ vi.mock('./colors', () => ({
15
+ colorize: (text: string) => text,
16
+ }));
17
+
18
+ // Default to 'both' agent config for tests
19
+ vi.mock('./agent-config', () => ({
20
+ getContextFileNames: vi.fn().mockReturnValue(['CLAUDE.md']),
21
+ getAllKnownContextFileNames: vi.fn().mockReturnValue(['AGENTS.md', 'CLAUDE.md', '.claude.md']),
22
+ }));
23
+
24
+ import { generateClaudeContext, loadClaudeConfig, backfillAgentRules, buildAgentRulesSection, preserveOperatorNotes, AGENT_RULES_VERSION } from './claude-integration';
25
+ import * as fs from 'fs/promises';
26
+ import { getContextFileNames } from './agent-config';
27
+
28
+ describe('loadClaudeConfig', () => {
29
+ beforeEach(() => {
30
+ vi.clearAllMocks();
31
+ });
32
+
33
+ it('returns default config when file does not exist', async () => {
34
+ vi.mocked(fs.readFile).mockRejectedValueOnce(new Error('ENOENT'));
35
+
36
+ const config = await loadClaudeConfig();
37
+
38
+ expect(config).toEqual({ autoLaunch: false, generateContext: true });
39
+ });
40
+
41
+ it('merges file config with defaults', async () => {
42
+ vi.mocked(fs.readFile).mockResolvedValueOnce(JSON.stringify({ autoLaunch: true }));
43
+
44
+ const config = await loadClaudeConfig();
45
+
46
+ expect(config).toEqual({ autoLaunch: true, generateContext: true });
47
+ });
48
+ });
49
+
50
+ describe('generateClaudeContext', () => {
51
+ beforeEach(() => {
52
+ vi.clearAllMocks();
53
+ // Per-repo context files (AGENTS.md / .claude.md inside each repo dir)
54
+ // don't exist in tests — reject all readFile calls by default so
55
+ // buildPerRepoContextSection skips gracefully.
56
+ vi.mocked(fs.readFile).mockRejectedValue(new Error('ENOENT'));
57
+ });
58
+
59
+ it('writes CLAUDE.md file to workspace path', async () => {
60
+ const repos = [
61
+ { name: 'repo-a', url: 'https://github.com/org/repo-a', sshUrl: 'git@github.com:org/repo-a.git', owner: { login: 'org' }, description: '', isPrivate: false },
62
+ { name: 'repo-b', url: 'https://github.com/org/repo-b', sshUrl: 'git@github.com:org/repo-b.git', owner: { login: 'org' }, description: '', isPrivate: false },
63
+ ];
64
+
65
+ await generateClaudeContext('/workspace/test', 'test', repos);
66
+
67
+ expect(fs.writeFile).toHaveBeenCalledWith(
68
+ '/workspace/test/CLAUDE.md',
69
+ expect.any(String),
70
+ 'utf-8'
71
+ );
72
+ });
73
+
74
+ it('includes workspace name in the content', async () => {
75
+ const repos = [
76
+ { name: 'my-repo', url: 'https://github.com/org/my-repo', sshUrl: 'git@github.com:org/my-repo.git', owner: { login: 'org' }, description: '', isPrivate: false },
77
+ ];
78
+
79
+ await generateClaudeContext('/workspace/my-ws', 'my-ws', repos);
80
+
81
+ const writtenContent = vi.mocked(fs.writeFile).mock.calls[0][1] as string;
82
+ expect(writtenContent).toContain('my-ws');
83
+ });
84
+
85
+ it('includes repo names in the content', async () => {
86
+ const repos = [
87
+ { name: 'alpha', url: 'https://github.com/org/alpha', sshUrl: 'git@github.com:org/alpha.git', owner: { login: 'org' }, description: '', isPrivate: false },
88
+ { name: 'beta', url: 'https://github.com/org/beta', sshUrl: 'git@github.com:org/beta.git', owner: { login: 'org' }, description: '', isPrivate: false },
89
+ ];
90
+
91
+ await generateClaudeContext('/workspace/test', 'test', repos);
92
+
93
+ const writtenContent = vi.mocked(fs.writeFile).mock.calls[0][1] as string;
94
+ expect(writtenContent).toContain('alpha');
95
+ expect(writtenContent).toContain('beta');
96
+ });
97
+
98
+ it('lists both instances when same repo is added with different suffixes', async () => {
99
+ const repos = [
100
+ { name: 'partnerships-api', url: 'https://github.com/acme/partnerships-api', sshUrl: 'git@github.com:acme/partnerships-api.git', owner: { login: 'acme' }, description: 'Partnerships API', isPrivate: false },
101
+ ];
102
+ const metadata = {
103
+ workspaceName: 'test-ws',
104
+ createdAt: '2024-01-01T00:00:00Z',
105
+ repositories: [
106
+ { name: 'partnerships-api', directoryName: 'partnerships-api', owner: 'acme', clonedAt: '2024-01-01T00:00:00Z', cloneUrl: 'git@github.com:acme/partnerships-api.git', status: 'success' as const },
107
+ { name: 'partnerships-api', directoryName: 'partnerships-api-v2', owner: 'acme', clonedAt: '2024-01-01T00:00:00Z', cloneUrl: 'git@github.com:acme/partnerships-api.git', status: 'success' as const },
108
+ ],
109
+ };
110
+
111
+ await generateClaudeContext('/workspace/test-ws', 'test-ws', repos, metadata);
112
+
113
+ const writtenContent = vi.mocked(fs.writeFile).mock.calls[0][1] as string;
114
+ expect(writtenContent).toContain('partnerships-api/');
115
+ expect(writtenContent).toContain('partnerships-api-v2');
116
+ });
117
+
118
+ it('shows correct directory names from metadata, not repo names', async () => {
119
+ const repos = [
120
+ { name: 'my-service', url: 'https://github.com/acme/my-service', sshUrl: 'git@github.com:acme/my-service.git', owner: { login: 'acme' }, description: '', isPrivate: false },
121
+ ];
122
+ const metadata = {
123
+ workspaceName: 'ws',
124
+ createdAt: '2024-01-01T00:00:00Z',
125
+ repositories: [
126
+ { name: 'my-service', directoryName: 'my-service-hotfix', owner: 'acme', clonedAt: '2024-01-01T00:00:00Z', cloneUrl: 'git@github.com:acme/my-service.git', status: 'success' as const },
127
+ ],
128
+ };
129
+
130
+ await generateClaudeContext('/workspace/ws', 'ws', repos, metadata);
131
+
132
+ const writtenContent = vi.mocked(fs.writeFile).mock.calls[0][1] as string;
133
+ expect(writtenContent).toContain('my-service-hotfix');
134
+ });
135
+
136
+ it('shows correct count when duplicate repos exist', async () => {
137
+ const repos = [
138
+ { name: 'svc', url: 'https://github.com/acme/svc', sshUrl: 'git@github.com:acme/svc.git', owner: { login: 'acme' }, description: '', isPrivate: false },
139
+ ];
140
+ const metadata = {
141
+ workspaceName: 'ws',
142
+ createdAt: '2024-01-01T00:00:00Z',
143
+ repositories: [
144
+ { name: 'svc', directoryName: 'svc', owner: 'acme', clonedAt: '2024-01-01T00:00:00Z', cloneUrl: '', status: 'success' as const },
145
+ { name: 'svc', directoryName: 'svc-v2', owner: 'acme', clonedAt: '2024-01-01T00:00:00Z', cloneUrl: '', status: 'success' as const },
146
+ { name: 'svc', directoryName: 'svc-v3', owner: 'acme', clonedAt: '2024-01-01T00:00:00Z', cloneUrl: '', status: 'failed' as const },
147
+ ],
148
+ };
149
+
150
+ await generateClaudeContext('/workspace/ws', 'ws', repos, metadata);
151
+
152
+ const writtenContent = vi.mocked(fs.writeFile).mock.calls[0][1] as string;
153
+ // Only 2 successful repos should be counted
154
+ expect(writtenContent).toContain('2 repositories');
155
+ });
156
+
157
+ it('generates AGENTS.md when Pi is configured', async () => {
158
+ vi.mocked(getContextFileNames).mockReturnValue(['AGENTS.md']);
159
+
160
+ const repos = [
161
+ { name: 'repo-a', url: 'https://github.com/org/repo-a', sshUrl: 'git@github.com:org/repo-a.git', owner: { login: 'org' }, description: '', isPrivate: false },
162
+ ];
163
+
164
+ await generateClaudeContext('/workspace/test', 'test', repos);
165
+
166
+ expect(fs.writeFile).toHaveBeenCalledWith(
167
+ '/workspace/test/AGENTS.md',
168
+ expect.any(String),
169
+ 'utf-8'
170
+ );
171
+ });
172
+
173
+ it('generates both CLAUDE.md and AGENTS.md when both agents configured', async () => {
174
+ vi.mocked(getContextFileNames).mockReturnValue(['CLAUDE.md', 'AGENTS.md']);
175
+
176
+ const repos = [
177
+ { name: 'repo-a', url: 'https://github.com/org/repo-a', sshUrl: 'git@github.com:org/repo-a.git', owner: { login: 'org' }, description: '', isPrivate: false },
178
+ ];
179
+
180
+ await generateClaudeContext('/workspace/test', 'test', repos);
181
+
182
+ const calls = vi.mocked(fs.writeFile).mock.calls;
183
+ const writtenPaths = calls.map(c => c[0]);
184
+ expect(writtenPaths).toContain('/workspace/test/CLAUDE.md');
185
+ expect(writtenPaths).toContain('/workspace/test/AGENTS.md');
186
+ });
187
+
188
+ it('uses agent-neutral language in tips section', async () => {
189
+ vi.mocked(getContextFileNames).mockReturnValue(['CLAUDE.md']);
190
+
191
+ const repos = [
192
+ { name: 'my-repo', url: 'https://github.com/org/my-repo', sshUrl: 'git@github.com:org/my-repo.git', owner: { login: 'org' }, description: '', isPrivate: false },
193
+ ];
194
+
195
+ await generateClaudeContext('/workspace/my-ws', 'my-ws', repos);
196
+
197
+ const writtenContent = vi.mocked(fs.writeFile).mock.calls[0][1] as string;
198
+ expect(writtenContent).toContain('Tips for Working with AI Agents');
199
+ expect(writtenContent).not.toContain('Tips for Working with Claude');
200
+ });
201
+ });
202
+
203
+ describe('buildPerRepoContextSection', () => {
204
+ beforeEach(() => {
205
+ vi.clearAllMocks();
206
+ });
207
+
208
+ it('returns null when no repos have context files', async () => {
209
+ vi.mocked(fs.readFile).mockRejectedValue(new Error('ENOENT'));
210
+ // also mock readdir to return empty so recursion stops
211
+ vi.mocked((fs as any).readdir).mockRejectedValue(new Error('ENOENT'));
212
+ const { buildPerRepoContextSection } = await import('./claude-integration');
213
+ const result = await buildPerRepoContextSection('/ws', ['repo-a']);
214
+ expect(result).toBeNull();
215
+ });
216
+
217
+ it('uses imperative scoping language and proper markdown (no blockquotes)', async () => {
218
+ vi.mocked(fs.readFile).mockImplementation(async (filePath: any) => {
219
+ if (String(filePath).endsWith('repo-a/AGENTS.md')) {
220
+ return '# Repo A\n\n## Architecture\n\nUse fastify.\n\n```bash\n# build it\nnpm run build\n```\n';
221
+ }
222
+ throw new Error('ENOENT');
223
+ });
224
+ vi.mocked((fs as any).readdir).mockRejectedValue(new Error('ENOENT'));
225
+
226
+ const { buildPerRepoContextSection } = await import('./claude-integration');
227
+ const result = await buildPerRepoContextSection('/ws', ['repo-a']);
228
+
229
+ expect(result).not.toBeNull();
230
+ // Imperative scoping at the top
231
+ expect(result).toContain('AUTHORITATIVE for that repo');
232
+ // Per-repo subsection has clear scoping directive
233
+ expect(result).toContain('You MUST follow these rules');
234
+ // No blockquote prefix on any embedded line
235
+ expect(result).not.toMatch(/^>\s/m);
236
+ // Headings demoted by 3 levels (# -> ####, ## -> #####)
237
+ expect(result).toContain('#### Repo A');
238
+ expect(result).toContain('##### Architecture');
239
+ // Code-block contents NOT demoted (shell comment stays as one #)
240
+ expect(result).toContain('# build it');
241
+ expect(result).not.toContain('#### build it');
242
+ });
243
+ });
244
+
245
+ describe('backfillAgentRules — versioned replace', () => {
246
+ beforeEach(() => {
247
+ vi.clearAllMocks();
248
+ });
249
+
250
+ it('embeds the current version marker and the new "current workspace only" rule', () => {
251
+ const block = buildAgentRulesSection('demo');
252
+ expect(block).toContain(`<!-- ws-rules:v${AGENT_RULES_VERSION} -->`);
253
+ expect(block).toContain('ONLY read code from repos in THIS workspace');
254
+ });
255
+
256
+ it('replaces a stale pre-versioning rules block in place (no duplicate heading)', async () => {
257
+ const staleFile = [
258
+ '# Workspace: demo',
259
+ '',
260
+ '## AI Agent Rules',
261
+ '',
262
+ '**NEVER** use `git clone` directly to add repositories to this workspace.',
263
+ '',
264
+ '## Tips for Working with AI Agents',
265
+ '',
266
+ 'be nice',
267
+ '',
268
+ ].join('\n');
269
+ vi.mocked(fs.readFile).mockResolvedValue(staleFile);
270
+
271
+ const updated = await backfillAgentRules('/ws/demo', 'demo');
272
+
273
+ expect(updated).toBeGreaterThan(0);
274
+ const written = vi.mocked(fs.writeFile).mock.calls[0][1] as string;
275
+ expect((written.match(/## AI Agent Rules/g) || []).length).toBe(1);
276
+ expect(written).toContain(`<!-- ws-rules:v${AGENT_RULES_VERSION} -->`);
277
+ expect(written).toContain('ONLY read code from repos in THIS workspace');
278
+ expect(written).toContain('## Tips for Working with AI Agents'); // following section preserved
279
+ });
280
+
281
+ it('is idempotent when the block is already at the current version', async () => {
282
+ const current = `# Workspace: demo\n\n${buildAgentRulesSection('demo')}## Notes\n\n- x\n`;
283
+ vi.mocked(fs.readFile).mockResolvedValue(current);
284
+
285
+ const updated = await backfillAgentRules('/ws/demo', 'demo');
286
+
287
+ expect(updated).toBe(0);
288
+ expect(vi.mocked(fs.writeFile)).not.toHaveBeenCalled();
289
+ });
290
+
291
+ it('replaces a stale block in CRLF (\\r\\n) files too', async () => {
292
+ const staleCrlf = [
293
+ '# Workspace: demo',
294
+ '',
295
+ '## AI Agent Rules',
296
+ '',
297
+ '**NEVER** use `git clone` directly.',
298
+ '',
299
+ '## Tips for Working with AI Agents',
300
+ '',
301
+ 'be nice',
302
+ '',
303
+ ].join('\r\n');
304
+ vi.mocked(fs.readFile).mockResolvedValue(staleCrlf);
305
+
306
+ const updated = await backfillAgentRules('/ws/demo', 'demo');
307
+
308
+ expect(updated).toBeGreaterThan(0);
309
+ const written = vi.mocked(fs.writeFile).mock.calls[0][1] as string;
310
+ expect((written.match(/## AI Agent Rules/g) || []).length).toBe(1);
311
+ expect(written).toContain(`<!-- ws-rules:v${AGENT_RULES_VERSION} -->`);
312
+ expect(written).toContain('ONLY read code from repos in THIS workspace');
313
+ expect(written).toContain('## Tips for Working with AI Agents');
314
+ });
315
+
316
+ it('inserts the section when missing, before Notes', async () => {
317
+ const noRules = '# Workspace: x\n\n## Notes\n\n- hi\n';
318
+ vi.mocked(fs.readFile).mockResolvedValue(noRules);
319
+
320
+ const updated = await backfillAgentRules('/ws/x', 'x');
321
+
322
+ expect(updated).toBeGreaterThan(0);
323
+ const written = vi.mocked(fs.writeFile).mock.calls[0][1] as string;
324
+ expect(written.indexOf('## AI Agent Rules')).toBeLessThan(written.indexOf('## Notes'));
325
+ expect(written).toContain('ONLY read code from repos in THIS workspace');
326
+ });
327
+ });
328
+
329
+ describe('preserveOperatorNotes', () => {
330
+ beforeEach(() => vi.clearAllMocks());
331
+
332
+ const NEW_CONTENT =
333
+ '# Workspace\n\n## Notes\n\nAdd your own notes here:\n\n- \n\n## Saved Context\n\nsee CONTEXT.md\n';
334
+
335
+ it('carries over operator-authored notes when the existing file has them', async () => {
336
+ const existing =
337
+ '# Old\n\n## Notes\n\nAdd your own notes here:\n\n- deploy is flaky on fridays\n- ask @dana\n\n## Saved Context\n\nold\n';
338
+ vi.mocked(fs.readFile).mockResolvedValueOnce(existing as any);
339
+
340
+ const out = await preserveOperatorNotes('/ws/CLAUDE.md', NEW_CONTENT);
341
+
342
+ expect(out).toContain('deploy is flaky on fridays');
343
+ expect(out).toContain('ask @dana');
344
+ expect(out).toContain('## Saved Context');
345
+ expect(out.match(/## Notes/g)?.length).toBe(1);
346
+ });
347
+
348
+ it('leaves the default (empty) Notes placeholder untouched', async () => {
349
+ const existing =
350
+ '# Old\n\n## Notes\n\nAdd your own notes here:\n\n- \n\n## Saved Context\n\nold\n';
351
+ vi.mocked(fs.readFile).mockResolvedValueOnce(existing as any);
352
+
353
+ const out = await preserveOperatorNotes('/ws/CLAUDE.md', NEW_CONTENT);
354
+ expect(out).toBe(NEW_CONTENT);
355
+ });
356
+
357
+ it('returns new content unchanged when no existing file', async () => {
358
+ vi.mocked(fs.readFile).mockRejectedValueOnce(new Error('ENOENT'));
359
+ const out = await preserveOperatorNotes('/ws/CLAUDE.md', NEW_CONTENT);
360
+ expect(out).toBe(NEW_CONTENT);
361
+ });
362
+
363
+ it('does NOT treat a deeper "### Notes" subheading as the real Notes section', async () => {
364
+ const existing =
365
+ '# Old\n\n## Overview\n\n### Notes\n\nthis is a sub-note under overview\n\n## Notes\n\nAdd your own notes here:\n\n- real operator note\n\n## Saved Context\n\nold\n';
366
+ vi.mocked(fs.readFile).mockResolvedValueOnce(existing as any);
367
+
368
+ const out = await preserveOperatorNotes('/ws/CLAUDE.md', NEW_CONTENT);
369
+ expect(out).toContain('real operator note');
370
+ expect(out).not.toContain('this is a sub-note under overview');
371
+ });
372
+
373
+ it('preserves notes containing $ sequences literally (no String.replace corruption)', async () => {
374
+ const existing =
375
+ '# Old\n\n## Notes\n\nAdd your own notes here:\n\n- export PATH=$PATH:/x and use $& $$ $1 literally\n\n## Saved Context\n\nold\n';
376
+ vi.mocked(fs.readFile).mockResolvedValueOnce(existing as any);
377
+
378
+ const out = await preserveOperatorNotes('/ws/CLAUDE.md', NEW_CONTENT);
379
+ expect(out).toContain('export PATH=$PATH:/x and use $& $$ $1 literally');
380
+ });
381
+ });