@aiwg/cli 2026.8.17 → 2026.8.19

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 (50) hide show
  1. package/bin/aiwg.mjs +24 -2
  2. package/dist/src/a2a/agent-card.js +4 -1
  3. package/dist/src/a2a/client.js +148 -68
  4. package/dist/src/a2a/codecs.js +480 -0
  5. package/dist/src/a2a/events.js +226 -0
  6. package/dist/src/a2a/hitl-driver.js +8 -6
  7. package/dist/src/a2a/hitl.js +2 -1
  8. package/dist/src/a2a/http.js +85 -5
  9. package/dist/src/a2a/protocol.js +136 -0
  10. package/dist/src/a2a/types.js +4 -14
  11. package/dist/src/a2a/webhook.js +101 -4
  12. package/dist/src/artifacts/index-builder.js +63 -12
  13. package/dist/src/artifacts/index-files.js +26 -5
  14. package/dist/src/artifacts/query-engine.js +67 -67
  15. package/dist/src/artifacts/stats.js +6 -2
  16. package/dist/src/artifacts/types.js +1 -1
  17. package/dist/src/audit/operator-decision.js +15 -1
  18. package/dist/src/channel/manager.mjs +89 -17
  19. package/dist/src/cli/handlers/index.js +3 -1
  20. package/dist/src/cli/handlers/installation.js +79 -0
  21. package/dist/src/cli/handlers/refresh.js +6 -2
  22. package/dist/src/cli/handlers/runtime-info.js +9 -1
  23. package/dist/src/cli/handlers/serve.js +107 -4
  24. package/dist/src/cli/handlers/session.js +12 -26
  25. package/dist/src/cli/handlers/use.js +19 -4
  26. package/dist/src/cli/handlers/utilities.js +4 -0
  27. package/dist/src/cli/handlers/version.js +4 -0
  28. package/dist/src/config/user-config-dir.mjs +29 -0
  29. package/dist/src/config/user-config.js +4 -22
  30. package/dist/src/extensions/commands/definitions.js +20 -1
  31. package/dist/src/features/catalog.js +2 -1
  32. package/dist/src/flow/graph-metadata.js +56 -0
  33. package/dist/src/installation/manager-command.mjs +31 -0
  34. package/dist/src/installation/manager.mjs +264 -0
  35. package/dist/src/serve/a2a-terminal-observer.js +28 -5
  36. package/dist/src/serve/dispatch-router.js +32 -4
  37. package/dist/src/serve/executor-registry.js +29 -0
  38. package/dist/src/serve/mission-conductor.js +15 -1
  39. package/dist/src/serve/stack-adapters.js +2 -1
  40. package/dist/src/serve/telemetry.js +5 -1
  41. package/dist/src/smiths/context-pipeline/claude-hook.js +8 -5
  42. package/dist/src/smiths/context-pipeline/line-endings.js +12 -0
  43. package/dist/src/smiths/context-pipeline/managed-hook.js +8 -5
  44. package/dist/src/smiths/context-pipeline/workspace-context.js +3 -1
  45. package/dist/src/update/checker.mjs +16 -15
  46. package/dist/src/update/notifier.mjs +8 -3
  47. package/dist/src/update/service.mjs +51 -5
  48. package/package.json +1 -1
  49. package/tools/agents/deploy-agents.mjs +28 -8
  50. package/tools/agents/providers/base.mjs +5 -3
@@ -14,6 +14,7 @@ import { buildNormalizedAiwgMd } from './finalization.js';
14
14
  import { getProviderDefinition, listProviderDefinitions, } from '../../providers/provider-definitions.js';
15
15
  import { readAiwgConfig } from '../../config/aiwg-config.js';
16
16
  import { projectAiwgPath, projectControlPath, resolveProjectAiwgDir, } from '../../config/project-artifacts.js';
17
+ import { dominantLineEnding, withLineEnding } from './line-endings.js';
17
18
  export const WORKSPACE_MANAGED_START = '<!-- AIWG:workspace-context:start -->';
18
19
  export const WORKSPACE_MANAGED_END = '<!-- AIWG:workspace-context:end -->';
19
20
  export const WORKSPACE_OPERATOR_START = '<!-- AIWG:workspace-operator:start -->';
@@ -90,7 +91,8 @@ function replaceBlock(content, start, end, block) {
90
91
  return null;
91
92
  if (startIndex < 0 || endIndex < startIndex)
92
93
  throw new Error(`Malformed managed block: ${start} / ${end}`);
93
- return content.slice(0, startIndex) + block + content.slice(endIndex + end.length);
94
+ const renderedBlock = withLineEnding(block, dominantLineEnding(content));
95
+ return content.slice(0, startIndex) + renderedBlock + content.slice(endIndex + end.length);
94
96
  }
95
97
  function stripGeneratedBlocks(content) {
96
98
  let stripped = content;
@@ -15,6 +15,7 @@ import https from 'https';
15
15
  import { execSync } from 'child_process';
16
16
  import { createInterface } from 'readline';
17
17
  import { loadConfig, saveConfig, getChannel, getPackageRoot } from '../channel/manager.mjs';
18
+ import { updateInstallation } from './service.mjs';
18
19
 
19
20
  const NPM_REGISTRY = 'https://registry.npmjs.org/aiwg';
20
21
 
@@ -212,13 +213,13 @@ async function checkStableUpdates(config) {
212
213
  console.log('');
213
214
  console.log('Updating aiwg...');
214
215
  try {
215
- execSync('npm install -g aiwg@latest', { stdio: 'inherit' });
216
+ await updateInstallation({ config, channel: 'stable' });
216
217
  console.log('Update complete! Please restart your terminal.');
217
218
  } catch (error) {
218
- console.error('Update failed. Run manually: npm install -g aiwg@latest');
219
+ console.error('Update failed. Run manually: aiwg update');
219
220
  }
220
221
  } else {
221
- console.log('Update skipped. Run `npm install -g aiwg@latest` when ready.');
222
+ console.log('Update skipped. Run `aiwg update` when ready.');
222
223
  }
223
224
  console.log('');
224
225
  }
@@ -251,13 +252,13 @@ async function checkNextUpdates(config) {
251
252
  console.log('');
252
253
  console.log('Updating aiwg@next...');
253
254
  try {
254
- execSync('npm install -g aiwg@next', { stdio: 'inherit' });
255
+ await updateInstallation({ config, channel: 'next' });
255
256
  console.log('Update complete! Please restart your terminal.');
256
257
  } catch {
257
- console.error('Update failed. Run manually: npm install -g aiwg@next');
258
+ console.error('Update failed. Run manually: aiwg update');
258
259
  }
259
260
  } else {
260
- console.log('Update skipped. Run `npm install -g aiwg@next` when ready.');
261
+ console.log('Update skipped. Run `aiwg update` when ready.');
261
262
  }
262
263
  console.log('');
263
264
  }
@@ -290,13 +291,13 @@ async function checkNightlyUpdates(config) {
290
291
  console.log('');
291
292
  console.log('Updating aiwg@nightly...');
292
293
  try {
293
- execSync('npm install -g aiwg@nightly', { stdio: 'inherit' });
294
+ await updateInstallation({ config, channel: 'nightly' });
294
295
  console.log('Update complete! Please restart your terminal.');
295
296
  } catch {
296
- console.error('Update failed. Run manually: npm install -g aiwg@nightly');
297
+ console.error('Update failed. Run manually: aiwg update');
297
298
  }
298
299
  } else {
299
- console.log('Update skipped. Run `npm install -g aiwg@nightly` when ready.');
300
+ console.log('Update skipped. Run `aiwg update` when ready.');
300
301
  }
301
302
  console.log('');
302
303
  }
@@ -348,13 +349,13 @@ export async function forceUpdateCheck() {
348
349
  console.log('Checking for updates on next channel...');
349
350
  const latestVersion = await fetchNpmDistTag('next');
350
351
  if (!latestVersion) {
351
- console.log('Could not check npm registry. Try: npm install -g aiwg@next');
352
+ console.log('Could not check npm registry. Try: aiwg update');
352
353
  return;
353
354
  }
354
355
  if (currentVersion !== latestVersion) {
355
356
  console.log(`Update available: ${currentVersion} → ${latestVersion}`);
356
357
  console.log('');
357
- console.log('Run: npm install -g aiwg@next');
358
+ console.log('Run: aiwg update');
358
359
  } else {
359
360
  console.log(`You are on the latest next release: ${currentVersion}`);
360
361
  }
@@ -362,13 +363,13 @@ export async function forceUpdateCheck() {
362
363
  console.log('Checking for updates on nightly channel...');
363
364
  const latestVersion = await fetchNpmDistTag('nightly');
364
365
  if (!latestVersion) {
365
- console.log('Could not check npm registry. Try: npm install -g aiwg@nightly');
366
+ console.log('Could not check npm registry. Try: aiwg update');
366
367
  return;
367
368
  }
368
369
  if (currentVersion !== latestVersion) {
369
370
  console.log(`Update available: ${currentVersion} → ${latestVersion}`);
370
371
  console.log('');
371
- console.log('Run: npm install -g aiwg@nightly');
372
+ console.log('Run: aiwg update');
372
373
  } else {
373
374
  console.log(`You are on the latest nightly snapshot: ${currentVersion}`);
374
375
  }
@@ -381,14 +382,14 @@ export async function forceUpdateCheck() {
381
382
  const latestVersion = await fetchLatestNpmVersion();
382
383
 
383
384
  if (!latestVersion) {
384
- console.log('Could not check npm registry. Try: npm install -g aiwg@latest');
385
+ console.log('Could not check npm registry. Try: aiwg update');
385
386
  return;
386
387
  }
387
388
 
388
389
  if (isNewerVersion(currentVersion, latestVersion)) {
389
390
  console.log(`Update available: ${currentVersion} → ${latestVersion}`);
390
391
  console.log('');
391
- console.log('Run: npm install -g aiwg@latest');
392
+ console.log('Run: aiwg update');
392
393
  } else {
393
394
  console.log(`You are on the latest version: ${currentVersion}`);
394
395
  }
@@ -27,12 +27,13 @@
27
27
 
28
28
  import https from 'https';
29
29
  import path from 'path';
30
- import os from 'os';
31
30
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
32
31
  import { spawn } from 'child_process';
33
32
  import { fileURLToPath } from 'url';
33
+ import { resolveUserConfigDir } from '../config/user-config-dir.mjs';
34
+ import { loadInstallationIdentity } from '../installation/manager.mjs';
34
35
 
35
- const CACHE_DIR = path.join(os.homedir(), '.aiwg');
36
+ const CACHE_DIR = resolveUserConfigDir();
36
37
  const CACHE_FILE = path.join(CACHE_DIR, 'update-notifier.json');
37
38
 
38
39
  /**
@@ -191,11 +192,13 @@ async function runCheck(packageRoot) {
191
192
  */
192
193
  export function scheduleBackgroundCheck(packageRoot) {
193
194
  if (isDisabled()) return;
195
+ const installation = loadInstallationIdentity({ actualRoot: packageRoot, createIfMissing: false });
196
+ if (installation?.checkOnStartup === false) return;
194
197
 
195
198
  const cache = readCache();
196
199
  if (cacheMatchesPackage(cache, packageRoot) && cache?.lastCheckAt) {
197
200
  const age = Date.now() - new Date(cache.lastCheckAt).getTime();
198
- if (age < CHECK_INTERVAL_MS) return; // recent enough — skip
201
+ if (age < (installation?.updateCheckInterval ?? CHECK_INTERVAL_MS)) return; // recent enough — skip
199
202
  }
200
203
 
201
204
  try {
@@ -225,6 +228,8 @@ export function scheduleBackgroundCheck(packageRoot) {
225
228
  */
226
229
  export function maybePrintNotice(packageRoot) {
227
230
  if (isDisabled()) return;
231
+ const installation = loadInstallationIdentity({ actualRoot: packageRoot, createIfMissing: false });
232
+ if (installation?.checkOnStartup === false) return;
228
233
  const cache = readCache();
229
234
  if (!cacheMatchesPackage(cache, packageRoot)) return;
230
235
  if (!cache?.hasUpdate || !cache.current || !cache.latest) return;
@@ -10,6 +10,13 @@ import { execFileSync } from 'node:child_process';
10
10
  import { existsSync, readFileSync } from 'node:fs';
11
11
  import path from 'node:path';
12
12
  import { getPackageRoot, loadConfig } from '../channel/manager.mjs';
13
+ import {
14
+ assertCanonicalInstallation,
15
+ createInstallationIdentity,
16
+ loadInstallationIdentity,
17
+ saveInstallationIdentity,
18
+ } from '../installation/manager.mjs';
19
+ import { resolveManagerCommand } from '../installation/manager-command.mjs';
13
20
 
14
21
  const VALID_MODES = new Set(['npm', 'web', 'source']);
15
22
 
@@ -24,14 +31,42 @@ function readPackageName(packageRoot) {
24
31
  export async function detectInstallMode(options = {}) {
25
32
  const packageRoot = options.packageRoot ?? getPackageRoot();
26
33
  const override = options.env?.AIWG_INSTALL_MODE ?? process.env.AIWG_INSTALL_MODE;
34
+ const config = options.config ?? await loadConfig(options);
35
+ let identity = options.identity ?? config.installation ?? null;
36
+ let identityPersistent = Boolean(config.installation) || options.identityPersistent === true;
37
+ if (!identity && options.config) {
38
+ const method = override ?? (readPackageName(packageRoot) === '@aiwg/cli'
39
+ ? 'web'
40
+ : (config.devMode || config.channel === 'edge' || existsSync(path.join(packageRoot, '.git'))) ? 'source' : 'npm');
41
+ identity = createInstallationIdentity({
42
+ ...options,
43
+ actualRoot: packageRoot,
44
+ method,
45
+ channel: config.channel,
46
+ runMode: config.devMode ? 'development' : undefined,
47
+ });
48
+ } else if (!identity) {
49
+ identity = loadInstallationIdentity({ ...options, actualRoot: packageRoot });
50
+ identityPersistent = Boolean(identity);
51
+ }
52
+
53
+ if (override && !VALID_MODES.has(override)) {
54
+ throw new Error(`AIWG_INSTALL_MODE must be one of: ${[...VALID_MODES].join(', ')}`);
55
+ }
56
+ if (override && identity && override !== identity.method) {
57
+ throw new Error(`AIWG_INSTALL_MODE=${override} conflicts with the canonical installation method ${identity.method}. Use \`aiwg installation switch\` instead.`);
58
+ }
59
+ if (identity) {
60
+ const status = assertCanonicalInstallation({ ...options, actualRoot: packageRoot, identity });
61
+ return { mode: identity.method, packageRoot, packageName: readPackageName(packageRoot), identity, identityPersistent, installation: status };
62
+ }
63
+
27
64
  if (override) {
28
65
  if (!VALID_MODES.has(override)) {
29
66
  throw new Error(`AIWG_INSTALL_MODE must be one of: ${[...VALID_MODES].join(', ')}`);
30
67
  }
31
68
  return { mode: override, packageRoot, packageName: readPackageName(packageRoot) };
32
69
  }
33
-
34
- const config = options.config ?? await loadConfig();
35
70
  const packageName = readPackageName(packageRoot);
36
71
  if (packageName === '@aiwg/cli') return { mode: 'web', packageRoot, packageName };
37
72
  if (config.devMode || config.channel === 'edge' || existsSync(path.join(packageRoot, '.git'))) {
@@ -56,7 +91,7 @@ function npmTag(channel) {
56
91
  export async function updateInstallation(options = {}) {
57
92
  const config = options.config ?? await loadConfig();
58
93
  const detected = await detectInstallMode({ ...options, config });
59
- const channel = options.channel ?? config.channel ?? 'stable';
94
+ const channel = options.channel ?? detected.identity?.channel ?? config.channel ?? 'stable';
60
95
  const dryRun = options.dryRun === true;
61
96
  const offline = options.offline === true;
62
97
 
@@ -84,6 +119,9 @@ export async function updateInstallation(options = {}) {
84
119
  return resolveWebRelease({ selector });
85
120
  });
86
121
  const release = await refreshWebResources(channel);
122
+ if (detected.identity && detected.identityPersistent && options.persistIdentity !== false) {
123
+ saveInstallationIdentity({ ...detected.identity, channel }, options);
124
+ }
87
125
  return {
88
126
  ...detected,
89
127
  channel,
@@ -106,16 +144,24 @@ export async function updateInstallation(options = {}) {
106
144
 
107
145
  const tag = npmTag(channel);
108
146
  const command = ['install', '--global', `aiwg@${tag}`];
147
+ const managerExecutable = detected.identity?.managerExecutable;
148
+ if (!managerExecutable) {
149
+ throw new Error('Canonical npm installation has no package-manager executable. Run `aiwg installation adopt --manager <absolute-path-to-npm>`.');
150
+ }
109
151
  if (!dryRun) {
152
+ const invocation = resolveManagerCommand(managerExecutable, command, options);
110
153
  const execute = options.execute ?? ((file, args) => execFileSync(file, args, { stdio: 'inherit' }));
111
- execute('npm', command);
154
+ execute(invocation.file, invocation.args);
155
+ if (detected.identity && detected.identityPersistent && options.persistIdentity !== false) {
156
+ saveInstallationIdentity({ ...detected.identity, channel }, options);
157
+ }
112
158
  }
113
159
  return {
114
160
  ...detected,
115
161
  channel,
116
162
  status: dryRun ? 'dry-run' : 'updated',
117
163
  changed: !dryRun,
118
- command: `npm ${command.join(' ')}`,
164
+ command: `${managerExecutable} ${command.join(' ')}`,
119
165
  message: dryRun
120
166
  ? `Would update the full AIWG npm distribution on the '${tag}' channel.`
121
167
  : `Updated the full AIWG npm distribution on the '${tag}' channel.`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwg/cli",
3
- "version": "2026.8.17",
3
+ "version": "2026.8.19",
4
4
  "description": "Lightweight AIWG CLI for signed, versioned web-backed resources.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -340,7 +340,7 @@ function mirrorSkillsAsCommands(provider, target, srcRoot, opts) {
340
340
  * @param {string|null} explicitSource the raw `--source` value (null when unset)
341
341
  */
342
342
  function pruneStaleAiwgArtifacts(provider, target, srcRoot, opts, explicitSource) {
343
- if (opts.skillsOnly) return; // skills run their own prune in the provider
343
+ if (opts.skillsOnly && !opts.kernelOnly) return; // skills run their own prune in the provider
344
344
 
345
345
  const aiwgRoot = resolveAiwgRoot(srcRoot);
346
346
  if (!aiwgRoot) return; // no AIWG tree → bundle/standalone deploy; never prune
@@ -355,6 +355,20 @@ function pruneStaleAiwgArtifacts(provider, target, srcRoot, opts, explicitSource
355
355
  if (!underCode) return; // project-local bundle / external source → skip
356
356
  }
357
357
 
358
+ if (opts.kernelOnly) {
359
+ for (const type of ['agents', 'commands', 'rules']) {
360
+ const relPath = provider.paths?.[type];
361
+ if (!relPath || relPath.endsWith('.md')) continue;
362
+ const destDir = path.isAbsolute(relPath) ? relPath : path.join(target, relPath);
363
+ pruneStaleAiwgFiles(destDir, new Set(), {
364
+ dryRun: opts.dryRun,
365
+ verbose: opts.verbose,
366
+ artifactExtensions: ['.md', '.mdc', '.toml'],
367
+ });
368
+ }
369
+ return;
370
+ }
371
+
358
372
  const typesThisRun = [];
359
373
  if (!opts.commandsOnly && !opts.rulesOnly) typesThisRun.push('agents');
360
374
  if (opts.deployCommands || opts.commandsOnly) typesThisRun.push('commands');
@@ -406,6 +420,7 @@ function parseArgs() {
406
420
  commandsOnly: false,
407
421
  skillsOnly: false,
408
422
  rulesOnly: false,
423
+ kernelOnly: false,
409
424
  filter: null, // Glob pattern for agent names
410
425
  filterRole: null, // Filter by role: reasoning|coding|efficiency
411
426
  save: false, // Save model config to project models.json
@@ -438,6 +453,7 @@ function parseArgs() {
438
453
  else if (a === '--commands-only') cfg.commandsOnly = true;
439
454
  else if (a === '--skills-only') cfg.skillsOnly = true;
440
455
  else if (a === '--rules-only') cfg.rulesOnly = true;
456
+ else if (a === '--kernel-only') cfg.kernelOnly = true;
441
457
  else if (a === '--deploy-behaviors') cfg.deployBehaviors = true;
442
458
  else if (a === '--filter' && args[i + 1]) cfg.filter = args[++i];
443
459
  else if (a === '--filter-role' && args[i + 1]) cfg.filterRole = args[++i];
@@ -474,6 +490,7 @@ Options:
474
490
  --commands-only Deploy only commands (skip agents)
475
491
  --skills-only Deploy only skills (skip agents)
476
492
  --rules-only Deploy only rules (skip agents)
493
+ --kernel-only Deploy kernel skills only and prune managed bulk artifacts
477
494
  --dry-run Show what would be deployed without writing
478
495
  --force Overwrite existing files
479
496
  --provider <name> Target provider (see below)
@@ -490,7 +507,9 @@ Options:
490
507
  --create-agents-md Create/update AGENTS.md template
491
508
  --skip-commands-migration Skip deleting the commands directory before skills deployment
492
509
  --copy-all Copy ALL skills per-project (legacy mirror at <provider>/.aiwg/skills/).
493
- Default is kernel-only + index-driven discovery for the rest (#1217).
510
+ For aiwg use all, this also restores the legacy full agent,
511
+ command, and expanded-rule copy. Default bulk deployment is
512
+ kernel-only + index-driven discovery for the rest (#1217).
494
513
  Use this for sandboxed runtimes / air-gapped corpora where
495
514
  $AIWG_ROOT isn't readable from the agent's working dir.
496
515
  Alias: --copy-standard-skills — rc.29 era).
@@ -879,13 +898,14 @@ export async function main() {
879
898
  modelsConfig,
880
899
  asAgentsMd: cfg.asAgentsMd,
881
900
  createAgentsMd: cfg.createAgentsMd,
882
- deployCommands: cfg.deployCommands,
883
- deploySkills: cfg.deploySkills,
884
- deployRules: cfg.deployRules,
885
- deployBehaviors: cfg.deployBehaviors,
901
+ deployCommands: cfg.kernelOnly ? false : cfg.deployCommands,
902
+ deploySkills: cfg.kernelOnly ? true : cfg.deploySkills,
903
+ deployRules: cfg.kernelOnly ? false : cfg.deployRules,
904
+ deployBehaviors: cfg.kernelOnly ? false : cfg.deployBehaviors,
886
905
  commandsOnly: cfg.commandsOnly,
887
- skillsOnly: cfg.skillsOnly,
906
+ skillsOnly: cfg.skillsOnly || cfg.kernelOnly,
888
907
  rulesOnly: cfg.rulesOnly,
908
+ kernelOnly: cfg.kernelOnly,
889
909
  filter: cfg.filter,
890
910
  filterRole: cfg.filterRole,
891
911
  save: cfg.save,
@@ -893,7 +913,7 @@ export async function main() {
893
913
  verbose: cfg.verbose,
894
914
  quiet: cfg.quiet,
895
915
  asPlugin: cfg.asPlugin,
896
- deployBehaviors: cfg.deployBehaviors,
916
+ deployBehaviors: cfg.kernelOnly ? false : cfg.deployBehaviors,
897
917
  skipCommandsMigration: cfg.skipCommandsMigration,
898
918
  // #1217 / #1219: --copy-all flag forces legacy per-project mirror
899
919
  // for the standard tier. Default is no-copy + index-driven discovery.
@@ -1183,7 +1183,7 @@ export function computeAllKernelNames(srcRoot) {
1183
1183
  * located — see `computeAllKernelNames`), pruning is skipped entirely so a
1184
1184
  * project-local-bundle deploy without AIWG_ROOT never empties the kernel
1185
1185
  * skills directory (#123).
1186
- * @param {object} opts `{ dryRun, verbose }`
1186
+ * @param {object} opts `{ dryRun, verbose, artifactExtensions }`
1187
1187
  * @returns {number} count of pruned entries
1188
1188
  */
1189
1189
  export function pruneStaleAiwgSkills(kernelDestDir, desiredKernelNames, opts = {}) {
@@ -1348,7 +1348,8 @@ export function resolveAiwgRoot(srcRoot) {
1348
1348
  * `pruneStaleAiwgSkills`.
1349
1349
  *
1350
1350
  * Removes a file from `destDir` only when ALL hold:
1351
- * 1. It is a deployed artifact file (`.md` / `.mdc`), not `RULES-INDEX.md`
1351
+ * 1. It has an allowed deployed-artifact extension (`.md` / `.mdc` by
1352
+ * default; callers may include `.toml`), is not `RULES-INDEX.md`,
1352
1353
  * and not the sidecar manifest.
1353
1354
  * 2. Its stem is NOT in `desiredStems` (the source no longer ships it).
1354
1355
  * 3. It carries an AIWG ownership signal — either a `.aiwg-manifest.json`
@@ -1369,6 +1370,7 @@ export function resolveAiwgRoot(srcRoot) {
1369
1370
  */
1370
1371
  export function pruneStaleAiwgFiles(destDir, desiredStems, opts = {}) {
1371
1372
  const { dryRun = false, verbose = false } = opts;
1373
+ const artifactExtensions = opts.artifactExtensions || ['.md', '.mdc'];
1372
1374
  const removed = [];
1373
1375
  if (!destDir || !fs.existsSync(destDir)) return removed;
1374
1376
 
@@ -1391,7 +1393,7 @@ export function pruneStaleAiwgFiles(destDir, desiredStems, opts = {}) {
1391
1393
  if (name === 'RULES-INDEX.md') continue;
1392
1394
  if (name === 'RULES-ONDEMAND.md') continue; // generated on-demand index (#1673)
1393
1395
  const lower = name.toLowerCase();
1394
- if (!lower.endsWith('.md') && !lower.endsWith('.mdc')) continue;
1396
+ if (!artifactExtensions.some(extension => lower.endsWith(extension))) continue;
1395
1397
 
1396
1398
  if (desired.has(artifactStem(name))) continue;
1397
1399