prjct-cli 0.60.1 → 0.61.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 +71 -0
- package/core/agentic/orchestrator-executor.ts +31 -33
- package/core/domain/agent-loader.ts +8 -8
- package/core/services/local-state-generator.ts +158 -0
- package/core/services/sync-service.ts +11 -0
- package/core/storage/state-storage.ts +3 -0
- package/dist/bin/prjct.mjs +453 -331
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,76 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.61.0] - 2026-02-05
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- add .prjct-state.md local state file for persistence (PRJ-112) (#102)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## [0.61.0] - 2026-02-05
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
- **Local state file (PRJ-112)**: New `.prjct-state.md` file generated in project root for local persistence
|
|
15
|
+
|
|
16
|
+
### Implementation Details
|
|
17
|
+
|
|
18
|
+
Created `LocalStateGenerator` service that generates a markdown file showing current task state. Integrated via write-through pattern - `StateStorage.write()` now also generates the local state file. Also hooks into `sync-service.ts` for state.json updates during sync.
|
|
19
|
+
|
|
20
|
+
### Learnings
|
|
21
|
+
|
|
22
|
+
- Write-through pattern: JSON storage triggers MD generation automatically
|
|
23
|
+
- State can be written from multiple entry points (storage class + sync service) - need hooks in both places
|
|
24
|
+
|
|
25
|
+
### Test Plan
|
|
26
|
+
|
|
27
|
+
#### For QA
|
|
28
|
+
1. Run `prjct sync` on any project - verify `.prjct-state.md` is generated in project root
|
|
29
|
+
2. Start a task with `p. task "test"` - verify `.prjct-state.md` updates with task info
|
|
30
|
+
3. Check that subtasks, progress, and status are displayed correctly
|
|
31
|
+
4. Verify the file has "DO NOT EDIT" header comment
|
|
32
|
+
|
|
33
|
+
#### For Users
|
|
34
|
+
- New `.prjct-state.md` file in project root shows current task state
|
|
35
|
+
- Automatic - file updates whenever prjct state changes
|
|
36
|
+
- No breaking changes
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
## [0.60.2] - 2026-02-05
|
|
40
|
+
|
|
41
|
+
### Performance
|
|
42
|
+
|
|
43
|
+
- parallelize agent/skill loading with Promise.all (PRJ-110) (#101)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
## [0.60.2] - 2026-02-05
|
|
47
|
+
|
|
48
|
+
### Performance
|
|
49
|
+
|
|
50
|
+
- **Parallel agent/skill loading (PRJ-110)**: Agent and skill loading now uses `Promise.all` for parallel I/O
|
|
51
|
+
|
|
52
|
+
### Implementation Details
|
|
53
|
+
|
|
54
|
+
Refactored `loadAgents()` and `loadSkills()` in `core/agentic/orchestrator-executor.ts` to use `Promise.all` with map instead of sequential for loops. Also parallelized `loadAllAgents()` in `core/domain/agent-loader.ts`. Pattern: collect items → map to async promises → Promise.all → filter nulls with type guard.
|
|
55
|
+
|
|
56
|
+
### Learnings
|
|
57
|
+
|
|
58
|
+
- Use `Promise.all(items.map(async (item) => ...))` for parallel async operations
|
|
59
|
+
- Return null for failed items, then filter - can't push to array in parallel
|
|
60
|
+
- Collect unique items first (deduplication), then parallelize reads
|
|
61
|
+
|
|
62
|
+
### Test Plan
|
|
63
|
+
|
|
64
|
+
#### For QA
|
|
65
|
+
1. Run `prjct sync --yes` - verify agents load successfully
|
|
66
|
+
2. Run `p. task "test"` - verify orchestrator works
|
|
67
|
+
3. Check no errors in agent/skill loading output
|
|
68
|
+
|
|
69
|
+
#### For Users
|
|
70
|
+
- Agent and skill loading is now faster (parallel I/O)
|
|
71
|
+
- No changes needed - improvement is automatic
|
|
72
|
+
|
|
73
|
+
|
|
3
74
|
## [0.60.1] - 2026-02-05
|
|
4
75
|
|
|
5
76
|
### Bug Fixes
|
|
@@ -341,13 +341,15 @@ export class OrchestratorExecutor {
|
|
|
341
341
|
*
|
|
342
342
|
* Reads agent markdown files from {globalPath}/agents/
|
|
343
343
|
* and extracts their content and skills from frontmatter.
|
|
344
|
+
*
|
|
345
|
+
* Uses parallel file reads for performance (PRJ-110).
|
|
344
346
|
*/
|
|
345
347
|
async loadAgents(domains: string[], projectId: string): Promise<LoadedAgent[]> {
|
|
346
348
|
const globalPath = pathManager.getGlobalProjectPath(projectId)
|
|
347
349
|
const agentsDir = path.join(globalPath, 'agents')
|
|
348
|
-
const agents: LoadedAgent[] = []
|
|
349
350
|
|
|
350
|
-
|
|
351
|
+
// Load all domain agents in parallel
|
|
352
|
+
const agentPromises = domains.map(async (domain): Promise<LoadedAgent | null> => {
|
|
351
353
|
// Try exact match first, then variations
|
|
352
354
|
const possibleNames = [`${domain}.md`, `${domain}-agent.md`, `prjct-${domain}.md`]
|
|
353
355
|
|
|
@@ -357,21 +359,22 @@ export class OrchestratorExecutor {
|
|
|
357
359
|
const content = await fs.readFile(filePath, 'utf-8')
|
|
358
360
|
const { frontmatter, body } = this.parseAgentFile(content)
|
|
359
361
|
|
|
360
|
-
|
|
362
|
+
return {
|
|
361
363
|
name: fileName.replace('.md', ''),
|
|
362
364
|
domain,
|
|
363
365
|
content: body,
|
|
364
366
|
skills: frontmatter.skills || [],
|
|
365
367
|
filePath,
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
//
|
|
369
|
-
|
|
370
|
-
} catch {}
|
|
368
|
+
}
|
|
369
|
+
} catch {
|
|
370
|
+
// Try next variation
|
|
371
|
+
}
|
|
371
372
|
}
|
|
372
|
-
|
|
373
|
+
return null
|
|
374
|
+
})
|
|
373
375
|
|
|
374
|
-
|
|
376
|
+
const results = await Promise.all(agentPromises)
|
|
377
|
+
return results.filter((agent): agent is LoadedAgent => agent !== null)
|
|
375
378
|
}
|
|
376
379
|
|
|
377
380
|
/**
|
|
@@ -400,51 +403,46 @@ export class OrchestratorExecutor {
|
|
|
400
403
|
* Load skills from agent frontmatter
|
|
401
404
|
*
|
|
402
405
|
* Skills are stored in ~/.claude/skills/{name}.md
|
|
406
|
+
*
|
|
407
|
+
* Uses parallel file reads for performance (PRJ-110).
|
|
403
408
|
*/
|
|
404
409
|
async loadSkills(agents: LoadedAgent[]): Promise<LoadedSkill[]> {
|
|
405
410
|
const skillsDir = path.join(os.homedir(), '.claude', 'skills')
|
|
406
|
-
const skills: LoadedSkill[] = []
|
|
407
|
-
const loadedSkillNames = new Set<string>()
|
|
408
411
|
|
|
412
|
+
// Collect unique skill names from all agents
|
|
413
|
+
const uniqueSkillNames = new Set<string>()
|
|
409
414
|
for (const agent of agents) {
|
|
410
415
|
for (const skillName of agent.skills) {
|
|
411
|
-
|
|
412
|
-
|
|
416
|
+
uniqueSkillNames.add(skillName)
|
|
417
|
+
}
|
|
418
|
+
}
|
|
413
419
|
|
|
420
|
+
// Load all skills in parallel
|
|
421
|
+
const skillPromises = Array.from(uniqueSkillNames).map(
|
|
422
|
+
async (skillName): Promise<LoadedSkill | null> => {
|
|
414
423
|
// Check both patterns: flat file and subdirectory (ecosystem standard)
|
|
415
424
|
const flatPath = path.join(skillsDir, `${skillName}.md`)
|
|
416
425
|
const subdirPath = path.join(skillsDir, skillName, 'SKILL.md')
|
|
417
426
|
|
|
418
|
-
let content: string | null = null
|
|
419
|
-
let resolvedPath = flatPath
|
|
420
|
-
|
|
421
427
|
// Prefer subdirectory format (ecosystem standard)
|
|
422
428
|
try {
|
|
423
|
-
content = await fs.readFile(subdirPath, 'utf-8')
|
|
424
|
-
|
|
429
|
+
const content = await fs.readFile(subdirPath, 'utf-8')
|
|
430
|
+
return { name: skillName, content, filePath: subdirPath }
|
|
425
431
|
} catch {
|
|
426
432
|
// Fall back to flat file
|
|
427
433
|
try {
|
|
428
|
-
content = await fs.readFile(flatPath, 'utf-8')
|
|
429
|
-
|
|
434
|
+
const content = await fs.readFile(flatPath, 'utf-8')
|
|
435
|
+
return { name: skillName, content, filePath: flatPath }
|
|
430
436
|
} catch {
|
|
431
437
|
// Skill not found - not an error, just skip
|
|
432
|
-
|
|
438
|
+
return null
|
|
433
439
|
}
|
|
434
440
|
}
|
|
435
|
-
|
|
436
|
-
if (content) {
|
|
437
|
-
skills.push({
|
|
438
|
-
name: skillName,
|
|
439
|
-
content,
|
|
440
|
-
filePath: resolvedPath,
|
|
441
|
-
})
|
|
442
|
-
loadedSkillNames.add(skillName)
|
|
443
|
-
}
|
|
444
441
|
}
|
|
445
|
-
|
|
442
|
+
)
|
|
446
443
|
|
|
447
|
-
|
|
444
|
+
const results = await Promise.all(skillPromises)
|
|
445
|
+
return results.filter((skill): skill is LoadedSkill => skill !== null)
|
|
448
446
|
}
|
|
449
447
|
|
|
450
448
|
/**
|
|
@@ -94,22 +94,22 @@ class AgentLoader {
|
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
96
|
* Load all agents for the project
|
|
97
|
+
*
|
|
98
|
+
* Uses parallel file reads for performance (PRJ-110).
|
|
97
99
|
*/
|
|
98
100
|
async loadAllAgents(): Promise<Agent[]> {
|
|
99
101
|
try {
|
|
100
102
|
const files = await fs.readdir(this.agentsDir)
|
|
101
103
|
const agentFiles = files.filter((f) => f.endsWith('.md') && !f.startsWith('.'))
|
|
102
104
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
// Load all agents in parallel
|
|
106
|
+
const agentPromises = agentFiles.map((file) => {
|
|
105
107
|
const agentName = file.replace('.md', '')
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
agents.push(agent)
|
|
109
|
-
}
|
|
110
|
-
}
|
|
108
|
+
return this.loadAgent(agentName)
|
|
109
|
+
})
|
|
111
110
|
|
|
112
|
-
|
|
111
|
+
const results = await Promise.all(agentPromises)
|
|
112
|
+
return results.filter((agent): agent is Agent => agent !== null)
|
|
113
113
|
} catch (error) {
|
|
114
114
|
if (isNotFoundError(error)) {
|
|
115
115
|
return [] // Agents directory doesn't exist yet
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local State Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates .prjct-state.md in the project root for local persistence.
|
|
5
|
+
* This provides a quick reference to current task state without needing
|
|
6
|
+
* to access global storage.
|
|
7
|
+
*
|
|
8
|
+
* @see PRJ-112
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs/promises'
|
|
12
|
+
import path from 'node:path'
|
|
13
|
+
import type { StateJson } from '../schemas/state'
|
|
14
|
+
import { isNotFoundError } from '../types/fs'
|
|
15
|
+
|
|
16
|
+
const LOCAL_STATE_FILENAME = '.prjct-state.md'
|
|
17
|
+
|
|
18
|
+
// Extended runtime types (state.json has fields not in strict schema)
|
|
19
|
+
interface RuntimeTask {
|
|
20
|
+
description: string
|
|
21
|
+
startedAt: string
|
|
22
|
+
linearId?: string
|
|
23
|
+
branch?: string
|
|
24
|
+
status?: string
|
|
25
|
+
subtasks?: Array<{
|
|
26
|
+
description: string
|
|
27
|
+
status: string
|
|
28
|
+
}>
|
|
29
|
+
currentSubtaskIndex?: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface RuntimePreviousTask {
|
|
33
|
+
description: string
|
|
34
|
+
status: string
|
|
35
|
+
prUrl?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class LocalStateGenerator {
|
|
39
|
+
/**
|
|
40
|
+
* Generate .prjct-state.md in the project root
|
|
41
|
+
*/
|
|
42
|
+
async generate(projectPath: string, state: StateJson): Promise<void> {
|
|
43
|
+
const filePath = path.join(projectPath, LOCAL_STATE_FILENAME)
|
|
44
|
+
const content = this.toMarkdown(state)
|
|
45
|
+
await fs.writeFile(filePath, content, 'utf-8')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Remove local state file
|
|
50
|
+
*/
|
|
51
|
+
async remove(projectPath: string): Promise<void> {
|
|
52
|
+
const filePath = path.join(projectPath, LOCAL_STATE_FILENAME)
|
|
53
|
+
try {
|
|
54
|
+
await fs.unlink(filePath)
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (!isNotFoundError(error)) throw error
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if local state file exists
|
|
62
|
+
*/
|
|
63
|
+
async exists(projectPath: string): Promise<boolean> {
|
|
64
|
+
const filePath = path.join(projectPath, LOCAL_STATE_FILENAME)
|
|
65
|
+
try {
|
|
66
|
+
await fs.access(filePath)
|
|
67
|
+
return true
|
|
68
|
+
} catch {
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Convert state to markdown format
|
|
75
|
+
* Note: Uses runtime types since state.json has fields not in strict Zod schema
|
|
76
|
+
*/
|
|
77
|
+
private toMarkdown(state: StateJson): string {
|
|
78
|
+
const lines: string[] = [
|
|
79
|
+
'<!-- Auto-generated by prjct - DO NOT EDIT -->',
|
|
80
|
+
'<!-- This file provides local state persistence for AI tools -->',
|
|
81
|
+
'',
|
|
82
|
+
'# prjct State',
|
|
83
|
+
'',
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
if (state.currentTask) {
|
|
87
|
+
// Cast to runtime type (state.json has additional fields)
|
|
88
|
+
const task = state.currentTask as unknown as RuntimeTask
|
|
89
|
+
lines.push('## Current Task')
|
|
90
|
+
lines.push('')
|
|
91
|
+
lines.push(`**${task.description}**`)
|
|
92
|
+
lines.push('')
|
|
93
|
+
lines.push(`- Started: ${task.startedAt}`)
|
|
94
|
+
if (task.linearId) {
|
|
95
|
+
lines.push(`- Linear: ${task.linearId}`)
|
|
96
|
+
}
|
|
97
|
+
if (task.branch) {
|
|
98
|
+
lines.push(`- Branch: ${task.branch}`)
|
|
99
|
+
}
|
|
100
|
+
lines.push(`- Status: ${task.status || 'active'}`)
|
|
101
|
+
lines.push('')
|
|
102
|
+
|
|
103
|
+
// Subtasks
|
|
104
|
+
if (task.subtasks && task.subtasks.length > 0) {
|
|
105
|
+
lines.push('### Subtasks')
|
|
106
|
+
lines.push('')
|
|
107
|
+
|
|
108
|
+
task.subtasks.forEach((subtask, index) => {
|
|
109
|
+
const statusIcon =
|
|
110
|
+
subtask.status === 'completed' ? '✅' : subtask.status === 'in_progress' ? '▶️' : '⏳'
|
|
111
|
+
const isActive = index === task.currentSubtaskIndex ? ' ← **Active**' : ''
|
|
112
|
+
lines.push(`${index + 1}. ${statusIcon} ${subtask.description}${isActive}`)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
lines.push('')
|
|
116
|
+
|
|
117
|
+
// Progress
|
|
118
|
+
const completed = task.subtasks.filter((s) => s.status === 'completed').length
|
|
119
|
+
const total = task.subtasks.length
|
|
120
|
+
const percentage = Math.round((completed / total) * 100)
|
|
121
|
+
lines.push(`**Progress**: ${completed}/${total} (${percentage}%)`)
|
|
122
|
+
lines.push('')
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
lines.push('*No active task*')
|
|
126
|
+
lines.push('')
|
|
127
|
+
lines.push('Start a task with `p. task "description"`')
|
|
128
|
+
lines.push('')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Previous task info
|
|
132
|
+
if (state.previousTask) {
|
|
133
|
+
// Cast to runtime type
|
|
134
|
+
const prevTask = state.previousTask as unknown as RuntimePreviousTask
|
|
135
|
+
lines.push('---')
|
|
136
|
+
lines.push('')
|
|
137
|
+
lines.push('## Previous Task')
|
|
138
|
+
lines.push('')
|
|
139
|
+
lines.push(`**${prevTask.description}**`)
|
|
140
|
+
lines.push('')
|
|
141
|
+
lines.push(`- Status: ${prevTask.status}`)
|
|
142
|
+
if (prevTask.prUrl) {
|
|
143
|
+
lines.push(`- PR: ${prevTask.prUrl}`)
|
|
144
|
+
}
|
|
145
|
+
lines.push('')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Footer
|
|
149
|
+
lines.push('---')
|
|
150
|
+
lines.push(`*Last updated: ${state.lastUpdated || new Date().toISOString()}*`)
|
|
151
|
+
lines.push('')
|
|
152
|
+
|
|
153
|
+
return lines.join('\n')
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const localStateGenerator = new LocalStateGenerator()
|
|
158
|
+
export default localStateGenerator
|
|
@@ -33,6 +33,7 @@ import { metricsStorage } from '../storage/metrics-storage'
|
|
|
33
33
|
import dateHelper from '../utils/date-helper'
|
|
34
34
|
import { ContextFileGenerator } from './context-generator'
|
|
35
35
|
import type { SyncDiff } from './diff-generator'
|
|
36
|
+
import { localStateGenerator } from './local-state-generator'
|
|
36
37
|
import { type StackDetection, StackDetector } from './stack-detector'
|
|
37
38
|
|
|
38
39
|
const execAsync = promisify(exec)
|
|
@@ -826,6 +827,16 @@ You are the ${name} expert for this project. Apply best practices for the detect
|
|
|
826
827
|
}
|
|
827
828
|
|
|
828
829
|
await fs.writeFile(statePath, JSON.stringify(state, null, 2), 'utf-8')
|
|
830
|
+
|
|
831
|
+
// Also generate local .prjct-state.md (PRJ-112)
|
|
832
|
+
try {
|
|
833
|
+
await localStateGenerator.generate(
|
|
834
|
+
this.projectPath,
|
|
835
|
+
state as import('../schemas/state').StateJson
|
|
836
|
+
)
|
|
837
|
+
} catch {
|
|
838
|
+
// Silently fail - local state is optional
|
|
839
|
+
}
|
|
829
840
|
}
|
|
830
841
|
|
|
831
842
|
// ==========================================================================
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Manages current task state via storage/state.json
|
|
5
5
|
* Generates context/now.md for Claude
|
|
6
|
+
*
|
|
7
|
+
* Note: Local .prjct-state.md is generated by sync-service which has access
|
|
8
|
+
* to projectPath. This class only has projectId.
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
11
|
import { generateUUID } from '../schemas'
|