specrails-core 4.11.3 → 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.
- package/README.md +56 -20
- package/bin/specrails-core.mjs +343 -20
- package/bin/tui-installer.mjs +103 -52
- package/dist/installer/cli.js +1 -1
- package/dist/installer/cli.js.map +1 -1
- package/dist/installer/commands/doctor.js +486 -24
- package/dist/installer/commands/doctor.js.map +1 -1
- package/dist/installer/commands/framework.js +49 -7
- package/dist/installer/commands/framework.js.map +1 -1
- package/dist/installer/commands/init.js +423 -25
- package/dist/installer/commands/init.js.map +1 -1
- package/dist/installer/commands/update.js +36 -9
- package/dist/installer/commands/update.js.map +1 -1
- package/dist/installer/phases/framework-lifecycle.js +125 -0
- package/dist/installer/phases/framework-lifecycle.js.map +1 -0
- package/dist/installer/phases/install-config.js +157 -5
- package/dist/installer/phases/install-config.js.map +1 -1
- package/dist/installer/phases/manifest.js +27 -2
- package/dist/installer/phases/manifest.js.map +1 -1
- package/dist/installer/phases/prereqs.js +57 -2
- package/dist/installer/phases/prereqs.js.map +1 -1
- package/dist/installer/phases/provider-detect.js +116 -6
- package/dist/installer/phases/provider-detect.js.map +1 -1
- package/dist/installer/phases/scaffold.js +1222 -12
- package/dist/installer/phases/scaffold.js.map +1 -1
- package/dist/installer/runtime/kimi.js +255 -0
- package/dist/installer/runtime/kimi.js.map +1 -0
- package/dist/installer/util/paths.js +12 -0
- package/dist/installer/util/paths.js.map +1 -1
- package/dist/installer/util/registry.js +234 -14
- package/dist/installer/util/registry.js.map +1 -1
- package/docs/README.md +1 -0
- package/docs/deployment.md +6 -7
- package/docs/getting-started.md +11 -7
- package/docs/installation.md +34 -16
- package/docs/plugin-architecture.md +11 -8
- package/docs/updating.md +21 -3
- package/docs/user-docs/cli-reference.md +43 -22
- package/docs/user-docs/codex-vs-claude-code.md +11 -9
- package/docs/user-docs/faq.md +1 -1
- package/docs/user-docs/getting-started-codex.md +5 -8
- package/docs/user-docs/getting-started-kimi.md +423 -0
- package/docs/user-docs/installation.md +49 -14
- package/docs/user-docs/quick-start.md +11 -8
- package/docs/windows.md +29 -4
- package/integration-contract.json +85 -13
- package/package.json +9 -5
- package/schemas/profile.v1.json +67 -5
- package/templates/kimi/specrails/run-skill.mjs +3005 -0
- package/templates/kimi/specrails/vendor/js-yaml/LICENSE +21 -0
- package/templates/kimi/specrails/vendor/js-yaml/NOTICE.md +16 -0
- package/templates/kimi/specrails/vendor/js-yaml/js-yaml.mjs +3856 -0
- 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'
|
|
306
|
-
|
|
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
|
-
|
|
340
|
-
|
|
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
|
-
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
/**
|