specrails-core 4.11.3 → 5.0.0

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 (108) hide show
  1. package/README.md +96 -89
  2. package/bin/specrails-core.mjs +282 -39
  3. package/bin/tui-installer.mjs +117 -149
  4. package/commands/doctor.md +1 -1
  5. package/dist/installer/cli.js +13 -3
  6. package/dist/installer/cli.js.map +1 -1
  7. package/dist/installer/commands/doctor.js +487 -27
  8. package/dist/installer/commands/doctor.js.map +1 -1
  9. package/dist/installer/commands/framework.js +49 -7
  10. package/dist/installer/commands/framework.js.map +1 -1
  11. package/dist/installer/commands/init.js +443 -41
  12. package/dist/installer/commands/init.js.map +1 -1
  13. package/dist/installer/commands/update.js +51 -23
  14. package/dist/installer/commands/update.js.map +1 -1
  15. package/dist/installer/commands/v5-migration.js +119 -0
  16. package/dist/installer/commands/v5-migration.js.map +1 -0
  17. package/dist/installer/phases/framework-lifecycle.js +125 -0
  18. package/dist/installer/phases/framework-lifecycle.js.map +1 -0
  19. package/dist/installer/phases/install-config.js +160 -11
  20. package/dist/installer/phases/install-config.js.map +1 -1
  21. package/dist/installer/phases/manifest.js +29 -8
  22. package/dist/installer/phases/manifest.js.map +1 -1
  23. package/dist/installer/phases/prereqs.js +57 -3
  24. package/dist/installer/phases/prereqs.js.map +1 -1
  25. package/dist/installer/phases/provider-detect.js +116 -6
  26. package/dist/installer/phases/provider-detect.js.map +1 -1
  27. package/dist/installer/phases/scaffold.js +1217 -117
  28. package/dist/installer/phases/scaffold.js.map +1 -1
  29. package/dist/installer/runtime/kimi.js +255 -0
  30. package/dist/installer/runtime/kimi.js.map +1 -0
  31. package/dist/installer/util/paths.js +12 -0
  32. package/dist/installer/util/paths.js.map +1 -1
  33. package/dist/installer/util/registry.js +234 -14
  34. package/dist/installer/util/registry.js.map +1 -1
  35. package/docs/README.md +1 -0
  36. package/docs/deployment.md +6 -7
  37. package/docs/getting-started.md +11 -7
  38. package/docs/installation.md +34 -16
  39. package/docs/plugin-architecture.md +11 -8
  40. package/docs/updating.md +21 -3
  41. package/docs/user-docs/cli-reference.md +43 -22
  42. package/docs/user-docs/codex-vs-claude-code.md +11 -9
  43. package/docs/user-docs/faq.md +1 -1
  44. package/docs/user-docs/getting-started-codex.md +5 -8
  45. package/docs/user-docs/getting-started-kimi.md +423 -0
  46. package/docs/user-docs/installation.md +49 -14
  47. package/docs/user-docs/quick-start.md +11 -8
  48. package/docs/windows.md +29 -4
  49. package/integration-contract.json +85 -13
  50. package/package.json +9 -5
  51. package/schemas/profile.v1.json +68 -6
  52. package/templates/agents/sr-architect.md +30 -0
  53. package/templates/agents/sr-developer.md +21 -8
  54. package/templates/agents/sr-reviewer.md +44 -31
  55. package/templates/codex-skills/batch-implement/SKILL.md +9 -32
  56. package/templates/codex-skills/implement/SKILL.md +61 -143
  57. package/templates/codex-skills/rails/sr-architect/SKILL.md +38 -20
  58. package/templates/codex-skills/rails/sr-developer/SKILL.md +29 -10
  59. package/templates/codex-skills/rails/sr-reviewer/SKILL.md +21 -10
  60. package/templates/commands/specrails/doctor.md +1 -1
  61. package/templates/commands/specrails/implement.md +117 -288
  62. package/templates/commands/specrails/memory-inspect.md +6 -4
  63. package/templates/commands/specrails/propose-spec.md +1 -1
  64. package/templates/commands/specrails/refactor-recommender.md +8 -51
  65. package/templates/commands/specrails/retry.md +12 -48
  66. package/templates/commands/specrails/telemetry.md +1 -1
  67. package/templates/gemini-commands/implement.toml +9 -0
  68. package/templates/kimi/specrails/run-skill.mjs +3005 -0
  69. package/templates/kimi/specrails/vendor/js-yaml/LICENSE +21 -0
  70. package/templates/kimi/specrails/vendor/js-yaml/NOTICE.md +16 -0
  71. package/templates/kimi/specrails/vendor/js-yaml/js-yaml.mjs +3856 -0
  72. package/templates/profiles/default.json +5 -18
  73. package/templates/profiles/kimi-default.json +15 -0
  74. package/commands/enrich.md +0 -1456
  75. package/templates/agents/sr-backend-developer.md +0 -91
  76. package/templates/agents/sr-backend-reviewer.md +0 -152
  77. package/templates/agents/sr-doc-sync.md +0 -247
  78. package/templates/agents/sr-frontend-developer.md +0 -85
  79. package/templates/agents/sr-frontend-reviewer.md +0 -145
  80. package/templates/agents/sr-merge-resolver.md +0 -195
  81. package/templates/agents/sr-performance-reviewer.md +0 -186
  82. package/templates/agents/sr-product-analyst.md +0 -36
  83. package/templates/agents/sr-product-manager.md +0 -148
  84. package/templates/agents/sr-security-reviewer.md +0 -191
  85. package/templates/agents/sr-test-writer.md +0 -176
  86. package/templates/codex-skills/enrich/SKILL.md +0 -191
  87. package/templates/codex-skills/merge-resolve/SKILL.md +0 -88
  88. package/templates/codex-skills/rails/sr-backend-developer/SKILL.md +0 -93
  89. package/templates/codex-skills/rails/sr-backend-reviewer/SKILL.md +0 -120
  90. package/templates/codex-skills/rails/sr-doc-sync/SKILL.md +0 -124
  91. package/templates/codex-skills/rails/sr-frontend-developer/SKILL.md +0 -106
  92. package/templates/codex-skills/rails/sr-frontend-reviewer/SKILL.md +0 -111
  93. package/templates/codex-skills/rails/sr-merge-resolver/SKILL.md +0 -156
  94. package/templates/codex-skills/rails/sr-performance-reviewer/SKILL.md +0 -109
  95. package/templates/codex-skills/rails/sr-product-analyst/SKILL.md +0 -85
  96. package/templates/codex-skills/rails/sr-product-manager/SKILL.md +0 -131
  97. package/templates/codex-skills/rails/sr-security-reviewer/SKILL.md +0 -121
  98. package/templates/codex-skills/rails/sr-test-writer/SKILL.md +0 -115
  99. package/templates/commands/specrails/auto-propose-backlog-specs.md +0 -312
  100. package/templates/commands/specrails/enrich.md +0 -1456
  101. package/templates/commands/specrails/get-backlog-specs.md +0 -226
  102. package/templates/commands/specrails/merge-resolve.md +0 -172
  103. package/templates/commands/specrails/reconfig.md +0 -80
  104. package/templates/commands/specrails/vpc-drift.md +0 -405
  105. package/templates/commands/test.md +0 -58
  106. package/templates/personas/persona.md +0 -43
  107. package/templates/personas/the-maintainer.md +0 -98
  108. package/templates/settings/perf-thresholds.yml +0 -25
@@ -6,8 +6,6 @@
6
6
  * in-process via the Node installer under dist/installer/. This file
7
7
  * only keeps logic that is local to the dispatcher:
8
8
  * - `profile validate` / `profile show` — schema validation via ajv.
9
- * - `enrich` — spawns Claude Code with
10
- * the /specrails:enrich command.
11
9
  * - `init` TUI short-circuit — spawns tui-installer.mjs
12
10
  * then re-enters init with
13
11
  * --from-config.
@@ -31,7 +29,11 @@ const subcommand = args[0]
31
29
 
32
30
  // ─── Global --version / -V flag ──────────────────────────────────────────────
33
31
 
34
- if (args.includes('--version') || args.includes('-V')) {
32
+ // Treat version as a GLOBAL flag only when it is the command itself. Offline
33
+ // lifecycle commands also carry a required `--version <target>` option; scanning
34
+ // the whole argv would intercept `swap-current --version 4.12.0` here and exit 0
35
+ // without ever validating or moving the framework pointer.
36
+ if (subcommand === '--version' || subcommand === '-V') {
35
37
  const pkg = require_(path.resolve(ROOT, 'package.json'))
36
38
  console.log(`specrails-core v${pkg.version}`)
37
39
  process.exit(0)
@@ -44,6 +46,15 @@ if (!subcommand) {
44
46
  process.exit(0)
45
47
  }
46
48
 
49
+ // ─── Removed-in-v5 subcommands (clear error, not a generic "unknown") ────────
50
+
51
+ const REMOVED_SUBCOMMANDS = new Set(['enrich'])
52
+
53
+ if (REMOVED_SUBCOMMANDS.has(subcommand)) {
54
+ console.error(`${subcommand} was removed in v5 — init now installs everything directly.`)
55
+ process.exit(1)
56
+ }
57
+
47
58
  // ─── Subcommand allowlist (kept for backwards compatibility) ─────────────────
48
59
 
49
60
  const KNOWN_SUBCOMMANDS = new Set([
@@ -51,8 +62,8 @@ const KNOWN_SUBCOMMANDS = new Set([
51
62
  'update',
52
63
  'doctor',
53
64
  'install-framework',
65
+ 'swap-current',
54
66
  'assemble',
55
- 'enrich',
56
67
  'version',
57
68
  'profile',
58
69
  'help',
@@ -61,11 +72,18 @@ const KNOWN_SUBCOMMANDS = new Set([
61
72
  if (!KNOWN_SUBCOMMANDS.has(subcommand)) {
62
73
  console.error(`Unknown command: ${subcommand}\n`)
63
74
  console.error(
64
- 'Available commands: init, update, doctor, install-framework, assemble, enrich, version, profile, help',
75
+ 'Available commands: init, update, doctor, install-framework, swap-current, assemble, version, profile, help',
65
76
  )
66
77
  process.exit(1)
67
78
  }
68
79
 
80
+ // ─── Removed-in-v5 flags (clear error) ───────────────────────────────────────
81
+
82
+ if (args.includes('--quick') || args.includes('--lite')) {
83
+ console.error('--quick was removed in v5 — init now installs everything directly.')
84
+ process.exit(1)
85
+ }
86
+
69
87
  const subargs = args.slice(1)
70
88
 
71
89
  // ─── help / version handled by the dispatcher ────────────────────────────────
@@ -89,32 +107,6 @@ if (subcommand === 'profile') {
89
107
  process.exit(0)
90
108
  }
91
109
 
92
- // ─── enrich ──────────────────────────────────────────────────────────────────
93
- // Launches Claude Code with the /specrails:enrich slash command.
94
-
95
- if (subcommand === 'enrich') {
96
- const enrichFlags = subargs.join(' ')
97
- const claudeCmd = `/specrails:enrich${enrichFlags ? ' ' + enrichFlags : ''}`
98
- const result = spawnSync(
99
- 'claude',
100
- ['--command', claudeCmd, '--dangerously-skip-permissions'],
101
- {
102
- stdio: 'inherit',
103
- cwd: process.cwd(),
104
- shell: process.platform === 'win32',
105
- },
106
- )
107
- if (result.error) {
108
- console.error(
109
- '\nFailed to launch Claude CLI for enrich:',
110
- result.error.message,
111
- '\nEnsure Claude Code is installed: npm install -g @anthropic-ai/claude-code\n',
112
- )
113
- process.exit(1)
114
- }
115
- process.exit(result.status ?? (result.error ? 1 : 0))
116
- }
117
-
118
110
  // ─── init: optional TUI short-circuit ────────────────────────────────────────
119
111
  // Default flow runs the TUI to collect agent/model configuration and
120
112
  // write .specrails/install-config.yaml, then falls through to the Node
@@ -144,6 +136,7 @@ if (subcommand === 'init') {
144
136
  const tuiArgs = [path.resolve(ROOT, 'bin', 'tui-installer.mjs'), rootDir]
145
137
  if (providerVal) tuiArgs.push('--provider', providerVal)
146
138
  else if (providerEqArg) tuiArgs.push(providerEqArg)
139
+ if (subargs.includes('--with-profiles')) tuiArgs.push('--with-profiles')
147
140
  const tuiResult = spawnSync('node', tuiArgs, {
148
141
  stdio: 'inherit',
149
142
  cwd: process.cwd(),
@@ -163,7 +156,7 @@ if (subcommand === 'init') {
163
156
  }
164
157
 
165
158
  // TUI succeeded — re-enter init with --from-config so the Node
166
- // command reads provider/tier from install-config.yaml.
159
+ // command reads provider/agents from install-config.yaml.
167
160
  const nextArgs = subargs
168
161
  .filter((a) => a !== '--no-tui' && a !== '--no-direct')
169
162
  .concat(['--yes', '--from-config'])
@@ -189,20 +182,24 @@ process.exit(0)
189
182
  // ─────────────────────────────────────────────────────────────────────────────
190
183
 
191
184
  function printUsage() {
192
- console.log(`specrails-core — Agent Workflow System for Claude Code
185
+ console.log(`specrails-core — Provider-independent AI agent workflow system
193
186
 
194
187
  Usage:
195
188
  specrails-core init [--root-dir <path>] [--yes|-y] [--no-tui] Install into a repository
196
189
  specrails-core update [--only <component>] [--dry-run] Update an existing installation
197
190
  specrails-core doctor Run health checks
198
- specrails-core enrich [--from-config <path>] Run /specrails:enrich via Claude CLI
191
+ specrails-core install-framework --framework-dir <path> --provider <value> --version <value>
192
+ Materialize an offline framework version
193
+ specrails-core swap-current --framework-dir <path> --version <value> [--providers <csv>]
194
+ Validate and atomically expose a framework version
195
+ specrails-core assemble --workspace <path> --framework-dir <path> Assemble a workspace from the framework
199
196
  specrails-core profile <validate|show> [<path>] Validate or pretty-print a profile JSON
200
197
  specrails-core version Show installed version
201
198
 
202
199
  Flags for init:
203
200
  --root-dir <path> Target repository path (default: current directory)
204
201
  --yes | -y Non-interactive; use defaults, skip TUI
205
- --provider <value> Force provider: claude (codex coming soon)
202
+ --provider <value> Force provider: claude, codex, gemini, or kimi
206
203
  --no-tui Skip TUI; use defaults / flags directly
207
204
  --from-config Skip TUI; use existing .specrails/install-config.yaml
208
205
 
@@ -212,6 +209,174 @@ Global flags:
212
209
  More info: https://github.com/fjpulidop/specrails-core`)
213
210
  }
214
211
 
212
+ function resolveEnrichOptions(cwd, argv, artifactRoot = cwd) {
213
+ const workflowArgs = []
214
+ let explicitProvider
215
+ let model
216
+ for (let index = 0; index < argv.length; index++) {
217
+ const token = argv[index]
218
+ if (token === '--provider' || token === '--model') {
219
+ const value = argv[++index]
220
+ if (!value) {
221
+ console.error(`${token} requires a value`)
222
+ process.exit(1)
223
+ }
224
+ if (token === '--provider') explicitProvider = value
225
+ else model = value
226
+ continue
227
+ }
228
+ if (token.startsWith('--provider=')) {
229
+ explicitProvider = token.slice('--provider='.length)
230
+ continue
231
+ }
232
+ if (token.startsWith('--model=')) {
233
+ model = token.slice('--model='.length)
234
+ continue
235
+ }
236
+ workflowArgs.push(token)
237
+ }
238
+
239
+ const supported = new Set(['claude', 'codex', 'gemini', 'kimi'])
240
+ if (explicitProvider && !supported.has(explicitProvider)) {
241
+ console.error(
242
+ `Unsupported provider "${explicitProvider}". Expected claude, codex, gemini, or kimi.`,
243
+ )
244
+ process.exit(1)
245
+ }
246
+
247
+ let provider = explicitProvider
248
+ if (!provider) {
249
+ for (const root of [cwd, artifactRoot]) {
250
+ const manifestPath = path.join(
251
+ root,
252
+ '.specrails',
253
+ 'specrails-manifest.json',
254
+ )
255
+ if (existsSync(manifestPath)) {
256
+ try {
257
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
258
+ if (supported.has(manifest.primary_provider)) {
259
+ provider = manifest.primary_provider
260
+ break
261
+ }
262
+ } catch {
263
+ // A malformed manifest is diagnosed by `specrails-core doctor`;
264
+ // enrich continues to config/directory fallback.
265
+ }
266
+ }
267
+ }
268
+ }
269
+ if (!provider) {
270
+ for (const root of [cwd, artifactRoot]) {
271
+ const configPath = path.join(root, '.specrails', 'install-config.yaml')
272
+ if (!existsSync(configPath)) continue
273
+ const match = readFileSync(configPath, 'utf8').match(
274
+ /^provider:\s*(claude|codex|gemini|kimi)\s*$/m,
275
+ )
276
+ if (match) {
277
+ provider = match[1]
278
+ break
279
+ }
280
+ }
281
+ }
282
+ if (!provider) {
283
+ provider =
284
+ ['claude', 'codex', 'gemini', 'kimi'].find((candidate) =>
285
+ existsSync(
286
+ path.join(
287
+ artifactRoot,
288
+ candidate === 'kimi' ? '.kimi-code' : `.${candidate}`,
289
+ ),
290
+ ),
291
+ ) ?? 'claude'
292
+ }
293
+ return { provider, model, workflowArgs }
294
+ }
295
+
296
+ async function resolveEnrichWorkspace(cwd) {
297
+ const registryModulePath = path.resolve(
298
+ ROOT,
299
+ 'dist',
300
+ 'installer',
301
+ 'util',
302
+ 'registry.js',
303
+ )
304
+ if (!existsSync(registryModulePath)) {
305
+ return { artifactRoot: cwd, codeRoot: cwd }
306
+ }
307
+ try {
308
+ const registry = await import(pathToFileURL(registryModulePath).href)
309
+ const resolution = registry.resolveArtifacts(cwd, { allocate: false })
310
+ return {
311
+ artifactRoot: path.resolve(resolution.artifactRoot),
312
+ codeRoot: path.resolve(resolution.codeRoot),
313
+ }
314
+ } catch {
315
+ // Doctor reports malformed registry state. Enrich retains the legacy
316
+ // in-repository fallback instead of guessing another relocated workspace.
317
+ return { artifactRoot: cwd, codeRoot: cwd }
318
+ }
319
+ }
320
+
321
+ function resolveConfiguredEnrichModel(codeRoot, artifactRoot) {
322
+ const activeProfile = process.env.SPECRAILS_PROFILE_PATH
323
+ if (activeProfile) {
324
+ const activePath = path.isAbsolute(activeProfile)
325
+ ? activeProfile
326
+ : path.resolve(codeRoot, activeProfile)
327
+ const activeModel = readProfileOrchestratorModel(activePath)
328
+ if (activeModel) return activeModel
329
+ }
330
+ const configCandidates = [
331
+ path.join(codeRoot, '.specrails', 'install-config.yaml'),
332
+ path.join(artifactRoot, '.specrails', 'install-config.yaml'),
333
+ ]
334
+ for (const configPath of configCandidates) {
335
+ if (!existsSync(configPath)) continue
336
+ try {
337
+ const config = require_('js-yaml').load(readFileSync(configPath, 'utf8'))
338
+ const configured = config?.models?.defaults?.model
339
+ if (typeof configured === 'string' && configured.trim() !== '') {
340
+ return configured.trim()
341
+ }
342
+ } catch {
343
+ // The installer/doctor owns full config diagnostics. Continue to a
344
+ // provider profile rather than extracting a value from malformed YAML.
345
+ }
346
+ }
347
+ const profileCandidates = [
348
+ path.join(artifactRoot, '.specrails', 'profiles', 'kimi-default.json'),
349
+ path.join(codeRoot, '.specrails', 'profiles', 'kimi-default.json'),
350
+ ]
351
+ for (const profilePath of profileCandidates) {
352
+ const configured = readProfileOrchestratorModel(profilePath)
353
+ if (configured) return configured
354
+ }
355
+ return null
356
+ }
357
+
358
+ function readProfileOrchestratorModel(profilePath) {
359
+ if (!existsSync(profilePath)) return null
360
+ try {
361
+ const profile = JSON.parse(readFileSync(profilePath, 'utf8'))
362
+ const configured = profile?.orchestrator?.model
363
+ return typeof configured === 'string' && configured.trim() !== ''
364
+ ? configured.trim()
365
+ : null
366
+ } catch {
367
+ // Profile validation provides the actionable error. Fall through.
368
+ return null
369
+ }
370
+ }
371
+
372
+ function serializeWorkflowArgs(argv) {
373
+ return argv
374
+ .map((value) =>
375
+ value === '' || /\s|["']/u.test(value) ? JSON.stringify(value) : value,
376
+ )
377
+ .join(' ')
378
+ }
379
+
215
380
  /**
216
381
  * Loads the compiled Node CLI (dist/installer/cli.js) and invokes its
217
382
  * `main` function with the given argv array. Propagates the returned
@@ -244,11 +409,33 @@ async function runProfile(subargs) {
244
409
  process.exit(1)
245
410
  }
246
411
 
412
+ const workspace = await resolveEnrichWorkspace(process.cwd())
247
413
  const resolveProfilePath = () => {
248
414
  if (pathArg) return path.resolve(pathArg)
249
415
  if (process.env.SPECRAILS_PROFILE_PATH) return path.resolve(process.env.SPECRAILS_PROFILE_PATH)
250
- const projectDefault = path.resolve(process.cwd(), '.specrails', 'profiles', 'project-default.json')
251
- if (existsSync(projectDefault)) return projectDefault
416
+ const configRoots = [workspace.codeRoot, workspace.artifactRoot]
417
+ let provider
418
+ for (const root of configRoots) {
419
+ const configPath = path.join(root, '.specrails', 'install-config.yaml')
420
+ if (!existsSync(configPath)) continue
421
+ const match = readFileSync(configPath, 'utf8').match(
422
+ /^provider:\s*(claude|codex|gemini|kimi)\s*$/m,
423
+ )
424
+ if (match) {
425
+ provider = match[1]
426
+ break
427
+ }
428
+ }
429
+ const names =
430
+ provider === 'kimi'
431
+ ? ['kimi-default.json', 'project-default.json']
432
+ : ['project-default.json', 'kimi-default.json']
433
+ for (const root of [workspace.artifactRoot, workspace.codeRoot]) {
434
+ for (const name of names) {
435
+ const candidate = path.join(root, '.specrails', 'profiles', name)
436
+ if (existsSync(candidate)) return candidate
437
+ }
438
+ }
252
439
  return null
253
440
  }
254
441
 
@@ -256,7 +443,9 @@ async function runProfile(subargs) {
256
443
  if (!profilePath) {
257
444
  console.error('No profile path given and none could be resolved.')
258
445
  console.error('Pass an explicit path or set SPECRAILS_PROFILE_PATH, or place a profile at')
259
- console.error(' .specrails/profiles/project-default.json')
446
+ console.error(
447
+ ' .specrails/profiles/project-default.json (or kimi-default.json for Kimi)',
448
+ )
260
449
  process.exit(1)
261
450
  }
262
451
  if (!existsSync(profilePath)) {
@@ -298,8 +487,12 @@ async function runProfile(subargs) {
298
487
  const schema = JSON.parse(readFileSync(schemaPath, 'utf8'))
299
488
  const ajv = new Ajv({ allErrors: true, strict: false })
300
489
  const validate = ajv.compile(schema)
490
+ const schemaValid = validate(profile)
491
+ const semanticErrors = schemaValid
492
+ ? validateProfileSemantics(profile)
493
+ : []
301
494
 
302
- if (validate(profile)) {
495
+ if (schemaValid && semanticErrors.length === 0) {
303
496
  console.log(`✓ ${profilePath} is a valid v1 profile.`)
304
497
  process.exit(0)
305
498
  }
@@ -307,5 +500,55 @@ async function runProfile(subargs) {
307
500
  for (const err of validate.errors || []) {
308
501
  console.error(` ${err.instancePath || '/'} ${err.message} (${JSON.stringify(err.params)})`)
309
502
  }
503
+ for (const err of semanticErrors) {
504
+ console.error(` ${err}`)
505
+ }
310
506
  process.exit(1)
311
507
  }
508
+
509
+ /**
510
+ * JSON Schema validates each profile entry in isolation. These invariants span
511
+ * multiple array entries and therefore require a second, semantic validation
512
+ * pass after AJV has established the profile's structural shape.
513
+ */
514
+ function validateProfileSemantics(profile) {
515
+ const errors = []
516
+ const firstAgentIndex = new Map()
517
+
518
+ for (const [index, agent] of profile.agents.entries()) {
519
+ const previous = firstAgentIndex.get(agent.id)
520
+ if (previous !== undefined) {
521
+ errors.push(
522
+ `/agents/${index}/id duplicates agent id "${agent.id}" first declared at /agents/${previous}/id`,
523
+ )
524
+ } else {
525
+ firstAgentIndex.set(agent.id, index)
526
+ }
527
+ }
528
+
529
+ const knownAgents = new Set(firstAgentIndex.keys())
530
+ const defaultIndices = []
531
+ for (const [index, rule] of profile.routing.entries()) {
532
+ if (!knownAgents.has(rule.agent)) {
533
+ errors.push(
534
+ `/routing/${index}/agent references unknown agent "${rule.agent}"`,
535
+ )
536
+ }
537
+ if (rule.default === true) {
538
+ defaultIndices.push(index)
539
+ }
540
+ }
541
+
542
+ if (defaultIndices.length > 1) {
543
+ errors.push(
544
+ `/routing must contain at most one default rule (found ${defaultIndices.length})`,
545
+ )
546
+ }
547
+ for (const index of defaultIndices) {
548
+ if (index !== profile.routing.length - 1) {
549
+ errors.push(`/routing/${index} default rule must be the last routing entry`)
550
+ }
551
+ }
552
+
553
+ return errors
554
+ }