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}`);
+5 -2
View File
@@ -666,8 +666,11 @@ function generatePlanFileDiscovery(): string {
setopt +o nomatch 2>/dev/null || true # zsh compat
BRANCH=$(git branch --show-current 2>/dev/null | tr '/' '-')
REPO=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)")
# Search common plan file locations
for PLAN_DIR in "$HOME/.claude/plans" "$HOME/.codex/plans" ".gstack/plans"; do
# Compute project slug for ~/.gstack/projects/ lookup
_PLAN_SLUG=$(git remote get-url origin 2>/dev/null | sed 's|.*[:/]\\([^/]*/[^/]*\\)\\.git$|\\1|;s|.*[:/]\\([^/]*/[^/]*\\)$|\\1|' | tr '/' '-' | tr -cd 'a-zA-Z0-9._-') || true
_PLAN_SLUG="\${_PLAN_SLUG:-$(basename "$PWD" | tr -cd 'a-zA-Z0-9._-')}"
# Search common plan file locations (project designs first, then personal/local)
for PLAN_DIR in "$HOME/.gstack/projects/$_PLAN_SLUG" "$HOME/.claude/plans" "$HOME/.codex/plans" ".gstack/plans"; do
[ -d "$PLAN_DIR" ] || continue
PLAN=$(ls -t "$PLAN_DIR"/*.md 2>/dev/null | xargs grep -l "$BRANCH" 2>/dev/null | head -1)
[ -z "$PLAN" ] && PLAN=$(ls -t "$PLAN_DIR"/*.md 2>/dev/null | xargs grep -l "$REPO" 2>/dev/null | head -1)