specrails-core 4.11.3 → 5.0.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 (108) hide show
  1. package/README.md +96 -89
  2. package/bin/specrails-core.mjs +282 -39
  3. package/bin/tui-installer.mjs +117 -149
  4. package/commands/doctor.md +1 -1
  5. package/dist/installer/cli.js +13 -3
  6. package/dist/installer/cli.js.map +1 -1
  7. package/dist/installer/commands/doctor.js +487 -27
  8. package/dist/installer/commands/doctor.js.map +1 -1
  9. package/dist/installer/commands/framework.js +49 -7
  10. package/dist/installer/commands/framework.js.map +1 -1
  11. package/dist/installer/commands/init.js +443 -41
  12. package/dist/installer/commands/init.js.map +1 -1
  13. package/dist/installer/commands/update.js +51 -23
  14. package/dist/installer/commands/update.js.map +1 -1
  15. package/dist/installer/commands/v5-migration.js +119 -0
  16. package/dist/installer/commands/v5-migration.js.map +1 -0
  17. package/dist/installer/phases/framework-lifecycle.js +125 -0
  18. package/dist/installer/phases/framework-lifecycle.js.map +1 -0
  19. package/dist/installer/phases/install-config.js +160 -11
  20. package/dist/installer/phases/install-config.js.map +1 -1
  21. package/dist/installer/phases/manifest.js +29 -8
  22. package/dist/installer/phases/manifest.js.map +1 -1
  23. package/dist/installer/phases/prereqs.js +57 -3
  24. package/dist/installer/phases/prereqs.js.map +1 -1
  25. package/dist/installer/phases/provider-detect.js +116 -6
  26. package/dist/installer/phases/provider-detect.js.map +1 -1
  27. package/dist/installer/phases/scaffold.js +1217 -117
  28. package/dist/installer/phases/scaffold.js.map +1 -1
  29. package/dist/installer/runtime/kimi.js +255 -0
  30. package/dist/installer/runtime/kimi.js.map +1 -0
  31. package/dist/installer/util/paths.js +12 -0
  32. package/dist/installer/util/paths.js.map +1 -1
  33. package/dist/installer/util/registry.js +234 -14
  34. package/dist/installer/util/registry.js.map +1 -1
  35. package/docs/README.md +1 -0
  36. package/docs/deployment.md +6 -7
  37. package/docs/getting-started.md +11 -7
  38. package/docs/installation.md +34 -16
  39. package/docs/plugin-architecture.md +11 -8
  40. package/docs/updating.md +21 -3
  41. package/docs/user-docs/cli-reference.md +43 -22
  42. package/docs/user-docs/codex-vs-claude-code.md +11 -9
  43. package/docs/user-docs/faq.md +1 -1
  44. package/docs/user-docs/getting-started-codex.md +5 -8
  45. package/docs/user-docs/getting-started-kimi.md +423 -0
  46. package/docs/user-docs/installation.md +49 -14
  47. package/docs/user-docs/quick-start.md +11 -8
  48. package/docs/windows.md +29 -4
  49. package/integration-contract.json +85 -13
  50. package/package.json +9 -5
  51. package/schemas/profile.v1.json +68 -6
  52. package/templates/agents/sr-architect.md +30 -0
  53. package/templates/agents/sr-developer.md +21 -8
  54. package/templates/agents/sr-reviewer.md +44 -31
  55. package/templates/codex-skills/batch-implement/SKILL.md +9 -32
  56. package/templates/codex-skills/implement/SKILL.md +61 -143
  57. package/templates/codex-skills/rails/sr-architect/SKILL.md +38 -20
  58. package/templates/codex-skills/rails/sr-developer/SKILL.md +29 -10
  59. package/templates/codex-skills/rails/sr-reviewer/SKILL.md +21 -10
  60. package/templates/commands/specrails/doctor.md +1 -1
  61. package/templates/commands/specrails/implement.md +117 -288
  62. package/templates/commands/specrails/memory-inspect.md +6 -4
  63. package/templates/commands/specrails/propose-spec.md +1 -1
  64. package/templates/commands/specrails/refactor-recommender.md +8 -51
  65. package/templates/commands/specrails/retry.md +12 -48
  66. package/templates/commands/specrails/telemetry.md +1 -1
  67. package/templates/gemini-commands/implement.toml +9 -0
  68. package/templates/kimi/specrails/run-skill.mjs +3005 -0
  69. package/templates/kimi/specrails/vendor/js-yaml/LICENSE +21 -0
  70. package/templates/kimi/specrails/vendor/js-yaml/NOTICE.md +16 -0
  71. package/templates/kimi/specrails/vendor/js-yaml/js-yaml.mjs +3856 -0
  72. package/templates/profiles/default.json +5 -18
  73. package/templates/profiles/kimi-default.json +15 -0
  74. package/commands/enrich.md +0 -1456
  75. package/templates/agents/sr-backend-developer.md +0 -91
  76. package/templates/agents/sr-backend-reviewer.md +0 -152
  77. package/templates/agents/sr-doc-sync.md +0 -247
  78. package/templates/agents/sr-frontend-developer.md +0 -85
  79. package/templates/agents/sr-frontend-reviewer.md +0 -145
  80. package/templates/agents/sr-merge-resolver.md +0 -195
  81. package/templates/agents/sr-performance-reviewer.md +0 -186
  82. package/templates/agents/sr-product-analyst.md +0 -36
  83. package/templates/agents/sr-product-manager.md +0 -148
  84. package/templates/agents/sr-security-reviewer.md +0 -191
  85. package/templates/agents/sr-test-writer.md +0 -176
  86. package/templates/codex-skills/enrich/SKILL.md +0 -191
  87. package/templates/codex-skills/merge-resolve/SKILL.md +0 -88
  88. package/templates/codex-skills/rails/sr-backend-developer/SKILL.md +0 -93
  89. package/templates/codex-skills/rails/sr-backend-reviewer/SKILL.md +0 -120
  90. package/templates/codex-skills/rails/sr-doc-sync/SKILL.md +0 -124
  91. package/templates/codex-skills/rails/sr-frontend-developer/SKILL.md +0 -106
  92. package/templates/codex-skills/rails/sr-frontend-reviewer/SKILL.md +0 -111
  93. package/templates/codex-skills/rails/sr-merge-resolver/SKILL.md +0 -156
  94. package/templates/codex-skills/rails/sr-performance-reviewer/SKILL.md +0 -109
  95. package/templates/codex-skills/rails/sr-product-analyst/SKILL.md +0 -85
  96. package/templates/codex-skills/rails/sr-product-manager/SKILL.md +0 -131
  97. package/templates/codex-skills/rails/sr-security-reviewer/SKILL.md +0 -121
  98. package/templates/codex-skills/rails/sr-test-writer/SKILL.md +0 -115
  99. package/templates/commands/specrails/auto-propose-backlog-specs.md +0 -312
  100. package/templates/commands/specrails/enrich.md +0 -1456
  101. package/templates/commands/specrails/get-backlog-specs.md +0 -226
  102. package/templates/commands/specrails/merge-resolve.md +0 -172
  103. package/templates/commands/specrails/reconfig.md +0 -80
  104. package/templates/commands/specrails/vpc-drift.md +0 -405
  105. package/templates/commands/test.md +0 -58
  106. package/templates/personas/persona.md +0 -43
  107. package/templates/personas/the-maintainer.md +0 -98
  108. package/templates/settings/perf-thresholds.yml +0 -25
@@ -1,15 +1,14 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { rmSync } from 'node:fs';
2
+ import { renameSync, rmSync } from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
- import { atomicSymlinkSwap, copyDir, copyFile, isDir, isSymlink, listDir, mkdirp, pathExists, readTextFile, removePath, symlinkOrCopy, writeFileLf, } from '../util/fs.js';
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
@@ -59,6 +53,20 @@ const OPSX_TO_GEMINI_SKILL = {
59
53
  explore: 'openspec-explore',
60
54
  onboard: 'openspec-onboard',
61
55
  };
56
+ /** OpenSpec's published Kimi skill ids. The mapping is intentionally explicit. */
57
+ const OPSX_TO_KIMI_SKILL = {
58
+ propose: 'openspec-propose',
59
+ ff: 'openspec-ff-change',
60
+ new: 'openspec-new-change',
61
+ apply: 'openspec-apply-change',
62
+ continue: 'openspec-continue-change',
63
+ archive: 'openspec-archive-change',
64
+ 'bulk-archive': 'openspec-bulk-archive-change',
65
+ sync: 'openspec-sync-specs',
66
+ verify: 'openspec-verify-change',
67
+ explore: 'openspec-explore',
68
+ onboard: 'openspec-onboard',
69
+ };
62
70
  /**
63
71
  * Rewrite every literal `Skill("opsx:<id>"[, …])` call in a Claude-authored agent
64
72
  * body into the Gemini `activate_skill(name="…")` form. Positional skill input
@@ -73,6 +81,39 @@ export function translateOpsxSkillCallsForGemini(body) {
73
81
  return skill ? `activate_skill(name="${skill}")` : match;
74
82
  });
75
83
  }
84
+ /**
85
+ * Port shared Claude-authored prose to Kimi's directory-skill contract.
86
+ *
87
+ * Kimi's TUI/ACP clients intercept `/skill:*`, but a materialized workflow is
88
+ * already running inside a Session and must activate nested workflows through
89
+ * Kimi's built-in `Skill` tool (`{ skill, args }`). Emitting slash text here
90
+ * would silently become ordinary model text under `kimi -p`. Interactive slash
91
+ * examples therefore belong only in AGENTS/docs, never generated skill bodies.
92
+ * This is render-only; canonical Claude/Codex/Gemini templates stay unchanged.
93
+ */
94
+ export function translateClaudeTextForKimi(body) {
95
+ let translated = body.replace(/Skill\("opsx:([a-z-]+)"(?:\s*,\s*("[^"]*"|'[^']*'|[^)]*))?\)/g, (_match, id, input) => {
96
+ const skill = OPSX_TO_KIMI_SKILL[id];
97
+ if (!skill)
98
+ return `Unresolved Kimi Skill tool mapping for "opsx:${id}"`;
99
+ const args = input?.replace(/^["']|["']$/g, '') ?? '';
100
+ return `Skill(skill="${skill}", args=${JSON.stringify(args)})`;
101
+ });
102
+ translated = translated
103
+ .replace(/\.claude\/agents\/personas\//g, '.kimi-code/personas/')
104
+ .replace(/\.claude\/agents\//g, '.kimi-code/skills/')
105
+ .replace(/(\.kimi-code\/skills\/[^\s`"'()]+)\.md/g, '$1/SKILL.md')
106
+ .replace(/\.claude\//g, '.kimi-code/')
107
+ .replace(/\.claude\b/g, '.kimi-code')
108
+ .replace(/\bCLAUDE\.md\b/g, '.kimi-code/AGENTS.md')
109
+ .replace(/\/(?:specrails|sr):([a-z0-9-]+)/g, 'Skill(skill="specrails-$1", args=<arguments following this command>)')
110
+ .replace(/\/(?:specrails|sr):/g, 'Skill(skill="specrails-<command>", args=<arguments following this command>)')
111
+ .replace(/\bsubagent_type\b/g, 'role_skill')
112
+ .replace(/\bClaude Code\b/g, 'Kimi Code')
113
+ .replace(/\bClaude CLI\b/g, 'Kimi CLI')
114
+ .replace(/\bAgent tool\b/g, 'external Kimi role process');
115
+ return translated;
116
+ }
76
117
  /**
77
118
  * Per-role gemini model. Defaults to `gemini-3.5-flash` — the stable flagship
78
119
  * (June 2026): strong agentic/coding, high quota, and unlike `gemini-2.5-pro`
@@ -94,14 +135,6 @@ const GEMINI_DEFAULT_MODEL = 'gemini-3.5-flash';
94
135
  // one with `max_turns: 40` → not found, one without → loads). The 30-turn default
95
136
  // cap is instead absorbed by the implement.toml MAX_TURNS → re-delegate/resume
96
137
  // contract. Re-introduce only if a future gemini build is reconfirmed to accept it.
97
- /**
98
- * Skills excluded from the quick tier because they depend on
99
- * VPC-only agents (sr-product-manager, sr-product-analyst).
100
- */
101
- const QUICK_EXCLUDED_SKILLS = new Set([
102
- 'sr-auto-propose-backlog-specs',
103
- 'sr-get-backlog-specs',
104
- ]);
105
138
  /**
106
139
  * Claude top-level `sr-*` skills, GENERATED at install time from their
107
140
  * canonical slash-command body under `templates/commands/specrails/<command>.md`.
@@ -131,26 +164,7 @@ const SKILL_FROM_COMMAND = {
131
164
  command: 'why',
132
165
  description: 'sr:why — Search explanation records written by specrails agents during the OpenSpec implementation pipeline.',
133
166
  },
134
- 'sr-get-backlog-specs': {
135
- command: 'get-backlog-specs',
136
- description: 'sr:get-backlog-specs — View product-driven backlog from GitHub Issues and propose top 3 for implementation.',
137
- },
138
- 'sr-auto-propose-backlog-specs': {
139
- command: 'auto-propose-backlog-specs',
140
- description: 'sr:auto-propose-backlog-specs — Generate new feature ideas through product discovery, create GitHub Issues.',
141
- },
142
167
  };
143
- /**
144
- * Command → required agent dependency map. A command is excluded
145
- * from the quick tier when its required agent was excluded (i.e.
146
- * VPC-dependent) or when the feature flag (Agent Teams) is off.
147
- */
148
- const COMMAND_AGENT_DEPENDENCIES = [
149
- { command: 'auto-propose-backlog-specs', requires: ['sr-product-manager'] },
150
- { command: 'vpc-drift', requires: ['sr-product-manager', 'sr-product-analyst'] },
151
- { command: 'get-backlog-specs', requires: ['sr-product-analyst'] },
152
- { command: 'merge-resolve', requires: ['sr-merge-resolver'] },
153
- ];
154
168
  /**
155
169
  * Agents that write "explanation" memory records. When any of them
156
170
  * ships we also need a shared `.claude/agent-memory/explanations/`
@@ -171,7 +185,18 @@ const LINKED_PROVIDER_SUBTREES = {
171
185
  claude: ['agents', 'commands', 'skills', 'rules'],
172
186
  codex: ['skills'],
173
187
  gemini: ['agents', 'commands'],
188
+ // Kimi skills are linked one directory at a time so direct-child OpenSpec
189
+ // skills and user-owned custom-* roles can coexist. The self-contained
190
+ // headless runner and its vendored parser are Core-owned and linked as a
191
+ // separate static subtree.
192
+ kimi: ['rules', 'specrails'],
174
193
  };
194
+ const KIMI_RUNNER_RELATIVE_FILES = [
195
+ 'run-skill.mjs',
196
+ path.join('vendor', 'js-yaml', 'js-yaml.mjs'),
197
+ path.join('vendor', 'js-yaml', 'LICENSE'),
198
+ path.join('vendor', 'js-yaml', 'NOTICE.md'),
199
+ ];
175
200
  /**
176
201
  * Returns true iff any of the provider directories already contains
177
202
  * content. The desktop-app-driven path skips the "merge existing?" prompt and
@@ -209,7 +234,6 @@ export function scaffoldInstallation(input) {
209
234
  // Codex skills live under <providerDir>/skills/ (e.g. .codex/skills/).
210
235
  // The pre-§18 code wrote to `.agents/skills/` which codex doesn't read;
211
236
  // that was a placeholder name from the gated state.
212
- mk(path.join(input.artifactRoot, input.providerDir, 'skills', 'enrich'));
213
237
  mk(path.join(input.artifactRoot, input.providerDir, 'skills', 'doctor'));
214
238
  mk(path.join(input.artifactRoot, input.providerDir, 'skills', 'rails'));
215
239
  }
@@ -219,6 +243,11 @@ export function scaffoldInstallation(input) {
219
243
  mk(path.join(input.artifactRoot, input.providerDir, 'commands', 'specrails'));
220
244
  mk(path.join(input.artifactRoot, input.providerDir, 'agents'));
221
245
  }
246
+ else if (input.provider === 'kimi') {
247
+ mk(path.join(input.artifactRoot, input.providerDir, 'skills'));
248
+ mk(path.join(input.artifactRoot, input.providerDir, 'specrails'));
249
+ mk(path.join(input.artifactRoot, input.providerDir, 'rules'));
250
+ }
222
251
  else {
223
252
  mk(path.join(input.artifactRoot, input.providerDir, 'commands', 'specrails'));
224
253
  mk(path.join(input.artifactRoot, input.providerDir, 'skills'));
@@ -228,7 +257,6 @@ export function scaffoldInstallation(input) {
228
257
  mk(path.join(setupTemplates, 'commands'));
229
258
  mk(path.join(setupTemplates, 'skills'));
230
259
  mk(path.join(setupTemplates, 'rules'));
231
- mk(path.join(setupTemplates, 'personas'));
232
260
  mk(path.join(setupTemplates, 'claude-md'));
233
261
  mk(path.join(setupTemplates, 'settings'));
234
262
  // --- .gitignore hygiene ---
@@ -240,6 +268,9 @@ export function scaffoldInstallation(input) {
240
268
  const gitignoreEntries = ['.claude/agent-memory/', '.specrails/'];
241
269
  if (input.provider === 'gemini')
242
270
  gitignoreEntries.push('.gemini/agent-memory/');
271
+ if (input.provider === 'kimi') {
272
+ gitignoreEntries.push('.kimi-code/agent-memory/', '.kimi-code/pipeline-state/', '.kimi-code/.dry-run/', '.kimi-code/telemetry/');
273
+ }
243
274
  ensureGitignore(input.codeRoot, gitignoreEntries);
244
275
  }
245
276
  // --- Copy bundled templates into setup-templates/ ---
@@ -261,16 +292,18 @@ export function scaffoldInstallation(input) {
261
292
  else {
262
293
  warn(`templates/ not found at ${templatesSrc} — skipping template copy`);
263
294
  }
264
- // --- Write bundled commands (enrich.md + doctor.md) ---
295
+ // --- Write bundled commands (doctor.md) ---
265
296
  copyBundledCommands({ ...input, copiedIncrement: (n) => (copiedFiles += n) });
266
297
  pruneLegacyArtifacts(input);
267
- // --- Quick tier: direct-placement short-circuit ---
268
- if (input.tier === 'quick') {
269
- const placed = placeQuickTierArtefacts({ ...input });
298
+ if (input.provider === 'kimi') {
299
+ copiedFiles += placeKimiSkillRunner(input);
300
+ }
301
+ // --- Direct placement (the only path) ---
302
+ {
303
+ const placed = placeArtefacts({ ...input });
270
304
  copiedFiles += placed.agents + placed.commands + placed.rules;
271
- const skippedNote = placed.skippedAgents > 0 ? ` (skipped ${placed.skippedAgents} VPC-dependent)` : '';
272
- info(`Quick tier: placed ${placed.agents} agent(s) + ${placed.commands} command(s) + ` +
273
- `${placed.rules} rule file(s) directly into ${input.providerDir}/${skippedNote}`);
305
+ info(`Placed ${placed.agents} agent(s) + ${placed.commands} command(s) + ` +
306
+ `${placed.rules} rule file(s) directly into ${input.providerDir}/`);
274
307
  }
275
308
  // --- Skills placement (both tiers, both providers) ---
276
309
  // Claude: top-level `sr-*` skills are generated from their canonical
@@ -280,10 +313,9 @@ export function scaffoldInstallation(input) {
280
313
  {
281
314
  const skills = placeSkills(input);
282
315
  copiedFiles += skills.filesCopied;
283
- const skillSkipNote = skills.skipped > 0 ? ` (skipped ${skills.skipped} VPC-dependent)` : '';
284
316
  const skillsLabel = input.provider === 'gemini' ? 'agent' : 'skill';
285
317
  const skillsSubdir = input.provider === 'gemini' ? 'agents' : 'skills';
286
- info(`Placed ${skills.placed} ${skillsLabel}(s) into ${input.providerDir}/${skillsSubdir}/${skillSkipNote}`);
318
+ info(`Placed ${skills.placed} ${skillsLabel}(s) into ${input.providerDir}/${skillsSubdir}/`);
287
319
  }
288
320
  // --- Codex provider settings + AGENTS.md initial content ---
289
321
  if (input.provider === 'codex') {
@@ -300,12 +332,12 @@ export function scaffoldInstallation(input) {
300
332
  info(`Gemini provider: wrote ${written} setting file(s) (settings.json, GEMINI.md)`);
301
333
  }
302
334
  }
303
- // --- Full-tier hint: enrich is required to generate VPC artefacts ---
304
- if (input.tier === 'full') {
305
- const cliName = input.provider === 'codex' ? 'Codex CLI' : input.provider === 'gemini' ? 'Gemini CLI' : 'Claude Code';
306
- info(`Full tier staged. Run \`/specrails:enrich\` in ${cliName} to generate ` +
307
- 'VPC personas and adapt agents (including sr-product-manager and ' +
308
- 'sr-product-analyst) to this codebase.');
335
+ else if (input.provider === 'kimi') {
336
+ const written = applyKimiSettings(input);
337
+ copiedFiles += written;
338
+ if (written > 0) {
339
+ info(`Kimi provider: wrote ${written} setting file(s) (.kimi-code/AGENTS.md, mcp.json)`);
340
+ }
309
341
  }
310
342
  ok(`Created ${createdDirs.length} directories, copied ${copiedFiles} files`);
311
343
  return {
@@ -319,11 +351,109 @@ export function scaffoldInstallation(input) {
319
351
  };
320
352
  }
321
353
  /** Path to the per-version, per-provider materialization marker (manifest hash). */
322
- function frameworkStampPath(versionDir, providerDir) {
354
+ export function frameworkStampPath(versionDir, providerDir) {
323
355
  // Store the stamp OUTSIDE the providerDir so it never leaks into the linked
324
356
  // subtree. `.stamp-<providerDir>.json` is provider-keyed.
325
357
  return path.join(versionDir, `.framework-stamp${providerDir}.json`);
326
358
  }
359
+ /**
360
+ * Stable Merkle-like digest over regular files. Relative POSIX paths and raw
361
+ * bytes are both framed into the hash, so renames, missing files, additions and
362
+ * byte corruption are detected. Directory mtimes and traversal order never
363
+ * affect the result.
364
+ */
365
+ function hashFrameworkTrees(roots, options = {}) {
366
+ const hash = createHash('sha256');
367
+ const walk = (root, current, label) => {
368
+ const entries = listDir(current).sort((a, b) => path.basename(a).localeCompare(path.basename(b)));
369
+ for (const entry of entries) {
370
+ const name = path.basename(entry);
371
+ if (options.ignorePackageNoise === true &&
372
+ (name === 'node_modules' || name === 'package-lock.json')) {
373
+ continue;
374
+ }
375
+ const rel = path.relative(root, entry).split(path.sep).join('/');
376
+ if (isDir(entry)) {
377
+ walk(root, entry, label);
378
+ continue;
379
+ }
380
+ if (!pathExists(entry))
381
+ continue;
382
+ const framedPath = `${label}/${rel}`;
383
+ const bytes = readBytes(entry);
384
+ hash.update(`file\0${Buffer.byteLength(framedPath)}\0${framedPath}\0`);
385
+ hash.update(`${bytes.byteLength}\0`);
386
+ hash.update(bytes);
387
+ }
388
+ };
389
+ for (const root of [...roots].sort((a, b) => a.label.localeCompare(b.label))) {
390
+ hash.update(`root\0${root.label}\0`);
391
+ if (isDir(root.dir)) {
392
+ walk(root.dir, root.dir, root.label);
393
+ }
394
+ else {
395
+ hash.update('missing\0');
396
+ }
397
+ }
398
+ return `sha256:${hash.digest('hex')}`;
399
+ }
400
+ /** Hash of every package input that can influence provider materialization. */
401
+ function frameworkSourceHash(scriptDir, provider) {
402
+ const treeHash = hashFrameworkTrees([
403
+ { label: 'templates', dir: path.join(scriptDir, 'templates') },
404
+ { label: 'commands', dir: path.join(scriptDir, 'commands') },
405
+ ], { ignorePackageNoise: true });
406
+ return `sha256:${createHash('sha256')
407
+ .update(treeHash)
408
+ .update('\0provider\0')
409
+ .update(provider)
410
+ .digest('hex')}`;
411
+ }
412
+ /** Hash of the provider-static tree workspace links consume. */
413
+ function frameworkContentHash(providerFrameworkDir) {
414
+ return hashFrameworkTrees([
415
+ { label: 'provider', dir: providerFrameworkDir },
416
+ ]);
417
+ }
418
+ function readFrameworkStamp(stampPath) {
419
+ if (!pathExists(stampPath))
420
+ return null;
421
+ try {
422
+ const parsed = JSON.parse(readTextFile(stampPath));
423
+ if (parsed.schema !== 1 ||
424
+ typeof parsed.version !== 'string' ||
425
+ typeof parsed.provider !== 'string' ||
426
+ typeof parsed.source_hash !== 'string' ||
427
+ typeof parsed.content_hash !== 'string') {
428
+ return null;
429
+ }
430
+ return parsed;
431
+ }
432
+ catch {
433
+ return null;
434
+ }
435
+ }
436
+ /**
437
+ * Validate one provider in a materialized version without needing the source
438
+ * package. Used by the final swap gate: the stamp identity and current output
439
+ * hash must still agree immediately before `current` moves.
440
+ */
441
+ export function frameworkMaterializationProblem(versionDir, version, provider, providerDir) {
442
+ const providerFrameworkDir = path.join(versionDir, providerDir);
443
+ const stampPath = frameworkStampPath(versionDir, providerDir);
444
+ if (!isDir(providerFrameworkDir))
445
+ return `missing ${providerDir}/`;
446
+ const stamp = readFrameworkStamp(stampPath);
447
+ if (!stamp)
448
+ return `missing or invalid ${path.basename(stampPath)}`;
449
+ if (stamp.version !== version || stamp.provider !== provider) {
450
+ return `invalid stamp (expected version=${version}, provider=${provider})`;
451
+ }
452
+ if (stamp.content_hash !== frameworkContentHash(providerFrameworkDir)) {
453
+ return 'managed content does not match stamp';
454
+ }
455
+ return null;
456
+ }
327
457
  /**
328
458
  * Materialize the provider-INVARIANT framework subtree ONCE into
329
459
  * `<frameworkDir>/<version>/<providerDir>/` (+ `<version>/setup-templates/`).
@@ -336,10 +466,24 @@ export function installFramework(input) {
336
466
  const versionDir = path.join(input.frameworkDir, input.version);
337
467
  const providerFrameworkDir = path.join(versionDir, input.providerDir);
338
468
  const stampPath = frameworkStampPath(versionDir, input.providerDir);
339
- // Idempotency: existing materialization with a matching stamp → skip.
340
- if (isDir(providerFrameworkDir) && pathExists(stampPath)) {
469
+ const sourceHash = frameworkSourceHash(input.scriptDir, input.provider);
470
+ const stamp = readFrameworkStamp(stampPath);
471
+ // Same-version reuse is allowed only when BOTH provenance and every managed
472
+ // output byte still match the deterministic stamp. A legacy/timestamp-only
473
+ // stamp, a changed package source, or any missing/corrupt/extra managed file
474
+ // falls through to a clean provider-tree repair.
475
+ if (isDir(providerFrameworkDir) &&
476
+ stamp?.version === input.version &&
477
+ stamp.provider === input.provider &&
478
+ stamp.source_hash === sourceHash &&
479
+ stamp.content_hash === frameworkContentHash(providerFrameworkDir)) {
341
480
  return { providerFrameworkDir, versionDir, materialized: false };
342
481
  }
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);
343
487
  // Reuse scaffoldInstallation's static-placement helpers by pointing
344
488
  // `artifactRoot` at the version dir. `seedProjectDirs: false` keeps the copy
345
489
  // free of per-workspace mutable state. The `codeRoot` is irrelevant to the
@@ -358,7 +502,6 @@ export function installFramework(input) {
358
502
  codeRoot: versionDir,
359
503
  provider: input.provider,
360
504
  providerDir: input.providerDir,
361
- tier: 'quick',
362
505
  selectedAgents: undefined,
363
506
  materializeAllAgents: true,
364
507
  seedProjectDirs: false,
@@ -372,7 +515,22 @@ export function installFramework(input) {
372
515
  for (const f of ['AGENTS.md', 'GEMINI.md', 'CLAUDE.md']) {
373
516
  rmSync(path.join(versionDir, f), { force: true });
374
517
  }
375
- writeFileLf(stampPath, `${JSON.stringify({ version: input.version, provider: input.provider, at: new Date().toISOString() }, null, 2)}\n`);
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 });
525
+ }
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`);
376
534
  return { providerFrameworkDir, versionDir, materialized: true };
377
535
  }
378
536
  /**
@@ -430,10 +588,22 @@ export function assembleProjectWorkspace(input) {
430
588
  links[sub] = symlinkOrCopy(target, dest, preferCopy);
431
589
  }
432
590
  }
433
- // Link the provider-invariant settings file (codex config.toml / gemini
434
- // settings.json) when the framework has one and the user has not authored a
435
- // local override in the workspace.
436
- const settingsFile = input.provider === 'codex' ? 'config.toml' : input.provider === 'gemini' ? 'settings.json' : null;
591
+ if (input.provider === 'kimi') {
592
+ const kimiSkillsTarget = path.join(currentProviderDir, 'skills');
593
+ const kimiSkillsDest = path.join(workspaceProviderDir, 'skills');
594
+ migrateLegacyKimiRoleLayout(kimiSkillsDest);
595
+ if (pathExists(kimiSkillsTarget)) {
596
+ links.skills = linkKimiSkillDirectories(kimiSkillsTarget, kimiSkillsDest, selectedAgentSet, preferCopy);
597
+ }
598
+ }
599
+ // Link only provider-invariant settings (codex config.toml / gemini
600
+ // settings.json). Kimi mcp.json is a mutable per-project registry and is
601
+ // seeded below as a real workspace file.
602
+ const settingsFile = input.provider === 'codex'
603
+ ? 'config.toml'
604
+ : input.provider === 'gemini'
605
+ ? 'settings.json'
606
+ : null;
437
607
  if (settingsFile) {
438
608
  const settingsTarget = path.join(currentProviderDir, settingsFile);
439
609
  const settingsLink = path.join(workspaceProviderDir, settingsFile);
@@ -449,6 +619,8 @@ export function assembleProjectWorkspace(input) {
449
619
  scriptDir: input.scriptDir,
450
620
  repoRoot: input.workspace,
451
621
  version: input.version,
622
+ providers: [input.provider],
623
+ primaryProvider: input.provider,
452
624
  });
453
625
  writeManifestFiles(input.workspace, manifest);
454
626
  return { links, seededMemoryAgents };
@@ -473,7 +645,7 @@ function seedProjectLayer(input, currentProviderDir) {
473
645
  if (!name.endsWith('.md'))
474
646
  continue;
475
647
  const id = name.slice(0, -3);
476
- if (selected.has(id) && !QUICK_EXCLUDED_AGENTS.has(id))
648
+ if (selected.has(id))
477
649
  placedAgentIds.push(id);
478
650
  }
479
651
  }
@@ -510,8 +682,63 @@ function seedProjectLayer(input, currentProviderDir) {
510
682
  warn(`gemini agent pre-acknowledgment skipped: ${err.message}`);
511
683
  }
512
684
  }
685
+ else if (input.provider === 'kimi') {
686
+ const skillsDir = path.join(currentProviderDir, 'skills');
687
+ if (isDir(skillsDir)) {
688
+ for (const roleDir of listDir(skillsDir)) {
689
+ if (!isDir(roleDir))
690
+ continue;
691
+ const id = path.basename(roleDir);
692
+ if (/^sr-[a-z0-9-]+$/.test(id) &&
693
+ selected.has(id) &&
694
+ pathExists(path.join(roleDir, 'SKILL.md'))) {
695
+ placedAgentIds.push(id);
696
+ }
697
+ }
698
+ }
699
+ for (const id of placedAgentIds) {
700
+ mkdirp(path.join(input.workspace, '.kimi-code', 'agent-memory', id));
701
+ seededMemoryAgents.push(id);
702
+ if (EXPLANATION_AUTHORS.has(id)) {
703
+ mkdirp(path.join(input.workspace, '.kimi-code', 'agent-memory', 'explanations'));
704
+ }
705
+ }
706
+ seedInstructionFile(path.join(input.workspace, '.kimi-code', 'AGENTS.md'), renderInitialKimiAgentsMd(input.codeRoot));
707
+ seedKimiMcpFile(path.join(input.workspace, '.kimi-code', 'mcp.json'));
708
+ if (input.workspace === input.codeRoot) {
709
+ ensureGitignore(input.codeRoot, [
710
+ '.kimi-code/agent-memory/',
711
+ '.kimi-code/pipeline-state/',
712
+ '.kimi-code/.dry-run/',
713
+ '.kimi-code/telemetry/',
714
+ '.specrails/',
715
+ ]);
716
+ }
717
+ }
513
718
  return seededMemoryAgents;
514
719
  }
720
+ /**
721
+ * Ensure Kimi's per-project MCP registry is a real writable file. Older Core
722
+ * builds could create a framework symlink here; migrate a readable link by
723
+ * copying its bytes locally, or seed an empty registry when the link is stale.
724
+ */
725
+ function seedKimiMcpFile(mcpPath) {
726
+ if (isSymlink(mcpPath)) {
727
+ let existing = '{\n "mcpServers": {}\n}\n';
728
+ try {
729
+ existing = readTextFile(mcpPath);
730
+ }
731
+ catch {
732
+ // A version swap can leave the obsolete shared-framework link dangling.
733
+ }
734
+ removePath(mcpPath);
735
+ writeFileLf(mcpPath, existing);
736
+ return;
737
+ }
738
+ if (!pathExists(mcpPath)) {
739
+ writeFileLf(mcpPath, '{\n "mcpServers": {}\n}\n');
740
+ }
741
+ }
515
742
  /**
516
743
  * Per-file link the framework `agents/` into a REAL workspace `agents/` dir.
517
744
  * Keeps `custom-*.md` (and any other user-authored file that the framework does
@@ -519,10 +746,10 @@ function seedProjectLayer(input, currentProviderDir) {
519
746
  * every SELECTED framework-owned agent at the shared read-only copy.
520
747
  *
521
748
  * `selectedIds` is the per-project agent allow-list (already unioned with the
522
- * CORE trio by the caller). Only framework agents whose id is in it AND not in
523
- * `QUICK_EXCLUDED_AGENTS` are linked — the shared framework store is the full
524
- * superset, so this is where per-project filtering lands. `undefined` ⇒ link
525
- * every framework agent (used by the legacy callers / parity tests).
749
+ * CORE trio by the caller). Only framework agents whose id is in it are linked —
750
+ * the shared framework store is the full superset, so this is where per-project
751
+ * filtering lands. `undefined` ⇒ link every framework agent (used by the legacy
752
+ * callers / parity tests).
526
753
  *
527
754
  * When `preferCopy` is true each agent is COPIED as a real file rather than
528
755
  * symlinked (the in-repo standalone install — so a standalone user's CLI finds
@@ -546,7 +773,7 @@ function linkAgentFiles(frameworkAgentsDir, workspaceAgentsDir, selectedIds, pre
546
773
  continue;
547
774
  frameworkProvided.add(name);
548
775
  const id = name.slice(0, -3);
549
- if (selectedIds && (!selectedIds.has(id) || QUICK_EXCLUDED_AGENTS.has(id)))
776
+ if (selectedIds && !selectedIds.has(id))
550
777
  continue;
551
778
  linkedNames.add(name);
552
779
  const m = symlinkOrCopy(src, path.join(workspaceAgentsDir, name), preferCopy);
@@ -585,6 +812,88 @@ function linkAgentFiles(frameworkAgentsDir, workspaceAgentsDir, selectedIds, pre
585
812
  }
586
813
  return mechanism;
587
814
  }
815
+ /**
816
+ * Assemble Kimi skills without turning the whole directory into a symlink.
817
+ * Kimi's loader inspects only immediate children of `.kimi-code/skills`, so
818
+ * workflows (`specrails-*`), OpenSpec skills (`openspec-*`), managed roles
819
+ * (`sr-*`), and user roles (`custom-*`) all share this flat directory.
820
+ * OpenSpec and custom/unknown skills must survive every update.
821
+ */
822
+ function linkKimiSkillDirectories(frameworkSkillsDir, workspaceSkillsDir, selectedRoleIds, preferCopy) {
823
+ mkdirp(workspaceSkillsDir);
824
+ const linkedFrameworkNames = new Set();
825
+ let mechanism = 'symlink';
826
+ for (const source of listDir(frameworkSkillsDir)) {
827
+ if (!isDir(source))
828
+ continue;
829
+ const name = path.basename(source);
830
+ if (name === 'rails') {
831
+ // A same-version framework materialized by the experimental build may
832
+ // still contain this container. `installFramework` normally rematerializes
833
+ // it, but never expose nested roles if a caller supplies one directly.
834
+ continue;
835
+ }
836
+ if (/^sr-[a-z0-9-]+$/.test(name) && !selectedRoleIds.has(name)) {
837
+ continue;
838
+ }
839
+ linkedFrameworkNames.add(name);
840
+ const used = symlinkOrCopy(source, path.join(workspaceSkillsDir, name), preferCopy);
841
+ if (used === 'copy')
842
+ mechanism = 'copy';
843
+ else if (used === 'junction' && mechanism !== 'copy')
844
+ mechanism = 'junction';
845
+ }
846
+ // `specrails-*` workflows and `sr-*` roles are framework-owned. OpenSpec,
847
+ // custom-* and unknown/user skill directories remain outside this boundary.
848
+ for (const existing of listDir(workspaceSkillsDir)) {
849
+ const name = path.basename(existing);
850
+ if (linkedFrameworkNames.has(name))
851
+ continue;
852
+ if (name.startsWith('specrails-') || /^sr-[a-z0-9-]+$/.test(name)) {
853
+ removePath(existing);
854
+ }
855
+ }
856
+ return mechanism;
857
+ }
858
+ /**
859
+ * Migrate the pre-release `skills/rails/<role>` layout without risking user
860
+ * data. Framework-owned `sr-*` directories are dropped (the caller recreates
861
+ * them at the discoverable flat path). Reserved `custom-*` roles are atomically
862
+ * moved to `skills/custom-*` when that target is free. A collision or unknown
863
+ * child remains byte-untouched under `rails/` and doctor reports it, requiring
864
+ * explicit user resolution rather than destructive guessing.
865
+ */
866
+ function migrateLegacyKimiRoleLayout(skillsDir) {
867
+ const legacyRolesDir = path.join(skillsDir, 'rails');
868
+ if (!isDir(legacyRolesDir))
869
+ return;
870
+ for (const source of listDir(legacyRolesDir)) {
871
+ if (!isDir(source))
872
+ continue;
873
+ const id = path.basename(source);
874
+ if (/^sr-[a-z0-9-]+$/.test(id)) {
875
+ removePath(source);
876
+ continue;
877
+ }
878
+ if (!id.startsWith('custom-'))
879
+ continue;
880
+ const destination = path.join(skillsDir, id);
881
+ if (pathExists(destination)) {
882
+ warn(`Kimi role migration kept ${path.relative(skillsDir, source)} because ` +
883
+ `${id}/ already exists; resolve the duplicate manually`);
884
+ continue;
885
+ }
886
+ try {
887
+ renameSync(source, destination);
888
+ info(`Migrated Kimi role skills/rails/${id} → skills/${id}`);
889
+ }
890
+ catch (err) {
891
+ warn(`failed to migrate Kimi role ${id}: ${err.message}`);
892
+ }
893
+ }
894
+ if (listDir(legacyRolesDir).length === 0)
895
+ removePath(legacyRolesDir);
896
+ }
588
897
  /**
589
898
  * True when `name` (an `<id>.md`) matches a framework-owned agent id (`sr-*`).
590
899
  * Used to identify a stale COPY-fallback framework agent on Windows that the
@@ -595,6 +904,19 @@ function linkAgentFiles(frameworkAgentsDir, workspaceAgentsDir, selectedIds, pre
595
904
  function isFrameworkAgentName(name) {
596
905
  return /^sr-[a-z0-9-]+\.md$/.test(name);
597
906
  }
907
+ /**
908
+ * Install Core's self-contained Kimi headless skill runner and the vendored
909
+ * js-yaml parser used by upstream Kimi 0.27. These files are provider-static
910
+ * and managed: updates replace them through the same framework copy/link
911
+ * lifecycle as rules. They intentionally live outside `skills/` so Kimi never
912
+ * attempts to discover executable support files as skills.
913
+ */
914
+ function placeKimiSkillRunner(input) {
915
+ for (const relative of KIMI_RUNNER_RELATIVE_FILES) {
916
+ copyFile(path.join(input.scriptDir, 'templates', 'kimi', 'specrails', relative), path.join(input.artifactRoot, input.providerDir, 'specrails', relative));
917
+ }
918
+ return KIMI_RUNNER_RELATIVE_FILES.length;
919
+ }
598
920
  /** Write or sentinel-upsert a project instruction file (AGENTS.md/GEMINI.md). */
599
921
  function seedInstructionFile(filePath, content) {
600
922
  if (!pathExists(filePath)) {
@@ -610,6 +932,11 @@ function copyBundledCommands(input) {
610
932
  const commandsSrc = path.join(input.scriptDir, 'commands');
611
933
  if (!isDir(commandsSrc))
612
934
  return;
935
+ // Kimi's complete workflow catalog is rendered from the canonical
936
+ // templates/commands/specrails sources in placeKimiSkills. Do not let this
937
+ // generic bundled-command pass fall through to Claude's file layout.
938
+ if (input.provider === 'kimi')
939
+ return;
613
940
  if (input.provider === 'codex') {
614
941
  // Codex: each bundled command ships as a SKILL under
615
942
  // `.codex/skills/<name>/SKILL.md`. A codex-native override (spawn_agent
@@ -627,9 +954,8 @@ function copyBundledCommands(input) {
627
954
  const destDir = path.join(input.artifactRoot, input.providerDir, 'skills', skillName);
628
955
  // A codex-native override (written for spawn_agent semantics + the
629
956
  // correct `.codex/skills/rails/` layout) wins over the claude port.
630
- // This is the ONLY codex command-placement pass in full tier, so
631
- // without the override check full-tier codex users get the claude
632
- // body — e.g. enrich's obsolete `.codex/agents/*.toml` model.
957
+ // This is the ONLY codex command-placement pass, so without the override
958
+ // check codex users get the claude body with no codex-native semantics.
633
959
  const overrideSkill = path.join(codexOverrides, skillName, 'SKILL.md');
634
960
  if (pathExists(overrideSkill)) {
635
961
  copyDir(path.join(codexOverrides, skillName), destDir);
@@ -764,6 +1090,657 @@ function writeClaudeSkillFromCommand(args) {
764
1090
  ].join('\n');
765
1091
  writeFileLf(args.dest, frontmatter + body);
766
1092
  }
1093
+ const KIMI_ROLE_EXECUTION_CONTRACT = [
1094
+ '## Kimi role execution contract',
1095
+ '',
1096
+ 'These rules override later Claude-specific `role_skill`, `isolation`, and',
1097
+ '`run_in_background` notation. When this workflow asks for one role or a',
1098
+ 'parallel group, submit exactly one foreground role wave. The managed helper',
1099
+ 'starts one external Kimi CLI per role, runs the wave concurrently, attributes',
1100
+ 'every output event, and waits for every required role. Never emulate a role',
1101
+ 'in the orchestrator and never start concurrent helper commands.',
1102
+ '',
1103
+ 'First use the structured WriteFile tool (never Shell, a heredoc, `printf`,',
1104
+ 'or `echo`) to write `.specrails/kimi-role-wave.json`. Choose one lowercase',
1105
+ 'letters/digits/hyphens run id (1–64 characters) and reuse it for the whole',
1106
+ 'workflow. The file must have exactly this shape (1–32 roles):',
1107
+ '',
1108
+ '```json',
1109
+ '{',
1110
+ ' "run": "<stable-run-id>",',
1111
+ ' "roles": [',
1112
+ ' {',
1113
+ ' "key": "<unique-role-call-id>",',
1114
+ ' "skill": "<role-skill>",',
1115
+ ' "model": "<exact profile model or k3>",',
1116
+ ' "profile": "inherit",',
1117
+ ' "args": "<complete role context>",',
1118
+ ' "workspace": "current"',
1119
+ ' }',
1120
+ ' ]',
1121
+ '}',
1122
+ '```',
1123
+ '',
1124
+ 'Every `key`, profile stem, and worktree id uses the same 1–64 character grammar as `run`.',
1125
+ 'Use `"current"` for roles that target the orchestrator repository. The',
1126
+ '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',
1128
+ 'collide. Where later instructions request `isolation: worktree`, use',
1129
+ '`"worktree:<feature-id>"`; reuse that exact value for the developer, test,',
1130
+ 'documentation, and other sequential roles belonging to the same feature.',
1131
+ 'Never put two roles for the same worktree in one wave.',
1132
+ '',
1133
+ 'Resolve each role model in the orchestrator and encode all context as JSON;',
1134
+ 'set `profile` to `inherit`, or to a validated profile filename stem for a',
1135
+ 'per-rail override under `.specrails/profiles/<stem>.json`. The helper passes',
1136
+ 'that absolute profile path only to that child process.',
1137
+ 'do not place any model or context text in a shell command. Then run this',
1138
+ 'exact static command in the foreground:',
1139
+ '',
1140
+ '```sh',
1141
+ 'node .kimi-code/specrails/run-skill.mjs \\',
1142
+ ' --role-wave-file .specrails/kimi-role-wave.json \\',
1143
+ ' --add-dir "${SPECRAILS_REPO_DIR:-.}"',
1144
+ '```',
1145
+ '',
1146
+ 'The helper accepts only that fixed, regular, non-symlink one-shot file,',
1147
+ 'bounds it to 1 MiB, validates the exact schema and every identifier, and',
1148
+ 'deletes it before creating a worktree or process. For an isolated role it',
1149
+ 'creates or reuses a detached git worktree from a synthetic baseline commit',
1150
+ 'that snapshots the starting tracked and non-ignored untracked workspace,',
1151
+ 'exposes the',
1152
+ 'managed `.kimi-code`, and sets the child repository root to that worktree.',
1153
+ 'It persists the base commit and key/path mapping under',
1154
+ '`.specrails/kimi-role-worktrees/<run>.json` and emits',
1155
+ '`specrails.role.workspace` frames. Replace every later',
1156
+ '`<worktree-path>` placeholder with that emitted `repoDir`; compare against',
1157
+ 'the manifest `baseCommit`, not a hard-coded `main` ref.',
1158
+ '',
1159
+ 'The helper frames child stdout/stderr with its role key, emits one completion',
1160
+ 'frame per role, and exits nonzero after the whole wave if any role failed.',
1161
+ 'A termination signal is forwarded to every live child. Partial failures',
1162
+ 'leave completed worktrees and the manifest available for retry; newly',
1163
+ 'created partial worktrees are removed only when setup itself fails. Apply',
1164
+ 'the workflow cleanup step after merge using the exact static command',
1165
+ '`node .kimi-code/specrails/run-skill.mjs --role-wave-cleanup <run>`.',
1166
+ 'Cleanup removes registered worktrees, execution state, the manifest, and',
1167
+ 'the private synthetic-baseline ref; never clean up a failed run before retry.',
1168
+ 'The helper normalizes only the three',
1169
+ 'official short model ids (`k3` launches as `kimi-code/k3`); safe custom',
1170
+ 'aliases pass through unchanged.',
1171
+ 'There is no SpecRails-owned Kimi server or bundled Kimi binary.',
1172
+ '',
1173
+ ].join('\n');
1174
+ const KIMI_NESTED_SKILL_CONTRACT = [
1175
+ '## Kimi nested skill activation',
1176
+ '',
1177
+ 'When these instructions show `Skill(skill="<id>", args="<raw args>")`, call',
1178
+ 'Kimi\'s built-in `Skill` tool with those `skill` and `args` fields. Do not',
1179
+ 'print the notation as prose and do not send an interactive slash command as',
1180
+ 'model text. This native tool path preserves Kimi\'s nested-skill behavior and',
1181
+ 'skill activation telemetry.',
1182
+ '',
1183
+ ].join('\n');
1184
+ const KIMI_RUNTIME_CONTEXT_CONTRACT = [
1185
+ '## Kimi runtime context contract',
1186
+ '',
1187
+ 'SpecRails deliberately resolves project-specific context at activation time;',
1188
+ 'the Kimi enrich workflow does not rewrite framework-owned role or workflow',
1189
+ 'SKILL.md files. Resolve every `KIMI_RUNTIME_*`, `KIMI_BACKLOG_*`, and',
1190
+ '`KIMI_PR_CREATE` marker below before acting. These are semantic markers,',
1191
+ 'never executable command names; do not pass them to Shell.',
1192
+ '',
1193
+ 'Use `${SPECRAILS_REPO_DIR}` when set as the code repository, otherwise the',
1194
+ 'current repository. Read `.kimi-code/project-context.md` for stack, layers,',
1195
+ 'CI commands, conventions, warnings, architecture, and important paths; when',
1196
+ 'a field is absent, inspect package/build/CI files and report the inferred',
1197
+ 'value explicitly. Scan `.kimi-code/personas/*.md` at runtime and read only',
1198
+ 'regular non-symlink files; derive persona names, roles, score columns, and',
1199
+ 'VPC sections from that live inventory. An empty inventory means “no personas',
1200
+ 'configured”, never fabricated rows or scores.',
1201
+ '',
1202
+ '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',
1205
+ 'approved `gh issue` operation; route `jira` only through the configured',
1206
+ 'project/base URL and credentials. Honour read-only mode and never perform a',
1207
+ 'write operation when configuration is missing, invalid, or read-only.',
1208
+ 'Arguments shown after a marker describe the operation; they are not shell',
1209
+ 'argv. Resolve `KIMI_PR_CREATE` with the repository’s configured PR workflow',
1210
+ 'and ask before publishing when the active workflow requires confirmation.',
1211
+ '',
1212
+ ].join('\n');
1213
+ const KIMI_RUNTIME_PLACEHOLDERS = {
1214
+ ARCHITECTURE_DIAGRAM: 'KIMI_RUNTIME_ARCHITECTURE_DIAGRAM',
1215
+ AREA_TABLE: 'KIMI_RUNTIME_AREA_TABLE',
1216
+ BACKEND_ARCHITECTURE_DIAGRAM: 'KIMI_RUNTIME_BACKEND_ARCHITECTURE_DIAGRAM',
1217
+ BACKEND_CRITICAL_RULES: 'KIMI_RUNTIME_BACKEND_CRITICAL_RULES',
1218
+ BACKEND_EXPERTISE: 'KIMI_RUNTIME_BACKEND_EXPERTISE',
1219
+ BACKEND_LAYER_CONVENTIONS: 'KIMI_RUNTIME_BACKEND_LAYER_CONVENTIONS',
1220
+ BACKEND_STACK: 'KIMI_RUNTIME_BACKEND_STACK',
1221
+ BACKEND_TECH_LIST: 'KIMI_RUNTIME_BACKEND_TECH_LIST',
1222
+ BACKLOG_COMMENT_CMD: 'KIMI_BACKLOG_COMMENT',
1223
+ BACKLOG_CREATE_CMD: 'KIMI_BACKLOG_CREATE',
1224
+ BACKLOG_DELETE_CMD: 'KIMI_BACKLOG_DELETE',
1225
+ BACKLOG_FETCH_ALL_CMD: 'KIMI_BACKLOG_FETCH_ALL',
1226
+ BACKLOG_FETCH_CLOSED_CMD: 'KIMI_BACKLOG_FETCH_CLOSED',
1227
+ BACKLOG_FETCH_CMD: 'KIMI_BACKLOG_FETCH',
1228
+ BACKLOG_INIT_LABELS_CMD: 'KIMI_BACKLOG_INIT_LABELS',
1229
+ BACKLOG_PARTIAL_COMMENT_CMD: 'KIMI_BACKLOG_PARTIAL_COMMENT',
1230
+ BACKLOG_PREFLIGHT: 'KIMI_BACKLOG_PREFLIGHT',
1231
+ BACKLOG_PROVIDER_NAME: 'KIMI_RUNTIME_BACKLOG_PROVIDER_NAME',
1232
+ BACKLOG_UPDATE_CMD: 'KIMI_BACKLOG_UPDATE',
1233
+ BACKLOG_VIEW_CMD: 'KIMI_BACKLOG_VIEW',
1234
+ CI_CHECK_TABLE_ROWS: 'KIMI_RUNTIME_CI_CHECK_TABLE_ROWS',
1235
+ CI_COMMANDS: 'KIMI_RUNTIME_CI_COMMANDS',
1236
+ CI_COMMANDS_BACKEND: 'KIMI_RUNTIME_CI_COMMANDS_BACKEND',
1237
+ CI_COMMANDS_FRONTEND: 'KIMI_RUNTIME_CI_COMMANDS_FRONTEND',
1238
+ CI_COMMANDS_FULL: 'KIMI_RUNTIME_CI_COMMANDS_FULL',
1239
+ CI_COMMON_PITFALLS: 'KIMI_RUNTIME_CI_COMMON_PITFALLS',
1240
+ CI_CRITICAL_WARNINGS: 'KIMI_RUNTIME_CI_CRITICAL_WARNINGS',
1241
+ CI_KNOWN_GAPS: 'KIMI_RUNTIME_CI_KNOWN_GAPS',
1242
+ CODE_QUALITY_CHECKLIST: 'KIMI_RUNTIME_CODE_QUALITY_CHECKLIST',
1243
+ CODE_QUALITY_STANDARDS: 'KIMI_RUNTIME_CODE_QUALITY_STANDARDS',
1244
+ COMPETITIVE_LANDSCAPE: 'KIMI_RUNTIME_COMPETITIVE_LANDSCAPE',
1245
+ DEPENDENCY_CHECK_COMMANDS: 'KIMI_RUNTIME_DEPENDENCY_CHECK_COMMANDS',
1246
+ DOMAIN_EXPERTISE: 'KIMI_RUNTIME_DOMAIN_EXPERTISE',
1247
+ DOMAIN_KNOWLEDGE: 'KIMI_RUNTIME_DOMAIN_KNOWLEDGE',
1248
+ FRONTEND_ARCHITECTURE_DIAGRAM: 'KIMI_RUNTIME_FRONTEND_ARCHITECTURE_DIAGRAM',
1249
+ FRONTEND_CRITICAL_RULES: 'KIMI_RUNTIME_FRONTEND_CRITICAL_RULES',
1250
+ FRONTEND_EXPERTISE: 'KIMI_RUNTIME_FRONTEND_EXPERTISE',
1251
+ FRONTEND_LAYER_CONVENTIONS: 'KIMI_RUNTIME_FRONTEND_LAYER_CONVENTIONS',
1252
+ FRONTEND_STACK: 'KIMI_RUNTIME_FRONTEND_STACK',
1253
+ FRONTEND_TECH_LIST: 'KIMI_RUNTIME_FRONTEND_TECH_LIST',
1254
+ GIT_ACCESS: 'KIMI_RUNTIME_GIT_ACCESS',
1255
+ JIRA_BASE_URL: 'KIMI_RUNTIME_JIRA_BASE_URL',
1256
+ JIRA_PROJECT_KEY: 'KIMI_RUNTIME_JIRA_PROJECT_KEY',
1257
+ KEY_FILE_PATHS: 'KIMI_RUNTIME_KEY_FILE_PATHS',
1258
+ LAYER_CLAUDE_MD_PATHS: 'KIMI_RUNTIME_LAYER_CONTEXT_PATHS',
1259
+ LAYER_CONVENTIONS: 'KIMI_RUNTIME_LAYER_CONVENTIONS',
1260
+ LAYER_LIST: 'KIMI_RUNTIME_LAYER_LIST',
1261
+ LAYER_NAME: 'KIMI_RUNTIME_LAYER_NAME',
1262
+ LAYER_PATH: 'KIMI_RUNTIME_LAYER_PATH',
1263
+ LAYER_TAGS: 'KIMI_RUNTIME_LAYER_TAGS',
1264
+ MAINTAINER_PERSONA_LINE: 'KIMI_RUNTIME_MAINTAINER_PERSONA_LINE',
1265
+ MAX_SCORE: 'KIMI_RUNTIME_PERSONA_MAX_SCORE',
1266
+ PERSONA_COUNT: 'KIMI_RUNTIME_PERSONA_COUNT',
1267
+ PERSONA_FILES: 'KIMI_RUNTIME_PERSONA_FILES',
1268
+ PERSONA_FILE_LIST: 'KIMI_RUNTIME_PERSONA_FILE_LIST',
1269
+ PERSONA_FILE_READ_LIST: 'KIMI_RUNTIME_PERSONA_FILE_READ_LIST',
1270
+ PERSONA_FIT_FORMAT: 'KIMI_RUNTIME_PERSONA_FIT_FORMAT',
1271
+ PERSONA_NAMES: 'KIMI_RUNTIME_PERSONA_NAMES',
1272
+ PERSONA_NAMES_WITH_ROLES: 'KIMI_RUNTIME_PERSONA_NAMES_WITH_ROLES',
1273
+ PERSONA_SCORE_FORMAT: 'KIMI_RUNTIME_PERSONA_SCORE_FORMAT',
1274
+ PERSONA_SCORE_HEADERS: 'KIMI_RUNTIME_PERSONA_SCORE_HEADERS',
1275
+ PERSONA_SCORE_SEPARATORS: 'KIMI_RUNTIME_PERSONA_SCORE_SEPARATORS',
1276
+ PERSONA_VPC_SECTIONS: 'KIMI_RUNTIME_PERSONA_VPC_SECTIONS',
1277
+ PR_CREATE_CMD: 'KIMI_PR_CREATE',
1278
+ PROJECT_CONTEXT: 'KIMI_RUNTIME_PROJECT_CONTEXT',
1279
+ TECH_EXPERTISE: 'KIMI_RUNTIME_TECH_EXPERTISE',
1280
+ TEST_QUALITY_CHECKLIST: 'KIMI_RUNTIME_TEST_QUALITY_CHECKLIST',
1281
+ TEST_RUNNER_CHECK: 'KIMI_RUNTIME_TEST_RUNNER_CHECK',
1282
+ WARNINGS: 'KIMI_RUNTIME_WARNINGS',
1283
+ };
1284
+ function writeKimiWorkflowSkill(args) {
1285
+ if (!pathExists(args.src))
1286
+ return;
1287
+ const { body, description } = stripFrontmatter(readTextFile(args.src));
1288
+ const skillName = `specrails-${args.commandName}`;
1289
+ const providerNeutral = renderPlaceholders(body, {
1290
+ ...KIMI_RUNTIME_PLACEHOLDERS,
1291
+ ...args.placeholders,
1292
+ MEMORY_PATH: '.kimi-code/agent-memory/',
1293
+ }).replaceAll('.specrails/profiles/project-default.json', '.specrails/profiles/kimi-default.json');
1294
+ const rendered = translateClaudeTextForKimi(adaptKimiWorkflowBody(args.commandName, providerNeutral));
1295
+ const frontmatter = [
1296
+ '---',
1297
+ `name: ${skillName}`,
1298
+ `description: ${JSON.stringify(translateClaudeTextForKimi(renderPlaceholders(description ?? `SpecRails ${args.commandName} workflow for Kimi Code.`, {
1299
+ ...KIMI_RUNTIME_PLACEHOLDERS,
1300
+ ...args.placeholders,
1301
+ })))}`,
1302
+ 'type: prompt',
1303
+ '---',
1304
+ '',
1305
+ ].join('\n');
1306
+ writeFileLf(args.dest, frontmatter +
1307
+ KIMI_NESTED_SKILL_CONTRACT +
1308
+ KIMI_ROLE_EXECUTION_CONTRACT +
1309
+ KIMI_RUNTIME_CONTEXT_CONTRACT +
1310
+ rendered);
1311
+ }
1312
+ function adaptKimiWorkflowBody(commandName, body) {
1313
+ if (commandName === 'batch-implement') {
1314
+ return adaptKimiBatchImplement(body);
1315
+ }
1316
+ if (commandName === 'auto-propose-backlog-specs') {
1317
+ return adaptKimiAutoPropose(body);
1318
+ }
1319
+ if (commandName === 'enrich') {
1320
+ return renderKimiEnrichWorkflow();
1321
+ }
1322
+ if (commandName === 'reconfig') {
1323
+ return renderKimiReconfigWorkflow();
1324
+ }
1325
+ if (commandName === 'telemetry') {
1326
+ return renderKimiTelemetryWorkflow();
1327
+ }
1328
+ if (commandName === 'retry') {
1329
+ return adaptKimiRetry(body);
1330
+ }
1331
+ if (commandName !== 'implement')
1332
+ 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
+ '```',
1357
+ '',
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.',
1365
+ '',
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.',
1420
+ '',
1421
+ ].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
+ }
1449
+ 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'));
1491
+ }
1492
+ function adaptKimiAutoPropose(body) {
1493
+ return body
1494
+ .replace('Launch a **single** explorer subagent (`subagent_type: Explore`, `run_in_background: true`) for product discovery.', [
1495
+ 'Launch one **sr-product-analyst** role in a foreground Kimi role wave.',
1496
+ 'Use `skill:"sr-product-analyst"`, `workspace:"current"`,',
1497
+ '`profile:"inherit"`, and pass the complete discovery prompt below as',
1498
+ '`args`. Before writing the wave, enumerate regular non-symlink',
1499
+ '`.kimi-code/personas/*.md` files and include their exact paths plus',
1500
+ 'contents in that context; stop with guidance to run',
1501
+ '`Skill(skill="specrails-enrich", args="")` when none exist. Wait for its attributed',
1502
+ 'completion/output frames. Kimi has no Claude Explore subagent type;',
1503
+ 'never select a non-existent Explore role.',
1504
+ ].join(' '))
1505
+ .replaceAll('The Explore agent receives this prompt:', 'The sr-product-analyst role receives this prompt:')
1506
+ .replaceAll('After the Explore agent completes:', 'After the sr-product-analyst role completes:');
1507
+ }
1508
+ 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):**',
1518
+ '',
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.',
1529
+ '',
1530
+ '**Validation:**',
1531
+ ].join('\n'))
1532
+ .replace('Include PR URL if ship ran successfully.', [
1533
+ 'Include PR URL if ship ran successfully.',
1534
+ '',
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'));
1539
+ }
1540
+ function renderKimiEnrichWorkflow() {
1541
+ return [
1542
+ '# Enrich SpecRails for Kimi Code',
1543
+ '',
1544
+ 'Refresh the Kimi-native SpecRails installation, analyze this repository,',
1545
+ 'and maintain project context/personas without generating Claude artifacts.',
1546
+ 'Kimi skills are managed provider artifacts; never rewrite their SKILL.md',
1547
+ 'frontmatter or create Claude-style command/agent trees.',
1548
+ '',
1549
+ '## Mode selection',
1550
+ '',
1551
+ 'Parse `$ARGUMENTS`: `--update`, `--quick`, and `--from-config` are mutually',
1552
+ 'exclusive. With no flag run interactive full mode.',
1553
+ '',
1554
+ '1. Resolve the repository as `${SPECRAILS_REPO_DIR:-.}` and verify',
1555
+ ' `.kimi-code/specrails/run-skill.mjs` plus',
1556
+ ' `.specrails/install-config.yaml` exist.',
1557
+ '2. Read install config and require provider `kimi` (or an explicitly',
1558
+ ' provider-neutral legacy config). Refuse a different provider.',
1559
+ '3. Refresh managed provider artifacts with the installed Core CLI:',
1560
+ ' `npx specrails-core update --provider kimi --root-dir "${SPECRAILS_REPO_DIR:-.}"`.',
1561
+ ' Use the process result as a hard gate. This provider-aware materializer',
1562
+ ' regenerates direct-child workflows/roles, rules, runner, OpenSpec skills,',
1563
+ ' settings, manifest, and framework links. Do not reproduce its templates',
1564
+ ' with model-authored file copying.',
1565
+ '4. Validate `.specrails/profiles/kimi-default.json`: schemaVersion 1,',
1566
+ ' provider `kimi`, required architect/developer/reviewer, unique role ids,',
1567
+ ' safe model ids, and valid routing. Preserve exact model identifiers.',
1568
+ '',
1569
+ '## Quick mode',
1570
+ '',
1571
+ 'Inspect package/build metadata and the top-level source tree. Atomically',
1572
+ 'write `.kimi-code/project-context.md` with stack, architecture, test/lint/',
1573
+ 'build commands, repository conventions, and the UTC refresh time. Preserve',
1574
+ 'existing `.kimi-code/personas/`. Report that full mode can add VPC personas.',
1575
+ '',
1576
+ '## From-config mode',
1577
+ '',
1578
+ 'Do not ask questions. Apply the tier, selected agents, backlog, git, and',
1579
+ 'model choices already present in install config/profile; the Core update is',
1580
+ 'the only artifact-generation authority. For quick tier run Quick mode. For',
1581
+ 'full tier perform the same repository analysis as Full mode, retain existing',
1582
+ 'personas when present, and generate conservative personas only when the',
1583
+ 'config enables product roles and the persona directory is empty.',
1584
+ '',
1585
+ '## Update mode',
1586
+ '',
1587
+ 'Run the provider-aware refresh, re-analyze commands/conventions, and',
1588
+ 'atomically refresh only `.kimi-code/project-context.md`. Keep user personas,',
1589
+ 'custom-* skills, agent memory, profiles, MCP configuration, and security',
1590
+ 'exemptions byte-for-byte. Report stale/missing persona references but do not',
1591
+ 'invent replacements.',
1592
+ '',
1593
+ '## Full mode',
1594
+ '',
1595
+ 'Analyze the complete codebase and present findings. Ask concise questions',
1596
+ 'about target users, pains, gains, product goals, and repository shipping',
1597
+ 'policy. Research externally only with user-approved network tooling.',
1598
+ 'Generate 2–4 Value Proposition Canvas personas as real Markdown files under',
1599
+ '`.kimi-code/personas/<safe-kebab-id>.md`; include jobs, pains, gains,',
1600
+ 'behavior, success criteria, and evidence/assumptions. On OSS projects also',
1601
+ 'materialize the bundled maintainer persona from setup templates. Never',
1602
+ 'overwrite an existing persona without showing the proposed change.',
1603
+ '',
1604
+ 'Refresh `.kimi-code/project-context.md` and ensure every selected product',
1605
+ 'role has an agent-memory directory. Workflows discover persona files at',
1606
+ 'runtime, so do not fork or mutate framework-owned skills to embed a static',
1607
+ 'persona list.',
1608
+ '',
1609
+ '## Verification and report',
1610
+ '',
1611
+ 'Run `npx specrails-core doctor --provider kimi --root-dir',
1612
+ '"${SPECRAILS_REPO_DIR:-.}"`. Verify every immediate skill directory has one',
1613
+ 'valid SKILL.md, no role is nested under `skills/rails`, no unresolved',
1614
+ 'template token remains, the Kimi profile validates, and no Claude model',
1615
+ 'alias/path was generated. Report mode, provider version, context/persona',
1616
+ 'files, selected roles, exact models, and doctor result.',
1617
+ '',
1618
+ ].join('\n');
1619
+ }
1620
+ function renderKimiReconfigWorkflow() {
1621
+ return [
1622
+ '# Reconfig: apply Kimi models to a provider profile',
1623
+ '',
1624
+ 'Kimi role skills do not carry per-role model frontmatter. Reconfiguration',
1625
+ 'updates a provider-bound profile; workflows read that profile and put each',
1626
+ 'exact model id in the structured role wave.',
1627
+ '',
1628
+ '1. Parse `$ARGUMENTS` as optional `--profile <safe-name>`; default to the',
1629
+ ' active `SPECRAILS_PROFILE_PATH`, then',
1630
+ ' `.specrails/profiles/kimi-default.json`.',
1631
+ '2. Require a regular non-symlink JSON file inside `.specrails/profiles/`.',
1632
+ ' Validate it against profile schema v1, require `provider:"kimi"`, unique',
1633
+ ' agents, the baseline trio, valid routing references, and Kimi-safe model',
1634
+ ' ids (`^[A-Za-z0-9][A-Za-z0-9._/:-]*$`, maximum 128 characters).',
1635
+ '3. Read `.specrails/agents.yaml` only as an optional legacy input. Ask for',
1636
+ ' confirmation before migrating its defaults/per-agent model values. Claude',
1637
+ ' aliases `opus`, `sonnet`, and `haiku` are not Kimi models and must never',
1638
+ ' be translated silently; require an explicit Kimi replacement.',
1639
+ '4. Present the orchestrator and per-role old→new model table. Apply approved',
1640
+ ' edits to a complete in-memory profile object, validate again, then use',
1641
+ ' structured WriteFile for one atomic logical replacement. Preserve name,',
1642
+ ' description, required flags, agent order, and routing.',
1643
+ '5. Re-read and validate the result. Report changed/unchanged/skipped roles.',
1644
+ '',
1645
+ 'Never edit any role SKILL.md under `.kimi-code/skills/`, never create Claude agent files,',
1646
+ 'and never put a model id in a shell command.',
1647
+ '',
1648
+ ].join('\n');
1649
+ }
1650
+ function renderKimiTelemetryWorkflow() {
1651
+ return [
1652
+ '# Kimi agent telemetry',
1653
+ '',
1654
+ 'Analyze real Kimi Code session usage for this repository. Accepted flags:',
1655
+ '`--period today|week|all` (default `week`), `--agent <id>`,',
1656
+ '`--format markdown|json`, and `--save`. Cost is not derivable from Kimi logs and must remain',
1657
+ '`null`/`unavailable`; never apply a Claude or invented rate card.',
1658
+ '',
1659
+ '## Discover and validate sessions',
1660
+ '',
1661
+ 'Read `~/.kimi-code/session_index.jsonl` line by line. Ignore only a final',
1662
+ 'truncated JSON line; warn on any other malformed line. Fold records by',
1663
+ '`sessionId`, keeping the latest valid entry and honoring an explicit latest',
1664
+ 'deletion/tombstone record. A live entry provides `sessionId`, `sessionDir`,',
1665
+ 'and `workDir`. Require safe scalar strings, match canonical `workDir` to',
1666
+ '`${SPECRAILS_REPO_DIR:-.}`, and require canonical `sessionDir` to remain',
1667
+ 'inside `~/.kimi-code/sessions/`. Reject symlinks/path traversal and ignore',
1668
+ 'missing or deleted session directories.',
1669
+ '',
1670
+ 'For each accepted session, read its regular non-symlink `state.json` and',
1671
+ 'validate its `workDir`, timestamps, and title. Then scan only regular',
1672
+ '`agents/*/wire.jsonl` files within that same session directory. Tolerate a',
1673
+ 'truncated final line; count and warn on other malformed records.',
1674
+ '',
1675
+ '## Usage schema and attribution',
1676
+ '',
1677
+ 'Consume only records with `type:"usage.record"` and a safe `model` plus',
1678
+ '`usage:{inputOther,output,inputCacheRead,inputCacheCreation}`. Treat absent',
1679
+ 'numeric counters as zero; reject negative/non-finite values. Preserve',
1680
+ '`usageScope` and aggregate input-other, output, cache-read, cache-creation,',
1681
+ 'and total tokens per model/session.',
1682
+ '',
1683
+ 'Attribute an external role session by canonical workDir: first use its',
1684
+ '`.specrails-role-workspace.json` marker; otherwise match `repoDir` in valid',
1685
+ '`.specrails/kimi-role-worktrees/*.json` manifests. Attribute the top-level',
1686
+ 'session to `orchestrator`; use `unknown` only when no verified mapping',
1687
+ 'exists. Apply period and agent filters after attribution. Deduplicate by',
1688
+ 'session id + agent wire path + record position.',
1689
+ '',
1690
+ 'Duration comes only from validated state timestamps. The wire schema does',
1691
+ 'not provide a trustworthy role success/failure outcome, so expose that',
1692
+ 'metric as unavailable rather than inferring it from the last event.',
1693
+ '',
1694
+ '## Output',
1695
+ '',
1696
+ 'Show session/run count, duration, exact models, and all four token counters',
1697
+ 'per role plus totals and cache ratio. In JSON use',
1698
+ '`cost_usd:null`, `avg_cost_per_run_usd:null`, and',
1699
+ '`success_rate:null` with `unavailable_reason`. In Markdown print',
1700
+ '`Cost: unavailable (Kimi logs contain usage, not billing rates)`.',
1701
+ 'Recommendations may discuss token/cache/runtime outliers only.',
1702
+ '',
1703
+ 'With `--save`, write the same JSON object under',
1704
+ '`.kimi-code/telemetry/<UTC-date>-<period>.json` using structured WriteFile;',
1705
+ 'never include prompt text, credentials, raw wire records, or paths outside',
1706
+ 'the repository/session identifiers.',
1707
+ '',
1708
+ ].join('\n');
1709
+ }
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
+ function writeKimiRoleSkill(args) {
1720
+ if (!pathExists(args.src))
1721
+ return;
1722
+ const { body, description } = stripFrontmatter(readTextFile(args.src));
1723
+ const rendered = translateClaudeTextForKimi(renderPlaceholders(body, {
1724
+ ...KIMI_RUNTIME_PLACEHOLDERS,
1725
+ ...args.placeholders,
1726
+ MEMORY_PATH: `.kimi-code/agent-memory/${args.roleId}/`,
1727
+ }));
1728
+ const frontmatter = [
1729
+ '---',
1730
+ `name: ${args.roleId}`,
1731
+ `description: ${JSON.stringify(translateClaudeTextForKimi(renderPlaceholders(description ?? `SpecRails ${args.roleId} role for Kimi Code.`, {
1732
+ ...KIMI_RUNTIME_PLACEHOLDERS,
1733
+ ...args.placeholders,
1734
+ })))}`,
1735
+ 'type: prompt',
1736
+ '---',
1737
+ '',
1738
+ ].join('\n');
1739
+ writeFileLf(args.dest, frontmatter +
1740
+ KIMI_NESTED_SKILL_CONTRACT +
1741
+ KIMI_RUNTIME_CONTEXT_CONTRACT +
1742
+ rendered);
1743
+ }
767
1744
  /**
768
1745
  * Strip a leading `---`-delimited YAML frontmatter block and return the
769
1746
  * remaining body plus the `description:` value if present. Defensive
@@ -860,7 +1837,6 @@ function placeGeminiAgents(input) {
860
1837
  const placeholders = {
861
1838
  PROJECT_NAME: path.basename(input.codeRoot),
862
1839
  SECURITY_EXEMPTIONS_PATH: '.gemini/security-exemptions.yaml',
863
- PERSONA_DIR: '.gemini/agents/personas/',
864
1840
  };
865
1841
  const placedIds = [];
866
1842
  for (const src of listDir(agentsSrc)) {
@@ -872,10 +1848,6 @@ function placeGeminiAgents(input) {
872
1848
  // filtering happens at the workspace LINK step (linkAgentFiles).
873
1849
  if (!input.materializeAllAgents && !selectedAgents.has(agentId))
874
1850
  continue;
875
- if (!input.materializeAllAgents && QUICK_EXCLUDED_AGENTS.has(agentId)) {
876
- result.skipped++;
877
- continue;
878
- }
879
1851
  writeGeminiAgentFromTemplate({
880
1852
  artifactRoot: input.artifactRoot,
881
1853
  src,
@@ -1014,6 +1986,66 @@ function applyGeminiSettings(input) {
1014
1986
  }
1015
1987
  return written;
1016
1988
  }
1989
+ /**
1990
+ * Kimi keeps both its project instructions and MCP configuration under
1991
+ * `.kimi-code`. Existing user MCP configuration is never rewritten.
1992
+ */
1993
+ function applyKimiSettings(input) {
1994
+ let written = 0;
1995
+ const providerRoot = path.join(input.artifactRoot, input.providerDir);
1996
+ const agentsMdPath = path.join(providerRoot, 'AGENTS.md');
1997
+ const content = renderInitialKimiAgentsMd(input.codeRoot);
1998
+ if (!pathExists(agentsMdPath)) {
1999
+ writeFileLf(agentsMdPath, content);
2000
+ written++;
2001
+ }
2002
+ else {
2003
+ const existing = readTextFile(agentsMdPath);
2004
+ const next = upsertAgentsMdManagedBlock(existing, extractManagedBlock(content));
2005
+ if (next !== existing) {
2006
+ writeFileLf(agentsMdPath, next);
2007
+ written++;
2008
+ }
2009
+ }
2010
+ const mcpPath = path.join(providerRoot, 'mcp.json');
2011
+ if (!pathExists(mcpPath)) {
2012
+ writeFileLf(mcpPath, '{\n "mcpServers": {}\n}\n');
2013
+ written++;
2014
+ }
2015
+ return written;
2016
+ }
2017
+ function renderInitialKimiAgentsMd(repoRoot) {
2018
+ const projectName = path.basename(repoRoot);
2019
+ return [
2020
+ AGENTS_MD_START,
2021
+ '',
2022
+ `# ${projectName} — Kimi Code instructions`,
2023
+ '',
2024
+ 'This project uses SpecRails skills under `.kimi-code/skills/`.',
2025
+ 'Kimi discovers only direct child skill directories. Interactive TUI sessions',
2026
+ 'invoke workflows as `/skill:specrails-<command>`. Headless prompt mode does',
2027
+ 'not dispatch slash skills, so automation must invoke',
2028
+ '`.kimi-code/specrails/run-skill.mjs`. Role skills live at',
2029
+ '`.kimi-code/skills/<sr-*|custom-*>/SKILL.md` and are launched by workflows in',
2030
+ 'separate helper-managed `kimi -p --output-format stream-json` processes.',
2031
+ '',
2032
+ '## Conventions',
2033
+ '',
2034
+ '- Read project source, `.git`, and `openspec/**` from',
2035
+ ' `${SPECRAILS_REPO_DIR:-.}`.',
2036
+ '- Read provider rules from `.kimi-code/rules/` and runtime memory from',
2037
+ ' `.kimi-code/agent-memory/`.',
2038
+ '- Preserve model ids from provider-aware profiles exactly. The Kimi CLI',
2039
+ ' accepts configured aliases; official short ids use the `kimi-code/`',
2040
+ ' prefix at launch (for example `kimi-code/k3`).',
2041
+ '- OpenSpec workflows are invoked as `/skill:openspec-*`.',
2042
+ '- Kimi is CLI-only: do not start a server, register a service, or copy',
2043
+ ' credentials into this project.',
2044
+ '',
2045
+ AGENTS_MD_END,
2046
+ '',
2047
+ ].join('\n');
2048
+ }
1017
2049
  function renderInitialGeminiMd(repoRoot) {
1018
2050
  const projectName = path.basename(repoRoot);
1019
2051
  return [
@@ -1057,6 +2089,17 @@ function pruneLegacyArtifacts(input) {
1057
2089
  legacyPaths.push(path.join(input.artifactRoot, input.providerDir, 'commands', 'setup.toml'));
1058
2090
  legacyPaths.push(path.join(input.artifactRoot, input.providerDir, 'commands', 'specrails', 'setup.toml'));
1059
2091
  }
2092
+ else if (input.provider === 'kimi') {
2093
+ // Early experimental builds used a Claude-shaped commands/agents layout
2094
+ // inside `.kimi-code`. They also nested role skills one level too deep at
2095
+ // `skills/rails/*`, which Kimi never discovers. Migrate reserved custom
2096
+ // roles and prune only framework-owned nested roles before rendering the
2097
+ // canonical flat layout. MCP config, AGENTS.md and unknown user files stay
2098
+ // untouched.
2099
+ migrateLegacyKimiRoleLayout(path.join(input.artifactRoot, input.providerDir, 'skills'));
2100
+ legacyPaths.push(path.join(input.artifactRoot, input.providerDir, 'commands'));
2101
+ legacyPaths.push(path.join(input.artifactRoot, input.providerDir, 'agents'));
2102
+ }
1060
2103
  else {
1061
2104
  legacyPaths.push(path.join(input.artifactRoot, input.providerDir, 'commands', 'setup.md'));
1062
2105
  legacyPaths.push(path.join(input.artifactRoot, input.providerDir, 'commands', 'specrails', 'setup.md'));
@@ -1081,20 +2124,19 @@ function pruneLegacyArtifacts(input) {
1081
2124
  }
1082
2125
  }
1083
2126
  /**
1084
- * Quick-tier placement: copy agents / commands / rules from the
2127
+ * Direct placement: copy agents / commands / rules from the
1085
2128
  * .specrails/setup-templates/ staging directory into the live
1086
- * provider directory, substituting template placeholders and
1087
- * excluding agents + commands whose dependencies are not present.
2129
+ * provider directory, substituting template placeholders. This is the
2130
+ * only placement path — there are no install tiers.
1088
2131
  *
1089
2132
  * Source is setup-templates/ (not scriptDir/templates/) so the pipeline
1090
2133
  * is: scriptDir/templates/ → setup-templates/ (earlier scaffold step)
1091
- * → <providerDir>/ (this function). The intermediate hop mirrors the
1092
- * retired bash installer and lets downstream consumers (specrails-desktop's
1093
- * deployTemplates, /specrails:enrich, update flow) read from a single
1094
- * canonical staging dir.
2134
+ * → <providerDir>/ (this function). The intermediate hop lets downstream
2135
+ * consumers (specrails-desktop's deployTemplates, update flow) read from a
2136
+ * single canonical staging dir.
1095
2137
  */
1096
- function placeQuickTierArtefacts(input) {
1097
- // Codex projects: the quick-tier `agents/` + `rules/` placement is
2138
+ function placeArtefacts(input) {
2139
+ // Codex projects: the `agents/` + `rules/` placement is
1098
2140
  // skipped (handled by `placeSkills` rail-skills + `applyCodexSettings`).
1099
2141
  // The slash-command catalogue under `setup-templates/commands/specrails/`
1100
2142
  // IS ported, but to `.codex/skills/<name>/SKILL.md` instead of
@@ -1171,6 +2213,29 @@ function placeQuickTierArtefacts(input) {
1171
2213
  }
1172
2214
  return { agents: 0, commands: commandsPlaced, rules: 0, skippedAgents: 0 };
1173
2215
  }
2216
+ if (input.provider === 'kimi') {
2217
+ const setupTemplates = path.join(input.artifactRoot, '.specrails', 'setup-templates');
2218
+ const rulesSrc = path.join(setupTemplates, 'rules');
2219
+ const rulesDest = path.join(input.artifactRoot, input.providerDir, 'rules');
2220
+ let rulesPlaced = 0;
2221
+ if (isDir(rulesSrc)) {
2222
+ mkdirp(rulesDest);
2223
+ for (const src of listDir(rulesSrc)) {
2224
+ const name = path.basename(src);
2225
+ if (!name.endsWith('.md'))
2226
+ continue;
2227
+ const rendered = translateClaudeTextForKimi(renderPlaceholders(readTextFile(src), {
2228
+ ...KIMI_RUNTIME_PLACEHOLDERS,
2229
+ PROJECT_NAME: path.basename(input.codeRoot),
2230
+ SECURITY_EXEMPTIONS_PATH: '.kimi-code/security-exemptions.yaml',
2231
+ PERSONA_DIR: '.kimi-code/personas/',
2232
+ }));
2233
+ writeFileLf(path.join(rulesDest, name), rendered);
2234
+ rulesPlaced++;
2235
+ }
2236
+ }
2237
+ return { agents: 0, commands: 0, rules: rulesPlaced, skippedAgents: 0 };
2238
+ }
1174
2239
  const setupTemplates = path.join(input.artifactRoot, '.specrails', 'setup-templates');
1175
2240
  // PROJECT_NAME is the real repo's basename, not the relocated workspace dir.
1176
2241
  const projectName = path.basename(input.codeRoot);
@@ -1178,18 +2243,16 @@ function placeQuickTierArtefacts(input) {
1178
2243
  const placeholders = {
1179
2244
  PROJECT_NAME: projectName,
1180
2245
  SECURITY_EXEMPTIONS_PATH: `${input.providerDir}/security-exemptions.yaml`,
1181
- PERSONA_DIR: `${input.providerDir}/agents/personas/`,
1182
2246
  };
1183
2247
  // --- Agents ---
1184
2248
  const agentsSrc = path.join(setupTemplates, 'agents');
1185
2249
  const agentsDest = path.join(providerDirAbs, 'agents');
1186
2250
  let agentsPlaced = 0;
1187
- let agentsSkipped = 0;
1188
- const installedAgentNames = new Set();
1189
- // When no agent selection is provided (fresh init with no install-config),
1190
- // default to placing only the three core agents. This keeps the default
1191
- // install lean — optional agents (sr-merge-resolver, layer specialists,
1192
- // product agents) are explicitly opt-in via the TUI or install-config.
2251
+ const agentsSkipped = 0;
2252
+ // The only shipped agents are the three core agents. A profile-driven
2253
+ // install may pass a selection; anything outside CORE_AGENTS has no template
2254
+ // to place, so the intersection is always the core trio (extension happens
2255
+ // via user-owned custom-*.md agents, never through the installer).
1193
2256
  const selectedAgents = input.selectedAgents
1194
2257
  ? new Set([...input.selectedAgents, ...CORE_AGENTS])
1195
2258
  : new Set([...CORE_AGENTS]);
@@ -1205,10 +2268,6 @@ function placeQuickTierArtefacts(input) {
1205
2268
  // filtering happens at the workspace LINK step, not here.
1206
2269
  if (!input.materializeAllAgents && selectedAgents && !selectedAgents.has(agentId))
1207
2270
  continue;
1208
- if (!input.materializeAllAgents && QUICK_EXCLUDED_AGENTS.has(agentId)) {
1209
- agentsSkipped++;
1210
- continue;
1211
- }
1212
2271
  const dest = path.join(agentsDest, name);
1213
2272
  const rendered = renderPlaceholders(readTextFile(src), {
1214
2273
  ...placeholders,
@@ -1216,7 +2275,6 @@ function placeQuickTierArtefacts(input) {
1216
2275
  });
1217
2276
  writeFileLf(dest, rendered);
1218
2277
  agentsPlaced++;
1219
- installedAgentNames.add(agentId);
1220
2278
  // Per-agent memory directory. Created even when empty so the first run of
1221
2279
  // the agent doesn't error on ENOENT. Skipped when materializing the SHARED
1222
2280
  // framework (`seedProjectDirs === false`): agent-memory is per-workspace
@@ -1231,13 +2289,6 @@ function placeQuickTierArtefacts(input) {
1231
2289
  }
1232
2290
  }
1233
2291
  // --- Commands ---
1234
- // Skip commands whose required agents were excluded.
1235
- const excludedCommands = new Set();
1236
- for (const dep of COMMAND_AGENT_DEPENDENCIES) {
1237
- const hasAllRequired = dep.requires.every((a) => installedAgentNames.has(a));
1238
- if (!hasAllRequired)
1239
- excludedCommands.add(dep.command);
1240
- }
1241
2292
  const commandsSrc = path.join(setupTemplates, 'commands', 'specrails');
1242
2293
  const commandsDest = path.join(providerDirAbs, 'commands', 'specrails');
1243
2294
  let commandsPlaced = 0;
@@ -1247,9 +2298,6 @@ function placeQuickTierArtefacts(input) {
1247
2298
  const name = path.basename(src);
1248
2299
  if (!name.endsWith('.md'))
1249
2300
  continue;
1250
- const cmdId = name.slice(0, -3);
1251
- if (excludedCommands.has(cmdId))
1252
- continue;
1253
2301
  const dest = path.join(commandsDest, name);
1254
2302
  const rendered = renderPlaceholders(readTextFile(src), {
1255
2303
  ...placeholders,
@@ -1301,7 +2349,7 @@ function applyCodexSettings(input) {
1301
2349
  written++;
1302
2350
  }
1303
2351
  // AGENTS.md — top-level instructions file the codex CLI loads on startup.
1304
- // Written with a sentinel block so update + enrich passes can refresh the
2352
+ // Written with a sentinel block so update passes can refresh the
1305
2353
  // managed content while preserving anything the user added outside it.
1306
2354
  const agentsMdPath = path.join(input.artifactRoot, 'AGENTS.md');
1307
2355
  const agentsMdContent = renderInitialAgentsMd(input.codeRoot);
@@ -1369,7 +2417,7 @@ function upsertAgentsMdManagedBlock(existing, managedBlock) {
1369
2417
  // CLAUDE: the top-level `sr-*` skills (sr-implement, sr-why, …) are GENERATED
1370
2418
  // from their canonical slash-command body (`templates/commands/specrails/
1371
2419
  // <command>.md`) — the command is the single source of truth, so the skill can
1372
- // never drift from it. Quick tier excludes VPC-dependent ones.
2420
+ // never drift from it.
1373
2421
  //
1374
2422
  // CODEX: top-level skills are NOT placed — every one has a command counterpart
1375
2423
  // that the command path ports to `.codex/skills/<name>/` (with codex-native
@@ -1385,10 +2433,6 @@ function placeSkills(input) {
1385
2433
  const commandsSrc = path.join(input.artifactRoot, '.specrails', 'setup-templates', 'commands', 'specrails');
1386
2434
  const skillEntries = Object.entries(SKILL_FROM_COMMAND);
1387
2435
  for (const [skillName, spec] of skillEntries) {
1388
- if (input.tier === 'quick' && QUICK_EXCLUDED_SKILLS.has(skillName)) {
1389
- result.skipped++;
1390
- continue;
1391
- }
1392
2436
  const src = path.join(commandsSrc, `${spec.command}.md`);
1393
2437
  if (!pathExists(src))
1394
2438
  continue;
@@ -1411,8 +2455,7 @@ function placeSkills(input) {
1411
2455
  // copies were vestigial — unused on Claude, always overridden on codex,
1412
2456
  // and shipped with unsubstituted placeholders — and were removed).
1413
2457
  //
1414
- // Only the three CORE_AGENTS are placed by default; sr-merge-resolver and
1415
- // every layer specialist are placed only when selectedAgents includes them.
2458
+ // Only the three CORE_AGENTS ship, so only their rail skills exist to place.
1416
2459
  const codexRailsOverridesDir = input.provider === 'codex'
1417
2460
  ? path.join(input.scriptDir, 'templates', 'codex-skills', 'rails')
1418
2461
  : null;
@@ -1447,6 +2490,63 @@ function placeSkills(input) {
1447
2490
  result.skipped += g.skipped;
1448
2491
  result.filesCopied += g.filesCopied;
1449
2492
  }
2493
+ if (input.provider === 'kimi') {
2494
+ const setupRoot = path.join(input.artifactRoot, '.specrails', 'setup-templates');
2495
+ const commandsSrc = path.join(setupRoot, 'commands', 'specrails');
2496
+ const agentsSrc = path.join(setupRoot, 'agents');
2497
+ const selectedAgents = input.selectedAgents
2498
+ ? new Set([...input.selectedAgents, ...CORE_AGENTS])
2499
+ : new Set([...CORE_AGENTS]);
2500
+ const placedRoleIds = new Set();
2501
+ const placeholders = {
2502
+ PROJECT_NAME: path.basename(input.codeRoot),
2503
+ SECURITY_EXEMPTIONS_PATH: '.kimi-code/security-exemptions.yaml',
2504
+ PERSONA_DIR: '.kimi-code/personas/',
2505
+ };
2506
+ if (isDir(agentsSrc)) {
2507
+ for (const src of listDir(agentsSrc)) {
2508
+ const name = path.basename(src);
2509
+ if (!name.endsWith('.md'))
2510
+ continue;
2511
+ const roleId = name.slice(0, -3);
2512
+ if (!input.materializeAllAgents && !selectedAgents.has(roleId))
2513
+ continue;
2514
+ writeKimiRoleSkill({
2515
+ src,
2516
+ dest: path.join(destBase, roleId, 'SKILL.md'),
2517
+ roleId,
2518
+ placeholders,
2519
+ });
2520
+ placedRoleIds.add(roleId);
2521
+ result.placed++;
2522
+ result.filesCopied++;
2523
+ if (input.seedProjectDirs !== false) {
2524
+ mkdirp(path.join(input.artifactRoot, input.providerDir, 'agent-memory', roleId));
2525
+ }
2526
+ }
2527
+ }
2528
+ // v5 ships only the core trio; every bundled command's role dependencies
2529
+ // are always satisfied, so no command exclusion set is needed.
2530
+ const excludedCommands = new Set();
2531
+ if (isDir(commandsSrc)) {
2532
+ for (const src of listDir(commandsSrc)) {
2533
+ const name = path.basename(src);
2534
+ if (!name.endsWith('.md') || name === 'setup.md')
2535
+ continue;
2536
+ const commandName = name.slice(0, -3);
2537
+ if (excludedCommands.has(commandName))
2538
+ continue;
2539
+ writeKimiWorkflowSkill({
2540
+ src,
2541
+ dest: path.join(destBase, `specrails-${commandName}`, 'SKILL.md'),
2542
+ commandName,
2543
+ placeholders,
2544
+ });
2545
+ result.placed++;
2546
+ result.filesCopied++;
2547
+ }
2548
+ }
2549
+ }
1450
2550
  return result;
1451
2551
  }
1452
2552
  /**