@aiwg/cli 2026.9.6 → 2026.9.9

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.
Files changed (45) hide show
  1. package/dist/src/artifacts/index-builder.js +43 -1
  2. package/dist/src/artifacts/query-engine.js +7 -0
  3. package/dist/src/cli/handlers/help.js +7 -1
  4. package/dist/src/cli/handlers/installation.js +106 -2
  5. package/dist/src/cli/handlers/mc.js +100 -37
  6. package/dist/src/cli/handlers/ralph.js +14 -4
  7. package/dist/src/cli/handlers/refresh.js +359 -31
  8. package/dist/src/cli/handlers/repo-access.js +155 -4
  9. package/dist/src/cli/handlers/runtime-info.js +3 -0
  10. package/dist/src/cli/handlers/serve.js +21 -3
  11. package/dist/src/cli/handlers/setup.js +5 -5
  12. package/dist/src/cli/handlers/steward.js +30 -1
  13. package/dist/src/cli/handlers/use.js +123 -12
  14. package/dist/src/cli/handlers/utilities.js +26 -10
  15. package/dist/src/cli/handlers/version.js +40 -14
  16. package/dist/src/cli/handlers/workspace-context.js +8 -0
  17. package/dist/src/cli/services/deployment-verification.js +156 -7
  18. package/dist/src/cli/watch-service.js +47 -4
  19. package/dist/src/config/aiwg-config.js +95 -3
  20. package/dist/src/config/cli.js +16 -1
  21. package/dist/src/config/gitignore.js +5 -0
  22. package/dist/src/config/project-artifacts-health.mjs +15 -2
  23. package/dist/src/cost/fleet-report.js +19 -5
  24. package/dist/src/extensions/claude-hooks-installer.js +22 -6
  25. package/dist/src/extensions/project-local-doctor.js +40 -2
  26. package/dist/src/extensions/project-quickref.js +4 -0
  27. package/dist/src/installation/manager.mjs +38 -3
  28. package/dist/src/lint/runner.js +138 -0
  29. package/dist/src/mcp/helpers.mjs +56 -22
  30. package/dist/src/mcp/registry.js +32 -22
  31. package/dist/src/mcp/registry.mjs +31 -26
  32. package/dist/src/mcp/toml-editor.mjs +117 -0
  33. package/dist/src/mcp/tools/orchestration.mjs +7 -7
  34. package/dist/src/mcp/tools/subsystems.mjs +7 -7
  35. package/dist/src/memory/context-pack.js +5 -1
  36. package/dist/src/plugin/skill-command-translator.js +70 -1
  37. package/dist/src/serve/a2a-terminal-observer.js +19 -1
  38. package/dist/src/serve/mission-hitl.js +91 -0
  39. package/dist/src/sessions/import-lease.js +5 -1
  40. package/dist/src/smiths/context-pipeline/workspace-context.js +132 -6
  41. package/dist/src/testing/fixtures/test-data-factory.js +3 -3
  42. package/dist/src/writing/pattern-library.js +29 -6
  43. package/package.json +2 -1
  44. package/tools/agents/deploy-agents.mjs +91 -5
  45. package/tools/agents/providers/base.mjs +162 -6
@@ -199,7 +199,7 @@ export function addManagedMarker(content, version, source, style = 'markdown') {
199
199
  /**
200
200
  * Compute SHA-256 hash of content (hex string).
201
201
  */
202
- function contentHash(content) {
202
+ export function contentHash(content) {
203
203
  return createHash('sha256').update(content).digest('hex');
204
204
  }
205
205
 
@@ -240,9 +240,12 @@ export function updateSidecarManifest(dir, deployedEntries, opts) {
240
240
  const existing = readSidecarManifest(dir) || { managed: {} };
241
241
 
242
242
  for (const entry of deployedEntries) {
243
- const { filename, hash, frameworkSlug } = entry;
243
+ const { filename, hash, frameworkSlug, kind } = entry;
244
244
  const sidecarEntry = { hash: `sha256:${hash}`, source, version };
245
245
  if (frameworkSlug) sidecarEntry.frameworkSlug = frameworkSlug;
246
+ // `kind` marks artifacts whose lifecycle is governed elsewhere — currently
247
+ // only `skill-command` wrappers, which follow their source skill (#2507).
248
+ if (kind) sidecarEntry.kind = kind;
246
249
  existing.managed[filename] = sidecarEntry;
247
250
  }
248
251
 
@@ -473,6 +476,41 @@ export function injectPlatformInContent(content, targetPlatform) {
473
476
  * copies destined for such providers must drop the field entirely. An
474
477
  * absent field is the documented "compatible with all platforms" default.
475
478
  */
479
+ /**
480
+ * Remove the `triggers:` field from a deployed rule's frontmatter.
481
+ *
482
+ * Rules declare trigger phrases so `aiwg discover` can reach them by the
483
+ * question an agent asks rather than by their policy name (#2544). That is
484
+ * index-time metadata: no provider matches a *rule* by trigger, and the agent
485
+ * reading the deployed rule gains nothing from the list. Shipping it spends
486
+ * startup context on noise, which is exactly the budget #2540 is defending —
487
+ * ~2KB across the 16 rules covered today, and ~16KB if every rule adopts.
488
+ *
489
+ * Skills are untouched: several providers do match skills by trigger.
490
+ */
491
+ export function stripTriggersFromContent(content) {
492
+ const fmMatch = content.match(/^(---\r?\n)([\s\S]*?)(\r?\n---(?:\r?\n|$))([\s\S]*)$/);
493
+ if (!fmMatch) return content;
494
+
495
+ const [, open, fm, close, body] = fmMatch;
496
+ // Block list form:
497
+ // triggers:
498
+ // - "am I allowed to do this"
499
+ let updated = fm.replace(
500
+ /^triggers:[ \t]*\r?\n(?:[ \t]+-[ \t]+\S[^\r\n]*(?:\r?\n|$))*/m,
501
+ '',
502
+ );
503
+ // Inline form: triggers: ["a", "b"]
504
+ if (updated === fm) updated = fm.replace(/^triggers:[^\r\n]*(?:\r?\n|$)/m, '');
505
+
506
+ if (updated === fm) return content;
507
+ // An otherwise-empty frontmatter block is dropped rather than left as `---\n---`.
508
+ if (updated.trim().length === 0) return body.replace(/^\r?\n/, '');
509
+ // Removing a block mid-frontmatter can leave a trailing blank line before the
510
+ // closing fence; the deployed file should not carry it.
511
+ return open + updated.replace(/\s+$/, '') + close + body;
512
+ }
513
+
476
514
  export function stripPlatformsFromContent(content) {
477
515
  const fmMatch = content.match(/^(---\r?\n)([\s\S]*?)(\r?\n---(?:\r?\n|$))([\s\S]*)$/);
478
516
  if (!fmMatch) return content;
@@ -704,6 +742,13 @@ export function deployFiles(files, destDir, opts, transformFn) {
704
742
  const srcContent = fs.readFileSync(f, 'utf8');
705
743
  let transformedContent = transformFn ? transformFn(f, srcContent, opts) : srcContent;
706
744
 
745
+ // Rule triggers are index metadata, not something the reading agent needs.
746
+ // Keyed on the source path so every provider's rule deploy gets it without
747
+ // eight call sites opting in, and so skill triggers are never touched (#2544).
748
+ if (/(?:^|[\\/])rules[\\/][^\\/]+$/.test(f)) {
749
+ transformedContent = stripTriggersFromContent(transformedContent);
750
+ }
751
+
707
752
  // Inject target platform into agent .md files that use platforms: [all]
708
753
  if (injectPlatform && provider && /platforms:\s*\[all\]/.test(transformedContent)) {
709
754
  const platformName = PROVIDER_TO_PLATFORM[provider] || provider;
@@ -1342,6 +1387,39 @@ export function resolveAiwgRoot(srcRoot) {
1342
1387
  return null;
1343
1388
  }
1344
1389
 
1390
+ /**
1391
+ * Names of every skill AIWG ships, kernel and standard alike (#2511).
1392
+ *
1393
+ * `computeAllKernelNames` filters to kernel skills, but skill-command wrappers
1394
+ * are generated from both tiers, so retiring an orphaned wrapper needs the full
1395
+ * set. Anchored to the AIWG root rather than `srcRoot` for the same reason
1396
+ * `computeAllArtifactBasenames` is: a bundle-scoped deploy must not produce an
1397
+ * empty desired set and retire everything.
1398
+ *
1399
+ * @param {string} srcRoot AIWG repo / install root (or a subdir of it)
1400
+ * @returns {Set<string>|null} skill directory names, or null when no AIWG tree
1401
+ * is found — callers MUST then skip pruning.
1402
+ */
1403
+ export function computeAllSkillNames(srcRoot) {
1404
+ const aiwgRoot = resolveAiwgRoot(srcRoot);
1405
+ if (!aiwgRoot) return null;
1406
+
1407
+ const names = new Set();
1408
+ for (const group of ['frameworks', 'addons']) {
1409
+ const root = path.join(aiwgRoot, 'agentic', 'code', group);
1410
+ if (!fs.existsSync(root)) continue;
1411
+ for (const component of fs.readdirSync(root, { withFileTypes: true })) {
1412
+ if (!component.isDirectory()) continue;
1413
+ const skillsDir = path.join(root, component.name, 'skills');
1414
+ if (!fs.existsSync(skillsDir)) continue;
1415
+ for (const skill of fs.readdirSync(skillsDir, { withFileTypes: true })) {
1416
+ if (skill.isDirectory()) names.add(skill.name);
1417
+ }
1418
+ }
1419
+ }
1420
+ return names;
1421
+ }
1422
+
1345
1423
  /**
1346
1424
  * Holistic post-deploy prune of stale AIWG-managed flat artifacts
1347
1425
  * (agents / commands / rules). The flat-file analogue of
@@ -1371,6 +1449,15 @@ export function resolveAiwgRoot(srcRoot) {
1371
1449
  export function pruneStaleAiwgFiles(destDir, desiredStems, opts = {}) {
1372
1450
  const { dryRun = false, verbose = false } = opts;
1373
1451
  const artifactExtensions = opts.artifactExtensions || ['.md', '.mdc'];
1452
+ // When supplied, skill-command wrappers are retired against the set of skills
1453
+ // that still exist rather than exempted outright (#2511). `null`/absent keeps
1454
+ // the blanket exemption, so callers without a skill inventory cannot retire a
1455
+ // wrapper by accident.
1456
+ const skillCommandStems = opts.skillCommandStems instanceof Set
1457
+ ? opts.skillCommandStems
1458
+ : Array.isArray(opts.skillCommandStems)
1459
+ ? new Set(opts.skillCommandStems)
1460
+ : null;
1374
1461
  const removed = [];
1375
1462
  if (!destDir || !fs.existsSync(destDir)) return removed;
1376
1463
 
@@ -1397,6 +1484,15 @@ export function pruneStaleAiwgFiles(destDir, desiredStems, opts = {}) {
1397
1484
 
1398
1485
  if (desired.has(artifactStem(name))) continue;
1399
1486
 
1487
+ // Skill-command wrappers are named after skills, not command sources, so
1488
+ // they are absent from the command desired set by construction — pruning
1489
+ // them against it would delete wrappers the same deploy just wrote (#2507).
1490
+ // They are retired against the skill inventory instead, when one is given
1491
+ // (#2511); without one they stay exempt.
1492
+ if (managed[name]?.kind === 'skill-command') {
1493
+ if (!skillCommandStems || skillCommandStems.has(artifactStem(name))) continue;
1494
+ }
1495
+
1400
1496
  // Ownership gate — never delete a file AIWG didn't deploy.
1401
1497
  let owned = Object.prototype.hasOwnProperty.call(managed, name);
1402
1498
  if (!owned) {
@@ -2878,8 +2974,47 @@ export function cleanupOldRuleFiles(rulesDir, opts = {}) {
2878
2974
  * @param {boolean} opts.skipCommandsMigration - User opted out; warn about duplicates instead
2879
2975
  * @returns {boolean} true if any AIWG command file was removed (or would be in dry-run)
2880
2976
  */
2977
+ /**
2978
+ * Commands directories already warned about this process. The stale-command
2979
+ * condition belongs to the directory, not to each deployed framework/addon, so
2980
+ * `aiwg use all` must not repeat it once per unit (#2541).
2981
+ */
2982
+ const warnedCommandsDirs = new Set();
2983
+
2984
+ /**
2985
+ * AIWG-managed command filenames in a directory — sidecar entries or files
2986
+ * carrying the managed marker. Operator-authored commands and current
2987
+ * skill-command wrappers are excluded.
2988
+ */
2989
+ function listManagedCommandFiles(commandsDir) {
2990
+ let entries;
2991
+ try {
2992
+ entries = fs.readdirSync(commandsDir, { withFileTypes: true });
2993
+ } catch {
2994
+ return [];
2995
+ }
2996
+ const sidecar = readSidecarManifest(commandsDir) || { managed: {} };
2997
+ const managed = sidecar.managed || {};
2998
+ const names = [];
2999
+ for (const entry of entries) {
3000
+ if (!entry.isFile()) continue;
3001
+ if (!entry.name.toLowerCase().endsWith('.md')) continue;
3002
+ if (managed[entry.name]?.kind === 'skill-command') continue;
3003
+ let owned = Object.prototype.hasOwnProperty.call(managed, entry.name);
3004
+ if (!owned) {
3005
+ try {
3006
+ owned = MANAGED_MARKER_RE.test(fs.readFileSync(path.join(commandsDir, entry.name), 'utf8'));
3007
+ } catch {
3008
+ owned = false;
3009
+ }
3010
+ }
3011
+ if (owned) names.push(entry.name);
3012
+ }
3013
+ return names;
3014
+ }
3015
+
2881
3016
  export function migrateCommandsDirectory(commandsDir, opts = {}) {
2882
- const { dryRun = false, skipCommandsMigration = false, verbose = false } = opts;
3017
+ const { dryRun = false, skipCommandsMigration = false, verbose = false, warnOnSkip = true } = opts;
2883
3018
 
2884
3019
  if (!fs.existsSync(commandsDir)) return false;
2885
3020
 
@@ -2887,11 +3022,27 @@ export function migrateCommandsDirectory(commandsDir, opts = {}) {
2887
3022
  if (entries.length === 0) return false;
2888
3023
 
2889
3024
  if (skipCommandsMigration) {
3025
+ // Structural opt-outs (project-local addon bundles) skip the migration because
3026
+ // it does not apply to them, not because the operator declined it. Warning
3027
+ // there is noise, and it fired once per bundle (#2541).
3028
+ if (!warnOnSkip) return false;
3029
+
2890
3030
  const rel = path.relative(process.cwd(), commandsDir);
3031
+ // The condition is a property of the directory, not of each deployed unit;
3032
+ // emit it once per run no matter how many units pass through.
3033
+ if (warnedCommandsDirs.has(commandsDir)) return false;
3034
+ warnedCommandsDirs.add(commandsDir);
3035
+
3036
+ const stale = listManagedCommandFiles(commandsDir);
3037
+ if (stale.length === 0) return false;
3038
+
2891
3039
  console.warn(`\nWarning: commands migration skipped for ${rel}`);
2892
- console.warn(' Duplicate entries may appear in the command palette because old command');
2893
- console.warn(' files overlap with newly deployed skills. Remove AIWG command files manually');
2894
- console.warn(` to fix: rm ${rel}/<command>.md`);
3040
+ console.warn(' Duplicate entries may appear in the command palette because these old');
3041
+ console.warn(' AIWG command files overlap with newly deployed skills:');
3042
+ for (const name of stale) console.warn(` ${path.join(rel, name)}`);
3043
+ console.warn(' Resolve automatically by re-running without --skip-commands-migration,');
3044
+ console.warn(' or remove them directly:');
3045
+ console.warn(` rm ${stale.map((name) => path.join(rel, name)).join(' ')}`);
2895
3046
  return false;
2896
3047
  }
2897
3048
 
@@ -2907,6 +3058,11 @@ export function migrateCommandsDirectory(commandsDir, opts = {}) {
2907
3058
  const lower = entry.name.toLowerCase();
2908
3059
  if (!lower.endsWith('.md')) continue; // only command markdown files
2909
3060
  const filePath = path.join(commandsDir, entry.name);
3061
+ // Skill-command wrappers ARE the current skill surface, not legacy command
3062
+ // files superseded by it. Migrating them away deletes what the same deploy
3063
+ // just wrote — and on a kernel-only run, which does not re-translate, they
3064
+ // are never restored (#2507).
3065
+ if (managed[entry.name]?.kind === 'skill-command') continue;
2910
3066
  let owned = Object.prototype.hasOwnProperty.call(managed, entry.name);
2911
3067
  if (!owned) {
2912
3068
  try {