specrails-core 5.0.0 → 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 -310
- package/bin/specrails-core.mjs +3 -1
- package/dist/installer/cli.js +4 -0
- package/dist/installer/cli.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 +102 -66
- package/dist/installer/commands/init.js.map +1 -1
- package/dist/installer/commands/update.js +80 -74
- package/dist/installer/commands/update.js.map +1 -1
- package/dist/installer/commands/v5-migration.js +14 -0
- package/dist/installer/commands/v5-migration.js.map +1 -1
- package/dist/installer/phases/framework-lifecycle.js +2 -0
- package/dist/installer/phases/framework-lifecycle.js.map +1 -1
- package/dist/installer/phases/scaffold.js +191 -258
- 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/exec.js +6 -1
- package/dist/installer/util/exec.js.map +1 -1
- package/dist/installer/util/fs.js +11 -2
- package/dist/installer/util/fs.js.map +1 -1
- 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/templates/agents/sr-developer.md +9 -11
- package/templates/agents/sr-reviewer.md +26 -33
- package/templates/codex-skills/batch-implement/SKILL.md +58 -244
- package/templates/codex-skills/implement/SKILL.md +136 -338
- package/templates/codex-skills/rails/sr-architect/SKILL.md +7 -0
- package/templates/codex-skills/rails/sr-developer/SKILL.md +13 -0
- package/templates/codex-skills/rails/sr-reviewer/SKILL.md +39 -5
- package/templates/codex-skills/retry/SKILL.md +37 -117
- package/templates/commands/specrails/batch-implement.md +16 -288
- package/templates/commands/specrails/implement.md +62 -1057
- package/templates/commands/specrails/retry.md +22 -314
- package/templates/gemini-commands/batch-implement.toml +28 -40
- package/templates/gemini-commands/implement.toml +55 -114
- package/templates/gemini-commands/retry.toml +21 -0
- package/templates/kimi/specrails/run-skill.mjs +51 -2
- package/templates/runtime/provider-pipeline.md +55 -0
|
@@ -1,5 +1,5 @@
|
|
|
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';
|
|
@@ -126,15 +126,17 @@ const GEMINI_MODEL_BY_AGENT = {
|
|
|
126
126
|
'sr-reviewer': 'gemini-3.5-flash',
|
|
127
127
|
};
|
|
128
128
|
const GEMINI_DEFAULT_MODEL = 'gemini-3.5-flash';
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
+
}
|
|
138
140
|
/**
|
|
139
141
|
* Claude top-level `sr-*` skills, GENERATED at install time from their
|
|
140
142
|
* canonical slash-command body under `templates/commands/specrails/<command>.md`.
|
|
@@ -222,6 +224,7 @@ export function detectExistingSetup(input) {
|
|
|
222
224
|
* .gitignore. Returns a summary for logging / tests.
|
|
223
225
|
*/
|
|
224
226
|
export function scaffoldInstallation(input) {
|
|
227
|
+
assertPipelineRuntimeSource(input.scriptDir);
|
|
225
228
|
const createdDirs = [];
|
|
226
229
|
let copiedFiles = 0;
|
|
227
230
|
const mk = (abs) => {
|
|
@@ -239,9 +242,10 @@ export function scaffoldInstallation(input) {
|
|
|
239
242
|
}
|
|
240
243
|
else if (input.provider === 'gemini') {
|
|
241
244
|
// Gemini: TOML commands under .gemini/commands/specrails/ + native
|
|
242
|
-
// subagents
|
|
245
|
+
// subagents and OpenSpec skills both live in the execution workspace.
|
|
243
246
|
mk(path.join(input.artifactRoot, input.providerDir, 'commands', 'specrails'));
|
|
244
247
|
mk(path.join(input.artifactRoot, input.providerDir, 'agents'));
|
|
248
|
+
mk(path.join(input.artifactRoot, input.providerDir, 'skills'));
|
|
245
249
|
}
|
|
246
250
|
else if (input.provider === 'kimi') {
|
|
247
251
|
mk(path.join(input.artifactRoot, input.providerDir, 'skills'));
|
|
@@ -295,6 +299,7 @@ export function scaffoldInstallation(input) {
|
|
|
295
299
|
// --- Write bundled commands (doctor.md) ---
|
|
296
300
|
copyBundledCommands({ ...input, copiedIncrement: (n) => (copiedFiles += n) });
|
|
297
301
|
pruneLegacyArtifacts(input);
|
|
302
|
+
copiedFiles += placePipelineRuntime(input);
|
|
298
303
|
if (input.provider === 'kimi') {
|
|
299
304
|
copiedFiles += placeKimiSkillRunner(input);
|
|
300
305
|
}
|
|
@@ -317,6 +322,21 @@ export function scaffoldInstallation(input) {
|
|
|
317
322
|
const skillsSubdir = input.provider === 'gemini' ? 'agents' : 'skills';
|
|
318
323
|
info(`Placed ${skills.placed} ${skillsLabel}(s) into ${input.providerDir}/${skillsSubdir}/`);
|
|
319
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
|
+
}
|
|
339
|
+
}
|
|
320
340
|
// --- Codex provider settings + AGENTS.md initial content ---
|
|
321
341
|
if (input.provider === 'codex') {
|
|
322
342
|
const written = applyCodexSettings(input);
|
|
@@ -402,6 +422,8 @@ function frameworkSourceHash(scriptDir, provider) {
|
|
|
402
422
|
const treeHash = hashFrameworkTrees([
|
|
403
423
|
{ label: 'templates', dir: path.join(scriptDir, 'templates') },
|
|
404
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') },
|
|
405
427
|
], { ignorePackageNoise: true });
|
|
406
428
|
return `sha256:${createHash('sha256')
|
|
407
429
|
.update(treeHash)
|
|
@@ -413,6 +435,7 @@ function frameworkSourceHash(scriptDir, provider) {
|
|
|
413
435
|
function frameworkContentHash(providerFrameworkDir) {
|
|
414
436
|
return hashFrameworkTrees([
|
|
415
437
|
{ label: 'provider', dir: providerFrameworkDir },
|
|
438
|
+
{ label: 'pipeline-runtime', dir: path.join(path.dirname(providerFrameworkDir), '.specrails', 'runtime') },
|
|
416
439
|
]);
|
|
417
440
|
}
|
|
418
441
|
function readFrameworkStamp(stampPath) {
|
|
@@ -479,59 +502,91 @@ export function installFramework(input) {
|
|
|
479
502
|
stamp.content_hash === frameworkContentHash(providerFrameworkDir)) {
|
|
480
503
|
return { providerFrameworkDir, versionDir, materialized: false };
|
|
481
504
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
artifactRoot:
|
|
502
|
-
codeRoot
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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 });
|
|
525
589
|
}
|
|
526
|
-
const frameworkStamp = {
|
|
527
|
-
schema: 1,
|
|
528
|
-
version: input.version,
|
|
529
|
-
provider: input.provider,
|
|
530
|
-
source_hash: sourceHash,
|
|
531
|
-
content_hash: frameworkContentHash(providerFrameworkDir),
|
|
532
|
-
};
|
|
533
|
-
writeFileLf(stampPath, `${JSON.stringify(frameworkStamp, null, 2)}\n`);
|
|
534
|
-
return { providerFrameworkDir, versionDir, materialized: true };
|
|
535
590
|
}
|
|
536
591
|
/**
|
|
537
592
|
* Atomically point `<frameworkDir>/current` at `<version>` so every workspace's
|
|
@@ -611,6 +666,10 @@ export function assembleProjectWorkspace(input) {
|
|
|
611
666
|
links[settingsFile] = symlinkOrCopy(settingsTarget, settingsLink, preferCopy);
|
|
612
667
|
}
|
|
613
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
|
+
}
|
|
614
673
|
// (b) Seed the PROJECT layer (real writable files / dirs).
|
|
615
674
|
const seededMemoryAgents = seedProjectLayer(input, currentProviderDir);
|
|
616
675
|
// Manifest: record the consumed framework version. `buildManifest` hashes the
|
|
@@ -1124,7 +1183,9 @@ const KIMI_ROLE_EXECUTION_CONTRACT = [
|
|
|
1124
1183
|
'Every `key`, profile stem, and worktree id uses the same 1–64 character grammar as `run`.',
|
|
1125
1184
|
'Use `"current"` for roles that target the orchestrator repository. The',
|
|
1126
1185
|
'helper gives each such role a private execution directory while setting its',
|
|
1127
|
-
'`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',
|
|
1128
1189
|
'collide. Where later instructions request `isolation: worktree`, use',
|
|
1129
1190
|
'`"worktree:<feature-id>"`; reuse that exact value for the developer, test,',
|
|
1130
1191
|
'documentation, and other sequential roles belonging to the same feature.',
|
|
@@ -1200,8 +1261,9 @@ const KIMI_RUNTIME_CONTEXT_CONTRACT = [
|
|
|
1200
1261
|
'configured”, never fabricated rows or scores.',
|
|
1201
1262
|
'',
|
|
1202
1263
|
'For every `KIMI_BACKLOG_*` marker, first read and validate',
|
|
1203
|
-
'
|
|
1204
|
-
'
|
|
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',
|
|
1205
1267
|
'approved `gh issue` operation; route `jira` only through the configured',
|
|
1206
1268
|
'project/base URL and credentials. Honour read-only mode and never perform a',
|
|
1207
1269
|
'write operation when configuration is missing, invalid, or read-only.',
|
|
@@ -1291,7 +1353,7 @@ function writeKimiWorkflowSkill(args) {
|
|
|
1291
1353
|
...args.placeholders,
|
|
1292
1354
|
MEMORY_PATH: '.kimi-code/agent-memory/',
|
|
1293
1355
|
}).replaceAll('.specrails/profiles/project-default.json', '.specrails/profiles/kimi-default.json');
|
|
1294
|
-
const rendered = translateClaudeTextForKimi(adaptKimiWorkflowBody(args.commandName, providerNeutral));
|
|
1356
|
+
const rendered = translateClaudeTextForKimi(adaptKimiWorkflowBody(args.commandName, providerNeutral)).replaceAll('.specrails/local-tickets.json', '${SPECRAILS_BACKLOG_PATH}');
|
|
1295
1357
|
const frontmatter = [
|
|
1296
1358
|
'---',
|
|
1297
1359
|
`name: ${skillName}`,
|
|
@@ -1304,6 +1366,8 @@ function writeKimiWorkflowSkill(args) {
|
|
|
1304
1366
|
'',
|
|
1305
1367
|
].join('\n');
|
|
1306
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' : '') +
|
|
1307
1371
|
KIMI_NESTED_SKILL_CONTRACT +
|
|
1308
1372
|
KIMI_ROLE_EXECUTION_CONTRACT +
|
|
1309
1373
|
KIMI_RUNTIME_CONTEXT_CONTRACT +
|
|
@@ -1330,164 +1394,25 @@ function adaptKimiWorkflowBody(commandName, body) {
|
|
|
1330
1394
|
}
|
|
1331
1395
|
if (commandName !== 'implement')
|
|
1332
1396
|
return body;
|
|
1333
|
-
|
|
1334
|
-
'#####
|
|
1335
|
-
'',
|
|
1336
|
-
'Keep each `AGENT_MODEL[id]` value exactly as declared in the profile.',
|
|
1337
|
-
'Do **not** rewrite role `SKILL.md` frontmatter: Kimi directory skills do',
|
|
1338
|
-
'not carry per-role model configuration. In the orchestrator, resolve the',
|
|
1339
|
-
'role id against the parsed `AGENT_MODEL` map, then write the exact result',
|
|
1340
|
-
'into that role wave entry\'s JSON `model` field using WriteFile. Use',
|
|
1341
|
-
'the provider default `k3` when absent; never depend on a shell array from',
|
|
1342
|
-
'a previous tool call. Only the official short ids `k3`,',
|
|
1343
|
-
'`kimi-for-coding`, and `kimi-for-coding-highspeed` gain the',
|
|
1344
|
-
'`kimi-code/` prefix at the CLI boundary. Never map Claude aliases.',
|
|
1345
|
-
'',
|
|
1346
|
-
].join('\n'));
|
|
1347
|
-
adapted = replaceMarkdownSection(adapted, '#### Merge Algorithm', '**Step 4: Record outcomes**', [
|
|
1348
|
-
'#### Kimi role-wave merge algorithm',
|
|
1349
|
-
'',
|
|
1350
|
-
'The role-wave contract above overrides the generic runtime-supplied',
|
|
1351
|
-
'worktree assumptions. Use the stable run id chosen for this workflow.',
|
|
1352
|
-
'First run this command (the run id grammar is validated before git):',
|
|
1353
|
-
'',
|
|
1354
|
-
'```sh',
|
|
1355
|
-
'node .kimi-code/specrails/run-skill.mjs --role-wave-status <stable-run-id>',
|
|
1356
|
-
'```',
|
|
1397
|
+
return body.replace('##### Invocation configuration', [
|
|
1398
|
+
'##### Kimi invocation configuration',
|
|
1357
1399
|
'',
|
|
1358
|
-
'
|
|
1359
|
-
'
|
|
1360
|
-
'`
|
|
1361
|
-
'
|
|
1362
|
-
'
|
|
1363
|
-
'excludes `.kimi-code` plus SpecRails run-state. Never discover changed',
|
|
1364
|
-
'files with a shell loop, newline splitting, or a hard-coded `main` ref.',
|
|
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.',
|
|
1365
1405
|
'',
|
|
1366
|
-
'
|
|
1367
|
-
'
|
|
1368
|
-
'-
|
|
1369
|
-
'
|
|
1370
|
-
'',
|
|
1371
|
-
'**Exclusive A/M/D actions**',
|
|
1372
|
-
'',
|
|
1373
|
-
'For each feature in `MERGE_ORDER`, use structured WriteFile (never shell',
|
|
1374
|
-
'interpolation) to write `.specrails/kimi-role-merge.json`:',
|
|
1375
|
-
'',
|
|
1376
|
-
'```json',
|
|
1377
|
-
'{',
|
|
1378
|
-
' "run": "<stable-run-id>",',
|
|
1379
|
-
' "actions": [',
|
|
1380
|
-
' {"worktree":"<safe-id>","path":"<exact-git-path>","operation":"copy"},',
|
|
1381
|
-
' {"worktree":"<safe-id>","path":"<deleted-path>","operation":"delete"}',
|
|
1382
|
-
' ]',
|
|
1383
|
-
'}',
|
|
1384
|
-
'```',
|
|
1385
|
-
'',
|
|
1386
|
-
'Use `copy` for A/M and `delete` for D, then run exactly:',
|
|
1387
|
-
'',
|
|
1388
|
-
'```sh',
|
|
1389
|
-
'node .kimi-code/specrails/run-skill.mjs \\',
|
|
1390
|
-
' --role-merge-file .specrails/kimi-role-merge.json',
|
|
1391
|
-
'```',
|
|
1392
|
-
'',
|
|
1393
|
-
'The helper validates the one-shot file, manifest, registered worktree,',
|
|
1394
|
-
'and path containment, then copies bytes/symlinks or deletes the target',
|
|
1395
|
-
'without a shell. Filenames may contain spaces, Unicode, quotes, `$()`,',
|
|
1396
|
-
'or leading dashes; never place them in a Bash command. It rejects',
|
|
1397
|
-
'provider/run-state paths, traversal, duplicate targets, directories,',
|
|
1398
|
-
'and symlinked target parents.',
|
|
1399
|
-
'',
|
|
1400
|
-
'**Shared paths**',
|
|
1401
|
-
'',
|
|
1402
|
-
'Process shared paths in `MERGE_ORDER`:',
|
|
1403
|
-
'1. D in every contributor: submit one validated `delete` action.',
|
|
1404
|
-
'2. D versus A/M: record a delete/modify conflict; do not silently copy',
|
|
1405
|
-
' or delete it.',
|
|
1406
|
-
'3. A/M text: use structured ReadFile on each emitted `repoDir` + exact',
|
|
1407
|
-
' path and on the current merge target. Apply the existing Markdown',
|
|
1408
|
-
' section-aware strategy for `.md`; for other text perform a three-way',
|
|
1409
|
-
' semantic merge against the current target, writing through WriteFile.',
|
|
1410
|
-
'4. Binary/type conflicts: record them for `sr-merge-resolver`; never',
|
|
1411
|
-
' decode or round-trip binary data through model text.',
|
|
1412
|
-
'5. A resolved whole-file winner may be applied with one validated copy',
|
|
1413
|
-
' action. Any unresolved region receives the existing conflict markers',
|
|
1414
|
-
' and `MERGE_REPORT` entry.',
|
|
1415
|
-
'',
|
|
1416
|
-
'When `DRY_RUN=true`, do not invoke the repository merge-action helper.',
|
|
1417
|
-
'Write resolved A/M outputs under `CACHE_DIR` with structured WriteFile',
|
|
1418
|
-
'and record D paths as deletion operations in `.cache-manifest.json`.',
|
|
1419
|
-
'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.',
|
|
1420
1411
|
'',
|
|
1421
1412
|
].join('\n'));
|
|
1422
|
-
adapted = adapted
|
|
1423
|
-
.replace(' "implemented_files": [],', [
|
|
1424
|
-
' "implemented_files": [],',
|
|
1425
|
-
' "kimi_role_wave": {',
|
|
1426
|
-
' "run": "<stable-run-id>",',
|
|
1427
|
-
' "manifest_path": ".specrails/kimi-role-worktrees/<stable-run-id>.json",',
|
|
1428
|
-
' "base_commit": null,',
|
|
1429
|
-
' "workspaces": {}',
|
|
1430
|
-
' },',
|
|
1431
|
-
].join('\n'))
|
|
1432
|
-
.replace('If the write succeeds: set `PIPELINE_STATE_AVAILABLE=true`.', [
|
|
1433
|
-
'If the write succeeds: set `PIPELINE_STATE_AVAILABLE=true`.',
|
|
1434
|
-
'',
|
|
1435
|
-
'**Kimi retry state:** after every `specrails.role.workspace` frame,',
|
|
1436
|
-
'atomically refresh `kimi_role_wave.manifest_path`, `base_commit`, and',
|
|
1437
|
-
'`workspaces[<feature-id>]` from the helper output. Never synthesize',
|
|
1438
|
-
'these values. Keep the same `run` and `worktree:<feature-id>` for that',
|
|
1439
|
-
'feature through developer, test, docs, and review. On any failure keep',
|
|
1440
|
-
'the manifest and worktrees. After every required change has been merged',
|
|
1441
|
-
'successfully, run the static cleanup command from the Kimi role',
|
|
1442
|
-
'contract and set `kimi_role_wave` to `null` in pipeline state.',
|
|
1443
|
-
].join('\n'))
|
|
1444
|
-
.replaceAll('git -C <worktree-path> diff main --name-only', 'git -C <worktree-path> diff <base-commit> --name-only')
|
|
1445
|
-
.replaceAll('git -C <worktree-path> diff main -- <file>', 'git -C <worktree-path> diff <base-commit> -- <file>')
|
|
1446
|
-
.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.)');
|
|
1447
|
-
return adapted;
|
|
1448
1413
|
}
|
|
1449
1414
|
function adaptKimiBatchImplement(body) {
|
|
1450
|
-
return
|
|
1451
|
-
'### Kimi wave invocation',
|
|
1452
|
-
'',
|
|
1453
|
-
'Nested `specrails-implement` executions are independent foreground Kimi',
|
|
1454
|
-
'processes. Do not call multiple built-in `Skill` tools in one Kimi',
|
|
1455
|
-
'session and do not share one checkout concurrently.',
|
|
1456
|
-
'',
|
|
1457
|
-
'Choose one safe `BATCH_RUN` id. For dependency wave `W`, derive the',
|
|
1458
|
-
'deterministic safe run id `<BATCH_RUN>-w<W>`. Process waves sequentially:',
|
|
1459
|
-
'',
|
|
1460
|
-
'1. For a normal repository launch, partition each dependency wave into',
|
|
1461
|
-
' foreground batches of at most `min(CONCURRENCY,32)` entries. Each entry uses',
|
|
1462
|
-
' `skill:"specrails-implement"`, `workspace:"worktree:<feature-id>"`,',
|
|
1463
|
-
' the complete `<ref> [--dry-run]` arguments, the selected profile stem',
|
|
1464
|
-
' (or `"inherit"`), and that profile\'s exact `orchestrator.model` (or',
|
|
1465
|
-
' `k3`). Feature ids and keys must be collision-free safe ids.',
|
|
1466
|
-
'2. Wait for all completion frames. A failed entry fails only that ticket;',
|
|
1467
|
-
' preserve its manifest/worktree for diagnosis and record the failure.',
|
|
1468
|
-
'3. Before a downstream dependency wave, call `--role-wave-status` for',
|
|
1469
|
-
' the completed wave. Merge each successful worktree\'s A/M/D inventory',
|
|
1470
|
-
' into the batch repository with the same structured merge-file and',
|
|
1471
|
-
' shared-path rules defined by `specrails-implement`. Never interpolate',
|
|
1472
|
-
' a filename into Shell. If merge succeeds, run',
|
|
1473
|
-
' `node .kimi-code/specrails/run-skill.mjs --role-wave-cleanup <run>`.',
|
|
1474
|
-
' This makes predecessor output part of the next wave\'s newly captured',
|
|
1475
|
-
' synthetic baseline. Do not cleanup failed or unmerged worktrees.',
|
|
1476
|
-
'4. Record `{ref,wave,status,profile,error_summary,run,manifest_path,',
|
|
1477
|
-
' workspace}` in `WAVE_RESULTS` before starting another batch.',
|
|
1478
|
-
'',
|
|
1479
|
-
'Inside a specrails-desktop isolated rail worktree, effective concurrency',
|
|
1480
|
-
'is exactly 1. Submit a one-entry foreground role wave per ticket with',
|
|
1481
|
-
'`workspace:"current"` and a deterministic unique run id; wait before the',
|
|
1482
|
-
'next ticket. No sibling worktree, status merge, or cleanup is needed',
|
|
1483
|
-
'because every nested implementation writes directly into the desktop',
|
|
1484
|
-
'rail\'s current repository.',
|
|
1485
|
-
'',
|
|
1486
|
-
'Per-ticket profiles remain isolated: `profile` is either `inherit` or the',
|
|
1487
|
-
'validated filename stem from `PROFILE_MAP`; `model` is resolved from the',
|
|
1488
|
-
'same profile before writing JSON. Never export a profile globally.',
|
|
1489
|
-
'',
|
|
1490
|
-
].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.');
|
|
1491
1416
|
}
|
|
1492
1417
|
function adaptKimiAutoPropose(body) {
|
|
1493
1418
|
return body
|
|
@@ -1506,36 +1431,19 @@ function adaptKimiAutoPropose(body) {
|
|
|
1506
1431
|
.replaceAll('After the Explore agent completes:', 'After the sr-product-analyst role completes:');
|
|
1507
1432
|
}
|
|
1508
1433
|
function adaptKimiRetry(body) {
|
|
1509
|
-
return body
|
|
1510
|
-
.replace('- `PHASE_STATUSES` ← `phases` map (`architect`, `developer`, `test-writer`, `doc-sync`, `reviewer`, `ship`, `ci` → `"done"`, `"failed"`, `"skipped"`, or `"pending"`)', [
|
|
1511
|
-
'- `PHASE_STATUSES` ← `phases` map (`architect`, `developer`, `test-writer`, `doc-sync`, `reviewer`, `ship`, `ci` → `"done"`, `"failed"`, `"skipped"`, or `"pending"`)',
|
|
1512
|
-
'- `KIMI_ROLE_WAVE` ← `kimi_role_wave` (required when an isolated Kimi',
|
|
1513
|
-
' phase has already started): persisted `run`, `manifest_path`,',
|
|
1514
|
-
' `base_commit`, and feature→workspace mapping.',
|
|
1515
|
-
].join('\n'))
|
|
1516
|
-
.replace('**Validation:**', [
|
|
1517
|
-
'**Kimi workspace validation (before any phase):**',
|
|
1434
|
+
return body + [
|
|
1518
1435
|
'',
|
|
1519
|
-
'
|
|
1520
|
-
'`node .kimi-code/specrails/run-skill.mjs --role-wave-status <run>`.',
|
|
1521
|
-
'The returned manifest path, base commit, and workspace ids must exactly',
|
|
1522
|
-
'match pipeline state. Any mismatch/missing/unregistered worktree is a',
|
|
1523
|
-
'hard stop: report recovery instructions and do not create a replacement',
|
|
1524
|
-
'worktree. A retry must use the same run and exact',
|
|
1525
|
-
'`worktree:<feature-id>` mapping so successful developer changes survive',
|
|
1526
|
-
'a later test/docs/reviewer failure. Refresh state from emitted frames',
|
|
1527
|
-
'after each resumed role. Never choose a new run while valid state',
|
|
1528
|
-
'exists; never cleanup before every required phase and merge succeeds.',
|
|
1436
|
+
'## Kimi direct-role continuation',
|
|
1529
1437
|
'',
|
|
1530
|
-
'
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
'
|
|
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.',
|
|
1534
1445
|
'',
|
|
1535
|
-
|
|
1536
|
-
'static `--role-wave-cleanup <run>` helper. Only after its cleanup frame',
|
|
1537
|
-
'succeeds set `kimi_role_wave` to `null`. A failed retry retains state.',
|
|
1538
|
-
].join('\n'));
|
|
1446
|
+
].join('\n');
|
|
1539
1447
|
}
|
|
1540
1448
|
function renderKimiEnrichWorkflow() {
|
|
1541
1449
|
return [
|
|
@@ -1707,15 +1615,6 @@ function renderKimiTelemetryWorkflow() {
|
|
|
1707
1615
|
'',
|
|
1708
1616
|
].join('\n');
|
|
1709
1617
|
}
|
|
1710
|
-
function replaceMarkdownSection(body, startHeading, endHeading, replacement) {
|
|
1711
|
-
const start = body.indexOf(startHeading);
|
|
1712
|
-
if (start < 0)
|
|
1713
|
-
return body;
|
|
1714
|
-
const end = body.indexOf(endHeading, start + startHeading.length);
|
|
1715
|
-
if (end < 0)
|
|
1716
|
-
return body;
|
|
1717
|
-
return body.slice(0, start) + replacement + body.slice(end);
|
|
1718
|
-
}
|
|
1719
1618
|
function writeKimiRoleSkill(args) {
|
|
1720
1619
|
if (!pathExists(args.src))
|
|
1721
1620
|
return;
|
|
@@ -1737,6 +1636,8 @@ function writeKimiRoleSkill(args) {
|
|
|
1737
1636
|
'',
|
|
1738
1637
|
].join('\n');
|
|
1739
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' : '') +
|
|
1740
1641
|
KIMI_NESTED_SKILL_CONTRACT +
|
|
1741
1642
|
KIMI_RUNTIME_CONTEXT_CONTRACT +
|
|
1742
1643
|
rendered);
|
|
@@ -1809,6 +1710,7 @@ function writeGeminiAgentFromTemplate(args) {
|
|
|
1809
1710
|
`description: ${JSON.stringify(description ?? args.agentId)}`,
|
|
1810
1711
|
`model: ${model}`,
|
|
1811
1712
|
`tools: [${GEMINI_AGENT_TOOLS.join(', ')}]`,
|
|
1713
|
+
...geminiAgentLimitMetadata(),
|
|
1812
1714
|
'---',
|
|
1813
1715
|
'',
|
|
1814
1716
|
].join('\n');
|
|
@@ -2070,6 +1972,38 @@ function renderInitialGeminiMd(repoRoot) {
|
|
|
2070
1972
|
'',
|
|
2071
1973
|
].join('\n');
|
|
2072
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
|
+
}
|
|
2073
2007
|
function pruneLegacyArtifacts(input) {
|
|
2074
2008
|
const legacyPaths = [
|
|
2075
2009
|
path.join(input.artifactRoot, '.specrails', 'bin', 'doctor.sh'),
|
|
@@ -2084,8 +2018,7 @@ function pruneLegacyArtifacts(input) {
|
|
|
2084
2018
|
legacyPaths.push(path.join(input.artifactRoot, input.providerDir, 'skills', 'setup'));
|
|
2085
2019
|
}
|
|
2086
2020
|
else if (input.provider === 'gemini') {
|
|
2087
|
-
//
|
|
2088
|
-
legacyPaths.push(path.join(input.artifactRoot, input.providerDir, 'skills'));
|
|
2021
|
+
// OpenSpec and user skills survive updates; only retired setup commands are managed.
|
|
2089
2022
|
legacyPaths.push(path.join(input.artifactRoot, input.providerDir, 'commands', 'setup.toml'));
|
|
2090
2023
|
legacyPaths.push(path.join(input.artifactRoot, input.providerDir, 'commands', 'specrails', 'setup.toml'));
|
|
2091
2024
|
}
|