@aiwg/cli 2026.7.19 → 2026.7.21

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 (88) hide show
  1. package/README.md +397 -385
  2. package/dist/src/api/index.d.ts +1 -0
  3. package/dist/src/api/index.js +1 -0
  4. package/dist/src/artifacts/browser-export.js +7 -0
  5. package/dist/src/artifacts/citation-parser.js +96 -35
  6. package/dist/src/artifacts/cli.js +59 -5
  7. package/dist/src/artifacts/discover-facets.js +15 -0
  8. package/dist/src/artifacts/discovery-eval.js +290 -0
  9. package/dist/src/artifacts/fortemi-core-query-adapter.js +1 -1
  10. package/dist/src/artifacts/fortemi-shard-export.js +1 -1
  11. package/dist/src/artifacts/index-builder.js +54 -17
  12. package/dist/src/artifacts/query-engine.js +10 -6
  13. package/dist/src/artifacts/state-transfer.js +27 -0
  14. package/dist/src/artifacts/stats.js +8 -0
  15. package/dist/src/cli/cli-extension-loader.js +73 -0
  16. package/dist/src/cli/handlers/help.js +2 -1
  17. package/dist/src/cli/handlers/index.js +6 -2
  18. package/dist/src/cli/handlers/resource-versions.js +247 -0
  19. package/dist/src/cli/handlers/sessions.js +966 -0
  20. package/dist/src/cli/handlers/skill-lint.js +49 -45
  21. package/dist/src/cli/handlers/subcommands.js +55 -3
  22. package/dist/src/cli/handlers/use.js +154 -59
  23. package/dist/src/cli/handlers/utilities.js +49 -34
  24. package/dist/src/cli/skill-usage.js +146 -24
  25. package/dist/src/config/cli.js +13 -9
  26. package/dist/src/config/project-artifacts-runtime.mjs +68 -0
  27. package/dist/src/config/project-artifacts.js +1 -68
  28. package/dist/src/extensions/commands/definitions.js +65 -2
  29. package/dist/src/extensions/manifest.js +29 -0
  30. package/dist/src/extensions/project-local-discovery.js +86 -2
  31. package/dist/src/extensions/project-local-remove.js +52 -56
  32. package/dist/src/extensions/shadow-resolver.js +3 -1
  33. package/dist/src/plugins/standalone-packager.js +143 -0
  34. package/dist/src/resources/cache-cleanup.js +67 -0
  35. package/dist/src/resources/doctor.js +107 -0
  36. package/dist/src/resources/lockfile.js +125 -0
  37. package/dist/src/resources/resolver.js +133 -0
  38. package/dist/src/resources/web-release.d.ts +8 -0
  39. package/dist/src/resources/web-release.js +159 -1
  40. package/dist/src/sessions/adapters/claude.js +357 -0
  41. package/dist/src/sessions/adapters/codex.js +521 -0
  42. package/dist/src/sessions/adapters/copilot.js +226 -0
  43. package/dist/src/sessions/adapters/cursor.js +372 -0
  44. package/dist/src/sessions/adapters/factory.js +345 -0
  45. package/dist/src/sessions/adapters/generic.js +225 -0
  46. package/dist/src/sessions/adapters/hermes.js +341 -0
  47. package/dist/src/sessions/adapters/openclaw.js +381 -0
  48. package/dist/src/sessions/adapters/opencode.js +454 -0
  49. package/dist/src/sessions/adapters/openhuman.js +315 -0
  50. package/dist/src/sessions/adapters/warp.js +160 -0
  51. package/dist/src/sessions/adapters/windsurf.js +212 -0
  52. package/dist/src/sessions/candidates.js +210 -0
  53. package/dist/src/sessions/contracts.js +310 -0
  54. package/dist/src/sessions/discovery.js +51 -0
  55. package/dist/src/sessions/fixtures.js +12 -0
  56. package/dist/src/sessions/importer.js +315 -0
  57. package/dist/src/sessions/index.js +25 -0
  58. package/dist/src/sessions/knowledge-shard.js +61 -0
  59. package/dist/src/sessions/optional-backends.js +238 -0
  60. package/dist/src/sessions/policy.js +192 -0
  61. package/dist/src/sessions/ports.js +2 -0
  62. package/dist/src/sessions/promotion.js +367 -0
  63. package/dist/src/sessions/readers.js +176 -0
  64. package/dist/src/sessions/repository.js +1551 -0
  65. package/dist/src/skills/adapters/agent-skills.js +59 -0
  66. package/dist/src/skills/adapters/local.js +19 -1
  67. package/dist/src/skills/agent-skills.js +249 -0
  68. package/dist/src/skills/cli.js +463 -7
  69. package/dist/src/skills/deployer.js +554 -0
  70. package/dist/src/skills/doctor.js +105 -0
  71. package/dist/src/skills/exporter.js +382 -0
  72. package/dist/src/skills/importer.js +921 -0
  73. package/dist/src/skills/registry.js +19 -0
  74. package/dist/src/skills/validator.js +323 -0
  75. package/dist/src/smiths/context-pipeline/aiwg-md.js +5 -1
  76. package/dist/src/smiths/context-pipeline/claude-hook.js +21 -1
  77. package/dist/src/smiths/context-pipeline/finalization.js +5 -3
  78. package/dist/src/smiths/context-pipeline/generator.js +4 -1
  79. package/dist/src/smiths/context-pipeline/parallelism-section.js +34 -1
  80. package/dist/src/smiths/context-pipeline/workspace-context.js +15 -3
  81. package/dist/src/smiths/mcpsmith/example.js +3 -1
  82. package/dist/src/smiths/mcpsmith/generator.js +3 -1
  83. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +2 -1
  84. package/dist/src/storage/cli.js +3 -2
  85. package/dist/src/storage/subsystem-cli.js +7 -2
  86. package/dist/src/update/notifier.mjs +1 -1
  87. package/dist/src/update/service.mjs +123 -0
  88. package/package.json +3 -2
@@ -11,8 +11,8 @@
11
11
  */
12
12
  import * as fs from 'fs/promises';
13
13
  import * as path from 'path';
14
- import * as yaml from 'js-yaml';
15
14
  import { validateSkillFrontmatter } from '../../extensions/validation.js';
15
+ import { validateAgentSkillFile } from '../../skills/validator.js';
16
16
  const THRESHOLDS = {
17
17
  strict: 80,
18
18
  standard: 60,
@@ -36,7 +36,14 @@ function partialDimension(score, notes) {
36
36
  * because the cleanup work in #1015 made schema correctness a baseline
37
37
  * expectation across the corpus.
38
38
  */
39
- function scoreSchema(frontmatter) {
39
+ function scoreSchema(frontmatter, diagnostics) {
40
+ const conformanceErrors = diagnostics.filter((item) => item.severity === 'error');
41
+ if (conformanceErrors.length > 0) {
42
+ const notes = conformanceErrors.map((item) => (item.code === 'AS_YAML_PARSE'
43
+ ? `YAML parse error: ${item.message}`
44
+ : `${item.yamlPath}: ${item.message} [${item.code}]`));
45
+ return scoreDimension(false, notes);
46
+ }
40
47
  const result = validateSkillFrontmatter(frontmatter);
41
48
  if (result.success)
42
49
  return scoreDimension(true);
@@ -190,38 +197,12 @@ function inventoryCompanionCli(filePath, body) {
190
197
  * @param filePath - Absolute or relative path to the SKILL.md
191
198
  * @param rubric - Strictness level (drives the pass threshold)
192
199
  */
193
- export async function lintSkillFile(filePath, rubric = 'standard') {
194
- const content = await fs.readFile(filePath, 'utf-8');
195
- const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
196
- if (!fmMatch) {
197
- return {
198
- file: filePath,
199
- score: 0,
200
- passes: false,
201
- dimensions: {
202
- schema: scoreDimension(false, ['no YAML frontmatter found']),
203
- description: scoreDimension(false, ['no frontmatter to derive description from']),
204
- discoverability: scoreDimension(false, ['no frontmatter']),
205
- body: scoreBody(content),
206
- },
207
- };
208
- }
209
- const [, fmText, body = ''] = fmMatch;
210
- let frontmatter = {};
211
- let yamlError = null;
212
- try {
213
- const parsed = yaml.load(fmText);
214
- if (parsed && typeof parsed === 'object') {
215
- frontmatter = parsed;
216
- }
217
- }
218
- catch (e) {
219
- yamlError = e.message.split('\n')[0];
220
- }
200
+ export async function lintSkillFile(filePath, rubric = 'standard', profile = 'compatible') {
201
+ const validation = validateAgentSkillFile(filePath, { profile });
202
+ const frontmatter = validation.frontmatter ?? {};
203
+ const body = validation.body;
221
204
  const dimensions = {
222
- schema: yamlError
223
- ? scoreDimension(false, [`YAML parse error: ${yamlError}`])
224
- : scoreSchema(frontmatter),
205
+ schema: scoreSchema(frontmatter, validation.diagnostics),
225
206
  description: scoreDescription(frontmatter),
226
207
  discoverability: scoreDiscoverability(frontmatter),
227
208
  body: scoreBody(body),
@@ -230,7 +211,12 @@ export async function lintSkillFile(filePath, rubric = 'standard') {
230
211
  return {
231
212
  file: filePath,
232
213
  score,
233
- passes: score >= THRESHOLDS[rubric],
214
+ conformance: {
215
+ profile,
216
+ state: validation.state,
217
+ diagnostics: validation.diagnostics,
218
+ },
219
+ passes: validation.valid && score >= THRESHOLDS[rubric],
234
220
  dimensions,
235
221
  };
236
222
  }
@@ -243,7 +229,8 @@ async function* walkSkillFiles(rootPath) {
243
229
  }
244
230
  if (!stat.isDirectory())
245
231
  return;
246
- const entries = await fs.readdir(rootPath, { withFileTypes: true });
232
+ const entries = (await fs.readdir(rootPath, { withFileTypes: true }))
233
+ .sort((left, right) => left.name.localeCompare(right.name));
247
234
  for (const e of entries) {
248
235
  if (e.name.startsWith('.') || e.name === 'node_modules')
249
236
  continue;
@@ -265,7 +252,7 @@ async function* walkSkillFiles(rootPath) {
265
252
  * Pure function — does not print or exit. The CLI handler renders
266
253
  * output and translates the report into an exit code.
267
254
  */
268
- export async function lintSkills(targetPaths, rubric = 'standard') {
255
+ export async function lintSkills(targetPaths, rubric = 'standard', profile = 'compatible') {
269
256
  const targets = Array.isArray(targetPaths) ? targetPaths : [targetPaths];
270
257
  const seen = new Set();
271
258
  const files = [];
@@ -275,7 +262,7 @@ export async function lintSkills(targetPaths, rubric = 'standard') {
275
262
  if (seen.has(resolved))
276
263
  continue;
277
264
  seen.add(resolved);
278
- files.push(await lintSkillFile(f, rubric));
265
+ files.push(await lintSkillFile(f, rubric, profile));
279
266
  }
280
267
  }
281
268
  const total = files.reduce((sum, f) => sum + f.score, 0);
@@ -289,6 +276,7 @@ export async function lintSkills(targetPaths, rubric = 'standard') {
289
276
  }
290
277
  return {
291
278
  rubric,
279
+ profile,
292
280
  threshold: THRESHOLDS[rubric],
293
281
  files,
294
282
  averageScore: files.length > 0 ? Math.round(total / files.length) : 0,
@@ -303,6 +291,7 @@ export async function lintSkills(targetPaths, rubric = 'standard') {
303
291
  function parseArgs(args) {
304
292
  const targets = [];
305
293
  let rubric = 'standard';
294
+ let profile = 'compatible';
306
295
  let json = false;
307
296
  for (let i = 0; i < args.length; i++) {
308
297
  const a = args[i];
@@ -315,27 +304,42 @@ function parseArgs(args) {
315
304
  rubric = next;
316
305
  }
317
306
  }
307
+ else if (a === '--profile' && i + 1 < args.length) {
308
+ const next = args[++i];
309
+ if (next === 'strict' || next === 'compatible' || next === 'discovery') {
310
+ profile = next;
311
+ }
312
+ }
318
313
  else if (!a.startsWith('-')) {
319
314
  targets.push(a);
320
315
  }
321
316
  }
322
317
  if (targets.length === 0)
323
318
  targets.push('agentic/code');
324
- return { targets, rubric, json };
319
+ return { targets, rubric, profile, json };
325
320
  }
326
321
  function renderTextReport(report) {
327
- const { files, threshold, rubric, averageScore, failedCount } = report;
322
+ const { files, threshold, rubric, profile, averageScore, failedCount } = report;
328
323
  for (const f of files) {
329
- if (f.passes)
324
+ if (f.passes && f.conformance.diagnostics.length === 0)
330
325
  continue;
331
- console.log(`✗ ${f.file} (${f.score}/100)`);
326
+ const mark = !f.passes
327
+ ? '✗'
328
+ : f.conformance.diagnostics.some((item) => item.severity === 'warning')
329
+ ? '⚠'
330
+ : '✓';
331
+ console.log(`${mark} ${f.file} (${f.score}/100, conformance=${f.conformance.state})`);
332
+ for (const item of f.conformance.diagnostics) {
333
+ console.log(` ${item.severity} ${item.code} ${item.yamlPath}: ${item.message}; `
334
+ + `fix: ${item.remediation}`);
335
+ }
332
336
  for (const [name, dim] of Object.entries(f.dimensions)) {
333
- if (dim.score < 100) {
337
+ if (!f.passes && dim.score < 100) {
334
338
  console.log(` ${name} (${dim.score}): ${dim.notes.join('; ')}`);
335
339
  }
336
340
  }
337
341
  }
338
- console.log(`\nskill-lint (rubric=${rubric}, threshold=${threshold}):`);
342
+ console.log(`\nskill-lint (rubric=${rubric}, profile=${profile}, threshold=${threshold}):`);
339
343
  console.log(` ${files.length} file(s) scanned`);
340
344
  console.log(` ${failedCount} below threshold`);
341
345
  console.log(` average score: ${averageScore}/100`);
@@ -351,8 +355,8 @@ export const skillLintHandler = {
351
355
  category: 'utility',
352
356
  aliases: ['-skill-lint', '--skill-lint'],
353
357
  async execute(ctx) {
354
- const { targets, rubric, json } = parseArgs(ctx.args);
355
- const report = await lintSkills(targets, rubric);
358
+ const { targets, rubric, profile, json } = parseArgs(ctx.args);
359
+ const report = await lintSkills(targets, rubric, profile);
356
360
  if (json) {
357
361
  console.log(JSON.stringify(report, null, 2));
358
362
  }
@@ -1115,9 +1115,44 @@ export const packagePluginHandler = {
1115
1115
  category: "plugin",
1116
1116
  aliases: ["-package-plugin", "--package-plugin"],
1117
1117
  async execute(ctx) {
1118
+ if (ctx.args.includes("--help") || ctx.args.includes("-h")) {
1119
+ return {
1120
+ exitCode: 0,
1121
+ message: [
1122
+ "aiwg package-plugin — package a project-local or built-in marketplace wrapper",
1123
+ "",
1124
+ "Usage:",
1125
+ " aiwg package-plugin <name> [--source <path>] [--output <path>] [--provider <name>] [--clean] [--dry-run]",
1126
+ " aiwg package-plugin --plugin <name> [options] # compatibility form",
1127
+ "",
1128
+ "Options:",
1129
+ " --source <path> explicit project-local wrapper source (must stay inside the project)",
1130
+ " --output <path> standalone archive output (default: dist/plugins)",
1131
+ " --provider <name> claude, codex, or all for standalone wrappers; built-ins retain all formats",
1132
+ " --clean clean generated plugin output before packaging",
1133
+ " --dry-run, -n preview without writing",
1134
+ " --help, -h show this help",
1135
+ "",
1136
+ "Project-local wrappers are discovered under .aiwg/plugins and packaged as deterministic archives.",
1137
+ ].join("\n"),
1138
+ };
1139
+ }
1140
+ const hasExplicitPlugin = ctx.args.includes("--plugin") || ctx.args.includes("-p");
1141
+ const positional = ctx.args[0] && !ctx.args[0].startsWith("-")
1142
+ ? ctx.args[0]
1143
+ : undefined;
1144
+ if (!hasExplicitPlugin && !positional) {
1145
+ return {
1146
+ exitCode: 1,
1147
+ message: "Error: plugin name is required.\n\nRun `aiwg package-plugin --help` for usage.",
1148
+ };
1149
+ }
1150
+ const normalizedArgs = hasExplicitPlugin
1151
+ ? ctx.args
1152
+ : ["--plugin", positional, ...ctx.args.slice(1)];
1118
1153
  const frameworkRoot = await getFrameworkRoot();
1119
1154
  const runner = createScriptRunner(frameworkRoot);
1120
- return runner.run("tools/plugin/package-plugins.mjs", ctx.args, {
1155
+ return runner.run("tools/plugin/package-plugins.mjs", normalizedArgs, {
1121
1156
  cwd: ctx.cwd,
1122
1157
  });
1123
1158
  },
@@ -1134,6 +1169,23 @@ export const packageAllPluginsHandler = {
1134
1169
  category: "plugin",
1135
1170
  aliases: ["-package-all-plugins", "--package-all-plugins"],
1136
1171
  async execute(ctx) {
1172
+ if (ctx.args.includes("--help") || ctx.args.includes("-h")) {
1173
+ return {
1174
+ exitCode: 0,
1175
+ message: [
1176
+ "aiwg package-all-plugins — package every built-in marketplace wrapper",
1177
+ "",
1178
+ "Usage:",
1179
+ " aiwg package-all-plugins [--provider <name>] [--clean] [--dry-run]",
1180
+ "",
1181
+ "Options:",
1182
+ " --provider <name> claude, codex, cursor, factory, openclaw, or all",
1183
+ " --clean clean generated plugin output before packaging",
1184
+ " --dry-run, -n preview without writing",
1185
+ " --help, -h show this help",
1186
+ ].join("\n"),
1187
+ };
1188
+ }
1137
1189
  const frameworkRoot = await getFrameworkRoot();
1138
1190
  const runner = createScriptRunner(frameworkRoot);
1139
1191
  return runner.run("tools/plugin/package-plugins.mjs", ["--all", ...ctx.args], {
@@ -1199,7 +1251,7 @@ export const corpusHandler = {
1199
1251
  * with the project's general-purpose artifact graph indices.
1200
1252
  *
1201
1253
  * aiwg discover "<phrase>" [--limit N] [--type skill,agent,...] [--json]
1202
- * [--resource-source local|web|auto] [--aiwg-version <exact-or-channel>] [--offline]
1254
+ * [--resource-source local|web|auto] [--aiwg-version <version|range|digest|channel>] [--offline]
1203
1255
  */
1204
1256
  export const discoverHandler = {
1205
1257
  id: "discover",
@@ -1254,7 +1306,7 @@ export const featuresHandler = {
1254
1306
  * so consumers don't need to navigate AIWG's storage paths themselves.
1255
1307
  *
1256
1308
  * aiwg show <name> [--type skill,agent,...] [--json] [--first]
1257
- * [--resource-source local|web|auto] [--aiwg-version <exact-or-channel>] [--offline]
1309
+ * [--resource-source local|web|auto] [--aiwg-version <version|range|digest|channel>] [--offline]
1258
1310
  */
1259
1311
  export const showHandler = {
1260
1312
  id: "show",
@@ -17,7 +17,7 @@ import { createScriptRunner } from './script-runner.js';
17
17
  import { getFrameworkRoot, getVersionInfo } from '../../channel/manager.mjs';
18
18
  import { getRegistry } from '../../extensions/registry.js';
19
19
  import { registerDeployedExtensions } from '../../extensions/deployment-registration.js';
20
- import { registerCliCommands, registerHooks } from '../cli-extension-loader.js';
20
+ import { loadCliCommandsContribution, registerCliCommands, registerHooks, } from '../cli-extension-loader.js';
21
21
  import { translateSkillsToCommands, providerNeedsCommands } from '../../plugin/skill-command-translator.js';
22
22
  import * as ui from '../ui.js';
23
23
  import { readAiwgConfig, writeAiwgConfig, updateInstalled, hashManifest, emptyConfig, getProjectDir } from '../../config/aiwg-config.js';
@@ -270,6 +270,25 @@ export function addonPath(frameworkRoot, name) {
270
270
  const folderName = resolveAddonFolderName(name);
271
271
  return path.join(frameworkRoot, 'agentic/code/addons', folderName);
272
272
  }
273
+ async function registerSourceCliCommands(opts) {
274
+ const contribution = await loadCliCommandsContribution(opts.source);
275
+ if (!contribution)
276
+ return 0;
277
+ const { manifest, commandsSource } = contribution;
278
+ const count = Object.keys(manifest.subcommands).length;
279
+ if (opts.dryRun) {
280
+ ui.dim(` [dry-run] Would register CLI namespace '${manifest.namespace}' (${count} subcommands)`);
281
+ return count;
282
+ }
283
+ await registerCliCommands(opts.target, manifest.namespace, manifest.description || opts.fallbackDescription, commandsSource, manifest.subcommands);
284
+ ui.success(`CLI namespace '${manifest.namespace}' registered (${count} subcommands)`);
285
+ if (opts.provider === 'claude') {
286
+ const registeredHooks = await registerHooks(opts.target, manifest.namespace, manifest.subcommands);
287
+ for (const hook of registeredHooks)
288
+ ui.success(`Hook registered: ${hook}`);
289
+ }
290
+ return count;
291
+ }
273
292
  function getProviderPaths(provider) {
274
293
  const paths = getProviderArtifactPathStrings(provider) ?? getProviderArtifactPathStrings('claude');
275
294
  if (!paths)
@@ -775,48 +794,84 @@ async function countBundleSourceArtifacts(bundlePath) {
775
794
  */
776
795
  async function deployOneProjectLocalBundle(opts) {
777
796
  const { bundle, ctx, frameworkRoot, provider, target, dryRun, verbose, quiet, modelArgs } = opts;
778
- const runner = createScriptRunner(frameworkRoot);
779
- const args = [
780
- '--source', bundle.bundlePath,
781
- '--deploy-commands', '--deploy-skills', '--deploy-rules',
782
- '--provider', provider,
783
- '--target', target,
784
- // Project-local skills MUST land in the per-project skills tier
785
- // (#1228 follow-up). Default deploy mode after #1217 is no-copy +
786
- // index-driven discovery, but that model assumes upstream skills at
787
- // $AIWG_ROOT project-local bundles live under the project's .aiwg/
788
- // tree and aren't reachable via `aiwg discover` of the framework
789
- // graph. Without --copy-all, the bundle's rules deploy but its skills
790
- // never reach <provider>/.aiwg/skills/, leaving them invisible to
791
- // both the platform and the index.
792
- '--copy-all',
793
- ...modelArgs,
794
- ];
795
- if (dryRun)
796
- args.push('--dry-run');
797
- if (verbose)
798
- args.push('--verbose');
799
- if (quiet && !verbose)
800
- args.push('--quiet');
801
- // Project-local bundles are addon-shaped — never trigger the legacy commands
802
- // migration prompt (which is only relevant for full-framework deploys).
803
- args.push('--skip-commands-migration');
804
- const captureOpts = quiet && !verbose ? { capture: true } : {};
805
- // Inject AIWG_ROOT so the deploy subprocess can resolve the upstream AIWG
806
- // install root. The bundle's `--source` is its project-local path, so
807
- // `computeAllKernelNames`/`computeAllArtifactBasenames` (which walk up from
808
- // srcRoot looking for agentic/code/{frameworks,addons}) would otherwise fail
809
- // and prune the provider's kernel skill directory with an empty desired set
810
- // (#123). `frameworkRoot` is the AIWG install root that owns these trees.
811
- const result = await runner.run('tools/agents/deploy-agents.mjs', args, {
812
- ...captureOpts,
813
- env: { AIWG_ROOT: frameworkRoot },
814
- });
797
+ const counts = await countBundleSourceArtifacts(bundle.artifactPath);
798
+ const artifactTotal = counts.agents + counts.commands + counts.skills + counts.rules;
799
+ let cliCommandCount = 0;
800
+ try {
801
+ const contribution = await loadCliCommandsContribution(bundle.artifactPath);
802
+ cliCommandCount = contribution ? Object.keys(contribution.manifest.subcommands).length : 0;
803
+ }
804
+ catch (error) {
805
+ ui.warn(`Invalid CLI contribution for project-local '${bundle.id}': ${error.message}`);
806
+ return { exitCode: 1, counts };
807
+ }
808
+ if (verbose || dryRun) {
809
+ ui.dim(` Artifacts: agents=${counts.agents} commands=${counts.commands} skills=${counts.skills} rules=${counts.rules} cli=${cliCommandCount}`);
810
+ }
811
+ if (artifactTotal === 0 && cliCommandCount === 0) {
812
+ ui.warn(`Project-local ${bundle.type} '${bundle.id}' has no deployable agents, commands, skills, rules, or CLI commands at ${bundle.artifactPath}`);
813
+ return { exitCode: 1, counts };
814
+ }
815
+ let exitCode = 0;
816
+ if (artifactTotal > 0) {
817
+ const runner = createScriptRunner(frameworkRoot);
818
+ const args = [
819
+ '--source', bundle.artifactPath,
820
+ '--deploy-commands', '--deploy-skills', '--deploy-rules',
821
+ '--provider', provider,
822
+ '--target', target,
823
+ // Project-local skills MUST land in the per-project skills tier
824
+ // (#1228 follow-up). Default deploy mode after #1217 is no-copy +
825
+ // index-driven discovery, but that model assumes upstream skills at
826
+ // $AIWG_ROOT project-local bundles live under the project's .aiwg/
827
+ // tree and aren't reachable via `aiwg discover` of the framework
828
+ // graph. Without --copy-all, the bundle's rules deploy but its skills
829
+ // never reach <provider>/.aiwg/skills/, leaving them invisible to
830
+ // both the platform and the index.
831
+ '--copy-all',
832
+ ...modelArgs,
833
+ ];
834
+ if (dryRun)
835
+ args.push('--dry-run');
836
+ if (verbose)
837
+ args.push('--verbose');
838
+ if (quiet && !verbose)
839
+ args.push('--quiet');
840
+ // Project-local bundles are addon-shaped — never trigger the legacy commands
841
+ // migration prompt (which is only relevant for full-framework deploys).
842
+ args.push('--skip-commands-migration');
843
+ const captureOpts = quiet && !verbose ? { capture: true } : {};
844
+ // Inject AIWG_ROOT so the deploy subprocess can resolve the upstream AIWG
845
+ // install root. The bundle's `--source` is its project-local path, so
846
+ // `computeAllKernelNames`/`computeAllArtifactBasenames` (which walk up from
847
+ // srcRoot looking for agentic/code/{frameworks,addons}) would otherwise fail
848
+ // and prune the provider's kernel skill directory with an empty desired set
849
+ // (#123). `frameworkRoot` is the AIWG install root that owns these trees.
850
+ const result = await runner.run('tools/agents/deploy-agents.mjs', args, {
851
+ ...captureOpts,
852
+ env: { AIWG_ROOT: frameworkRoot },
853
+ });
854
+ exitCode = result.exitCode;
855
+ }
856
+ if (exitCode === 0 && cliCommandCount > 0) {
857
+ try {
858
+ await registerSourceCliCommands({
859
+ source: bundle.artifactPath,
860
+ target,
861
+ provider,
862
+ dryRun,
863
+ fallbackDescription: `${bundle.id} project-local commands`,
864
+ });
865
+ }
866
+ catch (error) {
867
+ ui.warn(`Failed to register CLI commands for project-local '${bundle.id}': ${error.message}`);
868
+ exitCode = 1;
869
+ }
870
+ }
815
871
  // Approximate counts from the bundle's source dirs (deploy-agents.mjs is
816
872
  // idempotent and copies file-for-file from these dirs)
817
- const counts = await countBundleSourceArtifacts(bundle.bundlePath);
818
873
  void ctx;
819
- return { exitCode: result.exitCode, counts };
874
+ return { exitCode, counts };
820
875
  }
821
876
  /**
822
877
  * Discover and deploy artifact-bearing project-local bundles from
@@ -903,6 +958,10 @@ async function deployProjectLocalBundles(opts) {
903
958
  if (verbose || dryRun) {
904
959
  const action = dryRun ? '[dry-run] Would deploy' : 'Deploying';
905
960
  console.log(`${action} project-local ${bundle.type} '${bundle.id}' from ${bundle.localPath} → ${provider}`);
961
+ if (bundle.artifactPath !== bundle.bundlePath) {
962
+ const payloadDisplay = path.relative(projectDir, bundle.artifactPath) || '.';
963
+ ui.dim(` Resolved plugin payload: ${payloadDisplay}`);
964
+ }
906
965
  }
907
966
  const result = await deployOneProjectLocalBundle({
908
967
  bundle, ctx, frameworkRoot, provider, target, dryRun, verbose, quiet, modelArgs,
@@ -941,7 +1000,7 @@ async function deployProjectLocalBundles(opts) {
941
1000
  const mHash = await hashManifest(manifestAbsPath);
942
1001
  // #1037 — record per-artifact source hashes so `aiwg remove` can
943
1002
  // detect pristine vs mutated vs replaced deployed files.
944
- const artifactHashes = await hashBundleArtifacts(bundle.bundlePath);
1003
+ const artifactHashes = await hashBundleArtifacts(bundle.artifactPath);
945
1004
  const updated = updateInstalled(config, bundle.id, provider, result.counts, {
946
1005
  version: bundle.manifest.version,
947
1006
  source: 'project-local',
@@ -1449,7 +1508,25 @@ async function deploySourceDirectory(opts) {
1449
1508
  if (opts.quiet)
1450
1509
  args.unshift('--quiet');
1451
1510
  const runner = createScriptRunner(opts.frameworkRoot);
1452
- return runner.run('tools/agents/deploy-agents.mjs', args, opts.quiet ? { capture: true } : {});
1511
+ const result = await runner.run('tools/agents/deploy-agents.mjs', args, opts.quiet ? { capture: true } : {});
1512
+ if (result.exitCode === 0) {
1513
+ try {
1514
+ await registerSourceCliCommands({
1515
+ source: opts.source,
1516
+ target: opts.target,
1517
+ provider: opts.provider,
1518
+ dryRun: opts.dryRun,
1519
+ fallbackDescription: `${path.basename(opts.source)} addon commands`,
1520
+ });
1521
+ }
1522
+ catch (error) {
1523
+ return {
1524
+ exitCode: 1,
1525
+ message: `Failed to register addon CLI commands: ${error.message}`,
1526
+ };
1527
+ }
1528
+ }
1529
+ return result;
1453
1530
  }
1454
1531
  /**
1455
1532
  * Use command handler
@@ -1928,25 +2005,19 @@ export class UseHandler {
1928
2005
  }
1929
2006
  // Register CLI commands if addon declares them
1930
2007
  try {
1931
- const manifestPath = path.join(addonSource, 'manifest.json');
1932
- const manifestContent = await fs.readFile(manifestPath, 'utf-8');
1933
- const manifest = JSON.parse(manifestContent);
1934
- if (manifest.cli_commands?.namespace && manifest.cli_commands?.subcommands) {
1935
- const cmds = manifest.cli_commands;
1936
- const commandsSource = path.join(addonSource, cmds.entry || 'commands/');
1937
- await registerCliCommands(target, cmds.namespace, cmds.description || `${framework} addon commands`, commandsSource, cmds.subcommands);
1938
- ui.success(`CLI namespace '${cmds.namespace}' registered (${Object.keys(cmds.subcommands).length} subcommands)`);
1939
- // Register Claude Code hooks for subcommands with hook_event
1940
- if (provider === 'claude') {
1941
- const registeredHooks = await registerHooks(target, cmds.namespace, cmds.subcommands);
1942
- for (const hook of registeredHooks) {
1943
- ui.success(`Hook registered: ${hook}`);
1944
- }
1945
- }
1946
- }
2008
+ await registerSourceCliCommands({
2009
+ source: addonSource,
2010
+ target,
2011
+ provider,
2012
+ dryRun: false,
2013
+ fallbackDescription: `${framework} addon commands`,
2014
+ });
1947
2015
  }
1948
2016
  catch (error) {
1949
- ui.warn(`Failed to register CLI commands: ${error instanceof Error ? error.message : String(error)}`);
2017
+ return {
2018
+ exitCode: 1,
2019
+ message: `Failed to register CLI commands: ${error instanceof Error ? error.message : String(error)}`,
2020
+ };
1950
2021
  }
1951
2022
  // Profile picker for addons with memory topology and multiple templates
1952
2023
  try {
@@ -2235,6 +2306,18 @@ export class UseHandler {
2235
2306
  if (result.exitCode !== 0) {
2236
2307
  return result;
2237
2308
  }
2309
+ try {
2310
+ await registerSourceCliCommands({
2311
+ source,
2312
+ target,
2313
+ provider,
2314
+ dryRun,
2315
+ fallbackDescription: `${addon} addon commands`,
2316
+ });
2317
+ }
2318
+ catch (error) {
2319
+ ui.warn(`Failed to register CLI commands for '${addon}': ${error.message}`);
2320
+ }
2238
2321
  }
2239
2322
  // Deploy all extensions from agentic/code/extensions/* (#1222).
2240
2323
  // Extensions are addon-shaped bundles (manifest type: "addon") that live
@@ -2255,6 +2338,18 @@ export class UseHandler {
2255
2338
  if (result.exitCode !== 0) {
2256
2339
  return result;
2257
2340
  }
2341
+ try {
2342
+ await registerSourceCliCommands({
2343
+ source,
2344
+ target,
2345
+ provider,
2346
+ dryRun,
2347
+ fallbackDescription: `${ext} extension commands`,
2348
+ });
2349
+ }
2350
+ catch (error) {
2351
+ ui.warn(`Failed to register CLI commands for '${ext}': ${error.message}`);
2352
+ }
2258
2353
  }
2259
2354
  }
2260
2355
  // Deploy project-local bundles (#1035). Auto-runs after upstream addons unless