feat(#2099): drive GitHub Copilot through the EoS descriptor + multi-event hook bus (ADR-1239)
Fold Copilot's residual runtime-literal branches onto descriptor-driven hostBehaviors. Several issue premises were inaccurate (verified via research) and deliberately NOT followed: writesSharedSettings/"legacy exclusion list" (per-runtime descriptor data, copilot's false is correct); RUNTIME_CONTENT_DISPATCH.copilot + installSurface==='copilot-instructions' (already descriptor-driven); extendedHookEvents (closed Claude/Gemini enum — hooks extended in code instead); reapply ternary (already folded, kimi #2095). Real folds (all byte-parity — golden byte-identical for every runtime): - src/install-engine.cts + src/surface.cts: the two `.agent.md` filename cutovers (_copyStaged + _syncGsdDir) unified onto hostBehaviors.agentFileExtension via a new exported agentFileExtensionFor() accessor (kills the two-mechanism divergence). - src/runtime-artifact-conversion.cts: applyAgentPathRewrites' copilot skip → hostBehaviors.noPathRewrite:true (antigravity #2096 precedent). - bin/install.js uninstall: the two isCopilot cleanup branches → installSurface=== 'copilot-instructions' gate (symmetric with install-time). - bin/install.js: `!isCopilot` in the two skipSharedHooksInstall checks → hostBehaviors.skipSharedHooksInstall:true (copilot has no shared gsd-*.js hooks). - bin/install.js: three dead legacy inline-agent-loop isCopilot refs removed (copilot ∈ _DESCRIPTOR_AGENTS_RUNTIMES → unreachable; byte-parity proven by clean golden + real reachable-runtime install diffs). isCopilot dropped from 4 destructures. Zero live `runtime==='copilot'`/`isCopilot` branches remain in bin/install.js, install-engine.cts, surface.cts, or runtime-artifact-conversion.cts (AC2 guard scans all four). UPGRADE 1 (multi-event hook bus): buildCopilotHookConfig() now emits preToolUse/ postToolUse/userPromptSubmitted/sessionEnd advisory handlers alongside sessionStart (static inline bash/powershell — deterministic, golden-trackable). Only copilot.json's gsd-session.json hash changes. UPGRADE 2 (background dispatch): surfaced via the negotiated contract only — dispatch.background:true exceeds the declarative-cli baseline and survives negotiation with no downgrade warning. NO .agent.md frontmatter field (copilot has none). MCP companion out of scope (AC4 names only 2 upgrades). Tests: declarative-reference-copilot (adapter/axes/fail-closed + AC2 4-file source-grep guard) + copilot-upgrades (live 5-event hook wiring; dispatch.background negotiation). Matrix EoS note + how-to; changeset (Changed). capability-registry regenerated. Incidental flaky-test RE-ARCHITECTURE (no-defer, maintainer-directed): tests/opencode-review-reconstruction.property.test.cjs spawned ~600 synchronous execFileSync('jq') subprocesses (numRuns:200 × 3 fast-check properties, one jq per generated stream); a single jq freezing on a contended macos-22 CI runner hung the whole unit-test chunk to its 600s kill (this PR's CI). --test-force-exit can't interrupt a synchronous execFileSync, so the cure is to stop spawning per case, not just time-bound it. Re-architected to run the SHIPPED jq program over the whole fast-check corpus in ONE jq process: each generated stream is one compact-JSON array per line in a temp file, `jq -c <PROGRAM>` (no -s) applies PROGRAM to each array (`.` == the array, exactly what production's `jq -rs <file>` sees after slurping) and emits one result per line — empirically byte-identical to the per-stream form across embedded-newline/empty/quote/ unicode/null-drop cases, and file-input (like production) so there's no stdin pipe to deadlock on large I/O. ~600 spawns → 6; coverage unchanged (200-case corpus per property, deterministic seeds) plus explicit boundary/diagnostic example batches. Still property- tests the real shipped jq (no JS reimplementation). Per-call jq timeout retained as a belt-and-suspenders bound. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6851,7 +6851,9 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
||||
// consumer, so its former `&& !isKimi` uninstall guards were removed.
|
||||
// #2096: isAntigravity dropped — unused in this function.
|
||||
// #2098: isCodebuddy dropped — unused in this function.
|
||||
const { isOpencode, isCodex, isCopilot, isCursor, isWindsurf, isAugment, isQwen, isHermes, isCline } = runtimeFlags(runtime);
|
||||
// #2099: isCopilot dropped — both Copilot side-effect branches below are now
|
||||
// gated on resolveInstallPlan(runtime).installSurface === 'copilot-instructions'.
|
||||
const { isOpencode, isCodex, isCursor, isWindsurf, isAugment, isQwen, isHermes, isCline } = runtimeFlags(runtime);
|
||||
const dirName = getDirName(runtime);
|
||||
|
||||
// Get the target directory based on runtime and install type. Cline local
|
||||
@@ -6880,7 +6882,13 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
||||
// #786: AGENTS.md lives at the repo root (outside targetDir) for local Copilot
|
||||
// installs, so its cleanup must run even when .github (targetDir) was already
|
||||
// removed — i.e. BEFORE the "target directory missing" early-return below.
|
||||
if (isCopilot && !isGlobal) {
|
||||
// #2099: descriptor-driven via resolveInstallPlan(runtime).installSurface ===
|
||||
// 'copilot-instructions' (was hardcoded `isCopilot`). Mirrors the install-time
|
||||
// gate at the 'copilot-instructions' branch below (~line 10471 equivalent),
|
||||
// which writes this same repo-root AGENTS.md only for local ('!isGlobal')
|
||||
// installs — 'copilot-instructions' is unique to copilot's descriptor, so
|
||||
// this is byte-parity.
|
||||
if (resolveInstallPlan(runtime).installSurface === 'copilot-instructions' && !isGlobal) {
|
||||
const agentsMdPath = path.join(process.cwd(), 'AGENTS.md');
|
||||
if (fs.existsSync(agentsMdPath)) {
|
||||
const content = fs.readFileSync(agentsMdPath, 'utf8');
|
||||
@@ -7064,7 +7072,10 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
||||
}
|
||||
|
||||
// 1b. Non-layout Copilot side-effect: copilot-instructions.md cleanup
|
||||
if (isCopilot) {
|
||||
// #2099: descriptor-driven via resolveInstallPlan(runtime).installSurface ===
|
||||
// 'copilot-instructions' (was hardcoded `isCopilot`), mirroring the same
|
||||
// gate used at the install-time 'copilot-instructions' branch.
|
||||
if (resolveInstallPlan(runtime).installSurface === 'copilot-instructions') {
|
||||
const instructionsPath = path.join(targetDir, 'copilot-instructions.md');
|
||||
if (fs.existsSync(instructionsPath)) {
|
||||
const content = fs.readFileSync(instructionsPath, 'utf8');
|
||||
@@ -8223,7 +8234,9 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
|
||||
// settings-json-adjacent runtime, so the `&& !isKimi` term below was removed.
|
||||
// #2096: isAntigravity dropped — unused in this function.
|
||||
// #2098: isCodebuddy dropped — unused in this function.
|
||||
const { isOpencode, isCodex, isCopilot, isCursor, isWindsurf, isAugment, isQwen, isHermes, isCline } = runtimeFlags(runtime);
|
||||
// #2099: isCopilot dropped — was only used in the hooks-tracking conditional
|
||||
// above, now covered by hostBehaviors.skipSharedHooksInstall.
|
||||
const { isOpencode, isCodex, isCursor, isWindsurf, isAugment, isQwen, isHermes, isCline } = runtimeFlags(runtime);
|
||||
const gsdDir = path.join(configDir, 'gsd-core');
|
||||
// #1367: Claude local now writes flat gsd-*.md files at commands/ (not commands/gsd/).
|
||||
// Claude local uses flatCommandsDir instead for manifest recording.
|
||||
@@ -8335,7 +8348,9 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
|
||||
// skipSharedHooksInstall:true) — the redundant `&& !isTrae` was removed.
|
||||
// #2095: kimi is now a hooks/ consumer (native config.toml [[hooks]] bus) —
|
||||
// the redundant `&& !isKimi` was removed so its hook files are tracked too.
|
||||
if (!isCodex && !isCopilot && _hostBehaviors(runtime).skipSharedHooksInstall !== true && !isWindsurf) {
|
||||
// #2099: Copilot's exclusion is likewise descriptor-driven (copilot declares
|
||||
// skipSharedHooksInstall:true) — the redundant `&& !isCopilot` was removed.
|
||||
if (!isCodex && _hostBehaviors(runtime).skipSharedHooksInstall !== true && !isWindsurf) {
|
||||
const hooksDir = path.join(configDir, 'hooks');
|
||||
if (fs.existsSync(hooksDir)) {
|
||||
// Drive from INSTALLED_HOOK_FILES (the canonical HOOKS_TO_COPY set from
|
||||
@@ -8738,7 +8753,14 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
||||
// _DESCRIPTOR_AGENTS_RUNTIMES below, so its legacy converter-dispatch branch
|
||||
// (the `isCodebuddy` arm calling convertClaudeAgentToCodebuddyAgent) was
|
||||
// unreachable dead code and was removed rather than re-gated.
|
||||
const { isOpencode, isZcode, isCodex, isCopilot, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCline } = runtimeFlags(runtime);
|
||||
// #2099: isCopilot dropped — copilot is also in _DESCRIPTOR_AGENTS_RUNTIMES
|
||||
// below, so its three legacy-agent-loop branches (the path-rewrite skip,
|
||||
// the converter dispatch, and the .agent.md destName ternary) were
|
||||
// unreachable dead code and were removed rather than re-gated; the
|
||||
// .agent.md suffix now lives on hostBehaviors.agentFileExtension in
|
||||
// src/install-engine.cts, and the skipSharedHooksInstall check above no
|
||||
// longer needs `&& !isCopilot`.
|
||||
const { isOpencode, isZcode, isCodex, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCline } = runtimeFlags(runtime);
|
||||
const plan = resolveInstallPlan(runtime);
|
||||
const dirName = getDirName(runtime);
|
||||
const src = path.join(__dirname, '..');
|
||||
@@ -9608,12 +9630,14 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
||||
// _DESCRIPTOR_AGENTS_RUNTIMES above, so this whole branch is already
|
||||
// unreachable for it; the path-rewrite skip for antigravity now lives
|
||||
// in the descriptor-driven `applyAgentPathRewrites` (hostBehaviors.noPathRewrite).
|
||||
if (!isCopilot) {
|
||||
content = content.replace(dirRegex, pathPrefix);
|
||||
content = content.replace(homeDirRegex, pathPrefix);
|
||||
content = content.replace(bareDirRegex, normalizedPathPrefix);
|
||||
content = content.replace(bareHomeDirRegex, normalizedPathPrefix);
|
||||
}
|
||||
// #2099: `if (!isCopilot)` guard dropped — copilot is ALSO in
|
||||
// _DESCRIPTOR_AGENTS_RUNTIMES (line ~9564 above), so this whole
|
||||
// `else if (fs.existsSync(agentsSrc))` branch is unreachable for it;
|
||||
// isCopilot was therefore always false here, making the guard a no-op.
|
||||
content = content.replace(dirRegex, pathPrefix);
|
||||
content = content.replace(homeDirRegex, pathPrefix);
|
||||
content = content.replace(bareDirRegex, normalizedPathPrefix);
|
||||
content = content.replace(bareHomeDirRegex, normalizedPathPrefix);
|
||||
content = processAttribution(content, getCommitAttribution(runtime));
|
||||
// Convert frontmatter for runtime compatibility (agents need different handling)
|
||||
if (_hostBehaviors(runtime).frontmatterDialect === 'opencode') {
|
||||
@@ -9657,8 +9681,11 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
||||
content = convertClaudeToKiloFrontmatter(content, { isAgent: true, modelOverride: _kiloModelOverride });
|
||||
} else if (_hostBehaviors(runtime).frontmatterDialect === 'codex') {
|
||||
content = convertClaudeAgentToCodexAgent(content);
|
||||
} else if (isCopilot) {
|
||||
content = convertClaudeAgentToCopilotAgent(content, isGlobal);
|
||||
// #2099: `else if (isCopilot)` arm dropped — copilot is unreachable
|
||||
// here (see the isCopilot-guard-drop comment above); its content
|
||||
// conversion is applied pre-staging via the descriptor's
|
||||
// artifactLayout.converter (runtime-artifact-layout.cts), independent
|
||||
// of this legacy loop.
|
||||
} else if (isWindsurf) {
|
||||
content = convertClaudeAgentToWindsurfAgent(content);
|
||||
} else if (_hostBehaviors(runtime).frontmatterDialect === 'cline') {
|
||||
@@ -9698,7 +9725,12 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
||||
// shouldNormalizeHyphenNamespaceInAgentBody above. Mirrors the
|
||||
// SKILL.md-body fix shipped via #3629.
|
||||
content = normalizeAgentBodyForRuntime(content, runtime, readGsdCommandNames());
|
||||
const destName = isCopilot ? entry.name.replace('.md', '.agent.md') : entry.name;
|
||||
// #2099: `isCopilot ? ... : entry.name` ternary dropped — copilot is
|
||||
// unreachable here (see the isCopilot-guard-drop comment above), so
|
||||
// the ternary always evaluated to entry.name in practice; its
|
||||
// .agent.md suffix is applied by the descriptor-driven fold in
|
||||
// src/install-engine.cts (hostBehaviors.agentFileExtension).
|
||||
const destName = entry.name;
|
||||
fs.writeFileSync(path.join(agentsDest, destName), content);
|
||||
}
|
||||
}
|
||||
@@ -9886,7 +9918,9 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
||||
// resolveKimiHooksTomlDir) via installSharedHooksBundle, at the
|
||||
// kimi-hooks-toml branch further below — never under the generic
|
||||
// Agent-Skills configDir GSD installs skills/agents into for kimi.
|
||||
if (!isCodex && !isCopilot && _hostBehaviors(runtime).skipSharedHooksInstall !== true && !isWindsurf && !isZcode) {
|
||||
// #2099: Copilot's exclusion is likewise descriptor-driven (copilot declares
|
||||
// skipSharedHooksInstall:true) — the redundant `&& !isCopilot` was removed.
|
||||
if (!isCodex && _hostBehaviors(runtime).skipSharedHooksInstall !== true && !isWindsurf && !isZcode) {
|
||||
if (!installSharedHooksBundle(targetDir)) {
|
||||
failures.push('hooks');
|
||||
}
|
||||
@@ -10869,7 +10903,8 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
||||
// _hostBehaviors(runtime).doneBannerStyle === 'kimi-agent-file' (descriptor-driven), not this flag.
|
||||
// #2096: isAntigravity dropped — unused in this function.
|
||||
// #2098: isCodebuddy dropped — unused in this function.
|
||||
const { isOpencode, isCodex, isCopilot, isCursor, isWindsurf, isAugment, isQwen, isHermes, isCline } = runtimeFlags(runtime);
|
||||
// #2099: isCopilot dropped — unused in this function.
|
||||
const { isOpencode, isCodex, isCursor, isWindsurf, isAugment, isQwen, isHermes, isCline } = runtimeFlags(runtime);
|
||||
const plan = resolveInstallPlan(runtime);
|
||||
|
||||
if (shouldInstallStatusline && plan.writesSharedSettings && !_hostBehaviors(runtime).skipSettingsUi) {
|
||||
|
||||
Reference in New Issue
Block a user