specrails-core 4.0.8 → 4.1.1
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 +62 -0
- package/VERSION +1 -1
- package/bin/specrails-core.js +84 -0
- package/bin/tui-installer.mjs +29 -3
- package/install.sh +13 -0
- package/package.json +5 -1
- package/schemas/profile.v1.json +145 -0
- package/templates/commands/specrails/batch-implement.md +7 -2
- package/templates/commands/specrails/implement.md +190 -4
- package/templates/profiles/default.json +27 -0
- package/update.sh +13 -0
package/README.md
CHANGED
|
@@ -177,6 +177,68 @@ AI product discovery using your personas. Evaluates ideas, creates tickets (loca
|
|
|
177
177
|
|
|
178
178
|
---
|
|
179
179
|
|
|
180
|
+
## Agent profiles
|
|
181
|
+
|
|
182
|
+
> Available in `specrails-core >= 4.1.0`. Optional — without a profile, the pipeline behaves exactly as before.
|
|
183
|
+
|
|
184
|
+
Profiles are declarative JSON files that tell `/specrails:implement` which agents to use, which models to run them with, and how to route tasks to specialists. One project can define many profiles (e.g. `default`, `data-heavy`, `security-heavy`) and run different features with different profiles — useful for concurrent rails in `/specrails:batch-implement`.
|
|
185
|
+
|
|
186
|
+
### File layout
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
<project>/.specrails/
|
|
190
|
+
profiles/
|
|
191
|
+
default.json # checked into git, team-shared
|
|
192
|
+
data-heavy.json # checked into git, team-shared
|
|
193
|
+
.user-preferred.json # gitignored, your personal default
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Resolution order
|
|
197
|
+
|
|
198
|
+
When running the pipeline, the active profile is resolved in this order:
|
|
199
|
+
|
|
200
|
+
1. `$SPECRAILS_PROFILE_PATH` environment variable (absolute path to a JSON snapshot)
|
|
201
|
+
2. `<cwd>/.specrails/profiles/project-default.json`
|
|
202
|
+
3. No profile — legacy behavior (identical to pre-4.1.0)
|
|
203
|
+
|
|
204
|
+
Tools such as [specrails-hub](https://github.com/fjpulidop/specrails-hub) set `$SPECRAILS_PROFILE_PATH` to a job-scoped snapshot so concurrent rails can run independent profiles.
|
|
205
|
+
|
|
206
|
+
### Schema
|
|
207
|
+
|
|
208
|
+
The v1 profile schema is published at [`schemas/profile.v1.json`](./schemas/profile.v1.json). Example:
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
{
|
|
212
|
+
"schemaVersion": 1,
|
|
213
|
+
"name": "data-heavy",
|
|
214
|
+
"description": "Data engineering rail with stricter review",
|
|
215
|
+
"orchestrator": { "model": "opus" },
|
|
216
|
+
"agents": [
|
|
217
|
+
{ "id": "sr-architect", "model": "opus", "required": true },
|
|
218
|
+
{ "id": "sr-data-engineer", "model": "sonnet" },
|
|
219
|
+
{ "id": "sr-developer", "model": "sonnet", "required": true },
|
|
220
|
+
{ "id": "sr-reviewer", "model": "opus", "required": true }
|
|
221
|
+
],
|
|
222
|
+
"routing": [
|
|
223
|
+
{ "tags": ["etl", "schema", "data"], "agent": "sr-data-engineer" },
|
|
224
|
+
{ "default": true, "agent": "sr-developer" }
|
|
225
|
+
]
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Baseline agents (`sr-architect`, `sr-developer`, `sr-reviewer`) MUST appear in `agents[]`. The `routing` array is ordered — first rule whose `tags` intersects the task's tags wins; the terminal `default: true` rule catches everything else.
|
|
230
|
+
|
|
231
|
+
### Reserved paths
|
|
232
|
+
|
|
233
|
+
The following paths are **reserved** — `update.sh` will never create, modify, or delete anything inside them:
|
|
234
|
+
|
|
235
|
+
- `.specrails/profiles/**` — profile JSON files (yours and hub-authored).
|
|
236
|
+
- `.claude/agents/custom-*.md` — your custom agents. Use the `custom-` prefix to opt in to this protection.
|
|
237
|
+
|
|
238
|
+
This contract is what lets you safely hand-author (or let specrails-hub 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.sh`'s control.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
180
242
|
## Local ticket management
|
|
181
243
|
|
|
182
244
|
specrails-core ships with a built-in ticket system — no GitHub account or external tools required.
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.
|
|
1
|
+
4.1.1
|
package/bin/specrails-core.js
CHANGED
|
@@ -11,6 +11,7 @@ const COMMANDS = {
|
|
|
11
11
|
"perf-check": "bin/perf-check.sh",
|
|
12
12
|
enrich: null,
|
|
13
13
|
version: null,
|
|
14
|
+
profile: null,
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
const args = process.argv.slice(2);
|
|
@@ -33,6 +34,7 @@ Usage:
|
|
|
33
34
|
specrails-core doctor Run health checks
|
|
34
35
|
specrails-core perf-check [--files <list>] Performance regression check (CI)
|
|
35
36
|
specrails-core enrich [--from-config <path>] Run /specrails:enrich via Claude CLI
|
|
37
|
+
specrails-core profile <validate|show> [<path>] Validate or pretty-print a profile JSON
|
|
36
38
|
specrails-core version Show installed version
|
|
37
39
|
|
|
38
40
|
Flags for init:
|
|
@@ -65,6 +67,7 @@ const ALLOWED_FLAGS = {
|
|
|
65
67
|
"perf-check": ["--files", "--context"],
|
|
66
68
|
enrich: ["--from-config", "--quick"],
|
|
67
69
|
version: [],
|
|
70
|
+
profile: [],
|
|
68
71
|
};
|
|
69
72
|
|
|
70
73
|
const subargs = args.slice(1);
|
|
@@ -85,6 +88,87 @@ if (subcommand === "version") {
|
|
|
85
88
|
process.exit(0);
|
|
86
89
|
}
|
|
87
90
|
|
|
91
|
+
// ─── profile subcommand ──────────────────────────────────────────────────────
|
|
92
|
+
// `specrails-core profile validate [path]` — validate a profile JSON against v1 schema
|
|
93
|
+
// `specrails-core profile show [path]` — pretty-print the resolved profile
|
|
94
|
+
// Resolution order when no path: $SPECRAILS_PROFILE_PATH → .specrails/profiles/project-default.json
|
|
95
|
+
|
|
96
|
+
if (subcommand === "profile") {
|
|
97
|
+
const { existsSync, readFileSync } = require("fs");
|
|
98
|
+
const action = subargs[0];
|
|
99
|
+
const pathArg = subargs[1];
|
|
100
|
+
|
|
101
|
+
if (!action || (action !== "validate" && action !== "show")) {
|
|
102
|
+
console.error("Usage: specrails-core profile validate [<path>]");
|
|
103
|
+
console.error(" specrails-core profile show [<path>]");
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const resolveProfilePath = () => {
|
|
108
|
+
if (pathArg) return resolve(pathArg);
|
|
109
|
+
if (process.env.SPECRAILS_PROFILE_PATH) return resolve(process.env.SPECRAILS_PROFILE_PATH);
|
|
110
|
+
const projectDefault = resolve(process.cwd(), ".specrails/profiles/project-default.json");
|
|
111
|
+
if (existsSync(projectDefault)) return projectDefault;
|
|
112
|
+
return null;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const profilePath = resolveProfilePath();
|
|
116
|
+
if (!profilePath) {
|
|
117
|
+
console.error("No profile path given and none could be resolved.");
|
|
118
|
+
console.error("Pass an explicit path or set SPECRAILS_PROFILE_PATH, or place a profile at");
|
|
119
|
+
console.error(" .specrails/profiles/project-default.json");
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
if (!existsSync(profilePath)) {
|
|
123
|
+
console.error(`Profile file not found: ${profilePath}`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let profile;
|
|
128
|
+
try {
|
|
129
|
+
profile = JSON.parse(readFileSync(profilePath, "utf8"));
|
|
130
|
+
} catch (e) {
|
|
131
|
+
console.error(`Profile is not valid JSON: ${e.message}`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (action === "show") {
|
|
136
|
+
console.log(JSON.stringify(profile, null, 2));
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// action === "validate"
|
|
141
|
+
const schemaPath = resolve(ROOT, "schemas/profile.v1.json");
|
|
142
|
+
if (!existsSync(schemaPath)) {
|
|
143
|
+
console.error(`Schema not found at ${schemaPath} — install may be corrupt`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let Ajv;
|
|
148
|
+
try {
|
|
149
|
+
Ajv = require("ajv/dist/2020.js").default;
|
|
150
|
+
} catch {
|
|
151
|
+
console.error("'ajv' is not installed. Run `npm install` in the specrails-core package directory first,");
|
|
152
|
+
console.error("or rely on the hub's own validator for user-facing flows.");
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const schema = JSON.parse(readFileSync(schemaPath, "utf8"));
|
|
157
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
158
|
+
const validate = ajv.compile(schema);
|
|
159
|
+
|
|
160
|
+
if (validate(profile)) {
|
|
161
|
+
console.log(`✓ ${profilePath} is a valid v1 profile.`);
|
|
162
|
+
process.exit(0);
|
|
163
|
+
} else {
|
|
164
|
+
console.error(`✗ ${profilePath} failed validation:`);
|
|
165
|
+
for (const err of validate.errors || []) {
|
|
166
|
+
console.error(` ${err.instancePath || "/"} ${err.message} (${JSON.stringify(err.params)})`);
|
|
167
|
+
}
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
88
172
|
// ─── enrich subcommand ───────────────────────────────────────────────────────
|
|
89
173
|
// Launches `claude --command "/specrails:enrich [flags]"` so the AI-powered
|
|
90
174
|
// enrichment runs inside Claude Code with full model access.
|
package/bin/tui-installer.mjs
CHANGED
|
@@ -64,10 +64,12 @@ const CORE_AGENTS = new Set([
|
|
|
64
64
|
'sr-merge-resolver',
|
|
65
65
|
]);
|
|
66
66
|
|
|
67
|
+
// Only the CORE agents are pre-selected. Optional agents (product manager,
|
|
68
|
+
// test writer, layer specialists, reviewers, utilities) are opt-in so the
|
|
69
|
+
// default install is as lean as possible. Users can add optional agents via
|
|
70
|
+
// `/specrails:enrich` or by re-running init.
|
|
67
71
|
const DEFAULT_SELECTED = new Set([
|
|
68
72
|
...CORE_AGENTS,
|
|
69
|
-
'sr-test-writer',
|
|
70
|
-
'sr-product-manager',
|
|
71
73
|
]);
|
|
72
74
|
|
|
73
75
|
// ─── Model presets ────────────────────────────────────────────────────────────
|
|
@@ -178,12 +180,35 @@ function writeDefaultConfig(specrailsDir, provider) {
|
|
|
178
180
|
async function run() {
|
|
179
181
|
const rawArgs = process.argv.slice(2);
|
|
180
182
|
const autoYes = rawArgs.includes('--yes') || rawArgs.includes('-y');
|
|
183
|
+
const withProfiles = rawArgs.includes('--with-profiles');
|
|
181
184
|
const rootArg = rawArgs.find(a => !a.startsWith('-'));
|
|
182
185
|
const inputDir = rootArg ? resolve(rootArg) : process.cwd();
|
|
183
186
|
const rootDir = detectGitRoot(inputDir);
|
|
184
187
|
|
|
185
188
|
const specrailsDir = resolve(rootDir, '.specrails');
|
|
186
189
|
|
|
190
|
+
// Optional: scaffold .specrails/profiles/project-default.json from the shipped template.
|
|
191
|
+
// Off by default to keep standalone installs zero-noise.
|
|
192
|
+
if (withProfiles) {
|
|
193
|
+
try {
|
|
194
|
+
const { readFileSync, writeFileSync, existsSync, mkdirSync } = await import('node:fs');
|
|
195
|
+
const { dirname } = await import('node:path');
|
|
196
|
+
const scriptDir = new URL('..', import.meta.url).pathname;
|
|
197
|
+
const templatePath = resolve(scriptDir, 'templates/profiles/default.json');
|
|
198
|
+
const profilesDir = resolve(specrailsDir, 'profiles');
|
|
199
|
+
const targetPath = resolve(profilesDir, 'project-default.json');
|
|
200
|
+
if (existsSync(templatePath) && !existsSync(targetPath)) {
|
|
201
|
+
mkdirSync(profilesDir, { recursive: true });
|
|
202
|
+
writeFileSync(targetPath, readFileSync(templatePath));
|
|
203
|
+
console.log(` ✓ Profile scaffolded at .specrails/profiles/project-default.json`);
|
|
204
|
+
} else if (existsSync(targetPath)) {
|
|
205
|
+
console.log(` ↷ Profile already exists at .specrails/profiles/project-default.json — skipped`);
|
|
206
|
+
}
|
|
207
|
+
} catch (e) {
|
|
208
|
+
console.warn(` ⚠ Could not scaffold profile: ${e.message}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
187
212
|
// Auto-yes: write defaults and exit (no TUI needed)
|
|
188
213
|
//
|
|
189
214
|
// Codex (OpenAI) support is currently being tested in our lab ("Coming Soon").
|
|
@@ -296,7 +321,8 @@ async function run() {
|
|
|
296
321
|
message: 'Agents to install:',
|
|
297
322
|
choices: buildCheckboxChoices(),
|
|
298
323
|
pageSize: agentPageSize,
|
|
299
|
-
|
|
324
|
+
// Core agents are installed unconditionally (disabled rows above), so an
|
|
325
|
+
// empty optional selection is valid — means "only core, nothing extra".
|
|
300
326
|
});
|
|
301
327
|
|
|
302
328
|
// Core agents are always included regardless of checkbox state
|
package/install.sh
CHANGED
|
@@ -4,6 +4,19 @@ set -euo pipefail
|
|
|
4
4
|
# specrails installer
|
|
5
5
|
# Installs the agent workflow system into any repository.
|
|
6
6
|
# Step 1 of 2: Prerequisites + scaffold. Step 2: Run /specrails:enrich inside Claude Code.
|
|
7
|
+
#
|
|
8
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
# Reserved paths (MUST NOT be created, modified, or deleted by this script):
|
|
10
|
+
# - <repo>/.specrails/profiles/** (project + hub-authored profile JSON)
|
|
11
|
+
# - <repo>/.claude/agents/custom-*.md (user-authored custom agents)
|
|
12
|
+
#
|
|
13
|
+
# Rationale: these paths hold user/team configuration that must survive
|
|
14
|
+
# re-running the installer. specrails-hub writes profile files here.
|
|
15
|
+
# Audited by tests/test-profiles.sh.
|
|
16
|
+
#
|
|
17
|
+
# Other paths under .specrails/ (install-config.yaml, specrails-version,
|
|
18
|
+
# specrails-manifest.json, setup-templates/) ARE managed by this script.
|
|
19
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
7
20
|
|
|
8
21
|
# Detect pipe mode (curl | bash) vs local execution
|
|
9
22
|
if [[ -z "${BASH_SOURCE[0]:-}" || "${BASH_SOURCE[0]:-}" == "bash" ]]; then
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specrails-core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"description": "AI agent workflow system for Claude Code — installs 12 specialized agents, orchestration commands, and persona-driven product discovery into any repository",
|
|
5
5
|
"bin": {
|
|
6
6
|
"specrails-core": "bin/specrails-core.js"
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
".claude/skills/",
|
|
14
14
|
"commands/",
|
|
15
15
|
"docs/",
|
|
16
|
+
"schemas/",
|
|
16
17
|
"VERSION"
|
|
17
18
|
],
|
|
18
19
|
"engines": {
|
|
@@ -58,6 +59,9 @@
|
|
|
58
59
|
"dependencies": {
|
|
59
60
|
"@inquirer/prompts": "^7.0.0"
|
|
60
61
|
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"ajv": "^8.18.0"
|
|
64
|
+
},
|
|
61
65
|
"license": "MIT",
|
|
62
66
|
"author": "fjpulidop"
|
|
63
67
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/fjpulidop/specrails-core/main/schemas/profile.v1.json",
|
|
4
|
+
"title": "specrails agent profile (v1)",
|
|
5
|
+
"description": "Declarative configuration for the specrails implement pipeline. Describes which agents participate, which models they run with, and how tasks are routed to specialists. Consumed by `implement.md` and produced by tools like specrails-hub.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["schemaVersion", "name", "orchestrator", "agents", "routing"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"schemaVersion": {
|
|
11
|
+
"const": 1,
|
|
12
|
+
"description": "Profile schema version. v1 is the only supported value at this time."
|
|
13
|
+
},
|
|
14
|
+
"name": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$",
|
|
17
|
+
"minLength": 1,
|
|
18
|
+
"maxLength": 64,
|
|
19
|
+
"description": "Human-readable identifier for this profile (kebab-case)."
|
|
20
|
+
},
|
|
21
|
+
"description": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"maxLength": 512,
|
|
24
|
+
"description": "Optional short summary of when to use this profile."
|
|
25
|
+
},
|
|
26
|
+
"orchestrator": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"required": ["model"],
|
|
29
|
+
"additionalProperties": false,
|
|
30
|
+
"properties": {
|
|
31
|
+
"model": {
|
|
32
|
+
"$ref": "#/$defs/modelAlias",
|
|
33
|
+
"description": "Model used to run the top-level implement orchestrator."
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"agents": {
|
|
38
|
+
"type": "array",
|
|
39
|
+
"minItems": 1,
|
|
40
|
+
"items": { "$ref": "#/$defs/agentEntry" },
|
|
41
|
+
"description": "Ordered chain of agents that participate in the pipeline when this profile is active.",
|
|
42
|
+
"allOf": [
|
|
43
|
+
{
|
|
44
|
+
"description": "Required baseline agent: sr-architect",
|
|
45
|
+
"contains": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"properties": { "id": { "const": "sr-architect" } },
|
|
48
|
+
"required": ["id"]
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"description": "Required baseline agent: sr-developer",
|
|
53
|
+
"contains": {
|
|
54
|
+
"type": "object",
|
|
55
|
+
"properties": { "id": { "const": "sr-developer" } },
|
|
56
|
+
"required": ["id"]
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"description": "Required baseline agent: sr-reviewer",
|
|
61
|
+
"contains": {
|
|
62
|
+
"type": "object",
|
|
63
|
+
"properties": { "id": { "const": "sr-reviewer" } },
|
|
64
|
+
"required": ["id"]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
"routing": {
|
|
70
|
+
"type": "array",
|
|
71
|
+
"minItems": 1,
|
|
72
|
+
"description": "Ordered routing rules. The first rule whose `tags` intersects the task's tag set wins. Exactly one terminal entry with `default: true` MUST appear as the last element.",
|
|
73
|
+
"items": { "$ref": "#/$defs/routingRule" }
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"$defs": {
|
|
77
|
+
"modelAlias": {
|
|
78
|
+
"type": "string",
|
|
79
|
+
"enum": ["sonnet", "opus", "haiku"],
|
|
80
|
+
"description": "Accepted model alias. Resolved to the current concrete model ID by the pipeline."
|
|
81
|
+
},
|
|
82
|
+
"agentEntry": {
|
|
83
|
+
"type": "object",
|
|
84
|
+
"required": ["id"],
|
|
85
|
+
"additionalProperties": false,
|
|
86
|
+
"properties": {
|
|
87
|
+
"id": {
|
|
88
|
+
"type": "string",
|
|
89
|
+
"pattern": "^(sr|custom)-[a-z0-9][a-z0-9-]*$",
|
|
90
|
+
"description": "Agent identifier. MUST correspond to a file at `.claude/agents/<id>.md`."
|
|
91
|
+
},
|
|
92
|
+
"model": {
|
|
93
|
+
"$ref": "#/$defs/modelAlias",
|
|
94
|
+
"description": "Model override for this agent when this profile is active. When omitted, the agent's frontmatter `model:` is used as a fallback."
|
|
95
|
+
},
|
|
96
|
+
"required": {
|
|
97
|
+
"type": "boolean",
|
|
98
|
+
"default": false,
|
|
99
|
+
"description": "If true, the agent cannot be routed past. Baseline agents (sr-architect, sr-developer, sr-reviewer) SHOULD be marked required."
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"routingRule": {
|
|
104
|
+
"oneOf": [
|
|
105
|
+
{
|
|
106
|
+
"type": "object",
|
|
107
|
+
"required": ["tags", "agent"],
|
|
108
|
+
"additionalProperties": false,
|
|
109
|
+
"properties": {
|
|
110
|
+
"tags": {
|
|
111
|
+
"type": "array",
|
|
112
|
+
"minItems": 1,
|
|
113
|
+
"items": {
|
|
114
|
+
"type": "string",
|
|
115
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
|
116
|
+
},
|
|
117
|
+
"description": "Task tags that trigger this rule. Rule wins if any tag intersects."
|
|
118
|
+
},
|
|
119
|
+
"agent": {
|
|
120
|
+
"type": "string",
|
|
121
|
+
"pattern": "^(sr|custom)-[a-z0-9][a-z0-9-]*$",
|
|
122
|
+
"description": "Agent to route the task to when this rule wins."
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"type": "object",
|
|
128
|
+
"required": ["default", "agent"],
|
|
129
|
+
"additionalProperties": false,
|
|
130
|
+
"properties": {
|
|
131
|
+
"default": {
|
|
132
|
+
"const": true,
|
|
133
|
+
"description": "Marks this rule as the terminal catch-all. Exactly one entry with `default: true` is allowed, and it MUST be the last element of `routing`."
|
|
134
|
+
},
|
|
135
|
+
"agent": {
|
|
136
|
+
"type": "string",
|
|
137
|
+
"pattern": "^(sr|custom)-[a-z0-9][a-z0-9-]*$",
|
|
138
|
+
"description": "Agent to route the task to when no earlier rule matched."
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -34,6 +34,7 @@ Scan `$ARGUMENTS` for control flags:
|
|
|
34
34
|
- If `--deps "<spec>"` is present: capture the quoted string as `DEPS_SPEC`. Strip from arguments.
|
|
35
35
|
- If `--concurrency N` is present: set `CONCURRENCY=N` (integer ≥ 1). Default: 3.
|
|
36
36
|
- If `--wave-size N` is present: set `WAVE_SIZE=N` (integer ≥ 1). Default: unlimited (no per-wave cap).
|
|
37
|
+
- If `--profiles "<spec>"` is present: parse a per-rail profile map of the form `ref=profile-name,ref=profile-name,...`. Capture as `PROFILE_MAP` and strip from arguments. Each mapped ref's `/specrails:implement` invocation will run under the named profile (the profile must exist at `.specrails/profiles/<profile-name>.json`). Unmapped refs inherit the batch-level profile resolution (see below).
|
|
37
38
|
|
|
38
39
|
**If `DRY_RUN=true`**, print:
|
|
39
40
|
```
|
|
@@ -189,14 +190,18 @@ For each wave `W`:
|
|
|
189
190
|
|
|
190
191
|
1. Print: `[wave W/TOTAL_WAVES] Starting — features: <ref-list>`
|
|
191
192
|
2. For each feature batch of size ≤ `CONCURRENCY` within the wave:
|
|
193
|
+
- For every ref in the batch, determine the profile spawn env:
|
|
194
|
+
- If `PROFILE_MAP` contains an entry for this ref, set `SPECRAILS_PROFILE_PATH=<abs path to .specrails/profiles/<mapped-name>.json>` for this invocation only.
|
|
195
|
+
- Otherwise, inherit whatever `$SPECRAILS_PROFILE_PATH` was set by the caller (e.g. specrails-hub), or leave unset so the rail falls back to `.specrails/profiles/project-default.json` or legacy mode.
|
|
192
196
|
- Invoke `/specrails:implement` with the feature refs and forwarded flags:
|
|
193
197
|
```
|
|
194
|
-
/specrails:implement <
|
|
198
|
+
SPECRAILS_PROFILE_PATH=<resolved-per-rail-path> /specrails:implement <ref> [--dry-run]
|
|
195
199
|
```
|
|
200
|
+
- Each ref in the batch spawns with its own resolved profile path — **distinct rails in the same batch MAY use distinct profiles concurrently**.
|
|
196
201
|
- Run invocations in the batch in parallel (`run_in_background: true`).
|
|
197
202
|
- Wait for all in the batch to complete before starting the next batch.
|
|
198
203
|
3. For each completed invocation, record outcome in `WAVE_RESULTS`:
|
|
199
|
-
- `{ref, wave, status: "done" | "failed", error_summary: "..." | null}`
|
|
204
|
+
- `{ref, wave, status: "done" | "failed", profile: "<name or empty>", error_summary: "..." | null}`
|
|
200
205
|
|
|
201
206
|
### Failure isolation
|
|
202
207
|
|
|
@@ -61,13 +61,150 @@ which openspec && openspec --version
|
|
|
61
61
|
|
|
62
62
|
#### 5. Agent discovery
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
Agent discovery runs in one of two modes: **profile mode** (a profile JSON is active) or **legacy mode** (no profile — identical to pre-4.1.0 behavior).
|
|
65
|
+
|
|
66
|
+
##### Profile detection
|
|
67
|
+
|
|
68
|
+
A profile is active when either condition holds:
|
|
69
|
+
|
|
70
|
+
1. The environment variable `SPECRAILS_PROFILE_PATH` is set AND points to a readable file. Tools like `specrails-hub` set this to a job-scoped snapshot.
|
|
71
|
+
2. The file `.specrails/profiles/project-default.json` exists and is readable.
|
|
72
|
+
|
|
73
|
+
If condition 1 holds, use `$SPECRAILS_PROFILE_PATH` as the profile path. Otherwise, if condition 2 holds, use `.specrails/profiles/project-default.json`. Otherwise, fall through to **legacy mode**.
|
|
74
|
+
|
|
75
|
+
##### Preflight: `jq` availability (profile mode only)
|
|
76
|
+
|
|
77
|
+
When running in profile mode, `jq` is required to read the profile JSON. Run:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
command -v jq >/dev/null 2>&1 || { echo "[error] 'jq' is required for profile-aware mode. Install with: brew install jq / apt install jq / https://stedolan.github.io/jq/"; exit 1; }
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
##### Profile mode — load, validate, populate
|
|
84
|
+
|
|
85
|
+
Read the profile:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
PROFILE="$(cat "$PROFILE_PATH")"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Validate the schema version. Only `schemaVersion: 1` is supported:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
SCHEMA_VERSION="$(jq -r '.schemaVersion // empty' <<<"$PROFILE")"
|
|
95
|
+
case "$SCHEMA_VERSION" in
|
|
96
|
+
1) ;;
|
|
97
|
+
"") echo "[error] profile validation failed: missing required field 'schemaVersion'"; exit 1 ;;
|
|
98
|
+
*) echo "[error] profile validation failed: unsupported schemaVersion '$SCHEMA_VERSION'. Supported: 1"; exit 1 ;;
|
|
99
|
+
esac
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Validate required top-level fields. Every valid v1 profile MUST contain `name`, `orchestrator.model`, `agents` (non-empty array), and `routing` (non-empty array):
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
for field in name orchestrator agents routing; do
|
|
106
|
+
jq -e ".$field" <<<"$PROFILE" >/dev/null 2>&1 || { echo "[error] profile validation failed: missing required field '$field'"; exit 1; }
|
|
107
|
+
done
|
|
108
|
+
jq -e '.orchestrator.model' <<<"$PROFILE" >/dev/null 2>&1 || { echo "[error] profile validation failed: missing required field 'orchestrator.model'"; exit 1; }
|
|
109
|
+
jq -e '.agents | length > 0' <<<"$PROFILE" >/dev/null 2>&1 || { echo "[error] profile validation failed: 'agents' must be a non-empty array"; exit 1; }
|
|
110
|
+
jq -e '.routing | length > 0' <<<"$PROFILE" >/dev/null 2>&1 || { echo "[error] profile validation failed: 'routing' must be a non-empty array"; exit 1; }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Validate baseline agents — `sr-architect`, `sr-developer`, and `sr-reviewer` MUST appear in `agents[]`:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
for required in sr-architect sr-developer sr-reviewer; do
|
|
117
|
+
jq -e --arg id "$required" '[.agents[].id] | index($id)' <<<"$PROFILE" >/dev/null 2>&1 \
|
|
118
|
+
|| { echo "[error] profile validation failed: required baseline agent '$required' missing from 'agents[]'"; exit 1; }
|
|
119
|
+
done
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Validate routing terminal rule — exactly one entry SHALL have `default: true` and it MUST be the last element:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
DEFAULT_COUNT="$(jq '[.routing[] | select(.default == true)] | length' <<<"$PROFILE")"
|
|
126
|
+
if [[ "$DEFAULT_COUNT" -ne 1 ]]; then
|
|
127
|
+
echo "[error] profile validation failed: routing must contain exactly one entry with 'default: true' (found $DEFAULT_COUNT)"; exit 1
|
|
128
|
+
fi
|
|
129
|
+
IS_LAST="$(jq '(.routing | last | .default) == true' <<<"$PROFILE")"
|
|
130
|
+
if [[ "$IS_LAST" != "true" ]]; then
|
|
131
|
+
echo "[error] profile validation failed: the 'default: true' routing rule must be the last element of 'routing'"; exit 1
|
|
132
|
+
fi
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Populate `AVAILABLE_AGENTS` from the profile and verify each referenced agent file exists on disk:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
AVAILABLE_AGENTS="$(jq -r '.agents[].id' <<<"$PROFILE" | sort)"
|
|
139
|
+
for id in $AVAILABLE_AGENTS; do
|
|
140
|
+
[[ -f ".claude/agents/$id.md" ]] \
|
|
141
|
+
|| { echo "[error] profile references agent '$id' but .claude/agents/$id.md does not exist"; exit 1; }
|
|
142
|
+
done
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Also store per-agent model overrides and the orchestrator model for use in later phases:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# ORCHESTRATOR_MODEL is informational; the caller is responsible for spawning
|
|
149
|
+
# the orchestrator with this model (e.g. specrails-hub reads this field directly).
|
|
150
|
+
ORCHESTRATOR_MODEL="$(jq -r '.orchestrator.model' <<<"$PROFILE")"
|
|
151
|
+
|
|
152
|
+
# Per-agent model overrides keyed by agent id.
|
|
153
|
+
# Consumed by subagent invocation sites in later phases.
|
|
154
|
+
declare -A AGENT_MODEL
|
|
155
|
+
while IFS=$'\t' read -r id model; do
|
|
156
|
+
[[ -n "$model" && "$model" != "null" ]] && AGENT_MODEL[$id]="$model"
|
|
157
|
+
done < <(jq -r '.agents[] | [.id, (.model // "null")] | @tsv' <<<"$PROFILE")
|
|
158
|
+
|
|
159
|
+
# Routing rules (array), consumed by Phase 3b.
|
|
160
|
+
ROUTING="$(jq '.routing' <<<"$PROFILE")"
|
|
161
|
+
|
|
162
|
+
PROFILE_MODE="profile"
|
|
163
|
+
PROFILE_NAME="$(jq -r '.name' <<<"$PROFILE")"
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
##### Apply per-agent model overrides (profile mode only)
|
|
167
|
+
|
|
168
|
+
Claude Code's Agent tool determines a subagent's model from the `model:` line in the agent's `.md` frontmatter at invocation time — there is no per-call model parameter. When a profile is active, rewrite each agent's frontmatter `model:` value in-place to match `AGENT_MODEL[$id]`.
|
|
169
|
+
|
|
170
|
+
This rewrite is safe because:
|
|
171
|
+
- Multi-feature runs execute in **isolated git worktrees** (`isolation: worktree`), so each rail mutates its own copy of `.claude/agents/` without cross-rail contention.
|
|
172
|
+
- Single-feature runs are sequential within a single checkout.
|
|
173
|
+
- The hub writes a job-scoped snapshot of the profile and spawns `claude` with `$SPECRAILS_PROFILE_PATH` pointing at it; the frontmatter rewrite follows the snapshot, never the catalog.
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
for id in "${!AGENT_MODEL[@]}"; do
|
|
177
|
+
model="${AGENT_MODEL[$id]}"
|
|
178
|
+
file=".claude/agents/$id.md"
|
|
179
|
+
[[ -f "$file" ]] || continue
|
|
180
|
+
# Rewrite the first `model:` line within the frontmatter block (lines between the
|
|
181
|
+
# first two `---` separators). Use sed with portable syntax (macOS + Linux).
|
|
182
|
+
awk -v new="$model" '
|
|
183
|
+
BEGIN { in_fm=0; done=0 }
|
|
184
|
+
/^---$/ { in_fm = !in_fm; print; next }
|
|
185
|
+
in_fm && !done && /^model:[[:space:]]/ { print "model: " new; done=1; next }
|
|
186
|
+
{ print }
|
|
187
|
+
' "$file" > "$file.tmp" && mv "$file.tmp" "$file"
|
|
188
|
+
done
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
If a profile does not declare `model` for a given agent (the field is optional), that agent's frontmatter is left untouched.
|
|
192
|
+
|
|
193
|
+
##### Legacy mode — preserve current behavior
|
|
194
|
+
|
|
195
|
+
If no profile is active, scan the agents directory exactly as before:
|
|
65
196
|
|
|
66
197
|
```bash
|
|
67
|
-
ls .claude/agents/sr-*.md 2>/dev/null | sed 's|.*/||;s|\.md$||' | sort
|
|
198
|
+
AVAILABLE_AGENTS="$(ls .claude/agents/sr-*.md 2>/dev/null | sed 's|.*/||;s|\.md$||' | sort)"
|
|
199
|
+
PROFILE_MODE="legacy"
|
|
200
|
+
PROFILE_NAME=""
|
|
68
201
|
```
|
|
69
202
|
|
|
70
|
-
|
|
203
|
+
Per-agent model overrides are empty in legacy mode — subagent invocations inherit the `model:` value from each agent's `.md` frontmatter. Routing in Phase 3b uses the hardcoded legacy rules.
|
|
204
|
+
|
|
205
|
+
##### Agent roles (both modes)
|
|
206
|
+
|
|
207
|
+
The pipeline adapts dynamically to the installed agents:
|
|
71
208
|
|
|
72
209
|
| Agent | Role | Required? | Phase(s) affected |
|
|
73
210
|
|-------|------|-----------|-------------------|
|
|
@@ -88,6 +225,7 @@ Store the result as `AVAILABLE_AGENTS` (a list of agent IDs). The pipeline adapt
|
|
|
88
225
|
**Gate rules** (applied throughout the pipeline):
|
|
89
226
|
- If an optional agent is NOT in `AVAILABLE_AGENTS`, **skip** that phase/sub-step silently and note `"<agent> not installed — skipping"`.
|
|
90
227
|
- Core agents are guaranteed to exist. If a core agent is missing, **STOP** and print: `[error] Core agent <name> not found. Run /specrails:enrich or reinstall.`
|
|
228
|
+
- In **profile mode**, the profile's `agents[]` IS the source of truth for `AVAILABLE_AGENTS`. Agents not listed are considered unavailable regardless of what is on disk.
|
|
91
229
|
|
|
92
230
|
### Summary
|
|
93
231
|
|
|
@@ -518,7 +656,47 @@ Produce three sets: `FRONTEND_TASKS`, `BACKEND_TASKS`, `OTHER_TASKS`.
|
|
|
518
656
|
|
|
519
657
|
**Step 2 — Route tasks to developer agents:**
|
|
520
658
|
|
|
521
|
-
|
|
659
|
+
Routing operates in one of two modes depending on the value of `PROFILE_MODE` set in Phase -1.
|
|
660
|
+
|
|
661
|
+
##### Profile mode (`PROFILE_MODE=profile`)
|
|
662
|
+
|
|
663
|
+
When a profile is active, apply `ROUTING` rules in their array order. For each task, collect its tag set (layer tags plus any explicit `[tag]` markers in tasks.md). The first rule whose `tags` array intersects the task's tag set wins. The terminal `default: true` rule catches tasks matched by no earlier rule.
|
|
664
|
+
|
|
665
|
+
Example (pseudocode):
|
|
666
|
+
|
|
667
|
+
```bash
|
|
668
|
+
assigned_agent_for_task() {
|
|
669
|
+
local -a task_tags=("$@")
|
|
670
|
+
local rule_count
|
|
671
|
+
rule_count=$(jq 'length' <<<"$ROUTING")
|
|
672
|
+
local i=0
|
|
673
|
+
while [[ $i -lt $rule_count ]]; do
|
|
674
|
+
local is_default rule_tags agent
|
|
675
|
+
is_default=$(jq -r ".[$i].default // false" <<<"$ROUTING")
|
|
676
|
+
agent=$(jq -r ".[$i].agent" <<<"$ROUTING")
|
|
677
|
+
if [[ "$is_default" == "true" ]]; then
|
|
678
|
+
echo "$agent"
|
|
679
|
+
return
|
|
680
|
+
fi
|
|
681
|
+
rule_tags=$(jq -r ".[$i].tags[]" <<<"$ROUTING")
|
|
682
|
+
for rtag in $rule_tags; do
|
|
683
|
+
for ttag in "${task_tags[@]}"; do
|
|
684
|
+
if [[ "$rtag" == "$ttag" ]]; then
|
|
685
|
+
echo "$agent"
|
|
686
|
+
return
|
|
687
|
+
fi
|
|
688
|
+
done
|
|
689
|
+
done
|
|
690
|
+
i=$((i + 1))
|
|
691
|
+
done
|
|
692
|
+
}
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
Produce `DEVELOPER_ROUTING` from the per-task decisions, grouping by assigned agent. An agent assigned by the profile SHALL only be used if it also appears in `AVAILABLE_AGENTS` (the profile's own `agents[]`). If the profile routes a task to an agent not present in `agents[]`, that is a profile configuration bug — **STOP** and print: `[error] profile routing references agent '<id>' which is not declared in profile.agents[]`.
|
|
696
|
+
|
|
697
|
+
##### Legacy mode (`PROFILE_MODE=legacy`)
|
|
698
|
+
|
|
699
|
+
When no profile is active, evaluate available developer agents in `AVAILABLE_AGENTS` and apply these hardcoded rules in priority order:
|
|
522
700
|
|
|
523
701
|
| Condition | Agent(s) selected | Mode |
|
|
524
702
|
|-----------|-------------------|------|
|
|
@@ -531,6 +709,14 @@ Evaluate available developer agents in `AVAILABLE_AGENTS` and apply these rules
|
|
|
531
709
|
|
|
532
710
|
Store the result as `DEVELOPER_ROUTING`: a map of `{agent_id: [task_list]}`.
|
|
533
711
|
|
|
712
|
+
##### Routing trace (both modes)
|
|
713
|
+
|
|
714
|
+
After computing `DEVELOPER_ROUTING`, optionally emit a trace line to aid debugging:
|
|
715
|
+
|
|
716
|
+
```
|
|
717
|
+
[phase-3b] routing decision: mode=$PROFILE_MODE profile=${PROFILE_NAME:-none} agents=[list]
|
|
718
|
+
```
|
|
719
|
+
|
|
534
720
|
**Step 3 — Print routing decision:**
|
|
535
721
|
|
|
536
722
|
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"name": "default",
|
|
4
|
+
"description": "Baseline profile equivalent to pre-4.1.0 legacy behavior: full chain, per-agent models matching the shipped agent frontmatters, and legacy routing rules.",
|
|
5
|
+
"orchestrator": { "model": "sonnet" },
|
|
6
|
+
"agents": [
|
|
7
|
+
{ "id": "sr-product-manager", "model": "opus" },
|
|
8
|
+
{ "id": "sr-product-analyst", "model": "haiku" },
|
|
9
|
+
{ "id": "sr-architect", "model": "sonnet", "required": true },
|
|
10
|
+
{ "id": "sr-developer", "model": "sonnet", "required": true },
|
|
11
|
+
{ "id": "sr-frontend-developer", "model": "sonnet" },
|
|
12
|
+
{ "id": "sr-backend-developer", "model": "sonnet" },
|
|
13
|
+
{ "id": "sr-test-writer", "model": "sonnet" },
|
|
14
|
+
{ "id": "sr-doc-sync", "model": "sonnet" },
|
|
15
|
+
{ "id": "sr-merge-resolver", "model": "sonnet" },
|
|
16
|
+
{ "id": "sr-reviewer", "model": "sonnet", "required": true },
|
|
17
|
+
{ "id": "sr-frontend-reviewer", "model": "sonnet" },
|
|
18
|
+
{ "id": "sr-backend-reviewer", "model": "sonnet" },
|
|
19
|
+
{ "id": "sr-security-reviewer", "model": "sonnet" },
|
|
20
|
+
{ "id": "sr-performance-reviewer","model": "sonnet" }
|
|
21
|
+
],
|
|
22
|
+
"routing": [
|
|
23
|
+
{ "tags": ["frontend"], "agent": "sr-frontend-developer" },
|
|
24
|
+
{ "tags": ["backend"], "agent": "sr-backend-developer" },
|
|
25
|
+
{ "default": true, "agent": "sr-developer" }
|
|
26
|
+
]
|
|
27
|
+
}
|
package/update.sh
CHANGED
|
@@ -4,6 +4,19 @@ set -euo pipefail
|
|
|
4
4
|
# specrails updater
|
|
5
5
|
# Updates an existing specrails installation in a target repository.
|
|
6
6
|
# Preserves project-specific customizations (agents, personas, rules).
|
|
7
|
+
#
|
|
8
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
# Reserved paths (MUST NOT be created, modified, or deleted by this script):
|
|
10
|
+
# - <repo>/.specrails/profiles/** (project + hub-authored profile JSON)
|
|
11
|
+
# - <repo>/.claude/agents/custom-*.md (user-authored custom agents)
|
|
12
|
+
#
|
|
13
|
+
# Rationale: these paths hold user/team configuration that survives across
|
|
14
|
+
# specrails-core upgrades. specrails-hub writes profile files here. Breaking
|
|
15
|
+
# this contract silently destroys user work. Audited by tests/test-profiles.sh.
|
|
16
|
+
#
|
|
17
|
+
# Other paths under .specrails/ (install-config.yaml, specrails-version,
|
|
18
|
+
# specrails-manifest.json, setup-templates/) ARE managed by this script.
|
|
19
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
7
20
|
|
|
8
21
|
# Detect pipe mode (curl | bash) vs local execution
|
|
9
22
|
if [[ -z "${BASH_SOURCE[0]:-}" || "${BASH_SOURCE[0]:-}" == "bash" ]]; then
|