fix(#4132): verify durable runtime surface sources (#4182)

* fix(#4132): verify durable runtime surface sources

* chore(#4132): record PR number in changeset

* test(#4132): cover rejected commands source alias

* fix(#4132): reject aliased package fallback

* test(#4132): cover rejected agents source alias

* test(#4132): cover partially aliased marker provider

* fix(#4132): reject partially aliased source providers

* test(#4132): cover routed source identity probes

* fix(#4132): route installed source identity probes

* refactor(#4132): tighten installer source metadata

* test(#4132): cover corpus trust boundary attacks

* fix(#4132): close installed corpus trust gaps

* refactor(#4132): keep installer authority private

* fix(#4132): preserve private installer fallback

* test(#4132): preserve fixture source authority

* fix(#4132): reject overlapping source fallback

* fix(#4132): avoid redundant installed corpus reads

* refactor(#4132): simplify provider resolution

* test(#4132): sync install tree fixtures after rebase

---------

Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
This commit is contained in:
Zy Deng
2026-09-05 21:32:52 +02:00
committed by GitHub
parent 1017898cb9
commit 4c60879b5d
49 changed files with 3956 additions and 307 deletions

View File

@@ -0,0 +1,7 @@
---
type: Fixed
pr: 4182
---
**Global Runtime Surface materialization no longer breaks after the installing package disappears** — source-dependent global installs provision manifest-owned raw command and agent corpora below `gsd-core/`, while agents-only and empty layouts receive only what their descriptors require. Source selection uses one complete provider for the whole layout, retains the complete legacy marker path, rejects corpora observed during provider selection as missing, hash-mismatched, symlink-escaped, or unexpectedly extended, and preserves local-install behavior.
Surface materialization now stages every artifact kind before mutation and publishes the candidate surface state last. A source or staging failure therefore leaves the prior state and artifacts untouched. Fresh and upgraded Codex/Claude installs can materially change a surface using only deployed modules and the installed corpus, while an unmigrated source-less deployment fails with an install/upgrade diagnostic before changing state or artifacts.

View File

@@ -301,7 +301,7 @@ Single seam owning GSD's published-package coordinates so a repoint/rename is a
Module owning install detection for `/gsd:update`. `resolveUpdateContext({ home, cwd, env, fs, preferredConfigDir, preferredRuntime })` is a pure, injected-fs port of update.md's former ~280-line `get_installed_version` bash; it reproduces the full precedence cascade — preferred-config-dir fast path, local-over-global probe with same-path dedup, env-var overrides (`CLAUDE_CONFIG_DIR`, `OPENCODE_CONFIG`, `KILO_CONFIG`, `XDG_CONFIG_HOME`, `CODEX_HOME`, …), and semver validation — and returns the 4-field contract `{ installedVersion, scope, runtime, gsdDir }` (scope ∈ `LOCAL`/`GLOBAL`/`UNKNOWN`). Antigravity is modelled first-class (its `.gemini/antigravity{,-ide,-cli}` dirs probe before bare `.gemini`; #3608). Exposed to the workflow as `gsd-tools update-context [--config-dir <d>] [--runtime <r>] --json`; `loadUpdateContext` wires the real fs. The workflow keeps only the execution_context path → `PREFERRED_*` derivation (the one input it alone knows). Source: `gsd-core/bin/lib/update-context.cjs`; tests: `tests/update-context.test.cjs`. See Installer Module and Package Identity Module.
### Skill Surface Budget Module
Module owning which skills and agents are written to runtime config directories at install time (Phase 1) and at runtime via cluster-level toggles (Phase 2). Phase 1: `gsd-core/bin/lib/install-profiles.cjs` defines named profiles (`core`, `standard`, `full`), computes transitive closure over `requires:` frontmatter, stages skills/agents to runtime config dirs, and persists the chosen profile in a `.gsd-profile` marker. Profile resolution precedence: explicit `--profile=` flag > `.gsd-profile` marker > `full`. `--minimal`/`--core-only` are back-compat aliases for `--profile=core`. Phase 2: `gsd-core/bin/lib/surface.cjs` implements the `/gsd:surface` slash command for cluster-level enable/disable without reinstall; cluster definitions live in `gsd-core/bin/lib/clusters.cjs`; per-runtime state persists in `<runtimeConfigDir>/.gsd-surface.json` independent from the `.gsd-profile` marker. See ADR-0011.
Module owning which skills and agents are written to runtime config directories at install time (Phase 1) and at runtime via cluster-level toggles (Phase 2). Phase 1: `gsd-core/bin/lib/install-profiles.cjs` defines named profiles (`core`, `standard`, `full`), computes transitive closure over `requires:` frontmatter, stages skills/agents to runtime config dirs, and persists the chosen profile in a `.gsd-profile` marker. Profile resolution precedence: explicit `--profile=` flag > `.gsd-profile` marker > `full`. `--minimal`/`--core-only` are back-compat aliases for `--profile=core`. Phase 2: `gsd-core/bin/lib/surface.cjs` implements the `/gsd:surface` slash command for cluster-level enable/disable without reinstall; cluster definitions live in `gsd-core/bin/lib/clusters.cjs`; per-runtime state persists in `<runtimeConfigDir>/.gsd-surface.json` independent from the `.gsd-profile` marker. For a materializing mutation, `applySurface` accepts the candidate state in memory, stages every artifact kind before mutation, synchronizes the artifacts, and publishes or removes `.gsd-surface.json` last. A staging failure therefore leaves the prior artifacts and state untouched. See ADR-0011.
### Context Composer Module
@@ -318,6 +318,8 @@ Pure, no-I/O `when=` evaluator over `InvocationFacts`, mapping a document-order
### Runtime Artifact Layout Module
Module owning the per-runtime mapping from artifact kind to filesystem placement. ADR-3660 defines the typed `kinds` per runtime (`commands`, `agents`, `skills`) with destination subpath, prefix, and stage adapter (with per-runtime converters in `bin/install.js`: `convertClaudeCommandToClaudeSkill`, `…CodexSkill`, `…CopilotSkill`, `…AntigravitySkill`). Owns the per-runtime `nested` skill-bundle decision (#69): a `skillsKind` flag in `src/runtime-artifact-layout.cts` drives whether a runtime receives the nested router layout (6 `gsd-ns-*` routers + concrete skills under `<router>/skills/<name>/`) or the flat `skills/gsd-<stem>/` layout; the evidence/doc-link matrix is recorded in a comment above `resolveRuntimeArtifactLayout`. Phase 1 applies this seam to the Runtime Surface Module (`surface.cjs:applySurface`); as of #813, `applySurface` applies the same per-runtime skill-body path rewrites as `installRuntimeArtifacts` for `skills` kinds — re-surfacing no longer overwrites installed SKILL.md bodies with converter-default `~/.claude` paths. Per ADR-1508 / #1511 the former `getInstallExports`/`loadInstallExports` relay (a `GSD_TEST_MODE`-guarded `require('bin/install.js')` by which `surface.cjs` reached `computePathPrefix`/`applyRuntimeContentRewritesInPlace`) was DELETED from this module; content rewriting now lives in the Runtime Artifact Conversion Module and `surface.cjs:applySurface` calls its `rewriteStagedSkillBodies` directly. The resolved `scope` is still carried on the `Layout` object so `applySurface` derives the same `pathPrefix` (global `$HOME` form vs. absolute) as a fresh install. Phase 2 is planned to migrate install/uninstall in `bin/install.js` so all lifecycle sites iterate one shared layout table instead of re-encoding runtime layout logic. This design is intended to remove the #3659 class of omissions. Migrations remain under the Installer Migration Module (ADR-0008). The `.gsd-source` marker (#1477) is a two-party provisioning contract that lets source resolution succeed on the Claude global skills layout, which ships `gsd-core/{bin,contexts,references,templates,workflows}` but no `commands/gsd` source tree for `findInstallSourceRoot` to walk up to: the writer is `bin/install.js`, which writes `<configDir>/.gsd-source` (content: the absolute path to its own `commands/gsd`, terminated by a newline) when `runtime === 'claude' && isGlobal`, guarded by `fs.existsSync` so a half-published package never writes a dangling marker; the reader is `findInstallSourceRoot(configDir)`, which prefers the marker over its walk-up but falls through to the walk-up if the marker is absent, dangling, or empty/whitespace-only. #2871 Phase 2 widens the Module from placement to placement **+** trigger resolution: `resolveTriggerSurface(runtime, scopes, { stems, routerStems?, childToRouters?, registry? }) -> TriggerSurface[]` answers "what does a user type" rather than "where does a file land" — a new, pure function alongside `resolveRuntimeArtifactLayout` (untouched, still 7 callers), never a widened signature. Only `commands` and `skills` are trigger-bearing; `agents`/`kimi-agents` are excluded entirely — an agent is invoked through the Agent/Task tool's `subagent_type`, a separate dispatch interface point, never a `/gsd-<name>` a user types (ADR-2866 amendment below). Each `TriggerSurface` names its `trigger`, `kind`, `scope`, `destPath` (computed through the SAME `namespacedByDir` branch `_copyStaged` uses), `registration` (`'direct'` | `'via-router'`, the latter naming the owning router's `routerTrigger` for a nested-router runtime's concrete child skill — #69), and `shadowedBy` (the winning sibling entry, or `null`). The winner across scopes and kinds is decided by scope rank first (Install Scope Module's `scopeRank`, consumed not re-derived — global outranks local) then by the runtime's new `runtime.triggerPrecedence` descriptor axis (ordered kind names, highest priority first; default `['skills', 'commands']`, required-with-default so a pre-#2871 `capability.json` keeps validating). `shadowedBy` ships unread this phase — Phase 4 (#2873) is its first consumer. See ADR-3660.
Issue #4132 keeps `resolveRuntimeArtifactLayout` as the single no-I/O placement resolver. Ordinary kind stage closures lazily select one complete provider shared by every source class the layout requires. Global installs provision canonical raw commands under `gsd-core/commands/gsd` and agents under `gsd-core/agents`, with manifest-owned refresh/removal; installer staging first uses that normal installed/marker selection and privately retries against the executing package only when source resolution fails before creating any staged output. Deployed Runtime Surface staging prefers the complete installation-owned corpus, then a complete independent legacy `.gsd-source` compatibility provider, and otherwise fails closed with an install/upgrade diagnostic. Provider independence and installed-corpus integrity are synchronous admission checks over the filesystem state observed during provider selection; same-user concurrent mutation after resolution remains outside this issue's threat model. No layout may mix commands and agents from different providers.
### Runtime Artifact Conversion Module
Sibling Module to Runtime Artifact Layout Module. Owns projection from canonical Claude-authored command/agent/skill markdown into runtime-specific artifact bodies, including converter selection, frontmatter/body normalization, runtime path rewrites, and staged artifact generation. Runtime Artifact Layout remains responsible for filesystem placement (`kind`, destination subpath, prefix, nesting); Runtime Artifact Conversion owns the content Implementation behind that placement seam so install, uninstall/surface parity, and future plugin/package projections stop reaching back through `bin/install.js` for converter functions or `GSD_TEST_MODE`-guarded installer exports. Chosen direction: sibling Module, not an expanded Layout Module, to preserve ADR-3660's narrow placement responsibility while deepening artifact content locality. First slice: relocate only the layout-reached conversion family (`convertClaudeCommandTo*Skill`, converted command-file emitters, `buildKimiAgentArtifacts`) plus the minimal helper closure they need; do not leave helper dependencies in `bin/install.js` because that would preserve the same shallow seam under a new filename. Installer integration decision: `bin/install.js` imports the conversion Module at top level and re-exports the moved names for compatibility; the conversion Module must not import `bin/install.js` or Runtime Artifact Layout, so the dependency direction becomes installer/layout Adapters -> conversion Module, never conversion -> installer. First-slice Interface decision: export the existing compatibility names only; do not introduce a grouped `convertRuntimeArtifact` Interface until after relocation proves byte-for-byte behavior. SHIPPED (ADR-1508): the converter family relocated in #1510 Phase 1 (`getDirName`→runtime-name-policy, `processAttribution` here); #1511 Phase 2 moved the content-rewrite engine here in full — `_applyRuntimeRewrites` (per-runtime switch, injected attribution), the staged-content walkers `applyRuntimeContentRewritesInPlace`/`applyRuntimeContentRewritesForCommandsInPlace`, `computePathPrefix` (private; `_computePathPrefix` for tests), and the deep public seam `rewriteStagedSkillBodies`/`rewriteStagedCommandBodies({runtime,configDir,scope,homedir?,platform?,resolveAttribution?})`. `bin/install.js` binds these back (single owner, exports preserved); `getCommitAttribution` stays in `bin/install.js` (impure install-time config I/O) and is injected. The `getInstallExports` relay in Runtime Artifact Layout Module was deleted; the dependency direction installer/layout → conversion (never upward) is now enforced. Exception: opencode and kilo path-prefix rewriting is a deliberate `bin/install.js`-owned pre-conversion step (`applyOpencodeFamilyPathPrefix`) per #784, not a violation of the single-owner rule. Source: `gsd-core/bin/lib/runtime-artifact-conversion.cjs`. Also exports `resolveVersionFrom(libDir)` — a lazy, defensive GSD-version resolver (installed-tree `gsd-core/VERSION` first, then the source/npm `package.json` three dirs up, both validated against the repo's shared semver-prefix shape, degrading to `''` on failure) that replaced a module-load-time `require('../../../package.json')` which crashed on runtimes whose root carries no `package.json` (e.g. Codex) (#1383). #4032 added `appendAgentTools` as step 3 of the ADR-1235 pre-converter pipeline (`stageAgentsForRuntimeWithConverter`, `src/install-profiles.cts` — not this module): appends `readGsdEffectiveAgentTools` grants (Install Model Override Resolver Module) into the canonical agent's `tools:` frontmatter — line-surgical, both inline-comma and YAML block-list forms — before the runtime-specific converter runs, so every converter (including Kilo's `mcp__*`→`{server}_{tool}` permission mapping and ZCode's `mcp__*` omission policy) sees configured grants without a duplicated per-runtime write path.
@@ -407,7 +409,7 @@ A named, stable site on a host loop step (per-step `pre`/`post` plus per-wave in
ADR-857 phase 4b/6 unified resolver that composes the three toggle systems (install profile, runtime surface, config activation) into one per-capability view consumed by workflow hook rendering. Source of truth: `gsd-core/bin/lib/capability-state.cjs` (generated from `src/capability-state.cts`). Interface: `resolveCapabilityState({ registry, installedSkills, surfacedSkills, config, cwd? }) → { capabilities: CapabilityStateEntry[] }` (pure, no I/O); `resolveCapabilityRuntimeState(cwd, runtimeConfigDir)` (I/O resolver shared by workflow dispatch and diagnostics — returns `{ runtimeConfigDir, warnings, capabilities }` only; `registry` and `config` are NOT returned — callers that need them import `capability-registry.cjs` and call `loadConfig(cwd)` directly); `cmdCapabilityState(cwd, runtimeConfigDir, raw, opts)` (I/O output entry point); `isCapabilityActive(capId, cwd): boolean` (convenience predicate — calls `resolveCapabilityRuntimeState`, finds the entry, returns `entry.active`; `false` when capability not found). CLI surface: `gsd-tools capability state [--config-dir <path>]` — emits `{ runtimeConfigDir, capabilities[] }`. Per-capability output: `{ id, tier, skills[], installed, surfaced, enabled, active, hooks[] }` where `installed` = every owned skill ∈ installedSkills (or `installedSkills==='*'`; vacuously true for empty-skills caps), `surfaced` = every owned skill ∈ surfacedSkills (vacuously true for empty-skills caps), `enabled = installed && surfaced` (unchanged — install+surface toggle only), `active = enabled && configActivation` (tri-state deepening: configActivation resolves the capability's `activationKey` via `_resolveActivationValue`; absent `activationKey` → configActivation=true, so `active===enabled` for ungated capabilities), and `hooks` = `[{ point, kind: 'step'|'gate'|'contribution', when, configured, active }]` derived from the cap's `steps`, `gates`, `contributions` arrays (`configured` resolves `when`; `active = enabled && configured`). Capabilities sorted by `id` for determinism. Defensive: malformed registry → `{ capabilities: [] }`, never throws; inline literal `__proto__`/`constructor`/`prototype` prototype-pollution guard on capability id keys.
### Capability State Writer
The write-side mirror of the Capability State Resolver. Takes a desired capability state — per-capability `enabled` plus per-hook `gates` — and projects it onto the substrates: `enabled` drives the runtime surface (`.gsd-surface.json`) as the capability on/off switch; `gates` drive the federated config keys (`config.json` `workflow.*`) for hook-level granularity; the install profile (`.gsd-profile`) is a read-only floor it never writes. Writes the surface once and config once (atomic per substrate), then re-runs the resolver and reports divergence (assert-and-report) — so 'off means off' holds as a write-time invariant rather than by caller discipline. Source of truth: `src/capability-writer.cts`; the surface and config writers become its internal adapters.
The write-side mirror of the Capability State Resolver. Takes a desired capability state — per-capability `enabled` plus per-hook `gates` — and projects it onto the substrates: `enabled` drives the runtime surface (`.gsd-surface.json`) as the capability on/off switch; `gates` drive the federated config keys (`config.json` `workflow.*`) for hook-level granularity; the install profile (`.gsd-profile`) is a read-only floor it never writes. State-only and gate-only calls retain one atomic write per substrate. For a materializing request, it passes the in-memory surface candidate to `applySurface`; that writer publishes state only after artifact staging and synchronization succeed. It then re-runs the resolver and reports divergence (assert-and-report), so 'off means off' holds as a write-time invariant rather than by caller discipline. Source of truth: `src/capability-writer.cts`; the surface and config writers are its internal adapters.
### Capability Command Family [Planned — mechanism built, unconsumed]
ADR-959 (phase 4d) — a CLI command family (a top-level `gsd-tools` command and its subcommands) owned by a Capability via a new optional `commands: [{ family, module, router }]` field on the `feature` role. The Capability declares the `family` name, a first-party in-tree `module` (under `gsd-core/bin/lib/`), and the exported `router` — a standard `route*Command({ args, cwd, raw, error })` function identical in shape to the 12 existing host routers (so it routes through the stateless CommandRoutingHub via `routeCjsCommandFamily`, owning its own subcommand list and arg parsing). The registry materializes a `commandFamilies` index (`family → { capId, module, router }`); the formerly-dead `_dispatchNonFamily` shim is replaced by a real `dispatchCapabilityCommand` (exported from `gsd-core/bin/gsd-tools.cjs`) consulted in `runCommand`'s **`default` case** — an unmigrated command hits its hardcoded `case`; a migrated command's `case` is removed so it reaches `default` → registry → router, making collision structurally impossible. The registry *discovers* a router (it does not rebuild a handler table). First-party only; third-party command loading deferred. **Mechanism built (4d-impl-1):** `commands` schema + validator + single-family-ownership cross-check in `gen-capability-registry.cjs`; `commandFamilies` index emitted in the generated `capability-registry.cjs` (currently `{}` — no capability declares commands yet); `dispatchCapabilityCommand` wired into `runCommand`'s `default` case (behavior-preserving today). **Pilot complete (4d-impl-2):** `graphify` cut over as the first real capability command family — `capabilities/graphify/capability.json` bundles the command (`family: graphify`, `module: graphify-command-router.cjs`, `router: routeGraphifyCommand`), skill (`graphify`), config gate (`graphify.enabled`), and `tier: full`; the `case 'graphify':` arm removed from `gsd-tools.cjs`; dispatch flows `default → dispatchCapabilityCommand → commandFamilies.graphify → graphify-command-router.cjs → routeGraphifyCommand`; behavior proven equivalent (all subcommands: build, query, status, diff, build snapshot, unknown subcommand error, usage error, disabled gate). Template for phase-6 per-feature cutovers. **Audit cutover (4d-impl-3):** `audit-uat` and `audit-open` cut over as the second capability command family pair — `capabilities/audit/capability.json` declares two commands (`family: audit-uat`, `module: audit-command-router.cjs`, `router: routeAuditUat`) and (`family: audit-open`, `module: audit-command-router.cjs`, `router: routeAuditOpen`); the `case 'audit-uat':` and `case 'audit-open':` arms removed from `gsd-tools.cjs`; `commandFamilies` now holds `audit-uat`, `audit-open`, and `graphify`; dispatch flows `default → dispatchCapabilityCommand → commandFamilies["audit-uat"|"audit-open"] → audit-command-router.cjs → routeAuditUat|routeAuditOpen`; behavior equivalence proven by existing regression tests (bug-2659, bug-2911, uat.test.cjs) plus new cutover tests. Confirms hyphenated family names pass registry validator (no format restriction beyond non-empty + non-reserved). **Intel cutover (4d-impl-4, last first-party cutover):** `intel` cut over — `capabilities/intel/capability.json` declares the command (`family: intel`, `module: intel-command-router.cjs`, `router: routeIntelCommand`) and the existing config gate (`intel.enabled`, default false); the `case 'intel':` arm removed from `gsd-tools.cjs`; `commandFamilies` now holds `intel`, `audit-uat`, `audit-open`, and `graphify`; dispatch flows `default → dispatchCapabilityCommand → commandFamilies.intel → intel-command-router.cjs → routeIntelCommand`; all 9 subcommands (query, status, update, diff, snapshot, patch-meta, validate, extract-exports, api-surface) and both usage-error paths preserved; non-raw `timeAgo` transform on `status.files[*].updated_at` preserved exactly. `intel.enabled` is Capability-owned config after ADR-857 phase 6. Completes the initial 4d capability command cutover batch.

View File

@@ -815,6 +815,10 @@ const {
const {
resolveRuntimeArtifactLayout,
} = require(path.join(_gsdLibDir, 'runtime-artifact-layout.cjs'));
const {
readSurface,
resolveSurface,
} = require(path.join(_gsdLibDir, 'surface.cjs'));
const {
assertDestWithinConfigHome,
createRuntimeArtifactInstallPlan,
@@ -10620,7 +10624,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
// one) is honored on every profile, `full` included (#2322 blocker 2).
const _commandsDir = path.join(src, 'commands', 'gsd');
const _skillsManifest = _isCoreProfileAlias ? new Map() : loadSkillsManifest(_commandsDir);
const _resolvedProfile = resolveProfile({
let _resolvedProfile = resolveProfile({
modes: [_activeProfileName],
manifest: _skillsManifest,
registry: _installedCapabilityRegistry,
@@ -10971,6 +10975,28 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
return Array.isArray(scopeLayout) && scopeLayout.length > 0;
})();
// Install the distribution-owned gsd-core tree before layout materialization.
// installRuntimeArtifacts then provisions the durable Runtime Surface corpus
// exactly once into this final tree instead of having that corpus overwritten
// by a later whole-tree copy and needing a second provisioning pass.
const skillSrc = path.join(src, 'gsd-core');
const skillDest = path.join(targetDir, 'gsd-core');
const _gsdArtifactsStagingRoot = _tryResolveUserArtifactStagingRoot(targetDir);
if (_gsdArtifactsStagingRoot === null) {
console.warn(` ${yellow}!${reset} Skipping gsd-core/${USER_OWNED_ARTIFACTS.join(', gsd-core/')} preservation (staging unavailable) — it will be lost if present.`);
copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime, false, isGlobal, targetDir);
} else {
const stagedGsdArtifacts = stageUserArtifacts(skillDest, USER_OWNED_ARTIFACTS, _gsdArtifactsStagingRoot);
copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime, false, isGlobal, targetDir);
restoreStagedUserArtifacts(skillDest, stagedGsdArtifacts);
discardStagedUserArtifacts(stagedGsdArtifacts);
}
if (verifyInstalled(skillDest, 'gsd-core')) {
console.log(` ${green}✓${reset} Installed workflow assets`);
} else {
failures.push('gsd-core');
}
// #2624: write the .gsd-source marker. Extracted from its former late position so it can be
// called BEFORE staging reads the marker (see the call site below). Scoped to the Claude-global
// layout (issue #1477) — the only install path that ships the skills layout without a
@@ -10987,6 +11013,9 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
// ADR-1239 Phase B write-confinement: the descriptor-sourced marker filename
// must resolve under targetDir (parity with the other descriptor-driven writes).
const _markerPath = assertDestWithinConfigHome(targetDir, _hostBehaviors(runtime).sourceMarkerFile);
if (hasExistingSymlinkBetween(path.resolve(targetDir), _markerPath, { allowOptInFollow: isSymlinkedDestOptIn() })) {
throw new Error(`compatibility marker "${_markerPath}" contains an untrusted symlink`);
}
fs.writeFileSync(_markerPath, gsdSourceCommands + '\n', 'utf8');
} catch (err) {
// Non-fatal: install proceeds. But on the Claude-global layout walk-up
@@ -11013,6 +11042,19 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
if (_isSkillsRuntime) {
// Layout-driven install for skills-based runtimes (full and minimal modes)
const scope = _installScopeId;
// Preserve an existing Runtime Surface selection during upgrades. The
// installer refreshes the source corpus first, then emits exactly the
// already-committed selection instead of widening it to the base profile.
const _committedSurface = readSurface(targetDir);
if (_committedSurface) {
_resolvedProfile = resolveSurface(
targetDir,
loadSkillsManifest(_commandsDir),
undefined,
_installedCapabilityRegistry,
_committedSurface,
);
}
// ADR-1239 upgrade 3 / #2088: a kind may declare an alternate install `home`
// (e.g. Codex skills -> $HOME/.agents/skills) instead of the runtime's normal
// configDir. Resolve the ACTUAL on-disk skills root here, descriptor-driven
@@ -11305,36 +11347,6 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
_installNativePluginIfDeclared(runtime, targetDir, _hostBehaviors(runtime), src);
}
// Copy gsd-core skill with path replacement
// Stage user-generated files DURABLY to disk before the wipe-and-copy so
// they survive re-install even if the process dies mid-copy (#2875 /
// #1874-F19) — copyWithPathReplacement wipes and recursively re-copies the
// entire gsd-core/ tree, the single longest operation in the install, on
// the path every user takes (40-design.md "Site 4 is far worse...").
const skillSrc = path.join(src, 'gsd-core');
const skillDest = path.join(targetDir, 'gsd-core');
// #2875 defect fix: this IS the mainline install step (installing
// gsd-core/ itself) — unlike the optional legacy-cleanup blocks above,
// install must still be able to proceed and actually write gsd-core/ even
// when the staging root cannot be resolved. Degrade by skipping ONLY the
// USER_OWNED_ARTIFACTS preserve/restore wrapper around the copy (warn),
// never the copy itself.
const _gsdArtifactsStagingRoot = _tryResolveUserArtifactStagingRoot(targetDir);
if (_gsdArtifactsStagingRoot === null) {
console.warn(` ${yellow}!${reset} Skipping gsd-core/${USER_OWNED_ARTIFACTS.join(', gsd-core/')} preservation (staging unavailable) — it will be lost if present.`);
copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime, false, isGlobal, targetDir);
} else {
const stagedGsdArtifacts = stageUserArtifacts(skillDest, USER_OWNED_ARTIFACTS, _gsdArtifactsStagingRoot);
copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime, false, isGlobal, targetDir);
restoreStagedUserArtifacts(skillDest, stagedGsdArtifacts);
discardStagedUserArtifacts(stagedGsdArtifacts);
}
if (verifyInstalled(skillDest, 'gsd-core')) {
console.log(` ${green}✓${reset} Installed workflow assets`);
} else {
failures.push('gsd-core');
}
// #2624: the .gsd-source marker is now written by _writeGsdSourceMarker()
// BEFORE staging reads it (see the early call above the _isSkillsRuntime
// block). The former write lived here — AFTER staging — which on an upgrade

View File

@@ -64,16 +64,25 @@ Install profile: standard (from .gsd-profile)
---
## Mutation protocol
Derive the next `surfaceState` in memory and pass it to `applySurface` as
`opts.surfaceState`. Do not call `writeSurface` first: `applySurface` stages all
artifact kinds before mutation and publishes the candidate state only after
materialization succeeds. Pass `null` to reset to the install-time profile.
---
## profile \<name\>
1. Read current surface: `readSurface(runtimeConfigDir)` → if null, seed from `readActiveProfile(runtimeConfigDir)`.
2. Set `surfaceState.baseProfile = name`.
3. `writeSurface(runtimeConfigDir, surfaceState)`.
3. Keep the new state in memory; do not write it directly.
4. Resolve and re-apply:
```js
const registry = require(runtimeConfigDir + '/gsd-core/bin/lib/capability-registry.cjs');
const layout = resolveRuntimeArtifactLayout(runtime, runtimeConfigDir, scope);
applySurface(runtimeConfigDir, layout, manifest, CLUSTERS, registry);
applySurface(runtimeConfigDir, layout, manifest, CLUSTERS, registry, { surfaceState });
```
5. Confirm: "Surface updated to profile `<name>`. N skills enabled."
@@ -87,11 +96,11 @@ Valid cluster names: `core_loop`, `audit_review`, `milestone`, `research_ideate`
1. Validate cluster name against `Object.keys(CLUSTERS)`.
2. Read or initialize surface state.
3. Add cluster to `surfaceState.disabledClusters` (deduplicate).
4. `writeSurface` → resolve layout → `applySurface`:
4. Resolve layout and apply the in-memory candidate:
```js
const registry = require(runtimeConfigDir + '/gsd-core/bin/lib/capability-registry.cjs');
const layout = resolveRuntimeArtifactLayout(runtime, runtimeConfigDir, scope);
applySurface(runtimeConfigDir, layout, manifest, CLUSTERS, registry);
applySurface(runtimeConfigDir, layout, manifest, CLUSTERS, registry, { surfaceState });
```
5. Confirm: "Disabled cluster `<cluster>`. N skills removed from surface."
@@ -101,11 +110,11 @@ Valid cluster names: `core_loop`, `audit_review`, `milestone`, `research_ideate`
1. Read surface state; if null, nothing to enable — print "No surface delta active."
2. Remove cluster from `surfaceState.disabledClusters`.
3. `writeSurface` → resolve layout → `applySurface`:
3. Resolve layout and apply the in-memory candidate:
```js
const registry = require(runtimeConfigDir + '/gsd-core/bin/lib/capability-registry.cjs');
const layout = resolveRuntimeArtifactLayout(runtime, runtimeConfigDir, scope);
applySurface(runtimeConfigDir, layout, manifest, CLUSTERS, registry);
applySurface(runtimeConfigDir, layout, manifest, CLUSTERS, registry, { surfaceState });
```
4. Confirm: "Enabled cluster `<cluster>`. N skills added back to surface."
@@ -114,8 +123,9 @@ Valid cluster names: `core_loop`, `audit_review`, `milestone`, `research_ideate`
## reset
1. Check if `.gsd-surface.json` exists.
2. Delete it.
3. Re-apply using only `readActiveProfile(runtimeConfigDir)` (install-time profile).
2. Do not delete it directly.
3. Re-apply with `{ surfaceState: null }`; the state file is removed only after
the install-time profile materializes successfully.
4. Confirm: "Surface reset to install-time profile `<name>`."
---

View File

@@ -64,16 +64,25 @@ Install profile: standard (from .gsd-profile)
---
## Mutation protocol
Derive the next `surfaceState` in memory and pass it to `applySurface` as
`opts.surfaceState`. Do not call `writeSurface` first: `applySurface` stages all
artifact kinds before mutation and publishes the candidate state only after
materialization succeeds. Pass `null` to reset to the install-time profile.
---
## profile \<name\>
1. Read current surface: `readSurface(runtimeConfigDir)` → if null, seed from `readActiveProfile(runtimeConfigDir)`.
2. Set `surfaceState.baseProfile = name`.
3. `writeSurface(runtimeConfigDir, surfaceState)`.
3. Keep the new state in memory; do not write it directly.
4. Resolve and re-apply:
```js
const registry = require(runtimeConfigDir + '/gsd-core/bin/lib/capability-registry.cjs');
const layout = resolveRuntimeArtifactLayout(runtime, runtimeConfigDir, scope);
applySurface(runtimeConfigDir, layout, manifest, CLUSTERS, registry);
applySurface(runtimeConfigDir, layout, manifest, CLUSTERS, registry, { surfaceState });
```
5. Confirm: "Surface updated to profile `<name>`. N skills enabled."
@@ -87,11 +96,11 @@ Valid cluster names: `core_loop`, `audit_review`, `milestone`, `research_ideate`
1. Validate cluster name against `Object.keys(CLUSTERS)`.
2. Read or initialize surface state.
3. Add cluster to `surfaceState.disabledClusters` (deduplicate).
4. `writeSurface` → resolve layout → `applySurface`:
4. Resolve layout and apply the in-memory candidate:
```js
const registry = require(runtimeConfigDir + '/gsd-core/bin/lib/capability-registry.cjs');
const layout = resolveRuntimeArtifactLayout(runtime, runtimeConfigDir, scope);
applySurface(runtimeConfigDir, layout, manifest, CLUSTERS, registry);
applySurface(runtimeConfigDir, layout, manifest, CLUSTERS, registry, { surfaceState });
```
5. Confirm: "Disabled cluster `<cluster>`. N skills removed from surface."
@@ -101,11 +110,11 @@ Valid cluster names: `core_loop`, `audit_review`, `milestone`, `research_ideate`
1. Read surface state; if null, nothing to enable — print "No surface delta active."
2. Remove cluster from `surfaceState.disabledClusters`.
3. `writeSurface` → resolve layout → `applySurface`:
3. Resolve layout and apply the in-memory candidate:
```js
const registry = require(runtimeConfigDir + '/gsd-core/bin/lib/capability-registry.cjs');
const layout = resolveRuntimeArtifactLayout(runtime, runtimeConfigDir, scope);
applySurface(runtimeConfigDir, layout, manifest, CLUSTERS, registry);
applySurface(runtimeConfigDir, layout, manifest, CLUSTERS, registry, { surfaceState });
```
4. Confirm: "Enabled cluster `<cluster>`. N skills added back to surface."
@@ -114,8 +123,9 @@ Valid cluster names: `core_loop`, `audit_review`, `milestone`, `research_ideate`
## reset
1. Check if `.gsd-surface.json` exists.
2. Delete it.
3. Re-apply using only `readActiveProfile(runtimeConfigDir)` (install-time profile).
2. Do not delete it directly.
3. Re-apply with `{ surfaceState: null }`; the state file is removed only after
the install-time profile materializes successfully.
4. Confirm: "Surface reset to install-time profile `<name>`."
---

View File

@@ -274,6 +274,8 @@ function setCapabilityState(
// ── Step 4: APPLY PASS ────────────────────────────────────────────────────
let pendingSurfaceForCommit: SurfaceState | undefined;
// ── Surface writes ────────────────────────────────────────────────────────
if (needsSurface && (idsToDisable.length > 0 || idsToEnable.length > 0)) {
const existing = readSurface(resolvedConfigDir);
@@ -339,7 +341,7 @@ function setCapabilityState(
}
if (surfaceChanged) {
writeSurface(resolvedConfigDir, pendingSurface);
pendingSurfaceForCommit = pendingSurface;
}
}
@@ -348,6 +350,13 @@ function setCapabilityState(
setConfigValues(cwd, pendingGateWrites);
}
// State-only callers preserve the existing direct-write behavior. A caller
// that requests materialization lets applySurface publish the candidate only
// after every artifact kind has staged and synchronized successfully.
if (pendingSurfaceForCommit && !opts?.materialize) {
writeSurface(resolvedConfigDir, pendingSurfaceForCommit);
}
// ── Materialize (optional) ────────────────────────────────────────────────
if (opts?.materialize) {
const { runtime, scope } = opts.materialize;
@@ -376,9 +385,12 @@ function setCapabilityState(
// folded:issue-1575-agent-descriptor-parity describe block in
// tests/golden-parity-single-source.test.cjs).
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
applySurface(resolvedConfigDir, layout, manifest, undefined, registry, opts?.materialize?.resolveAttribution
? { resolveAttribution: opts.materialize.resolveAttribution }
: undefined);
applySurface(resolvedConfigDir, layout, manifest, undefined, registry, {
...(pendingSurfaceForCommit ? { surfaceState: pendingSurfaceForCommit } : {}),
...(opts.materialize.resolveAttribution
? { resolveAttribution: opts.materialize.resolveAttribution }
: {}),
});
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
// Fix C: materialise was explicitly requested — a failure is an error (non-zero exit),

View File

@@ -68,6 +68,163 @@ const { getDirName } = runtimeNamePolicy;
type ResolveAttribution = (runtime: string) => any;
type RuntimeSurfaceSourceClass = 'commands' | 'agents';
function withInstallerPackageSource<T>(
configDir: string,
fn: () => T,
): T {
// Reuse the existing compatibility-marker contract through the existing fs
// seam. The marker exists only in this synchronous call tree: no disk state,
// layout export, stage argument, or caller-settable authority flag is added.
const markerPath = path.resolve(configDir, '.gsd-source');
const packageCommandsRoot = runtimeArtifactLayout.findInstallSourceRoot();
const markerBytes = Buffer.from(packageCommandsRoot + '\n');
const base = installFs();
const overlay = {
...base,
existsSync: (candidate: string): boolean =>
path.resolve(candidate) === markerPath || base.existsSync(candidate),
lstatSync: (candidate: string): ReturnType<typeof base.lstatSync> =>
path.resolve(candidate) === markerPath
? { isFile: () => true, isDirectory: () => false, isSymbolicLink: () => false }
: base.lstatSync(candidate),
readFileSync: ((candidate: string, encoding?: BufferEncoding): string | Buffer => {
if (path.resolve(candidate) !== markerPath) {
return encoding ? base.readFileSync(candidate, encoding) : base.readFileSync(candidate);
}
return encoding ? markerBytes.toString(encoding) : Buffer.from(markerBytes);
}) as typeof base.readFileSync,
};
return withInstallFs(overlay, fn);
}
function isRuntimeSurfaceSourceUnavailable(message: string): boolean {
return message.startsWith('Runtime Surface source is unavailable or incomplete for ') &&
message.endsWith('install or upgrade gsd-core before materializing this surface.');
}
function assertCorpusTreeHasNoSymlinks(root: string): void {
if (!installFs().existsSync(root)) return;
const stat = installFs().lstatSync(root);
if (stat.isSymbolicLink()) {
throw new Error(`Runtime Surface corpus path is a symlink: ${root}`);
}
if (!stat.isDirectory()) return;
for (const name of installFs().readdirSync(root)) {
assertCorpusTreeHasNoSymlinks(path.join(root, name));
}
}
function previousOwnedCorpusFiles(configDir: string, prefix: string): string[] {
try {
const files = installerMigrations.readInstallManifest(configDir).files;
return Object.keys(files)
.filter((entry) => entry.startsWith(prefix))
.map((entry) => entry.slice(prefix.length))
.filter((entry) => entry !== '' && !path.posix.isAbsolute(entry) && !entry.split('/').some((part) => part === '' || part === '.' || part === '..'));
} catch {
// An absent or unreadable prior manifest provides no ownership evidence.
// Preserve existing entries rather than guessing that they are stale.
return [];
}
}
function pruneEmptyCorpusParents(start: string, stop: string): void {
let current = path.dirname(start);
while (current !== stop && current.startsWith(stop + path.sep)) {
if (installFs().readdirSync(current).length > 0) return;
installFs().rmdirSync(current);
current = path.dirname(current);
}
}
function syncRuntimeSurfaceCorpus(source: string, destination: string, configDir: string, manifestPrefix: string): void {
if (hasExistingSymlinkBetween(path.resolve(configDir), destination, { allowOptInFollow: isSymlinkedDestOptIn() })) {
throw new Error(
`syncRuntimeSurfaceCorpus: destination "${destination}" contains a symlink the install root "${configDir}" does not trust — refusing to write.`,
);
}
assertCorpusTreeHasNoSymlinks(destination);
// Remove only paths the previous manifest proves GSD owned and which the
// executing package no longer ships. Unknown neighbouring files survive.
for (const relative of previousOwnedCorpusFiles(configDir, manifestPrefix)) {
const sourceEntry = path.join(source, ...relative.split('/'));
let sourceIsFile = false;
try {
sourceIsFile = installFs().lstatSync(sourceEntry).isFile();
} catch {
sourceIsFile = false;
}
if (sourceIsFile) continue;
const target = path.join(destination, ...relative.split('/'));
if (!installFs().existsSync(target)) continue;
const targetStat = installFs().lstatSync(target);
if (!targetStat.isFile()) {
throw new Error(`Runtime Surface corpus ownership conflict at ${target}`);
}
installFs().rmSync(target, { force: true });
pruneEmptyCorpusParents(target, destination);
}
installFs().mkdirSync(path.dirname(destination), { recursive: true });
installFs().cpSync(source, destination, { recursive: true });
}
/**
* Provision the raw, installation-owned input needed to re-materialize a
* global Runtime Surface after the executing package tree disappears.
*
* The corpus deliberately lives below the already-installed `gsd-core/`
* tree and is accepted only after its manifest ownership and hashes verify.
* The compatibility marker does not replace that installed-corpus authority.
*/
function provisionRuntimeSurfaceCorpus(
layout: { runtime: string; kinds: Iterable<{ kind?: string }> },
configDir: string,
scope: string,
): void {
const required = new Set<RuntimeSurfaceSourceClass>();
if (isGlobalScope(scope as InstallScope)) {
for (const kind of layout.kinds) {
if (kind.kind === 'commands' || kind.kind === 'skills') required.add('commands');
if (kind.kind === 'agents' || kind.kind === 'kimi-agents') required.add('agents');
}
}
if (required.size === 0) return;
const corpusRoot = path.join(configDir, 'gsd-core');
if (required.has('commands')) {
const source = runtimeArtifactLayout.findInstallSourceRoot();
const destination = path.join(corpusRoot, 'commands', 'gsd');
syncRuntimeSurfaceCorpus(source, destination, configDir, 'gsd-core/commands/gsd/');
}
if (required.has('agents')) {
const source = path.join(executingPackageRoot(), 'agents');
const destination = path.join(corpusRoot, 'agents');
syncRuntimeSurfaceCorpus(source, destination, configDir, 'gsd-core/agents/');
}
const markerFile = _hostBehaviors(layout.runtime).sourceMarkerFile;
if (typeof markerFile === 'string' && markerFile !== '' && required.has('commands')) {
try {
const markerPath = runtimeArtifactInstallPlan.assertDestWithinConfigHome(configDir, markerFile);
if (hasExistingSymlinkBetween(path.resolve(configDir), markerPath, { allowOptInFollow: isSymlinkedDestOptIn() })) {
throw new Error(`compatibility marker "${markerPath}" contains an untrusted symlink`);
}
installFs().writeFileSync(markerPath, path.join(corpusRoot, 'commands', 'gsd') + '\n', 'utf8');
} catch {
// The existing installer marker writer owns the user-facing warning and
// keeps marker failure non-fatal. The installed corpus remains usable
// without the compatibility marker.
}
}
}
function executingPackageRoot(): string { return path.dirname(path.dirname(runtimeArtifactLayout.findInstallSourceRoot())); }
// ---------------------------------------------------------------------------
// USER_OWNED_ARTIFACTS
// ---------------------------------------------------------------------------
@@ -1042,9 +1199,15 @@ function installRuntimeArtifacts(
resolvedProfile: any,
resolveAttribution: ResolveAttribution = () => undefined,
capabilityRegistry?: any,
deps: { fs?: any; os?: any; env?: Record<string, string | undefined> } = {},
deps: { fs?: any; os?: any; env?: Record<string, string | undefined>; packageRoot?: string } = {},
): any {
return withInstallFs(deps.fs, (): any => {
const layout = runtimeArtifactLayout.resolveRuntimeArtifactLayout(
runtime,
configDir,
scope as 'global' | 'local',
capabilityRegistry,
);
// A removed descriptor kind is no longer visited by the layout loop, so it
// cannot prune its own previous output. Clean manifest-proven retired files
// before materializing the current layout (#2644).
@@ -1064,19 +1227,34 @@ function installRuntimeArtifacts(
// #2874 design row 2: this early return must ALSO return an executed
// plan — installOpencodeFamilyArtifacts reports what it wrote, so a
// whole runtime family returning undefined is no longer a hole.
return installOpencodeFamilyArtifacts(runtime, configDir, scope, resolvedProfile, resolveAttribution, behaviors, capabilityRegistry, projectDir);
// An injected filesystem supplies its own hermetic corpus fixture. The
// real installer is the authority that refreshes package bytes into the
// durable installed corpus; attempting that cross-filesystem copy through
// an in-memory destination adapter would read from the wrong filesystem.
if (!deps.fs) provisionRuntimeSurfaceCorpus(layout, configDir, scope);
return installOpencodeFamilyArtifacts(
runtime,
configDir,
scope,
resolvedProfile,
resolveAttribution,
behaviors,
capabilityRegistry,
deps.packageRoot,
projectDir,
);
}
// Legacy cleanup before layout-driven writes
_runLegacyInstallMigrations(runtime, configDir, scope);
const layout = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, configDir, scope as 'global' | 'local', capabilityRegistry);
if (!deps.fs) provisionRuntimeSurfaceCorpus(layout, configDir, scope);
// #3712: a global `home` override escapes the sandboxed configDir. Refuse to
// execute when a test run would land that escape in the developer's real home.
testHomeGuard.assertTestHomeSandboxed('installRuntimeArtifacts', runtime, layout?.kinds, {
os: deps.os, env: deps.env,
});
const planResult = runtimeArtifactInstallPlan.createRuntimeArtifactInstallPlan({
const createPlan = () => runtimeArtifactInstallPlan.createRuntimeArtifactInstallPlan({
// `Layout` is structurally identical across the layout/install-plan .cjs
// modules but nominally distinct to tsc (untyped .cjs boundary) — bridge it.
layout: layout as any,
@@ -1086,6 +1264,16 @@ function installRuntimeArtifacts(
resolveAttribution,
projectDir,
});
let planResult = createPlan();
if (
scope === 'global' &&
!planResult.ok &&
planResult.kind === 'stage_failed' &&
planResult.cleanupDirs.length === 0 &&
isRuntimeSurfaceSourceUnavailable(planResult.message)
) {
planResult = withInstallerPackageSource(configDir, createPlan);
}
const cleanupDirs = planResult.ok ? planResult.plan.cleanupDirs : planResult.cleanupDirs;
// #2874 row 1/4/5: per-kind executed-plan entries, appended only as the
@@ -1252,8 +1440,10 @@ function installRuntimeArtifacts(
// is safe even when configDir has no .gsd-source marker (artifactLayout: []).
let nativePluginInstalled = false;
if (behaviors.nativePlugin) {
const commandsGsdDir = runtimeArtifactLayout.findInstallSourceRoot(configDir);
const src = path.dirname(path.dirname(commandsGsdDir));
// Native plugin sources live only in the executing package, never in the
// durable Runtime Surface corpus. Do not derive this package root from a
// config-scoped provider that may now correctly resolve installed input.
const src = deps.packageRoot ?? executingPackageRoot();
_installNativePluginIfDeclared(runtime, configDir, behaviors, src);
nativePluginInstalled = true;
}
@@ -1496,8 +1686,11 @@ function installAgentsKindStandalone(
capabilityRegistry?: any,
projectDir?: string | null,
): { sourceDir: string; destDir: string } | null {
const layout: any = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, targetDir, scope as 'global' | 'local', capabilityRegistry);
const agentsKindEntry = layout.kinds.find((k: any) => k.kind === 'agents');
const layout: Pick<
ReturnType<typeof runtimeArtifactLayout.resolveRuntimeArtifactLayout>,
'runtime' | 'kinds'
> = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, targetDir, scope as 'global' | 'local', capabilityRegistry);
const agentsKindEntry = layout.kinds.find((kind) => kind.kind === 'agents');
if (!agentsKindEntry) return null;
// #3712: this writer selects `agentsKindEntry.home` over targetDir below and then
// prunes that destination via _removeGsdEntries, so it is a fifth route into the
@@ -1513,7 +1706,13 @@ function installAgentsKindStandalone(
// targetDir IS the install root the inline agent loop called `targetDir`.
const attribution = resolveAttribution ? resolveAttribution(runtime) : undefined;
const agentCtx = { runtime, pathPrefix, attribution, targetDir, projectDir: projectDir ?? targetDir };
const stagedDir: string = agentsKindEntry.stage(resolvedProfile, agentCtx);
let stagedDir: string;
try {
stagedDir = agentsKindEntry.stage(resolvedProfile, agentCtx);
} catch (err) {
if (scope !== 'global' || !isRuntimeSurfaceSourceUnavailable((err as Error).message)) throw err;
stagedDir = withInstallerPackageSource(targetDir, () => agentsKindEntry.stage(resolvedProfile, agentCtx));
}
const stagedAgentFiles: string[] = installFs().existsSync(stagedDir)
? installFs().readdirSync(stagedDir).filter((f: string) => f.endsWith('.md'))
@@ -1810,6 +2009,7 @@ function installOpencodeFamilyArtifacts(
resolveAttribution: ResolveAttribution = () => undefined,
behaviors: any = {},
capabilityRegistry?: any,
packageRoot?: string,
projectDir?: string | null,
): any {
// #2870: `scope` keeps its exported required `string` signature (no
@@ -1824,7 +2024,7 @@ function installOpencodeFamilyArtifacts(
// into stageSkillsForProfile/stageSkillsForRuntimeAsSkills. The repo/package
// root (needed below for the native plugin source) is two levels up.
const commandsGsdDir = runtimeArtifactLayout.findInstallSourceRoot(configDir);
const src = path.dirname(path.dirname(commandsGsdDir));
const src = packageRoot ?? executingPackageRoot();
const rawCommandsDir = installProfiles.stageSkillsForProfile(commandsGsdDir, resolvedProfile);
const pathPrefix = (runtimeArtifactConversion as any)._computePathPrefix({

View File

@@ -185,11 +185,10 @@ function createRuntimeArtifactInstallPlan(args: CreateRuntimeArtifactInstallPlan
resolveAttribution,
};
// ADR-1235 §1: build agentCtx once per plan so agents kind entries can apply
// the CORRECT pre-converter cross-cutting (path rewrites → attribution → converter
// → normalize). This mirrors the exact per-file order in the inline agent loop
// in bin/install.js (lines 9330-9415). agentCtx is passed as the second arg
// to kind.stage() for agents kind entries with a converter (convertedAgentsKind).
// ADR-1235 §1: build the staging context once per plan. Agent kinds apply
// the CORRECT pre-converter cross-cutting (path rewrites → attribution →
// converter → normalize). This
// mirrors the exact per-file order in the former inline agent loop.
// NO _stampNonClaudeRuntimeDefaults — agents are NOT stamped in the inline loop.
const os = _require('node:os') as typeof import('node:os');
const { posixNormalize } = _require('./shell-command-projection.cjs') as { posixNormalize: (p: string) => string };
@@ -214,18 +213,12 @@ function createRuntimeArtifactInstallPlan(args: CreateRuntimeArtifactInstallPlan
targetDir: layout.configDir,
projectDir: projectDir ?? layout.configDir,
};
for (const kind of layout.kinds) {
let stagedDir: string;
try {
if (kind.kind === 'agents' || kind.kind === 'kimi-agents') {
// ADR-1235 §1: pass agentCtx so stageAgentsForRuntimeWithConverter applies
// the full inline-loop order: pathRewrites → attribution → converter → normalize.
// The cross-cutting is now PRE-converter (inside staging), not POST.
stagedDir = kind.stage(resolvedProfile, agentCtx);
} else {
stagedDir = kind.stage(resolvedProfile);
}
// Agent kinds use the context for their pre-converter cross-cutting
// sequence; other kinds ignore it.
stagedDir = kind.stage(resolvedProfile, agentCtx);
} catch (err) {
return { ok: false, kind: 'stage_failed', message: errorMessage(err), cleanupDirs, failedKind: kind.kind };
}

View File

@@ -24,6 +24,10 @@ import os from 'node:os';
// eslint-disable-next-line @typescript-eslint/no-require-imports
import installFsAdapter = require('./install-fs-adapter.cjs');
const { installFs, mkInstallTempDir } = installFsAdapter;
// Reuse the install manifest's existing parser and streamed SHA-256
// classification instead of deriving a second integrity implementation here.
// eslint-disable-next-line @typescript-eslint/no-require-imports
import installerMigrations = require('./installer-migrations.cjs');
// eslint-disable-next-line @typescript-eslint/no-require-imports
import installProfiles = require('./install-profiles.cjs');
const {
@@ -92,9 +96,8 @@ interface CapabilityRegistryForSkills {
/**
* Cross-cutting context for descriptor-driven agent staging (ADR-1235 §1).
* Passed as the optional second arg to ArtifactKind.stage() for agents kind
* entries so that stageAgentsForRuntimeWithConverter can apply the exact
* inline-loop transform order: pathRewrites → attribution → converter → normalize.
* Agent kinds use these fields for the exact pathRewrites → attribution →
* converter → normalize order.
*/
interface AgentCtx {
runtime: string;
@@ -113,8 +116,8 @@ interface ArtifactKind {
kind: KimiArtifactKindName;
destSubpath: string;
prefix: string;
/** For agents kind with a converter, accepts an optional AgentCtx as the second
* arg so cross-cutting can be applied pre-converter (ADR-1235 §1). */
/** For agent kinds, accepts optional pre-converter cross-cutting context
* (ADR-1235 §1). */
stage: (resolvedProfile: ResolvedProfile, agentCtx?: AgentCtx) => string;
/** Resolved absolute alternate install root for this kind, if the descriptor
* specifies one (e.g. codex skills → $HOME/.agents). Undefined means the
@@ -128,6 +131,31 @@ interface ArtifactKind {
converter?: string;
}
type RuntimeSurfaceSourceClass = 'commands' | 'agents';
interface RuntimeSurfaceSourceProvider {
kind: 'installed' | 'marker' | 'package';
commandsRoot: string;
agentsRoot: string;
}
interface SourceResolutionContext {
runtimeConfigDir: string;
scope: 'local' | 'global';
required: Set<RuntimeSurfaceSourceClass>;
authority: 'runtime' | 'compatible';
provider?: RuntimeSurfaceSourceProvider;
}
function requiredRuntimeSurfaceSourceClasses(kinds: Iterable<{ kind?: string }>): Set<RuntimeSurfaceSourceClass> {
const required = new Set<RuntimeSurfaceSourceClass>();
for (const kind of kinds) {
if (kind.kind === 'commands' || kind.kind === 'skills') required.add('commands');
if (kind.kind === 'agents' || kind.kind === 'kimi-agents') required.add('agents');
}
return required;
}
interface Layout {
runtime: string;
configDir: string;
@@ -139,124 +167,283 @@ interface Layout {
// Source root finders
// ---------------------------------------------------------------------------
/**
* Locate the GSD commands/gsd source directory.
*
* Resolution order:
* 1. If runtimeConfigDir provided, check <runtimeConfigDir>/.gsd-source marker.
* 2. Walk up from __dirname using path.dirname (no literal .. segments).
* 3. Throw a descriptive error if neither succeeds.
*/
function findInstallSourceRoot(runtimeConfigDir?: string): string {
// Step 1: marker check — reads `<runtimeConfigDir>/.gsd-source`, a path
// under the INSTALL DESTINATION, so this probe goes through the injected
// adapter (installFs()).
if (runtimeConfigDir) {
const markerPath = path.join(runtimeConfigDir, '.gsd-source');
if (installFs().existsSync(markerPath)) {
try {
const src = installFs().readFileSync(markerPath, 'utf8').trim();
if (src && installFs().existsSync(src)) return src;
} catch { /* fall through */ }
function isReadableDirectory(candidate: string, routed: boolean): boolean {
try {
const io = routed ? installFs() : fs;
const stat = io.lstatSync(candidate);
if (!stat.isDirectory() || stat.isSymbolicLink()) return false;
const entries: string[] = io.readdirSync(candidate);
let readableFiles = 0;
for (const name of entries) {
const child = path.join(candidate, name);
const childStat = io.lstatSync(child);
if (childStat.isSymbolicLink()) return false;
if (childStat.isDirectory()) {
if (!isReadableDirectory(child, routed)) return false;
readableFiles += 1;
} else if (childStat.isFile()) {
if (routed) io.readFileSync(child);
else fs.accessSync(child, fs.constants.R_OK);
readableFiles += 1;
} else {
return false;
}
}
return readableFiles > 0;
} catch {
return false;
}
}
// Step 2: walk up from __dirname to locate the GSD PACKAGE'S OWN source
// tree (commands/gsd/) — this resolves where the installer's own code is
// running FROM, not anything under the install destination, so it is
// deliberately NOT routed through the injected fs adapter (#2874): a fake
// "destination" adapter has no reason to know about the real package's own
// on-disk layout (an injected adapter's store starts empty and is never
// seeded with real repo paths), and routing it through would make this
// resolution unconditionally throw rather than gracefully staging nothing.
//
// Uses `fs.statSync` in a try/catch rather than `fs.existsSync` — this is
// LOAD-BEARING, not a style choice: tests/executed-plan.test.cjs's F2 cases
// poison every method on the ROUTED fs surface (including `existsSync`,
// since installFs()'s REAL_ADAPTER also calls it) to prove nothing on the
// installRuntimeArtifacts call tree reaches real fs. The F2 "nativePlugin
// runtime: pi" test calls this function (via findInstallSourceRoot()) AFTER
// installing that poison, specifically to resolve the pi nativePlugin
// source path against this repo's own real layout — an operation this
// function must still be able to perform even while `existsSync` is
// poisoned, because this Step 2 walk is real-fs-only by design and was
// never meant to be covered by that poison list. `statSync` is not on the
// poisoned surface, so this probe survives; switching back to `existsSync`
// makes that F2 test throw (verified: reverting this to `existsSync` trips
// the poison and breaks the pi nativePlugin case).
function isPhysicallyConfinedTo(root: string, candidate: string): boolean {
try {
const physicalRoot = installFs().realpathSync(root);
const physicalCandidate = installFs().realpathSync(candidate);
return physicalCandidate === physicalRoot || physicalCandidate.startsWith(physicalRoot + path.sep);
} catch {
return false;
}
}
function installedManifestIsComplete(
runtimeConfigDir: string,
required: ReadonlySet<RuntimeSurfaceSourceClass>,
): boolean {
// This synchronous admission check binds provider selection to the corpus
// observed here. Same-user mutation after resolution is outside #4132's
// threat model and would require a broader snapshot/transaction design.
const io = installFs();
const manifestPath = path.join(runtimeConfigDir, 'gsd-file-manifest.json');
if (!io.existsSync(manifestPath)) return false;
if (!isPhysicallyConfinedTo(runtimeConfigDir, manifestPath)) return false;
try {
const manifest = installerMigrations.readInstallManifest(runtimeConfigDir);
if (manifest.manifestVersion === null) return false;
const keys = Object.keys(manifest.files);
const prefixes: string[] = [];
if (required.has('commands')) prefixes.push('gsd-core/commands/gsd/');
if (required.has('agents')) prefixes.push('gsd-core/agents/');
for (const prefix of prefixes) {
const expected = keys.filter((key) => key.startsWith(prefix));
if (expected.length === 0) return false;
const expectedSet = new Set(expected);
const corpusRoot = path.resolve(runtimeConfigDir, ...prefix.slice(0, -1).split('/'));
if (!isPhysicallyConfinedTo(runtimeConfigDir, corpusRoot)) return false;
let actualFiles = 0;
const visit = (dir: string): boolean => {
for (const name of io.readdirSync(dir)) {
const candidate = path.join(dir, name);
const stat = io.lstatSync(candidate);
if (stat.isSymbolicLink()) return false;
if (stat.isDirectory()) {
if (!visit(candidate)) return false;
} else if (stat.isFile()) {
const relative = path.relative(corpusRoot, candidate).split(path.sep).join('/');
if (!expectedSet.has(prefix + relative)) return false;
actualFiles += 1;
} else {
return false;
}
}
return true;
};
if (!visit(corpusRoot) || actualFiles !== expected.length) return false;
for (const key of expected) {
const parts = key.split('/');
if (parts.some((part) => part === '' || part === '.' || part === '..')) return false;
const candidate = path.resolve(runtimeConfigDir, ...parts);
const root = path.resolve(runtimeConfigDir);
if (!candidate.startsWith(root + path.sep)) return false;
const stat = io.lstatSync(candidate);
if (!stat.isFile() || stat.isSymbolicLink()) return false;
if (installerMigrations.classifyArtifact(runtimeConfigDir, key, manifest).classification !== 'managed-pristine') {
return false;
}
}
}
return true;
} catch {
return false;
}
}
function providerHasRequiredClasses(
provider: RuntimeSurfaceSourceProvider,
required: ReadonlySet<RuntimeSurfaceSourceClass>,
routed: boolean,
runtimeConfigDir?: string,
): boolean {
if (provider.kind === 'installed' && runtimeConfigDir) {
return installedManifestIsComplete(runtimeConfigDir, required);
}
return (!required.has('commands') || isReadableDirectory(provider.commandsRoot, routed)) &&
(!required.has('agents') || isReadableDirectory(provider.agentsRoot, routed));
}
function providersShareRequiredRoots(
left: RuntimeSurfaceSourceProvider,
right: RuntimeSurfaceSourceProvider,
required: ReadonlySet<RuntimeSurfaceSourceClass>,
): boolean {
const leftFs = left.kind === 'package' ? fs : installFs();
const rightFs = right.kind === 'package' ? fs : installFs();
const physicalRootsOverlap = (leftRoot: string, rightRoot: string): boolean => {
const canonicalize = (io: typeof leftFs, root: string): string | null => {
let existing = path.resolve(root);
const missingSegments: string[] = [];
while (true) {
try {
return path.resolve(io.realpathSync(existing), ...missingSegments);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') return null;
const parent = path.dirname(existing);
if (parent === existing) return null;
missingSegments.unshift(path.basename(existing));
existing = parent;
}
}
};
const overlap = (leftPath: string, rightPath: string): boolean => {
const relative = path.relative(leftPath, rightPath);
return relative === '' ||
(relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
};
const physicalLeft = canonicalize(leftFs, leftRoot);
const physicalRight = canonicalize(rightFs, rightRoot);
if (!physicalLeft || !physicalRight) return true;
return overlap(physicalLeft, physicalRight) || overlap(physicalRight, physicalLeft);
};
return (required.has('commands') && physicalRootsOverlap(left.commandsRoot, right.commandsRoot)) ||
(required.has('agents') && physicalRootsOverlap(left.agentsRoot, right.agentsRoot));
}
function markerProvider(runtimeConfigDir: string): RuntimeSurfaceSourceProvider | null {
const markerPath = path.join(runtimeConfigDir, '.gsd-source');
try {
if (!installFs().existsSync(markerPath)) return null;
const markerStat = installFs().lstatSync(markerPath);
if (!markerStat.isFile() || markerStat.isSymbolicLink()) return null;
const commandsRoot = installFs().readFileSync(markerPath, 'utf8').trim();
if (!commandsRoot) return null;
// A marker written by the installer may point at this process's executing
// package. Preserve package-source IO on real fs so the existing injected
// destination adapter remains destination-only (#2874).
const packaged = packageProvider(new Set(['commands']));
if (packaged && path.resolve(commandsRoot) === path.resolve(packaged.commandsRoot)) {
return packaged;
}
return {
kind: 'marker',
commandsRoot,
agentsRoot: path.resolve(path.dirname(commandsRoot), '..', 'agents'),
};
} catch {
return null;
}
}
function packageProvider(required: ReadonlySet<RuntimeSurfaceSourceClass>): RuntimeSurfaceSourceProvider | null {
// Package-source IO deliberately stays on real fs; injected install adapters
// model destinations, not the executing package tree (#2874).
let dir = __dirname;
for (let i = 0; i < 6; i++) {
const candidate = path.join(dir, 'commands', 'gsd');
try {
fs.statSync(candidate);
return candidate;
} catch { /* not here — keep walking up */ }
const candidate: RuntimeSurfaceSourceProvider = {
kind: 'package',
commandsRoot: path.join(dir, 'commands', 'gsd'),
agentsRoot: path.join(dir, 'agents'),
};
if (providerHasRequiredClasses(candidate, required, false)) return candidate;
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
throw new Error(`findInstallSourceRoot: could not locate commands/gsd from ${__dirname}`);
return null;
}
/**
* Locate the GSD agents source directory.
*
* Resolution order:
* 1. If runtimeConfigDir provided, check <runtimeConfigDir>/.gsd-source marker.
* 2. Walk up from __dirname using path.dirname (no literal .. segments).
* 3. Throw a descriptive error if neither succeeds.
* Select one complete source provider for an entire resolved layout.
* Provider mixing is forbidden: a skills+agents layout cannot take commands
* from one package version and agents from another.
*/
function findAgentsSourceRoot(runtimeConfigDir?: string): string {
// Step 1: marker check (destination-relative — routed through installFs()).
if (runtimeConfigDir) {
const markerPath = path.join(runtimeConfigDir, '.gsd-source');
if (installFs().existsSync(markerPath)) {
try {
const src = installFs().readFileSync(markerPath, 'utf8').trim();
if (src && installFs().existsSync(src)) {
// Marker points to commands/gsd; agents/ is a sibling of commands/
const agentsCandidate = path.resolve(path.dirname(src), '..', 'agents');
if (installFs().existsSync(agentsCandidate)) return agentsCandidate;
}
} catch { /* fall through */ }
function resolveSourceProvider(
runtimeConfigDir: string | undefined,
requiredClasses: Iterable<RuntimeSurfaceSourceClass>,
scope: 'local' | 'global' = 'global',
authority: 'runtime' | 'compatible' = 'compatible',
): RuntimeSurfaceSourceProvider {
const required = new Set(requiredClasses);
let rejectedInstalled: RuntimeSurfaceSourceProvider | null = null;
if (required.size === 0) {
return { kind: 'package', commandsRoot: '', agentsRoot: '' };
}
if (runtimeConfigDir && scope === 'global') {
const installed: RuntimeSurfaceSourceProvider = {
kind: 'installed',
commandsRoot: path.join(runtimeConfigDir, 'gsd-core', 'commands', 'gsd'),
agentsRoot: path.join(runtimeConfigDir, 'gsd-core', 'agents'),
};
if (providerHasRequiredClasses(installed, required, true, runtimeConfigDir)) return installed;
rejectedInstalled = installed;
const marker = markerProvider(runtimeConfigDir);
if (marker && !providersShareRequiredRoots(marker, installed, required) && providerHasRequiredClasses(marker, required, marker.kind !== 'package')) {
return marker;
}
} else if (runtimeConfigDir) {
const marker = markerProvider(runtimeConfigDir);
if (marker && providerHasRequiredClasses(marker, required, marker.kind !== 'package')) return marker;
}
if (authority === 'compatible') {
const packaged = packageProvider(required);
if (
packaged &&
(!rejectedInstalled || !providersShareRequiredRoots(packaged, rejectedInstalled, required))
) {
return packaged;
}
}
// Step 2: walk up from __dirname — locates THIS package's own agents/
// source tree, not the install destination. See findInstallSourceRoot's
// Step 2 comment (#2874) for why this stays unrouted, real-fs-only, and why
// it uses `statSync` rather than `existsSync` (load-bearing against F2's
// poison of the routed fs surface, not a style choice).
let dir = __dirname;
for (let i = 0; i < 6; i++) {
const candidate = path.join(dir, 'agents');
try {
fs.statSync(candidate);
return candidate;
} catch { /* not here — keep walking up */ }
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
throw new Error(
`Runtime Surface source is unavailable or incomplete for ${[...required].sort().join('+')}; ` +
'install or upgrade gsd-core before materializing this surface.',
);
}
throw new Error(`findAgentsSourceRoot: could not locate agents/ from ${__dirname}`);
function sourceRootFor(
context: SourceResolutionContext,
sourceClass: RuntimeSurfaceSourceClass,
): string {
context.provider ??= resolveSourceProvider(
context.runtimeConfigDir,
context.required,
context.scope,
context.authority,
);
return sourceClass === 'commands' ? context.provider.commandsRoot : context.provider.agentsRoot;
}
function findInstallSourceRoot(runtimeConfigDir?: string): string {
return resolveSourceProvider(runtimeConfigDir, ['commands']).commandsRoot;
}
// ---------------------------------------------------------------------------
// Layout table builders
// ---------------------------------------------------------------------------
function commandsKind(destSubpath: string, prefix: string, configDir: string): ArtifactKind {
function commandsKind(destSubpath: string, prefix: string, sourceContext: SourceResolutionContext): ArtifactKind {
return {
kind: 'commands',
destSubpath,
prefix,
stage: (resolved) => stageSkillsForProfile(findInstallSourceRoot(configDir), resolved),
stage: (resolved) => stageSkillsForProfile(sourceRootFor(sourceContext, 'commands'), resolved),
};
}
function agentsKind(destSubpath: string, prefix: string, configDir: string): ArtifactKind {
function agentsKind(destSubpath: string, prefix: string, configDir: string, sourceContext: SourceResolutionContext): ArtifactKind {
return {
kind: 'agents',
destSubpath,
@@ -280,11 +467,11 @@ function agentsKind(destSubpath: string, prefix: string, configDir: string): Art
// a caller with NO agentCtx in scope is unaffected (row I2: converter-only,
// as today), matching stageAgentsForRuntimeWithConverter's own contract.
stage: (resolved, agentCtx) => stageAgentsForRuntimeWithConverter(
findAgentsSourceRoot(configDir),
sourceRootFor(sourceContext, 'agents'),
resolved,
(content: string) => content,
false,
agentCtx,
agentCtx?.runtime ? agentCtx : undefined,
),
};
}
@@ -425,7 +612,8 @@ function convertedAgentsKind(
prefix: string,
converterName: string,
configDir: string,
scope: 'local' | 'global' = 'global',
sourceContext: SourceResolutionContext,
scope: 'local' | 'global',
): ArtifactKind {
return {
kind: 'agents',
@@ -501,17 +689,17 @@ function convertedAgentsKind(
// for descriptor-driven runtimes), thread it through so stageAgentsForRuntimeWithConverter
// can apply the full pre-converter + post-converter sequence in the correct order.
return stageAgentsForRuntimeWithConverter(
findAgentsSourceRoot(configDir),
sourceRootFor(sourceContext, 'agents'),
resolved,
converter,
isGlobalScope(scope),
agentCtx,
agentCtx?.runtime ? agentCtx : undefined,
);
},
};
}
function kimiAgentsKind(destSubpath: string, prefix: string, configDir: string): ArtifactKind {
function kimiAgentsKind(destSubpath: string, prefix: string, configDir: string, sourceContext: SourceResolutionContext): ArtifactKind {
return {
kind: 'kimi-agents',
destSubpath,
@@ -527,7 +715,7 @@ function kimiAgentsKind(destSubpath: string, prefix: string, configDir: string):
// #2995: compose at staging (identity converter) so the readFileSync below
// sees marker-free content — same single composing stager as agentsKind.
const stagedAgents = stageAgentsForRuntimeWithConverter(
findAgentsSourceRoot(configDir),
sourceRootFor(sourceContext, 'agents'),
resolved,
(content: string) => content,
false,
@@ -586,8 +774,9 @@ function skillsKind(
converterName: string,
runtime: string,
configDir: string,
nested = false,
scope: 'local' | 'global' = 'global',
nested: boolean,
scope: 'local' | 'global',
sourceContext: SourceResolutionContext,
capabilityRegistry?: CapabilityRegistryForSkills,
): ArtifactKind {
return {
@@ -623,7 +812,7 @@ function skillsKind(
// rewritten shape.
const wrappedConverter = (content: string, skillName: string): string =>
realConverter(content, skillName, runtime, cmdNames, isGlobal);
return stageSkillsForRuntimeAsSkills(findInstallSourceRoot(configDir), resolved, wrappedConverter, prefix, nested, capabilityRegistry);
return stageSkillsForRuntimeAsSkills(sourceRootFor(sourceContext, 'commands'), resolved, wrappedConverter, prefix, nested, capabilityRegistry);
},
};
}
@@ -648,7 +837,7 @@ function convertedCommandsKind(
destSubpath: string,
prefix: string,
converterName: string,
configDir: string,
sourceContext: SourceResolutionContext,
): ArtifactKind {
return {
kind: 'commands',
@@ -656,7 +845,7 @@ function convertedCommandsKind(
prefix,
stage: (resolved) => {
const converter = _resolveNamedConverter(converterName, 'commands') as (content: string, commandName: string) => string;
return stageCommandsForRuntimeFlat(findInstallSourceRoot(configDir), resolved, converter, prefix);
return stageCommandsForRuntimeFlat(sourceRootFor(sourceContext, 'commands'), resolved, converter, prefix);
},
};
}
@@ -743,7 +932,7 @@ function getRegistry(): RegistryLike {
* Map a single ArtifactKindDescriptor entry to an ArtifactKind using the
* matching builder function. Mirrors the hand-built calls in the old switch.
*/
function dispatchKindEntry(entry: ArtifactKindDescriptor, runtime: string, configDir: string, scope: 'local' | 'global', capabilityRegistry?: CapabilityRegistryForSkills): ArtifactKind {
function dispatchKindEntry(entry: ArtifactKindDescriptor, runtime: string, configDir: string, scope: 'local' | 'global', capabilityRegistry: CapabilityRegistryForSkills | undefined, sourceContext: SourceResolutionContext): ArtifactKind {
const { kind, destSubpath, prefix, nesting, converter } = entry;
const nested = nesting === 'nested';
@@ -751,14 +940,14 @@ function dispatchKindEntry(entry: ArtifactKindDescriptor, runtime: string, confi
switch (kind) {
case 'commands':
result = converter == null
? commandsKind(destSubpath, prefix, configDir)
: convertedCommandsKind(destSubpath, prefix, converter, configDir);
? commandsKind(destSubpath, prefix, sourceContext)
: convertedCommandsKind(destSubpath, prefix, converter, sourceContext);
break;
case 'agents':
result = converter == null
? agentsKind(destSubpath, prefix, configDir)
: convertedAgentsKind(destSubpath, prefix, converter, configDir, scope);
? agentsKind(destSubpath, prefix, configDir, sourceContext)
: convertedAgentsKind(destSubpath, prefix, converter, configDir, sourceContext, scope);
break;
case 'skills':
@@ -767,11 +956,11 @@ function dispatchKindEntry(entry: ArtifactKindDescriptor, runtime: string, confi
`resolveRuntimeArtifactLayout: skills entry for '${runtime}' has converter=null (converter is required for skills)`,
);
}
result = skillsKind(destSubpath, prefix, converter, runtime, configDir, nested, scope, capabilityRegistry);
result = skillsKind(destSubpath, prefix, converter, runtime, configDir, nested, scope, sourceContext, capabilityRegistry);
break;
case 'kimi-agents':
result = kimiAgentsKind(destSubpath, prefix, configDir);
result = kimiAgentsKind(destSubpath, prefix, configDir, sourceContext);
break;
default:
@@ -807,7 +996,12 @@ function dispatchKindEntry(entry: ArtifactKindDescriptor, runtime: string, confi
* is not threaded in at layout-build time a `full`-profile install stages no
* third-party capability skills regardless of registration (#2322 blocker 2).
*/
function resolveRuntimeArtifactLayout(runtime: string, configDir: string, scope: 'local' | 'global' = 'global', capabilityRegistry?: CapabilityRegistryForSkills): Layout {
function resolveRuntimeArtifactLayout(
runtime: string,
configDir: string,
scope: 'local' | 'global' = 'global',
capabilityRegistry?: CapabilityRegistryForSkills,
): Layout {
return resolveRuntimeArtifactLayoutFromRegistry(getRegistry(), runtime, configDir, scope, capabilityRegistry);
}
@@ -831,7 +1025,20 @@ function resolveRuntimeArtifactLayoutFromRegistry(
}
const entries: ArtifactKindDescriptor[] = desc[scope] ?? [];
const kinds: ArtifactKind[] = entries.map((entry) => dispatchKindEntry(entry, runtime, configDir, scope, capabilityRegistry));
const required = requiredRuntimeSurfaceSourceClasses(entries);
// Stage closures share one lazy source-resolution context, so the first kind
// to stage selects and caches one complete provider for every required source
// class. Global layouts accept only installed or marker providers; the
// installer owns its private package fallback by retrying through a transient
// compatibility marker after source resolution fails. Local layouts retain
// compatible marker/package resolution and never provision the global corpus.
const sourceContext: SourceResolutionContext = {
runtimeConfigDir: configDir,
scope,
required,
authority: scope === 'local' ? 'compatible' : 'runtime',
};
const kinds: ArtifactKind[] = entries.map((entry) => dispatchKindEntry(entry, runtime, configDir, scope, capabilityRegistry, sourceContext));
return { runtime, configDir, scope, kinds };
}

View File

@@ -110,6 +110,8 @@ interface ApplySurfaceOptions {
resolveAttribution?: (runtime: string) => string | null | undefined;
homedir?: () => string;
platform?: string;
/** Candidate state to publish only after every artifact kind materializes. */
surfaceState?: SurfaceState | null;
}
// ---------------------------------------------------------------------------
@@ -235,7 +237,7 @@ function normalizeSkillManifest(runtimeConfigDir: string, manifest: Map<string,
* - registry is threaded into resolveProfile so capability skills
* participate in the base skill set and their requires: chains expand.
*/
function resolveSurface(runtimeConfigDir: string, manifest: Map<string, string[]> | object, clusterMap?: ClusterMap | Record<string, string[]>, registry?: { capabilityClusters?: Record<string, string[]>; profileMembership?: Record<string, { tier: string; profiles: string[] }> }): { name: string; skills: Set<string>; agents: Set<string> } {
function resolveSurface(runtimeConfigDir: string, manifest: Map<string, string[]> | object, clusterMap?: ClusterMap | Record<string, string[]>, registry?: { capabilityClusters?: Record<string, string[]>; profileMembership?: Record<string, { tier: string; profiles: string[] }> }, surfaceOverride?: SurfaceState | null): { name: string; skills: Set<string>; agents: Set<string> } {
// Merge capability clusters into the cluster map when registry is provided.
// The ADR-857 phase 4a HARD gate guarantees that when a capId matches a CLUSTERS
// key, the values are EQUAL — so the spread is idempotent for matching names.
@@ -269,7 +271,7 @@ function resolveSurface(runtimeConfigDir: string, manifest: Map<string, string[]
cm = merged;
}
const skillManifest = normalizeSkillManifest(runtimeConfigDir, manifest);
const surface = readSurface(runtimeConfigDir);
const surface = surfaceOverride === undefined ? readSurface(runtimeConfigDir) : surfaceOverride;
// Determine base profile name: from surface state or from .gsd-profile marker
const baseProfileName = (surface && surface.baseProfile)
@@ -382,10 +384,9 @@ function applySurface(runtimeConfigDir: string, layout: Layout, manifest: Map<st
os: deps.os, env: deps.env,
});
const skillManifest = normalizeSkillManifest(layout.configDir, manifest);
const resolved = resolveSurface(layout.configDir, skillManifest, clusterMap, registry);
// Profile toggles must converge retired surfaces too. Once a kind disappears
// from artifactLayout there is no normal sync pass left to prune it (#2644).
retiredArtifactCleanup.pruneRetiredRuntimeArtifacts(layout.runtime, layout.configDir);
const hasCandidateState = opts !== undefined && Object.prototype.hasOwnProperty.call(opts, 'surfaceState');
const candidateState = hasCandidateState ? (opts.surfaceState ?? null) : undefined;
const resolved = resolveSurface(layout.configDir, skillManifest, clusterMap, registry, candidateState);
// #1575: agents kind now mirrors createRuntimeArtifactInstallPlan — build
// agentCtx (pathPrefix + attribution) and pass it to kind.stage() so
// stageAgentsForRuntimeWithConverter applies the full inline-loop pipeline
@@ -420,7 +421,7 @@ function applySurface(runtimeConfigDir: string, layout: Layout, manifest: Map<st
// not referenced by any skill's _calls_agents_ manifest entry would be silently
// dropped from the surface path. For tiered profiles (core/standard) or when
// surface mods exist, pass the resolved set so only the filtered subset stages.
const _surfaceState = readSurface(layout.configDir);
const _surfaceState = candidateState === undefined ? readSurface(layout.configDir) : candidateState;
const _baseProfileName = (_surfaceState && _surfaceState.baseProfile)
? _surfaceState.baseProfile
: (readActiveProfile(layout.configDir) || 'full');
@@ -430,6 +431,7 @@ function applySurface(runtimeConfigDir: string, layout: Layout, manifest: Map<st
_surfaceState.explicitRemoves.length > 0
);
const _isUnmodifiedFull = _baseProfileName === 'full' && !_hasSurfaceMods;
const stagedKinds: { kind: ArtifactKind; staged: string; dest: string }[] = [];
try {
for (const kind of layout.kinds) {
let staged: string;
@@ -463,7 +465,23 @@ function applySurface(runtimeConfigDir: string, layout: Layout, manifest: Map<st
// the parity test in tests/runtime-artifact-layout-surface.test.cjs
// enforces that the two writers never diverge again.
const dest = assertDestWithinConfigHome(kind.home ?? layout.configDir, kind.destSubpath);
_syncGsdDir(staged, dest, kind, skillManifest, layout.runtime);
stagedKinds.push({ kind, staged, dest });
}
// Do not mutate installed artifacts until every kind has staged
// successfully. A missing source provider therefore leaves both artifacts
// and the persisted surface state untouched.
retiredArtifactCleanup.pruneRetiredRuntimeArtifacts(layout.runtime, layout.configDir);
for (const item of stagedKinds) {
_syncGsdDir(item.staged, item.dest, item.kind, skillManifest, layout.runtime);
}
if (hasCandidateState) {
if (candidateState === null) {
fs.rmSync(path.join(runtimeConfigDir, SURFACE_FILE_NAME), { force: true });
} else if (candidateState !== undefined) {
writeSurface(runtimeConfigDir, candidateState);
}
}
} finally {
for (const dir of tempDirsToClean) {

View File

@@ -143,6 +143,10 @@ function buildSourceTree(agentFiles) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-agent-parity-src-'));
const commandsGsd = path.join(root, 'commands', 'gsd');
fs.mkdirSync(commandsGsd, { recursive: true });
// Runtime Surface source providers are atomic across every source class a
// layout needs. Keep this marker fixture complete for commands + agents so
// the installer resolver does not correctly fall back to the package tree.
fs.writeFileSync(path.join(commandsGsd, 'fixture-command.md'), '# Fixture command\n');
const agentsDir = path.join(root, 'agents');
fs.mkdirSync(agentsDir, { recursive: true });
for (const [name, content] of Object.entries(agentFiles)) {

View File

@@ -136,14 +136,16 @@ function spawnInstall(repoRoot, runtime, scope) {
}
/** Emitted files under `root` that are derived from the probe agent AND contain
* `token`. The `derivedOnly` filter is what keeps shipped library sources —
* which legitimately carry the marker grammar — out of the leak set. */
* `token`. The `derivedOnly` filter keeps shipped library sources and the
* installation-owned raw re-staging corpus — both of which legitimately
* preserve canonical marker grammar — out of the emitted-artifact leak set. */
function filesContaining(root, token, derivedOnly = true) {
if (!fs.existsSync(root)) return [];
const hits = [];
for (const abs of walk(root)) {
const rel = path.relative(root, abs).replace(/\\/g, '/');
if (derivedOnly && !rel.includes(PROBE_STEM)) continue;
if (derivedOnly && (rel.startsWith('gsd-core/agents/') || rel.includes('/gsd-core/agents/'))) continue;
let buf;
try {
buf = fs.readFileSync(abs);

View File

@@ -253,7 +253,8 @@ test('Kimi neutralizes ~/.claude/gsd-core references instead of leaking a repoin
// to see the ORIGINAL `~/.claude/gsd-core` text; kimi/kimi-code capability.json
// now opt out via hostBehaviors.noPathRewrite so that still holds.
const install = installRuntime(t, 'kimi', { scope: 'global' });
const artifacts = emittedAgentArtifacts(install, 'gsd-executor');
// Inspect generated prompts, not the installation-owned raw source corpus.
const artifacts = emittedAgentArtifacts(install, 'gsd-executor', path.join(install.configDir, 'agents'));
assert.ok(artifacts.some((artifact) => artifact.includes('GSD core')),
'a ~/.claude/gsd-core reference must neutralize to prose, not a repointed Kimi path');
assert.ok(artifacts.every((artifact) =>

View File

@@ -1145,6 +1145,10 @@ describe('issue-2322: capability set --runtime materializes an installed third-p
// Step 3 of the issue repro: capability set my-thing --runtime claude --scope global
const rcd = tmpDir('cap-cli-rcd-');
// A global materialization needs one complete Runtime Surface provider.
// This fixture exercises the supported legacy compatibility-marker path;
// source-less global trees now fail closed and must be upgraded first.
fs.writeFileSync(path.join(rcd, '.gsd-source'), path.join(__dirname, '..', 'commands', 'gsd') + '\n');
const setResult = runGsdTools(
['capability', 'set', 'my-thing', '--runtime', 'claude', '--scope', 'global', '--config-dir', rcd, '--raw'],
cwd,

View File

@@ -447,6 +447,11 @@ describe('capability-writer: setCapabilityState', () => {
const materialiseWarning = result.warnings.find((w) => w.includes('materialize failed'));
assert.equal(materialiseWarning, undefined,
`materialise failure must not appear in warnings, got: ${JSON.stringify(result.warnings)}`);
assert.equal(
readSurface(rcd),
null,
'a failed materialization must not publish the computed surface state',
);
} finally {
cleanup(rcd);
cleanup(cwd);

View File

@@ -40,6 +40,18 @@ const {
const _manifest = loadSkillsManifest();
const resolvedProfileFull = resolveProfile({ modes: [], manifest: _manifest });
function installFromFixture(configDir) {
return installRuntimeArtifacts(
'claude',
configDir,
'global',
resolvedProfileFull,
undefined,
undefined,
{ fs },
);
}
/**
* Set up a configDir backed by a custom srcDir via .gsd-source marker.
* Returns { configDir, srcDir } both under tmpDir.
@@ -50,6 +62,12 @@ function setupConfigDir(tmpDir, commandFiles) {
for (const [name, content] of Object.entries(commandFiles)) {
fs.writeFileSync(path.join(srcDir, name), content);
}
// Claude's layout consumes commands and agents from one atomic provider.
// Keep the marker fixture complete so these tests exercise their custom
// command bytes instead of correctly falling back to the package corpus.
const agentsDir = path.join(tmpDir, 'agents');
fs.mkdirSync(agentsDir, { recursive: true });
fs.writeFileSync(path.join(agentsDir, 'gsd-fixture-agent.md'), '---\nname: gsd-fixture-agent\n---\n');
const configDir = path.join(tmpDir, 'config');
fs.mkdirSync(configDir, { recursive: true });
// .gsd-source marker tells findInstallSourceRoot to use our custom srcDir
@@ -212,7 +230,7 @@ describe('installRuntimeArtifacts (claude global) — skill layout', () => {
'health.md': '---\nname: gsd:health\ndescription: Check health\n---\n\nHealth body.',
});
installRuntimeArtifacts('claude', configDir, 'global', resolvedProfileFull);
installFromFixture(configDir);
const skillsDir = path.join(configDir, 'skills');
assert.ok(
@@ -238,7 +256,7 @@ describe('installRuntimeArtifacts (claude global) — skill layout', () => {
// Production sequence: uninstall wipes gsd-* entries, install writes fresh ones
uninstallRuntimeArtifacts('claude', configDir, 'global');
installRuntimeArtifacts('claude', configDir, 'global', resolvedProfileFull);
installFromFixture(configDir);
// Stale skill removed by uninstall
assert.ok(
@@ -264,7 +282,7 @@ describe('installRuntimeArtifacts (claude global) — skill layout', () => {
fs.writeFileSync(path.join(otherDir, 'SKILL.md'), 'custom content');
// Install (no pre-uninstall: uninstall only removes gsd-* prefixed entries)
installRuntimeArtifacts('claude', configDir, 'global', resolvedProfileFull);
installFromFixture(configDir);
// Non-GSD skill preserved
assert.ok(
@@ -291,15 +309,19 @@ describe('installRuntimeArtifacts (claude global) — skill layout', () => {
// an empty dir — _copyStaged then copies nothing into skills/.
const emptySrc = path.join(tmpDir, 'empty-commands', 'gsd');
fs.mkdirSync(emptySrc, { recursive: true });
fs.writeFileSync(path.join(emptySrc, 'README.txt'), 'fixture only\n');
const agentsDir = path.join(tmpDir, 'agents');
fs.mkdirSync(agentsDir, { recursive: true });
fs.writeFileSync(path.join(agentsDir, 'gsd-fixture-agent.md'), '---\nname: gsd-fixture-agent\n---\n');
const configDir = path.join(tmpDir, 'config-empty');
fs.mkdirSync(configDir, { recursive: true });
fs.writeFileSync(path.join(configDir, '.gsd-source'), emptySrc + '\n');
// Should not throw
installRuntimeArtifacts('claude', configDir, 'global', resolvedProfileFull);
// Use the existing installer fs seam to supply this fixture corpus and
// skip production provisioning, which would intentionally replace it.
installFromFixture(configDir);
const skillsDir = path.join(configDir, 'skills');
// If skills dir was created it must contain no gsd-* entries
if (fs.existsSync(skillsDir)) {
const gsdEntries = fs.readdirSync(skillsDir).filter(n => n.startsWith('gsd-'));
assert.strictEqual(gsdEntries.length, 0, 'no gsd-* skills created when src has no .md files');
@@ -339,7 +361,7 @@ describe('installRuntimeArtifacts path replacement in Claude global skills (#165
].join('\n'),
});
installRuntimeArtifacts('claude', configDir, 'global', resolvedProfileFull);
installFromFixture(configDir);
const content = fs.readFileSync(
path.join(configDir, 'skills', 'gsd-manager', 'SKILL.md'), 'utf8'
@@ -363,7 +385,7 @@ describe('installRuntimeArtifacts path replacement in Claude global skills (#165
'debug.md': '---\nname: gsd:debug\ndescription: Debug\n---\n\n@$HOME/.claude/gsd-core/workflows/debug.md',
});
installRuntimeArtifacts('claude', configDir, 'global', resolvedProfileFull);
installFromFixture(configDir);
const content = fs.readFileSync(
path.join(configDir, 'skills', 'gsd-debug', 'SKILL.md'), 'utf8'
@@ -383,7 +405,7 @@ describe('installRuntimeArtifacts path replacement in Claude global skills (#165
'next.md': '---\nname: gsd:next\ndescription: Next\n---\n\n@~/.claude/gsd-core/workflows/next.md',
});
installRuntimeArtifacts('claude', configDir, 'global', resolvedProfileFull);
installFromFixture(configDir);
const content = fs.readFileSync(
path.join(configDir, 'skills', 'gsd-next', 'SKILL.md'), 'utf8'
@@ -422,7 +444,7 @@ describe('Legacy commands/gsd/ cleanup', () => {
// installRuntimeArtifacts calls _runLegacyInstallMigrations which removes
// commands/gsd/ for claude runtime (it's the legacy location for global).
installRuntimeArtifacts('claude', configDir, 'global', resolvedProfileFull);
installFromFixture(configDir);
assert.ok(!fs.existsSync(legacyDir), 'legacy commands/gsd/ removed');
assert.ok(

View File

@@ -47,6 +47,7 @@ const commandRoster = require('../gsd-core/bin/lib/command-roster.cjs');
const slashCommandTransformer = require('../scripts/fix-slash-commands.cjs');
const REAL_COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
const REAL_PACKAGE_ROOT = path.resolve(__dirname, '..');
const MANIFEST = loadSkillsManifest(REAL_COMMANDS_DIR);
const RESOLVED_CORE = resolveProfile({ modes: ['core'], manifest: MANIFEST });
const RESOLVED_FULL = resolveProfile({ modes: ['full'], manifest: MANIFEST });
@@ -149,13 +150,11 @@ const REAL_FS_WRITE_SURFACE = [
'mkdtempSync', 'openSync', 'readSync', 'closeSync',
];
// Package-source roots a correct install is expected to read for real, even
// while a fake DESTINATION adapter is injected (40-design.md "Known limits":
// this seam makes destination IO fake-able; package-source IO stays real by
// design). Mirrors findInstallSourceRoot's/findAgentsSourceRoot's/
// readGsdCommandNames's own targets (commands/gsd/, agents/), all resolved
// the same way REAL_COMMANDS_DIR is above.
const PACKAGE_SOURCE_ROOTS = [REAL_COMMANDS_DIR, path.join(__dirname, '..', 'agents')];
// The provider resolver probes upward through the executing package before it
// reaches commands/gsd and agents. All read-only package-tree probes are valid
// real IO while a fake DESTINATION adapter is active; destination paths remain
// poisoned below.
const PACKAGE_SOURCE_ROOTS = [path.resolve(__dirname, '..')];
function isPackageSourcePath(resolvedPath) {
return PACKAGE_SOURCE_ROOTS.some(
@@ -390,6 +389,29 @@ function sha256Hex(content) {
return crypto.createHash('sha256').update(content).digest('hex');
}
function fakeInstalledCorpusSeed(configDir) {
const coreDir = path.join(configDir, 'gsd-core');
const commandsDir = path.join(coreDir, 'commands');
const commandsGsdDir = path.join(commandsDir, 'gsd');
const agentsDir = path.join(coreDir, 'agents');
return [
[configDir, { type: 'dir' }],
[coreDir, { type: 'dir' }],
[commandsDir, { type: 'dir' }],
[commandsGsdDir, { type: 'dir' }],
[path.join(commandsGsdDir, 'fixture.md'), { type: 'file', content: '# Fixture command\n' }],
[agentsDir, { type: 'dir' }],
[path.join(agentsDir, 'fixture-agent.md'), { type: 'file', content: '# Fixture agent\n' }],
];
}
function fakeInstalledCorpusManifestFiles() {
return {
'gsd-core/commands/gsd/fixture.md': sha256Hex('# Fixture command\n'),
'gsd-core/agents/fixture-agent.md': sha256Hex('# Fixture agent\n'),
};
}
describe('installRuntimeArtifacts — F2: fake-adapter install touches no real filesystem', () => {
test('fake-adapter install touches no real filesystem (claude, skills-only)', (t) => {
// Every real fs method this call tree could reach is poisoned BY PATH
@@ -399,15 +421,16 @@ describe('installRuntimeArtifacts — F2: fake-adapter install touches no real f
// try/finally inside test bodies").
const packageSourceHits = poisonRealFsAgainstDestination(t, '');
const fakeFs = createFakeInstallFs();
// configDir deliberately never created for real — F2 asserts nothing
// real ever gets written under it.
const configDir = path.join(os.tmpdir(), `gsd-f2-must-not-exist-${crypto.randomUUID()}`);
const fakeFs = createFakeInstallFs(fakeInstalledCorpusSeed(configDir));
// configDir exists only in the fake store — F2 asserts nothing real ever
// gets written under it. The installed corpus seed is the new provider
// contract, not destination setup on the host filesystem.
const result = installRuntimeArtifacts(
'claude', configDir, 'global', RESOLVED_CORE, undefined, undefined,
{ fs: fakeFs },
{ fs: fakeFs, packageRoot: REAL_PACKAGE_ROOT },
);
assert.notStrictEqual(
@@ -436,10 +459,13 @@ describe('installRuntimeArtifacts — F2: fake-adapter install touches no real f
const legacyFile = path.join(legacyDir, 'gsd-old-cmd.md');
const content = '# stale legacy command\n';
const manifestPath = path.join(configDir, 'gsd-file-manifest.json');
const manifestJson = JSON.stringify({ files: { 'command/gsd-old-cmd.md': sha256Hex(content) } });
const manifestJson = JSON.stringify({ files: {
...fakeInstalledCorpusManifestFiles(),
'command/gsd-old-cmd.md': sha256Hex(content),
} });
const fakeFs = createFakeInstallFs([
[configDir, { type: 'dir' }],
...fakeInstalledCorpusSeed(configDir),
[legacyDir, { type: 'dir' }],
[legacyFile, { type: 'file', content }],
[manifestPath, { type: 'file', content: manifestJson }],
@@ -447,7 +473,7 @@ describe('installRuntimeArtifacts — F2: fake-adapter install touches no real f
const result = installRuntimeArtifacts(
'opencode', configDir, 'global', RESOLVED_CORE, undefined, undefined,
{ fs: fakeFs },
{ fs: fakeFs, packageRoot: REAL_PACKAGE_ROOT },
);
assert.notStrictEqual(result, undefined, 'F2 (opencode legacy migration): must still return a plan');
@@ -476,7 +502,7 @@ describe('installRuntimeArtifacts — F2: fake-adapter install touches no real f
// the poisoned list (see install-fs-adapter.cts's module doc — it is
// deliberately unrouted, real-fs-only, package-source introspection), so
// resolving this here is safe even after poisoning existsSync et al.
const commandsGsdDir = runtimeArtifactLayout.findInstallSourceRoot();
const commandsGsdDir = REAL_COMMANDS_DIR;
const repoRoot = path.dirname(path.dirname(commandsGsdDir));
const nativePlugin = registry.runtimes.pi.runtime.hostBehaviors.nativePlugin;
assert.ok(nativePlugin && nativePlugin.source, 'pi must declare hostBehaviors.nativePlugin.source (registry drifted)');
@@ -489,7 +515,7 @@ describe('installRuntimeArtifacts — F2: fake-adapter install touches no real f
const result = installRuntimeArtifacts(
'pi', configDir, 'global', RESOLVED_CORE, undefined, undefined,
{ fs: fakeFs },
{ fs: fakeFs, packageRoot: REAL_PACKAGE_ROOT },
);
assert.notStrictEqual(result, undefined, 'F2 (nativePlugin): must still return a plan');
@@ -522,10 +548,13 @@ describe('installRuntimeArtifacts — F2: fake-adapter install touches no real f
const content = '# stale retired artifact\n';
const relPath = `${destSubpath.replace(/\\/g, '/')}/${staleName}`;
const manifestPath = path.join(configDir, 'gsd-file-manifest.json');
const manifestJson = JSON.stringify({ files: { [relPath]: sha256Hex(content) } });
const manifestJson = JSON.stringify({ files: {
...fakeInstalledCorpusManifestFiles(),
[relPath]: sha256Hex(content),
} });
const fakeFs = createFakeInstallFs([
[configDir, { type: 'dir' }],
...fakeInstalledCorpusSeed(configDir),
[destDir, { type: 'dir' }],
[staleFile, { type: 'file', content }],
[manifestPath, { type: 'file', content: manifestJson }],
@@ -1196,6 +1225,19 @@ describe('installRuntimeArtifacts — K3: real install before/after, full recurs
`K3 (${runtime}): the file sets written by two independent installs must match`,
);
for (const [relPath, contentA] of filesA) {
if (relPath === '.gsd-source') {
assert.strictEqual(
contentA.toString('utf8'),
`${path.join(dirA, 'gsd-core', 'commands', 'gsd')}\n`,
`K3 (${runtime}): first marker must target its own installed corpus`,
);
assert.strictEqual(
filesB.get(relPath).toString('utf8'),
`${path.join(dirB, 'gsd-core', 'commands', 'gsd')}\n`,
`K3 (${runtime}): second marker must target its own installed corpus`,
);
continue;
}
assert.ok(
contentA.equals(filesB.get(relPath)),
`K3 (${runtime}): ${relPath} content drifted between two independent installs`,

View File

@@ -37,6 +37,41 @@
"agents/gsd-verifier.md",
"gsd-core/.gsd-runtime",
"gsd-core/VERSION",
"gsd-core/agents/gsd-advisor-researcher.md",
"gsd-core/agents/gsd-ai-researcher.md",
"gsd-core/agents/gsd-assumptions-analyzer.md",
"gsd-core/agents/gsd-code-fixer.md",
"gsd-core/agents/gsd-code-reviewer.md",
"gsd-core/agents/gsd-codebase-mapper.md",
"gsd-core/agents/gsd-debug-session-manager.md",
"gsd-core/agents/gsd-debugger.md",
"gsd-core/agents/gsd-doc-classifier.md",
"gsd-core/agents/gsd-doc-synthesizer.md",
"gsd-core/agents/gsd-doc-verifier.md",
"gsd-core/agents/gsd-doc-writer.md",
"gsd-core/agents/gsd-dom-verifier.md",
"gsd-core/agents/gsd-domain-researcher.md",
"gsd-core/agents/gsd-eval-auditor.md",
"gsd-core/agents/gsd-eval-planner.md",
"gsd-core/agents/gsd-executor.md",
"gsd-core/agents/gsd-framework-selector.md",
"gsd-core/agents/gsd-integration-checker.md",
"gsd-core/agents/gsd-intel-updater.md",
"gsd-core/agents/gsd-mempalace-curator.md",
"gsd-core/agents/gsd-nyquist-auditor.md",
"gsd-core/agents/gsd-pattern-mapper.md",
"gsd-core/agents/gsd-phase-researcher.md",
"gsd-core/agents/gsd-plan-checker.md",
"gsd-core/agents/gsd-planner.md",
"gsd-core/agents/gsd-project-researcher.md",
"gsd-core/agents/gsd-research-synthesizer.md",
"gsd-core/agents/gsd-roadmapper.md",
"gsd-core/agents/gsd-security-auditor.md",
"gsd-core/agents/gsd-ui-auditor.md",
"gsd-core/agents/gsd-ui-checker.md",
"gsd-core/agents/gsd-ui-researcher.md",
"gsd-core/agents/gsd-user-profiler.md",
"gsd-core/agents/gsd-verifier.md",
"gsd-core/bin/check-latest-version.cjs",
"gsd-core/bin/ensure-runtime-build.cjs",
"gsd-core/bin/gsd-tools.cjs",
@@ -48,6 +83,78 @@
"gsd-core/bin/shared/model-catalog.json",
"gsd-core/bin/shared/runtime-aliases.manifest.json",
"gsd-core/bin/verify-reapply-patches.cjs",
"gsd-core/commands/gsd/add-tests.md",
"gsd-core/commands/gsd/ai-integration-phase.md",
"gsd-core/commands/gsd/audit-fix.md",
"gsd-core/commands/gsd/audit-milestone.md",
"gsd-core/commands/gsd/audit-uat.md",
"gsd-core/commands/gsd/autonomous.md",
"gsd-core/commands/gsd/capture.md",
"gsd-core/commands/gsd/cleanup.md",
"gsd-core/commands/gsd/code-review.md",
"gsd-core/commands/gsd/complete-milestone.md",
"gsd-core/commands/gsd/config.md",
"gsd-core/commands/gsd/debug.md",
"gsd-core/commands/gsd/discuss-phase.md",
"gsd-core/commands/gsd/docs-update.md",
"gsd-core/commands/gsd/eval-review.md",
"gsd-core/commands/gsd/execute-phase.md",
"gsd-core/commands/gsd/explore.md",
"gsd-core/commands/gsd/extract-learnings.md",
"gsd-core/commands/gsd/fast.md",
"gsd-core/commands/gsd/forensics.md",
"gsd-core/commands/gsd/graphify.md",
"gsd-core/commands/gsd/health.md",
"gsd-core/commands/gsd/help.md",
"gsd-core/commands/gsd/import.md",
"gsd-core/commands/gsd/inbox.md",
"gsd-core/commands/gsd/ingest-docs.md",
"gsd-core/commands/gsd/manager.md",
"gsd-core/commands/gsd/map-codebase.md",
"gsd-core/commands/gsd/mempalace-capture.md",
"gsd-core/commands/gsd/mempalace-recall.md",
"gsd-core/commands/gsd/milestone-summary.md",
"gsd-core/commands/gsd/mvp-phase.md",
"gsd-core/commands/gsd/new-milestone.md",
"gsd-core/commands/gsd/new-project.md",
"gsd-core/commands/gsd/next.md",
"gsd-core/commands/gsd/ns-context.md",
"gsd-core/commands/gsd/ns-ideate.md",
"gsd-core/commands/gsd/ns-manage.md",
"gsd-core/commands/gsd/ns-project.md",
"gsd-core/commands/gsd/ns-review.md",
"gsd-core/commands/gsd/ns-workflow.md",
"gsd-core/commands/gsd/onboard.md",
"gsd-core/commands/gsd/pause-work.md",
"gsd-core/commands/gsd/phase.md",
"gsd-core/commands/gsd/plan-phase.md",
"gsd-core/commands/gsd/plan-review-convergence.md",
"gsd-core/commands/gsd/pr-branch.md",
"gsd-core/commands/gsd/profile-user.md",
"gsd-core/commands/gsd/progress.md",
"gsd-core/commands/gsd/quick-batch.md",
"gsd-core/commands/gsd/quick.md",
"gsd-core/commands/gsd/resume-work.md",
"gsd-core/commands/gsd/review-backlog.md",
"gsd-core/commands/gsd/review.md",
"gsd-core/commands/gsd/secure-phase.md",
"gsd-core/commands/gsd/settings.md",
"gsd-core/commands/gsd/ship.md",
"gsd-core/commands/gsd/sketch.md",
"gsd-core/commands/gsd/spec-phase.md",
"gsd-core/commands/gsd/spike.md",
"gsd-core/commands/gsd/stats.md",
"gsd-core/commands/gsd/surface.md",
"gsd-core/commands/gsd/thread.md",
"gsd-core/commands/gsd/ui-phase.md",
"gsd-core/commands/gsd/ui-review.md",
"gsd-core/commands/gsd/ultraplan-phase.md",
"gsd-core/commands/gsd/undo.md",
"gsd-core/commands/gsd/update.md",
"gsd-core/commands/gsd/validate-phase.md",
"gsd-core/commands/gsd/verify-work.md",
"gsd-core/commands/gsd/workspace.md",
"gsd-core/commands/gsd/workstreams.md",
"gsd-core/contexts/dev.md",
"gsd-core/contexts/research.md",
"gsd-core/contexts/review.md",

View File

@@ -109,6 +109,41 @@
"commands/gsd-workstreams.md",
"gsd-core/.gsd-runtime",
"gsd-core/VERSION",
"gsd-core/agents/gsd-advisor-researcher.md",
"gsd-core/agents/gsd-ai-researcher.md",
"gsd-core/agents/gsd-assumptions-analyzer.md",
"gsd-core/agents/gsd-code-fixer.md",
"gsd-core/agents/gsd-code-reviewer.md",
"gsd-core/agents/gsd-codebase-mapper.md",
"gsd-core/agents/gsd-debug-session-manager.md",
"gsd-core/agents/gsd-debugger.md",
"gsd-core/agents/gsd-doc-classifier.md",
"gsd-core/agents/gsd-doc-synthesizer.md",
"gsd-core/agents/gsd-doc-verifier.md",
"gsd-core/agents/gsd-doc-writer.md",
"gsd-core/agents/gsd-dom-verifier.md",
"gsd-core/agents/gsd-domain-researcher.md",
"gsd-core/agents/gsd-eval-auditor.md",
"gsd-core/agents/gsd-eval-planner.md",
"gsd-core/agents/gsd-executor.md",
"gsd-core/agents/gsd-framework-selector.md",
"gsd-core/agents/gsd-integration-checker.md",
"gsd-core/agents/gsd-intel-updater.md",
"gsd-core/agents/gsd-mempalace-curator.md",
"gsd-core/agents/gsd-nyquist-auditor.md",
"gsd-core/agents/gsd-pattern-mapper.md",
"gsd-core/agents/gsd-phase-researcher.md",
"gsd-core/agents/gsd-plan-checker.md",
"gsd-core/agents/gsd-planner.md",
"gsd-core/agents/gsd-project-researcher.md",
"gsd-core/agents/gsd-research-synthesizer.md",
"gsd-core/agents/gsd-roadmapper.md",
"gsd-core/agents/gsd-security-auditor.md",
"gsd-core/agents/gsd-ui-auditor.md",
"gsd-core/agents/gsd-ui-checker.md",
"gsd-core/agents/gsd-ui-researcher.md",
"gsd-core/agents/gsd-user-profiler.md",
"gsd-core/agents/gsd-verifier.md",
"gsd-core/bin/check-latest-version.cjs",
"gsd-core/bin/ensure-runtime-build.cjs",
"gsd-core/bin/gsd-tools.cjs",
@@ -120,6 +155,78 @@
"gsd-core/bin/shared/model-catalog.json",
"gsd-core/bin/shared/runtime-aliases.manifest.json",
"gsd-core/bin/verify-reapply-patches.cjs",
"gsd-core/commands/gsd/add-tests.md",
"gsd-core/commands/gsd/ai-integration-phase.md",
"gsd-core/commands/gsd/audit-fix.md",
"gsd-core/commands/gsd/audit-milestone.md",
"gsd-core/commands/gsd/audit-uat.md",
"gsd-core/commands/gsd/autonomous.md",
"gsd-core/commands/gsd/capture.md",
"gsd-core/commands/gsd/cleanup.md",
"gsd-core/commands/gsd/code-review.md",
"gsd-core/commands/gsd/complete-milestone.md",
"gsd-core/commands/gsd/config.md",
"gsd-core/commands/gsd/debug.md",
"gsd-core/commands/gsd/discuss-phase.md",
"gsd-core/commands/gsd/docs-update.md",
"gsd-core/commands/gsd/eval-review.md",
"gsd-core/commands/gsd/execute-phase.md",
"gsd-core/commands/gsd/explore.md",
"gsd-core/commands/gsd/extract-learnings.md",
"gsd-core/commands/gsd/fast.md",
"gsd-core/commands/gsd/forensics.md",
"gsd-core/commands/gsd/graphify.md",
"gsd-core/commands/gsd/health.md",
"gsd-core/commands/gsd/help.md",
"gsd-core/commands/gsd/import.md",
"gsd-core/commands/gsd/inbox.md",
"gsd-core/commands/gsd/ingest-docs.md",
"gsd-core/commands/gsd/manager.md",
"gsd-core/commands/gsd/map-codebase.md",
"gsd-core/commands/gsd/mempalace-capture.md",
"gsd-core/commands/gsd/mempalace-recall.md",
"gsd-core/commands/gsd/milestone-summary.md",
"gsd-core/commands/gsd/mvp-phase.md",
"gsd-core/commands/gsd/new-milestone.md",
"gsd-core/commands/gsd/new-project.md",
"gsd-core/commands/gsd/next.md",
"gsd-core/commands/gsd/ns-context.md",
"gsd-core/commands/gsd/ns-ideate.md",
"gsd-core/commands/gsd/ns-manage.md",
"gsd-core/commands/gsd/ns-project.md",
"gsd-core/commands/gsd/ns-review.md",
"gsd-core/commands/gsd/ns-workflow.md",
"gsd-core/commands/gsd/onboard.md",
"gsd-core/commands/gsd/pause-work.md",
"gsd-core/commands/gsd/phase.md",
"gsd-core/commands/gsd/plan-phase.md",
"gsd-core/commands/gsd/plan-review-convergence.md",
"gsd-core/commands/gsd/pr-branch.md",
"gsd-core/commands/gsd/profile-user.md",
"gsd-core/commands/gsd/progress.md",
"gsd-core/commands/gsd/quick-batch.md",
"gsd-core/commands/gsd/quick.md",
"gsd-core/commands/gsd/resume-work.md",
"gsd-core/commands/gsd/review-backlog.md",
"gsd-core/commands/gsd/review.md",
"gsd-core/commands/gsd/secure-phase.md",
"gsd-core/commands/gsd/settings.md",
"gsd-core/commands/gsd/ship.md",
"gsd-core/commands/gsd/sketch.md",
"gsd-core/commands/gsd/spec-phase.md",
"gsd-core/commands/gsd/spike.md",
"gsd-core/commands/gsd/stats.md",
"gsd-core/commands/gsd/surface.md",
"gsd-core/commands/gsd/thread.md",
"gsd-core/commands/gsd/ui-phase.md",
"gsd-core/commands/gsd/ui-review.md",
"gsd-core/commands/gsd/ultraplan-phase.md",
"gsd-core/commands/gsd/undo.md",
"gsd-core/commands/gsd/update.md",
"gsd-core/commands/gsd/validate-phase.md",
"gsd-core/commands/gsd/verify-work.md",
"gsd-core/commands/gsd/workspace.md",
"gsd-core/commands/gsd/workstreams.md",
"gsd-core/contexts/dev.md",
"gsd-core/contexts/research.md",
"gsd-core/contexts/review.md",

View File

@@ -37,6 +37,41 @@
"agents/gsd-verifier.md",
"gsd-core/.gsd-runtime",
"gsd-core/VERSION",
"gsd-core/agents/gsd-advisor-researcher.md",
"gsd-core/agents/gsd-ai-researcher.md",
"gsd-core/agents/gsd-assumptions-analyzer.md",
"gsd-core/agents/gsd-code-fixer.md",
"gsd-core/agents/gsd-code-reviewer.md",
"gsd-core/agents/gsd-codebase-mapper.md",
"gsd-core/agents/gsd-debug-session-manager.md",
"gsd-core/agents/gsd-debugger.md",
"gsd-core/agents/gsd-doc-classifier.md",
"gsd-core/agents/gsd-doc-synthesizer.md",
"gsd-core/agents/gsd-doc-verifier.md",
"gsd-core/agents/gsd-doc-writer.md",
"gsd-core/agents/gsd-dom-verifier.md",
"gsd-core/agents/gsd-domain-researcher.md",
"gsd-core/agents/gsd-eval-auditor.md",
"gsd-core/agents/gsd-eval-planner.md",
"gsd-core/agents/gsd-executor.md",
"gsd-core/agents/gsd-framework-selector.md",
"gsd-core/agents/gsd-integration-checker.md",
"gsd-core/agents/gsd-intel-updater.md",
"gsd-core/agents/gsd-mempalace-curator.md",
"gsd-core/agents/gsd-nyquist-auditor.md",
"gsd-core/agents/gsd-pattern-mapper.md",
"gsd-core/agents/gsd-phase-researcher.md",
"gsd-core/agents/gsd-plan-checker.md",
"gsd-core/agents/gsd-planner.md",
"gsd-core/agents/gsd-project-researcher.md",
"gsd-core/agents/gsd-research-synthesizer.md",
"gsd-core/agents/gsd-roadmapper.md",
"gsd-core/agents/gsd-security-auditor.md",
"gsd-core/agents/gsd-ui-auditor.md",
"gsd-core/agents/gsd-ui-checker.md",
"gsd-core/agents/gsd-ui-researcher.md",
"gsd-core/agents/gsd-user-profiler.md",
"gsd-core/agents/gsd-verifier.md",
"gsd-core/bin/check-latest-version.cjs",
"gsd-core/bin/ensure-runtime-build.cjs",
"gsd-core/bin/gsd-tools.cjs",
@@ -48,6 +83,78 @@
"gsd-core/bin/shared/model-catalog.json",
"gsd-core/bin/shared/runtime-aliases.manifest.json",
"gsd-core/bin/verify-reapply-patches.cjs",
"gsd-core/commands/gsd/add-tests.md",
"gsd-core/commands/gsd/ai-integration-phase.md",
"gsd-core/commands/gsd/audit-fix.md",
"gsd-core/commands/gsd/audit-milestone.md",
"gsd-core/commands/gsd/audit-uat.md",
"gsd-core/commands/gsd/autonomous.md",
"gsd-core/commands/gsd/capture.md",
"gsd-core/commands/gsd/cleanup.md",
"gsd-core/commands/gsd/code-review.md",
"gsd-core/commands/gsd/complete-milestone.md",
"gsd-core/commands/gsd/config.md",
"gsd-core/commands/gsd/debug.md",
"gsd-core/commands/gsd/discuss-phase.md",
"gsd-core/commands/gsd/docs-update.md",
"gsd-core/commands/gsd/eval-review.md",
"gsd-core/commands/gsd/execute-phase.md",
"gsd-core/commands/gsd/explore.md",
"gsd-core/commands/gsd/extract-learnings.md",
"gsd-core/commands/gsd/fast.md",
"gsd-core/commands/gsd/forensics.md",
"gsd-core/commands/gsd/graphify.md",
"gsd-core/commands/gsd/health.md",
"gsd-core/commands/gsd/help.md",
"gsd-core/commands/gsd/import.md",
"gsd-core/commands/gsd/inbox.md",
"gsd-core/commands/gsd/ingest-docs.md",
"gsd-core/commands/gsd/manager.md",
"gsd-core/commands/gsd/map-codebase.md",
"gsd-core/commands/gsd/mempalace-capture.md",
"gsd-core/commands/gsd/mempalace-recall.md",
"gsd-core/commands/gsd/milestone-summary.md",
"gsd-core/commands/gsd/mvp-phase.md",
"gsd-core/commands/gsd/new-milestone.md",
"gsd-core/commands/gsd/new-project.md",
"gsd-core/commands/gsd/next.md",
"gsd-core/commands/gsd/ns-context.md",
"gsd-core/commands/gsd/ns-ideate.md",
"gsd-core/commands/gsd/ns-manage.md",
"gsd-core/commands/gsd/ns-project.md",
"gsd-core/commands/gsd/ns-review.md",
"gsd-core/commands/gsd/ns-workflow.md",
"gsd-core/commands/gsd/onboard.md",
"gsd-core/commands/gsd/pause-work.md",
"gsd-core/commands/gsd/phase.md",
"gsd-core/commands/gsd/plan-phase.md",
"gsd-core/commands/gsd/plan-review-convergence.md",
"gsd-core/commands/gsd/pr-branch.md",
"gsd-core/commands/gsd/profile-user.md",
"gsd-core/commands/gsd/progress.md",
"gsd-core/commands/gsd/quick-batch.md",
"gsd-core/commands/gsd/quick.md",
"gsd-core/commands/gsd/resume-work.md",
"gsd-core/commands/gsd/review-backlog.md",
"gsd-core/commands/gsd/review.md",
"gsd-core/commands/gsd/secure-phase.md",
"gsd-core/commands/gsd/settings.md",
"gsd-core/commands/gsd/ship.md",
"gsd-core/commands/gsd/sketch.md",
"gsd-core/commands/gsd/spec-phase.md",
"gsd-core/commands/gsd/spike.md",
"gsd-core/commands/gsd/stats.md",
"gsd-core/commands/gsd/surface.md",
"gsd-core/commands/gsd/thread.md",
"gsd-core/commands/gsd/ui-phase.md",
"gsd-core/commands/gsd/ui-review.md",
"gsd-core/commands/gsd/ultraplan-phase.md",
"gsd-core/commands/gsd/undo.md",
"gsd-core/commands/gsd/update.md",
"gsd-core/commands/gsd/validate-phase.md",
"gsd-core/commands/gsd/verify-work.md",
"gsd-core/commands/gsd/workspace.md",
"gsd-core/commands/gsd/workstreams.md",
"gsd-core/contexts/dev.md",
"gsd-core/contexts/research.md",
"gsd-core/contexts/review.md",

View File

@@ -39,6 +39,41 @@
"agents/gsd-verifier.md",
"gsd-core/.gsd-runtime",
"gsd-core/VERSION",
"gsd-core/agents/gsd-advisor-researcher.md",
"gsd-core/agents/gsd-ai-researcher.md",
"gsd-core/agents/gsd-assumptions-analyzer.md",
"gsd-core/agents/gsd-code-fixer.md",
"gsd-core/agents/gsd-code-reviewer.md",
"gsd-core/agents/gsd-codebase-mapper.md",
"gsd-core/agents/gsd-debug-session-manager.md",
"gsd-core/agents/gsd-debugger.md",
"gsd-core/agents/gsd-doc-classifier.md",
"gsd-core/agents/gsd-doc-synthesizer.md",
"gsd-core/agents/gsd-doc-verifier.md",
"gsd-core/agents/gsd-doc-writer.md",
"gsd-core/agents/gsd-dom-verifier.md",
"gsd-core/agents/gsd-domain-researcher.md",
"gsd-core/agents/gsd-eval-auditor.md",
"gsd-core/agents/gsd-eval-planner.md",
"gsd-core/agents/gsd-executor.md",
"gsd-core/agents/gsd-framework-selector.md",
"gsd-core/agents/gsd-integration-checker.md",
"gsd-core/agents/gsd-intel-updater.md",
"gsd-core/agents/gsd-mempalace-curator.md",
"gsd-core/agents/gsd-nyquist-auditor.md",
"gsd-core/agents/gsd-pattern-mapper.md",
"gsd-core/agents/gsd-phase-researcher.md",
"gsd-core/agents/gsd-plan-checker.md",
"gsd-core/agents/gsd-planner.md",
"gsd-core/agents/gsd-project-researcher.md",
"gsd-core/agents/gsd-research-synthesizer.md",
"gsd-core/agents/gsd-roadmapper.md",
"gsd-core/agents/gsd-security-auditor.md",
"gsd-core/agents/gsd-ui-auditor.md",
"gsd-core/agents/gsd-ui-checker.md",
"gsd-core/agents/gsd-ui-researcher.md",
"gsd-core/agents/gsd-user-profiler.md",
"gsd-core/agents/gsd-verifier.md",
"gsd-core/bin/check-latest-version.cjs",
"gsd-core/bin/ensure-runtime-build.cjs",
"gsd-core/bin/gsd-tools.cjs",
@@ -50,6 +85,78 @@
"gsd-core/bin/shared/model-catalog.json",
"gsd-core/bin/shared/runtime-aliases.manifest.json",
"gsd-core/bin/verify-reapply-patches.cjs",
"gsd-core/commands/gsd/add-tests.md",
"gsd-core/commands/gsd/ai-integration-phase.md",
"gsd-core/commands/gsd/audit-fix.md",
"gsd-core/commands/gsd/audit-milestone.md",
"gsd-core/commands/gsd/audit-uat.md",
"gsd-core/commands/gsd/autonomous.md",
"gsd-core/commands/gsd/capture.md",
"gsd-core/commands/gsd/cleanup.md",
"gsd-core/commands/gsd/code-review.md",
"gsd-core/commands/gsd/complete-milestone.md",
"gsd-core/commands/gsd/config.md",
"gsd-core/commands/gsd/debug.md",
"gsd-core/commands/gsd/discuss-phase.md",
"gsd-core/commands/gsd/docs-update.md",
"gsd-core/commands/gsd/eval-review.md",
"gsd-core/commands/gsd/execute-phase.md",
"gsd-core/commands/gsd/explore.md",
"gsd-core/commands/gsd/extract-learnings.md",
"gsd-core/commands/gsd/fast.md",
"gsd-core/commands/gsd/forensics.md",
"gsd-core/commands/gsd/graphify.md",
"gsd-core/commands/gsd/health.md",
"gsd-core/commands/gsd/help.md",
"gsd-core/commands/gsd/import.md",
"gsd-core/commands/gsd/inbox.md",
"gsd-core/commands/gsd/ingest-docs.md",
"gsd-core/commands/gsd/manager.md",
"gsd-core/commands/gsd/map-codebase.md",
"gsd-core/commands/gsd/mempalace-capture.md",
"gsd-core/commands/gsd/mempalace-recall.md",
"gsd-core/commands/gsd/milestone-summary.md",
"gsd-core/commands/gsd/mvp-phase.md",
"gsd-core/commands/gsd/new-milestone.md",
"gsd-core/commands/gsd/new-project.md",
"gsd-core/commands/gsd/next.md",
"gsd-core/commands/gsd/ns-context.md",
"gsd-core/commands/gsd/ns-ideate.md",
"gsd-core/commands/gsd/ns-manage.md",
"gsd-core/commands/gsd/ns-project.md",
"gsd-core/commands/gsd/ns-review.md",
"gsd-core/commands/gsd/ns-workflow.md",
"gsd-core/commands/gsd/onboard.md",
"gsd-core/commands/gsd/pause-work.md",
"gsd-core/commands/gsd/phase.md",
"gsd-core/commands/gsd/plan-phase.md",
"gsd-core/commands/gsd/plan-review-convergence.md",
"gsd-core/commands/gsd/pr-branch.md",
"gsd-core/commands/gsd/profile-user.md",
"gsd-core/commands/gsd/progress.md",
"gsd-core/commands/gsd/quick-batch.md",
"gsd-core/commands/gsd/quick.md",
"gsd-core/commands/gsd/resume-work.md",
"gsd-core/commands/gsd/review-backlog.md",
"gsd-core/commands/gsd/review.md",
"gsd-core/commands/gsd/secure-phase.md",
"gsd-core/commands/gsd/settings.md",
"gsd-core/commands/gsd/ship.md",
"gsd-core/commands/gsd/sketch.md",
"gsd-core/commands/gsd/spec-phase.md",
"gsd-core/commands/gsd/spike.md",
"gsd-core/commands/gsd/stats.md",
"gsd-core/commands/gsd/surface.md",
"gsd-core/commands/gsd/thread.md",
"gsd-core/commands/gsd/ui-phase.md",
"gsd-core/commands/gsd/ui-review.md",
"gsd-core/commands/gsd/ultraplan-phase.md",
"gsd-core/commands/gsd/undo.md",
"gsd-core/commands/gsd/update.md",
"gsd-core/commands/gsd/validate-phase.md",
"gsd-core/commands/gsd/verify-work.md",
"gsd-core/commands/gsd/workspace.md",
"gsd-core/commands/gsd/workstreams.md",
"gsd-core/contexts/dev.md",
"gsd-core/contexts/research.md",
"gsd-core/contexts/review.md",

View File

@@ -109,6 +109,41 @@
"commands/gsd-workstreams.md",
"gsd-core/.gsd-runtime",
"gsd-core/VERSION",
"gsd-core/agents/gsd-advisor-researcher.md",
"gsd-core/agents/gsd-ai-researcher.md",
"gsd-core/agents/gsd-assumptions-analyzer.md",
"gsd-core/agents/gsd-code-fixer.md",
"gsd-core/agents/gsd-code-reviewer.md",
"gsd-core/agents/gsd-codebase-mapper.md",
"gsd-core/agents/gsd-debug-session-manager.md",
"gsd-core/agents/gsd-debugger.md",
"gsd-core/agents/gsd-doc-classifier.md",
"gsd-core/agents/gsd-doc-synthesizer.md",
"gsd-core/agents/gsd-doc-verifier.md",
"gsd-core/agents/gsd-doc-writer.md",
"gsd-core/agents/gsd-dom-verifier.md",
"gsd-core/agents/gsd-domain-researcher.md",
"gsd-core/agents/gsd-eval-auditor.md",
"gsd-core/agents/gsd-eval-planner.md",
"gsd-core/agents/gsd-executor.md",
"gsd-core/agents/gsd-framework-selector.md",
"gsd-core/agents/gsd-integration-checker.md",
"gsd-core/agents/gsd-intel-updater.md",
"gsd-core/agents/gsd-mempalace-curator.md",
"gsd-core/agents/gsd-nyquist-auditor.md",
"gsd-core/agents/gsd-pattern-mapper.md",
"gsd-core/agents/gsd-phase-researcher.md",
"gsd-core/agents/gsd-plan-checker.md",
"gsd-core/agents/gsd-planner.md",
"gsd-core/agents/gsd-project-researcher.md",
"gsd-core/agents/gsd-research-synthesizer.md",
"gsd-core/agents/gsd-roadmapper.md",
"gsd-core/agents/gsd-security-auditor.md",
"gsd-core/agents/gsd-ui-auditor.md",
"gsd-core/agents/gsd-ui-checker.md",
"gsd-core/agents/gsd-ui-researcher.md",
"gsd-core/agents/gsd-user-profiler.md",
"gsd-core/agents/gsd-verifier.md",
"gsd-core/bin/check-latest-version.cjs",
"gsd-core/bin/ensure-runtime-build.cjs",
"gsd-core/bin/gsd-tools.cjs",
@@ -120,6 +155,78 @@
"gsd-core/bin/shared/model-catalog.json",
"gsd-core/bin/shared/runtime-aliases.manifest.json",
"gsd-core/bin/verify-reapply-patches.cjs",
"gsd-core/commands/gsd/add-tests.md",
"gsd-core/commands/gsd/ai-integration-phase.md",
"gsd-core/commands/gsd/audit-fix.md",
"gsd-core/commands/gsd/audit-milestone.md",
"gsd-core/commands/gsd/audit-uat.md",
"gsd-core/commands/gsd/autonomous.md",
"gsd-core/commands/gsd/capture.md",
"gsd-core/commands/gsd/cleanup.md",
"gsd-core/commands/gsd/code-review.md",
"gsd-core/commands/gsd/complete-milestone.md",
"gsd-core/commands/gsd/config.md",
"gsd-core/commands/gsd/debug.md",
"gsd-core/commands/gsd/discuss-phase.md",
"gsd-core/commands/gsd/docs-update.md",
"gsd-core/commands/gsd/eval-review.md",
"gsd-core/commands/gsd/execute-phase.md",
"gsd-core/commands/gsd/explore.md",
"gsd-core/commands/gsd/extract-learnings.md",
"gsd-core/commands/gsd/fast.md",
"gsd-core/commands/gsd/forensics.md",
"gsd-core/commands/gsd/graphify.md",
"gsd-core/commands/gsd/health.md",
"gsd-core/commands/gsd/help.md",
"gsd-core/commands/gsd/import.md",
"gsd-core/commands/gsd/inbox.md",
"gsd-core/commands/gsd/ingest-docs.md",
"gsd-core/commands/gsd/manager.md",
"gsd-core/commands/gsd/map-codebase.md",
"gsd-core/commands/gsd/mempalace-capture.md",
"gsd-core/commands/gsd/mempalace-recall.md",
"gsd-core/commands/gsd/milestone-summary.md",
"gsd-core/commands/gsd/mvp-phase.md",
"gsd-core/commands/gsd/new-milestone.md",
"gsd-core/commands/gsd/new-project.md",
"gsd-core/commands/gsd/next.md",
"gsd-core/commands/gsd/ns-context.md",
"gsd-core/commands/gsd/ns-ideate.md",
"gsd-core/commands/gsd/ns-manage.md",
"gsd-core/commands/gsd/ns-project.md",
"gsd-core/commands/gsd/ns-review.md",
"gsd-core/commands/gsd/ns-workflow.md",
"gsd-core/commands/gsd/onboard.md",
"gsd-core/commands/gsd/pause-work.md",
"gsd-core/commands/gsd/phase.md",
"gsd-core/commands/gsd/plan-phase.md",
"gsd-core/commands/gsd/plan-review-convergence.md",
"gsd-core/commands/gsd/pr-branch.md",
"gsd-core/commands/gsd/profile-user.md",
"gsd-core/commands/gsd/progress.md",
"gsd-core/commands/gsd/quick-batch.md",
"gsd-core/commands/gsd/quick.md",
"gsd-core/commands/gsd/resume-work.md",
"gsd-core/commands/gsd/review-backlog.md",
"gsd-core/commands/gsd/review.md",
"gsd-core/commands/gsd/secure-phase.md",
"gsd-core/commands/gsd/settings.md",
"gsd-core/commands/gsd/ship.md",
"gsd-core/commands/gsd/sketch.md",
"gsd-core/commands/gsd/spec-phase.md",
"gsd-core/commands/gsd/spike.md",
"gsd-core/commands/gsd/stats.md",
"gsd-core/commands/gsd/surface.md",
"gsd-core/commands/gsd/thread.md",
"gsd-core/commands/gsd/ui-phase.md",
"gsd-core/commands/gsd/ui-review.md",
"gsd-core/commands/gsd/ultraplan-phase.md",
"gsd-core/commands/gsd/undo.md",
"gsd-core/commands/gsd/update.md",
"gsd-core/commands/gsd/validate-phase.md",
"gsd-core/commands/gsd/verify-work.md",
"gsd-core/commands/gsd/workspace.md",
"gsd-core/commands/gsd/workstreams.md",
"gsd-core/contexts/dev.md",
"gsd-core/contexts/research.md",
"gsd-core/contexts/review.md",

View File

@@ -73,6 +73,41 @@
"config.toml",
"gsd-core/.gsd-runtime",
"gsd-core/VERSION",
"gsd-core/agents/gsd-advisor-researcher.md",
"gsd-core/agents/gsd-ai-researcher.md",
"gsd-core/agents/gsd-assumptions-analyzer.md",
"gsd-core/agents/gsd-code-fixer.md",
"gsd-core/agents/gsd-code-reviewer.md",
"gsd-core/agents/gsd-codebase-mapper.md",
"gsd-core/agents/gsd-debug-session-manager.md",
"gsd-core/agents/gsd-debugger.md",
"gsd-core/agents/gsd-doc-classifier.md",
"gsd-core/agents/gsd-doc-synthesizer.md",
"gsd-core/agents/gsd-doc-verifier.md",
"gsd-core/agents/gsd-doc-writer.md",
"gsd-core/agents/gsd-dom-verifier.md",
"gsd-core/agents/gsd-domain-researcher.md",
"gsd-core/agents/gsd-eval-auditor.md",
"gsd-core/agents/gsd-eval-planner.md",
"gsd-core/agents/gsd-executor.md",
"gsd-core/agents/gsd-framework-selector.md",
"gsd-core/agents/gsd-integration-checker.md",
"gsd-core/agents/gsd-intel-updater.md",
"gsd-core/agents/gsd-mempalace-curator.md",
"gsd-core/agents/gsd-nyquist-auditor.md",
"gsd-core/agents/gsd-pattern-mapper.md",
"gsd-core/agents/gsd-phase-researcher.md",
"gsd-core/agents/gsd-plan-checker.md",
"gsd-core/agents/gsd-planner.md",
"gsd-core/agents/gsd-project-researcher.md",
"gsd-core/agents/gsd-research-synthesizer.md",
"gsd-core/agents/gsd-roadmapper.md",
"gsd-core/agents/gsd-security-auditor.md",
"gsd-core/agents/gsd-ui-auditor.md",
"gsd-core/agents/gsd-ui-checker.md",
"gsd-core/agents/gsd-ui-researcher.md",
"gsd-core/agents/gsd-user-profiler.md",
"gsd-core/agents/gsd-verifier.md",
"gsd-core/bin/check-latest-version.cjs",
"gsd-core/bin/ensure-runtime-build.cjs",
"gsd-core/bin/gsd-tools.cjs",
@@ -84,6 +119,78 @@
"gsd-core/bin/shared/model-catalog.json",
"gsd-core/bin/shared/runtime-aliases.manifest.json",
"gsd-core/bin/verify-reapply-patches.cjs",
"gsd-core/commands/gsd/add-tests.md",
"gsd-core/commands/gsd/ai-integration-phase.md",
"gsd-core/commands/gsd/audit-fix.md",
"gsd-core/commands/gsd/audit-milestone.md",
"gsd-core/commands/gsd/audit-uat.md",
"gsd-core/commands/gsd/autonomous.md",
"gsd-core/commands/gsd/capture.md",
"gsd-core/commands/gsd/cleanup.md",
"gsd-core/commands/gsd/code-review.md",
"gsd-core/commands/gsd/complete-milestone.md",
"gsd-core/commands/gsd/config.md",
"gsd-core/commands/gsd/debug.md",
"gsd-core/commands/gsd/discuss-phase.md",
"gsd-core/commands/gsd/docs-update.md",
"gsd-core/commands/gsd/eval-review.md",
"gsd-core/commands/gsd/execute-phase.md",
"gsd-core/commands/gsd/explore.md",
"gsd-core/commands/gsd/extract-learnings.md",
"gsd-core/commands/gsd/fast.md",
"gsd-core/commands/gsd/forensics.md",
"gsd-core/commands/gsd/graphify.md",
"gsd-core/commands/gsd/health.md",
"gsd-core/commands/gsd/help.md",
"gsd-core/commands/gsd/import.md",
"gsd-core/commands/gsd/inbox.md",
"gsd-core/commands/gsd/ingest-docs.md",
"gsd-core/commands/gsd/manager.md",
"gsd-core/commands/gsd/map-codebase.md",
"gsd-core/commands/gsd/mempalace-capture.md",
"gsd-core/commands/gsd/mempalace-recall.md",
"gsd-core/commands/gsd/milestone-summary.md",
"gsd-core/commands/gsd/mvp-phase.md",
"gsd-core/commands/gsd/new-milestone.md",
"gsd-core/commands/gsd/new-project.md",
"gsd-core/commands/gsd/next.md",
"gsd-core/commands/gsd/ns-context.md",
"gsd-core/commands/gsd/ns-ideate.md",
"gsd-core/commands/gsd/ns-manage.md",
"gsd-core/commands/gsd/ns-project.md",
"gsd-core/commands/gsd/ns-review.md",
"gsd-core/commands/gsd/ns-workflow.md",
"gsd-core/commands/gsd/onboard.md",
"gsd-core/commands/gsd/pause-work.md",
"gsd-core/commands/gsd/phase.md",
"gsd-core/commands/gsd/plan-phase.md",
"gsd-core/commands/gsd/plan-review-convergence.md",
"gsd-core/commands/gsd/pr-branch.md",
"gsd-core/commands/gsd/profile-user.md",
"gsd-core/commands/gsd/progress.md",
"gsd-core/commands/gsd/quick-batch.md",
"gsd-core/commands/gsd/quick.md",
"gsd-core/commands/gsd/resume-work.md",
"gsd-core/commands/gsd/review-backlog.md",
"gsd-core/commands/gsd/review.md",
"gsd-core/commands/gsd/secure-phase.md",
"gsd-core/commands/gsd/settings.md",
"gsd-core/commands/gsd/ship.md",
"gsd-core/commands/gsd/sketch.md",
"gsd-core/commands/gsd/spec-phase.md",
"gsd-core/commands/gsd/spike.md",
"gsd-core/commands/gsd/stats.md",
"gsd-core/commands/gsd/surface.md",
"gsd-core/commands/gsd/thread.md",
"gsd-core/commands/gsd/ui-phase.md",
"gsd-core/commands/gsd/ui-review.md",
"gsd-core/commands/gsd/ultraplan-phase.md",
"gsd-core/commands/gsd/undo.md",
"gsd-core/commands/gsd/update.md",
"gsd-core/commands/gsd/validate-phase.md",
"gsd-core/commands/gsd/verify-work.md",
"gsd-core/commands/gsd/workspace.md",
"gsd-core/commands/gsd/workstreams.md",
"gsd-core/contexts/dev.md",
"gsd-core/contexts/research.md",
"gsd-core/contexts/review.md",

View File

@@ -38,6 +38,41 @@
"copilot-instructions.md",
"gsd-core/.gsd-runtime",
"gsd-core/VERSION",
"gsd-core/agents/gsd-advisor-researcher.md",
"gsd-core/agents/gsd-ai-researcher.md",
"gsd-core/agents/gsd-assumptions-analyzer.md",
"gsd-core/agents/gsd-code-fixer.md",
"gsd-core/agents/gsd-code-reviewer.md",
"gsd-core/agents/gsd-codebase-mapper.md",
"gsd-core/agents/gsd-debug-session-manager.md",
"gsd-core/agents/gsd-debugger.md",
"gsd-core/agents/gsd-doc-classifier.md",
"gsd-core/agents/gsd-doc-synthesizer.md",
"gsd-core/agents/gsd-doc-verifier.md",
"gsd-core/agents/gsd-doc-writer.md",
"gsd-core/agents/gsd-dom-verifier.md",
"gsd-core/agents/gsd-domain-researcher.md",
"gsd-core/agents/gsd-eval-auditor.md",
"gsd-core/agents/gsd-eval-planner.md",
"gsd-core/agents/gsd-executor.md",
"gsd-core/agents/gsd-framework-selector.md",
"gsd-core/agents/gsd-integration-checker.md",
"gsd-core/agents/gsd-intel-updater.md",
"gsd-core/agents/gsd-mempalace-curator.md",
"gsd-core/agents/gsd-nyquist-auditor.md",
"gsd-core/agents/gsd-pattern-mapper.md",
"gsd-core/agents/gsd-phase-researcher.md",
"gsd-core/agents/gsd-plan-checker.md",
"gsd-core/agents/gsd-planner.md",
"gsd-core/agents/gsd-project-researcher.md",
"gsd-core/agents/gsd-research-synthesizer.md",
"gsd-core/agents/gsd-roadmapper.md",
"gsd-core/agents/gsd-security-auditor.md",
"gsd-core/agents/gsd-ui-auditor.md",
"gsd-core/agents/gsd-ui-checker.md",
"gsd-core/agents/gsd-ui-researcher.md",
"gsd-core/agents/gsd-user-profiler.md",
"gsd-core/agents/gsd-verifier.md",
"gsd-core/bin/check-latest-version.cjs",
"gsd-core/bin/ensure-runtime-build.cjs",
"gsd-core/bin/gsd-tools.cjs",
@@ -49,6 +84,78 @@
"gsd-core/bin/shared/model-catalog.json",
"gsd-core/bin/shared/runtime-aliases.manifest.json",
"gsd-core/bin/verify-reapply-patches.cjs",
"gsd-core/commands/gsd/add-tests.md",
"gsd-core/commands/gsd/ai-integration-phase.md",
"gsd-core/commands/gsd/audit-fix.md",
"gsd-core/commands/gsd/audit-milestone.md",
"gsd-core/commands/gsd/audit-uat.md",
"gsd-core/commands/gsd/autonomous.md",
"gsd-core/commands/gsd/capture.md",
"gsd-core/commands/gsd/cleanup.md",
"gsd-core/commands/gsd/code-review.md",
"gsd-core/commands/gsd/complete-milestone.md",
"gsd-core/commands/gsd/config.md",
"gsd-core/commands/gsd/debug.md",
"gsd-core/commands/gsd/discuss-phase.md",
"gsd-core/commands/gsd/docs-update.md",
"gsd-core/commands/gsd/eval-review.md",
"gsd-core/commands/gsd/execute-phase.md",
"gsd-core/commands/gsd/explore.md",
"gsd-core/commands/gsd/extract-learnings.md",
"gsd-core/commands/gsd/fast.md",
"gsd-core/commands/gsd/forensics.md",
"gsd-core/commands/gsd/graphify.md",
"gsd-core/commands/gsd/health.md",
"gsd-core/commands/gsd/help.md",
"gsd-core/commands/gsd/import.md",
"gsd-core/commands/gsd/inbox.md",
"gsd-core/commands/gsd/ingest-docs.md",
"gsd-core/commands/gsd/manager.md",
"gsd-core/commands/gsd/map-codebase.md",
"gsd-core/commands/gsd/mempalace-capture.md",
"gsd-core/commands/gsd/mempalace-recall.md",
"gsd-core/commands/gsd/milestone-summary.md",
"gsd-core/commands/gsd/mvp-phase.md",
"gsd-core/commands/gsd/new-milestone.md",
"gsd-core/commands/gsd/new-project.md",
"gsd-core/commands/gsd/next.md",
"gsd-core/commands/gsd/ns-context.md",
"gsd-core/commands/gsd/ns-ideate.md",
"gsd-core/commands/gsd/ns-manage.md",
"gsd-core/commands/gsd/ns-project.md",
"gsd-core/commands/gsd/ns-review.md",
"gsd-core/commands/gsd/ns-workflow.md",
"gsd-core/commands/gsd/onboard.md",
"gsd-core/commands/gsd/pause-work.md",
"gsd-core/commands/gsd/phase.md",
"gsd-core/commands/gsd/plan-phase.md",
"gsd-core/commands/gsd/plan-review-convergence.md",
"gsd-core/commands/gsd/pr-branch.md",
"gsd-core/commands/gsd/profile-user.md",
"gsd-core/commands/gsd/progress.md",
"gsd-core/commands/gsd/quick-batch.md",
"gsd-core/commands/gsd/quick.md",
"gsd-core/commands/gsd/resume-work.md",
"gsd-core/commands/gsd/review-backlog.md",
"gsd-core/commands/gsd/review.md",
"gsd-core/commands/gsd/secure-phase.md",
"gsd-core/commands/gsd/settings.md",
"gsd-core/commands/gsd/ship.md",
"gsd-core/commands/gsd/sketch.md",
"gsd-core/commands/gsd/spec-phase.md",
"gsd-core/commands/gsd/spike.md",
"gsd-core/commands/gsd/stats.md",
"gsd-core/commands/gsd/surface.md",
"gsd-core/commands/gsd/thread.md",
"gsd-core/commands/gsd/ui-phase.md",
"gsd-core/commands/gsd/ui-review.md",
"gsd-core/commands/gsd/ultraplan-phase.md",
"gsd-core/commands/gsd/undo.md",
"gsd-core/commands/gsd/update.md",
"gsd-core/commands/gsd/validate-phase.md",
"gsd-core/commands/gsd/verify-work.md",
"gsd-core/commands/gsd/workspace.md",
"gsd-core/commands/gsd/workstreams.md",
"gsd-core/contexts/dev.md",
"gsd-core/contexts/research.md",
"gsd-core/contexts/review.md",

View File

@@ -37,6 +37,41 @@
"agents/gsd-verifier.md",
"gsd-core/.gsd-runtime",
"gsd-core/VERSION",
"gsd-core/agents/gsd-advisor-researcher.md",
"gsd-core/agents/gsd-ai-researcher.md",
"gsd-core/agents/gsd-assumptions-analyzer.md",
"gsd-core/agents/gsd-code-fixer.md",
"gsd-core/agents/gsd-code-reviewer.md",
"gsd-core/agents/gsd-codebase-mapper.md",
"gsd-core/agents/gsd-debug-session-manager.md",
"gsd-core/agents/gsd-debugger.md",
"gsd-core/agents/gsd-doc-classifier.md",
"gsd-core/agents/gsd-doc-synthesizer.md",
"gsd-core/agents/gsd-doc-verifier.md",
"gsd-core/agents/gsd-doc-writer.md",
"gsd-core/agents/gsd-dom-verifier.md",
"gsd-core/agents/gsd-domain-researcher.md",
"gsd-core/agents/gsd-eval-auditor.md",
"gsd-core/agents/gsd-eval-planner.md",
"gsd-core/agents/gsd-executor.md",
"gsd-core/agents/gsd-framework-selector.md",
"gsd-core/agents/gsd-integration-checker.md",
"gsd-core/agents/gsd-intel-updater.md",
"gsd-core/agents/gsd-mempalace-curator.md",
"gsd-core/agents/gsd-nyquist-auditor.md",
"gsd-core/agents/gsd-pattern-mapper.md",
"gsd-core/agents/gsd-phase-researcher.md",
"gsd-core/agents/gsd-plan-checker.md",
"gsd-core/agents/gsd-planner.md",
"gsd-core/agents/gsd-project-researcher.md",
"gsd-core/agents/gsd-research-synthesizer.md",
"gsd-core/agents/gsd-roadmapper.md",
"gsd-core/agents/gsd-security-auditor.md",
"gsd-core/agents/gsd-ui-auditor.md",
"gsd-core/agents/gsd-ui-checker.md",
"gsd-core/agents/gsd-ui-researcher.md",
"gsd-core/agents/gsd-user-profiler.md",
"gsd-core/agents/gsd-verifier.md",
"gsd-core/bin/check-latest-version.cjs",
"gsd-core/bin/ensure-runtime-build.cjs",
"gsd-core/bin/gsd-tools.cjs",
@@ -48,6 +83,78 @@
"gsd-core/bin/shared/model-catalog.json",
"gsd-core/bin/shared/runtime-aliases.manifest.json",
"gsd-core/bin/verify-reapply-patches.cjs",
"gsd-core/commands/gsd/add-tests.md",
"gsd-core/commands/gsd/ai-integration-phase.md",
"gsd-core/commands/gsd/audit-fix.md",
"gsd-core/commands/gsd/audit-milestone.md",
"gsd-core/commands/gsd/audit-uat.md",
"gsd-core/commands/gsd/autonomous.md",
"gsd-core/commands/gsd/capture.md",
"gsd-core/commands/gsd/cleanup.md",
"gsd-core/commands/gsd/code-review.md",
"gsd-core/commands/gsd/complete-milestone.md",
"gsd-core/commands/gsd/config.md",
"gsd-core/commands/gsd/debug.md",
"gsd-core/commands/gsd/discuss-phase.md",
"gsd-core/commands/gsd/docs-update.md",
"gsd-core/commands/gsd/eval-review.md",
"gsd-core/commands/gsd/execute-phase.md",
"gsd-core/commands/gsd/explore.md",
"gsd-core/commands/gsd/extract-learnings.md",
"gsd-core/commands/gsd/fast.md",
"gsd-core/commands/gsd/forensics.md",
"gsd-core/commands/gsd/graphify.md",
"gsd-core/commands/gsd/health.md",
"gsd-core/commands/gsd/help.md",
"gsd-core/commands/gsd/import.md",
"gsd-core/commands/gsd/inbox.md",
"gsd-core/commands/gsd/ingest-docs.md",
"gsd-core/commands/gsd/manager.md",
"gsd-core/commands/gsd/map-codebase.md",
"gsd-core/commands/gsd/mempalace-capture.md",
"gsd-core/commands/gsd/mempalace-recall.md",
"gsd-core/commands/gsd/milestone-summary.md",
"gsd-core/commands/gsd/mvp-phase.md",
"gsd-core/commands/gsd/new-milestone.md",
"gsd-core/commands/gsd/new-project.md",
"gsd-core/commands/gsd/next.md",
"gsd-core/commands/gsd/ns-context.md",
"gsd-core/commands/gsd/ns-ideate.md",
"gsd-core/commands/gsd/ns-manage.md",
"gsd-core/commands/gsd/ns-project.md",
"gsd-core/commands/gsd/ns-review.md",
"gsd-core/commands/gsd/ns-workflow.md",
"gsd-core/commands/gsd/onboard.md",
"gsd-core/commands/gsd/pause-work.md",
"gsd-core/commands/gsd/phase.md",
"gsd-core/commands/gsd/plan-phase.md",
"gsd-core/commands/gsd/plan-review-convergence.md",
"gsd-core/commands/gsd/pr-branch.md",
"gsd-core/commands/gsd/profile-user.md",
"gsd-core/commands/gsd/progress.md",
"gsd-core/commands/gsd/quick-batch.md",
"gsd-core/commands/gsd/quick.md",
"gsd-core/commands/gsd/resume-work.md",
"gsd-core/commands/gsd/review-backlog.md",
"gsd-core/commands/gsd/review.md",
"gsd-core/commands/gsd/secure-phase.md",
"gsd-core/commands/gsd/settings.md",
"gsd-core/commands/gsd/ship.md",
"gsd-core/commands/gsd/sketch.md",
"gsd-core/commands/gsd/spec-phase.md",
"gsd-core/commands/gsd/spike.md",
"gsd-core/commands/gsd/stats.md",
"gsd-core/commands/gsd/surface.md",
"gsd-core/commands/gsd/thread.md",
"gsd-core/commands/gsd/ui-phase.md",
"gsd-core/commands/gsd/ui-review.md",
"gsd-core/commands/gsd/ultraplan-phase.md",
"gsd-core/commands/gsd/undo.md",
"gsd-core/commands/gsd/update.md",
"gsd-core/commands/gsd/validate-phase.md",
"gsd-core/commands/gsd/verify-work.md",
"gsd-core/commands/gsd/workspace.md",
"gsd-core/commands/gsd/workstreams.md",
"gsd-core/contexts/dev.md",
"gsd-core/contexts/research.md",
"gsd-core/contexts/review.md",

View File

@@ -37,6 +37,41 @@
"agents/gsd-verifier.md",
"gsd-core/.gsd-runtime",
"gsd-core/VERSION",
"gsd-core/agents/gsd-advisor-researcher.md",
"gsd-core/agents/gsd-ai-researcher.md",
"gsd-core/agents/gsd-assumptions-analyzer.md",
"gsd-core/agents/gsd-code-fixer.md",
"gsd-core/agents/gsd-code-reviewer.md",
"gsd-core/agents/gsd-codebase-mapper.md",
"gsd-core/agents/gsd-debug-session-manager.md",
"gsd-core/agents/gsd-debugger.md",
"gsd-core/agents/gsd-doc-classifier.md",
"gsd-core/agents/gsd-doc-synthesizer.md",
"gsd-core/agents/gsd-doc-verifier.md",
"gsd-core/agents/gsd-doc-writer.md",
"gsd-core/agents/gsd-dom-verifier.md",
"gsd-core/agents/gsd-domain-researcher.md",
"gsd-core/agents/gsd-eval-auditor.md",
"gsd-core/agents/gsd-eval-planner.md",
"gsd-core/agents/gsd-executor.md",
"gsd-core/agents/gsd-framework-selector.md",
"gsd-core/agents/gsd-integration-checker.md",
"gsd-core/agents/gsd-intel-updater.md",
"gsd-core/agents/gsd-mempalace-curator.md",
"gsd-core/agents/gsd-nyquist-auditor.md",
"gsd-core/agents/gsd-pattern-mapper.md",
"gsd-core/agents/gsd-phase-researcher.md",
"gsd-core/agents/gsd-plan-checker.md",
"gsd-core/agents/gsd-planner.md",
"gsd-core/agents/gsd-project-researcher.md",
"gsd-core/agents/gsd-research-synthesizer.md",
"gsd-core/agents/gsd-roadmapper.md",
"gsd-core/agents/gsd-security-auditor.md",
"gsd-core/agents/gsd-ui-auditor.md",
"gsd-core/agents/gsd-ui-checker.md",
"gsd-core/agents/gsd-ui-researcher.md",
"gsd-core/agents/gsd-user-profiler.md",
"gsd-core/agents/gsd-verifier.md",
"gsd-core/bin/check-latest-version.cjs",
"gsd-core/bin/ensure-runtime-build.cjs",
"gsd-core/bin/gsd-tools.cjs",
@@ -48,6 +83,78 @@
"gsd-core/bin/shared/model-catalog.json",
"gsd-core/bin/shared/runtime-aliases.manifest.json",
"gsd-core/bin/verify-reapply-patches.cjs",
"gsd-core/commands/gsd/add-tests.md",
"gsd-core/commands/gsd/ai-integration-phase.md",
"gsd-core/commands/gsd/audit-fix.md",
"gsd-core/commands/gsd/audit-milestone.md",
"gsd-core/commands/gsd/audit-uat.md",
"gsd-core/commands/gsd/autonomous.md",
"gsd-core/commands/gsd/capture.md",
"gsd-core/commands/gsd/cleanup.md",
"gsd-core/commands/gsd/code-review.md",
"gsd-core/commands/gsd/complete-milestone.md",
"gsd-core/commands/gsd/config.md",
"gsd-core/commands/gsd/debug.md",
"gsd-core/commands/gsd/discuss-phase.md",
"gsd-core/commands/gsd/docs-update.md",
"gsd-core/commands/gsd/eval-review.md",
"gsd-core/commands/gsd/execute-phase.md",
"gsd-core/commands/gsd/explore.md",
"gsd-core/commands/gsd/extract-learnings.md",
"gsd-core/commands/gsd/fast.md",
"gsd-core/commands/gsd/forensics.md",
"gsd-core/commands/gsd/graphify.md",
"gsd-core/commands/gsd/health.md",
"gsd-core/commands/gsd/help.md",
"gsd-core/commands/gsd/import.md",
"gsd-core/commands/gsd/inbox.md",
"gsd-core/commands/gsd/ingest-docs.md",
"gsd-core/commands/gsd/manager.md",
"gsd-core/commands/gsd/map-codebase.md",
"gsd-core/commands/gsd/mempalace-capture.md",
"gsd-core/commands/gsd/mempalace-recall.md",
"gsd-core/commands/gsd/milestone-summary.md",
"gsd-core/commands/gsd/mvp-phase.md",
"gsd-core/commands/gsd/new-milestone.md",
"gsd-core/commands/gsd/new-project.md",
"gsd-core/commands/gsd/next.md",
"gsd-core/commands/gsd/ns-context.md",
"gsd-core/commands/gsd/ns-ideate.md",
"gsd-core/commands/gsd/ns-manage.md",
"gsd-core/commands/gsd/ns-project.md",
"gsd-core/commands/gsd/ns-review.md",
"gsd-core/commands/gsd/ns-workflow.md",
"gsd-core/commands/gsd/onboard.md",
"gsd-core/commands/gsd/pause-work.md",
"gsd-core/commands/gsd/phase.md",
"gsd-core/commands/gsd/plan-phase.md",
"gsd-core/commands/gsd/plan-review-convergence.md",
"gsd-core/commands/gsd/pr-branch.md",
"gsd-core/commands/gsd/profile-user.md",
"gsd-core/commands/gsd/progress.md",
"gsd-core/commands/gsd/quick-batch.md",
"gsd-core/commands/gsd/quick.md",
"gsd-core/commands/gsd/resume-work.md",
"gsd-core/commands/gsd/review-backlog.md",
"gsd-core/commands/gsd/review.md",
"gsd-core/commands/gsd/secure-phase.md",
"gsd-core/commands/gsd/settings.md",
"gsd-core/commands/gsd/ship.md",
"gsd-core/commands/gsd/sketch.md",
"gsd-core/commands/gsd/spec-phase.md",
"gsd-core/commands/gsd/spike.md",
"gsd-core/commands/gsd/stats.md",
"gsd-core/commands/gsd/surface.md",
"gsd-core/commands/gsd/thread.md",
"gsd-core/commands/gsd/ui-phase.md",
"gsd-core/commands/gsd/ui-review.md",
"gsd-core/commands/gsd/ultraplan-phase.md",
"gsd-core/commands/gsd/undo.md",
"gsd-core/commands/gsd/update.md",
"gsd-core/commands/gsd/validate-phase.md",
"gsd-core/commands/gsd/verify-work.md",
"gsd-core/commands/gsd/workspace.md",
"gsd-core/commands/gsd/workstreams.md",
"gsd-core/contexts/dev.md",
"gsd-core/contexts/research.md",
"gsd-core/contexts/review.md",

View File

@@ -109,6 +109,41 @@
"command/gsd-workstreams.md",
"gsd-core/.gsd-runtime",
"gsd-core/VERSION",
"gsd-core/agents/gsd-advisor-researcher.md",
"gsd-core/agents/gsd-ai-researcher.md",
"gsd-core/agents/gsd-assumptions-analyzer.md",
"gsd-core/agents/gsd-code-fixer.md",
"gsd-core/agents/gsd-code-reviewer.md",
"gsd-core/agents/gsd-codebase-mapper.md",
"gsd-core/agents/gsd-debug-session-manager.md",
"gsd-core/agents/gsd-debugger.md",
"gsd-core/agents/gsd-doc-classifier.md",
"gsd-core/agents/gsd-doc-synthesizer.md",
"gsd-core/agents/gsd-doc-verifier.md",
"gsd-core/agents/gsd-doc-writer.md",
"gsd-core/agents/gsd-dom-verifier.md",
"gsd-core/agents/gsd-domain-researcher.md",
"gsd-core/agents/gsd-eval-auditor.md",
"gsd-core/agents/gsd-eval-planner.md",
"gsd-core/agents/gsd-executor.md",
"gsd-core/agents/gsd-framework-selector.md",
"gsd-core/agents/gsd-integration-checker.md",
"gsd-core/agents/gsd-intel-updater.md",
"gsd-core/agents/gsd-mempalace-curator.md",
"gsd-core/agents/gsd-nyquist-auditor.md",
"gsd-core/agents/gsd-pattern-mapper.md",
"gsd-core/agents/gsd-phase-researcher.md",
"gsd-core/agents/gsd-plan-checker.md",
"gsd-core/agents/gsd-planner.md",
"gsd-core/agents/gsd-project-researcher.md",
"gsd-core/agents/gsd-research-synthesizer.md",
"gsd-core/agents/gsd-roadmapper.md",
"gsd-core/agents/gsd-security-auditor.md",
"gsd-core/agents/gsd-ui-auditor.md",
"gsd-core/agents/gsd-ui-checker.md",
"gsd-core/agents/gsd-ui-researcher.md",
"gsd-core/agents/gsd-user-profiler.md",
"gsd-core/agents/gsd-verifier.md",
"gsd-core/bin/check-latest-version.cjs",
"gsd-core/bin/ensure-runtime-build.cjs",
"gsd-core/bin/gsd-tools.cjs",
@@ -120,6 +155,78 @@
"gsd-core/bin/shared/model-catalog.json",
"gsd-core/bin/shared/runtime-aliases.manifest.json",
"gsd-core/bin/verify-reapply-patches.cjs",
"gsd-core/commands/gsd/add-tests.md",
"gsd-core/commands/gsd/ai-integration-phase.md",
"gsd-core/commands/gsd/audit-fix.md",
"gsd-core/commands/gsd/audit-milestone.md",
"gsd-core/commands/gsd/audit-uat.md",
"gsd-core/commands/gsd/autonomous.md",
"gsd-core/commands/gsd/capture.md",
"gsd-core/commands/gsd/cleanup.md",
"gsd-core/commands/gsd/code-review.md",
"gsd-core/commands/gsd/complete-milestone.md",
"gsd-core/commands/gsd/config.md",
"gsd-core/commands/gsd/debug.md",
"gsd-core/commands/gsd/discuss-phase.md",
"gsd-core/commands/gsd/docs-update.md",
"gsd-core/commands/gsd/eval-review.md",
"gsd-core/commands/gsd/execute-phase.md",
"gsd-core/commands/gsd/explore.md",
"gsd-core/commands/gsd/extract-learnings.md",
"gsd-core/commands/gsd/fast.md",
"gsd-core/commands/gsd/forensics.md",
"gsd-core/commands/gsd/graphify.md",
"gsd-core/commands/gsd/health.md",
"gsd-core/commands/gsd/help.md",
"gsd-core/commands/gsd/import.md",
"gsd-core/commands/gsd/inbox.md",
"gsd-core/commands/gsd/ingest-docs.md",
"gsd-core/commands/gsd/manager.md",
"gsd-core/commands/gsd/map-codebase.md",
"gsd-core/commands/gsd/mempalace-capture.md",
"gsd-core/commands/gsd/mempalace-recall.md",
"gsd-core/commands/gsd/milestone-summary.md",
"gsd-core/commands/gsd/mvp-phase.md",
"gsd-core/commands/gsd/new-milestone.md",
"gsd-core/commands/gsd/new-project.md",
"gsd-core/commands/gsd/next.md",
"gsd-core/commands/gsd/ns-context.md",
"gsd-core/commands/gsd/ns-ideate.md",
"gsd-core/commands/gsd/ns-manage.md",
"gsd-core/commands/gsd/ns-project.md",
"gsd-core/commands/gsd/ns-review.md",
"gsd-core/commands/gsd/ns-workflow.md",
"gsd-core/commands/gsd/onboard.md",
"gsd-core/commands/gsd/pause-work.md",
"gsd-core/commands/gsd/phase.md",
"gsd-core/commands/gsd/plan-phase.md",
"gsd-core/commands/gsd/plan-review-convergence.md",
"gsd-core/commands/gsd/pr-branch.md",
"gsd-core/commands/gsd/profile-user.md",
"gsd-core/commands/gsd/progress.md",
"gsd-core/commands/gsd/quick-batch.md",
"gsd-core/commands/gsd/quick.md",
"gsd-core/commands/gsd/resume-work.md",
"gsd-core/commands/gsd/review-backlog.md",
"gsd-core/commands/gsd/review.md",
"gsd-core/commands/gsd/secure-phase.md",
"gsd-core/commands/gsd/settings.md",
"gsd-core/commands/gsd/ship.md",
"gsd-core/commands/gsd/sketch.md",
"gsd-core/commands/gsd/spec-phase.md",
"gsd-core/commands/gsd/spike.md",
"gsd-core/commands/gsd/stats.md",
"gsd-core/commands/gsd/surface.md",
"gsd-core/commands/gsd/thread.md",
"gsd-core/commands/gsd/ui-phase.md",
"gsd-core/commands/gsd/ui-review.md",
"gsd-core/commands/gsd/ultraplan-phase.md",
"gsd-core/commands/gsd/undo.md",
"gsd-core/commands/gsd/update.md",
"gsd-core/commands/gsd/validate-phase.md",
"gsd-core/commands/gsd/verify-work.md",
"gsd-core/commands/gsd/workspace.md",
"gsd-core/commands/gsd/workstreams.md",
"gsd-core/contexts/dev.md",
"gsd-core/contexts/research.md",
"gsd-core/contexts/review.md",

View File

@@ -38,6 +38,41 @@
"config.toml",
"gsd-core/.gsd-runtime",
"gsd-core/VERSION",
"gsd-core/agents/gsd-advisor-researcher.md",
"gsd-core/agents/gsd-ai-researcher.md",
"gsd-core/agents/gsd-assumptions-analyzer.md",
"gsd-core/agents/gsd-code-fixer.md",
"gsd-core/agents/gsd-code-reviewer.md",
"gsd-core/agents/gsd-codebase-mapper.md",
"gsd-core/agents/gsd-debug-session-manager.md",
"gsd-core/agents/gsd-debugger.md",
"gsd-core/agents/gsd-doc-classifier.md",
"gsd-core/agents/gsd-doc-synthesizer.md",
"gsd-core/agents/gsd-doc-verifier.md",
"gsd-core/agents/gsd-doc-writer.md",
"gsd-core/agents/gsd-dom-verifier.md",
"gsd-core/agents/gsd-domain-researcher.md",
"gsd-core/agents/gsd-eval-auditor.md",
"gsd-core/agents/gsd-eval-planner.md",
"gsd-core/agents/gsd-executor.md",
"gsd-core/agents/gsd-framework-selector.md",
"gsd-core/agents/gsd-integration-checker.md",
"gsd-core/agents/gsd-intel-updater.md",
"gsd-core/agents/gsd-mempalace-curator.md",
"gsd-core/agents/gsd-nyquist-auditor.md",
"gsd-core/agents/gsd-pattern-mapper.md",
"gsd-core/agents/gsd-phase-researcher.md",
"gsd-core/agents/gsd-plan-checker.md",
"gsd-core/agents/gsd-planner.md",
"gsd-core/agents/gsd-project-researcher.md",
"gsd-core/agents/gsd-research-synthesizer.md",
"gsd-core/agents/gsd-roadmapper.md",
"gsd-core/agents/gsd-security-auditor.md",
"gsd-core/agents/gsd-ui-auditor.md",
"gsd-core/agents/gsd-ui-checker.md",
"gsd-core/agents/gsd-ui-researcher.md",
"gsd-core/agents/gsd-user-profiler.md",
"gsd-core/agents/gsd-verifier.md",
"gsd-core/bin/check-latest-version.cjs",
"gsd-core/bin/ensure-runtime-build.cjs",
"gsd-core/bin/gsd-tools.cjs",
@@ -49,6 +84,78 @@
"gsd-core/bin/shared/model-catalog.json",
"gsd-core/bin/shared/runtime-aliases.manifest.json",
"gsd-core/bin/verify-reapply-patches.cjs",
"gsd-core/commands/gsd/add-tests.md",
"gsd-core/commands/gsd/ai-integration-phase.md",
"gsd-core/commands/gsd/audit-fix.md",
"gsd-core/commands/gsd/audit-milestone.md",
"gsd-core/commands/gsd/audit-uat.md",
"gsd-core/commands/gsd/autonomous.md",
"gsd-core/commands/gsd/capture.md",
"gsd-core/commands/gsd/cleanup.md",
"gsd-core/commands/gsd/code-review.md",
"gsd-core/commands/gsd/complete-milestone.md",
"gsd-core/commands/gsd/config.md",
"gsd-core/commands/gsd/debug.md",
"gsd-core/commands/gsd/discuss-phase.md",
"gsd-core/commands/gsd/docs-update.md",
"gsd-core/commands/gsd/eval-review.md",
"gsd-core/commands/gsd/execute-phase.md",
"gsd-core/commands/gsd/explore.md",
"gsd-core/commands/gsd/extract-learnings.md",
"gsd-core/commands/gsd/fast.md",
"gsd-core/commands/gsd/forensics.md",
"gsd-core/commands/gsd/graphify.md",
"gsd-core/commands/gsd/health.md",
"gsd-core/commands/gsd/help.md",
"gsd-core/commands/gsd/import.md",
"gsd-core/commands/gsd/inbox.md",
"gsd-core/commands/gsd/ingest-docs.md",
"gsd-core/commands/gsd/manager.md",
"gsd-core/commands/gsd/map-codebase.md",
"gsd-core/commands/gsd/mempalace-capture.md",
"gsd-core/commands/gsd/mempalace-recall.md",
"gsd-core/commands/gsd/milestone-summary.md",
"gsd-core/commands/gsd/mvp-phase.md",
"gsd-core/commands/gsd/new-milestone.md",
"gsd-core/commands/gsd/new-project.md",
"gsd-core/commands/gsd/next.md",
"gsd-core/commands/gsd/ns-context.md",
"gsd-core/commands/gsd/ns-ideate.md",
"gsd-core/commands/gsd/ns-manage.md",
"gsd-core/commands/gsd/ns-project.md",
"gsd-core/commands/gsd/ns-review.md",
"gsd-core/commands/gsd/ns-workflow.md",
"gsd-core/commands/gsd/onboard.md",
"gsd-core/commands/gsd/pause-work.md",
"gsd-core/commands/gsd/phase.md",
"gsd-core/commands/gsd/plan-phase.md",
"gsd-core/commands/gsd/plan-review-convergence.md",
"gsd-core/commands/gsd/pr-branch.md",
"gsd-core/commands/gsd/profile-user.md",
"gsd-core/commands/gsd/progress.md",
"gsd-core/commands/gsd/quick-batch.md",
"gsd-core/commands/gsd/quick.md",
"gsd-core/commands/gsd/resume-work.md",
"gsd-core/commands/gsd/review-backlog.md",
"gsd-core/commands/gsd/review.md",
"gsd-core/commands/gsd/secure-phase.md",
"gsd-core/commands/gsd/settings.md",
"gsd-core/commands/gsd/ship.md",
"gsd-core/commands/gsd/sketch.md",
"gsd-core/commands/gsd/spec-phase.md",
"gsd-core/commands/gsd/spike.md",
"gsd-core/commands/gsd/stats.md",
"gsd-core/commands/gsd/surface.md",
"gsd-core/commands/gsd/thread.md",
"gsd-core/commands/gsd/ui-phase.md",
"gsd-core/commands/gsd/ui-review.md",
"gsd-core/commands/gsd/ultraplan-phase.md",
"gsd-core/commands/gsd/undo.md",
"gsd-core/commands/gsd/update.md",
"gsd-core/commands/gsd/validate-phase.md",
"gsd-core/commands/gsd/verify-work.md",
"gsd-core/commands/gsd/workspace.md",
"gsd-core/commands/gsd/workstreams.md",
"gsd-core/contexts/dev.md",
"gsd-core/contexts/research.md",
"gsd-core/contexts/review.md",

View File

@@ -74,6 +74,41 @@
"agents/subagents/gsd-verifier.yaml",
"gsd-core/.gsd-runtime",
"gsd-core/VERSION",
"gsd-core/agents/gsd-advisor-researcher.md",
"gsd-core/agents/gsd-ai-researcher.md",
"gsd-core/agents/gsd-assumptions-analyzer.md",
"gsd-core/agents/gsd-code-fixer.md",
"gsd-core/agents/gsd-code-reviewer.md",
"gsd-core/agents/gsd-codebase-mapper.md",
"gsd-core/agents/gsd-debug-session-manager.md",
"gsd-core/agents/gsd-debugger.md",
"gsd-core/agents/gsd-doc-classifier.md",
"gsd-core/agents/gsd-doc-synthesizer.md",
"gsd-core/agents/gsd-doc-verifier.md",
"gsd-core/agents/gsd-doc-writer.md",
"gsd-core/agents/gsd-dom-verifier.md",
"gsd-core/agents/gsd-domain-researcher.md",
"gsd-core/agents/gsd-eval-auditor.md",
"gsd-core/agents/gsd-eval-planner.md",
"gsd-core/agents/gsd-executor.md",
"gsd-core/agents/gsd-framework-selector.md",
"gsd-core/agents/gsd-integration-checker.md",
"gsd-core/agents/gsd-intel-updater.md",
"gsd-core/agents/gsd-mempalace-curator.md",
"gsd-core/agents/gsd-nyquist-auditor.md",
"gsd-core/agents/gsd-pattern-mapper.md",
"gsd-core/agents/gsd-phase-researcher.md",
"gsd-core/agents/gsd-plan-checker.md",
"gsd-core/agents/gsd-planner.md",
"gsd-core/agents/gsd-project-researcher.md",
"gsd-core/agents/gsd-research-synthesizer.md",
"gsd-core/agents/gsd-roadmapper.md",
"gsd-core/agents/gsd-security-auditor.md",
"gsd-core/agents/gsd-ui-auditor.md",
"gsd-core/agents/gsd-ui-checker.md",
"gsd-core/agents/gsd-ui-researcher.md",
"gsd-core/agents/gsd-user-profiler.md",
"gsd-core/agents/gsd-verifier.md",
"gsd-core/bin/check-latest-version.cjs",
"gsd-core/bin/ensure-runtime-build.cjs",
"gsd-core/bin/gsd-tools.cjs",
@@ -85,6 +120,78 @@
"gsd-core/bin/shared/model-catalog.json",
"gsd-core/bin/shared/runtime-aliases.manifest.json",
"gsd-core/bin/verify-reapply-patches.cjs",
"gsd-core/commands/gsd/add-tests.md",
"gsd-core/commands/gsd/ai-integration-phase.md",
"gsd-core/commands/gsd/audit-fix.md",
"gsd-core/commands/gsd/audit-milestone.md",
"gsd-core/commands/gsd/audit-uat.md",
"gsd-core/commands/gsd/autonomous.md",
"gsd-core/commands/gsd/capture.md",
"gsd-core/commands/gsd/cleanup.md",
"gsd-core/commands/gsd/code-review.md",
"gsd-core/commands/gsd/complete-milestone.md",
"gsd-core/commands/gsd/config.md",
"gsd-core/commands/gsd/debug.md",
"gsd-core/commands/gsd/discuss-phase.md",
"gsd-core/commands/gsd/docs-update.md",
"gsd-core/commands/gsd/eval-review.md",
"gsd-core/commands/gsd/execute-phase.md",
"gsd-core/commands/gsd/explore.md",
"gsd-core/commands/gsd/extract-learnings.md",
"gsd-core/commands/gsd/fast.md",
"gsd-core/commands/gsd/forensics.md",
"gsd-core/commands/gsd/graphify.md",
"gsd-core/commands/gsd/health.md",
"gsd-core/commands/gsd/help.md",
"gsd-core/commands/gsd/import.md",
"gsd-core/commands/gsd/inbox.md",
"gsd-core/commands/gsd/ingest-docs.md",
"gsd-core/commands/gsd/manager.md",
"gsd-core/commands/gsd/map-codebase.md",
"gsd-core/commands/gsd/mempalace-capture.md",
"gsd-core/commands/gsd/mempalace-recall.md",
"gsd-core/commands/gsd/milestone-summary.md",
"gsd-core/commands/gsd/mvp-phase.md",
"gsd-core/commands/gsd/new-milestone.md",
"gsd-core/commands/gsd/new-project.md",
"gsd-core/commands/gsd/next.md",
"gsd-core/commands/gsd/ns-context.md",
"gsd-core/commands/gsd/ns-ideate.md",
"gsd-core/commands/gsd/ns-manage.md",
"gsd-core/commands/gsd/ns-project.md",
"gsd-core/commands/gsd/ns-review.md",
"gsd-core/commands/gsd/ns-workflow.md",
"gsd-core/commands/gsd/onboard.md",
"gsd-core/commands/gsd/pause-work.md",
"gsd-core/commands/gsd/phase.md",
"gsd-core/commands/gsd/plan-phase.md",
"gsd-core/commands/gsd/plan-review-convergence.md",
"gsd-core/commands/gsd/pr-branch.md",
"gsd-core/commands/gsd/profile-user.md",
"gsd-core/commands/gsd/progress.md",
"gsd-core/commands/gsd/quick-batch.md",
"gsd-core/commands/gsd/quick.md",
"gsd-core/commands/gsd/resume-work.md",
"gsd-core/commands/gsd/review-backlog.md",
"gsd-core/commands/gsd/review.md",
"gsd-core/commands/gsd/secure-phase.md",
"gsd-core/commands/gsd/settings.md",
"gsd-core/commands/gsd/ship.md",
"gsd-core/commands/gsd/sketch.md",
"gsd-core/commands/gsd/spec-phase.md",
"gsd-core/commands/gsd/spike.md",
"gsd-core/commands/gsd/stats.md",
"gsd-core/commands/gsd/surface.md",
"gsd-core/commands/gsd/thread.md",
"gsd-core/commands/gsd/ui-phase.md",
"gsd-core/commands/gsd/ui-review.md",
"gsd-core/commands/gsd/ultraplan-phase.md",
"gsd-core/commands/gsd/undo.md",
"gsd-core/commands/gsd/update.md",
"gsd-core/commands/gsd/validate-phase.md",
"gsd-core/commands/gsd/verify-work.md",
"gsd-core/commands/gsd/workspace.md",
"gsd-core/commands/gsd/workstreams.md",
"gsd-core/contexts/dev.md",
"gsd-core/contexts/research.md",
"gsd-core/contexts/review.md",

View File

@@ -109,6 +109,41 @@
"commands/gsd-workstreams.md",
"gsd-core/.gsd-runtime",
"gsd-core/VERSION",
"gsd-core/agents/gsd-advisor-researcher.md",
"gsd-core/agents/gsd-ai-researcher.md",
"gsd-core/agents/gsd-assumptions-analyzer.md",
"gsd-core/agents/gsd-code-fixer.md",
"gsd-core/agents/gsd-code-reviewer.md",
"gsd-core/agents/gsd-codebase-mapper.md",
"gsd-core/agents/gsd-debug-session-manager.md",
"gsd-core/agents/gsd-debugger.md",
"gsd-core/agents/gsd-doc-classifier.md",
"gsd-core/agents/gsd-doc-synthesizer.md",
"gsd-core/agents/gsd-doc-verifier.md",
"gsd-core/agents/gsd-doc-writer.md",
"gsd-core/agents/gsd-dom-verifier.md",
"gsd-core/agents/gsd-domain-researcher.md",
"gsd-core/agents/gsd-eval-auditor.md",
"gsd-core/agents/gsd-eval-planner.md",
"gsd-core/agents/gsd-executor.md",
"gsd-core/agents/gsd-framework-selector.md",
"gsd-core/agents/gsd-integration-checker.md",
"gsd-core/agents/gsd-intel-updater.md",
"gsd-core/agents/gsd-mempalace-curator.md",
"gsd-core/agents/gsd-nyquist-auditor.md",
"gsd-core/agents/gsd-pattern-mapper.md",
"gsd-core/agents/gsd-phase-researcher.md",
"gsd-core/agents/gsd-plan-checker.md",
"gsd-core/agents/gsd-planner.md",
"gsd-core/agents/gsd-project-researcher.md",
"gsd-core/agents/gsd-research-synthesizer.md",
"gsd-core/agents/gsd-roadmapper.md",
"gsd-core/agents/gsd-security-auditor.md",
"gsd-core/agents/gsd-ui-auditor.md",
"gsd-core/agents/gsd-ui-checker.md",
"gsd-core/agents/gsd-ui-researcher.md",
"gsd-core/agents/gsd-user-profiler.md",
"gsd-core/agents/gsd-verifier.md",
"gsd-core/bin/check-latest-version.cjs",
"gsd-core/bin/ensure-runtime-build.cjs",
"gsd-core/bin/gsd-tools.cjs",
@@ -120,6 +155,78 @@
"gsd-core/bin/shared/model-catalog.json",
"gsd-core/bin/shared/runtime-aliases.manifest.json",
"gsd-core/bin/verify-reapply-patches.cjs",
"gsd-core/commands/gsd/add-tests.md",
"gsd-core/commands/gsd/ai-integration-phase.md",
"gsd-core/commands/gsd/audit-fix.md",
"gsd-core/commands/gsd/audit-milestone.md",
"gsd-core/commands/gsd/audit-uat.md",
"gsd-core/commands/gsd/autonomous.md",
"gsd-core/commands/gsd/capture.md",
"gsd-core/commands/gsd/cleanup.md",
"gsd-core/commands/gsd/code-review.md",
"gsd-core/commands/gsd/complete-milestone.md",
"gsd-core/commands/gsd/config.md",
"gsd-core/commands/gsd/debug.md",
"gsd-core/commands/gsd/discuss-phase.md",
"gsd-core/commands/gsd/docs-update.md",
"gsd-core/commands/gsd/eval-review.md",
"gsd-core/commands/gsd/execute-phase.md",
"gsd-core/commands/gsd/explore.md",
"gsd-core/commands/gsd/extract-learnings.md",
"gsd-core/commands/gsd/fast.md",
"gsd-core/commands/gsd/forensics.md",
"gsd-core/commands/gsd/graphify.md",
"gsd-core/commands/gsd/health.md",
"gsd-core/commands/gsd/help.md",
"gsd-core/commands/gsd/import.md",
"gsd-core/commands/gsd/inbox.md",
"gsd-core/commands/gsd/ingest-docs.md",
"gsd-core/commands/gsd/manager.md",
"gsd-core/commands/gsd/map-codebase.md",
"gsd-core/commands/gsd/mempalace-capture.md",
"gsd-core/commands/gsd/mempalace-recall.md",
"gsd-core/commands/gsd/milestone-summary.md",
"gsd-core/commands/gsd/mvp-phase.md",
"gsd-core/commands/gsd/new-milestone.md",
"gsd-core/commands/gsd/new-project.md",
"gsd-core/commands/gsd/next.md",
"gsd-core/commands/gsd/ns-context.md",
"gsd-core/commands/gsd/ns-ideate.md",
"gsd-core/commands/gsd/ns-manage.md",
"gsd-core/commands/gsd/ns-project.md",
"gsd-core/commands/gsd/ns-review.md",
"gsd-core/commands/gsd/ns-workflow.md",
"gsd-core/commands/gsd/onboard.md",
"gsd-core/commands/gsd/pause-work.md",
"gsd-core/commands/gsd/phase.md",
"gsd-core/commands/gsd/plan-phase.md",
"gsd-core/commands/gsd/plan-review-convergence.md",
"gsd-core/commands/gsd/pr-branch.md",
"gsd-core/commands/gsd/profile-user.md",
"gsd-core/commands/gsd/progress.md",
"gsd-core/commands/gsd/quick-batch.md",
"gsd-core/commands/gsd/quick.md",
"gsd-core/commands/gsd/resume-work.md",
"gsd-core/commands/gsd/review-backlog.md",
"gsd-core/commands/gsd/review.md",
"gsd-core/commands/gsd/secure-phase.md",
"gsd-core/commands/gsd/settings.md",
"gsd-core/commands/gsd/ship.md",
"gsd-core/commands/gsd/sketch.md",
"gsd-core/commands/gsd/spec-phase.md",
"gsd-core/commands/gsd/spike.md",
"gsd-core/commands/gsd/stats.md",
"gsd-core/commands/gsd/surface.md",
"gsd-core/commands/gsd/thread.md",
"gsd-core/commands/gsd/ui-phase.md",
"gsd-core/commands/gsd/ui-review.md",
"gsd-core/commands/gsd/ultraplan-phase.md",
"gsd-core/commands/gsd/undo.md",
"gsd-core/commands/gsd/update.md",
"gsd-core/commands/gsd/validate-phase.md",
"gsd-core/commands/gsd/verify-work.md",
"gsd-core/commands/gsd/workspace.md",
"gsd-core/commands/gsd/workstreams.md",
"gsd-core/contexts/dev.md",
"gsd-core/contexts/research.md",
"gsd-core/contexts/review.md",

View File

@@ -37,6 +37,41 @@
"agents/gsd-verifier.md",
"gsd-core/.gsd-runtime",
"gsd-core/VERSION",
"gsd-core/agents/gsd-advisor-researcher.md",
"gsd-core/agents/gsd-ai-researcher.md",
"gsd-core/agents/gsd-assumptions-analyzer.md",
"gsd-core/agents/gsd-code-fixer.md",
"gsd-core/agents/gsd-code-reviewer.md",
"gsd-core/agents/gsd-codebase-mapper.md",
"gsd-core/agents/gsd-debug-session-manager.md",
"gsd-core/agents/gsd-debugger.md",
"gsd-core/agents/gsd-doc-classifier.md",
"gsd-core/agents/gsd-doc-synthesizer.md",
"gsd-core/agents/gsd-doc-verifier.md",
"gsd-core/agents/gsd-doc-writer.md",
"gsd-core/agents/gsd-dom-verifier.md",
"gsd-core/agents/gsd-domain-researcher.md",
"gsd-core/agents/gsd-eval-auditor.md",
"gsd-core/agents/gsd-eval-planner.md",
"gsd-core/agents/gsd-executor.md",
"gsd-core/agents/gsd-framework-selector.md",
"gsd-core/agents/gsd-integration-checker.md",
"gsd-core/agents/gsd-intel-updater.md",
"gsd-core/agents/gsd-mempalace-curator.md",
"gsd-core/agents/gsd-nyquist-auditor.md",
"gsd-core/agents/gsd-pattern-mapper.md",
"gsd-core/agents/gsd-phase-researcher.md",
"gsd-core/agents/gsd-plan-checker.md",
"gsd-core/agents/gsd-planner.md",
"gsd-core/agents/gsd-project-researcher.md",
"gsd-core/agents/gsd-research-synthesizer.md",
"gsd-core/agents/gsd-roadmapper.md",
"gsd-core/agents/gsd-security-auditor.md",
"gsd-core/agents/gsd-ui-auditor.md",
"gsd-core/agents/gsd-ui-checker.md",
"gsd-core/agents/gsd-ui-researcher.md",
"gsd-core/agents/gsd-user-profiler.md",
"gsd-core/agents/gsd-verifier.md",
"gsd-core/bin/check-latest-version.cjs",
"gsd-core/bin/ensure-runtime-build.cjs",
"gsd-core/bin/gsd-tools.cjs",
@@ -48,6 +83,78 @@
"gsd-core/bin/shared/model-catalog.json",
"gsd-core/bin/shared/runtime-aliases.manifest.json",
"gsd-core/bin/verify-reapply-patches.cjs",
"gsd-core/commands/gsd/add-tests.md",
"gsd-core/commands/gsd/ai-integration-phase.md",
"gsd-core/commands/gsd/audit-fix.md",
"gsd-core/commands/gsd/audit-milestone.md",
"gsd-core/commands/gsd/audit-uat.md",
"gsd-core/commands/gsd/autonomous.md",
"gsd-core/commands/gsd/capture.md",
"gsd-core/commands/gsd/cleanup.md",
"gsd-core/commands/gsd/code-review.md",
"gsd-core/commands/gsd/complete-milestone.md",
"gsd-core/commands/gsd/config.md",
"gsd-core/commands/gsd/debug.md",
"gsd-core/commands/gsd/discuss-phase.md",
"gsd-core/commands/gsd/docs-update.md",
"gsd-core/commands/gsd/eval-review.md",
"gsd-core/commands/gsd/execute-phase.md",
"gsd-core/commands/gsd/explore.md",
"gsd-core/commands/gsd/extract-learnings.md",
"gsd-core/commands/gsd/fast.md",
"gsd-core/commands/gsd/forensics.md",
"gsd-core/commands/gsd/graphify.md",
"gsd-core/commands/gsd/health.md",
"gsd-core/commands/gsd/help.md",
"gsd-core/commands/gsd/import.md",
"gsd-core/commands/gsd/inbox.md",
"gsd-core/commands/gsd/ingest-docs.md",
"gsd-core/commands/gsd/manager.md",
"gsd-core/commands/gsd/map-codebase.md",
"gsd-core/commands/gsd/mempalace-capture.md",
"gsd-core/commands/gsd/mempalace-recall.md",
"gsd-core/commands/gsd/milestone-summary.md",
"gsd-core/commands/gsd/mvp-phase.md",
"gsd-core/commands/gsd/new-milestone.md",
"gsd-core/commands/gsd/new-project.md",
"gsd-core/commands/gsd/next.md",
"gsd-core/commands/gsd/ns-context.md",
"gsd-core/commands/gsd/ns-ideate.md",
"gsd-core/commands/gsd/ns-manage.md",
"gsd-core/commands/gsd/ns-project.md",
"gsd-core/commands/gsd/ns-review.md",
"gsd-core/commands/gsd/ns-workflow.md",
"gsd-core/commands/gsd/onboard.md",
"gsd-core/commands/gsd/pause-work.md",
"gsd-core/commands/gsd/phase.md",
"gsd-core/commands/gsd/plan-phase.md",
"gsd-core/commands/gsd/plan-review-convergence.md",
"gsd-core/commands/gsd/pr-branch.md",
"gsd-core/commands/gsd/profile-user.md",
"gsd-core/commands/gsd/progress.md",
"gsd-core/commands/gsd/quick-batch.md",
"gsd-core/commands/gsd/quick.md",
"gsd-core/commands/gsd/resume-work.md",
"gsd-core/commands/gsd/review-backlog.md",
"gsd-core/commands/gsd/review.md",
"gsd-core/commands/gsd/secure-phase.md",
"gsd-core/commands/gsd/settings.md",
"gsd-core/commands/gsd/ship.md",
"gsd-core/commands/gsd/sketch.md",
"gsd-core/commands/gsd/spec-phase.md",
"gsd-core/commands/gsd/spike.md",
"gsd-core/commands/gsd/stats.md",
"gsd-core/commands/gsd/surface.md",
"gsd-core/commands/gsd/thread.md",
"gsd-core/commands/gsd/ui-phase.md",
"gsd-core/commands/gsd/ui-review.md",
"gsd-core/commands/gsd/ultraplan-phase.md",
"gsd-core/commands/gsd/undo.md",
"gsd-core/commands/gsd/update.md",
"gsd-core/commands/gsd/validate-phase.md",
"gsd-core/commands/gsd/verify-work.md",
"gsd-core/commands/gsd/workspace.md",
"gsd-core/commands/gsd/workstreams.md",
"gsd-core/contexts/dev.md",
"gsd-core/contexts/research.md",
"gsd-core/contexts/review.md",

View File

@@ -37,6 +37,41 @@
"agents/gsd-verifier.md",
"gsd-core/.gsd-runtime",
"gsd-core/VERSION",
"gsd-core/agents/gsd-advisor-researcher.md",
"gsd-core/agents/gsd-ai-researcher.md",
"gsd-core/agents/gsd-assumptions-analyzer.md",
"gsd-core/agents/gsd-code-fixer.md",
"gsd-core/agents/gsd-code-reviewer.md",
"gsd-core/agents/gsd-codebase-mapper.md",
"gsd-core/agents/gsd-debug-session-manager.md",
"gsd-core/agents/gsd-debugger.md",
"gsd-core/agents/gsd-doc-classifier.md",
"gsd-core/agents/gsd-doc-synthesizer.md",
"gsd-core/agents/gsd-doc-verifier.md",
"gsd-core/agents/gsd-doc-writer.md",
"gsd-core/agents/gsd-dom-verifier.md",
"gsd-core/agents/gsd-domain-researcher.md",
"gsd-core/agents/gsd-eval-auditor.md",
"gsd-core/agents/gsd-eval-planner.md",
"gsd-core/agents/gsd-executor.md",
"gsd-core/agents/gsd-framework-selector.md",
"gsd-core/agents/gsd-integration-checker.md",
"gsd-core/agents/gsd-intel-updater.md",
"gsd-core/agents/gsd-mempalace-curator.md",
"gsd-core/agents/gsd-nyquist-auditor.md",
"gsd-core/agents/gsd-pattern-mapper.md",
"gsd-core/agents/gsd-phase-researcher.md",
"gsd-core/agents/gsd-plan-checker.md",
"gsd-core/agents/gsd-planner.md",
"gsd-core/agents/gsd-project-researcher.md",
"gsd-core/agents/gsd-research-synthesizer.md",
"gsd-core/agents/gsd-roadmapper.md",
"gsd-core/agents/gsd-security-auditor.md",
"gsd-core/agents/gsd-ui-auditor.md",
"gsd-core/agents/gsd-ui-checker.md",
"gsd-core/agents/gsd-ui-researcher.md",
"gsd-core/agents/gsd-user-profiler.md",
"gsd-core/agents/gsd-verifier.md",
"gsd-core/bin/check-latest-version.cjs",
"gsd-core/bin/ensure-runtime-build.cjs",
"gsd-core/bin/gsd-tools.cjs",
@@ -48,6 +83,78 @@
"gsd-core/bin/shared/model-catalog.json",
"gsd-core/bin/shared/runtime-aliases.manifest.json",
"gsd-core/bin/verify-reapply-patches.cjs",
"gsd-core/commands/gsd/add-tests.md",
"gsd-core/commands/gsd/ai-integration-phase.md",
"gsd-core/commands/gsd/audit-fix.md",
"gsd-core/commands/gsd/audit-milestone.md",
"gsd-core/commands/gsd/audit-uat.md",
"gsd-core/commands/gsd/autonomous.md",
"gsd-core/commands/gsd/capture.md",
"gsd-core/commands/gsd/cleanup.md",
"gsd-core/commands/gsd/code-review.md",
"gsd-core/commands/gsd/complete-milestone.md",
"gsd-core/commands/gsd/config.md",
"gsd-core/commands/gsd/debug.md",
"gsd-core/commands/gsd/discuss-phase.md",
"gsd-core/commands/gsd/docs-update.md",
"gsd-core/commands/gsd/eval-review.md",
"gsd-core/commands/gsd/execute-phase.md",
"gsd-core/commands/gsd/explore.md",
"gsd-core/commands/gsd/extract-learnings.md",
"gsd-core/commands/gsd/fast.md",
"gsd-core/commands/gsd/forensics.md",
"gsd-core/commands/gsd/graphify.md",
"gsd-core/commands/gsd/health.md",
"gsd-core/commands/gsd/help.md",
"gsd-core/commands/gsd/import.md",
"gsd-core/commands/gsd/inbox.md",
"gsd-core/commands/gsd/ingest-docs.md",
"gsd-core/commands/gsd/manager.md",
"gsd-core/commands/gsd/map-codebase.md",
"gsd-core/commands/gsd/mempalace-capture.md",
"gsd-core/commands/gsd/mempalace-recall.md",
"gsd-core/commands/gsd/milestone-summary.md",
"gsd-core/commands/gsd/mvp-phase.md",
"gsd-core/commands/gsd/new-milestone.md",
"gsd-core/commands/gsd/new-project.md",
"gsd-core/commands/gsd/next.md",
"gsd-core/commands/gsd/ns-context.md",
"gsd-core/commands/gsd/ns-ideate.md",
"gsd-core/commands/gsd/ns-manage.md",
"gsd-core/commands/gsd/ns-project.md",
"gsd-core/commands/gsd/ns-review.md",
"gsd-core/commands/gsd/ns-workflow.md",
"gsd-core/commands/gsd/onboard.md",
"gsd-core/commands/gsd/pause-work.md",
"gsd-core/commands/gsd/phase.md",
"gsd-core/commands/gsd/plan-phase.md",
"gsd-core/commands/gsd/plan-review-convergence.md",
"gsd-core/commands/gsd/pr-branch.md",
"gsd-core/commands/gsd/profile-user.md",
"gsd-core/commands/gsd/progress.md",
"gsd-core/commands/gsd/quick-batch.md",
"gsd-core/commands/gsd/quick.md",
"gsd-core/commands/gsd/resume-work.md",
"gsd-core/commands/gsd/review-backlog.md",
"gsd-core/commands/gsd/review.md",
"gsd-core/commands/gsd/secure-phase.md",
"gsd-core/commands/gsd/settings.md",
"gsd-core/commands/gsd/ship.md",
"gsd-core/commands/gsd/sketch.md",
"gsd-core/commands/gsd/spec-phase.md",
"gsd-core/commands/gsd/spike.md",
"gsd-core/commands/gsd/stats.md",
"gsd-core/commands/gsd/surface.md",
"gsd-core/commands/gsd/thread.md",
"gsd-core/commands/gsd/ui-phase.md",
"gsd-core/commands/gsd/ui-review.md",
"gsd-core/commands/gsd/ultraplan-phase.md",
"gsd-core/commands/gsd/undo.md",
"gsd-core/commands/gsd/update.md",
"gsd-core/commands/gsd/validate-phase.md",
"gsd-core/commands/gsd/verify-work.md",
"gsd-core/commands/gsd/workspace.md",
"gsd-core/commands/gsd/workstreams.md",
"gsd-core/contexts/dev.md",
"gsd-core/contexts/research.md",
"gsd-core/contexts/review.md",

View File

@@ -37,6 +37,41 @@
"agents/gsd-verifier.md",
"gsd-core/.gsd-runtime",
"gsd-core/VERSION",
"gsd-core/agents/gsd-advisor-researcher.md",
"gsd-core/agents/gsd-ai-researcher.md",
"gsd-core/agents/gsd-assumptions-analyzer.md",
"gsd-core/agents/gsd-code-fixer.md",
"gsd-core/agents/gsd-code-reviewer.md",
"gsd-core/agents/gsd-codebase-mapper.md",
"gsd-core/agents/gsd-debug-session-manager.md",
"gsd-core/agents/gsd-debugger.md",
"gsd-core/agents/gsd-doc-classifier.md",
"gsd-core/agents/gsd-doc-synthesizer.md",
"gsd-core/agents/gsd-doc-verifier.md",
"gsd-core/agents/gsd-doc-writer.md",
"gsd-core/agents/gsd-dom-verifier.md",
"gsd-core/agents/gsd-domain-researcher.md",
"gsd-core/agents/gsd-eval-auditor.md",
"gsd-core/agents/gsd-eval-planner.md",
"gsd-core/agents/gsd-executor.md",
"gsd-core/agents/gsd-framework-selector.md",
"gsd-core/agents/gsd-integration-checker.md",
"gsd-core/agents/gsd-intel-updater.md",
"gsd-core/agents/gsd-mempalace-curator.md",
"gsd-core/agents/gsd-nyquist-auditor.md",
"gsd-core/agents/gsd-pattern-mapper.md",
"gsd-core/agents/gsd-phase-researcher.md",
"gsd-core/agents/gsd-plan-checker.md",
"gsd-core/agents/gsd-planner.md",
"gsd-core/agents/gsd-project-researcher.md",
"gsd-core/agents/gsd-research-synthesizer.md",
"gsd-core/agents/gsd-roadmapper.md",
"gsd-core/agents/gsd-security-auditor.md",
"gsd-core/agents/gsd-ui-auditor.md",
"gsd-core/agents/gsd-ui-checker.md",
"gsd-core/agents/gsd-ui-researcher.md",
"gsd-core/agents/gsd-user-profiler.md",
"gsd-core/agents/gsd-verifier.md",
"gsd-core/bin/check-latest-version.cjs",
"gsd-core/bin/ensure-runtime-build.cjs",
"gsd-core/bin/gsd-tools.cjs",

View File

@@ -109,6 +109,41 @@
"commands/gsd-workstreams.md",
"gsd-core/.gsd-runtime",
"gsd-core/VERSION",
"gsd-core/agents/gsd-advisor-researcher.md",
"gsd-core/agents/gsd-ai-researcher.md",
"gsd-core/agents/gsd-assumptions-analyzer.md",
"gsd-core/agents/gsd-code-fixer.md",
"gsd-core/agents/gsd-code-reviewer.md",
"gsd-core/agents/gsd-codebase-mapper.md",
"gsd-core/agents/gsd-debug-session-manager.md",
"gsd-core/agents/gsd-debugger.md",
"gsd-core/agents/gsd-doc-classifier.md",
"gsd-core/agents/gsd-doc-synthesizer.md",
"gsd-core/agents/gsd-doc-verifier.md",
"gsd-core/agents/gsd-doc-writer.md",
"gsd-core/agents/gsd-dom-verifier.md",
"gsd-core/agents/gsd-domain-researcher.md",
"gsd-core/agents/gsd-eval-auditor.md",
"gsd-core/agents/gsd-eval-planner.md",
"gsd-core/agents/gsd-executor.md",
"gsd-core/agents/gsd-framework-selector.md",
"gsd-core/agents/gsd-integration-checker.md",
"gsd-core/agents/gsd-intel-updater.md",
"gsd-core/agents/gsd-mempalace-curator.md",
"gsd-core/agents/gsd-nyquist-auditor.md",
"gsd-core/agents/gsd-pattern-mapper.md",
"gsd-core/agents/gsd-phase-researcher.md",
"gsd-core/agents/gsd-plan-checker.md",
"gsd-core/agents/gsd-planner.md",
"gsd-core/agents/gsd-project-researcher.md",
"gsd-core/agents/gsd-research-synthesizer.md",
"gsd-core/agents/gsd-roadmapper.md",
"gsd-core/agents/gsd-security-auditor.md",
"gsd-core/agents/gsd-ui-auditor.md",
"gsd-core/agents/gsd-ui-checker.md",
"gsd-core/agents/gsd-ui-researcher.md",
"gsd-core/agents/gsd-user-profiler.md",
"gsd-core/agents/gsd-verifier.md",
"gsd-core/bin/check-latest-version.cjs",
"gsd-core/bin/ensure-runtime-build.cjs",
"gsd-core/bin/gsd-tools.cjs",
@@ -120,6 +155,78 @@
"gsd-core/bin/shared/model-catalog.json",
"gsd-core/bin/shared/runtime-aliases.manifest.json",
"gsd-core/bin/verify-reapply-patches.cjs",
"gsd-core/commands/gsd/add-tests.md",
"gsd-core/commands/gsd/ai-integration-phase.md",
"gsd-core/commands/gsd/audit-fix.md",
"gsd-core/commands/gsd/audit-milestone.md",
"gsd-core/commands/gsd/audit-uat.md",
"gsd-core/commands/gsd/autonomous.md",
"gsd-core/commands/gsd/capture.md",
"gsd-core/commands/gsd/cleanup.md",
"gsd-core/commands/gsd/code-review.md",
"gsd-core/commands/gsd/complete-milestone.md",
"gsd-core/commands/gsd/config.md",
"gsd-core/commands/gsd/debug.md",
"gsd-core/commands/gsd/discuss-phase.md",
"gsd-core/commands/gsd/docs-update.md",
"gsd-core/commands/gsd/eval-review.md",
"gsd-core/commands/gsd/execute-phase.md",
"gsd-core/commands/gsd/explore.md",
"gsd-core/commands/gsd/extract-learnings.md",
"gsd-core/commands/gsd/fast.md",
"gsd-core/commands/gsd/forensics.md",
"gsd-core/commands/gsd/graphify.md",
"gsd-core/commands/gsd/health.md",
"gsd-core/commands/gsd/help.md",
"gsd-core/commands/gsd/import.md",
"gsd-core/commands/gsd/inbox.md",
"gsd-core/commands/gsd/ingest-docs.md",
"gsd-core/commands/gsd/manager.md",
"gsd-core/commands/gsd/map-codebase.md",
"gsd-core/commands/gsd/mempalace-capture.md",
"gsd-core/commands/gsd/mempalace-recall.md",
"gsd-core/commands/gsd/milestone-summary.md",
"gsd-core/commands/gsd/mvp-phase.md",
"gsd-core/commands/gsd/new-milestone.md",
"gsd-core/commands/gsd/new-project.md",
"gsd-core/commands/gsd/next.md",
"gsd-core/commands/gsd/ns-context.md",
"gsd-core/commands/gsd/ns-ideate.md",
"gsd-core/commands/gsd/ns-manage.md",
"gsd-core/commands/gsd/ns-project.md",
"gsd-core/commands/gsd/ns-review.md",
"gsd-core/commands/gsd/ns-workflow.md",
"gsd-core/commands/gsd/onboard.md",
"gsd-core/commands/gsd/pause-work.md",
"gsd-core/commands/gsd/phase.md",
"gsd-core/commands/gsd/plan-phase.md",
"gsd-core/commands/gsd/plan-review-convergence.md",
"gsd-core/commands/gsd/pr-branch.md",
"gsd-core/commands/gsd/profile-user.md",
"gsd-core/commands/gsd/progress.md",
"gsd-core/commands/gsd/quick-batch.md",
"gsd-core/commands/gsd/quick.md",
"gsd-core/commands/gsd/resume-work.md",
"gsd-core/commands/gsd/review-backlog.md",
"gsd-core/commands/gsd/review.md",
"gsd-core/commands/gsd/secure-phase.md",
"gsd-core/commands/gsd/settings.md",
"gsd-core/commands/gsd/ship.md",
"gsd-core/commands/gsd/sketch.md",
"gsd-core/commands/gsd/spec-phase.md",
"gsd-core/commands/gsd/spike.md",
"gsd-core/commands/gsd/stats.md",
"gsd-core/commands/gsd/surface.md",
"gsd-core/commands/gsd/thread.md",
"gsd-core/commands/gsd/ui-phase.md",
"gsd-core/commands/gsd/ui-review.md",
"gsd-core/commands/gsd/ultraplan-phase.md",
"gsd-core/commands/gsd/undo.md",
"gsd-core/commands/gsd/update.md",
"gsd-core/commands/gsd/validate-phase.md",
"gsd-core/commands/gsd/verify-work.md",
"gsd-core/commands/gsd/workspace.md",
"gsd-core/commands/gsd/workstreams.md",
"gsd-core/contexts/dev.md",
"gsd-core/contexts/research.md",
"gsd-core/contexts/review.md",

View File

@@ -133,6 +133,7 @@ const { applySurface } = require('../gsd-core/bin/lib/surface.cjs');
const { loadSkillsManifest, resolveProfile } = require('../gsd-core/bin/lib/install-profiles.cjs');
const { resolveRuntimeArtifactLayout } = require('../gsd-core/bin/lib/runtime-artifact-layout.cjs');
const { cleanup, sandboxHome } = require('./helpers.cjs');
const { runMinimalInstall } = require('./helpers/install-shared.cjs');
const COMMANDS_GSD = path.join(ROOT, 'commands', 'gsd');
@@ -169,13 +170,14 @@ describe('#1575 — golden-parity: surface path matches install path for descrip
for (const runtime of DESCRIPTOR_RUNTIMES) {
test(`${runtime}: surface agents byte-identical to install agents`, (t) => {
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), `gsd-1575-${runtime}-`));
t.after(() => { try { cleanup(configDir); } catch { /* best-effort */ } });
const installed = runMinimalInstall({ runtime, scope: 'global' });
const { configDir, root } = installed;
t.after(() => { try { cleanup(root); } catch { /* best-effort */ } });
// #3738: antigravity's skills/agents kinds declare a global `home`
// override resolved from os.homedir() — sandbox HOME to the configDir
// override resolved from os.homedir() — sandbox HOME to the install root
// (the #3712 marker real-home-guard needs) so the override resolves
// inside the sandbox instead of the runner's real home.
sandboxHome(t, configDir);
sandboxHome(t, root);
// Step 1: install path writes agents
installRuntimeArtifacts(runtime, configDir, 'global', parity1575Profile, resolveAttribution1575);
@@ -224,8 +226,10 @@ describe('#1575 — golden-parity: surface path matches install path for descrip
// is a no-op (it replaces existing lines, doesn't add new ones). But this test
// proves the agentCtx threading is correct for both paths regardless.
const attrResolver = () => 'Test Bot <test@example.com>';
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-1575-attr-'));
t.after(() => { try { cleanup(configDir); } catch { /* best-effort */ } });
const installed = runMinimalInstall({ runtime: 'cursor', scope: 'global' });
const { configDir, root } = installed;
t.after(() => { try { cleanup(root); } catch { /* best-effort */ } });
sandboxHome(t, root);
installRuntimeArtifacts('cursor', configDir, 'global', parity1575Profile, attrResolver);
@@ -262,8 +266,9 @@ describe('#1575 — golden-parity: surface path matches install path for descrip
describe('#1575 — surface path: no prune data-loss over pre-existing legacy agents', () => {
test('pre-existing gsd-* agents not in staged set are pruned; user agents preserved', (t) => {
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-1575-prune-'));
t.after(() => { try { cleanup(configDir); } catch { /* best-effort */ } });
const installed = runMinimalInstall({ runtime: 'copilot', scope: 'global' });
const { configDir, root } = installed;
t.after(() => { try { cleanup(root); } catch { /* best-effort */ } });
// Seed a pre-existing legacy .agent.md (simulating a prior install)
const agentsDir = path.join(configDir, 'agents');

View File

@@ -1173,7 +1173,16 @@ function sandboxHome(t, dir) {
});
}
module.exports = { runGsdTools, createTempDir, createTempProject, createTempGitProject, cleanup, tmpRootCandidates, readFileNormalized, readWorkflowCombined, parseFrontmatter, isUsageOutput, captureConsole, toPosixPath, absPlanningPath, runNpm, isolatedNpmEnv, withIsolatedProcessState, delay, waitFor, resetRuntimeWarningCaches, SESSION_ENV_KEYS, saveSessionEnv, restoreSessionEnv, clearSessionEnv, isolateWorkstreamEnv, restoreWorkstreamEnv, TOOLS_PATH, SESSION_IDENTITY_ENV_KEYS, scrubConfigLocationEnv, installSpawnEnv, installSpawnHome, sandboxHome, TEST_HOME_SANDBOX_MARKER, mockPartialWriteThenThrow, captureFdSync };
function writePackageSourceMarkerFixture(configDir) {
fs.mkdirSync(configDir, { recursive: true });
fs.writeFileSync(
path.join(configDir, '.gsd-source'),
path.join(__dirname, '..', 'commands', 'gsd') + '\n',
);
return configDir;
}
module.exports = { runGsdTools, createTempDir, createTempProject, createTempGitProject, cleanup, tmpRootCandidates, readFileNormalized, readWorkflowCombined, parseFrontmatter, isUsageOutput, captureConsole, toPosixPath, absPlanningPath, runNpm, isolatedNpmEnv, withIsolatedProcessState, delay, waitFor, resetRuntimeWarningCaches, SESSION_ENV_KEYS, saveSessionEnv, restoreSessionEnv, clearSessionEnv, isolateWorkstreamEnv, restoreWorkstreamEnv, TOOLS_PATH, SESSION_IDENTITY_ENV_KEYS, scrubConfigLocationEnv, installSpawnEnv, installSpawnHome, sandboxHome, writePackageSourceMarkerFixture, TEST_HOME_SANDBOX_MARKER, mockPartialWriteThenThrow, captureFdSync };
// Lazy, for the reason builtLib() is lazy: reading either of these is what
// forces the built-lib require, so a test file that needs neither can still

View File

@@ -274,6 +274,22 @@ const PROVENANCE_RULES = [
pattern: /^(workflows|references|templates|contexts|bin)\/.+$/,
sources: (m) => [`gsd-core/${m[0]}`],
},
{
id: 'gsd-core-commands-corpus',
kind: 'rewrite',
roots: ['gsd-core'],
pattern: /^commands\/gsd\/(.+)$/,
sources: (m) => [`commands/gsd/${m[1]}`],
transforms: [INSTALL_ENGINE_SRC, INSTALLER_SRC],
},
{
id: 'gsd-core-agents-corpus',
kind: 'rewrite',
roots: ['gsd-core'],
pattern: /^agents\/(.+)$/,
sources: (m) => [`agents/${m[1]}`],
transforms: [INSTALL_ENGINE_SRC, INSTALLER_SRC],
},
{
id: 'scripts-verbatim',
kind: 'identity',

View File

@@ -182,6 +182,12 @@ describe('Hermes Agent: installRuntimeArtifacts', () => {
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-hermes-test-'));
// Hermes consumes commands and agents from one source provider. Keep the
// compatibility-marker fixture complete even when an assertion exercises
// only its command-to-skill output.
const agentsDir = path.join(tmpDir, 'src', 'agents');
fs.mkdirSync(agentsDir, { recursive: true });
fs.writeFileSync(path.join(agentsDir, 'fixture-agent.md'), '# Fixture agent\n');
});
afterEach(() => {
@@ -944,7 +950,7 @@ describe('#2284 fail-closed role resolution', () => {
assert.throws(
() => install(true, 'hermes'),
/could not resolve|refusing to install/i,
/could not resolve|refusing to install|source is unavailable or incomplete/i,
'a real hermes install must fail closed, never silently install workflows with unverifiable role references',
);
});

View File

@@ -37,6 +37,10 @@ const commandRoster = require('../gsd-core/bin/lib/command-roster.cjs');
const {
withInstallFs,
} = require('../gsd-core/bin/lib/install-fs-adapter.cjs');
const {
findInstallSourceRoot,
resolveRuntimeArtifactLayout,
} = require('../gsd-core/bin/lib/runtime-artifact-layout.cjs');
const {
stageSkillsForRuntimeAsSkills,
cleanupStagedSkills,
@@ -155,6 +159,141 @@ describe('command-roster readGsdCommandNames — package-source read stays unrou
});
});
describe('runtime artifact source identity — destination probes stay routed', () => {
test('#4132: installed provider admission streams hashes before reading corpus content', (t) => {
const configDir = createTempDir('gsd-installed-provider-');
t.after(() => cleanup(configDir));
const installedCommands = path.join(configDir, 'gsd-core', 'commands', 'gsd');
const installedAgents = path.join(configDir, 'gsd-core', 'agents');
const commandPath = path.join(installedCommands, 'help.md');
const agentPath = path.join(installedAgents, 'gsd-planner.md');
const manifestPath = path.join(configDir, 'gsd-file-manifest.json');
fs.mkdirSync(installedCommands, { recursive: true });
fs.mkdirSync(installedAgents, { recursive: true });
fs.writeFileSync(commandPath, '# installed command\n');
fs.writeFileSync(agentPath, '# installed agent\n');
fs.writeFileSync(manifestPath, JSON.stringify({ files: {
'gsd-core/commands/gsd/help.md': crypto.createHash('sha256').update(fs.readFileSync(commandPath)).digest('hex'),
'gsd-core/agents/gsd-planner.md': crypto.createHash('sha256').update(fs.readFileSync(agentPath)).digest('hex'),
} }));
const corpusPaths = new Set([commandPath, agentPath].map((candidate) => path.resolve(candidate)));
const hashingDescriptors = new Map();
const streamedHashes = new Set();
const hashedBeforeRead = new Set();
const corpusReadsBeforeHash = [];
const readCalls = [];
const recordingFs = {
...fs,
readFileSync: (candidate, ...args) => {
const resolved = path.resolve(String(candidate));
readCalls.push(resolved);
if (corpusPaths.has(resolved) && !hashedBeforeRead.has(resolved)) {
corpusReadsBeforeHash.push(resolved);
}
return fs.readFileSync(candidate, ...args);
},
openSync: (candidate, ...args) => {
const fd = fs.openSync(candidate, ...args);
const resolved = path.resolve(String(candidate));
if (corpusPaths.has(resolved)) hashingDescriptors.set(fd, resolved);
return fd;
},
readSync: (fd, ...args) => {
const resolved = hashingDescriptors.get(fd);
if (resolved) streamedHashes.add(resolved);
return fs.readSync(fd, ...args);
},
closeSync: (fd) => {
fs.closeSync(fd);
const resolved = hashingDescriptors.get(fd);
if (resolved) hashedBeforeRead.add(resolved);
},
};
const layout = resolveRuntimeArtifactLayout('claude', configDir, 'global');
let stagedSkills;
let stagedAgents;
t.after(() => {
if (stagedSkills) cleanup(stagedSkills);
if (stagedAgents) cleanup(stagedAgents);
});
withInstallFs(recordingFs, () => {
stagedSkills = layout.kinds.find((kind) => kind.kind === 'skills').stage({ skills: '*', agents: '*' });
stagedAgents = layout.kinds.find((kind) => kind.kind === 'agents').stage({ skills: '*', agents: '*' });
});
assert.deepStrictEqual(corpusReadsBeforeHash, [], 'installed corpus files must not be pre-read before manifest hashing');
assert.deepStrictEqual(streamedHashes, corpusPaths, 'commands and agents hashes must stream through readSync');
assert.deepStrictEqual(hashedBeforeRead, corpusPaths, 'commands and agents hashes must use the streaming fd path');
assert.ok(readCalls.includes(path.resolve(manifestPath)), 'provider admission must still read the install manifest');
assert.match(fs.readFileSync(path.join(stagedSkills, 'gsd-help', 'SKILL.md'), 'utf8'), /installed command/);
assert.match(fs.readFileSync(path.join(stagedAgents, 'gsd-planner.md'), 'utf8'), /installed agent/);
});
test('#4132: an installed-root alias is resolved through installFs without routing the package root', () => {
const configDir = path.join(os.tmpdir(), `gsd-fake-config-${crypto.randomUUID()}`);
const installedCommands = path.join(configDir, 'gsd-core', 'commands', 'gsd');
const probes = [];
const missing = (p) => {
const error = new Error(`ENOENT: no such file or directory, '${p}'`);
error.code = 'ENOENT';
throw error;
};
const fakeFs = {
existsSync: () => false,
lstatSync: missing,
realpathSync: (p) => {
probes.push(path.normalize(String(p)));
return path.normalize(String(p)) === path.normalize(installedCommands)
? fs.realpathSync(REAL_COMMANDS_DIR)
: path.normalize(String(p));
},
};
assert.throws(
() => withInstallFs(fakeFs, () => findInstallSourceRoot(configDir)),
/install or upgrade gsd-core/,
);
assert.deepStrictEqual(
probes,
[path.normalize(installedCommands)],
'installed destination identity must use installFs, while package-source identity stays on raw fs',
);
});
test('#4132: an unreadable physical root cannot prove a marker is independent', () => {
const configDir = path.join(os.tmpdir(), `gsd-fake-config-${crypto.randomUUID()}`);
const installedCommands = path.join(configDir, 'gsd-core', 'commands', 'gsd');
const markerCommands = path.join(configDir, 'independent', 'commands', 'gsd');
const fakeFs = createFakeFs([
[configDir, { type: 'dir' }],
[path.join(configDir, 'gsd-core'), { type: 'dir' }],
[path.join(configDir, 'gsd-core', 'commands'), { type: 'dir' }],
[installedCommands, { type: 'dir' }],
[path.join(installedCommands, 'help.md'), { type: 'file', content: '# unverified installed command\n' }],
[path.join(configDir, 'independent'), { type: 'dir' }],
[path.join(configDir, 'independent', 'commands'), { type: 'dir' }],
[markerCommands, { type: 'dir' }],
[path.join(markerCommands, 'help.md'), { type: 'file', content: '# marker command\n' }],
[path.join(configDir, '.gsd-source'), { type: 'file', content: markerCommands + '\n' }],
]);
fakeFs.realpathSync = (p) => {
if ([installedCommands, markerCommands].includes(path.normalize(String(p)))) {
const error = new Error(`EACCES: permission denied, realpath '${p}'`);
error.code = 'EACCES';
throw error;
}
return path.normalize(String(p));
};
assert.throws(
() => withInstallFs(fakeFs, () => findInstallSourceRoot(configDir)),
/install or upgrade gsd-core/,
);
});
});
// ─── (b) cleanupStagedSkills must not perform real IO on fake-staged dirs ──
describe('install-profiles cleanupStagedSkills — deferred cleanup does not leak past the restore', () => {

View File

@@ -29,7 +29,7 @@ const os = require('node:os');
const espree = require('espree');
const { splitLines, joinLines } = require('../gsd-core/bin/lib/text-lines.cjs');
const { createTempDir, cleanup } = require('./helpers.cjs');
const { createTempDir, cleanup, writePackageSourceMarkerFixture } = require('./helpers.cjs');
const { runNode } = require('./helpers/process-seam.cjs');
const { INSTALL_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
const {
@@ -172,6 +172,299 @@ describe('installRuntimeArtifacts — consumes Runtime Artifact Install Plan Mod
});
});
describe('installRuntimeArtifacts — durable Runtime Surface corpus (#4132)', () => {
test('#4132: provisioning refuses a gsd-core ancestor symlink that escapes configDir', (t) => {
const root = createTempDir('gsd-corpus-parent-symlink-');
const configDir = path.join(root, 'config');
const outside = path.join(root, 'outside');
fs.mkdirSync(configDir);
fs.mkdirSync(outside);
t.after(() => cleanup(root));
try {
fs.symlinkSync(outside, path.join(configDir, 'gsd-core'), process.platform === 'win32' ? 'junction' : 'dir');
} catch (error) {
t.skip(`directory symlink creation unavailable: ${error.code || error.message}`);
return;
}
assert.throws(
() => installRuntimeArtifacts('codex', configDir, 'global', RESOLVED_CORE),
/symlink|refusing/i,
);
assert.equal(fs.existsSync(path.join(outside, 'commands')), false, 'commands must not escape configDir');
assert.equal(fs.existsSync(path.join(outside, 'agents')), false, 'agents must not escape configDir');
});
test('#4132: provisioning does not follow a compatibility-marker symlink outside configDir', (t) => {
const root = createTempDir('gsd-corpus-marker-symlink-');
const configDir = path.join(root, '.claude');
const outsideMarker = path.join(root, 'outside-marker');
const outsideCheckout = path.join(root, 'outside-checkout');
fs.mkdirSync(configDir);
fs.cpSync(path.join(__dirname, '..', 'commands'), path.join(outsideCheckout, 'commands'), { recursive: true });
fs.cpSync(path.join(__dirname, '..', 'agents'), path.join(outsideCheckout, 'agents'), { recursive: true });
fs.appendFileSync(path.join(outsideCheckout, 'commands', 'gsd', 'help.md'), '\nMARKER_SYMLINK_SOURCE\n');
const outsideMarkerBytes = path.join(outsideCheckout, 'commands', 'gsd') + '\n';
fs.writeFileSync(outsideMarker, outsideMarkerBytes);
t.after(() => cleanup(root));
try {
fs.symlinkSync(outsideMarker, path.join(configDir, '.gsd-source'), 'file');
} catch (error) {
t.skip(`file symlink creation unavailable: ${error.code || error.message}`);
return;
}
runMinimalInstall({ runtime: 'claude', scope: 'global', root });
assert.equal(
fs.readFileSync(outsideMarker, 'utf8'),
outsideMarkerBytes,
'the compatibility marker write must remain confined to configDir',
);
assert.doesNotMatch(
fs.readFileSync(path.join(configDir, 'skills', 'gsd-help', 'SKILL.md'), 'utf8'),
/MARKER_SYMLINK_SOURCE/,
'the installer must not read a source provider through a symlinked marker file',
);
});
test('global Codex owns the canonical commands and agents corpus', (t) => {
const configDir = createTempDir('gsd-codex-surface-corpus-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
installRuntimeArtifacts('codex', configDir, 'global', RESOLVED_CORE);
assert.deepStrictEqual(
snapshot2362Dir(path.join(configDir, 'gsd-core', 'commands', 'gsd')),
snapshot2362Dir(path.join(__dirname, '..', 'commands', 'gsd')),
'installed commands corpus must match the package source tree',
);
assert.deepStrictEqual(
snapshot2362Dir(path.join(configDir, 'gsd-core', 'agents')),
snapshot2362Dir(path.join(__dirname, '..', 'agents')),
'installed agents corpus must match the package source tree',
);
});
test('agents-only and empty layouts derive corpus needs from layout kinds', (t) => {
const windsurfDir = createTempDir('gsd-windsurf-surface-corpus-');
const piDir = createTempDir('gsd-pi-surface-corpus-');
const vscodeDir = createTempDir('gsd-vscode-surface-corpus-');
t.after(() => { cleanup(windsurfDir); cleanup(piDir); cleanup(vscodeDir); });
installRuntimeArtifacts('windsurf', windsurfDir, 'global', RESOLVED_CORE);
assert.ok(fs.existsSync(path.join(windsurfDir, 'gsd-core', 'agents')));
assert.ok(!fs.existsSync(path.join(windsurfDir, 'gsd-core', 'commands', 'gsd')));
installRuntimeArtifacts('pi', piDir, 'global', RESOLVED_CORE);
installRuntimeArtifacts('vscode', vscodeDir, 'global', RESOLVED_CORE);
assert.ok(!fs.existsSync(path.join(piDir, 'gsd-core', 'commands')));
assert.ok(!fs.existsSync(path.join(piDir, 'gsd-core', 'agents')));
assert.ok(!fs.existsSync(path.join(vscodeDir, 'gsd-core', 'commands')));
assert.ok(!fs.existsSync(path.join(vscodeDir, 'gsd-core', 'agents')));
});
test('local Claude and Codex installs do not gain the global corpus', (t) => {
const claudeDir = createTempDir('gsd-claude-local-no-corpus-');
const codexDir = createTempDir('gsd-codex-local-no-corpus-');
t.after(() => { cleanup(claudeDir); cleanup(codexDir); });
installRuntimeArtifacts('claude', claudeDir, 'local', RESOLVED_CORE);
installRuntimeArtifacts('codex', codexDir, 'local', RESOLVED_CORE);
for (const configDir of [claudeDir, codexDir]) {
assert.ok(!fs.existsSync(path.join(configDir, 'gsd-core', 'commands')));
assert.ok(!fs.existsSync(path.join(configDir, 'gsd-core', 'agents')));
}
});
test('refresh restores canonical bytes and removes only manifest-proven stale corpus files', (t) => {
const configDir = createTempDir('gsd-corpus-refresh-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
installRuntimeArtifacts('codex', configDir, 'global', RESOLVED_CORE);
const commandsDir = path.join(configDir, 'gsd-core', 'commands', 'gsd');
const canonicalName = fs.readdirSync(commandsDir).find((name) => name.endsWith('.md'));
assert.ok(canonicalName);
fs.writeFileSync(path.join(commandsDir, canonicalName), '# corrupted\n');
fs.writeFileSync(path.join(commandsDir, 'stale-owned.md'), '# stale\n');
fs.writeFileSync(path.join(commandsDir, 'unknown-neighbour.md'), '# preserve\n');
fs.writeFileSync(path.join(configDir, 'gsd-file-manifest.json'), JSON.stringify({
files: { 'gsd-core/commands/gsd/stale-owned.md': '0'.repeat(64) },
}));
installRuntimeArtifacts('codex', configDir, 'global', RESOLVED_CORE);
assert.strictEqual(
fs.readFileSync(path.join(commandsDir, canonicalName), 'utf8'),
fs.readFileSync(path.join(__dirname, '..', 'commands', 'gsd', canonicalName), 'utf8'),
);
assert.ok(!fs.existsSync(path.join(commandsDir, 'stale-owned.md')));
assert.strictEqual(fs.readFileSync(path.join(commandsDir, 'unknown-neighbour.md'), 'utf8'), '# preserve\n');
});
test('a partial corpus copy failure is rejected until a later install repairs it', (t) => {
const configDir = createTempDir('gsd-corpus-copy-failure-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
const agentsDestination = path.join(configDir, 'gsd-core', 'agents');
const originalCpSync = fs.cpSync;
t.mock.method(fs, 'cpSync', (source, destination, options) => {
if (destination === agentsDestination) {
fs.mkdirSync(destination, { recursive: true });
fs.writeFileSync(path.join(destination, 'gsd-planner.md'), '# partial agent\n');
throw new Error('injected corpus copy failure');
}
return originalCpSync(source, destination, options);
});
assert.throws(
() => installRuntimeArtifacts('codex', configDir, 'global', RESOLVED_CORE),
/injected corpus copy failure/,
);
const incompleteLayout = resolveRuntimeArtifactLayout('codex', configDir, 'global');
assert.throws(
() => incompleteLayout.kinds.find((kind) => kind.kind === 'skills').stage(RESOLVED_CORE),
/install or upgrade gsd-core/,
);
t.mock.restoreAll();
installRuntimeArtifacts('codex', configDir, 'global', RESOLVED_CORE);
assert.deepStrictEqual(
snapshot2362Dir(agentsDestination),
snapshot2362Dir(path.join(__dirname, '..', 'agents')),
'a later install must heal the partial agents corpus',
);
});
test('an unreadable prior manifest preserves unproven neighbours while refreshing canonical bytes', (t) => {
const configDir = createTempDir('gsd-corpus-manifest-read-failure-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
const commandsDir = path.join(configDir, 'gsd-core', 'commands', 'gsd');
fs.mkdirSync(commandsDir, { recursive: true });
const canonicalName = fs.readdirSync(path.join(__dirname, '..', 'commands', 'gsd'))
.find((name) => name.endsWith('.md'));
assert.ok(canonicalName);
fs.writeFileSync(path.join(commandsDir, canonicalName), '# corrupted\n');
fs.writeFileSync(path.join(commandsDir, 'unproven-neighbour.md'), '# preserve\n');
const manifestPath = path.join(configDir, 'gsd-file-manifest.json');
fs.writeFileSync(manifestPath, JSON.stringify({ files: {
'gsd-core/commands/gsd/unproven-neighbour.md': '0'.repeat(64),
} }));
const originalReadFileSync = fs.readFileSync;
t.mock.method(fs, 'readFileSync', (filePath, ...args) => {
if (filePath === manifestPath) throw new Error('injected manifest read failure');
return originalReadFileSync(filePath, ...args);
});
installRuntimeArtifacts('codex', configDir, 'global', RESOLVED_CORE);
assert.equal(
fs.readFileSync(path.join(commandsDir, canonicalName), 'utf8'),
fs.readFileSync(path.join(__dirname, '..', 'commands', 'gsd', canonicalName), 'utf8'),
);
assert.equal(
fs.readFileSync(path.join(commandsDir, 'unproven-neighbour.md'), 'utf8'),
'# preserve\n',
'an unreadable manifest provides no ownership proof for deletion',
);
});
test('a stale corpus removal failure leaves a resolver-rejected corpus', (t) => {
const configDir = createTempDir('gsd-corpus-remove-failure-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
const commandsDir = path.join(configDir, 'gsd-core', 'commands', 'gsd');
const agentsDir = path.join(configDir, 'gsd-core', 'agents');
fs.mkdirSync(commandsDir, { recursive: true });
fs.mkdirSync(agentsDir, { recursive: true });
const stalePath = path.join(commandsDir, 'removed-by-4132-test.md');
fs.writeFileSync(stalePath, '# stale\n');
fs.writeFileSync(path.join(agentsDir, 'gsd-planner.md'), '# stale agent\n');
const manifestPath = path.join(configDir, 'gsd-file-manifest.json');
const manifestBytes = JSON.stringify({ files: {
'gsd-core/commands/gsd/removed-by-4132-test.md': '0'.repeat(64),
'gsd-core/agents/gsd-planner.md': '0'.repeat(64),
} });
fs.writeFileSync(manifestPath, manifestBytes);
const originalRmSync = fs.rmSync;
t.mock.method(fs, 'rmSync', (target, options) => {
if (target === stalePath) throw new Error('injected stale corpus removal failure');
// eslint-disable-next-line local/no-raw-rmsync-in-tests -- delegate non-target production writes; fixture cleanup still uses cleanup().
return originalRmSync(target, options);
});
assert.throws(
() => installRuntimeArtifacts('codex', configDir, 'global', RESOLVED_CORE),
/injected stale corpus removal failure/,
);
assert.equal(fs.readFileSync(manifestPath, 'utf8'), manifestBytes, 'failed provisioning must not claim a new manifest');
const incompleteLayout = resolveRuntimeArtifactLayout('codex', configDir, 'global');
assert.throws(
() => incompleteLayout.kinds.find((kind) => kind.kind === 'skills').stage(RESOLVED_CORE),
/install or upgrade gsd-core/,
);
});
test('an empty-parent prune failure leaves a resolver-rejected corpus', (t) => {
const configDir = createTempDir('gsd-corpus-prune-failure-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
const commandsDir = path.join(configDir, 'gsd-core', 'commands', 'gsd');
const agentsDir = path.join(configDir, 'gsd-core', 'agents');
const staleParent = path.join(commandsDir, 'retired');
const stalePath = path.join(staleParent, 'removed-by-4132-test.md');
fs.mkdirSync(staleParent, { recursive: true });
fs.mkdirSync(agentsDir, { recursive: true });
fs.writeFileSync(stalePath, '# stale\n');
fs.writeFileSync(path.join(agentsDir, 'gsd-planner.md'), '# stale agent\n');
const manifestPath = path.join(configDir, 'gsd-file-manifest.json');
const manifestBytes = JSON.stringify({ files: {
'gsd-core/commands/gsd/retired/removed-by-4132-test.md': '0'.repeat(64),
'gsd-core/agents/gsd-planner.md': '0'.repeat(64),
} });
fs.writeFileSync(manifestPath, manifestBytes);
const originalRmdirSync = fs.rmdirSync;
t.mock.method(fs, 'rmdirSync', (target, options) => {
if (target === staleParent) throw new Error('injected corpus parent prune failure');
return originalRmdirSync(target, options);
});
assert.throws(
() => installRuntimeArtifacts('codex', configDir, 'global', RESOLVED_CORE),
/injected corpus parent prune failure/,
);
assert.equal(fs.existsSync(stalePath), false, 'owned stale file was removed before parent pruning failed');
assert.equal(fs.existsSync(staleParent), true, 'failed empty parent removal must leave the directory in place');
assert.equal(fs.readFileSync(manifestPath, 'utf8'), manifestBytes, 'failed provisioning must not claim a new manifest');
const incompleteLayout = resolveRuntimeArtifactLayout('codex', configDir, 'global');
assert.throws(
() => incompleteLayout.kinds.find((kind) => kind.kind === 'skills').stage(RESOLVED_CORE),
/install or upgrade gsd-core/,
);
});
for (const runtime of ['codex', 'claude']) {
test(`full global ${runtime} manifest covers every installed corpus file with its hash`, (t) => {
const installed = runMinimalInstall({ runtime, scope: 'global' });
t.after(() => cleanup(installed.root));
const corpusRoots = [
['gsd-core/commands/gsd/', path.join(installed.configDir, 'gsd-core', 'commands', 'gsd')],
['gsd-core/agents/', path.join(installed.configDir, 'gsd-core', 'agents')],
];
for (const [prefix, root] of corpusRoots) {
for (const file of walk(root)) {
const key = prefix + path.relative(root, file).replace(/\\/g, '/');
const hash = crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
assert.strictEqual(installed.manifest.files[key], hash, `${key} must be manifest-owned with the installed hash`);
}
}
});
}
});
const SKILLS_RUNTIMES_LAYOUT = [
'claude', 'cursor', 'codex', 'copilot', 'antigravity',
'augment', 'trae', 'qwen', 'kimi', 'codebuddy',
@@ -415,6 +708,7 @@ describe('installOpencodeFamilySkills — emits skills/<name>/SKILL.md (#784)',
test(`${runtime}: writes gsd-help/SKILL.md with name + description`, (t) => {
const configDir = createTempDir(`gsd-ocs-${runtime}-`);
t.after(() => cleanup(configDir));
writePackageSourceMarkerFixture(configDir);
const raw = stageRawCommands(runtime, configDir);
const count = installOpencodeFamilySkills(runtime, configDir, raw, `${configDir}/`);
@@ -431,6 +725,7 @@ describe('installOpencodeFamilySkills — emits skills/<name>/SKILL.md (#784)',
test(`${runtime}: rewrites body paths to the actual install target (#784 path fix)`, (t) => {
const configDir = createTempDir(`gsd-ocp-${runtime}-`);
t.after(() => cleanup(configDir));
writePackageSourceMarkerFixture(configDir);
// Simulate a custom/local install: pathPrefix points at configDir, NOT the
// runtime's default global config dir. Body refs must use pathPrefix.
@@ -461,6 +756,7 @@ describe('installOpencodeFamilySkills — emits skills/<name>/SKILL.md (#784)',
test(`${runtime}: preserves user-owned gsd-dev-preferences across reinstall (#784)`, (t) => {
const configDir = createTempDir(`gsd-ocd-${runtime}-`);
t.after(() => cleanup(configDir));
writePackageSourceMarkerFixture(configDir);
const userSkill = path.join(configDir, 'skills', 'gsd-dev-preferences');
fs.mkdirSync(userSkill, { recursive: true });
@@ -2656,6 +2952,7 @@ describe('fix-2644 — Cursor has one menu entry per GSD workflow', () => {
version: '1.8.0', timestamp: '2026-07-28T00:00:00.000Z', mode: 'core',
files: { 'commands/gsd-help.md': crypto.createHash('sha256').update(content).digest('hex') },
}));
writePackageSourceMarkerFixture(configDir);
const layout = resolveRuntimeArtifactLayout('cursor', configDir, 'global');
applySurface(configDir, layout, MANIFEST);
@@ -5582,7 +5879,13 @@ const {
*/
function writeMinimalSourceTree(baseDir, stems) {
const srcDir = path.join(baseDir, 'src', 'commands', 'gsd');
const agentsDir = path.join(baseDir, 'src', 'agents');
fs.mkdirSync(srcDir, { recursive: true });
fs.mkdirSync(agentsDir, { recursive: true });
// Hermes resolves one complete provider for its skills+agents layout. Keep
// this legacy-marker fixture complete so it exercises marker compatibility
// without relying on forbidden per-kind provider mixing (#4132).
fs.writeFileSync(path.join(agentsDir, 'gsd-test-agent.md'), '# test agent\n');
for (const stem of stems) {
fs.writeFileSync(path.join(srcDir, `${stem}.md`), [
'---',
@@ -7583,6 +7886,11 @@ describe('#3719: real global Claude install — agents/*.md @-refs must resolve
const failures = [];
for (const file of walk(rootDir)) {
if (!file.endsWith('.md')) continue;
const relative = path.relative(claudeGlobal.configDir, file).replace(/\\/g, '/');
// The installation-owned Runtime Surface corpus is raw package input,
// not an emitted runtime artifact. It must remain byte-identical to the
// package and is validated separately by the corpus parity matrix.
if (relative.startsWith('gsd-core/commands/gsd/') || relative.startsWith('gsd-core/agents/')) continue;
const content = fs.readFileSync(file, 'utf8');
for (const line of splitLines(content)) {
if (/@\$HOME\//.test(line.replace(INLINE_CODE_SPAN_RE, ''))) failures.push({ file, line });

View File

@@ -738,6 +738,13 @@ describe('#3544: spawned installer — gsd-core/ spec tree @-refs resolve on til
const walk = (dir) => {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
const rel = path.relative(rootDir, full);
const topLevel = rel.split(path.sep)[0];
// The durable Runtime Surface corpus is intentionally raw canonical
// input, not an emitted/runtime-rewritten spec tree. Its paths are
// validated when staged, so exclude only these two known corpus roots
// from this legacy emitted-content oracle.
if (topLevel === 'commands' || topLevel === 'agents') continue;
if (entry.isDirectory()) walk(full);
else if (entry.name.endsWith('.md')) {
const content = fs.readFileSync(full, 'utf8');

View File

@@ -136,6 +136,12 @@ describe('Qwen Code: installRuntimeArtifacts', () => {
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-qwen-test-'));
// Qwen's global layout consumes commands and agents from one provider.
// Keep this legacy-marker fixture complete even though these assertions
// exercise only the command-to-skill half of the install.
const agentsDir = path.join(tmpDir, 'src', 'agents');
fs.mkdirSync(agentsDir, { recursive: true });
fs.writeFileSync(path.join(agentsDir, 'fixture-agent.md'), '# Fixture agent\n');
});
afterEach(() => {

View File

@@ -11,6 +11,8 @@ const { describe, test, before, after } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
const { spawnSync } = require('node:child_process');
const { cleanup, createTempDir, runNpm, isolatedNpmEnv } = require('./helpers.cjs');
const { SMOKE, runSmoke, CHILD_TIMEOUT_MS } = require('../scripts/release-tarball-smoke.cjs');
@@ -21,6 +23,31 @@ const smokeMsg = (label, result) =>
const PKG_PATH = path.join(__dirname, '..', 'package.json');
const pkg = JSON.parse(fs.readFileSync(PKG_PATH, 'utf-8'));
function installedPackageRoot(prefix) {
const parts = pkg.name.split('/');
const posix = path.join(prefix, 'lib', 'node_modules', ...parts);
const windows = path.join(prefix, 'node_modules', ...parts);
return fs.existsSync(posix) ? posix : windows;
}
function hashTree(root) {
const result = new Map();
const visit = (dir) => {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const absolute = path.join(dir, entry.name);
if (entry.isDirectory()) visit(absolute);
else if (entry.isFile()) {
result.set(
path.relative(root, absolute).replace(/\\/g, '/'),
crypto.createHash('sha256').update(fs.readFileSync(absolute)).digest('hex'),
);
}
}
};
visit(root);
return result;
}
// ─── runSmoke install timeout must clear a slow-host cold install (#2335) ────
// Regression: runSmoke()'s internal `npm install -g` used CHILD_TIMEOUT_MS,
// which was 120 s on non-Windows while before()'s pack+install used 600 s. A
@@ -207,6 +234,208 @@ describe('release-tarball-smoke', () => {
smokeMsg('E', result),
);
});
test('F: packed package carries the complete canonical Runtime Surface corpus', () => {
const packageRoot = installedPackageRoot(installPrefix);
assert.deepStrictEqual(
hashTree(path.join(packageRoot, 'commands', 'gsd')),
hashTree(path.join(__dirname, '..', 'commands', 'gsd')),
);
assert.deepStrictEqual(
hashTree(path.join(packageRoot, 'agents')),
hashTree(path.join(__dirname, '..', 'agents')),
);
});
test('G: deployed Codex and Claude modules materially change a surface after package source is unreachable', (t) => {
const runtimeRoot = createTempDir('gsd-smoke-offline-runtime-');
const packageRoot = installedPackageRoot(installPrefix);
const hiddenPackageRoot = `${packageRoot}.offline-${process.pid}`;
t.after(() => {
if (fs.existsSync(hiddenPackageRoot) && !fs.existsSync(packageRoot)) fs.renameSync(hiddenPackageRoot, packageRoot);
cleanup(runtimeRoot);
});
const installs = [];
for (const runtime of ['codex', 'claude']) {
const configDir = path.join(runtimeRoot, `.${runtime}`);
const result = spawnSync(process.execPath, [
path.join(packageRoot, 'bin', 'install.js'),
`--${runtime}`,
'--global',
'--config-dir',
configDir,
], {
cwd: runtimeRoot,
env: {
...process.env,
...isolatedNpmEnv(),
HOME: runtimeRoot,
USERPROFILE: runtimeRoot,
GSD_TEST_MODE: '',
NO_UPDATE_NOTIFIER: '1',
npm_config_update_notifier: 'false',
},
encoding: 'utf8',
timeout: CHILD_TIMEOUT_MS,
});
assert.equal(result.status, 0, `${runtime} packed install failed:\n${result.stdout}\n${result.stderr}`);
installs.push({ runtime, configDir });
}
// Synthetic untouched 1.12-style deployed tree: converted outputs and a
// hash manifest exist, but the raw corpus/marker do not. It must not use
// those outputs as source when the executing package disappears.
const legacyConfigDir = path.join(runtimeRoot, '.legacy-codex');
fs.cpSync(installs.find((entry) => entry.runtime === 'codex').configDir, legacyConfigDir, { recursive: true });
cleanup(path.join(legacyConfigDir, 'gsd-core', 'commands'));
cleanup(path.join(legacyConfigDir, 'gsd-core', 'agents'));
const legacyMarker = path.join(legacyConfigDir, '.gsd-source');
if (fs.existsSync(legacyMarker)) fs.unlinkSync(legacyMarker);
// S04: upgrade a synthetic source-less legacy tree while the fixed
// package is available. Preserve its exact committed selection and an
// unrelated file, converge artifacts to that selection, and later prove
// an offline expansion works from the provisioned corpus alone.
const upgradedHome = path.join(runtimeRoot, 'legacy-codex-upgrade-home');
const upgradeCwd = path.join(runtimeRoot, 'legacy-codex-upgrade-worktree');
const upgradedConfigDir = path.join(upgradedHome, '.codex');
fs.mkdirSync(upgradeCwd, { recursive: true });
fs.cpSync(installs.find((entry) => entry.runtime === 'codex').configDir, upgradedConfigDir, { recursive: true });
fs.cpSync(path.join(runtimeRoot, '.agents', 'skills'), path.join(upgradedHome, '.agents', 'skills'), { recursive: true });
fs.mkdirSync(path.join(upgradedHome, '.agents', 'skills', 'user-owned-skill'), { recursive: true });
fs.writeFileSync(path.join(upgradedHome, '.agents', 'skills', 'user-owned-skill', 'SKILL.md'), '# preserve me\n');
cleanup(path.join(upgradedConfigDir, 'gsd-core', 'commands'));
cleanup(path.join(upgradedConfigDir, 'gsd-core', 'agents'));
const upgradedMarker = path.join(upgradedConfigDir, '.gsd-source');
if (fs.existsSync(upgradedMarker)) fs.unlinkSync(upgradedMarker);
const selectedState = JSON.stringify({ baseProfile: 'core', disabledClusters: [], explicitAdds: [], explicitRemoves: [] }, null, 2) + '\n';
const priorGates = JSON.stringify({ workflow: { code_review: false, research: true } }, null, 2) + '\n';
fs.writeFileSync(path.join(upgradedConfigDir, '.gsd-surface.json'), selectedState);
fs.writeFileSync(path.join(upgradedConfigDir, 'user-owned.txt'), 'preserve me\n');
fs.mkdirSync(path.join(upgradeCwd, '.planning'), { recursive: true });
fs.writeFileSync(path.join(upgradeCwd, '.planning', 'config.json'), priorGates);
const upgradeResult = spawnSync(process.execPath, [
path.join(packageRoot, 'bin', 'install.js'),
'--codex',
'--global',
'--config-dir',
upgradedConfigDir,
], {
cwd: upgradeCwd,
env: {
...process.env,
...isolatedNpmEnv(),
HOME: upgradedHome,
USERPROFILE: upgradedHome,
GSD_TEST_MODE: '',
NO_UPDATE_NOTIFIER: '1',
npm_config_update_notifier: 'false',
},
encoding: 'utf8',
timeout: CHILD_TIMEOUT_MS,
});
assert.equal(upgradeResult.status, 0, `legacy upgrade failed:\n${upgradeResult.stdout}\n${upgradeResult.stderr}`);
assert.equal(fs.readFileSync(path.join(upgradedConfigDir, '.gsd-surface.json'), 'utf8'), selectedState);
assert.equal(fs.readFileSync(path.join(upgradedConfigDir, 'user-owned.txt'), 'utf8'), 'preserve me\n');
assert.equal(fs.readFileSync(path.join(upgradeCwd, '.planning', 'config.json'), 'utf8'), priorGates);
assert.deepStrictEqual(
hashTree(path.join(upgradedConfigDir, 'gsd-core', 'commands', 'gsd')),
hashTree(path.join(packageRoot, 'commands', 'gsd')),
);
assert.deepStrictEqual(
hashTree(path.join(upgradedConfigDir, 'gsd-core', 'agents')),
hashTree(path.join(packageRoot, 'agents')),
);
const upgradedSkillRoot = path.join(upgradedHome, '.agents', 'skills');
const upgradedSkillCount = fs.readdirSync(upgradedSkillRoot).filter((name) => name.startsWith('gsd-')).length;
assert.ok(upgradedSkillCount > 0 && upgradedSkillCount < 71, `upgrade must converge the selected core surface, got ${upgradedSkillCount}`);
assert.equal(fs.readFileSync(path.join(upgradedSkillRoot, 'user-owned-skill', 'SKILL.md'), 'utf8'), '# preserve me\n');
fs.renameSync(packageRoot, hiddenPackageRoot);
const upgradedOfflineScript = [
"const fs=require('node:fs'),path=require('node:path');",
`const configDir=${JSON.stringify(upgradedConfigDir)};`,
"const lib=path.join(configDir,'gsd-core','bin','lib');",
"const surface=require(path.join(lib,'surface.cjs'));",
"const layoutModule=require(path.join(lib,'runtime-artifact-layout.cjs'));",
"const profiles=require(path.join(lib,'install-profiles.cjs'));",
"const clusters=require(path.join(lib,'clusters.cjs'));",
"const manifest=profiles.loadSkillsManifest(path.join(configDir,'gsd-core','commands','gsd'));",
"const layout=layoutModule.resolveRuntimeArtifactLayout('codex',configDir,'global');",
"const skillKind=layout.kinds.find(kind=>kind.kind==='skills');",
"const skillRoot=path.join(skillKind.home||configDir,skillKind.destSubpath);",
"const before=fs.readdirSync(skillRoot).filter(n=>n.startsWith('gsd-')).length;",
"surface.applySurface(configDir,layout,manifest,clusters.CLUSTERS,undefined,{surfaceState:{baseProfile:'full',disabledClusters:[],explicitAdds:[],explicitRemoves:[]}});",
"const after=fs.readdirSync(skillRoot).filter(n=>n.startsWith('gsd-')).length;",
"if(!(after>before))throw new Error(`offline upgraded expansion failed: ${before} -> ${after}`);",
].join('');
const upgradedOfflineResult = spawnSync(process.execPath, ['-e', upgradedOfflineScript], {
cwd: runtimeRoot,
env: { ...process.env, HOME: upgradedHome, USERPROFILE: upgradedHome, GSD_TEST_MODE: '' },
encoding: 'utf8',
timeout: CHILD_TIMEOUT_MS,
});
assert.equal(upgradedOfflineResult.status, 0, `upgraded offline surface failed:\n${upgradedOfflineResult.stdout}\n${upgradedOfflineResult.stderr}`);
const legacyChildScript = [
"const fs=require('node:fs'),path=require('node:path');",
`const configDir=${JSON.stringify(legacyConfigDir)};`,
"const lib=path.join(configDir,'gsd-core','bin','lib');",
"const surface=require(path.join(lib,'surface.cjs'));",
"const layoutModule=require(path.join(lib,'runtime-artifact-layout.cjs'));",
"const profiles=require(path.join(lib,'install-profiles.cjs'));",
"const clusters=require(path.join(lib,'clusters.cjs'));",
"const surfacePath=path.join(configDir,'.gsd-surface.json');",
"const agentPath=path.join(configDir,'agents','gsd-planner.md');",
"const beforeSurface=fs.existsSync(surfacePath)?fs.readFileSync(surfacePath):null;",
"const beforeAgent=fs.readFileSync(agentPath);",
"const layout=layoutModule.resolveRuntimeArtifactLayout('codex',configDir,'global');",
"let error=null;try{surface.applySurface(configDir,layout,new Map(),clusters.CLUSTERS,undefined,{surfaceState:{baseProfile:'core',disabledClusters:[],explicitAdds:[],explicitRemoves:[]}})}catch(value){error=value}",
"if(!error||!/install or upgrade gsd-core/.test(error.message))throw new Error(`expected actionable source failure, got ${error&&error.message}`);",
"const afterSurface=fs.existsSync(surfacePath)?fs.readFileSync(surfacePath):null;",
"if(!Buffer.isBuffer(beforeAgent)||!beforeAgent.equals(fs.readFileSync(agentPath)))throw new Error('legacy output changed');",
"if(beforeSurface===null?afterSurface!==null:!beforeSurface.equals(afterSurface))throw new Error('legacy surface state changed');",
].join('');
const legacyResult = spawnSync(process.execPath, ['-e', legacyChildScript], {
cwd: runtimeRoot,
env: { ...process.env, HOME: runtimeRoot, USERPROFILE: runtimeRoot, GSD_TEST_MODE: '' },
encoding: 'utf8',
timeout: CHILD_TIMEOUT_MS,
});
assert.equal(legacyResult.status, 0, `legacy source-less refusal failed:\n${legacyResult.stdout}\n${legacyResult.stderr}`);
for (const { runtime, configDir } of installs) {
const childScript = [
"const path=require('node:path');",
`const configDir=${JSON.stringify(configDir)};`,
`const runtime=${JSON.stringify(runtime)};`,
"const lib=path.join(configDir,'gsd-core','bin','lib');",
"const surface=require(path.join(lib,'surface.cjs'));",
"const layoutModule=require(path.join(lib,'runtime-artifact-layout.cjs'));",
"const profiles=require(path.join(lib,'install-profiles.cjs'));",
"const clusters=require(path.join(lib,'clusters.cjs'));",
"const corpus=path.join(configDir,'gsd-core','commands','gsd');",
"const manifest=profiles.loadSkillsManifest(corpus);",
"const layout=layoutModule.resolveRuntimeArtifactLayout(runtime,configDir,'global');",
"const before=layout.kinds.map(k=>{const root=path.join(k.home||configDir,k.destSubpath);try{return require('node:fs').readdirSync(root).length}catch{return 0}}).reduce((a,b)=>a+b,0);",
"const state={baseProfile:'core',disabledClusters:[],explicitAdds:[],explicitRemoves:[]};",
"surface.applySurface(configDir,layout,manifest,clusters.CLUSTERS,undefined,{surfaceState:state});",
"const after=layout.kinds.map(k=>{const root=path.join(k.home||configDir,k.destSubpath);try{return require('node:fs').readdirSync(root).length}catch{return 0}}).reduce((a,b)=>a+b,0);",
"if(!(after>0&&after<before))throw new Error(`surface did not materially shrink: ${before} -> ${after}`);",
"process.stdout.write(JSON.stringify({before,after}));",
].join('');
const result = spawnSync(process.execPath, ['-e', childScript], {
cwd: runtimeRoot,
env: { ...process.env, HOME: runtimeRoot, USERPROFILE: runtimeRoot, GSD_TEST_MODE: '' },
encoding: 'utf8',
timeout: CHILD_TIMEOUT_MS,
});
assert.equal(result.status, 0, `${runtime} offline materialization failed:\n${result.stdout}\n${result.stderr}`);
const counts = JSON.parse(result.stdout);
assert.ok(counts.after > 0 && counts.after < counts.before);
}
});
});

View File

@@ -38,7 +38,8 @@ const {
capabilityClusterStems,
CAPABILITY_SKILL_MARKER,
} = require('../gsd-core/bin/lib/install-profiles.cjs');
const { createTempDir, cleanup } = require('./helpers.cjs');
const { createTempDir, cleanup, writePackageSourceMarkerFixture } = require('./helpers.cjs');
const { resolveRuntimeArtifactLayout: resolveLayout } = require('../gsd-core/bin/lib/runtime-artifact-layout.cjs');
const REAL_COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
const REAL_AGENTS_DIR = path.join(__dirname, '..', 'agents');
@@ -738,6 +739,7 @@ function createFixture() {
const configDir = createTempDir('gsd-bug3659-');
const skillsDir = path.join(configDir, 'skills');
fs.mkdirSync(skillsDir, { recursive: true });
fs.writeFileSync(path.join(configDir, '.gsd-source'), REAL_COMMANDS_DIR + '\n');
const gsdExplore = path.join(skillsDir, 'gsd-explore');
const gsdHelp = path.join(skillsDir, 'gsd-help');
@@ -953,7 +955,7 @@ describe('bug-3659: applySurface prunes ~/.claude/skills/gsd-*/ on cluster disab
const { describe: __issueDescribe, test: __issueTest } = require('node:test');
const surfaceMod = require('../gsd-core/bin/lib/surface.cjs');
const { writeSurface: __writeSurface, applySurface: __applySurface } = surfaceMod;
const { resolveRuntimeArtifactLayout: __resolveRuntimeArtifactLayout } = require('../gsd-core/bin/lib/runtime-artifact-layout.cjs');
const __resolveRuntimeArtifactLayout = resolveLayout;
/** Install a fake already-installed third-party capability skill under a sandboxed GSD_HOME. */
function __installCapSkill(gsdHome, capId, stem, content) {
@@ -1005,7 +1007,7 @@ describe('bug-3659: applySurface prunes ~/.claude/skills/gsd-*/ on cluster disab
__issueDescribe('issue-2322: applySurface materializes installed third-party capability skills', () => {
__issueTest('(1) primary: installed capability skill materializes at <prefix><stem>/SKILL.md and IS subject to the same runtime body rewrites as first-party content', () => {
const gsdHome = createTempDir('gsd-2322-home-');
const configDir = createTempDir('gsd-2322-cfg-');
const configDir = writePackageSourceMarkerFixture(createTempDir('gsd-2322-cfg-'));
const savedHome = process.env.GSD_HOME;
try {
// #2322 MEDIUM-4: a fixture with NO rewrite-triggering content passes
@@ -1048,7 +1050,7 @@ describe('bug-3659: applySurface prunes ~/.claude/skills/gsd-*/ on cluster disab
__issueTest('(2) collision: a first-party stem always wins over a same-named third-party capability skill', () => {
const gsdHome = createTempDir('gsd-2322-home-');
const configDir = createTempDir('gsd-2322-cfg-');
const configDir = writePackageSourceMarkerFixture(createTempDir('gsd-2322-cfg-'));
const savedHome = process.env.GSD_HOME;
try {
const EVIL = '---\nname: phase\n---\n\n# EVIL PHASE — must never win over the first-party gsd-phase skill\n';
@@ -1094,7 +1096,7 @@ describe('bug-3659: applySurface prunes ~/.claude/skills/gsd-*/ on cluster disab
__issueTest('(3) profile filter: a third-party skill outside the resolved profile is not staged; sibling first-party skills are unaffected', () => {
const gsdHome = createTempDir('gsd-2322-home-');
const configDir = createTempDir('gsd-2322-cfg-');
const configDir = writePackageSourceMarkerFixture(createTempDir('gsd-2322-cfg-'));
const savedHome = process.env.GSD_HOME;
try {
__installCapSkill(gsdHome, 'my-thing', 'my-thing', '# my-thing\n');
@@ -1129,7 +1131,7 @@ describe('bug-3659: applySurface prunes ~/.claude/skills/gsd-*/ on cluster disab
__issueTest('(4) control: a nested-router (doNest) runtime layout is unperturbed by an installed capability skill', () => {
const gsdHome = createTempDir('gsd-2322-home-');
const configDir = createTempDir('gsd-2322-cfg-');
const configDir = writePackageSourceMarkerFixture(createTempDir('gsd-2322-cfg-'));
const savedHome = process.env.GSD_HOME;
try {
__installCapSkill(gsdHome, 'my-thing', 'my-thing', '# my-thing\n');
@@ -1170,8 +1172,8 @@ describe('bug-3659: applySurface prunes ~/.claude/skills/gsd-*/ on cluster disab
__issueTest('(5) absent/malformed: a registry-referenced capability skill missing on disk must not throw and must not affect first-party output', () => {
const gsdHome = createTempDir('gsd-2322-home-');
const configDirGhost = createTempDir('gsd-2322-cfg-ghost-');
const configDirPartial = createTempDir('gsd-2322-cfg-partial-');
const configDirGhost = writePackageSourceMarkerFixture(createTempDir('gsd-2322-cfg-ghost-'));
const configDirPartial = writePackageSourceMarkerFixture(createTempDir('gsd-2322-cfg-partial-'));
const savedHome = process.env.GSD_HOME;
try {
// Partial case: the capability dir exists but the declared stem's
@@ -1215,7 +1217,7 @@ describe('bug-3659: applySurface prunes ~/.claude/skills/gsd-*/ on cluster disab
__issueDescribe('issue-2322 BLOCKER 1: capability skill stem hijack via an undeclared/unregistered directory', () => {
__issueTest('an UNDECLARED skills/<stem>/ directory shipped by one capability must never supply another capability\'s declared+registered stem', () => {
const gsdHome = createTempDir('gsd-2322-home-');
const configDir = createTempDir('gsd-2322-cfg-');
const configDir = writePackageSourceMarkerFixture(createTempDir('gsd-2322-cfg-'));
const savedHome = process.env.GSD_HOME;
try {
const EVIL = '---\nname: deploy\n---\n\n# EVIL deploy — planted by aaa-utils, which never declared this skill\n';
@@ -1282,7 +1284,7 @@ describe('bug-3659: applySurface prunes ~/.claude/skills/gsd-*/ on cluster disab
__issueDescribe('issue-2322 BLOCKER 2: mode=full (the "*" sentinel) must still stage registered third-party capability skills', () => {
__issueTest('resolveProfile({modes:["full"]})\'s "*" sentinel, passed straight to stageSkillsForRuntimeAsSkills, still materializes a registered capability skill', () => {
const gsdHome = createTempDir('gsd-2322-home-');
const configDir = createTempDir('gsd-2322-cfg-');
const configDir = writePackageSourceMarkerFixture(createTempDir('gsd-2322-cfg-'));
const savedHome = process.env.GSD_HOME;
let unitStaged;
let e2eStaged;
@@ -1354,7 +1356,7 @@ describe('bug-3659: applySurface prunes ~/.claude/skills/gsd-*/ on cluster disab
__issueDescribe('issue-2322 HIGH-3: an orphaned (uninstalled/unsurfaced) capability skill is pruned; a genuinely unknown gsd-* dir is still preserved', () => {
__issueTest('uninstalling the capability + re-applying removes its staged skill; an unrelated hand-made gsd-* dir survives with a warning', () => {
const gsdHome = createTempDir('gsd-2322-home-');
const configDir = createTempDir('gsd-2322-cfg-');
const configDir = writePackageSourceMarkerFixture(createTempDir('gsd-2322-cfg-'));
const savedHome = process.env.GSD_HOME;
try {
__installCapSkill(gsdHome, 'my-thing', 'my-thing', '# my-thing\n');
@@ -1521,7 +1523,7 @@ const {
resolveProfile,
} = require('../gsd-core/bin/lib/install-profiles.cjs');
const { applySurface } = require('../gsd-core/bin/lib/surface.cjs');
const { resolveRuntimeArtifactLayout } = require('../gsd-core/bin/lib/runtime-artifact-layout.cjs');
const resolveRuntimeArtifactLayout = resolveLayout;
const MANIFEST = loadSkillsManifest(COMMANDS_GSD);
const RESOLVED_FULL = resolveProfile({ modes: ['full'], manifest: MANIFEST });
@@ -1627,6 +1629,7 @@ describe('bug-924: applySurface on claude preserves flat layout (no re-nesting)'
before(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-924-surface-'));
installRuntimeArtifacts('claude', tmpDir, 'global', RESOLVED_FULL);
writePackageSourceMarkerFixture(tmpDir);
});
after(() => {

View File

@@ -16,7 +16,8 @@ const { writeSurface, readSurface, resolveSurface, listSurface, applySurface } =
const { loadSkillsManifest, writeActiveProfile, resolveProfile } = require('../gsd-core/bin/lib/install-profiles.cjs');
const { resolveRuntimeArtifactLayout } = require('../gsd-core/bin/lib/runtime-artifact-layout.cjs');
const { CLUSTERS, allClusteredSkills } = require('../gsd-core/bin/lib/clusters.cjs');
const { createTempDir, cleanup, sandboxHome } = require('./helpers.cjs');
const { createTempDir, cleanup, sandboxHome, writePackageSourceMarkerFixture } = require('./helpers.cjs');
const { runMinimalInstall } = require('./helpers/install-shared.cjs');
const REAL_COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
@@ -34,6 +35,7 @@ function createFixtureRuntime() {
const agentsDir = path.join(runtimeConfigDir, 'agents');
fs.mkdirSync(commandsDir, { recursive: true });
fs.mkdirSync(agentsDir, { recursive: true });
fs.writeFileSync(path.join(runtimeConfigDir, '.gsd-source'), REAL_COMMANDS_DIR + '\n');
return { base, runtimeConfigDir, commandsDir, agentsDir };
}
@@ -56,9 +58,68 @@ function readFrontmatterDescription(markdown) {
return '';
}
function surfaceBytesForTest(state) {
return JSON.stringify(state, null, 2) + '\n';
}
// ─── applySurface ────────────────────────────────────────────────────────────
describe('applySurface', () => {
test('#4132: an unmanifested installed command cannot become a live instruction', (t) => {
const installed = runMinimalInstall({ runtime: 'claude', scope: 'global' });
const configDir = installed.configDir;
t.after(() => cleanup(installed.root));
const rogueSource = path.join(configDir, 'gsd-core', 'commands', 'gsd', 'rogue.md');
const rogueSkill = path.join(configDir, 'skills', 'gsd-rogue', 'SKILL.md');
fs.writeFileSync(rogueSource, '<instructions>ROGUE</instructions>\n');
const layout = resolveRuntimeArtifactLayout('claude', configDir, 'global');
assert.throws(
() => applySurface(configDir, layout, realManifest(), CLUSTERS, undefined, {
surfaceState: { baseProfile: 'full', disabledClusters: [], explicitAdds: [], explicitRemoves: [] },
}),
/install or upgrade gsd-core/,
);
assert.equal(fs.existsSync(rogueSkill), false, 'unmanifested Markdown must not reach the live skill surface');
});
test('#4132: a marker below rejected installed commands cannot promote instructions', (t) => {
const installed = runMinimalInstall({ runtime: 'claude', scope: 'global' });
const configDir = installed.configDir;
t.after(() => cleanup(installed.root));
const manifest = JSON.parse(fs.readFileSync(path.join(configDir, 'gsd-file-manifest.json'), 'utf8'));
const commandKey = Object.keys(manifest.files).find((key) => key.startsWith('gsd-core/commands/gsd/'));
assert.ok(commandKey, 'precondition: install manifest must own a command corpus file');
fs.appendFileSync(path.join(configDir, ...commandKey.split('/')), '\n# corrupted after install\n');
const installedCommands = path.join(configDir, 'gsd-core', 'commands', 'gsd');
const nestedMarkerCommands = path.join(installedCommands, 'nested');
const markerAgents = path.resolve(path.dirname(nestedMarkerCommands), '..', 'agents');
fs.mkdirSync(nestedMarkerCommands, { recursive: true });
fs.mkdirSync(markerAgents, { recursive: true });
fs.writeFileSync(path.join(nestedMarkerCommands, 'rogue.md'), '<instructions>ROGUE</instructions>\n');
fs.copyFileSync(path.join(__dirname, '..', 'agents', 'gsd-planner.md'), path.join(markerAgents, 'gsd-planner.md'));
fs.appendFileSync(path.join(markerAgents, 'gsd-planner.md'), '\nMARKER_AGENT_USED\n');
fs.writeFileSync(path.join(configDir, '.gsd-source'), nestedMarkerCommands + '\n');
const rogueSkill = path.join(configDir, 'skills', 'gsd-rogue', 'SKILL.md');
const liveAgent = path.join(configDir, 'agents', 'gsd-planner.md');
const oldAgent = fs.readFileSync(liveAgent);
const oldState = { baseProfile: 'full', disabledClusters: [], explicitAdds: [], explicitRemoves: [] };
writeSurface(configDir, oldState);
const oldSurface = fs.readFileSync(path.join(configDir, '.gsd-surface.json'));
const layout = resolveRuntimeArtifactLayout('claude', configDir, 'global');
assert.throws(
() => applySurface(configDir, layout, realManifest(), CLUSTERS, undefined, { surfaceState: oldState }),
/install or upgrade gsd-core/,
);
assert.equal(fs.existsSync(rogueSkill), false, 'nested marker Markdown must not reach the live skill surface');
assert.deepEqual(fs.readFileSync(liveAgent), oldAgent, 'marker agents must not replace live agents');
assert.deepEqual(fs.readFileSync(path.join(configDir, '.gsd-surface.json')), oldSurface);
});
test('core profile: only core skills appear in commandsDir', (t) => {
// #1367: claude local uses flat gsd-<stem>.md files at commands/ (not commands/gsd/<stem>.md).
const { base, runtimeConfigDir, commandsDir } = createFixtureRuntime();
@@ -324,6 +385,7 @@ describe('applySurface', () => {
test('applySurface writes gsd-prefixed command files matching install and preserves user commands (#816)', (t) => {
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-surface-816-'));
t.after(() => cleanup(configDir));
writePackageSourceMarkerFixture(configDir);
writeActiveProfile(configDir, 'standard');
writeSurface(configDir, {
@@ -406,6 +468,7 @@ describe('applySurface', () => {
const installDir = fs.mkdtempSync(path.join(os.tmpdir(), `gsd-816-install-${runtime}-`));
const surfaceDir = fs.mkdtempSync(path.join(os.tmpdir(), `gsd-816-surface-${runtime}-`));
t.after(() => { cleanup(installDir); cleanup(surfaceDir); });
writePackageSourceMarkerFixture(surfaceDir);
// --- Install path ---
installRuntimeArtifacts(runtime, installDir, 'global', resolvedProfile);
@@ -531,6 +594,118 @@ describe('applySurface', () => {
);
});
}
test('#4132: a later kind staging failure leaves exact all-old state and artifacts', (t) => {
const root = createTempDir('gsd-surface-stage-failure-');
t.after(() => cleanup(root));
const firstDest = path.join(root, 'first');
const secondDest = path.join(root, 'second');
fs.mkdirSync(firstDest, { recursive: true });
fs.mkdirSync(secondDest, { recursive: true });
fs.writeFileSync(path.join(firstDest, 'gsd-alpha.md'), 'old alpha\n');
fs.writeFileSync(path.join(secondDest, 'gsd-beta.md'), 'old beta\n');
const stagedFirst = path.join(root, 'staged-first');
fs.mkdirSync(stagedFirst);
fs.writeFileSync(path.join(stagedFirst, 'alpha.md'), 'new alpha\n');
const oldSurface = surfaceBytesForTest({ baseProfile: 'full', disabledClusters: [], explicitAdds: [], explicitRemoves: [] });
fs.writeFileSync(path.join(root, '.gsd-surface.json'), oldSurface);
const layout = {
runtime: 'claude', configDir: root, scope: 'local', kinds: [
{ kind: 'commands', destSubpath: 'first', prefix: 'gsd-', stage: () => stagedFirst },
{ kind: 'commands', destSubpath: 'second', prefix: 'gsd-', stage: () => { throw new Error('later stage failed'); } },
],
};
assert.throws(() => applySurface(root, layout, new Map([['alpha', []], ['beta', []]])), /later stage failed/);
assert.equal(fs.readFileSync(path.join(firstDest, 'gsd-alpha.md'), 'utf8'), 'old alpha\n');
assert.equal(fs.readFileSync(path.join(secondDest, 'gsd-beta.md'), 'utf8'), 'old beta\n');
assert.equal(fs.readFileSync(path.join(root, '.gsd-surface.json'), 'utf8'), oldSurface);
});
test('#4132: a hash-mismatched installed corpus leaves exact all-old state and artifacts', (t) => {
const root = createTempDir('gsd-surface-corrupt-corpus-');
t.after(() => cleanup(root));
const installedCommands = path.join(root, 'gsd-core', 'commands', 'gsd');
const installedAgents = path.join(root, 'gsd-core', 'agents');
fs.mkdirSync(installedCommands, { recursive: true });
fs.mkdirSync(installedAgents, { recursive: true });
fs.writeFileSync(path.join(installedCommands, 'help.md'), '# corrupted command\n');
fs.writeFileSync(path.join(installedAgents, 'gsd-planner.md'), '# corrupted agent\n');
fs.writeFileSync(path.join(root, 'gsd-file-manifest.json'), JSON.stringify({ files: {
'gsd-core/commands/gsd/help.md': '0'.repeat(64),
'gsd-core/agents/gsd-planner.md': '0'.repeat(64),
} }));
const skillPath = path.join(root, 'skills', 'gsd-help', 'SKILL.md');
const agentPath = path.join(root, 'agents', 'gsd-planner.md');
fs.mkdirSync(path.dirname(skillPath), { recursive: true });
fs.mkdirSync(path.dirname(agentPath), { recursive: true });
fs.writeFileSync(skillPath, 'old skill\n');
fs.writeFileSync(agentPath, 'old agent\n');
const oldState = { baseProfile: 'full', disabledClusters: [], explicitAdds: [], explicitRemoves: [] };
const candidate = { ...oldState, explicitRemoves: ['help'] };
const oldSurface = surfaceBytesForTest(oldState);
fs.writeFileSync(path.join(root, '.gsd-surface.json'), oldSurface);
const layout = resolveRuntimeArtifactLayout('claude', root, 'global');
assert.throws(
() => applySurface(root, layout, realManifest(), CLUSTERS, undefined, { surfaceState: candidate }),
/install or upgrade gsd-core/,
);
assert.equal(fs.readFileSync(skillPath, 'utf8'), 'old skill\n');
assert.equal(fs.readFileSync(agentPath, 'utf8'), 'old agent\n');
assert.equal(fs.readFileSync(path.join(root, '.gsd-surface.json'), 'utf8'), oldSurface);
});
test('#4132: candidate surface state is published after materialization', (t) => {
const root = createTempDir('gsd-surface-state-last-');
t.after(() => cleanup(root));
const dest = path.join(root, 'commands');
const staged = path.join(root, 'staged');
fs.mkdirSync(dest, { recursive: true });
fs.mkdirSync(staged);
fs.writeFileSync(path.join(dest, 'gsd-alpha.md'), 'old alpha\n');
fs.writeFileSync(path.join(staged, 'alpha.md'), 'new alpha\n');
const oldState = { baseProfile: 'full', disabledClusters: [], explicitAdds: [], explicitRemoves: [] };
const newState = { ...oldState, disabledClusters: ['ui'] };
writeSurface(root, oldState);
const layout = {
runtime: 'claude', configDir: root, scope: 'local', kinds: [
{ kind: 'commands', destSubpath: 'commands', prefix: 'gsd-', stage: () => {
assert.deepEqual(readSurface(root), oldState, 'candidate must not be visible while staging');
return staged;
} },
],
};
applySurface(root, layout, new Map([['alpha', []]]), undefined, undefined, { surfaceState: newState });
assert.equal(fs.readFileSync(path.join(dest, 'gsd-alpha.md'), 'utf8'), 'new alpha\n');
assert.deepEqual(readSurface(root), newState);
});
test('#4132: reset removes surface state after materialization', (t) => {
const root = createTempDir('gsd-surface-reset-state-last-');
t.after(() => cleanup(root));
const dest = path.join(root, 'commands');
const staged = path.join(root, 'staged');
fs.mkdirSync(dest, { recursive: true });
fs.mkdirSync(staged);
fs.writeFileSync(path.join(staged, 'alpha.md'), 'reset alpha\n');
writeActiveProfile(root, 'full');
writeSurface(root, { baseProfile: 'core', disabledClusters: ['ui'], explicitAdds: [], explicitRemoves: [] });
const layout = {
runtime: 'claude', configDir: root, scope: 'local', kinds: [
{ kind: 'commands', destSubpath: 'commands', prefix: 'gsd-', stage: () => staged },
],
};
applySurface(root, layout, new Map([['alpha', []]]), undefined, undefined, { surfaceState: null });
assert.equal(fs.existsSync(path.join(root, '.gsd-surface.json')), false);
assert.equal(fs.readFileSync(path.join(dest, 'gsd-alpha.md'), 'utf8'), 'reset alpha\n');
});
});
// ─── resolveSurface ──────────────────────────────────────────────────────────
@@ -1201,6 +1376,7 @@ describe('skills-kind destination parity: installer vs surface-apply (#2911)', (
for (const runtime of RUNTIME_IDS) {
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), `gsd-2911-parity-${runtime}-${scope}-`));
t.after(() => cleanup(configDir));
writePackageSourceMarkerFixture(configDir);
withFakeHome(fakeHome, () => {
let layout;
@@ -1295,6 +1471,7 @@ describe('codex skills-kind destination: home override (#2911)', () => {
t.after(() => cleanup(fakeHome));
const codexHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2911-codex-home-dir-'));
t.after(() => cleanup(codexHome));
writePackageSourceMarkerFixture(codexHome);
withFakeHome(fakeHome, () => {
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
@@ -1387,6 +1564,7 @@ describe('installOpencodeFamilySkills destination parity (#2911 sibling coverage
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), `gsd-2911-ocfs-${runtime}-`));
const fakeHomeOverride = fs.mkdtempSync(path.join(os.tmpdir(), `gsd-2911-ocfs-home-${runtime}-`));
t.after(() => { cleanup(configDir); cleanup(fakeHomeOverride); });
writePackageSourceMarkerFixture(configDir);
// #3712 — this row drives a skills-kind `home` override on purpose, which is
// exactly what the test-home guard exists to police, so it has to declare the
// sandbox rather than rely on the destination happening to sit outside the
@@ -1458,15 +1636,12 @@ describe('installOpencodeFamilySkills destination parity (#2911 sibling coverage
// flat layout is preserved (not re-nested) rather than a nested one.
describe('issue-69: applySurface preserves nested skill layout (no re-flatten)', () => {
test('cline global full: applySurface keeps 6 router dirs and nested gsd-ns-manage/skills/help/SKILL.md', (t) => {
process.env.GSD_TEST_MODE = '1';
const { installRuntimeArtifacts } = require('../gsd-core/bin/lib/install-engine.cjs');
const dir = tmpDir('gsd-69-surface-');
t.after(() => { try { cleanup(dir); } catch { /* best-effort */ } });
const installed = runMinimalInstall({ runtime: 'cline', scope: 'global' });
const dir = installed.configDir;
t.after(() => { try { cleanup(installed.root); } catch { /* best-effort */ } });
// Step 1: full install
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
const resolved = resolveProfile({ modes: ['full'], manifest });
installRuntimeArtifacts('cline', dir, 'global', resolved);
const skillsDir = path.join(dir, 'skills');
@@ -1510,14 +1685,11 @@ describe('issue-69: applySurface preserves nested skill layout (no re-flatten)',
// #924 companion: Claude must use FLAT layout and applySurface must NOT re-nest it.
test('claude global full: install produces flat layout and applySurface preserves it (#924)', (t) => {
process.env.GSD_TEST_MODE = '1';
const { installRuntimeArtifacts } = require('../gsd-core/bin/lib/install-engine.cjs');
const dir = tmpDir('gsd-924-69-');
t.after(() => { try { cleanup(dir); } catch { /* best-effort */ } });
const installed = runMinimalInstall({ runtime: 'claude', scope: 'global' });
const dir = installed.configDir;
t.after(() => { try { cleanup(installed.root); } catch { /* best-effort */ } });
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
const resolved = resolveProfile({ modes: ['full'], manifest });
installRuntimeArtifacts('claude', dir, 'global', resolved);
const skillsDir = path.join(dir, 'skills');

View File

@@ -21,12 +21,14 @@ const { test, describe, beforeEach, afterEach, mock } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const crypto = require('node:crypto');
const { resolveRuntimeArtifactLayout, findInstallSourceRoot } = require('../gsd-core/bin/lib/runtime-artifact-layout.cjs');
const capabilityRegistry = require('../gsd-core/bin/lib/capability-registry.cjs');
const installProfiles = require('../gsd-core/bin/lib/install-profiles.cjs');
const { withInstallFs } = require('../gsd-core/bin/lib/install-fs-adapter.cjs');
const { install } = require('../bin/install.js');
const { createTempDir, cleanup, scrubConfigLocationEnv } = require('./helpers.cjs');
const { createTempDir, cleanup, scrubConfigLocationEnv, writePackageSourceMarkerFixture } = require('./helpers.cjs');
const REPO_ROOT = path.join(__dirname, '..');
@@ -533,11 +535,16 @@ const CORE_SKILLS = new Set(['help', 'phase', 'new-project']);
const CORE_AGENTS = new Set(['gsd-planner']);
const PROFILE_CORE = { skills: CORE_SKILLS, agents: CORE_AGENTS };
const PROFILE_FULL = { skills: '*', agents: new Set() };
const FAKE_STAGE_DIR = '/tmp/fake-config-dir-stage';
function createStageConfigDir(t) {
const configDir = writePackageSourceMarkerFixture(createTempDir('gsd-layout-stage-'));
t.after(() => cleanup(configDir));
return configDir;
}
describe('stage — agents kind (claude local)', () => {
test('stage returns a valid directory for the agents kind', () => {
const layout = resolveRuntimeArtifactLayout('claude', FAKE_STAGE_DIR, 'local');
test('stage returns a valid directory for the agents kind', (t) => {
const layout = resolveRuntimeArtifactLayout('claude', createStageConfigDir(t), 'local');
const agentsKind = layout.kinds.find(k => k.kind === 'agents');
assert.ok(agentsKind, 'should have an agents kind');
@@ -548,8 +555,40 @@ describe('stage — agents kind (claude local)', () => {
});
describe('stage — skills kind (claude global)', () => {
test('stage returns a directory containing gsd-<stem>/SKILL.md entries', () => {
const layout = resolveRuntimeArtifactLayout('claude', FAKE_STAGE_DIR, 'global');
test('#4132: an independent source checkout can replace an invalid installed corpus', (t) => {
const configDir = createTempDir('gsd-4132-installer-source-');
t.after(() => cleanup(configDir));
const installedCommands = path.join(configDir, 'gsd-core', 'commands', 'gsd');
const installedAgents = path.join(configDir, 'gsd-core', 'agents');
fs.mkdirSync(installedCommands, { recursive: true });
fs.mkdirSync(installedAgents, { recursive: true });
fs.writeFileSync(path.join(installedCommands, 'help.md'), 'OLD INSTALLED COMMAND\n');
fs.writeFileSync(path.join(installedAgents, 'gsd-planner.md'), 'OLD INSTALLED AGENT\n');
fs.writeFileSync(path.join(configDir, '.gsd-source'), path.join(REPO_ROOT, 'commands', 'gsd') + '\n');
const layout = resolveRuntimeArtifactLayout('claude', configDir, 'global');
const skillsKind = layout.kinds.find(k => k.kind === 'skills');
const agentsKind = layout.kinds.find(k => k.kind === 'agents');
assert.ok(skillsKind);
assert.ok(agentsKind);
const stagedSkills = skillsKind.stage(PROFILE_CORE);
const stagedAgents = agentsKind.stage(PROFILE_CORE);
t.after(() => cleanup(stagedSkills));
t.after(() => cleanup(stagedAgents));
assert.match(
fs.readFileSync(path.join(stagedSkills, 'gsd-help', 'SKILL.md'), 'utf8'),
/Show available GSD commands and usage guide/,
);
assert.doesNotMatch(
fs.readFileSync(path.join(stagedAgents, 'gsd-planner.md'), 'utf8'),
/OLD INSTALLED AGENT/,
);
});
test('stage returns a directory containing gsd-<stem>/SKILL.md entries', (t) => {
const layout = resolveRuntimeArtifactLayout('claude', createStageConfigDir(t), 'global');
const skillsKind = layout.kinds.find(k => k.kind === 'skills');
assert.ok(skillsKind, 'should have a skills kind');
@@ -564,8 +603,8 @@ describe('stage — skills kind (claude global)', () => {
assert.ok(entries.length >= 1, 'at least one skill dir should be staged');
});
test('stage with skills="*" produces flat layout for claude (#924: reverted from nested)', () => {
const layout = resolveRuntimeArtifactLayout('claude', FAKE_STAGE_DIR, 'global');
test('stage with skills="*" produces flat layout for claude (#924: reverted from nested)', (t) => {
const layout = resolveRuntimeArtifactLayout('claude', createStageConfigDir(t), 'global');
const skillsKind = layout.kinds.find(k => k.kind === 'skills');
assert.ok(skillsKind, 'should have a skills kind');
@@ -592,8 +631,8 @@ describe('stage — skills kind (claude global)', () => {
});
describe('stage — skills kind (kimi global)', () => {
test('stage returns Kimi SKILL.md dirs and agent YAML/prompt artifacts', () => {
const layout = resolveRuntimeArtifactLayout('kimi', FAKE_STAGE_DIR, 'global');
test('stage returns Kimi SKILL.md dirs and agent YAML/prompt artifacts', (t) => {
const layout = resolveRuntimeArtifactLayout('kimi', createStageConfigDir(t), 'global');
const skillsKind = layout.kinds.find(k => k.kind === 'skills');
assert.ok(skillsKind, 'should have a skills kind');
@@ -635,8 +674,8 @@ describe('stage — skills kind (kimi global)', () => {
assert.doesNotMatch(executorYaml, /mcp__/);
});
test('tracks Kimi agent staging dir before writing artifacts', () => {
const layout = resolveRuntimeArtifactLayout('kimi', FAKE_STAGE_DIR, 'global');
test('tracks Kimi agent staging dir before writing artifacts', (t) => {
const layout = resolveRuntimeArtifactLayout('kimi', createStageConfigDir(t), 'global');
const agentsKind = layout.kinds.find(k => k.kind === 'kimi-agents');
assert.ok(agentsKind, 'should have a kimi-agents kind');
@@ -673,8 +712,8 @@ describe('stage — skills kind (kimi global)', () => {
});
describe('stage — opencode commands kind', () => {
test('opencode stage returns directory with .md files for selected skills', () => {
const layout = resolveRuntimeArtifactLayout('opencode', FAKE_STAGE_DIR);
test('opencode stage returns directory with .md files for selected skills', (t) => {
const layout = resolveRuntimeArtifactLayout('opencode', createStageConfigDir(t));
const commandsKind = layout.kinds.find(k => k.kind === 'commands');
assert.ok(commandsKind, 'should have a commands kind');
@@ -690,8 +729,8 @@ describe('stage — opencode commands kind', () => {
describe('stage — opencode/kilo skills kind (#784)', () => {
for (const runtime of ['opencode', 'kilo']) {
test(`${runtime} skills stage writes gsd-<stem>/SKILL.md with name + description`, () => {
const layout = resolveRuntimeArtifactLayout(runtime, FAKE_STAGE_DIR);
test(`${runtime} skills stage writes gsd-<stem>/SKILL.md with name + description`, (t) => {
const layout = resolveRuntimeArtifactLayout(runtime, createStageConfigDir(t));
const skillsKind = layout.kinds.find(k => k.kind === 'skills');
assert.ok(skillsKind, 'should have a skills kind');
@@ -717,7 +756,7 @@ describe('stage — opencode/kilo skills kind (#784)', () => {
describe('stage — cursor retired commands kind (#2644)', () => {
test('cursor layout exposes no commands staging surface', () => {
const layout = resolveRuntimeArtifactLayout('cursor', FAKE_STAGE_DIR);
const layout = resolveRuntimeArtifactLayout('cursor', FAKE_DIR);
const commandsKind = layout.kinds.find(k => k.kind === 'commands');
assert.equal(commandsKind, undefined);
});
@@ -921,9 +960,10 @@ describe('#1477 .gsd-source marker provisioning', () => {
});
// ── Failure 1 end-to-end: resolution succeeds FROM the deployed tree ─────────
// The deployed module's __dirname is <claudeDir>/gsd-core/bin/lib, which has no
// commands/gsd ancestor (global skills layout). Only the marker rescues it.
test('deployed global layout resolves the source root via the marker', () => {
// Fixed installs own a raw corpus below the deployed gsd-core tree. The
// marker remains compatible, but offline resolution no longer depends on
// the executing package path surviving.
test('deployed global layout resolves its installation-owned source corpus', () => {
const claudeDir = path.join(tmpRoot, '.claude');
fs.mkdirSync(claudeDir, { recursive: true });
runInstall(true /* isGlobal */, 'claude');
@@ -939,21 +979,93 @@ describe('#1477 .gsd-source marker provisioning', () => {
delete require.cache[deployedLayoutPath];
const deployed = require(deployedLayoutPath);
// Negative proof that the bug condition exists: WITHOUT consulting the marker
// (no configDir argument), walk-up from the deployed tree has nothing to find.
assert.throws(
() => deployed.findInstallSourceRoot(),
/could not locate commands\/gsd/,
'deployed walk-up must fail without the marker — this is the regression condition',
);
const installedCorpus = path.join(claudeDir, 'gsd-core', 'commands', 'gsd');
assert.ok(fs.existsSync(installedCorpus), 'fixed install must own commands/gsd below gsd-core');
assert.equal(fs.realpathSync(deployed.findInstallSourceRoot()), fs.realpathSync(installedCorpus));
// With the marker (configDir provided), list/status resolution succeeds.
let resolved;
assert.doesNotThrow(() => {
resolved = deployed.findInstallSourceRoot(claudeDir);
}, 'findInstallSourceRoot must resolve via the .gsd-source marker');
assert.equal(path.basename(resolved), 'gsd');
assert.ok(fs.existsSync(resolved));
assert.equal(fs.realpathSync(resolved), fs.realpathSync(installedCorpus));
});
test('#4132: deployed findInstallSourceRoot rejects an installed commands corpus whose hash changed', () => {
const claudeDir = path.join(tmpRoot, '.claude');
fs.mkdirSync(claudeDir, { recursive: true });
runInstall(true /* isGlobal */, 'claude');
const manifest = JSON.parse(fs.readFileSync(path.join(claudeDir, 'gsd-file-manifest.json'), 'utf8'));
const commandKey = Object.keys(manifest.files).find((key) => key.startsWith('gsd-core/commands/gsd/'));
assert.ok(commandKey, 'precondition: install manifest must own at least one command corpus file');
const commandPath = path.join(claudeDir, ...commandKey.split('/'));
fs.appendFileSync(commandPath, '\n# corrupted after install\n');
const deployedLayoutPath = path.join(claudeDir, 'gsd-core', 'bin', 'lib', 'runtime-artifact-layout.cjs');
delete require.cache[deployedLayoutPath];
const deployed = require(deployedLayoutPath);
assert.throws(
() => deployed.findInstallSourceRoot(claudeDir),
/install or upgrade gsd-core/,
);
});
test('#4132: deployed finder rejects an installed corpus reached through an ancestor symlink', (t) => {
const claudeDir = path.join(tmpRoot, '.claude');
fs.mkdirSync(claudeDir, { recursive: true });
runInstall(true /* isGlobal */, 'claude');
const commandsParent = path.join(claudeDir, 'gsd-core', 'commands');
const outsideParent = path.join(tmpRoot, 'outside-commands');
fs.renameSync(commandsParent, outsideParent);
try {
fs.symlinkSync(outsideParent, commandsParent, process.platform === 'win32' ? 'junction' : 'dir');
} catch (error) {
fs.renameSync(outsideParent, commandsParent);
if (['EPERM', 'EACCES', 'ENOSYS'].includes(error.code)) {
t.skip(`directory symlink creation unavailable: ${error.code || error.message}`);
return;
}
throw error;
}
const deployedLayoutPath = path.join(claudeDir, 'gsd-core', 'bin', 'lib', 'runtime-artifact-layout.cjs');
delete require.cache[deployedLayoutPath];
const deployed = require(deployedLayoutPath);
const priorOptIn = process.env.GSD_ALLOW_SYMLINKED_DEST;
process.env.GSD_ALLOW_SYMLINKED_DEST = '1';
t.after(() => {
if (priorOptIn === undefined) delete process.env.GSD_ALLOW_SYMLINKED_DEST;
else process.env.GSD_ALLOW_SYMLINKED_DEST = priorOptIn;
});
assert.throws(
() => deployed.findInstallSourceRoot(claudeDir),
/install or upgrade gsd-core/,
);
});
test('#4132: deployed agents layout rejects an installed agents corpus whose hash changed', () => {
const claudeDir = path.join(tmpRoot, '.claude');
fs.mkdirSync(claudeDir, { recursive: true });
runInstall(true /* isGlobal */, 'claude');
const manifest = JSON.parse(fs.readFileSync(path.join(claudeDir, 'gsd-file-manifest.json'), 'utf8'));
const agentKey = Object.keys(manifest.files).find((key) => key.startsWith('gsd-core/agents/'));
assert.ok(agentKey, 'precondition: install manifest must own at least one agent corpus file');
fs.appendFileSync(path.join(claudeDir, ...agentKey.split('/')), '\n# corrupted after install\n');
const deployedLayoutPath = path.join(claudeDir, 'gsd-core', 'bin', 'lib', 'runtime-artifact-layout.cjs');
delete require.cache[deployedLayoutPath];
const deployed = require(deployedLayoutPath);
assert.throws(
() => deployed.resolveRuntimeArtifactLayout('claude', claudeDir, 'global')
.kinds.find((kind) => kind.kind === 'agents').stage(PROFILE_CORE),
/install or upgrade gsd-core/,
);
});
// ── Adversarial marker-reader cases (no full install needed) ─────────────────
@@ -965,6 +1077,7 @@ describe('#1477 .gsd-source marker provisioning', () => {
test('marker pointing at a valid commands/gsd takes precedence over walk-up', () => {
const fakeSrc = path.join(cfgDir, 'pkg', 'commands', 'gsd');
fs.mkdirSync(fakeSrc, { recursive: true });
fs.writeFileSync(path.join(fakeSrc, 'gsd-test.md'), '# marker source\n');
fs.writeFileSync(path.join(cfgDir, '.gsd-source'), fakeSrc + '\n', 'utf8');
const resolved = findInstallSourceRoot(cfgDir);
@@ -981,11 +1094,335 @@ describe('#1477 .gsd-source marker provisioning', () => {
assert.equal(path.resolve(resolved), path.resolve(REPO_ROOT, 'commands', 'gsd'));
});
test('#4132: a missing installed leaf still rejects a marker containing it through a symlinked ancestor', (t) => {
const outsideCore = path.join(cfgDir, 'outside-core');
const markerCommands = path.join(outsideCore, 'commands');
const markerAgents = path.join(cfgDir, 'agents');
fs.mkdirSync(markerCommands, { recursive: true });
fs.mkdirSync(markerAgents, { recursive: true });
fs.writeFileSync(path.join(markerCommands, 'help.md'), '# marker command\n');
fs.writeFileSync(path.join(markerAgents, 'gsd-planner.md'), '# marker agent\n');
try {
fs.symlinkSync(
outsideCore,
path.join(cfgDir, 'gsd-core'),
process.platform === 'win32' ? 'junction' : 'dir',
);
} catch (error) {
t.skip(`directory symlink creation unavailable: ${error.code || error.message}`);
return;
}
fs.writeFileSync(path.join(cfgDir, '.gsd-source'), markerCommands + '\n');
assert.throws(
() => resolveRuntimeArtifactLayout('claude', cfgDir, 'global')
.kinds.find((kind) => kind.kind === 'skills').stage(PROFILE_CORE),
/install or upgrade gsd-core/,
);
});
test('empty / whitespace-only marker is ignored', () => {
fs.writeFileSync(path.join(cfgDir, '.gsd-source'), ' \n', 'utf8');
const resolved = findInstallSourceRoot(cfgDir);
assert.equal(path.resolve(resolved), path.resolve(REPO_ROOT, 'commands', 'gsd'));
});
test('one complete installed provider wins over a different complete marker provider', (t) => {
const installedCommands = path.join(cfgDir, 'gsd-core', 'commands', 'gsd');
const installedAgents = path.join(cfgDir, 'gsd-core', 'agents');
fs.mkdirSync(installedCommands, { recursive: true });
fs.mkdirSync(installedAgents, { recursive: true });
const installedCommandPath = path.join(installedCommands, 'help.md');
const installedAgentPath = path.join(installedAgents, 'gsd-planner.md');
fs.writeFileSync(installedCommandPath, '# installed command\n');
fs.writeFileSync(installedAgentPath, '# installed agent\n');
fs.writeFileSync(path.join(cfgDir, 'gsd-file-manifest.json'), JSON.stringify({ files: {
'gsd-core/commands/gsd/help.md': crypto.createHash('sha256').update(fs.readFileSync(installedCommandPath)).digest('hex'),
'gsd-core/agents/gsd-planner.md': crypto.createHash('sha256').update(fs.readFileSync(installedAgentPath)).digest('hex'),
} }));
const markerCommands = path.join(cfgDir, 'legacy-package', 'commands', 'gsd');
const markerAgents = path.join(cfgDir, 'legacy-package', 'agents');
fs.mkdirSync(markerCommands, { recursive: true });
fs.mkdirSync(markerAgents, { recursive: true });
fs.writeFileSync(path.join(markerCommands, 'help.md'), '# marker command\n');
fs.writeFileSync(path.join(markerAgents, 'gsd-planner.md'), '# marker agent\n');
fs.writeFileSync(path.join(cfgDir, '.gsd-source'), markerCommands + '\n');
const layout = resolveRuntimeArtifactLayout('claude', cfgDir, 'global');
const stagedSkills = layout.kinds.find((kind) => kind.kind === 'skills').stage(PROFILE_CORE);
const stagedAgents = layout.kinds.find((kind) => kind.kind === 'agents').stage(PROFILE_CORE);
t.after(() => cleanup(stagedSkills));
t.after(() => cleanup(stagedAgents));
assert.match(fs.readFileSync(path.join(stagedSkills, 'gsd-help', 'SKILL.md'), 'utf8'), /installed command/);
assert.match(fs.readFileSync(path.join(stagedAgents, 'gsd-planner.md'), 'utf8'), /installed agent/);
});
test('a partial installed corpus is not mixed with a complete marker provider', (t) => {
const installedCommands = path.join(cfgDir, 'gsd-core', 'commands', 'gsd');
fs.mkdirSync(installedCommands, { recursive: true });
fs.writeFileSync(path.join(installedCommands, 'help.md'), '# installed command\n');
const markerCommands = path.join(cfgDir, 'legacy-package', 'commands', 'gsd');
const markerAgents = path.join(cfgDir, 'legacy-package', 'agents');
fs.mkdirSync(markerCommands, { recursive: true });
fs.mkdirSync(markerAgents, { recursive: true });
fs.writeFileSync(path.join(markerCommands, 'help.md'), '# marker command\n');
fs.writeFileSync(path.join(markerAgents, 'gsd-planner.md'), '# marker agent\n');
fs.writeFileSync(path.join(cfgDir, '.gsd-source'), markerCommands + '\n');
const layout = resolveRuntimeArtifactLayout('claude', cfgDir, 'global');
const stagedSkills = layout.kinds.find((kind) => kind.kind === 'skills').stage(PROFILE_CORE);
const stagedAgents = layout.kinds.find((kind) => kind.kind === 'agents').stage(PROFILE_CORE);
t.after(() => cleanup(stagedSkills));
t.after(() => cleanup(stagedAgents));
assert.match(fs.readFileSync(path.join(stagedSkills, 'gsd-help', 'SKILL.md'), 'utf8'), /marker command/);
assert.match(fs.readFileSync(path.join(stagedAgents, 'gsd-planner.md'), 'utf8'), /marker agent/);
});
test('partial providers are not mixed when package fallback is disabled', () => {
const installedCommands = path.join(cfgDir, 'gsd-core', 'commands', 'gsd');
fs.mkdirSync(installedCommands, { recursive: true });
fs.writeFileSync(path.join(installedCommands, 'gsd-test.md'), '# installed command\n');
const markerCommands = path.join(cfgDir, 'legacy-package', 'commands', 'gsd');
fs.mkdirSync(markerCommands, { recursive: true });
fs.writeFileSync(path.join(markerCommands, 'gsd-test.md'), '# marker command\n');
fs.writeFileSync(path.join(cfgDir, '.gsd-source'), markerCommands + '\n');
const layout = resolveRuntimeArtifactLayout('claude', cfgDir, 'global');
assert.throws(() => layout.kinds.find((kind) => kind.kind === 'skills').stage(PROFILE_CORE), /install or upgrade gsd-core/);
});
test('ordinary Runtime Surface staging never falls back to the source checkout package', () => {
const layout = resolveRuntimeArtifactLayout('claude', cfgDir, 'global');
const skills = layout.kinds.find((kind) => kind.kind === 'skills');
assert.ok(skills);
assert.throws(
() => skills.stage(PROFILE_CORE),
/install or upgrade gsd-core/,
);
});
test('#4132: caller-supplied stage context cannot select installer source authority', () => {
const layout = resolveRuntimeArtifactLayout('claude', cfgDir, 'global');
const skills = layout.kinds.find((kind) => kind.kind === 'skills');
assert.throws(
() => skills.stage(PROFILE_CORE, {
[Symbol.for('@open-gsd/runtime-artifact-installer-source-authority')]: true,
}),
/install or upgrade gsd-core/,
);
assert.throws(
() => withInstallFs(undefined, () => skills.stage(PROFILE_CORE)),
/install or upgrade gsd-core/,
);
});
test('an installed corpus without a manifest is not a working fallback', () => {
const installedCommands = path.join(cfgDir, 'gsd-core', 'commands', 'gsd');
const installedAgents = path.join(cfgDir, 'gsd-core', 'agents');
fs.mkdirSync(installedCommands, { recursive: true });
fs.mkdirSync(installedAgents, { recursive: true });
fs.writeFileSync(path.join(installedCommands, 'help.md'), '# unverified command\n');
fs.writeFileSync(path.join(installedAgents, 'gsd-planner.md'), '# unverified agent\n');
const layout = resolveRuntimeArtifactLayout('claude', cfgDir, 'global');
assert.throws(
() => layout.kinds.find((kind) => kind.kind === 'skills').stage(PROFILE_CORE),
/install or upgrade gsd-core/,
);
});
test('an installed corpus whose bytes do not match its manifest is not a working fallback', () => {
const installedCommands = path.join(cfgDir, 'gsd-core', 'commands', 'gsd');
const installedAgents = path.join(cfgDir, 'gsd-core', 'agents');
fs.mkdirSync(installedCommands, { recursive: true });
fs.mkdirSync(installedAgents, { recursive: true });
fs.writeFileSync(path.join(installedCommands, 'help.md'), '# corrupted command\n');
fs.writeFileSync(path.join(installedAgents, 'gsd-planner.md'), '# corrupted agent\n');
fs.writeFileSync(path.join(cfgDir, 'gsd-file-manifest.json'), JSON.stringify({ files: {
'gsd-core/commands/gsd/help.md': '0'.repeat(64),
'gsd-core/agents/gsd-planner.md': '0'.repeat(64),
} }));
const layout = resolveRuntimeArtifactLayout('claude', cfgDir, 'global');
assert.throws(
() => layout.kinds.find((kind) => kind.kind === 'skills').stage(PROFILE_CORE),
/install or upgrade gsd-core/,
);
});
test('a hash-mismatched installed corpus falls back to an independent complete marker provider', (t) => {
const installedCommands = path.join(cfgDir, 'gsd-core', 'commands', 'gsd');
const installedAgents = path.join(cfgDir, 'gsd-core', 'agents');
fs.mkdirSync(installedCommands, { recursive: true });
fs.mkdirSync(installedAgents, { recursive: true });
fs.writeFileSync(path.join(installedCommands, 'help.md'), '# corrupted command\n');
fs.writeFileSync(path.join(installedAgents, 'gsd-planner.md'), '# corrupted agent\n');
fs.writeFileSync(path.join(cfgDir, 'gsd-file-manifest.json'), JSON.stringify({ files: {
'gsd-core/commands/gsd/help.md': '0'.repeat(64),
'gsd-core/agents/gsd-planner.md': '0'.repeat(64),
} }));
const markerCommands = path.join(cfgDir, 'legacy-package', 'commands', 'gsd');
const markerAgents = path.join(cfgDir, 'legacy-package', 'agents');
fs.mkdirSync(markerCommands, { recursive: true });
fs.mkdirSync(markerAgents, { recursive: true });
fs.writeFileSync(path.join(markerCommands, 'help.md'), '# marker command\n');
fs.writeFileSync(path.join(markerAgents, 'gsd-planner.md'), '# marker agent\n');
fs.writeFileSync(path.join(cfgDir, '.gsd-source'), markerCommands + '\n');
const layout = resolveRuntimeArtifactLayout('claude', cfgDir, 'global');
const stagedSkills = layout.kinds.find((kind) => kind.kind === 'skills').stage(PROFILE_CORE);
const stagedAgents = layout.kinds.find((kind) => kind.kind === 'agents').stage(PROFILE_CORE);
t.after(() => cleanup(stagedSkills));
t.after(() => cleanup(stagedAgents));
assert.match(fs.readFileSync(path.join(stagedSkills, 'gsd-help', 'SKILL.md'), 'utf8'), /marker command/);
assert.match(fs.readFileSync(path.join(stagedAgents, 'gsd-planner.md'), 'utf8'), /marker agent/);
});
test('#4132: a marker aliasing any rejected installed root is not an independent provider', (t) => {
const installedCommandsParent = path.join(cfgDir, 'gsd-core', 'commands');
const installedCommands = path.join(installedCommandsParent, 'gsd');
const installedAgents = path.join(cfgDir, 'gsd-core', 'agents');
fs.mkdirSync(installedCommands, { recursive: true });
fs.mkdirSync(installedAgents, { recursive: true });
fs.writeFileSync(path.join(installedCommands, 'help.md'), '# corrupted installed command\n');
const installedAgentPath = path.join(installedAgents, 'gsd-planner.md');
fs.writeFileSync(installedAgentPath, '# installed agent\n');
fs.writeFileSync(path.join(cfgDir, 'gsd-file-manifest.json'), JSON.stringify({ files: {
'gsd-core/commands/gsd/help.md': '0'.repeat(64),
'gsd-core/agents/gsd-planner.md': crypto.createHash('sha256').update(fs.readFileSync(installedAgentPath)).digest('hex'),
} }));
const markerRoot = path.join(cfgDir, 'hybrid-marker');
fs.mkdirSync(markerRoot, { recursive: true });
try {
fs.symlinkSync(
installedCommandsParent,
path.join(markerRoot, 'commands'),
process.platform === 'win32' ? 'junction' : 'dir',
);
} catch (error) {
t.skip(`directory symlink creation unavailable: ${error.code || error.message}`);
return;
}
const markerCommands = path.join(markerRoot, 'commands', 'gsd');
const markerAgents = path.join(markerRoot, 'agents');
fs.mkdirSync(markerAgents, { recursive: true });
fs.writeFileSync(path.join(markerAgents, 'gsd-planner.md'), '# independent marker agent\n');
fs.writeFileSync(path.join(cfgDir, '.gsd-source'), markerCommands + '\n');
assert.equal(fs.realpathSync(markerCommands), fs.realpathSync(installedCommands));
assert.notEqual(fs.realpathSync(markerAgents), fs.realpathSync(installedAgents));
const layout = resolveRuntimeArtifactLayout('claude', cfgDir, 'global');
const skills = layout.kinds.find((kind) => kind.kind === 'skills');
let stagedSkills;
t.after(() => { if (stagedSkills) cleanup(stagedSkills); });
assert.throws(
() => { stagedSkills = skills.stage(PROFILE_CORE); },
/install or upgrade gsd-core/,
);
});
test('#4132: a marker containing a rejected installed root is not an independent provider', () => {
const installedCommandsParent = path.join(cfgDir, 'gsd-core', 'commands');
const installedCommands = path.join(installedCommandsParent, 'gsd');
const installedAgents = path.join(cfgDir, 'gsd-core', 'agents');
const markerAgents = path.join(cfgDir, 'agents');
fs.mkdirSync(installedCommands, { recursive: true });
fs.mkdirSync(installedAgents, { recursive: true });
fs.mkdirSync(markerAgents, { recursive: true });
fs.writeFileSync(path.join(installedCommands, 'help.md'), '# corrupted installed command\n');
const installedAgentPath = path.join(installedAgents, 'gsd-planner.md');
fs.writeFileSync(installedAgentPath, '# installed agent\n');
fs.writeFileSync(path.join(installedCommandsParent, 'rogue.md'), '<instructions>ROGUE</instructions>\n');
fs.writeFileSync(path.join(markerAgents, 'gsd-planner.md'), '# marker agent\n');
fs.writeFileSync(path.join(cfgDir, 'gsd-file-manifest.json'), JSON.stringify({ files: {
'gsd-core/commands/gsd/help.md': '0'.repeat(64),
'gsd-core/agents/gsd-planner.md': crypto.createHash('sha256').update(fs.readFileSync(installedAgentPath)).digest('hex'),
} }));
fs.writeFileSync(path.join(cfgDir, '.gsd-source'), installedCommandsParent + '\n');
const layout = resolveRuntimeArtifactLayout('claude', cfgDir, 'global');
assert.throws(
() => layout.kinds.find((kind) => kind.kind === 'skills').stage(PROFILE_CORE),
/install or upgrade gsd-core/,
);
});
test('#4132: an agents descendant of a rejected installed root rejects the whole provider', () => {
const installedCommands = path.join(cfgDir, 'gsd-core', 'commands', 'gsd');
const installedAgents = path.join(cfgDir, 'gsd-core', 'agents');
fs.mkdirSync(installedCommands, { recursive: true });
fs.mkdirSync(installedAgents, { recursive: true });
const installedCommandPath = path.join(installedCommands, 'help.md');
fs.writeFileSync(installedCommandPath, '# installed command\n');
fs.writeFileSync(path.join(installedAgents, 'gsd-planner.md'), '# corrupted installed agent\n');
fs.writeFileSync(path.join(cfgDir, 'gsd-file-manifest.json'), JSON.stringify({ files: {
'gsd-core/commands/gsd/help.md': '0'.repeat(64),
'gsd-core/agents/gsd-planner.md': '0'.repeat(64),
} }));
const nestedProviderRoot = path.join(installedAgents, 'nested');
const markerCommands = path.join(nestedProviderRoot, 'commands', 'gsd');
const markerAgents = path.join(nestedProviderRoot, 'agents');
fs.mkdirSync(markerCommands, { recursive: true });
fs.mkdirSync(markerAgents, { recursive: true });
fs.writeFileSync(path.join(markerCommands, 'help.md'), '# marker command\n');
fs.writeFileSync(path.join(markerAgents, 'gsd-planner.md'), '# marker agent\n');
fs.writeFileSync(path.join(cfgDir, '.gsd-source'), markerCommands + '\n');
assert.equal(
fs.realpathSync(findInstallSourceRoot(cfgDir)),
fs.realpathSync(markerCommands),
'commands-only resolution must ignore overlap in the non-required agents class',
);
const layout = resolveRuntimeArtifactLayout('claude', cfgDir, 'global');
assert.throws(
() => layout.kinds.find((kind) => kind.kind === 'skills').stage(PROFILE_CORE),
/install or upgrade gsd-core/,
);
});
test('manifest-expected missing corpus file fails closed without reusing its marker alias', () => {
const installedCommands = path.join(cfgDir, 'gsd-core', 'commands', 'gsd');
const installedAgents = path.join(cfgDir, 'gsd-core', 'agents');
fs.mkdirSync(installedCommands, { recursive: true });
fs.mkdirSync(installedAgents, { recursive: true });
fs.writeFileSync(path.join(installedCommands, 'gsd-test.md'), '# installed command\n');
fs.writeFileSync(path.join(installedAgents, 'gsd-test-agent.md'), '# installed agent\n');
fs.writeFileSync(path.join(cfgDir, 'gsd-file-manifest.json'), JSON.stringify({ files: {
'gsd-core/commands/gsd/gsd-test.md': '0'.repeat(64),
'gsd-core/agents/gsd-test-agent.md': '0'.repeat(64),
'gsd-core/agents/removed.md': '0'.repeat(64),
} }));
fs.writeFileSync(path.join(cfgDir, '.gsd-source'), installedCommands + '\n');
const layout = resolveRuntimeArtifactLayout('claude', cfgDir, 'global');
assert.throws(() => layout.kinds.find((kind) => kind.kind === 'skills').stage(PROFILE_CORE), /install or upgrade gsd-core/);
});
test('a symlink inside the installed corpus is not a confined source', (t) => {
const installedCommands = path.join(cfgDir, 'gsd-core', 'commands', 'gsd');
const installedAgents = path.join(cfgDir, 'gsd-core', 'agents');
fs.mkdirSync(installedCommands, { recursive: true });
fs.mkdirSync(installedAgents, { recursive: true });
fs.writeFileSync(path.join(installedCommands, 'gsd-test.md'), '# installed command\n');
const outside = path.join(cfgDir, 'outside.md');
fs.writeFileSync(outside, '# outside\n');
try {
fs.symlinkSync(outside, path.join(installedAgents, 'gsd-test-agent.md'));
} catch (error) {
t.skip(`symlink creation unavailable: ${error.code || error.message}`);
return;
}
const layout = resolveRuntimeArtifactLayout('claude', cfgDir, 'global');
assert.throws(() => layout.kinds.find((kind) => kind.kind === 'skills').stage(PROFILE_CORE), /install or upgrade gsd-core/);
assert.strictEqual(fs.readFileSync(outside, 'utf8'), '# outside\n');
});
});
});
@@ -1054,8 +1491,7 @@ describe('#2624 .gsd-source marker is rewritten before staging reads it', () =>
cleanup(tmpRoot);
});
// The current package's commands/gsd — what the marker MUST point at after install.
const CURRENT_SOURCE = path.join(REPO_ROOT, 'commands', 'gsd');
const installedSource = (claudeDir) => path.join(claudeDir, 'gsd-core', 'commands', 'gsd');
test('claude-global install overwrites a stale .gsd-source marker before staging reads it', (t) => {
const claudeDir = path.join(tmpRoot, '.claude');
@@ -1090,8 +1526,8 @@ describe('#2624 .gsd-source marker is rewritten before staging reads it', () =>
const markerPath = path.join(claudeDir, '.gsd-source');
assert.ok(fs.existsSync(markerPath), 'marker must exist after install');
const finalMarker = path.resolve(fs.readFileSync(markerPath, 'utf8').trim());
assert.equal(finalMarker, path.resolve(CURRENT_SOURCE),
`final marker must point at the current package source, not ${finalMarker}`);
assert.equal(finalMarker, path.resolve(installedSource(claudeDir)),
`final marker must point at the installed current-version corpus, not ${finalMarker}`);
// The decisive assertion: staging must NEVER have resolved the stale source. Before the
// fix, at least one staging resolution returned the stale path (read before rewrite).
@@ -1112,8 +1548,8 @@ describe('#2624 .gsd-source marker is rewritten before staging reads it', () =>
const resolved = fs.readFileSync(markerPath, 'utf8').trim();
assert.equal(
path.resolve(resolved),
path.resolve(CURRENT_SOURCE),
'fresh install marker must point at the current package source',
path.resolve(installedSource(claudeDir)),
'fresh install marker must point at the installed corpus',
);
});
@@ -1132,8 +1568,8 @@ describe('#2624 .gsd-source marker is rewritten before staging reads it', () =>
const resolved = fs.readFileSync(markerPath, 'utf8').trim();
assert.equal(
path.resolve(resolved),
path.resolve(CURRENT_SOURCE),
'ghost marker must be rewritten to the current package source',
path.resolve(installedSource(claudeDir)),
'ghost marker must be rewritten to the installed corpus',
);
});
});

View File

@@ -2508,6 +2508,7 @@ describe('#3706: the layout seam actually threads the variant', () => {
fs.mkdirSync(commandsDir, { recursive: true });
fs.mkdirSync(agentsDir, { recursive: true });
fs.writeFileSync(path.join(configDir, '.gsd-source'), commandsDir + '\n', 'utf8');
fs.writeFileSync(path.join(commandsDir, 'gsd-help.md'), '# complete marker provider\n', 'utf8');
fs.writeFileSync(path.join(agentsDir, 'gsd-executor.md'), agentContent, 'utf8');
return configDir;
}
@@ -2587,4 +2588,3 @@ describe('#3706: the layout seam actually threads the variant', () => {
}
});
});

View File

@@ -39,3 +39,18 @@ describe('#2116 regression: surface.md resolvable require + correct package', ()
);
});
});
describe('#4132 regression: surface.md uses the state-last public contract', () => {
// allow-test-rule: source-text-is-the-product (#4132)
const content = fs.readFileSync(SURFACE_MD, 'utf-8');
test('mutations pass an in-memory candidate without an eager state write', () => {
assert.match(content, /surfaceState/);
assert.match(content, /publishes the candidate state only after/);
assert.doesNotMatch(content, /writeSurface\(runtimeConfigDir, surfaceState\)/);
});
test('reset is represented as an absent-state candidate, not an eager delete', () => {
assert.match(content, /surfaceState: null/);
});
});

View File

@@ -27,7 +27,7 @@
"adr857-predicate-boundary.test.cjs": 113,
"affected-tests-lib.test.cjs": 240,
"agent-classification-parity.test.cjs": 49,
"agent-fragments-emission.install.test.cjs": 79456,
"agent-fragments-emission.install.test.cjs": 228000,
"agent-frontmatter.test.cjs": 1076,
"agent-install-check.test.cjs": 274,
"agent-install-validation.test.cjs": 12405,
@@ -282,7 +282,7 @@
"fixture-builder.test.cjs": 324,
"forensics.test.cjs": 164,
"format-github-release-notes.test.cjs": 69,
"fragment-single-edit-propagation.install.test.cjs": 229914,
"fragment-single-edit-propagation.install.test.cjs": 575000,
"frontmatter-cli.test.cjs": 25728,
"frontmatter.property.test.cjs": 2551,
"frontmatter.test.cjs": 10565,