mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-17 15:20:11 +02:00
Merge remote-tracking branch 'origin/main' into garrytan/codex-review-default-on
This commit is contained in:
@@ -50,6 +50,24 @@ function regenerateAndValidate() {
|
||||
console.log(` [check] \u2705 ${output} — ${totalValid} commands, all valid`);
|
||||
}
|
||||
}
|
||||
|
||||
// Dev workspace render isolation: the default in-place regen above keeps the
|
||||
// worktree canonical. If bin/dev-setup set up an untracked brain-aware render
|
||||
// (.claude/gstack-rendered), refresh it too so live template edits reflect at
|
||||
// this workspace's runtime. Only runs when the render dir already exists — we
|
||||
// never create it during plain template dev.
|
||||
const RENDER_DIR = path.join(ROOT, '.claude', 'gstack-rendered');
|
||||
if (fs.existsSync(RENDER_DIR)) {
|
||||
try {
|
||||
execSync(
|
||||
`bun run scripts/gen-skill-docs.ts --respect-detection --host claude --out-dir ${JSON.stringify(RENDER_DIR)}`,
|
||||
{ cwd: ROOT, stdio: 'pipe' },
|
||||
);
|
||||
console.log(' [render] refreshed .claude/gstack-rendered (brain-aware workspace copy)');
|
||||
} catch (err: any) {
|
||||
console.log(` [render] ERROR: ${err.stderr?.toString().trim() || err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initial run
|
||||
|
||||
@@ -137,6 +137,39 @@ const EXPLAIN_LEVEL: 'default' | 'terse' = (() => {
|
||||
return val;
|
||||
})();
|
||||
|
||||
// ─── Out-dir (dev workspace render isolation) ───────────────
|
||||
// --out-dir <abs-dir> redirects Claude SKILL.md + section output to a separate
|
||||
// (untracked) directory instead of writing in place, AND rewrites the literal
|
||||
// section-base path (`~/.claude/skills/gstack/<skill>/sections/`) inside the
|
||||
// generated content to point at the out-dir, so section Reads resolve to the
|
||||
// rendered copy rather than the global install. Used by bin/dev-setup to render
|
||||
// the gbrain `:user` variant for a Conductor workspace without dirtying tracked
|
||||
// source. Default (unset) = in-place, behavior unchanged. Claude host only.
|
||||
const OUT_DIR_ARG = process.argv.find(a => a.startsWith('--out-dir'));
|
||||
const OUT_DIR: string | null = (() => {
|
||||
if (!OUT_DIR_ARG) return null;
|
||||
const val = OUT_DIR_ARG.includes('=')
|
||||
? OUT_DIR_ARG.split('=')[1]
|
||||
: process.argv[process.argv.indexOf(OUT_DIR_ARG) + 1];
|
||||
if (!val) throw new Error('--out-dir requires a directory path');
|
||||
return path.resolve(val);
|
||||
})();
|
||||
|
||||
/**
|
||||
* When rendering to an out-dir, repoint the literal section-base path at the
|
||||
* out-dir so section Reads resolve to the rendered copy, not the global install.
|
||||
* Surgical: ONLY paths containing `/sections/` are rewritten — bin/, browse/,
|
||||
* docs/ references keep pointing at `~/.claude/skills/gstack` (the global
|
||||
* install, which still works). No-op when --out-dir is unset.
|
||||
*/
|
||||
function rewriteSectionBase(content: string): string {
|
||||
if (!OUT_DIR) return content;
|
||||
return content.replace(
|
||||
/~\/\.claude\/skills\/gstack\/([^\s)`"'*]+\/sections\/)/g,
|
||||
`${OUT_DIR}/$1`,
|
||||
);
|
||||
}
|
||||
|
||||
// HostPaths, HOST_PATHS, and TemplateContext imported from ./resolvers/types (line 7-8)
|
||||
// Design constants (AI_SLOP_BLACKLIST, OPENAI_HARD_REJECTIONS, OPENAI_LITMUS_CHECKS)
|
||||
// live in ./resolvers/constants and are consumed by resolvers directly.
|
||||
@@ -768,6 +801,12 @@ function processTemplate(tmplPath: string, host: Host = 'claude'): { outputPath:
|
||||
// Determine skill directory relative to ROOT
|
||||
const skillDir = path.relative(ROOT, path.dirname(tmplPath));
|
||||
|
||||
// --out-dir (Claude only): mirror the skill tree into the out-dir instead of
|
||||
// writing in place. External hosts compute their own paths below.
|
||||
if (OUT_DIR && host === 'claude') {
|
||||
outputPath = path.join(OUT_DIR, skillDir, path.basename(tmplPath).replace(/\.tmpl$/, ''));
|
||||
}
|
||||
|
||||
// Extract name/description: name drives external skill naming + setup symlinks
|
||||
// (and TemplateContext.skillName via buildContext); description feeds external
|
||||
// host metadata. When frontmatter name: differs from directory name (e.g.
|
||||
@@ -822,6 +861,9 @@ function processTemplate(tmplPath: string, host: Host = 'claude'): { outputPath:
|
||||
}
|
||||
}
|
||||
|
||||
// --out-dir: repoint section-base paths to the out-dir (no-op otherwise).
|
||||
if (host === 'claude') content = rewriteSectionBase(content);
|
||||
|
||||
return { outputPath, content, symlinkLoop, catalogParts };
|
||||
}
|
||||
|
||||
@@ -860,6 +902,10 @@ function processSectionTemplate(
|
||||
// External hosts: rewrite cross-reference paths/tools (no frontmatter to transform).
|
||||
if (host !== 'claude') {
|
||||
content = applyHostRewrites(content, hostConfig);
|
||||
} else {
|
||||
// --out-dir: a section may cross-reference another section by absolute path;
|
||||
// repoint those to the out-dir too (no-op when --out-dir is unset).
|
||||
content = rewriteSectionBase(content);
|
||||
}
|
||||
|
||||
// Plain generated header (no frontmatter to insert after).
|
||||
@@ -868,7 +914,7 @@ function processSectionTemplate(
|
||||
const fileName = path.basename(sectionTmplPath).replace(/\.tmpl$/, '');
|
||||
let outputPath: string;
|
||||
if (host === 'claude') {
|
||||
outputPath = path.join(ROOT, skillDir, 'sections', fileName);
|
||||
outputPath = path.join(OUT_DIR || ROOT, skillDir, 'sections', fileName);
|
||||
} else {
|
||||
const externalName = externalSkillName(skillDir, parentName);
|
||||
outputPath = path.join(ROOT, hostConfig.hostSubdir, 'skills', externalName, 'sections', fileName);
|
||||
@@ -933,7 +979,7 @@ for (const currentHost of hostsToRun) {
|
||||
voice_line: catalogParts.voiceLine,
|
||||
};
|
||||
}
|
||||
const relOutput = path.relative(ROOT, outputPath);
|
||||
const relOutput = path.relative(OUT_DIR || ROOT, outputPath);
|
||||
|
||||
if (symlinkLoop) {
|
||||
console.log(`SKIPPED (symlink loop): ${relOutput}`);
|
||||
@@ -946,6 +992,9 @@ for (const currentHost of hostsToRun) {
|
||||
console.log(`FRESH: ${relOutput}`);
|
||||
}
|
||||
} else {
|
||||
// In-place writes land in existing dirs; --out-dir needs the mirrored
|
||||
// skill dir created first.
|
||||
if (OUT_DIR) fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
||||
fs.writeFileSync(outputPath, content);
|
||||
console.log(`GENERATED: ${relOutput}`);
|
||||
}
|
||||
@@ -982,7 +1031,7 @@ for (const currentHost of hostsToRun) {
|
||||
currentHostConfig.generation.skipSkills.includes(sec.skillDir)) continue;
|
||||
|
||||
const { outputPath, content } = processSectionTemplate(path.join(ROOT, sec.tmpl), sec.skillDir, currentHost);
|
||||
const relOutput = path.relative(ROOT, outputPath);
|
||||
const relOutput = path.relative(OUT_DIR || ROOT, outputPath);
|
||||
|
||||
if (DRY_RUN) {
|
||||
const existing = fs.existsSync(outputPath) ? fs.readFileSync(outputPath, 'utf-8') : '';
|
||||
@@ -1079,7 +1128,9 @@ The orchestrator will persist the plan link to its own memory/knowledge store.
|
||||
// No timestamp field — keeps the file content-deterministic across runs so
|
||||
// CI dry-run freshness checks don't flap on regen. If a per-run timestamp
|
||||
// is ever needed for debugging, write it to a separate `.gen-stamp` file.
|
||||
if (currentHost === 'claude' && CATALOG_MODE === 'trim' && Object.keys(proactiveAggregate).length > 0 && !DRY_RUN) {
|
||||
// Skip the global proactive-suggestions.json in --out-dir mode: it lives at
|
||||
// a repo path (scripts/) and the dev workspace render doesn't need it.
|
||||
if (currentHost === 'claude' && CATALOG_MODE === 'trim' && Object.keys(proactiveAggregate).length > 0 && !DRY_RUN && !OUT_DIR) {
|
||||
const proactivePath = path.join(ROOT, 'scripts', 'proactive-suggestions.json');
|
||||
// Sort keys alphabetically so the serialized JSON is identical across
|
||||
// machines regardless of filesystem-iteration order. Without this, CI
|
||||
|
||||
Reference in New Issue
Block a user