@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.
- package/README.md +67 -3
- package/dist/src/core/adapters/agent-definitions.d.ts +2 -1
- package/dist/src/core/adapters/agent-definitions.js +13 -4
- package/dist/src/core/adapters/index.d.ts +2 -1
- package/dist/src/core/adapters/index.js +3 -3
- package/dist/src/core/adapters/rules.d.ts +2 -1
- package/dist/src/core/adapters/rules.js +12 -5
- package/dist/src/core/concurrency.d.ts +5 -0
- package/dist/src/core/concurrency.js +19 -0
- package/dist/src/core/deploy.js +157 -99
- package/dist/src/core/init.d.ts +1 -0
- package/dist/src/core/init.js +79 -28
- package/dist/src/core/preflight.js +6 -4
- package/dist/src/core/validation.d.ts +31 -3
- package/dist/src/core/validation.js +64 -10
- package/dist/src/schemas/manifest.js +1 -1
- package/dist/src/types.d.ts +1 -1
- package/dist/test/unit/adapters.test.js +84 -83
- package/dist/test/unit/cli.test.js +57 -1
- package/dist/test/unit/deploy.test.js +253 -0
- package/dist/test/unit/gemini-north-star.test.js +9 -8
- package/dist/test/unit/init.test.d.ts +1 -0
- package/dist/test/unit/init.test.js +14 -0
- package/dist/test/unit/manifest.test.js +25 -0
- package/dist/test/unit/preflight.test.js +29 -0
- package/dist/test/unit/validation.test.d.ts +1 -0
- package/dist/test/unit/validation.test.js +102 -0
- package/package.json +4 -4
package/dist/src/core/deploy.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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,
|
|
492
|
+
return deploySkillDir(action, dryRun, verbose, home, deps);
|
|
404
493
|
case "file-write":
|
|
405
|
-
return deployFileWrite(action, dryRun, verbose, home,
|
|
494
|
+
return deployFileWrite(action, dryRun, verbose, home, deps);
|
|
406
495
|
case "config-patch":
|
|
407
|
-
return deployConfigPatch(action, dryRun, verbose, home,
|
|
496
|
+
return deployConfigPatch(action, dryRun, verbose, home, deps);
|
|
408
497
|
case "toml-patch":
|
|
409
|
-
return deployTomlPatch(action, dryRun, verbose, home,
|
|
498
|
+
return deployTomlPatch(action, dryRun, verbose, home, deps);
|
|
410
499
|
case "frontmatter-emit":
|
|
411
|
-
return deployFrontmatterEmit(action, dryRun, verbose, home,
|
|
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,
|
|
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
|
-
|
|
531
|
-
return { error: msg };
|
|
619
|
+
return buildFailureResult(label, msg);
|
|
532
620
|
}
|
|
533
621
|
if (dryRun) {
|
|
534
|
-
|
|
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
|
-
|
|
553
|
-
return { error: msg };
|
|
639
|
+
return buildFailureResult(label, msg);
|
|
554
640
|
}
|
|
555
641
|
}
|
|
556
|
-
async function deployFileWrite(action, dryRun, verbose, home,
|
|
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
|
-
|
|
564
|
-
return { error: msg };
|
|
649
|
+
return buildFailureResult(label, msg);
|
|
565
650
|
}
|
|
566
651
|
if (dryRun) {
|
|
567
|
-
|
|
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
|
-
|
|
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
|
-
|
|
594
|
-
return { error: msg };
|
|
673
|
+
return buildFailureResult(label, msg);
|
|
595
674
|
}
|
|
596
675
|
}
|
|
597
|
-
async function deployConfigPatch(action, dryRun, verbose, home,
|
|
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
|
-
|
|
602
|
-
return { error: msg };
|
|
680
|
+
return buildFailureResult(label, msg);
|
|
603
681
|
}
|
|
604
682
|
const patch = action.patch;
|
|
605
683
|
if (dryRun) {
|
|
606
|
-
|
|
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
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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
|
-
|
|
651
|
-
return { error: msg };
|
|
725
|
+
return buildFailureResult(label, msg);
|
|
652
726
|
}
|
|
653
727
|
}
|
|
654
|
-
async function deployTomlPatch(action, dryRun, verbose, home,
|
|
728
|
+
async function deployTomlPatch(action, dryRun, verbose, home, deps) {
|
|
655
729
|
const label = `${action.skill} -> ${action.agent}`;
|
|
656
730
|
if (dryRun) {
|
|
657
|
-
|
|
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
|
-
|
|
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
|
-
|
|
689
|
-
return { error: msg };
|
|
757
|
+
return buildFailureResult(label, msg);
|
|
690
758
|
}
|
|
691
759
|
}
|
|
692
|
-
async function deployFrontmatterEmit(action, dryRun, verbose, home,
|
|
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
|
-
|
|
697
|
-
return { error: msg };
|
|
764
|
+
return buildFailureResult(label, msg);
|
|
698
765
|
}
|
|
699
766
|
if (dryRun) {
|
|
700
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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
|
-
|
|
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
|
package/dist/src/core/init.d.ts
CHANGED
package/dist/src/core/init.js
CHANGED
|
@@ -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
|
|
83
|
-
|
|
84
|
-
return;
|
|
85
|
-
let entries;
|
|
83
|
+
async function inspectSkillScanDir(dir) {
|
|
84
|
+
let handle = null;
|
|
86
85
|
try {
|
|
87
|
-
|
|
86
|
+
handle = await opendir(dir, { encoding: "utf-8" });
|
|
88
87
|
}
|
|
89
88
|
catch {
|
|
90
|
-
return;
|
|
89
|
+
return null;
|
|
91
90
|
}
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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;
|