fix: resolve merge conflicts with origin/main (v0.4.3 + v0.4.4)

Merge origin/main which added:
- v0.4.3: /document-release skill, simplified AskUserQuestion (always ELI16),
  runtime branch detection via _BRANCH preamble variable
- v0.4.4: 60-min update check TTL, --force flag for gstack-update-check

Resolved conflicts:
- VERSION: keep 0.6.0 (our version, above 0.4.4)
- CHANGELOG: all entries preserved (0.6.0 > 0.4.4 > 0.4.3 > 0.4.2)
- TODOS.md: keep both sections (Brainstorm/Design + Document-Release)
- test/skill-validation.test.ts: include all three new skills in arrays
  (brainstorm, debug, document-release)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-03-16 17:44:33 -07:00
28 changed files with 1229 additions and 86 deletions

View File

@@ -26,6 +26,8 @@ touch ~/.gstack/sessions/"$PPID"
_SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr -d ' ')
find ~/.gstack/sessions -mmin +120 -type f -delete 2>/dev/null || true
_CONTRIB=$(~/.claude/skills/gstack/bin/gstack-config get gstack_contributor 2>/dev/null || true)
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
echo "BRANCH: $_BRANCH"
```
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
@@ -33,12 +35,12 @@ If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/g
## AskUserQuestion Format
**ALWAYS follow this structure for every AskUserQuestion call:**
1. Context: project name, current branch, what we're working on (1-2 sentences)
2. The specific question or decision point
3. `RECOMMENDATION: Choose [X] because [one-line reason]`
4. Lettered options: `A) ... B) ... C) ...`
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
2. **Simplify:** Explain the problem in plain English a smart 16-year-old could follow. No raw function names, no internal jargon, no implementation details. Use concrete examples and analogies. Say what it DOES, not what it's called.
3. **Recommend:** `RECOMMENDATION: Choose [X] because [one-line reason]`
4. **Options:** Lettered options: `A) ... B) ... C) ...`
If `_SESSIONS` is 3 or more: the user is juggling multiple gstack sessions and context-switching heavily. **ELI16 mode** — they may not remember what this conversation is about. Every AskUserQuestion MUST re-ground them: state the project, the branch, the current plan/task, then the specific problem, THEN the recommendation and options. Be extra clear and self-contained — assume they haven't looked at this window in 20 minutes.
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
Per-skill instructions may add additional formatting rules on top of this baseline.

View File

@@ -7,7 +7,7 @@
*/
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import { mkdtempSync, writeFileSync, rmSync, existsSync, readFileSync, mkdirSync, symlinkSync } from 'fs';
import { mkdtempSync, writeFileSync, rmSync, existsSync, readFileSync, mkdirSync, symlinkSync, utimesSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
@@ -16,8 +16,8 @@ const SCRIPT = join(import.meta.dir, '..', '..', 'bin', 'gstack-update-check');
let gstackDir: string;
let stateDir: string;
function run(extraEnv: Record<string, string> = {}) {
const result = Bun.spawnSync(['bash', SCRIPT], {
function run(extraEnv: Record<string, string> = {}, args: string[] = []) {
const result = Bun.spawnSync(['bash', SCRIPT, ...args], {
env: {
...process.env,
GSTACK_DIR: gstackDir,
@@ -412,4 +412,56 @@ describe('gstack-update-check', () => {
expect(exitCode).toBe(0);
expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0');
});
// ─── --force flag tests ──────────────────────────────────────
test('--force busts fresh UP_TO_DATE cache', () => {
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n');
writeFileSync(join(stateDir, 'last-update-check'), 'UP_TO_DATE 0.3.3');
// Without --force: cache hit, silent
const cached = run();
expect(cached.stdout).toBe('');
// With --force: cache busted, re-fetches, finds upgrade
const forced = run({}, ['--force']);
expect(forced.exitCode).toBe(0);
expect(forced.stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0');
});
test('--force busts fresh UPGRADE_AVAILABLE cache', () => {
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.3.3\n');
writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0');
// Without --force: cache hit, outputs stale upgrade
const cached = run();
expect(cached.stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0');
// With --force: cache busted, re-fetches, now up to date
const forced = run({}, ['--force']);
expect(forced.exitCode).toBe(0);
expect(forced.stdout).toBe('');
const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8');
expect(cache).toContain('UP_TO_DATE');
});
// ─── Split TTL tests ─────────────────────────────────────────
test('UP_TO_DATE cache expires after 60 min (not 720)', () => {
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n');
writeFileSync(join(stateDir, 'last-update-check'), 'UP_TO_DATE 0.3.3');
// Set cache mtime to 90 minutes ago (past 60-min TTL)
const ninetyMinAgo = new Date(Date.now() - 90 * 60 * 1000);
const cachePath = join(stateDir, 'last-update-check');
utimesSync(cachePath, ninetyMinAgo, ninetyMinAgo);
// Cache should be stale at 60-min TTL, re-fetches and finds upgrade
const { exitCode, stdout } = run();
expect(exitCode).toBe(0);
expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0');
});
});