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.
Files changed (48) hide show
  1. package/README.md +103 -310
  2. package/bin/specrails-core.mjs +3 -1
  3. package/dist/installer/cli.js +4 -0
  4. package/dist/installer/cli.js.map +1 -1
  5. package/dist/installer/commands/framework.js +64 -49
  6. package/dist/installer/commands/framework.js.map +1 -1
  7. package/dist/installer/commands/init.js +102 -66
  8. package/dist/installer/commands/init.js.map +1 -1
  9. package/dist/installer/commands/update.js +80 -74
  10. package/dist/installer/commands/update.js.map +1 -1
  11. package/dist/installer/commands/v5-migration.js +14 -0
  12. package/dist/installer/commands/v5-migration.js.map +1 -1
  13. package/dist/installer/phases/framework-lifecycle.js +2 -0
  14. package/dist/installer/phases/framework-lifecycle.js.map +1 -1
  15. package/dist/installer/phases/scaffold.js +191 -258
  16. package/dist/installer/phases/scaffold.js.map +1 -1
  17. package/dist/installer/runtime/pipeline-state.js +801 -0
  18. package/dist/installer/runtime/pipeline-state.js.map +1 -0
  19. package/dist/installer/util/exec.js +6 -1
  20. package/dist/installer/util/exec.js.map +1 -1
  21. package/dist/installer/util/fs.js +11 -2
  22. package/dist/installer/util/fs.js.map +1 -1
  23. package/dist/installer/util/install-transaction.js +246 -0
  24. package/dist/installer/util/install-transaction.js.map +1 -0
  25. package/dist/installer/util/registry.js +20 -0
  26. package/dist/installer/util/registry.js.map +1 -1
  27. package/docs/ci-cd.md +57 -0
  28. package/docs/user-docs/codex-vs-claude-code.md +23 -151
  29. package/docs/user-docs/core-updates.md +70 -0
  30. package/docs/user-docs/provider-pipelines.md +53 -0
  31. package/integration-contract.json +179 -66
  32. package/package.json +5 -2
  33. package/templates/agents/sr-developer.md +9 -11
  34. package/templates/agents/sr-reviewer.md +26 -33
  35. package/templates/codex-skills/batch-implement/SKILL.md +58 -244
  36. package/templates/codex-skills/implement/SKILL.md +136 -338
  37. package/templates/codex-skills/rails/sr-architect/SKILL.md +7 -0
  38. package/templates/codex-skills/rails/sr-developer/SKILL.md +13 -0
  39. package/templates/codex-skills/rails/sr-reviewer/SKILL.md +39 -5
  40. package/templates/codex-skills/retry/SKILL.md +37 -117
  41. package/templates/commands/specrails/batch-implement.md +16 -288
  42. package/templates/commands/specrails/implement.md +62 -1057
  43. package/templates/commands/specrails/retry.md +22 -314
  44. package/templates/gemini-commands/batch-implement.toml +28 -40
  45. package/templates/gemini-commands/implement.toml +55 -114
  46. package/templates/gemini-commands/retry.toml +21 -0
  47. package/templates/kimi/specrails/run-skill.mjs +51 -2
  48. 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
- // NOTE: do NOT emit a `max_turns` (or `maxTurns`/`runConfig`) key in the gemini
130
- // agent frontmatter. Although gemini's documented agent schema lists `max_turns`,
131
- // the 0.46 runtime loader REJECTS a `.gemini/agents/*.md` file that carries it —
132
- // the agent silently fails to register and `invoke_agent` reports "Subagent
133
- // '<name>' not found", so the orchestrator falls back to a generic agent and the
134
- // specialised personas never run. Verified empirically (two identical agents,
135
- // one with `max_turns: 40` not found, one without → loads). The 30-turn default
136
- // cap is instead absorbed by the implement.toml MAX_TURNS re-delegate/resume
137
- // contract. Re-introduce only if a future gemini build is reconfirmed to accept it.
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 under .gemini/agents/. No skills/ tree.
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
- // Framework provider trees are entirely Core-owned. Rebuilding from a clean
483
- // destination removes stale files as well as repairing corrupt/missing ones,
484
- // without touching sibling providers already materialized in this version.
485
- removePath(providerFrameworkDir);
486
- removePath(stampPath);
487
- // Reuse scaffoldInstallation's static-placement helpers by pointing
488
- // `artifactRoot` at the version dir. `seedProjectDirs: false` keeps the copy
489
- // free of per-workspace mutable state. The `codeRoot` is irrelevant to the
490
- // STATIC subtree (the project-named instruction files are skipped below), so
491
- // we hand it the framework dir to satisfy the contract — and we DELETE any
492
- // project-named instruction file the settings helpers wrote.
493
- // The SHARED framework store is always the FULL SUPERSET — EVERY agent — so a
494
- // SECOND project with a DIFFERENT agent selection links its specialists from
495
- // the same materialized copy instead of inheriting the first project's
496
- // narrower set. Per-project filtering moves to the workspace LINK step
497
- // (`linkAgentFiles` via `assembleProjectWorkspace`). `selectedAgents` on the
498
- // input is intentionally IGNORED here.
499
- const staticInput = {
500
- scriptDir: input.scriptDir,
501
- artifactRoot: versionDir,
502
- codeRoot: versionDir,
503
- provider: input.provider,
504
- providerDir: input.providerDir,
505
- selectedAgents: undefined,
506
- materializeAllAgents: true,
507
- seedProjectDirs: false,
508
- };
509
- scaffoldInstallation(staticInput);
510
- // The settings helpers also emit a project-named root instruction file
511
- // (AGENTS.md/GEMINI.md/CLAUDE.md) + (for codex) config.toml / (gemini)
512
- // settings.json. The instruction file is per-project → strip it from the
513
- // shared copy; the settings file IS provider-invariant and stays as a
514
- // link target inside the providerDir.
515
- for (const f of ['AGENTS.md', 'GEMINI.md', 'CLAUDE.md']) {
516
- rmSync(path.join(versionDir, f), { force: true });
517
- }
518
- // Kimi's instruction and MCP files are provider-local rather than root-local,
519
- // but both are project-specific and must be real files in each workspace.
520
- // In particular, linking mcp.json would let Desktop mutate the shared
521
- // framework and leak one project's MCP registry into every other project.
522
- if (input.provider === 'kimi') {
523
- rmSync(path.join(providerFrameworkDir, 'AGENTS.md'), { force: true });
524
- rmSync(path.join(providerFrameworkDir, 'mcp.json'), { force: true });
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, so nested calls and run-state do not',
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
- '`.specrails/backlog-config.json`. Route `local` through structured reads and',
1204
- 'atomic writes of `.specrails/local-tickets.json`; route `github` through the',
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
- let adapted = replaceMarkdownSection(body, '##### Apply per-agent model overrides (only when a profile declares them)', '##### Agent roles', [
1334
- '##### Resolve per-role model overrides (profile mode only)',
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
- 'The single `specrails.merge.inventory` frame supplies `baseCommit`,',
1359
- '`manifestPath`, and each safe worktree id, `repoDir`, and complete',
1360
- '`changes` list. Every change is `{status:"A"|"M"|"D",path}`. This',
1361
- 'inventory compares against the synthetic baseline snapshot, includes',
1362
- 'committed/staged/unstaged and non-ignored untracked role output, and',
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
- 'Classify paths across all worktrees before applying anything:',
1367
- '- `exclusive_files`: appears in one worktree only.',
1368
- '- `shared_files`: appears in two or more worktrees.',
1369
- '- Preserve each A/M/D status; a D path has no source file to copy.',
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 replaceMarkdownSection(body, '### Wave invocation', '### Failure isolation', [
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
- 'If `KIMI_ROLE_WAVE` is non-null, validate its safe run id by invoking',
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
- '**Validation:**',
1531
- ].join('\n'))
1532
- .replace('Include PR URL if ship ran successfully.', [
1533
- 'Include PR URL if ship ran successfully.',
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
- 'After all required isolated outputs have been safely merged, invoke the',
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
- // Prune a stale WIP skills/ tree + any setup command leftovers.
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
  }