prjct-cli 1.5.0 → 1.5.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.
package/CHANGELOG.md CHANGED
@@ -1,12 +1,55 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.5.1] - 2026-02-06
4
+
5
+ ### Refactoring
6
+
7
+ - standardize on fs/promises across codebase (PRJ-93) (#118)
8
+
9
+
10
+ ## [1.5.1] - 2026-02-06
11
+
12
+ ### Changed
13
+
14
+ - **Standardize on fs/promises across codebase (PRJ-93)**: Replaced all synchronous `fs` operations (`existsSync`, `readFileSync`, `writeFileSync`, `mkdirSync`, etc.) with async `fs/promises` equivalents across 22 files
15
+
16
+ ### Implementation Details
17
+
18
+ - Created shared `fileExists()` utility in `core/utils/fs-helpers.ts` replacing `existsSync` with `fs.access`
19
+ - Converted all detection functions in `ai-provider.ts` to async with `Promise.all` for parallel checks
20
+ - Applied lazy initialization pattern in `CommandInstaller` to handle async `getActiveProvider()` in constructor
21
+ - Replaced `execSync` with `promisify(exec)` in `registry.ts` and `ai-provider.ts`
22
+ - Converted `setup.ts` (~60 sync ops), `hooks-service.ts` (~30 sync ops), and all command/CLI files
23
+ - Updated prompt-builder and command-executor tests to handle async `build()` and `signalStart/End()`
24
+ - Intentional exceptions: `version.ts` (module-level constants), `jsonl-helper.ts` (`createReadStream`), test files
25
+
26
+ ### Learnings
27
+
28
+ - Module-level constants (`VERSION`, `PACKAGE_ROOT`) cannot use async — sync reads at import time are a valid exception
29
+ - `createReadStream` is inherently sync (returns a stream) — the correct pattern for streaming reads
30
+ - Making a function async cascades to all callers — `ai-provider.ts` changes rippled to 10+ files
31
+ - Constructor methods can't be async — solved with lazy `ensureInit()` pattern in `CommandInstaller`
32
+
33
+ ### Test Plan
34
+
35
+ #### For QA
36
+ 1. Run `bun run build` — verify clean build with no errors
37
+ 2. Run `bun test` — verify all 416 tests pass
38
+ 3. Run `prjct sync` on a project — verify async fs operations work correctly
39
+ 4. Run `prjct start` — verify setup flow works with async file operations
40
+ 5. Verify `prjct linear list` works (uses converted linear/sync.ts)
41
+
42
+ #### For Users
43
+ **What changed:** Internal refactor — no user-facing API changes. All sync filesystem operations replaced with async equivalents for better performance.
44
+ **How to use:** No changes needed. All commands work identically.
45
+ **Breaking changes:** None
46
+
3
47
  ## [1.5.0] - 2026-02-06
4
48
 
5
49
  ### Features
6
50
 
7
51
  - add citation format for context sources in templates (PRJ-113) (#117)
8
52
 
9
-
10
53
  ## [1.4.2] - 2026-02-06
11
54
 
12
55
  ### Features
package/bin/prjct.ts CHANGED
@@ -8,7 +8,6 @@
8
8
  * auto-install on first CLI use. This is the reliable path.
9
9
  */
10
10
 
11
- import fs from 'node:fs'
12
11
  import os from 'node:os'
13
12
  import path from 'node:path'
14
13
  import chalk from 'chalk'
@@ -16,20 +15,21 @@ import { detectAllProviders } from '../core/infrastructure/ai-provider'
16
15
  import configManager from '../core/infrastructure/config-manager'
17
16
  import editorsConfig from '../core/infrastructure/editors-config'
18
17
  import { DEFAULT_PORT, startServer } from '../core/server/server'
18
+ import { fileExists } from '../core/utils/fs-helpers'
19
19
  import { VERSION } from '../core/utils/version'
20
20
 
21
21
  /**
22
22
  * Check if routers are installed for detected providers
23
23
  * Returns true if at least one provider has its router installed
24
24
  */
25
- function checkRoutersInstalled(): boolean {
25
+ async function checkRoutersInstalled(): Promise<boolean> {
26
26
  const home = os.homedir()
27
- const detection = detectAllProviders()
27
+ const detection = await detectAllProviders()
28
28
 
29
29
  // Check Claude router
30
30
  if (detection.claude.installed) {
31
31
  const claudeRouter = path.join(home, '.claude', 'commands', 'p.md')
32
- if (!fs.existsSync(claudeRouter)) {
32
+ if (!(await fileExists(claudeRouter))) {
33
33
  return false
34
34
  }
35
35
  }
@@ -37,7 +37,7 @@ function checkRoutersInstalled(): boolean {
37
37
  // Check Gemini router
38
38
  if (detection.gemini.installed) {
39
39
  const geminiRouter = path.join(home, '.gemini', 'commands', 'p.toml')
40
- if (!fs.existsSync(geminiRouter)) {
40
+ if (!(await fileExists(geminiRouter))) {
41
41
  return false
42
42
  }
43
43
  }
@@ -218,15 +218,24 @@ if (args[0] === 'start' || args[0] === 'setup') {
218
218
  process.exitCode = 0
219
219
  } else if (args[0] === 'version' || args[0] === '-v' || args[0] === '--version') {
220
220
  // Show version with provider status
221
- const detection = detectAllProviders()
221
+ const detection = await detectAllProviders()
222
222
  const home = os.homedir()
223
223
  const cwd = process.cwd()
224
- const claudeConfigured = fs.existsSync(path.join(home, '.claude', 'commands', 'p.md'))
225
- const geminiConfigured = fs.existsSync(path.join(home, '.gemini', 'commands', 'p.toml'))
226
- const cursorDetected = fs.existsSync(path.join(cwd, '.cursor'))
227
- const cursorConfigured = fs.existsSync(path.join(cwd, '.cursor', 'rules', 'prjct.mdc'))
228
- const windsurfDetected = fs.existsSync(path.join(cwd, '.windsurf'))
229
- const windsurfConfigured = fs.existsSync(path.join(cwd, '.windsurf', 'rules', 'prjct.md'))
224
+ const [
225
+ claudeConfigured,
226
+ geminiConfigured,
227
+ cursorDetected,
228
+ cursorConfigured,
229
+ windsurfDetected,
230
+ windsurfConfigured,
231
+ ] = await Promise.all([
232
+ fileExists(path.join(home, '.claude', 'commands', 'p.md')),
233
+ fileExists(path.join(home, '.gemini', 'commands', 'p.toml')),
234
+ fileExists(path.join(cwd, '.cursor')),
235
+ fileExists(path.join(cwd, '.cursor', 'rules', 'prjct.mdc')),
236
+ fileExists(path.join(cwd, '.windsurf')),
237
+ fileExists(path.join(cwd, '.windsurf', 'rules', 'prjct.md')),
238
+ ])
230
239
 
231
240
  console.log(`
232
241
  ${chalk.cyan('p/')} prjct v${VERSION}
@@ -276,9 +285,9 @@ ${chalk.cyan('https://prjct.app')}
276
285
  } else {
277
286
  // Check if setup has been done
278
287
  const configPath = path.join(os.homedir(), '.prjct-cli', 'config', 'installed-editors.json')
279
- const routersInstalled = checkRoutersInstalled()
288
+ const routersInstalled = await checkRoutersInstalled()
280
289
 
281
- if (!fs.existsSync(configPath) || !routersInstalled) {
290
+ if (!(await fileExists(configPath)) || !routersInstalled) {
282
291
  // First time - prompt to run start
283
292
  console.log(`
284
293
  ${chalk.cyan.bold(' Welcome to prjct!')}
@@ -106,40 +106,40 @@ describe('CommandExecutor', () => {
106
106
  }
107
107
  })
108
108
 
109
- it('should create status file with command name', () => {
110
- signalStart('test-command')
109
+ it('should create status file with command name', async () => {
110
+ await signalStart('test-command')
111
111
 
112
112
  expect(fs.existsSync(RUNNING_FILE)).toBe(true)
113
113
  const content = fs.readFileSync(RUNNING_FILE, 'utf-8')
114
114
  expect(content).toBe('/p:test-command')
115
115
  })
116
116
 
117
- it('should overwrite existing status file', () => {
118
- signalStart('first-command')
119
- signalStart('second-command')
117
+ it('should overwrite existing status file', async () => {
118
+ await signalStart('first-command')
119
+ await signalStart('second-command')
120
120
 
121
121
  const content = fs.readFileSync(RUNNING_FILE, 'utf-8')
122
122
  expect(content).toBe('/p:second-command')
123
123
  })
124
124
 
125
- it('should handle filesystem errors gracefully', () => {
125
+ it('should handle filesystem errors gracefully', async () => {
126
126
  // This test verifies that errors are silently ignored
127
127
  // We can't easily simulate fs errors, but we can verify the function doesn't throw
128
- expect(() => signalStart('test-command')).not.toThrow()
128
+ await expect(signalStart('test-command')).resolves.toBeUndefined()
129
129
  })
130
130
  })
131
131
 
132
132
  describe('signalEnd', () => {
133
- it('should remove status file if it exists', () => {
133
+ it('should remove status file if it exists', async () => {
134
134
  // Create the file first
135
- signalStart('test-command')
135
+ await signalStart('test-command')
136
136
  expect(fs.existsSync(RUNNING_FILE)).toBe(true)
137
137
 
138
- signalEnd()
138
+ await signalEnd()
139
139
  expect(fs.existsSync(RUNNING_FILE)).toBe(false)
140
140
  })
141
141
 
142
- it('should not throw if file does not exist', () => {
142
+ it('should not throw if file does not exist', async () => {
143
143
  // Ensure file doesn't exist
144
144
  try {
145
145
  fs.unlinkSync(RUNNING_FILE)
@@ -147,7 +147,7 @@ describe('CommandExecutor', () => {
147
147
  // Ignore
148
148
  }
149
149
 
150
- expect(() => signalEnd()).not.toThrow()
150
+ await expect(signalEnd()).resolves.toBeUndefined()
151
151
  })
152
152
  })
153
153
 
@@ -158,20 +158,20 @@ describe('CommandExecutor', () => {
158
158
  executor = new CommandExecutor()
159
159
  })
160
160
 
161
- it('should have signalStart method that calls module function', () => {
162
- executor.signalStart('class-test')
161
+ it('should have signalStart method that calls module function', async () => {
162
+ await executor.signalStart('class-test')
163
163
 
164
164
  expect(fs.existsSync(RUNNING_FILE)).toBe(true)
165
165
  const content = fs.readFileSync(RUNNING_FILE, 'utf-8')
166
166
  expect(content).toBe('/p:class-test')
167
167
 
168
168
  // Cleanup
169
- executor.signalEnd()
169
+ await executor.signalEnd()
170
170
  })
171
171
 
172
- it('should have signalEnd method that calls module function', () => {
173
- executor.signalStart('class-test')
174
- executor.signalEnd()
172
+ it('should have signalEnd method that calls module function', async () => {
173
+ await executor.signalStart('class-test')
174
+ await executor.signalEnd()
175
175
 
176
176
  expect(fs.existsSync(RUNNING_FILE)).toBe(false)
177
177
  })
@@ -336,7 +336,7 @@ describe('execute', () => {
336
336
  memorySystem.getRelevantMemories = mock(() => Promise.resolve([]))
337
337
 
338
338
  // Mock promptBuilder
339
- promptBuilder.build = mock(() => 'built prompt')
339
+ promptBuilder.build = mock(() => Promise.resolve('built prompt'))
340
340
  })
341
341
 
342
342
  afterEach(() => {
@@ -96,7 +96,7 @@ describe('PromptBuilder', () => {
96
96
  })
97
97
 
98
98
  describe('Context Filtering by Command Type', () => {
99
- it('should include patterns for code commands', () => {
99
+ it('should include patterns for code commands', async () => {
100
100
  const template = {
101
101
  frontmatter: { description: 'Build feature', name: 'p:build' },
102
102
  content: '## Flow\nBuild something',
@@ -105,13 +105,13 @@ describe('PromptBuilder', () => {
105
105
  const context = { projectPath: '/test', files: ['file1.js'] }
106
106
  const state = { analysis: 'Stack: Node.js, React' }
107
107
 
108
- const prompt = builder.build(template, context, state)
108
+ const prompt = await builder.build(template, context, state)
109
109
 
110
110
  expect(prompt).toContain('PATTERNS')
111
111
  expect(prompt).toContain('Node.js')
112
112
  })
113
113
 
114
- it('should NOT include patterns for non-code commands', () => {
114
+ it('should NOT include patterns for non-code commands', async () => {
115
115
  const template = {
116
116
  frontmatter: { description: 'Show current task', name: 'p:now' },
117
117
  content: '## Flow\nShow task',
@@ -120,14 +120,14 @@ describe('PromptBuilder', () => {
120
120
  const context = { projectPath: '/test', files: ['file1.js'] }
121
121
  const state = { analysis: 'Stack: Node.js, React' }
122
122
 
123
- const prompt = builder.build(template, context, state)
123
+ const prompt = await builder.build(template, context, state)
124
124
 
125
125
  expect(prompt).not.toContain('## PATTERNS')
126
126
  })
127
127
  })
128
128
 
129
129
  describe('Project Files Listing', () => {
130
- it('should list available files when context has files', () => {
130
+ it('should list available files when context has files', async () => {
131
131
  const template = {
132
132
  frontmatter: { description: 'Test command' },
133
133
  content: '## Flow\nDo something',
@@ -140,7 +140,7 @@ describe('PromptBuilder', () => {
140
140
 
141
141
  const state = {}
142
142
 
143
- const prompt = builder.build(template, context, state)
143
+ const prompt = await builder.build(template, context, state)
144
144
 
145
145
  expect(prompt).toContain('## FILES:')
146
146
  expect(prompt).toContain('3 available')
@@ -148,7 +148,7 @@ describe('PromptBuilder', () => {
148
148
  expect(prompt).toContain('Read')
149
149
  })
150
150
 
151
- it('should show project path when no files listed', () => {
151
+ it('should show project path when no files listed', async () => {
152
152
  const template = {
153
153
  frontmatter: { description: 'Test command' },
154
154
  content: '## Flow\nDo something',
@@ -157,7 +157,7 @@ describe('PromptBuilder', () => {
157
157
  const context = { projectPath: '/test/project' }
158
158
  const state = {}
159
159
 
160
- const prompt = builder.build(template, context, state)
160
+ const prompt = await builder.build(template, context, state)
161
161
 
162
162
  expect(prompt).toContain('## PROJECT:')
163
163
  expect(prompt).toContain('/test/project')
@@ -165,7 +165,7 @@ describe('PromptBuilder', () => {
165
165
  })
166
166
 
167
167
  describe('Build Complete Prompt', () => {
168
- it('should include all critical sections', () => {
168
+ it('should include all critical sections', async () => {
169
169
  const template = {
170
170
  frontmatter: {
171
171
  description: 'Test command',
@@ -182,7 +182,7 @@ describe('PromptBuilder', () => {
182
182
 
183
183
  const state = { now: '# NOW\n\n**Current task**' }
184
184
 
185
- const prompt = builder.build(template, context, state)
185
+ const prompt = await builder.build(template, context, state)
186
186
 
187
187
  expect(prompt).toContain('TASK:')
188
188
  expect(prompt).toContain('TOOLS:')
@@ -191,7 +191,7 @@ describe('PromptBuilder', () => {
191
191
  expect(prompt).toContain('## FILES:')
192
192
  })
193
193
 
194
- it('should be concise (under 2000 chars for simple prompt)', () => {
194
+ it('should be concise (under 2000 chars for simple prompt)', async () => {
195
195
  const template = {
196
196
  frontmatter: { description: 'Test', 'allowed-tools': ['Read'] },
197
197
  content: '## Flow\n1. Test',
@@ -200,14 +200,14 @@ describe('PromptBuilder', () => {
200
200
  const context = { projectPath: '/test', files: ['a.js'] }
201
201
  const state = {}
202
202
 
203
- const prompt = builder.build(template, context, state)
203
+ const prompt = await builder.build(template, context, state)
204
204
 
205
205
  expect(prompt.length).toBeLessThan(2000)
206
206
  })
207
207
  })
208
208
 
209
209
  describe('Plan Mode (Compressed)', () => {
210
- it('should include compact plan mode instructions', () => {
210
+ it('should include compact plan mode instructions', async () => {
211
211
  const template = {
212
212
  frontmatter: { description: 'Test' },
213
213
  content: '## Flow\nTest',
@@ -217,14 +217,14 @@ describe('PromptBuilder', () => {
217
217
  const state = {}
218
218
  const planInfo = { isPlanning: true, allowedTools: ['Read', 'Glob'] }
219
219
 
220
- const prompt = builder.build(template, context, state, null, null, null, null, planInfo)
220
+ const prompt = await builder.build(template, context, state, null, null, null, null, planInfo)
221
221
 
222
222
  expect(prompt).toContain('PLAN MODE')
223
223
  expect(prompt).toContain('Read-only')
224
224
  expect(prompt).toContain('Tools: Read, Glob')
225
225
  })
226
226
 
227
- it('should include approval required section', () => {
227
+ it('should include approval required section', async () => {
228
228
  const template = {
229
229
  frontmatter: { description: 'Test' },
230
230
  content: '## Flow\nTest',
@@ -234,7 +234,7 @@ describe('PromptBuilder', () => {
234
234
  const state = {}
235
235
  const planInfo = { requiresApproval: true }
236
236
 
237
- const prompt = builder.build(template, context, state, null, null, null, null, planInfo)
237
+ const prompt = await builder.build(template, context, state, null, null, null, null, planInfo)
238
238
 
239
239
  expect(prompt).toContain('APPROVAL REQUIRED')
240
240
  expect(prompt).toContain('confirmation')
@@ -6,7 +6,7 @@
6
6
  * @version 3.4
7
7
  */
8
8
 
9
- import fs from 'node:fs'
9
+ import fs from 'node:fs/promises'
10
10
  import os from 'node:os'
11
11
  import path from 'node:path'
12
12
  import type {
@@ -18,6 +18,7 @@ import type {
18
18
  SimpleExecutionResult,
19
19
  } from '../types'
20
20
  import { agentStream } from '../utils/agent-stream'
21
+ import { fileExists } from '../utils/fs-helpers'
21
22
  import { printSubtaskProgress, type SubtaskDisplay } from '../utils/subtask-table'
22
23
  import chainOfThought from './chain-of-thought'
23
24
  import contextBuilder from './context-builder'
@@ -40,13 +41,13 @@ const RUNNING_FILE = path.join(os.homedir(), '.prjct-cli', '.running')
40
41
  /**
41
42
  * Signal that a command is running (for status line)
42
43
  */
43
- export function signalStart(commandName: string): void {
44
+ export async function signalStart(commandName: string): Promise<void> {
44
45
  try {
45
46
  const dir = path.dirname(RUNNING_FILE)
46
- if (!fs.existsSync(dir)) {
47
- fs.mkdirSync(dir, { recursive: true })
47
+ if (!(await fileExists(dir))) {
48
+ await fs.mkdir(dir, { recursive: true })
48
49
  }
49
- fs.writeFileSync(RUNNING_FILE, `/p:${commandName}`)
50
+ await fs.writeFile(RUNNING_FILE, `/p:${commandName}`)
50
51
  } catch (_error) {
51
52
  // Silently ignore - status line is optional
52
53
  }
@@ -55,10 +56,10 @@ export function signalStart(commandName: string): void {
55
56
  /**
56
57
  * Signal that command has finished (for status line)
57
58
  */
58
- export function signalEnd(): void {
59
+ export async function signalEnd(): Promise<void> {
59
60
  try {
60
- if (fs.existsSync(RUNNING_FILE)) {
61
- fs.unlinkSync(RUNNING_FILE)
61
+ if (await fileExists(RUNNING_FILE)) {
62
+ await fs.unlink(RUNNING_FILE)
62
63
  }
63
64
  } catch (_error) {
64
65
  // Silently ignore - status line is optional
@@ -73,15 +74,15 @@ export class CommandExecutor {
73
74
  /**
74
75
  * Signal that a command is running (for status line)
75
76
  */
76
- signalStart(commandName: string): void {
77
- signalStart(commandName)
77
+ async signalStart(commandName: string): Promise<void> {
78
+ await signalStart(commandName)
78
79
  }
79
80
 
80
81
  /**
81
82
  * Signal that command has finished (for status line)
82
83
  */
83
- signalEnd(): void {
84
- signalEnd()
84
+ async signalEnd(): Promise<void> {
85
+ await signalEnd()
85
86
  }
86
87
 
87
88
  /**
@@ -93,7 +94,7 @@ export class CommandExecutor {
93
94
  projectPath: string
94
95
  ): Promise<ExecutionResult> {
95
96
  // Signal start for status line
96
- this.signalStart(commandName)
97
+ await this.signalStart(commandName)
97
98
 
98
99
  // Context for loop detection
99
100
  const loopContext = (params.task as string) || (params.description as string) || ''
@@ -101,7 +102,7 @@ export class CommandExecutor {
101
102
  // Check if we're in a loop BEFORE attempting
102
103
  if (loopDetector.shouldEscalate(commandName, loopContext)) {
103
104
  const escalation = loopDetector.getEscalationInfo(commandName, loopContext)
104
- this.signalEnd()
105
+ await this.signalEnd()
105
106
  return {
106
107
  success: false,
107
108
  error: escalation?.message,
@@ -264,7 +265,7 @@ export class CommandExecutor {
264
265
  }
265
266
  // Agent is null - Claude assigns via Task tool using agent-routing.md
266
267
  // Pass orchestratorContext for domain/agent/subtask injection
267
- const prompt = promptBuilder.build(
268
+ const prompt = await promptBuilder.build(
268
269
  template,
269
270
  context,
270
271
  state,
@@ -286,7 +287,7 @@ export class CommandExecutor {
286
287
  loopDetector.recordSuccess(commandName, loopContext)
287
288
 
288
289
  // Signal end for status line
289
- this.signalEnd()
290
+ await this.signalEnd()
290
291
 
291
292
  return {
292
293
  success: true,
@@ -373,7 +374,7 @@ export class CommandExecutor {
373
374
  }
374
375
  } catch (error) {
375
376
  // Signal end for status line
376
- this.signalEnd()
377
+ await this.signalEnd()
377
378
 
378
379
  // Record failed attempt for loop detection
379
380
  const attemptInfo = loopDetector.recordAttempt(commandName, loopContext, {
@@ -9,7 +9,7 @@
9
9
  * @version 5.0
10
10
  */
11
11
 
12
- import fs from 'node:fs'
12
+ import fs from 'node:fs/promises'
13
13
  import path from 'node:path'
14
14
  import { outcomeAnalyzer } from '../outcomes'
15
15
  import { queueStorage, stateStorage } from '../storage'
@@ -26,6 +26,7 @@ import type {
26
26
  ThinkBlock,
27
27
  } from '../types'
28
28
  import { isNotFoundError } from '../types/fs'
29
+ import { fileExists } from '../utils/fs-helpers'
29
30
  import { getPackageRoot } from '../utils/version'
30
31
 
31
32
  // Re-export types for convenience
@@ -77,7 +78,7 @@ class PromptBuilder {
77
78
  * Returns cached content if within TTL, otherwise loads from disk.
78
79
  * @see PRJ-76
79
80
  */
80
- getTemplate(templatePath: string): string | null {
81
+ async getTemplate(templatePath: string): Promise<string | null> {
81
82
  const cached = this._templateCache.get(templatePath)
82
83
  const now = Date.now()
83
84
 
@@ -86,8 +87,8 @@ class PromptBuilder {
86
87
  }
87
88
 
88
89
  try {
89
- if (fs.existsSync(templatePath)) {
90
- const content = fs.readFileSync(templatePath, 'utf-8')
90
+ if (await fileExists(templatePath)) {
91
+ const content = await fs.readFile(templatePath, 'utf-8')
91
92
  this._templateCache.set(templatePath, { content, loadedAt: now })
92
93
  return content
93
94
  }
@@ -130,7 +131,7 @@ class PromptBuilder {
130
131
  * Load a specific CLAUDE module for SMART commands (PRJ-94)
131
132
  * These modules extend the base global CLAUDE.md for complex operations
132
133
  */
133
- loadModule(moduleName: string): string | null {
134
+ async loadModule(moduleName: string): Promise<string | null> {
134
135
  const modulePath = path.join(getPackageRoot(), 'templates/global/modules', moduleName)
135
136
  return this.getTemplate(modulePath)
136
137
  }
@@ -156,7 +157,7 @@ class PromptBuilder {
156
157
  * Uses lazy loading with TTL cache.
157
158
  * @see PRJ-76
158
159
  */
159
- loadChecklists(): Record<string, string> {
160
+ async loadChecklists(): Promise<Record<string, string>> {
160
161
  const now = Date.now()
161
162
 
162
163
  // Check if cache is still valid
@@ -168,13 +169,13 @@ class PromptBuilder {
168
169
  const checklists: Record<string, string> = {}
169
170
 
170
171
  try {
171
- if (fs.existsSync(checklistsDir)) {
172
- const files = fs.readdirSync(checklistsDir).filter((f) => f.endsWith('.md'))
172
+ if (await fileExists(checklistsDir)) {
173
+ const files = (await fs.readdir(checklistsDir)).filter((f: string) => f.endsWith('.md'))
173
174
  for (const file of files) {
174
175
  const name = file.replace('.md', '')
175
176
  const templatePath = path.join(checklistsDir, file)
176
177
  // Use getTemplate for individual files to leverage per-file caching
177
- const content = this.getTemplate(templatePath)
178
+ const content = await this.getTemplate(templatePath)
178
179
  if (content) {
179
180
  checklists[name] = content
180
181
  }
@@ -312,7 +313,7 @@ class PromptBuilder {
312
313
  * Uses lazy loading with TTL cache.
313
314
  * @see PRJ-76
314
315
  */
315
- loadChecklistRouting(): string | null {
316
+ async loadChecklistRouting(): Promise<string | null> {
316
317
  const now = Date.now()
317
318
 
318
319
  // Check if cache is still valid
@@ -333,7 +334,7 @@ class PromptBuilder {
333
334
  )
334
335
 
335
336
  // Use getTemplate for consistent caching behavior
336
- const content = this.getTemplate(routingPath)
337
+ const content = await this.getTemplate(routingPath)
337
338
  if (content) {
338
339
  this._checklistRoutingCache = content
339
340
  this._checklistRoutingCacheTime = now
@@ -367,7 +368,7 @@ class PromptBuilder {
367
368
  }
368
369
 
369
370
  // Build the rest using existing method
370
- const basePrompt = this.build(
371
+ const basePrompt = await this.build(
371
372
  template,
372
373
  context,
373
374
  state,
@@ -387,7 +388,7 @@ class PromptBuilder {
387
388
  * Build a complete prompt for Claude from template, context, and enhancements
388
389
  * @deprecated Use buildWithInjection for auto-injected context
389
390
  */
390
- build(
391
+ async build(
391
392
  template: Template,
392
393
  context: Context,
393
394
  state: State,
@@ -397,7 +398,7 @@ class PromptBuilder {
397
398
  relevantMemories: Memory[] | null = null,
398
399
  planInfo: PlanInfo | null = null,
399
400
  orchestratorContext: OrchestratorContext | null = null
400
- ): string {
401
+ ): Promise<string> {
401
402
  const parts: string[] = []
402
403
 
403
404
  // Store context for use in helper methods
@@ -591,7 +592,7 @@ class PromptBuilder {
591
592
  const additionalModules = this.getModulesForCommand(commandName)
592
593
  if (additionalModules.length > 0) {
593
594
  for (const moduleName of additionalModules) {
594
- const moduleContent = this.loadModule(moduleName)
595
+ const moduleContent = await this.loadModule(moduleName)
595
596
  if (moduleContent) {
596
597
  parts.push('\n')
597
598
  parts.push(moduleContent)
@@ -662,8 +663,8 @@ class PromptBuilder {
662
663
  'work',
663
664
  ]
664
665
  if (checklistCommands.includes(commandName)) {
665
- const routing = this.loadChecklistRouting()
666
- const checklists = this.loadChecklists()
666
+ const routing = await this.loadChecklistRouting()
667
+ const checklists = await this.loadChecklists()
667
668
 
668
669
  if (routing && Object.keys(checklists).length > 0) {
669
670
  parts.push('\n## QUALITY CHECKLISTS\n')
@@ -89,7 +89,7 @@ export class TemplateExecutor {
89
89
  const projectId = await this.getProjectId(projectPath)
90
90
  const globalPath = pathManager.getGlobalProjectPath(projectId)
91
91
  const aiProvider = require('../infrastructure/ai-provider')
92
- const activeProvider = aiProvider.getActiveProvider()
92
+ const activeProvider = await aiProvider.getActiveProvider()
93
93
 
94
94
  // Get templates directory - use local path during development
95
95
  let templatesDir: string
@@ -108,7 +108,7 @@ export class TemplateExecutor {
108
108
  command,
109
109
  args,
110
110
  agentName: activeProvider.displayName,
111
- agentSettingsPath: pathManager.getAgentSettingsPath(),
111
+ agentSettingsPath: await pathManager.getAgentSettingsPath(),
112
112
  paths: {
113
113
  orchestrator: path.join(templatesDir, 'agentic', 'orchestrator.md'),
114
114
  agentRouting: path.join(templatesDir, 'agentic', 'agent-routing.md'),