prjct-cli 0.60.1 → 0.60.2

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,40 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.60.2] - 2026-02-05
4
+
5
+ ### Performance
6
+
7
+ - parallelize agent/skill loading with Promise.all (PRJ-110) (#101)
8
+
9
+
10
+ ## [0.60.2] - 2026-02-05
11
+
12
+ ### Performance
13
+
14
+ - **Parallel agent/skill loading (PRJ-110)**: Agent and skill loading now uses `Promise.all` for parallel I/O
15
+
16
+ ### Implementation Details
17
+
18
+ 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.
19
+
20
+ ### Learnings
21
+
22
+ - Use `Promise.all(items.map(async (item) => ...))` for parallel async operations
23
+ - Return null for failed items, then filter - can't push to array in parallel
24
+ - Collect unique items first (deduplication), then parallelize reads
25
+
26
+ ### Test Plan
27
+
28
+ #### For QA
29
+ 1. Run `prjct sync --yes` - verify agents load successfully
30
+ 2. Run `p. task "test"` - verify orchestrator works
31
+ 3. Check no errors in agent/skill loading output
32
+
33
+ #### For Users
34
+ - Agent and skill loading is now faster (parallel I/O)
35
+ - No changes needed - improvement is automatic
36
+
37
+
3
38
  ## [0.60.1] - 2026-02-05
4
39
 
5
40
  ### 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
- for (const domain of domains) {
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
- agents.push({
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
- // Found one, no need to try other variations
369
- break
370
- } catch {}
368
+ }
369
+ } catch {
370
+ // Try next variation
371
+ }
371
372
  }
372
- }
373
+ return null
374
+ })
373
375
 
374
- return agents
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
- // Skip if already loaded
412
- if (loadedSkillNames.has(skillName)) continue
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
- resolvedPath = subdirPath
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
- resolvedPath = flatPath
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
- console.warn(`Skill not found: ${skillName}`)
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
- return skills
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
- const agents: Agent[] = []
104
- for (const file of agentFiles) {
105
+ // Load all agents in parallel
106
+ const agentPromises = agentFiles.map((file) => {
105
107
  const agentName = file.replace('.md', '')
106
- const agent = await this.loadAgent(agentName)
107
- if (agent) {
108
- agents.push(agent)
109
- }
110
- }
108
+ return this.loadAgent(agentName)
109
+ })
111
110
 
112
- return agents
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
@@ -11788,31 +11788,33 @@ var init_orchestrator_executor = __esm({
11788
11788
  *
11789
11789
  * Reads agent markdown files from {globalPath}/agents/
11790
11790
  * and extracts their content and skills from frontmatter.
11791
+ *
11792
+ * Uses parallel file reads for performance (PRJ-110).
11791
11793
  */
11792
11794
  async loadAgents(domains, projectId) {
11793
11795
  const globalPath = path_manager_default.getGlobalProjectPath(projectId);
11794
11796
  const agentsDir = path23.join(globalPath, "agents");
11795
- const agents = [];
11796
- for (const domain of domains) {
11797
+ const agentPromises = domains.map(async (domain) => {
11797
11798
  const possibleNames = [`${domain}.md`, `${domain}-agent.md`, `prjct-${domain}.md`];
11798
11799
  for (const fileName of possibleNames) {
11799
11800
  const filePath = path23.join(agentsDir, fileName);
11800
11801
  try {
11801
11802
  const content = await fs24.readFile(filePath, "utf-8");
11802
11803
  const { frontmatter, body } = this.parseAgentFile(content);
11803
- agents.push({
11804
+ return {
11804
11805
  name: fileName.replace(".md", ""),
11805
11806
  domain,
11806
11807
  content: body,
11807
11808
  skills: frontmatter.skills || [],
11808
11809
  filePath
11809
- });
11810
- break;
11810
+ };
11811
11811
  } catch {
11812
11812
  }
11813
11813
  }
11814
- }
11815
- return agents;
11814
+ return null;
11815
+ });
11816
+ const results = await Promise.all(agentPromises);
11817
+ return results.filter((agent) => agent !== null);
11816
11818
  }
11817
11819
  /**
11818
11820
  * Parse agent markdown file to extract frontmatter and body
@@ -11832,40 +11834,36 @@ var init_orchestrator_executor = __esm({
11832
11834
  * Load skills from agent frontmatter
11833
11835
  *
11834
11836
  * Skills are stored in ~/.claude/skills/{name}.md
11837
+ *
11838
+ * Uses parallel file reads for performance (PRJ-110).
11835
11839
  */
11836
11840
  async loadSkills(agents) {
11837
11841
  const skillsDir = path23.join(os8.homedir(), ".claude", "skills");
11838
- const skills = [];
11839
- const loadedSkillNames = /* @__PURE__ */ new Set();
11842
+ const uniqueSkillNames = /* @__PURE__ */ new Set();
11840
11843
  for (const agent of agents) {
11841
11844
  for (const skillName of agent.skills) {
11842
- if (loadedSkillNames.has(skillName)) continue;
11845
+ uniqueSkillNames.add(skillName);
11846
+ }
11847
+ }
11848
+ const skillPromises = Array.from(uniqueSkillNames).map(
11849
+ async (skillName) => {
11843
11850
  const flatPath = path23.join(skillsDir, `${skillName}.md`);
11844
11851
  const subdirPath = path23.join(skillsDir, skillName, "SKILL.md");
11845
- let content = null;
11846
- let resolvedPath = flatPath;
11847
11852
  try {
11848
- content = await fs24.readFile(subdirPath, "utf-8");
11849
- resolvedPath = subdirPath;
11853
+ const content = await fs24.readFile(subdirPath, "utf-8");
11854
+ return { name: skillName, content, filePath: subdirPath };
11850
11855
  } catch {
11851
11856
  try {
11852
- content = await fs24.readFile(flatPath, "utf-8");
11853
- resolvedPath = flatPath;
11857
+ const content = await fs24.readFile(flatPath, "utf-8");
11858
+ return { name: skillName, content, filePath: flatPath };
11854
11859
  } catch {
11855
- console.warn(`Skill not found: ${skillName}`);
11860
+ return null;
11856
11861
  }
11857
11862
  }
11858
- if (content) {
11859
- skills.push({
11860
- name: skillName,
11861
- content,
11862
- filePath: resolvedPath
11863
- });
11864
- loadedSkillNames.add(skillName);
11865
- }
11866
11863
  }
11867
- }
11868
- return skills;
11864
+ );
11865
+ const results = await Promise.all(skillPromises);
11866
+ return results.filter((skill) => skill !== null);
11869
11867
  }
11870
11868
  /**
11871
11869
  * Determine if task should be fragmented into subtasks
@@ -26412,7 +26410,7 @@ var require_package = __commonJS({
26412
26410
  "package.json"(exports, module) {
26413
26411
  module.exports = {
26414
26412
  name: "prjct-cli",
26415
- version: "0.60.1",
26413
+ version: "0.60.2",
26416
26414
  description: "Context layer for AI agents. Project context for Claude Code, Gemini CLI, and more.",
26417
26415
  main: "core/index.ts",
26418
26416
  bin: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prjct-cli",
3
- "version": "0.60.1",
3
+ "version": "0.60.2",
4
4
  "description": "Context layer for AI agents. Project context for Claude Code, Gemini CLI, and more.",
5
5
  "main": "core/index.ts",
6
6
  "bin": {