prjct-cli 1.5.0 → 1.6.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 +117 -1
- package/bin/prjct.ts +23 -14
- package/core/__tests__/agentic/command-executor.test.ts +19 -19
- package/core/__tests__/agentic/prompt-builder.test.ts +16 -16
- package/core/__tests__/utils/output.test.ts +7 -1
- package/core/agentic/command-executor.ts +18 -17
- package/core/agentic/orchestrator-executor.ts +126 -8
- package/core/agentic/prompt-builder.ts +58 -20
- package/core/agentic/template-executor.ts +2 -2
- package/core/ai-tools/registry.ts +18 -15
- package/core/cli/start.ts +18 -17
- package/core/commands/analysis.ts +6 -1
- package/core/commands/setup.ts +8 -8
- package/core/commands/uninstall.ts +11 -11
- package/core/context-tools/token-counter.ts +2 -0
- package/core/domain/agent-loader.ts +35 -1
- package/core/index.ts +12 -11
- package/core/infrastructure/agent-detector.ts +8 -8
- package/core/infrastructure/ai-provider.ts +49 -37
- package/core/infrastructure/command-installer.ts +18 -10
- package/core/infrastructure/path-manager.ts +4 -4
- package/core/infrastructure/setup.ts +124 -119
- package/core/infrastructure/update-checker.ts +14 -13
- package/core/integrations/linear/sync.ts +4 -4
- package/core/services/context-selector.ts +6 -3
- package/core/services/hierarchical-agent-resolver.ts +2 -0
- package/core/services/hooks-service.ts +78 -68
- package/core/services/sync-service.ts +115 -3
- package/core/session/compaction.ts +1 -1
- package/core/types/agentic.ts +22 -0
- package/core/types/agents.ts +5 -0
- package/core/types/index.ts +1 -0
- package/core/utils/fs-helpers.ts +14 -0
- package/core/utils/output.ts +15 -1
- package/core/utils/project-credentials.ts +8 -7
- package/dist/bin/prjct.mjs +1446 -750
- package/dist/core/infrastructure/command-installer.js +118 -87
- package/dist/core/infrastructure/setup.js +246 -210
- package/package.json +1 -1
- package/templates/config/skill-mappings.json +23 -60
- package/templates/subagents/domain/backend.md +1 -0
- package/templates/subagents/domain/database.md +1 -0
- package/templates/subagents/domain/devops.md +1 -0
- package/templates/subagents/domain/frontend.md +1 -0
- package/templates/subagents/domain/testing.md +1 -0
- package/templates/subagents/workflow/chief-architect.md +2 -1
- package/templates/subagents/workflow/prjct-planner.md +2 -1
- package/templates/subagents/workflow/prjct-shipper.md +1 -0
- package/templates/subagents/workflow/prjct-workflow.md +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,128 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.6.0] - 2026-02-06
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- super context for agents — skills.sh, proactive codebase context, effort/model (#121)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## [1.6.0] - 2026-02-06
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
- **Skills.sh auto-install**: During `prjct sync`, skills from skills.sh are automatically installed for generated agents. Real packages like `anthropics/skills/frontend-design`, `obra/superpowers/systematic-debugging`, and `obra/superpowers/test-driven-development` are mapped per agent domain.
|
|
15
|
+
- **Proactive codebase context**: The orchestrator now gathers real context before agent execution — git state, relevant files (scored by task relevance), code signatures from top files, and recently changed files. Agents start with a complete briefing instead of exploring first.
|
|
16
|
+
- **Effort/model metadata wiring**: Agent frontmatter `effort` and `model` fields are now extracted and injected into prompts, enabling per-agent reasoning depth control.
|
|
17
|
+
|
|
18
|
+
### Improved
|
|
19
|
+
|
|
20
|
+
- **Skill loading warnings**: Missing skills now log visible warnings with the agent that needs them and a hint to run `prjct sync`
|
|
21
|
+
- **Skill content in prompts**: Increased skill content truncation from 1000 to 2000 characters for richer context
|
|
22
|
+
- **Skill mappings v3**: Updated `skill-mappings.json` from generic names to real installable skills.sh packages
|
|
23
|
+
|
|
24
|
+
### Implementation Details
|
|
25
|
+
|
|
26
|
+
- `sync-service.ts`: New `autoInstallSkills()` method reads `skill-mappings.json`, checks if each skill is installed, and calls `skillInstaller.install()` for missing ones
|
|
27
|
+
- `orchestrator-executor.ts`: New `gatherRealContext()` calls `findRelevantFiles()`, `getRecentFiles()`, and `extractSignatures()` in parallel to build a proactive briefing
|
|
28
|
+
- `prompt-builder.ts`: New "CODEBASE CONTEXT" section with git state, relevant files table, code signatures, and recently changed files; plus effort/model per agent
|
|
29
|
+
- `agent-loader.ts`: New `extractFrontmatterMeta()` parses YAML frontmatter for effort/model fields
|
|
30
|
+
- `agentic.ts`: New `RealCodebaseContext` interface; `LoadedAgent` extended with `effort?` and `model?`
|
|
31
|
+
|
|
32
|
+
### Test Plan
|
|
33
|
+
|
|
34
|
+
#### For QA
|
|
35
|
+
1. Run `prjct sync` — verify skills auto-install (check `~/.claude/skills/`)
|
|
36
|
+
2. Run `p. task "test"` — verify prompt includes git state, relevant files, signatures, effort/model
|
|
37
|
+
3. Verify warnings for missing skills
|
|
38
|
+
4. Build and all 416 tests pass
|
|
39
|
+
|
|
40
|
+
#### For Users
|
|
41
|
+
**What changed:** Agents receive proactive codebase context before starting work. Skills auto-install during sync.
|
|
42
|
+
**How to use:** No action needed — automatic during `prjct sync` and `p. task`.
|
|
43
|
+
**Breaking changes:** None
|
|
44
|
+
|
|
45
|
+
## [1.5.2] - 2026-02-06
|
|
46
|
+
|
|
47
|
+
### Improved
|
|
48
|
+
|
|
49
|
+
- **TTY detection for CLI spinners**: Spinner, step, and progress animations now detect non-TTY environments (CI/CD, Claude Code, piped output) and print a single static line instead of animating
|
|
50
|
+
|
|
51
|
+
### Implementation Details
|
|
52
|
+
|
|
53
|
+
- Added `process.stdout.isTTY` guard to `spin()`, `step()`, and `progress()` in `core/utils/output.ts`
|
|
54
|
+
- Non-TTY environments get a single line with `\n` instead of `setInterval` with `\r` carriage returns
|
|
55
|
+
- Made `clear()` a no-op in non-TTY (the `\r` + spaces trick doesn't work outside terminals)
|
|
56
|
+
- Updated test for `stop()` to handle non-TTY behavior in test runner
|
|
57
|
+
|
|
58
|
+
### Learnings
|
|
59
|
+
|
|
60
|
+
- `process.stdout.isTTY` is the reliable built-in way to detect interactive terminals in Node.js
|
|
61
|
+
- Test suites (bun test) also run as non-TTY, so test assertions need to account for both paths
|
|
62
|
+
|
|
63
|
+
### Test Plan
|
|
64
|
+
|
|
65
|
+
#### For QA
|
|
66
|
+
1. Run `prjct sync --yes` in an interactive terminal — spinner should animate normally
|
|
67
|
+
2. Run `prjct sync --yes > out.txt` — output should show a single static line, no repeated frames
|
|
68
|
+
3. Run inside Claude Code Bash tool — output should be clean, no spinner noise
|
|
69
|
+
4. Verify `step()` and `progress()` behave the same way in both environments
|
|
70
|
+
|
|
71
|
+
#### For Users
|
|
72
|
+
**What changed:** CLI spinners no longer produce garbage output in non-interactive terminals
|
|
73
|
+
**How to use:** No action needed — automatic TTY detection
|
|
74
|
+
**Breaking changes:** None
|
|
75
|
+
|
|
76
|
+
## [1.5.1] - 2026-02-06
|
|
77
|
+
|
|
78
|
+
### Refactoring
|
|
79
|
+
|
|
80
|
+
- standardize on fs/promises across codebase (PRJ-93) (#118)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
## [1.5.1] - 2026-02-06
|
|
84
|
+
|
|
85
|
+
### Changed
|
|
86
|
+
|
|
87
|
+
- **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
|
|
88
|
+
|
|
89
|
+
### Implementation Details
|
|
90
|
+
|
|
91
|
+
- Created shared `fileExists()` utility in `core/utils/fs-helpers.ts` replacing `existsSync` with `fs.access`
|
|
92
|
+
- Converted all detection functions in `ai-provider.ts` to async with `Promise.all` for parallel checks
|
|
93
|
+
- Applied lazy initialization pattern in `CommandInstaller` to handle async `getActiveProvider()` in constructor
|
|
94
|
+
- Replaced `execSync` with `promisify(exec)` in `registry.ts` and `ai-provider.ts`
|
|
95
|
+
- Converted `setup.ts` (~60 sync ops), `hooks-service.ts` (~30 sync ops), and all command/CLI files
|
|
96
|
+
- Updated prompt-builder and command-executor tests to handle async `build()` and `signalStart/End()`
|
|
97
|
+
- Intentional exceptions: `version.ts` (module-level constants), `jsonl-helper.ts` (`createReadStream`), test files
|
|
98
|
+
|
|
99
|
+
### Learnings
|
|
100
|
+
|
|
101
|
+
- Module-level constants (`VERSION`, `PACKAGE_ROOT`) cannot use async — sync reads at import time are a valid exception
|
|
102
|
+
- `createReadStream` is inherently sync (returns a stream) — the correct pattern for streaming reads
|
|
103
|
+
- Making a function async cascades to all callers — `ai-provider.ts` changes rippled to 10+ files
|
|
104
|
+
- Constructor methods can't be async — solved with lazy `ensureInit()` pattern in `CommandInstaller`
|
|
105
|
+
|
|
106
|
+
### Test Plan
|
|
107
|
+
|
|
108
|
+
#### For QA
|
|
109
|
+
1. Run `bun run build` — verify clean build with no errors
|
|
110
|
+
2. Run `bun test` — verify all 416 tests pass
|
|
111
|
+
3. Run `prjct sync` on a project — verify async fs operations work correctly
|
|
112
|
+
4. Run `prjct start` — verify setup flow works with async file operations
|
|
113
|
+
5. Verify `prjct linear list` works (uses converted linear/sync.ts)
|
|
114
|
+
|
|
115
|
+
#### For Users
|
|
116
|
+
**What changed:** Internal refactor — no user-facing API changes. All sync filesystem operations replaced with async equivalents for better performance.
|
|
117
|
+
**How to use:** No changes needed. All commands work identically.
|
|
118
|
+
**Breaking changes:** None
|
|
119
|
+
|
|
3
120
|
## [1.5.0] - 2026-02-06
|
|
4
121
|
|
|
5
122
|
### Features
|
|
6
123
|
|
|
7
124
|
- add citation format for context sources in templates (PRJ-113) (#117)
|
|
8
125
|
|
|
9
|
-
|
|
10
126
|
## [1.4.2] - 2026-02-06
|
|
11
127
|
|
|
12
128
|
### 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 (!
|
|
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 (!
|
|
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
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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 (!
|
|
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(
|
|
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(
|
|
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')
|
|
@@ -124,7 +124,13 @@ describe('Output Module', () => {
|
|
|
124
124
|
stdoutWriteSpy.mockClear()
|
|
125
125
|
out.stop()
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
// In TTY, stop() writes a clear sequence; in non-TTY, spinner doesn't
|
|
128
|
+
// use setInterval so stop() is a no-op (clear skips in non-TTY)
|
|
129
|
+
if (process.stdout.isTTY) {
|
|
130
|
+
expect(stdoutWriteSpy).toHaveBeenCalled()
|
|
131
|
+
} else {
|
|
132
|
+
expect(stdoutWriteSpy).not.toHaveBeenCalled()
|
|
133
|
+
}
|
|
128
134
|
})
|
|
129
135
|
|
|
130
136
|
it('should be safe to call multiple times', () => {
|
|
@@ -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 (!
|
|
47
|
-
fs.
|
|
47
|
+
if (!(await fileExists(dir))) {
|
|
48
|
+
await fs.mkdir(dir, { recursive: true })
|
|
48
49
|
}
|
|
49
|
-
fs.
|
|
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 (
|
|
61
|
-
fs.
|
|
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, {
|