@aiwg/cli 2026.10.1 → 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 (166) 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 +210 -4
  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 +75 -27
  38. package/dist/src/decision/driver.js +3 -1
  39. package/dist/src/decision/entry.js +38 -6
  40. package/dist/src/decision/heldout/collector.js +10 -1
  41. package/dist/src/decision/index.js +1 -0
  42. package/dist/src/decision/interop.js +60 -8
  43. package/dist/src/decision/ledger.js +839 -0
  44. package/dist/src/decision/validate.js +6 -3
  45. package/dist/src/effects/dispatch-verifier.js +252 -0
  46. package/dist/src/effects/index.js +1 -0
  47. package/dist/src/features/installer.js +2 -2
  48. package/dist/src/flow/graph-metadata.js +5 -0
  49. package/dist/src/gates/providers/loader.js +3 -1
  50. package/dist/src/installation/manager-command.mjs +1 -1
  51. package/dist/src/installation/manager.mjs +17 -2
  52. package/dist/src/lint/runner.js +2 -2
  53. package/dist/src/mcp/adapters/codex-runtime.js +360 -56
  54. package/dist/src/mcp/cli.mjs +587 -77
  55. package/dist/src/mcp/config-file.mjs +279 -0
  56. package/dist/src/mcp/config-layers.mjs +348 -0
  57. package/dist/src/mcp/credentials.mjs +484 -0
  58. package/dist/src/mcp/grok-build-config.mjs +16 -26
  59. package/dist/src/mcp/helpers.mjs +106 -2
  60. package/dist/src/mcp/omp-config.mjs +50 -29
  61. package/dist/src/mcp/profiles.js +97 -23
  62. package/dist/src/mcp/profiles.mjs +99 -27
  63. package/dist/src/mcp/registry.js +264 -57
  64. package/dist/src/mcp/registry.mjs +198 -45
  65. package/dist/src/mcp/server.mjs +403 -20
  66. package/dist/src/mcp/toml-string.mjs +9 -0
  67. package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
  68. package/dist/src/mcp/tool-filters.mjs +357 -0
  69. package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
  70. package/dist/src/mcp/tools/decision.mjs +21 -3
  71. package/dist/src/mcp/tools/orchestration.mjs +38 -0
  72. package/dist/src/mcp/tools/subsystems.mjs +39 -8
  73. package/dist/src/mcp/tools/worker-invoke.mjs +1539 -0
  74. package/dist/src/mcp/tools/worker-shell-control.mjs +258 -0
  75. package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
  76. package/dist/src/memory/project-registry.js +2 -2
  77. package/dist/src/mission-protocol/codecs.js +639 -1
  78. package/dist/src/mission-protocol/types.js +110 -0
  79. package/dist/src/mission-protocol/worker-status.js +2 -0
  80. package/dist/src/models/config-loader.js +70 -13
  81. package/dist/src/models/grok-build-models.js +93 -41
  82. package/dist/src/models/model-capabilities.v1.json +3091 -24
  83. package/dist/src/models/model-discovery.js +862 -63
  84. package/dist/src/models/provider-policy.js +625 -11
  85. package/dist/src/models/qualification-digest.mjs +17 -0
  86. package/dist/src/models/resolver.js +259 -0
  87. package/dist/src/models/worker-direct-readiness.js +529 -0
  88. package/dist/src/models/worker-dispatch-offer.js +169 -0
  89. package/dist/src/models/worker-health-readiness.js +131 -0
  90. package/dist/src/models/worker-installation.mjs +879 -0
  91. package/dist/src/models/wrapper-route.js +18 -0
  92. package/dist/src/ops/registry.js +9 -14
  93. package/dist/src/plugins/standalone-packager.js +4 -3
  94. package/dist/src/process/bounded-child-process.mjs +387 -0
  95. package/dist/src/process/cli-command.mjs +75 -0
  96. package/dist/src/providers/capability-matrix.js +8 -0
  97. package/dist/src/providers/capability-matrix.yaml +270 -0
  98. package/dist/src/providers/grok-build-headless.js +43 -21
  99. package/dist/src/providers/provider-definitions.js +9 -1
  100. package/dist/src/providers/provider-definitions.mjs +9 -2
  101. package/dist/src/serve/executor-registry.js +16 -24
  102. package/dist/src/serve/fleet-mission-conductor.js +9 -0
  103. package/dist/src/serve/mission-conductor.js +9 -0
  104. package/dist/src/serve/shared-host-scheduler.js +2 -0
  105. package/dist/src/serve/worker-invocation.js +538 -0
  106. package/dist/src/serve/worker-stdin-frame.mjs +107 -0
  107. package/dist/src/sessions/adapter-registry.js +45 -0
  108. package/dist/src/sessions/adapters/generic.js +37 -3
  109. package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
  110. package/dist/src/sessions/adapters/grokbot.js +423 -24
  111. package/dist/src/sessions/adapters/muse-workspace-scan.js +108 -0
  112. package/dist/src/sessions/adapters/muse.js +68 -29
  113. package/dist/src/sessions/adapters/pi.js +1 -0
  114. package/dist/src/sessions/batch-contracts.js +13 -1
  115. package/dist/src/sessions/batch-import.js +23 -42
  116. package/dist/src/sessions/candidates.js +47 -11
  117. package/dist/src/sessions/contracts.js +35 -0
  118. package/dist/src/sessions/coverage-preflight.js +134 -0
  119. package/dist/src/sessions/discovery.js +3 -0
  120. package/dist/src/sessions/grok-build-cli.js +67 -5
  121. package/dist/src/sessions/importer.js +24 -8
  122. package/dist/src/sessions/index.js +2 -0
  123. package/dist/src/sessions/repository.js +54 -13
  124. package/dist/src/sessions/workspace-discovery.js +99 -37
  125. package/dist/src/skills/adapters/clawhub.js +10 -8
  126. package/dist/src/skills/adapters/openclaw.js +10 -7
  127. package/dist/src/smiths/context-pipeline/parallelism-section.js +192 -3
  128. package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
  129. package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
  130. package/dist/src/smiths/mcpsmith/example.js +1 -1
  131. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +10 -14
  132. package/dist/src/storage/backends/logseq.js +68 -7
  133. package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
  134. package/dist/src/testing/generators/test-code-generator.js +57 -30
  135. package/dist/src/uhp/client.js +11 -1
  136. package/dist/src/uhp/mission.js +5 -0
  137. package/dist/src/update/checker.mjs +5 -5
  138. package/dist/src/update/notifier.mjs +54 -2
  139. package/dist/src/writing/fidelity.js +18 -1
  140. package/package.json +18 -1
  141. package/schemas/mission-v1.schema.json +2429 -0
  142. package/schemas/models/model-catalog.v1.schema.json +301 -0
  143. package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
  144. package/schemas/models/model-policy.v1.schema.json +14 -0
  145. package/schemas/models/provider-model-capabilities.v1.schema.json +1066 -0
  146. package/tools/_resolve-impl.mjs +39 -0
  147. package/tools/agents/deploy-agents.mjs +15 -2
  148. package/tools/agents/providers/base.mjs +49 -32
  149. package/tools/agents/providers/grokbot.mjs +11 -12
  150. package/tools/agents/providers/hermes.mjs +6 -3
  151. package/tools/agents/providers/omp.mjs +5 -2
  152. package/tools/agents/providers/openclaw-translator.mjs +19 -5
  153. package/tools/providers/deepseek-harness-transport.mjs +358 -0
  154. package/tools/providers/omp-transport.mjs +192 -0
  155. package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
  156. package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
  157. package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
  158. package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
  159. package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
  160. package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
  161. package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
  162. package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
  163. package/tools/ralph-external/lib/pi-adapter.mjs +333 -0
  164. package/tools/ralph-external/lib/provider-adapter.mjs +690 -0
  165. package/tools/ralph-external/lib/worker-health.mjs +439 -0
  166. package/vscode-extension/schemas/aiwg.config.v1.json +1953 -0
@@ -1,2 +1,112 @@
1
1
  export const MISSION_API_VERSION = 'mission.aiwg.io/v1';
2
+ // Worker dispatch payloads merged from 05 §5.1 into schemas/mission-v1.schema.json.
3
+ // The schema file is the executable contract; these types mirror its shapes.
4
+ export const WORKER_CONTRACT_REVISION = 1;
5
+ export const WORKER_BASELINE_FEATURES = [
6
+ 'worker-dispatch.v1',
7
+ 'worker-policy-origins.v1',
8
+ 'worker-admission-fencing.v1',
9
+ 'worker-claims.v1',
10
+ 'worker-activation.v1',
11
+ ];
12
+ export const WORKER_OPTIONAL_FEATURES = ['worker-health.v1'];
13
+ /** Adapter capability additionally required before Flow invokeNode. */
14
+ export const WORKER_FLOW_VALUE_FEATURE = 'worker-flow-values.v1';
15
+ export const WORKER_KNOWN_FEATURES = [
16
+ ...WORKER_BASELINE_FEATURES,
17
+ ...WORKER_OPTIONAL_FEATURES,
18
+ WORKER_FLOW_VALUE_FEATURE,
19
+ ];
20
+ export const WORKER_DEFINITIONS = [
21
+ 'workerArtifact',
22
+ 'workerEffort',
23
+ 'workerTuple',
24
+ 'workerLimits',
25
+ 'workerSelectionIntent',
26
+ 'workerBrief',
27
+ 'workerClaims',
28
+ 'workerUsage',
29
+ 'workerExecution',
30
+ 'workerVerification',
31
+ 'workerResult',
32
+ 'workerDispatch',
33
+ 'workerDirectIntent',
34
+ 'workerDirectBrief',
35
+ 'workerResolvedInvocation',
36
+ 'workerDirectVerification',
37
+ 'workerDirectResult',
38
+ 'workerHealthSummary',
39
+ 'workerEffectiveHealth',
40
+ ];
41
+ export const WORKER_DEFINITION_PRODUCERS = {
42
+ workerArtifact: 'shared-component',
43
+ workerEffort: 'shared-component',
44
+ workerTuple: 'shared-component',
45
+ workerLimits: 'shared-component',
46
+ workerSelectionIntent: 'conductor',
47
+ workerBrief: 'conductor',
48
+ workerClaims: 'worker',
49
+ workerUsage: 'shared-component',
50
+ workerExecution: 'adapter',
51
+ workerVerification: 'verifier',
52
+ workerResult: 'trusted-assembly',
53
+ workerDispatch: 'conductor',
54
+ workerDirectIntent: 'conductor',
55
+ workerDirectBrief: 'conductor',
56
+ workerResolvedInvocation: 'conductor',
57
+ workerDirectVerification: 'verifier',
58
+ workerDirectResult: 'trusted-assembly',
59
+ workerHealthSummary: 'shared-component',
60
+ workerEffectiveHealth: 'shared-component',
61
+ };
62
+ /** Bounded activity-log transition summary budget (UTF-8 bytes, marker included). */
63
+ export const HEALTH_ACTIVITY_SUMMARY_MAX_UTF8_BYTES = 1024;
64
+ /**
65
+ * Closed observer reason-code allowlist for activity summaries. Mirrors the
66
+ * `reasons` set in tools/ralph-external/lib/worker-health.mjs: B-OG-50 owns
67
+ * the observer set, B-OG-54 owns this mapping allowlist; extend them together.
68
+ * Unlisted codes project as UNCLASSIFIED#digest so free text (and any secret
69
+ * material it carries) never crosses into the shared trace.
70
+ */
71
+ export const HEALTH_KNOWN_REASON_CODES = [
72
+ 'OK',
73
+ 'AWAITING_HANDSHAKE',
74
+ 'PROCESS_ONLY',
75
+ 'ACCEPTED',
76
+ 'PROGRESS',
77
+ 'QUIET',
78
+ 'NO_PROGRESS',
79
+ 'STARTUP_DEADLINE',
80
+ 'HARD_DEADLINE',
81
+ 'STEP_LIMIT',
82
+ 'AUTH',
83
+ 'RATE_LIMIT',
84
+ 'NETWORK',
85
+ 'MODEL',
86
+ 'TRUST',
87
+ 'STDIN_WAIT',
88
+ 'SANDBOX',
89
+ 'NOT_FOUND',
90
+ 'RESOURCE',
91
+ 'PROVIDER_ERROR',
92
+ 'CRASH',
93
+ 'EXIT_ERROR',
94
+ 'FINAL_INVALID',
95
+ 'CANCELLED',
96
+ 'DISCONNECTED',
97
+ 'OBSERVER_GAP',
98
+ 'CAPTURE_WRITE',
99
+ 'OUTPUT_LIMIT',
100
+ 'MALFORMED_FRAME',
101
+ 'SOURCE_GAP',
102
+ 'IDENTITY_CHANGED',
103
+ ];
104
+ export class WorkerContractError extends Error {
105
+ code;
106
+ constructor(code, message) {
107
+ super(message);
108
+ this.name = 'WorkerContractError';
109
+ this.code = code;
110
+ }
111
+ }
2
112
  //# sourceMappingURL=types.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=worker-status.js.map
@@ -9,15 +9,18 @@ import { readFile } from 'fs/promises';
9
9
  import { resolve } from 'path';
10
10
  import { homedir } from 'os';
11
11
  import { existsSync } from 'fs';
12
- import { validateUserProjectModelConfig } from './provider-policy.js';
12
+ import { workerConfigDigest, validateWorkerGroupReferences } from '../config/aiwg-config.js';
13
+ import { DEFAULT_WORKER_SELECTION, mergeWorkerSelection, WorkerSelectionPolicySchema, validateUserProjectModelConfig } from './provider-policy.js';
13
14
  /**
14
15
  * Loads and merges model configurations
15
16
  */
16
17
  export class ConfigLoader {
17
18
  cache = null;
19
+ cacheDigest = null;
20
+ inputDigests = null;
18
21
  locations;
19
- constructor(projectPath) {
20
- const aiwgHome = process.env.AIWG_HOME || resolve(homedir(), '.local/share/ai-writing-guide');
22
+ constructor(projectPath, options) {
23
+ const aiwgHome = options?.aiwgHome || process.env.AIWG_HOME || resolve(homedir(), '.local/share/ai-writing-guide');
21
24
  const userConfigDir = process.env.AIWG_CONFIG || resolve(homedir(), '.config/aiwg');
22
25
  this.locations = {
23
26
  aiwgDefault: resolve(aiwgHome, 'agentic/code/frameworks/sdlc-complete/config/models-v2.json'),
@@ -29,17 +32,65 @@ export class ConfigLoader {
29
32
  * Load and merge all configurations
30
33
  * Precedence: project > user > AIWG default
31
34
  */
32
- async load() {
33
- if (this.cache) {
34
- return this.cache;
35
- }
35
+ async load(parallelism) {
36
36
  // Load all configs
37
37
  const aiwgDefault = await this.loadAiwgDefault();
38
38
  const userConfig = await this.loadUserConfig();
39
39
  const projectConfig = await this.loadProjectConfig();
40
+ const digest = workerConfigDigest([aiwgDefault, userConfig, projectConfig, parallelism]);
41
+ if (this.cache && this.cacheDigest === digest)
42
+ return this.cache;
40
43
  // Merge with precedence
41
44
  const merged = this.merge(aiwgDefault, userConfig, projectConfig);
45
+ // A group-bound wrapper's inherited exact pin survives a higher-precedence
46
+ // tier-only preference. Preserve the legacy unbound merge otherwise.
47
+ if (parallelism?.worker_dispatch) {
48
+ for (const [agent, override] of Object.entries(projectConfig?.agentOverrides ?? {})) {
49
+ merged.agentOverrides[agent] = { ...userConfig?.agentOverrides?.[agent], ...override };
50
+ }
51
+ }
52
+ if (parallelism?.worker_dispatch || [aiwgDefault, userConfig, projectConfig].some(config => config?.defaults?.workerSelection !== undefined)) {
53
+ const result = mergeWorkerSelection([
54
+ { source: this.locations.aiwgDefault, authority: 'builtin', digest: workerConfigDigest(aiwgDefault), policy: DEFAULT_WORKER_SELECTION },
55
+ { source: this.locations.aiwgDefault, authority: 'builtin', digest: workerConfigDigest(aiwgDefault), policy: aiwgDefault.defaults.workerSelection },
56
+ { source: this.locations.userConfig, authority: 'user', digest: workerConfigDigest(userConfig), policy: userConfig?.defaults?.workerSelection },
57
+ { source: this.locations.projectConfig, authority: 'project', digest: workerConfigDigest(projectConfig), policy: projectConfig?.defaults?.workerSelection },
58
+ ]);
59
+ if (result.diagnostics.length)
60
+ throw new Error(result.diagnostics.map(d => `${d.code}: ${d.pointer}: ${d.message}; origins=${JSON.stringify(d.origins ?? [])}`).join('; '));
61
+ merged.defaults.workerSelection = result.policy;
62
+ merged.workerPolicyOrigins = result.origins;
63
+ }
64
+ if (parallelism?.worker_dispatch) {
65
+ // Exact wrapper pins remain explainable alongside worker restrictions.
66
+ for (const layer of [
67
+ { config: userConfig, source: this.locations.userConfig, authority: 'user' },
68
+ { config: projectConfig, source: this.locations.projectConfig, authority: 'project' },
69
+ ]) {
70
+ for (const [agent, override] of Object.entries(layer.config?.agentOverrides ?? {})) {
71
+ if (override['model-override'] === undefined)
72
+ continue;
73
+ const pointer = `/agentOverrides/${agent.replace(/~/g, '~0').replace(/\//g, '~1')}/model-override`;
74
+ merged.workerPolicyOrigins[pointer] ??= [];
75
+ merged.workerPolicyOrigins[pointer].push({
76
+ source: layer.source, authority: layer.authority, digest: workerConfigDigest(layer.config),
77
+ pointer, contribution: override['model-override'],
78
+ });
79
+ }
80
+ }
81
+ const errors = validateWorkerGroupReferences(parallelism, merged.defaults.workerSelection);
82
+ if (errors.length)
83
+ throw new Error(errors.join('; '));
84
+ }
85
+ // Publish only a fully validated candidate. A failed merge must not become
86
+ // a cache hit on the next read, nor replace the prior inspectable value.
42
87
  this.cache = merged;
88
+ this.cacheDigest = digest;
89
+ this.inputDigests = {
90
+ builtinModelsDigest: workerConfigDigest(aiwgDefault),
91
+ userModelsDigest: workerConfigDigest(userConfig),
92
+ projectModelsDigest: workerConfigDigest(projectConfig),
93
+ };
43
94
  return merged;
44
95
  }
45
96
  /**
@@ -53,6 +104,8 @@ export class ConfigLoader {
53
104
  if (!config.version || !config.tiers || !config.providers) {
54
105
  throw new Error('Invalid AIWG default config: missing required v2 fields');
55
106
  }
107
+ if (config.defaults?.workerSelection !== undefined)
108
+ WorkerSelectionPolicySchema.parse(config.defaults.workerSelection);
56
109
  return config;
57
110
  }
58
111
  catch (error) {
@@ -63,9 +116,6 @@ export class ConfigLoader {
63
116
  * Load user configuration (optional)
64
117
  */
65
118
  async loadUserConfig() {
66
- if (!existsSync(this.locations.userConfig)) {
67
- return null;
68
- }
69
119
  try {
70
120
  const content = await readFile(this.locations.userConfig, 'utf-8');
71
121
  const parsed = JSON.parse(content);
@@ -76,6 +126,8 @@ export class ConfigLoader {
76
126
  return parsed;
77
127
  }
78
128
  catch (error) {
129
+ if (error.code === 'ENOENT')
130
+ return null;
79
131
  throw new Error(`Invalid user model config ${this.locations.userConfig}: ${error}`);
80
132
  }
81
133
  }
@@ -83,9 +135,6 @@ export class ConfigLoader {
83
135
  * Load project configuration (optional)
84
136
  */
85
137
  async loadProjectConfig() {
86
- if (!existsSync(this.locations.projectConfig)) {
87
- return null;
88
- }
89
138
  try {
90
139
  const content = await readFile(this.locations.projectConfig, 'utf-8');
91
140
  const parsed = JSON.parse(content);
@@ -96,6 +145,8 @@ export class ConfigLoader {
96
145
  return parsed;
97
146
  }
98
147
  catch (error) {
148
+ if (error.code === 'ENOENT')
149
+ return null;
99
150
  throw new Error(`Invalid project model config ${this.locations.projectConfig}: ${error}`);
100
151
  }
101
152
  }
@@ -217,6 +268,12 @@ export class ConfigLoader {
217
268
  */
218
269
  clearCache() {
219
270
  this.cache = null;
271
+ this.cacheDigest = null;
272
+ this.inputDigests = null;
273
+ }
274
+ /** Digests of the last successful read; no marker or snapshot writes. */
275
+ getInputDigests() {
276
+ return this.inputDigests ? { ...this.inputDigests } : null;
220
277
  }
221
278
  /**
222
279
  * Get configuration file locations
@@ -33,6 +33,8 @@ function stringArray(value) {
33
33
  if (!text.startsWith('[') || !text.endsWith(']'))
34
34
  return undefined;
35
35
  const matches = [...text.matchAll(/"((?:\\.|[^"\\])*)"|'([^']*)'/g)];
36
+ if (!/^\[\s*(?:s\s*(?:,\s*s\s*)*,?\s*)?\]$/.test(text.replace(/"((?:\\.|[^"\\])*)"|'([^']*)'/g, 's')))
37
+ return undefined;
36
38
  return matches.map(match => match[1] !== undefined
37
39
  ? JSON.parse(`"${match[1]}"`)
38
40
  : match[2]);
@@ -55,17 +57,25 @@ function mapKeys(value) {
55
57
  }
56
58
  /** Parse only non-secret fields needed for model discovery. */
57
59
  export function parseGrokBuildModelConfig(text, scope) {
58
- const parsed = { models: new Map(), projectModelFields: false };
60
+ const parsed = { models: new Map(), projectModelFields: false, diagnostics: [] };
59
61
  let table = '';
60
- for (const raw of text.split(/\r?\n/)) {
62
+ for (const [index, raw] of text.split(/\r?\n/).entries()) {
63
+ const invalid = (field) => parsed.diagnostics.push(`line ${index + 1}: malformed ${field}`);
61
64
  const nextTable = tableName(raw);
62
65
  if (nextTable !== undefined) {
63
66
  table = nextTable;
64
67
  continue;
65
68
  }
69
+ if (/^\s*\[models?(?:[.\]\s]|$)/.test(raw)) {
70
+ invalid('model table');
71
+ continue;
72
+ }
66
73
  const match = raw.match(/^\s*([A-Za-z0-9_-]+)\s*=\s*(.*?)\s*(?:#.*)?$/);
67
- if (!match)
74
+ if (!match) {
75
+ if (/^models?(?:\.|$)/.test(table) && raw.trim() && !raw.trim().startsWith('#'))
76
+ invalid('model setting');
68
77
  continue;
78
+ }
69
79
  const [, key, value] = match;
70
80
  const top = table.split('.', 1)[0];
71
81
  if (scope === 'project' && !PROJECT_ALLOWED.has(top)) {
@@ -80,6 +90,10 @@ export function parseGrokBuildModelConfig(text, scope) {
80
90
  parsed.allowedModels = stringArray(value);
81
91
  else if (key === 'disabled_models')
82
92
  parsed.disabledModels = stringArray(value);
93
+ if (key === 'default' && !parsed.defaultModel
94
+ || key === 'allowed_models' && !parsed.allowedModels
95
+ || key === 'disabled_models' && !parsed.disabledModels)
96
+ invalid(key);
83
97
  continue;
84
98
  }
85
99
  const alias = modelAlias(table);
@@ -108,49 +122,76 @@ export function parseGrokBuildModelConfig(text, scope) {
108
122
  model.supportsBackendSearch = value.trim() === 'true';
109
123
  else if (key === 'reasoning_efforts')
110
124
  model.reasoningEfforts = stringArray(value);
125
+ if (['model', 'name', 'base_url', 'api_base_url'].includes(key) && unquote(value) === undefined
126
+ || key === 'env_key' && unquote(value) === undefined && stringArray(value) === undefined
127
+ || ['hidden', 'supports_backend_search'].includes(key) && !/^(true|false)$/.test(value.trim())
128
+ || key === 'context_window' && (!Number.isSafeInteger(Number(value)) || Number(value) < 1)
129
+ || key === 'reasoning_efforts' && !model.reasoningEfforts)
130
+ invalid(key);
111
131
  parsed.models.set(alias, model);
112
132
  }
113
133
  return parsed;
114
134
  }
115
- function inspectLayers(report) {
135
+ function inspectLayers(report, diagnostics) {
116
136
  const sources = (report.configSources ?? report.config_sources);
117
- if (!sources || !Array.isArray(sources.layers))
137
+ if (!sources || !Array.isArray(sources.layers)) {
138
+ if (sources !== undefined)
139
+ diagnostics.push('Grok Build /configSources/layers: expected array');
118
140
  return [];
141
+ }
119
142
  return sources.layers.flatMap((entry, index) => {
120
- if (!entry || typeof entry !== 'object')
143
+ if (!entry || typeof entry !== 'object') {
144
+ diagnostics.push(`Grok Build /configSources/layers/${index}: malformed config layer`);
121
145
  return [];
146
+ }
122
147
  const item = entry;
123
- return typeof item.role === 'string' && typeof item.path === 'string'
124
- ? [{ role: item.role, path: item.path, ...(typeof item.note === 'string' ? { note: item.note } : {}), precedence: index + 1 }]
125
- : [];
126
- });
127
- }
128
- function inspectModels(report) {
129
- const candidates = [report.models, report.modelCatalog, report.model_catalog]
130
- .find(Array.isArray);
131
- if (!candidates)
132
- return [];
133
- return candidates.flatMap(entry => {
134
- if (typeof entry === 'string')
135
- return [{ id: entry, alias: entry }];
136
- if (!entry || typeof entry !== 'object')
148
+ if (typeof item.role !== 'string' || !item.role || typeof item.path !== 'string' || !item.path) {
149
+ diagnostics.push(`Grok Build /configSources/layers/${index}: missing role or path`);
137
150
  return [];
138
- const model = entry;
139
- const alias = [model.alias, model.key, model.id].find(value => typeof value === 'string');
140
- if (!alias)
141
- return [];
142
- return [{
143
- id: alias,
144
- alias,
145
- ...(typeof model.name === 'string' ? { displayName: model.name } : {}),
146
- ...(model.default === true || model.isDefault === true ? { isDefault: true } : {}),
147
- ...(model.hidden === true ? { hidden: true } : {}),
148
- ...(Array.isArray(model.reasoningEfforts)
149
- ? { reasoningEfforts: model.reasoningEfforts.filter((item) => typeof item === 'string') }
150
- : {}),
151
- }];
151
+ }
152
+ return [{ role: item.role, path: item.path, ...(typeof item.note === 'string' ? { note: item.note } : {}), precedence: index + 1 }];
152
153
  });
153
154
  }
155
+ function inspectModels(report, diagnostics) {
156
+ for (const key of ['models', 'modelCatalog', 'model_catalog']) {
157
+ if (!(key in report))
158
+ continue;
159
+ const candidates = report[key];
160
+ if (!Array.isArray(candidates)) {
161
+ diagnostics.push(`Grok Build /${key}: expected model array`);
162
+ continue;
163
+ }
164
+ return candidates.flatMap((entry, index) => {
165
+ const invalid = () => {
166
+ diagnostics.push(`Grok Build /${key}/${index}: malformed model row`);
167
+ return [];
168
+ };
169
+ if (typeof entry === 'string')
170
+ return entry.trim() ? [{ id: entry, alias: entry }] : invalid();
171
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
172
+ return invalid();
173
+ const model = entry;
174
+ const alias = [model.alias, model.key, model.id].find(value => typeof value === 'string');
175
+ if (!alias?.trim()
176
+ || (model.name !== undefined && typeof model.name !== 'string')
177
+ || ['default', 'isDefault', 'hidden'].some(field => model[field] !== undefined && typeof model[field] !== 'boolean')
178
+ || (model.reasoningEfforts !== undefined && (!Array.isArray(model.reasoningEfforts)
179
+ || model.reasoningEfforts.some(item => typeof item !== 'string' || !item.trim()))))
180
+ return invalid();
181
+ return [{
182
+ id: alias,
183
+ alias,
184
+ ...(typeof model.name === 'string' ? { displayName: model.name } : {}),
185
+ ...(model.default === true || model.isDefault === true ? { isDefault: true } : {}),
186
+ ...(model.hidden === true ? { hidden: true } : {}),
187
+ ...(Array.isArray(model.reasoningEfforts)
188
+ ? { reasoningEfforts: model.reasoningEfforts.filter((item) => typeof item === 'string') }
189
+ : {}),
190
+ }];
191
+ });
192
+ }
193
+ return [];
194
+ }
154
195
  function setSelected(selected, key, value, layer, constraint) {
155
196
  if (value === undefined)
156
197
  return;
@@ -174,7 +215,7 @@ function parseEnvOverlay(env) {
174
215
  if (!content || typeof content !== 'object' || Array.isArray(content))
175
216
  return null;
176
217
  const value = content;
177
- const parsed = { models: new Map(), projectModelFields: false };
218
+ const parsed = { models: new Map(), projectModelFields: false, diagnostics: [] };
178
219
  const settings = value.models;
179
220
  if (settings && typeof settings === 'object' && !Array.isArray(settings)) {
180
221
  const table = settings;
@@ -184,6 +225,13 @@ function parseEnvOverlay(env) {
184
225
  parsed.allowedModels = table.allowed_models;
185
226
  if (Array.isArray(table.disabled_models) && table.disabled_models.every(entry => typeof entry === 'string'))
186
227
  parsed.disabledModels = table.disabled_models;
228
+ if (table.default !== undefined && (typeof table.default !== 'string' || !table.default)
229
+ || table.allowed_models !== undefined && !parsed.allowedModels
230
+ || table.disabled_models !== undefined && !parsed.disabledModels)
231
+ parsed.diagnostics.push('/models: malformed model settings');
232
+ }
233
+ else if (settings !== undefined) {
234
+ parsed.diagnostics.push('/models: expected object');
187
235
  }
188
236
  // The host allowlists env-overlay sections; per-model secrets and model definitions
189
237
  // are intentionally not imported from arbitrary JSON here.
@@ -239,7 +287,8 @@ export async function discoverGrokBuildModels(options = {}) {
239
287
  error: 'Grok Build inspect returned an unsupported JSON shape; update Grok Build or AIWG.' };
240
288
  }
241
289
  }
242
- const layers = inspectLayers(report);
290
+ const validationDiagnostics = [];
291
+ const layers = inspectLayers(report, validationDiagnostics);
243
292
  if (layers.length === 0 && !Object.keys(report).some(key => ['models', 'modelCatalog', 'model_catalog'].includes(key))) {
244
293
  return { ...base, ...(runtimeVersion ? { runtimeVersion } : {}), errorKind: 'invalid-output',
245
294
  error: 'Grok Build inspect JSON omitted configSources; update Grok Build or AIWG.' };
@@ -247,7 +296,7 @@ export async function discoverGrokBuildModels(options = {}) {
247
296
  const diagnostics = [];
248
297
  const selected = {};
249
298
  const constraints = [];
250
- const discovered = inspectModels(report);
299
+ const discovered = inspectModels(report, validationDiagnostics);
251
300
  const parsedByLayer = await Promise.all(layers.map(layer => layer.role === 'env_overlay'
252
301
  && layer.path.includes('(inline)') ? parseEnvOverlay(env) : readLayer(layer)));
253
302
  let requirementsDefault;
@@ -255,8 +304,11 @@ export async function discoverGrokBuildModels(options = {}) {
255
304
  for (let i = 0; i < layers.length; i++) {
256
305
  const layer = layers[i];
257
306
  const parsed = parsedByLayer[i];
258
- if (!parsed)
307
+ if (!parsed) {
308
+ validationDiagnostics.push(`${layer.path}: config layer could not be read or parsed within bounds`);
259
309
  continue;
310
+ }
311
+ validationDiagnostics.push(...parsed.diagnostics.map(diagnostic => `${layer.path}: ${diagnostic}`));
260
312
  if (parsed.projectModelFields)
261
313
  diagnostics.push(`${layer.path}: ignored model settings because Grok Build project scope does not allow them`);
262
314
  const requirement = /requirements|mdm/i.test(layer.role);
@@ -334,10 +386,10 @@ export async function discoverGrokBuildModels(options = {}) {
334
386
  policyConstraint: /requirements|mdm/i.test(layer.role) })),
335
387
  selected,
336
388
  constraints,
337
- diagnostics,
389
+ diagnostics: [...validationDiagnostics, ...diagnostics],
338
390
  },
339
- ...(models.length === 0 ? { errorKind: 'invalid-output',
340
- error: 'Grok Build exposed no model aliases. Configure [models].default or [model.<alias>] in $GROK_HOME/config.toml; AIWG will not fabricate an xAI model ID.' } : {}),
391
+ ...(validationDiagnostics.length || models.length === 0 ? { errorKind: 'invalid-output',
392
+ error: validationDiagnostics.join('; ') || 'Grok Build exposed no model aliases. Configure [models].default or [model.<alias>] in $GROK_HOME/config.toml; AIWG will not fabricate an xAI model ID.' } : {}),
341
393
  };
342
394
  }
343
395
  /** Resolve aliases only from the observed catalog. Exact API model ids are never synthesized. */