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.
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 +108 -66
  8. package/dist/installer/commands/init.js.map +1 -1
  9. package/dist/installer/commands/update.js +86 -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 +198 -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 +141 -2
  22. package/dist/installer/util/fs.js.map +1 -1
  23. package/dist/installer/util/install-transaction.js +266 -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,98 @@ 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
+ //
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, so nested calls and run-state do not',
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
- '`.specrails/backlog-config.json`. Route `local` through structured reads and',
1204
- 'atomic writes of `.specrails/local-tickets.json`; route `github` through the',
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
- 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
- '```',
1404
+ return body.replace('##### Invocation configuration', [
1405
+ '##### Kimi invocation configuration',
1357
1406
  '',
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.',
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
- '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.',
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 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'));
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
- '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.',
1443
+ '## Kimi direct-role continuation',
1529
1444
  '',
1530
- '**Validation:**',
1531
- ].join('\n'))
1532
- .replace('Include PR URL if ship ran successfully.', [
1533
- 'Include PR URL if ship ran successfully.',
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
- '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'));
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
- // Prune a stale WIP skills/ tree + any setup command leftovers.
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
  }