feat(#2092): migrate Qwen Code onto EoS imperative adapter + native subagents + SubagentStart (ADR-1239)
Fold all runtime==='qwen'/isQwen logic branches (skill-priority frontmatter, branding/path rewrites, legacy commands/gsd cleanup, hyphen-namespace normalization, RUNTIME_CONTENT_DISPATCH, hooks-surface label) into descriptor-driven runtime.hostBehaviors on capabilities/qwen/capability.json, read via _hostBehaviors(). Shared claude/qwen/hermes legacy-migration branches in install-engine.cts folded to descriptor flags (claude+hermes descriptors updated; FALLBACK_HOST_BEHAVIORS.claude floored). Byte-identical golden parity for qwen/hermes/claude(global+local). UPGRADE 1: native .qwen/agents/*.md subagent projection — new agents artifact-layout kind + convertClaudeAgentToQwenAgent converter (name + description + tools YAML block list; color/model dropped). qwen routed onto the descriptor-driven agents path (_DESCRIPTOR_AGENTS_RUNTIMES). UPGRADE 2: SubagentStart hook wired into extendedHookEvents + the descriptor-gated hook-writer loop (activates only for qwen). Tests: qwen-imperative-reference (adapter/axes/fail-closed/hostBehaviors + no runtime==='qwen' source-grep across 4 files) + qwen-upgrades (agents file validity + SubagentStart mirrors SubagentStop, descriptor-gated). Docs matrix + how-to updated; changeset added. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -470,7 +470,7 @@ function _runLegacyInstallMigrations(runtime: string, configDir: string, scope:
|
||||
// that for Hermes the flat skills/gsd-*/ removal (below) does not delete the freshly
|
||||
// created skills/gsd-dev-preferences/ skill dir.
|
||||
let savedLegacyArtifacts: Map<string, string> | null = null;
|
||||
if (runtime === 'claude' || runtime === 'qwen' || runtime === 'hermes') {
|
||||
if (_hostBehaviors(runtime).legacyCommandsGsdInstallMigration) {
|
||||
if (fs.existsSync(legacyCommandsGsd)) {
|
||||
savedLegacyArtifacts = preserveUserArtifacts(legacyCommandsGsd, ['dev-preferences.md']);
|
||||
fs.rmSync(legacyCommandsGsd, { recursive: true });
|
||||
@@ -531,7 +531,8 @@ function _runLegacyUninstallCleanup(runtime: string, configDir: string, scope: s
|
||||
// commands/gsd/ for claude local, preserving dev-preferences.md by restoring it
|
||||
// to the same location (#1423). Using migrateLegacyDevPreferencesToSkill here
|
||||
// (which would redirect to skills/) conflicts with the test contract for local installs.
|
||||
const isLegacyCommandsGsd = runtime === 'qwen' || runtime === 'hermes' || (runtime === 'claude' && scope === 'global');
|
||||
const _lu = _hostBehaviors(runtime).legacyCommandsGsdUninstall;
|
||||
const isLegacyCommandsGsd = _lu === true || (_lu === 'global' && scope === 'global');
|
||||
if (isLegacyCommandsGsd) {
|
||||
const legacyCommandsGsd = path.join(configDir, 'commands', 'gsd');
|
||||
if (fs.existsSync(legacyCommandsGsd)) {
|
||||
|
||||
@@ -14,22 +14,45 @@
|
||||
* per-instance CLI flags). An instance is available iff its base `cli` is
|
||||
* detected. The instance→cli mapping lives HERE (single source; see the parity
|
||||
* test in tests/review-reviewer-instances.test.cjs — DEFECT.GENERATIVE-FIX).
|
||||
*
|
||||
* KNOWN_REVIEWER_SLUGS (post-review #2092): registry-derived, not a flat
|
||||
* hand-maintained array. Each capability-runtime descriptor that is a valid
|
||||
* reviewer CLI declares `runtime.hostBehaviors.reviewerCli: true`
|
||||
* (capabilities/<id>/capability.json); this module reads that flag off the
|
||||
* generated capability-registry.cjs at require-time. A handful of reviewer
|
||||
* CLIs are NOT install-time runtimes at all (no capabilities/<id>/ descriptor
|
||||
* exists) — those stay a small hardcoded tail:
|
||||
* - `gemini` — hook-event dialect name only (see runtime-hooks-surface.cts);
|
||||
* the Gemini CLI reviewer is not an installable runtime (#1928 folded
|
||||
* gemini into antigravity's descriptor).
|
||||
* - `coderabbit` / `ollama` / `lm_studio` / `llama_cpp` — third-party
|
||||
* review/model CLIs with no GSD install surface at all.
|
||||
*/
|
||||
|
||||
export const KNOWN_REVIEWER_SLUGS: ReadonlyArray<string> = [
|
||||
const NON_RUNTIME_REVIEWER_SLUGS: ReadonlyArray<string> = [
|
||||
'gemini',
|
||||
'claude',
|
||||
'codex',
|
||||
'coderabbit',
|
||||
'opencode',
|
||||
'qwen',
|
||||
'cursor',
|
||||
'antigravity',
|
||||
'ollama',
|
||||
'lm_studio',
|
||||
'llama_cpp',
|
||||
];
|
||||
|
||||
function deriveRuntimeReviewerSlugs(): string[] {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const registry = require('./capability-registry.cjs') as {
|
||||
runtimes?: Record<string, { runtime?: { hostBehaviors?: { reviewerCli?: boolean } } }>;
|
||||
};
|
||||
const runtimes = registry.runtimes || {};
|
||||
return Object.keys(runtimes).filter(
|
||||
(id) => runtimes[id]?.runtime?.hostBehaviors?.reviewerCli === true,
|
||||
);
|
||||
}
|
||||
|
||||
export const KNOWN_REVIEWER_SLUGS: ReadonlyArray<string> = [
|
||||
...deriveRuntimeReviewerSlugs(),
|
||||
...NON_RUNTIME_REVIEWER_SLUGS,
|
||||
];
|
||||
|
||||
/** Instance names are lowercase slugs that must not shadow a built-in slug. */
|
||||
export const INSTANCE_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
||||
|
||||
|
||||
@@ -62,6 +62,27 @@ function gsdVersion(): string {
|
||||
return cachedVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Host-specific install behaviors declared on the runtime descriptor
|
||||
* (capabilities/<runtime>/capability.json -> runtime.hostBehaviors). Mirrors
|
||||
* bin/install.js's / install-engine.cts's `_hostBehaviors` (ADR-1239 / #2086
|
||||
* / #2092). Returns {} for runtimes that declare none, so every behavior
|
||||
* branch degrades to the generic path by default. Unlike the bin/install.js
|
||||
* and install-engine.cts variants, this module already imports
|
||||
* `capabilityRegistry` statically (see NON_CLAUDE_RUNTIMES below), so this
|
||||
* reads it directly rather than re-require()-ing inside a try/catch.
|
||||
*/
|
||||
function _hostBehaviors(runtime: string): Record<string, unknown> {
|
||||
return (
|
||||
(capabilityRegistry &&
|
||||
capabilityRegistry.runtimes &&
|
||||
capabilityRegistry.runtimes[runtime] &&
|
||||
capabilityRegistry.runtimes[runtime].runtime &&
|
||||
capabilityRegistry.runtimes[runtime].runtime.hostBehaviors) ||
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const colorNameToHex = {
|
||||
cyan: '#00FFFF',
|
||||
@@ -434,10 +455,12 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
|
||||
const version = gsdVersion();
|
||||
if (version) fm += `version: ${yamlQuote(version)}\n`;
|
||||
}
|
||||
// #778 (b) — Qwen-only numeric priority for /skills ordering. Scoped to qwen
|
||||
// so Claude/Hermes skill frontmatter is unchanged (they ignore the field, but
|
||||
// we keep their output byte-stable). skillName is the `gsd-<stem>` dir name.
|
||||
if (runtime === 'qwen') {
|
||||
// #778 (b) — numeric priority for /skills ordering, declared on the runtime
|
||||
// descriptor (runtime.hostBehaviors.skillPriorityFrontmatter). Scoped to
|
||||
// runtimes that declare the flag so Claude/Hermes skill frontmatter is
|
||||
// unchanged (they ignore the field, but we keep their output byte-stable).
|
||||
// skillName is the `gsd-<stem>` dir name. (ADR-1239 / #2092)
|
||||
if (_hostBehaviors(runtime).skillPriorityFrontmatter) {
|
||||
const stem = typeof skillName === 'string' && skillName.startsWith('gsd-')
|
||||
? skillName.slice(4)
|
||||
: skillName;
|
||||
@@ -2053,6 +2076,77 @@ function convertClaudeAgentToTraeAgent(content) {
|
||||
return `${cleanFrontmatter}\n${body}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Claude agent (.md) to a native Qwen Code subagent file
|
||||
* (`.qwen/agents/gsd-*.md` / `<qwenhome>/agents/gsd-*.md`, ADR-1239 / #2092
|
||||
* Phase B Upgrade 1). Qwen Code is a Claude-dialect host: its docs' "Claude
|
||||
* Code Compatibility Fields" section confirms CC agent files parse under
|
||||
* `.qwen/agents/` (https://qwenlm.github.io/qwen-code-docs/en/users/features/sub-agents),
|
||||
* so — unlike Cursor/Trae/Copilot/Antigravity — tool names pass through
|
||||
* UNCHANGED (no remapping table).
|
||||
*
|
||||
* Emits DETERMINISTIC frontmatter: `name:` + `description:` (mirrors
|
||||
* convertClaudeAgentToCursorAgent), plus `tools:` as a YAML block list when the
|
||||
* source declares one. Qwen's documented `tools:` schema is a YAML array
|
||||
* (`tools:\n- tool1\n- tool2`), not Claude's single-line comma-separated string
|
||||
* — passing the raw single-line string through unchanged would parse as one
|
||||
* malformed tool name and be silently dropped ("Optional fields with invalid
|
||||
* values are silently dropped at parse time" — same docs page). Reuses
|
||||
* `parseFrontmatterTools` (already relied on by the Kimi agent path), which
|
||||
* tolerates BOTH source formats Claude's own agents/*.md files use — the
|
||||
* single-line comma list (most agents) and the YAML block list (e.g.
|
||||
* agents/gsd-nyquist-auditor.md, agents/gsd-security-auditor.md) — so no tools
|
||||
* are lost regardless of which the source agent uses.
|
||||
*
|
||||
* `color` IS preserved: Qwen's docs list `color` under "Claude Code
|
||||
* Compatibility Fields" as a supported optional field, so it is passed
|
||||
* through as a plain scalar (unlike the cursor/trae/augment/windsurf
|
||||
* reduced-frontmatter converters, which drop it — those hosts have no such
|
||||
* compatibility field). `model:` and `approvalMode:` are intentionally NOT
|
||||
* emitted: both are optional per the docs and out of scope for #2092 (model:
|
||||
* would couple to the model catalog and introduce nondeterminism;
|
||||
* approvalMode is a deliberate follow-on).
|
||||
*
|
||||
* Body: preserved verbatim after the qwen branding rewrite (CLAUDE.md /
|
||||
* Claude Code / .claude/ literal-substring values — descriptor-driven via
|
||||
* runtime.hostBehaviors.brandingRewrites, mirrors the qwen case in
|
||||
* _applyRuntimeRewrites). The anchored `~/.claude/` / `$HOME/.claude/` forms
|
||||
* are already rewritten upstream by applyAgentPathRewrites (agentCtx Step 1 in
|
||||
* stageAgentsForRuntimeWithConverter) before this converter runs, so only the
|
||||
* bare/non-anchored forms are handled here — mirrors how
|
||||
* convertClaudeToTraeMarkdown orders its bare-form rewrites after the slash
|
||||
* forms to avoid double-rewriting the same substring.
|
||||
*/
|
||||
function convertClaudeAgentToQwenAgent(content) {
|
||||
const _b = _hostBehaviors('qwen').brandingRewrites || {};
|
||||
let converted = content;
|
||||
if (_b['CLAUDE.md']) converted = converted.replace(/CLAUDE\.md/g, _b['CLAUDE.md']);
|
||||
if (_b['Claude Code']) converted = converted.replace(/\bClaude Code\b/g, _b['Claude Code']);
|
||||
if (_b['.claude/']) converted = converted.replace(/\.claude\//g, _b['.claude/']);
|
||||
|
||||
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
||||
if (!frontmatter) return converted;
|
||||
|
||||
const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
|
||||
const description = extractFrontmatterField(frontmatter, 'description') || '';
|
||||
const tools = parseFrontmatterTools(frontmatter);
|
||||
const color = extractFrontmatterField(frontmatter, 'color');
|
||||
|
||||
let fm = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n`;
|
||||
if (tools.length > 0) {
|
||||
fm += 'tools:\n';
|
||||
for (const tool of tools) {
|
||||
fm += ` - ${yamlIdentifier(tool)}\n`;
|
||||
}
|
||||
}
|
||||
if (color) {
|
||||
fm += `color: ${yamlIdentifier(color)}\n`;
|
||||
}
|
||||
fm += '---';
|
||||
|
||||
return `${fm}\n${body}`;
|
||||
}
|
||||
|
||||
function convertClaudeAgentToCodebuddyAgent(content) {
|
||||
const converted = convertClaudeToCodebuddyMarkdown(content);
|
||||
|
||||
@@ -2357,9 +2451,20 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
|
||||
content = processAttribution(content, attribution);
|
||||
break;
|
||||
|
||||
case 'qwen':
|
||||
content = content.replace(/CLAUDE\.md/g, 'QWEN.md');
|
||||
content = content.replace(/\bClaude Code\b/g, 'Qwen Code');
|
||||
// Descriptor-driven brand literals (ADR-1239 / #2092): the qwen/hermes
|
||||
// brand VALUES (CLAUDE.md/Claude Code/.claude/ replacements) now read from
|
||||
// runtime.hostBehaviors.brandingRewrites instead of hardcoded literals.
|
||||
// EXACT regexes/order preserved — only the replacement values changed.
|
||||
case 'qwen': {
|
||||
// Guarded (post-review #2092): brandingRewrites is undefined if the
|
||||
// capability registry fails to load — degrade closed (skip the
|
||||
// brand-literal replacements, still apply the non-branding path
|
||||
// rewrites below) instead of throwing on `_b['CLAUDE.md']`.
|
||||
const _b = _hostBehaviors(runtime).brandingRewrites;
|
||||
if (_b) {
|
||||
content = content.replace(/CLAUDE\.md/g, _b['CLAUDE.md']);
|
||||
content = content.replace(/\bClaude Code\b/g, _b['Claude Code']);
|
||||
}
|
||||
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
||||
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
||||
content = content.replace(/~\/\.qwen\//g, pathPrefix);
|
||||
@@ -2368,15 +2473,23 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
|
||||
content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
||||
content = content.replace(/~\/\.qwen(?![\w-])/g, normalizedPathPrefix);
|
||||
content = content.replace(/\$HOME\/\.qwen(?![\w-])/g, normalizedPathPrefix);
|
||||
content = content.replace(/\.claude\//g, '.qwen/');
|
||||
if (_b) {
|
||||
content = content.replace(/\.claude\//g, _b['.claude/']);
|
||||
}
|
||||
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
||||
content = content.replace(/\.\/\.qwen\//g, `./${dirName}/`);
|
||||
content = processAttribution(content, attribution);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'hermes':
|
||||
content = content.replace(/CLAUDE\.md/g, 'HERMES.md');
|
||||
content = content.replace(/\bClaude Code\b/g, 'Hermes Agent');
|
||||
case 'hermes': {
|
||||
// Guarded (post-review #2092): see qwen case above — same degrade-closed
|
||||
// rationale.
|
||||
const _b = _hostBehaviors(runtime).brandingRewrites;
|
||||
if (_b) {
|
||||
content = content.replace(/CLAUDE\.md/g, _b['CLAUDE.md']);
|
||||
content = content.replace(/\bClaude Code\b/g, _b['Claude Code']);
|
||||
}
|
||||
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
||||
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
||||
content = content.replace(/~\/\.hermes\//g, pathPrefix);
|
||||
@@ -2385,11 +2498,14 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
|
||||
content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
||||
content = content.replace(/~\/\.hermes(?![\w-])/g, normalizedPathPrefix);
|
||||
content = content.replace(/\$HOME\/\.hermes(?![\w-])/g, normalizedPathPrefix);
|
||||
content = content.replace(/\.claude\//g, '.hermes/');
|
||||
if (_b) {
|
||||
content = content.replace(/\.claude\//g, _b['.claude/']);
|
||||
}
|
||||
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
||||
content = content.replace(/\.\/\.hermes\//g, `./${dirName}/`);
|
||||
content = processAttribution(content, attribution);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'kimi':
|
||||
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
||||
@@ -2553,26 +2669,21 @@ function rewriteStagedCommandBodies(stagedDir, opts) {
|
||||
return applyRuntimeContentRewritesForCommandsInPlace(stagedDir, runtime, pathPrefix, isGlobal, attribution);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtimes that use the hyphen-namespace form `/gsd-<cmd>` in agent bodies.
|
||||
* claude/qwen/hermes use hyphen-name:`...` frontmatter; cursor/windsurf/etc
|
||||
* self-convert. Mirrors the `HYPHEN_NAME_AGENT_RUNTIMES` set in bin/install.js.
|
||||
*
|
||||
* @private — export normalizeAgentBodyForRuntime for callers.
|
||||
*/
|
||||
const HYPHEN_NAME_AGENT_RUNTIMES: ReadonlySet<string> = new Set(['claude', 'qwen', 'hermes']);
|
||||
|
||||
/**
|
||||
* Normalize `/gsd:<cmd>` colon refs in the agent body to `/gsd-<cmd>` for
|
||||
* hyphen-`name:` runtimes (claude / qwen / hermes). No-op for all other
|
||||
* runtimes. Mirrors the per-file call in bin/install.js line 9400.
|
||||
* runtimes that declare `runtime.hostBehaviors.hyphenNameAgentBody` on their
|
||||
* descriptor (claude / qwen / hermes use hyphen-`name:` frontmatter;
|
||||
* cursor/windsurf/etc self-convert and don't declare the flag). Descriptor-
|
||||
* driven (ADR-1239 / #2092) — folded from the hardcoded
|
||||
* `HYPHEN_NAME_AGENT_RUNTIMES` allow-list set. Mirrors the per-file call in
|
||||
* bin/install.js line 9370 / `shouldNormalizeHyphenNamespaceInAgentBody`.
|
||||
*
|
||||
* @param content raw agent file content (post-converter)
|
||||
* @param runtime canonical runtime ID
|
||||
* @param cmdNames gsd command names from readGsdCommandNames()
|
||||
*/
|
||||
function normalizeAgentBodyForRuntime(content: string, runtime: string, cmdNames: string[]): string {
|
||||
if (!HYPHEN_NAME_AGENT_RUNTIMES.has(runtime)) return content;
|
||||
if (_hostBehaviors(runtime).hyphenNameAgentBody !== true) return content;
|
||||
return transformContentToHyphen(content, cmdNames);
|
||||
}
|
||||
|
||||
@@ -2698,6 +2809,11 @@ export = {
|
||||
convertClaudeAgentToCodebuddyAgent,
|
||||
convertClaudeAgentToClineAgent,
|
||||
convertClaudeAgentToCodexAgent,
|
||||
// ADR-1239 / #2092 Phase B Upgrade 1: native .qwen/agents/*.md subagent
|
||||
// projection — registered by name so convertedAgentsKind's
|
||||
// conversionExports[converterName] dispatch (runtime-artifact-layout.cts)
|
||||
// can resolve it from capabilities/qwen/capability.json's agents kind.
|
||||
convertClaudeAgentToQwenAgent,
|
||||
// #1511 ADR-1508 Phase 2: rewrite engine deep seam
|
||||
// Low-level walkers (pathPrefix + attribution pre-resolved by caller):
|
||||
applyRuntimeContentRewritesInPlace,
|
||||
|
||||
@@ -193,19 +193,27 @@ function agentsKind(destSubpath: string, prefix: string, configDir: string): Art
|
||||
* Agent filenames are preserved verbatim (the prefix is already embedded in the
|
||||
* agent stem — e.g. `gsd-planner.md`).
|
||||
*
|
||||
* #1173 SCOPE — plumbing only (declarations deferred): this provides the
|
||||
* converter dispatch + `isGlobal` scope threading for the descriptor's `agents`
|
||||
* kind, but NO runtime currently declares a converted `agents` kind in its
|
||||
* `capability.json`. The descriptor declarations for the 8 non-Claude runtimes
|
||||
* (copilot/antigravity/cursor/windsurf/augment/trae/codebuddy/cline) are
|
||||
* DEFERRED to a follow-up that first ships the ADR-1235 §0 byte-for-byte parity
|
||||
* harness, because the second `layout.kinds` consumer — `applySurface` /
|
||||
* `/gsd:surface` / `--materialize` (`src/surface.cts`) — does not yet mirror the
|
||||
* legacy agent pipeline (Copilot's `.agent.md` filename rename, the cross-cutting
|
||||
* path-prefix rewrite + attribution, stale-file cleanup, config-reading steps),
|
||||
* so declaring the kind now would regress the surface path. Until then the legacy
|
||||
* `bin/install.js` agent loop remains authoritative for the real install, and
|
||||
* this `convertedAgentsKind` is exercised only by synthetic-descriptor seam tests.
|
||||
* #1173 SCOPE — plumbing only (real install still elsewhere): this provides
|
||||
* the converter dispatch + `isGlobal` scope threading for the descriptor's
|
||||
* `agents` kind. As of #2092, 8 non-Claude runtimes DO declare a converted
|
||||
* `agents` kind in their `capability.json` — qwen (`convertClaudeAgentToQwenAgent`)
|
||||
* plus the 7 that already declared one before it (antigravity, augment,
|
||||
* codebuddy, copilot, cursor, trae, windsurf) — so the descriptor-level
|
||||
* declaration is no longer deferred. What IS still deferred is wiring
|
||||
* `resolveRuntimeArtifactLayout`'s `agents` kind into the REAL install:
|
||||
* `bin/install.js`'s agent-staging loop does not consume this module's
|
||||
* `convertedAgentsKind` resolution at all — it dispatches the very same
|
||||
* converter functions directly via `_hostBehaviors(runtime)` checks
|
||||
* (`frontmatterDialect`, `brandingRewrites`, `isCopilot`/`isAntigravity`/…),
|
||||
* duplicating the mapping declared here. That duplication is deliberate until
|
||||
* the second `layout.kinds` consumer — `applySurface` / `/gsd:surface` /
|
||||
* `--materialize` (`src/surface.cts`) — mirrors the legacy agent pipeline
|
||||
* (Copilot's `.agent.md` filename rename, the cross-cutting path-prefix
|
||||
* rewrite + attribution, stale-file cleanup, config-reading steps); declaring
|
||||
* `bin/install.js` itself against this resolver before then would risk
|
||||
* regressing the surface path. Until that follow-up lands, `bin/install.js`
|
||||
* remains authoritative for the real install, and this `convertedAgentsKind`
|
||||
* is exercised only by `/gsd:surface` and synthetic-descriptor seam tests.
|
||||
*
|
||||
* Mirrors the `convertedCommandsKind` pattern (#785).
|
||||
*
|
||||
|
||||
@@ -107,6 +107,31 @@ const CURSOR_MANAGED_EVENTS = CURSOR_HOOK_EVENTS;
|
||||
const GSD_AGENTS_MD_MARKER = '<!-- GSD Configuration — managed by gsd-core installer -->';
|
||||
const GSD_AGENTS_MD_CLOSE_MARKER = '<!-- End GSD Configuration -->';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Descriptor-driven runtime title lookup (ADR-1239 / #2092)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Console-log label for a runtime, sourced from the capability registry's
|
||||
* `title` field (capabilities/<runtime>/capability.json). Folded from a
|
||||
* hardcoded `runtime === 'qwen' ? 'Qwen Code' : runtime === 'claude' ?
|
||||
* 'Claude Code' : runtime` ternary — cosmetic (log text) only, but resolves
|
||||
* to the same 'Qwen Code' / 'Claude Code' values for those two runtimes.
|
||||
* Falls back to the raw runtime id if the registry can't be loaded or the
|
||||
* runtime has no title.
|
||||
*/
|
||||
function _capabilityTitle(runtime: string): string {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const reg = require('./capability-registry.cjs') as {
|
||||
runtimes?: Record<string, { title?: string } | undefined>;
|
||||
};
|
||||
return reg?.runtimes?.[runtime]?.title || runtime;
|
||||
} catch {
|
||||
return runtime;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// atomicWriteFileSync — shared canonical implementation.
|
||||
//
|
||||
@@ -1548,12 +1573,18 @@ function applySettingsJsonHooks(settings: any, opts: ApplySettingsJsonHooksOpts)
|
||||
console.warn(` ${yellow}⚠${reset} Skipped phase boundary hook — Bash executable path unavailable (#3393)`);
|
||||
}
|
||||
|
||||
// ── Extended hook events: SubagentStop / Stop / PreCompact (#788 + #770) ──
|
||||
// Claude Code (since #770) and Qwen Code (since #788) both support these
|
||||
// three lifecycle events. Wire gsd-context-monitor so agents get context-
|
||||
// headroom warnings at subagent completion, model stop, and pre-compaction
|
||||
// (the most critical moment to surface headroom info).
|
||||
// ── Extended hook events: SubagentStop / Stop / PreCompact / SubagentStart
|
||||
// (#788 + #770 + #2092) ────────────────────────────────────────────────
|
||||
// Claude Code (since #770) and Qwen Code (since #788) both support the
|
||||
// SubagentStop / Stop / PreCompact lifecycle events. Qwen Code additionally
|
||||
// supports SubagentStart (#2092 Phase B, Upgrade 2). Wire gsd-context-
|
||||
// monitor so agents get context-headroom warnings at subagent start,
|
||||
// subagent completion, model stop, and pre-compaction (the most critical
|
||||
// moment to surface headroom info).
|
||||
//
|
||||
// SubagentStart — subagent lifecycle start (context headroom tracking;
|
||||
// qwen-only today — no other runtime declares it in
|
||||
// extendedHookEvents)
|
||||
// SubagentStop — subagent lifecycle completion (context headroom tracking)
|
||||
// Stop — model stop / final-response moment (context headroom)
|
||||
// PreCompact — fires before conversation compaction (most critical
|
||||
@@ -1563,11 +1594,15 @@ function applySettingsJsonHooks(settings: any, opts: ApplySettingsJsonHooksOpts)
|
||||
// user prompt text, not a tool invocation, so gsd-prompt-guard (which
|
||||
// exits unless tool_name is Write/Edit) would be a silent no-op. A
|
||||
// dedicated handler for UserPromptSubmit is deferred to a follow-on issue.
|
||||
// SubagentStop, Stop, PreCompact — route through the context monitor.
|
||||
// Guard is now descriptor-driven: only events present in extendedEvents are wired.
|
||||
// SubagentStart, SubagentStop, Stop, PreCompact — route through the context monitor.
|
||||
// Guard is descriptor-driven: only events present in extendedEvents are wired,
|
||||
// so this loop is a no-op for every runtime that doesn't list SubagentStart.
|
||||
{
|
||||
const runtimeLabel = runtime === 'qwen' ? 'Qwen Code' : runtime === 'claude' ? 'Claude Code' : runtime;
|
||||
for (const event of ['SubagentStop', 'Stop', 'PreCompact']) {
|
||||
// Descriptor-driven (ADR-1239 / #2092): folded from a hardcoded
|
||||
// `runtime === 'qwen' ? ... : ...` ternary into a capability-title
|
||||
// lookup (see _capabilityTitle above).
|
||||
const runtimeLabel = _capabilityTitle(runtime);
|
||||
for (const event of ['SubagentStop', 'Stop', 'PreCompact', 'SubagentStart']) {
|
||||
if (!extendedEvents.includes(event)) continue;
|
||||
if (!settings.hooks[event]) {
|
||||
settings.hooks[event] = [];
|
||||
@@ -1591,7 +1626,7 @@ function applySettingsJsonHooks(settings: any, opts: ApplySettingsJsonHooksOpts)
|
||||
}
|
||||
}
|
||||
}
|
||||
// ── end SubagentStop / Stop / PreCompact events ────────────────────────────
|
||||
// ── end SubagentStop / Stop / PreCompact / SubagentStart events ────────────
|
||||
|
||||
// ── Extended hook events (#776; Gemini runtime removed #1928) ──────────────
|
||||
// The Gemini-3-backend dialect exposes several hook events beyond
|
||||
|
||||
Reference in New Issue
Block a user