kyro-ai 3.3.2 → 3.4.1

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 (57) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/WORKFLOW.yaml +1 -1
  3. package/dist/cli/adapters/claude.d.ts +3 -0
  4. package/dist/cli/adapters/claude.d.ts.map +1 -0
  5. package/dist/cli/adapters/claude.js +98 -0
  6. package/dist/cli/adapters/claude.js.map +1 -0
  7. package/dist/cli/adapters/cursor.d.ts +3 -0
  8. package/dist/cli/adapters/cursor.d.ts.map +1 -0
  9. package/dist/cli/adapters/cursor.js +74 -0
  10. package/dist/cli/adapters/cursor.js.map +1 -0
  11. package/dist/cli/adapters/registry.d.ts.map +1 -1
  12. package/dist/cli/adapters/registry.js +4 -52
  13. package/dist/cli/adapters/registry.js.map +1 -1
  14. package/dist/cli/app.d.ts.map +1 -1
  15. package/dist/cli/app.js +11 -0
  16. package/dist/cli/app.js.map +1 -1
  17. package/dist/cli/commands/adapter.d.ts +4 -0
  18. package/dist/cli/commands/adapter.d.ts.map +1 -0
  19. package/dist/cli/commands/adapter.js +41 -0
  20. package/dist/cli/commands/adapter.js.map +1 -0
  21. package/dist/cli/commands/doctor.d.ts +1 -1
  22. package/dist/cli/commands/doctor.d.ts.map +1 -1
  23. package/dist/cli/commands/doctor.js +70 -2
  24. package/dist/cli/commands/doctor.js.map +1 -1
  25. package/dist/cli/commands/install.d.ts.map +1 -1
  26. package/dist/cli/commands/install.js +40 -4
  27. package/dist/cli/commands/install.js.map +1 -1
  28. package/dist/cli/commands/mcp.d.ts +2 -0
  29. package/dist/cli/commands/mcp.d.ts.map +1 -0
  30. package/dist/cli/commands/mcp.js +93 -0
  31. package/dist/cli/commands/mcp.js.map +1 -0
  32. package/dist/cli/commands/preflight.d.ts +3 -1
  33. package/dist/cli/commands/preflight.d.ts.map +1 -1
  34. package/dist/cli/commands/preflight.js +3 -2
  35. package/dist/cli/commands/preflight.js.map +1 -1
  36. package/dist/cli/commands/tui.js +2 -0
  37. package/dist/cli/commands/tui.js.map +1 -1
  38. package/dist/cli/drift.js +3 -1
  39. package/dist/cli/drift.js.map +1 -1
  40. package/dist/cli/help.d.ts.map +1 -1
  41. package/dist/cli/help.js +16 -4
  42. package/dist/cli/help.js.map +1 -1
  43. package/dist/cli/options.d.ts.map +1 -1
  44. package/dist/cli/options.js +10 -0
  45. package/dist/cli/options.js.map +1 -1
  46. package/dist/cli/types.d.ts +2 -0
  47. package/dist/cli/types.d.ts.map +1 -1
  48. package/docs/adapter-matrix.md +15 -0
  49. package/docs/adr/0002-agent-platform-v2.md +41 -0
  50. package/docs/host-smoke-tests.md +15 -0
  51. package/docs/mcp.md +19 -0
  52. package/docs/migration.md +16 -0
  53. package/docs/release-3.4.0.md +42 -0
  54. package/docs/release-agent-platform-v2.md +21 -0
  55. package/package.json +3 -2
  56. package/scripts/check-adapter-fixtures.mjs +88 -18
  57. package/scripts/smoke-adapters.mjs +144 -0
@@ -0,0 +1,42 @@
1
+ # Kyro AI 3.4.0 Release Notes
2
+
3
+ ## Summary
4
+
5
+ Kyro 3.4.0 stabilizes the adapter harness v1 release. It adds concrete OpenCode and Codex adapter projections, keeps Claude plugin support first-class, and hardens sync/uninstall behavior around managed files, shared config, drift, prune, purge, and dry-run safety.
6
+
7
+ ## Highlights
8
+
9
+ - Adapter harness v1 for `standard`, `opencode`, and `codex`.
10
+ - `kyro detect` and adapter inventory through `kyro doctor --adapters`.
11
+ - OpenCode native skills, `/kyro/*` command files, and a Kyro-owned `agent.kyro-orchestrator` overlay.
12
+ - Codex command skills plus a managed root `AGENTS.md` block.
13
+ - Global runtime under `~/.agents/kyro/versions/{version}` with `current` symlink.
14
+ - Project-local state under `.agents/kyro/`.
15
+ - `kyro sync` refreshes installed adapters without silently adding `standard`.
16
+ - `kyro sync --prune` removes stale runtime versions and obsolete Kyro-owned adapter entrypoints.
17
+ - Shared config such as `~/.config/opencode/opencode.json` is preserved and reported separately.
18
+ - `kyro uninstall --purge-adapter-assets` explicitly removes adapter entrypoints while preserving shared config.
19
+ - Dry-run behavior is covered for prune and purge paths.
20
+ - Pipeline rollback and managed block/JSON merge behavior are covered by fixtures.
21
+
22
+ ## Non-Goals
23
+
24
+ - No generic adapter is provided.
25
+ - Cursor remains planned until concrete projection behavior is defined.
26
+ - Claude plugin support is not replaced by the CLI adapter path.
27
+ - MCP remains modeled but inactive until there is a concrete Kyro MCP server contract.
28
+
29
+ ## Validation
30
+
31
+ ```bash
32
+ npm run build
33
+ npm run check
34
+ npm run check:adapters
35
+ npm_config_cache=/tmp/kyro-npm-cache npm pack --dry-run
36
+ ```
37
+
38
+ Additional smoke coverage was run with an isolated `HOME` and workspace under `/tmp`, covering install, doctor, sync dry-run, prune dry-run, uninstall, purge, and host binary detection for OpenCode and Codex.
39
+
40
+ ## Notes
41
+
42
+ `doctor --adapters` now treats the global runtime as the files under `~/.agents/kyro/`. Adapter entrypoints are validated through installed adapter projection checks, so purging adapter assets after uninstall does not make the preserved global runtime appear broken.
@@ -0,0 +1,21 @@
1
+ # Agent Platform V2 Release Notes
2
+
3
+ This scope expands Kyro from adapter harness v1 into a host-aware agent platform slice.
4
+
5
+ Added:
6
+
7
+ - Cursor workspace adapter at `.cursor/rules/kyro-ai.mdc`
8
+ - Claude CLI-native workspace skills under `.claude/skills/kyro-*`
9
+ - `kyro adapter matrix` and `kyro adapter matrix --json`
10
+ - `kyro doctor --migration` and `kyro doctor --migration --json`
11
+ - JSON output for install/sync planning
12
+ - read-only `kyro mcp serve`
13
+ - `npm run smoke:adapters`
14
+ - adapter, migration, MCP, and smoke test docs
15
+
16
+ Safety behavior:
17
+
18
+ - Normal install/sync does not enable MCP host config.
19
+ - Cursor uninstall removes only the Kyro-owned rule file.
20
+ - Claude CLI-native install does not modify `.claude-plugin/`.
21
+ - `sync --prune` preserves shared host config.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kyro-ai",
3
- "version": "3.3.2",
3
+ "version": "3.4.1",
4
4
  "description": "Portable sprint workflow kit for AI coding agents, with markdown artifacts, adapter guides, and formal debt tracking",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -16,7 +16,8 @@
16
16
  "check:tokens": "node dist/cli.js doctor --tokens",
17
17
  "check:artifacts": "node dist/cli.js doctor --artifacts",
18
18
  "check:artifact-fixtures": "node scripts/check-artifact-fixtures.mjs",
19
- "check:adapters": "node scripts/check-adapter-fixtures.mjs"
19
+ "check:adapters": "node scripts/check-adapter-fixtures.mjs",
20
+ "smoke:adapters": "node scripts/smoke-adapters.mjs"
20
21
  },
21
22
  "keywords": [
22
23
  "ai-agents",
@@ -70,6 +70,8 @@ function cliOptions(overrides = {}) {
70
70
  json: false,
71
71
  purgeAdapterAssets: false,
72
72
  prune: false,
73
+ migration: false,
74
+ enableMcp: false,
73
75
  ...overrides,
74
76
  };
75
77
  }
@@ -113,11 +115,15 @@ function assertStandardCommandSkills(plan, name) {
113
115
  const standardPlan = dryRunPlan('standard');
114
116
  const openCodePlan = dryRunPlan('opencode');
115
117
  const codexPlan = dryRunPlan('codex');
116
- const combinedPlan = dryRunPlan('standard,opencode,codex');
118
+ const cursorPlan = dryRunPlan('cursor');
119
+ const claudePlan = dryRunPlan('claude');
120
+ const combinedPlan = dryRunPlan('standard,opencode,codex,cursor,claude');
117
121
 
118
122
  assertCommonPlan(standardPlan, 'standard');
119
123
  assertCommonPlan(openCodePlan, 'opencode');
120
124
  assertCommonPlan(codexPlan, 'codex');
125
+ assertCommonPlan(cursorPlan, 'cursor');
126
+ assertCommonPlan(claudePlan, 'claude');
121
127
  assertCommonPlan(combinedPlan, 'combined');
122
128
  assertStandardCommandSkills(standardPlan, 'standard');
123
129
  assertStandardCommandSkills(codexPlan, 'codex');
@@ -127,10 +133,15 @@ assert(!standardPlan.includes('- upsert-block AGENTS.md # agents-md'), 'standard
127
133
  assert(!openCodePlan.includes('- upsert-block AGENTS.md # agents-md'), 'opencode: should not manage AGENTS.md block');
128
134
  assert(codexPlan.includes('- upsert-block AGENTS.md # agents-md'), 'codex: should manage AGENTS.md block');
129
135
  assert(combinedPlan.includes('- upsert-block AGENTS.md # agents-md'), 'combined: should manage AGENTS.md block once');
136
+ assert(cursorPlan.includes('- write .cursor/rules/kyro-ai.mdc'), 'cursor: missing workspace rule projection');
137
+ assert(claudePlan.includes('- write .claude/skills/kyro-forge/SKILL.md'), 'claude: missing CLI-native skill projection');
138
+ assert(!claudePlan.includes('.claude-plugin'), 'claude: CLI-native projection should not touch .claude-plugin');
130
139
 
131
140
  assert(standardPlan.includes('- standard: status=implemented;'), 'standard: missing preflight status');
132
141
  assert(openCodePlan.includes('- opencode: status=implemented;'), 'opencode: missing preflight status');
133
142
  assert(codexPlan.includes('- codex: status=implemented;'), 'codex: missing preflight status');
143
+ assert(cursorPlan.includes('- cursor: status=implemented;'), 'cursor: missing preflight status');
144
+ assert(claudePlan.includes('- claude: status=implemented;'), 'claude: missing preflight status');
134
145
  assert(openCodePlan.includes('- write ~/.config/opencode/skills/kyro-forge/SKILL.md'), 'opencode: missing native forge skill projection');
135
146
  assert(openCodePlan.includes('- write ~/.config/opencode/commands/kyro/forge.md'), 'opencode: missing native forge command projection');
136
147
  assert(openCodePlan.includes('- merge-json ~/.config/opencode/opencode.json'), 'opencode: missing native settings overlay');
@@ -138,20 +149,6 @@ assert(!openCodePlan.includes('- write ~/.agents/skills/kyro-forge/SKILL.md'), '
138
149
  assert(countIncludes(combinedPlan, '- write ~/.agents/skills/kyro-forge/SKILL.md') === 1, 'combined: forge skill should be projected once');
139
150
  assert(countIncludes(combinedPlan, '- upsert-block AGENTS.md # agents-md') === 1, 'combined: AGENTS.md block should be projected once');
140
151
 
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
152
  {
156
153
  const { mergeJsonObjectContent } = require(join(repo, 'dist/cli/injectors/json-merge.js'));
157
154
  const merged = mergeJsonObjectContent(`{
@@ -268,9 +265,11 @@ withWorkspace('kyro-adapter-contract-', (cwd) => {
268
265
  assert(byAgent.codex.mcpStrategy() === 'toml-file', 'codex: unexpected MCP strategy');
269
266
  assert(byAgent.codex.detect({ homeDir, envPath: '' }).configFound === true, 'codex: expected config detection');
270
267
 
271
- assert(byAgent.claude.status === 'planned', 'claude: expected planned status');
268
+ assert(byAgent.claude.status === 'implemented', 'claude: expected implemented status');
269
+ assert(byAgent.claude.capabilities().includes('command-skills'), 'claude: missing command skill capability');
272
270
  assert(byAgent.claude.paths(homeDir).subAgentsDir.endsWith('/.claude/agents'), 'claude: unexpected sub-agent path');
273
- assert(byAgent.cursor.status === 'planned', 'cursor: expected planned status');
271
+ assert(byAgent.cursor.status === 'implemented', 'cursor: expected implemented status');
272
+ assert(byAgent.cursor.capabilities().includes('system-prompt'), 'cursor: missing rule/system prompt capability');
274
273
  assert(byAgent.cursor.systemPromptStrategy() === 'instructions-file', 'cursor: unexpected system prompt strategy');
275
274
  });
276
275
 
@@ -309,10 +308,65 @@ withWorkspace('kyro-adapter-install-', (installDir) => {
309
308
  assert(agentsText.includes('Keep this user content.'), 'uninstall: user AGENTS.md content was not preserved');
310
309
  });
311
310
 
311
+ withWorkspace('kyro-adapter-cursor-install-', (installDir) => {
312
+ const { parseAgent } = require(join(repo, 'dist/cli/options.js'));
313
+ const { install, sync } = require(join(repo, 'dist/cli/commands/install.js'));
314
+ const { uninstall } = require(join(repo, 'dist/cli/commands/uninstall.js'));
315
+ const { doctor } = require(join(repo, 'dist/cli/commands/doctor.js'));
316
+ const cursor = parseAgent('cursor');
317
+ const userRulePath = join(installDir, '.cursor', 'rules', 'user-owned.mdc');
318
+ mkdirSync(join(installDir, '.cursor', 'rules'), { recursive: true });
319
+ writeFileSync(userRulePath, '# User rule\n\nKeep this rule.\n', 'utf-8');
320
+
321
+ const dryRun = captureLogs(() => install(cliOptions({ agents: [cursor], dryRun: true })));
322
+ assert(dryRun.includes('- write .cursor/rules/kyro-ai.mdc'), 'cursor dry-run: missing Kyro rule write');
323
+ assert(!existsSync(join(installDir, '.cursor', 'rules', 'kyro-ai.mdc')), 'cursor dry-run: should not write Kyro rule');
324
+
325
+ captureLogs(() => install(cliOptions({ agents: [cursor] })));
326
+ const kyroRulePath = join(installDir, '.cursor', 'rules', 'kyro-ai.mdc');
327
+ assert(existsSync(kyroRulePath), 'cursor install: missing Kyro rule');
328
+ assert(readFileSync(kyroRulePath, 'utf-8').includes('~/.agents/kyro/current'), 'cursor install: rule should reference global runtime');
329
+ assert(readFileSync(userRulePath, 'utf-8').includes('Keep this rule.'), 'cursor install: user rule should be preserved');
330
+
331
+ const doctorOutput = captureLogs(() => doctor(cliOptions({ adapters: true })));
332
+ assert(doctorOutput.includes('[PASS] Cursor adapter'), 'cursor doctor: adapter should pass after install');
333
+
334
+ captureLogs(() => sync(cliOptions({ agents: [cursor] })));
335
+ assert(readFileSync(userRulePath, 'utf-8').includes('Keep this rule.'), 'cursor sync: user rule should be preserved');
336
+
337
+ captureLogs(() => uninstall(cliOptions({ purgeAdapterAssets: true })));
338
+ assert(!existsSync(kyroRulePath), 'cursor uninstall purge: Kyro rule should be removed');
339
+ assert(existsSync(userRulePath), 'cursor uninstall purge: user rule should be preserved');
340
+ });
341
+
342
+ withWorkspace('kyro-adapter-claude-install-', (installDir) => {
343
+ const { parseAgent } = require(join(repo, 'dist/cli/options.js'));
344
+ const { install } = require(join(repo, 'dist/cli/commands/install.js'));
345
+ const { uninstall } = require(join(repo, 'dist/cli/commands/uninstall.js'));
346
+ const claude = parseAgent('claude');
347
+ const pluginPath = join(installDir, '.claude-plugin', 'plugin.json');
348
+ mkdirSync(join(installDir, '.claude-plugin'), { recursive: true });
349
+ writeFileSync(pluginPath, '{ "name": "user-plugin-fixture" }\n', 'utf-8');
350
+
351
+ captureLogs(() => install(cliOptions({ agents: [claude] })));
352
+ for (const command of ['forge', 'status', 'wrap-up']) {
353
+ const skillPath = join(installDir, '.claude', 'skills', `kyro-${command}`, 'SKILL.md');
354
+ assert(existsSync(skillPath), `claude install: missing CLI-native skill ${command}`);
355
+ }
356
+ assert(readFileSync(pluginPath, 'utf-8').includes('user-plugin-fixture'), 'claude install: .claude-plugin should be untouched');
357
+
358
+ captureLogs(() => uninstall(cliOptions({ purgeAdapterAssets: true })));
359
+ for (const command of ['forge', 'status', 'wrap-up']) {
360
+ assert(!existsSync(join(installDir, '.claude', 'skills', `kyro-${command}`, 'SKILL.md')), `claude uninstall purge: skill ${command} should be removed`);
361
+ }
362
+ assert(readFileSync(pluginPath, 'utf-8').includes('user-plugin-fixture'), 'claude uninstall purge: .claude-plugin should be untouched');
363
+ });
364
+
312
365
  withWorkspace('kyro-adapter-opencode-install-', (installDir) => {
313
366
  const { parseAgent } = require(join(repo, 'dist/cli/options.js'));
314
367
  const { install, sync } = require(join(repo, 'dist/cli/commands/install.js'));
315
368
  const { uninstall } = require(join(repo, 'dist/cli/commands/uninstall.js'));
369
+ const { doctor } = require(join(repo, 'dist/cli/commands/doctor.js'));
316
370
  const opencode = parseAgent('opencode');
317
371
  const home = join(installDir, '.home');
318
372
  const settingsPath = join(home, '.config', 'opencode', 'opencode.json');
@@ -397,6 +451,10 @@ withWorkspace('kyro-adapter-opencode-install-', (installDir) => {
397
451
  }
398
452
  assert(!existsSync(join(home, '.config', 'opencode', 'commands', 'kyro')), 'opencode purge: empty command namespace still present');
399
453
  assert(existsSync(join(home, '.config', 'opencode')), 'opencode purge: shared OpenCode config directory should remain');
454
+
455
+ const doctorAfterPurgeOutput = captureLogs(() => doctor(cliOptions({ adapters: true })));
456
+ assert(doctorAfterPurgeOutput.includes('[PASS] global runtime'), 'opencode purge doctor: global runtime should remain valid after purging adapter assets');
457
+ assert(!doctorAfterPurgeOutput.includes('[FAIL]'), 'opencode purge doctor: should not fail after purging adapter assets');
400
458
  });
401
459
 
402
460
  withWorkspace('kyro-pipeline-rollback-', (cwd) => {
@@ -455,7 +513,6 @@ withWorkspace('kyro-adapter-doctor-', () => {
455
513
  assert(output.includes(`adapter inventory: ${agent}`), `doctor --adapters: missing ${agent}`);
456
514
  }
457
515
  assert(output.includes('status=implemented'), 'doctor --adapters: missing implemented status');
458
- assert(output.includes('status=planned'), 'doctor --adapters: missing planned status');
459
516
  assert(output.includes('capabilities=command-skills'), 'doctor --adapters: missing command-skills capability');
460
517
  assert(output.includes('workspace-agents-block'), 'doctor --adapters: missing workspace AGENTS block capability');
461
518
  });
@@ -574,7 +631,9 @@ withWorkspace('kyro-sync-shared-config-only-', (cwd) => {
574
631
 
575
632
  withWorkspace('kyro-adapter-detect-', () => {
576
633
  const { parseAgent } = require(join(repo, 'dist/cli/options.js'));
634
+ const { adapterMatrix } = require(join(repo, 'dist/cli/commands/adapter.js'));
577
635
  const { detect } = require(join(repo, 'dist/cli/commands/detect.js'));
636
+ const { doctor } = require(join(repo, 'dist/cli/commands/doctor.js'));
578
637
 
579
638
  const textOutput = captureLogs(() => detect(cliOptions()));
580
639
  for (const agent of ['standard', 'opencode', 'codex', 'claude', 'cursor']) {
@@ -590,6 +649,17 @@ withWorkspace('kyro-adapter-detect-', () => {
590
649
  assert(payload.adapters[0].agent === 'codex', 'detect --json: expected codex adapter');
591
650
  assert(payload.adapters[0].systemPromptStrategy === 'managed-block', 'detect --json: missing codex system prompt strategy');
592
651
  assert(payload.adapters[0].mcpStrategy === 'toml-file', 'detect --json: missing codex MCP strategy');
652
+
653
+ const matrixOutput = captureLogs(() => adapterMatrix(cliOptions({ json: true })));
654
+ const matrixPayload = JSON.parse(matrixOutput);
655
+ assert(matrixPayload.schemaVersion === 1, 'adapter matrix --json: missing schema version');
656
+ assert(matrixPayload.adapters.some((adapter) => adapter.agent === 'cursor' && adapter.installable === true), 'adapter matrix --json: missing installable cursor');
657
+ assert(matrixPayload.adapters.some((adapter) => adapter.agent === 'claude' && adapter.systemPromptStrategy === 'instructions-file'), 'adapter matrix --json: missing claude strategy');
658
+
659
+ const migrationOutput = captureLogs(() => doctor(cliOptions({ migration: true, json: true })));
660
+ const migrationPayload = JSON.parse(migrationOutput);
661
+ assert(migrationPayload.schemaVersion === 1, 'doctor --migration --json: missing schema version');
662
+ assert(migrationPayload.migration.availableAdapters.includes('cursor'), 'doctor --migration --json: missing cursor availability');
593
663
  });
594
664
 
595
665
  console.log('Adapter fixtures passed');
@@ -0,0 +1,144 @@
1
+ import { createRequire } from 'node:module';
2
+ import { existsSync, mkdtempSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join, resolve } from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+
7
+ const repo = resolve(new URL('..', import.meta.url).pathname);
8
+ const distRoot = join(repo, 'dist');
9
+ const require = createRequire(import.meta.url);
10
+ const requestedAgents = parseAgents(process.argv.slice(2));
11
+ const agents = requestedAgents.length > 0 ? requestedAgents : ['standard', 'opencode', 'codex', 'cursor', 'claude'];
12
+ const hostBinaryByAgent = {
13
+ standard: null,
14
+ opencode: 'opencode',
15
+ codex: 'codex',
16
+ cursor: 'cursor',
17
+ claude: 'claude',
18
+ };
19
+
20
+ if (!existsSync(join(distRoot, 'cli.js'))) {
21
+ throw new Error('dist/cli.js not found. Run npm run build before npm run smoke:adapters.');
22
+ }
23
+
24
+ const results = [];
25
+
26
+ for (const agentName of agents) {
27
+ const workspace = mkdtempSync(join(tmpdir(), `kyro-smoke-${agentName}-`));
28
+ const previousCwd = process.cwd();
29
+ const previousHome = process.env.HOME;
30
+ try {
31
+ process.chdir(workspace);
32
+ process.env.HOME = join(workspace, '.home');
33
+ clearDistCache();
34
+
35
+ const { parseAgent } = require(join(distRoot, 'cli', 'options.js'));
36
+ const { install, sync } = require(join(distRoot, 'cli', 'commands', 'install.js'));
37
+ const { doctor } = require(join(distRoot, 'cli', 'commands', 'doctor.js'));
38
+ const { uninstall } = require(join(distRoot, 'cli', 'commands', 'uninstall.js'));
39
+ const agent = parseAgent(agentName);
40
+
41
+ captureLogs(() => install(options({ agents: [agent], dryRun: true })));
42
+ captureLogs(() => install(options({ agents: [agent] })));
43
+ captureLogs(() => doctor(options({ adapters: true })));
44
+ captureLogs(() => sync(options({ agents: [agent], dryRun: true })));
45
+ const syncJson = captureLogs(() => sync(options({ agents: [agent], prune: true, dryRun: true, json: true })));
46
+ assert(JSON.parse(syncJson).command === 'sync', `${agentName}: sync --json did not return sync payload`);
47
+ captureLogs(() => uninstall(options({ dryRun: true })));
48
+ captureLogs(() => uninstall(options({ purgeAdapterAssets: true, dryRun: true })));
49
+
50
+ const hostBinary = hostBinaryByAgent[agentName];
51
+ const hostStatus = hostBinary ? checkHostBinary(hostBinary) : 'skipped:no-host-binary';
52
+ results.push({ agent: agentName, status: 'pass', hostStatus });
53
+ } catch (error) {
54
+ results.push({ agent: agentName, status: 'fail', detail: error instanceof Error ? error.message : String(error) });
55
+ } finally {
56
+ process.chdir(previousCwd);
57
+ if (previousHome === undefined) {
58
+ delete process.env.HOME;
59
+ } else {
60
+ process.env.HOME = previousHome;
61
+ }
62
+ clearDistCache();
63
+ rmSync(workspace, { recursive: true, force: true });
64
+ }
65
+ }
66
+
67
+ for (const result of results) {
68
+ if (result.status === 'pass') {
69
+ console.log(`[PASS] ${result.agent}: lifecycle ok; host=${result.hostStatus}`);
70
+ } else {
71
+ console.log(`[FAIL] ${result.agent}: ${result.detail}`);
72
+ }
73
+ }
74
+
75
+ if (results.some((result) => result.status === 'fail')) process.exit(1);
76
+
77
+ function options(overrides = {}) {
78
+ return {
79
+ agents: [],
80
+ scope: 'workspace',
81
+ dryRun: false,
82
+ yes: true,
83
+ help: false,
84
+ tokens: false,
85
+ artifacts: false,
86
+ adapters: false,
87
+ kyroScope: null,
88
+ json: false,
89
+ purgeAdapterAssets: false,
90
+ prune: false,
91
+ migration: false,
92
+ enableMcp: false,
93
+ ...overrides,
94
+ };
95
+ }
96
+
97
+ function captureLogs(callback) {
98
+ const logs = [];
99
+ const originalLog = console.log;
100
+ try {
101
+ console.log = (...args) => logs.push(args.join(' '));
102
+ callback();
103
+ } finally {
104
+ console.log = originalLog;
105
+ }
106
+ return `${logs.join('\n')}\n`;
107
+ }
108
+
109
+ function clearDistCache() {
110
+ for (const key of Object.keys(require.cache)) {
111
+ if (key.startsWith(distRoot)) delete require.cache[key];
112
+ }
113
+ }
114
+
115
+ function assert(condition, message) {
116
+ if (!condition) throw new Error(message);
117
+ }
118
+
119
+ function parseAgents(args) {
120
+ const agentArgIndex = args.findIndex((arg) => arg === '--agent' || arg === '--agents');
121
+ if (agentArgIndex >= 0) {
122
+ return (args[agentArgIndex + 1] ?? '').split(',').map((agent) => agent.trim()).filter(Boolean);
123
+ }
124
+ const inlineArg = args.find((arg) => arg.startsWith('--agent=') || arg.startsWith('--agents='));
125
+ if (inlineArg) {
126
+ return inlineArg.slice(inlineArg.indexOf('=') + 1).split(',').map((agent) => agent.trim()).filter(Boolean);
127
+ }
128
+ return [];
129
+ }
130
+
131
+ function checkHostBinary(binary) {
132
+ try {
133
+ const command = process.platform === 'win32' ? 'where' : 'command';
134
+ const args = process.platform === 'win32' ? [binary] : ['-v', binary];
135
+ const found = spawnSync(command, args, { encoding: 'utf-8', shell: process.platform !== 'win32' });
136
+ if (found.error || found.status !== 0) return 'skipped:not-installed';
137
+
138
+ const version = spawnSync(binary, ['--version'], { encoding: 'utf-8' });
139
+ if (version.error || version.status !== 0) return 'detected:version-check-unavailable';
140
+ return `detected:${version.stdout.trim() || version.stderr.trim() || 'version-ok'}`;
141
+ } catch {
142
+ return 'skipped:host-probe-unavailable';
143
+ }
144
+ }