specrails-core 4.11.3 โ 4.12.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 +56 -20
- package/bin/specrails-core.mjs +343 -20
- package/bin/tui-installer.mjs +103 -52
- package/dist/installer/cli.js +1 -1
- package/dist/installer/cli.js.map +1 -1
- package/dist/installer/commands/doctor.js +486 -24
- 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 +423 -25
- package/dist/installer/commands/init.js.map +1 -1
- package/dist/installer/commands/update.js +36 -9
- package/dist/installer/commands/update.js.map +1 -1
- 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 +157 -5
- package/dist/installer/phases/install-config.js.map +1 -1
- package/dist/installer/phases/manifest.js +27 -2
- package/dist/installer/phases/manifest.js.map +1 -1
- package/dist/installer/phases/prereqs.js +57 -2
- 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 +1222 -12
- 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 +67 -5
- 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/kimi-default.json +15 -0
package/README.md
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
[](https://github.com/fjpulidop/specrails-core)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://www.npmjs.com/package/specrails-core)
|
|
7
|
-
[-lightgrey)](https://github.com/openai/codex)
|
|
7
|
+
[](#provider-support)
|
|
9
8
|
|
|
10
9
|
**Your agentic development team. From idea to production code.**
|
|
11
10
|
|
|
@@ -13,12 +12,12 @@ One command turns your repo into a spec-driven pipeline with a team of specializ
|
|
|
13
12
|
|
|
14
13
|
```bash
|
|
15
14
|
npx specrails-core@latest init # install into the current repo
|
|
16
|
-
/specrails:enrich #
|
|
15
|
+
/specrails:enrich # Claude/Gemini (Kimi: /skill:specrails-enrich)
|
|
17
16
|
```
|
|
18
17
|
|
|
19
|
-
> **Requirements:**
|
|
20
|
-
>
|
|
21
|
-
>
|
|
18
|
+
> **Requirements:** one supported AI CLI, git, and Node 20.19.0+. Cross-platform:
|
|
19
|
+
> macOS, Linux, and Windows. Use `--provider claude|codex|gemini|kimi` to
|
|
20
|
+
> override auto-detection.
|
|
22
21
|
|
|
23
22
|
---
|
|
24
23
|
|
|
@@ -44,7 +43,8 @@ npx specrails-core@latest init
|
|
|
44
43
|
|
|
45
44
|
The TUI asks you to pick a tier:
|
|
46
45
|
|
|
47
|
-
- **Quick** (default) โ agents and commands installed
|
|
46
|
+
- **Quick** (default) โ provider-native agents/skills and commands are installed
|
|
47
|
+
under `.claude/`, `.codex/`, `.gemini/`, or `.kimi-code/`. No AI interaction.
|
|
48
48
|
- **Full** โ same as Quick plus `/specrails:enrich` (5-phase deep analysis: stack detection, VPC personas, competitive research). ~5 min.
|
|
49
49
|
|
|
50
50
|
```bash
|
|
@@ -61,15 +61,34 @@ That's it. The pipeline takes over.
|
|
|
61
61
|
|
|
62
62
|
---
|
|
63
63
|
|
|
64
|
+
## Provider support
|
|
65
|
+
|
|
66
|
+
| Provider | Runtime command | Project surface | Workflow syntax |
|
|
67
|
+
|----------|-----------------|-----------------|-----------------|
|
|
68
|
+
| Claude Code | `claude` | `.claude/` | `/specrails:<command>` |
|
|
69
|
+
| Codex CLI | `codex` | `.codex/` | provider-native skills |
|
|
70
|
+
| Gemini CLI | `gemini` | `.gemini/` | `/specrails:<command>` |
|
|
71
|
+
| Kimi Code | managed Node skill runner โ external `kimi -p` | `.kimi-code/` | `/skill:specrails-<command>` in the TUI |
|
|
72
|
+
|
|
73
|
+
Kimi is an external CLI dependency, just like the other providers. SpecRails
|
|
74
|
+
does not bundle a Kimi binary, start `kimi web`, or own a Kimi server. Install
|
|
75
|
+
Kimi Code separately, run `kimi login` once, then select it explicitly or let
|
|
76
|
+
the installer detect it. Parallel Kimi roles are submitted as one bounded
|
|
77
|
+
foreground wave; Core creates/reuses their git worktrees, attributes each
|
|
78
|
+
child stream, and waits for aggregate completionโwithout a server. See the
|
|
79
|
+
[Kimi setup guide](./docs/user-docs/getting-started-kimi.md).
|
|
80
|
+
|
|
64
81
|
## What gets installed
|
|
65
82
|
|
|
66
83
|
Everything lands in your repo โ nothing auto-updates, nothing phones home. You own it, you commit it.
|
|
67
84
|
|
|
68
85
|
| Category | Location | Purpose |
|
|
69
86
|
|----------|----------|---------|
|
|
70
|
-
| **
|
|
71
|
-
| **
|
|
72
|
-
| **
|
|
87
|
+
| **Provider workflows** | `.claude/`, `.codex/`, `.gemini/`, or `.kimi-code/` | Provider-native SpecRails commands and role definitions |
|
|
88
|
+
| **Kimi workflow skills** | `.kimi-code/skills/specrails-*/SKILL.md` | `/skill:specrails-*` directory-form skills |
|
|
89
|
+
| **Kimi role skills** | `.kimi-code/skills/sr-*/SKILL.md` | Specialized role prompts used by Kimi workflows; Kimi discovers only direct skill children |
|
|
90
|
+
| **Kimi headless runner** | `.kimi-code/specrails/run-skill.mjs` | Materializes Kimi's native skill-activation prompt before launching the external CLI |
|
|
91
|
+
| **OpenSpec skills** | Provider-native skills directory | Structured proposal/design/tasks/apply workflows |
|
|
73
92
|
| **Config** | `.specrails/config.yaml` | Stack, CI commands, git workflow |
|
|
74
93
|
| **Personas** | `.specrails/personas/*.md` | VPC user profiles, generated from your users |
|
|
75
94
|
| **Rules** | `.specrails/rules/*.md` | Per-layer coding conventions |
|
|
@@ -204,7 +223,8 @@ Profiles are declarative JSON files that tell `/specrails:implement` which agent
|
|
|
204
223
|
When running the pipeline, the active profile is resolved in this order:
|
|
205
224
|
|
|
206
225
|
1. `$SPECRAILS_PROFILE_PATH` environment variable (absolute path to a JSON snapshot)
|
|
207
|
-
2. `<cwd>/.specrails/profiles/project-default.json`
|
|
226
|
+
2. Provider default: `<cwd>/.specrails/profiles/project-default.json` for
|
|
227
|
+
Claude, or `<cwd>/.specrails/profiles/kimi-default.json` for Kimi
|
|
208
228
|
3. No profile โ legacy behavior (identical to pre-4.1.0)
|
|
209
229
|
|
|
210
230
|
Tools such as [specrails-desktop](https://github.com/fjpulidop/specrails-desktop) set `$SPECRAILS_PROFILE_PATH` to a job-scoped snapshot so concurrent rails can run independent profiles.
|
|
@@ -240,6 +260,9 @@ The following paths are **reserved** โ `specrails-core update` will never crea
|
|
|
240
260
|
|
|
241
261
|
- `.specrails/profiles/**` โ profile JSON files (yours and desktop-authored).
|
|
242
262
|
- `.claude/agents/custom-*.md` โ your custom agents. Use the `custom-` prefix to opt in to this protection.
|
|
263
|
+
- `.kimi-code/skills/custom-*/**` โ your custom Kimi role skills. Pre-release
|
|
264
|
+
`.kimi-code/skills/rails/custom-*` roles are also reserved while Core safely
|
|
265
|
+
migrates them into this discoverable direct-child layout.
|
|
243
266
|
|
|
244
267
|
This contract is what lets you safely hand-author (or let specrails-desktop author) profiles and custom agents without fear of the next `update` overwriting your work. Other paths managed by specrails-core (`.specrails/install-config.yaml`, `.specrails/specrails-version`, etc.) remain under update's control. Audited by `src/installer/__tests__/reserved-paths.test.ts` on every CI run.
|
|
245
268
|
|
|
@@ -287,10 +310,10 @@ Each persona scores features 0โ5. Features are ranked by score / effort ratio.
|
|
|
287
310
|
|
|
288
311
|
| Tool | Required | Purpose |
|
|
289
312
|
|------|----------|---------|
|
|
290
|
-
| **
|
|
291
|
-
| **
|
|
313
|
+
| **One supported AI CLI** | Yes | Claude Code, Codex CLI, Gemini CLI, or Kimi Code |
|
|
314
|
+
| **Kimi Code 0.27.0+** | For Kimi projects | Install from the [official Kimi Code guide](https://www.kimi.com/code/docs/en/kimi-code-cli/guides/getting-started), then run `kimi login` |
|
|
292
315
|
| **git** | Yes | Repository detection |
|
|
293
|
-
| **Node 20+** | Yes | Needed for `npx specrails-core@latest init
|
|
316
|
+
| **Node 20.19.0+** | Yes | Needed for `npx specrails-core@latest init` (the floor required by the pinned OpenSpec 1.4.1 CLI). Cross-platform: macOS, Linux, Windows (10/11, x64 + ARM64 via emulation). |
|
|
294
317
|
| **GitHub CLI** (`gh`) | Optional | Backlog sync to GitHub Issues, PR creation. Not needed with local tickets. |
|
|
295
318
|
| **JIRA CLI** (`jira`) | Optional | Backlog sync to JIRA. Not needed with local tickets. |
|
|
296
319
|
|
|
@@ -324,7 +347,9 @@ Stack-agnostic. The `/specrails:enrich` wizard detects and adapts to whatever yo
|
|
|
324
347
|
## FAQ
|
|
325
348
|
|
|
326
349
|
**Can I customise the agents after installation?**
|
|
327
|
-
Yes. Everything
|
|
350
|
+
Yes. Everything in the selected provider tree and `.specrails/` is yours to
|
|
351
|
+
edit. For Kimi, customize `.kimi-code/skills/`, `.kimi-code/rules/`, and the
|
|
352
|
+
managed block in `.kimi-code/AGENTS.md`; `custom-*` role skills are preserved.
|
|
328
353
|
|
|
329
354
|
**Can I re-run the wizard?**
|
|
330
355
|
Run `/specrails:enrich` again at any time to regenerate or update project data files. Re-running `npx specrails-core@latest init` refreshes the agents/commands without touching `.specrails/`.
|
|
@@ -336,13 +361,24 @@ Yes. Local tickets are the default and need no external tools. `/specrails:imple
|
|
|
336
361
|
Not simultaneously for the same project โ backlog commands use one active provider at a time. You can migrate from GitHub Issues to local tickets using the [migration guide](./docs/migration-guide.md).
|
|
337
362
|
|
|
338
363
|
**How much does it cost to run?**
|
|
339
|
-
|
|
364
|
+
Cost depends on the selected provider, model, and workload. SpecRails does not
|
|
365
|
+
add a model surcharge. Kimi's stream output does not currently report a native
|
|
366
|
+
USD cost, so consumers must display it as unavailable rather than inventing an
|
|
367
|
+
estimate.
|
|
340
368
|
|
|
341
369
|
**Does it work with private repos?**
|
|
342
|
-
Yes.
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
370
|
+
Yes. Orchestration runs through the selected local CLI. The provider still
|
|
371
|
+
connects to its model API and any MCP/integration endpoints you configure.
|
|
372
|
+
|
|
373
|
+
**How do I use specrails with Kimi?**
|
|
374
|
+
Install and authenticate Kimi Code, then run
|
|
375
|
+
`npx specrails-core@latest init --provider kimi`. Invoke the generated workflows
|
|
376
|
+
as `/skill:specrails-implement`, `/skill:specrails-enrich`, and so on in Kimi's
|
|
377
|
+
interactive TUI. Headless callers use the managed
|
|
378
|
+
`.kimi-code/specrails/run-skill.mjs` helper: Kimi 0.27 sends a slash command
|
|
379
|
+
passed directly to `kimi -p` as literal text, so the helper first renders the
|
|
380
|
+
same skill prompt as Kimi's native activation path and then starts external
|
|
381
|
+
`kimi -p --output-format stream-json`. No server installation is required.
|
|
346
382
|
|
|
347
383
|
---
|
|
348
384
|
|
package/bin/specrails-core.mjs
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
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` โ
|
|
10
|
-
*
|
|
9
|
+
* - `enrich` โ launches the installed provider's
|
|
10
|
+
* native enrich workflow.
|
|
11
11
|
* - `init` TUI short-circuit โ spawns tui-installer.mjs
|
|
12
12
|
* then re-enters init with
|
|
13
13
|
* --from-config.
|
|
@@ -31,7 +31,11 @@ const subcommand = args[0]
|
|
|
31
31
|
|
|
32
32
|
// โโโ Global --version / -V flag โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
// Treat version as a GLOBAL flag only when it is the command itself. Offline
|
|
35
|
+
// lifecycle commands also carry a required `--version <target>` option; scanning
|
|
36
|
+
// the whole argv would intercept `swap-current --version 4.12.0` here and exit 0
|
|
37
|
+
// without ever validating or moving the framework pointer.
|
|
38
|
+
if (subcommand === '--version' || subcommand === '-V') {
|
|
35
39
|
const pkg = require_(path.resolve(ROOT, 'package.json'))
|
|
36
40
|
console.log(`specrails-core v${pkg.version}`)
|
|
37
41
|
process.exit(0)
|
|
@@ -51,6 +55,7 @@ const KNOWN_SUBCOMMANDS = new Set([
|
|
|
51
55
|
'update',
|
|
52
56
|
'doctor',
|
|
53
57
|
'install-framework',
|
|
58
|
+
'swap-current',
|
|
54
59
|
'assemble',
|
|
55
60
|
'enrich',
|
|
56
61
|
'version',
|
|
@@ -61,7 +66,7 @@ const KNOWN_SUBCOMMANDS = new Set([
|
|
|
61
66
|
if (!KNOWN_SUBCOMMANDS.has(subcommand)) {
|
|
62
67
|
console.error(`Unknown command: ${subcommand}\n`)
|
|
63
68
|
console.error(
|
|
64
|
-
'Available commands: init, update, doctor, install-framework, assemble, enrich, version, profile, help',
|
|
69
|
+
'Available commands: init, update, doctor, install-framework, swap-current, assemble, enrich, version, profile, help',
|
|
65
70
|
)
|
|
66
71
|
process.exit(1)
|
|
67
72
|
}
|
|
@@ -90,25 +95,91 @@ if (subcommand === 'profile') {
|
|
|
90
95
|
}
|
|
91
96
|
|
|
92
97
|
// โโโ enrich โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
93
|
-
// Launches
|
|
98
|
+
// Launches the configured provider's native enrich workflow.
|
|
94
99
|
|
|
95
100
|
if (subcommand === 'enrich') {
|
|
96
|
-
const
|
|
97
|
-
const
|
|
101
|
+
const workspace = await resolveEnrichWorkspace(process.cwd())
|
|
102
|
+
const { provider, model: explicitModel, workflowArgs } = resolveEnrichOptions(
|
|
103
|
+
workspace.codeRoot,
|
|
104
|
+
subargs,
|
|
105
|
+
workspace.artifactRoot,
|
|
106
|
+
)
|
|
107
|
+
const model =
|
|
108
|
+
explicitModel ??
|
|
109
|
+
resolveConfiguredEnrichModel(workspace.codeRoot, workspace.artifactRoot) ??
|
|
110
|
+
'k3'
|
|
111
|
+
const enrichFlags = serializeWorkflowArgs(workflowArgs)
|
|
112
|
+
const launch =
|
|
113
|
+
provider === 'kimi'
|
|
114
|
+
? {
|
|
115
|
+
command: process.execPath,
|
|
116
|
+
args: [
|
|
117
|
+
path.resolve(
|
|
118
|
+
workspace.artifactRoot,
|
|
119
|
+
'.kimi-code',
|
|
120
|
+
'specrails',
|
|
121
|
+
'run-skill.mjs',
|
|
122
|
+
),
|
|
123
|
+
'--skill',
|
|
124
|
+
'specrails-enrich',
|
|
125
|
+
'--model',
|
|
126
|
+
model,
|
|
127
|
+
'--add-dir',
|
|
128
|
+
workspace.codeRoot,
|
|
129
|
+
...(enrichFlags ? ['--args', enrichFlags] : []),
|
|
130
|
+
],
|
|
131
|
+
label: 'Kimi Code',
|
|
132
|
+
}
|
|
133
|
+
: provider === 'gemini'
|
|
134
|
+
? {
|
|
135
|
+
command: 'gemini',
|
|
136
|
+
args: [
|
|
137
|
+
'-p',
|
|
138
|
+
`/specrails:enrich${enrichFlags ? ` ${enrichFlags}` : ''}`,
|
|
139
|
+
'--output-format',
|
|
140
|
+
'stream-json',
|
|
141
|
+
],
|
|
142
|
+
label: 'Gemini CLI',
|
|
143
|
+
}
|
|
144
|
+
: provider === 'codex'
|
|
145
|
+
? {
|
|
146
|
+
command: 'codex',
|
|
147
|
+
args: [
|
|
148
|
+
'exec',
|
|
149
|
+
`run enrich${enrichFlags ? ` ${enrichFlags}` : ''}`,
|
|
150
|
+
],
|
|
151
|
+
label: 'Codex CLI',
|
|
152
|
+
}
|
|
153
|
+
: {
|
|
154
|
+
command: 'claude',
|
|
155
|
+
args: [
|
|
156
|
+
'--command',
|
|
157
|
+
`/specrails:enrich${enrichFlags ? ` ${enrichFlags}` : ''}`,
|
|
158
|
+
'--dangerously-skip-permissions',
|
|
159
|
+
],
|
|
160
|
+
label: 'Claude Code',
|
|
161
|
+
}
|
|
98
162
|
const result = spawnSync(
|
|
99
|
-
|
|
100
|
-
|
|
163
|
+
launch.command,
|
|
164
|
+
launch.args,
|
|
101
165
|
{
|
|
102
166
|
stdio: 'inherit',
|
|
103
|
-
cwd: process.cwd(),
|
|
104
|
-
|
|
167
|
+
cwd: provider === 'kimi' ? workspace.artifactRoot : process.cwd(),
|
|
168
|
+
env:
|
|
169
|
+
provider === 'kimi'
|
|
170
|
+
? {
|
|
171
|
+
...process.env,
|
|
172
|
+
SPECRAILS_REPO_DIR: workspace.codeRoot,
|
|
173
|
+
}
|
|
174
|
+
: process.env,
|
|
175
|
+
shell: process.platform === 'win32' && provider !== 'kimi',
|
|
105
176
|
},
|
|
106
177
|
)
|
|
107
178
|
if (result.error) {
|
|
108
179
|
console.error(
|
|
109
|
-
|
|
180
|
+
`\nFailed to launch ${launch.label} for enrich:`,
|
|
110
181
|
result.error.message,
|
|
111
|
-
|
|
182
|
+
`\nEnsure the configured ${provider} provider is installed and initialized.\n`,
|
|
112
183
|
)
|
|
113
184
|
process.exit(1)
|
|
114
185
|
}
|
|
@@ -144,6 +215,7 @@ if (subcommand === 'init') {
|
|
|
144
215
|
const tuiArgs = [path.resolve(ROOT, 'bin', 'tui-installer.mjs'), rootDir]
|
|
145
216
|
if (providerVal) tuiArgs.push('--provider', providerVal)
|
|
146
217
|
else if (providerEqArg) tuiArgs.push(providerEqArg)
|
|
218
|
+
if (subargs.includes('--with-profiles')) tuiArgs.push('--with-profiles')
|
|
147
219
|
const tuiResult = spawnSync('node', tuiArgs, {
|
|
148
220
|
stdio: 'inherit',
|
|
149
221
|
cwd: process.cwd(),
|
|
@@ -189,20 +261,25 @@ process.exit(0)
|
|
|
189
261
|
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
190
262
|
|
|
191
263
|
function printUsage() {
|
|
192
|
-
console.log(`specrails-core โ
|
|
264
|
+
console.log(`specrails-core โ Provider-independent AI agent workflow system
|
|
193
265
|
|
|
194
266
|
Usage:
|
|
195
267
|
specrails-core init [--root-dir <path>] [--yes|-y] [--no-tui] Install into a repository
|
|
196
268
|
specrails-core update [--only <component>] [--dry-run] Update an existing installation
|
|
197
269
|
specrails-core doctor Run health checks
|
|
198
|
-
specrails-core
|
|
270
|
+
specrails-core install-framework --framework-dir <path> --provider <value> --version <value>
|
|
271
|
+
Materialize an offline framework version
|
|
272
|
+
specrails-core swap-current --framework-dir <path> --version <value> [--providers <csv>]
|
|
273
|
+
Validate and atomically expose a framework version
|
|
274
|
+
specrails-core assemble --workspace <path> --framework-dir <path> Assemble a workspace from the framework
|
|
275
|
+
specrails-core enrich [--provider <value>] [workflow flags] Run the configured provider's enrich workflow
|
|
199
276
|
specrails-core profile <validate|show> [<path>] Validate or pretty-print a profile JSON
|
|
200
277
|
specrails-core version Show installed version
|
|
201
278
|
|
|
202
279
|
Flags for init:
|
|
203
280
|
--root-dir <path> Target repository path (default: current directory)
|
|
204
281
|
--yes | -y Non-interactive; use defaults, skip TUI
|
|
205
|
-
--provider <value> Force provider: claude
|
|
282
|
+
--provider <value> Force provider: claude, codex, gemini, or kimi
|
|
206
283
|
--no-tui Skip TUI; use defaults / flags directly
|
|
207
284
|
--from-config Skip TUI; use existing .specrails/install-config.yaml
|
|
208
285
|
|
|
@@ -212,6 +289,174 @@ Global flags:
|
|
|
212
289
|
More info: https://github.com/fjpulidop/specrails-core`)
|
|
213
290
|
}
|
|
214
291
|
|
|
292
|
+
function resolveEnrichOptions(cwd, argv, artifactRoot = cwd) {
|
|
293
|
+
const workflowArgs = []
|
|
294
|
+
let explicitProvider
|
|
295
|
+
let model
|
|
296
|
+
for (let index = 0; index < argv.length; index++) {
|
|
297
|
+
const token = argv[index]
|
|
298
|
+
if (token === '--provider' || token === '--model') {
|
|
299
|
+
const value = argv[++index]
|
|
300
|
+
if (!value) {
|
|
301
|
+
console.error(`${token} requires a value`)
|
|
302
|
+
process.exit(1)
|
|
303
|
+
}
|
|
304
|
+
if (token === '--provider') explicitProvider = value
|
|
305
|
+
else model = value
|
|
306
|
+
continue
|
|
307
|
+
}
|
|
308
|
+
if (token.startsWith('--provider=')) {
|
|
309
|
+
explicitProvider = token.slice('--provider='.length)
|
|
310
|
+
continue
|
|
311
|
+
}
|
|
312
|
+
if (token.startsWith('--model=')) {
|
|
313
|
+
model = token.slice('--model='.length)
|
|
314
|
+
continue
|
|
315
|
+
}
|
|
316
|
+
workflowArgs.push(token)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const supported = new Set(['claude', 'codex', 'gemini', 'kimi'])
|
|
320
|
+
if (explicitProvider && !supported.has(explicitProvider)) {
|
|
321
|
+
console.error(
|
|
322
|
+
`Unsupported provider "${explicitProvider}". Expected claude, codex, gemini, or kimi.`,
|
|
323
|
+
)
|
|
324
|
+
process.exit(1)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let provider = explicitProvider
|
|
328
|
+
if (!provider) {
|
|
329
|
+
for (const root of [cwd, artifactRoot]) {
|
|
330
|
+
const manifestPath = path.join(
|
|
331
|
+
root,
|
|
332
|
+
'.specrails',
|
|
333
|
+
'specrails-manifest.json',
|
|
334
|
+
)
|
|
335
|
+
if (existsSync(manifestPath)) {
|
|
336
|
+
try {
|
|
337
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
|
338
|
+
if (supported.has(manifest.primary_provider)) {
|
|
339
|
+
provider = manifest.primary_provider
|
|
340
|
+
break
|
|
341
|
+
}
|
|
342
|
+
} catch {
|
|
343
|
+
// A malformed manifest is diagnosed by `specrails-core doctor`;
|
|
344
|
+
// enrich continues to config/directory fallback.
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (!provider) {
|
|
350
|
+
for (const root of [cwd, artifactRoot]) {
|
|
351
|
+
const configPath = path.join(root, '.specrails', 'install-config.yaml')
|
|
352
|
+
if (!existsSync(configPath)) continue
|
|
353
|
+
const match = readFileSync(configPath, 'utf8').match(
|
|
354
|
+
/^provider:\s*(claude|codex|gemini|kimi)\s*$/m,
|
|
355
|
+
)
|
|
356
|
+
if (match) {
|
|
357
|
+
provider = match[1]
|
|
358
|
+
break
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (!provider) {
|
|
363
|
+
provider =
|
|
364
|
+
['claude', 'codex', 'gemini', 'kimi'].find((candidate) =>
|
|
365
|
+
existsSync(
|
|
366
|
+
path.join(
|
|
367
|
+
artifactRoot,
|
|
368
|
+
candidate === 'kimi' ? '.kimi-code' : `.${candidate}`,
|
|
369
|
+
),
|
|
370
|
+
),
|
|
371
|
+
) ?? 'claude'
|
|
372
|
+
}
|
|
373
|
+
return { provider, model, workflowArgs }
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function resolveEnrichWorkspace(cwd) {
|
|
377
|
+
const registryModulePath = path.resolve(
|
|
378
|
+
ROOT,
|
|
379
|
+
'dist',
|
|
380
|
+
'installer',
|
|
381
|
+
'util',
|
|
382
|
+
'registry.js',
|
|
383
|
+
)
|
|
384
|
+
if (!existsSync(registryModulePath)) {
|
|
385
|
+
return { artifactRoot: cwd, codeRoot: cwd }
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
const registry = await import(pathToFileURL(registryModulePath).href)
|
|
389
|
+
const resolution = registry.resolveArtifacts(cwd, { allocate: false })
|
|
390
|
+
return {
|
|
391
|
+
artifactRoot: path.resolve(resolution.artifactRoot),
|
|
392
|
+
codeRoot: path.resolve(resolution.codeRoot),
|
|
393
|
+
}
|
|
394
|
+
} catch {
|
|
395
|
+
// Doctor reports malformed registry state. Enrich retains the legacy
|
|
396
|
+
// in-repository fallback instead of guessing another relocated workspace.
|
|
397
|
+
return { artifactRoot: cwd, codeRoot: cwd }
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function resolveConfiguredEnrichModel(codeRoot, artifactRoot) {
|
|
402
|
+
const activeProfile = process.env.SPECRAILS_PROFILE_PATH
|
|
403
|
+
if (activeProfile) {
|
|
404
|
+
const activePath = path.isAbsolute(activeProfile)
|
|
405
|
+
? activeProfile
|
|
406
|
+
: path.resolve(codeRoot, activeProfile)
|
|
407
|
+
const activeModel = readProfileOrchestratorModel(activePath)
|
|
408
|
+
if (activeModel) return activeModel
|
|
409
|
+
}
|
|
410
|
+
const configCandidates = [
|
|
411
|
+
path.join(codeRoot, '.specrails', 'install-config.yaml'),
|
|
412
|
+
path.join(artifactRoot, '.specrails', 'install-config.yaml'),
|
|
413
|
+
]
|
|
414
|
+
for (const configPath of configCandidates) {
|
|
415
|
+
if (!existsSync(configPath)) continue
|
|
416
|
+
try {
|
|
417
|
+
const config = require_('js-yaml').load(readFileSync(configPath, 'utf8'))
|
|
418
|
+
const configured = config?.models?.defaults?.model
|
|
419
|
+
if (typeof configured === 'string' && configured.trim() !== '') {
|
|
420
|
+
return configured.trim()
|
|
421
|
+
}
|
|
422
|
+
} catch {
|
|
423
|
+
// The installer/doctor owns full config diagnostics. Continue to a
|
|
424
|
+
// provider profile rather than extracting a value from malformed YAML.
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const profileCandidates = [
|
|
428
|
+
path.join(artifactRoot, '.specrails', 'profiles', 'kimi-default.json'),
|
|
429
|
+
path.join(codeRoot, '.specrails', 'profiles', 'kimi-default.json'),
|
|
430
|
+
]
|
|
431
|
+
for (const profilePath of profileCandidates) {
|
|
432
|
+
const configured = readProfileOrchestratorModel(profilePath)
|
|
433
|
+
if (configured) return configured
|
|
434
|
+
}
|
|
435
|
+
return null
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function readProfileOrchestratorModel(profilePath) {
|
|
439
|
+
if (!existsSync(profilePath)) return null
|
|
440
|
+
try {
|
|
441
|
+
const profile = JSON.parse(readFileSync(profilePath, 'utf8'))
|
|
442
|
+
const configured = profile?.orchestrator?.model
|
|
443
|
+
return typeof configured === 'string' && configured.trim() !== ''
|
|
444
|
+
? configured.trim()
|
|
445
|
+
: null
|
|
446
|
+
} catch {
|
|
447
|
+
// Profile validation provides the actionable error. Fall through.
|
|
448
|
+
return null
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function serializeWorkflowArgs(argv) {
|
|
453
|
+
return argv
|
|
454
|
+
.map((value) =>
|
|
455
|
+
value === '' || /\s|["']/u.test(value) ? JSON.stringify(value) : value,
|
|
456
|
+
)
|
|
457
|
+
.join(' ')
|
|
458
|
+
}
|
|
459
|
+
|
|
215
460
|
/**
|
|
216
461
|
* Loads the compiled Node CLI (dist/installer/cli.js) and invokes its
|
|
217
462
|
* `main` function with the given argv array. Propagates the returned
|
|
@@ -244,11 +489,33 @@ async function runProfile(subargs) {
|
|
|
244
489
|
process.exit(1)
|
|
245
490
|
}
|
|
246
491
|
|
|
492
|
+
const workspace = await resolveEnrichWorkspace(process.cwd())
|
|
247
493
|
const resolveProfilePath = () => {
|
|
248
494
|
if (pathArg) return path.resolve(pathArg)
|
|
249
495
|
if (process.env.SPECRAILS_PROFILE_PATH) return path.resolve(process.env.SPECRAILS_PROFILE_PATH)
|
|
250
|
-
const
|
|
251
|
-
|
|
496
|
+
const configRoots = [workspace.codeRoot, workspace.artifactRoot]
|
|
497
|
+
let provider
|
|
498
|
+
for (const root of configRoots) {
|
|
499
|
+
const configPath = path.join(root, '.specrails', 'install-config.yaml')
|
|
500
|
+
if (!existsSync(configPath)) continue
|
|
501
|
+
const match = readFileSync(configPath, 'utf8').match(
|
|
502
|
+
/^provider:\s*(claude|codex|gemini|kimi)\s*$/m,
|
|
503
|
+
)
|
|
504
|
+
if (match) {
|
|
505
|
+
provider = match[1]
|
|
506
|
+
break
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
const names =
|
|
510
|
+
provider === 'kimi'
|
|
511
|
+
? ['kimi-default.json', 'project-default.json']
|
|
512
|
+
: ['project-default.json', 'kimi-default.json']
|
|
513
|
+
for (const root of [workspace.artifactRoot, workspace.codeRoot]) {
|
|
514
|
+
for (const name of names) {
|
|
515
|
+
const candidate = path.join(root, '.specrails', 'profiles', name)
|
|
516
|
+
if (existsSync(candidate)) return candidate
|
|
517
|
+
}
|
|
518
|
+
}
|
|
252
519
|
return null
|
|
253
520
|
}
|
|
254
521
|
|
|
@@ -256,7 +523,9 @@ async function runProfile(subargs) {
|
|
|
256
523
|
if (!profilePath) {
|
|
257
524
|
console.error('No profile path given and none could be resolved.')
|
|
258
525
|
console.error('Pass an explicit path or set SPECRAILS_PROFILE_PATH, or place a profile at')
|
|
259
|
-
console.error(
|
|
526
|
+
console.error(
|
|
527
|
+
' .specrails/profiles/project-default.json (or kimi-default.json for Kimi)',
|
|
528
|
+
)
|
|
260
529
|
process.exit(1)
|
|
261
530
|
}
|
|
262
531
|
if (!existsSync(profilePath)) {
|
|
@@ -298,8 +567,12 @@ async function runProfile(subargs) {
|
|
|
298
567
|
const schema = JSON.parse(readFileSync(schemaPath, 'utf8'))
|
|
299
568
|
const ajv = new Ajv({ allErrors: true, strict: false })
|
|
300
569
|
const validate = ajv.compile(schema)
|
|
570
|
+
const schemaValid = validate(profile)
|
|
571
|
+
const semanticErrors = schemaValid
|
|
572
|
+
? validateProfileSemantics(profile)
|
|
573
|
+
: []
|
|
301
574
|
|
|
302
|
-
if (
|
|
575
|
+
if (schemaValid && semanticErrors.length === 0) {
|
|
303
576
|
console.log(`โ ${profilePath} is a valid v1 profile.`)
|
|
304
577
|
process.exit(0)
|
|
305
578
|
}
|
|
@@ -307,5 +580,55 @@ async function runProfile(subargs) {
|
|
|
307
580
|
for (const err of validate.errors || []) {
|
|
308
581
|
console.error(` ${err.instancePath || '/'} ${err.message} (${JSON.stringify(err.params)})`)
|
|
309
582
|
}
|
|
583
|
+
for (const err of semanticErrors) {
|
|
584
|
+
console.error(` ${err}`)
|
|
585
|
+
}
|
|
310
586
|
process.exit(1)
|
|
311
587
|
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* JSON Schema validates each profile entry in isolation. These invariants span
|
|
591
|
+
* multiple array entries and therefore require a second, semantic validation
|
|
592
|
+
* pass after AJV has established the profile's structural shape.
|
|
593
|
+
*/
|
|
594
|
+
function validateProfileSemantics(profile) {
|
|
595
|
+
const errors = []
|
|
596
|
+
const firstAgentIndex = new Map()
|
|
597
|
+
|
|
598
|
+
for (const [index, agent] of profile.agents.entries()) {
|
|
599
|
+
const previous = firstAgentIndex.get(agent.id)
|
|
600
|
+
if (previous !== undefined) {
|
|
601
|
+
errors.push(
|
|
602
|
+
`/agents/${index}/id duplicates agent id "${agent.id}" first declared at /agents/${previous}/id`,
|
|
603
|
+
)
|
|
604
|
+
} else {
|
|
605
|
+
firstAgentIndex.set(agent.id, index)
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const knownAgents = new Set(firstAgentIndex.keys())
|
|
610
|
+
const defaultIndices = []
|
|
611
|
+
for (const [index, rule] of profile.routing.entries()) {
|
|
612
|
+
if (!knownAgents.has(rule.agent)) {
|
|
613
|
+
errors.push(
|
|
614
|
+
`/routing/${index}/agent references unknown agent "${rule.agent}"`,
|
|
615
|
+
)
|
|
616
|
+
}
|
|
617
|
+
if (rule.default === true) {
|
|
618
|
+
defaultIndices.push(index)
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (defaultIndices.length > 1) {
|
|
623
|
+
errors.push(
|
|
624
|
+
`/routing must contain at most one default rule (found ${defaultIndices.length})`,
|
|
625
|
+
)
|
|
626
|
+
}
|
|
627
|
+
for (const index of defaultIndices) {
|
|
628
|
+
if (index !== profile.routing.length - 1) {
|
|
629
|
+
errors.push(`/routing/${index} default rule must be the last routing entry`)
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return errors
|
|
634
|
+
}
|