diff --git a/.changeset/vivid-rams-rally.md b/.changeset/vivid-rams-rally.md new file mode 100644 index 000000000..515a581af --- /dev/null +++ b/.changeset/vivid-rams-rally.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 4188 +--- +**ZCode installs: command `` @-refs now resolve to `~/.zcode/gsd-core/` instead of the Claude copy** — the installer's runtime rewrite pass had no ZCode case, so every generated command loaded the Claude runtime's workflow copy and the ZCode-adapted core was never read. Re-running the installer repairs existing installs. (#4002) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index fc9afd5b1..9c204dedd 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -43,6 +43,7 @@ body: - Antigravity - Cursor - Windsurf + - ZCode (Z.ai) - Multiple (specify in description) validations: required: true diff --git a/package-lock.json b/package-lock.json index 0ba183a1c..8d34afbcb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2668,9 +2668,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -3595,9 +3595,9 @@ } }, "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -4526,12 +4526,13 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" @@ -4731,14 +4732,14 @@ } }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, diff --git a/src/runtime-artifact-conversion.cts b/src/runtime-artifact-conversion.cts index 7addddf27..5fe609e9e 100644 --- a/src/runtime-artifact-conversion.cts +++ b/src/runtime-artifact-conversion.cts @@ -3177,6 +3177,21 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a content = processAttribution(content, attribution); break; + case 'zcode': + // #4002: ZCode is a Claude-Code-shaped host (dot-home `.zcode`, `@~`-ref + // expansion, `~/.zcode/...` documented paths) whose commands install with + // `converter: null` — this pass is their only chance to receive + // runtime-correct paths. Same shape as `claude`, including the tilde + // restore: the tilde form is what ZCode expands and what its docs use. + // `${_GSD_RUNTIME_ROOT}/.claude/…` matches none of these regexes and + // survives as the project-local fallback, exactly as on every sibling. + content = content.replace(/~\/\.claude\//g, pathPrefix); + content = content.replace(/\$HOME\/\.claude\//g, pathPrefix); + content = content.replace(/\.\/\.claude\//g, `./${dirName}/`); + content = restoreClaudeGlobalAtRefTilde(content, pathPrefix); + content = processAttribution(content, attribution); + break; + default: // Unknown runtime — no rewrites (OpenCode/Kilo handled by their own install path). break; diff --git a/tests/declarative-reference-zcode.test.cjs b/tests/declarative-reference-zcode.test.cjs index ca4b64df3..f9b44182a 100644 --- a/tests/declarative-reference-zcode.test.cjs +++ b/tests/declarative-reference-zcode.test.cjs @@ -139,6 +139,69 @@ test('a partial/empty zcode descriptor degrades to the safe floor, not the decla assert.ok(result.warnings.length > 0); }); +test('#4002: zcode command bodies rewrite @-refs to the zcode runtime home', () => { + const { configDir, root } = runMinimalInstall({ runtime: 'zcode', scope: 'global' }); + try { + const commandsDir = path.join(configDir, 'commands'); + const files = fs.readdirSync(commandsDir).filter((f) => f.startsWith('gsd-') && f.endsWith('.md')); + assert.ok(files.length > 0, 'zcode install must emit command files'); + + // The reporter's evidence: 59 of 71 emitted command files still carried the + // Claude home literal, so every lazy load resolved inside ~/.claude/gsd-core + // and the ZCode-adapted copy was never read. None may remain. + const offenders = files.filter((f) => + fs.readFileSync(path.join(commandsDir, f), 'utf8').includes('~/.claude/gsd-core')); + assert.deepEqual(offenders, [], + `command files still carrying the Claude home literal: ${offenders.join(', ')}`); + + // The @-ref must land on the zcode home, in the tilde form ZCode documents + // (docs/reference/host-integration-capability-matrix.md: `~/.zcode/...`). + const planPhase = fs.readFileSync(path.join(commandsDir, 'gsd-plan-phase.md'), 'utf8'); + assert.match(planPhase, /@~\/\.zcode\/gsd-core\/workflows\/plan-phase\.md/, + 'execution_context must reference the zcode runtime home'); + assert.doesNotMatch(planPhase, /@~\/\.claude\//, 'no @-ref may stay on the Claude home'); + + // The same rewrite pass owns skill bodies — the emitted skills must not + // carry the Claude literal either. + const skillsDir = path.join(configDir, 'skills'); + if (fs.existsSync(skillsDir)) { + const stack = [skillsDir]; + while (stack.length) { + const cur = stack.pop(); + for (const ent of fs.readdirSync(cur, { withFileTypes: true })) { + const p = path.join(cur, ent.name); + if (ent.isDirectory()) stack.push(p); + else if (ent.name === 'SKILL.md') { + assert.ok(!fs.readFileSync(p, 'utf8').includes('~/.claude/gsd-core'), + `skill body still carrying the Claude home literal: ${path.relative(configDir, p)}`); + } + } + } + } + } finally { + cleanup(root); + } +}); + +test('#4002 negative space: the project-local shim fallback survives the zcode rewrite', () => { + const { configDir, root } = runMinimalInstall({ runtime: 'zcode', scope: 'global' }); + try { + // `${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/` is the legitimate + // project-local fallback (the reporter's OpenCode control keeps exactly + // this literal) — the rewrite must not touch it. Only 3 source commands + // carry the full shim chain; discuss-phase is one. + const discuss = fs.readFileSync(path.join(configDir, 'commands', 'gsd-discuss-phase.md'), 'utf8'); + assert.match(discuss, /\$\{_GSD_RUNTIME_ROOT\}\/\.claude\/gsd-core\/bin\//, + 'the project-local shim fallback literal must survive'); + // ... while the bare $HOME/.claude fallback slot in the same chain is + // rewritten to the runtime home, exactly as every sibling runtime does. + assert.doesNotMatch(discuss, /\$HOME\/\.claude\/gsd-core\/bin\//, + 'the $HOME fallback slot must resolve to the zcode home like every sibling runtime'); + } finally { + cleanup(root); + } +}); + // AC-style proof: the 2 still-`undocumented` dispatch sub-axes (nested/ // maxDepth) must degrade to the most-restrictive KNOWN value, never their // optimistic value. Unlike augment (3 undocumented sub-axes) or antigravity diff --git a/tests/helpers/emitted-provenance.cjs b/tests/helpers/emitted-provenance.cjs index 0e7861fdc..cb6aa5b6b 100644 --- a/tests/helpers/emitted-provenance.cjs +++ b/tests/helpers/emitted-provenance.cjs @@ -172,6 +172,16 @@ const ANTIGRAVITY_SKILL_TRANSFORM_SRCS = [ 'bin/install.js', ]; +// #4002: zcode's command AND skill bodies flow through `_applyRuntimeRewrites` +// (converter: null — the rewrite pass is their only path-rewriting step), so a +// converter change moves emitted bytes with no commands/gsd source changing. +// Same permanent-attribution shape as ANTIGRAVITY_SKILL_TRANSFORM_SRCS (#3738), +// scoped to runtime 'zcode' for the same reason. +const ZCODE_BODY_TRANSFORM_SRCS = [ + 'src/runtime-artifact-conversion.cts', + 'bin/install.js', +]; + /** * A `sources` entry ending in `/` is a PREFIX, not a file: it means "any repo path * under this directory legitimately explains this emitted path". Used where an @@ -495,7 +505,13 @@ const PROVENANCE_RULES = [ sources: (m) => [`${COMMANDS_SRC}/${stripSkillPrefix(m[1])}.md`], // #3738: see ANTIGRAVITY_SKILL_TRANSFORM_SRCS above — antigravity's skill // bytes are converter-produced, so a converter change explains the ripple. - transforms: (_m, ctx) => (ctx.runtime === 'antigravity' ? ANTIGRAVITY_SKILL_TRANSFORM_SRCS : []), + // #4002: zcode's skills flow through the same rewrite pass (see + // ZCODE_BODY_TRANSFORM_SRCS), so the converter change explains theirs too. + transforms: (_m, ctx) => { + if (ctx.runtime === 'antigravity') return ANTIGRAVITY_SKILL_TRANSFORM_SRCS; + if (ctx.runtime === 'zcode') return ZCODE_BODY_TRANSFORM_SRCS; + return []; + }, }, { id: 'skills-nested-from-commands', @@ -506,6 +522,9 @@ const PROVENANCE_RULES = [ // source — attributing to the router would be wrong for every nested skill. pattern: /^([^/]+)\/skills\/([^/]+)\/SKILL\.md$/, sources: (m) => [`${COMMANDS_SRC}/${stripSkillPrefix(m[2])}.md`], + // #4002: zcode's nested router children pass through the same rewrite pass + // as its flat skills — see ZCODE_BODY_TRANSFORM_SRCS. + transforms: (_m, ctx) => (ctx.runtime === 'zcode' ? ZCODE_BODY_TRANSFORM_SRCS : []), }, { id: 'flat-commands-from-commands', @@ -513,6 +532,9 @@ const PROVENANCE_RULES = [ roots: ['commands', 'command'], pattern: /^gsd-([^/]+)\.md$/, sources: (m) => [`${COMMANDS_SRC}/${m[1]}.md`], + // #4002: zcode command bodies pass through _applyRuntimeRewrites with + // converter: null — see ZCODE_BODY_TRANSFORM_SRCS above. + transforms: (_m, ctx) => (ctx.runtime === 'zcode' ? ZCODE_BODY_TRANSFORM_SRCS : []), }, // ── Descriptor-declared native plugin / extension ─────────────────────────