specrails-core 5.0.0 → 5.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +108 -66
- package/dist/installer/commands/init.js.map +1 -1
- package/dist/installer/commands/update.js +86 -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 +198 -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 +141 -2
- package/dist/installer/util/fs.js.map +1 -1
- package/dist/installer/util/install-transaction.js +266 -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,98 @@ 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
|
-
|
|
502
|
-
|
|
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
|
+
//
|
|
515
|
+
// INVARIANT: the framework store holds real files only. `cpSync` with
|
|
516
|
+
// `verbatimSymlinks` does not copy a link, it RECREATES it via
|
|
517
|
+
// `symlinkSync` without a type — a privileged operation on Windows that
|
|
518
|
+
// throws EPERM on an ordinary account (this is what broke `init` in 5.1.0;
|
|
519
|
+
// see `withInstallRollback`). If the store ever gains a link, this call has
|
|
520
|
+
// to move to `snapshotTree`/`restoreTree` in `util/fs.ts`.
|
|
521
|
+
if (isDir(versionDir))
|
|
522
|
+
cpSync(versionDir, stagedVersionDir, { recursive: true, dereference: false, verbatimSymlinks: true, filter: () => true, mode: constants.COPYFILE_FICLONE });
|
|
523
|
+
else
|
|
524
|
+
mkdirp(stagedVersionDir);
|
|
525
|
+
// Framework provider trees are entirely Core-owned. Rebuilding from a clean
|
|
526
|
+
// destination removes stale files as well as repairing corrupt/missing ones,
|
|
527
|
+
// without touching sibling providers already materialized in this version.
|
|
528
|
+
removePath(stagedProviderDir);
|
|
529
|
+
removePath(stagedStampPath);
|
|
530
|
+
// Reuse scaffoldInstallation's static-placement helpers by pointing
|
|
531
|
+
// `artifactRoot` at the version dir. `seedProjectDirs: false` keeps the copy
|
|
532
|
+
// free of per-workspace mutable state. The `codeRoot` is irrelevant to the
|
|
533
|
+
// STATIC subtree (the project-named instruction files are skipped below), so
|
|
534
|
+
// we hand it the framework dir to satisfy the contract — and we DELETE any
|
|
535
|
+
// project-named instruction file the settings helpers wrote.
|
|
536
|
+
// The SHARED framework store is always the FULL SUPERSET — EVERY agent — so a
|
|
537
|
+
// SECOND project with a DIFFERENT agent selection links its specialists from
|
|
538
|
+
// the same materialized copy instead of inheriting the first project's
|
|
539
|
+
// narrower set. Per-project filtering moves to the workspace LINK step
|
|
540
|
+
// (`linkAgentFiles` via `assembleProjectWorkspace`). `selectedAgents` on the
|
|
541
|
+
// input is intentionally IGNORED here.
|
|
542
|
+
const staticInput = {
|
|
543
|
+
scriptDir: input.scriptDir,
|
|
544
|
+
artifactRoot: stagedVersionDir,
|
|
545
|
+
codeRoot: versionDir,
|
|
546
|
+
provider: input.provider,
|
|
547
|
+
providerDir: input.providerDir,
|
|
548
|
+
selectedAgents: undefined,
|
|
549
|
+
materializeAllAgents: true,
|
|
550
|
+
seedProjectDirs: false,
|
|
551
|
+
};
|
|
552
|
+
scaffoldInstallation(staticInput);
|
|
553
|
+
// The settings helpers also emit a project-named root instruction file
|
|
554
|
+
// (AGENTS.md/GEMINI.md/CLAUDE.md) + (for codex) config.toml / (gemini)
|
|
555
|
+
// settings.json. The instruction file is per-project → strip it from the
|
|
556
|
+
// shared copy; the settings file IS provider-invariant and stays as a
|
|
557
|
+
// link target inside the providerDir.
|
|
558
|
+
for (const f of ['AGENTS.md', 'GEMINI.md', 'CLAUDE.md']) {
|
|
559
|
+
rmSync(path.join(stagedVersionDir, f), { force: true });
|
|
560
|
+
}
|
|
561
|
+
// Kimi's instruction and MCP files are provider-local rather than root-local,
|
|
562
|
+
// but both are project-specific and must be real files in each workspace.
|
|
563
|
+
// In particular, linking mcp.json would let Desktop mutate the shared
|
|
564
|
+
// framework and leak one project's MCP registry into every other project.
|
|
565
|
+
if (input.provider === 'kimi') {
|
|
566
|
+
rmSync(path.join(stagedProviderDir, 'AGENTS.md'), { force: true });
|
|
567
|
+
rmSync(path.join(stagedProviderDir, 'mcp.json'), { force: true });
|
|
568
|
+
}
|
|
569
|
+
const frameworkStamp = {
|
|
570
|
+
schema: 1,
|
|
571
|
+
version: input.version,
|
|
572
|
+
provider: input.provider,
|
|
573
|
+
source_hash: sourceHash,
|
|
574
|
+
content_hash: frameworkContentHash(stagedProviderDir),
|
|
575
|
+
};
|
|
576
|
+
writeFileLf(stagedStampPath, `${JSON.stringify(frameworkStamp, null, 2)}\n`);
|
|
577
|
+
// Keep the previous complete version outside the disposable staging root.
|
|
578
|
+
// It remains available for manual recovery even after successful publication.
|
|
579
|
+
const previous = path.join(input.frameworkDir, `.previous-${input.version}-${randomUUID()}`);
|
|
580
|
+
const hadPrevious = pathExists(versionDir);
|
|
581
|
+
if (hadPrevious)
|
|
582
|
+
renameSync(versionDir, previous);
|
|
583
|
+
try {
|
|
584
|
+
renameSync(stagedVersionDir, versionDir);
|
|
585
|
+
}
|
|
586
|
+
catch (error) {
|
|
587
|
+
if (hadPrevious)
|
|
588
|
+
renameSync(previous, versionDir);
|
|
589
|
+
throw error;
|
|
590
|
+
}
|
|
591
|
+
return { providerFrameworkDir, versionDir, materialized: true };
|
|
592
|
+
}
|
|
593
|
+
finally {
|
|
594
|
+
// This contains only newly generated candidate files, never the prior version.
|
|
595
|
+
rmSync(stageRoot, { recursive: true, force: true });
|
|
525
596
|
}
|
|
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
597
|
}
|
|
536
598
|
/**
|
|
537
599
|
* Atomically point `<frameworkDir>/current` at `<version>` so every workspace's
|
|
@@ -611,6 +673,10 @@ export function assembleProjectWorkspace(input) {
|
|
|
611
673
|
links[settingsFile] = symlinkOrCopy(settingsTarget, settingsLink, preferCopy);
|
|
612
674
|
}
|
|
613
675
|
}
|
|
676
|
+
const runtimeTarget = path.join(input.frameworkDir, 'current', '.specrails', 'runtime');
|
|
677
|
+
if (pathExists(runtimeTarget)) {
|
|
678
|
+
links.pipelineRuntime = symlinkOrCopy(runtimeTarget, path.join(input.workspace, '.specrails', 'runtime'), preferCopy);
|
|
679
|
+
}
|
|
614
680
|
// (b) Seed the PROJECT layer (real writable files / dirs).
|
|
615
681
|
const seededMemoryAgents = seedProjectLayer(input, currentProviderDir);
|
|
616
682
|
// Manifest: record the consumed framework version. `buildManifest` hashes the
|
|
@@ -1124,7 +1190,9 @@ const KIMI_ROLE_EXECUTION_CONTRACT = [
|
|
|
1124
1190
|
'Every `key`, profile stem, and worktree id uses the same 1–64 character grammar as `run`.',
|
|
1125
1191
|
'Use `"current"` for roles that target the orchestrator repository. The',
|
|
1126
1192
|
'helper gives each such role a private execution directory while setting its',
|
|
1127
|
-
'`SPECRAILS_REPO_DIR` to that repository
|
|
1193
|
+
'`SPECRAILS_REPO_DIR` to that repository. The child preserves the absolute',
|
|
1194
|
+
'`SPECRAILS_EXECUTION_CONTEXT`, `SPECRAILS_BACKLOG_PATH` and pipeline helper.',
|
|
1195
|
+
'Read the frozen specs there; never infer task scope from the child cwd. Nested calls do not',
|
|
1128
1196
|
'collide. Where later instructions request `isolation: worktree`, use',
|
|
1129
1197
|
'`"worktree:<feature-id>"`; reuse that exact value for the developer, test,',
|
|
1130
1198
|
'documentation, and other sequential roles belonging to the same feature.',
|
|
@@ -1200,8 +1268,9 @@ const KIMI_RUNTIME_CONTEXT_CONTRACT = [
|
|
|
1200
1268
|
'configured”, never fabricated rows or scores.',
|
|
1201
1269
|
'',
|
|
1202
1270
|
'For every `KIMI_BACKLOG_*` marker, first read and validate',
|
|
1203
|
-
'
|
|
1204
|
-
'
|
|
1271
|
+
'`${SPECRAILS_BACKLOG_ROOT}/.specrails/backlog-config.json` when configured.',
|
|
1272
|
+
'The frozen execution context takes priority. Route `local` through',
|
|
1273
|
+
'`${SPECRAILS_BACKLOG_PATH}` only when ownership allows writes; route `github` through the',
|
|
1205
1274
|
'approved `gh issue` operation; route `jira` only through the configured',
|
|
1206
1275
|
'project/base URL and credentials. Honour read-only mode and never perform a',
|
|
1207
1276
|
'write operation when configuration is missing, invalid, or read-only.',
|
|
@@ -1291,7 +1360,7 @@ function writeKimiWorkflowSkill(args) {
|
|
|
1291
1360
|
...args.placeholders,
|
|
1292
1361
|
MEMORY_PATH: '.kimi-code/agent-memory/',
|
|
1293
1362
|
}).replaceAll('.specrails/profiles/project-default.json', '.specrails/profiles/kimi-default.json');
|
|
1294
|
-
const rendered = translateClaudeTextForKimi(adaptKimiWorkflowBody(args.commandName, providerNeutral));
|
|
1363
|
+
const rendered = translateClaudeTextForKimi(adaptKimiWorkflowBody(args.commandName, providerNeutral)).replaceAll('.specrails/local-tickets.json', '${SPECRAILS_BACKLOG_PATH}');
|
|
1295
1364
|
const frontmatter = [
|
|
1296
1365
|
'---',
|
|
1297
1366
|
`name: ${skillName}`,
|
|
@@ -1304,6 +1373,8 @@ function writeKimiWorkflowSkill(args) {
|
|
|
1304
1373
|
'',
|
|
1305
1374
|
].join('\n');
|
|
1306
1375
|
writeFileLf(args.dest, frontmatter +
|
|
1376
|
+
(pathExists(path.join(path.dirname(args.src), '..', '..', 'runtime', 'provider-pipeline.md'))
|
|
1377
|
+
? readTextFile(path.join(path.dirname(args.src), '..', '..', 'runtime', 'provider-pipeline.md')) + '\n' : '') +
|
|
1307
1378
|
KIMI_NESTED_SKILL_CONTRACT +
|
|
1308
1379
|
KIMI_ROLE_EXECUTION_CONTRACT +
|
|
1309
1380
|
KIMI_RUNTIME_CONTEXT_CONTRACT +
|
|
@@ -1330,164 +1401,25 @@ function adaptKimiWorkflowBody(commandName, body) {
|
|
|
1330
1401
|
}
|
|
1331
1402
|
if (commandName !== 'implement')
|
|
1332
1403
|
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
|
-
'```',
|
|
1404
|
+
return body.replace('##### Invocation configuration', [
|
|
1405
|
+
'##### Kimi invocation configuration',
|
|
1357
1406
|
'',
|
|
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.',
|
|
1407
|
+
'Keep the parsed `AGENT_MODEL` map as structured orchestration data.',
|
|
1408
|
+
'Resolve each role model from its exact profile value and put it in the',
|
|
1409
|
+
'role-wave JSON `model` field; absent values use `k3`. Shell variables',
|
|
1410
|
+
'from earlier tools are not persistent. Never rewrite role frontmatter',
|
|
1411
|
+
'or translate a Claude model alias into a Kimi model.',
|
|
1365
1412
|
'',
|
|
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.',
|
|
1413
|
+
'Every implementation role uses `workspace:"current"` and the same',
|
|
1414
|
+
'aggregate execution context. Serialize writers within supplied roots;',
|
|
1415
|
+
'no per-ticket full pipeline, nested worktree, copied-file merge or',
|
|
1416
|
+
'replacement run. The runner preserves shared backlog and frozen specs',
|
|
1417
|
+
'even though each role has a private execution cwd.',
|
|
1420
1418
|
'',
|
|
1421
1419
|
].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
1420
|
}
|
|
1449
1421
|
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'));
|
|
1422
|
+
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
1423
|
}
|
|
1492
1424
|
function adaptKimiAutoPropose(body) {
|
|
1493
1425
|
return body
|
|
@@ -1506,36 +1438,19 @@ function adaptKimiAutoPropose(body) {
|
|
|
1506
1438
|
.replaceAll('After the Explore agent completes:', 'After the sr-product-analyst role completes:');
|
|
1507
1439
|
}
|
|
1508
1440
|
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):**',
|
|
1441
|
+
return body + [
|
|
1518
1442
|
'',
|
|
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.',
|
|
1443
|
+
'## Kimi direct-role continuation',
|
|
1529
1444
|
'',
|
|
1530
|
-
'
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
'
|
|
1445
|
+
'Use the existing runtime status and exact absolute context. Invoke only',
|
|
1446
|
+
'the required sr-* or profile role through a foreground role wave, using',
|
|
1447
|
+
'`workspace:"current"`; do not activate a nested specrails-implement.',
|
|
1448
|
+
'Pass the complete bounded handoff explicitly, including every frozen',
|
|
1449
|
+
'criterion, selected roots, current phase and next action. Native session',
|
|
1450
|
+
'memory is not a substitute. Preserve valid completed phases and source',
|
|
1451
|
+
'work when a later reviewer or archive step is blocked.',
|
|
1534
1452
|
'',
|
|
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'));
|
|
1453
|
+
].join('\n');
|
|
1539
1454
|
}
|
|
1540
1455
|
function renderKimiEnrichWorkflow() {
|
|
1541
1456
|
return [
|
|
@@ -1707,15 +1622,6 @@ function renderKimiTelemetryWorkflow() {
|
|
|
1707
1622
|
'',
|
|
1708
1623
|
].join('\n');
|
|
1709
1624
|
}
|
|
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
1625
|
function writeKimiRoleSkill(args) {
|
|
1720
1626
|
if (!pathExists(args.src))
|
|
1721
1627
|
return;
|
|
@@ -1737,6 +1643,8 @@ function writeKimiRoleSkill(args) {
|
|
|
1737
1643
|
'',
|
|
1738
1644
|
].join('\n');
|
|
1739
1645
|
writeFileLf(args.dest, frontmatter +
|
|
1646
|
+
(pathExists(path.join(path.dirname(args.src), '..', '..', 'runtime', 'provider-pipeline.md'))
|
|
1647
|
+
? readTextFile(path.join(path.dirname(args.src), '..', '..', 'runtime', 'provider-pipeline.md')) + '\n' : '') +
|
|
1740
1648
|
KIMI_NESTED_SKILL_CONTRACT +
|
|
1741
1649
|
KIMI_RUNTIME_CONTEXT_CONTRACT +
|
|
1742
1650
|
rendered);
|
|
@@ -1809,6 +1717,7 @@ function writeGeminiAgentFromTemplate(args) {
|
|
|
1809
1717
|
`description: ${JSON.stringify(description ?? args.agentId)}`,
|
|
1810
1718
|
`model: ${model}`,
|
|
1811
1719
|
`tools: [${GEMINI_AGENT_TOOLS.join(', ')}]`,
|
|
1720
|
+
...geminiAgentLimitMetadata(),
|
|
1812
1721
|
'---',
|
|
1813
1722
|
'',
|
|
1814
1723
|
].join('\n');
|
|
@@ -2070,6 +1979,38 @@ function renderInitialGeminiMd(repoRoot) {
|
|
|
2070
1979
|
'',
|
|
2071
1980
|
].join('\n');
|
|
2072
1981
|
}
|
|
1982
|
+
function providerPipelineContract(scriptDir) {
|
|
1983
|
+
const source = path.join(scriptDir, 'templates', 'runtime', 'provider-pipeline.md');
|
|
1984
|
+
return pathExists(source) ? readTextFile(source) + '\n\n' : '';
|
|
1985
|
+
}
|
|
1986
|
+
function prependSkillContract(file, contract) {
|
|
1987
|
+
if (!contract || !pathExists(file))
|
|
1988
|
+
return;
|
|
1989
|
+
const source = readTextFile(file);
|
|
1990
|
+
const end = source.startsWith('---\n') ? source.indexOf('\n---\n', 4) : -1;
|
|
1991
|
+
const index = end < 0 ? 0 : end + 5;
|
|
1992
|
+
writeFileLf(file, source.slice(0, index) + '\n' + contract + source.slice(index));
|
|
1993
|
+
}
|
|
1994
|
+
function assertPipelineRuntimeSource(scriptDir) {
|
|
1995
|
+
const contractFile = path.join(scriptDir, 'integration-contract.json');
|
|
1996
|
+
if (!pathExists(contractFile))
|
|
1997
|
+
return;
|
|
1998
|
+
const contract = JSON.parse(readTextFile(contractFile));
|
|
1999
|
+
if (contract.execution?.runtime && !pathExists(path.join(scriptDir, 'dist', 'installer', 'runtime', 'pipeline-state.js'))) {
|
|
2000
|
+
throw new Error('Core declares a pipeline runtime but its compiled module is missing; rebuild or reinstall this Core package before refreshing providers');
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
function placePipelineRuntime(input) {
|
|
2004
|
+
const source = path.join(input.scriptDir, 'dist', 'installer', 'runtime', 'pipeline-state.js');
|
|
2005
|
+
// Source-only fixture installations may not include a compiled runtime.
|
|
2006
|
+
if (!pathExists(source))
|
|
2007
|
+
return 0;
|
|
2008
|
+
const dest = path.join(input.artifactRoot, '.specrails', 'runtime');
|
|
2009
|
+
copyFile(source, path.join(dest, 'pipeline-state.mjs'));
|
|
2010
|
+
writeFileLf(path.join(dest, 'pipeline.mjs'), "import { runPipelineCli } from './pipeline-state.mjs'\n" +
|
|
2011
|
+
"process.exitCode = await runPipelineCli(process.argv.slice(2))\n");
|
|
2012
|
+
return 2;
|
|
2013
|
+
}
|
|
2073
2014
|
function pruneLegacyArtifacts(input) {
|
|
2074
2015
|
const legacyPaths = [
|
|
2075
2016
|
path.join(input.artifactRoot, '.specrails', 'bin', 'doctor.sh'),
|
|
@@ -2084,8 +2025,7 @@ function pruneLegacyArtifacts(input) {
|
|
|
2084
2025
|
legacyPaths.push(path.join(input.artifactRoot, input.providerDir, 'skills', 'setup'));
|
|
2085
2026
|
}
|
|
2086
2027
|
else if (input.provider === 'gemini') {
|
|
2087
|
-
//
|
|
2088
|
-
legacyPaths.push(path.join(input.artifactRoot, input.providerDir, 'skills'));
|
|
2028
|
+
// OpenSpec and user skills survive updates; only retired setup commands are managed.
|
|
2089
2029
|
legacyPaths.push(path.join(input.artifactRoot, input.providerDir, 'commands', 'setup.toml'));
|
|
2090
2030
|
legacyPaths.push(path.join(input.artifactRoot, input.providerDir, 'commands', 'specrails', 'setup.toml'));
|
|
2091
2031
|
}
|