Commit Graph

473 Commits

Author SHA1 Message Date
Tom Boucher
c4f17a8d6e fix(#2152): accept a homedir option in readGsd(Effective|Global)ModelOverrides
readGsdEffectiveModelOverrides resolved ~/.gsd/defaults.json via os.homedir()
with no seam, so a test asserting project-only overrides could not isolate the
global file. Add an optional { homedir } option (defaults to os.homedir()) to
readGsdGlobalModelOverrides and readGsdEffectiveModelOverrides — the same
dependency-injection shape the sibling warnIfStaleBake already uses. Backward-
compatible: existing callers pass no option and behave identically.

Closes #2152
2026-07-12 16:36:36 -04:00
Tom Boucher
79d7657eff feat(#2102): make pi a first-class installable runtime + fix its dispatch (ADR-1239)
Net-new EoS/pi installable runtime — purely additive (no prior runtime==='pi'
branches). pi is a bun-runtime programmatic-CLI whose /gsd command is registered
by a native ExtensionAPI extension and dispatches through the embedded engine.

Stage 1 (install plumbing):
- capabilities/pi/capability.json: full hostIntegration descriptor (imperative /
  slash-programmatic / active-model / native-extension / bun) + hostBehaviors
  {nativePlugin, pluginOnlyInstall}.
- --pi flag + interactive-menu renumber (All 17->18); pi added to RUNTIME_FLAG_IDS,
  RUNTIME_LABELS, RUNTIME_META, allRuntimes/runtimeMap, model-catalog defaults.
- Install mirrors OpenCode: pi installs the gsd.cjs extension + the shared engine
  payload (gsd-core + scripts + config markers) + the shared hooks bundle (spawned
  by the extension at lifecycle events, like OpenCode's plugin). pluginOnlyInstall
  EXCLUDES declarative command/agent/skill markdown, which pi has no host-read
  surface for (its /gsd is programmatic). _installNativePluginIfDeclared (extracted
  from the opencode-family path) copies pi/gsd.cjs -> ~/.pi/agent/extensions/gsd.cjs
  (global) / .pi/extensions/ (local). pi added to package.json files.
- Golden: new pi.json (320 files: extension + engine + 27-file hooks bundle, no
  markdown); the 16 other fixtures + claude-local change only by the shared
  model-catalog hash line.

Stage 2 (real dispatch + upgrades):
- Shared dispatchGsdCommand() (shell-command-projection): bounded, no-throw
  subprocess-shim to gsd-tools.cjs (the only full-surface dispatch path; no
  in-process full-hub factory exists). Fixes pi/gsd.cjs's createHub()-no-args bug
  (every dispatch was UnknownCommand) AND the identical bug in mcp-server.cts's
  gsd_invoke_command, which a vacuous unknown-family-only test had masked (now has
  a real dispatch regression test).
- pi/gsd.cjs: /gsd handler now (args, ctx) - tokenizes (quote-aware, via the
  shipped hooks/lib/git-cmd.js) + dispatches real family/subcommand (not hardcoded
  query/help); gsd_invoke gets a TypeBox (JSON-schema-fallback) parameters schema +
  consumes params; getArgumentCompletions; before_provider_request active-model
  steering (fail-open on null resolution); functional session_start /
  before_agent_start / session_before_compact hook bridges (spawn the shipped GSD
  hook scripts).
- EXTENSION_EVENT_SURFACES.pi expanded from ['tool_call'] to the full 30-event
  vocabulary.

Docs (host-integration matrix + how-to) + changeset (Added).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:07:38 -04:00
Tom Boucher
a2c7b879d0 feat(#2101): dogfood ZCode through the EoS declarative adapter + fold shared-hooks exclusion (ADR-1239)
The issue's "0 conditional branches" premise missed a live one: the `!isZcode`
shared-hooks exclusion (bin/install.js). Fold it onto descriptor-driven
hostBehaviors.skipSharedHooksInstall:true (zcode's golden has zero hook files —
byte-parity verified) and drop the now-unused isZcode destructure. Zero live
runtime==='zcode'/isZcode branches remain (AC2 source-grep guard over
bin/install.js + install-engine.cts + surface.cts + runtime-artifact-conversion.cts).
The 6 CLI-bookkeeping zcode mentions (--zcode flag, menu, roster, help) stay.

Reference test (declarative-reference-zcode.test.cjs): profileOf → declarative-cli;
createDeclarativeAdapter({runtime:'zcode'}).kind → declarative; a real install emits
the invocable nested-skills/commands/agents surface (no hooks); negotiateHostCapabilities
fail-closes (empty/corrupt descriptor; the nested/maxDepth undocumented sub-axes degrade
to most-restrictive); validateCapability clean.

UPGRADES documented as BLOCKED (verified doc gaps — NOT guessed, to avoid a
non-functional false-green): both of ZCode's documented capabilities lack a published
on-disk config format.
- Hook automation: zcode.z.ai/en/docs/plugin documents the Hook component only as
  "automation hooks triggered on specific events" (capability detected from directory
  layout) — no config file format/location/event schema. Cannot faithfully wire.
- MCP registration: zcode.z.ai/en/docs/mcp-services says servers are "stored in the
  .zcode configuration file" (UI-only) with no documented on-disk filename/path/schema —
  exactly the settings-filename gap the issue AC anticipated.
Both are documented (with the search trail) in the capability matrix + how-to, per AC4's
block-documentation clause; hookBus/transport stay declared for when ZCode publishes the
formats. No hook scripts or MCP artifacts added → no golden change, no other-runtime impact.

Golden: byte-identical for all 16 runtimes (the fold is byte-parity; no upgrade artifacts).
Matrix ## zcode EoS note + how-to; changeset (Changed). capability-registry regenerated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 17:14:48 -04:00
Tom Boucher
bd613566cb feat(#2100): drive Windsurf through the EoS descriptor + wire Cascade's blocking hook bus (ADR-1239)
Fold all 10 residual isWindsurf branches in bin/install.js onto descriptor-driven
hostBehaviors (byte-parity — no fold changes any install output):
- 2 dead destructures dropped (uninstall, finishInstall); the dead
  `else if (isWindsurf)` legacy agent-loop arm removed (windsurf ∈
  _DESCRIPTOR_AGENTS_RUNTIMES → unreachable).
- skipSharedHooksInstall:true folds the two `!isWindsurf` shared-hooks exclusions.
- legacyDevinSkillsCleanup:true folds the `.devin`→`.windsurf` one-time cleanup gate.
- installsCommandBodiesForWorkflowDelegation:true folds the #1629 command-body copy
  (workflow-delegation target — load-bearing; local-install verified intact).
- verificationStyle:"windsurf-workflows" folds the workflow-count report.
- Corrected stale _LEGACY_SCAN_SUBDIR_NAMES + hooks-json manifest comments (cursor + windsurf).
Zero live runtime==='windsurf'/isWindsurf branches remain across bin/install.js,
install-engine.cts, surface.cts, runtime-artifact-conversion.cts (AC2 guard scans all four).

UPGRADE (Cascade hook bus): wire GSD's write/command safety guards into Windsurf's
native hook bus. New hooksSurface 'windsurf-hooks-json' (VALID_HOOKS_SURFACES 7→8, GATE A
profile-marker-only allowlist, the HooksSurface union) + writeWindsurfHooksJson
(Cursor-templated, Cascade's flat {hooks:{<event>:[{command}]}} shape) writing
.windsurf/hooks.json with two BLOCKING pre-hooks:
- pre_write_code → gsd-windsurf-pre-write.js: blocks writes to a file outside the
  active git worktree / into .git internals.
- pre_run_command → gsd-windsurf-pre-command.js: conservative destructive-command
  deny-list (rm -rf of root/home incl. sudo/env/path-prefixed forms; fork bombs;
  force-push refspec forms — HEAD:main, +main, --force/-f — to main/master/next).
Both use Cascade's protocol (stdin JSON, exit 2 + stderr to block, exit 0 to allow,
fail-open on error/timeout). Tokenize-based classifier (no catastrophic-backtracking regex;
4096-char cap) with the fail-closed false-positives fixed post-review.

The 4 advisory GSD guards + pre_mcp_tool_use + 5 post_* logging events are deliberately
NOT wired: Cascade has no context-injection channel for advisory hooks and GSD has no MCP
guard — porting them would be non-functional padding (documented; codebuddy #2098 / copilot
#2099 faithful-subset precedent). extendedHookEvents stays [].

Golden: the 2 guard scripts ship in the shared hook bundle (HOOKS_TO_COPY + the shared
managed-hooks-registry), exactly like cursor's 6 gsd-cursor-*.js scripts — so the 8
shared-bundle runtimes' fixtures gain the 2 inert windsurf scripts + the registry hash
(functionally inert for non-windsurf; the established cursor pattern). No install-output
change beyond that (the folds are byte-parity; skip-bundle runtimes untouched). New scripts
registered in managed-hooks-registry + build-hooks + INVENTORY. Tests: declarative-reference-
windsurf (adapter/axes/fail-closed + AC2 guard) + windsurf-hooks-bridge (live exit-2 blocking
+ allow/fail-open + ReDoS-bound + writer/reconcile/remove idempotency); VALID_HOOKS_SURFACES
pin updated to 8. Matrix hookBus delta + changeset (Changed). capability-registry regenerated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 16:04:24 -04:00
Tom Boucher
d1e9491fef 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>
2026-07-11 13:10:31 -04:00
Tom Boucher
9e1fad1503 feat(#2098): drive CodeBuddy through the EoS descriptor + wire subagent hooks (ADR-1239)
Fold CodeBuddy's residual runtime-literal branches onto descriptor-driven
hostBehaviors (the issue's "zero branches remain" premise was inaccurate — 2
lived):
- Folded the `if (isCodebuddy)` commands/ report duplicate (byte-identical to
  the generic reportCommandsDir block above it) onto
  hostBehaviors.reportCommandsDir:true, matching cursor's #2089 precedent.
- Deleted the dead `else if (isCodebuddy)` agent-conversion arm (codebuddy ∈
  _DESCRIPTOR_AGENTS_RUNTIMES → gated out before the legacy chain, same dead-arm
  pattern removed for augment/trae in #2097).
- Removed isCodebuddy from all 4 destructure sites (now comments only, mirroring
  the #2096 isAntigravity fold). Zero live isCodebuddy reads remain.

UPGRADE 1 (extended hook bus): populate extendedHookEvents with the full
extended set SubagentStop/Stop/PreCompact/SubagentStart (qwen #2092 / kimi
precedent — codebuddy previously had extendedHookEvents:[] so it got NONE of
these). The generic applySettingsJsonHooks loop wires them, so CodeBuddy's
settings.json now gains all four subagent-lifecycle + stop/compact hooks it
previously lacked, matching Qwen/Kimi coverage. No source change (the
HOOK_EVENT_SURFACES SDK catalog is a locked dict; all 4 events are already in
the validator enum; hookEvents already 'claude'). settings.json is golden-
excluded → no golden change.

UPGRADE 2 (background dispatch): surfaced via the negotiated capability contract
only. CodeBuddy's dispatch.background:true legitimately exceeds the declarative-
cli baseline (false) and survives negotiation with no warning. NO agent-file
frontmatter field is emitted: the CodeBuddy CLI (GSD's install target,
~/.codebuddy/agents/) has NO background-dispatch frontmatter field — background
is a caller-side run_in_background invocation param (verified against
codebuddy.ai/docs/cli/sub-agents); the issue's agentMode/enabledAutoRun are
IDE-only (codebuddy.cn, a different product). Emitting them would be a non-
functional false-green, so it is deliberately not done.

Golden: byte-identical for all 16 runtimes (folds are console/failures-report
only; UPGRADE 1 → golden-excluded settings.json; UPGRADE 2 → no artifact
change). Tests: declarative-reference-codebuddy (adapter/axes/fail-closed
negotiation + AC2 source-grep guard) + codebuddy-upgrades (live install asserts
all 4 extended hooks wired; dispatch.background survives negotiation above
baseline). Matrix + install-on-your-runtime updated; changeset (Changed).
capability-registry regenerated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 09:39:34 -04:00
Tom Boucher
17aa35fba7 feat(#2097): migrate Augment onto EoS declarative adapter + settings.json MCP companion (ADR-1239)
Fold augment's runtime-literal conversion branches onto descriptor-driven
hostBehaviors and delete dead code:
- Site A (_applyRuntimeRewrites case 'augment'): the 4 ~/.augment dot-dir
  regexes now derive from getDirName('augment') via escapeRegExp (byte-
  identical; getDirName('augment')==='.augment') — no runtime literal.
- Site B (applyRuntimeContentRewritesForCommandsInPlace): the
  `if (runtime==='augment')` markdown-converter branch now reads
  runtime.hostBehaviors.commandBodyConverter and dispatches through a local
  COMMAND_BODY_CONVERTERS map (degrade-closed on unknown/absent name).
- Deleted dead `claudeToAugmentTools` map (zero refs; orphaned by ADR-1508
  single-sourcing) and the unreachable `else if (isAugment)` agent-conversion
  branch (augment ∈ _DESCRIPTOR_AGENTS_RUNTIMES → gated out upstream).
- Incidental orphan cleanup (no-defer): removed the equally-unreachable
  `else if (isTrae)` agent-conversion arm left behind by trae's already-merged
  migration #2094 (trae ∈ _DESCRIPTOR_AGENTS_RUNTIMES, same upstream gate).
  copilot/windsurf/codebuddy arms are removed by their own pending migrations.

UPGRADE 3 (transport:mcp): register the GSD companion MCP server in Augment's
settings.json under mcpServers.gsd (Augment hosts MCP in settings.json, not a
standalone file). mergeGsdMcpServerIntoSettings mutates the in-memory settings
object finishInstall already writes (gated on hostBehaviors.mcpCompanion===
'settings-json'); non-destructive + idempotent; symmetric uninstall removal.
settings.json is golden-excluded, so no golden change. UPGRADE 1 (named/
background dispatch) + UPGRADE 2 (settings-json hook bus, Claude dialect)
were already live in production — this adds tests exercising both.

Golden: byte-identical for all 16 runtimes (folds preserve regex behavior;
MCP lives in golden-excluded settings.json) — verified by a real double-install
tree diff. Tests: declarative-reference-augment (adapter/axes/fail-closed/
undocumented-sub-axes + source-grep guard scoped to conversion-logic branches)
+ augment-upgrades (dispatch negotiation, hook-bus live install, MCP add/
idempotent/preserve/uninstall). Matrix + connect-gsd-mcp-server + a stale
install-on-your-runtime hook-ownership claim corrected; changeset (Changed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 04:47:56 -04:00
Tom Boucher
5695522d5f feat(#2096): migrate Antigravity onto EoS declarative adapter + permission-writer + MCP companion (ADR-1239)
Fold all antigravity literal branches into descriptor-driven reads:
getConfigDirFromHome (→ configHome.kind 'dot-home-nested'), projectLocalHookPrefix
(→ hostBehaviors.hookPathStyle 'raw'), applyAgentPathRewrites (→ noPathRewrite),
getProjectInstructionFile (→ projectInstructionFile 'GEMINI.md'); removed the dead
inline convertClaudeAgentToAntigravityAgent branch + dead isAntigravity
destructures (antigravity is already on the descriptor-agents path). subagentToolkit
flipped undocumented→full (Context7: antigravity.google/docs/cli/features);
namedDispatch/nested/maxDepth/backgroundDispatch stay undocumented. Byte-identical
golden parity for all 16 runtimes.

UPGRADE 1 (permission-writer): permissionWriter 'antigravity' + configureAntigravityPermissions
merges a scoped permissions.allow block (GSD's own tree + hooks) into Antigravity's
settings.json — non-destructive, idempotent, symmetric uninstall. Added to
VALID_PERMISSION_WRITERS + the FinishPermissionWriter union.
UPGRADE 2 (MCP companion): configureAntigravityMcpConfig writes mcp_config.json
registering the gsd-core companion MCP server (Gemini-successor mcpServers schema,
best-effort — raw schema unpublished). Both writers dispatch from finishInstall.
settings.json is golden-excluded (HOOK_CONFIG_FILES); mcp_config.json (portable,
no absolute paths) is golden-tracked → only antigravity.json changes.

Tests: declarative-reference-antigravity extended (source-grep guard across 4
modules, fail-closed for the 4 undocumented sub-axes, validator acceptance) +
antigravity-upgrades (permission-writer + mcp_config live-install, idempotency,
user-preservation). Matrix + ADR-1016 + capability-manifest + CONTEXT.md +
connect-gsd-mcp-server docs updated; changeset (Changed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 03:07:01 -04:00
Tom Boucher
ab04916682 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>
2026-07-11 00:53:34 -04:00
Tom Boucher
f744635b3d feat(#2094): migrate Trae onto EoS imperative adapter + SOLO stage-metadata upgrade (ADR-1239)
Fold trae logic branches into descriptor-driven reads: skipSharedHooksInstall
gates (dropped && !isTrae), and the case 'trae' path-rewrite arm now computes
the self-alias from the descriptor-driven dirName (.trae). Dead isTrae bindings
removed from uninstall/writeManifest/finishInstall. trae's skills dispatch was
already descriptor-driven (converter-by-name). RUNTIME_CONTENT_DISPATCH.trae is
left as a runtime-keyed table registration (its regex/callback rewrites can't be
a byte-identical descriptor map — matches cursor/windsurf/cline). trae stays in
RUNTIME_FLAG_IDS: isTrae still gates the agents-converter selection (agents out
of scope; removal gated on the cross-runtime agents-dispatch migration).
Byte-identical golden parity for all 16 runtimes.

UPGRADE: SOLO stage/trigger metadata — emitted Trae SKILL.md now carries
stage: workflow (descriptor-gated via hostBehaviors.soloStageMetadata) so
Trae's SOLO Agent can auto-invoke GSD skills at the corresponding stage. Field
shape is best-effort/inferred (Trae publishes no formal schema). trae.json
golden regenerated.

Tests: trae-imperative-reference (adapter/axes/fail-closed shouldFlattenDispatch
+ no runtime==='trae' source-grep, isTrae exempted for agents) + trae-upgrades
(stage: workflow on installed SKILL.md, descriptor-gated). Matrix note +
changeset added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 21:31:10 -04:00
Tom Boucher
f014ec83bd feat(#2093): migrate Kilo onto EoS imperative adapter + hook-bus/model/MCP/dispatch upgrades (ADR-1239)
Fold remaining isKilo logic branches into descriptor-driven reads:
finishPermissionWriter (uninstall cleanup), skipSharedHooksInstall (hooks
copy), and a skills converter-name registry (the artifactLayout.converter
field is now load-bearing, not decorative). frontmatterDialect stays the
documented dispatch key for frontmatter (no descriptor field for it). Dead
isKilo destructure bindings removed. Byte-identical golden parity for all 16
runtimes (opencode, which shares kilo's combined-family path, verified clean).

UPGRADE 1 (hook bus): install .kilo/plugins/gsd-core.js native plugin +
extensionEvents:"kilo" + EXTENSION_EVENT_SURFACES.kilo (OpenCode-fork bus).
UPGRADE 2 (active model): populate runtimeTierDefaults.kilo + thread
modelOverride through convertClaudeToKiloFrontmatter — model no longer stripped
from agents. UPGRADE 3 (MCP): document the gsd-core MCP companion under kilo's
mcp config key. UPGRADE 4 (named dispatch): agents/*.md mode:subagent roster is
the Task-tool dispatch surface (tested); subagentToolkit stays 'undocumented'
per AC so dispatch degrades to 'degraded' by design.

Model-catalog single-source edit ripples the shared model-catalog.json hash
into all 16 golden fixtures (expected). Inline defect fixes (no-defer): stale-
bake-guard resolveAgentDir 'agent'->'agents' (was a silent no-op for opencode/
codex), hardcoded 'Removed OpenCode plugin' uninstall log -> generic, and the
connect-gsd-mcp-server.md OpenCode mcpServers->mcp doc error.

Tests: kilo-imperative-reference (adapter/axes/fail-closed/degradation/
hostBehaviors + widened isKilo source-grep across 4 modules) + kilo-upgrades
(plugin parity+load, model-override converter, agents dispatch surface, MCP
doc). Matrix + how-to + config docs updated; changeset added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 19:55:15 -04:00
Tom Boucher
c6ce110efa 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>
2026-07-10 15:44:19 -04:00
Tom Boucher
f152e9a069 feat(#2091): migrate hermes onto EoS imperative adapter + extensionEvents dialect (ADR-1239)
- Fold 7 hardcoded isHermes/runtime === 'hermes' branches in bin/install.js into
  descriptor-driven _hostBehaviors lookups (skillFrontmatterVersion,
  skillsManifestPrefix, trackCategoryDescription, writeCategoryDescription,
  reportSkillsCount, legacyCommandsGsdCleanup, brandingRewrites)
- Add runtime.hostBehaviors block to capabilities/hermes/capability.json
- Register EXTENSION_EVENT_SURFACES.hermes (13 real plugin hook events) in
  src/host-integration.cts — replaces the borrowed hookEvents:'claude' 6-event
  surface that silently never fired on Hermes
- Add extensionEvents:'hermes' to the descriptor
- Add 'hermes' to VALID_EXTENSION_EVENTS in capability-validator.cjs
- Regenerate capability-registry.cjs
- Tests: hermes-imperative-reference (negotiation, axes, fail-closed, source-guard),
  hermes-dispatch-upgrade (dispatch posture, degradation, fail-closed)
- Changeset + docs update
2026-07-09 20:47:35 -04:00
Tom Boucher
760eb71b6b feat(#2090): migrate cline onto imperative adapter + beforeTool/createAgentModel upgrades
Fold all hardcoded runtime === 'cline' / isCline branches in bin/install.js
into descriptor-driven runtime.hostBehaviors lookups (reapplyCommand,
frontmatterDialect, skipSharedHooksInstall, localTargetIsProjectRoot,
clineRulesSurface, localCommandsViaRules). Add cline-sdk-binding adapter
(ADR-1239 Phase D): UPGRADE 1 re-implements the .clinerules/hooks/PreToolUse
guard as a real AgentPlugin.hooks.beforeTool handler (fail-open, same
semantics); UPGRADE 2 wires DefaultGateway.createAgentModel params from
model_overrides/model_profile_overrides resolution (modelMode: active).
Install output is byte-identical (golden parity asserted for cline +
claude/cursor/codex/opencode).
2026-07-09 19:38:27 -04:00
Tom Boucher
45f3a2a5f9 fix(#2089): wire adapter into install path + address all review findings
MEDIUM fixes (code review):
- Wire resolveManagedHookEvents + resolveHookScripts + buildHookBusEntries
  from imperative-hook-bus.cts into writeCursorHooksJson — the install path
  is now truly descriptor-driven (reads hostBehaviors.managedHookEvents),
  not a hardcoded constant that happens to match the descriptor. bin/install.js
  passes the descriptor list via opts.managedHookEvents.
- buildHookBusEntries is now consumed (was dead code); entry-building is no
  longer duplicated inline.
- Remove try/finally from cursor-hook-bus-upgrade.test.cjs test bodies
  (violated CONTRIBUTING.md L342; redundant with t.after cleanup).

LOW fixes:
- Remove dead require('fs')/require('path') from gsd-cursor-pre-tool.js
- Fix resolveManagedHookEvents docstring (all-invalid fallback behavior)
- Add src/runtime-hooks-surface.cts to the AC2 source-guard file list

Security review: no CRITICAL/HIGH/MEDIUM findings (3 LOW are pre-existing
#777 baseline patterns, not regressions).
2026-07-09 13:17:04 -04:00
Tom Boucher
b0d985ccb3 feat(#2089): migrate cursor onto imperative adapter + hook-bus/dispatch upgrades 2026-07-09 00:21:39 -04:00
Tom Boucher
6e773d97df feat(#2088): migrate Codex onto the Embeddable Orchestration System (ADR-1239)
Drive Codex install/uninstall through the descriptor-driven Host-Integration
Interface (declarative embedding adapter → engine surface dispatch) and fold
every positive `runtime === 'codex'` / `isCodex` projection into descriptor-driven
`runtime.hostBehaviors`. Install/uninstall output stays byte-parity-gated
(tests/fixtures/golden-install-parity/codex.json); no other runtime changes.

Three Context7-verified upgrades, each with a test on the user-reachable surface:
- Skill root → canonical $HOME/.agents/skills via a skills-kind `home` override,
  with pre-move migration cleanup (stale ~/.codex/skills/gsd-* removed on install
  and uninstall; user content preserved). Fixes getGlobalSkillsBase, writeManifest,
  and the skill-manifest inventory to honor the override so --skills-root /
  sync-skills / the manifest report the real location.
- Six new hooks.json lifecycle events (PreToolUse, PermissionRequest, PreCompact,
  PostCompact, SubagentStop, UserPromptSubmit) shared by install + uninstall;
  extendedHookEvents reconciled [] -> the schema-valid wired subset.
- Explicit `[agents] max_depth = 1` in the managed config.toml block, pinning the
  negotiated dispatch.maxDepth:1 axis. validateCodexConfigSchema now permits a
  known-scalar-only bare `[agents]` AgentsToml table (still rejects [[agents]] and
  unknown-key break-forms, #2760); mergeCodexConfig preserves the user's own
  AgentsToml scalars (max_threads etc.) instead of dropping them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 21:42:11 -04:00
Tom Boucher
396f44bd0b feat(architecture): [EoS/opencode] Migrate OpenCode onto the Embeddable Orchestration System (ADR-1239, #2087)
Route OpenCode (and its Kilo sibling) through the public Host-Integration Interface and
land two Context7-verified capability upgrades. Byte-identical install output for all 16
runtimes (golden parity asserted).

Through the interface (AC2):
- OpenCode/Kilo's bespoke commands+skills+plugin install (the inline
  `else if (isOpencode || isKilo)` block) moves into the engine
  (installOpencodeFamilyCommands/Artifacts in src/install-engine.cts), dispatched by
  installRuntimeArtifacts when the descriptor declares hostBehaviors.combinedFamilyInstall.
  opencode/kilo now flow CLI -> _runtimeAdapter -> installRuntimeArtifacts like the skills
  runtimes. _isSkillsRuntime no longer excludes them; the bespoke block + dead
  copyFlattenedCommands are removed.
- Every hardcoded `runtime === 'opencode'`/`isOpencode` branch is folded into
  descriptor-driven runtime.hostBehaviors. ZERO `runtime === 'opencode'`/`'kilo'`
  string-equality remain in bin/install.js / install-engine.cts / runtime-artifact-conversion.cts.

Upgrades (AC4):
- Background dispatch: OpenCode shipped experimental background subagents in v1.15 and
  made them default-on in v1.17 -> dispatch.background/backgroundDispatch flip to true;
  shouldFlattenDispatch(opencode) now returns false (behavioral change; type: Changed).
- Expanded event surface: the OpenCode plugin subscribes permission.asked/replied +
  session.error.

Tests: opencode-imperative-reference (adapter/profile, shouldFlattenDispatch pin,
fail-closed negotiate, hostBehaviors, AC2 source-guard) + extended plugin surface test.
Docs: capability matrix v1.15/v1.17 citations. Changeset (Changed). gitignore .memdb//.memtrace/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 17:17:05 -04:00
Tom Boucher
eeec6b512e fix(#2086): AC2 source-guard must ignore comments/backtick prose, not just code
The #338 fail-safe commit added a comment containing the literal `runtime === 'claude'`
(explaining what the data lookup is NOT), which the AC2 source-grep test matched as a
false positive (the test read the whole file, prose included). Strip block/line comments
+ backtick spans before matching so the guard flags only LIVE code, and reword the
comment. CRLF-safe line-comment strip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 15:26:19 -04:00
Tom Boucher
102ffa0f9e fix(#2086): #338 fail-safe floor for reference-host behaviors on registry-load failure
Reviewer (PR #2106, elevated): if capability-registry.cjs fails to load,
_hostBehaviors('claude') returned {} — silently routing a claude LOCAL install to
the repo-shared settings.json instead of the gitignored settings.local.json (#338),
skipping mergeClaudePermissions + the .gsd-source marker. The migration is what
introduced that registry dependency (pre-PR the path had none).

Add FALLBACK_HOST_BEHAVIORS (keyed by runtime id — a data lookup, not a
runtime==='claude' branch) mirroring the reference host's #338-privacy-critical keys
(settingsFileByScope, permissionsSchema, sourceMarkerFile), consulted only when the
registry (or the descriptor) is unavailable. Behavior degrades CLOSED, never open;
the live descriptor stays the source of truth. Normal (registry-present) output is
unchanged (golden parity preserved). Pinned by tests via a registry-injected
_resolveHostBehaviors helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 15:19:12 -04:00
Tom Boucher
fdd5e401eb feat(architecture): [EoS/claude] drive claude through the imperative adapter + descriptor-driven hostBehaviors (#2086)
Fold Claude Code's install/uninstall onto the Embeddable Orchestration System
(ADR-1239 Phase D). claude is GSD's tier-1 reference host, but its install path
was still driven by 13 hardcoded `runtime === 'claude'` string-equality branches
scattered across bin/install.js rather than the public Host-Integration Interface.

- Route install()/uninstall() through `createImperativeAdapter({runtime})` — the
  adapter delegates to the SAME installRuntimeArtifacts/uninstallRuntimeArtifacts
  engine calls, so output is byte-identical (proven pre/post, both scopes).
- Replace all 13 `runtime === 'claude'` / `runtime !== 'claude'` branches with
  descriptor-driven `runtime.hostBehaviors` lookups on capabilities/claude/
  capability.json (attributionSource, authorsCanonicalWorkflow, localInstallStyle,
  permissionsSchema, settingsFileByScope, sourceMarkerFile, agentFrontmatterExtensions,
  ownsClaudePaths, nativeModelAliases, skillsGlobalOnboarding). Behavior is
  identical; the brittle string-equality coupling (the add-a-host tax) is gone.
- Single-source the scattered literal 'claude' defaults/rosters behind DEFAULT_RUNTIME.
- Extend golden-install-parity to assert the claude LOCAL legacy layout is
  byte-identical too (AC1 "both scopes"); exclude the platform-varying
  settings.local.json (same reason settings.json is excluded).
- New tests/claude-imperative-reference.test.cjs: adapter kind, programmatic-cli
  profile, fail-closed negotiation on a corrupted/partial descriptor, and an AC2
  source guard that no `runtime === 'claude'` branch remains.

No user-visible install-output change (internal architecture only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 13:31:53 -04:00
Tom Boucher
015c3a7fda fix(#2071): extract install-time effort resolvers so effort sync stops requiring the un-shipped bin/install.js
`gsd-tools effort sync` crashed in every installed runtime (e.g. ~/.claude/gsd-core/)
with `Cannot find module '../../../bin/install.js'`: cmdEffortSync (src/commands.cts)
required the package-root bin/install.js for its install-time effort resolvers, but the
installer only copies the gsd-core/ subtree into a runtime home — bin/install.js is never
present there. So `effort` config changes silently never reached installed agents without
a full reinstall (exactly the gap #488 was meant to close). 4th instance of the recurring
"runtime code under gsd-core/ requires a file outside the shipped subtree via ../../../"
anti-pattern (#1223/#1920/#1383 were the prior three, all already mitigated).

Fix (ADR-457 direction — extract, single source): move readGsdEffectiveEffortConfig +
resolveInstallTimeEffort (with their _getGsdEffortCatalog + _readGsdConfigFile helpers)
out of the hand-authored bin/install.js into a new src/install-effort-resolver.cts that
compiles into the shipped gsd-core/bin/lib/install-effort-resolver.cjs. commands.cts now
requires it as a sibling (`./install-effort-resolver.cjs`) — always present in the
installed tree — instead of `../../../bin/install.js`. bin/install.js imports the same four
symbols back from the new module (it still calls them + re-exports them), so there is one
source of truth and no duplication/drift. The lazy manifest read is repointed from the
package-root layout (`.., gsd-core, bin, shared`) to the bin/lib layout (`.., shared`).

Scope note: this is one of four instances of the anti-pattern; the other three are already
shipped/guarded. A build-time guard rejecting new cross-boundary requires whose target isn't
in the installer copy manifest (to prevent instance #5) is recommended on the issue but kept
out of this fix.

Tests: tests/effort-sync-installed-runtime.test.cjs does a real minimal install into a temp
home (the golden-parity helper) and runs the issue's exact repro
(`gsd-tools effort sync --config-dir <temp>`), asserting no MODULE_NOT_FOUND for
bin/install.js. Fail-first verified: against pristine next the same test throws
`Cannot find module '../../../bin/install.js'` at cmdEffortSync; post-fix it syncs cleanly.

New module registered in .gitignore (ADR-457), eslint ignores, docs/INVENTORY.md +
INVENTORY-MANIFEST.json. bin/install.js is not shipped and the new module is under bin/lib
(excluded from golden parity), so no golden fixtures change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 22:21:45 -04:00
Tom Boucher
2b806904e2 fix(#1821): stop copying dead hook scripts for Kilo and ZCode (hooksSurface:none)
Kilo and ZCode both declare `hooksSurface: 'none'` and have no plugin surface,
so the GSD installer staged lifecycle hook scripts (hooks/*.js, hooks/*.sh,
hooks/lib/) plus a `{"type":"commonjs"}` package.json marker into their config
dirs where nothing ever invokes them — dead weight (#1821).

The installer's two hook-copy guards at bin/install.js were still on the legacy
hardcoded runtime-name list and never excluded Kilo or ZCode. Add
`&& !isKilo && !isZcode` to both (isZcode added to the install() runtimeFlags
destructure).

OpenCode — which #1821 also named — is deliberately NOT excluded: since the
issue was filed, #1914 shipped a native OpenCode plugin (plugins/gsd-core.js)
that spawns those exact staged hooks via OpenCode's event bus and requires both
the hook scripts and the CommonJS package.json marker. Excluding OpenCode would
regress #1914, so its hooks stay live. The genuinely-dead cases are Kilo & ZCode.

- bin/install.js: add `&& !isKilo && !isZcode` to the hooks/dist copy guard and
  the hooks/lib copy guard; document the OpenCode-vs-Kilo/ZCode split.
- tests/install-minimal-hooks.test.cjs: regression test asserting Kilo and ZCode
  receive no gsd-*.js/.sh hooks or hooks/lib, while OpenCode keeps its hooks +
  #1914 plugin and Claude keeps its hooks (over-exclusion guard).
- tests/fixtures/golden-install-parity/{kilo,zcode}.json: drop the 21 hooks/*
  entries and the package.json marker they no longer receive (opencode unchanged).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 09:40:01 -04:00
Tom Boucher
d671171698 feat(#1575): complete agent-converter descriptor cutover for copilot/antigravity + surface path parity
- Teach applySurface to build agentCtx (pathPrefix + attribution) and pass it
  to kind.stage() for agents kind, mirroring createRuntimeArtifactInstallPlan
  (ADR-1235 §1). Surface-path agents now receive path-rewrite + attribution +
  converter + normalize, matching install output byte-for-byte.

- Pass skills:'*' sentinel for agents staging when no surface state modifications
  exist, so ALL agents are staged (not just those referenced by _calls_agents_).

- Declare converted agents kind in copilot and antigravity capability.json;
  add to _DESCRIPTOR_AGENTS_RUNTIMES in bin/install.js.

- Handle copilot .agent.md filename rename in both _copyStaged (install path)
  and _syncGsdDir (surface path).

- Ship golden-parity harness (ADR-1235 §0): tests/issue-1575-agent-descriptor-
  parity.test.cjs asserts applySurface output is byte-identical to
  installRuntimeArtifacts for all 7 descriptor-driven runtimes, plus stale-
  cleanup convergence and prune data-loss coverage.

- Update ADR-1235 with cutover progress.

Cline remains deferred (rules-only local branch + local/global complication).
2026-07-06 11:34:32 -04:00
Tom Boucher
d6cf377064 fix(#1925): correct _isSkillsRuntime discriminator + zcode install contract
The descriptor-driven gate must treat a runtime as layout-driven when its scoped
artifactLayout is non-empty (any kind), not only when it has a skills kind —
windsurf's global layout is agents-only and is a legitimate layout-driven runtime.
Preserve the three legacy special-cased paths (opencode/kilo combined path,
claude-local copyWithPathReplacement).

zcode falls into the default package.json+hooks path (not in the CommonJS-mode
exclusion roster), so its install contract is packageJson: true — matching
observed install output. No runtime === 'zcode' branch is added (AC#3): zcode
gets the default by not being excluded.
2026-07-06 08:31:52 -04:00
Tom Boucher
50aaf09342 fix(#1925): make _isSkillsRuntime gate descriptor-driven so zcode installs skills
The legacy _isSkillsRuntime gate was a hardcoded isCodex || isCopilot || ...
roster; zcode was absent so the layout-driven skills-install path was skipped
(commands + agents installed, but no skills). Adding || isZcode would violate
AC#3 (no runtime === 'zcode' branches), so the gate is now derived from the
descriptor: a runtime takes the skills path iff its scoped artifactLayout
declares a skills kind. Behavior-preserving for all 15 existing runtimes
(verified by their install contracts); includes zcode (and any future skills
runtime) with zero per-runtime branches. opencode/kilo keep their specialized
combined path.

Regenerate the golden-install-parity fixtures: the non-zcode runtimes shift only
the shared model-catalog.json hash (zcode was added to the catalog); zcode now
includes its skills tree.
2026-07-06 08:31:52 -04:00
Tom Boucher
69b309e4e0 feat(#1925): add ZCode (Z.ai) as a pluggable runtime descriptor
Add ZCode as a first-party runtime via a declarative capability descriptor
(capabilities/zcode/capability.json) with zero hardcoded runtime === 'zcode'
branches — exercising the de-hardcoded, data-driven runtime path that 1.7.0
(ADR-1016 / ADR-1239) enables.

Descriptor (all axes sourced verbatim from zcode.z.ai docs):
- configHome ~/.zcode; nested skills + flat commands/agents; profile-marker install
- Claude-shaped skill format reuses convertClaudeCommandToClaudeSkill (no new converter)
- hostIntegration: declarative / slash-file / mcp / electron; dispatch background=false
  (foreground-only per docs); nested+maxDepth undocumented; passive model mode

Installer registration (data, not branches): --zcode flag, allRuntimes, runtimeMap,
interactive menu, --all list. getGlobalConfigDir + resolveRuntimeArtifactLayout +
ALLOWED_CONFIG_RUNTIMES + resolveInstallPlan all derive from the descriptor.

Revamped the brittle per-runtime golden-master tests to be count-agnostic,
descriptor-derived property tests (1.7.0 makes runtimes pluggable data, so pinning
frozen '15 runtime' snapshots is the wrong invariant): getdirname, label-policy,
config-adapter-registry (intent + install-plan golden master), capability-registry,
host-integration-descriptors (counts derive from curated maps). Adding a runtime
descriptor now extends coverage with zero edits to those suites.

Welcome banner, --zcode help, supported-runtimes how-to, and the host-integration
capability matrix (every axis cited) updated.
2026-07-06 08:31:51 -04:00
Tom Boucher
0656f2e831 fix(#1477): write .gsd-source marker at install for Claude-global (#1487)
fix(#1477): write .gsd-source marker at install for the Claude-global layout
2026-07-05 14:09:17 -04:00
Tom Boucher
8f2ebbe9bf feat(#1928): remove sunset Gemini CLI runtime, redirect to Antigravity (#1996)
* feat(#1928): remove sunset gemini cli runtime, redirect to antigravity

Google sunset Gemini CLI on 2026-06-18; Antigravity CLI is its official successor (already a first-class GSD runtime). Remove the gemini runtime from the enum (16->15), aliases, labels, config-home fragment, install path, converters (convertClaudeToGemini{Markdown,Toml,Agent}, convertSlashCommandsToGeminiMentions), capability descriptor, gemini-extension.json, RULESET.GEMINI.*, and the interactive menu (renumbered, no gap).

--gemini now prints an explicit deprecation notice citing the 2026-06-18 sunset and redirects to --antigravity (no silent alias, per the issue's Hyrum's-Law rejection). Antigravity is preserved throughout: its GEMINI.md contextFileName, .gemini/antigravity config home, the shared convertGeminiToolName/claudeToGeminiTools tool vocabulary, and the 'gemini' hookEvents dialect it declares. GEMINI.md retargeted as Antigravity's context file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(#1928): backfill changeset PR number (#1996)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(#1928): drop Gemini CLI from issue templates (review nit)

Removes the sunset Gemini CLI runtime from the two GitHub issue-template
runtime lists that the removal PR missed, per @davesienkowski's review nit:
- feature_request.yml: 'Applicable runtimes' checkbox (a user could otherwise
  request a feature for a runtime GSD no longer supports)
- bug_report.yml: 'Runtime' dropdown + the stale ~/.gemini/settings.json
  retrieval-help line

Leaves the post-removal templates fully consistent with the Antigravity redirect.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 13:32:51 -04:00
Joe Slitzker
4948ad848e Merge remote-tracking branch 'origin/next' into fix/1477-surface-source-marker
# Conflicts:
#	tests/golden-install-parity.test.cjs
2026-07-04 08:22:49 -05:00
Tom Boucher
d3d689a5e9 fix(#1920): resolve host version from gsd-core/VERSION + ship capability generators (#1938)
The flattened install layout broke the third-party capability ecosystem in two
ways. Both are fixed at the host-version / installer boundary.

Gap 1 — host version read as 0.0.0. The running GSD version was resolved via
require('../../../package.json') (loader/source) and require('../../package.json')
(the gsd-tools CLI), which in the installed layout is the versionless CommonJS
marker → the fail-closed fallback reported 0.0.0, so `capability install` rejected
any manifest with a real engines.gsd range as "incompatible with GSD 0.0.0". For
runtimes that get no marker, and for local installs, that walked-up package.json
could even be the USER's own project, reporting a wrong version. Fix:
readHostVersion() (capability-loader.cts, capability-source.cts) and capHostVersion()
(gsd-tools.cjs) now prefer the authoritative gsd-core/VERSION the installer already
writes for EVERY runtime (mirrors resolveVersionFrom(), #1383), falling back to the
runtime-root package.json for the dev/source tree, then fail-closing. This fixes
every runtime and the actual `capability install` CLI path without touching the
marker package.json, so uninstall is unchanged (no data-loss surface).

Gap 2 — scripts/gen-capability-registry.cjs (+ its sibling
gen-loop-host-contract.cjs) were never copied by the installer, so the loader's
never-crash invariant discarded every overlay and fell back to the frozen
first-party registry (installed capabilities silently inert). Now copied,
uninstalled, and manifest-tracked exactly like fix-slash-commands.cjs (#1223).

Regenerates the 16 golden-install-parity fixtures to capture the two newly shipped
generator scripts and the gsd-tools.cjs change.

Regression tests (RED→GREEN): readHostVersion VERSION-first / fallback / fail-closed
resolution; an end-to-end `capability install` against a REAL installed layout
proving the engines gate sees the real host version, not 0.0.0; and a real-install
check that both generators are shipped and manifest-tracked.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:34:30 -04:00
Tom Boucher
541de6894f feat(#1682): OpenCode companion-MCP binding (mcp.gsd) — Phase 5 Slice 1a (#1929)
* docs: align PR-FLOW push gate from gsd-test-summary to gsd-test

gsd-test is the application (open-gsd/gsd-test-runner); gsd-test-summary is
the legacy local wrapper. RULESET.PR-FLOW.docker-before-push now names gsd-test
as the pre-push gate (exit 0 / verdict outcome 'passed').

* docs: drop WORKTREE.SEAM local node --test rule; align PROC dispatch to gsd-test

- Remove WORKTREE.SEAM.execution-rule (prefer local node --test) — contradicts
  CLAUDE.md 'NEVER run node --test locally'; CLAUDE.md wins.
- PROC.PARALLEL-FIX-DISPATCH: gsd-test-summary --both -> gsd-test (app; --both
  was a legacy wrapper flag).

* feat(#1682): OpenCode companion-MCP binding (mcp.gsd) — Phase 5 Slice 1a

configureOpencodePermissions registers the Phase-4 companion MCP server
(gsd-mcp-server) as opencode mcp.gsd, so OpenCode connects to GSD's command
(point 1) + state-IO (point 5) with no bespoke plugin (ADR-1239 Phase D).
Idempotent + non-clobbering (add-if-absent; respects a user-defined mcp.gsd).
Local-stdio schema per OpenCode config (packages/core/src/config/mcp.ts);
`-p @opengsd/gsd-core` resolves the bin (name != package) under npx.

Tests: registers-on-object-config; does-not-clobber-user-entry.

* docs(changeset): OpenCode companion-MCP binding (#1682)

* fix(#1682): use PACKAGE_NAME single-source (#516) + refresh opencode golden parity

- mcp.gsd command: replace hardcoded '@opengsd/gsd-core' literal with
  PACKAGE_NAME from gsd-core/bin/lib/package-identity.cjs (#516 single-source).
- opencode golden-install-parity fixture: refresh opencode.json hash for the
  added mcp.gsd block (configureOpencodePermissions output change).

* docs(changeset): add docs-exempt marker (Phase 5 slice)

* docs(changeset): backfill PR #1929
2026-07-02 14:57:44 -04:00
Tom Boucher
ff6b5cb024 feat(#1914): OpenCode native plugin integration (Option 1 file-copy) (#1923)
* feat(#1914): OpenCode native plugin integration (Option 1 file-copy)

Ship a native OpenCode plugin (.opencode/plugins/gsd-core.js) plus the
installer step that delivers it, so GSD's lifecycle hooks run on OpenCode.
OpenCode declares hooksSurface:'none', so GSD's hook scripts already ship to
<configDir>/hooks/ but nothing invokes them; the plugin bridges OpenCode's
event bus onto those scripts as subprocesses (prompt/read/worktree/workflow
guards, injection scanner, context monitor).

Distribution is Option 1 (file copy) per the #1914 triage decision: no
scripts.build rename, no prepare/prepack removal. package.json gains
main + .opencode in files[] for discovery.

Corrected against OpenCode's docs + loader source (not the reference branch):
- Auto-discovery globs {plugin,plugins}/*.{ts,js} — .cjs is never matched, so
  the installed adapter must be .js (config dir carries {"type":"commonjs"}).
- No opencode.json plugin-array patch — that array is npm-only; local files
  are auto-discovered.
- REPO_ROOT is resolved by walking up to the dir holding hooks/ + gsd-core/,
  correct for package tree, global install, and local install.
- Config-hook registration is gated (IS_PACKAGE_TREE) so it never
  double-registers commands/agents/skills already delivered by native copy.

Also fixes an incidental .gitignore drift: 8 ADR-1239 .cts-generated .cjs
artifacts were untracked-and-not-ignored (leak risk) — now ignored.

Tests: tests/opencode-plugin-adapter.test.cjs (14, pure helpers + real
subprocess bridge against stub hooks); golden-install-parity regenerated.
Green on Mac + Linux (gsd-test): 0 failures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#1914): harden OpenCode plugin export shape + advisory accumulation (adversarial findings)

Address Codex adversarial-review findings:
- HIGH: export `{ id, server }` could trip OpenCode's loader
  (`for (entry of Object.values(mod)) getServerPlugin(entry)` throws on a
  non-extractable value). Make `id` NON-ENUMERABLE and assign module.exports
  from a variable (not a literal) so no string `id` is ever iterated — verified
  loader-safe under real import(pathToFileURL) (default + module.exports alias,
  both objects with .server; no bare id string).
- MEDIUM: sequential advisory hooks clobbered output.metadata._gsdAdvisory;
  now accumulate into an array.
- LOW: resolveRepoRoot fallback returned ".." while the comment said "../.." —
  aligned to "../.." (package-tree depth).

Tests: added a faithful loader-loop emulation (raw CJS + ESM namespace views),
advisory-accumulation, and a real bin/install.js copy→manifest→uninstall
integration test. golden regenerated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(#1914): add changeset fragment for OpenCode plugin integration (PR #1923)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#1914): Windows — assert rewritten path with string include, not path-regex

The Read content-rewrite test built a RegExp from `path.join(root,'gsd-core')`.
On Windows the backslashes in the path are interpreted as regex escapes, so the
assertion never matched and `test (windows-latest, 24)` failed — even though the
adapter rewrote the path correctly. Replace the RegExp with a separator-agnostic
`String.includes` check (the repo's no-path-literal-in-assert concern).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:01:51 -04:00
Tom Boucher
a47979bb92 refactor(#1679): ADR-1239 Phase B — collapse program + command chains [AC2 slice 4] (#1813)
* refactor(#1679): ADR-1239 Phase B — collapse program + command chains [AC2 slice 4]

Phase 2 AC2 slice 4. Collapses two more duplicated runtime->string chains in
bin/install.js's post-install next-step message:

- program (14 branches): an EXACT duplicate of runtimeLabel -> getRuntimeLabel.
- command (14 branches): the per-runtime /gsd-new-project invocation syntax
  (gemini '/gsd:', codex '$', cursor skill-mention, kimi '/skill:', default
  '/gsd-new-project') -> new getRuntimeNewProjectCommand(runtime) helper.

- src/runtime-name-policy.cts: RUNTIME_NEW_PROJECT_COMMANDS table +
  getRuntimeNewProjectCommand(runtime) (sibling to runtimeFlags/getRuntimeLabel).
- bin/install.js: import getRuntimeNewProjectCommand; replace the program +
  command chains with single lookups.
- tests/runtime-label-policy.test.cjs: 2 new tests for
  getRuntimeNewProjectCommand (4 overrides + default for the other 12).

runtime === count: 53 -> 25 (-28). Cumulative Phase 2 this session: 129 -> 25
(-104). golden-install-parity 16/16 (program/command are stdout-only so not
parity-covered, but program matches RUNTIME_LABELS exactly and command values
are preserved verbatim in the table). AC2 data-collapse now essentially
exhausted; remaining 25 branches are the ADR-1235 agent-loop tail + per-runtime
semantic behavior.

* chore(changeset): add Changed fragment for program+command collapse (#1679)
2026-06-28 15:22:13 -04:00
Tom Boucher
f954bb4cac refactor(#1679): ADR-1239 Phase B — collapse is<Runtime> flag blocks into runtimeFlags [AC2 slice 3] (#1811)
* refactor(#1679): ADR-1239 Phase B — collapse is<Runtime> flag blocks into runtimeFlags [AC2 slice 3]

Phase 2 AC2 slice 3. Collapses the four duplicated 'const isX = runtime === x'
declaration blocks in bin/install.js (uninstall / writeManifest / install / a
fourth helper — 48 of the 101 remaining runtime=== branches) into a single
runtimeFlags(runtime) helper in src/runtime-name-policy.cts, sibling to
getDirName / getRuntimeLabel / getGlobalConfigHomeFragment.

The purest add-a-host tax: a new runtime meant remembering to add ~12 flag lines
to each of four functions. Now it is one entry in RUNTIME_FLAG_IDS.

- src/runtime-name-policy.cts: RUNTIME_FLAG_IDS + runtimeFlags(runtime) -> frozen
  map of is<Runtime> booleans (single runtime=== source, via loop).
- bin/install.js: import runtimeFlags; replace the 4 declaration blocks with one
  destructure each. ZERO usage-site churn (flag names preserved; install.js's
  eslint block has no no-unused-vars rule so destructure-all is clean).
- tests/runtime-flags.test.cjs: 4 tests (each runtime sets exactly its flag,
  claude/unknown/empty -> all false, all 15 flags present + frozen, drift guard).

runtime === count: 101 -> 53 (-48). golden-install-parity 16/16 byte-identical
(behavior-identical collapse). AC2 data-collapse now substantially complete;
ADR-1235 agent-loop tail + per-runtime semantic residue remain (separate).

* chore(changeset): add Changed fragment for runtimeFlags collapse (#1679)
2026-06-28 14:59:29 -04:00
Tom Boucher
41193a44bd feat(#1681): ADR-1239 Phase C-2 — gsd-mcp-server bin entry + lifecycle test [slice 3b] (#1810)
* feat(#1681): ADR-1239 Phase C-2 — gsd-mcp-server bin entry + lifecycle test [slice 3b]

Phase 4 slice 3b (closes #1681). The companion MCP server bin entry so any
MCP-consuming host connects via 'npx gsd-mcp-server' (or its bin on PATH) and
gets GSD command (point 1) + state IO (point 5) with no bespoke plugin.

- gsd-core/bin/gsd-mcp-server.cjs: #!/usr/bin/env node shim requiring
  ./lib/mcp-server.cjs + runServer({stdin, stdout}); non-zero exit on fatal
  error (justified n/no-process-exit disable). Mirrors gsd-tools.cjs.
- package.json: add 'gsd-mcp-server' bin entry.
- tests/gsd-mcp-server-bin.test.cjs: 3 process-lifecycle tests — initialize +
  tools/list round-trip + clean exit, malformed-line -> parse error + server
  keeps running, empty stdin -> clean exit. Synchronous spawnSync (bounded;
  server exits on stdin EOF, no orphan).

Phase 4 trust-gate (#1806) + loader wiring (#1808) + server module (#1809) +
this bin/lifecycle slice = all of #1681's deliverables. Concrete host binding ->
Phase 5 (#1682). npm-integrity + eslint + security + inventory all clean.

* docs(#1681)+chore(changeset): how-to for the companion MCP server + Added fragment

docs/how-to/connect-gsd-mcp-server.md — Diataxis how-to guide for connecting
any MCP-capable host to gsd-mcp-server: goal-oriented flow (add config → restart
→ verify), real-world per-host conditionals, troubleshooting, and a trimmed
reference table. Explanation/reference linked out (ADR-1239, capability-trust-
model) per Diataxis boundary rules rather than mixed in.

.changeset/humble-seals-rest.md — type: Added (first user-reachable surface of
the epic: a new bin command). The how-to doc satisfies the docs-required gate.

* fix(#1681): move gsd-mcp-server shim to top-level bin/ (out of the runtime-copied tree)

The shim at gsd-core/bin/gsd-mcp-server.cjs was inside the tree the installer
copies into every runtime config dir, so it leaked into all 16 runtimes and
broke golden-install-parity. The MCP server is a PACKAGE bin the host spawns
(npx gsd-mcp-server), not a per-runtime artifact — so it belongs at top-level
bin/ alongside install.js (which is also never copied into a runtime config).

- gsd-core/bin/gsd-mcp-server.cjs -> bin/gsd-mcp-server.js (require path now
  ../gsd-core/bin/lib/mcp-server.cjs).
- package.json: bin entry -> bin/gsd-mcp-server.js.
- tests/gsd-mcp-server-bin.test.cjs: SHIM path updated.
- eslint.config.mjs: add bin/gsd-mcp-server.js to the bin/install.js block
  (drops the n/no-process-exit disable — the n plugin isn't loaded for that
  block, so the disable referenced an undefined rule).

golden-install-parity 16/16 restored; lifecycle + unit tests green; eslint 0;
lint:ci all ok.
2026-06-28 14:27:43 -04:00
Tom Boucher
4810bfe4df refactor(#1679): ADR-1239 Phase B — collapse getConfigDirFromHome chain into getGlobalConfigHomeFragment [AC2 slice 2/6] (#1801)
* refactor(#1679): ADR-1239 Phase B — collapse getConfigDirFromHome chain into getGlobalConfigHomeFragment

AC2 slice 2. Collapses the 14-branch runtime->global-config-home source-fragment
chain in bin/install.js getConfigDirFromHome (the hook path.join() codegen mapping)
into a single getGlobalConfigHomeFragment(runtime) lookup in runtime-name-policy.cts,
sibling to getDirName / getRuntimeLabel.

The antigravity branch stays dynamic in the caller (resolveAntigravityGlobalDir +
path.relative — env-overridable, multi-segment); the prior inner unreachable
`if (!isGlobal) return "'agents'"` (dead: !isGlobal returns at the fn top) is dropped.

Behavior: byte-identical. Fragments preserved verbatim in the table.
- claude/unknown/empty -> default '.claude' fragment
- 14 runtimes (copilot..kimi) -> table lookup
- antigravity -> dynamic (unchanged)

Verification:
- TDD: tests/global-config-home-fragment.test.cjs (golden map + 2 drift guards +
  fallbacks). Red->green.
- 16-runtime golden install parity: byte-identical (hook codegen output unchanged)
- eslint + test-file-count + regression-names clean
- runtime === count in install.js: 115 -> 101 (-14)

* chore(changeset): add Changed fragment for getConfigDirFromHome collapse (#1679)
2026-06-28 00:26:13 -04:00
Tom Boucher
ad79e99fc9 refactor(#1679): ADR-1239 Phase B — collapse runtimeLabel chains into getRuntimeLabel [AC2 slice 1/6] (#1800)
* refactor(#1679): ADR-1239 Phase B — collapse runtimeLabel chains into getRuntimeLabel

Collapses the two duplicated runtimeLabel assignment chains in bin/install.js
(uninstall() and install()) into a single getRuntimeLabel(runtime) lookup in
src/runtime-name-policy.cts — a curated short-form label table, sibling to the
registry-derived getDirName precedent.

This is slice 1 of AC2 (regional residue-collapse in install.js) under
ADR-1239 Phase B / #1679. The install/uninstall console label was the add-a-host
tax poster child: a new runtime meant adding a label line to BOTH chains, and
they had drifted out of sync:
  - kimi:  install 'Kimi' / uninstall 'Kimi CLI'  -> canonical 'Kimi CLI'
  - cline: install 'Cline' / uninstall (omitted)  -> canonical 'Cline'

Each canonical value matches the majority chain AND the descriptor title.

Behavior:
- 14 of 16 runtime labels unchanged in both sites (zero observable change).
- 2 unifications (kimi-install, cline-uninstall) move toward consistency.
- Unknown/empty runtime id fails closed to 'Claude Code'.
- Raw-id lookup only (no alias expansion); callers pass canonicalized ids.

Voice: these SHORT UI labels are intentionally distinct from the descriptor
title (the long product name) which serves docs/registry display, not the
console. A future slice may relocate this to a runtime.label descriptor field.

Verification:
- TDD: tests/runtime-label-policy.test.cjs (golden map + drift guard + fallbacks)
- 16-runtime golden install parity: byte-identical (labels are stdout-only)
- 162-test neighbor cluster green; eslint + test-file-count + regression-names clean
- runtime === count in install.js: 129 -> 115 (-14, the uninstalled label chain)

* chore(changeset): add Changed fragment for runtimeLabel collapse (#1679)

PR #1800 touches bin/ → changeset-required gate. Mirrors the sibling
ADR-1239 Phase B slice (eager-elks-frolic): type Changed + docs-exempt
marker (internal refactor, no user-facing doc surface).
2026-06-28 00:05:33 -04:00
Joe Slitzker
165e98b972 Merge remote-tracking branch 'origin/next' into fix/1477-surface-source-marker 2026-06-26 17:05:43 -05:00
Joe Slitzker
bc2e4395ac fix(#1477): warn on marker-write failure; writer fault-injection test
Address re-review minors on #1487:
- bin/install.js: replace the silent .gsd-source marker catch with a
  console.warn so a failed write is diagnosable (walk-up also fails on the
  Claude-global layout, so a swallowed error still breaks /gsd-surface).
- tests/runtime-artifact-layout.test.cjs: add a writer fault-injection case
  (mock fs.writeFileSync to throw for the marker) proving the catch branch is
  reachable, install stays non-fatal, and the warning is emitted.
2026-06-26 16:43:31 -05:00
Tom Boucher
ce62f2b68d refactor(#1763): ADR-1235 agent migration — cut over the trivial-converter group to the descriptor path (#1764)
ADR-1235 step 1: route the trivial-converter runtime group (cursor, windsurf, augment, trae, codebuddy) off the inline install() agent loop onto the descriptor-driven installRuntimeArtifacts path. Establishes the converter-context foundation (pre-converter cross-cutting + no agent-stamp). Agent install output is byte-identical for all 16 runtimes (golden-parity, global + verified local). cline deliberately excluded (local rules-only). Closes #1763.
2026-06-26 15:38:51 -04:00
Tom Boucher
901dabe6f1 refactor(#1758): data-drive copyWithPathReplacement converter dispatch (#1759)
ADR-1239 Phase B (parent #1679). Replace copyWithPathReplacement's 13
hardcoded `const isX = runtime === 'x'` flags + two ~100-line per-runtime
if/else converter chains with a module-level RUNTIME_CONTENT_DISPATCH table
(one entry per runtime: md transform, mdSkipGenericRewrite, mdReattributeAfter,
mdTomlRenameOnCommand, js transform) + a uniform dispatch loop that applies the
cross-cutting steps (path rewrite -> attribution -> stamp -> normalize) once.

Byte-identical install output for all 16 runtimes (golden-parity harness #1730);
codex-verified the transform order + every per-runtime quirk (gemini .toml
rename, copilot/antigravity skip-generic + reattribute, qwen/hermes inline
swaps, .cjs/.js fall-through) is preserved.

Also folds in a pre-existing bookkeeping fix (no-defer): gsd-core/bin/lib/
cli-skew-check.cjs (tsc build artifact from #1755) was eslint-ignored but
missing from .gitignore — added for consistency with the other built artifacts.

Closes #1758

Co-authored-by: review-bot <review-bot@gsd>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 13:13:48 -04:00
Tom Boucher
b307c4cfde refactor(#1734): extract install engine from bin/install.js (ADR-1239 Phase B deep move) (#1735)
* refactor(#1734): extract install engine from bin/install.js (ADR-1239 Phase B deep move)

Relocate the runtime-artifact install cluster out of the 12,490-line
bin/install.js into a dedicated src/install-engine.cts -> install-engine.cjs:
installRuntimeArtifacts, uninstallRuntimeArtifacts, installOpencodeFamilySkills,
and their cluster helpers (_copyStaged, snapshot/restore, legacy migration,
GSD-entry pruning, preserve/restoreUserArtifacts, OpenCode-family converters,
USER_OWNED_ARTIFACTS).

- bin/install.js imports the engine and re-exports the moved symbols for
  back-compat; getCommitAttribution STAYS in install.js (impure config I/O +
  argv explicitConfigDir global) and is injected via a resolveAttribution param.
- 17 test files migrated to import the moved symbols from the engine.
- Bookkeeping: eslint built-artifact ignore, .gitignore, INVENTORY manifest+row,
  CONTEXT.md Install Engine Module glossary seam.

Behaviour-preserving: install output is byte-identical for all 16 runtimes
(golden-parity harness #1730) — the only delta is the new install-engine.cjs
file shipping in the installed gsd-core/bin/lib/ tree.

Closes #1734

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#1734): backfill changeset PR number (#1735)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: review-bot <review-bot@gsd>
2026-06-25 21:16:47 -04:00
Tom Boucher
41dfeed45a feat(#1724): complete install write-confinement (copyWithPathReplacement, installCodexConfig) (ADR-1239 Phase B) (#1725)
* feat(#1724): complete install write-confinement (copyWithPathReplacement, installCodexConfig)

ADR-1239 Phase B (parent #1679). PR #1706 (2a) confined the layout-driven
plan path and _copyStaged's inline guard; this completes the destSubpath
write-confinement acceptance criterion for the two remaining write sites
and canonicalizes _copyStaged.

- copyWithPathReplacement: new required confinementRoot param; a fail-closed
  gate (assertDestWithinConfigHome + hasExistingSymlinkBetween) runs BEFORE
  the rmSync/mkdirSync; root threaded through recursion + all 4 call sites
  (stageRoot for pristine staging, targetDir for the 3 install sites); writes
  go through the validated absolute path. Exported for behavioral testing.
- installCodexConfig: confines config.toml, agents/, and per-agent
  agents/<name>.toml (name from agent frontmatter) via the canonical gate +
  symlink-escape guard (parity with the other two functions).
- _copyStaged: fail-closed when configDir omitted (all callers pass it);
  delegates strict-subpath to the canonical gate, keeps its symlink guard,
  writes through the validated absolute path.

Reuses the existing assertDestWithinConfigHome (handles absolute dests via
path.resolve) and hasExistingSymlinkBetween — no new module. Behavioral
regression tests (escape/dest==root/fail-closed/symlink/name-injection),
red-first proven; cross-platform symlink tests use t.skip not bare return.

Closes #1724

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#1724): backfill changeset PR number (#1725)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 16:54:59 -04:00
Tom Boucher
fb5f89db10 feat(#1704): destSubpath write-confinement (ADR-1239 Phase B) (#1706)
* feat(#1679): confine install writes within configHome

ADR-1239 Phase B write-confinement: a pure assertDestWithinConfigHome(configDir, destSubpath) rejects a destSubpath that escapes configHome (path traversal / NUL byte) at plan-build time on BOTH the install and uninstall plan paths; surface.applySurface and installOpencodeFamilySkills route through it, and _copyStaged carries a defense-in-depth containment check. Security-load-bearing for the Phase C third-party-descriptor loader.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(#1704): add changeset for destSubpath write-confinement

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(#1704): fix windows path-portability in confinement test

The N1 'accepts a true child subpath' assertion compared against path.join (no drive resolution) while the helper uses path.resolve — on Windows that mismatches the C: drive prefix. Compute the expected via path.resolve to mirror the helper. Windows-CI-only failure (local gsd-test is Mac+Linux).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 13:20:25 -04:00
Joe Slitzker
d9c8df04d7 Merge remote-tracking branch 'origin/next' into fix/1477-surface-source-marker
# Conflicts:
#	bin/install.js
2026-06-25 09:48:15 -05:00
Tom Boucher
466a2f2866 refactor(#1675): dedup augment converter family — single-source from conversion module (#1685)
bin/install.js held byte-identical duplicate definitions of the augment
converter family (convertSlashCommandsToAugmentSkillMentions,
convertClaudeToAugmentMarkdown, getAugmentSkillAdapterHeader,
convertClaudeCommandToAugmentSkill, convertClaudeAgentToAugmentAgent) that
already exist canonically in src/runtime-artifact-conversion.cts (generated
to gsd-core/bin/lib/runtime-artifact-conversion.cjs). Deferred Phase 1->2
cleanup tracked in #1675 (epic #1507 / ADR-1508).

Deleted the five local copies; install.js now binds the three PUBLIC
converters from runtimeArtifactConversion (same pattern as getDirName /
processAttribution in #1510). The two private helpers live only in the
conversion module now. module.exports preserved (re-exported).

Behavior-preserving: four converters byte-identical; the fifth
(convertClaudeAgentToAugmentAgent) differed only by an inert let->const
(variable never reassigned). Extends the DEFECT.GENERATIVE-FIX
reference-identity parity guard in enh-1511 to assert single-sourcing.

Closes #1675
2026-06-24 21:34:53 -04:00
Andreas Brauchli
cbf7c82841 feat(#323): fish-shell support in post-install PATH suggestion (#727)
* feat(#323): fish-shell support in post-install PATH suggestion

Two additive changes to the post-install PATH-suggestion seam, both scoped
to existing functions.

A. Projection: add a fish entry to the persist-mode shell-action list in
   projectPathActionProjection() (src/shell-command-projection.cts). fish has
   no `export`/`$PATH`-list syntax, so the existing zsh/bash `export PATH=...`
   commands are inert when pasted. The new entry emits the fish-native
   `fish_add_path '<dir>'` (fish 3.2+, persists via the universal-variable
   store, de-duplicating). The directory is single-quoted with the same POSIX
   literal escaping as the zsh/bash siblings; verified round-tripping through
   real fish 3.7.0 for paths containing quotes, spaces, `$`, `*`, backticks
   and unicode.

B. Detection: add homePathCoveredByFishConfig() in bin/install.js, called
   from maybeSuggestPathExport() alongside homePathCoveredByRc(). fish does
   not use sh-style `export PATH=` rc files, so a fish user whose
   fish_user_paths already covers the global bin would otherwise get a
   false-positive "not on your PATH" warning on every install. Two
   side-effect-free detection routes (no fish subprocess):

   1. The universal-variable store (~/.config/fish/fish_variables). fish
      serializes this with `full_escape`: every byte outside [A-Za-z0-9/_]
      becomes `\xHH` (space -> \x20, `-` -> \x2d, `.` -> \x2e, `$` -> \x24,
      unicode -> \uXXXX) and list elements are joined by the literal 4-char
      token `\x1e` (NOT a raw 0x1e byte). The detector splits on `\x1e`,
      decodes the escapes, then compares each as an absolute literal — a
      decoded `$` is part of the directory name, not an unexpanded variable.
      Verified against real fish 3.7.0 output.
   2. config.fish (`fish_add_path`, `set -gx PATH`, `set -Ux fish_user_paths`)
      — plain shell tokens: HOME forms ($HOME/${HOME}/~) are expanded and a
      token still holding `$` (e.g. `$PATH`, `$fish_user_paths`) is skipped.

   Honours $XDG_CONFIG_HOME and always also checks ~/.config/fish.

No behaviour change for bash/zsh/PowerShell/cmd/Git-Bash users: their entries
and command strings are unchanged; the fish entry is additive and the fish
detector only narrows the set of cases that warn.

Tests: update the projection length assertion (2 -> 3) and fish escaping in
bug-3441; add fish detection + suppression cases in install-path-detection
(uvar store with real fish escaping, dot/hyphen/space/$-literal decode
regressions, config.fish routes, commented-out, relative-segment guard,
unreadable-file fault injection, suppression and emission via
maybeSuggestPathExport).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(changeset): add Changed fragment for #323 fish PATH support (#727)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#323): address review — action-only fish docs, decoder property test, win32 guard

Addresses @trek-e's review on #727:

- docs (blocker): keep the how-to action-only (Diátaxis). Drop the
  `# fish — persists via …` comment and the internal-mechanism clause
  naming fish_variables/config.fish; leave one command + the exec-fish
  directive.
- tests (minor): extract decodeFishUniversalValue to a pure, exported
  module function and add fast-check round-trip properties
  (decode(fishEscape(p)) === p over arbitrary unicode, abs-path variant,
  totality). Consolidated into install-path-detection.test.cjs to respect
  the install test-file-count ratchet.
- tests (follow-up): port #721's win32 negative-projection test (no fish
  action on win32; persist projection is PowerShell/cmd.exe/Git Bash).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#323): address review — drop unused 'after' import, clarify escaping comment

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
2026-06-24 17:20:52 -04:00
Tom Boucher
752df8adb4 fix(#1657): recover malformed (non-object) ~/.gsd/defaults.json in finishInstall (#1661)
* fix(#1657): recover malformed (non-object) ~/.gsd/defaults.json in finishInstall

JSON.parse of defaults.json succeeds for valid-JSON-but-non-object values (null, [],
42, "str"), which then bypassed the parse catch: null threw a TypeError on property
access (swallowed by the outer try/catch), and array/number/string had resolve_model_ids
set on a non-object whose JSON.stringify round-trip kept the broken shape. The non-Claude
finishInstall step now resets any non-object (null, non-object, or array) parse result to
{} before reading/writing, so the file is repaired and resolve_model_ids defaults normally.
Regression folded into the owning tests/bug-410-install-defaults-test-mode-guard.test.cjs
(parameterized over null/[]/42/"str").

* chore(#1657): backfill changeset pr ref to 1661
2026-06-24 13:48:16 -04:00
Tom Boucher
4631923982 fix(#1569): preserve explicit resolve_model_ids in non-Claude installs (#1653)
* fix(#1569): preserve explicit resolve_model_ids in non-Claude installs

The non-Claude finishInstall step keyed its resolve_model_ids:"omit" write on
!== "omit", so an explicit true opt-in (resolveModelInternal returns full model
IDs) was silently clobbered on every install/upgrade across all 14 non-Claude
runtimes, making generated agent manifests inherit the active chat model instead
of pinning the resolved model. Now only absent/falsy is defaulted to "omit"; an
explicit true (and an existing "omit") is preserved. Regression test
parameterizes across codex/opencode/gemini and covers the absent/false/idempotent/
claude/malformed boundaries.

* chore(#1569): backfill changeset pr ref to 1653

* fix(#1569): default non-canonical resolve_model_ids values to omit (codex review)

Adversarial review (codex, gpt-5.5/high) flagged that the original allowlist-by-
enumeration condition (undefined/null/false -> omit) preserved malformed values
(0, "", "yes", {}) instead of defaulting them to omit, letting them leak Claude
aliases a non-Claude runtime cannot resolve. Switch to an allowlist condition
(existing !== true && existing !== 'omit') so only an explicit canonical true
opt-in and an existing omit are preserved; everything else defaults to the safe
non-Claude omit. Adds a parameterized test over [0, "", "yes", {}].
2026-06-24 13:21:19 -04:00