specrails-core 4.11.2 → 4.12.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 (56) hide show
  1. package/README.md +56 -20
  2. package/bin/specrails-core.mjs +343 -20
  3. package/bin/tui-installer.mjs +103 -52
  4. package/dist/installer/cli.js +1 -1
  5. package/dist/installer/cli.js.map +1 -1
  6. package/dist/installer/commands/doctor.js +486 -24
  7. package/dist/installer/commands/doctor.js.map +1 -1
  8. package/dist/installer/commands/framework.js +49 -7
  9. package/dist/installer/commands/framework.js.map +1 -1
  10. package/dist/installer/commands/init.js +423 -25
  11. package/dist/installer/commands/init.js.map +1 -1
  12. package/dist/installer/commands/update.js +36 -9
  13. package/dist/installer/commands/update.js.map +1 -1
  14. package/dist/installer/phases/framework-lifecycle.js +125 -0
  15. package/dist/installer/phases/framework-lifecycle.js.map +1 -0
  16. package/dist/installer/phases/install-config.js +157 -5
  17. package/dist/installer/phases/install-config.js.map +1 -1
  18. package/dist/installer/phases/manifest.js +27 -2
  19. package/dist/installer/phases/manifest.js.map +1 -1
  20. package/dist/installer/phases/prereqs.js +57 -2
  21. package/dist/installer/phases/prereqs.js.map +1 -1
  22. package/dist/installer/phases/provider-detect.js +116 -6
  23. package/dist/installer/phases/provider-detect.js.map +1 -1
  24. package/dist/installer/phases/scaffold.js +1222 -12
  25. package/dist/installer/phases/scaffold.js.map +1 -1
  26. package/dist/installer/runtime/kimi.js +255 -0
  27. package/dist/installer/runtime/kimi.js.map +1 -0
  28. package/dist/installer/util/paths.js +12 -0
  29. package/dist/installer/util/paths.js.map +1 -1
  30. package/dist/installer/util/registry.js +234 -14
  31. package/dist/installer/util/registry.js.map +1 -1
  32. package/docs/README.md +1 -0
  33. package/docs/deployment.md +6 -7
  34. package/docs/getting-started.md +11 -7
  35. package/docs/installation.md +34 -16
  36. package/docs/plugin-architecture.md +11 -8
  37. package/docs/updating.md +21 -3
  38. package/docs/user-docs/cli-reference.md +43 -22
  39. package/docs/user-docs/codex-vs-claude-code.md +11 -9
  40. package/docs/user-docs/faq.md +1 -1
  41. package/docs/user-docs/getting-started-codex.md +5 -8
  42. package/docs/user-docs/getting-started-kimi.md +423 -0
  43. package/docs/user-docs/installation.md +49 -14
  44. package/docs/user-docs/quick-start.md +11 -8
  45. package/docs/windows.md +29 -4
  46. package/integration-contract.json +85 -13
  47. package/package.json +9 -5
  48. package/schemas/profile.v1.json +67 -5
  49. package/templates/codex-skills/batch-implement/SKILL.md +4 -0
  50. package/templates/commands/specrails/batch-implement.md +11 -0
  51. package/templates/gemini-commands/batch-implement.toml +4 -0
  52. package/templates/kimi/specrails/run-skill.mjs +3005 -0
  53. package/templates/kimi/specrails/vendor/js-yaml/LICENSE +21 -0
  54. package/templates/kimi/specrails/vendor/js-yaml/NOTICE.md +16 -0
  55. package/templates/kimi/specrails/vendor/js-yaml/js-yaml.mjs +3856 -0
  56. package/templates/profiles/kimi-default.json +15 -0
@@ -1,8 +1,8 @@
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
  /**
@@ -59,6 +59,20 @@ const OPSX_TO_GEMINI_SKILL = {
59
59
  explore: 'openspec-explore',
60
60
  onboard: 'openspec-onboard',
61
61
  };
62
+ /** OpenSpec's published Kimi skill ids. The mapping is intentionally explicit. */
63
+ const OPSX_TO_KIMI_SKILL = {
64
+ propose: 'openspec-propose',
65
+ ff: 'openspec-ff-change',
66
+ new: 'openspec-new-change',
67
+ apply: 'openspec-apply-change',
68
+ continue: 'openspec-continue-change',
69
+ archive: 'openspec-archive-change',
70
+ 'bulk-archive': 'openspec-bulk-archive-change',
71
+ sync: 'openspec-sync-specs',
72
+ verify: 'openspec-verify-change',
73
+ explore: 'openspec-explore',
74
+ onboard: 'openspec-onboard',
75
+ };
62
76
  /**
63
77
  * Rewrite every literal `Skill("opsx:<id>"[, …])` call in a Claude-authored agent
64
78
  * body into the Gemini `activate_skill(name="…")` form. Positional skill input
@@ -73,6 +87,39 @@ export function translateOpsxSkillCallsForGemini(body) {
73
87
  return skill ? `activate_skill(name="${skill}")` : match;
74
88
  });
75
89
  }
90
+ /**
91
+ * Port shared Claude-authored prose to Kimi's directory-skill contract.
92
+ *
93
+ * Kimi's TUI/ACP clients intercept `/skill:*`, but a materialized workflow is
94
+ * already running inside a Session and must activate nested workflows through
95
+ * Kimi's built-in `Skill` tool (`{ skill, args }`). Emitting slash text here
96
+ * would silently become ordinary model text under `kimi -p`. Interactive slash
97
+ * examples therefore belong only in AGENTS/docs, never generated skill bodies.
98
+ * This is render-only; canonical Claude/Codex/Gemini templates stay unchanged.
99
+ */
100
+ export function translateClaudeTextForKimi(body) {
101
+ let translated = body.replace(/Skill\("opsx:([a-z-]+)"(?:\s*,\s*("[^"]*"|'[^']*'|[^)]*))?\)/g, (_match, id, input) => {
102
+ const skill = OPSX_TO_KIMI_SKILL[id];
103
+ if (!skill)
104
+ return `Unresolved Kimi Skill tool mapping for "opsx:${id}"`;
105
+ const args = input?.replace(/^["']|["']$/g, '') ?? '';
106
+ return `Skill(skill="${skill}", args=${JSON.stringify(args)})`;
107
+ });
108
+ translated = translated
109
+ .replace(/\.claude\/agents\/personas\//g, '.kimi-code/personas/')
110
+ .replace(/\.claude\/agents\//g, '.kimi-code/skills/')
111
+ .replace(/(\.kimi-code\/skills\/[^\s`"'()]+)\.md/g, '$1/SKILL.md')
112
+ .replace(/\.claude\//g, '.kimi-code/')
113
+ .replace(/\.claude\b/g, '.kimi-code')
114
+ .replace(/\bCLAUDE\.md\b/g, '.kimi-code/AGENTS.md')
115
+ .replace(/\/(?:specrails|sr):([a-z0-9-]+)/g, 'Skill(skill="specrails-$1", args=<arguments following this command>)')
116
+ .replace(/\/(?:specrails|sr):/g, 'Skill(skill="specrails-<command>", args=<arguments following this command>)')
117
+ .replace(/\bsubagent_type\b/g, 'role_skill')
118
+ .replace(/\bClaude Code\b/g, 'Kimi Code')
119
+ .replace(/\bClaude CLI\b/g, 'Kimi CLI')
120
+ .replace(/\bAgent tool\b/g, 'external Kimi role process');
121
+ return translated;
122
+ }
76
123
  /**
77
124
  * Per-role gemini model. Defaults to `gemini-3.5-flash` — the stable flagship
78
125
  * (June 2026): strong agentic/coding, high quota, and unlike `gemini-2.5-pro`
@@ -171,7 +218,18 @@ const LINKED_PROVIDER_SUBTREES = {
171
218
  claude: ['agents', 'commands', 'skills', 'rules'],
172
219
  codex: ['skills'],
173
220
  gemini: ['agents', 'commands'],
221
+ // Kimi skills are linked one directory at a time so direct-child OpenSpec
222
+ // skills and user-owned custom-* roles can coexist. The self-contained
223
+ // headless runner and its vendored parser are Core-owned and linked as a
224
+ // separate static subtree.
225
+ kimi: ['rules', 'specrails'],
174
226
  };
227
+ const KIMI_RUNNER_RELATIVE_FILES = [
228
+ 'run-skill.mjs',
229
+ path.join('vendor', 'js-yaml', 'js-yaml.mjs'),
230
+ path.join('vendor', 'js-yaml', 'LICENSE'),
231
+ path.join('vendor', 'js-yaml', 'NOTICE.md'),
232
+ ];
175
233
  /**
176
234
  * Returns true iff any of the provider directories already contains
177
235
  * content. The desktop-app-driven path skips the "merge existing?" prompt and
@@ -219,6 +277,11 @@ export function scaffoldInstallation(input) {
219
277
  mk(path.join(input.artifactRoot, input.providerDir, 'commands', 'specrails'));
220
278
  mk(path.join(input.artifactRoot, input.providerDir, 'agents'));
221
279
  }
280
+ else if (input.provider === 'kimi') {
281
+ mk(path.join(input.artifactRoot, input.providerDir, 'skills'));
282
+ mk(path.join(input.artifactRoot, input.providerDir, 'specrails'));
283
+ mk(path.join(input.artifactRoot, input.providerDir, 'rules'));
284
+ }
222
285
  else {
223
286
  mk(path.join(input.artifactRoot, input.providerDir, 'commands', 'specrails'));
224
287
  mk(path.join(input.artifactRoot, input.providerDir, 'skills'));
@@ -240,6 +303,9 @@ export function scaffoldInstallation(input) {
240
303
  const gitignoreEntries = ['.claude/agent-memory/', '.specrails/'];
241
304
  if (input.provider === 'gemini')
242
305
  gitignoreEntries.push('.gemini/agent-memory/');
306
+ if (input.provider === 'kimi') {
307
+ gitignoreEntries.push('.kimi-code/agent-memory/', '.kimi-code/pipeline-state/', '.kimi-code/.dry-run/', '.kimi-code/telemetry/');
308
+ }
243
309
  ensureGitignore(input.codeRoot, gitignoreEntries);
244
310
  }
245
311
  // --- Copy bundled templates into setup-templates/ ---
@@ -264,6 +330,9 @@ export function scaffoldInstallation(input) {
264
330
  // --- Write bundled commands (enrich.md + doctor.md) ---
265
331
  copyBundledCommands({ ...input, copiedIncrement: (n) => (copiedFiles += n) });
266
332
  pruneLegacyArtifacts(input);
333
+ if (input.provider === 'kimi') {
334
+ copiedFiles += placeKimiSkillRunner(input);
335
+ }
267
336
  // --- Quick tier: direct-placement short-circuit ---
268
337
  if (input.tier === 'quick') {
269
338
  const placed = placeQuickTierArtefacts({ ...input });
@@ -300,10 +369,24 @@ export function scaffoldInstallation(input) {
300
369
  info(`Gemini provider: wrote ${written} setting file(s) (settings.json, GEMINI.md)`);
301
370
  }
302
371
  }
372
+ else if (input.provider === 'kimi') {
373
+ const written = applyKimiSettings(input);
374
+ copiedFiles += written;
375
+ if (written > 0) {
376
+ info(`Kimi provider: wrote ${written} setting file(s) (.kimi-code/AGENTS.md, mcp.json)`);
377
+ }
378
+ }
303
379
  // --- Full-tier hint: enrich is required to generate VPC artefacts ---
304
380
  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 ` +
381
+ const cliName = input.provider === 'codex'
382
+ ? 'Codex CLI'
383
+ : input.provider === 'gemini'
384
+ ? 'Gemini CLI'
385
+ : input.provider === 'kimi'
386
+ ? 'Kimi CLI'
387
+ : 'Claude Code';
388
+ const enrichCommand = input.provider === 'kimi' ? '/skill:specrails-enrich' : '/specrails:enrich';
389
+ info(`Full tier staged. Run \`${enrichCommand}\` in ${cliName} to generate ` +
307
390
  'VPC personas and adapt agents (including sr-product-manager and ' +
308
391
  'sr-product-analyst) to this codebase.');
309
392
  }
@@ -319,11 +402,109 @@ export function scaffoldInstallation(input) {
319
402
  };
320
403
  }
321
404
  /** Path to the per-version, per-provider materialization marker (manifest hash). */
322
- function frameworkStampPath(versionDir, providerDir) {
405
+ export function frameworkStampPath(versionDir, providerDir) {
323
406
  // Store the stamp OUTSIDE the providerDir so it never leaks into the linked
324
407
  // subtree. `.stamp-<providerDir>.json` is provider-keyed.
325
408
  return path.join(versionDir, `.framework-stamp${providerDir}.json`);
326
409
  }
410
+ /**
411
+ * Stable Merkle-like digest over regular files. Relative POSIX paths and raw
412
+ * bytes are both framed into the hash, so renames, missing files, additions and
413
+ * byte corruption are detected. Directory mtimes and traversal order never
414
+ * affect the result.
415
+ */
416
+ function hashFrameworkTrees(roots, options = {}) {
417
+ const hash = createHash('sha256');
418
+ const walk = (root, current, label) => {
419
+ const entries = listDir(current).sort((a, b) => path.basename(a).localeCompare(path.basename(b)));
420
+ for (const entry of entries) {
421
+ const name = path.basename(entry);
422
+ if (options.ignorePackageNoise === true &&
423
+ (name === 'node_modules' || name === 'package-lock.json')) {
424
+ continue;
425
+ }
426
+ const rel = path.relative(root, entry).split(path.sep).join('/');
427
+ if (isDir(entry)) {
428
+ walk(root, entry, label);
429
+ continue;
430
+ }
431
+ if (!pathExists(entry))
432
+ continue;
433
+ const framedPath = `${label}/${rel}`;
434
+ const bytes = readBytes(entry);
435
+ hash.update(`file\0${Buffer.byteLength(framedPath)}\0${framedPath}\0`);
436
+ hash.update(`${bytes.byteLength}\0`);
437
+ hash.update(bytes);
438
+ }
439
+ };
440
+ for (const root of [...roots].sort((a, b) => a.label.localeCompare(b.label))) {
441
+ hash.update(`root\0${root.label}\0`);
442
+ if (isDir(root.dir)) {
443
+ walk(root.dir, root.dir, root.label);
444
+ }
445
+ else {
446
+ hash.update('missing\0');
447
+ }
448
+ }
449
+ return `sha256:${hash.digest('hex')}`;
450
+ }
451
+ /** Hash of every package input that can influence provider materialization. */
452
+ function frameworkSourceHash(scriptDir, provider) {
453
+ const treeHash = hashFrameworkTrees([
454
+ { label: 'templates', dir: path.join(scriptDir, 'templates') },
455
+ { label: 'commands', dir: path.join(scriptDir, 'commands') },
456
+ ], { ignorePackageNoise: true });
457
+ return `sha256:${createHash('sha256')
458
+ .update(treeHash)
459
+ .update('\0provider\0')
460
+ .update(provider)
461
+ .digest('hex')}`;
462
+ }
463
+ /** Hash of the provider-static tree workspace links consume. */
464
+ function frameworkContentHash(providerFrameworkDir) {
465
+ return hashFrameworkTrees([
466
+ { label: 'provider', dir: providerFrameworkDir },
467
+ ]);
468
+ }
469
+ function readFrameworkStamp(stampPath) {
470
+ if (!pathExists(stampPath))
471
+ return null;
472
+ try {
473
+ const parsed = JSON.parse(readTextFile(stampPath));
474
+ if (parsed.schema !== 1 ||
475
+ typeof parsed.version !== 'string' ||
476
+ typeof parsed.provider !== 'string' ||
477
+ typeof parsed.source_hash !== 'string' ||
478
+ typeof parsed.content_hash !== 'string') {
479
+ return null;
480
+ }
481
+ return parsed;
482
+ }
483
+ catch {
484
+ return null;
485
+ }
486
+ }
487
+ /**
488
+ * Validate one provider in a materialized version without needing the source
489
+ * package. Used by the final swap gate: the stamp identity and current output
490
+ * hash must still agree immediately before `current` moves.
491
+ */
492
+ export function frameworkMaterializationProblem(versionDir, version, provider, providerDir) {
493
+ const providerFrameworkDir = path.join(versionDir, providerDir);
494
+ const stampPath = frameworkStampPath(versionDir, providerDir);
495
+ if (!isDir(providerFrameworkDir))
496
+ return `missing ${providerDir}/`;
497
+ const stamp = readFrameworkStamp(stampPath);
498
+ if (!stamp)
499
+ return `missing or invalid ${path.basename(stampPath)}`;
500
+ if (stamp.version !== version || stamp.provider !== provider) {
501
+ return `invalid stamp (expected version=${version}, provider=${provider})`;
502
+ }
503
+ if (stamp.content_hash !== frameworkContentHash(providerFrameworkDir)) {
504
+ return 'managed content does not match stamp';
505
+ }
506
+ return null;
507
+ }
327
508
  /**
328
509
  * Materialize the provider-INVARIANT framework subtree ONCE into
329
510
  * `<frameworkDir>/<version>/<providerDir>/` (+ `<version>/setup-templates/`).
@@ -336,10 +517,24 @@ export function installFramework(input) {
336
517
  const versionDir = path.join(input.frameworkDir, input.version);
337
518
  const providerFrameworkDir = path.join(versionDir, input.providerDir);
338
519
  const stampPath = frameworkStampPath(versionDir, input.providerDir);
339
- // Idempotency: existing materialization with a matching stamp → skip.
340
- if (isDir(providerFrameworkDir) && pathExists(stampPath)) {
520
+ const sourceHash = frameworkSourceHash(input.scriptDir, input.provider);
521
+ const stamp = readFrameworkStamp(stampPath);
522
+ // Same-version reuse is allowed only when BOTH provenance and every managed
523
+ // output byte still match the deterministic stamp. A legacy/timestamp-only
524
+ // stamp, a changed package source, or any missing/corrupt/extra managed file
525
+ // falls through to a clean provider-tree repair.
526
+ if (isDir(providerFrameworkDir) &&
527
+ stamp?.version === input.version &&
528
+ stamp.provider === input.provider &&
529
+ stamp.source_hash === sourceHash &&
530
+ stamp.content_hash === frameworkContentHash(providerFrameworkDir)) {
341
531
  return { providerFrameworkDir, versionDir, materialized: false };
342
532
  }
533
+ // Framework provider trees are entirely Core-owned. Rebuilding from a clean
534
+ // destination removes stale files as well as repairing corrupt/missing ones,
535
+ // without touching sibling providers already materialized in this version.
536
+ removePath(providerFrameworkDir);
537
+ removePath(stampPath);
343
538
  // Reuse scaffoldInstallation's static-placement helpers by pointing
344
539
  // `artifactRoot` at the version dir. `seedProjectDirs: false` keeps the copy
345
540
  // free of per-workspace mutable state. The `codeRoot` is irrelevant to the
@@ -372,7 +567,22 @@ export function installFramework(input) {
372
567
  for (const f of ['AGENTS.md', 'GEMINI.md', 'CLAUDE.md']) {
373
568
  rmSync(path.join(versionDir, f), { force: true });
374
569
  }
375
- writeFileLf(stampPath, `${JSON.stringify({ version: input.version, provider: input.provider, at: new Date().toISOString() }, null, 2)}\n`);
570
+ // Kimi's instruction and MCP files are provider-local rather than root-local,
571
+ // but both are project-specific and must be real files in each workspace.
572
+ // In particular, linking mcp.json would let Desktop mutate the shared
573
+ // framework and leak one project's MCP registry into every other project.
574
+ if (input.provider === 'kimi') {
575
+ rmSync(path.join(providerFrameworkDir, 'AGENTS.md'), { force: true });
576
+ rmSync(path.join(providerFrameworkDir, 'mcp.json'), { force: true });
577
+ }
578
+ const frameworkStamp = {
579
+ schema: 1,
580
+ version: input.version,
581
+ provider: input.provider,
582
+ source_hash: sourceHash,
583
+ content_hash: frameworkContentHash(providerFrameworkDir),
584
+ };
585
+ writeFileLf(stampPath, `${JSON.stringify(frameworkStamp, null, 2)}\n`);
376
586
  return { providerFrameworkDir, versionDir, materialized: true };
377
587
  }
378
588
  /**
@@ -430,10 +640,22 @@ export function assembleProjectWorkspace(input) {
430
640
  links[sub] = symlinkOrCopy(target, dest, preferCopy);
431
641
  }
432
642
  }
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;
643
+ if (input.provider === 'kimi') {
644
+ const kimiSkillsTarget = path.join(currentProviderDir, 'skills');
645
+ const kimiSkillsDest = path.join(workspaceProviderDir, 'skills');
646
+ migrateLegacyKimiRoleLayout(kimiSkillsDest);
647
+ if (pathExists(kimiSkillsTarget)) {
648
+ links.skills = linkKimiSkillDirectories(kimiSkillsTarget, kimiSkillsDest, selectedAgentSet, preferCopy);
649
+ }
650
+ }
651
+ // Link only provider-invariant settings (codex config.toml / gemini
652
+ // settings.json). Kimi mcp.json is a mutable per-project registry and is
653
+ // seeded below as a real workspace file.
654
+ const settingsFile = input.provider === 'codex'
655
+ ? 'config.toml'
656
+ : input.provider === 'gemini'
657
+ ? 'settings.json'
658
+ : null;
437
659
  if (settingsFile) {
438
660
  const settingsTarget = path.join(currentProviderDir, settingsFile);
439
661
  const settingsLink = path.join(workspaceProviderDir, settingsFile);
@@ -449,6 +671,8 @@ export function assembleProjectWorkspace(input) {
449
671
  scriptDir: input.scriptDir,
450
672
  repoRoot: input.workspace,
451
673
  version: input.version,
674
+ providers: [input.provider],
675
+ primaryProvider: input.provider,
452
676
  });
453
677
  writeManifestFiles(input.workspace, manifest);
454
678
  return { links, seededMemoryAgents };
@@ -510,8 +734,64 @@ function seedProjectLayer(input, currentProviderDir) {
510
734
  warn(`gemini agent pre-acknowledgment skipped: ${err.message}`);
511
735
  }
512
736
  }
737
+ else if (input.provider === 'kimi') {
738
+ const skillsDir = path.join(currentProviderDir, 'skills');
739
+ if (isDir(skillsDir)) {
740
+ for (const roleDir of listDir(skillsDir)) {
741
+ if (!isDir(roleDir))
742
+ continue;
743
+ const id = path.basename(roleDir);
744
+ if (/^sr-[a-z0-9-]+$/.test(id) &&
745
+ selected.has(id) &&
746
+ !QUICK_EXCLUDED_AGENTS.has(id) &&
747
+ pathExists(path.join(roleDir, 'SKILL.md'))) {
748
+ placedAgentIds.push(id);
749
+ }
750
+ }
751
+ }
752
+ for (const id of placedAgentIds) {
753
+ mkdirp(path.join(input.workspace, '.kimi-code', 'agent-memory', id));
754
+ seededMemoryAgents.push(id);
755
+ if (EXPLANATION_AUTHORS.has(id)) {
756
+ mkdirp(path.join(input.workspace, '.kimi-code', 'agent-memory', 'explanations'));
757
+ }
758
+ }
759
+ seedInstructionFile(path.join(input.workspace, '.kimi-code', 'AGENTS.md'), renderInitialKimiAgentsMd(input.codeRoot));
760
+ seedKimiMcpFile(path.join(input.workspace, '.kimi-code', 'mcp.json'));
761
+ if (input.workspace === input.codeRoot) {
762
+ ensureGitignore(input.codeRoot, [
763
+ '.kimi-code/agent-memory/',
764
+ '.kimi-code/pipeline-state/',
765
+ '.kimi-code/.dry-run/',
766
+ '.kimi-code/telemetry/',
767
+ '.specrails/',
768
+ ]);
769
+ }
770
+ }
513
771
  return seededMemoryAgents;
514
772
  }
773
+ /**
774
+ * Ensure Kimi's per-project MCP registry is a real writable file. Older Core
775
+ * builds could create a framework symlink here; migrate a readable link by
776
+ * copying its bytes locally, or seed an empty registry when the link is stale.
777
+ */
778
+ function seedKimiMcpFile(mcpPath) {
779
+ if (isSymlink(mcpPath)) {
780
+ let existing = '{\n "mcpServers": {}\n}\n';
781
+ try {
782
+ existing = readTextFile(mcpPath);
783
+ }
784
+ catch {
785
+ // A version swap can leave the obsolete shared-framework link dangling.
786
+ }
787
+ removePath(mcpPath);
788
+ writeFileLf(mcpPath, existing);
789
+ return;
790
+ }
791
+ if (!pathExists(mcpPath)) {
792
+ writeFileLf(mcpPath, '{\n "mcpServers": {}\n}\n');
793
+ }
794
+ }
515
795
  /**
516
796
  * Per-file link the framework `agents/` into a REAL workspace `agents/` dir.
517
797
  * Keeps `custom-*.md` (and any other user-authored file that the framework does
@@ -585,6 +865,89 @@ function linkAgentFiles(frameworkAgentsDir, workspaceAgentsDir, selectedIds, pre
585
865
  }
586
866
  return mechanism;
587
867
  }
868
+ /**
869
+ * Assemble Kimi skills without turning the whole directory into a symlink.
870
+ * Kimi's loader inspects only immediate children of `.kimi-code/skills`, so
871
+ * workflows (`specrails-*`), OpenSpec skills (`openspec-*`), managed roles
872
+ * (`sr-*`), and user roles (`custom-*`) all share this flat directory.
873
+ * OpenSpec and custom/unknown skills must survive every update.
874
+ */
875
+ function linkKimiSkillDirectories(frameworkSkillsDir, workspaceSkillsDir, selectedRoleIds, preferCopy) {
876
+ mkdirp(workspaceSkillsDir);
877
+ const linkedFrameworkNames = new Set();
878
+ let mechanism = 'symlink';
879
+ for (const source of listDir(frameworkSkillsDir)) {
880
+ if (!isDir(source))
881
+ continue;
882
+ const name = path.basename(source);
883
+ if (name === 'rails') {
884
+ // A same-version framework materialized by the experimental build may
885
+ // still contain this container. `installFramework` normally rematerializes
886
+ // it, but never expose nested roles if a caller supplies one directly.
887
+ continue;
888
+ }
889
+ if (/^sr-[a-z0-9-]+$/.test(name) &&
890
+ (!selectedRoleIds.has(name) || QUICK_EXCLUDED_AGENTS.has(name))) {
891
+ continue;
892
+ }
893
+ linkedFrameworkNames.add(name);
894
+ const used = symlinkOrCopy(source, path.join(workspaceSkillsDir, name), preferCopy);
895
+ if (used === 'copy')
896
+ mechanism = 'copy';
897
+ else if (used === 'junction' && mechanism !== 'copy')
898
+ mechanism = 'junction';
899
+ }
900
+ // `specrails-*` workflows and `sr-*` roles are framework-owned. OpenSpec,
901
+ // custom-* and unknown/user skill directories remain outside this boundary.
902
+ for (const existing of listDir(workspaceSkillsDir)) {
903
+ const name = path.basename(existing);
904
+ if (linkedFrameworkNames.has(name))
905
+ continue;
906
+ if (name.startsWith('specrails-') || /^sr-[a-z0-9-]+$/.test(name)) {
907
+ removePath(existing);
908
+ }
909
+ }
910
+ return mechanism;
911
+ }
912
+ /**
913
+ * Migrate the pre-release `skills/rails/<role>` layout without risking user
914
+ * data. Framework-owned `sr-*` directories are dropped (the caller recreates
915
+ * them at the discoverable flat path). Reserved `custom-*` roles are atomically
916
+ * moved to `skills/custom-*` when that target is free. A collision or unknown
917
+ * child remains byte-untouched under `rails/` and doctor reports it, requiring
918
+ * explicit user resolution rather than destructive guessing.
919
+ */
920
+ function migrateLegacyKimiRoleLayout(skillsDir) {
921
+ const legacyRolesDir = path.join(skillsDir, 'rails');
922
+ if (!isDir(legacyRolesDir))
923
+ return;
924
+ for (const source of listDir(legacyRolesDir)) {
925
+ if (!isDir(source))
926
+ continue;
927
+ const id = path.basename(source);
928
+ if (/^sr-[a-z0-9-]+$/.test(id)) {
929
+ removePath(source);
930
+ continue;
931
+ }
932
+ if (!id.startsWith('custom-'))
933
+ continue;
934
+ const destination = path.join(skillsDir, id);
935
+ if (pathExists(destination)) {
936
+ warn(`Kimi role migration kept ${path.relative(skillsDir, source)} because ` +
937
+ `${id}/ already exists; resolve the duplicate manually`);
938
+ continue;
939
+ }
940
+ try {
941
+ renameSync(source, destination);
942
+ info(`Migrated Kimi role skills/rails/${id} → skills/${id}`);
943
+ }
944
+ catch (err) {
945
+ warn(`failed to migrate Kimi role ${id}: ${err.message}`);
946
+ }
947
+ }
948
+ if (listDir(legacyRolesDir).length === 0)
949
+ removePath(legacyRolesDir);
950
+ }
588
951
  /**
589
952
  * True when `name` (an `<id>.md`) matches a framework-owned agent id (`sr-*`).
590
953
  * Used to identify a stale COPY-fallback framework agent on Windows that the
@@ -595,6 +958,19 @@ function linkAgentFiles(frameworkAgentsDir, workspaceAgentsDir, selectedIds, pre
595
958
  function isFrameworkAgentName(name) {
596
959
  return /^sr-[a-z0-9-]+\.md$/.test(name);
597
960
  }
961
+ /**
962
+ * Install Core's self-contained Kimi headless skill runner and the vendored
963
+ * js-yaml parser used by upstream Kimi 0.27. These files are provider-static
964
+ * and managed: updates replace them through the same framework copy/link
965
+ * lifecycle as rules. They intentionally live outside `skills/` so Kimi never
966
+ * attempts to discover executable support files as skills.
967
+ */
968
+ function placeKimiSkillRunner(input) {
969
+ for (const relative of KIMI_RUNNER_RELATIVE_FILES) {
970
+ copyFile(path.join(input.scriptDir, 'templates', 'kimi', 'specrails', relative), path.join(input.artifactRoot, input.providerDir, 'specrails', relative));
971
+ }
972
+ return KIMI_RUNNER_RELATIVE_FILES.length;
973
+ }
598
974
  /** Write or sentinel-upsert a project instruction file (AGENTS.md/GEMINI.md). */
599
975
  function seedInstructionFile(filePath, content) {
600
976
  if (!pathExists(filePath)) {
@@ -610,6 +986,11 @@ function copyBundledCommands(input) {
610
986
  const commandsSrc = path.join(input.scriptDir, 'commands');
611
987
  if (!isDir(commandsSrc))
612
988
  return;
989
+ // Kimi's complete workflow catalog is rendered from the canonical
990
+ // templates/commands/specrails sources in placeKimiSkills. Do not let this
991
+ // generic bundled-command pass fall through to Claude's file layout.
992
+ if (input.provider === 'kimi')
993
+ return;
613
994
  if (input.provider === 'codex') {
614
995
  // Codex: each bundled command ships as a SKILL under
615
996
  // `.codex/skills/<name>/SKILL.md`. A codex-native override (spawn_agent
@@ -764,6 +1145,675 @@ function writeClaudeSkillFromCommand(args) {
764
1145
  ].join('\n');
765
1146
  writeFileLf(args.dest, frontmatter + body);
766
1147
  }
1148
+ const KIMI_ROLE_EXECUTION_CONTRACT = [
1149
+ '## Kimi role execution contract',
1150
+ '',
1151
+ 'These rules override later Claude-specific `role_skill`, `isolation`, and',
1152
+ '`run_in_background` notation. When this workflow asks for one role or a',
1153
+ 'parallel group, submit exactly one foreground role wave. The managed helper',
1154
+ 'starts one external Kimi CLI per role, runs the wave concurrently, attributes',
1155
+ 'every output event, and waits for every required role. Never emulate a role',
1156
+ 'in the orchestrator and never start concurrent helper commands.',
1157
+ '',
1158
+ 'First use the structured WriteFile tool (never Shell, a heredoc, `printf`,',
1159
+ 'or `echo`) to write `.specrails/kimi-role-wave.json`. Choose one lowercase',
1160
+ 'letters/digits/hyphens run id (1–64 characters) and reuse it for the whole',
1161
+ 'workflow. The file must have exactly this shape (1–32 roles):',
1162
+ '',
1163
+ '```json',
1164
+ '{',
1165
+ ' "run": "<stable-run-id>",',
1166
+ ' "roles": [',
1167
+ ' {',
1168
+ ' "key": "<unique-role-call-id>",',
1169
+ ' "skill": "<role-skill>",',
1170
+ ' "model": "<exact profile model or k3>",',
1171
+ ' "profile": "inherit",',
1172
+ ' "args": "<complete role context>",',
1173
+ ' "workspace": "current"',
1174
+ ' }',
1175
+ ' ]',
1176
+ '}',
1177
+ '```',
1178
+ '',
1179
+ 'Every `key`, profile stem, and worktree id uses the same 1–64 character grammar as `run`.',
1180
+ 'Use `"current"` for roles that target the orchestrator repository. The',
1181
+ 'helper gives each such role a private execution directory while setting its',
1182
+ '`SPECRAILS_REPO_DIR` to that repository, so nested calls and run-state do not',
1183
+ 'collide. Where later instructions request `isolation: worktree`, use',
1184
+ '`"worktree:<feature-id>"`; reuse that exact value for the developer, test,',
1185
+ 'documentation, and other sequential roles belonging to the same feature.',
1186
+ 'Never put two roles for the same worktree in one wave.',
1187
+ '',
1188
+ 'Resolve each role model in the orchestrator and encode all context as JSON;',
1189
+ 'set `profile` to `inherit`, or to a validated profile filename stem for a',
1190
+ 'per-rail override under `.specrails/profiles/<stem>.json`. The helper passes',
1191
+ 'that absolute profile path only to that child process.',
1192
+ 'do not place any model or context text in a shell command. Then run this',
1193
+ 'exact static command in the foreground:',
1194
+ '',
1195
+ '```sh',
1196
+ 'node .kimi-code/specrails/run-skill.mjs \\',
1197
+ ' --role-wave-file .specrails/kimi-role-wave.json \\',
1198
+ ' --add-dir "${SPECRAILS_REPO_DIR:-.}"',
1199
+ '```',
1200
+ '',
1201
+ 'The helper accepts only that fixed, regular, non-symlink one-shot file,',
1202
+ 'bounds it to 1 MiB, validates the exact schema and every identifier, and',
1203
+ 'deletes it before creating a worktree or process. For an isolated role it',
1204
+ 'creates or reuses a detached git worktree from a synthetic baseline commit',
1205
+ 'that snapshots the starting tracked and non-ignored untracked workspace,',
1206
+ 'exposes the',
1207
+ 'managed `.kimi-code`, and sets the child repository root to that worktree.',
1208
+ 'It persists the base commit and key/path mapping under',
1209
+ '`.specrails/kimi-role-worktrees/<run>.json` and emits',
1210
+ '`specrails.role.workspace` frames. Replace every later',
1211
+ '`<worktree-path>` placeholder with that emitted `repoDir`; compare against',
1212
+ 'the manifest `baseCommit`, not a hard-coded `main` ref.',
1213
+ '',
1214
+ 'The helper frames child stdout/stderr with its role key, emits one completion',
1215
+ 'frame per role, and exits nonzero after the whole wave if any role failed.',
1216
+ 'A termination signal is forwarded to every live child. Partial failures',
1217
+ 'leave completed worktrees and the manifest available for retry; newly',
1218
+ 'created partial worktrees are removed only when setup itself fails. Apply',
1219
+ 'the workflow cleanup step after merge using the exact static command',
1220
+ '`node .kimi-code/specrails/run-skill.mjs --role-wave-cleanup <run>`.',
1221
+ 'Cleanup removes registered worktrees, execution state, the manifest, and',
1222
+ 'the private synthetic-baseline ref; never clean up a failed run before retry.',
1223
+ 'The helper normalizes only the three',
1224
+ 'official short model ids (`k3` launches as `kimi-code/k3`); safe custom',
1225
+ 'aliases pass through unchanged.',
1226
+ 'There is no SpecRails-owned Kimi server or bundled Kimi binary.',
1227
+ '',
1228
+ ].join('\n');
1229
+ const KIMI_NESTED_SKILL_CONTRACT = [
1230
+ '## Kimi nested skill activation',
1231
+ '',
1232
+ 'When these instructions show `Skill(skill="<id>", args="<raw args>")`, call',
1233
+ 'Kimi\'s built-in `Skill` tool with those `skill` and `args` fields. Do not',
1234
+ 'print the notation as prose and do not send an interactive slash command as',
1235
+ 'model text. This native tool path preserves Kimi\'s nested-skill behavior and',
1236
+ 'skill activation telemetry.',
1237
+ '',
1238
+ ].join('\n');
1239
+ const KIMI_RUNTIME_CONTEXT_CONTRACT = [
1240
+ '## Kimi runtime context contract',
1241
+ '',
1242
+ 'SpecRails deliberately resolves project-specific context at activation time;',
1243
+ 'the Kimi enrich workflow does not rewrite framework-owned role or workflow',
1244
+ 'SKILL.md files. Resolve every `KIMI_RUNTIME_*`, `KIMI_BACKLOG_*`, and',
1245
+ '`KIMI_PR_CREATE` marker below before acting. These are semantic markers,',
1246
+ 'never executable command names; do not pass them to Shell.',
1247
+ '',
1248
+ 'Use `${SPECRAILS_REPO_DIR}` when set as the code repository, otherwise the',
1249
+ 'current repository. Read `.kimi-code/project-context.md` for stack, layers,',
1250
+ 'CI commands, conventions, warnings, architecture, and important paths; when',
1251
+ 'a field is absent, inspect package/build/CI files and report the inferred',
1252
+ 'value explicitly. Scan `.kimi-code/personas/*.md` at runtime and read only',
1253
+ 'regular non-symlink files; derive persona names, roles, score columns, and',
1254
+ 'VPC sections from that live inventory. An empty inventory means “no personas',
1255
+ 'configured”, never fabricated rows or scores.',
1256
+ '',
1257
+ 'For every `KIMI_BACKLOG_*` marker, first read and validate',
1258
+ '`.specrails/backlog-config.json`. Route `local` through structured reads and',
1259
+ 'atomic writes of `.specrails/local-tickets.json`; route `github` through the',
1260
+ 'approved `gh issue` operation; route `jira` only through the configured',
1261
+ 'project/base URL and credentials. Honour read-only mode and never perform a',
1262
+ 'write operation when configuration is missing, invalid, or read-only.',
1263
+ 'Arguments shown after a marker describe the operation; they are not shell',
1264
+ 'argv. Resolve `KIMI_PR_CREATE` with the repository’s configured PR workflow',
1265
+ 'and ask before publishing when the active workflow requires confirmation.',
1266
+ '',
1267
+ ].join('\n');
1268
+ const KIMI_RUNTIME_PLACEHOLDERS = {
1269
+ ARCHITECTURE_DIAGRAM: 'KIMI_RUNTIME_ARCHITECTURE_DIAGRAM',
1270
+ AREA_TABLE: 'KIMI_RUNTIME_AREA_TABLE',
1271
+ BACKEND_ARCHITECTURE_DIAGRAM: 'KIMI_RUNTIME_BACKEND_ARCHITECTURE_DIAGRAM',
1272
+ BACKEND_CRITICAL_RULES: 'KIMI_RUNTIME_BACKEND_CRITICAL_RULES',
1273
+ BACKEND_EXPERTISE: 'KIMI_RUNTIME_BACKEND_EXPERTISE',
1274
+ BACKEND_LAYER_CONVENTIONS: 'KIMI_RUNTIME_BACKEND_LAYER_CONVENTIONS',
1275
+ BACKEND_STACK: 'KIMI_RUNTIME_BACKEND_STACK',
1276
+ BACKEND_TECH_LIST: 'KIMI_RUNTIME_BACKEND_TECH_LIST',
1277
+ BACKLOG_COMMENT_CMD: 'KIMI_BACKLOG_COMMENT',
1278
+ BACKLOG_CREATE_CMD: 'KIMI_BACKLOG_CREATE',
1279
+ BACKLOG_DELETE_CMD: 'KIMI_BACKLOG_DELETE',
1280
+ BACKLOG_FETCH_ALL_CMD: 'KIMI_BACKLOG_FETCH_ALL',
1281
+ BACKLOG_FETCH_CLOSED_CMD: 'KIMI_BACKLOG_FETCH_CLOSED',
1282
+ BACKLOG_FETCH_CMD: 'KIMI_BACKLOG_FETCH',
1283
+ BACKLOG_INIT_LABELS_CMD: 'KIMI_BACKLOG_INIT_LABELS',
1284
+ BACKLOG_PARTIAL_COMMENT_CMD: 'KIMI_BACKLOG_PARTIAL_COMMENT',
1285
+ BACKLOG_PREFLIGHT: 'KIMI_BACKLOG_PREFLIGHT',
1286
+ BACKLOG_PROVIDER_NAME: 'KIMI_RUNTIME_BACKLOG_PROVIDER_NAME',
1287
+ BACKLOG_UPDATE_CMD: 'KIMI_BACKLOG_UPDATE',
1288
+ BACKLOG_VIEW_CMD: 'KIMI_BACKLOG_VIEW',
1289
+ CI_CHECK_TABLE_ROWS: 'KIMI_RUNTIME_CI_CHECK_TABLE_ROWS',
1290
+ CI_COMMANDS: 'KIMI_RUNTIME_CI_COMMANDS',
1291
+ CI_COMMANDS_BACKEND: 'KIMI_RUNTIME_CI_COMMANDS_BACKEND',
1292
+ CI_COMMANDS_FRONTEND: 'KIMI_RUNTIME_CI_COMMANDS_FRONTEND',
1293
+ CI_COMMANDS_FULL: 'KIMI_RUNTIME_CI_COMMANDS_FULL',
1294
+ CI_COMMON_PITFALLS: 'KIMI_RUNTIME_CI_COMMON_PITFALLS',
1295
+ CI_CRITICAL_WARNINGS: 'KIMI_RUNTIME_CI_CRITICAL_WARNINGS',
1296
+ CI_KNOWN_GAPS: 'KIMI_RUNTIME_CI_KNOWN_GAPS',
1297
+ CODE_QUALITY_CHECKLIST: 'KIMI_RUNTIME_CODE_QUALITY_CHECKLIST',
1298
+ CODE_QUALITY_STANDARDS: 'KIMI_RUNTIME_CODE_QUALITY_STANDARDS',
1299
+ COMPETITIVE_LANDSCAPE: 'KIMI_RUNTIME_COMPETITIVE_LANDSCAPE',
1300
+ DEPENDENCY_CHECK_COMMANDS: 'KIMI_RUNTIME_DEPENDENCY_CHECK_COMMANDS',
1301
+ DOMAIN_EXPERTISE: 'KIMI_RUNTIME_DOMAIN_EXPERTISE',
1302
+ DOMAIN_KNOWLEDGE: 'KIMI_RUNTIME_DOMAIN_KNOWLEDGE',
1303
+ FRONTEND_ARCHITECTURE_DIAGRAM: 'KIMI_RUNTIME_FRONTEND_ARCHITECTURE_DIAGRAM',
1304
+ FRONTEND_CRITICAL_RULES: 'KIMI_RUNTIME_FRONTEND_CRITICAL_RULES',
1305
+ FRONTEND_EXPERTISE: 'KIMI_RUNTIME_FRONTEND_EXPERTISE',
1306
+ FRONTEND_LAYER_CONVENTIONS: 'KIMI_RUNTIME_FRONTEND_LAYER_CONVENTIONS',
1307
+ FRONTEND_STACK: 'KIMI_RUNTIME_FRONTEND_STACK',
1308
+ FRONTEND_TECH_LIST: 'KIMI_RUNTIME_FRONTEND_TECH_LIST',
1309
+ GIT_ACCESS: 'KIMI_RUNTIME_GIT_ACCESS',
1310
+ JIRA_BASE_URL: 'KIMI_RUNTIME_JIRA_BASE_URL',
1311
+ JIRA_PROJECT_KEY: 'KIMI_RUNTIME_JIRA_PROJECT_KEY',
1312
+ KEY_FILE_PATHS: 'KIMI_RUNTIME_KEY_FILE_PATHS',
1313
+ LAYER_CLAUDE_MD_PATHS: 'KIMI_RUNTIME_LAYER_CONTEXT_PATHS',
1314
+ LAYER_CONVENTIONS: 'KIMI_RUNTIME_LAYER_CONVENTIONS',
1315
+ LAYER_LIST: 'KIMI_RUNTIME_LAYER_LIST',
1316
+ LAYER_NAME: 'KIMI_RUNTIME_LAYER_NAME',
1317
+ LAYER_PATH: 'KIMI_RUNTIME_LAYER_PATH',
1318
+ LAYER_TAGS: 'KIMI_RUNTIME_LAYER_TAGS',
1319
+ MAINTAINER_PERSONA_LINE: 'KIMI_RUNTIME_MAINTAINER_PERSONA_LINE',
1320
+ MAX_SCORE: 'KIMI_RUNTIME_PERSONA_MAX_SCORE',
1321
+ PERSONA_COUNT: 'KIMI_RUNTIME_PERSONA_COUNT',
1322
+ PERSONA_FILES: 'KIMI_RUNTIME_PERSONA_FILES',
1323
+ PERSONA_FILE_LIST: 'KIMI_RUNTIME_PERSONA_FILE_LIST',
1324
+ PERSONA_FILE_READ_LIST: 'KIMI_RUNTIME_PERSONA_FILE_READ_LIST',
1325
+ PERSONA_FIT_FORMAT: 'KIMI_RUNTIME_PERSONA_FIT_FORMAT',
1326
+ PERSONA_NAMES: 'KIMI_RUNTIME_PERSONA_NAMES',
1327
+ PERSONA_NAMES_WITH_ROLES: 'KIMI_RUNTIME_PERSONA_NAMES_WITH_ROLES',
1328
+ PERSONA_SCORE_FORMAT: 'KIMI_RUNTIME_PERSONA_SCORE_FORMAT',
1329
+ PERSONA_SCORE_HEADERS: 'KIMI_RUNTIME_PERSONA_SCORE_HEADERS',
1330
+ PERSONA_SCORE_SEPARATORS: 'KIMI_RUNTIME_PERSONA_SCORE_SEPARATORS',
1331
+ PERSONA_VPC_SECTIONS: 'KIMI_RUNTIME_PERSONA_VPC_SECTIONS',
1332
+ PR_CREATE_CMD: 'KIMI_PR_CREATE',
1333
+ PROJECT_CONTEXT: 'KIMI_RUNTIME_PROJECT_CONTEXT',
1334
+ TECH_EXPERTISE: 'KIMI_RUNTIME_TECH_EXPERTISE',
1335
+ TEST_QUALITY_CHECKLIST: 'KIMI_RUNTIME_TEST_QUALITY_CHECKLIST',
1336
+ TEST_RUNNER_CHECK: 'KIMI_RUNTIME_TEST_RUNNER_CHECK',
1337
+ WARNINGS: 'KIMI_RUNTIME_WARNINGS',
1338
+ };
1339
+ function writeKimiWorkflowSkill(args) {
1340
+ if (!pathExists(args.src))
1341
+ return;
1342
+ const { body, description } = stripFrontmatter(readTextFile(args.src));
1343
+ const skillName = `specrails-${args.commandName}`;
1344
+ const providerNeutral = renderPlaceholders(body, {
1345
+ ...KIMI_RUNTIME_PLACEHOLDERS,
1346
+ ...args.placeholders,
1347
+ MEMORY_PATH: '.kimi-code/agent-memory/',
1348
+ }).replaceAll('.specrails/profiles/project-default.json', '.specrails/profiles/kimi-default.json');
1349
+ const rendered = translateClaudeTextForKimi(adaptKimiWorkflowBody(args.commandName, providerNeutral));
1350
+ const frontmatter = [
1351
+ '---',
1352
+ `name: ${skillName}`,
1353
+ `description: ${JSON.stringify(translateClaudeTextForKimi(renderPlaceholders(description ?? `SpecRails ${args.commandName} workflow for Kimi Code.`, {
1354
+ ...KIMI_RUNTIME_PLACEHOLDERS,
1355
+ ...args.placeholders,
1356
+ })))}`,
1357
+ 'type: prompt',
1358
+ '---',
1359
+ '',
1360
+ ].join('\n');
1361
+ writeFileLf(args.dest, frontmatter +
1362
+ KIMI_NESTED_SKILL_CONTRACT +
1363
+ KIMI_ROLE_EXECUTION_CONTRACT +
1364
+ KIMI_RUNTIME_CONTEXT_CONTRACT +
1365
+ rendered);
1366
+ }
1367
+ function adaptKimiWorkflowBody(commandName, body) {
1368
+ if (commandName === 'batch-implement') {
1369
+ return adaptKimiBatchImplement(body);
1370
+ }
1371
+ if (commandName === 'auto-propose-backlog-specs') {
1372
+ return adaptKimiAutoPropose(body);
1373
+ }
1374
+ if (commandName === 'enrich') {
1375
+ return renderKimiEnrichWorkflow();
1376
+ }
1377
+ if (commandName === 'reconfig') {
1378
+ return renderKimiReconfigWorkflow();
1379
+ }
1380
+ if (commandName === 'telemetry') {
1381
+ return renderKimiTelemetryWorkflow();
1382
+ }
1383
+ if (commandName === 'retry') {
1384
+ return adaptKimiRetry(body);
1385
+ }
1386
+ if (commandName !== 'implement')
1387
+ return body;
1388
+ let adapted = replaceMarkdownSection(body, '##### Apply per-agent model overrides (profile mode only)', '##### Legacy mode — preserve current behavior', [
1389
+ '##### Resolve per-role model overrides (profile mode only)',
1390
+ '',
1391
+ 'Keep each `AGENT_MODEL[id]` value exactly as declared in the profile.',
1392
+ 'Do **not** rewrite role `SKILL.md` frontmatter: Kimi directory skills do',
1393
+ 'not carry per-role model configuration. In the orchestrator, resolve the',
1394
+ 'role id against the parsed `AGENT_MODEL` map, then write the exact result',
1395
+ 'into that role wave entry\'s JSON `model` field using WriteFile. Use',
1396
+ 'the provider default `k3` when absent; never depend on a shell array from',
1397
+ 'a previous tool call. Only the official short ids `k3`,',
1398
+ '`kimi-for-coding`, and `kimi-for-coding-highspeed` gain the',
1399
+ '`kimi-code/` prefix at the CLI boundary. Never map Claude aliases.',
1400
+ '',
1401
+ ].join('\n'));
1402
+ adapted = replaceMarkdownSection(adapted, '##### Legacy mode — preserve current behavior', '##### Agent roles (both modes)', [
1403
+ '##### Legacy mode — preserve current behavior',
1404
+ '',
1405
+ 'If no profile is active, discover role directories without mutating them:',
1406
+ '',
1407
+ '```bash',
1408
+ 'AVAILABLE_AGENTS="$(find -L .kimi-code/skills -mindepth 2 -maxdepth 2 \\',
1409
+ ' -type f -name SKILL.md -print 2>/dev/null | sed \'s|.*/skills/||;s|/SKILL.md$||\' \\',
1410
+ ' | grep -E \'^(sr|custom)-\' | sort)"',
1411
+ 'PROFILE_MODE="legacy"',
1412
+ 'PROFILE_NAME=""',
1413
+ '```',
1414
+ '',
1415
+ 'Per-role model overrides are empty in legacy mode. External role',
1416
+ 'processes use the explicit Kimi default (`k3`, launched as',
1417
+ '`kimi-code/k3`).',
1418
+ '',
1419
+ ].join('\n'));
1420
+ adapted = replaceMarkdownSection(adapted, '#### Merge Algorithm', '**Step 4: Record outcomes**', [
1421
+ '#### Kimi role-wave merge algorithm',
1422
+ '',
1423
+ 'The role-wave contract above overrides the generic runtime-supplied',
1424
+ 'worktree assumptions. Use the stable run id chosen for this workflow.',
1425
+ 'First run this command (the run id grammar is validated before git):',
1426
+ '',
1427
+ '```sh',
1428
+ 'node .kimi-code/specrails/run-skill.mjs --role-wave-status <stable-run-id>',
1429
+ '```',
1430
+ '',
1431
+ 'The single `specrails.merge.inventory` frame supplies `baseCommit`,',
1432
+ '`manifestPath`, and each safe worktree id, `repoDir`, and complete',
1433
+ '`changes` list. Every change is `{status:"A"|"M"|"D",path}`. This',
1434
+ 'inventory compares against the synthetic baseline snapshot, includes',
1435
+ 'committed/staged/unstaged and non-ignored untracked role output, and',
1436
+ 'excludes `.kimi-code` plus SpecRails run-state. Never discover changed',
1437
+ 'files with a shell loop, newline splitting, or a hard-coded `main` ref.',
1438
+ '',
1439
+ 'Classify paths across all worktrees before applying anything:',
1440
+ '- `exclusive_files`: appears in one worktree only.',
1441
+ '- `shared_files`: appears in two or more worktrees.',
1442
+ '- Preserve each A/M/D status; a D path has no source file to copy.',
1443
+ '',
1444
+ '**Exclusive A/M/D actions**',
1445
+ '',
1446
+ 'For each feature in `MERGE_ORDER`, use structured WriteFile (never shell',
1447
+ 'interpolation) to write `.specrails/kimi-role-merge.json`:',
1448
+ '',
1449
+ '```json',
1450
+ '{',
1451
+ ' "run": "<stable-run-id>",',
1452
+ ' "actions": [',
1453
+ ' {"worktree":"<safe-id>","path":"<exact-git-path>","operation":"copy"},',
1454
+ ' {"worktree":"<safe-id>","path":"<deleted-path>","operation":"delete"}',
1455
+ ' ]',
1456
+ '}',
1457
+ '```',
1458
+ '',
1459
+ 'Use `copy` for A/M and `delete` for D, then run exactly:',
1460
+ '',
1461
+ '```sh',
1462
+ 'node .kimi-code/specrails/run-skill.mjs \\',
1463
+ ' --role-merge-file .specrails/kimi-role-merge.json',
1464
+ '```',
1465
+ '',
1466
+ 'The helper validates the one-shot file, manifest, registered worktree,',
1467
+ 'and path containment, then copies bytes/symlinks or deletes the target',
1468
+ 'without a shell. Filenames may contain spaces, Unicode, quotes, `$()`,',
1469
+ 'or leading dashes; never place them in a Bash command. It rejects',
1470
+ 'provider/run-state paths, traversal, duplicate targets, directories,',
1471
+ 'and symlinked target parents.',
1472
+ '',
1473
+ '**Shared paths**',
1474
+ '',
1475
+ 'Process shared paths in `MERGE_ORDER`:',
1476
+ '1. D in every contributor: submit one validated `delete` action.',
1477
+ '2. D versus A/M: record a delete/modify conflict; do not silently copy',
1478
+ ' or delete it.',
1479
+ '3. A/M text: use structured ReadFile on each emitted `repoDir` + exact',
1480
+ ' path and on the current merge target. Apply the existing Markdown',
1481
+ ' section-aware strategy for `.md`; for other text perform a three-way',
1482
+ ' semantic merge against the current target, writing through WriteFile.',
1483
+ '4. Binary/type conflicts: record them for `sr-merge-resolver`; never',
1484
+ ' decode or round-trip binary data through model text.',
1485
+ '5. A resolved whole-file winner may be applied with one validated copy',
1486
+ ' action. Any unresolved region receives the existing conflict markers',
1487
+ ' and `MERGE_REPORT` entry.',
1488
+ '',
1489
+ 'When `DRY_RUN=true`, do not invoke the repository merge-action helper.',
1490
+ 'Write resolved A/M outputs under `CACHE_DIR` with structured WriteFile',
1491
+ 'and record D paths as deletion operations in `.cache-manifest.json`.',
1492
+ 'Keep worktrees for inspection as the surrounding dry-run rule requires.',
1493
+ '',
1494
+ ].join('\n'));
1495
+ adapted = adapted
1496
+ .replace(' "implemented_files": [],', [
1497
+ ' "implemented_files": [],',
1498
+ ' "kimi_role_wave": {',
1499
+ ' "run": "<stable-run-id>",',
1500
+ ' "manifest_path": ".specrails/kimi-role-worktrees/<stable-run-id>.json",',
1501
+ ' "base_commit": null,',
1502
+ ' "workspaces": {}',
1503
+ ' },',
1504
+ ].join('\n'))
1505
+ .replace('If the write succeeds: set `PIPELINE_STATE_AVAILABLE=true`.', [
1506
+ 'If the write succeeds: set `PIPELINE_STATE_AVAILABLE=true`.',
1507
+ '',
1508
+ '**Kimi retry state:** after every `specrails.role.workspace` frame,',
1509
+ 'atomically refresh `kimi_role_wave.manifest_path`, `base_commit`, and',
1510
+ '`workspaces[<feature-id>]` from the helper output. Never synthesize',
1511
+ 'these values. Keep the same `run` and `worktree:<feature-id>` for that',
1512
+ 'feature through developer, test, docs, and review. On any failure keep',
1513
+ 'the manifest and worktrees. After every required change has been merged',
1514
+ 'successfully, run the static cleanup command from the Kimi role',
1515
+ 'contract and set `kimi_role_wave` to `null` in pipeline state.',
1516
+ ].join('\n'))
1517
+ .replaceAll('git -C <worktree-path> diff main --name-only', 'git -C <worktree-path> diff <base-commit> --name-only')
1518
+ .replaceAll('git -C <worktree-path> diff main -- <file>', 'git -C <worktree-path> diff <base-commit> -- <file>')
1519
+ .replace('(`<worktree-path>` is an absolute git-worktree path supplied by the runtime; `git -C <worktree-path>` already targets it directly.)', '(`<worktree-path>` is the `repoDir` emitted by the role-wave helper, and `<base-commit>` is read from its persisted manifest; `git -C <worktree-path>` already targets it directly.)');
1520
+ return adapted;
1521
+ }
1522
+ function adaptKimiBatchImplement(body) {
1523
+ return replaceMarkdownSection(body, '### Wave invocation', '### Failure isolation', [
1524
+ '### Kimi wave invocation',
1525
+ '',
1526
+ 'Nested `specrails-implement` executions are independent foreground Kimi',
1527
+ 'processes. Do not call multiple built-in `Skill` tools in one Kimi',
1528
+ 'session and do not share one checkout concurrently.',
1529
+ '',
1530
+ 'Choose one safe `BATCH_RUN` id. For dependency wave `W`, derive the',
1531
+ 'deterministic safe run id `<BATCH_RUN>-w<W>`. Process waves sequentially:',
1532
+ '',
1533
+ '1. For a normal repository launch, partition each dependency wave into',
1534
+ ' foreground batches of at most `min(CONCURRENCY,32)` entries. Each entry uses',
1535
+ ' `skill:"specrails-implement"`, `workspace:"worktree:<feature-id>"`,',
1536
+ ' the complete `<ref> [--dry-run]` arguments, the selected profile stem',
1537
+ ' (or `"inherit"`), and that profile\'s exact `orchestrator.model` (or',
1538
+ ' `k3`). Feature ids and keys must be collision-free safe ids.',
1539
+ '2. Wait for all completion frames. A failed entry fails only that ticket;',
1540
+ ' preserve its manifest/worktree for diagnosis and record the failure.',
1541
+ '3. Before a downstream dependency wave, call `--role-wave-status` for',
1542
+ ' the completed wave. Merge each successful worktree\'s A/M/D inventory',
1543
+ ' into the batch repository with the same structured merge-file and',
1544
+ ' shared-path rules defined by `specrails-implement`. Never interpolate',
1545
+ ' a filename into Shell. If merge succeeds, run',
1546
+ ' `node .kimi-code/specrails/run-skill.mjs --role-wave-cleanup <run>`.',
1547
+ ' This makes predecessor output part of the next wave\'s newly captured',
1548
+ ' synthetic baseline. Do not cleanup failed or unmerged worktrees.',
1549
+ '4. Record `{ref,wave,status,profile,error_summary,run,manifest_path,',
1550
+ ' workspace}` in `WAVE_RESULTS` before starting another batch.',
1551
+ '',
1552
+ 'Inside a specrails-desktop isolated rail worktree, effective concurrency',
1553
+ 'is exactly 1. Submit a one-entry foreground role wave per ticket with',
1554
+ '`workspace:"current"` and a deterministic unique run id; wait before the',
1555
+ 'next ticket. No sibling worktree, status merge, or cleanup is needed',
1556
+ 'because every nested implementation writes directly into the desktop',
1557
+ 'rail\'s current repository.',
1558
+ '',
1559
+ 'Per-ticket profiles remain isolated: `profile` is either `inherit` or the',
1560
+ 'validated filename stem from `PROFILE_MAP`; `model` is resolved from the',
1561
+ 'same profile before writing JSON. Never export a profile globally.',
1562
+ '',
1563
+ ].join('\n'));
1564
+ }
1565
+ function adaptKimiAutoPropose(body) {
1566
+ return body
1567
+ .replace('Launch a **single** explorer subagent (`subagent_type: Explore`, `run_in_background: true`) for product discovery.', [
1568
+ 'Launch one **sr-product-analyst** role in a foreground Kimi role wave.',
1569
+ 'Use `skill:"sr-product-analyst"`, `workspace:"current"`,',
1570
+ '`profile:"inherit"`, and pass the complete discovery prompt below as',
1571
+ '`args`. Before writing the wave, enumerate regular non-symlink',
1572
+ '`.kimi-code/personas/*.md` files and include their exact paths plus',
1573
+ 'contents in that context; stop with guidance to run',
1574
+ '`Skill(skill="specrails-enrich", args="")` when none exist. Wait for its attributed',
1575
+ 'completion/output frames. Kimi has no Claude Explore subagent type;',
1576
+ 'never select a non-existent Explore role.',
1577
+ ].join(' '))
1578
+ .replaceAll('The Explore agent receives this prompt:', 'The sr-product-analyst role receives this prompt:')
1579
+ .replaceAll('After the Explore agent completes:', 'After the sr-product-analyst role completes:');
1580
+ }
1581
+ function adaptKimiRetry(body) {
1582
+ return body
1583
+ .replace('- `PHASE_STATUSES` ← `phases` map (`architect`, `developer`, `test-writer`, `doc-sync`, `reviewer`, `ship`, `ci` → `"done"`, `"failed"`, `"skipped"`, or `"pending"`)', [
1584
+ '- `PHASE_STATUSES` ← `phases` map (`architect`, `developer`, `test-writer`, `doc-sync`, `reviewer`, `ship`, `ci` → `"done"`, `"failed"`, `"skipped"`, or `"pending"`)',
1585
+ '- `KIMI_ROLE_WAVE` ← `kimi_role_wave` (required when an isolated Kimi',
1586
+ ' phase has already started): persisted `run`, `manifest_path`,',
1587
+ ' `base_commit`, and feature→workspace mapping.',
1588
+ ].join('\n'))
1589
+ .replace('**Validation:**', [
1590
+ '**Kimi workspace validation (before any phase):**',
1591
+ '',
1592
+ 'If `KIMI_ROLE_WAVE` is non-null, validate its safe run id by invoking',
1593
+ '`node .kimi-code/specrails/run-skill.mjs --role-wave-status <run>`.',
1594
+ 'The returned manifest path, base commit, and workspace ids must exactly',
1595
+ 'match pipeline state. Any mismatch/missing/unregistered worktree is a',
1596
+ 'hard stop: report recovery instructions and do not create a replacement',
1597
+ 'worktree. A retry must use the same run and exact',
1598
+ '`worktree:<feature-id>` mapping so successful developer changes survive',
1599
+ 'a later test/docs/reviewer failure. Refresh state from emitted frames',
1600
+ 'after each resumed role. Never choose a new run while valid state',
1601
+ 'exists; never cleanup before every required phase and merge succeeds.',
1602
+ '',
1603
+ '**Validation:**',
1604
+ ].join('\n'))
1605
+ .replace('Include PR URL if ship ran successfully.', [
1606
+ 'Include PR URL if ship ran successfully.',
1607
+ '',
1608
+ 'After all required isolated outputs have been safely merged, invoke the',
1609
+ 'static `--role-wave-cleanup <run>` helper. Only after its cleanup frame',
1610
+ 'succeeds set `kimi_role_wave` to `null`. A failed retry retains state.',
1611
+ ].join('\n'));
1612
+ }
1613
+ function renderKimiEnrichWorkflow() {
1614
+ return [
1615
+ '# Enrich SpecRails for Kimi Code',
1616
+ '',
1617
+ 'Refresh the Kimi-native SpecRails installation, analyze this repository,',
1618
+ 'and maintain project context/personas without generating Claude artifacts.',
1619
+ 'Kimi skills are managed provider artifacts; never rewrite their SKILL.md',
1620
+ 'frontmatter or create Claude-style command/agent trees.',
1621
+ '',
1622
+ '## Mode selection',
1623
+ '',
1624
+ 'Parse `$ARGUMENTS`: `--update`, `--quick`, and `--from-config` are mutually',
1625
+ 'exclusive. With no flag run interactive full mode.',
1626
+ '',
1627
+ '1. Resolve the repository as `${SPECRAILS_REPO_DIR:-.}` and verify',
1628
+ ' `.kimi-code/specrails/run-skill.mjs` plus',
1629
+ ' `.specrails/install-config.yaml` exist.',
1630
+ '2. Read install config and require provider `kimi` (or an explicitly',
1631
+ ' provider-neutral legacy config). Refuse a different provider.',
1632
+ '3. Refresh managed provider artifacts with the installed Core CLI:',
1633
+ ' `npx specrails-core update --provider kimi --root-dir "${SPECRAILS_REPO_DIR:-.}"`.',
1634
+ ' Use the process result as a hard gate. This provider-aware materializer',
1635
+ ' regenerates direct-child workflows/roles, rules, runner, OpenSpec skills,',
1636
+ ' settings, manifest, and framework links. Do not reproduce its templates',
1637
+ ' with model-authored file copying.',
1638
+ '4. Validate `.specrails/profiles/kimi-default.json`: schemaVersion 1,',
1639
+ ' provider `kimi`, required architect/developer/reviewer, unique role ids,',
1640
+ ' safe model ids, and valid routing. Preserve exact model identifiers.',
1641
+ '',
1642
+ '## Quick mode',
1643
+ '',
1644
+ 'Inspect package/build metadata and the top-level source tree. Atomically',
1645
+ 'write `.kimi-code/project-context.md` with stack, architecture, test/lint/',
1646
+ 'build commands, repository conventions, and the UTC refresh time. Preserve',
1647
+ 'existing `.kimi-code/personas/`. Report that full mode can add VPC personas.',
1648
+ '',
1649
+ '## From-config mode',
1650
+ '',
1651
+ 'Do not ask questions. Apply the tier, selected agents, backlog, git, and',
1652
+ 'model choices already present in install config/profile; the Core update is',
1653
+ 'the only artifact-generation authority. For quick tier run Quick mode. For',
1654
+ 'full tier perform the same repository analysis as Full mode, retain existing',
1655
+ 'personas when present, and generate conservative personas only when the',
1656
+ 'config enables product roles and the persona directory is empty.',
1657
+ '',
1658
+ '## Update mode',
1659
+ '',
1660
+ 'Run the provider-aware refresh, re-analyze commands/conventions, and',
1661
+ 'atomically refresh only `.kimi-code/project-context.md`. Keep user personas,',
1662
+ 'custom-* skills, agent memory, profiles, MCP configuration, and security',
1663
+ 'exemptions byte-for-byte. Report stale/missing persona references but do not',
1664
+ 'invent replacements.',
1665
+ '',
1666
+ '## Full mode',
1667
+ '',
1668
+ 'Analyze the complete codebase and present findings. Ask concise questions',
1669
+ 'about target users, pains, gains, product goals, and repository shipping',
1670
+ 'policy. Research externally only with user-approved network tooling.',
1671
+ 'Generate 2–4 Value Proposition Canvas personas as real Markdown files under',
1672
+ '`.kimi-code/personas/<safe-kebab-id>.md`; include jobs, pains, gains,',
1673
+ 'behavior, success criteria, and evidence/assumptions. On OSS projects also',
1674
+ 'materialize the bundled maintainer persona from setup templates. Never',
1675
+ 'overwrite an existing persona without showing the proposed change.',
1676
+ '',
1677
+ 'Refresh `.kimi-code/project-context.md` and ensure every selected product',
1678
+ 'role has an agent-memory directory. Workflows discover persona files at',
1679
+ 'runtime, so do not fork or mutate framework-owned skills to embed a static',
1680
+ 'persona list.',
1681
+ '',
1682
+ '## Verification and report',
1683
+ '',
1684
+ 'Run `npx specrails-core doctor --provider kimi --root-dir',
1685
+ '"${SPECRAILS_REPO_DIR:-.}"`. Verify every immediate skill directory has one',
1686
+ 'valid SKILL.md, no role is nested under `skills/rails`, no unresolved',
1687
+ 'template token remains, the Kimi profile validates, and no Claude model',
1688
+ 'alias/path was generated. Report mode, provider version, context/persona',
1689
+ 'files, selected roles, exact models, and doctor result.',
1690
+ '',
1691
+ ].join('\n');
1692
+ }
1693
+ function renderKimiReconfigWorkflow() {
1694
+ return [
1695
+ '# Reconfig: apply Kimi models to a provider profile',
1696
+ '',
1697
+ 'Kimi role skills do not carry per-role model frontmatter. Reconfiguration',
1698
+ 'updates a provider-bound profile; workflows read that profile and put each',
1699
+ 'exact model id in the structured role wave.',
1700
+ '',
1701
+ '1. Parse `$ARGUMENTS` as optional `--profile <safe-name>`; default to the',
1702
+ ' active `SPECRAILS_PROFILE_PATH`, then',
1703
+ ' `.specrails/profiles/kimi-default.json`.',
1704
+ '2. Require a regular non-symlink JSON file inside `.specrails/profiles/`.',
1705
+ ' Validate it against profile schema v1, require `provider:"kimi"`, unique',
1706
+ ' agents, the baseline trio, valid routing references, and Kimi-safe model',
1707
+ ' ids (`^[A-Za-z0-9][A-Za-z0-9._/:-]*$`, maximum 128 characters).',
1708
+ '3. Read `.specrails/agents.yaml` only as an optional legacy input. Ask for',
1709
+ ' confirmation before migrating its defaults/per-agent model values. Claude',
1710
+ ' aliases `opus`, `sonnet`, and `haiku` are not Kimi models and must never',
1711
+ ' be translated silently; require an explicit Kimi replacement.',
1712
+ '4. Present the orchestrator and per-role old→new model table. Apply approved',
1713
+ ' edits to a complete in-memory profile object, validate again, then use',
1714
+ ' structured WriteFile for one atomic logical replacement. Preserve name,',
1715
+ ' description, required flags, agent order, and routing.',
1716
+ '5. Re-read and validate the result. Report changed/unchanged/skipped roles.',
1717
+ '',
1718
+ 'Never edit any role SKILL.md under `.kimi-code/skills/`, never create Claude agent files,',
1719
+ 'and never put a model id in a shell command.',
1720
+ '',
1721
+ ].join('\n');
1722
+ }
1723
+ function renderKimiTelemetryWorkflow() {
1724
+ return [
1725
+ '# Kimi agent telemetry',
1726
+ '',
1727
+ 'Analyze real Kimi Code session usage for this repository. Accepted flags:',
1728
+ '`--period today|week|all` (default `week`), `--agent <id>`,',
1729
+ '`--format markdown|json`, and `--save`. Cost is not derivable from Kimi logs and must remain',
1730
+ '`null`/`unavailable`; never apply a Claude or invented rate card.',
1731
+ '',
1732
+ '## Discover and validate sessions',
1733
+ '',
1734
+ 'Read `~/.kimi-code/session_index.jsonl` line by line. Ignore only a final',
1735
+ 'truncated JSON line; warn on any other malformed line. Fold records by',
1736
+ '`sessionId`, keeping the latest valid entry and honoring an explicit latest',
1737
+ 'deletion/tombstone record. A live entry provides `sessionId`, `sessionDir`,',
1738
+ 'and `workDir`. Require safe scalar strings, match canonical `workDir` to',
1739
+ '`${SPECRAILS_REPO_DIR:-.}`, and require canonical `sessionDir` to remain',
1740
+ 'inside `~/.kimi-code/sessions/`. Reject symlinks/path traversal and ignore',
1741
+ 'missing or deleted session directories.',
1742
+ '',
1743
+ 'For each accepted session, read its regular non-symlink `state.json` and',
1744
+ 'validate its `workDir`, timestamps, and title. Then scan only regular',
1745
+ '`agents/*/wire.jsonl` files within that same session directory. Tolerate a',
1746
+ 'truncated final line; count and warn on other malformed records.',
1747
+ '',
1748
+ '## Usage schema and attribution',
1749
+ '',
1750
+ 'Consume only records with `type:"usage.record"` and a safe `model` plus',
1751
+ '`usage:{inputOther,output,inputCacheRead,inputCacheCreation}`. Treat absent',
1752
+ 'numeric counters as zero; reject negative/non-finite values. Preserve',
1753
+ '`usageScope` and aggregate input-other, output, cache-read, cache-creation,',
1754
+ 'and total tokens per model/session.',
1755
+ '',
1756
+ 'Attribute an external role session by canonical workDir: first use its',
1757
+ '`.specrails-role-workspace.json` marker; otherwise match `repoDir` in valid',
1758
+ '`.specrails/kimi-role-worktrees/*.json` manifests. Attribute the top-level',
1759
+ 'session to `orchestrator`; use `unknown` only when no verified mapping',
1760
+ 'exists. Apply period and agent filters after attribution. Deduplicate by',
1761
+ 'session id + agent wire path + record position.',
1762
+ '',
1763
+ 'Duration comes only from validated state timestamps. The wire schema does',
1764
+ 'not provide a trustworthy role success/failure outcome, so expose that',
1765
+ 'metric as unavailable rather than inferring it from the last event.',
1766
+ '',
1767
+ '## Output',
1768
+ '',
1769
+ 'Show session/run count, duration, exact models, and all four token counters',
1770
+ 'per role plus totals and cache ratio. In JSON use',
1771
+ '`cost_usd:null`, `avg_cost_per_run_usd:null`, and',
1772
+ '`success_rate:null` with `unavailable_reason`. In Markdown print',
1773
+ '`Cost: unavailable (Kimi logs contain usage, not billing rates)`.',
1774
+ 'Recommendations may discuss token/cache/runtime outliers only.',
1775
+ '',
1776
+ 'With `--save`, write the same JSON object under',
1777
+ '`.kimi-code/telemetry/<UTC-date>-<period>.json` using structured WriteFile;',
1778
+ 'never include prompt text, credentials, raw wire records, or paths outside',
1779
+ 'the repository/session identifiers.',
1780
+ '',
1781
+ ].join('\n');
1782
+ }
1783
+ function replaceMarkdownSection(body, startHeading, endHeading, replacement) {
1784
+ const start = body.indexOf(startHeading);
1785
+ if (start < 0)
1786
+ return body;
1787
+ const end = body.indexOf(endHeading, start + startHeading.length);
1788
+ if (end < 0)
1789
+ return body;
1790
+ return body.slice(0, start) + replacement + body.slice(end);
1791
+ }
1792
+ function writeKimiRoleSkill(args) {
1793
+ if (!pathExists(args.src))
1794
+ return;
1795
+ const { body, description } = stripFrontmatter(readTextFile(args.src));
1796
+ const rendered = translateClaudeTextForKimi(renderPlaceholders(body, {
1797
+ ...KIMI_RUNTIME_PLACEHOLDERS,
1798
+ ...args.placeholders,
1799
+ MEMORY_PATH: `.kimi-code/agent-memory/${args.roleId}/`,
1800
+ }));
1801
+ const frontmatter = [
1802
+ '---',
1803
+ `name: ${args.roleId}`,
1804
+ `description: ${JSON.stringify(translateClaudeTextForKimi(renderPlaceholders(description ?? `SpecRails ${args.roleId} role for Kimi Code.`, {
1805
+ ...KIMI_RUNTIME_PLACEHOLDERS,
1806
+ ...args.placeholders,
1807
+ })))}`,
1808
+ 'type: prompt',
1809
+ '---',
1810
+ '',
1811
+ ].join('\n');
1812
+ writeFileLf(args.dest, frontmatter +
1813
+ KIMI_NESTED_SKILL_CONTRACT +
1814
+ KIMI_RUNTIME_CONTEXT_CONTRACT +
1815
+ rendered);
1816
+ }
767
1817
  /**
768
1818
  * Strip a leading `---`-delimited YAML frontmatter block and return the
769
1819
  * remaining body plus the `description:` value if present. Defensive
@@ -1014,6 +2064,66 @@ function applyGeminiSettings(input) {
1014
2064
  }
1015
2065
  return written;
1016
2066
  }
2067
+ /**
2068
+ * Kimi keeps both its project instructions and MCP configuration under
2069
+ * `.kimi-code`. Existing user MCP configuration is never rewritten.
2070
+ */
2071
+ function applyKimiSettings(input) {
2072
+ let written = 0;
2073
+ const providerRoot = path.join(input.artifactRoot, input.providerDir);
2074
+ const agentsMdPath = path.join(providerRoot, 'AGENTS.md');
2075
+ const content = renderInitialKimiAgentsMd(input.codeRoot);
2076
+ if (!pathExists(agentsMdPath)) {
2077
+ writeFileLf(agentsMdPath, content);
2078
+ written++;
2079
+ }
2080
+ else {
2081
+ const existing = readTextFile(agentsMdPath);
2082
+ const next = upsertAgentsMdManagedBlock(existing, extractManagedBlock(content));
2083
+ if (next !== existing) {
2084
+ writeFileLf(agentsMdPath, next);
2085
+ written++;
2086
+ }
2087
+ }
2088
+ const mcpPath = path.join(providerRoot, 'mcp.json');
2089
+ if (!pathExists(mcpPath)) {
2090
+ writeFileLf(mcpPath, '{\n "mcpServers": {}\n}\n');
2091
+ written++;
2092
+ }
2093
+ return written;
2094
+ }
2095
+ function renderInitialKimiAgentsMd(repoRoot) {
2096
+ const projectName = path.basename(repoRoot);
2097
+ return [
2098
+ AGENTS_MD_START,
2099
+ '',
2100
+ `# ${projectName} — Kimi Code instructions`,
2101
+ '',
2102
+ 'This project uses SpecRails skills under `.kimi-code/skills/`.',
2103
+ 'Kimi discovers only direct child skill directories. Interactive TUI sessions',
2104
+ 'invoke workflows as `/skill:specrails-<command>`. Headless prompt mode does',
2105
+ 'not dispatch slash skills, so automation must invoke',
2106
+ '`.kimi-code/specrails/run-skill.mjs`. Role skills live at',
2107
+ '`.kimi-code/skills/<sr-*|custom-*>/SKILL.md` and are launched by workflows in',
2108
+ 'separate helper-managed `kimi -p --output-format stream-json` processes.',
2109
+ '',
2110
+ '## Conventions',
2111
+ '',
2112
+ '- Read project source, `.git`, and `openspec/**` from',
2113
+ ' `${SPECRAILS_REPO_DIR:-.}`.',
2114
+ '- Read provider rules from `.kimi-code/rules/` and runtime memory from',
2115
+ ' `.kimi-code/agent-memory/`.',
2116
+ '- Preserve model ids from provider-aware profiles exactly. The Kimi CLI',
2117
+ ' accepts configured aliases; official short ids use the `kimi-code/`',
2118
+ ' prefix at launch (for example `kimi-code/k3`).',
2119
+ '- OpenSpec workflows are invoked as `/skill:openspec-*`.',
2120
+ '- Kimi is CLI-only: do not start a server, register a service, or copy',
2121
+ ' credentials into this project.',
2122
+ '',
2123
+ AGENTS_MD_END,
2124
+ '',
2125
+ ].join('\n');
2126
+ }
1017
2127
  function renderInitialGeminiMd(repoRoot) {
1018
2128
  const projectName = path.basename(repoRoot);
1019
2129
  return [
@@ -1057,6 +2167,17 @@ function pruneLegacyArtifacts(input) {
1057
2167
  legacyPaths.push(path.join(input.artifactRoot, input.providerDir, 'commands', 'setup.toml'));
1058
2168
  legacyPaths.push(path.join(input.artifactRoot, input.providerDir, 'commands', 'specrails', 'setup.toml'));
1059
2169
  }
2170
+ else if (input.provider === 'kimi') {
2171
+ // Early experimental builds used a Claude-shaped commands/agents layout
2172
+ // inside `.kimi-code`. They also nested role skills one level too deep at
2173
+ // `skills/rails/*`, which Kimi never discovers. Migrate reserved custom
2174
+ // roles and prune only framework-owned nested roles before rendering the
2175
+ // canonical flat layout. MCP config, AGENTS.md and unknown user files stay
2176
+ // untouched.
2177
+ migrateLegacyKimiRoleLayout(path.join(input.artifactRoot, input.providerDir, 'skills'));
2178
+ legacyPaths.push(path.join(input.artifactRoot, input.providerDir, 'commands'));
2179
+ legacyPaths.push(path.join(input.artifactRoot, input.providerDir, 'agents'));
2180
+ }
1060
2181
  else {
1061
2182
  legacyPaths.push(path.join(input.artifactRoot, input.providerDir, 'commands', 'setup.md'));
1062
2183
  legacyPaths.push(path.join(input.artifactRoot, input.providerDir, 'commands', 'specrails', 'setup.md'));
@@ -1171,6 +2292,29 @@ function placeQuickTierArtefacts(input) {
1171
2292
  }
1172
2293
  return { agents: 0, commands: commandsPlaced, rules: 0, skippedAgents: 0 };
1173
2294
  }
2295
+ if (input.provider === 'kimi') {
2296
+ const setupTemplates = path.join(input.artifactRoot, '.specrails', 'setup-templates');
2297
+ const rulesSrc = path.join(setupTemplates, 'rules');
2298
+ const rulesDest = path.join(input.artifactRoot, input.providerDir, 'rules');
2299
+ let rulesPlaced = 0;
2300
+ if (isDir(rulesSrc)) {
2301
+ mkdirp(rulesDest);
2302
+ for (const src of listDir(rulesSrc)) {
2303
+ const name = path.basename(src);
2304
+ if (!name.endsWith('.md'))
2305
+ continue;
2306
+ const rendered = translateClaudeTextForKimi(renderPlaceholders(readTextFile(src), {
2307
+ ...KIMI_RUNTIME_PLACEHOLDERS,
2308
+ PROJECT_NAME: path.basename(input.codeRoot),
2309
+ SECURITY_EXEMPTIONS_PATH: '.kimi-code/security-exemptions.yaml',
2310
+ PERSONA_DIR: '.kimi-code/personas/',
2311
+ }));
2312
+ writeFileLf(path.join(rulesDest, name), rendered);
2313
+ rulesPlaced++;
2314
+ }
2315
+ }
2316
+ return { agents: 0, commands: 0, rules: rulesPlaced, skippedAgents: 0 };
2317
+ }
1174
2318
  const setupTemplates = path.join(input.artifactRoot, '.specrails', 'setup-templates');
1175
2319
  // PROJECT_NAME is the real repo's basename, not the relocated workspace dir.
1176
2320
  const projectName = path.basename(input.codeRoot);
@@ -1447,6 +2591,72 @@ function placeSkills(input) {
1447
2591
  result.skipped += g.skipped;
1448
2592
  result.filesCopied += g.filesCopied;
1449
2593
  }
2594
+ if (input.provider === 'kimi') {
2595
+ const setupRoot = path.join(input.artifactRoot, '.specrails', 'setup-templates');
2596
+ const commandsSrc = path.join(setupRoot, 'commands', 'specrails');
2597
+ const agentsSrc = path.join(setupRoot, 'agents');
2598
+ const selectedAgents = input.selectedAgents
2599
+ ? new Set([...input.selectedAgents, ...CORE_AGENTS])
2600
+ : new Set([...CORE_AGENTS]);
2601
+ const placedRoleIds = new Set();
2602
+ const placeholders = {
2603
+ PROJECT_NAME: path.basename(input.codeRoot),
2604
+ SECURITY_EXEMPTIONS_PATH: '.kimi-code/security-exemptions.yaml',
2605
+ PERSONA_DIR: '.kimi-code/personas/',
2606
+ };
2607
+ if (isDir(agentsSrc)) {
2608
+ for (const src of listDir(agentsSrc)) {
2609
+ const name = path.basename(src);
2610
+ if (!name.endsWith('.md'))
2611
+ continue;
2612
+ const roleId = name.slice(0, -3);
2613
+ if (!input.materializeAllAgents && !selectedAgents.has(roleId))
2614
+ continue;
2615
+ if (!input.materializeAllAgents && QUICK_EXCLUDED_AGENTS.has(roleId)) {
2616
+ result.skipped++;
2617
+ continue;
2618
+ }
2619
+ writeKimiRoleSkill({
2620
+ src,
2621
+ dest: path.join(destBase, roleId, 'SKILL.md'),
2622
+ roleId,
2623
+ placeholders,
2624
+ });
2625
+ placedRoleIds.add(roleId);
2626
+ result.placed++;
2627
+ result.filesCopied++;
2628
+ if (input.seedProjectDirs !== false) {
2629
+ mkdirp(path.join(input.artifactRoot, input.providerDir, 'agent-memory', roleId));
2630
+ }
2631
+ }
2632
+ }
2633
+ const excludedCommands = new Set();
2634
+ if (!input.materializeAllAgents) {
2635
+ for (const dep of COMMAND_AGENT_DEPENDENCIES) {
2636
+ if (!dep.requires.every((role) => placedRoleIds.has(role))) {
2637
+ excludedCommands.add(dep.command);
2638
+ }
2639
+ }
2640
+ }
2641
+ if (isDir(commandsSrc)) {
2642
+ for (const src of listDir(commandsSrc)) {
2643
+ const name = path.basename(src);
2644
+ if (!name.endsWith('.md') || name === 'setup.md')
2645
+ continue;
2646
+ const commandName = name.slice(0, -3);
2647
+ if (excludedCommands.has(commandName))
2648
+ continue;
2649
+ writeKimiWorkflowSkill({
2650
+ src,
2651
+ dest: path.join(destBase, `specrails-${commandName}`, 'SKILL.md'),
2652
+ commandName,
2653
+ placeholders,
2654
+ });
2655
+ result.placed++;
2656
+ result.filesCopied++;
2657
+ }
2658
+ }
2659
+ }
1450
2660
  return result;
1451
2661
  }
1452
2662
  /**