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