feat(#2095): migrate Kimi CLI onto EoS imperative adapter + native hook-bus + background dispatch (ADR-1239)
Fold all runtime==='kimi'/isKimi logic branches into descriptor-driven hostBehaviors (localInstallDeferred, verificationStyle, agentManifestStyle, reapplyCommand, doneBannerStyle) + add 'kimi' to _DESCRIPTOR_AGENTS_RUNTIMES. Kimi's skills/kimi-agents dispatch was already descriptor-driven (converter-by- name + kimi-agents kind). Zero isKimi/runtime==='kimi' branches remain. UPGRADE 1 (native hook bus): new hooksSurface 'kimi-hooks-toml' + a marker- delimited config.toml [[hooks]] emitter (buildKimiHooksTomlBlock/writeKimiHooksToml in runtime-hooks-surface.cts; resolveKimiHooksTomlDir in runtime-homes.cts). GSD's lifecycle hooks now wire into Kimi's native ~/.kimi/config.toml (Context7- confirmed path) at SessionStart/PreToolUse/Stop/PreCompact/SubagentStart/ SubagentStop — kimi becomes a hooks/ consumer (the 3 && !isKimi exclusion guards removed). config.toml holds absolute install paths so it's golden-excluded via an exact relative-path (.kimi/config.toml), not a basename (which would blind Codex's config.toml). New hooksSurface value added to the closed enum in capability-validator + runtime-config-adapter-registry. UPGRADE 2 (background dispatch): flip dispatch.backgroundDispatch true (Kimi's Agent tool takes run_in_background; root agent already gets the Agent tool), so negotiation no longer flattens dispatch. subagentToolkit stays 'undocumented' per AC (coder/explore/plan have distinct tool policies). MCP transport explicitly deferred (no installer-driven MCP for any runtime). Golden: only kimi.json changes (hooks/ scripts now installed); all 15 others + claude-local byte-identical (kilo/zcode keep their own exclusions). Tests: kimi-imperative-reference (adapter/axes/fail-closed/hostBehaviors + source-grep guard) + kimi-upgrades (config.toml [[hooks]] SessionStart + marker idempotency + backgroundDispatch negotiation). CONTEXT.md glossary + matrix + how-to updated; changeset (Added). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3412,11 +3412,13 @@ describe('ADR-1016 phase 5a: sample axis value assertions', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('copilot/trae/kimi/windsurf/cline/opencode/kilo: hooksSurface none or copilot-inline/cline-rules, no hookEvents', () => {
|
||||
test('copilot/trae/windsurf/cline/opencode/kilo: hooksSurface none or copilot-inline/cline-rules, no hookEvents', () => {
|
||||
const { capMap } = loadAndValidate(new Set());
|
||||
const registry = buildRegistry(capMap);
|
||||
// opencode and kilo register ZERO lifecycle hooks → hooksSurface none, no hookEvents
|
||||
const noEventsRuntimes = ['trae', 'kimi', 'windsurf', 'opencode', 'kilo'];
|
||||
// #2095: kimi moved OUT of this group — it now has hooksSurface:'kimi-hooks-toml'
|
||||
// and hookEvents:'claude' (see the dedicated kimi hooksSurface test below).
|
||||
const noEventsRuntimes = ['trae', 'windsurf', 'opencode', 'kilo'];
|
||||
for (const id of noEventsRuntimes) {
|
||||
const rt = registry.runtimes[id].runtime;
|
||||
assert.ok(
|
||||
@@ -3440,6 +3442,22 @@ describe('ADR-1016 phase 5a: sample axis value assertions', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('kimi: hooksSurface === kimi-hooks-toml, hookEvents === claude, extendedHookEvents wired (#2095)', () => {
|
||||
const { capMap } = loadAndValidate(new Set());
|
||||
const registry = buildRegistry(capMap);
|
||||
const kimiRt = registry.runtimes['kimi'].runtime;
|
||||
assert.strictEqual(kimiRt.hooksSurface, 'kimi-hooks-toml', 'kimi.hooksSurface must be "kimi-hooks-toml"');
|
||||
assert.strictEqual(kimiRt.hookEvents, 'claude', 'kimi.hookEvents must be "claude" (Kimi\'s 13 lifecycle events include exact-name equivalents for the Claude dialect)');
|
||||
assert.deepStrictEqual(
|
||||
[...kimiRt.extendedHookEvents].sort(),
|
||||
['PreCompact', 'Stop', 'SubagentStart', 'SubagentStop'].sort(),
|
||||
'kimi.extendedHookEvents must wire SubagentStop/Stop/PreCompact/SubagentStart',
|
||||
);
|
||||
// installSurface stays profile-marker-only — the native config.toml
|
||||
// [[hooks]] write is independent of the artifact-install surface (#2095).
|
||||
assert.strictEqual(kimiRt.installSurface, 'profile-marker-only', 'kimi.installSurface must remain "profile-marker-only"');
|
||||
});
|
||||
|
||||
test('codex: hooksSurface === codex-hooks-json, cursor: hooksSurface === cursor-hooks-json', () => {
|
||||
const { capMap } = loadAndValidate(new Set());
|
||||
const registry = buildRegistry(capMap);
|
||||
@@ -3949,12 +3967,12 @@ describe('ADR-1016 phase 5a: closed-vocab set exports', () => {
|
||||
assert.strictEqual(VALID_COMMAND_STYLES.size, 2);
|
||||
});
|
||||
|
||||
test('VALID_HOOKS_SURFACES has exactly 6 values', () => {
|
||||
test('VALID_HOOKS_SURFACES has exactly 7 values', () => {
|
||||
assert.ok(VALID_HOOKS_SURFACES instanceof Set);
|
||||
for (const v of ['settings-json', 'codex-hooks-json', 'cursor-hooks-json', 'copilot-inline', 'cline-rules', 'none']) {
|
||||
for (const v of ['settings-json', 'codex-hooks-json', 'cursor-hooks-json', 'copilot-inline', 'cline-rules', 'kimi-hooks-toml', 'none']) {
|
||||
assert.ok(VALID_HOOKS_SURFACES.has(v), 'VALID_HOOKS_SURFACES must contain "' + v + '"');
|
||||
}
|
||||
assert.strictEqual(VALID_HOOKS_SURFACES.size, 6);
|
||||
assert.strictEqual(VALID_HOOKS_SURFACES.size, 7);
|
||||
});
|
||||
|
||||
test('VALID_HOOK_EVENTS has exactly 2 managed-hook dialects (claude/gemini)', () => {
|
||||
|
||||
26
tests/fixtures/golden-install-parity/kimi.json
vendored
26
tests/fixtures/golden-install-parity/kimi.json
vendored
@@ -1,6 +1,32 @@
|
||||
{
|
||||
".gsd-profile": "0e716a5fef4e6dc1",
|
||||
".gsd/defaults.json": "560664b045e645cb",
|
||||
".kimi/hooks/gsd-check-update-worker.js": "6593f80914f4edee",
|
||||
".kimi/hooks/gsd-check-update.js": "aac7612ee04ff5fe",
|
||||
".kimi/hooks/gsd-config-reload.js": "96546e0e8bb47904",
|
||||
".kimi/hooks/gsd-context-monitor.js": "783c93351e82151f",
|
||||
".kimi/hooks/gsd-cursor-post-tool.js": "9168e0a09de1972a",
|
||||
".kimi/hooks/gsd-cursor-pre-tool.js": "873998b25e308c29",
|
||||
".kimi/hooks/gsd-cursor-session-start.js": "9b2e6f4f0c405375",
|
||||
".kimi/hooks/gsd-cursor-stop.js": "bfaaf60f419e3238",
|
||||
".kimi/hooks/gsd-cursor-subagent-start.js": "06d77fde5c1372b6",
|
||||
".kimi/hooks/gsd-cursor-subagent-stop.js": "4bbf22917da4d389",
|
||||
".kimi/hooks/gsd-ensure-canonical-path.js": "8bd016237c88b738",
|
||||
".kimi/hooks/gsd-graphify-update.sh": "e4c6e14fe6ad64ff",
|
||||
".kimi/hooks/gsd-phase-boundary.sh": "32739d5fbe0d0a1c",
|
||||
".kimi/hooks/gsd-prompt-guard.js": "a749b8cb2c5248de",
|
||||
".kimi/hooks/gsd-read-guard.js": "9e423cd03e2d1b16",
|
||||
".kimi/hooks/gsd-read-injection-scanner.js": "c519598b9257aafa",
|
||||
".kimi/hooks/gsd-session-state.sh": "e54379ba86bf1b6d",
|
||||
".kimi/hooks/gsd-statusline.js": "2736b0885aa97bbf",
|
||||
".kimi/hooks/gsd-update-banner.js": "55143a25f978f301",
|
||||
".kimi/hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
".kimi/hooks/gsd-workflow-guard.js": "91ae24a15d2bca6f",
|
||||
".kimi/hooks/gsd-worktree-path-guard.js": "cfde29a547677422",
|
||||
".kimi/hooks/lib/git-cmd.js": "268ba15992ca0b23",
|
||||
".kimi/hooks/lib/gsd-graphify-rebuild.sh": "66af89601074d2a9",
|
||||
".kimi/hooks/managed-hooks-registry.cjs": "07c6a5ecd724edb3",
|
||||
".kimi/package.json": "dbf8353f77358bc1",
|
||||
"agents/gsd.md": "60fee7782ae4f2c6",
|
||||
"agents/gsd.yaml": "253a23ddda06c6c2",
|
||||
"agents/subagents/gsd-advisor-researcher.md": "81bdc6cbd8fcde40",
|
||||
|
||||
@@ -84,6 +84,24 @@ const PKG_VERSION = require('../package.json').version;
|
||||
// node-runner command as settings.json, so excluded for the same reason (#2086).
|
||||
const HOOK_CONFIG_FILES = new Set(['settings.json', 'settings.local.json', 'hooks.json']);
|
||||
|
||||
// Kimi's native config.toml (#2095 EoS/kimi Upgrade 1) embeds the same
|
||||
// platform-varying node-runner command as the HOOK_CONFIG_FILES above (via the
|
||||
// same buildHookCommand/projectManagedHookCommand machinery), so it needs the
|
||||
// same exclusion — but it is NOT matched by basename like HOOK_CONFIG_FILES:
|
||||
// Codex's OWN config.toml (installSurface 'codex-toml') is a stable, tracked
|
||||
// top-level `config.toml` entry in its golden fixture (it only ever gets a
|
||||
// platform-stable `[features] hooks = true` flag — the real hook commands
|
||||
// live in Codex's separate hooks.json, already excluded above). Blanket-
|
||||
// excluding the 'config.toml' basename would silently blind Codex's fixture
|
||||
// to any future regression there. Kimi's config.toml instead lives OUTSIDE
|
||||
// its GSD configDir at runtime (resolveKimiHooksTomlDir resolves ~/.kimi, a
|
||||
// sibling of the configDir ~/.config/agents) — it only appears inside this
|
||||
// harness's walked tree at all because runMinimalInstall sets HOME to the
|
||||
// same temp root used as --config-dir, collapsing the two into one directory
|
||||
// for the isolated test run. So it is excluded by its exact relative path
|
||||
// under that collapsed root, not by basename.
|
||||
const HOOK_CONFIG_RELATIVE_PATHS = new Set(['.kimi/config.toml']);
|
||||
|
||||
// Path prefixes excluded from the parity manifest. `gsd-core/bin/lib/` holds the
|
||||
// tsc-built runtime artifacts (compiled from src/*.cts) that the install COPIES
|
||||
// verbatim — they are NOT produced by installRuntimeArtifacts (the move's parity
|
||||
@@ -130,6 +148,7 @@ function buildParityManifest(configDir, root) {
|
||||
|
||||
if (VOLATILE_FILES.has(rel)) continue;
|
||||
if (HOOK_CONFIG_FILES.has(path.basename(rel))) continue;
|
||||
if (HOOK_CONFIG_RELATIVE_PATHS.has(rel)) continue;
|
||||
if (EXCLUDED_PREFIXES.some((p) => rel.startsWith(p))) continue;
|
||||
|
||||
const content = fs.readFileSync(full);
|
||||
|
||||
@@ -262,7 +262,8 @@ describe('ADR-1239 Phase A: hostIntegration descriptors', () => {
|
||||
|
||||
// ─── shouldFlattenDispatch per-host (#853 discriminator) ─────────────────────
|
||||
|
||||
// Expected: false (may background) for codex and cursor ONLY; true (must inline) for the other 15.
|
||||
// Expected: false (may background) for codex, cursor, kimi, and opencode;
|
||||
// true (must inline) for the other 13.
|
||||
const EXPECTED_FLATTEN = {
|
||||
antigravity: true,
|
||||
augment: true,
|
||||
@@ -274,7 +275,11 @@ describe('ADR-1239 Phase A: hostIntegration descriptors', () => {
|
||||
cursor: false,
|
||||
hermes: true,
|
||||
kilo: true,
|
||||
kimi: true,
|
||||
// #2095: Kimi Upgrade 2 — Kimi's Agent tool takes a run_in_background
|
||||
// call-time param (Context7 agents.html) → backgroundDispatch flipped to
|
||||
// true → dispatch.background/backgroundDispatch both true → NOT
|
||||
// force-flattened (mirrors the #2087 OpenCode precedent below).
|
||||
kimi: false,
|
||||
// #2087: OpenCode background subagents (v1.15 param, v1.17 default-on) →
|
||||
// dispatch.background/backgroundDispatch true → NOT force-flattened.
|
||||
opencode: false,
|
||||
|
||||
155
tests/kimi-imperative-reference.test.cjs
Normal file
155
tests/kimi-imperative-reference.test.cjs
Normal file
@@ -0,0 +1,155 @@
|
||||
// allow-test-rule: structural-regression-guard — AC2 requires asserting no `runtime === 'kimi'` string-equality branch (nor an `isKimi` helper) remains in bin/install.js, src/install-engine.cts, and src/runtime-artifact-conversion.cts — the descriptor-migration contract is a property of the source text, so a source-grep is the only faithful check (#2095)
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* kimi imperative reference host — ADR-1239 Phase D / #2095 (EoS/kimi).
|
||||
*
|
||||
* Proves kimi is driven through the PUBLIC Host-Integration Interface (the
|
||||
* imperative adapter), that its negotiated axes classify + negotiate correctly,
|
||||
* that negotiation fails CLOSED on a corrupted descriptor, and that the
|
||||
* migration retired the hardcoded `runtime === 'kimi'` / `isKimi` branches
|
||||
* across the install engine and artifact conversion modules (folded into
|
||||
* descriptor-driven `runtime.hostBehaviors`).
|
||||
*
|
||||
* kimi contributes two real upgrades covered in tests/kimi-upgrades.test.cjs:
|
||||
*
|
||||
* UPGRADE 1 — native hook bus: `hooksSurface` moved from `"none"` to
|
||||
* `"kimi-hooks-toml"` — GSD's lifecycle hooks are now registered as
|
||||
* `[[hooks]]` array-of-tables entries in Kimi's own native
|
||||
* `~/.kimi/config.toml` (Beta feature on Kimi's side), marker-delimited so
|
||||
* reinstall only ever touches GSD's own block.
|
||||
*
|
||||
* UPGRADE 2 — background dispatch: `hostIntegration.dispatch.backgroundDispatch`
|
||||
* flipped `false` → `true`, which flips `shouldFlattenDispatch` to `false`
|
||||
* for kimi (joining codex/cursor/opencode as background-eligible).
|
||||
*
|
||||
* kimi's `dispatch.subagentToolkit` stays `'undocumented'` (no authoritative
|
||||
* doc found), so this file also proves that axis degrades closed to
|
||||
* `'read-only'` on negotiation rather than being trusted at face value.
|
||||
*/
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const { createImperativeAdapter } = require('../gsd-core/bin/lib/adapter-imperative.cjs');
|
||||
const {
|
||||
profileOf,
|
||||
negotiateHostCapabilities,
|
||||
PROFILE_BASELINES,
|
||||
UNDOCUMENTED,
|
||||
} = require('../gsd-core/bin/lib/host-integration.cjs');
|
||||
|
||||
const KIMI_CAP = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, '..', 'capabilities', 'kimi', 'capability.json'), 'utf8'),
|
||||
);
|
||||
const KIMI_AXES = KIMI_CAP.runtime.hostIntegration;
|
||||
|
||||
// -- AC2: driven through the public interface (imperative adapter) -----------
|
||||
|
||||
test('createImperativeAdapter classifies kimi as imperative + composes the registry', () => {
|
||||
const adapter = createImperativeAdapter({ runtime: 'kimi' });
|
||||
assert.equal(adapter.kind, 'imperative');
|
||||
assert.equal(adapter.runtime, 'kimi');
|
||||
assert.ok(adapter.registry && typeof adapter.registry === 'object');
|
||||
assert.equal(typeof adapter.install, 'function');
|
||||
assert.equal(typeof adapter.uninstall, 'function');
|
||||
});
|
||||
|
||||
test('kimi axes classify as the programmatic-cli reference profile', () => {
|
||||
assert.equal(profileOf(KIMI_AXES), 'programmatic-cli');
|
||||
});
|
||||
|
||||
// -- AC3: all axes populated + validated -------------------------------------
|
||||
|
||||
test('kimi descriptor declares all 8 axes + 6 dispatch sub-axes with exact values', () => {
|
||||
assert.equal(KIMI_AXES.embeddingMode, 'imperative');
|
||||
assert.equal(KIMI_AXES.commandSurface, 'slash-file');
|
||||
assert.equal(KIMI_AXES.modelMode, 'passive');
|
||||
assert.equal(KIMI_AXES.hookBus, 'host');
|
||||
assert.equal(KIMI_AXES.stateIO, 'filesystem');
|
||||
assert.equal(KIMI_AXES.transport, 'mcp');
|
||||
assert.equal(KIMI_AXES.runtime, 'python');
|
||||
const d = KIMI_AXES.dispatch;
|
||||
assert.equal(d.namedDispatch, true);
|
||||
assert.equal(d.nested, false);
|
||||
assert.equal(d.maxDepth, 1);
|
||||
assert.equal(d.background, true);
|
||||
assert.equal(d.subagentToolkit, 'undocumented');
|
||||
assert.equal(d.backgroundDispatch, true);
|
||||
});
|
||||
|
||||
// -- AC5: negotiation fails CLOSED on a corrupted descriptor ------------------
|
||||
|
||||
test('negotiateHostCapabilities never throws for kimi, even fully corrupted', () => {
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({}));
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({ ...KIMI_AXES, embeddingMode: UNDOCUMENTED }));
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({ ...KIMI_AXES, embeddingMode: 'future-unknown' }));
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({ ...KIMI_AXES, dispatch: 'corrupted-not-an-object' }));
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({ ...KIMI_AXES, dispatch: { ...KIMI_AXES.dispatch, maxDepth: 'not-a-number' } }));
|
||||
});
|
||||
|
||||
test('a partial/empty kimi descriptor degrades to the safe floor, not the programmatic-cli baseline', () => {
|
||||
const result = negotiateHostCapabilities({});
|
||||
assert.equal(result.effective.embeddingMode, 'declarative', 'omitted embeddingMode degrades closed');
|
||||
assert.equal(result.effective.hookBus, 'none');
|
||||
assert.notDeepEqual(result.effective, PROFILE_BASELINES['programmatic-cli']);
|
||||
assert.ok(result.warnings.length > 0);
|
||||
});
|
||||
|
||||
test("kimi's undocumented dispatch.subagentToolkit degrades closed to 'read-only' on negotiation", () => {
|
||||
const { effective, warnings } = negotiateHostCapabilities(KIMI_AXES);
|
||||
assert.equal(KIMI_AXES.dispatch.subagentToolkit, 'undocumented', 'sanity: the descriptor itself stays undocumented');
|
||||
assert.equal(effective.dispatch.subagentToolkit, 'read-only', 'undocumented never propagates as full — degrades closed');
|
||||
assert.ok(
|
||||
warnings.some((w) => /subagentToolkit is undocumented/.test(w)),
|
||||
'a warning must be raised for the undocumented axis',
|
||||
);
|
||||
});
|
||||
|
||||
// -- AC2: the folded-in behaviors ---------------------------------------------
|
||||
|
||||
test('kimi descriptor declares runtime.hostBehaviors (the folded-in behaviors)', () => {
|
||||
const hb = KIMI_CAP.runtime.hostBehaviors;
|
||||
assert.ok(hb && typeof hb === 'object');
|
||||
assert.equal(hb.reapplyCommand, '/skill:gsd-update --reapply');
|
||||
assert.equal(hb.localInstallDeferred, true);
|
||||
assert.equal(hb.verificationStyle, 'kimi');
|
||||
assert.equal(hb.agentManifestStyle, 'kimi-nested');
|
||||
assert.equal(hb.doneBannerStyle, 'kimi-agent-file');
|
||||
});
|
||||
|
||||
// -- boundary/negative: hooksSurface + extendedHookEvents are exactly what's documented
|
||||
|
||||
test('capabilities/kimi/capability.json hooksSurface is "kimi-hooks-toml" and extendedHookEvents is exactly the 4 documented events', () => {
|
||||
assert.equal(KIMI_CAP.runtime.hooksSurface, 'kimi-hooks-toml');
|
||||
const events = KIMI_CAP.runtime.extendedHookEvents;
|
||||
assert.deepEqual(events, ['SubagentStop', 'Stop', 'PreCompact', 'SubagentStart']);
|
||||
assert.equal(events.length, 4);
|
||||
});
|
||||
|
||||
// -- AC2: the hardcoded branches are retired across all folded modules -------
|
||||
|
||||
test('no `runtime === "kimi"` string-equality branch (nor an `isKimi` helper) remains in the descriptor-migrated modules (AC2)', () => {
|
||||
const strip = (src) => src
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
.replace(/\/\/[^\r\n]*/g, '')
|
||||
.replace(/`[^`]*`/g, '');
|
||||
const repoRoot = path.join(__dirname, '..');
|
||||
const files = [
|
||||
path.join(repoRoot, 'bin', 'install.js'),
|
||||
path.join(repoRoot, 'src', 'install-engine.cts'),
|
||||
path.join(repoRoot, 'src', 'runtime-artifact-conversion.cts'),
|
||||
];
|
||||
for (const file of files) {
|
||||
const src = fs.readFileSync(file, 'utf8');
|
||||
const stripped = strip(src);
|
||||
const offenders = stripped.match(/runtime\s*[!=]==\s*'kimi'/g) || [];
|
||||
assert.deepEqual(offenders, [],
|
||||
`AC2: no hardcoded runtime==='kimi' branch may remain in ${path.relative(repoRoot, file)}; found: ${offenders.join(', ')}`);
|
||||
const isKimiHits = stripped.match(/\bisKimi\b/g) || [];
|
||||
assert.deepEqual(isKimiHits, [],
|
||||
`AC2: no isKimi helper may remain in ${path.relative(repoRoot, file)}; found ${isKimiHits.length} occurrence(s)`);
|
||||
}
|
||||
});
|
||||
321
tests/kimi-upgrades.test.cjs
Normal file
321
tests/kimi-upgrades.test.cjs
Normal file
@@ -0,0 +1,321 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* kimi capability UPGRADES — ADR-1239 Phase D / #2095 (EoS/kimi).
|
||||
*
|
||||
* Drives the user-reachable surface (spawned `bin/install.js` via
|
||||
* `runMinimalInstall`) plus targeted unit coverage to prove the two real
|
||||
* upgrades kimi contributes as part of the imperative-adapter migration:
|
||||
*
|
||||
* UPGRADE 1 — native hook bus: `hooksSurface` moved from `"none"` to
|
||||
* `"kimi-hooks-toml"`. GSD's lifecycle hook scripts (session-state,
|
||||
* phase-boundary, graphify, context monitor, the prompt/read/workflow/
|
||||
* worktree guards, commit validation) are now registered as `[[hooks]]`
|
||||
* array-of-tables entries inside Kimi's own native config.toml (default
|
||||
* `~/.kimi/config.toml`, overridable via Kimi's `KIMI_SHARE_DIR`), wrapped
|
||||
* in `# GSD Hooks BEGIN`/`END` marker comments so a reinstall only ever
|
||||
* rewrites GSD's own block (idempotency contract). `hooks/`, `hooks/lib/`,
|
||||
* and the CommonJS `package.json` marker now also install for kimi — but
|
||||
* SELF-CONTAINED under `~/.kimi/` (alongside config.toml), never under the
|
||||
* generic Agent-Skills configDir GSD installs skills/agents into. Kimi's
|
||||
* contract forbids hooks/ or package.json under that generic root (see
|
||||
* capabilities/kimi/capability.json hostBehaviors.skipSharedHooksInstall,
|
||||
* and tests/kimi-imperative-reference.test.cjs's source-grep guard).
|
||||
*
|
||||
* Kimi's local install is intentionally deferred (Phase 2): `--kimi
|
||||
* --local` exits 0 and writes nothing (`hostBehaviors.localInstallDeferred:
|
||||
* true`, verified by manual spawn during authoring — see PR description).
|
||||
* This file therefore only exercises `--global`.
|
||||
*
|
||||
* UPGRADE 2 — background dispatch: `hostIntegration.dispatch.backgroundDispatch`
|
||||
* flipped `false` → `true` (Kimi's `Agent` tool takes a call-time
|
||||
* `run_in_background` param), which flips `shouldFlattenDispatch` to
|
||||
* `false` for kimi — a negotiation-only axis with no install-output effect.
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
|
||||
const { runMinimalInstall, INSTALL_SCRIPT, installerEnv } = require('./helpers/install-shared.cjs');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
const {
|
||||
negotiateHostCapabilities,
|
||||
shouldFlattenDispatch,
|
||||
} = require('../gsd-core/bin/lib/host-integration.cjs');
|
||||
const {
|
||||
stripKimiHooksTomlBlock,
|
||||
writeKimiHooksToml,
|
||||
removeKimiHooksToml,
|
||||
KIMI_HOOKS_TOML_MARKER_BEGIN,
|
||||
KIMI_HOOKS_TOML_MARKER_END,
|
||||
} = require('../gsd-core/bin/lib/runtime-hooks-surface.cjs');
|
||||
|
||||
const KIMI_CAP = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, '..', 'capabilities', 'kimi', 'capability.json'), 'utf8'),
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UPGRADE 1: native hook bus (~/.kimi/config.toml [[hooks]])
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('kimi --global: native config.toml [[hooks]] bus wired at <HOME>/.kimi/config.toml (UPGRADE 1)', (t) => {
|
||||
const { root } = runMinimalInstall({ runtime: 'kimi', scope: 'global' });
|
||||
t.after(() => cleanup(root));
|
||||
|
||||
// FACTS: root === sandbox HOME for this helper (env HOME/USERPROFILE are
|
||||
// both pointed at `root`), and Kimi's native hook config is a sibling of
|
||||
// the generic-agents-root GSD installs skills/agents into: <HOME>/.kimi/config.toml.
|
||||
const tomlPath = path.join(root, '.kimi', 'config.toml');
|
||||
assert.ok(fs.existsSync(tomlPath), `${tomlPath} must exist`);
|
||||
|
||||
const toml = fs.readFileSync(tomlPath, 'utf8');
|
||||
|
||||
// Idempotency contract: GSD's block is delimited by these exact markers so
|
||||
// a reinstall can strip-and-rewrite only its own content.
|
||||
assert.match(toml, /^# GSD Hooks BEGIN — managed by GSD, do not edit between these markers$/m,
|
||||
'config.toml must carry the GSD Hooks BEGIN marker');
|
||||
assert.match(toml, /^# GSD Hooks END$/m,
|
||||
'config.toml must carry the GSD Hooks END marker');
|
||||
|
||||
const beginIdx = toml.indexOf('# GSD Hooks BEGIN');
|
||||
const endIdx = toml.indexOf('# GSD Hooks END');
|
||||
assert.ok(beginIdx !== -1 && endIdx !== -1 && beginIdx < endIdx,
|
||||
'both markers must be present and correctly ordered');
|
||||
const managedBlock = toml.slice(beginIdx, endIdx);
|
||||
|
||||
// Minimal TOML parse-check: an array-of-tables ([[hooks]]), each with an
|
||||
// event key — not a single [hooks] table.
|
||||
const hooksTables = managedBlock.match(/^\[\[hooks\]\]$/gm) || [];
|
||||
assert.ok(hooksTables.length > 0, 'the managed block must contain at least one [[hooks]] array-of-tables entry');
|
||||
const eventLines = managedBlock.match(/^event = ".+"$/gm) || [];
|
||||
assert.equal(eventLines.length, hooksTables.length,
|
||||
'every [[hooks]] table must declare an event key (array-of-tables shape, not a bare [hooks] map)');
|
||||
|
||||
// A SessionStart [[hooks]] entry must exist inside the markers.
|
||||
assert.match(managedBlock, /\[\[hooks\]\]\nevent = "SessionStart"/,
|
||||
'a [[hooks]] block with event = "SessionStart" must be inside the GSD markers');
|
||||
|
||||
// At least one hook command references an installed hook script path.
|
||||
assert.match(managedBlock, /command = ".*gsd-check-update\.js.*"/,
|
||||
'a hook command must reference the installed gsd-check-update.js script');
|
||||
|
||||
// The hooks/ scripts themselves are installed under Kimi's OWN native hook
|
||||
// root (~/.kimi/hooks) — NOT under the generic Agent-Skills configDir
|
||||
// (targetDir) GSD installs skills/agents into. Kimi's contract forbids
|
||||
// hooks/ or package.json under that generic root (capabilities/kimi/
|
||||
// capability.json declares hostBehaviors.skipSharedHooksInstall:true), so
|
||||
// the shared hook bundle is self-contained alongside config.toml instead.
|
||||
const hooksDir = path.join(root, '.kimi', 'hooks');
|
||||
assert.ok(fs.existsSync(path.join(hooksDir, 'gsd-check-update.js')),
|
||||
'hooks/gsd-check-update.js must be installed under ~/.kimi/hooks — the command above references it by path');
|
||||
assert.ok(fs.existsSync(path.join(hooksDir, 'lib')),
|
||||
'hooks/lib/ helpers must also install for kimi, under ~/.kimi/hooks/lib');
|
||||
assert.ok(!fs.existsSync(path.join(root, 'hooks')),
|
||||
'hooks/ must NOT be installed under the generic Agent-Skills configDir for kimi');
|
||||
assert.ok(!fs.existsSync(path.join(root, 'package.json')),
|
||||
'package.json (CommonJS marker) must NOT be installed under the generic Agent-Skills configDir for kimi');
|
||||
assert.ok(fs.existsSync(path.join(root, '.kimi', 'package.json')),
|
||||
'package.json (CommonJS marker) must be installed alongside config.toml under ~/.kimi');
|
||||
});
|
||||
|
||||
test('kimi --global: reinstalling is idempotent — the GSD [[hooks]] block is not duplicated', (t) => {
|
||||
const { root } = runMinimalInstall({ runtime: 'kimi', scope: 'global' });
|
||||
t.after(() => cleanup(root));
|
||||
|
||||
const tomlPath = path.join(root, '.kimi', 'config.toml');
|
||||
const first = fs.readFileSync(tomlPath, 'utf8');
|
||||
|
||||
const beginMarkers = (content) => (content.match(/# GSD Hooks BEGIN/g) || []).length;
|
||||
const endMarkers = (content) => (content.match(/# GSD Hooks END/g) || []).length;
|
||||
assert.equal(beginMarkers(first), 1, 'exactly one BEGIN marker after a fresh install');
|
||||
assert.equal(endMarkers(first), 1, 'exactly one END marker after a fresh install');
|
||||
|
||||
// Reinstall over the SAME root/config (runMinimalInstall always mkdtemps a
|
||||
// fresh root, so the reinstall is driven directly against this test's root
|
||||
// exactly the way runMinimalInstall drives its own install internally).
|
||||
const reinstall = spawnSync(process.execPath, [INSTALL_SCRIPT, '--kimi', '--global', '--config-dir', root], {
|
||||
cwd: process.cwd(), encoding: 'utf8',
|
||||
env: installerEnv({ HOME: root, USERPROFILE: root }),
|
||||
});
|
||||
assert.strictEqual(reinstall.status, 0,
|
||||
`reinstall exited with status ${reinstall.status}\nstdout: ${reinstall.stdout}\nstderr: ${reinstall.stderr}`);
|
||||
const second = fs.readFileSync(tomlPath, 'utf8');
|
||||
assert.equal(beginMarkers(second), 1, 'reinstall must not duplicate the BEGIN marker');
|
||||
assert.equal(endMarkers(second), 1, 'reinstall must not duplicate the END marker');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Code-review regression: stripKimiHooksTomlBlock must never glue two user
|
||||
// TOML sections together when the GSD block sits between them, and must
|
||||
// never delete to EOF when the marker pair is malformed (BEGIN without a
|
||||
// matching END). Exercised directly against the exported pure function
|
||||
// (unit-level) and via writeKimiHooksToml/removeKimiHooksToml (the real
|
||||
// install/uninstall callers), so both the read seam and both call sites are
|
||||
// covered.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function gsdHooksBlock(entry = '[[hooks]]\nevent = "SessionStart"\ncommand = "x"') {
|
||||
return `${KIMI_HOOKS_TOML_MARKER_BEGIN}\n\n${entry}\n\n${KIMI_HOOKS_TOML_MARKER_END}`;
|
||||
}
|
||||
|
||||
describe('stripKimiHooksTomlBlock (code-review regression)', () => {
|
||||
test('block at EOF is stripped cleanly (existing idempotent case still works)', () => {
|
||||
const input = `[providers]\nx = 1\n\n${gsdHooksBlock()}\n`;
|
||||
const result = stripKimiHooksTomlBlock(input);
|
||||
assert.equal(result, '[providers]\nx = 1');
|
||||
});
|
||||
|
||||
test('block BETWEEN two user sections is stripped WITHOUT gluing the sections together', () => {
|
||||
const input = `[providers]\nx = 1\n\n${gsdHooksBlock()}\n\n[models]\ny = 2\n`;
|
||||
const result = stripKimiHooksTomlBlock(input);
|
||||
assert.equal(result, '[providers]\nx = 1\n\n[models]\ny = 2\n');
|
||||
assert.doesNotMatch(result, /x = 1\[models\]/, 'the two user sections must not be glued onto one line');
|
||||
});
|
||||
|
||||
test('block at START with user content after is stripped without a stray leading blank line', () => {
|
||||
const input = `${gsdHooksBlock()}\n\n[models]\ny = 2\n`;
|
||||
const result = stripKimiHooksTomlBlock(input);
|
||||
assert.equal(result, '[models]\ny = 2\n');
|
||||
});
|
||||
|
||||
test('BEGIN present but END missing leaves content UNCHANGED (never deletes to EOF)', () => {
|
||||
const input = `[providers]\nx = 1\n\n${KIMI_HOOKS_TOML_MARKER_BEGIN}\n\n[[hooks]]\nevent = "SessionStart"\n\n[models]\ny = 2\n`;
|
||||
const result = stripKimiHooksTomlBlock(input);
|
||||
assert.equal(result, input, 'a malformed marker pair (no END) must be a no-op, not a deletion to EOF');
|
||||
});
|
||||
|
||||
test('no markers at all is a no-op', () => {
|
||||
const input = '[providers]\nx = 1\n\n[models]\ny = 2\n';
|
||||
const result = stripKimiHooksTomlBlock(input);
|
||||
assert.equal(result, input);
|
||||
});
|
||||
});
|
||||
|
||||
describe('writeKimiHooksToml / removeKimiHooksToml (code-review regression: surrounding user content)', () => {
|
||||
function makeHooksDir(t) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'kimi-hooksdir-'));
|
||||
t.after(() => cleanup(dir));
|
||||
fs.mkdirSync(path.join(dir, 'hooks'), { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, 'hooks', 'gsd-check-update.js'), '// stub');
|
||||
return dir;
|
||||
}
|
||||
|
||||
test('reinstall over a config.toml with user sections BEFORE and AFTER the GSD block preserves both, un-glued', (t) => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'kimi-toml-'));
|
||||
t.after(() => cleanup(tmp));
|
||||
const configPath = path.join(tmp, 'config.toml');
|
||||
fs.writeFileSync(configPath, '[providers]\nx = 1\n\n[models]\ny = 2\n');
|
||||
const hooksDir = makeHooksDir(t);
|
||||
const hookOpts = { platform: 'linux', node: 'node', bash: '/bin/bash' };
|
||||
|
||||
writeKimiHooksToml(configPath, hooksDir, { hookOpts });
|
||||
// Re-seed a user section AFTER the newly-written GSD block, mirroring a
|
||||
// hand-edited config.toml, then reinstall on top of it.
|
||||
fs.appendFileSync(configPath, '\n[extra]\nz = 3\n');
|
||||
writeKimiHooksToml(configPath, hooksDir, { hookOpts });
|
||||
|
||||
const content = fs.readFileSync(configPath, 'utf8');
|
||||
assert.match(content, /\[providers\]\s*\nx = 1/, 'the earlier [providers] section must survive');
|
||||
assert.match(content, /\[extra\]\s*\nz = 3/, 'the later [extra] section must survive');
|
||||
assert.doesNotMatch(content, /GSD Hooks END\s*\[extra\]/, 'GSD Hooks END must not glue directly onto [extra]');
|
||||
});
|
||||
|
||||
test('uninstall (removeKimiHooksToml) with user content on both sides of the block preserves it, un-glued', (t) => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'kimi-toml-remove-'));
|
||||
t.after(() => cleanup(tmp));
|
||||
const configPath = path.join(tmp, 'config.toml');
|
||||
const block = gsdHooksBlock();
|
||||
fs.writeFileSync(configPath, `[providers]\nx = 1\n\n${block}\n\n[models]\ny = 2\n`);
|
||||
|
||||
const result = removeKimiHooksToml(configPath);
|
||||
assert.equal(result.changed, true);
|
||||
const content = fs.readFileSync(configPath, 'utf8');
|
||||
assert.equal(content, '[providers]\nx = 1\n\n[models]\ny = 2\n');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UPGRADE 2: background dispatch (backgroundDispatch: false -> true)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('UPGRADE 2: capabilities/kimi/capability.json declares dispatch.background && dispatch.backgroundDispatch as true', () => {
|
||||
const d = KIMI_CAP.runtime.hostIntegration.dispatch;
|
||||
assert.equal(d.background, true);
|
||||
assert.equal(d.backgroundDispatch, true);
|
||||
});
|
||||
|
||||
test('UPGRADE 2: negotiateHostCapabilities against kimi axes yields effective.dispatch.backgroundDispatch === true, and shouldFlattenDispatch is false (background now allowed)', () => {
|
||||
const KIMI_AXES = KIMI_CAP.runtime.hostIntegration;
|
||||
const { effective } = negotiateHostCapabilities(KIMI_AXES);
|
||||
assert.equal(effective.dispatch.backgroundDispatch, true);
|
||||
assert.equal(shouldFlattenDispatch(effective.dispatch), false,
|
||||
'kimi may now background — dispatch must not be flattened to inline');
|
||||
});
|
||||
|
||||
test('UPGRADE 2: a corrupted/undeclared dispatch still fails closed to inline (shouldFlattenDispatch === true)', () => {
|
||||
assert.equal(shouldFlattenDispatch(null), true, 'null dispatch must flatten (fail-closed)');
|
||||
assert.equal(shouldFlattenDispatch(undefined), true, 'undefined dispatch must flatten (fail-closed)');
|
||||
assert.equal(shouldFlattenDispatch({ background: true, backgroundDispatch: 'undocumented' }), true,
|
||||
'a non-boolean-true backgroundDispatch must still flatten');
|
||||
});
|
||||
|
||||
// Code-review AC4 follow-up: negotiating backgroundDispatch=true is necessary
|
||||
// but not sufficient — Kimi's run_in_background dispatch (moonshotai.github.io
|
||||
// /kimi-cli/en/customization/agents.html) is only reachable through a root
|
||||
// agent whose YAML `tools:` list actually grants kimi_cli.tools.agent:Agent.
|
||||
// This is the installer-testable proxy for UPGRADE 2: a real `--kimi --global`
|
||||
// install (which generates subagents from GSD's own agents/ dir) must emit
|
||||
// that tool grant on the root agent. Exercising the actual run_in_background
|
||||
// call is Kimi's own runtime behavior, out of the installer's test scope (see
|
||||
// docs/reference/host-integration-capability-matrix.md's kimi EoS-status
|
||||
// paragraph) — the installer's deliverable stops at the Agent-tool grant plus
|
||||
// the negotiated backgroundDispatch axis asserted above.
|
||||
test('UPGRADE 2 (installer-testable proxy): kimi --global install with subagents present grants kimi_cli.tools.agent:Agent on the root agent', (t) => {
|
||||
const { root } = runMinimalInstall({ runtime: 'kimi', scope: 'global' });
|
||||
t.after(() => cleanup(root));
|
||||
|
||||
const rootYamlPath = path.join(root, 'agents', 'gsd.yaml');
|
||||
assert.ok(fs.existsSync(rootYamlPath), 'kimi: agents/gsd.yaml must exist');
|
||||
const rootYaml = fs.readFileSync(rootYamlPath, 'utf8');
|
||||
|
||||
// Sanity: subagents must actually be present, otherwise this assertion
|
||||
// would prove nothing about the background-dispatch grant.
|
||||
assert.match(rootYaml, /^\s*subagents:\s*$/m,
|
||||
'a real kimi install must generate at least one subagent for this proxy to be meaningful');
|
||||
assert.match(rootYaml, /kimi_cli\.tools\.agent:Agent/,
|
||||
'root agent YAML must grant kimi_cli.tools.agent:Agent — the tool grant that enables run_in_background dispatch');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Boundary: extendedHookEvents is exactly the 4 documented events, and each
|
||||
// one is a real, wired Kimi event — not merely declared in capability.json.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('capabilities/kimi/capability.json extendedHookEvents contains exactly the 4 documented events', () => {
|
||||
const events = KIMI_CAP.runtime.extendedHookEvents;
|
||||
assert.deepEqual(events, ['SubagentStop', 'Stop', 'PreCompact', 'SubagentStart']);
|
||||
assert.equal(events.length, 4);
|
||||
});
|
||||
|
||||
test('boundary: every capability-declared extendedHookEvent is wired as a real event = "..." entry in config.toml, not merely declared', (t) => {
|
||||
const { root } = runMinimalInstall({ runtime: 'kimi', scope: 'global' });
|
||||
t.after(() => cleanup(root));
|
||||
|
||||
const tomlPath = path.join(root, '.kimi', 'config.toml');
|
||||
const toml = fs.readFileSync(tomlPath, 'utf8');
|
||||
|
||||
for (const event of KIMI_CAP.runtime.extendedHookEvents) {
|
||||
assert.match(toml, new RegExp(`event = "${event}"`),
|
||||
`${event} must be wired as an actual [[hooks]] entry, not merely declared in capability.json`);
|
||||
}
|
||||
// Sanity: the base (non-extended) claude-dialect events are also wired,
|
||||
// since kimi's hookEvents dialect is "claude" (SessionStart/PreToolUse/PostToolUse).
|
||||
for (const event of ['SessionStart', 'PreToolUse', 'PostToolUse']) {
|
||||
assert.match(toml, new RegExp(`event = "${event}"`),
|
||||
`base claude-dialect event ${event} must also be wired for kimi`);
|
||||
}
|
||||
});
|
||||
@@ -20,6 +20,8 @@ const {
|
||||
convertClaudeCommandToOpencodeSkill,
|
||||
convertClaudeCommandToKiloSkill,
|
||||
convertClaudeCommandToTraeSkill,
|
||||
convertClaudeCommandToKimiSkill,
|
||||
buildKimiAgentArtifacts,
|
||||
neutralizeAgentReferences,
|
||||
} = require('../bin/install.js');
|
||||
|
||||
@@ -310,6 +312,58 @@ Do nothing.`;
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// DEFECT.GENERATIVE-FIX output-parity guard: convertClaudeCommandToKimiSkill
|
||||
// and buildKimiAgentArtifacts are each defined TWICE — once in bin/install.js
|
||||
// (dead for the live install path; kept for this file's own module-level
|
||||
// export/test surface) and once in src/runtime-artifact-conversion.cts,
|
||||
// compiled to gsd-core/bin/lib/runtime-artifact-conversion.cjs (used by
|
||||
// src/install-engine.cts's skills-install path via SKILLS_CONVERTER_REGISTRY
|
||||
// for skills, and by src/runtime-artifact-layout.cts's kimiAgentsKind for
|
||||
// agent YAML — see runtime-artifact-layout.cts ~L268/L289). Both copies are
|
||||
// LIVE call surfaces — neither re-exports the other, so any future
|
||||
// kimi-specific fix (e.g. the #2095 code-review fixes) must be applied to
|
||||
// both by hand. Source-text identity can't be asserted (different module
|
||||
// systems: plain CJS vs a tsc-compiled .cts output with different
|
||||
// surrounding comments), so this instead proves the two implementations
|
||||
// still produce IDENTICAL output for representative command and agent
|
||||
// input. If a future edit changes one copy's behavior without mirroring it
|
||||
// into the other, this test is the guard that catches the divergence.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
describe('convertClaudeCommandToKimiSkill / buildKimiAgentArtifacts output parity: bin/install.js vs runtime-artifact-conversion.cjs (#2095)', () => {
|
||||
const {
|
||||
convertClaudeCommandToKimiSkill: convertViaConversionModule,
|
||||
buildKimiAgentArtifacts: buildViaConversionModule,
|
||||
} = require('../gsd-core/bin/lib/runtime-artifact-conversion.cjs');
|
||||
|
||||
test('identical skill output for a representative command', () => {
|
||||
const viaInstall = convertClaudeCommandToKimiSkill(SAMPLE_COMMAND, 'gsd-execute-phase');
|
||||
const viaModule = convertViaConversionModule(SAMPLE_COMMAND, 'gsd-execute-phase');
|
||||
assert.equal(viaInstall, viaModule, 'bin/install.js and runtime-artifact-conversion.cjs must emit identical Kimi skill output');
|
||||
});
|
||||
|
||||
test('identical skill output when the source has no description (falls back to generic description)', () => {
|
||||
const noDescriptionCommand = `---
|
||||
name: gsd-noop
|
||||
allowed-tools:
|
||||
- Read
|
||||
---
|
||||
|
||||
Do nothing.`;
|
||||
const viaInstall = convertClaudeCommandToKimiSkill(noDescriptionCommand, 'gsd-noop');
|
||||
const viaModule = convertViaConversionModule(noDescriptionCommand, 'gsd-noop');
|
||||
assert.equal(viaInstall, viaModule, 'bin/install.js and runtime-artifact-conversion.cjs must emit identical output when description is absent');
|
||||
});
|
||||
|
||||
test('identical agent-artifact output for a representative agent with a subagent, including the Agent tool grant', () => {
|
||||
const viaInstall = buildKimiAgentArtifacts({ rootAgent: SAMPLE_AGENT, subagents: [SAMPLE_AGENT] });
|
||||
const viaModule = buildViaConversionModule({ rootAgent: SAMPLE_AGENT, subagents: [SAMPLE_AGENT] });
|
||||
assert.deepEqual(viaInstall, viaModule, 'bin/install.js and runtime-artifact-conversion.cjs must emit an identical artifact bundle');
|
||||
assert.match(viaInstall.root.yaml, /kimi_cli\.tools\.agent:Agent/,
|
||||
'both copies must grant the Agent tool when a subagent is present (#2095 Upgrade 2)');
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Antigravity agent conversion — shared Gemini-backend tool mapping (#1394 / #1928)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -27,11 +27,31 @@ const INSTALL_JS = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
// include both files so structural invariants are verified against the correct source.
|
||||
const HOOKS_SURFACE_SRC = path.join(__dirname, '..', 'src', 'runtime-hooks-surface.cts');
|
||||
|
||||
// Hooks whose registration lives in runtime-hooks-surface module, not install.js.
|
||||
// These are excluded from the install.js source-scan and validated behaviorally.
|
||||
// Hooks whose registration is NOT a literal buildHookCommand(..., '<name>', ...)
|
||||
// call in install.js/runtime-hooks-surface.cts, so the source-scan below cannot
|
||||
// find them there. These are excluded from that scan and validated/documented
|
||||
// elsewhere.
|
||||
const MODULE_OWNED_HOOKS = new Set([
|
||||
// Cursor lifecycle hooks — registered by writeCursorHooksJson via the
|
||||
// descriptor-driven CURSOR_EVENT_SCRIPT_MAP indirection
|
||||
// (src/host-integration-adapters/imperative-hook-bus.cts), never a literal
|
||||
// buildHookCommand(..., '<hook-name>', ...) call this source-scan can match.
|
||||
// Validated behaviorally in the describe block below.
|
||||
'gsd-cursor-session-start.js',
|
||||
'gsd-cursor-post-tool.js',
|
||||
'gsd-cursor-pre-tool.js',
|
||||
'gsd-cursor-stop.js',
|
||||
'gsd-cursor-subagent-start.js',
|
||||
'gsd-cursor-subagent-stop.js',
|
||||
// gsd-check-update-worker.js is an implementation detail of gsd-check-update.js
|
||||
// (spawned internally via child_process.spawn), never itself registered as a
|
||||
// hook entry point.
|
||||
'gsd-check-update-worker.js',
|
||||
// gsd-ensure-canonical-path.js (#997) is registered ONLY in the
|
||||
// marketplace-plugin manifest (hooks/hooks.json's SessionStart entry), not
|
||||
// via install.js's classic-installer settings.json path — the plugin
|
||||
// install mode never runs bin/install.js at all.
|
||||
'gsd-ensure-canonical-path.js',
|
||||
]);
|
||||
|
||||
// ADR-857 phase 5f-1b: settings-json hook registration moved to runtime-hooks-surface.cts.
|
||||
|
||||
Reference in New Issue
Block a user