specrails-core 4.12.1 → 5.1.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 +103 -339
- package/bin/specrails-core.mjs +20 -98
- package/bin/tui-installer.mjs +22 -105
- package/commands/doctor.md +1 -1
- package/dist/installer/cli.js +16 -2
- package/dist/installer/cli.js.map +1 -1
- package/dist/installer/commands/doctor.js +3 -5
- package/dist/installer/commands/doctor.js.map +1 -1
- package/dist/installer/commands/framework.js +64 -49
- package/dist/installer/commands/framework.js.map +1 -1
- package/dist/installer/commands/init.js +122 -82
- package/dist/installer/commands/init.js.map +1 -1
- package/dist/installer/commands/update.js +90 -83
- package/dist/installer/commands/update.js.map +1 -1
- package/dist/installer/commands/v5-migration.js +133 -0
- package/dist/installer/commands/v5-migration.js.map +1 -0
- package/dist/installer/phases/framework-lifecycle.js +2 -0
- package/dist/installer/phases/framework-lifecycle.js.map +1 -1
- package/dist/installer/phases/install-config.js +3 -6
- package/dist/installer/phases/install-config.js.map +1 -1
- package/dist/installer/phases/manifest.js +2 -6
- package/dist/installer/phases/manifest.js.map +1 -1
- package/dist/installer/phases/prereqs.js +0 -1
- package/dist/installer/phases/prereqs.js.map +1 -1
- package/dist/installer/phases/scaffold.js +228 -405
- package/dist/installer/phases/scaffold.js.map +1 -1
- package/dist/installer/runtime/pipeline-state.js +801 -0
- package/dist/installer/runtime/pipeline-state.js.map +1 -0
- package/dist/installer/util/install-transaction.js +246 -0
- package/dist/installer/util/install-transaction.js.map +1 -0
- package/dist/installer/util/registry.js +20 -0
- package/dist/installer/util/registry.js.map +1 -1
- package/docs/ci-cd.md +57 -0
- package/docs/user-docs/codex-vs-claude-code.md +23 -151
- package/docs/user-docs/core-updates.md +70 -0
- package/docs/user-docs/provider-pipelines.md +53 -0
- package/integration-contract.json +179 -66
- package/package.json +5 -2
- package/schemas/profile.v1.json +1 -1
- package/templates/agents/sr-architect.md +30 -0
- package/templates/agents/sr-developer.md +30 -19
- package/templates/agents/sr-reviewer.md +70 -64
- package/templates/codex-skills/batch-implement/SKILL.md +58 -267
- package/templates/codex-skills/implement/SKILL.md +136 -420
- package/templates/codex-skills/rails/sr-architect/SKILL.md +45 -20
- package/templates/codex-skills/rails/sr-developer/SKILL.md +42 -10
- package/templates/codex-skills/rails/sr-reviewer/SKILL.md +60 -15
- package/templates/codex-skills/retry/SKILL.md +37 -117
- package/templates/commands/specrails/batch-implement.md +16 -288
- package/templates/commands/specrails/doctor.md +1 -1
- package/templates/commands/specrails/implement.md +94 -1260
- 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 +22 -350
- package/templates/commands/specrails/telemetry.md +1 -1
- package/templates/gemini-commands/batch-implement.toml +28 -40
- package/templates/gemini-commands/implement.toml +55 -105
- package/templates/gemini-commands/retry.toml +21 -0
- package/templates/kimi/specrails/run-skill.mjs +51 -2
- package/templates/profiles/default.json +5 -18
- package/templates/runtime/provider-pipeline.md +55 -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
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { renameSync, rmSync } from 'node:fs';
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { constants, cpSync, mkdtempSync, renameSync, rmSync } from 'node:fs';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { atomicSymlinkSwap, copyDir, copyFile, isDir, isSymlink, listDir, mkdirp, pathExists, readBytes, readTextFile, removePath, symlinkOrCopy, writeFileLf, } from '../util/fs.js';
|
|
6
6
|
import { info, ok, warn } from '../util/logger.js';
|
|
7
7
|
import { buildManifest, writeManifestFiles } from './manifest.js';
|
|
8
8
|
/**
|
|
9
|
-
* The three baseline agents
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* agent are optional add-ons selected at install time.
|
|
9
|
+
* The three baseline agents — the COMPLETE set of agents the installer
|
|
10
|
+
* ships. The implement pipeline depends on all three. Any additional agent
|
|
11
|
+
* comes from a user-authored profile (`custom-*`), never the installer.
|
|
13
12
|
*
|
|
14
13
|
* Mirrors the `allOf` baseline in schemas/profile.v1.json — update
|
|
15
14
|
* both files together if this set ever changes.
|
|
@@ -19,11 +18,6 @@ export const CORE_AGENTS = new Set([
|
|
|
19
18
|
'sr-developer',
|
|
20
19
|
'sr-reviewer',
|
|
21
20
|
]);
|
|
22
|
-
/**
|
|
23
|
-
* Agents excluded from the quick tier because they require a full
|
|
24
|
-
* /specrails:enrich persona pass to function correctly.
|
|
25
|
-
*/
|
|
26
|
-
const QUICK_EXCLUDED_AGENTS = new Set(['sr-product-manager', 'sr-product-analyst']);
|
|
27
21
|
/**
|
|
28
22
|
* Gemini built-in tool ids granted to every `.gemini/agents/sr-*.md` subagent.
|
|
29
23
|
* (Validated headless in the desktop spike — read/write/shell/glob/grep.) Because
|
|
@@ -132,23 +126,17 @@ const GEMINI_MODEL_BY_AGENT = {
|
|
|
132
126
|
'sr-reviewer': 'gemini-3.5-flash',
|
|
133
127
|
};
|
|
134
128
|
const GEMINI_DEFAULT_MODEL = 'gemini-3.5-flash';
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
* VPC-only agents (sr-product-manager, sr-product-analyst).
|
|
147
|
-
*/
|
|
148
|
-
const QUICK_EXCLUDED_SKILLS = new Set([
|
|
149
|
-
'sr-auto-propose-backlog-specs',
|
|
150
|
-
'sr-get-backlog-specs',
|
|
151
|
-
]);
|
|
129
|
+
// Older Gemini loaders reject optional agent-limit fields. Opt in only after
|
|
130
|
+
// the caller verified the installed loader capability; never guess from a model.
|
|
131
|
+
export function geminiAgentLimitMetadata(env = process.env) {
|
|
132
|
+
if (env.SPECRAILS_GEMINI_AGENT_LIMITS !== 'supported')
|
|
133
|
+
return [];
|
|
134
|
+
const value = Number(env.SPECRAILS_GEMINI_MAX_TURNS ?? '60');
|
|
135
|
+
if (!Number.isInteger(value) || value < 1 || value > 200) {
|
|
136
|
+
throw new Error('SPECRAILS_GEMINI_MAX_TURNS must be an integer from 1 to 200');
|
|
137
|
+
}
|
|
138
|
+
return [`max_turns: ${value}`];
|
|
139
|
+
}
|
|
152
140
|
/**
|
|
153
141
|
* Claude top-level `sr-*` skills, GENERATED at install time from their
|
|
154
142
|
* canonical slash-command body under `templates/commands/specrails/<command>.md`.
|
|
@@ -178,26 +166,7 @@ const SKILL_FROM_COMMAND = {
|
|
|
178
166
|
command: 'why',
|
|
179
167
|
description: 'sr:why — Search explanation records written by specrails agents during the OpenSpec implementation pipeline.',
|
|
180
168
|
},
|
|
181
|
-
'sr-get-backlog-specs': {
|
|
182
|
-
command: 'get-backlog-specs',
|
|
183
|
-
description: 'sr:get-backlog-specs — View product-driven backlog from GitHub Issues and propose top 3 for implementation.',
|
|
184
|
-
},
|
|
185
|
-
'sr-auto-propose-backlog-specs': {
|
|
186
|
-
command: 'auto-propose-backlog-specs',
|
|
187
|
-
description: 'sr:auto-propose-backlog-specs — Generate new feature ideas through product discovery, create GitHub Issues.',
|
|
188
|
-
},
|
|
189
169
|
};
|
|
190
|
-
/**
|
|
191
|
-
* Command → required agent dependency map. A command is excluded
|
|
192
|
-
* from the quick tier when its required agent was excluded (i.e.
|
|
193
|
-
* VPC-dependent) or when the feature flag (Agent Teams) is off.
|
|
194
|
-
*/
|
|
195
|
-
const COMMAND_AGENT_DEPENDENCIES = [
|
|
196
|
-
{ command: 'auto-propose-backlog-specs', requires: ['sr-product-manager'] },
|
|
197
|
-
{ command: 'vpc-drift', requires: ['sr-product-manager', 'sr-product-analyst'] },
|
|
198
|
-
{ command: 'get-backlog-specs', requires: ['sr-product-analyst'] },
|
|
199
|
-
{ command: 'merge-resolve', requires: ['sr-merge-resolver'] },
|
|
200
|
-
];
|
|
201
170
|
/**
|
|
202
171
|
* Agents that write "explanation" memory records. When any of them
|
|
203
172
|
* ships we also need a shared `.claude/agent-memory/explanations/`
|
|
@@ -255,6 +224,7 @@ export function detectExistingSetup(input) {
|
|
|
255
224
|
* .gitignore. Returns a summary for logging / tests.
|
|
256
225
|
*/
|
|
257
226
|
export function scaffoldInstallation(input) {
|
|
227
|
+
assertPipelineRuntimeSource(input.scriptDir);
|
|
258
228
|
const createdDirs = [];
|
|
259
229
|
let copiedFiles = 0;
|
|
260
230
|
const mk = (abs) => {
|
|
@@ -267,15 +237,15 @@ export function scaffoldInstallation(input) {
|
|
|
267
237
|
// Codex skills live under <providerDir>/skills/ (e.g. .codex/skills/).
|
|
268
238
|
// The pre-§18 code wrote to `.agents/skills/` which codex doesn't read;
|
|
269
239
|
// that was a placeholder name from the gated state.
|
|
270
|
-
mk(path.join(input.artifactRoot, input.providerDir, 'skills', 'enrich'));
|
|
271
240
|
mk(path.join(input.artifactRoot, input.providerDir, 'skills', 'doctor'));
|
|
272
241
|
mk(path.join(input.artifactRoot, input.providerDir, 'skills', 'rails'));
|
|
273
242
|
}
|
|
274
243
|
else if (input.provider === 'gemini') {
|
|
275
244
|
// Gemini: TOML commands under .gemini/commands/specrails/ + native
|
|
276
|
-
// subagents
|
|
245
|
+
// subagents and OpenSpec skills both live in the execution workspace.
|
|
277
246
|
mk(path.join(input.artifactRoot, input.providerDir, 'commands', 'specrails'));
|
|
278
247
|
mk(path.join(input.artifactRoot, input.providerDir, 'agents'));
|
|
248
|
+
mk(path.join(input.artifactRoot, input.providerDir, 'skills'));
|
|
279
249
|
}
|
|
280
250
|
else if (input.provider === 'kimi') {
|
|
281
251
|
mk(path.join(input.artifactRoot, input.providerDir, 'skills'));
|
|
@@ -291,7 +261,6 @@ export function scaffoldInstallation(input) {
|
|
|
291
261
|
mk(path.join(setupTemplates, 'commands'));
|
|
292
262
|
mk(path.join(setupTemplates, 'skills'));
|
|
293
263
|
mk(path.join(setupTemplates, 'rules'));
|
|
294
|
-
mk(path.join(setupTemplates, 'personas'));
|
|
295
264
|
mk(path.join(setupTemplates, 'claude-md'));
|
|
296
265
|
mk(path.join(setupTemplates, 'settings'));
|
|
297
266
|
// --- .gitignore hygiene ---
|
|
@@ -327,19 +296,19 @@ export function scaffoldInstallation(input) {
|
|
|
327
296
|
else {
|
|
328
297
|
warn(`templates/ not found at ${templatesSrc} — skipping template copy`);
|
|
329
298
|
}
|
|
330
|
-
// --- Write bundled commands (
|
|
299
|
+
// --- Write bundled commands (doctor.md) ---
|
|
331
300
|
copyBundledCommands({ ...input, copiedIncrement: (n) => (copiedFiles += n) });
|
|
332
301
|
pruneLegacyArtifacts(input);
|
|
302
|
+
copiedFiles += placePipelineRuntime(input);
|
|
333
303
|
if (input.provider === 'kimi') {
|
|
334
304
|
copiedFiles += placeKimiSkillRunner(input);
|
|
335
305
|
}
|
|
336
|
-
// ---
|
|
337
|
-
|
|
338
|
-
const placed =
|
|
306
|
+
// --- Direct placement (the only path) ---
|
|
307
|
+
{
|
|
308
|
+
const placed = placeArtefacts({ ...input });
|
|
339
309
|
copiedFiles += placed.agents + placed.commands + placed.rules;
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
`${placed.rules} rule file(s) directly into ${input.providerDir}/${skippedNote}`);
|
|
310
|
+
info(`Placed ${placed.agents} agent(s) + ${placed.commands} command(s) + ` +
|
|
311
|
+
`${placed.rules} rule file(s) directly into ${input.providerDir}/`);
|
|
343
312
|
}
|
|
344
313
|
// --- Skills placement (both tiers, both providers) ---
|
|
345
314
|
// Claude: top-level `sr-*` skills are generated from their canonical
|
|
@@ -349,10 +318,24 @@ export function scaffoldInstallation(input) {
|
|
|
349
318
|
{
|
|
350
319
|
const skills = placeSkills(input);
|
|
351
320
|
copiedFiles += skills.filesCopied;
|
|
352
|
-
const skillSkipNote = skills.skipped > 0 ? ` (skipped ${skills.skipped} VPC-dependent)` : '';
|
|
353
321
|
const skillsLabel = input.provider === 'gemini' ? 'agent' : 'skill';
|
|
354
322
|
const skillsSubdir = input.provider === 'gemini' ? 'agents' : 'skills';
|
|
355
|
-
info(`Placed ${skills.placed} ${skillsLabel}(s) into ${input.providerDir}/${skillsSubdir}
|
|
323
|
+
info(`Placed ${skills.placed} ${skillsLabel}(s) into ${input.providerDir}/${skillsSubdir}/`);
|
|
324
|
+
}
|
|
325
|
+
const pipelineContract = providerPipelineContract(input.scriptDir);
|
|
326
|
+
if (input.provider === 'codex') {
|
|
327
|
+
for (const name of ['implement', 'batch-implement', 'retry']) {
|
|
328
|
+
prependSkillContract(path.join(input.artifactRoot, '.codex', 'skills', name, 'SKILL.md'), pipelineContract);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
else if (input.provider === 'gemini') {
|
|
332
|
+
for (const name of ['implement', 'batch-implement', 'retry']) {
|
|
333
|
+
const file = path.join(input.artifactRoot, '.gemini', 'commands', 'specrails', `${name}.toml`);
|
|
334
|
+
if (pipelineContract && pathExists(file)) {
|
|
335
|
+
const source = readTextFile(file);
|
|
336
|
+
writeFileLf(file, source.replace("prompt = '''\n", "prompt = '''\n" + pipelineContract));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
356
339
|
}
|
|
357
340
|
// --- Codex provider settings + AGENTS.md initial content ---
|
|
358
341
|
if (input.provider === 'codex') {
|
|
@@ -376,20 +359,6 @@ export function scaffoldInstallation(input) {
|
|
|
376
359
|
info(`Kimi provider: wrote ${written} setting file(s) (.kimi-code/AGENTS.md, mcp.json)`);
|
|
377
360
|
}
|
|
378
361
|
}
|
|
379
|
-
// --- Full-tier hint: enrich is required to generate VPC artefacts ---
|
|
380
|
-
if (input.tier === 'full') {
|
|
381
|
-
const cliName = input.provider === 'codex'
|
|
382
|
-
? 'Codex CLI'
|
|
383
|
-
: input.provider === 'gemini'
|
|
384
|
-
? 'Gemini CLI'
|
|
385
|
-
: input.provider === 'kimi'
|
|
386
|
-
? 'Kimi CLI'
|
|
387
|
-
: 'Claude Code';
|
|
388
|
-
const enrichCommand = input.provider === 'kimi' ? '/skill:specrails-enrich' : '/specrails:enrich';
|
|
389
|
-
info(`Full tier staged. Run \`${enrichCommand}\` in ${cliName} to generate ` +
|
|
390
|
-
'VPC personas and adapt agents (including sr-product-manager and ' +
|
|
391
|
-
'sr-product-analyst) to this codebase.');
|
|
392
|
-
}
|
|
393
362
|
ok(`Created ${createdDirs.length} directories, copied ${copiedFiles} files`);
|
|
394
363
|
return {
|
|
395
364
|
existingSetup: detectExistingSetup({
|
|
@@ -453,6 +422,8 @@ function frameworkSourceHash(scriptDir, provider) {
|
|
|
453
422
|
const treeHash = hashFrameworkTrees([
|
|
454
423
|
{ label: 'templates', dir: path.join(scriptDir, 'templates') },
|
|
455
424
|
{ label: 'commands', dir: path.join(scriptDir, 'commands') },
|
|
425
|
+
{ label: 'pipeline-runtime', dir: path.join(scriptDir, 'dist', 'installer', 'runtime') },
|
|
426
|
+
{ label: 'installer-renderers', dir: path.join(scriptDir, 'dist', 'installer', 'phases') },
|
|
456
427
|
], { ignorePackageNoise: true });
|
|
457
428
|
return `sha256:${createHash('sha256')
|
|
458
429
|
.update(treeHash)
|
|
@@ -464,6 +435,7 @@ function frameworkSourceHash(scriptDir, provider) {
|
|
|
464
435
|
function frameworkContentHash(providerFrameworkDir) {
|
|
465
436
|
return hashFrameworkTrees([
|
|
466
437
|
{ label: 'provider', dir: providerFrameworkDir },
|
|
438
|
+
{ label: 'pipeline-runtime', dir: path.join(path.dirname(providerFrameworkDir), '.specrails', 'runtime') },
|
|
467
439
|
]);
|
|
468
440
|
}
|
|
469
441
|
function readFrameworkStamp(stampPath) {
|
|
@@ -530,60 +502,91 @@ export function installFramework(input) {
|
|
|
530
502
|
stamp.content_hash === frameworkContentHash(providerFrameworkDir)) {
|
|
531
503
|
return { providerFrameworkDir, versionDir, materialized: false };
|
|
532
504
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
artifactRoot:
|
|
553
|
-
codeRoot
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
505
|
+
mkdirp(input.frameworkDir);
|
|
506
|
+
const stageRoot = mkdtempSync(path.join(input.frameworkDir, '.materialize-'));
|
|
507
|
+
const stagedVersionDir = path.join(stageRoot, input.version);
|
|
508
|
+
const stagedProviderDir = path.join(stagedVersionDir, input.providerDir);
|
|
509
|
+
const stagedStampPath = frameworkStampPath(stagedVersionDir, input.providerDir);
|
|
510
|
+
try {
|
|
511
|
+
// Preserve all sibling providers while rebuilding the requested provider.
|
|
512
|
+
// The stage is new: JS traversal keeps these copies away from Node 22's
|
|
513
|
+
// native Unicode directory-copy defect on Windows (nodejs/node#61878).
|
|
514
|
+
if (isDir(versionDir))
|
|
515
|
+
cpSync(versionDir, stagedVersionDir, { recursive: true, dereference: false, verbatimSymlinks: true, filter: () => true, mode: constants.COPYFILE_FICLONE });
|
|
516
|
+
else
|
|
517
|
+
mkdirp(stagedVersionDir);
|
|
518
|
+
// Framework provider trees are entirely Core-owned. Rebuilding from a clean
|
|
519
|
+
// destination removes stale files as well as repairing corrupt/missing ones,
|
|
520
|
+
// without touching sibling providers already materialized in this version.
|
|
521
|
+
removePath(stagedProviderDir);
|
|
522
|
+
removePath(stagedStampPath);
|
|
523
|
+
// Reuse scaffoldInstallation's static-placement helpers by pointing
|
|
524
|
+
// `artifactRoot` at the version dir. `seedProjectDirs: false` keeps the copy
|
|
525
|
+
// free of per-workspace mutable state. The `codeRoot` is irrelevant to the
|
|
526
|
+
// STATIC subtree (the project-named instruction files are skipped below), so
|
|
527
|
+
// we hand it the framework dir to satisfy the contract — and we DELETE any
|
|
528
|
+
// project-named instruction file the settings helpers wrote.
|
|
529
|
+
// The SHARED framework store is always the FULL SUPERSET — EVERY agent — so a
|
|
530
|
+
// SECOND project with a DIFFERENT agent selection links its specialists from
|
|
531
|
+
// the same materialized copy instead of inheriting the first project's
|
|
532
|
+
// narrower set. Per-project filtering moves to the workspace LINK step
|
|
533
|
+
// (`linkAgentFiles` via `assembleProjectWorkspace`). `selectedAgents` on the
|
|
534
|
+
// input is intentionally IGNORED here.
|
|
535
|
+
const staticInput = {
|
|
536
|
+
scriptDir: input.scriptDir,
|
|
537
|
+
artifactRoot: stagedVersionDir,
|
|
538
|
+
codeRoot: versionDir,
|
|
539
|
+
provider: input.provider,
|
|
540
|
+
providerDir: input.providerDir,
|
|
541
|
+
selectedAgents: undefined,
|
|
542
|
+
materializeAllAgents: true,
|
|
543
|
+
seedProjectDirs: false,
|
|
544
|
+
};
|
|
545
|
+
scaffoldInstallation(staticInput);
|
|
546
|
+
// The settings helpers also emit a project-named root instruction file
|
|
547
|
+
// (AGENTS.md/GEMINI.md/CLAUDE.md) + (for codex) config.toml / (gemini)
|
|
548
|
+
// settings.json. The instruction file is per-project → strip it from the
|
|
549
|
+
// shared copy; the settings file IS provider-invariant and stays as a
|
|
550
|
+
// link target inside the providerDir.
|
|
551
|
+
for (const f of ['AGENTS.md', 'GEMINI.md', 'CLAUDE.md']) {
|
|
552
|
+
rmSync(path.join(stagedVersionDir, f), { force: true });
|
|
553
|
+
}
|
|
554
|
+
// Kimi's instruction and MCP files are provider-local rather than root-local,
|
|
555
|
+
// but both are project-specific and must be real files in each workspace.
|
|
556
|
+
// In particular, linking mcp.json would let Desktop mutate the shared
|
|
557
|
+
// framework and leak one project's MCP registry into every other project.
|
|
558
|
+
if (input.provider === 'kimi') {
|
|
559
|
+
rmSync(path.join(stagedProviderDir, 'AGENTS.md'), { force: true });
|
|
560
|
+
rmSync(path.join(stagedProviderDir, 'mcp.json'), { force: true });
|
|
561
|
+
}
|
|
562
|
+
const frameworkStamp = {
|
|
563
|
+
schema: 1,
|
|
564
|
+
version: input.version,
|
|
565
|
+
provider: input.provider,
|
|
566
|
+
source_hash: sourceHash,
|
|
567
|
+
content_hash: frameworkContentHash(stagedProviderDir),
|
|
568
|
+
};
|
|
569
|
+
writeFileLf(stagedStampPath, `${JSON.stringify(frameworkStamp, null, 2)}\n`);
|
|
570
|
+
// Keep the previous complete version outside the disposable staging root.
|
|
571
|
+
// It remains available for manual recovery even after successful publication.
|
|
572
|
+
const previous = path.join(input.frameworkDir, `.previous-${input.version}-${randomUUID()}`);
|
|
573
|
+
const hadPrevious = pathExists(versionDir);
|
|
574
|
+
if (hadPrevious)
|
|
575
|
+
renameSync(versionDir, previous);
|
|
576
|
+
try {
|
|
577
|
+
renameSync(stagedVersionDir, versionDir);
|
|
578
|
+
}
|
|
579
|
+
catch (error) {
|
|
580
|
+
if (hadPrevious)
|
|
581
|
+
renameSync(previous, versionDir);
|
|
582
|
+
throw error;
|
|
583
|
+
}
|
|
584
|
+
return { providerFrameworkDir, versionDir, materialized: true };
|
|
585
|
+
}
|
|
586
|
+
finally {
|
|
587
|
+
// This contains only newly generated candidate files, never the prior version.
|
|
588
|
+
rmSync(stageRoot, { recursive: true, force: true });
|
|
577
589
|
}
|
|
578
|
-
const frameworkStamp = {
|
|
579
|
-
schema: 1,
|
|
580
|
-
version: input.version,
|
|
581
|
-
provider: input.provider,
|
|
582
|
-
source_hash: sourceHash,
|
|
583
|
-
content_hash: frameworkContentHash(providerFrameworkDir),
|
|
584
|
-
};
|
|
585
|
-
writeFileLf(stampPath, `${JSON.stringify(frameworkStamp, null, 2)}\n`);
|
|
586
|
-
return { providerFrameworkDir, versionDir, materialized: true };
|
|
587
590
|
}
|
|
588
591
|
/**
|
|
589
592
|
* Atomically point `<frameworkDir>/current` at `<version>` so every workspace's
|
|
@@ -663,6 +666,10 @@ export function assembleProjectWorkspace(input) {
|
|
|
663
666
|
links[settingsFile] = symlinkOrCopy(settingsTarget, settingsLink, preferCopy);
|
|
664
667
|
}
|
|
665
668
|
}
|
|
669
|
+
const runtimeTarget = path.join(input.frameworkDir, 'current', '.specrails', 'runtime');
|
|
670
|
+
if (pathExists(runtimeTarget)) {
|
|
671
|
+
links.pipelineRuntime = symlinkOrCopy(runtimeTarget, path.join(input.workspace, '.specrails', 'runtime'), preferCopy);
|
|
672
|
+
}
|
|
666
673
|
// (b) Seed the PROJECT layer (real writable files / dirs).
|
|
667
674
|
const seededMemoryAgents = seedProjectLayer(input, currentProviderDir);
|
|
668
675
|
// Manifest: record the consumed framework version. `buildManifest` hashes the
|
|
@@ -697,7 +704,7 @@ function seedProjectLayer(input, currentProviderDir) {
|
|
|
697
704
|
if (!name.endsWith('.md'))
|
|
698
705
|
continue;
|
|
699
706
|
const id = name.slice(0, -3);
|
|
700
|
-
if (selected.has(id)
|
|
707
|
+
if (selected.has(id))
|
|
701
708
|
placedAgentIds.push(id);
|
|
702
709
|
}
|
|
703
710
|
}
|
|
@@ -743,7 +750,6 @@ function seedProjectLayer(input, currentProviderDir) {
|
|
|
743
750
|
const id = path.basename(roleDir);
|
|
744
751
|
if (/^sr-[a-z0-9-]+$/.test(id) &&
|
|
745
752
|
selected.has(id) &&
|
|
746
|
-
!QUICK_EXCLUDED_AGENTS.has(id) &&
|
|
747
753
|
pathExists(path.join(roleDir, 'SKILL.md'))) {
|
|
748
754
|
placedAgentIds.push(id);
|
|
749
755
|
}
|
|
@@ -799,10 +805,10 @@ function seedKimiMcpFile(mcpPath) {
|
|
|
799
805
|
* every SELECTED framework-owned agent at the shared read-only copy.
|
|
800
806
|
*
|
|
801
807
|
* `selectedIds` is the per-project agent allow-list (already unioned with the
|
|
802
|
-
* CORE trio by the caller). Only framework agents whose id is in it
|
|
803
|
-
*
|
|
804
|
-
*
|
|
805
|
-
*
|
|
808
|
+
* CORE trio by the caller). Only framework agents whose id is in it are linked —
|
|
809
|
+
* the shared framework store is the full superset, so this is where per-project
|
|
810
|
+
* filtering lands. `undefined` ⇒ link every framework agent (used by the legacy
|
|
811
|
+
* callers / parity tests).
|
|
806
812
|
*
|
|
807
813
|
* When `preferCopy` is true each agent is COPIED as a real file rather than
|
|
808
814
|
* symlinked (the in-repo standalone install — so a standalone user's CLI finds
|
|
@@ -826,7 +832,7 @@ function linkAgentFiles(frameworkAgentsDir, workspaceAgentsDir, selectedIds, pre
|
|
|
826
832
|
continue;
|
|
827
833
|
frameworkProvided.add(name);
|
|
828
834
|
const id = name.slice(0, -3);
|
|
829
|
-
if (selectedIds &&
|
|
835
|
+
if (selectedIds && !selectedIds.has(id))
|
|
830
836
|
continue;
|
|
831
837
|
linkedNames.add(name);
|
|
832
838
|
const m = symlinkOrCopy(src, path.join(workspaceAgentsDir, name), preferCopy);
|
|
@@ -886,8 +892,7 @@ function linkKimiSkillDirectories(frameworkSkillsDir, workspaceSkillsDir, select
|
|
|
886
892
|
// it, but never expose nested roles if a caller supplies one directly.
|
|
887
893
|
continue;
|
|
888
894
|
}
|
|
889
|
-
if (/^sr-[a-z0-9-]+$/.test(name) &&
|
|
890
|
-
(!selectedRoleIds.has(name) || QUICK_EXCLUDED_AGENTS.has(name))) {
|
|
895
|
+
if (/^sr-[a-z0-9-]+$/.test(name) && !selectedRoleIds.has(name)) {
|
|
891
896
|
continue;
|
|
892
897
|
}
|
|
893
898
|
linkedFrameworkNames.add(name);
|
|
@@ -1008,9 +1013,8 @@ function copyBundledCommands(input) {
|
|
|
1008
1013
|
const destDir = path.join(input.artifactRoot, input.providerDir, 'skills', skillName);
|
|
1009
1014
|
// A codex-native override (written for spawn_agent semantics + the
|
|
1010
1015
|
// correct `.codex/skills/rails/` layout) wins over the claude port.
|
|
1011
|
-
// This is the ONLY codex command-placement pass
|
|
1012
|
-
//
|
|
1013
|
-
// body — e.g. enrich's obsolete `.codex/agents/*.toml` model.
|
|
1016
|
+
// This is the ONLY codex command-placement pass, so without the override
|
|
1017
|
+
// check codex users get the claude body with no codex-native semantics.
|
|
1014
1018
|
const overrideSkill = path.join(codexOverrides, skillName, 'SKILL.md');
|
|
1015
1019
|
if (pathExists(overrideSkill)) {
|
|
1016
1020
|
copyDir(path.join(codexOverrides, skillName), destDir);
|
|
@@ -1179,7 +1183,9 @@ const KIMI_ROLE_EXECUTION_CONTRACT = [
|
|
|
1179
1183
|
'Every `key`, profile stem, and worktree id uses the same 1–64 character grammar as `run`.',
|
|
1180
1184
|
'Use `"current"` for roles that target the orchestrator repository. The',
|
|
1181
1185
|
'helper gives each such role a private execution directory while setting its',
|
|
1182
|
-
'`SPECRAILS_REPO_DIR` to that repository
|
|
1186
|
+
'`SPECRAILS_REPO_DIR` to that repository. The child preserves the absolute',
|
|
1187
|
+
'`SPECRAILS_EXECUTION_CONTEXT`, `SPECRAILS_BACKLOG_PATH` and pipeline helper.',
|
|
1188
|
+
'Read the frozen specs there; never infer task scope from the child cwd. Nested calls do not',
|
|
1183
1189
|
'collide. Where later instructions request `isolation: worktree`, use',
|
|
1184
1190
|
'`"worktree:<feature-id>"`; reuse that exact value for the developer, test,',
|
|
1185
1191
|
'documentation, and other sequential roles belonging to the same feature.',
|
|
@@ -1255,8 +1261,9 @@ const KIMI_RUNTIME_CONTEXT_CONTRACT = [
|
|
|
1255
1261
|
'configured”, never fabricated rows or scores.',
|
|
1256
1262
|
'',
|
|
1257
1263
|
'For every `KIMI_BACKLOG_*` marker, first read and validate',
|
|
1258
|
-
'
|
|
1259
|
-
'
|
|
1264
|
+
'`${SPECRAILS_BACKLOG_ROOT}/.specrails/backlog-config.json` when configured.',
|
|
1265
|
+
'The frozen execution context takes priority. Route `local` through',
|
|
1266
|
+
'`${SPECRAILS_BACKLOG_PATH}` only when ownership allows writes; route `github` through the',
|
|
1260
1267
|
'approved `gh issue` operation; route `jira` only through the configured',
|
|
1261
1268
|
'project/base URL and credentials. Honour read-only mode and never perform a',
|
|
1262
1269
|
'write operation when configuration is missing, invalid, or read-only.',
|
|
@@ -1346,7 +1353,7 @@ function writeKimiWorkflowSkill(args) {
|
|
|
1346
1353
|
...args.placeholders,
|
|
1347
1354
|
MEMORY_PATH: '.kimi-code/agent-memory/',
|
|
1348
1355
|
}).replaceAll('.specrails/profiles/project-default.json', '.specrails/profiles/kimi-default.json');
|
|
1349
|
-
const rendered = translateClaudeTextForKimi(adaptKimiWorkflowBody(args.commandName, providerNeutral));
|
|
1356
|
+
const rendered = translateClaudeTextForKimi(adaptKimiWorkflowBody(args.commandName, providerNeutral)).replaceAll('.specrails/local-tickets.json', '${SPECRAILS_BACKLOG_PATH}');
|
|
1350
1357
|
const frontmatter = [
|
|
1351
1358
|
'---',
|
|
1352
1359
|
`name: ${skillName}`,
|
|
@@ -1359,6 +1366,8 @@ function writeKimiWorkflowSkill(args) {
|
|
|
1359
1366
|
'',
|
|
1360
1367
|
].join('\n');
|
|
1361
1368
|
writeFileLf(args.dest, frontmatter +
|
|
1369
|
+
(pathExists(path.join(path.dirname(args.src), '..', '..', 'runtime', 'provider-pipeline.md'))
|
|
1370
|
+
? readTextFile(path.join(path.dirname(args.src), '..', '..', 'runtime', 'provider-pipeline.md')) + '\n' : '') +
|
|
1362
1371
|
KIMI_NESTED_SKILL_CONTRACT +
|
|
1363
1372
|
KIMI_ROLE_EXECUTION_CONTRACT +
|
|
1364
1373
|
KIMI_RUNTIME_CONTEXT_CONTRACT +
|
|
@@ -1385,182 +1394,25 @@ function adaptKimiWorkflowBody(commandName, body) {
|
|
|
1385
1394
|
}
|
|
1386
1395
|
if (commandName !== 'implement')
|
|
1387
1396
|
return body;
|
|
1388
|
-
|
|
1389
|
-
'#####
|
|
1390
|
-
'',
|
|
1391
|
-
'Keep each `AGENT_MODEL[id]` value exactly as declared in the profile.',
|
|
1392
|
-
'Do **not** rewrite role `SKILL.md` frontmatter: Kimi directory skills do',
|
|
1393
|
-
'not carry per-role model configuration. In the orchestrator, resolve the',
|
|
1394
|
-
'role id against the parsed `AGENT_MODEL` map, then write the exact result',
|
|
1395
|
-
'into that role wave entry\'s JSON `model` field using WriteFile. Use',
|
|
1396
|
-
'the provider default `k3` when absent; never depend on a shell array from',
|
|
1397
|
-
'a previous tool call. Only the official short ids `k3`,',
|
|
1398
|
-
'`kimi-for-coding`, and `kimi-for-coding-highspeed` gain the',
|
|
1399
|
-
'`kimi-code/` prefix at the CLI boundary. Never map Claude aliases.',
|
|
1400
|
-
'',
|
|
1401
|
-
].join('\n'));
|
|
1402
|
-
adapted = replaceMarkdownSection(adapted, '##### Legacy mode — preserve current behavior', '##### Agent roles (both modes)', [
|
|
1403
|
-
'##### Legacy mode — preserve current behavior',
|
|
1404
|
-
'',
|
|
1405
|
-
'If no profile is active, discover role directories without mutating them:',
|
|
1406
|
-
'',
|
|
1407
|
-
'```bash',
|
|
1408
|
-
'AVAILABLE_AGENTS="$(find -L .kimi-code/skills -mindepth 2 -maxdepth 2 \\',
|
|
1409
|
-
' -type f -name SKILL.md -print 2>/dev/null | sed \'s|.*/skills/||;s|/SKILL.md$||\' \\',
|
|
1410
|
-
' | grep -E \'^(sr|custom)-\' | sort)"',
|
|
1411
|
-
'PROFILE_MODE="legacy"',
|
|
1412
|
-
'PROFILE_NAME=""',
|
|
1413
|
-
'```',
|
|
1414
|
-
'',
|
|
1415
|
-
'Per-role model overrides are empty in legacy mode. External role',
|
|
1416
|
-
'processes use the explicit Kimi default (`k3`, launched as',
|
|
1417
|
-
'`kimi-code/k3`).',
|
|
1418
|
-
'',
|
|
1419
|
-
].join('\n'));
|
|
1420
|
-
adapted = replaceMarkdownSection(adapted, '#### Merge Algorithm', '**Step 4: Record outcomes**', [
|
|
1421
|
-
'#### Kimi role-wave merge algorithm',
|
|
1397
|
+
return body.replace('##### Invocation configuration', [
|
|
1398
|
+
'##### Kimi invocation configuration',
|
|
1422
1399
|
'',
|
|
1423
|
-
'
|
|
1424
|
-
'
|
|
1425
|
-
'
|
|
1400
|
+
'Keep the parsed `AGENT_MODEL` map as structured orchestration data.',
|
|
1401
|
+
'Resolve each role model from its exact profile value and put it in the',
|
|
1402
|
+
'role-wave JSON `model` field; absent values use `k3`. Shell variables',
|
|
1403
|
+
'from earlier tools are not persistent. Never rewrite role frontmatter',
|
|
1404
|
+
'or translate a Claude model alias into a Kimi model.',
|
|
1426
1405
|
'',
|
|
1427
|
-
'
|
|
1428
|
-
'
|
|
1429
|
-
'
|
|
1430
|
-
'',
|
|
1431
|
-
'
|
|
1432
|
-
'`manifestPath`, and each safe worktree id, `repoDir`, and complete',
|
|
1433
|
-
'`changes` list. Every change is `{status:"A"|"M"|"D",path}`. This',
|
|
1434
|
-
'inventory compares against the synthetic baseline snapshot, includes',
|
|
1435
|
-
'committed/staged/unstaged and non-ignored untracked role output, and',
|
|
1436
|
-
'excludes `.kimi-code` plus SpecRails run-state. Never discover changed',
|
|
1437
|
-
'files with a shell loop, newline splitting, or a hard-coded `main` ref.',
|
|
1438
|
-
'',
|
|
1439
|
-
'Classify paths across all worktrees before applying anything:',
|
|
1440
|
-
'- `exclusive_files`: appears in one worktree only.',
|
|
1441
|
-
'- `shared_files`: appears in two or more worktrees.',
|
|
1442
|
-
'- Preserve each A/M/D status; a D path has no source file to copy.',
|
|
1443
|
-
'',
|
|
1444
|
-
'**Exclusive A/M/D actions**',
|
|
1445
|
-
'',
|
|
1446
|
-
'For each feature in `MERGE_ORDER`, use structured WriteFile (never shell',
|
|
1447
|
-
'interpolation) to write `.specrails/kimi-role-merge.json`:',
|
|
1448
|
-
'',
|
|
1449
|
-
'```json',
|
|
1450
|
-
'{',
|
|
1451
|
-
' "run": "<stable-run-id>",',
|
|
1452
|
-
' "actions": [',
|
|
1453
|
-
' {"worktree":"<safe-id>","path":"<exact-git-path>","operation":"copy"},',
|
|
1454
|
-
' {"worktree":"<safe-id>","path":"<deleted-path>","operation":"delete"}',
|
|
1455
|
-
' ]',
|
|
1456
|
-
'}',
|
|
1457
|
-
'```',
|
|
1458
|
-
'',
|
|
1459
|
-
'Use `copy` for A/M and `delete` for D, then run exactly:',
|
|
1460
|
-
'',
|
|
1461
|
-
'```sh',
|
|
1462
|
-
'node .kimi-code/specrails/run-skill.mjs \\',
|
|
1463
|
-
' --role-merge-file .specrails/kimi-role-merge.json',
|
|
1464
|
-
'```',
|
|
1465
|
-
'',
|
|
1466
|
-
'The helper validates the one-shot file, manifest, registered worktree,',
|
|
1467
|
-
'and path containment, then copies bytes/symlinks or deletes the target',
|
|
1468
|
-
'without a shell. Filenames may contain spaces, Unicode, quotes, `$()`,',
|
|
1469
|
-
'or leading dashes; never place them in a Bash command. It rejects',
|
|
1470
|
-
'provider/run-state paths, traversal, duplicate targets, directories,',
|
|
1471
|
-
'and symlinked target parents.',
|
|
1472
|
-
'',
|
|
1473
|
-
'**Shared paths**',
|
|
1474
|
-
'',
|
|
1475
|
-
'Process shared paths in `MERGE_ORDER`:',
|
|
1476
|
-
'1. D in every contributor: submit one validated `delete` action.',
|
|
1477
|
-
'2. D versus A/M: record a delete/modify conflict; do not silently copy',
|
|
1478
|
-
' or delete it.',
|
|
1479
|
-
'3. A/M text: use structured ReadFile on each emitted `repoDir` + exact',
|
|
1480
|
-
' path and on the current merge target. Apply the existing Markdown',
|
|
1481
|
-
' section-aware strategy for `.md`; for other text perform a three-way',
|
|
1482
|
-
' semantic merge against the current target, writing through WriteFile.',
|
|
1483
|
-
'4. Binary/type conflicts: record them for `sr-merge-resolver`; never',
|
|
1484
|
-
' decode or round-trip binary data through model text.',
|
|
1485
|
-
'5. A resolved whole-file winner may be applied with one validated copy',
|
|
1486
|
-
' action. Any unresolved region receives the existing conflict markers',
|
|
1487
|
-
' and `MERGE_REPORT` entry.',
|
|
1488
|
-
'',
|
|
1489
|
-
'When `DRY_RUN=true`, do not invoke the repository merge-action helper.',
|
|
1490
|
-
'Write resolved A/M outputs under `CACHE_DIR` with structured WriteFile',
|
|
1491
|
-
'and record D paths as deletion operations in `.cache-manifest.json`.',
|
|
1492
|
-
'Keep worktrees for inspection as the surrounding dry-run rule requires.',
|
|
1406
|
+
'Every implementation role uses `workspace:"current"` and the same',
|
|
1407
|
+
'aggregate execution context. Serialize writers within supplied roots;',
|
|
1408
|
+
'no per-ticket full pipeline, nested worktree, copied-file merge or',
|
|
1409
|
+
'replacement run. The runner preserves shared backlog and frozen specs',
|
|
1410
|
+
'even though each role has a private execution cwd.',
|
|
1493
1411
|
'',
|
|
1494
1412
|
].join('\n'));
|
|
1495
|
-
adapted = adapted
|
|
1496
|
-
.replace(' "implemented_files": [],', [
|
|
1497
|
-
' "implemented_files": [],',
|
|
1498
|
-
' "kimi_role_wave": {',
|
|
1499
|
-
' "run": "<stable-run-id>",',
|
|
1500
|
-
' "manifest_path": ".specrails/kimi-role-worktrees/<stable-run-id>.json",',
|
|
1501
|
-
' "base_commit": null,',
|
|
1502
|
-
' "workspaces": {}',
|
|
1503
|
-
' },',
|
|
1504
|
-
].join('\n'))
|
|
1505
|
-
.replace('If the write succeeds: set `PIPELINE_STATE_AVAILABLE=true`.', [
|
|
1506
|
-
'If the write succeeds: set `PIPELINE_STATE_AVAILABLE=true`.',
|
|
1507
|
-
'',
|
|
1508
|
-
'**Kimi retry state:** after every `specrails.role.workspace` frame,',
|
|
1509
|
-
'atomically refresh `kimi_role_wave.manifest_path`, `base_commit`, and',
|
|
1510
|
-
'`workspaces[<feature-id>]` from the helper output. Never synthesize',
|
|
1511
|
-
'these values. Keep the same `run` and `worktree:<feature-id>` for that',
|
|
1512
|
-
'feature through developer, test, docs, and review. On any failure keep',
|
|
1513
|
-
'the manifest and worktrees. After every required change has been merged',
|
|
1514
|
-
'successfully, run the static cleanup command from the Kimi role',
|
|
1515
|
-
'contract and set `kimi_role_wave` to `null` in pipeline state.',
|
|
1516
|
-
].join('\n'))
|
|
1517
|
-
.replaceAll('git -C <worktree-path> diff main --name-only', 'git -C <worktree-path> diff <base-commit> --name-only')
|
|
1518
|
-
.replaceAll('git -C <worktree-path> diff main -- <file>', 'git -C <worktree-path> diff <base-commit> -- <file>')
|
|
1519
|
-
.replace('(`<worktree-path>` is an absolute git-worktree path supplied by the runtime; `git -C <worktree-path>` already targets it directly.)', '(`<worktree-path>` is the `repoDir` emitted by the role-wave helper, and `<base-commit>` is read from its persisted manifest; `git -C <worktree-path>` already targets it directly.)');
|
|
1520
|
-
return adapted;
|
|
1521
1413
|
}
|
|
1522
1414
|
function adaptKimiBatchImplement(body) {
|
|
1523
|
-
return
|
|
1524
|
-
'### Kimi wave invocation',
|
|
1525
|
-
'',
|
|
1526
|
-
'Nested `specrails-implement` executions are independent foreground Kimi',
|
|
1527
|
-
'processes. Do not call multiple built-in `Skill` tools in one Kimi',
|
|
1528
|
-
'session and do not share one checkout concurrently.',
|
|
1529
|
-
'',
|
|
1530
|
-
'Choose one safe `BATCH_RUN` id. For dependency wave `W`, derive the',
|
|
1531
|
-
'deterministic safe run id `<BATCH_RUN>-w<W>`. Process waves sequentially:',
|
|
1532
|
-
'',
|
|
1533
|
-
'1. For a normal repository launch, partition each dependency wave into',
|
|
1534
|
-
' foreground batches of at most `min(CONCURRENCY,32)` entries. Each entry uses',
|
|
1535
|
-
' `skill:"specrails-implement"`, `workspace:"worktree:<feature-id>"`,',
|
|
1536
|
-
' the complete `<ref> [--dry-run]` arguments, the selected profile stem',
|
|
1537
|
-
' (or `"inherit"`), and that profile\'s exact `orchestrator.model` (or',
|
|
1538
|
-
' `k3`). Feature ids and keys must be collision-free safe ids.',
|
|
1539
|
-
'2. Wait for all completion frames. A failed entry fails only that ticket;',
|
|
1540
|
-
' preserve its manifest/worktree for diagnosis and record the failure.',
|
|
1541
|
-
'3. Before a downstream dependency wave, call `--role-wave-status` for',
|
|
1542
|
-
' the completed wave. Merge each successful worktree\'s A/M/D inventory',
|
|
1543
|
-
' into the batch repository with the same structured merge-file and',
|
|
1544
|
-
' shared-path rules defined by `specrails-implement`. Never interpolate',
|
|
1545
|
-
' a filename into Shell. If merge succeeds, run',
|
|
1546
|
-
' `node .kimi-code/specrails/run-skill.mjs --role-wave-cleanup <run>`.',
|
|
1547
|
-
' This makes predecessor output part of the next wave\'s newly captured',
|
|
1548
|
-
' synthetic baseline. Do not cleanup failed or unmerged worktrees.',
|
|
1549
|
-
'4. Record `{ref,wave,status,profile,error_summary,run,manifest_path,',
|
|
1550
|
-
' workspace}` in `WAVE_RESULTS` before starting another batch.',
|
|
1551
|
-
'',
|
|
1552
|
-
'Inside a specrails-desktop isolated rail worktree, effective concurrency',
|
|
1553
|
-
'is exactly 1. Submit a one-entry foreground role wave per ticket with',
|
|
1554
|
-
'`workspace:"current"` and a deterministic unique run id; wait before the',
|
|
1555
|
-
'next ticket. No sibling worktree, status merge, or cleanup is needed',
|
|
1556
|
-
'because every nested implementation writes directly into the desktop',
|
|
1557
|
-
'rail\'s current repository.',
|
|
1558
|
-
'',
|
|
1559
|
-
'Per-ticket profiles remain isolated: `profile` is either `inherit` or the',
|
|
1560
|
-
'validated filename stem from `PROFILE_MAP`; `model` is resolved from the',
|
|
1561
|
-
'same profile before writing JSON. Never export a profile globally.',
|
|
1562
|
-
'',
|
|
1563
|
-
].join('\n'));
|
|
1415
|
+
return body.replace('Delegate to implement once with all frozen specs and selected roots.', 'Activate `Skill(skill="specrails-implement", args="<all original arguments>")` once in this orchestrator with all frozen specs and selected roots. Do not launch a role wave of full implementations or one implementation per ticket.');
|
|
1564
1416
|
}
|
|
1565
1417
|
function adaptKimiAutoPropose(body) {
|
|
1566
1418
|
return body
|
|
@@ -1579,36 +1431,19 @@ function adaptKimiAutoPropose(body) {
|
|
|
1579
1431
|
.replaceAll('After the Explore agent completes:', 'After the sr-product-analyst role completes:');
|
|
1580
1432
|
}
|
|
1581
1433
|
function adaptKimiRetry(body) {
|
|
1582
|
-
return body
|
|
1583
|
-
.replace('- `PHASE_STATUSES` ← `phases` map (`architect`, `developer`, `test-writer`, `doc-sync`, `reviewer`, `ship`, `ci` → `"done"`, `"failed"`, `"skipped"`, or `"pending"`)', [
|
|
1584
|
-
'- `PHASE_STATUSES` ← `phases` map (`architect`, `developer`, `test-writer`, `doc-sync`, `reviewer`, `ship`, `ci` → `"done"`, `"failed"`, `"skipped"`, or `"pending"`)',
|
|
1585
|
-
'- `KIMI_ROLE_WAVE` ← `kimi_role_wave` (required when an isolated Kimi',
|
|
1586
|
-
' phase has already started): persisted `run`, `manifest_path`,',
|
|
1587
|
-
' `base_commit`, and feature→workspace mapping.',
|
|
1588
|
-
].join('\n'))
|
|
1589
|
-
.replace('**Validation:**', [
|
|
1590
|
-
'**Kimi workspace validation (before any phase):**',
|
|
1434
|
+
return body + [
|
|
1591
1435
|
'',
|
|
1592
|
-
'
|
|
1593
|
-
'`node .kimi-code/specrails/run-skill.mjs --role-wave-status <run>`.',
|
|
1594
|
-
'The returned manifest path, base commit, and workspace ids must exactly',
|
|
1595
|
-
'match pipeline state. Any mismatch/missing/unregistered worktree is a',
|
|
1596
|
-
'hard stop: report recovery instructions and do not create a replacement',
|
|
1597
|
-
'worktree. A retry must use the same run and exact',
|
|
1598
|
-
'`worktree:<feature-id>` mapping so successful developer changes survive',
|
|
1599
|
-
'a later test/docs/reviewer failure. Refresh state from emitted frames',
|
|
1600
|
-
'after each resumed role. Never choose a new run while valid state',
|
|
1601
|
-
'exists; never cleanup before every required phase and merge succeeds.',
|
|
1436
|
+
'## Kimi direct-role continuation',
|
|
1602
1437
|
'',
|
|
1603
|
-
'
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
'
|
|
1438
|
+
'Use the existing runtime status and exact absolute context. Invoke only',
|
|
1439
|
+
'the required sr-* or profile role through a foreground role wave, using',
|
|
1440
|
+
'`workspace:"current"`; do not activate a nested specrails-implement.',
|
|
1441
|
+
'Pass the complete bounded handoff explicitly, including every frozen',
|
|
1442
|
+
'criterion, selected roots, current phase and next action. Native session',
|
|
1443
|
+
'memory is not a substitute. Preserve valid completed phases and source',
|
|
1444
|
+
'work when a later reviewer or archive step is blocked.',
|
|
1607
1445
|
'',
|
|
1608
|
-
|
|
1609
|
-
'static `--role-wave-cleanup <run>` helper. Only after its cleanup frame',
|
|
1610
|
-
'succeeds set `kimi_role_wave` to `null`. A failed retry retains state.',
|
|
1611
|
-
].join('\n'));
|
|
1446
|
+
].join('\n');
|
|
1612
1447
|
}
|
|
1613
1448
|
function renderKimiEnrichWorkflow() {
|
|
1614
1449
|
return [
|
|
@@ -1780,15 +1615,6 @@ function renderKimiTelemetryWorkflow() {
|
|
|
1780
1615
|
'',
|
|
1781
1616
|
].join('\n');
|
|
1782
1617
|
}
|
|
1783
|
-
function replaceMarkdownSection(body, startHeading, endHeading, replacement) {
|
|
1784
|
-
const start = body.indexOf(startHeading);
|
|
1785
|
-
if (start < 0)
|
|
1786
|
-
return body;
|
|
1787
|
-
const end = body.indexOf(endHeading, start + startHeading.length);
|
|
1788
|
-
if (end < 0)
|
|
1789
|
-
return body;
|
|
1790
|
-
return body.slice(0, start) + replacement + body.slice(end);
|
|
1791
|
-
}
|
|
1792
1618
|
function writeKimiRoleSkill(args) {
|
|
1793
1619
|
if (!pathExists(args.src))
|
|
1794
1620
|
return;
|
|
@@ -1810,6 +1636,8 @@ function writeKimiRoleSkill(args) {
|
|
|
1810
1636
|
'',
|
|
1811
1637
|
].join('\n');
|
|
1812
1638
|
writeFileLf(args.dest, frontmatter +
|
|
1639
|
+
(pathExists(path.join(path.dirname(args.src), '..', '..', 'runtime', 'provider-pipeline.md'))
|
|
1640
|
+
? readTextFile(path.join(path.dirname(args.src), '..', '..', 'runtime', 'provider-pipeline.md')) + '\n' : '') +
|
|
1813
1641
|
KIMI_NESTED_SKILL_CONTRACT +
|
|
1814
1642
|
KIMI_RUNTIME_CONTEXT_CONTRACT +
|
|
1815
1643
|
rendered);
|
|
@@ -1882,6 +1710,7 @@ function writeGeminiAgentFromTemplate(args) {
|
|
|
1882
1710
|
`description: ${JSON.stringify(description ?? args.agentId)}`,
|
|
1883
1711
|
`model: ${model}`,
|
|
1884
1712
|
`tools: [${GEMINI_AGENT_TOOLS.join(', ')}]`,
|
|
1713
|
+
...geminiAgentLimitMetadata(),
|
|
1885
1714
|
'---',
|
|
1886
1715
|
'',
|
|
1887
1716
|
].join('\n');
|
|
@@ -1910,7 +1739,6 @@ function placeGeminiAgents(input) {
|
|
|
1910
1739
|
const placeholders = {
|
|
1911
1740
|
PROJECT_NAME: path.basename(input.codeRoot),
|
|
1912
1741
|
SECURITY_EXEMPTIONS_PATH: '.gemini/security-exemptions.yaml',
|
|
1913
|
-
PERSONA_DIR: '.gemini/agents/personas/',
|
|
1914
1742
|
};
|
|
1915
1743
|
const placedIds = [];
|
|
1916
1744
|
for (const src of listDir(agentsSrc)) {
|
|
@@ -1922,10 +1750,6 @@ function placeGeminiAgents(input) {
|
|
|
1922
1750
|
// filtering happens at the workspace LINK step (linkAgentFiles).
|
|
1923
1751
|
if (!input.materializeAllAgents && !selectedAgents.has(agentId))
|
|
1924
1752
|
continue;
|
|
1925
|
-
if (!input.materializeAllAgents && QUICK_EXCLUDED_AGENTS.has(agentId)) {
|
|
1926
|
-
result.skipped++;
|
|
1927
|
-
continue;
|
|
1928
|
-
}
|
|
1929
1753
|
writeGeminiAgentFromTemplate({
|
|
1930
1754
|
artifactRoot: input.artifactRoot,
|
|
1931
1755
|
src,
|
|
@@ -2148,6 +1972,38 @@ function renderInitialGeminiMd(repoRoot) {
|
|
|
2148
1972
|
'',
|
|
2149
1973
|
].join('\n');
|
|
2150
1974
|
}
|
|
1975
|
+
function providerPipelineContract(scriptDir) {
|
|
1976
|
+
const source = path.join(scriptDir, 'templates', 'runtime', 'provider-pipeline.md');
|
|
1977
|
+
return pathExists(source) ? readTextFile(source) + '\n\n' : '';
|
|
1978
|
+
}
|
|
1979
|
+
function prependSkillContract(file, contract) {
|
|
1980
|
+
if (!contract || !pathExists(file))
|
|
1981
|
+
return;
|
|
1982
|
+
const source = readTextFile(file);
|
|
1983
|
+
const end = source.startsWith('---\n') ? source.indexOf('\n---\n', 4) : -1;
|
|
1984
|
+
const index = end < 0 ? 0 : end + 5;
|
|
1985
|
+
writeFileLf(file, source.slice(0, index) + '\n' + contract + source.slice(index));
|
|
1986
|
+
}
|
|
1987
|
+
function assertPipelineRuntimeSource(scriptDir) {
|
|
1988
|
+
const contractFile = path.join(scriptDir, 'integration-contract.json');
|
|
1989
|
+
if (!pathExists(contractFile))
|
|
1990
|
+
return;
|
|
1991
|
+
const contract = JSON.parse(readTextFile(contractFile));
|
|
1992
|
+
if (contract.execution?.runtime && !pathExists(path.join(scriptDir, 'dist', 'installer', 'runtime', 'pipeline-state.js'))) {
|
|
1993
|
+
throw new Error('Core declares a pipeline runtime but its compiled module is missing; rebuild or reinstall this Core package before refreshing providers');
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
function placePipelineRuntime(input) {
|
|
1997
|
+
const source = path.join(input.scriptDir, 'dist', 'installer', 'runtime', 'pipeline-state.js');
|
|
1998
|
+
// Source-only fixture installations may not include a compiled runtime.
|
|
1999
|
+
if (!pathExists(source))
|
|
2000
|
+
return 0;
|
|
2001
|
+
const dest = path.join(input.artifactRoot, '.specrails', 'runtime');
|
|
2002
|
+
copyFile(source, path.join(dest, 'pipeline-state.mjs'));
|
|
2003
|
+
writeFileLf(path.join(dest, 'pipeline.mjs'), "import { runPipelineCli } from './pipeline-state.mjs'\n" +
|
|
2004
|
+
"process.exitCode = await runPipelineCli(process.argv.slice(2))\n");
|
|
2005
|
+
return 2;
|
|
2006
|
+
}
|
|
2151
2007
|
function pruneLegacyArtifacts(input) {
|
|
2152
2008
|
const legacyPaths = [
|
|
2153
2009
|
path.join(input.artifactRoot, '.specrails', 'bin', 'doctor.sh'),
|
|
@@ -2162,8 +2018,7 @@ function pruneLegacyArtifacts(input) {
|
|
|
2162
2018
|
legacyPaths.push(path.join(input.artifactRoot, input.providerDir, 'skills', 'setup'));
|
|
2163
2019
|
}
|
|
2164
2020
|
else if (input.provider === 'gemini') {
|
|
2165
|
-
//
|
|
2166
|
-
legacyPaths.push(path.join(input.artifactRoot, input.providerDir, 'skills'));
|
|
2021
|
+
// OpenSpec and user skills survive updates; only retired setup commands are managed.
|
|
2167
2022
|
legacyPaths.push(path.join(input.artifactRoot, input.providerDir, 'commands', 'setup.toml'));
|
|
2168
2023
|
legacyPaths.push(path.join(input.artifactRoot, input.providerDir, 'commands', 'specrails', 'setup.toml'));
|
|
2169
2024
|
}
|
|
@@ -2202,20 +2057,19 @@ function pruneLegacyArtifacts(input) {
|
|
|
2202
2057
|
}
|
|
2203
2058
|
}
|
|
2204
2059
|
/**
|
|
2205
|
-
*
|
|
2060
|
+
* Direct placement: copy agents / commands / rules from the
|
|
2206
2061
|
* .specrails/setup-templates/ staging directory into the live
|
|
2207
|
-
* provider directory, substituting template placeholders
|
|
2208
|
-
*
|
|
2062
|
+
* provider directory, substituting template placeholders. This is the
|
|
2063
|
+
* only placement path — there are no install tiers.
|
|
2209
2064
|
*
|
|
2210
2065
|
* Source is setup-templates/ (not scriptDir/templates/) so the pipeline
|
|
2211
2066
|
* is: scriptDir/templates/ → setup-templates/ (earlier scaffold step)
|
|
2212
|
-
* → <providerDir>/ (this function). The intermediate hop
|
|
2213
|
-
*
|
|
2214
|
-
*
|
|
2215
|
-
* canonical staging dir.
|
|
2067
|
+
* → <providerDir>/ (this function). The intermediate hop lets downstream
|
|
2068
|
+
* consumers (specrails-desktop's deployTemplates, update flow) read from a
|
|
2069
|
+
* single canonical staging dir.
|
|
2216
2070
|
*/
|
|
2217
|
-
function
|
|
2218
|
-
// Codex projects: the
|
|
2071
|
+
function placeArtefacts(input) {
|
|
2072
|
+
// Codex projects: the `agents/` + `rules/` placement is
|
|
2219
2073
|
// skipped (handled by `placeSkills` rail-skills + `applyCodexSettings`).
|
|
2220
2074
|
// The slash-command catalogue under `setup-templates/commands/specrails/`
|
|
2221
2075
|
// IS ported, but to `.codex/skills/<name>/SKILL.md` instead of
|
|
@@ -2322,18 +2176,16 @@ function placeQuickTierArtefacts(input) {
|
|
|
2322
2176
|
const placeholders = {
|
|
2323
2177
|
PROJECT_NAME: projectName,
|
|
2324
2178
|
SECURITY_EXEMPTIONS_PATH: `${input.providerDir}/security-exemptions.yaml`,
|
|
2325
|
-
PERSONA_DIR: `${input.providerDir}/agents/personas/`,
|
|
2326
2179
|
};
|
|
2327
2180
|
// --- Agents ---
|
|
2328
2181
|
const agentsSrc = path.join(setupTemplates, 'agents');
|
|
2329
2182
|
const agentsDest = path.join(providerDirAbs, 'agents');
|
|
2330
2183
|
let agentsPlaced = 0;
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
//
|
|
2334
|
-
//
|
|
2335
|
-
//
|
|
2336
|
-
// product agents) are explicitly opt-in via the TUI or install-config.
|
|
2184
|
+
const agentsSkipped = 0;
|
|
2185
|
+
// The only shipped agents are the three core agents. A profile-driven
|
|
2186
|
+
// install may pass a selection; anything outside CORE_AGENTS has no template
|
|
2187
|
+
// to place, so the intersection is always the core trio (extension happens
|
|
2188
|
+
// via user-owned custom-*.md agents, never through the installer).
|
|
2337
2189
|
const selectedAgents = input.selectedAgents
|
|
2338
2190
|
? new Set([...input.selectedAgents, ...CORE_AGENTS])
|
|
2339
2191
|
: new Set([...CORE_AGENTS]);
|
|
@@ -2349,10 +2201,6 @@ function placeQuickTierArtefacts(input) {
|
|
|
2349
2201
|
// filtering happens at the workspace LINK step, not here.
|
|
2350
2202
|
if (!input.materializeAllAgents && selectedAgents && !selectedAgents.has(agentId))
|
|
2351
2203
|
continue;
|
|
2352
|
-
if (!input.materializeAllAgents && QUICK_EXCLUDED_AGENTS.has(agentId)) {
|
|
2353
|
-
agentsSkipped++;
|
|
2354
|
-
continue;
|
|
2355
|
-
}
|
|
2356
2204
|
const dest = path.join(agentsDest, name);
|
|
2357
2205
|
const rendered = renderPlaceholders(readTextFile(src), {
|
|
2358
2206
|
...placeholders,
|
|
@@ -2360,7 +2208,6 @@ function placeQuickTierArtefacts(input) {
|
|
|
2360
2208
|
});
|
|
2361
2209
|
writeFileLf(dest, rendered);
|
|
2362
2210
|
agentsPlaced++;
|
|
2363
|
-
installedAgentNames.add(agentId);
|
|
2364
2211
|
// Per-agent memory directory. Created even when empty so the first run of
|
|
2365
2212
|
// the agent doesn't error on ENOENT. Skipped when materializing the SHARED
|
|
2366
2213
|
// framework (`seedProjectDirs === false`): agent-memory is per-workspace
|
|
@@ -2375,13 +2222,6 @@ function placeQuickTierArtefacts(input) {
|
|
|
2375
2222
|
}
|
|
2376
2223
|
}
|
|
2377
2224
|
// --- Commands ---
|
|
2378
|
-
// Skip commands whose required agents were excluded.
|
|
2379
|
-
const excludedCommands = new Set();
|
|
2380
|
-
for (const dep of COMMAND_AGENT_DEPENDENCIES) {
|
|
2381
|
-
const hasAllRequired = dep.requires.every((a) => installedAgentNames.has(a));
|
|
2382
|
-
if (!hasAllRequired)
|
|
2383
|
-
excludedCommands.add(dep.command);
|
|
2384
|
-
}
|
|
2385
2225
|
const commandsSrc = path.join(setupTemplates, 'commands', 'specrails');
|
|
2386
2226
|
const commandsDest = path.join(providerDirAbs, 'commands', 'specrails');
|
|
2387
2227
|
let commandsPlaced = 0;
|
|
@@ -2391,9 +2231,6 @@ function placeQuickTierArtefacts(input) {
|
|
|
2391
2231
|
const name = path.basename(src);
|
|
2392
2232
|
if (!name.endsWith('.md'))
|
|
2393
2233
|
continue;
|
|
2394
|
-
const cmdId = name.slice(0, -3);
|
|
2395
|
-
if (excludedCommands.has(cmdId))
|
|
2396
|
-
continue;
|
|
2397
2234
|
const dest = path.join(commandsDest, name);
|
|
2398
2235
|
const rendered = renderPlaceholders(readTextFile(src), {
|
|
2399
2236
|
...placeholders,
|
|
@@ -2445,7 +2282,7 @@ function applyCodexSettings(input) {
|
|
|
2445
2282
|
written++;
|
|
2446
2283
|
}
|
|
2447
2284
|
// AGENTS.md — top-level instructions file the codex CLI loads on startup.
|
|
2448
|
-
// Written with a sentinel block so update
|
|
2285
|
+
// Written with a sentinel block so update passes can refresh the
|
|
2449
2286
|
// managed content while preserving anything the user added outside it.
|
|
2450
2287
|
const agentsMdPath = path.join(input.artifactRoot, 'AGENTS.md');
|
|
2451
2288
|
const agentsMdContent = renderInitialAgentsMd(input.codeRoot);
|
|
@@ -2513,7 +2350,7 @@ function upsertAgentsMdManagedBlock(existing, managedBlock) {
|
|
|
2513
2350
|
// CLAUDE: the top-level `sr-*` skills (sr-implement, sr-why, …) are GENERATED
|
|
2514
2351
|
// from their canonical slash-command body (`templates/commands/specrails/
|
|
2515
2352
|
// <command>.md`) — the command is the single source of truth, so the skill can
|
|
2516
|
-
// never drift from it.
|
|
2353
|
+
// never drift from it.
|
|
2517
2354
|
//
|
|
2518
2355
|
// CODEX: top-level skills are NOT placed — every one has a command counterpart
|
|
2519
2356
|
// that the command path ports to `.codex/skills/<name>/` (with codex-native
|
|
@@ -2529,10 +2366,6 @@ function placeSkills(input) {
|
|
|
2529
2366
|
const commandsSrc = path.join(input.artifactRoot, '.specrails', 'setup-templates', 'commands', 'specrails');
|
|
2530
2367
|
const skillEntries = Object.entries(SKILL_FROM_COMMAND);
|
|
2531
2368
|
for (const [skillName, spec] of skillEntries) {
|
|
2532
|
-
if (input.tier === 'quick' && QUICK_EXCLUDED_SKILLS.has(skillName)) {
|
|
2533
|
-
result.skipped++;
|
|
2534
|
-
continue;
|
|
2535
|
-
}
|
|
2536
2369
|
const src = path.join(commandsSrc, `${spec.command}.md`);
|
|
2537
2370
|
if (!pathExists(src))
|
|
2538
2371
|
continue;
|
|
@@ -2555,8 +2388,7 @@ function placeSkills(input) {
|
|
|
2555
2388
|
// copies were vestigial — unused on Claude, always overridden on codex,
|
|
2556
2389
|
// and shipped with unsubstituted placeholders — and were removed).
|
|
2557
2390
|
//
|
|
2558
|
-
// Only the three CORE_AGENTS
|
|
2559
|
-
// every layer specialist are placed only when selectedAgents includes them.
|
|
2391
|
+
// Only the three CORE_AGENTS ship, so only their rail skills exist to place.
|
|
2560
2392
|
const codexRailsOverridesDir = input.provider === 'codex'
|
|
2561
2393
|
? path.join(input.scriptDir, 'templates', 'codex-skills', 'rails')
|
|
2562
2394
|
: null;
|
|
@@ -2612,10 +2444,6 @@ function placeSkills(input) {
|
|
|
2612
2444
|
const roleId = name.slice(0, -3);
|
|
2613
2445
|
if (!input.materializeAllAgents && !selectedAgents.has(roleId))
|
|
2614
2446
|
continue;
|
|
2615
|
-
if (!input.materializeAllAgents && QUICK_EXCLUDED_AGENTS.has(roleId)) {
|
|
2616
|
-
result.skipped++;
|
|
2617
|
-
continue;
|
|
2618
|
-
}
|
|
2619
2447
|
writeKimiRoleSkill({
|
|
2620
2448
|
src,
|
|
2621
2449
|
dest: path.join(destBase, roleId, 'SKILL.md'),
|
|
@@ -2630,14 +2458,9 @@ function placeSkills(input) {
|
|
|
2630
2458
|
}
|
|
2631
2459
|
}
|
|
2632
2460
|
}
|
|
2461
|
+
// v5 ships only the core trio; every bundled command's role dependencies
|
|
2462
|
+
// are always satisfied, so no command exclusion set is needed.
|
|
2633
2463
|
const excludedCommands = new Set();
|
|
2634
|
-
if (!input.materializeAllAgents) {
|
|
2635
|
-
for (const dep of COMMAND_AGENT_DEPENDENCIES) {
|
|
2636
|
-
if (!dep.requires.every((role) => placedRoleIds.has(role))) {
|
|
2637
|
-
excludedCommands.add(dep.command);
|
|
2638
|
-
}
|
|
2639
|
-
}
|
|
2640
|
-
}
|
|
2641
2464
|
if (isDir(commandsSrc)) {
|
|
2642
2465
|
for (const src of listDir(commandsSrc)) {
|
|
2643
2466
|
const name = path.basename(src);
|