* test(#4002): zcode commands must rewrite at-refs to the zcode home * fix(#4002): add the missing zcode case to the runtime rewrite engine * chore(#4002): add ZCode to the bug-report runtime dropdown and drop the changeset * fix(#4002): attribute zcode command and skill ripples to the rewrite engine * fix(#4002): attribute zcode nested-skill ripples to the rewrite engine * chore(#4002): backfill changeset pr number * fix: bump qs past GHSA-x5fp-wj9c-mxmx (transitive, advisory reddened next) --------- Co-authored-by: sim <sim@local>
This commit is contained in:
5
.changeset/vivid-rams-rally.md
Normal file
5
.changeset/vivid-rams-rally.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 4188
|
||||
---
|
||||
**ZCode installs: command `<execution_context>` @-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)
|
||||
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -43,6 +43,7 @@ body:
|
||||
- Antigravity
|
||||
- Cursor
|
||||
- Windsurf
|
||||
- ZCode (Z.ai)
|
||||
- Multiple (specify in description)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
31
package-lock.json
generated
31
package-lock.json
generated
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 <execution_context> @-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
|
||||
|
||||
@@ -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 ─────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user