@aiwg/cli 2026.10.0 → 2026.10.3

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 (168) hide show
  1. package/agentic/code/providers/capability-matrix.yaml +270 -0
  2. package/agentic/code/providers/model-capabilities.v1.json +3091 -24
  3. package/bin/aiwg.mjs +86 -21
  4. package/dist/src/a2a/codecs.js +9 -0
  5. package/dist/src/artifacts/corpus-tools/vision-extract.js +105 -15
  6. package/dist/src/artifacts/repair.js +9 -4
  7. package/dist/src/channel/manager.mjs +59 -10
  8. package/dist/src/cli/child-process-result.js +40 -0
  9. package/dist/src/cli/env.js +38 -16
  10. package/dist/src/cli/flag-parsing.js +236 -0
  11. package/dist/src/cli/handlers/decision.js +297 -2
  12. package/dist/src/cli/handlers/diagnose.js +9 -2
  13. package/dist/src/cli/handlers/feedback.js +14 -5
  14. package/dist/src/cli/handlers/init.js +245 -30
  15. package/dist/src/cli/handlers/mc.js +23 -0
  16. package/dist/src/cli/handlers/models.js +69 -6
  17. package/dist/src/cli/handlers/ralph.js +3 -0
  18. package/dist/src/cli/handlers/refresh.js +16 -0
  19. package/dist/src/cli/handlers/regenerate.js +14 -0
  20. package/dist/src/cli/handlers/runtime-info.js +24 -9
  21. package/dist/src/cli/handlers/script-runner.js +16 -6
  22. package/dist/src/cli/handlers/session.js +396 -62
  23. package/dist/src/cli/handlers/sessions.js +146 -40
  24. package/dist/src/cli/handlers/setup-manifest.js +439 -39
  25. package/dist/src/cli/handlers/setup.js +139 -39
  26. package/dist/src/cli/handlers/subcommands.js +3 -1
  27. package/dist/src/cli/handlers/team.js +18 -8
  28. package/dist/src/cli/handlers/use.js +13 -0
  29. package/dist/src/cli/handlers/utilities.js +106 -23
  30. package/dist/src/cli/prompt-utils.js +72 -0
  31. package/dist/src/cli/provider-resolution.js +11 -0
  32. package/dist/src/config/aiwg-config.js +268 -4
  33. package/dist/src/config/cli.js +146 -19
  34. package/dist/src/config/dispatch-thresholds-runtime.mjs +38 -0
  35. package/dist/src/config/project-artifacts-health.mjs +14 -3
  36. package/dist/src/config/user-config.js +36 -10
  37. package/dist/src/decision/adapters/jev.js +80 -28
  38. package/dist/src/decision/ask.js +352 -0
  39. package/dist/src/decision/driver.js +4 -0
  40. package/dist/src/decision/entry.js +38 -6
  41. package/dist/src/decision/heldout/collector.js +10 -1
  42. package/dist/src/decision/index.js +3 -0
  43. package/dist/src/decision/interop.js +60 -8
  44. package/dist/src/decision/jev-credentials.js +121 -0
  45. package/dist/src/decision/ledger.js +839 -0
  46. package/dist/src/decision/validate.js +6 -3
  47. package/dist/src/effects/dispatch-verifier.js +252 -0
  48. package/dist/src/effects/index.js +1 -0
  49. package/dist/src/features/installer.js +2 -2
  50. package/dist/src/flow/graph-metadata.js +5 -0
  51. package/dist/src/gates/providers/loader.js +3 -1
  52. package/dist/src/installation/manager-command.mjs +1 -1
  53. package/dist/src/installation/manager.mjs +17 -2
  54. package/dist/src/lint/runner.js +2 -2
  55. package/dist/src/mcp/adapters/codex-runtime.js +360 -56
  56. package/dist/src/mcp/cli.mjs +587 -77
  57. package/dist/src/mcp/config-file.mjs +279 -0
  58. package/dist/src/mcp/config-layers.mjs +348 -0
  59. package/dist/src/mcp/credentials.mjs +484 -0
  60. package/dist/src/mcp/grok-build-config.mjs +16 -26
  61. package/dist/src/mcp/helpers.mjs +106 -2
  62. package/dist/src/mcp/omp-config.mjs +50 -29
  63. package/dist/src/mcp/profiles.js +97 -23
  64. package/dist/src/mcp/profiles.mjs +99 -27
  65. package/dist/src/mcp/registry.js +264 -57
  66. package/dist/src/mcp/registry.mjs +198 -45
  67. package/dist/src/mcp/server.mjs +403 -20
  68. package/dist/src/mcp/toml-string.mjs +9 -0
  69. package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
  70. package/dist/src/mcp/tool-filters.mjs +357 -0
  71. package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
  72. package/dist/src/mcp/tools/decision.mjs +58 -0
  73. package/dist/src/mcp/tools/orchestration.mjs +38 -0
  74. package/dist/src/mcp/tools/subsystems.mjs +39 -8
  75. package/dist/src/mcp/tools/worker-invoke.mjs +1539 -0
  76. package/dist/src/mcp/tools/worker-shell-control.mjs +258 -0
  77. package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
  78. package/dist/src/memory/project-registry.js +2 -2
  79. package/dist/src/mission-protocol/codecs.js +639 -1
  80. package/dist/src/mission-protocol/types.js +110 -0
  81. package/dist/src/mission-protocol/worker-status.js +2 -0
  82. package/dist/src/models/config-loader.js +70 -13
  83. package/dist/src/models/grok-build-models.js +93 -41
  84. package/dist/src/models/model-capabilities.v1.json +3091 -24
  85. package/dist/src/models/model-discovery.js +862 -63
  86. package/dist/src/models/provider-policy.js +625 -11
  87. package/dist/src/models/qualification-digest.mjs +17 -0
  88. package/dist/src/models/resolver.js +259 -0
  89. package/dist/src/models/worker-direct-readiness.js +529 -0
  90. package/dist/src/models/worker-dispatch-offer.js +169 -0
  91. package/dist/src/models/worker-health-readiness.js +131 -0
  92. package/dist/src/models/worker-installation.mjs +879 -0
  93. package/dist/src/models/wrapper-route.js +18 -0
  94. package/dist/src/ops/registry.js +9 -14
  95. package/dist/src/plugins/standalone-packager.js +4 -3
  96. package/dist/src/process/bounded-child-process.mjs +387 -0
  97. package/dist/src/process/cli-command.mjs +75 -0
  98. package/dist/src/providers/capability-matrix.js +8 -0
  99. package/dist/src/providers/capability-matrix.yaml +270 -0
  100. package/dist/src/providers/grok-build-headless.js +43 -21
  101. package/dist/src/providers/provider-definitions.js +9 -1
  102. package/dist/src/providers/provider-definitions.mjs +9 -2
  103. package/dist/src/serve/executor-registry.js +16 -24
  104. package/dist/src/serve/fleet-mission-conductor.js +9 -0
  105. package/dist/src/serve/mission-conductor.js +9 -0
  106. package/dist/src/serve/shared-host-scheduler.js +2 -0
  107. package/dist/src/serve/worker-invocation.js +538 -0
  108. package/dist/src/serve/worker-stdin-frame.mjs +107 -0
  109. package/dist/src/sessions/adapter-registry.js +45 -0
  110. package/dist/src/sessions/adapters/generic.js +37 -3
  111. package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
  112. package/dist/src/sessions/adapters/grokbot.js +423 -24
  113. package/dist/src/sessions/adapters/muse-workspace-scan.js +108 -0
  114. package/dist/src/sessions/adapters/muse.js +68 -29
  115. package/dist/src/sessions/adapters/pi.js +1 -0
  116. package/dist/src/sessions/batch-contracts.js +13 -1
  117. package/dist/src/sessions/batch-import.js +23 -42
  118. package/dist/src/sessions/candidates.js +47 -11
  119. package/dist/src/sessions/contracts.js +35 -0
  120. package/dist/src/sessions/coverage-preflight.js +134 -0
  121. package/dist/src/sessions/discovery.js +3 -0
  122. package/dist/src/sessions/grok-build-cli.js +67 -5
  123. package/dist/src/sessions/importer.js +24 -8
  124. package/dist/src/sessions/index.js +2 -0
  125. package/dist/src/sessions/repository.js +54 -13
  126. package/dist/src/sessions/workspace-discovery.js +99 -37
  127. package/dist/src/skills/adapters/clawhub.js +10 -8
  128. package/dist/src/skills/adapters/openclaw.js +10 -7
  129. package/dist/src/smiths/context-pipeline/parallelism-section.js +192 -3
  130. package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
  131. package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
  132. package/dist/src/smiths/mcpsmith/example.js +1 -1
  133. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +10 -14
  134. package/dist/src/storage/backends/logseq.js +68 -7
  135. package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
  136. package/dist/src/testing/generators/test-code-generator.js +57 -30
  137. package/dist/src/uhp/client.js +11 -1
  138. package/dist/src/uhp/mission.js +5 -0
  139. package/dist/src/update/checker.mjs +5 -5
  140. package/dist/src/update/notifier.mjs +54 -2
  141. package/dist/src/writing/fidelity.js +18 -1
  142. package/package.json +18 -1
  143. package/schemas/mission-v1.schema.json +2429 -0
  144. package/schemas/models/model-catalog.v1.schema.json +301 -0
  145. package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
  146. package/schemas/models/model-policy.v1.schema.json +14 -0
  147. package/schemas/models/provider-model-capabilities.v1.schema.json +1066 -0
  148. package/tools/_resolve-impl.mjs +39 -0
  149. package/tools/agents/deploy-agents.mjs +15 -2
  150. package/tools/agents/providers/base.mjs +63 -43
  151. package/tools/agents/providers/grokbot.mjs +11 -12
  152. package/tools/agents/providers/hermes.mjs +6 -3
  153. package/tools/agents/providers/omp.mjs +5 -2
  154. package/tools/agents/providers/openclaw-translator.mjs +19 -5
  155. package/tools/providers/deepseek-harness-transport.mjs +358 -0
  156. package/tools/providers/omp-transport.mjs +192 -0
  157. package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
  158. package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
  159. package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
  160. package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
  161. package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
  162. package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
  163. package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
  164. package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
  165. package/tools/ralph-external/lib/pi-adapter.mjs +333 -0
  166. package/tools/ralph-external/lib/provider-adapter.mjs +690 -0
  167. package/tools/ralph-external/lib/worker-health.mjs +439 -0
  168. package/vscode-extension/schemas/aiwg.config.v1.json +1953 -0
@@ -1,26 +1,42 @@
1
- import { spawnSync } from 'child_process';
1
+ import { boundedSpawnSync } from '../../process/bounded-child-process.mjs';
2
2
  import { existsSync } from 'fs';
3
3
  import { emptyConfig, FORCE_PUSH_POLICY_ALIAS_NOTE, normalizeForcePushPolicy as normalizeForcePushPolicyShared, getConfigPath, getProjectDir, readAiwgConfig, resolveRemoteProvider, VALID_PROVIDERS, writeAiwgConfig, } from '../../config/aiwg-config.js';
4
4
  import { AiwgError, EXIT_CODES } from '../errors.js';
5
- import { askChoice, askString, askYesNo, createPromptInterface } from '../prompt-utils.js';
5
+ import { askChoice, askConsent, askString, askYesNo, createPromptInterface } from '../prompt-utils.js';
6
6
  import * as ui from '../ui.js';
7
7
  import { projectAiwgPath } from '../../config/project-artifacts.js';
8
8
  import { normalizeProviderDefinitionId } from '../../providers/provider-definitions.js';
9
+ import { isInteractive } from '../env.js';
10
+ import { requireFlagValue as strictFlagValue, MissingFlagValueError } from '../flag-parsing.js';
11
+ import { detectInstalledWorkerClis, evaluateWorkerDispatchOffer, workerDispatchOfferHintWithArg, workerDispatchOfferQuestion, WORKER_DISPATCH_ARG_VALUES, } from '../../models/worker-dispatch-offer.js';
9
12
  const DELIVERY_MODES = ['pr-required', 'feature-branch', 'direct'];
10
13
  const FORCE_PUSH_POLICIES = ['never', 'own-branch-only', 'allowed'];
11
14
  const ISSUE_PROVIDERS = ['gitea', 'github', 'local'];
12
15
  const SIGNING_FORMATS = ['openpgp', 'ssh', 'x509'];
13
16
  const TRACKER_VIA = ['tea', 'gh', 'mcp', 'api'];
17
+ /**
18
+ * A value-taking flag present without a usable value is a usage error, not
19
+ * "not given" (#3062 review item 11) — delegates to the shared strict
20
+ * parser, translating its `MissingFlagValueError` into this file's own
21
+ * `AiwgError`/`EXIT_CODES.USAGE` convention. `precedence: 'first'` matches
22
+ * this command's own pre-#3062 behavior (a hand-rolled loop that returned
23
+ * on the first match) — unchanged by the shared parser's second-round fix
24
+ * (#3062 second review, finding 4).
25
+ */
14
26
  function flagValue(args, ...names) {
15
- for (let i = 0; i < args.length; i += 1) {
16
- if (names.includes(args[i] ?? '')) {
17
- const value = args[i + 1];
18
- if (!value || value.startsWith('--'))
19
- return undefined;
20
- return value;
27
+ try {
28
+ return strictFlagValue(args, {}, ...names);
29
+ }
30
+ catch (err) {
31
+ if (err instanceof MissingFlagValueError) {
32
+ throw new AiwgError({
33
+ code: 'ERR_USAGE_MISSING_FLAG_VALUE',
34
+ message: `${err.message}.`,
35
+ exitCode: EXIT_CODES.USAGE,
36
+ });
21
37
  }
38
+ throw err;
22
39
  }
23
- return undefined;
24
40
  }
25
41
  function boolFlag(args, name) {
26
42
  return args.includes(name);
@@ -92,6 +108,10 @@ export function parseSetupProjectOptions(ctx) {
92
108
  trackerActorVia: parseEnum(flagValue(args, '--tracker-actor-via'), TRACKER_VIA, '--tracker-actor-via'),
93
109
  customerTrackerActorLogin: flagValue(args, '--customer-tracker-actor-login'),
94
110
  customerTrackerActorVia: parseEnum(flagValue(args, '--customer-tracker-actor-via'), TRACKER_VIA, '--customer-tracker-actor-via'),
111
+ // #3062: answers the worker-dispatch question directly, with or without
112
+ // a terminal, and suppresses the interactive question below (see
113
+ // `SetupProjectOptions.workerDispatchChoice`'s doc comment).
114
+ workerDispatchChoice: parseEnum(flagValue(args, '--worker-dispatch'), WORKER_DISPATCH_ARG_VALUES, '--worker-dispatch'),
95
115
  providers: (() => {
96
116
  const providers = parseStringList(flagValue(args, '--providers'));
97
117
  return providers ? normalizeProviders(providers) : undefined;
@@ -99,7 +119,7 @@ export function parseSetupProjectOptions(ctx) {
99
119
  };
100
120
  }
101
121
  export function detectGitRemotes(projectDir) {
102
- const result = spawnSync('git', ['-C', projectDir, 'remote', '-v'], {
122
+ const result = boundedSpawnSync('git', ['-C', projectDir, 'remote', '-v'], 'quick', {
103
123
  encoding: 'utf-8',
104
124
  stdio: ['ignore', 'pipe', 'ignore'],
105
125
  });
@@ -122,21 +142,21 @@ export function detectGitRemotes(projectDir) {
122
142
  return [...byName.values()];
123
143
  }
124
144
  function detectDefaultBranch(projectDir) {
125
- const symbolic = spawnSync('git', ['-C', projectDir, 'symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], {
145
+ const symbolic = boundedSpawnSync('git', ['-C', projectDir, 'symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], 'quick', {
126
146
  encoding: 'utf-8',
127
147
  stdio: ['ignore', 'pipe', 'ignore'],
128
148
  });
129
149
  const fromOrigin = symbolic.stdout.trim().replace(/^origin\//, '');
130
150
  if (symbolic.status === 0 && fromOrigin)
131
151
  return fromOrigin;
132
- const branch = spawnSync('git', ['-C', projectDir, 'branch', '--show-current'], {
152
+ const branch = boundedSpawnSync('git', ['-C', projectDir, 'branch', '--show-current'], 'quick', {
133
153
  encoding: 'utf-8',
134
154
  stdio: ['ignore', 'pipe', 'ignore'],
135
155
  });
136
156
  return branch.stdout.trim() || 'main';
137
157
  }
138
158
  function gitConfig(projectDir, key) {
139
- const result = spawnSync('git', ['-C', projectDir, 'config', '--get', key], {
159
+ const result = boundedSpawnSync('git', ['-C', projectDir, 'config', '--get', key], 'quick', {
140
160
  encoding: 'utf-8',
141
161
  stdio: ['ignore', 'pipe', 'ignore'],
142
162
  });
@@ -321,6 +341,19 @@ export async function buildSetupProjectPlan(options) {
321
341
  }
322
342
  base.remotes = remotesConfig;
323
343
  base.delivery = delivery;
344
+ // #3050: apply an already-made worker-dispatch opt-in choice (from the
345
+ // interactive wizard), then recompute whether any surface should still
346
+ // offer/hint it — a choice just applied here always clears the offer.
347
+ // The decline is a setup/onboarding preference, not worker policy, so it
348
+ // lives in `setup`, not in the frozen `parallelism` contract fragment
349
+ // (B-OG-29).
350
+ if (options.workerDispatchChoice === 'enable') {
351
+ base.parallelism = { ...base.parallelism, worker_dispatch: true };
352
+ }
353
+ else if (options.workerDispatchChoice === 'decline') {
354
+ base.setup = { ...base.setup, worker_dispatch_offer: 'declined' };
355
+ }
356
+ const workerDispatchOffer = evaluateWorkerDispatchOffer(base.parallelism, base.setup, detectInstalledWorkerClis());
324
357
  if (primaryRemote?.provider === 'unknown' && issueProvider !== 'local') {
325
358
  warnings.push(`Remote '${primary}' is self-hosted or unknown; provider '${issueProvider}' was selected explicitly/defaulted.`);
326
359
  }
@@ -347,9 +380,10 @@ export async function buildSetupProjectPlan(options) {
347
380
  issueProvider,
348
381
  warnings,
349
382
  diff: formatPreviewDiff(existing, base),
383
+ workerDispatchOffer,
350
384
  };
351
385
  }
352
- async function applyInteractivePrompts(plan, rl, signal) {
386
+ export async function applyInteractivePrompts(plan, rl, signal, supplied = {}) {
353
387
  const remoteNames = plan.remotes.map(r => r.name);
354
388
  const current = plan.next;
355
389
  console.log('');
@@ -360,28 +394,59 @@ async function applyInteractivePrompts(plan, rl, signal) {
360
394
  }
361
395
  if (plan.remotes.length === 0)
362
396
  console.log(' (none)');
363
- const primary = remoteNames.length > 0
397
+ // #3062 review item 10: a value already supplied by flag is never
398
+ // re-asked — only the fields `supplied` leaves `undefined` prompt here.
399
+ // `??`'s right-hand side is only evaluated when the left is nullish, so
400
+ // a supplied value skips the corresponding `ask*` call entirely.
401
+ const primary = supplied.primary ?? (remoteNames.length > 0
364
402
  ? await askChoice(rl, `Primary remote [${current.remotes?.primary}]: `, remoteNames, current.remotes?.primary, signal)
365
- : await askString(rl, `Primary remote [${current.remotes?.primary}]: `, current.remotes?.primary ?? 'origin', signal);
366
- const issueProvider = await askChoice(rl, `Issue provider (${ISSUE_PROVIDERS.join('/')}): `, ISSUE_PROVIDERS, plan.issueProvider, signal);
403
+ : await askString(rl, `Primary remote [${current.remotes?.primary}]: `, current.remotes?.primary ?? 'origin', signal));
404
+ const issueProvider = supplied.issueProvider
405
+ ?? await askChoice(rl, `Issue provider (${ISSUE_PROVIDERS.join('/')}): `, ISSUE_PROVIDERS, plan.issueProvider, signal);
367
406
  const issueTracker = issueProvider === 'local'
368
407
  ? 'local'
369
- : remoteNames.length > 0
408
+ : supplied.issueTracker ?? (remoteNames.length > 0
370
409
  ? await askChoice(rl, `Issue tracker remote [${current.remotes?.issue_tracker}]: `, remoteNames, current.remotes?.issue_tracker, signal)
371
- : await askString(rl, `Issue tracker remote [${current.remotes?.issue_tracker}]: `, current.remotes?.issue_tracker ?? primary, signal);
372
- const ci = remoteNames.length > 0
410
+ : await askString(rl, `Issue tracker remote [${current.remotes?.issue_tracker}]: `, current.remotes?.issue_tracker ?? primary, signal));
411
+ const ci = supplied.ci ?? (remoteNames.length > 0
373
412
  ? await askChoice(rl, `CI remote [${current.remotes?.ci}]: `, remoteNames, current.remotes?.ci, signal)
374
- : await askString(rl, `CI remote [${current.remotes?.ci}]: `, current.remotes?.ci ?? primary, signal);
375
- const deliveryMode = await askChoice(rl, `Delivery mode (${DELIVERY_MODES.join('/')}): `, DELIVERY_MODES, current.delivery?.mode, signal);
376
- const defaultBranch = await askString(rl, `Default branch [${current.delivery?.default_branch}]: `, current.delivery?.default_branch ?? 'main', signal);
377
- const requireCiGreen = await askYesNo(rl, 'Require CI green before done? [Y/n]: ', current.delivery?.require_ci_green ?? true, signal);
378
- const autoCloseIssues = await askYesNo(rl, 'Auto-close linked issues from delivery? [Y/n]: ', current.delivery?.auto_close_issues ?? true, signal);
379
- const forcePushPolicy = await askChoice(rl, `Force-push policy (${FORCE_PUSH_POLICIES.join('/')}): `, FORCE_PUSH_POLICIES, current.delivery?.force_push_policy, signal);
380
- const requireSignedCommits = await askYesNo(rl, 'Require signed delivery commits? [y/N]: ', current.delivery?.require_signed_commits ?? false, signal);
381
- const providersRaw = await askString(rl, `Providers [${current.providers.join(',')}]: `, current.providers.join(','), signal);
413
+ : await askString(rl, `CI remote [${current.remotes?.ci}]: `, current.remotes?.ci ?? primary, signal));
414
+ const deliveryMode = supplied.deliveryMode
415
+ ?? await askChoice(rl, `Delivery mode (${DELIVERY_MODES.join('/')}): `, DELIVERY_MODES, current.delivery?.mode, signal);
416
+ const defaultBranch = supplied.defaultBranch
417
+ ?? await askString(rl, `Default branch [${current.delivery?.default_branch}]: `, current.delivery?.default_branch ?? 'main', signal);
418
+ const requireCiGreen = supplied.requireCiGreen
419
+ ?? await askYesNo(rl, 'Require CI green before done? [Y/n]: ', current.delivery?.require_ci_green ?? true, signal);
420
+ const autoCloseIssues = supplied.autoCloseIssues
421
+ ?? await askYesNo(rl, 'Auto-close linked issues from delivery? [Y/n]: ', current.delivery?.auto_close_issues ?? true, signal);
422
+ const forcePushPolicy = supplied.forcePushPolicy
423
+ ?? await askChoice(rl, `Force-push policy (${FORCE_PUSH_POLICIES.join('/')}): `, FORCE_PUSH_POLICIES, current.delivery?.force_push_policy, signal);
424
+ const requireSignedCommits = supplied.requireSignedCommits
425
+ ?? await askYesNo(rl, 'Require signed delivery commits? [y/N]: ', current.delivery?.require_signed_commits ?? false, signal);
426
+ const providers = supplied.providers
427
+ ?? parseProviders(await askString(rl, `Providers [${current.providers.join(',')}]: `, current.providers.join(','), signal), current.providers);
428
+ // #3050: offer direct worker dispatch, only when there is something to
429
+ // offer (two or more worker CLIs detected, dispatch not already on or
430
+ // declined), and only in a context where the answer could actually be
431
+ // acted on: CI and dry-run never prompt (review item 5) — an unattended
432
+ // CI run could otherwise stall on a prompt timeout, and a dry-run would
433
+ // ask a question whose answer is never written. Both cases fall back to
434
+ // the informational hint at the end of `execute()` instead (via
435
+ // `plan.workerDispatchOffer.shouldOffer` staying true on the rebuilt plan).
436
+ // Strict/distinguishable consent (review items 2 and 4): only an actually
437
+ // answered "no" is recorded as a persistent decline. An explicit
438
+ // `--worker-dispatch` already answers this directly (#3062) — skip the
439
+ // question the same way as every other supplied field.
440
+ let workerDispatchChoice = supplied.workerDispatchChoice;
441
+ if (!workerDispatchChoice && plan.workerDispatchOffer.shouldOffer && process.env.CI !== 'true' && !supplied.dryRun) {
442
+ console.log('');
443
+ const outcome = await askConsent(rl, workerDispatchOfferQuestion(plan.workerDispatchOffer.detected), undefined, signal);
444
+ if (outcome.kind === 'answered')
445
+ workerDispatchChoice = outcome.value ? 'enable' : 'decline';
446
+ }
382
447
  return {
383
448
  projectDir: plan.projectDir,
384
- providers: parseProviders(providersRaw, current.providers),
449
+ providers,
385
450
  primary,
386
451
  issueProvider,
387
452
  issueTracker,
@@ -392,6 +457,7 @@ async function applyInteractivePrompts(plan, rl, signal) {
392
457
  autoCloseIssues,
393
458
  forcePushPolicy,
394
459
  requireSignedCommits,
460
+ ...(workerDispatchChoice ? { workerDispatchChoice } : {}),
395
461
  };
396
462
  }
397
463
  function printPlan(plan) {
@@ -420,12 +486,32 @@ export const setupHandler = {
420
486
  }
421
487
  const args = subcommand === 'project' ? ctx.args.slice(1) : ctx.args;
422
488
  const options = parseSetupProjectOptions({ ...ctx, args });
489
+ // #3062 review item 12: the one shared interactive decision (TTY, TERM,
490
+ // CI, DEBIAN_FRONTEND) — this used to check only `process.stdin.isTTY &&
491
+ // process.stdout.isTTY`, so a CI job with a pseudo-terminal could still
492
+ // reach these prompts.
493
+ const canPrompt = isInteractive(options.nonInteractive);
494
+ // #3062 second review, finding 6: refuse before any child process runs
495
+ // — `buildSetupProjectPlan()` below makes four `git` calls on its own,
496
+ // and this exact refusal used to happen only after building (and
497
+ // printing) the full plan. A write that can never be confirmed (no
498
+ // `--yes`, no terminal, not a dry run) is known from `options` alone.
499
+ if (!options.dryRun && !options.yes && !canPrompt) {
500
+ throw new AiwgError({
501
+ code: 'ERR_NON_INTERACTIVE_CONFIRMATION_REQUIRED',
502
+ message: 'Refusing to write project setup in a non-interactive context without --yes.',
503
+ hint: 'Re-run with `aiwg setup project --yes` or use `--dry-run` to preview only.',
504
+ exitCode: EXIT_CODES.USAGE,
505
+ });
506
+ }
423
507
  let plan = await buildSetupProjectPlan(options);
424
- const canPrompt = !options.nonInteractive && process.stdin.isTTY && process.stdout.isTTY;
425
508
  if (!options.yes && canPrompt) {
426
509
  const rl = createPromptInterface();
427
510
  try {
428
- const prompted = await applyInteractivePrompts(plan, rl, ctx.signal);
511
+ // #3062 review item 10: every field already supplied by flag is
512
+ // passed straight through so it is never re-asked — only genuinely
513
+ // missing ones prompt.
514
+ const prompted = await applyInteractivePrompts(plan, rl, ctx.signal, options);
429
515
  plan = await buildSetupProjectPlan({ ...options, ...prompted });
430
516
  }
431
517
  finally {
@@ -445,6 +531,19 @@ export const setupHandler = {
445
531
  exitCode: EXIT_CODES.CONFIG,
446
532
  });
447
533
  }
534
+ // #3050: non-interactive/--yes/CI/dry-run runs never prompt — the
535
+ // question above already applied a choice when it was asked (which
536
+ // clears `shouldOffer` on the rebuilt plan), so `shouldOffer` is only
537
+ // ever still true here when nothing asked. In `--json` mode the offer
538
+ // is structured data on the JSON object, never a separate
539
+ // human-readable line — a stray print after the JSON would make
540
+ // `setup project --json` unparseable (review item 6).
541
+ // #3062: an explicit `--worker-dispatch` argument clears `shouldOffer`
542
+ // the same way an answered prompt does, so `applied_choice` (alongside
543
+ // the top-level `dry_run` flag) is how `--json --dry-run` reports what
544
+ // the argument would have written — `previewBlocks()` deliberately
545
+ // excludes `parallelism`/`setup` (an unrelated, pre-existing contract),
546
+ // so this field carries that information instead of widening it.
448
547
  if (options.json) {
449
548
  console.log(JSON.stringify({
450
549
  project_dir: plan.projectDir,
@@ -453,21 +552,22 @@ export const setupHandler = {
453
552
  issue_provider: plan.issueProvider,
454
553
  warnings: plan.warnings,
455
554
  config: previewBlocks(plan.next),
555
+ worker_dispatch_offer: plan.workerDispatchOffer.shouldOffer
556
+ ? { should_offer: true, detected: plan.workerDispatchOffer.detected,
557
+ hint: workerDispatchOfferHintWithArg(plan.workerDispatchOffer.detected) }
558
+ : { should_offer: false, ...(options.workerDispatchChoice ? { applied_choice: options.workerDispatchChoice } : {}) },
456
559
  }, null, 2));
457
560
  }
458
561
  else {
459
562
  printPlan(plan);
563
+ if (plan.workerDispatchOffer.shouldOffer) {
564
+ ui.info(workerDispatchOfferHintWithArg(plan.workerDispatchOffer.detected));
565
+ }
460
566
  }
461
567
  if (options.dryRun)
462
568
  return { exitCode: 0 };
463
- if (!options.yes && !canPrompt) {
464
- throw new AiwgError({
465
- code: 'ERR_NON_INTERACTIVE_CONFIRMATION_REQUIRED',
466
- message: 'Refusing to write project setup in a non-interactive context without --yes.',
467
- hint: 'Re-run with `aiwg setup project --yes` or use `--dry-run` to preview only.',
468
- exitCode: EXIT_CODES.USAGE,
469
- });
470
- }
569
+ // The `!options.yes && !canPrompt` refusal above (review finding 6)
570
+ // already covers this exact condition before any git call ran.
471
571
  if (!options.yes && canPrompt) {
472
572
  const rl = createPromptInterface();
473
573
  try {
@@ -699,7 +699,9 @@ export const removeHandler = {
699
699
  // adapter validates receipt hashes and preserves operator modifications.
700
700
  const nativeRemovalProvider = parseRemoveProvider(ctx.args);
701
701
  if (firstRemovePositional(ctx.args) === 'omp' && nativeRemovalProvider.provider === 'omp') {
702
- const { uninstall } = await import('../../../tools/agents/providers/omp.mjs');
702
+ // Loaded from the package root, not by a path relative to this file: the
703
+ // compiled handler sits one directory deeper than its source.
704
+ const { uninstall } = (await import(pathToFileURL(path.join(await getFrameworkRoot(), 'tools/agents/providers/omp.mjs')).href));
703
705
  const { getProjectDir } = await import('../../config/aiwg-config.js');
704
706
  const dryRun = ctx.args.includes('--dry-run');
705
707
  const scope = ctx.args.includes('--user') || isScopeUser(ctx.args) ? 'user' : 'project';
@@ -16,15 +16,25 @@ import { join, basename, resolve } from 'node:path';
16
16
  import { pathToFileURL } from 'node:url';
17
17
  import { projectAiwgPath } from '../../config/project-artifacts.js';
18
18
  import { normalizeProviderDefinitionId } from '../../providers/provider-definitions.js';
19
+ import { isSessionProviderResolution, resolveActiveProvider } from '../provider-resolution.js';
19
20
  // ── Provider Detection ───────────────────────────────────────
20
- function detectProvider(args) {
21
+ async function detectProvider(args, cwd) {
21
22
  const providerFlag = parseFlag(args, '--provider');
22
23
  if (providerFlag)
23
24
  return normalizeProviderDefinitionId(providerFlag) ?? providerFlag;
24
- // Claude Code sets CLAUDE_CODE_VERSION; API key presence is secondary heuristic
25
- const isClaudeCode = process.env.CLAUDE_CODE_VERSION !== undefined ||
26
- process.env.ANTHROPIC_API_KEY !== undefined;
27
- return isClaudeCode ? 'claude' : 'unknown';
25
+ // Resolved through the same shared provider-resolution path `runtime-info`
26
+ // and `steward capabilities` use (`resolveActiveProvider` -> the `claude`
27
+ // provider's `detection.env` markers in provider-definitions.ts) so none
28
+ // of them disagree about which provider the current session is running
29
+ // under (#2623, #2627, #3054). The ad-hoc `CLAUDE_CODE_VERSION`/
30
+ // `ANTHROPIC_API_KEY` check this replaced was stale (neither is set by
31
+ // current Claude Code) and `ANTHROPIC_API_KEY` was a false positive for
32
+ // any non-Claude session with that key set.
33
+ // Only a session signal counts. Native team dispatch needs the command to
34
+ // be running inside that harness; project configuration alone does not
35
+ // establish that.
36
+ const resolution = await resolveActiveProvider({ cwd });
37
+ return isSessionProviderResolution(resolution) ? (resolution.provider ?? 'unknown') : 'unknown';
28
38
  }
29
39
  function isNativeTeamsProvider(provider) {
30
40
  // Only Claude Code has native agent team support
@@ -154,7 +164,7 @@ async function teamRunOmp(ctx) {
154
164
  }
155
165
  }
156
166
  async function teamRun(ctx) {
157
- if (['omp', 'oh-my-pi'].includes(detectProvider(ctx.args)))
167
+ if (['omp', 'oh-my-pi'].includes(await detectProvider(ctx.args, ctx.cwd)))
158
168
  return teamRunOmp(ctx);
159
169
  const positional = getPositionalArgs(ctx.args);
160
170
  const slug = positional[0];
@@ -162,7 +172,7 @@ async function teamRun(ctx) {
162
172
  ui.error('Usage: aiwg team run <name> [--provider <p>] [--objective "<text>"]');
163
173
  return { exitCode: 1 };
164
174
  }
165
- const provider = detectProvider(ctx.args);
175
+ const provider = await detectProvider(ctx.args, ctx.cwd);
166
176
  const objective = parseFlag(ctx.args, '--objective') || `Execute ${slug} team workflow`;
167
177
  const team = await loadTeam(slug, ctx.frameworkRoot, ctx.cwd);
168
178
  if (!team) {
@@ -221,7 +231,7 @@ async function teamRun(ctx) {
221
231
  // ── Subcommand: list ─────────────────────────────────────────
222
232
  async function teamList(ctx) {
223
233
  const json = hasFlag(ctx.args, '--json');
224
- const provider = detectProvider(ctx.args);
234
+ const provider = await detectProvider(ctx.args, ctx.cwd);
225
235
  const teams = await listAllTeams(ctx.frameworkRoot, ctx.cwd);
226
236
  if (json) {
227
237
  console.log(JSON.stringify(teams.map(t => ({
@@ -145,6 +145,11 @@ const MODEL_OVERRIDE_VALUE_FLAGS = new Set([
145
145
  '--model', '--reasoning-model', '--coding-model', '--efficiency-model', '--model-tier',
146
146
  ]);
147
147
  const MODEL_DEPLOY_BOOLEAN_FLAGS = new Set(['--save', '--save-user']);
148
+ /** Forward explicit user scope to every provider deploy subprocess. */
149
+ export function collectUseScopeDeployArgs(args) {
150
+ const scope = detectScope(args);
151
+ return scope === 'user' || args.includes('--user') ? ['--scope', 'user'] : [];
152
+ }
148
153
  export function collectUseModelDeployArgs(args) {
149
154
  const forwarded = [];
150
155
  for (let i = 0; i < args.length; i++) {
@@ -1291,6 +1296,7 @@ async function deployOneProjectLocalBundle(opts) {
1291
1296
  '--deploy-version', projectLocalDeployVersion(bundle),
1292
1297
  ...modelArgs,
1293
1298
  ];
1299
+ args.push(...collectUseScopeDeployArgs(ctx.args));
1294
1300
  if (dryRun)
1295
1301
  args.push('--dry-run');
1296
1302
  if (verbose)
@@ -2031,6 +2037,7 @@ async function deploySourceDirectory(opts) {
2031
2037
  '--target', opts.target,
2032
2038
  ...opts.modelArgs,
2033
2039
  ];
2040
+ args.push(...collectUseScopeDeployArgs(opts.ctx.args));
2034
2041
  if (opts.dryRun)
2035
2042
  args.push('--dry-run');
2036
2043
  if (opts.verbose)
@@ -2975,6 +2982,8 @@ export class UseHandler {
2975
2982
  }
2976
2983
  const runner = createScriptRunner(frameworkRoot);
2977
2984
  const addonBaseArgs = ['--deploy-commands', '--deploy-skills', '--deploy-rules'];
2985
+ if (addonScope === 'user')
2986
+ addonBaseArgs.push('--scope', 'user');
2978
2987
  // An explicitly selected upstream addon must be self-contained in the
2979
2988
  // project. Unlike a full framework deploy, its standard skills cannot be
2980
2989
  // left index-only: the user asked to install this specific bundle and
@@ -3253,6 +3262,8 @@ export class UseHandler {
3253
3262
  + ` (active: ${process.env.AIWG_USER_REGISTRY_PATH})`);
3254
3263
  }
3255
3264
  }
3265
+ if (scope === 'user' && !deployArgs.includes('--scope'))
3266
+ deployArgs.push('--scope', 'user');
3256
3267
  const filteredArgs = deployArgs.filter(a => a !== '--no-utils' && a !== '--no-project-local' && a !== '--ci-hooks-enabled' && a !== '--force' && a !== '--skip-conflicts' && a !== '--no-harness-agents');
3257
3268
  const deployFilteredArgs = removeFlagWithOptionalValue(filteredArgs, '--harness-agents');
3258
3269
  // Pass --quiet to suppress deploy-agents.mjs header/footer in default mode (#460)
@@ -3430,6 +3441,8 @@ export class UseHandler {
3430
3441
  }
3431
3442
  // Build common args for addon deployments (inherit provider and target)
3432
3443
  const addonBaseArgs = ['--deploy-commands', '--deploy-skills', '--deploy-rules'];
3444
+ if (scope === 'user')
3445
+ addonBaseArgs.push('--scope', 'user');
3433
3446
  if (bulkKernelOnly)
3434
3447
  addonBaseArgs.push('--kernel-only');
3435
3448
  addonBaseArgs.push(...modelDeployArgs);
@@ -17,6 +17,7 @@ import { getFrameworkRoot } from '../../channel/manager.mjs';
17
17
  import { updateInstallation } from '../../update/service.mjs';
18
18
  import { useHandler as useFrameworkHandler } from './use.js';
19
19
  import { projectAiwgPath } from '../../config/project-artifacts.js';
20
+ import { argsTerminatorIndex, hasPositionalOrSwitch, requireFlagValue, valueTokenIndexes } from '../flag-parsing.js';
20
21
  import { checkCollisions, } from '../../smiths/skillsmith/collision-detector.js';
21
22
  import { renderUseDeploymentResult, verifyConfiguredDeployments, } from '../services/deployment-verification.js';
22
23
  /**
@@ -326,6 +327,62 @@ export const validateMetadataHandler = {
326
327
  return result;
327
328
  },
328
329
  };
330
+ /**
331
+ * A directory is a deployed skill only if it carries a SKILL.md manifest —
332
+ * the same marker `isOwnedByNamespace()` in collision-detector.ts reads for
333
+ * ownership. Harness container/bucket directories (e.g. Claude Code's own
334
+ * `.claude/skills/synced/` sync storage, which holds UUID-named dirs with
335
+ * `manifest.json`, not SKILL.md) have no manifest of their own and must
336
+ * never be reported as a colliding skill (#278).
337
+ *
338
+ * A container directory without its own SKILL.md is still descended exactly
339
+ * one level: a real skill nested inside it (`<container>/<skill>/SKILL.md`)
340
+ * is reported as `<container>/<skill>`, while the container itself never is.
341
+ */
342
+ async function hasSkillManifest(dirPath) {
343
+ try {
344
+ await fsp.access(path.join(dirPath, 'SKILL.md'));
345
+ return true;
346
+ }
347
+ catch {
348
+ return false;
349
+ }
350
+ }
351
+ async function collectDeployedSkillDirNames(skillsDir) {
352
+ let topEntries;
353
+ try {
354
+ topEntries = await fsp.readdir(skillsDir, { withFileTypes: true });
355
+ }
356
+ catch {
357
+ return [];
358
+ }
359
+ const names = [];
360
+ for (const entry of topEntries) {
361
+ if (!entry.isDirectory())
362
+ continue;
363
+ const dirPath = path.join(skillsDir, entry.name);
364
+ if (await hasSkillManifest(dirPath)) {
365
+ names.push(entry.name);
366
+ continue;
367
+ }
368
+ let nestedEntries;
369
+ try {
370
+ nestedEntries = await fsp.readdir(dirPath, { withFileTypes: true });
371
+ }
372
+ catch {
373
+ continue;
374
+ }
375
+ for (const nested of nestedEntries) {
376
+ if (!nested.isDirectory())
377
+ continue;
378
+ const nestedPath = path.join(dirPath, nested.name);
379
+ if (await hasSkillManifest(nestedPath)) {
380
+ names.push(path.join(entry.name, nested.name));
381
+ }
382
+ }
383
+ }
384
+ return names;
385
+ }
329
386
  /**
330
387
  * #1156 Phase 1 — Validate the per-user registry at `~/.aiwg/installed.json`.
331
388
  *
@@ -450,8 +507,9 @@ Options:
450
507
  --bundle <name> Limit deployment verification to one bundle
451
508
  --scope <scope> Verification scope: project or user
452
509
  --user Alias for --scope user
510
+ --probe-workers Run bounded worker CLI version probes (default: config-only, no subprocess)
453
511
  --project-local Include project-local artifact diagnostics
454
- --json Emit machine-readable deployment verification output
512
+ --json Emit machine-readable output (with --probe-workers: one worker-probe record per CLI; with --deployment: deployment verification)
455
513
  --verbose, -v Include verbose diagnostics
456
514
  --help, -h Show this help
457
515
  `;
@@ -477,28 +535,55 @@ export const doctorHandler = {
477
535
  return { exitCode: 0, message: DOCTOR_HELP, rawOutput: true };
478
536
  },
479
537
  async execute(ctx) {
480
- if (ctx.args.includes('--help') || ctx.args.includes('-h')) {
538
+ // Validate before any early-return mode: deployment and user-scope doctor
539
+ // never invoke doctor.mjs, so they must enforce the same flag contract.
540
+ const valueFlags = ['--provider', '--bundle', '--scope', '--context-baseline', '--context-budget-tokens'];
541
+ const knownFlags = new Set([
542
+ ...valueFlags, '--all-providers', '--probe-workers', '--no-budget-check', '--strict-context', '--json',
543
+ '--deployment', '--user', '--verbose', '-v', '--project-local', '--quiet', '--help', '-h',
544
+ ]);
545
+ const hasSwitch = (name) => hasPositionalOrSwitch(ctx.args, name, valueFlags);
546
+ const values = new Map();
547
+ try {
548
+ for (const flag of valueFlags) {
549
+ values.set(flag, requireFlagValue(ctx.args, { rejectEmpty: true, precedence: 'last' }, flag));
550
+ }
551
+ const consumed = valueTokenIndexes(ctx.args, valueFlags);
552
+ for (let i = 0; i < argsTerminatorIndex(ctx.args); i += 1) {
553
+ const token = ctx.args[i];
554
+ if (consumed.has(i) || !token.startsWith('-'))
555
+ continue;
556
+ const name = token.split('=')[0];
557
+ if (!knownFlags.has(name) || (token.includes('=') && !valueFlags.includes(name))) {
558
+ throw new Error(`Unknown flag: ${name}`);
559
+ }
560
+ }
561
+ if (hasSwitch('--json') && !hasSwitch('--probe-workers') && !hasSwitch('--deployment')) {
562
+ throw new Error('--json requires --probe-workers or --deployment');
563
+ }
564
+ }
565
+ catch (error) {
566
+ return { exitCode: 2, message: `${error instanceof Error ? error.message : String(error)}. See 'aiwg doctor --help'.` };
567
+ }
568
+ if (hasSwitch('--help') || hasSwitch('-h')) {
481
569
  return { exitCode: 0, message: DOCTOR_HELP, rawOutput: true };
482
570
  }
483
- if (ctx.args.includes('--deployment')) {
484
- const providerIndex = ctx.args.indexOf('--provider');
485
- const bundleIndex = ctx.args.indexOf('--bundle');
486
- const scopeIndex = ctx.args.indexOf('--scope');
487
- const scopeValue = scopeIndex >= 0 ? ctx.args[scopeIndex + 1] : undefined;
571
+ if (hasSwitch('--deployment')) {
572
+ const scopeValue = values.get('--scope');
488
573
  if (scopeValue && scopeValue !== 'project' && scopeValue !== 'user') {
489
574
  return { exitCode: 2, message: `Invalid deployment verification scope: ${scopeValue}` };
490
575
  }
491
576
  const result = await verifyConfiguredDeployments(ctx.cwd || process.cwd(), {
492
- provider: providerIndex >= 0 ? ctx.args[providerIndex + 1] : undefined,
493
- bundle: bundleIndex >= 0 ? ctx.args[bundleIndex + 1] : undefined,
577
+ provider: values.get('--provider'),
578
+ bundle: values.get('--bundle'),
494
579
  scope: scopeValue,
495
580
  }, ctx.frameworkRoot);
496
581
  return {
497
582
  exitCode: result.exitCode,
498
- message: ctx.args.includes('--json')
583
+ message: hasSwitch('--json')
499
584
  ? JSON.stringify(result, null, 2)
500
585
  : renderUseDeploymentResult(result),
501
- rawOutput: ctx.args.includes('--json'),
586
+ rawOutput: hasSwitch('--json'),
502
587
  };
503
588
  }
504
589
  // #1156 Phase 1 — `aiwg doctor --scope user` / `aiwg doctor --user`
@@ -506,15 +591,20 @@ export const doctorHandler = {
506
591
  // running the project-scope diagnostics. Operators need this to verify
507
592
  // their user-scope deployments from any cwd, including shells with no
508
593
  // project at all.
509
- const userScopeRequested = ctx.args.includes('--user') ||
510
- (ctx.args.includes('--scope') && ctx.args[ctx.args.indexOf('--scope') + 1] === 'user');
511
- if (userScopeRequested) {
512
- return await runUserScopeDoctor(ctx.args.includes('--verbose') || ctx.args.includes('-v'));
594
+ const userScopeRequested = hasSwitch('--user') || values.get('--scope') === 'user';
595
+ if (userScopeRequested && !hasSwitch('--probe-workers')) {
596
+ return await runUserScopeDoctor(hasSwitch('--verbose') || hasSwitch('-v'));
513
597
  }
514
598
  const frameworkRoot = await getFrameworkRoot();
515
599
  const runner = createScriptRunner(frameworkRoot);
516
600
  // Run core doctor diagnostics
517
601
  const result = await runner.run('tools/cli/doctor.mjs', ctx.args, { cwd: ctx.cwd });
602
+ // Machine-readable worker probes (#3066): tools/cli/doctor.mjs already
603
+ // printed exactly one JSON document (or a usage error) on its own
604
+ // stdio — every section below writes human-readable text, so returning
605
+ // here keeps stdout to the JSON document alone.
606
+ if (hasSwitch('--json') || result.exitCode === 2)
607
+ return result;
518
608
  let agentSkillsFailure = false;
519
609
  try {
520
610
  const { inspectGrokBuildNative } = await import('../../mcp/grok-build-config.mjs');
@@ -562,14 +652,7 @@ export const doctorHandler = {
562
652
  try {
563
653
  const projectDir = ctx.cwd || process.cwd();
564
654
  const skillsDir = path.join(projectDir, '.claude', 'skills');
565
- let deployedSkillNames = [];
566
- try {
567
- const entries = await fsp.readdir(skillsDir, { withFileTypes: true });
568
- deployedSkillNames = entries.filter(e => e.isDirectory()).map(e => e.name);
569
- }
570
- catch {
571
- // No .claude/skills — nothing to check
572
- }
655
+ const deployedSkillNames = await collectDeployedSkillDirNames(skillsDir);
573
656
  if (deployedSkillNames.length > 0) {
574
657
  const collisions = await checkCollisions({
575
658
  platform: 'claude',