specrails-core 4.12.1 → 5.1.0

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