@aiwg/cli 2026.10.3 → 2026.10.4

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 (279) hide show
  1. package/README.md +2 -2
  2. package/agentic/code/providers/model-capabilities.v1.json +96 -4
  3. package/bin/aiwg.mjs +50 -27
  4. package/dist/src/agents/grounding/knowledge-base.js +54 -6
  5. package/dist/src/artifacts/browser-export.js +6 -0
  6. package/dist/src/artifacts/checksum-manifest.js +1 -0
  7. package/dist/src/artifacts/citation-parser.js +173 -92
  8. package/dist/src/artifacts/citation-table-schema.js +59 -0
  9. package/dist/src/artifacts/cli.js +57 -18
  10. package/dist/src/artifacts/corpus-tools/snapshot.js +1 -1
  11. package/dist/src/artifacts/corpus-tools/vision-extract.js +2 -0
  12. package/dist/src/artifacts/corpus-views/ref-parser.js +3 -1
  13. package/dist/src/artifacts/corpus-views/renderers.js +3 -3
  14. package/dist/src/artifacts/discovery-eval.js +6 -4
  15. package/dist/src/artifacts/fortemi-core-query-adapter.js +36 -3
  16. package/dist/src/artifacts/fortemi-core-sync.js +166 -40
  17. package/dist/src/artifacts/framework-capabilities.js +171 -0
  18. package/dist/src/artifacts/framework-index-paths.js +73 -0
  19. package/dist/src/artifacts/framework-source-digest.js +75 -0
  20. package/dist/src/artifacts/index-builder.js +285 -131
  21. package/dist/src/artifacts/index-files.js +132 -18
  22. package/dist/src/artifacts/index-preparation.js +65 -0
  23. package/dist/src/artifacts/index-refresh.js +576 -0
  24. package/dist/src/artifacts/index-worker.mjs +9 -0
  25. package/dist/src/artifacts/prebuilt-build-lock.js +47 -4
  26. package/dist/src/artifacts/prebuilt-deployment.js +46 -0
  27. package/dist/src/artifacts/prebuilt-relations.js +80 -0
  28. package/dist/src/artifacts/query-engine.js +410 -88
  29. package/dist/src/artifacts/relations-builder.js +168 -0
  30. package/dist/src/artifacts/relations.js +148 -0
  31. package/dist/src/artifacts/stats.js +5 -2
  32. package/dist/src/artifacts/types.js +30 -10
  33. package/dist/src/artifacts/watcher.js +1 -1
  34. package/dist/src/auth/credential-store.js +2 -1
  35. package/dist/src/catalog/cli.d.mts +5 -0
  36. package/dist/src/channel/manager.d.mts +20 -0
  37. package/dist/src/channel/manager.mjs +19 -2
  38. package/dist/src/cli/command-log.js +2 -3
  39. package/dist/src/cli/handlers/artifacts.js +3 -0
  40. package/dist/src/cli/handlers/auth.js +3 -0
  41. package/dist/src/cli/handlers/decision.js +19 -12
  42. package/dist/src/cli/handlers/diagnose.js +4 -3
  43. package/dist/src/cli/handlers/effect.js +4 -2
  44. package/dist/src/cli/handlers/evidence.js +3 -0
  45. package/dist/src/cli/handlers/help.js +1 -0
  46. package/dist/src/cli/handlers/index.js +3 -1
  47. package/dist/src/cli/handlers/init.js +4 -1
  48. package/dist/src/cli/handlers/lint.js +5 -0
  49. package/dist/src/cli/handlers/models.js +104 -8
  50. package/dist/src/cli/handlers/ralph-launcher.js +2 -0
  51. package/dist/src/cli/handlers/refresh.js +12 -0
  52. package/dist/src/cli/handlers/resource-versions.js +3 -0
  53. package/dist/src/cli/handlers/rules.js +195 -0
  54. package/dist/src/cli/handlers/run.js +4 -0
  55. package/dist/src/cli/handlers/runtime-info.js +1 -2
  56. package/dist/src/cli/handlers/script-runner.js +3 -2
  57. package/dist/src/cli/handlers/session.js +19 -9
  58. package/dist/src/cli/handlers/sessions.js +98 -37
  59. package/dist/src/cli/handlers/setup-manifest.js +39 -44
  60. package/dist/src/cli/handlers/setup.js +41 -0
  61. package/dist/src/cli/handlers/steward.js +11 -0
  62. package/dist/src/cli/handlers/subcommands.js +57 -2
  63. package/dist/src/cli/handlers/uhp.js +3 -0
  64. package/dist/src/cli/handlers/use.js +294 -55
  65. package/dist/src/cli/handlers/utilities.js +26 -10
  66. package/dist/src/cli/handlers/version.js +8 -0
  67. package/dist/src/cli/router.js +15 -1
  68. package/dist/src/cli/services/deployment-verification.js +19 -9
  69. package/dist/src/cli/services/provider-prepare-coordinator.js +24 -0
  70. package/dist/src/cli/skill-usage.js +2 -3
  71. package/dist/src/config/aiwg-config.js +87 -37
  72. package/dist/src/config/cli.js +111 -5
  73. package/dist/src/config/decision-config.js +106 -0
  74. package/dist/src/config/dispatch-thresholds-runtime.d.mts +8 -0
  75. package/dist/src/config/gitignore.js +50 -0
  76. package/dist/src/config/index-graph-overrides.js +36 -0
  77. package/dist/src/config/project-artifacts-health.d.mts +37 -0
  78. package/dist/src/config/project-artifacts-runtime.d.mts +16 -0
  79. package/dist/src/config/user-config-dir.d.mts +9 -0
  80. package/dist/src/config/user-config.js +2 -1
  81. package/dist/src/config/user-registry.js +23 -15
  82. package/dist/src/config/user-state.js +7 -0
  83. package/dist/src/decision/ask.js +203 -43
  84. package/dist/src/decision/context-live-qualification.js +2 -2
  85. package/dist/src/decision/driver.js +13 -8
  86. package/dist/src/decision/ensemble/aggregate.js +23 -10
  87. package/dist/src/decision/ensemble-study/multifact-study.js +6 -2
  88. package/dist/src/decision/entry.js +2 -15
  89. package/dist/src/decision/graph-live-qualification.js +1 -1
  90. package/dist/src/decision/graph-run.js +9 -0
  91. package/dist/src/decision/heldout/collector.js +5 -13
  92. package/dist/src/decision/heldout/contract.js +30 -7
  93. package/dist/src/decision/jev-credentials.js +15 -1
  94. package/dist/src/decision/ledger.js +131 -28
  95. package/dist/src/decision/offload-activation.js +137 -0
  96. package/dist/src/decision/offload.js +550 -0
  97. package/dist/src/decision/qualification/load.js +4 -6
  98. package/dist/src/decision/savings.js +28 -0
  99. package/dist/src/effects/types.js +5 -1
  100. package/dist/src/effects/verifiers/git.js +2 -1
  101. package/dist/src/effects/verifiers/tracker.js +26 -14
  102. package/dist/src/extensions/commands/definitions.js +19 -0
  103. package/dist/src/extensions/project-local-remove.js +36 -1
  104. package/dist/src/gates/providers/loader.js +61 -17
  105. package/dist/src/installation/manager-command.d.mts +18 -0
  106. package/dist/src/installation/manager-command.mjs +14 -2
  107. package/dist/src/installation/manager.d.mts +49 -0
  108. package/dist/src/installation/manager.mjs +78 -3
  109. package/dist/src/issues/cli.js +4 -4
  110. package/dist/src/issues/local.js +24 -5
  111. package/dist/src/jobs/runner.js +1 -1
  112. package/dist/src/lint/cli.js +27 -1
  113. package/dist/src/lint/loader.js +2 -1
  114. package/dist/src/lint/reporters.js +25 -1
  115. package/dist/src/lint/rule-activation.js +43 -0
  116. package/dist/src/lint/runner.js +137 -62
  117. package/dist/src/mcp/adapters/codex-runtime.js +7 -1
  118. package/dist/src/mcp/claude-install.d.mts +4 -0
  119. package/dist/src/mcp/claude-install.mjs +41 -0
  120. package/dist/src/mcp/cli.d.mts +1 -0
  121. package/dist/src/mcp/cli.mjs +36 -11
  122. package/dist/src/mcp/config-file.d.mts +28 -0
  123. package/dist/src/mcp/config-layers.d.mts +36 -0
  124. package/dist/src/mcp/credentials.d.mts +51 -0
  125. package/dist/src/mcp/grok-build-config.d.mts +6 -0
  126. package/dist/src/mcp/grok-build-config.mjs +1 -1
  127. package/dist/src/mcp/helpers.mjs +3 -2
  128. package/dist/src/mcp/muse-native-config.d.mts +13 -0
  129. package/dist/src/mcp/muse-native-config.mjs +1 -0
  130. package/dist/src/mcp/omp-config.d.mts +4 -0
  131. package/dist/src/mcp/registry.js +3 -93
  132. package/dist/src/mcp/registry.mjs +3 -87
  133. package/dist/src/mcp/server-config.d.mts +3 -0
  134. package/dist/src/mcp/server-config.mjs +89 -0
  135. package/dist/src/mcp/server.mjs +37 -375
  136. package/dist/src/mcp/test-simple.mjs +2 -1
  137. package/dist/src/mcp/toml-editor.d.mts +4 -0
  138. package/dist/src/mcp/toml-string.d.mts +1 -0
  139. package/dist/src/mcp/toml-strip-mcp.d.mts +1 -0
  140. package/dist/src/mcp/tool-filters.d.mts +46 -0
  141. package/dist/src/mcp/tools/decision.mjs +12 -11
  142. package/dist/src/mcp/tools/discovery.mjs +62 -55
  143. package/dist/src/mcp/tools/orchestration.mjs +12 -2
  144. package/dist/src/mcp/tools/worker-invoke.mjs +1340 -23
  145. package/dist/src/mcp/tools/worker-shell-control.mjs +52 -21
  146. package/dist/src/mcp/tools/worker-stdin-runtime.mjs +3 -3
  147. package/dist/src/mission-protocol/codecs.js +8 -0
  148. package/dist/src/models/config-loader.js +7 -2
  149. package/dist/src/models/model-capabilities.v1.json +96 -4
  150. package/dist/src/models/model-discovery.js +76 -11
  151. package/dist/src/models/provider-policy.js +101 -7
  152. package/dist/src/models/qualification-digest.d.mts +2 -0
  153. package/dist/src/models/resolver.js +12 -10
  154. package/dist/src/models/worker-direct-readiness.js +65 -44
  155. package/dist/src/models/worker-dispatch-offer.js +3 -3
  156. package/dist/src/models/worker-health-readiness.js +16 -3
  157. package/dist/src/models/worker-installation.d.mts +39 -0
  158. package/dist/src/models/worker-installation.mjs +135 -28
  159. package/dist/src/process/bounded-child-process.d.mts +84 -0
  160. package/dist/src/process/bounded-child-process.mjs +43 -0
  161. package/dist/src/process/cli-command.d.mts +16 -0
  162. package/dist/src/process/signal-child.d.mts +6 -0
  163. package/dist/src/process/signal-child.mjs +7 -0
  164. package/dist/src/providers/deployed-skill-roots.js +20 -0
  165. package/dist/src/providers/grok-build-acp.js +3 -2
  166. package/dist/src/providers/grok-build-headless.js +12 -4
  167. package/dist/src/providers/grok-build-inspect.js +1 -0
  168. package/dist/src/providers/omp-agent.d.mts +8 -0
  169. package/dist/src/providers/omp-diagnostics.d.mts +1 -0
  170. package/dist/src/providers/omp-paths.d.mts +13 -0
  171. package/dist/src/providers/skill-ownership.d.mts +9 -0
  172. package/dist/src/providers/skill-ownership.mjs +88 -0
  173. package/dist/src/rules/activation.d.mts +21 -0
  174. package/dist/src/rules/activation.mjs +62 -0
  175. package/dist/src/rules/inline-budget.d.mts +26 -0
  176. package/dist/src/rules/inline-budget.mjs +66 -0
  177. package/dist/src/rules/inventory.js +129 -0
  178. package/dist/src/rules/pin-state.js +73 -0
  179. package/dist/src/rules/pins.d.mts +14 -0
  180. package/dist/src/rules/pins.mjs +112 -0
  181. package/dist/src/rules/rule-frontmatter.schema.json +197 -0
  182. package/dist/src/serve/mission-conductor.js +38 -12
  183. package/dist/src/serve/worker-invocation.js +30 -10
  184. package/dist/src/serve/worker-stdin-frame.d.mts +10 -0
  185. package/dist/src/sessions/adapter-registry.js +23 -19
  186. package/dist/src/sessions/adapters/claude.js +39 -54
  187. package/dist/src/sessions/adapters/codex.js +82 -67
  188. package/dist/src/sessions/adapters/copilot.js +4 -4
  189. package/dist/src/sessions/adapters/cursor.js +40 -57
  190. package/dist/src/sessions/adapters/deepseek-harness.js +15 -12
  191. package/dist/src/sessions/adapters/factory.js +31 -27
  192. package/dist/src/sessions/adapters/generic.js +10 -10
  193. package/dist/src/sessions/adapters/grok-build.js +11 -11
  194. package/dist/src/sessions/adapters/grokbot.js +24 -13
  195. package/dist/src/sessions/adapters/hermes.js +27 -29
  196. package/dist/src/sessions/adapters/muse-workspace-scan.js +31 -11
  197. package/dist/src/sessions/adapters/muse.js +150 -93
  198. package/dist/src/sessions/adapters/omp.js +28 -14
  199. package/dist/src/sessions/adapters/openclaw.js +89 -79
  200. package/dist/src/sessions/adapters/opencode.js +27 -24
  201. package/dist/src/sessions/adapters/openhuman.js +18 -60
  202. package/dist/src/sessions/adapters/pi.js +29 -14
  203. package/dist/src/sessions/adapters/warp.js +3 -3
  204. package/dist/src/sessions/adapters/windsurf.js +19 -48
  205. package/dist/src/sessions/batch-contracts.js +16 -8
  206. package/dist/src/sessions/batch-import.js +51 -11
  207. package/dist/src/sessions/contracts.js +40 -2
  208. package/dist/src/sessions/coverage-preflight.js +1 -1
  209. package/dist/src/sessions/export-plan.js +4 -3
  210. package/dist/src/sessions/import-exclusions.js +72 -0
  211. package/dist/src/sessions/import-lease.js +34 -18
  212. package/dist/src/sessions/import-limits.js +37 -0
  213. package/dist/src/sessions/importer.js +60 -38
  214. package/dist/src/sessions/lifecycle.js +45 -0
  215. package/dist/src/sessions/policy.js +133 -25
  216. package/dist/src/sessions/readers.js +106 -53
  217. package/dist/src/sessions/refresh.js +106 -0
  218. package/dist/src/sessions/repository.js +147 -52
  219. package/dist/src/sessions/workspace-discovery.js +221 -56
  220. package/dist/src/sessions/workspace-matching.js +107 -0
  221. package/dist/src/skills/importer.js +1 -0
  222. package/dist/src/smiths/context-pipeline/aiwg-md.js +11 -0
  223. package/dist/src/smiths/context-pipeline/claude-hook.js +3 -26
  224. package/dist/src/smiths/context-pipeline/finalization.js +6 -5
  225. package/dist/src/smiths/context-pipeline/generator.js +21 -4
  226. package/dist/src/smiths/context-pipeline/parallelism-section.js +7 -15
  227. package/dist/src/smiths/mcpsmith/generator.js +1 -1
  228. package/dist/src/smiths/toolsmith/runtime-discovery.d.mts +10 -0
  229. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +76 -18
  230. package/dist/src/storage/backends/fs.js +18 -76
  231. package/dist/src/storage/backends/obsidian.js +1 -0
  232. package/dist/src/tracker/forge-http.js +2 -1
  233. package/dist/src/update/checker.d.mts +2 -0
  234. package/dist/src/update/service.d.mts +22 -0
  235. package/dist/src/utils/contained-filesystem.d.mts +37 -0
  236. package/dist/src/utils/contained-filesystem.mjs +683 -0
  237. package/dist/src/utils/temporary-directory.js +36 -0
  238. package/dist/src/writing/example-generator.js +1 -1
  239. package/package.json +15 -2
  240. package/schemas/models/model-catalog.v1.schema.json +1 -1
  241. package/schemas/models/provider-model-capabilities.v1.schema.json +6 -0
  242. package/tools/_resolve-impl.mjs +13 -15
  243. package/tools/agents/deploy-agents.mjs +49 -21
  244. package/tools/agents/providers/base.mjs +104 -134
  245. package/tools/agents/providers/claude.mjs +8 -24
  246. package/tools/agents/providers/codex.mjs +18 -11
  247. package/tools/agents/providers/cursor.mjs +19 -18
  248. package/tools/agents/providers/grok-build.mjs +3 -1
  249. package/tools/agents/providers/hermes.mjs +57 -47
  250. package/tools/agents/providers/openclaw.mjs +9 -4
  251. package/tools/agents/providers/pi.mjs +5 -0
  252. package/tools/agents/providers/provider-prepare-channel.mjs +5 -0
  253. package/tools/agents/providers/provider-prepare-coordinator.mjs +247 -0
  254. package/tools/agents/providers/provider-prepare-worker.mjs +84 -0
  255. package/tools/agents/providers/rule-delivery.mjs +1270 -0
  256. package/tools/agents/providers/warp.mjs +20 -26
  257. package/tools/cli/decision-offload-report.mjs +40 -0
  258. package/tools/cli/workspace-status.mjs +862 -0
  259. package/tools/plugin/package-plugins.mjs +186 -8
  260. package/tools/plugin/plugin-links.mjs +114 -0
  261. package/tools/providers/antigravity-transport.mjs +3 -1
  262. package/tools/providers/deepseek-harness-transport.mjs +6 -4
  263. package/tools/providers/omp-transport.mjs +4 -2
  264. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +1748 -0
  265. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +55 -0
  266. package/tools/ralph-external/lib/omp-adapter.mjs +1 -1
  267. package/tools/ralph-external/lib/pi-adapter.mjs +0 -4
  268. package/tools/ralph-external/lib/provider-adapter.mjs +59 -1
  269. package/tools/ralph-external/lib/recovery-probe.mjs +72 -0
  270. package/tools/ralph-external/lib/session-environment.mjs +28 -0
  271. package/tools/ralph-external/lib/systemd-client.mjs +49 -0
  272. package/tools/ralph-external/lib/worker-health.mjs +1 -1
  273. package/tools/ralph-external/lib/worker-isolation.mjs +717 -0
  274. package/tools/ralph-external/lib/worker-overflow.mjs +50 -0
  275. package/tools/ralph-external/process-monitor.mjs +1017 -0
  276. package/tools/ralph-external/session-launcher.mjs +5057 -0
  277. package/tools/scripts/resource-preflight.mjs +541 -0
  278. package/tools/skills/deploy-skills-codex.mjs +2 -1
  279. package/vscode-extension/schemas/aiwg.config.v1.json +1194 -231
@@ -19,12 +19,36 @@
19
19
  */
20
20
  import { fileURLToPath } from 'url';
21
21
  import path from 'path';
22
+ import { isDeepStrictEqual } from 'node:util';
23
+ import { normalizeProviderDefinitionId } from '../providers/provider-definitions.js';
22
24
  import { UserConfig } from './user-config.js';
23
25
  import { AiwgError, EXIT_CODES } from '../cli/errors.js';
24
26
  import { isInteractive } from '../cli/env.js';
25
27
  import { exitCodeForSpawnResult } from '../cli/child-process-result.js';
26
28
  import { projectControlPath, resolveProjectAiwgDir } from './project-artifacts.js';
27
29
  const _scriptDir = path.dirname(fileURLToPath(import.meta.url));
30
+ function warnStaleParallelism(cfg, projectDir, key, before) {
31
+ if (key !== 'parallelism' && !key.startsWith('parallelism.'))
32
+ return;
33
+ if (isDeepStrictEqual(before, getDottedPath(cfg, key)))
34
+ return;
35
+ const configured = (cfg.providers ?? []).map(normalizeProviderDefinitionId).filter((id) => !!id);
36
+ const recorded = Object.values(cfg.installed ?? {}).flatMap(entry => Object.keys(entry.deployedTo ?? {}))
37
+ .map(normalizeProviderDefinitionId).filter((id) => !!id).sort();
38
+ const providers = [...new Set([...configured, ...recorded])];
39
+ const targets = providers.length ? providers : ['<provider>'];
40
+ const lines = [
41
+ `WARNING: ${key} changed project configuration; generated workspace/provider guidance may now be stale.`,
42
+ `From project root ${path.resolve(projectDir)}, preview and refresh every configured or recorded provider context:`,
43
+ ...targets.flatMap(provider => [
44
+ ` aiwg regenerate --workspace --provider ${provider} --dry-run`,
45
+ ` aiwg regenerate --workspace --provider ${provider}`,
46
+ ]),
47
+ ' aiwg workspace-context doctor',
48
+ `Shared parallelism defaults and labels use primary provider '${cfg.providers?.[0] ?? 'unknown'}' (providers[0]); --provider selects the adapter/context target.`,
49
+ ];
50
+ console.error(lines.join('\n'));
51
+ }
28
52
  /**
29
53
  * Main CLI entry point for `aiwg config <subcommand> [args]`
30
54
  */
@@ -102,6 +126,11 @@ async function handleGet(config, args) {
102
126
  await projectConfigGet(key, args);
103
127
  return;
104
128
  }
129
+ if (key === 'rules' || key.startsWith('rules.')) {
130
+ const { loadRulePinConfigs } = await import('../rules/pins.mjs');
131
+ console.log(JSON.stringify(getDottedPath(loadRulePinConfigs(process.cwd(), undefined, config.getPath()).user, key)) ?? '(not set)');
132
+ return;
133
+ }
105
134
  const value = await config.get(key);
106
135
  if (value === undefined) {
107
136
  console.log(`(not set)`);
@@ -130,6 +159,11 @@ async function handleSet(config, args) {
130
159
  await projectConfigSet(key, value, args);
131
160
  return;
132
161
  }
162
+ if (key === 'rules' || key.startsWith('rules.')) {
163
+ await setRuleConfigKey(process.cwd(), 'user', key, value, config.getPath());
164
+ console.log(`Set --user ${key} = ${value}`);
165
+ return;
166
+ }
133
167
  await config.set(key, value);
134
168
  console.log(`Set ${key} = ${value}`);
135
169
  }
@@ -176,6 +210,7 @@ const ENUM_RULES = {
176
210
  'setup.worker_dispatch_offer': ['declined'],
177
211
  };
178
212
  const BOOLEAN_FIELDS = new Set([
213
+ 'decision.enabled',
179
214
  'delivery.delete_branch_on_merge',
180
215
  'delivery.require_ci_green',
181
216
  'delivery.require_signed_commits',
@@ -194,6 +229,10 @@ const STRING_ARRAY_FIELDS = new Set([
194
229
  // Integer fields with valid range constraints. Validated at `aiwg config set`.
195
230
  // Bounds mirror the JSON schema (vscode-extension/schemas/aiwg.config.v1.json).
196
231
  const INTEGER_FIELDS = {
232
+ 'decision.ask.passes': { min: 1, max: Number.MAX_SAFE_INTEGER },
233
+ 'decision.ask.escalation_passes': { min: 1, max: Number.MAX_SAFE_INTEGER },
234
+ 'decision.ask.savings.reasoning_tokens': { min: 0, max: 1_000_000 },
235
+ 'decision.ask.savings.output_tokens': { min: 0, max: 1_000_000 },
197
236
  'parallelism.max_parallel_subagents': { min: 1, max: 50 },
198
237
  'parallelism.max_parallel_ralph_loops': { min: 1, max: 20 },
199
238
  'parallelism.max_parallel_mc_missions': { min: 1, max: 20 },
@@ -239,7 +278,7 @@ async function projectConfigGet(key, args) {
239
278
  console.log(String(value));
240
279
  }
241
280
  }
242
- async function projectConfigSet(key, raw, args) {
281
+ async function projectConfigSet(key, raw, args, quiet = false) {
243
282
  assertSafeConfigPath(key);
244
283
  if (key === 'defaults' || key.startsWith('defaults.')) {
245
284
  throw new AiwgError({
@@ -270,7 +309,7 @@ async function projectConfigSet(key, raw, args) {
270
309
  }
271
310
  // Coerce booleans for known boolean fields
272
311
  let value = raw;
273
- if (key === 'parallelism' || /^parallelism\.(worker_dispatch|worker_mode|groups|clis|admission|budgets|health|retry|activation)(\.|$)/.test(key)) {
312
+ if (key === 'rules' || key.startsWith('rules.') || key === 'parallelism' || /^parallelism\.(worker_dispatch|worker_mode|groups|clis|admission|budgets|health|retry|activation)(\.|$)/.test(key)) {
274
313
  try {
275
314
  value = JSON.parse(raw);
276
315
  }
@@ -340,7 +379,7 @@ async function projectConfigSet(key, raw, args) {
340
379
  const intRule = INTEGER_FIELDS[key];
341
380
  if (intRule) {
342
381
  const n = Number(raw);
343
- if (!Number.isInteger(n) || n < intRule.min || n > intRule.max) {
382
+ if (!raw.trim() || !Number.isInteger(n) || n < intRule.min || n > intRule.max) {
344
383
  throw new AiwgError({
345
384
  code: 'ERR_INVALID_VALUE',
346
385
  message: `${key} must be an integer between ${intRule.min} and ${intRule.max}, got '${raw}'`,
@@ -350,9 +389,32 @@ async function projectConfigSet(key, raw, args) {
350
389
  }
351
390
  value = n;
352
391
  }
392
+ if (key === 'decision.ask.min_agreement' || key === 'decision.ask.agreement_confidence_floor') {
393
+ const n = Number(raw);
394
+ if (!raw.trim() || !Number.isFinite(n) || n < 0 || n > 1) {
395
+ throw new AiwgError({
396
+ code: 'ERR_INVALID_VALUE',
397
+ message: `${key} must be a probability from 0 to 1, got '${raw}'`,
398
+ exitCode: EXIT_CODES.USAGE,
399
+ });
400
+ }
401
+ value = n;
402
+ }
403
+ if (key === 'decision.ask.cost.llm_per_mtok' || key === 'decision.ask.cost.jev_per_mtok') {
404
+ const n = Number(raw);
405
+ if (!raw.trim() || !Number.isFinite(n) || n < 0 || n > 1_000_000) {
406
+ throw new AiwgError({ code: 'ERR_INVALID_VALUE', message: `${key} must be a non-negative number <= 1000000`, exitCode: EXIT_CODES.USAGE });
407
+ }
408
+ value = n;
409
+ }
353
410
  // Read-modify-write — preserve unrelated fields. emptyConfig() seeds the
354
411
  // base shape when no config file exists yet (e.g. brand-new project).
355
- const cfg = (await readAiwgConfig(projectDir)) ?? emptyConfig();
412
+ const { existsSync, readFileSync } = await import('node:fs');
413
+ const file = projectControlPath(projectDir, 'aiwg.config');
414
+ const cfg = key === 'rules' || key.startsWith('rules.')
415
+ ? (existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : emptyConfig())
416
+ : (await readAiwgConfig(projectDir)) ?? emptyConfig();
417
+ const before = structuredClone(getDottedPath(cfg, key));
356
418
  setDottedPath(cfg, key, value);
357
419
  const externalLinkErrors = validateExternalLinks(cfg.externalLinks);
358
420
  if (externalLinkErrors.length > 0) {
@@ -374,7 +436,10 @@ async function projectConfigSet(key, raw, args) {
374
436
  });
375
437
  }
376
438
  await writeAiwgConfig(projectDir, cfg);
377
- console.log(`Set --project ${key} = ${raw}`);
439
+ if (!quiet)
440
+ console.log(`Set --project ${key} = ${raw}`);
441
+ if (!quiet)
442
+ warnStaleParallelism(cfg, projectDir, key, before);
378
443
  }
379
444
  /** Read a dotted path from a nested record. Returns undefined when any segment is missing. */
380
445
  function getDottedPath(obj, key) {
@@ -441,6 +506,16 @@ async function handleValidate(config, args) {
441
506
  return;
442
507
  }
443
508
  const issues = await config.validate();
509
+ try {
510
+ const { loadRulePinConfigs, installedRuleCapabilities, validateRulePinCapabilities } = await import('../rules/pins.mjs');
511
+ const configs = loadRulePinConfigs(process.cwd(), undefined, config.getPath());
512
+ for (const message of validateRulePinCapabilities(configs.user.rules, installedRuleCapabilities(configs), configs.project.rules?.pin_groups)) {
513
+ issues.push({ file: 'aiwg.config', severity: 'error', message });
514
+ }
515
+ }
516
+ catch (error) {
517
+ issues.push({ file: 'aiwg.config', severity: 'error', message: error instanceof Error ? error.message : String(error) });
518
+ }
444
519
  console.log(`Config directory: ${config.getPath()}\n`);
445
520
  if (issues.length === 0) {
446
521
  console.log('✓ All config files valid');
@@ -583,6 +658,7 @@ async function projectConfigReset(key, args) {
583
658
  }
584
659
  const primary = cfg.providers[0];
585
660
  const defaults = getProviderParallelismDefaults(primary);
661
+ const before = structuredClone(getDottedPath(cfg, key));
586
662
  if (key === 'parallelism') {
587
663
  cfg.parallelism = {
588
664
  max_parallel_subagents: defaults.max_parallel_subagents,
@@ -592,6 +668,7 @@ async function projectConfigReset(key, args) {
592
668
  };
593
669
  await writeAiwgConfig(projectDir, cfg);
594
670
  console.log(`Reset --project parallelism to provider default (${primary ?? 'unknown'})`);
671
+ warnStaleParallelism(cfg, projectDir, key, before);
595
672
  return;
596
673
  }
597
674
  if (key.startsWith('parallelism.')) {
@@ -606,6 +683,7 @@ async function projectConfigReset(key, args) {
606
683
  delete current[last];
607
684
  await writeAiwgConfig(projectDir, cfg);
608
685
  console.log(`Reset --project ${key}`);
686
+ warnStaleParallelism(cfg, projectDir, key, before);
609
687
  return;
610
688
  }
611
689
  const field = key.slice('parallelism.'.length);
@@ -614,6 +692,7 @@ async function projectConfigReset(key, args) {
614
692
  cfg.parallelism[field] = defaults[field];
615
693
  await writeAiwgConfig(projectDir, cfg);
616
694
  console.log(`Reset --project ${key} to provider default (${defaults[field]})`);
695
+ warnStaleParallelism(cfg, projectDir, key, before);
617
696
  return;
618
697
  }
619
698
  }
@@ -872,4 +951,31 @@ Examples:
872
951
  aiwg config gitignore --fix
873
952
  aiwg config gitignore --check`);
874
953
  }
954
+ /** Same configuration owner and validation used by the rules command wrappers. */
955
+ export async function setRuleConfigKey(cwd, source, key, raw, userConfigDir) {
956
+ assertSafeConfigPath(key);
957
+ if (source === 'project') {
958
+ await projectConfigSet(key, raw, ['--project', '--target', cwd], true);
959
+ return;
960
+ }
961
+ const { loadRulePinConfigs, rulePinConfigPath, installedRuleCapabilities, validateRulePinCapabilities } = await import('../rules/pins.mjs');
962
+ const { mkdir, writeFile, rename } = await import('node:fs/promises');
963
+ const configs = loadRulePinConfigs(cwd, undefined, userConfigDir);
964
+ let value;
965
+ try {
966
+ value = JSON.parse(raw);
967
+ }
968
+ catch {
969
+ value = raw;
970
+ }
971
+ setDottedPath(configs.user, key, value);
972
+ const errors = validateRulePinCapabilities(configs.user.rules, installedRuleCapabilities(configs), configs.project.rules?.pin_groups);
973
+ if (errors.length)
974
+ throw new Error(errors.join('\n'));
975
+ const file = rulePinConfigPath(cwd, 'user', userConfigDir);
976
+ await mkdir(path.dirname(file), { recursive: true });
977
+ const temp = `${file}.${process.pid}.tmp`;
978
+ await writeFile(temp, JSON.stringify(configs.user, null, 2) + '\n');
979
+ await rename(temp, file);
980
+ }
875
981
  //# sourceMappingURL=cli.js.map
@@ -0,0 +1,106 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { parseYamlSimple } from './user-config.js';
4
+ import { resolveUserConfigDir } from './user-config-dir.mjs';
5
+ export function validateDecisionConfig(value) {
6
+ if (value === undefined)
7
+ return [];
8
+ if (!value || typeof value !== 'object' || Array.isArray(value))
9
+ return ['decision must be an object'];
10
+ const errors = [];
11
+ const { enabled, ask, offload_report: report } = value;
12
+ if (enabled !== undefined && typeof enabled !== 'boolean')
13
+ errors.push('decision.enabled must be a boolean');
14
+ if (report !== undefined) {
15
+ if (!report || typeof report !== 'object' || Array.isArray(report))
16
+ errors.push('decision.offload_report must be an object');
17
+ else {
18
+ for (const key of ['window_hours', 'minimum_eligible', 'repeated_fallbacks']) {
19
+ const configured = report[key];
20
+ if (configured !== undefined && (!Number.isSafeInteger(configured) || configured < (key === 'minimum_eligible' ? 0 : 1))) {
21
+ errors.push(`decision.offload_report.${key} must be a ${key === 'minimum_eligible' ? 'non-negative' : 'positive'} safe integer`);
22
+ }
23
+ }
24
+ if (report.minimum_attempt_rate !== undefined && (typeof report.minimum_attempt_rate !== 'number'
25
+ || !Number.isFinite(report.minimum_attempt_rate) || report.minimum_attempt_rate < 0 || report.minimum_attempt_rate > 1)) {
26
+ errors.push('decision.offload_report.minimum_attempt_rate must be a probability from 0 to 1');
27
+ }
28
+ for (const key of Object.keys(report))
29
+ if (!['window_hours', 'minimum_eligible', 'minimum_attempt_rate', 'repeated_fallbacks'].includes(key)) {
30
+ errors.push(`unknown decision.offload_report.${key}`);
31
+ }
32
+ }
33
+ }
34
+ if (ask === undefined)
35
+ return errors;
36
+ if (!ask || typeof ask !== 'object' || Array.isArray(ask))
37
+ return [...errors, 'decision.ask must be an object'];
38
+ const sampling = [];
39
+ for (const key of ['passes', 'escalation_passes']) {
40
+ if (ask[key] !== undefined && (!Number.isSafeInteger(ask[key]) || ask[key] <= 0)) {
41
+ sampling.push(`decision.ask.${key} must be a positive safe integer`);
42
+ }
43
+ }
44
+ if (!sampling.length && ask.escalation_passes !== undefined && ask.escalation_passes < (ask.passes ?? 5)) {
45
+ sampling.push('decision.ask.escalation_passes must be >= decision.ask.passes');
46
+ }
47
+ for (const key of ['min_agreement', 'agreement_confidence_floor']) {
48
+ if (ask[key] !== undefined && (typeof ask[key] !== 'number' || !Number.isFinite(ask[key]) || ask[key] < 0 || ask[key] > 1)) {
49
+ sampling.push(`decision.ask.${key} must be a probability from 0 to 1`);
50
+ }
51
+ }
52
+ for (const section of ['cost', 'savings']) {
53
+ const settings = ask[section];
54
+ if (settings === undefined)
55
+ continue;
56
+ if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
57
+ sampling.push(`decision.ask.${section} must be an object`);
58
+ continue;
59
+ }
60
+ const allowed = section === 'cost' ? ['llm_per_mtok', 'jev_per_mtok'] : ['caller_model_class', 'reasoning_tokens', 'output_tokens'];
61
+ for (const [key, value] of Object.entries(settings)) {
62
+ if (!allowed.includes(key))
63
+ sampling.push(`unknown decision.ask.${section}.${key}`);
64
+ else if (key === 'caller_model_class') {
65
+ if (value !== 'standard' && value !== 'reasoning')
66
+ sampling.push('decision.ask.savings.caller_model_class must be standard or reasoning');
67
+ }
68
+ else if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 1_000_000
69
+ || (section === 'savings' && !Number.isSafeInteger(value))) {
70
+ sampling.push(`decision.ask.${section}.${key} must be a non-negative ${section === 'savings' ? 'integer' : 'number'} <= 1000000`);
71
+ }
72
+ }
73
+ }
74
+ return [...errors, ...sampling];
75
+ }
76
+ function readEnabled(filePath, yaml = false) {
77
+ let content;
78
+ try {
79
+ content = readFileSync(filePath, 'utf8');
80
+ }
81
+ catch (error) {
82
+ if (error.code === 'ENOENT')
83
+ return undefined;
84
+ throw error;
85
+ }
86
+ const config = (yaml ? parseYamlSimple(content) : JSON.parse(content));
87
+ const errors = validateDecisionConfig(config.decision);
88
+ if (errors.length)
89
+ throw new Error(`${filePath}: ${errors.join('; ')}`);
90
+ return config.decision?.enabled;
91
+ }
92
+ /** Resolve only enablement; never read credentials or probe a backend. */
93
+ export function resolveDecisionEnablement(options = {}) {
94
+ const env = options.env ?? process.env;
95
+ if (env.AIWG_DECISION_ENABLED === '0' || env.AIWG_DECISION_ENABLED === '1') {
96
+ return { enabled: env.AIWG_DECISION_ENABLED === '1', source: 'env' };
97
+ }
98
+ const project = readEnabled(path.join(options.cwd ?? process.cwd(), '.aiwg', 'aiwg.config'));
99
+ if (project !== undefined)
100
+ return { enabled: project, source: 'project' };
101
+ const user = readEnabled(path.join(resolveUserConfigDir({ env, homeDir: options.homeDir }), 'config.yaml'), true);
102
+ if (user !== undefined)
103
+ return { enabled: user, source: 'user' };
104
+ return { enabled: false, source: 'default' };
105
+ }
106
+ //# sourceMappingURL=decision-config.js.map
@@ -0,0 +1,8 @@
1
+ import type { DispatchHostThresholds } from './aiwg-config.js';
2
+
3
+ export const DEFAULT_DISPATCH_HOST_THRESHOLDS: Readonly<Required<Omit<DispatchHostThresholds, 'max_workers'>>>;
4
+ export function resolveDispatchHostThresholds(
5
+ buildDispatch?: DispatchHostThresholds,
6
+ admissionHostResources?: DispatchHostThresholds,
7
+ overlay?: DispatchHostThresholds,
8
+ ): Required<Omit<DispatchHostThresholds, 'max_workers'>> & Pick<DispatchHostThresholds, 'max_workers'>;
@@ -8,9 +8,11 @@
8
8
  * @module src/config/gitignore
9
9
  */
10
10
  import * as fs from 'fs/promises';
11
+ import { existsSync, realpathSync } from 'fs';
11
12
  import * as path from 'path';
12
13
  import { execFile } from 'child_process';
13
14
  import { promisify } from 'util';
15
+ import { boundedSpawnSync } from '../process/bounded-child-process.mjs';
14
16
  const execFileAsync = promisify(execFile);
15
17
  const GITIGNORE_PROBE_BASENAME = '.aiwg-ignore-probe';
16
18
  // ===========================
@@ -205,4 +207,52 @@ export async function appendGitignore(projectRoot, patterns) {
205
207
  created: !fileExists && toAdd.length > 0,
206
208
  };
207
209
  }
210
+ /**
211
+ * Drop files git ignores (#2555).
212
+ *
213
+ * Shared by lint and artifact discovery. `git check-ignore` is asked once for
214
+ * the whole set so the semantics are git's own (negations, nested
215
+ * `.gitignore`, `core.excludesFile`) rather than a re-implementation. Outside a
216
+ * repository, or without git, every file is kept. NUL delimiters preserve
217
+ * whitespace and quoting in filenames.
218
+ */
219
+ export function dropGitignored(targetDir, files) {
220
+ if (files.length === 0)
221
+ return files;
222
+ try {
223
+ // Avoid starting Git for synthetic/source trees outside a repository. Resolve
224
+ // symlinked working directories just as Git does, and probe on every call so
225
+ // initializing a repository between scans takes effect immediately. Explicit
226
+ // Git environment and bare repositories still use Git's own discovery.
227
+ if (!process.env.GIT_DIR && !process.env.GIT_WORK_TREE && !process.env.GIT_COMMON_DIR) {
228
+ let directory = realpathSync(targetDir);
229
+ for (;;) {
230
+ if (existsSync(path.join(directory, '.git')) ||
231
+ (existsSync(path.join(directory, 'HEAD')) &&
232
+ (existsSync(path.join(directory, 'objects')) || existsSync(path.join(directory, 'commondir')))))
233
+ break;
234
+ const parent = path.dirname(directory);
235
+ if (parent === directory)
236
+ return files;
237
+ directory = parent;
238
+ }
239
+ }
240
+ const result = boundedSpawnSync('git', ['check-ignore', '-z', '--stdin'], 'quick', {
241
+ cwd: targetDir,
242
+ input: `${files.join('\0')}\0`,
243
+ encoding: 'utf8',
244
+ });
245
+ // 0 = some paths ignored, 1 = none ignored, anything else = not a repo or
246
+ // git unavailable, in which case nothing is filtered.
247
+ if (result.error || (result.status !== 0 && result.status !== 1))
248
+ return files;
249
+ const ignored = new Set((result.stdout ?? '').split('\0').filter(Boolean));
250
+ if (ignored.size === 0)
251
+ return files;
252
+ return files.filter((file) => !ignored.has(file));
253
+ }
254
+ catch {
255
+ return files;
256
+ }
257
+ }
208
258
  //# sourceMappingURL=gitignore.js.map
@@ -0,0 +1,36 @@
1
+ export const OVERRIDABLE_INDEX_GRAPHS = ['project', 'codebase'];
2
+ /** Shared by config validation and graph loading, including non-CLI callers. */
3
+ export function validateIndexGraphOverrides(overrides) {
4
+ if (overrides === undefined)
5
+ return [];
6
+ if (typeof overrides !== 'object' || overrides === null || Array.isArray(overrides)) {
7
+ return ['index.graphOverrides: must be an object mapping supported built-in graph names to overrides'];
8
+ }
9
+ const errors = [];
10
+ for (const [name, raw] of Object.entries(overrides)) {
11
+ const where = `index.graphOverrides.${name}`;
12
+ if (!OVERRIDABLE_INDEX_GRAPHS.includes(name)) {
13
+ errors.push(`${where}: unsupported built-in graph override (supported: ${OVERRIDABLE_INDEX_GRAPHS.join(', ')})`);
14
+ continue;
15
+ }
16
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
17
+ errors.push(`${where}: must be an object`);
18
+ continue;
19
+ }
20
+ const override = raw;
21
+ for (const field of Object.keys(override)) {
22
+ if (field !== 'scanDirs' && field !== 'extensions') {
23
+ errors.push(`${where}.${field}: unknown field (supported: scanDirs, extensions)`);
24
+ }
25
+ }
26
+ for (const field of ['scanDirs', 'extensions']) {
27
+ const value = override[field];
28
+ if (value !== undefined && (!Array.isArray(value) || value.length === 0 ||
29
+ !value.every(item => typeof item === 'string' && item.trim().length > 0))) {
30
+ errors.push(`${where}.${field}: must be a non-empty array of strings`);
31
+ }
32
+ }
33
+ }
34
+ return errors;
35
+ }
36
+ //# sourceMappingURL=index-graph-overrides.js.map
@@ -0,0 +1,37 @@
1
+ export type ProjectArtifactHealthClassification =
2
+ | 'local'
3
+ | 'healthy-split-root'
4
+ | 'degraded-offline'
5
+ | 'legacy-missing-control-plane'
6
+ | 'duplicated-identical'
7
+ | 'duplicated-divergent';
8
+
9
+ export interface ProjectControlFileHealth {
10
+ file: string;
11
+ local: boolean;
12
+ external: boolean;
13
+ identical: boolean | null;
14
+ }
15
+
16
+ export interface ProjectArtifactHealth {
17
+ classification: ProjectArtifactHealthClassification;
18
+ severity: 'ok' | 'warning' | 'error';
19
+ external_configured: boolean;
20
+ source: { kind: string; key?: string; path?: string };
21
+ local_control_root: string;
22
+ artifact_root: string;
23
+ external_reachable: boolean;
24
+ control_files: ProjectControlFileHealth[];
25
+ missing_local_control_files: string[];
26
+ divergent_control_files: string[];
27
+ duplicated_local_corpus_files: string[];
28
+ divergent_local_corpus_files: string[];
29
+ repairable: boolean;
30
+ action: string | null;
31
+ }
32
+
33
+ export const PROJECT_CONTROL_PLANE_FILES: readonly string[];
34
+ export function auditProjectArtifactHealth(
35
+ projectDir: string,
36
+ env?: Record<string, string | undefined>,
37
+ ): ProjectArtifactHealth;
@@ -0,0 +1,16 @@
1
+ export const DEFAULT_PROJECT_AIWG_DIR: '.aiwg';
2
+ export const AIWG_ARTIFACTS_PATH_ENV: 'AIWG_ARTIFACTS_PATH';
3
+ export const PROJECT_AIWG_LOCATION_FILE: '.aiwg-location';
4
+
5
+ export type ProjectArtifactEnv = Record<string, string | undefined>;
6
+
7
+ export function expandProjectArtifactPath(pathValue: string, projectDir: string): string;
8
+ export function parseProjectArtifactLocation(contents: string): string | null;
9
+ export function readProjectArtifactLocation(projectDir: string): string | null;
10
+ export function resolveProjectAiwgDir(projectDir: string, env?: ProjectArtifactEnv): string;
11
+ export function isProjectArtifactRootExternal(projectDir: string, env?: ProjectArtifactEnv): boolean;
12
+ export function resolveProjectAiwgDirForWrite(projectDir: string, env?: ProjectArtifactEnv): string;
13
+ export function resolveProjectControlDir(projectDir: string): string;
14
+ export function projectAiwgPath(projectDir: string, ...segments: string[]): string;
15
+ export function projectAiwgWritePath(projectDir: string, ...segments: string[]): string;
16
+ export function projectControlPath(projectDir: string, ...segments: string[]): string;
@@ -0,0 +1,9 @@
1
+ export interface UserConfigDirOptions {
2
+ configDir?: string;
3
+ env?: Record<string, string | undefined>;
4
+ homeDir?: string;
5
+ exists?: (path: string) => boolean;
6
+ }
7
+
8
+ export function resolveUserConfigDir(options?: UserConfigDirOptions): string;
9
+ export function userConfigFile(name: string, options?: UserConfigDirOptions): string;
@@ -24,6 +24,7 @@ import { resolveUserConfigDir } from './user-config-dir.mjs';
24
24
  */
25
25
  export const KNOWN_CONFIG_FILES = [
26
26
  { filename: 'config.yaml', description: 'Global AIWG user preferences' },
27
+ { filename: 'aiwg.config', description: 'Operator rule pins and capability groups' },
27
28
  { filename: 'models.json', description: 'Model tier and provider overrides' },
28
29
  { filename: 'ops.json', description: 'Ops workspace registry' },
29
30
  { filename: 'mcp-servers.json', description: 'MCP server registry (single source of truth)' },
@@ -122,7 +123,7 @@ export class UserConfig {
122
123
  try {
123
124
  await access(filePath);
124
125
  const content = await readFile(filePath, 'utf-8');
125
- if (spec.filename.endsWith('.json')) {
126
+ if (spec.filename.endsWith('.json') || spec.filename === 'aiwg.config') {
126
127
  result[spec.filename] = JSON.parse(content);
127
128
  }
128
129
  else {
@@ -12,7 +12,6 @@
12
12
  * --scope user` works from any cwd.
13
13
  */
14
14
  import { readFile, writeFile, mkdir } from 'node:fs/promises';
15
- import { existsSync } from 'node:fs';
16
15
  import { dirname } from 'node:path';
17
16
  import { userScopeConfigPath } from '../cli/scope-resolver.js';
18
17
  /**
@@ -46,28 +45,37 @@ export function emptyUserRegistry() {
46
45
  return { version: '1', installed: {} };
47
46
  }
48
47
  /**
49
- * Read the per-user registry. Returns an empty registry when the file doesn't
50
- * exist — that's the normal case before the operator's first `aiwg use --scope
51
- * user`. On parse failure returns the empty registry and writes a stderr
52
- * warning (the broken file is preserved so the operator can inspect it).
48
+ * Read the user registry while preserving whether empty means absent or
49
+ * unreadable. Callers making adoption/readiness claims must not collapse a
50
+ * corrupt or inaccessible registry into proof that an addon is missing.
53
51
  */
54
- export async function readUserRegistry() {
55
- const path = userRegistryPath();
56
- if (!existsSync(path))
57
- return emptyUserRegistry();
52
+ export async function inspectUserRegistry() {
53
+ const registryFile = userRegistryPath();
58
54
  try {
59
- const raw = await readFile(path, 'utf-8');
55
+ const raw = await readFile(registryFile, 'utf-8');
60
56
  const parsed = JSON.parse(raw);
61
- if (!parsed.installed || typeof parsed.installed !== 'object') {
62
- return emptyUserRegistry();
57
+ if (!parsed.installed || typeof parsed.installed !== 'object' || Array.isArray(parsed.installed)) {
58
+ throw new Error('installed must be an object');
63
59
  }
64
- return { version: '1', installed: parsed.installed };
60
+ return { registry: { version: '1', installed: parsed.installed }, status: 'available' };
65
61
  }
66
62
  catch (err) {
67
- process.stderr.write(`[user-registry] failed to parse ${path}: ${err instanceof Error ? err.message : String(err)} — using empty registry\n`);
68
- return emptyUserRegistry();
63
+ if (err.code === 'ENOENT') {
64
+ return { registry: emptyUserRegistry(), status: 'absent' };
65
+ }
66
+ process.stderr.write(`[user-registry] failed to parse ${registryFile}: ${err instanceof Error ? err.message : String(err)} — using empty registry\n`);
67
+ return { registry: emptyUserRegistry(), status: 'unreadable' };
69
68
  }
70
69
  }
70
+ /**
71
+ * Read the per-user registry. Returns an empty registry when the file doesn't
72
+ * exist — that's the normal case before the operator's first `aiwg use --scope
73
+ * user`. On parse failure returns the empty registry and writes a stderr
74
+ * warning (the broken file is preserved so the operator can inspect it).
75
+ */
76
+ export async function readUserRegistry() {
77
+ return (await inspectUserRegistry()).registry;
78
+ }
71
79
  /**
72
80
  * Persist the user registry. Creates the parent directory if missing.
73
81
  */
@@ -0,0 +1,7 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ /** Host-local AIWG state, shared by CLI telemetry and maintenance leases. */
4
+ export function getUserStateDir(env = process.env) {
5
+ return path.join(env.XDG_STATE_HOME || path.join(env.HOME || os.homedir(), '.local', 'state'), 'aiwg');
6
+ }
7
+ //# sourceMappingURL=user-state.js.map