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.
- package/README.md +96 -89
- package/bin/specrails-core.mjs +282 -39
- package/bin/tui-installer.mjs +117 -149
- package/commands/doctor.md +1 -1
- package/dist/installer/cli.js +13 -3
- package/dist/installer/cli.js.map +1 -1
- package/dist/installer/commands/doctor.js +487 -27
- package/dist/installer/commands/doctor.js.map +1 -1
- package/dist/installer/commands/framework.js +49 -7
- package/dist/installer/commands/framework.js.map +1 -1
- package/dist/installer/commands/init.js +443 -41
- package/dist/installer/commands/init.js.map +1 -1
- package/dist/installer/commands/update.js +51 -23
- package/dist/installer/commands/update.js.map +1 -1
- package/dist/installer/commands/v5-migration.js +119 -0
- package/dist/installer/commands/v5-migration.js.map +1 -0
- package/dist/installer/phases/framework-lifecycle.js +125 -0
- package/dist/installer/phases/framework-lifecycle.js.map +1 -0
- package/dist/installer/phases/install-config.js +160 -11
- package/dist/installer/phases/install-config.js.map +1 -1
- package/dist/installer/phases/manifest.js +29 -8
- package/dist/installer/phases/manifest.js.map +1 -1
- package/dist/installer/phases/prereqs.js +57 -3
- package/dist/installer/phases/prereqs.js.map +1 -1
- package/dist/installer/phases/provider-detect.js +116 -6
- package/dist/installer/phases/provider-detect.js.map +1 -1
- package/dist/installer/phases/scaffold.js +1217 -117
- package/dist/installer/phases/scaffold.js.map +1 -1
- package/dist/installer/runtime/kimi.js +255 -0
- package/dist/installer/runtime/kimi.js.map +1 -0
- package/dist/installer/util/paths.js +12 -0
- package/dist/installer/util/paths.js.map +1 -1
- package/dist/installer/util/registry.js +234 -14
- package/dist/installer/util/registry.js.map +1 -1
- package/docs/README.md +1 -0
- package/docs/deployment.md +6 -7
- package/docs/getting-started.md +11 -7
- package/docs/installation.md +34 -16
- package/docs/plugin-architecture.md +11 -8
- package/docs/updating.md +21 -3
- package/docs/user-docs/cli-reference.md +43 -22
- package/docs/user-docs/codex-vs-claude-code.md +11 -9
- package/docs/user-docs/faq.md +1 -1
- package/docs/user-docs/getting-started-codex.md +5 -8
- package/docs/user-docs/getting-started-kimi.md +423 -0
- package/docs/user-docs/installation.md +49 -14
- package/docs/user-docs/quick-start.md +11 -8
- package/docs/windows.md +29 -4
- package/integration-contract.json +85 -13
- package/package.json +9 -5
- package/schemas/profile.v1.json +68 -6
- package/templates/agents/sr-architect.md +30 -0
- package/templates/agents/sr-developer.md +21 -8
- package/templates/agents/sr-reviewer.md +44 -31
- package/templates/codex-skills/batch-implement/SKILL.md +9 -32
- package/templates/codex-skills/implement/SKILL.md +61 -143
- package/templates/codex-skills/rails/sr-architect/SKILL.md +38 -20
- package/templates/codex-skills/rails/sr-developer/SKILL.md +29 -10
- package/templates/codex-skills/rails/sr-reviewer/SKILL.md +21 -10
- package/templates/commands/specrails/doctor.md +1 -1
- package/templates/commands/specrails/implement.md +117 -288
- package/templates/commands/specrails/memory-inspect.md +6 -4
- package/templates/commands/specrails/propose-spec.md +1 -1
- package/templates/commands/specrails/refactor-recommender.md +8 -51
- package/templates/commands/specrails/retry.md +12 -48
- package/templates/commands/specrails/telemetry.md +1 -1
- package/templates/gemini-commands/implement.toml +9 -0
- package/templates/kimi/specrails/run-skill.mjs +3005 -0
- package/templates/kimi/specrails/vendor/js-yaml/LICENSE +21 -0
- package/templates/kimi/specrails/vendor/js-yaml/NOTICE.md +16 -0
- package/templates/kimi/specrails/vendor/js-yaml/js-yaml.mjs +3856 -0
- package/templates/profiles/default.json +5 -18
- package/templates/profiles/kimi-default.json +15 -0
- package/commands/enrich.md +0 -1456
- package/templates/agents/sr-backend-developer.md +0 -91
- package/templates/agents/sr-backend-reviewer.md +0 -152
- package/templates/agents/sr-doc-sync.md +0 -247
- package/templates/agents/sr-frontend-developer.md +0 -85
- package/templates/agents/sr-frontend-reviewer.md +0 -145
- package/templates/agents/sr-merge-resolver.md +0 -195
- package/templates/agents/sr-performance-reviewer.md +0 -186
- package/templates/agents/sr-product-analyst.md +0 -36
- package/templates/agents/sr-product-manager.md +0 -148
- package/templates/agents/sr-security-reviewer.md +0 -191
- package/templates/agents/sr-test-writer.md +0 -176
- package/templates/codex-skills/enrich/SKILL.md +0 -191
- package/templates/codex-skills/merge-resolve/SKILL.md +0 -88
- package/templates/codex-skills/rails/sr-backend-developer/SKILL.md +0 -93
- package/templates/codex-skills/rails/sr-backend-reviewer/SKILL.md +0 -120
- package/templates/codex-skills/rails/sr-doc-sync/SKILL.md +0 -124
- package/templates/codex-skills/rails/sr-frontend-developer/SKILL.md +0 -106
- package/templates/codex-skills/rails/sr-frontend-reviewer/SKILL.md +0 -111
- package/templates/codex-skills/rails/sr-merge-resolver/SKILL.md +0 -156
- package/templates/codex-skills/rails/sr-performance-reviewer/SKILL.md +0 -109
- package/templates/codex-skills/rails/sr-product-analyst/SKILL.md +0 -85
- package/templates/codex-skills/rails/sr-product-manager/SKILL.md +0 -131
- package/templates/codex-skills/rails/sr-security-reviewer/SKILL.md +0 -121
- package/templates/codex-skills/rails/sr-test-writer/SKILL.md +0 -115
- package/templates/commands/specrails/auto-propose-backlog-specs.md +0 -312
- package/templates/commands/specrails/enrich.md +0 -1456
- package/templates/commands/specrails/get-backlog-specs.md +0 -226
- package/templates/commands/specrails/merge-resolve.md +0 -172
- package/templates/commands/specrails/reconfig.md +0 -80
- package/templates/commands/specrails/vpc-drift.md +0 -405
- package/templates/commands/test.md +0 -58
- package/templates/personas/persona.md +0 -43
- package/templates/personas/the-maintainer.md +0 -98
- package/templates/settings/perf-thresholds.yml +0 -25
package/bin/specrails-core.mjs
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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/
|
|
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 —
|
|
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
|
|
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
|
|
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
|
|
251
|
-
|
|
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(
|
|
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 (
|
|
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
|
+
}
|