@kuznai/inception-engine 0.25.1 → 1.0.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.
@@ -8,11 +8,12 @@ import * as frontmatterAdapter from "./adapters/frontmatter.js";
8
8
  import { compileAdapterActions } from "./adapters/index.js";
9
9
  import { applyTomlMcpPatch } from "./adapters/toml.js";
10
10
  import { planCapabilityForDeploy } from "./capabilities.js";
11
+ import { mapConcurrentOrdered } from "./concurrency.js";
11
12
  import { applyMergePatch, computeUndoPatch, isPlainObject, } from "./merge-patch.js";
12
13
  import { defaultRegistryPersistence, lookupDeployment, RunRegistry, registerDeployment, registryDirPath, verifyDeployment, } from "./ownership.js";
13
14
  import { getDeployMethod, resolveAgentSkillPath } from "./resolve.js";
14
15
  import { resolveTargetTemplate } from "./runtime-paths.js";
15
- import { sourceAccessError, validateSkillDefinitionFile, validateSourceFile, validateSourcePath, } from "./validation.js";
16
+ import { createSourcePathValidator, sourceAccessError, validateSkillDefinitionFile, validateSourceFile, } from "./validation.js";
16
17
  async function readJsonConfigFile(filePath) {
17
18
  let rawContent;
18
19
  try {
@@ -232,7 +233,7 @@ function assertNoAntigravityPathCollisions(manifest) {
232
233
  return;
233
234
  throw new UserError("MANIFEST_INVALID", collisions.map((warning) => warning.message).join("; "));
234
235
  }
235
- async function planSkillDirActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home) {
236
+ async function planSkillDirActions(manifest, sourceDir, resolvedSourceDir, validateSource, detectedAgents, home) {
236
237
  const method = getDeployMethod();
237
238
  const actions = [];
238
239
  const warnings = [];
@@ -241,7 +242,7 @@ async function planSkillDirActions(manifest, sourceDir, resolvedSourceDir, realR
241
242
  if (targetAgents.length === 0)
242
243
  return;
243
244
  const source = path.resolve(sourceDir, skill.path);
244
- await validateSourcePath(source, skill.path, resolvedSourceDir, realRoot);
245
+ await validateSource(source, skill.path, resolvedSourceDir);
245
246
  await validateSkillContract(source, skill.path);
246
247
  for (const agentId of targetAgents) {
247
248
  const plan = planCapabilityForDeploy({
@@ -273,7 +274,7 @@ async function planSkillDirActions(manifest, sourceDir, resolvedSourceDir, realR
273
274
  await Promise.all(manifest.skills.map(planSkillEntry));
274
275
  return { actions, warnings };
275
276
  }
276
- async function planFileWriteActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home, repo, workspace) {
277
+ async function planFileWriteActions(manifest, sourceDir, resolvedSourceDir, validateSource, detectedAgents, home, repo, workspace) {
277
278
  const actions = [];
278
279
  await Promise.all((manifest.files ?? []).map(async (fileEntry) => {
279
280
  const targetAgents = fileEntry.agents.filter((agentId) => detectedAgents.includes(agentId));
@@ -281,7 +282,7 @@ async function planFileWriteActions(manifest, sourceDir, resolvedSourceDir, real
281
282
  return;
282
283
  assertApprovedManagedTargetTemplate(fileEntry.target, targetAgents, "files");
283
284
  const source = path.resolve(sourceDir, fileEntry.path);
284
- await validateSourcePath(source, fileEntry.path, resolvedSourceDir, realRoot);
285
+ await validateSource(source, fileEntry.path, resolvedSourceDir);
285
286
  await validateSourceFile(source, fileEntry.path);
286
287
  for (const agentId of targetAgents) {
287
288
  const agent = AGENT_REGISTRY_BY_ID[agentId];
@@ -334,18 +335,19 @@ export async function planDeploy(manifest, sourceDir, detectedAgents, home, repo
334
335
  catch {
335
336
  realRoot = resolvedSourceDir;
336
337
  }
338
+ const validateSource = createSourcePathValidator(realRoot);
337
339
  const repoDir = repo ?? realRoot;
338
- const skillPlan = await planSkillDirActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home);
340
+ const skillPlan = await planSkillDirActions(manifest, sourceDir, resolvedSourceDir, validateSource, detectedAgents, home);
339
341
  if (signal?.aborted)
340
342
  return { actions: [], warnings: [] };
341
343
  const actions = [
342
344
  ...skillPlan.actions,
343
- ...(await planFileWriteActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home, repoDir, workspace)),
345
+ ...(await planFileWriteActions(manifest, sourceDir, resolvedSourceDir, validateSource, detectedAgents, home, repoDir, workspace)),
344
346
  ...planConfigPatchActions(manifest, detectedAgents, home, repoDir, workspace),
345
347
  ];
346
348
  if (signal?.aborted)
347
349
  return { actions: [], warnings: [] };
348
- const adapterResult = await compileAdapterActions(manifest.mcpServers, manifest.agentRules, manifest.permissions ?? [], sourceDir, resolvedSourceDir, realRoot, detectedAgents, home, repoDir, manifest.agentDefinitions ?? [], workspace, manifest.hooks ?? [], manifest.executionConfigs ?? []);
350
+ const adapterResult = await compileAdapterActions(manifest.mcpServers, manifest.agentRules, manifest.permissions ?? [], sourceDir, resolvedSourceDir, validateSource, detectedAgents, home, repoDir, manifest.agentDefinitions ?? [], workspace, manifest.hooks ?? [], manifest.executionConfigs ?? []);
349
351
  actions.push(...adapterResult.actions);
350
352
  const warnings = [
351
353
  ...skillPlan.warnings,
@@ -360,55 +362,142 @@ export async function planDeploy(manifest, sourceDir, detectedAgents, home, repo
360
362
  return { actions, warnings };
361
363
  }
362
364
  export async function executeDeploy(actions, dryRun, verbose, home, deps = {}, signal) {
363
- let succeeded = 0;
364
- const failed = [];
365
365
  const planned = [];
366
366
  const runRegistry = new RunRegistry(deps.registry ?? defaultRegistryPersistence);
367
367
  const depsWithRegistry = {
368
368
  ...deps,
369
369
  registry: runRegistry,
370
370
  };
371
+ const preflightFailure = await preflightDeployRegistry(actions, dryRun, home, runRegistry);
372
+ if (preflightFailure) {
373
+ return preflightFailure;
374
+ }
375
+ const results = await executeDeployActions(actions, dryRun, verbose, home, depsWithRegistry, signal);
371
376
  if (!dryRun) {
372
- try {
373
- await runRegistry.preflight(home);
374
- }
375
- catch (err) {
376
- const message = err instanceof Error ? err.message : String(err);
377
- return {
378
- succeeded: 0,
379
- failed: actions.map((action) => ({ action, error: message })),
380
- planned,
381
- };
382
- }
377
+ await runRegistry.flush(home);
383
378
  }
384
- for (const action of actions) {
379
+ return summarizeDeployResults(actions, results, planned);
380
+ }
381
+ async function preflightDeployRegistry(actions, dryRun, home, runRegistry) {
382
+ if (dryRun)
383
+ return null;
384
+ try {
385
+ await runRegistry.preflight(home);
386
+ return null;
387
+ }
388
+ catch (err) {
389
+ const message = err instanceof Error ? err.message : String(err);
390
+ return {
391
+ succeeded: 0,
392
+ failed: actions.map((action) => ({ action, error: message })),
393
+ planned: [],
394
+ };
395
+ }
396
+ }
397
+ async function executeDeployActions(actions, dryRun, verbose, home, deps, signal) {
398
+ const results = new Array(actions.length);
399
+ if (dryRun) {
400
+ await executeDryRunDeployActions(results, actions, verbose, home, deps, signal);
401
+ return results;
402
+ }
403
+ const lanes = partitionDeployActionLanes(actions);
404
+ await mapConcurrentOrdered(lanes, async (lane) => {
405
+ for (const indexedAction of lane) {
406
+ if (signal?.aborted)
407
+ return;
408
+ results[indexedAction.index] = await dispatchDeployAction(indexedAction.action, false, verbose, home, deps);
409
+ }
410
+ }, { signal });
411
+ return results;
412
+ }
413
+ async function executeDryRunDeployActions(results, actions, verbose, home, deps, signal) {
414
+ for (const [index, action] of actions.entries()) {
385
415
  if (signal?.aborted)
386
416
  break;
387
- const result = await dispatchDeployAction(action, dryRun, verbose, home, planned, depsWithRegistry);
417
+ results[index] = await dispatchDeployAction(action, true, verbose, home, deps);
418
+ }
419
+ }
420
+ function summarizeDeployResults(actions, results, planned) {
421
+ let succeeded = 0;
422
+ const failed = [];
423
+ for (const [index, result] of results.entries()) {
424
+ if (!result)
425
+ continue;
426
+ emitDeployLogs(result.logs);
427
+ if (result.plannedChange) {
428
+ planned.push(result.plannedChange);
429
+ }
388
430
  if (result.error === null) {
389
- succeeded++;
431
+ succeeded += 1;
390
432
  }
391
433
  else {
392
- failed.push({ action, error: result.error });
434
+ failed.push({
435
+ action: actions[index],
436
+ error: result.error,
437
+ });
393
438
  }
394
439
  }
395
- if (!dryRun) {
396
- await runRegistry.flush(home);
397
- }
398
440
  return { succeeded, failed, planned };
399
441
  }
400
- async function dispatchDeployAction(action, dryRun, verbose, home, planned, deps) {
442
+ function emitDeployLogs(logs) {
443
+ for (const event of logs) {
444
+ switch (event.kind) {
445
+ case "ok":
446
+ logger.ok(event.label);
447
+ break;
448
+ case "fail":
449
+ logger.fail(event.label, event.message);
450
+ break;
451
+ case "detail":
452
+ logger.detail(event.message);
453
+ break;
454
+ default:
455
+ throw new Error(`Unhandled deploy log event: ${event.kind}`);
456
+ }
457
+ }
458
+ }
459
+ function buildSuccessResult(label, detail, plannedChange) {
460
+ const logs = [];
461
+ if (!plannedChange) {
462
+ logs.push({ kind: "ok", label });
463
+ if (detail) {
464
+ logs.push({ kind: "detail", message: detail });
465
+ }
466
+ }
467
+ return { error: null, logs, plannedChange };
468
+ }
469
+ function buildFailureResult(label, message) {
470
+ return {
471
+ error: message,
472
+ logs: [{ kind: "fail", label, message }],
473
+ };
474
+ }
475
+ function pushVerboseDetail(logs, verbose, message) {
476
+ if (verbose) {
477
+ logs.push({ kind: "detail", message });
478
+ }
479
+ }
480
+ function partitionDeployActionLanes(actions) {
481
+ const lanes = new Map();
482
+ for (const [index, action] of actions.entries()) {
483
+ const lane = lanes.get(action.target) ?? [];
484
+ lane.push({ action, index });
485
+ lanes.set(action.target, lane);
486
+ }
487
+ return Array.from(lanes.values());
488
+ }
489
+ async function dispatchDeployAction(action, dryRun, verbose, home, deps) {
401
490
  switch (action.kind) {
402
491
  case "skill-dir":
403
- return deploySkillDir(action, dryRun, verbose, home, planned, deps);
492
+ return deploySkillDir(action, dryRun, verbose, home, deps);
404
493
  case "file-write":
405
- return deployFileWrite(action, dryRun, verbose, home, planned, deps);
494
+ return deployFileWrite(action, dryRun, verbose, home, deps);
406
495
  case "config-patch":
407
- return deployConfigPatch(action, dryRun, verbose, home, planned, deps);
496
+ return deployConfigPatch(action, dryRun, verbose, home, deps);
408
497
  case "toml-patch":
409
- return deployTomlPatch(action, dryRun, verbose, home, planned, deps);
498
+ return deployTomlPatch(action, dryRun, verbose, home, deps);
410
499
  case "frontmatter-emit":
411
- return deployFrontmatterEmit(action, dryRun, verbose, home, planned, deps);
500
+ return deployFrontmatterEmit(action, dryRun, verbose, home, deps);
412
501
  default:
413
502
  throw new Error(`Unhandled deploy action kind: ${action.kind}`);
414
503
  }
@@ -520,18 +609,17 @@ async function replaceFileAtomically(targetPath, deps, stageTempFile, prepareBac
520
609
  await fileOps.rm(backupPath, { recursive: true, force: true });
521
610
  }
522
611
  }
523
- async function deploySkillDir(action, dryRun, verbose, home, planned, deps) {
612
+ async function deploySkillDir(action, dryRun, verbose, home, deps) {
524
613
  const label = `${action.skill} -> ${action.agent}`;
525
614
  try {
526
615
  await access(action.source);
527
616
  }
528
617
  catch (err) {
529
618
  const msg = sourceAccessError(err, action.source);
530
- logger.fail(label, msg);
531
- return { error: msg };
619
+ return buildFailureResult(label, msg);
532
620
  }
533
621
  if (dryRun) {
534
- planned.push({
622
+ return buildSuccessResult(label, undefined, {
535
623
  verb: action.method === "symlink" ? "create-symlink" : "copy-dir",
536
624
  kind: "skill-dir",
537
625
  skill: action.skill,
@@ -541,30 +629,27 @@ async function deploySkillDir(action, dryRun, verbose, home, planned, deps) {
541
629
  method: action.method,
542
630
  confidence: action.confidence,
543
631
  });
544
- return { error: null };
545
632
  }
546
633
  try {
547
- await executeDeployAction(action, verbose, home, deps);
548
- return { error: null };
634
+ const logs = await executeDeployAction(action, verbose, home, deps);
635
+ return { error: null, logs };
549
636
  }
550
637
  catch (err) {
551
638
  const msg = err instanceof Error ? err.message : String(err);
552
- logger.fail(label, msg);
553
- return { error: msg };
639
+ return buildFailureResult(label, msg);
554
640
  }
555
641
  }
556
- async function deployFileWrite(action, dryRun, verbose, home, planned, deps) {
642
+ async function deployFileWrite(action, dryRun, verbose, home, deps) {
557
643
  const label = `${action.skill} -> ${action.agent}`;
558
644
  try {
559
645
  await access(action.source);
560
646
  }
561
647
  catch (err) {
562
648
  const msg = sourceAccessError(err, action.source);
563
- logger.fail(label, msg);
564
- return { error: msg };
649
+ return buildFailureResult(label, msg);
565
650
  }
566
651
  if (dryRun) {
567
- planned.push({
652
+ return buildSuccessResult(label, undefined, {
568
653
  verb: "write-file",
569
654
  kind: "file-write",
570
655
  skill: action.skill,
@@ -572,7 +657,6 @@ async function deployFileWrite(action, dryRun, verbose, home, planned, deps) {
572
657
  source: action.source,
573
658
  target: action.target,
574
659
  });
575
- return { error: null };
576
660
  }
577
661
  try {
578
662
  await replaceFileAtomically(action.target, deps, (tempPath, fileOps) => fileOps.copyFile(action.source, tempPath), () => backupManagedFileWriteTarget(action, home, deps), () => registerDeployment(home, action.target, {
@@ -582,28 +666,22 @@ async function deployFileWrite(action, dryRun, verbose, home, planned, deps) {
582
666
  agent: action.agent,
583
667
  migratedFrom: action.migratedFrom,
584
668
  }, deps.registry));
585
- logger.ok(label);
586
- if (verbose) {
587
- logger.detail(`write-file: ${action.source} -> ${action.target}`);
588
- }
589
- return { error: null };
669
+ return buildSuccessResult(label, verbose ? `write-file: ${action.source} -> ${action.target}` : undefined);
590
670
  }
591
671
  catch (err) {
592
672
  const msg = err instanceof Error ? err.message : String(err);
593
- logger.fail(label, msg);
594
- return { error: msg };
673
+ return buildFailureResult(label, msg);
595
674
  }
596
675
  }
597
- async function deployConfigPatch(action, dryRun, verbose, home, planned, deps) {
676
+ async function deployConfigPatch(action, dryRun, verbose, home, deps) {
598
677
  const label = `${action.skill} -> ${action.agent}`;
599
678
  if (!isPlainObject(action.patch)) {
600
679
  const msg = `Config patch for skill "${action.skill}" must be a plain object`;
601
- logger.fail(label, msg);
602
- return { error: msg };
680
+ return buildFailureResult(label, msg);
603
681
  }
604
682
  const patch = action.patch;
605
683
  if (dryRun) {
606
- planned.push({
684
+ return buildSuccessResult(label, undefined, {
607
685
  verb: "patch-config",
608
686
  kind: "config-patch",
609
687
  skill: action.skill,
@@ -611,7 +689,6 @@ async function deployConfigPatch(action, dryRun, verbose, home, planned, deps) {
611
689
  target: action.target,
612
690
  patch,
613
691
  });
614
- return { error: null };
615
692
  }
616
693
  try {
617
694
  // Guard against double-patching by a different skill/agent
@@ -639,22 +716,19 @@ async function deployConfigPatch(action, dryRun, verbose, home, planned, deps) {
639
716
  skill: action.skill,
640
717
  agent: action.agent,
641
718
  }, deps.registry));
642
- logger.ok(label);
643
- if (verbose) {
644
- logger.detail(`patch-config: applied ${Object.keys(patch).length} key(s) to ${action.target}`);
645
- }
646
- return { error: null };
719
+ return buildSuccessResult(label, verbose
720
+ ? `patch-config: applied ${Object.keys(patch).length} key(s) to ${action.target}`
721
+ : undefined);
647
722
  }
648
723
  catch (err) {
649
724
  const msg = err instanceof Error ? err.message : String(err);
650
- logger.fail(label, msg);
651
- return { error: msg };
725
+ return buildFailureResult(label, msg);
652
726
  }
653
727
  }
654
- async function deployTomlPatch(action, dryRun, verbose, home, planned, deps) {
728
+ async function deployTomlPatch(action, dryRun, verbose, home, deps) {
655
729
  const label = `${action.skill} -> ${action.agent}`;
656
730
  if (dryRun) {
657
- planned.push({
731
+ return buildSuccessResult(label, undefined, {
658
732
  verb: "patch-toml",
659
733
  kind: "toml-patch",
660
734
  skill: action.skill,
@@ -662,7 +736,6 @@ async function deployTomlPatch(action, dryRun, verbose, home, planned, deps) {
662
736
  target: action.target,
663
737
  patch: action.config,
664
738
  });
665
- return { error: null };
666
739
  }
667
740
  try {
668
741
  const { previousValue } = await (deps.registry
@@ -677,27 +750,21 @@ async function deployTomlPatch(action, dryRun, verbose, home, planned, deps) {
677
750
  skill: action.skill,
678
751
  agent: action.agent,
679
752
  }, deps.registry);
680
- logger.ok(label);
681
- if (verbose) {
682
- logger.detail(`patch-toml: ${action.target}`);
683
- }
684
- return { error: null };
753
+ return buildSuccessResult(label, verbose ? `patch-toml: ${action.target}` : undefined);
685
754
  }
686
755
  catch (err) {
687
756
  const msg = err instanceof Error ? err.message : String(err);
688
- logger.fail(label, msg);
689
- return { error: msg };
757
+ return buildFailureResult(label, msg);
690
758
  }
691
759
  }
692
- async function deployFrontmatterEmit(action, dryRun, verbose, home, planned, deps) {
760
+ async function deployFrontmatterEmit(action, dryRun, verbose, home, deps) {
693
761
  const label = `${action.skill} -> ${action.agent}`;
694
762
  if (!isPlainObject(action.frontmatter)) {
695
763
  const msg = `Frontmatter patch for skill "${action.skill}" must be a plain object`;
696
- logger.fail(label, msg);
697
- return { error: msg };
764
+ return buildFailureResult(label, msg);
698
765
  }
699
766
  if (dryRun) {
700
- planned.push({
767
+ return buildSuccessResult(label, undefined, {
701
768
  verb: "emit-frontmatter",
702
769
  kind: "frontmatter-emit",
703
770
  skill: action.skill,
@@ -705,7 +772,6 @@ async function deployFrontmatterEmit(action, dryRun, verbose, home, planned, dep
705
772
  target: action.target,
706
773
  frontmatter: action.frontmatter,
707
774
  });
708
- return { error: null };
709
775
  }
710
776
  try {
711
777
  const existingEntry = await lookupDeployment(home, action.target, deps.registry);
@@ -742,16 +808,11 @@ async function deployFrontmatterEmit(action, dryRun, verbose, home, planned, dep
742
808
  skill: action.skill,
743
809
  agent: action.agent,
744
810
  }, deps.registry));
745
- logger.ok(label);
746
- if (verbose) {
747
- logger.detail(`emit-frontmatter: ${action.target}`);
748
- }
749
- return { error: null };
811
+ return buildSuccessResult(label, verbose ? `emit-frontmatter: ${action.target}` : undefined);
750
812
  }
751
813
  catch (err) {
752
814
  const msg = err instanceof Error ? err.message : String(err);
753
- logger.fail(label, msg);
754
- return { error: msg };
815
+ return buildFailureResult(label, msg);
755
816
  }
756
817
  }
757
818
  async function validateSkillContract(source, skillPath) {
@@ -816,13 +877,13 @@ async function createDeployTarget(action, home, deps) {
816
877
  }, deps.registry);
817
878
  }
818
879
  async function executeDeployAction(action, verbose, home, deps) {
819
- const label = `${action.skill} -> ${action.agent}`;
880
+ const logs = [];
820
881
  const backupPath = await backupExisting(action.target, verbose, home, {
821
882
  kind: action.kind,
822
883
  source: action.source,
823
884
  skill: action.skill,
824
885
  agent: action.agent,
825
- }, deps);
886
+ }, deps, logs);
826
887
  await mkdir(path.dirname(action.target), { recursive: true });
827
888
  try {
828
889
  await assertTargetAbsent(action.target);
@@ -847,12 +908,11 @@ async function executeDeployAction(action, verbose, home, deps) {
847
908
  if (backupPath) {
848
909
  await (deps.skillDirOps ?? defaultSkillDirOps).removeTarget(backupPath);
849
910
  }
850
- logger.ok(label);
851
- if (verbose) {
852
- logger.detail(`${action.method}: ${action.source} -> ${action.target}`);
853
- }
911
+ logs.push({ kind: "ok", label: `${action.skill} -> ${action.agent}` });
912
+ pushVerboseDetail(logs, verbose, `${action.method}: ${action.source} -> ${action.target}`);
913
+ return logs;
854
914
  }
855
- async function backupExisting(targetPath, verbose, home, expected, deps) {
915
+ async function backupExisting(targetPath, verbose, home, expected, deps, logs) {
856
916
  try {
857
917
  await lstat(targetPath);
858
918
  }
@@ -868,9 +928,7 @@ async function backupExisting(targetPath, verbose, home, expected, deps) {
868
928
  throw new UserError("DEPLOY_FAILED", `Target "${targetPath}" exists but is not managed by inception-engine - refusing to overwrite`);
869
929
  }
870
930
  const backupPath = `${targetPath}.inception-backup`;
871
- if (verbose) {
872
- logger.detail(`backing up existing target: ${targetPath}`);
873
- }
931
+ pushVerboseDetail(logs, verbose, `backing up existing target: ${targetPath}`);
874
932
  // Remove any stale backup from a previous failed attempt. Using rm with
875
933
  // { force: true } avoids a separate lstat existence check and handles
876
934
  // the case where the stale backup is a directory (which rename cannot
@@ -1,4 +1,5 @@
1
1
  import type { AgentId } from "../schemas/manifest.ts";
2
+ export declare function isInsideSkillDir(relPath: string, skillDirRelPaths: Set<string>): boolean;
2
3
  export interface InitOptions {
3
4
  directory: string;
4
5
  agents: AgentId[] | null;
@@ -1,4 +1,4 @@
1
- import { access, readdir, readFile } from "node:fs/promises";
1
+ import { access, opendir, readdir, readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { AGENT_REGISTRY } from "../config/agents.js";
4
4
  import { dryRunPrefix, logger } from "../logger.js";
@@ -7,6 +7,7 @@ import { parseFrontmatterDocument } from "./adapters/frontmatter.js";
7
7
  import { writeFileAtomic } from "./atomic-write.js";
8
8
  import { shouldInitIncludeAgent } from "./capabilities.js";
9
9
  const SAFE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
10
+ const SKILL_SCAN_CONCURRENCY = 8;
10
11
  // Ordered list: first match wins. Catch-all is applied at call site.
11
12
  // Derived from the registry: group agents by the filename of their global
12
13
  // agentRulesSupport path. Agents with requiresPrimary (e.g. github-copilot)
@@ -79,36 +80,81 @@ async function hasAntigravityMcpFrontmatter(absPath) {
79
80
  return (Object.hasOwn(attributes, "mcp-servers") ||
80
81
  Object.hasOwn(attributes, "mcpServers"));
81
82
  }
82
- async function findSkillDirs(baseDir, dir, found, signal) {
83
- if (signal?.aborted)
84
- return;
85
- let entries;
83
+ async function inspectSkillScanDir(dir) {
84
+ let handle = null;
86
85
  try {
87
- entries = await readdir(dir, { withFileTypes: true, encoding: "utf-8" });
86
+ handle = await opendir(dir, { encoding: "utf-8" });
88
87
  }
89
88
  catch {
90
- return;
89
+ return null;
91
90
  }
92
- const hasSkillMd = entries.some((e) => e.isFile() && e.name === "SKILL.md");
93
- if (hasSkillMd) {
94
- const relPath = path.relative(baseDir, dir).split(path.sep).join("/");
95
- const name = path.basename(dir);
96
- if (SAFE_NAME_RE.test(name)) {
97
- found.push({ relPath, name });
98
- }
99
- else {
100
- logger.warn("init", `Skipping "${relPath}": directory name "${name}" is not a valid skill name`);
91
+ const subdirs = [];
92
+ try {
93
+ while (true) {
94
+ const entry = await handle.read();
95
+ if (entry === null)
96
+ break;
97
+ if (entry.isFile() && entry.name === "SKILL.md") {
98
+ return { hasSkillMd: true, subdirs: [] };
99
+ }
100
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
101
+ subdirs.push(path.join(dir, entry.name));
102
+ }
101
103
  }
102
- // Don't recurse into a skill directory
104
+ }
105
+ finally {
106
+ await handle.close().catch(() => {
107
+ // Ignore double-close when returning early after finding SKILL.md.
108
+ });
109
+ }
110
+ return { hasSkillMd: false, subdirs };
111
+ }
112
+ function addFoundSkill(baseDir, dir, found) {
113
+ const relPath = path.relative(baseDir, dir).split(path.sep).join("/");
114
+ const name = path.basename(dir);
115
+ if (SAFE_NAME_RE.test(name)) {
116
+ found.push({ relPath, name });
103
117
  return;
104
118
  }
105
- for (const entry of entries) {
119
+ logger.warn("init", `Skipping "${relPath}": directory name "${name}" is not a valid skill name`);
120
+ }
121
+ function enqueueSubdirs(queue, subdirs, signal) {
122
+ for (const subdir of subdirs) {
106
123
  if (signal?.aborted)
107
- return;
108
- if (entry.isDirectory() && !entry.name.startsWith(".")) {
109
- await findSkillDirs(baseDir, path.join(dir, entry.name), found, signal);
124
+ return false;
125
+ queue.push(subdir);
126
+ }
127
+ return true;
128
+ }
129
+ async function processSkillScanDir(baseDir, dir, queue, found, signal) {
130
+ const scanned = await inspectSkillScanDir(dir);
131
+ if (scanned === null)
132
+ return true;
133
+ if (scanned.hasSkillMd) {
134
+ addFoundSkill(baseDir, dir, found);
135
+ return true;
136
+ }
137
+ return enqueueSubdirs(queue, scanned.subdirs, signal);
138
+ }
139
+ async function findSkillDirs(baseDir, startDir, signal) {
140
+ const found = [];
141
+ const queue = [startDir];
142
+ async function worker() {
143
+ while (queue.length > 0) {
144
+ if (signal?.aborted)
145
+ return;
146
+ const dir = queue.shift();
147
+ if (!dir)
148
+ return;
149
+ if (!(await processSkillScanDir(baseDir, dir, queue, found, signal))) {
150
+ return;
151
+ }
110
152
  }
111
153
  }
154
+ const workerCount = Math.min(SKILL_SCAN_CONCURRENCY, queue.length || 1);
155
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
156
+ found.sort((a, b) => a.relPath.localeCompare(b.relPath));
157
+ return found;
112
158
  }
113
159
  function resolveSkillName(relPath, name, namesSeen) {
114
160
  if (!namesSeen.has(name))
@@ -141,11 +187,17 @@ function defaultAgentsForFile(fileName, fallback) {
141
187
  }
142
188
  return fallback;
143
189
  }
144
- function isInsideSkillDir(relPath, skillDirRelPaths) {
145
- const parentRelPath = path.dirname(relPath).split(path.sep).join("/");
146
- if (skillDirRelPaths.has(parentRelPath))
147
- return true;
148
- return [...skillDirRelPaths].some((sp) => parentRelPath === sp || parentRelPath.startsWith(`${sp}/`));
190
+ export function isInsideSkillDir(relPath, skillDirRelPaths) {
191
+ let parentRelPath = path.posix.dirname(relPath.split(path.sep).join("/"));
192
+ while (parentRelPath !== "." && parentRelPath !== "/") {
193
+ if (skillDirRelPaths.has(parentRelPath))
194
+ return true;
195
+ const nextParent = path.posix.dirname(parentRelPath);
196
+ if (nextParent === parentRelPath)
197
+ break;
198
+ parentRelPath = nextParent;
199
+ }
200
+ return false;
149
201
  }
150
202
  function deriveAgentRulesName(relPath, fileName) {
151
203
  const ext = path.extname(fileName).toLowerCase();
@@ -634,8 +686,7 @@ export async function runInit(options) {
634
686
  logger.error(`Error: ${manifestPath} already exists. Use --force to overwrite.`);
635
687
  return 2;
636
688
  }
637
- const found = [];
638
- await findSkillDirs(directory, directory, found, signal);
689
+ const found = await findSkillDirs(directory, directory, signal);
639
690
  if (signal?.aborted)
640
691
  return 0;
641
692
  if (found.length === 0) {
@@ -2,6 +2,7 @@ import { readFile, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
4
4
  import { describeCapabilityConfidence, planCapabilityForDeploy, } from "./capabilities.js";
5
+ import { mapConcurrentOrdered } from "./concurrency.js";
5
6
  import { resolveRuntimePaths } from "./runtime-paths.js";
6
7
  const BUDGET_WARN_BYTES = 50 * 1024; // 50 KB
7
8
  async function detectEnterpriseManagement(agentId, home) {
@@ -314,10 +315,11 @@ async function collectAgentWarnings(agentId, manifest, home) {
314
315
  }
315
316
  export async function runPreflight(options, manifest, home, detectedAgents, signal) {
316
317
  const warnings = [];
317
- for (const agentId of detectedAgents) {
318
- if (signal?.aborted)
319
- return warnings;
320
- warnings.push(...(await collectAgentWarnings(agentId, manifest, home)));
318
+ const agentWarnings = await mapConcurrentOrdered(detectedAgents, (agentId) => collectAgentWarnings(agentId, manifest, home), { signal });
319
+ for (const warningSet of agentWarnings) {
320
+ if (!warningSet)
321
+ continue;
322
+ warnings.push(...warningSet);
321
323
  }
322
324
  if (signal?.aborted)
323
325
  return warnings;