kyro-ai 3.3.0 → 3.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/WORKFLOW.yaml +1 -1
  3. package/dist/cli/adapters/codex.d.ts.map +1 -1
  4. package/dist/cli/adapters/codex.js +24 -0
  5. package/dist/cli/adapters/codex.js.map +1 -1
  6. package/dist/cli/adapters/command-skills.d.ts +3 -0
  7. package/dist/cli/adapters/command-skills.d.ts.map +1 -1
  8. package/dist/cli/adapters/command-skills.js +16 -4
  9. package/dist/cli/adapters/command-skills.js.map +1 -1
  10. package/dist/cli/adapters/detection.d.ts +5 -0
  11. package/dist/cli/adapters/detection.d.ts.map +1 -0
  12. package/dist/cli/adapters/detection.js +43 -0
  13. package/dist/cli/adapters/detection.js.map +1 -0
  14. package/dist/cli/adapters/opencode.d.ts.map +1 -1
  15. package/dist/cli/adapters/opencode.js +94 -7
  16. package/dist/cli/adapters/opencode.js.map +1 -1
  17. package/dist/cli/adapters/registry-types.d.ts +31 -0
  18. package/dist/cli/adapters/registry-types.d.ts.map +1 -1
  19. package/dist/cli/adapters/registry.d.ts.map +1 -1
  20. package/dist/cli/adapters/registry.js +34 -3
  21. package/dist/cli/adapters/registry.js.map +1 -1
  22. package/dist/cli/adapters/standard.d.ts.map +1 -1
  23. package/dist/cli/adapters/standard.js +25 -0
  24. package/dist/cli/adapters/standard.js.map +1 -1
  25. package/dist/cli/app.d.ts.map +1 -1
  26. package/dist/cli/app.js +4 -0
  27. package/dist/cli/app.js.map +1 -1
  28. package/dist/cli/commands/detect.d.ts +3 -0
  29. package/dist/cli/commands/detect.d.ts.map +1 -0
  30. package/dist/cli/commands/detect.js +39 -0
  31. package/dist/cli/commands/detect.js.map +1 -0
  32. package/dist/cli/commands/doctor.d.ts +1 -1
  33. package/dist/cli/commands/doctor.d.ts.map +1 -1
  34. package/dist/cli/commands/doctor.js +26 -2
  35. package/dist/cli/commands/doctor.js.map +1 -1
  36. package/dist/cli/commands/install.d.ts.map +1 -1
  37. package/dist/cli/commands/install.js +25 -2
  38. package/dist/cli/commands/install.js.map +1 -1
  39. package/dist/cli/commands/preflight.d.ts +11 -0
  40. package/dist/cli/commands/preflight.d.ts.map +1 -0
  41. package/dist/cli/commands/preflight.js +50 -0
  42. package/dist/cli/commands/preflight.js.map +1 -0
  43. package/dist/cli/commands/tui.d.ts.map +1 -1
  44. package/dist/cli/commands/tui.js +19 -3
  45. package/dist/cli/commands/tui.js.map +1 -1
  46. package/dist/cli/commands/uninstall.d.ts.map +1 -1
  47. package/dist/cli/commands/uninstall.js +34 -7
  48. package/dist/cli/commands/uninstall.js.map +1 -1
  49. package/dist/cli/drift.d.ts +19 -0
  50. package/dist/cli/drift.d.ts.map +1 -0
  51. package/dist/cli/drift.js +136 -0
  52. package/dist/cli/drift.js.map +1 -0
  53. package/dist/cli/fs.d.ts.map +1 -1
  54. package/dist/cli/fs.js +6 -70
  55. package/dist/cli/fs.js.map +1 -1
  56. package/dist/cli/help.d.ts.map +1 -1
  57. package/dist/cli/help.js +12 -3
  58. package/dist/cli/help.js.map +1 -1
  59. package/dist/cli/injectors/json-merge.d.ts +3 -0
  60. package/dist/cli/injectors/json-merge.d.ts.map +1 -0
  61. package/dist/cli/injectors/json-merge.js +136 -0
  62. package/dist/cli/injectors/json-merge.js.map +1 -0
  63. package/dist/cli/injectors/managed-block.d.ts +7 -0
  64. package/dist/cli/injectors/managed-block.d.ts.map +1 -0
  65. package/dist/cli/injectors/managed-block.js +39 -0
  66. package/dist/cli/injectors/managed-block.js.map +1 -0
  67. package/dist/cli/options.d.ts.map +1 -1
  68. package/dist/cli/options.js +20 -0
  69. package/dist/cli/options.js.map +1 -1
  70. package/dist/cli/pipeline/operation-steps.d.ts +10 -0
  71. package/dist/cli/pipeline/operation-steps.d.ts.map +1 -0
  72. package/dist/cli/pipeline/operation-steps.js +141 -0
  73. package/dist/cli/pipeline/operation-steps.js.map +1 -0
  74. package/dist/cli/pipeline/orchestrator.d.ts +12 -0
  75. package/dist/cli/pipeline/orchestrator.d.ts.map +1 -0
  76. package/dist/cli/pipeline/orchestrator.js +111 -0
  77. package/dist/cli/pipeline/orchestrator.js.map +1 -0
  78. package/dist/cli/pipeline/types.d.ts +40 -0
  79. package/dist/cli/pipeline/types.d.ts.map +1 -0
  80. package/dist/cli/pipeline/types.js +3 -0
  81. package/dist/cli/pipeline/types.js.map +1 -0
  82. package/dist/cli/types.d.ts +6 -1
  83. package/dist/cli/types.d.ts.map +1 -1
  84. package/docs/HOW-TO-USE-OPENCODE.md +10 -6
  85. package/docs/agent-adapters.md +9 -4
  86. package/docs/cli.md +48 -2
  87. package/docs/getting-started.md +8 -0
  88. package/package.json +3 -2
  89. package/scripts/check-adapter-fixtures.mjs +595 -0
  90. package/skills/sprint-forge/assets/modes/ad3c-cycle.md +89 -0
  91. package/skills/sprint-forge/assets/modes/execute-task.md +5 -3
@@ -0,0 +1,595 @@
1
+ import { createRequire } from 'node:module';
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join, resolve } from 'node:path';
5
+
6
+ const repo = resolve(new URL('..', import.meta.url).pathname);
7
+ const require = createRequire(import.meta.url);
8
+ const packageJson = JSON.parse(readFileSync(join(repo, 'package.json'), 'utf-8'));
9
+ const version = packageJson.version;
10
+
11
+ function assert(condition, message) {
12
+ if (!condition) throw new Error(message);
13
+ }
14
+
15
+ function createWorkspace(prefix) {
16
+ return mkdtempSync(join(tmpdir(), prefix));
17
+ }
18
+
19
+ function clearDistCache() {
20
+ const distRoot = join(repo, 'dist');
21
+ for (const key of Object.keys(require.cache)) {
22
+ if (key.startsWith(distRoot)) delete require.cache[key];
23
+ }
24
+ }
25
+
26
+ function withWorkspace(prefix, callback) {
27
+ const cwd = createWorkspace(prefix);
28
+ const previousCwd = process.cwd();
29
+ const previousHome = process.env.HOME;
30
+ try {
31
+ process.chdir(cwd);
32
+ process.env.HOME = join(cwd, '.home');
33
+ clearDistCache();
34
+ return callback(cwd);
35
+ } finally {
36
+ process.chdir(previousCwd);
37
+ if (previousHome === undefined) {
38
+ delete process.env.HOME;
39
+ } else {
40
+ process.env.HOME = previousHome;
41
+ }
42
+ clearDistCache();
43
+ rmSync(cwd, { recursive: true, force: true });
44
+ }
45
+ }
46
+
47
+ function captureLogs(callback) {
48
+ const logs = [];
49
+ const originalLog = console.log;
50
+ try {
51
+ console.log = (...args) => logs.push(args.join(' '));
52
+ callback();
53
+ } finally {
54
+ console.log = originalLog;
55
+ }
56
+ return `${logs.join('\n')}\n`;
57
+ }
58
+
59
+ function cliOptions(overrides = {}) {
60
+ return {
61
+ agents: [],
62
+ scope: 'workspace',
63
+ dryRun: false,
64
+ yes: true,
65
+ help: false,
66
+ tokens: false,
67
+ artifacts: false,
68
+ adapters: false,
69
+ kyroScope: null,
70
+ json: false,
71
+ purgeAdapterAssets: false,
72
+ prune: false,
73
+ ...overrides,
74
+ };
75
+ }
76
+
77
+ function dryRunPlan(agentArg) {
78
+ return withWorkspace('kyro-adapter-plan-', () => {
79
+ const { parseAgent } = require(join(repo, 'dist/cli/options.js'));
80
+ const { install } = require(join(repo, 'dist/cli/commands/install.js'));
81
+ const agents = agentArg.split(',').map((agent) => parseAgent(agent));
82
+ return captureLogs(() => install(cliOptions({ agents, dryRun: true })));
83
+ });
84
+ }
85
+
86
+ function countIncludes(text, needle) {
87
+ return text.split(needle).length - 1;
88
+ }
89
+
90
+ function installPlanSection(output) {
91
+ const marker = 'Install plan\n';
92
+ const index = output.indexOf(marker);
93
+ assert(index >= 0, 'missing install plan section');
94
+ return output.slice(index + marker.length);
95
+ }
96
+
97
+ function assertCommonPlan(plan, name) {
98
+ assert(plan.includes('Adapter preflight (install)'), `${name}: missing adapter preflight`);
99
+ assert(plan.includes('Plan summary:'), `${name}: missing plan summary`);
100
+ assert(plan.includes('Install plan'), `${name}: missing install plan title`);
101
+ assert(plan.includes('- mkdir .agents/kyro/scopes'), `${name}: missing artifact root mkdir`);
102
+ assert(plan.includes(`- write ~/.agents/kyro/versions/${version}/manifest.json`), `${name}: missing version manifest`);
103
+ assert(plan.includes(`- write ~/.agents/kyro/versions/${version}/KYRO.md`), `${name}: missing runtime bootstrap`);
104
+ assert(plan.includes('- symlink ~/.agents/kyro/current'), `${name}: missing current symlink`);
105
+ }
106
+
107
+ function assertStandardCommandSkills(plan, name) {
108
+ for (const command of ['forge', 'status', 'wrap-up']) {
109
+ assert(plan.includes(`- write ~/.agents/skills/kyro-${command}/SKILL.md`), `${name}: missing ${command} command skill`);
110
+ }
111
+ }
112
+
113
+ const standardPlan = dryRunPlan('standard');
114
+ const openCodePlan = dryRunPlan('opencode');
115
+ const codexPlan = dryRunPlan('codex');
116
+ const combinedPlan = dryRunPlan('standard,opencode,codex');
117
+
118
+ assertCommonPlan(standardPlan, 'standard');
119
+ assertCommonPlan(openCodePlan, 'opencode');
120
+ assertCommonPlan(codexPlan, 'codex');
121
+ assertCommonPlan(combinedPlan, 'combined');
122
+ assertStandardCommandSkills(standardPlan, 'standard');
123
+ assertStandardCommandSkills(codexPlan, 'codex');
124
+ assertStandardCommandSkills(combinedPlan, 'combined');
125
+
126
+ assert(!standardPlan.includes('- upsert-block AGENTS.md # agents-md'), 'standard: should not manage AGENTS.md block');
127
+ assert(!openCodePlan.includes('- upsert-block AGENTS.md # agents-md'), 'opencode: should not manage AGENTS.md block');
128
+ assert(codexPlan.includes('- upsert-block AGENTS.md # agents-md'), 'codex: should manage AGENTS.md block');
129
+ assert(combinedPlan.includes('- upsert-block AGENTS.md # agents-md'), 'combined: should manage AGENTS.md block once');
130
+
131
+ assert(standardPlan.includes('- standard: status=implemented;'), 'standard: missing preflight status');
132
+ assert(openCodePlan.includes('- opencode: status=implemented;'), 'opencode: missing preflight status');
133
+ assert(codexPlan.includes('- codex: status=implemented;'), 'codex: missing preflight status');
134
+ assert(openCodePlan.includes('- write ~/.config/opencode/skills/kyro-forge/SKILL.md'), 'opencode: missing native forge skill projection');
135
+ assert(openCodePlan.includes('- write ~/.config/opencode/commands/kyro/forge.md'), 'opencode: missing native forge command projection');
136
+ assert(openCodePlan.includes('- merge-json ~/.config/opencode/opencode.json'), 'opencode: missing native settings overlay');
137
+ assert(!openCodePlan.includes('- write ~/.agents/skills/kyro-forge/SKILL.md'), 'opencode: should not use standard global command skill projection');
138
+ assert(countIncludes(combinedPlan, '- write ~/.agents/skills/kyro-forge/SKILL.md') === 1, 'combined: forge skill should be projected once');
139
+ assert(countIncludes(combinedPlan, '- upsert-block AGENTS.md # agents-md') === 1, 'combined: AGENTS.md block should be projected once');
140
+
141
+ withWorkspace('kyro-adapter-preflight-', () => {
142
+ const { parseAgent } = require(join(repo, 'dist/cli/options.js'));
143
+ const { install } = require(join(repo, 'dist/cli/commands/install.js'));
144
+ let failed = false;
145
+ try {
146
+ captureLogs(() => install(cliOptions({ agents: [parseAgent('claude')], dryRun: true })));
147
+ } catch (error) {
148
+ failed = true;
149
+ assert(String(error).includes('not implemented yet: claude'), 'preflight: planned adapter failure should name claude');
150
+ assert(String(error).includes('native projection'), 'preflight: planned adapter failure should mention native projection');
151
+ }
152
+ assert(failed, 'preflight: expected planned adapter install to fail');
153
+ });
154
+
155
+ {
156
+ const { mergeJsonObjectContent } = require(join(repo, 'dist/cli/injectors/json-merge.js'));
157
+ const merged = mergeJsonObjectContent(`{
158
+ // Preserve user settings and comments should parse.
159
+ "model": "anthropic/claude",
160
+ "agent": {
161
+ "existing": {
162
+ "model": "custom",
163
+ }
164
+ },
165
+ "mcp": {
166
+ "user-server": {
167
+ "command": ["node", "server.js"]
168
+ }
169
+ }
170
+ }`, JSON.stringify({
171
+ agent: {
172
+ 'kyro-orchestrator': {
173
+ mode: 'primary',
174
+ prompt: '{file:~/.agents/kyro/current/KYRO.md}',
175
+ },
176
+ },
177
+ mcp: {
178
+ kyro: {
179
+ command: ['kyro', 'mcp'],
180
+ },
181
+ },
182
+ }));
183
+ const parsed = JSON.parse(merged);
184
+ assert(parsed.model === 'anthropic/claude', 'json-merge: did not preserve root setting');
185
+ assert(parsed.agent.existing.model === 'custom', 'json-merge: did not preserve nested agent setting');
186
+ assert(parsed.agent['kyro-orchestrator'].mode === 'primary', 'json-merge: did not add nested agent setting');
187
+ assert(parsed.mcp['user-server'].command[1] === 'server.js', 'json-merge: did not preserve existing MCP setting');
188
+ assert(parsed.mcp.kyro.command[0] === 'kyro', 'json-merge: did not add nested MCP setting');
189
+ }
190
+
191
+ {
192
+ const {
193
+ endMarker,
194
+ formatManagedBlock,
195
+ hasManagedBlockContent,
196
+ removeManagedBlock,
197
+ startMarker,
198
+ upsertManagedBlock,
199
+ } = require(join(repo, 'dist/cli/injectors/managed-block.js'));
200
+
201
+ const empty = upsertManagedBlock('', 'agents-md', 'hello');
202
+ assert(empty === `${formatManagedBlock('agents-md', 'hello')}\n`, 'managed-block: unexpected empty-file upsert');
203
+ assert(hasManagedBlockContent(empty, 'agents-md'), 'managed-block: missing block after upsert');
204
+
205
+ const userContent = '# Notes\n\nKeep me.\n';
206
+ const inserted = upsertManagedBlock(userContent, 'agents-md', 'hello');
207
+ assert(inserted.includes('Keep me.'), 'managed-block: user content not preserved');
208
+ assert(countIncludes(inserted, startMarker('agents-md')) === 1, 'managed-block: duplicate start marker after insert');
209
+ assert(countIncludes(inserted, endMarker('agents-md')) === 1, 'managed-block: duplicate end marker after insert');
210
+
211
+ const updated = upsertManagedBlock(inserted, 'agents-md', 'updated');
212
+ assert(updated.includes('updated'), 'managed-block: updated content missing');
213
+ assert(!updated.includes('hello'), 'managed-block: old content still present after update');
214
+ assert(updated.includes('Keep me.'), 'managed-block: user content not preserved after update');
215
+ assert(countIncludes(updated, startMarker('agents-md')) === 1, 'managed-block: duplicate start marker after update');
216
+
217
+ const removed = removeManagedBlock(updated, 'agents-md');
218
+ assert(!hasManagedBlockContent(removed, 'agents-md'), 'managed-block: block still present after remove');
219
+ assert(removed.includes('Keep me.'), 'managed-block: user content not preserved after remove');
220
+
221
+ const removeMissing = removeManagedBlock(userContent, 'missing');
222
+ assert(removeMissing === userContent.trimEnd() + '\n', 'managed-block: remove missing block should preserve normalized content');
223
+
224
+ const specialName = 'agent.block+name?';
225
+ const special = upsertManagedBlock('', specialName, 'symbols');
226
+ assert(hasManagedBlockContent(special, specialName), 'managed-block: special block name not detected');
227
+ assert(removeManagedBlock(special, specialName) === '\n', 'managed-block: special block name not removed');
228
+ }
229
+
230
+ withWorkspace('kyro-adapter-contract-', (cwd) => {
231
+ const { ADAPTERS } = require(join(repo, 'dist/cli/adapters/registry.js'));
232
+ const homeDir = join(cwd, '.home');
233
+ mkdirSync(join(homeDir, '.agents'), { recursive: true });
234
+ mkdirSync(join(homeDir, '.codex'), { recursive: true });
235
+ mkdirSync(join(homeDir, '.config', 'opencode'), { recursive: true });
236
+ mkdirSync(join(homeDir, '.claude'), { recursive: true });
237
+ mkdirSync(join(homeDir, '.cursor'), { recursive: true });
238
+
239
+ assert(ADAPTERS.length === 5, `adapter contract: expected 5 adapters, got ${ADAPTERS.length}`);
240
+ for (const adapter of ADAPTERS) {
241
+ assert(typeof adapter.capabilities === 'function', `${adapter.agent}: missing capabilities()`);
242
+ assert(typeof adapter.paths === 'function', `${adapter.agent}: missing paths()`);
243
+ assert(typeof adapter.detect === 'function', `${adapter.agent}: missing detect()`);
244
+ assert(typeof adapter.systemPromptStrategy === 'function', `${adapter.agent}: missing systemPromptStrategy()`);
245
+ assert(typeof adapter.mcpStrategy === 'function', `${adapter.agent}: missing mcpStrategy()`);
246
+
247
+ const paths = adapter.paths(homeDir);
248
+ const detection = adapter.detect({ homeDir, envPath: '' });
249
+ assert(detection.agent === adapter.agent, `${adapter.agent}: detection agent mismatch`);
250
+ assert(detection.configPath === (paths.globalConfigDir ?? null), `${adapter.agent}: detection config path mismatch`);
251
+ }
252
+
253
+ const byAgent = Object.fromEntries(ADAPTERS.map((adapter) => [adapter.agent, adapter]));
254
+ assert(byAgent.standard.capabilities().join(',') === 'command-skills', 'standard: unexpected capabilities');
255
+ assert(byAgent.standard.systemPromptStrategy() === 'none', 'standard: unexpected system prompt strategy');
256
+ assert(byAgent.standard.mcpStrategy() === 'none', 'standard: unexpected MCP strategy');
257
+ assert(byAgent.standard.detect({ homeDir, envPath: '' }).installed === true, 'standard: should always be installed compatibility adapter');
258
+
259
+ assert(byAgent.opencode.paths(homeDir).globalConfigDir.endsWith('/.config/opencode'), 'opencode: unexpected config root');
260
+ assert(byAgent.opencode.capabilities().join(',') === 'command-skills,filesystem-detect,system-prompt,slash-commands', 'opencode: unexpected capabilities');
261
+ assert(byAgent.opencode.systemPromptStrategy() === 'json-agent-overlay', 'opencode: unexpected system prompt strategy');
262
+ assert(byAgent.opencode.mcpStrategy() === 'none', 'opencode: unexpected MCP strategy');
263
+ assert(byAgent.opencode.detect({ homeDir, envPath: '' }).configFound === true, 'opencode: expected config detection');
264
+
265
+ assert(byAgent.codex.capabilities().includes('workspace-agents-block'), 'codex: missing workspace block capability');
266
+ assert(byAgent.codex.paths(homeDir).mcpConfigPath.endsWith('/.codex/config.toml'), 'codex: unexpected MCP config path');
267
+ assert(byAgent.codex.systemPromptStrategy() === 'managed-block', 'codex: unexpected system prompt strategy');
268
+ assert(byAgent.codex.mcpStrategy() === 'toml-file', 'codex: unexpected MCP strategy');
269
+ assert(byAgent.codex.detect({ homeDir, envPath: '' }).configFound === true, 'codex: expected config detection');
270
+
271
+ assert(byAgent.claude.status === 'planned', 'claude: expected planned status');
272
+ assert(byAgent.claude.paths(homeDir).subAgentsDir.endsWith('/.claude/agents'), 'claude: unexpected sub-agent path');
273
+ assert(byAgent.cursor.status === 'planned', 'cursor: expected planned status');
274
+ assert(byAgent.cursor.systemPromptStrategy() === 'instructions-file', 'cursor: unexpected system prompt strategy');
275
+ });
276
+
277
+ withWorkspace('kyro-adapter-install-', (installDir) => {
278
+ const { parseAgent } = require(join(repo, 'dist/cli/options.js'));
279
+ const { install, sync } = require(join(repo, 'dist/cli/commands/install.js'));
280
+ const { uninstall } = require(join(repo, 'dist/cli/commands/uninstall.js'));
281
+ const codex = parseAgent('codex');
282
+ writeFileSync(join(installDir, 'AGENTS.md'), '# Workspace Notes\n\nKeep this user content.\n', 'utf-8');
283
+
284
+ captureLogs(() => install(cliOptions({ agents: [codex] })));
285
+
286
+ const home = join(installDir, '.home');
287
+ for (const command of ['forge', 'status', 'wrap-up']) {
288
+ const skillPath = join(home, '.agents', 'skills', `kyro-${command}`, 'SKILL.md');
289
+ assert(existsSync(skillPath), `install: missing projected skill ${skillPath}`);
290
+ }
291
+ assert(existsSync(join(home, '.agents', 'kyro', 'versions', version, 'manifest.json')), 'install: missing runtime manifest');
292
+ assert(existsSync(join(home, '.agents', 'kyro', 'current')), 'install: missing current runtime link');
293
+
294
+ let agentsText = readFileSync(join(installDir, 'AGENTS.md'), 'utf-8');
295
+ assert(agentsText.includes('Keep this user content.'), 'install: user AGENTS.md content was not preserved');
296
+ assert(countIncludes(agentsText, '<!-- kyro-ai:agents-md:start -->') === 1, 'install: expected one Kyro start marker');
297
+ assert(countIncludes(agentsText, '<!-- kyro-ai:agents-md:end -->') === 1, 'install: expected one Kyro end marker');
298
+
299
+ captureLogs(() => sync(cliOptions({ agents: [codex] })));
300
+ agentsText = readFileSync(join(installDir, 'AGENTS.md'), 'utf-8');
301
+ assert(countIncludes(agentsText, '<!-- kyro-ai:agents-md:start -->') === 1, 'sync: duplicated Kyro start marker');
302
+ assert(countIncludes(agentsText, '<!-- kyro-ai:agents-md:end -->') === 1, 'sync: duplicated Kyro end marker');
303
+ assert(agentsText.includes('Keep this user content.'), 'sync: user AGENTS.md content was not preserved');
304
+
305
+ captureLogs(() => uninstall(cliOptions()));
306
+ agentsText = readFileSync(join(installDir, 'AGENTS.md'), 'utf-8');
307
+ assert(!agentsText.includes('<!-- kyro-ai:agents-md:start -->'), 'uninstall: Kyro start marker still present');
308
+ assert(!agentsText.includes('<!-- kyro-ai:agents-md:end -->'), 'uninstall: Kyro end marker still present');
309
+ assert(agentsText.includes('Keep this user content.'), 'uninstall: user AGENTS.md content was not preserved');
310
+ });
311
+
312
+ withWorkspace('kyro-adapter-opencode-install-', (installDir) => {
313
+ const { parseAgent } = require(join(repo, 'dist/cli/options.js'));
314
+ const { install, sync } = require(join(repo, 'dist/cli/commands/install.js'));
315
+ const { uninstall } = require(join(repo, 'dist/cli/commands/uninstall.js'));
316
+ const opencode = parseAgent('opencode');
317
+ const home = join(installDir, '.home');
318
+ const settingsPath = join(home, '.config', 'opencode', 'opencode.json');
319
+ mkdirSync(join(home, '.config', 'opencode'), { recursive: true });
320
+ writeFileSync(settingsPath, `{
321
+ // Existing OpenCode config should survive.
322
+ "model": "user/model",
323
+ "agent": {
324
+ "existing": {
325
+ "model": "kept",
326
+ }
327
+ },
328
+ "mcp": {
329
+ "user-server": {
330
+ "command": ["node", "server.js"]
331
+ }
332
+ }
333
+ }`, 'utf-8');
334
+
335
+ captureLogs(() => install(cliOptions({ agents: [opencode] })));
336
+
337
+ for (const command of ['forge', 'status', 'wrap-up']) {
338
+ const skillPath = join(home, '.config', 'opencode', 'skills', `kyro-${command}`, 'SKILL.md');
339
+ const commandPath = join(home, '.config', 'opencode', 'commands', 'kyro', `${command}.md`);
340
+ assert(existsSync(skillPath), `opencode install: missing native skill ${skillPath}`);
341
+ assert(existsSync(commandPath), `opencode install: missing native command ${commandPath}`);
342
+ const commandText = readFileSync(commandPath, 'utf-8');
343
+ assert(commandText.includes(`kyro-${command}/SKILL.md`), `opencode install: command ${command} should route to native skill`);
344
+ }
345
+
346
+ let settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
347
+ assert(settings.model === 'user/model', 'opencode install: did not preserve root setting');
348
+ assert(settings.agent.existing.model === 'kept', 'opencode install: did not preserve existing agent');
349
+ assert(settings.mcp['user-server'].command[1] === 'server.js', 'opencode install: did not preserve existing MCP config');
350
+ assert(settings.agent['kyro-orchestrator'].mode === 'primary', 'opencode install: missing kyro orchestrator overlay');
351
+ assert(settings.agent['kyro-orchestrator'].prompt.includes('Kyro workflow orchestrator'), 'opencode install: missing kyro orchestrator prompt');
352
+
353
+ const afterInstall = readFileSync(settingsPath, 'utf-8');
354
+ captureLogs(() => sync(cliOptions({ agents: [opencode] })));
355
+ assert(readFileSync(settingsPath, 'utf-8') === afterInstall, 'opencode sync: settings overlay should be idempotent');
356
+
357
+ const uninstallOutput = captureLogs(() => uninstall(cliOptions()));
358
+ assert(uninstallOutput.includes('Uninstall summary:'), 'opencode uninstall: missing uninstall summary');
359
+ assert(uninstallOutput.includes('purgeAdapterAssets=no'), 'opencode uninstall: summary should report purge disabled');
360
+ settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
361
+ assert(settings.model === 'user/model', 'opencode uninstall: did not preserve root setting');
362
+ assert(settings.agent.existing.model === 'kept', 'opencode uninstall: did not preserve existing agent');
363
+ assert(!settings.agent['kyro-orchestrator'], 'opencode uninstall: Kyro orchestrator overlay still present');
364
+ assert(settings.mcp['user-server'].command[1] === 'server.js', 'opencode uninstall: did not preserve existing MCP config');
365
+ for (const command of ['forge', 'status', 'wrap-up']) {
366
+ assert(existsSync(join(home, '.config', 'opencode', 'skills', `kyro-${command}`, 'SKILL.md')), `opencode uninstall: should preserve native skill ${command} without purge`);
367
+ assert(existsSync(join(home, '.config', 'opencode', 'commands', 'kyro', `${command}.md`)), `opencode uninstall: should preserve native command ${command} without purge`);
368
+ }
369
+
370
+ assert(!existsSync(join(home, '.agents', 'skills', 'kyro-forge', 'SKILL.md')), 'opencode install: should not install standard global skill projection');
371
+
372
+ captureLogs(() => install(cliOptions({ agents: [opencode] })));
373
+ const purgeDryRunOutput = captureLogs(() => uninstall(cliOptions({ purgeAdapterAssets: true, dryRun: true })));
374
+ assert(purgeDryRunOutput.includes('purgeAdapterAssets=yes'), 'opencode purge dry-run: summary should report purge enabled');
375
+ assert(purgeDryRunOutput.includes('- remove ~/.config/opencode/skills/kyro-forge/SKILL.md'), 'opencode purge dry-run: plan should include native skill removal');
376
+ assert(purgeDryRunOutput.includes('- remove ~/.config/opencode/commands/kyro/forge.md'), 'opencode purge dry-run: plan should include native command removal');
377
+ assert(purgeDryRunOutput.includes('Dry run complete. No files changed.'), 'opencode purge dry-run: should report no file changes');
378
+ settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
379
+ assert(settings.agent['kyro-orchestrator'], 'opencode purge dry-run: Kyro orchestrator overlay should remain');
380
+ for (const command of ['forge', 'status', 'wrap-up']) {
381
+ assert(existsSync(join(home, '.config', 'opencode', 'skills', `kyro-${command}`, 'SKILL.md')), `opencode purge dry-run: native skill ${command} should remain`);
382
+ assert(existsSync(join(home, '.config', 'opencode', 'commands', 'kyro', `${command}.md`)), `opencode purge dry-run: native command ${command} should remain`);
383
+ }
384
+
385
+ const purgeOutput = captureLogs(() => uninstall(cliOptions({ purgeAdapterAssets: true })));
386
+ assert(purgeOutput.includes('purgeAdapterAssets=yes'), 'opencode purge: summary should report purge enabled');
387
+ assert(purgeOutput.includes('- rmdir-if-empty ~/.config/opencode/commands/kyro'), 'opencode purge: plan should clean command namespace directory');
388
+ assert(purgeOutput.includes('- rmdir-if-empty ~/.config/opencode/skills/kyro-forge'), 'opencode purge: plan should clean skill directory');
389
+ settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
390
+ assert(settings.model === 'user/model', 'opencode purge: did not preserve root setting');
391
+ assert(settings.agent.existing.model === 'kept', 'opencode purge: did not preserve existing agent');
392
+ assert(!settings.agent['kyro-orchestrator'], 'opencode purge: Kyro orchestrator overlay still present');
393
+ for (const command of ['forge', 'status', 'wrap-up']) {
394
+ assert(!existsSync(join(home, '.config', 'opencode', 'skills', `kyro-${command}`, 'SKILL.md')), `opencode purge: native skill ${command} still present`);
395
+ assert(!existsSync(join(home, '.config', 'opencode', 'commands', 'kyro', `${command}.md`)), `opencode purge: native command ${command} still present`);
396
+ assert(!existsSync(join(home, '.config', 'opencode', 'skills', `kyro-${command}`)), `opencode purge: empty skill directory ${command} still present`);
397
+ }
398
+ assert(!existsSync(join(home, '.config', 'opencode', 'commands', 'kyro')), 'opencode purge: empty command namespace still present');
399
+ assert(existsSync(join(home, '.config', 'opencode')), 'opencode purge: shared OpenCode config directory should remain');
400
+ });
401
+
402
+ withWorkspace('kyro-pipeline-rollback-', (cwd) => {
403
+ const { applyPlan } = require(join(repo, 'dist/cli/fs.js'));
404
+ const agentsPath = join(cwd, 'AGENTS.md');
405
+ const original = '# Existing\n\nKeep this exact content.\n';
406
+ writeFileSync(agentsPath, original, 'utf-8');
407
+
408
+ let failed = false;
409
+ try {
410
+ applyPlan([
411
+ { action: 'upsert-block', path: 'AGENTS.md', blockName: 'agents-md', content: 'temporary mutation' },
412
+ { action: 'copy', path: 'SHOULD_NOT_EXIST.md', source: 'missing-source-file.md' },
413
+ ]);
414
+ } catch (error) {
415
+ failed = true;
416
+ assert(String(error).includes('rollback completed'), 'pipeline rollback: failure did not report rollback completion');
417
+ }
418
+
419
+ assert(failed, 'pipeline rollback: expected applyPlan to fail');
420
+ assert(readFileSync(agentsPath, 'utf-8') === original, 'pipeline rollback: AGENTS.md was not restored');
421
+ assert(!existsSync(join(cwd, 'SHOULD_NOT_EXIST.md')), 'pipeline rollback: failed copy target should not exist');
422
+ });
423
+
424
+ withWorkspace('kyro-json-merge-rollback-', (cwd) => {
425
+ const { applyPlan } = require(join(repo, 'dist/cli/fs.js'));
426
+ const settingsPath = join(cwd, '.config', 'opencode', 'opencode.json');
427
+ const original = '{\n "model": "user/model",\n "agent": {\n "existing": {\n "model": "kept"\n }\n }\n}\n';
428
+ mkdirSync(join(cwd, '.config', 'opencode'), { recursive: true });
429
+ writeFileSync(settingsPath, original, 'utf-8');
430
+
431
+ let failed = false;
432
+ try {
433
+ applyPlan([
434
+ {
435
+ action: 'merge-json',
436
+ path: '.config/opencode/opencode.json',
437
+ content: JSON.stringify({ agent: { kyro: { prompt: 'temporary mutation' } } }),
438
+ },
439
+ { action: 'copy', path: 'SHOULD_NOT_EXIST.md', source: 'missing-source-file.md' },
440
+ ]);
441
+ } catch (error) {
442
+ failed = true;
443
+ assert(String(error).includes('rollback completed'), 'json merge rollback: failure did not report rollback completion');
444
+ }
445
+
446
+ assert(failed, 'json merge rollback: expected applyPlan to fail');
447
+ assert(readFileSync(settingsPath, 'utf-8') === original, 'json merge rollback: opencode.json was not restored');
448
+ assert(!existsSync(join(cwd, 'SHOULD_NOT_EXIST.md')), 'json merge rollback: failed copy target should not exist');
449
+ });
450
+
451
+ withWorkspace('kyro-adapter-doctor-', () => {
452
+ const { doctor } = require(join(repo, 'dist/cli/commands/doctor.js'));
453
+ const output = captureLogs(() => doctor(cliOptions({ adapters: true })));
454
+ for (const agent of ['standard', 'opencode', 'codex', 'claude', 'cursor']) {
455
+ assert(output.includes(`adapter inventory: ${agent}`), `doctor --adapters: missing ${agent}`);
456
+ }
457
+ assert(output.includes('status=implemented'), 'doctor --adapters: missing implemented status');
458
+ assert(output.includes('status=planned'), 'doctor --adapters: missing planned status');
459
+ assert(output.includes('capabilities=command-skills'), 'doctor --adapters: missing command-skills capability');
460
+ assert(output.includes('workspace-agents-block'), 'doctor --adapters: missing workspace AGENTS block capability');
461
+ });
462
+
463
+ withWorkspace('kyro-sync-drift-', (cwd) => {
464
+ const { parseAgent } = require(join(repo, 'dist/cli/options.js'));
465
+ const { install, sync } = require(join(repo, 'dist/cli/commands/install.js'));
466
+ const { readPackageVersion } = require(join(repo, 'dist/cli/help.js'));
467
+ const codex = parseAgent('codex');
468
+ const home = join(cwd, '.home');
469
+
470
+ captureLogs(() => install(cliOptions({ agents: [codex] })));
471
+
472
+ const version = readPackageVersion();
473
+ const versionDir = join(home, '.agents', 'kyro', 'versions', version);
474
+ assert(existsSync(versionDir), 'sync-drift: version dir should exist after install');
475
+
476
+ const obsoleteSkill = join(home, '.agents', 'skills', 'kyro-obsolete-fixture', 'SKILL.md');
477
+ const sharedOpenCodeConfig = join(home, '.config', 'opencode', 'opencode.json');
478
+ mkdirSync(join(home, '.agents', 'skills', 'kyro-obsolete-fixture'), { recursive: true });
479
+ mkdirSync(join(home, '.config', 'opencode'), { recursive: true });
480
+ writeFileSync(obsoleteSkill, 'legacy', 'utf-8');
481
+ writeFileSync(sharedOpenCodeConfig, '{ "model": "user/model" }\n', 'utf-8');
482
+
483
+ const manifestPath = join(versionDir, 'manifest.json');
484
+ const oldManifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
485
+ oldManifest.managedFiles.push('~/.agents/skills/kyro-obsolete-fixture/SKILL.md', '~/.config/opencode/opencode.json');
486
+ writeFileSync(manifestPath, `${JSON.stringify(oldManifest, null, 2)}\n`, 'utf-8');
487
+
488
+ const staleDir = join(home, '.agents', 'kyro', 'versions', '0.0.0');
489
+ mkdirSync(staleDir, { recursive: true });
490
+ writeFileSync(join(staleDir, 'stale.txt'), 'stale', 'utf-8');
491
+
492
+ const syncOutput = captureLogs(() => sync(cliOptions({ agents: [codex] })));
493
+ assert(syncOutput.includes('Drift analysis:'), 'sync-drift: missing drift report');
494
+ assert(syncOutput.includes('Stale runtime versions'), 'sync-drift: missing stale versions in drift');
495
+ assert(syncOutput.includes('Orphaned managed files'), 'sync-drift: missing orphaned files in drift');
496
+ assert(syncOutput.includes('~/.agents/skills/kyro-obsolete-fixture/SKILL.md'), 'sync-drift: missing obsolete adapter skill in drift');
497
+ assert(syncOutput.includes('Shared config preserved'), 'sync-drift: missing preserved shared config report');
498
+ assert(syncOutput.includes('~/.config/opencode/opencode.json'), 'sync-drift: shared opencode config should be reported as preserved');
499
+ assert(syncOutput.includes('Tip: run with --prune'), 'sync-drift: missing --prune tip');
500
+ assert(existsSync(staleDir), 'sync-drift: stale dir should still exist without --prune');
501
+ assert(existsSync(obsoleteSkill), 'sync-drift: obsolete skill should still exist without --prune');
502
+ assert(existsSync(sharedOpenCodeConfig), 'sync-drift: shared opencode config should still exist without --prune');
503
+
504
+ const oldManifestBeforePrune = JSON.parse(readFileSync(manifestPath, 'utf-8'));
505
+ oldManifestBeforePrune.managedFiles.push('~/.agents/skills/kyro-obsolete-fixture/SKILL.md', '~/.config/opencode/opencode.json');
506
+ writeFileSync(manifestPath, `${JSON.stringify(oldManifestBeforePrune, null, 2)}\n`, 'utf-8');
507
+
508
+ const staleDir2 = join(home, '.agents', 'kyro', 'versions', '0.0.1');
509
+ mkdirSync(staleDir2, { recursive: true });
510
+ writeFileSync(join(staleDir2, 'stale.txt'), 'stale', 'utf-8');
511
+
512
+ const pruneDryRunOutput = captureLogs(() => sync(cliOptions({ agents: [codex], prune: true, dryRun: true })));
513
+ assert(pruneDryRunOutput.includes('Prune plan:'), 'sync-prune dry-run: missing prune plan');
514
+ assert(pruneDryRunOutput.includes('Dry run complete. No files changed.'), 'sync-prune dry-run: should report no file changes');
515
+ assert(pruneDryRunOutput.includes('~/.agents/skills/kyro-obsolete-fixture/SKILL.md'), 'sync-prune dry-run: prune plan should include obsolete adapter skill');
516
+ assert(pruneDryRunOutput.includes('Shared config preserved'), 'sync-prune dry-run: preserved shared config should still be reported');
517
+ assert(!pruneDryRunOutput.includes('- remove ~/.config/opencode/opencode.json'), 'sync-prune dry-run: prune plan should not remove shared opencode config');
518
+ assert(existsSync(staleDir), 'sync-prune dry-run: stale dir 0.0.0 should remain');
519
+ assert(existsSync(staleDir2), 'sync-prune dry-run: stale dir 0.0.1 should remain');
520
+ assert(existsSync(obsoleteSkill), 'sync-prune dry-run: obsolete skill should remain');
521
+ assert(existsSync(sharedOpenCodeConfig), 'sync-prune dry-run: shared opencode config should remain');
522
+
523
+ const pruneOutput = captureLogs(() => sync(cliOptions({ agents: [codex], prune: true })));
524
+ assert(pruneOutput.includes('Prune plan:'), 'sync-prune: missing prune plan');
525
+ assert(pruneOutput.includes('remove'), 'sync-prune: prune plan should include remove operations');
526
+ assert(pruneOutput.includes('~/.agents/skills/kyro-obsolete-fixture/SKILL.md'), 'sync-prune: prune plan should include obsolete adapter skill');
527
+ assert(pruneOutput.includes('Shared config preserved'), 'sync-prune: preserved shared config should still be reported');
528
+ assert(pruneOutput.includes('~/.config/opencode/opencode.json'), 'sync-prune: shared opencode config should be reported as preserved');
529
+ assert(!pruneOutput.includes('- remove ~/.config/opencode/opencode.json'), 'sync-prune: prune plan should not remove shared opencode config');
530
+ assert(!existsSync(staleDir), 'sync-prune: stale dir 0.0.0 should be removed');
531
+ assert(!existsSync(staleDir2), 'sync-prune: stale dir 0.0.1 should be removed');
532
+ assert(!existsSync(obsoleteSkill), 'sync-prune: obsolete adapter skill should be removed');
533
+ assert(existsSync(sharedOpenCodeConfig), 'sync-prune: shared opencode config should be preserved');
534
+ assert(existsSync(versionDir), 'sync-prune: current version dir should be preserved');
535
+ for (const runtimeFile of ['manifest.json', 'KYRO.md', 'commands/forge.md', 'skills/sprint-forge/SKILL.md', 'core/WORKFLOW.yaml']) {
536
+ assert(existsSync(join(versionDir, runtimeFile)), `sync-prune: current runtime file ${runtimeFile} should be preserved`);
537
+ }
538
+ });
539
+
540
+ withWorkspace('kyro-sync-shared-config-only-', (cwd) => {
541
+ const { parseAgent } = require(join(repo, 'dist/cli/options.js'));
542
+ const { install, sync } = require(join(repo, 'dist/cli/commands/install.js'));
543
+ const { readPackageVersion } = require(join(repo, 'dist/cli/help.js'));
544
+ const codex = parseAgent('codex');
545
+ const home = join(cwd, '.home');
546
+
547
+ captureLogs(() => install(cliOptions({ agents: [codex] })));
548
+
549
+ const versionDir = join(home, '.agents', 'kyro', 'versions', readPackageVersion());
550
+ const sharedOpenCodeConfig = join(home, '.config', 'opencode', 'opencode.json');
551
+ mkdirSync(join(home, '.config', 'opencode'), { recursive: true });
552
+ writeFileSync(sharedOpenCodeConfig, '{ "model": "user/model" }\n', 'utf-8');
553
+
554
+ const manifestPath = join(versionDir, 'manifest.json');
555
+ const oldManifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
556
+ oldManifest.managedFiles.push('~/.config/opencode/opencode.json');
557
+ writeFileSync(manifestPath, `${JSON.stringify(oldManifest, null, 2)}\n`, 'utf-8');
558
+
559
+ const syncOutput = captureLogs(() => sync(cliOptions({ agents: [codex] })));
560
+ assert(syncOutput.includes('Shared config preserved'), 'sync-shared-config-only: missing preserved shared config report');
561
+ assert(syncOutput.includes('~/.config/opencode/opencode.json'), 'sync-shared-config-only: shared opencode config should be reported as preserved');
562
+ assert(!syncOutput.includes('Tip: run with --prune'), 'sync-shared-config-only: should not suggest prune when nothing is prunable');
563
+ assert(existsSync(sharedOpenCodeConfig), 'sync-shared-config-only: shared opencode config should remain');
564
+
565
+ const oldManifestBeforePrune = JSON.parse(readFileSync(manifestPath, 'utf-8'));
566
+ oldManifestBeforePrune.managedFiles.push('~/.config/opencode/opencode.json');
567
+ writeFileSync(manifestPath, `${JSON.stringify(oldManifestBeforePrune, null, 2)}\n`, 'utf-8');
568
+
569
+ const pruneDryRunOutput = captureLogs(() => sync(cliOptions({ agents: [codex], prune: true, dryRun: true })));
570
+ assert(pruneDryRunOutput.includes('No prunable drift found. Shared config was preserved.'), 'sync-shared-config-only prune dry-run: should explain that no files are prunable');
571
+ assert(!pruneDryRunOutput.includes('Prune plan:'), 'sync-shared-config-only prune dry-run: should not print an empty prune plan');
572
+ assert(existsSync(sharedOpenCodeConfig), 'sync-shared-config-only prune dry-run: shared opencode config should remain');
573
+ });
574
+
575
+ withWorkspace('kyro-adapter-detect-', () => {
576
+ const { parseAgent } = require(join(repo, 'dist/cli/options.js'));
577
+ const { detect } = require(join(repo, 'dist/cli/commands/detect.js'));
578
+
579
+ const textOutput = captureLogs(() => detect(cliOptions()));
580
+ for (const agent of ['standard', 'opencode', 'codex', 'claude', 'cursor']) {
581
+ assert(textOutput.includes(`${agent}: status=`), `detect: missing ${agent}`);
582
+ }
583
+ assert(textOutput.includes('capabilities=command-skills'), 'detect: missing capabilities');
584
+ assert(textOutput.includes('systemPromptStrategy='), 'detect: missing system prompt strategy');
585
+ assert(textOutput.includes('mcpStrategy='), 'detect: missing MCP strategy');
586
+
587
+ const jsonOutput = captureLogs(() => detect(cliOptions({ agents: [parseAgent('codex')], json: true })));
588
+ const payload = JSON.parse(jsonOutput);
589
+ assert(payload.adapters.length === 1, 'detect --json: expected filtered adapter');
590
+ assert(payload.adapters[0].agent === 'codex', 'detect --json: expected codex adapter');
591
+ assert(payload.adapters[0].systemPromptStrategy === 'managed-block', 'detect --json: missing codex system prompt strategy');
592
+ assert(payload.adapters[0].mcpStrategy === 'toml-file', 'detect --json: missing codex MCP strategy');
593
+ });
594
+
595
+ console.log('Adapter fixtures passed');