mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-05 00:03:15 +01:00
fix(setup): every env-var runtime root carries the files its skills read as $GSTACK_ROOT/<path> (#1077 residue)
Codex, Factory, Kiro, OpenCode, Cursor and Copilot runtime roots linked bin/, lib/ and the compiled tools but not the files the generated skills read on demand: the preamble's jargon list and question registry, the AskUserQuestion split and CJK docs, the DX Hall of Fame, the test-value bar, the review checklists and specialists, design-html's vendored pretext.js, VERSION and plan-design-review/SKILL.md. The reads failed silently on every non-Claude host. _link_runtime_dists now links the shared list for every root, and _copy_runtime_skill_refs copies the host-rendered office-hours and plan-design-review SKILL.md (the #2449 pattern) into each root. The per-root review loops it replaces had drifted (Cursor shipped two of four checklists, only OpenCode had specialists). hosts/define-host.ts mirrors the list in sharedRuntimeRoot(). test/runtime-root-assets.test.ts renders every host, builds each root with setup's own functions and checks every $GSTACK_ROOT literal on disk; it fails on the previous setup for all five roots. scripts/ship-measure.ts stays deferred (it needs the eval harness). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
4637fae03b
commit
34b6d3e4e1
11 files changed
+240
-114
No files matched your search
@@ -39,15 +39,6 @@ const codex = defineHost({
|
||||
{ from: 'CLAUDE.md', to: 'AGENTS.md' },
|
||||
],
|
||||
|
||||
// Mirrors create_codex_runtime_root in setup. design/dist and make-pdf/dist
|
||||
// back $GSTACK_DESIGN and $GSTACK_MAKE_PDF (#2891).
|
||||
runtimeRoot: {
|
||||
globalSymlinks: ['bin', 'lib', 'browse/dist', 'browse/bin', 'design/dist', 'make-pdf/dist', 'freeze/bin', 'careful/bin', 'gstack-upgrade', 'ETHOS.md'],
|
||||
globalFiles: {
|
||||
'review': ['checklist.md', 'design-checklist.md', 'greptile-triage.md', 'TODOS-format.md'],
|
||||
},
|
||||
},
|
||||
|
||||
// Outside-review resolvers route to Claude Code; Review Army has its own restriction.
|
||||
suppressedResolvers: ['REVIEW_ARMY', ...GBRAIN_RESOLVERS],
|
||||
|
||||
|
||||
+23
-6
@@ -73,6 +73,28 @@ export function preambleToolGlossary(glossary: string): Record<string, string> {
|
||||
return { [PREAMBLE_GLOSSARY_ANCHOR]: `${glossary}\n\n${PREAMBLE_GLOSSARY_ANCHOR}` };
|
||||
}
|
||||
|
||||
/**
|
||||
* The runtime-root assets setup installs for every env-var host: the tools
|
||||
* the skills run plus every file they read as "$GSTACK_ROOT/<path>". Mirrors
|
||||
* setup's _link_runtime_dists and _copy_runtime_skill_refs (each root also
|
||||
* links bin, lib, browse and ETHOS.md); test/runtime-root-assets.test.ts
|
||||
* checks the staged roots on disk. `extraSymlinks` appends host-only assets.
|
||||
*/
|
||||
export function sharedRuntimeRoot(extraSymlinks: string[] = []): HostConfig['runtimeRoot'] {
|
||||
return {
|
||||
globalSymlinks: ['bin', 'lib', 'browse/dist', 'browse/bin', 'design/dist', 'make-pdf/dist', 'freeze/bin', 'careful/bin',
|
||||
'review/specialists', 'design-html/vendor', 'gstack-upgrade', 'ETHOS.md', 'VERSION', ...extraSymlinks],
|
||||
globalFiles: {
|
||||
'review': ['checklist.md', 'design-checklist.md', 'greptile-triage.md', 'TODOS-format.md'],
|
||||
'scripts': ['jargon-list.json', 'question-registry.ts'],
|
||||
'docs': ['askuserquestion-split.md', 'askuserquestion-cjk.md', 'test-value-bar.md'],
|
||||
'plan-devex-review': ['dx-hall-of-fame.md'],
|
||||
'office-hours': ['SKILL.md'],
|
||||
'plan-design-review': ['SKILL.md'],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Host definition input: name + displayName are required, everything else is
|
||||
* an override on the common external-host defaults documented above.
|
||||
@@ -125,12 +147,7 @@ export function defineHost<const N extends string>(overrides: HostOverrides<N>):
|
||||
extraPathRewrites,
|
||||
toolRewrites,
|
||||
suppressedResolvers = [...GBRAIN_RESOLVERS],
|
||||
runtimeRoot = {
|
||||
globalSymlinks: ['bin', 'lib', 'browse/dist', 'browse/bin', 'design/dist', 'make-pdf/dist', 'freeze/bin', 'careful/bin', 'gstack-upgrade', 'ETHOS.md'],
|
||||
globalFiles: {
|
||||
'review': ['checklist.md', 'TODOS-format.md'],
|
||||
},
|
||||
},
|
||||
runtimeRoot = sharedRuntimeRoot(),
|
||||
install = {
|
||||
linkingStrategy: 'symlink-generated',
|
||||
},
|
||||
|
||||
+2
-7
@@ -1,4 +1,4 @@
|
||||
import { defineHost, preambleToolGlossary } from './define-host';
|
||||
import { defineHost, preambleToolGlossary, sharedRuntimeRoot } from './define-host';
|
||||
|
||||
const opencode = defineHost({
|
||||
name: 'opencode',
|
||||
@@ -13,12 +13,7 @@ const opencode = defineHost({
|
||||
|
||||
// OpenCode links a wider runtime asset set than the shared default
|
||||
// (design binary, review specialists, qa templates/references, DX hall of fame).
|
||||
runtimeRoot: {
|
||||
globalSymlinks: ['bin', 'lib', 'browse/dist', 'browse/bin', 'design/dist', 'make-pdf/dist', 'freeze/bin', 'careful/bin', 'gstack-upgrade', 'ETHOS.md', 'review/specialists', 'qa/templates', 'qa/references', 'plan-devex-review/dx-hall-of-fame.md'],
|
||||
globalFiles: {
|
||||
'review': ['checklist.md', 'design-checklist.md', 'greptile-triage.md', 'TODOS-format.md'],
|
||||
},
|
||||
},
|
||||
runtimeRoot: sharedRuntimeRoot(['qa/templates', 'qa/references']),
|
||||
});
|
||||
|
||||
export default opencode;
|
||||
@@ -630,8 +630,15 @@ _link_or_copy() {
|
||||
# "$GSTACK_ROOT/freeze/bin/...". Subdirectories only, never the whole skill
|
||||
# directory: make-pdf/, freeze/ and careful/ ship a Claude SKILL.md a host
|
||||
# scanning its skills root would discover as a duplicate skill.
|
||||
# Also the files the skills read on demand as "$GSTACK_ROOT/<path>" (#1077
|
||||
# residue): the preamble's jargon list, question registry and AskUserQuestion
|
||||
# docs, the review checklists and specialists, /plan-devex-review's Hall of
|
||||
# Fame, the test-value bar, design-html's vendored pretext.js and VERSION.
|
||||
# Pinned by test/runtime-root-assets.test.ts. A destination that already exists
|
||||
# as a real entry is left alone: in the repo-local sidecar review/ is a link to
|
||||
# the source, so its files already resolve and must never be linked over.
|
||||
_link_runtime_dists() {
|
||||
local d
|
||||
local d f
|
||||
# Record the source checkout (#3026): on Windows the root's browse/dist is a
|
||||
# copy with no node_modules beside it, and the browse CLI runs the checkout's
|
||||
# Node server bundle from here instead. /gstack-upgrade reads it too.
|
||||
@@ -640,13 +647,37 @@ _link_runtime_dists() {
|
||||
else
|
||||
printf '%s\n' "$1" > "$2/.source-path"
|
||||
fi
|
||||
for d in design/dist make-pdf/dist freeze/bin careful/bin; do
|
||||
for d in design/dist make-pdf/dist freeze/bin careful/bin review/specialists design-html/vendor; do
|
||||
[ -d "$1/$d" ] || continue
|
||||
if [ "$IS_WINDOWS" -eq 1 ] || [ -L "$2/$d" ] || [ ! -e "$2/$d" ]; then
|
||||
mkdir -p "$2/${d%/*}"
|
||||
_link_or_copy "$1/$d" "$2/$d"
|
||||
fi
|
||||
done
|
||||
for f in VERSION scripts/jargon-list.json scripts/question-registry.ts \
|
||||
docs/askuserquestion-split.md docs/askuserquestion-cjk.md docs/test-value-bar.md \
|
||||
review/checklist.md review/design-checklist.md review/greptile-triage.md review/TODOS-format.md \
|
||||
plan-devex-review/dx-hall-of-fame.md; do
|
||||
[ -f "$1/$f" ] || continue
|
||||
if [ "$IS_WINDOWS" -eq 1 ] || [ -L "$2/$f" ] || [ ! -e "$2/$f" ]; then
|
||||
case "$f" in */*) mkdir -p "$2/${f%/*}" ;; esac
|
||||
_link_or_copy "$1/$f" "$2/$f"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# _copy_runtime_skill_refs RENDER_SKILLS ROOT — skills that read another
|
||||
# skill's instructions as "$GSTACK_ROOT/<skill>/SKILL.md" get that host's
|
||||
# rendered copy there: plan-eng-review's inline office-hours step (#2449) and
|
||||
# the plan-design-review sections. Real-file copies, like every SKILL.md in a
|
||||
# runtime root, and only the SKILL.md, never the skill's source directory.
|
||||
_copy_runtime_skill_refs() {
|
||||
local s
|
||||
for s in office-hours plan-design-review; do
|
||||
[ -f "$1/gstack-$s/SKILL.md" ] || continue
|
||||
mkdir -p "$2/$s"
|
||||
_copy_skill_md "$1/gstack-$s/SKILL.md" "$2/$s/SKILL.md"
|
||||
done
|
||||
}
|
||||
|
||||
# ─── Real-file SKILL.md copies in host runtime roots (C3, #333) ──────────────
|
||||
@@ -2716,19 +2747,7 @@ create_codex_runtime_root() {
|
||||
if [ -f "$agents_dir/gstack-upgrade/SKILL.md" ]; then
|
||||
_copy_skill_md "$agents_dir/gstack-upgrade/SKILL.md" "$codex_gstack/gstack-upgrade/SKILL.md"
|
||||
fi
|
||||
# plan-eng-review's inline office-hours step reads
|
||||
# $GSTACK_ROOT/office-hours/SKILL.md (#2449) — install the codex-rendered
|
||||
# variant there so the documented path exists.
|
||||
if [ -f "${agents_dir}/gstack-office-hours/SKILL.md" ]; then
|
||||
mkdir -p "${codex_gstack}/office-hours"
|
||||
_copy_skill_md "${agents_dir}/gstack-office-hours/SKILL.md" "${codex_gstack}/office-hours/SKILL.md"
|
||||
fi
|
||||
# Review runtime assets (individual files, NOT the whole review/ dir which has SKILL.md)
|
||||
for f in checklist.md design-checklist.md greptile-triage.md TODOS-format.md; do
|
||||
if [ -f "$gstack_dir/review/$f" ]; then
|
||||
_link_or_copy "$gstack_dir/review/$f" "$codex_gstack/review/$f"
|
||||
fi
|
||||
done
|
||||
_copy_runtime_skill_refs "$agents_dir" "$codex_gstack"
|
||||
# ETHOS.md — referenced by "Search Before Building" in all skill preambles
|
||||
if [ -f "$gstack_dir/ETHOS.md" ]; then
|
||||
_link_or_copy "$gstack_dir/ETHOS.md" "$codex_gstack/ETHOS.md"
|
||||
@@ -2772,18 +2791,7 @@ create_factory_runtime_root() {
|
||||
if [ -f "$factory_dir/gstack-upgrade/SKILL.md" ]; then
|
||||
_copy_skill_md "$factory_dir/gstack-upgrade/SKILL.md" "$factory_gstack/gstack-upgrade/SKILL.md"
|
||||
fi
|
||||
# plan-eng-review's inline office-hours step reads
|
||||
# $GSTACK_ROOT/office-hours/SKILL.md (#2449) — install the factory-rendered
|
||||
# variant there so the documented path exists.
|
||||
if [ -f "${factory_dir}/gstack-office-hours/SKILL.md" ]; then
|
||||
mkdir -p "${factory_gstack}/office-hours"
|
||||
_copy_skill_md "${factory_dir}/gstack-office-hours/SKILL.md" "${factory_gstack}/office-hours/SKILL.md"
|
||||
fi
|
||||
for f in checklist.md design-checklist.md greptile-triage.md TODOS-format.md; do
|
||||
if [ -f "$gstack_dir/review/$f" ]; then
|
||||
_link_or_copy "$gstack_dir/review/$f" "$factory_gstack/review/$f"
|
||||
fi
|
||||
done
|
||||
_copy_runtime_skill_refs "$factory_dir" "$factory_gstack"
|
||||
if [ -f "$gstack_dir/ETHOS.md" ]; then
|
||||
_link_or_copy "$gstack_dir/ETHOS.md" "$factory_gstack/ETHOS.md"
|
||||
fi
|
||||
@@ -2805,7 +2813,7 @@ create_opencode_runtime_root() {
|
||||
rm -rf "$opencode_gstack"
|
||||
fi
|
||||
|
||||
mkdir -p "$opencode_gstack" "$opencode_gstack/browse" "$opencode_gstack/gstack-upgrade" "$opencode_gstack/review" "$opencode_gstack/qa" "$opencode_gstack/plan-devex-review"
|
||||
mkdir -p "$opencode_gstack" "$opencode_gstack/browse" "$opencode_gstack/gstack-upgrade" "$opencode_gstack/review" "$opencode_gstack/qa"
|
||||
|
||||
if [ -f "$opencode_dir/gstack/SKILL.md" ]; then
|
||||
_copy_skill_md "$opencode_dir/gstack/SKILL.md" "$opencode_gstack/SKILL.md"
|
||||
@@ -2826,30 +2834,13 @@ create_opencode_runtime_root() {
|
||||
if [ -f "$opencode_dir/gstack-upgrade/SKILL.md" ]; then
|
||||
_copy_skill_md "$opencode_dir/gstack-upgrade/SKILL.md" "$opencode_gstack/gstack-upgrade/SKILL.md"
|
||||
fi
|
||||
# plan-eng-review's inline office-hours step reads
|
||||
# $GSTACK_ROOT/office-hours/SKILL.md (#2449) — install the opencode-rendered
|
||||
# variant there so the documented path exists.
|
||||
if [ -f "${opencode_dir}/gstack-office-hours/SKILL.md" ]; then
|
||||
mkdir -p "${opencode_gstack}/office-hours"
|
||||
_copy_skill_md "${opencode_dir}/gstack-office-hours/SKILL.md" "${opencode_gstack}/office-hours/SKILL.md"
|
||||
fi
|
||||
for f in checklist.md design-checklist.md greptile-triage.md TODOS-format.md; do
|
||||
if [ -f "$gstack_dir/review/$f" ]; then
|
||||
_link_or_copy "$gstack_dir/review/$f" "$opencode_gstack/review/$f"
|
||||
fi
|
||||
done
|
||||
if [ -d "$gstack_dir/review/specialists" ]; then
|
||||
_link_or_copy "$gstack_dir/review/specialists" "$opencode_gstack/review/specialists"
|
||||
fi
|
||||
_copy_runtime_skill_refs "$opencode_dir" "$opencode_gstack"
|
||||
if [ -d "$gstack_dir/qa/templates" ]; then
|
||||
_link_or_copy "$gstack_dir/qa/templates" "$opencode_gstack/qa/templates"
|
||||
fi
|
||||
if [ -d "$gstack_dir/qa/references" ]; then
|
||||
_link_or_copy "$gstack_dir/qa/references" "$opencode_gstack/qa/references"
|
||||
fi
|
||||
if [ -f "$gstack_dir/plan-devex-review/dx-hall-of-fame.md" ]; then
|
||||
_link_or_copy "$gstack_dir/plan-devex-review/dx-hall-of-fame.md" "$opencode_gstack/plan-devex-review/dx-hall-of-fame.md"
|
||||
fi
|
||||
if [ -f "$gstack_dir/ETHOS.md" ]; then
|
||||
_link_or_copy "$gstack_dir/ETHOS.md" "$opencode_gstack/ETHOS.md"
|
||||
fi
|
||||
@@ -2989,12 +2980,7 @@ create_cursor_runtime_root() {
|
||||
if [ -f "$cursor_dir/gstack-upgrade/SKILL.md" ]; then
|
||||
_copy_skill_md "$cursor_dir/gstack-upgrade/SKILL.md" "$cursor_gstack/gstack-upgrade/SKILL.md"
|
||||
fi
|
||||
# Review runtime assets — the cursor host config ships the lean pair.
|
||||
for f in checklist.md TODOS-format.md; do
|
||||
if [ -f "$gstack_dir/review/$f" ]; then
|
||||
_link_or_copy "$gstack_dir/review/$f" "$cursor_gstack/review/$f"
|
||||
fi
|
||||
done
|
||||
_copy_runtime_skill_refs "$cursor_dir" "$cursor_gstack"
|
||||
if [ -f "$gstack_dir/ETHOS.md" ]; then
|
||||
_link_or_copy "$gstack_dir/ETHOS.md" "$cursor_gstack/ETHOS.md"
|
||||
fi
|
||||
@@ -3049,13 +3035,6 @@ create_cursor_sidecar() {
|
||||
_link_or_copy "$cursor_dir/gstack-upgrade/SKILL.md" "$cursor_gstack/gstack-upgrade/SKILL.md"
|
||||
fi
|
||||
fi
|
||||
for f in checklist.md TODOS-format.md; do
|
||||
if [ -f "$repo_root/review/$f" ]; then
|
||||
if [ "$IS_WINDOWS" -eq 1 ] || [ -L "$cursor_gstack/review/$f" ] || [ ! -e "$cursor_gstack/review/$f" ]; then
|
||||
_link_or_copy "$repo_root/review/$f" "$cursor_gstack/review/$f"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if [ -f "$repo_root/ETHOS.md" ]; then
|
||||
if [ "$IS_WINDOWS" -eq 1 ] || [ -L "$cursor_gstack/ETHOS.md" ] || [ ! -e "$cursor_gstack/ETHOS.md" ]; then
|
||||
_link_or_copy "$repo_root/ETHOS.md" "$cursor_gstack/ETHOS.md"
|
||||
@@ -3161,15 +3140,7 @@ create_copilot_runtime_root() {
|
||||
if [ -f "$copilot_dir/gstack-upgrade/SKILL.md" ]; then
|
||||
_copy_skill_md "$copilot_dir/gstack-upgrade/SKILL.md" "$copilot_gstack/gstack-upgrade/SKILL.md"
|
||||
fi
|
||||
if [ -f "$copilot_dir/gstack-office-hours/SKILL.md" ]; then
|
||||
mkdir -p "$copilot_gstack/office-hours"
|
||||
_copy_skill_md "$copilot_dir/gstack-office-hours/SKILL.md" "$copilot_gstack/office-hours/SKILL.md"
|
||||
fi
|
||||
for f in checklist.md design-checklist.md greptile-triage.md TODOS-format.md; do
|
||||
if [ -f "$gstack_dir/review/$f" ]; then
|
||||
_link_or_copy "$gstack_dir/review/$f" "$copilot_gstack/review/$f"
|
||||
fi
|
||||
done
|
||||
_copy_runtime_skill_refs "$copilot_dir" "$copilot_gstack"
|
||||
if [ -f "$gstack_dir/ETHOS.md" ]; then
|
||||
_link_or_copy "$gstack_dir/ETHOS.md" "$copilot_gstack/ETHOS.md"
|
||||
fi
|
||||
@@ -3460,15 +3431,7 @@ if [ "$INSTALL_KIRO" -eq 1 ]; then
|
||||
if [ -f "$KIRO_DIR/gstack/SKILL.md" ]; then
|
||||
_copy_skill_md "$KIRO_DIR/gstack/SKILL.md" "$KIRO_GSTACK/SKILL.md"
|
||||
fi
|
||||
if [ -f "$KIRO_DIR/gstack-office-hours/SKILL.md" ]; then
|
||||
mkdir -p "$KIRO_GSTACK/office-hours"
|
||||
_copy_skill_md "$KIRO_DIR/gstack-office-hours/SKILL.md" "$KIRO_GSTACK/office-hours/SKILL.md"
|
||||
fi
|
||||
for f in checklist.md design-checklist.md greptile-triage.md TODOS-format.md; do
|
||||
if [ -f "$SOURCE_GSTACK_DIR/review/$f" ]; then
|
||||
_link_or_copy "$SOURCE_GSTACK_DIR/review/$f" "$KIRO_GSTACK/review/$f"
|
||||
fi
|
||||
done
|
||||
_copy_runtime_skill_refs "$KIRO_DIR" "$KIRO_GSTACK"
|
||||
for skill_dir in "$KIRO_DIR"/gstack*/; do
|
||||
[ -f "$skill_dir/SKILL.md" ] || continue
|
||||
skill_name="$(basename "$skill_dir")"
|
||||
|
||||
@@ -93,7 +93,7 @@ describe.skipIf(process.platform === 'win32')('Codex global install: the freeze
|
||||
const rootDir = path.join(codexHome, 'skills', 'gstack');
|
||||
const project = path.join(sandbox, 'project', 'src');
|
||||
fs.mkdirSync(project, { recursive: true });
|
||||
const functions = ['_link_or_copy', '_link_runtime_dists', '_copy_skill_md', '_gstack_generated_header', 'create_codex_runtime_root']
|
||||
const functions = ['_link_or_copy', '_link_runtime_dists', '_copy_runtime_skill_refs', '_copy_skill_md', '_gstack_generated_header', 'create_codex_runtime_root']
|
||||
.filter(name => SETUP_SRC.includes(`\n${name}() {`)).map(extractFunction).join('\n');
|
||||
const built = spawnSync('bash', ['-c', `IS_WINDOWS=0\n_CODEX_RENDER_ROOT="${renderDir}"\n${functions}\ncreate_codex_runtime_root "${ROOT}" "${rootDir}"`],
|
||||
{ encoding: 'utf8', timeout: 30_000, env: { PATH: process.env.PATH ?? '/usr/bin:/bin', HOME: home } });
|
||||
|
||||
+14
-11
@@ -3492,13 +3492,13 @@ describe('setup script validation', () => {
|
||||
expect(fnBody).toContain('browse/dist');
|
||||
expect(fnBody).toContain('browse/bin');
|
||||
expect(fnBody).toContain('gstack-upgrade/SKILL.md');
|
||||
expect(fnBody).toContain('checklist.md');
|
||||
expect(fnBody).toContain('TODOS-format.md');
|
||||
// Review checklists and the other read-on-demand files come from the shared
|
||||
// helpers, the same set every env-var root gets (#1077 residue): the Cursor
|
||||
// render reads design-checklist.md and review/specialists too.
|
||||
expect(fnBody).toContain('_link_runtime_dists "$gstack_dir" "$cursor_gstack"');
|
||||
expect(fnBody).toContain('_copy_runtime_skill_refs "$cursor_dir" "$cursor_gstack"');
|
||||
// bin scripts import ../lib — the two must travel together.
|
||||
expect(fnBody).toContain('$cursor_gstack/lib');
|
||||
expect(fnBody).not.toContain('design-checklist.md');
|
||||
expect(fnBody).not.toContain('greptile-triage.md');
|
||||
expect(fnBody).not.toContain('review/specialists');
|
||||
expect(fnBody).not.toContain('qa/templates');
|
||||
expect(fnBody).not.toContain('_link_or_copy "$gstack_dir" "$cursor_gstack"');
|
||||
});
|
||||
@@ -3572,19 +3572,22 @@ describe('setup script validation', () => {
|
||||
|
||||
test('create_codex_runtime_root exposes only runtime assets', () => {
|
||||
const fnStart = setupContent.indexOf('create_codex_runtime_root()');
|
||||
const fnEnd = setupContent.indexOf('}', setupContent.indexOf('done', setupContent.indexOf('review/', fnStart)));
|
||||
const fnEnd = setupContent.indexOf('\n}\n', fnStart);
|
||||
const fnBody = setupContent.slice(fnStart, fnEnd);
|
||||
expect(fnBody).toContain('gstack/SKILL.md');
|
||||
expect(fnBody).toContain('$codex_gstack/lib');
|
||||
expect(fnBody).toContain('browse/dist');
|
||||
expect(fnBody).toContain('browse/bin');
|
||||
expect(fnBody).toContain('gstack-upgrade/SKILL.md');
|
||||
// Review runtime assets (individual files, not the whole dir)
|
||||
expect(fnBody).toContain('checklist.md');
|
||||
expect(fnBody).toContain('design-checklist.md');
|
||||
expect(fnBody).toContain('greptile-triage.md');
|
||||
expect(fnBody).toContain('TODOS-format.md');
|
||||
expect(fnBody).toContain('_link_runtime_dists "$gstack_dir" "$codex_gstack"');
|
||||
expect(fnBody).not.toContain('_link_or_copy "$gstack_dir" "$codex_gstack"');
|
||||
// Review runtime assets: individual files from the shared helper, never
|
||||
// the whole review/ dir, which has a SKILL.md.
|
||||
const shared = setupContent.slice(setupContent.indexOf('_link_runtime_dists() {'), setupContent.indexOf('_copy_runtime_skill_refs() {'));
|
||||
for (const f of ['review/checklist.md', 'review/design-checklist.md', 'review/greptile-triage.md', 'review/TODOS-format.md']) {
|
||||
expect(shared).toContain(f);
|
||||
}
|
||||
expect(shared).not.toMatch(/for d in [^;]*\breview(?:\s|;)/);
|
||||
});
|
||||
|
||||
test('create_factory_runtime_root links shared lib modules beside bin', () => {
|
||||
|
||||
@@ -529,7 +529,7 @@ describe('installed QA pointers', () => {
|
||||
'_gstack_generated_header', '_claude_entry_owned_strongly', '_claude_entry_is_ours', '_write_owned_marker',
|
||||
'_backup_skill_md', '_cleanup_weak_dir', '_gstack_dir_only_links', '_cleanup_linked_dir',
|
||||
'_owned_for_windows_refresh', '_sidecar_root_user_owned', '_prune_stale_generated', '_skill_source_exists',
|
||||
'_link_runtime_dists', '_copy_skill_md', '_skill_copy_hash', '_skill_copy_unmodified', '_preserve_skill_copy_edits', '_record_skill_copies',
|
||||
'_link_runtime_dists', '_copy_runtime_skill_refs', '_copy_skill_md', '_skill_copy_hash', '_skill_copy_unmodified', '_preserve_skill_copy_edits', '_record_skill_copies',
|
||||
].map(setupFunction).join('\n')
|
||||
// Install-registry rows (setup's _setup_arm_* / _setup_row) are not under test here.
|
||||
+ '\n_setup_arm_begin() { :; }\n_setup_arm_publish() { :; }\n_setup_row() { :; }';
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* Every file a generated skill reads through $GSTACK_ROOT exists in the
|
||||
* runtime root setup builds for that host (#1077 residue, fix wave C2).
|
||||
*
|
||||
* Env-var hosts reach gstack only through their runtime root
|
||||
* (~/.codex/skills/gstack, ~/.factory/skills/gstack, ...). The roots carried
|
||||
* bin/, lib/ and the compiled tools, but not the files the shared preamble and
|
||||
* several skills read on demand: the jargon list and question registry, the
|
||||
* AskUserQuestion split and CJK docs, the DX Hall of Fame, the test-value bar,
|
||||
* VERSION, design-html's vendored pretext.js and plan-design-review/SKILL.md.
|
||||
* Those reads failed silently on every non-Claude host.
|
||||
*
|
||||
* This test renders every host, builds each runtime root with setup's own
|
||||
* shell functions, then checks every $GSTACK_ROOT/<path> literal in that
|
||||
* host's render (prose and fences) against the staged root on disk.
|
||||
*/
|
||||
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
||||
import { spawnSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { HOST_CONFIG_MAP } from '../hosts';
|
||||
import { runGeneration } from '../scripts/gen-skill-docs';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const SETUP_SRC = fs.readFileSync(path.join(ROOT, 'setup'), 'utf-8');
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-root-assets-'));
|
||||
const renderDir = path.join(tmp, 'render');
|
||||
const src = path.join(tmp, 'gstack');
|
||||
|
||||
// Host -> the setup function that builds its global runtime root. Kiro builds
|
||||
// its root inline in the Kiro arm and is covered by a source assertion below.
|
||||
const ROOT_BUILDERS: Record<string, string> = {
|
||||
codex: 'create_codex_runtime_root',
|
||||
factory: 'create_factory_runtime_root',
|
||||
opencode: 'create_opencode_runtime_root',
|
||||
cursor: 'create_cursor_runtime_root',
|
||||
copilot: 'create_copilot_runtime_root',
|
||||
};
|
||||
|
||||
// Deferred, with the reason recorded in TODOS.md: /ship's measure loop runs
|
||||
// scripts/ship-measure.ts, which imports gstack's test helpers and eval
|
||||
// harness; a runtime root would need the whole development tree.
|
||||
const DEFERRED = new Set(['scripts/ship-measure.ts']);
|
||||
|
||||
// Compiled outputs: the stub source carries placeholder binaries, so a fresh
|
||||
// checkout without a build still exercises the links to them.
|
||||
const BUILT = ['browse/dist/browse', 'browse/dist/find-browse', 'design/dist/design', 'make-pdf/dist/pdf'];
|
||||
// Compiled into bin/, which every root links whole: checked through that link.
|
||||
const BUILT_IN_BIN = new Set(['bin/gstack-global-discover']);
|
||||
|
||||
function extractFunction(name: string): string {
|
||||
const start = SETUP_SRC.indexOf(`\n${name}() {`);
|
||||
const end = SETUP_SRC.indexOf('\n}\n', start);
|
||||
if (start < 0 || end < 0) throw new Error(`Could not locate ${name}() in setup`);
|
||||
return SETUP_SRC.slice(start + 1, end + 2);
|
||||
}
|
||||
|
||||
function walk(dir: string): string[] {
|
||||
return fs.readdirSync(dir, { withFileTypes: true }).flatMap(e =>
|
||||
e.isDirectory() ? walk(path.join(dir, e.name)) : [path.join(dir, e.name)]);
|
||||
}
|
||||
|
||||
/** Every $GSTACK_ROOT/<path> literal in a host's rendered skills. */
|
||||
function referencedPaths(host: string): Set<string> {
|
||||
const out = new Set<string>();
|
||||
for (const file of walk(path.join(renderDir, HOST_CONFIG_MAP[host]!.hostSubdir, 'skills'))) {
|
||||
for (const m of fs.readFileSync(file, 'utf8').matchAll(/\$\{?GSTACK_ROOT\}?\/([A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*)/g)) {
|
||||
out.add(m[1]!.replace(/\.+$/, ''));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// A source tree that links every tracked top-level entry of this checkout,
|
||||
// with stub compiled binaries and each host's render in its usual place.
|
||||
function makeSource(): void {
|
||||
fs.mkdirSync(src, { recursive: true });
|
||||
const built = new Set(BUILT.map(rel => rel.split('/')[0]!));
|
||||
for (const name of fs.readdirSync(ROOT)) {
|
||||
if (name.startsWith('.') || name === 'node_modules') continue;
|
||||
if (!built.has(name)) { fs.symlinkSync(path.join(ROOT, name), path.join(src, name)); continue; }
|
||||
fs.mkdirSync(path.join(src, name));
|
||||
for (const child of fs.readdirSync(path.join(ROOT, name))) {
|
||||
if (child !== 'dist') fs.symlinkSync(path.join(ROOT, name, child), path.join(src, name, child));
|
||||
}
|
||||
}
|
||||
for (const rel of BUILT) {
|
||||
fs.mkdirSync(path.dirname(path.join(src, rel)), { recursive: true });
|
||||
fs.writeFileSync(path.join(src, rel), '#!/bin/sh\nexit 0\n', { mode: 0o755 });
|
||||
}
|
||||
for (const host of Object.keys(ROOT_BUILDERS)) {
|
||||
const sub = HOST_CONFIG_MAP[host]!.hostSubdir;
|
||||
fs.symlinkSync(path.join(renderDir, sub), path.join(src, sub));
|
||||
}
|
||||
}
|
||||
|
||||
function buildRoot(host: string): { root: string; stderr: string; status: number | null } {
|
||||
const root = path.join(tmp, 'home', host, 'skills', 'gstack');
|
||||
fs.mkdirSync(path.dirname(root), { recursive: true });
|
||||
const functions = ['_link_or_copy', '_link_runtime_dists', '_copy_runtime_skill_refs', '_copy_skill_md',
|
||||
'_sidecar_root_user_owned', '_gstack_generated_header', ROOT_BUILDERS[host]!]
|
||||
.filter(name => SETUP_SRC.includes(`\n${name}() {`)).map(extractFunction).join('\n');
|
||||
const r = spawnSync('bash', ['-c', `set -e\nIS_WINDOWS=0\n${functions}\n${ROOT_BUILDERS[host]} "${src}" "${root}"`],
|
||||
{ encoding: 'utf8', timeout: 30_000, env: { PATH: process.env.PATH ?? '/usr/bin:/bin', HOME: path.join(tmp, 'home') } });
|
||||
return { root, stderr: r.stderr, status: r.status };
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
const result = await runGeneration({ host: 'all', outputRoot: renderDir });
|
||||
if (result.exitCode !== 0) throw new Error(result.diagnostics.filter(d => d.kind === 'error').map(d => d.message).join('\n'));
|
||||
makeSource();
|
||||
});
|
||||
afterAll(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
||||
|
||||
describe.skipIf(process.platform === 'win32')('runtime roots carry every $GSTACK_ROOT file their skills read', () => {
|
||||
test('the extractor finds the known read-on-demand files in the Codex render', () => {
|
||||
const paths = referencedPaths('codex');
|
||||
for (const rel of ['scripts/jargon-list.json', 'scripts/question-registry.ts', 'docs/askuserquestion-split.md', 'plan-devex-review/dx-hall-of-fame.md']) {
|
||||
expect(paths.has(rel), rel).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
for (const host of Object.keys(ROOT_BUILDERS)) {
|
||||
test(`${host}: every referenced path exists in the staged runtime root`, () => {
|
||||
const { root, stderr, status } = buildRoot(host);
|
||||
expect(stderr).toBe('');
|
||||
expect(status).toBe(0);
|
||||
const missing = [...referencedPaths(host)]
|
||||
.filter(rel => !DEFERRED.has(rel) && !BUILT_IN_BIN.has(rel) && !fs.existsSync(path.join(root, rel))).sort();
|
||||
expect(missing).toEqual([]);
|
||||
// Each one is the same kind of entry as in the source: a file stays a file.
|
||||
const wrongKind = [...referencedPaths(host)].filter(rel => fs.existsSync(path.join(root, rel)) && fs.existsSync(path.join(ROOT, rel))
|
||||
&& fs.statSync(path.join(root, rel)).isFile() !== fs.statSync(path.join(ROOT, rel)).isFile());
|
||||
expect(wrongKind).toEqual([]);
|
||||
expect(fs.realpathSync(path.join(root, 'bin'))).toBe(fs.realpathSync(path.join(ROOT, 'bin')));
|
||||
});
|
||||
}
|
||||
|
||||
test('a runtime root never gains a whole skill directory beside the rendered copies', () => {
|
||||
const { root } = buildRoot('codex');
|
||||
for (const dir of ['plan-design-review', 'office-hours']) {
|
||||
expect(fs.readdirSync(path.join(root, dir))).toEqual(['SKILL.md']);
|
||||
expect(fs.lstatSync(path.join(root, dir, 'SKILL.md')).isSymbolicLink()).toBe(false);
|
||||
expect(fs.readFileSync(path.join(root, dir, 'SKILL.md'), 'utf8')).toContain('$GSTACK_ROOT');
|
||||
}
|
||||
expect(fs.existsSync(path.join(root, 'design-html', 'SKILL.md'))).toBe(false);
|
||||
expect(fs.existsSync(path.join(root, 'plan-devex-review', 'SKILL.md'))).toBe(false);
|
||||
});
|
||||
|
||||
test('the Kiro arm uses the same shared asset helpers as every other root', () => {
|
||||
const arm = SETUP_SRC.slice(SETUP_SRC.indexOf('# 6. Install for Kiro CLI'), SETUP_SRC.indexOf('# 6b. Install for Factory Droid'));
|
||||
expect(arm.includes('_link_runtime_dists "$SOURCE_GSTACK_DIR" "$KIRO_GSTACK"')).toBe(true);
|
||||
expect(arm.includes('_copy_runtime_skill_refs "$KIRO_DIR" "$KIRO_GSTACK"')).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -97,7 +97,7 @@ const BUILDERS: Record<string, (sandbox: string, src: string) => Built> = {
|
||||
},
|
||||
};
|
||||
|
||||
const FUNCTIONS = ['_link_or_copy', '_link_runtime_dists', '_copy_skill_md', '_sidecar_root_user_owned', '_gstack_generated_header', 'create_agents_sidecar', 'create_codex_runtime_root']
|
||||
const FUNCTIONS = ['_link_or_copy', '_link_runtime_dists', '_copy_runtime_skill_refs', '_copy_skill_md', '_sidecar_root_user_owned', '_gstack_generated_header', 'create_agents_sidecar', 'create_codex_runtime_root']
|
||||
.filter(name => SETUP_SRC.includes(`\n${name}() {`))
|
||||
.map(extractFunction)
|
||||
.join('\n');
|
||||
|
||||
@@ -17,7 +17,7 @@ const fn = (name: string) => {
|
||||
const blockStart = setup.indexOf('# 6. Install for Kiro CLI');
|
||||
const block = setup.slice(blockStart, setup.indexOf('# 6b.', blockStart));
|
||||
const helpers = [
|
||||
'_link_or_copy', '_link_runtime_dists', '_sidecar_root_user_owned', '_claude_entry_is_ours',
|
||||
'_link_or_copy', '_link_runtime_dists', '_copy_runtime_skill_refs', '_sidecar_root_user_owned', '_claude_entry_is_ours',
|
||||
'_claude_entry_owned_strongly', '_gstack_link_target_abs', '_gstack_target_is_ours',
|
||||
'_gstack_generated_header', '_backup_skill_md', '_prune_stale_generated',
|
||||
'_skill_source_exists', '_owned_for_windows_refresh', '_cleanup_weak_dir',
|
||||
|
||||
@@ -160,6 +160,7 @@ function runInstaller(
|
||||
extraVars,
|
||||
extractFn('_link_or_copy'),
|
||||
extractFn('_link_runtime_dists'),
|
||||
extractFn('_copy_runtime_skill_refs'),
|
||||
// Ownership gates (#2142) — dependencies of every installer under test.
|
||||
extractFn('_owned_for_windows_refresh'),
|
||||
extractFn('_sidecar_root_user_owned'),
|
||||
|
||||
Reference in new issue
Block a user