prjct-cli 0.44.1 → 0.45.3

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 (207) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/bin/prjct.ts +131 -10
  3. package/core/__tests__/agentic/memory-system.test.ts +39 -26
  4. package/core/__tests__/agentic/plan-mode.test.ts +64 -46
  5. package/core/__tests__/agentic/prompt-builder.test.ts +14 -14
  6. package/core/__tests__/services/project-index.test.ts +353 -0
  7. package/core/__tests__/types/fs.test.ts +3 -3
  8. package/core/__tests__/utils/date-helper.test.ts +10 -10
  9. package/core/__tests__/utils/output.test.ts +9 -6
  10. package/core/__tests__/utils/project-commands.test.ts +5 -6
  11. package/core/agentic/agent-router.ts +9 -10
  12. package/core/agentic/chain-of-thought.ts +16 -4
  13. package/core/agentic/command-executor.ts +66 -40
  14. package/core/agentic/context-builder.ts +8 -5
  15. package/core/agentic/ground-truth.ts +15 -9
  16. package/core/agentic/index.ts +145 -152
  17. package/core/agentic/loop-detector.ts +40 -11
  18. package/core/agentic/memory-system.ts +98 -35
  19. package/core/agentic/orchestrator-executor.ts +135 -71
  20. package/core/agentic/plan-mode.ts +46 -16
  21. package/core/agentic/prompt-builder.ts +108 -42
  22. package/core/agentic/services.ts +10 -9
  23. package/core/agentic/skill-loader.ts +9 -15
  24. package/core/agentic/smart-context.ts +129 -79
  25. package/core/agentic/template-executor.ts +13 -12
  26. package/core/agentic/template-loader.ts +7 -4
  27. package/core/agentic/tool-registry.ts +16 -13
  28. package/core/agents/index.ts +1 -1
  29. package/core/agents/performance.ts +10 -27
  30. package/core/ai-tools/formatters.ts +8 -6
  31. package/core/ai-tools/generator.ts +4 -4
  32. package/core/ai-tools/index.ts +1 -1
  33. package/core/ai-tools/registry.ts +21 -11
  34. package/core/bus/bus.ts +23 -16
  35. package/core/bus/index.ts +2 -2
  36. package/core/cli/linear.ts +3 -5
  37. package/core/cli/start.ts +28 -25
  38. package/core/commands/analysis.ts +287 -29
  39. package/core/commands/analytics.ts +52 -44
  40. package/core/commands/base.ts +15 -13
  41. package/core/commands/cleanup.ts +6 -13
  42. package/core/commands/command-data.ts +49 -8
  43. package/core/commands/commands.ts +60 -23
  44. package/core/commands/context.ts +4 -4
  45. package/core/commands/design.ts +3 -10
  46. package/core/commands/index.ts +5 -8
  47. package/core/commands/maintenance.ts +7 -4
  48. package/core/commands/planning.ts +179 -56
  49. package/core/commands/register.ts +14 -9
  50. package/core/commands/registry.ts +15 -14
  51. package/core/commands/setup.ts +26 -14
  52. package/core/commands/shipping.ts +11 -16
  53. package/core/commands/snapshots.ts +16 -32
  54. package/core/commands/uninstall.ts +541 -0
  55. package/core/commands/workflow.ts +24 -28
  56. package/core/constants/index.ts +10 -22
  57. package/core/context/generator.ts +82 -33
  58. package/core/context-tools/files-tool.ts +583 -0
  59. package/core/context-tools/imports-tool.ts +403 -0
  60. package/core/context-tools/index.ts +433 -0
  61. package/core/context-tools/recent-tool.ts +307 -0
  62. package/core/context-tools/signatures-tool.ts +501 -0
  63. package/core/context-tools/summary-tool.ts +307 -0
  64. package/core/context-tools/token-counter.ts +284 -0
  65. package/core/context-tools/types.ts +253 -0
  66. package/core/domain/agent-generator.ts +7 -5
  67. package/core/domain/agent-loader.ts +2 -2
  68. package/core/domain/analyzer.ts +19 -16
  69. package/core/domain/architecture-generator.ts +6 -3
  70. package/core/domain/context-estimator.ts +3 -4
  71. package/core/domain/snapshot-manager.ts +25 -22
  72. package/core/domain/task-stack.ts +24 -14
  73. package/core/errors.ts +1 -1
  74. package/core/events/events.ts +2 -4
  75. package/core/events/index.ts +1 -2
  76. package/core/index.ts +28 -12
  77. package/core/infrastructure/agent-detector.ts +3 -3
  78. package/core/infrastructure/ai-provider.ts +23 -20
  79. package/core/infrastructure/author-detector.ts +16 -10
  80. package/core/infrastructure/capability-installer.ts +2 -2
  81. package/core/infrastructure/claude-agent.ts +6 -6
  82. package/core/infrastructure/command-installer.ts +22 -17
  83. package/core/infrastructure/config-manager.ts +18 -14
  84. package/core/infrastructure/editors-config.ts +8 -4
  85. package/core/infrastructure/path-manager.ts +8 -6
  86. package/core/infrastructure/permission-manager.ts +20 -17
  87. package/core/infrastructure/setup.ts +42 -38
  88. package/core/infrastructure/update-checker.ts +5 -5
  89. package/core/integrations/issue-tracker/enricher.ts +8 -19
  90. package/core/integrations/issue-tracker/index.ts +2 -2
  91. package/core/integrations/issue-tracker/manager.ts +15 -15
  92. package/core/integrations/issue-tracker/types.ts +5 -22
  93. package/core/integrations/jira/client.ts +67 -59
  94. package/core/integrations/jira/index.ts +11 -14
  95. package/core/integrations/jira/mcp-adapter.ts +5 -10
  96. package/core/integrations/jira/service.ts +10 -10
  97. package/core/integrations/linear/client.ts +27 -18
  98. package/core/integrations/linear/index.ts +9 -12
  99. package/core/integrations/linear/service.ts +11 -11
  100. package/core/integrations/linear/sync.ts +8 -8
  101. package/core/outcomes/analyzer.ts +5 -18
  102. package/core/outcomes/index.ts +2 -2
  103. package/core/outcomes/recorder.ts +3 -3
  104. package/core/plugin/builtin/webhook.ts +19 -15
  105. package/core/plugin/hooks.ts +29 -21
  106. package/core/plugin/index.ts +7 -7
  107. package/core/plugin/loader.ts +19 -19
  108. package/core/plugin/registry.ts +12 -23
  109. package/core/schemas/agents.ts +1 -1
  110. package/core/schemas/analysis.ts +1 -1
  111. package/core/schemas/enriched-task.ts +62 -49
  112. package/core/schemas/ideas.ts +13 -13
  113. package/core/schemas/index.ts +17 -27
  114. package/core/schemas/issues.ts +40 -25
  115. package/core/schemas/metrics.ts +143 -0
  116. package/core/schemas/outcomes.ts +70 -62
  117. package/core/schemas/permissions.ts +15 -12
  118. package/core/schemas/prd.ts +27 -14
  119. package/core/schemas/project.ts +3 -3
  120. package/core/schemas/roadmap.ts +47 -34
  121. package/core/schemas/schemas.ts +3 -4
  122. package/core/schemas/shipped.ts +3 -3
  123. package/core/schemas/state.ts +43 -29
  124. package/core/server/index.ts +5 -6
  125. package/core/server/routes-extended.ts +68 -72
  126. package/core/server/routes.ts +3 -3
  127. package/core/server/server.ts +31 -26
  128. package/core/services/agent-generator.ts +237 -0
  129. package/core/services/agent-service.ts +2 -2
  130. package/core/services/breakdown-service.ts +2 -4
  131. package/core/services/context-generator.ts +299 -0
  132. package/core/services/context-selector.ts +420 -0
  133. package/core/services/doctor-service.ts +426 -0
  134. package/core/services/file-categorizer.ts +448 -0
  135. package/core/services/file-scorer.ts +270 -0
  136. package/core/services/git-analyzer.ts +267 -0
  137. package/core/services/index.ts +27 -10
  138. package/core/services/memory-service.ts +3 -4
  139. package/core/services/project-index.ts +911 -0
  140. package/core/services/project-service.ts +4 -4
  141. package/core/services/skill-installer.ts +14 -17
  142. package/core/services/skill-lock.ts +3 -3
  143. package/core/services/skill-service.ts +12 -6
  144. package/core/services/stack-detector.ts +245 -0
  145. package/core/services/sync-service.ts +170 -329
  146. package/core/services/watch-service.ts +294 -0
  147. package/core/session/compaction.ts +23 -31
  148. package/core/session/index.ts +11 -5
  149. package/core/session/log-migration.ts +3 -3
  150. package/core/session/metrics.ts +19 -14
  151. package/core/session/session-log-manager.ts +12 -17
  152. package/core/session/task-session-manager.ts +25 -25
  153. package/core/session/utils.ts +1 -1
  154. package/core/storage/ideas-storage.ts +41 -57
  155. package/core/storage/index-storage.ts +514 -0
  156. package/core/storage/index.ts +41 -13
  157. package/core/storage/metrics-storage.ts +320 -0
  158. package/core/storage/queue-storage.ts +35 -45
  159. package/core/storage/shipped-storage.ts +17 -20
  160. package/core/storage/state-storage.ts +50 -30
  161. package/core/storage/storage-manager.ts +6 -6
  162. package/core/storage/storage.ts +18 -15
  163. package/core/sync/auth-config.ts +3 -3
  164. package/core/sync/index.ts +13 -19
  165. package/core/sync/oauth-handler.ts +3 -3
  166. package/core/sync/sync-client.ts +4 -9
  167. package/core/sync/sync-manager.ts +12 -14
  168. package/core/types/commands.ts +42 -7
  169. package/core/types/index.ts +284 -302
  170. package/core/types/integrations.ts +3 -3
  171. package/core/types/storage.ts +49 -0
  172. package/core/types/utils.ts +3 -3
  173. package/core/utils/agent-stream.ts +3 -1
  174. package/core/utils/animations.ts +14 -11
  175. package/core/utils/branding.ts +7 -7
  176. package/core/utils/cache.ts +1 -3
  177. package/core/utils/collection-filters.ts +3 -15
  178. package/core/utils/date-helper.ts +2 -7
  179. package/core/utils/file-helper.ts +13 -8
  180. package/core/utils/jsonl-helper.ts +13 -10
  181. package/core/utils/keychain.ts +4 -8
  182. package/core/utils/logger.ts +1 -1
  183. package/core/utils/next-steps.ts +3 -3
  184. package/core/utils/output.ts +58 -11
  185. package/core/utils/project-commands.ts +6 -6
  186. package/core/utils/project-credentials.ts +5 -12
  187. package/core/utils/runtime.ts +2 -2
  188. package/core/utils/session-helper.ts +3 -4
  189. package/core/utils/version.ts +3 -3
  190. package/core/wizard/index.ts +13 -0
  191. package/core/wizard/onboarding.ts +633 -0
  192. package/core/workflow/state-machine.ts +7 -7
  193. package/dist/bin/prjct.mjs +18907 -13189
  194. package/dist/core/infrastructure/command-installer.js +96 -111
  195. package/dist/core/infrastructure/editors-config.js +6 -6
  196. package/dist/core/infrastructure/setup.js +256 -257
  197. package/dist/core/utils/version.js +9 -9
  198. package/package.json +11 -12
  199. package/scripts/build.js +3 -3
  200. package/scripts/postinstall.js +2 -2
  201. package/templates/mcp-config.json +6 -1
  202. package/templates/permissions/permissive.jsonc +1 -1
  203. package/templates/permissions/strict.jsonc +5 -9
  204. package/templates/global/docs/agents.md +0 -88
  205. package/templates/global/docs/architecture.md +0 -103
  206. package/templates/global/docs/commands.md +0 -96
  207. package/templates/global/docs/validation.md +0 -95
@@ -5,7 +5,7 @@
5
5
  * OPTIMIZED: Tests updated to match compressed prompt structure
6
6
  */
7
7
 
8
- import { describe, it, expect, beforeEach } from 'bun:test'
8
+ import { beforeEach, describe, expect, it } from 'bun:test'
9
9
  import promptBuilder from '../../agentic/prompt-builder'
10
10
 
11
11
  describe('PromptBuilder', () => {
@@ -63,7 +63,7 @@ describe('PromptBuilder', () => {
63
63
  next: '# NEXT\n\n## Priority Queue\n\n1. Task 1',
64
64
  context: 'Project context information',
65
65
  analysis: 'Stack: Node.js\nPatterns: ES6 modules',
66
- metrics: 'Some metrics data'
66
+ metrics: 'Some metrics data',
67
67
  }
68
68
 
69
69
  const filtered = builder.filterRelevantState(state)
@@ -79,7 +79,7 @@ describe('PromptBuilder', () => {
79
79
  const largeContent = 'x'.repeat(2000)
80
80
  const state = {
81
81
  now: '# NOW\n\n**Task**',
82
- largeFile: largeContent
82
+ largeFile: largeContent,
83
83
  }
84
84
 
85
85
  const filtered = builder.filterRelevantState(state)
@@ -99,7 +99,7 @@ describe('PromptBuilder', () => {
99
99
  it('should include patterns for code commands', () => {
100
100
  const template = {
101
101
  frontmatter: { description: 'Build feature', name: 'p:build' },
102
- content: '## Flow\nBuild something'
102
+ content: '## Flow\nBuild something',
103
103
  }
104
104
 
105
105
  const context = { projectPath: '/test', files: ['file1.js'] }
@@ -114,7 +114,7 @@ describe('PromptBuilder', () => {
114
114
  it('should NOT include patterns for non-code commands', () => {
115
115
  const template = {
116
116
  frontmatter: { description: 'Show current task', name: 'p:now' },
117
- content: '## Flow\nShow task'
117
+ content: '## Flow\nShow task',
118
118
  }
119
119
 
120
120
  const context = { projectPath: '/test', files: ['file1.js'] }
@@ -130,12 +130,12 @@ describe('PromptBuilder', () => {
130
130
  it('should list available files when context has files', () => {
131
131
  const template = {
132
132
  frontmatter: { description: 'Test command' },
133
- content: '## Flow\nDo something'
133
+ content: '## Flow\nDo something',
134
134
  }
135
135
 
136
136
  const context = {
137
137
  projectPath: '/test',
138
- files: ['src/file1.js', 'src/file2.js', 'tests/test1.js']
138
+ files: ['src/file1.js', 'src/file2.js', 'tests/test1.js'],
139
139
  }
140
140
 
141
141
  const state = {}
@@ -151,7 +151,7 @@ describe('PromptBuilder', () => {
151
151
  it('should show project path when no files listed', () => {
152
152
  const template = {
153
153
  frontmatter: { description: 'Test command' },
154
- content: '## Flow\nDo something'
154
+ content: '## Flow\nDo something',
155
155
  }
156
156
 
157
157
  const context = { projectPath: '/test/project' }
@@ -169,15 +169,15 @@ describe('PromptBuilder', () => {
169
169
  const template = {
170
170
  frontmatter: {
171
171
  description: 'Test command',
172
- 'allowed-tools': ['Read', 'Write']
172
+ 'allowed-tools': ['Read', 'Write'],
173
173
  },
174
- content: '## Flow\n1. Do step 1\n2. Do step 2'
174
+ content: '## Flow\n1. Do step 1\n2. Do step 2',
175
175
  }
176
176
 
177
177
  const context = {
178
178
  projectPath: '/test',
179
179
  params: { task: 'test task' },
180
- files: ['file1.js']
180
+ files: ['file1.js'],
181
181
  }
182
182
 
183
183
  const state = { now: '# NOW\n\n**Current task**' }
@@ -194,7 +194,7 @@ describe('PromptBuilder', () => {
194
194
  it('should be concise (under 2000 chars for simple prompt)', () => {
195
195
  const template = {
196
196
  frontmatter: { description: 'Test', 'allowed-tools': ['Read'] },
197
- content: '## Flow\n1. Test'
197
+ content: '## Flow\n1. Test',
198
198
  }
199
199
 
200
200
  const context = { projectPath: '/test', files: ['a.js'] }
@@ -210,7 +210,7 @@ describe('PromptBuilder', () => {
210
210
  it('should include compact plan mode instructions', () => {
211
211
  const template = {
212
212
  frontmatter: { description: 'Test' },
213
- content: '## Flow\nTest'
213
+ content: '## Flow\nTest',
214
214
  }
215
215
 
216
216
  const context = { projectPath: '/test' }
@@ -227,7 +227,7 @@ describe('PromptBuilder', () => {
227
227
  it('should include approval required section', () => {
228
228
  const template = {
229
229
  frontmatter: { description: 'Test' },
230
- content: '## Flow\nTest'
230
+ content: '## Flow\nTest',
231
231
  }
232
232
 
233
233
  const context = { projectPath: '/test' }
@@ -0,0 +1,353 @@
1
+ /**
2
+ * ProjectIndex Tests
3
+ *
4
+ * Tests for the persistent project scanner with scoring
5
+ */
6
+
7
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
8
+ import fs from 'node:fs/promises'
9
+ import os from 'node:os'
10
+ import path from 'node:path'
11
+ import pathManager from '../../infrastructure/path-manager'
12
+ import { FileScorer } from '../../services/file-scorer'
13
+ import { createProjectIndexer, RELEVANCE_THRESHOLD } from '../../services/project-index'
14
+ import { getDefaultIndex, indexStorage } from '../../storage/index-storage'
15
+
16
+ describe('FileScorer', () => {
17
+ describe('scoreFile', () => {
18
+ it('should give higher scores to recently modified files', () => {
19
+ const scorer = new FileScorer()
20
+
21
+ const recentFile = {
22
+ path: 'src/recent.ts',
23
+ size: 1000,
24
+ mtime: new Date(), // today
25
+ }
26
+
27
+ const oldFile = {
28
+ path: 'src/old.ts',
29
+ size: 1000,
30
+ mtime: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000), // 1 year ago
31
+ }
32
+
33
+ const context = {
34
+ allFiles: new Map([
35
+ ['src/recent.ts', recentFile],
36
+ ['src/old.ts', oldFile],
37
+ ]),
38
+ configFiles: new Set<string>(),
39
+ maxFileSize: 1000,
40
+ maxRecentCommits: 0,
41
+ now: new Date(),
42
+ }
43
+
44
+ const recentScore = scorer.scoreFile(recentFile, context)
45
+ const oldScore = scorer.scoreFile(oldFile, context)
46
+
47
+ expect(recentScore.factors.recency).toBeGreaterThan(oldScore.factors.recency)
48
+ })
49
+
50
+ it('should give higher scores to config files', () => {
51
+ const scorer = new FileScorer()
52
+
53
+ const configFile = {
54
+ path: 'package.json',
55
+ size: 500,
56
+ mtime: new Date(),
57
+ }
58
+
59
+ const sourceFile = {
60
+ path: 'src/utils.ts',
61
+ size: 500,
62
+ mtime: new Date(),
63
+ }
64
+
65
+ const context = {
66
+ allFiles: new Map([
67
+ ['package.json', configFile],
68
+ ['src/utils.ts', sourceFile],
69
+ ]),
70
+ configFiles: new Set(['package.json']),
71
+ maxFileSize: 500,
72
+ maxRecentCommits: 0,
73
+ now: new Date(),
74
+ }
75
+
76
+ const configScore = scorer.scoreFile(configFile, context)
77
+ const sourceScore = scorer.scoreFile(sourceFile, context)
78
+
79
+ expect(configScore.factors.configRelevance).toBe(20)
80
+ expect(sourceScore.factors.configRelevance).toBe(0)
81
+ })
82
+
83
+ it('should give higher scores to index/main files', () => {
84
+ const scorer = new FileScorer()
85
+
86
+ const indexFile = {
87
+ path: 'src/index.ts',
88
+ size: 500,
89
+ mtime: new Date(),
90
+ }
91
+
92
+ const helperFile = {
93
+ path: 'src/random-helper.ts',
94
+ size: 500,
95
+ mtime: new Date(),
96
+ }
97
+
98
+ const context = {
99
+ allFiles: new Map([
100
+ ['src/index.ts', indexFile],
101
+ ['src/random-helper.ts', helperFile],
102
+ ]),
103
+ configFiles: new Set<string>(),
104
+ maxFileSize: 500,
105
+ maxRecentCommits: 0,
106
+ now: new Date(),
107
+ }
108
+
109
+ const indexScore = scorer.scoreFile(indexFile, context)
110
+ const helperScore = scorer.scoreFile(helperFile, context)
111
+
112
+ expect(indexScore.factors.nameRelevance).toBe(15)
113
+ expect(helperScore.factors.nameRelevance).toBe(0)
114
+ })
115
+
116
+ it('should penalize very large files', () => {
117
+ const scorer = new FileScorer()
118
+
119
+ const normalFile = {
120
+ path: 'src/normal.ts',
121
+ size: 5000, // 5KB - optimal
122
+ mtime: new Date(),
123
+ }
124
+
125
+ const hugeFile = {
126
+ path: 'src/huge.ts',
127
+ size: 500000, // 500KB - too large
128
+ mtime: new Date(),
129
+ }
130
+
131
+ const context = {
132
+ allFiles: new Map([
133
+ ['src/normal.ts', normalFile],
134
+ ['src/huge.ts', hugeFile],
135
+ ]),
136
+ configFiles: new Set<string>(),
137
+ maxFileSize: 500000,
138
+ maxRecentCommits: 0,
139
+ now: new Date(),
140
+ }
141
+
142
+ const normalScore = scorer.scoreFile(normalFile, context)
143
+ const hugeScore = scorer.scoreFile(hugeFile, context)
144
+
145
+ expect(normalScore.factors.sizeOptimal).toBeGreaterThan(hugeScore.factors.sizeOptimal)
146
+ })
147
+ })
148
+
149
+ describe('getRelevantFiles', () => {
150
+ it('should filter files below threshold', () => {
151
+ const scorer = new FileScorer()
152
+
153
+ // Config file should be relevant
154
+ const configFile = {
155
+ path: 'package.json',
156
+ size: 500,
157
+ mtime: new Date(),
158
+ }
159
+
160
+ // Random old file should not be relevant
161
+ const randomFile = {
162
+ path: 'src/old-unused.ts',
163
+ size: 50, // very small
164
+ mtime: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000), // old
165
+ }
166
+
167
+ const context = {
168
+ allFiles: new Map([
169
+ ['package.json', configFile],
170
+ ['src/old-unused.ts', randomFile],
171
+ ]),
172
+ configFiles: new Set(['package.json']),
173
+ maxFileSize: 500,
174
+ maxRecentCommits: 0,
175
+ now: new Date(),
176
+ }
177
+
178
+ const relevant = scorer.getRelevantFiles(context, RELEVANCE_THRESHOLD)
179
+
180
+ expect(relevant.length).toBeGreaterThanOrEqual(1)
181
+ expect(relevant[0].path).toBe('package.json')
182
+ })
183
+ })
184
+ })
185
+
186
+ describe('IndexStorage', () => {
187
+ const testProjectId = `test-project-${Date.now()}`
188
+
189
+ beforeEach(async () => {
190
+ // Set up test directory
191
+ const testDir = path.join(os.tmpdir(), `prjct-test-${Date.now()}`)
192
+ pathManager.setGlobalBaseDir(testDir)
193
+ })
194
+
195
+ afterEach(async () => {
196
+ // Cleanup is handled by temp directory
197
+ })
198
+
199
+ describe('readIndex/writeIndex', () => {
200
+ it('should return null for non-existent index', async () => {
201
+ const index = await indexStorage.readIndex(testProjectId)
202
+ expect(index).toBeNull()
203
+ })
204
+
205
+ it('should persist and retrieve index', async () => {
206
+ const testIndex = getDefaultIndex('/test/project')
207
+ testIndex.lastFullScan = new Date().toISOString()
208
+ testIndex.totalFiles = 100
209
+
210
+ await indexStorage.writeIndex(testProjectId, testIndex)
211
+ const retrieved = await indexStorage.readIndex(testProjectId)
212
+
213
+ expect(retrieved).not.toBeNull()
214
+ expect(retrieved!.totalFiles).toBe(100)
215
+ expect(retrieved!.lastFullScan).toBe(testIndex.lastFullScan)
216
+ })
217
+ })
218
+
219
+ describe('hasValidIndex', () => {
220
+ it('should return false for non-existent index', async () => {
221
+ const valid = await indexStorage.hasValidIndex(testProjectId)
222
+ expect(valid).toBe(false)
223
+ })
224
+
225
+ it('should return true for index with lastFullScan', async () => {
226
+ const testIndex = getDefaultIndex('/test/project')
227
+ testIndex.lastFullScan = new Date().toISOString()
228
+
229
+ await indexStorage.writeIndex(testProjectId, testIndex)
230
+ const valid = await indexStorage.hasValidIndex(testProjectId)
231
+
232
+ expect(valid).toBe(true)
233
+ })
234
+ })
235
+ })
236
+
237
+ describe('ProjectIndexer', () => {
238
+ const testProjectId = `test-indexer-${Date.now()}`
239
+ let testProjectPath: string
240
+
241
+ beforeEach(async () => {
242
+ // Create a temp project directory
243
+ testProjectPath = path.join(os.tmpdir(), `prjct-indexer-test-${Date.now()}`)
244
+ await fs.mkdir(testProjectPath, { recursive: true })
245
+
246
+ // Create some test files
247
+ await fs.mkdir(path.join(testProjectPath, 'src'), { recursive: true })
248
+ await fs.writeFile(
249
+ path.join(testProjectPath, 'src', 'index.ts'),
250
+ 'export const main = () => {}'
251
+ )
252
+ await fs.writeFile(
253
+ path.join(testProjectPath, 'src', 'utils.ts'),
254
+ 'export const helper = () => {}'
255
+ )
256
+ await fs.writeFile(
257
+ path.join(testProjectPath, 'package.json'),
258
+ JSON.stringify(
259
+ {
260
+ name: 'test-project',
261
+ version: '1.0.0',
262
+ dependencies: { express: '^4.0.0' },
263
+ devDependencies: { typescript: '^5.0.0' },
264
+ },
265
+ null,
266
+ 2
267
+ )
268
+ )
269
+
270
+ // Set up test storage directory
271
+ const testStorageDir = path.join(os.tmpdir(), `prjct-storage-${Date.now()}`)
272
+ pathManager.setGlobalBaseDir(testStorageDir)
273
+ })
274
+
275
+ afterEach(async () => {
276
+ // Cleanup
277
+ try {
278
+ await fs.rm(testProjectPath, { recursive: true })
279
+ } catch {
280
+ // Ignore cleanup errors
281
+ }
282
+ })
283
+
284
+ describe('fullScan', () => {
285
+ it('should scan project and create index', async () => {
286
+ const indexer = createProjectIndexer(testProjectPath, testProjectId)
287
+ const result = await indexer.fullScan()
288
+
289
+ expect(result.fromCache).toBe(false)
290
+ expect(result.index.totalFiles).toBeGreaterThanOrEqual(2)
291
+ expect(result.index.lastFullScan).not.toBe('')
292
+ })
293
+
294
+ it('should detect TypeScript from config files', async () => {
295
+ // Add tsconfig.json
296
+ await fs.writeFile(
297
+ path.join(testProjectPath, 'tsconfig.json'),
298
+ JSON.stringify({
299
+ compilerOptions: { target: 'ES2020' },
300
+ })
301
+ )
302
+
303
+ const indexer = createProjectIndexer(testProjectPath, testProjectId)
304
+ const result = await indexer.fullScan()
305
+
306
+ expect(result.index.configFiles.some((cf) => cf.type === 'tsconfig.json')).toBe(true)
307
+ })
308
+
309
+ it('should detect Express backend', async () => {
310
+ const indexer = createProjectIndexer(testProjectPath, testProjectId)
311
+ const result = await indexer.fullScan()
312
+
313
+ expect(result.index.detectedStack.frameworks).toContain('Express')
314
+ })
315
+ })
316
+
317
+ describe('loadOrScan', () => {
318
+ it('should use cached index if valid', async () => {
319
+ const indexer = createProjectIndexer(testProjectPath, testProjectId)
320
+
321
+ // First scan
322
+ const firstResult = await indexer.fullScan()
323
+ expect(firstResult.fromCache).toBe(false)
324
+
325
+ // Second load should use cache
326
+ const secondResult = await indexer.loadOrScan()
327
+ expect(secondResult.fromCache).toBe(true)
328
+ })
329
+
330
+ it('should rescan if forceFullScan is true', async () => {
331
+ const indexer = createProjectIndexer(testProjectPath, testProjectId)
332
+
333
+ // First scan
334
+ await indexer.fullScan()
335
+
336
+ // Force rescan
337
+ const result = await indexer.loadOrScan({ forceFullScan: true })
338
+ expect(result.fromCache).toBe(false)
339
+ })
340
+ })
341
+
342
+ describe('getRelevantContext', () => {
343
+ it('should return files within token limit', async () => {
344
+ const indexer = createProjectIndexer(testProjectPath, testProjectId)
345
+ await indexer.fullScan()
346
+
347
+ const context = await indexer.getRelevantContext(10000)
348
+
349
+ expect(context.estimatedTokens).toBeLessThanOrEqual(10000)
350
+ expect(context.compressionRate).toBeGreaterThanOrEqual(0)
351
+ })
352
+ })
353
+ })
@@ -3,13 +3,13 @@
3
3
  * Tests for file system error utilities
4
4
  */
5
5
 
6
- import { describe, it, expect } from 'bun:test'
6
+ import { describe, expect, it } from 'bun:test'
7
7
  import {
8
- isNotFoundError,
9
- isPermissionError,
10
8
  isDirNotEmptyError,
11
9
  isFileExistsError,
12
10
  isNodeError,
11
+ isNotFoundError,
12
+ isPermissionError,
13
13
  } from '../../types/fs'
14
14
 
15
15
  describe('FS Error Utilities', () => {
@@ -3,24 +3,24 @@
3
3
  * Tests for centralized date operations and formatting
4
4
  */
5
5
 
6
- import { describe, it, expect, beforeEach, afterEach, setSystemTime } from 'bun:test'
6
+ import { afterEach, beforeEach, describe, expect, it, setSystemTime } from 'bun:test'
7
7
  import {
8
+ calculateDuration,
8
9
  formatDate,
10
+ formatDuration,
9
11
  formatMonth,
10
- getTodayKey,
11
12
  getDateKey,
12
- getYearMonthDay,
13
- parseDate,
14
- getTimestamp,
13
+ getDateRange,
15
14
  getDaysAgo,
16
15
  getDaysFromNow,
17
- getDateRange,
16
+ getEndOfDay,
17
+ getStartOfDay,
18
+ getTimestamp,
19
+ getTodayKey,
20
+ getYearMonthDay,
18
21
  isToday,
19
22
  isWithinLastDays,
20
- formatDuration,
21
- calculateDuration,
22
- getStartOfDay,
23
- getEndOfDay,
23
+ parseDate,
24
24
  } from '../../utils/date-helper'
25
25
 
26
26
  describe('DateHelper', () => {
@@ -3,21 +3,24 @@
3
3
  * Minimal output system for prjct-cli
4
4
  */
5
5
 
6
- import { describe, it, expect, beforeEach, afterEach, spyOn } from 'bun:test'
6
+ import { afterEach, beforeEach, describe, expect, it, spyOn } from 'bun:test'
7
7
  import out from '../../utils/output'
8
8
 
9
9
  describe('Output Module', () => {
10
10
  let consoleLogSpy: ReturnType<typeof spyOn>
11
+ let consoleErrorSpy: ReturnType<typeof spyOn>
11
12
  let stdoutWriteSpy: ReturnType<typeof spyOn>
12
13
 
13
14
  beforeEach(() => {
14
15
  consoleLogSpy = spyOn(console, 'log').mockImplementation(() => {})
16
+ consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {})
15
17
  stdoutWriteSpy = spyOn(process.stdout, 'write').mockImplementation(() => true)
16
18
  out.stop()
17
19
  })
18
20
 
19
21
  afterEach(() => {
20
22
  consoleLogSpy.mockRestore()
23
+ consoleErrorSpy.mockRestore()
21
24
  stdoutWriteSpy.mockRestore()
22
25
  out.stop()
23
26
  })
@@ -50,8 +53,8 @@ describe('Output Module', () => {
50
53
  it('should output error message with X mark', () => {
51
54
  out.fail('something failed')
52
55
 
53
- expect(consoleLogSpy).toHaveBeenCalledTimes(1)
54
- const output = consoleLogSpy.mock.calls[0][0]
56
+ expect(consoleErrorSpy).toHaveBeenCalledTimes(1)
57
+ const output = consoleErrorSpy.mock.calls[0][0]
55
58
  expect(output).toContain('✗')
56
59
  expect(output).toContain('something failed')
57
60
  })
@@ -60,7 +63,7 @@ describe('Output Module', () => {
60
63
  const longMessage = 'error '.repeat(50)
61
64
  out.fail(longMessage)
62
65
 
63
- const output = consoleLogSpy.mock.calls[0][0]
66
+ const output = consoleErrorSpy.mock.calls[0][0]
64
67
  expect(output.length).toBeLessThan(80)
65
68
  })
66
69
 
@@ -90,7 +93,7 @@ describe('Output Module', () => {
90
93
  it('should start spinner with message', async () => {
91
94
  out.spin('loading')
92
95
 
93
- await new Promise(resolve => setTimeout(resolve, 150))
96
+ await new Promise((resolve) => setTimeout(resolve, 150))
94
97
 
95
98
  expect(stdoutWriteSpy).toHaveBeenCalled()
96
99
  const output = stdoutWriteSpy.mock.calls[0][0]
@@ -109,7 +112,7 @@ describe('Output Module', () => {
109
112
  describe('stop()', () => {
110
113
  it('should stop spinner and clear line', async () => {
111
114
  out.spin('loading')
112
- await new Promise(resolve => setTimeout(resolve, 150))
115
+ await new Promise((resolve) => setTimeout(resolve, 150))
113
116
 
114
117
  stdoutWriteSpy.mockClear()
115
118
  out.stop()
@@ -3,9 +3,10 @@
3
3
  * Ensures prjct-cli uses the repo's own test/lint tooling (not hardcoded).
4
4
  */
5
5
 
6
- import { describe, it, expect, beforeEach, afterEach } from 'bun:test'
7
- import fs from 'fs/promises'
8
- import path from 'path'
6
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
7
+ import fs from 'node:fs/promises'
8
+ import os from 'node:os'
9
+ import path from 'node:path'
9
10
 
10
11
  import { detectProjectCommands } from '../../utils/project-commands'
11
12
 
@@ -23,7 +24,7 @@ async function writeText(filePath: string, content: string): Promise<void> {
23
24
 
24
25
  describe('detectProjectCommands', () => {
25
26
  beforeEach(async () => {
26
- tmpRoot = await fs.mkdtemp(path.join(process.cwd(), '.tmp', 'detect-commands-'))
27
+ tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'prjct-detect-commands-'))
27
28
  })
28
29
 
29
30
  afterEach(async () => {
@@ -68,5 +69,3 @@ describe('detectProjectCommands', () => {
68
69
  expect(detected.test?.command).toBe('cargo test')
69
70
  })
70
71
  })
71
-
72
-
@@ -11,12 +11,12 @@
11
11
  * @version 4.0.0
12
12
  */
13
13
 
14
- import fs from 'fs/promises'
15
- import path from 'path'
14
+ import fs from 'node:fs/promises'
15
+ import path from 'node:path'
16
16
  import configManager from '../infrastructure/config-manager'
17
17
  import pathManager from '../infrastructure/path-manager'
18
- import { isNotFoundError } from '../types/fs'
19
18
  import type { Agent, AssignmentContext } from '../types'
19
+ import { isNotFoundError } from '../types/fs'
20
20
 
21
21
  // Re-export types for convenience
22
22
  export type { Agent, AssignmentContext } from '../types'
@@ -130,13 +130,12 @@ class AgentRouter {
130
130
  'agent-usage.jsonl'
131
131
  )
132
132
 
133
- const entry =
134
- JSON.stringify({
135
- timestamp: new Date().toISOString(),
136
- task: typeof task === 'string' ? task : task.description,
137
- agent: typeof agent === 'string' ? agent : agent.name,
138
- projectId: this.projectId,
139
- }) + '\n'
133
+ const entry = `${JSON.stringify({
134
+ timestamp: new Date().toISOString(),
135
+ task: typeof task === 'string' ? task : task.description,
136
+ agent: typeof agent === 'string' ? agent : agent.name,
137
+ projectId: this.projectId,
138
+ })}\n`
140
139
 
141
140
  await fs.appendFile(logPath, entry)
142
141
  } catch (error) {
@@ -6,7 +6,7 @@
6
6
  * @version 1.0.0
7
7
  */
8
8
 
9
- import type { ProjectContext, ContextState } from '../types'
9
+ import type { ContextState, ProjectContext } from '../types'
10
10
 
11
11
  // Type aliases for compatibility with ProjectContext from contextBuilder.build()
12
12
  type Context = Pick<ProjectContext, 'projectId' | 'projectPath' | 'params'>
@@ -44,7 +44,11 @@ function requiresReasoning(commandName: string): boolean {
44
44
  /**
45
45
  * Reason through a command before execution
46
46
  */
47
- async function reason(commandName: string, context: Context, state: State): Promise<ReasoningResult> {
47
+ async function reason(
48
+ commandName: string,
49
+ context: Context,
50
+ state: State
51
+ ): Promise<ReasoningResult> {
48
52
  const steps: ReasoningStep[] = []
49
53
  const plan: string[] = []
50
54
  const criticalIssues: string[] = []
@@ -63,7 +67,11 @@ async function reason(commandName: string, context: Context, state: State): Prom
63
67
  if (state.shipped) {
64
68
  steps.push({ step: 'Shipped log accessible', passed: true })
65
69
  } else {
66
- steps.push({ step: 'Shipped log accessible', passed: false, details: 'shipped.md not found' })
70
+ steps.push({
71
+ step: 'Shipped log accessible',
72
+ passed: false,
73
+ details: 'shipped.md not found',
74
+ })
67
75
  }
68
76
 
69
77
  // Plan
@@ -79,7 +87,11 @@ async function reason(commandName: string, context: Context, state: State): Prom
79
87
  if (context.params.description || context.params.feature) {
80
88
  steps.push({ step: 'Has feature description', passed: true })
81
89
  } else {
82
- steps.push({ step: 'Has feature description', passed: false, details: 'No description provided' })
90
+ steps.push({
91
+ step: 'Has feature description',
92
+ passed: false,
93
+ details: 'No description provided',
94
+ })
83
95
  criticalIssues.push('Missing feature description')
84
96
  }
85
97