merge: incorporate origin/main into community-mode branch

Conflicts resolved:
- VERSION: keep 0.14.0.0 (our branch > main's 0.13.3.0)
- package.json: same version resolution
- CHANGELOG.md: keep both entries, 0.14.0.0 above 0.13.3.0
- .gitignore: merge both sides (our bun.lock + main's env patterns)

Main brought in v0.13.3.0 "Lock It Down": pinned dependencies via
bun.lock, gstack-slug non-git fallback, setup CI timeout, Windows
lockfile fix, design doc discovery fix, autoplan sequential voices,
community PR guardrails in CLAUDE.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-03-28 10:34:17 -07:00
14 changed files with 317 additions and 41 deletions
+23 -5
View File
@@ -2097,7 +2097,7 @@ Source: [OpenAI "Designing Delightful Frontends with GPT-5.4"](https://developer
const GENERATED_HEADER = `<!-- AUTO-GENERATED from {{SOURCE}} — do not edit directly -->\n<!-- Regenerate: bun run gen:skill-docs -->\n`;
function processTemplate(tmplPath: string, host: Host = 'claude'): { outputPath: string; content: string } {
function processTemplate(tmplPath: string, host: Host = 'claude'): { outputPath: string; content: string; symlinkLoop?: boolean } {
const tmplContent = fs.readFileSync(tmplPath, 'utf-8');
const relTmplPath = path.relative(ROOT, tmplPath);
let outputPath = tmplPath.replace(/\.tmpl$/, '');
@@ -2108,11 +2108,27 @@ function processTemplate(tmplPath: string, host: Host = 'claude'): { outputPath:
let outputDir: string | null = null;
// For codex host, route output to .agents/skills/{codexSkillName}/SKILL.md
let symlinkLoop = false;
if (host === 'codex') {
const codexName = codexSkillName(skillDir === '.' ? '' : skillDir);
outputDir = path.join(ROOT, '.agents', 'skills', codexName);
fs.mkdirSync(outputDir, { recursive: true });
outputPath = path.join(outputDir, 'SKILL.md');
// Guard against symlink loops: if .agents/skills/gstack → repo root,
// writing to .agents/skills/gstack/SKILL.md would overwrite the Claude version.
// Skip the write entirely for this skill — the codex content is still generated
// for token budget tracking.
const claudePath = tmplPath.replace(/\.tmpl$/, '');
try {
const resolvedClaude = fs.realpathSync(claudePath);
const resolvedCodex = fs.realpathSync(path.dirname(outputPath)) + '/' + path.basename(outputPath);
if (resolvedClaude === resolvedCodex) {
symlinkLoop = true;
}
} catch {
// realpathSync fails if file doesn't exist yet — that's fine, no symlink loop
}
}
// Extract skill name from frontmatter for TemplateContext
@@ -2166,7 +2182,7 @@ function processTemplate(tmplPath: string, host: Host = 'claude'): { outputPath:
content = content.replace(/~\/\.claude\/plans/g, '~/.codex/plans');
content = content.replace(/~\/\.claude\//g, '~/.codex/');
if (outputDir) {
if (outputDir && !symlinkLoop) {
const codexName = codexSkillName(skillDir === '.' ? '' : skillDir);
const agentsDir = path.join(outputDir, 'agents');
fs.mkdirSync(agentsDir, { recursive: true });
@@ -2186,7 +2202,7 @@ function processTemplate(tmplPath: string, host: Host = 'claude'): { outputPath:
content = header + content;
}
return { outputPath, content };
return { outputPath, content, symlinkLoop };
}
// ─── Main ───────────────────────────────────────────────────
@@ -2205,10 +2221,12 @@ for (const tmplPath of findTemplates()) {
if (dir === 'codex') continue;
}
const { outputPath, content } = processTemplate(tmplPath, HOST);
const { outputPath, content, symlinkLoop } = processTemplate(tmplPath, HOST);
const relOutput = path.relative(ROOT, outputPath);
if (DRY_RUN) {
if (symlinkLoop) {
console.log(`SKIPPED (symlink loop): ${relOutput}`);
} else if (DRY_RUN) {
const existing = fs.existsSync(outputPath) ? fs.readFileSync(outputPath, 'utf-8') : '';
if (existing !== content) {
console.log(`STALE: ${relOutput}`);