prjct-cli 0.58.0 → 0.59.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.59.0] - 2026-02-05
4
+
5
+ ### Features
6
+
7
+ - add staleness detection for CLAUDE.md context (PRJ-120) (#97)
8
+
9
+
3
10
  ## [0.58.0] - 2026-02-05
4
11
 
5
12
  ### Features
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Staleness Checker Tests (PRJ-120)
3
+ *
4
+ * Tests for the StalenessChecker service:
5
+ * - Fresh context detection
6
+ * - Stale context detection (commits, days, significant files)
7
+ * - No sync history handling
8
+ * - Output formatting
9
+ */
10
+
11
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
12
+ import fs from 'node:fs/promises'
13
+ import os from 'node:os'
14
+ import path from 'node:path'
15
+ import pathManager from '../../infrastructure/path-manager'
16
+ import { createStalenessChecker, type StalenessStatus } from '../../services/staleness-checker'
17
+
18
+ // =============================================================================
19
+ // Test Setup
20
+ // =============================================================================
21
+
22
+ let tmpRoot: string | null = null
23
+ let testProjectId: string
24
+ const originalGetGlobalProjectPath = pathManager.getGlobalProjectPath.bind(pathManager)
25
+
26
+ describe('StalenessChecker', () => {
27
+ beforeEach(async () => {
28
+ tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'prjct-staleness-test-'))
29
+ testProjectId = 'staleness-test-project'
30
+
31
+ // Mock pathManager to use temp directory
32
+ pathManager.getGlobalProjectPath = (projectId: string) => {
33
+ return path.join(tmpRoot!, projectId)
34
+ }
35
+ })
36
+
37
+ afterEach(async () => {
38
+ pathManager.getGlobalProjectPath = originalGetGlobalProjectPath
39
+
40
+ if (tmpRoot) {
41
+ await fs.rm(tmpRoot, { recursive: true, force: true })
42
+ tmpRoot = null
43
+ }
44
+ })
45
+
46
+ // ===========================================================================
47
+ // No Sync History Tests
48
+ // ===========================================================================
49
+
50
+ describe('no sync history', () => {
51
+ it('should report stale when no project.json exists', async () => {
52
+ const checker = createStalenessChecker(process.cwd())
53
+ const status = await checker.check(testProjectId)
54
+
55
+ expect(status.isStale).toBe(true)
56
+ expect(status.reason).toContain('No sync history found')
57
+ })
58
+
59
+ it('should report stale when no lastSyncCommit in project.json', async () => {
60
+ // Create project.json without lastSyncCommit
61
+ const projectPath = path.join(tmpRoot!, testProjectId)
62
+ await fs.mkdir(projectPath, { recursive: true })
63
+ await fs.writeFile(
64
+ path.join(projectPath, 'project.json'),
65
+ JSON.stringify({ name: 'test', lastSync: new Date().toISOString() })
66
+ )
67
+
68
+ const checker = createStalenessChecker(process.cwd())
69
+ const status = await checker.check(testProjectId)
70
+
71
+ expect(status.isStale).toBe(true)
72
+ expect(status.reason).toContain('No sync commit recorded')
73
+ })
74
+ })
75
+
76
+ // ===========================================================================
77
+ // Status Formatting Tests
78
+ // ===========================================================================
79
+
80
+ describe('formatStatus', () => {
81
+ it('should format fresh status correctly', () => {
82
+ const checker = createStalenessChecker(process.cwd())
83
+ const status: StalenessStatus = {
84
+ isStale: false,
85
+ reason: 'Context is up to date',
86
+ lastSyncCommit: 'abc123',
87
+ currentCommit: 'abc123',
88
+ commitsSinceSync: 0,
89
+ daysSinceSync: 0,
90
+ changedFiles: [],
91
+ significantChanges: [],
92
+ }
93
+
94
+ const formatted = checker.formatStatus(status)
95
+
96
+ expect(formatted).toContain('āœ“ Fresh')
97
+ expect(formatted).toContain('Last sync:')
98
+ expect(formatted).toContain('abc123')
99
+ expect(formatted).not.toContain('Run `prjct sync`')
100
+ })
101
+
102
+ it('should format stale status with sync prompt', () => {
103
+ const checker = createStalenessChecker(process.cwd())
104
+ const status: StalenessStatus = {
105
+ isStale: true,
106
+ reason: '15 commits since last sync (threshold: 10)',
107
+ lastSyncCommit: 'abc123',
108
+ currentCommit: 'def456',
109
+ commitsSinceSync: 15,
110
+ daysSinceSync: 2,
111
+ changedFiles: ['src/index.ts', 'package.json'],
112
+ significantChanges: ['package.json'],
113
+ }
114
+
115
+ const formatted = checker.formatStatus(status)
116
+
117
+ expect(formatted).toContain('āš ļø STALE')
118
+ expect(formatted).toContain('15')
119
+ expect(formatted).toContain('Significant changes')
120
+ expect(formatted).toContain('package.json')
121
+ expect(formatted).toContain('Run `prjct sync`')
122
+ })
123
+ })
124
+
125
+ // ===========================================================================
126
+ // Warning Message Tests
127
+ // ===========================================================================
128
+
129
+ describe('getWarning', () => {
130
+ it('should return null for fresh status', () => {
131
+ const checker = createStalenessChecker(process.cwd())
132
+ const status: StalenessStatus = {
133
+ isStale: false,
134
+ reason: 'Context is up to date',
135
+ lastSyncCommit: 'abc123',
136
+ currentCommit: 'abc123',
137
+ commitsSinceSync: 0,
138
+ daysSinceSync: 0,
139
+ changedFiles: [],
140
+ significantChanges: [],
141
+ }
142
+
143
+ const warning = checker.getWarning(status)
144
+ expect(warning).toBeNull()
145
+ })
146
+
147
+ it('should return commit-based warning', () => {
148
+ const checker = createStalenessChecker(process.cwd())
149
+ const status: StalenessStatus = {
150
+ isStale: true,
151
+ reason: '15 commits since last sync',
152
+ lastSyncCommit: 'abc123',
153
+ currentCommit: 'def456',
154
+ commitsSinceSync: 15,
155
+ daysSinceSync: 0,
156
+ changedFiles: [],
157
+ significantChanges: [],
158
+ }
159
+
160
+ const warning = checker.getWarning(status)
161
+ expect(warning).toContain('15 commits behind')
162
+ expect(warning).toContain('prjct sync')
163
+ })
164
+
165
+ it('should return days-based warning', () => {
166
+ const checker = createStalenessChecker(process.cwd())
167
+ const status: StalenessStatus = {
168
+ isStale: true,
169
+ reason: '5 days since last sync',
170
+ lastSyncCommit: 'abc123',
171
+ currentCommit: 'abc123',
172
+ commitsSinceSync: 0,
173
+ daysSinceSync: 5,
174
+ changedFiles: [],
175
+ significantChanges: [],
176
+ }
177
+
178
+ const warning = checker.getWarning(status)
179
+ expect(warning).toContain('5 days old')
180
+ expect(warning).toContain('prjct sync')
181
+ })
182
+ })
183
+
184
+ // ===========================================================================
185
+ // Configuration Tests
186
+ // ===========================================================================
187
+
188
+ describe('configuration', () => {
189
+ it('should use default thresholds', () => {
190
+ const checker = createStalenessChecker(process.cwd())
191
+ // Default commitThreshold is 10, dayThreshold is 3
192
+ expect(checker).toBeDefined()
193
+ })
194
+
195
+ it('should accept custom thresholds', () => {
196
+ const checker = createStalenessChecker(process.cwd(), {
197
+ commitThreshold: 5,
198
+ dayThreshold: 1,
199
+ significantFiles: ['custom.config.js'],
200
+ })
201
+ expect(checker).toBeDefined()
202
+ })
203
+ })
204
+ })
@@ -0,0 +1,379 @@
1
+ /**
2
+ * Storage Manager Tests
3
+ *
4
+ * Tests for the base StorageManager class:
5
+ * - Read/write JSON operations
6
+ * - Missing file handling
7
+ * - Directory creation
8
+ * - Cache behavior
9
+ * - State consistency
10
+ */
11
+
12
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
13
+ import fs from 'node:fs/promises'
14
+ import os from 'node:os'
15
+ import path from 'node:path'
16
+ import pathManager from '../../infrastructure/path-manager'
17
+ import { StorageManager } from '../../storage/storage-manager'
18
+
19
+ // =============================================================================
20
+ // Test Implementation
21
+ // =============================================================================
22
+
23
+ interface TestData {
24
+ value: string
25
+ count: number
26
+ items: string[]
27
+ }
28
+
29
+ /**
30
+ * Concrete implementation for testing the abstract StorageManager
31
+ */
32
+ class TestStorageManager extends StorageManager<TestData> {
33
+ constructor() {
34
+ super('test-data.json')
35
+ }
36
+
37
+ protected getLayer(): string {
38
+ return 'context'
39
+ }
40
+
41
+ protected getDefault(): TestData {
42
+ return { value: '', count: 0, items: [] }
43
+ }
44
+
45
+ protected toMarkdown(data: TestData): string {
46
+ return `# Test Data\n\nValue: ${data.value}\nCount: ${data.count}\nItems: ${data.items.join(', ')}`
47
+ }
48
+
49
+ protected getMdFilename(): string {
50
+ return 'test-data.md'
51
+ }
52
+
53
+ protected getEventType(action: 'update' | 'create' | 'delete'): string {
54
+ return `test.${action}`
55
+ }
56
+ }
57
+
58
+ // =============================================================================
59
+ // Test Setup
60
+ // =============================================================================
61
+
62
+ let tmpRoot: string | null = null
63
+ let testProjectId: string
64
+ let manager: TestStorageManager
65
+
66
+ // Mock pathManager to use temp directory
67
+ const originalGetGlobalProjectPath = pathManager.getGlobalProjectPath.bind(pathManager)
68
+ const originalGetStoragePath = pathManager.getStoragePath.bind(pathManager)
69
+ const originalGetFilePath = pathManager.getFilePath.bind(pathManager)
70
+
71
+ describe('StorageManager', () => {
72
+ beforeEach(async () => {
73
+ // Create temp directory for test isolation
74
+ tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'prjct-storage-test-'))
75
+ testProjectId = 'test-project-123'
76
+
77
+ // Mock pathManager to use temp directory
78
+ pathManager.getGlobalProjectPath = (projectId: string) => {
79
+ return path.join(tmpRoot!, projectId)
80
+ }
81
+
82
+ pathManager.getStoragePath = (projectId: string, filename: string) => {
83
+ return path.join(tmpRoot!, projectId, 'storage', filename)
84
+ }
85
+
86
+ pathManager.getFilePath = (projectId: string, layer: string, filename: string) => {
87
+ return path.join(tmpRoot!, projectId, layer, filename)
88
+ }
89
+
90
+ // Create fresh manager instance
91
+ manager = new TestStorageManager()
92
+ })
93
+
94
+ afterEach(async () => {
95
+ // Restore original pathManager methods
96
+ pathManager.getGlobalProjectPath = originalGetGlobalProjectPath
97
+ pathManager.getStoragePath = originalGetStoragePath
98
+ pathManager.getFilePath = originalGetFilePath
99
+
100
+ // Clean up temp directory
101
+ if (tmpRoot) {
102
+ await fs.rm(tmpRoot, { recursive: true, force: true })
103
+ tmpRoot = null
104
+ }
105
+ })
106
+
107
+ // ===========================================================================
108
+ // Read/Write Tests
109
+ // ===========================================================================
110
+
111
+ describe('read/write', () => {
112
+ it('should write and read JSON correctly', async () => {
113
+ const testData: TestData = {
114
+ value: 'hello',
115
+ count: 42,
116
+ items: ['a', 'b', 'c'],
117
+ }
118
+
119
+ await manager.write(testProjectId, testData)
120
+ const result = await manager.read(testProjectId)
121
+
122
+ expect(result).toEqual(testData)
123
+ })
124
+
125
+ it('should create storage file with correct JSON format', async () => {
126
+ const testData: TestData = {
127
+ value: 'test',
128
+ count: 1,
129
+ items: ['item1'],
130
+ }
131
+
132
+ await manager.write(testProjectId, testData)
133
+
134
+ // Verify file exists and has correct content
135
+ const storagePath = path.join(tmpRoot!, testProjectId, 'storage', 'test-data.json')
136
+ const content = await fs.readFile(storagePath, 'utf-8')
137
+ const parsed = JSON.parse(content)
138
+
139
+ expect(parsed).toEqual(testData)
140
+ })
141
+
142
+ it('should create context markdown file', async () => {
143
+ const testData: TestData = {
144
+ value: 'markdown-test',
145
+ count: 5,
146
+ items: ['x', 'y'],
147
+ }
148
+
149
+ await manager.write(testProjectId, testData)
150
+
151
+ // Verify markdown file exists
152
+ const contextPath = path.join(tmpRoot!, testProjectId, 'context', 'test-data.md')
153
+ const content = await fs.readFile(contextPath, 'utf-8')
154
+
155
+ expect(content).toContain('# Test Data')
156
+ expect(content).toContain('Value: markdown-test')
157
+ expect(content).toContain('Count: 5')
158
+ expect(content).toContain('Items: x, y')
159
+ })
160
+
161
+ it('should overwrite existing data', async () => {
162
+ const data1: TestData = { value: 'first', count: 1, items: [] }
163
+ const data2: TestData = { value: 'second', count: 2, items: ['new'] }
164
+
165
+ await manager.write(testProjectId, data1)
166
+ await manager.write(testProjectId, data2)
167
+
168
+ const result = await manager.read(testProjectId)
169
+ expect(result).toEqual(data2)
170
+ })
171
+ })
172
+
173
+ // ===========================================================================
174
+ // Missing File Handling
175
+ // ===========================================================================
176
+
177
+ describe('missing file handling', () => {
178
+ it('should return default when file does not exist', async () => {
179
+ const result = await manager.read('non-existent-project')
180
+
181
+ expect(result).toEqual({ value: '', count: 0, items: [] })
182
+ })
183
+
184
+ it('should return default for invalid JSON', async () => {
185
+ // Create invalid JSON file
186
+ const storagePath = path.join(tmpRoot!, testProjectId, 'storage', 'test-data.json')
187
+ await fs.mkdir(path.dirname(storagePath), { recursive: true })
188
+ await fs.writeFile(storagePath, 'not valid json {{{', 'utf-8')
189
+
190
+ const result = await manager.read(testProjectId)
191
+
192
+ expect(result).toEqual({ value: '', count: 0, items: [] })
193
+ })
194
+
195
+ it('should report exists=false for missing file', async () => {
196
+ const exists = await manager.exists('non-existent-project')
197
+ expect(exists).toBe(false)
198
+ })
199
+
200
+ it('should report exists=true after write', async () => {
201
+ await manager.write(testProjectId, { value: 'test', count: 1, items: [] })
202
+
203
+ const exists = await manager.exists(testProjectId)
204
+ expect(exists).toBe(true)
205
+ })
206
+ })
207
+
208
+ // ===========================================================================
209
+ // Directory Creation
210
+ // ===========================================================================
211
+
212
+ describe('directory creation', () => {
213
+ it('should create storage directory if it does not exist', async () => {
214
+ const testData: TestData = { value: 'dir-test', count: 1, items: [] }
215
+
216
+ // Directory shouldn't exist yet
217
+ const storageDir = path.join(tmpRoot!, testProjectId, 'storage')
218
+ await expect(fs.access(storageDir)).rejects.toThrow()
219
+
220
+ // Write should create it
221
+ await manager.write(testProjectId, testData)
222
+
223
+ // Now it should exist
224
+ const stat = await fs.stat(storageDir)
225
+ expect(stat.isDirectory()).toBe(true)
226
+ })
227
+
228
+ it('should create context directory if it does not exist', async () => {
229
+ const testData: TestData = { value: 'ctx-test', count: 1, items: [] }
230
+
231
+ // Directory shouldn't exist yet
232
+ const contextDir = path.join(tmpRoot!, testProjectId, 'context')
233
+ await expect(fs.access(contextDir)).rejects.toThrow()
234
+
235
+ // Write should create it
236
+ await manager.write(testProjectId, testData)
237
+
238
+ // Now it should exist
239
+ const stat = await fs.stat(contextDir)
240
+ expect(stat.isDirectory()).toBe(true)
241
+ })
242
+
243
+ it('should create nested directories', async () => {
244
+ const deepProjectId = 'deep/nested/project'
245
+ const testData: TestData = { value: 'nested', count: 1, items: [] }
246
+
247
+ await manager.write(deepProjectId, testData)
248
+
249
+ const result = await manager.read(deepProjectId)
250
+ expect(result).toEqual(testData)
251
+ })
252
+ })
253
+
254
+ // ===========================================================================
255
+ // Cache Behavior
256
+ // ===========================================================================
257
+
258
+ describe('cache behavior', () => {
259
+ it('should cache read results', async () => {
260
+ const testData: TestData = { value: 'cached', count: 1, items: [] }
261
+ await manager.write(testProjectId, testData)
262
+
263
+ // First read
264
+ const result1 = await manager.read(testProjectId)
265
+
266
+ // Modify file directly (bypass manager)
267
+ const storagePath = path.join(tmpRoot!, testProjectId, 'storage', 'test-data.json')
268
+ await fs.writeFile(
269
+ storagePath,
270
+ JSON.stringify({ value: 'modified', count: 2, items: [] }),
271
+ 'utf-8'
272
+ )
273
+
274
+ // Second read should return cached value
275
+ const result2 = await manager.read(testProjectId)
276
+ expect(result2).toEqual(result1)
277
+ })
278
+
279
+ it('should clear cache for specific project', async () => {
280
+ const testData: TestData = { value: 'to-clear', count: 1, items: [] }
281
+ await manager.write(testProjectId, testData)
282
+
283
+ // Read to populate cache
284
+ await manager.read(testProjectId)
285
+
286
+ // Modify file directly
287
+ const storagePath = path.join(tmpRoot!, testProjectId, 'storage', 'test-data.json')
288
+ const newData = { value: 'updated', count: 99, items: ['new'] }
289
+ await fs.writeFile(storagePath, JSON.stringify(newData), 'utf-8')
290
+
291
+ // Clear cache
292
+ manager.clearCache(testProjectId)
293
+
294
+ // Now read should get fresh data
295
+ const result = await manager.read(testProjectId)
296
+ expect(result).toEqual(newData)
297
+ })
298
+
299
+ it('should clear all cache', async () => {
300
+ // Write to multiple projects
301
+ await manager.write('project-a', { value: 'a', count: 1, items: [] })
302
+ await manager.write('project-b', { value: 'b', count: 2, items: [] })
303
+
304
+ // Read to populate cache
305
+ await manager.read('project-a')
306
+ await manager.read('project-b')
307
+
308
+ // Clear all cache
309
+ manager.clearCache()
310
+
311
+ // Verify cache stats
312
+ const stats = manager.getCacheStats()
313
+ expect(stats.size).toBe(0)
314
+ })
315
+
316
+ it('should return cache stats', async () => {
317
+ const stats = manager.getCacheStats()
318
+
319
+ expect(stats).toHaveProperty('size')
320
+ expect(stats).toHaveProperty('maxSize')
321
+ expect(stats).toHaveProperty('ttl')
322
+ expect(typeof stats.size).toBe('number')
323
+ expect(typeof stats.maxSize).toBe('number')
324
+ expect(typeof stats.ttl).toBe('number')
325
+ })
326
+ })
327
+
328
+ // ===========================================================================
329
+ // State Consistency (Update Operations)
330
+ // ===========================================================================
331
+
332
+ describe('state consistency', () => {
333
+ it('should update data atomically with updater function', async () => {
334
+ const initial: TestData = { value: 'initial', count: 0, items: [] }
335
+ await manager.write(testProjectId, initial)
336
+
337
+ const result = await manager.update(testProjectId, (current) => ({
338
+ ...current,
339
+ count: current.count + 1,
340
+ items: [...current.items, 'new-item'],
341
+ }))
342
+
343
+ expect(result.count).toBe(1)
344
+ expect(result.items).toEqual(['new-item'])
345
+
346
+ // Verify persisted
347
+ manager.clearCache(testProjectId)
348
+ const persisted = await manager.read(testProjectId)
349
+ expect(persisted).toEqual(result)
350
+ })
351
+
352
+ it('should handle multiple sequential updates', async () => {
353
+ await manager.write(testProjectId, { value: 'start', count: 0, items: [] })
354
+
355
+ // Multiple updates
356
+ for (let i = 0; i < 5; i++) {
357
+ await manager.update(testProjectId, (current) => ({
358
+ ...current,
359
+ count: current.count + 1,
360
+ }))
361
+ }
362
+
363
+ const result = await manager.read(testProjectId)
364
+ expect(result.count).toBe(5)
365
+ })
366
+
367
+ it('should maintain data integrity after failed read during update', async () => {
368
+ // Start with no file (will use default)
369
+ const result = await manager.update(testProjectId, (current) => ({
370
+ ...current,
371
+ value: 'from-default',
372
+ count: 100,
373
+ }))
374
+
375
+ expect(result.value).toBe('from-default')
376
+ expect(result.count).toBe(100)
377
+ })
378
+ })
379
+ })
@@ -10,7 +10,7 @@ import { generateContext } from '../context/generator'
10
10
  import analyzer from '../domain/analyzer'
11
11
  import commandInstaller from '../infrastructure/command-installer'
12
12
  import { formatCost } from '../schemas/metrics'
13
- import { memoryService, syncService } from '../services'
13
+ import { createStalenessChecker, memoryService, syncService } from '../services'
14
14
  import { formatDiffPreview, formatFullDiff, generateSyncDiff } from '../services/diff-generator'
15
15
  import { metricsStorage } from '../storage/metrics-storage'
16
16
  import type { AnalyzeOptions, CommandResult, ProjectContext } from '../types'
@@ -735,6 +735,64 @@ export class AnalysisCommands extends PrjctCommandsBase {
735
735
  }
736
736
  }
737
737
 
738
+ /**
739
+ * /p:status - Check if CLAUDE.md context is stale
740
+ *
741
+ * Uses git commit history to detect when significant changes
742
+ * have occurred since the last sync.
743
+ *
744
+ * @see PRJ-120
745
+ */
746
+ async status(
747
+ projectPath: string = process.cwd(),
748
+ options: { json?: boolean } = {}
749
+ ): Promise<CommandResult> {
750
+ try {
751
+ const initResult = await this.ensureProjectInit(projectPath)
752
+ if (!initResult.success) return initResult
753
+
754
+ const projectId = await configManager.getProjectId(projectPath)
755
+ if (!projectId) {
756
+ if (options.json) {
757
+ console.log(JSON.stringify({ success: false, error: 'No project ID found' }))
758
+ } else {
759
+ out.fail('No project ID found')
760
+ }
761
+ return { success: false, error: 'No project ID found' }
762
+ }
763
+
764
+ // Create staleness checker and run check
765
+ const checker = createStalenessChecker(projectPath)
766
+ const status = await checker.check(projectId)
767
+
768
+ // JSON output mode
769
+ if (options.json) {
770
+ console.log(
771
+ JSON.stringify({
772
+ success: true,
773
+ ...status,
774
+ })
775
+ )
776
+ return { success: true, data: status }
777
+ }
778
+
779
+ // Human-readable output
780
+ console.log('')
781
+ console.log(checker.formatStatus(status))
782
+ console.log('')
783
+
784
+ return { success: true, data: status }
785
+ } catch (error) {
786
+ const errMsg = (error as Error).message
787
+ if (options.json) {
788
+ console.log(JSON.stringify({ success: false, error: errMsg }))
789
+ } else {
790
+ out.fail(errMsg)
791
+ }
792
+ return { success: false, error: errMsg }
793
+ }
794
+ }
795
+
738
796
  /**
739
797
  * Get session activity stats from today's events
740
798
  * @see PRJ-89
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import path from 'node:path'
7
+ import { createStalenessChecker } from '../services'
7
8
  import { ideasStorage, queueStorage, shippedStorage, stateStorage } from '../storage'
8
9
  import type { CommandResult, ProjectContext } from '../types'
9
10
  import {
@@ -135,6 +136,14 @@ export class AnalyticsCommands extends PrjctCommandsBase {
135
136
  console.log(`\nšŸ“Š DASHBOARD - ${projectName}\n`)
136
137
  console.log('═'.repeat(50))
137
138
 
139
+ // Check staleness (PRJ-120)
140
+ const checker = createStalenessChecker(projectPath)
141
+ const stalenessStatus = await checker.check(projectId)
142
+ const stalenessWarning = checker.getWarning(stalenessStatus)
143
+ if (stalenessWarning) {
144
+ console.log(`\n${stalenessWarning}`)
145
+ }
146
+
138
147
  // Current task
139
148
  console.log('\nšŸŽÆ CURRENT FOCUS')
140
149
  if (currentTask) {