mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 11:45:20 +02:00
9dbaf906cf
* feat(gbrain-sync): queue primitives + writer shims
Adds bin/gstack-brain-enqueue (atomic append to sync queue) and
bin/gstack-jsonl-merge (git merge driver, ts-sort with SHA-256 fallback).
Wires one backgrounded enqueue call into learnings-log, timeline-log,
review-log, and developer-profile --migrate. question-log and
question-preferences stay local per Codex v2 decision.
gstack-config gains gbrain_sync_mode (off/artifacts-only/full) and
gbrain_sync_mode_prompted keys, plus GSTACK_HOME env alignment so
tests don't leak into real ~/.gstack/config.yaml.
* feat(gbrain-sync): --once drain + secret scan + push
bin/gstack-brain-sync is the core sync binary. Subcommands: --once
(drain queue, allowlist-filter, privacy-class-filter, secret-scan
staged diff, commit with template, push with fetch+merge retry),
--status, --skip-file <path>, --drop-queue --yes, --discover-new
(cursor-based detection of artifact writes that skip the shim).
Secret regex families: AWS keys, GitHub tokens (ghp_/gho_/ghu_/ghs_/
ghr_/github_pat_), OpenAI sk-, PEM blocks, JWTs, bearer-token-in-JSON.
On hit: unstage, preserve queue, print remediation hint (--skip-file
or edit), exit clean. No daemon — invoked by preamble at skill
boundaries.
* feat(gbrain-sync): init, restore, uninstall, consumer registry
bin/gstack-brain-init: idempotent first-run. git init ~/.gstack/,
.gitignore=*, canonical .brain-allowlist + .brain-privacy-map.json,
pre-commit secret-scan hook (defense-in-depth), merge driver registration
via git config, gh repo create --private OR arbitrary --remote <url>,
initial push, ~/.gstack-brain-remote.txt for new-machine discovery,
GBrain consumer registration via HTTP POST.
bin/gstack-brain-restore: safe new-machine bootstrap. Refuses clobber
of existing allowlisted files, clones to staging, rsync-copies tracked
files, re-registers merge drivers (required — not cloned from remote),
rehydrates consumers.json, prompts for per-consumer tokens.
bin/gstack-brain-uninstall: clean off-ramp. Removes .git + .brain-*
files + consumers.json + config keys. Preserves user data (learnings,
plans, retros, profile). Optional --delete-remote for GitHub repos.
bin/gstack-brain-consumer + bin/gstack-brain-reader (symlink alias):
registry management. Internal 'consumer' term; user-facing 'reader'
per DX review decision.
* feat(gbrain-sync): preamble block — privacy gate + boundary sync
scripts/resolvers/preamble/generate-brain-sync-block.ts emits bash that
runs at every skill invocation:
- Detects ~/.gstack-brain-remote.txt on machines without local .git
and surfaces a restore-available hint (does NOT auto-run restore).
- Runs gstack-brain-sync --once at skill start to drain any pending
writes (and at skill end via prose instruction).
- Once-per-day auto-pull (cached via .brain-last-pull) for append-only
JSONL files.
- Emits BRAIN_SYNC: status line every skill run.
Also emits prose for the host LLM to fire the one-time privacy
stop-gate (full / artifacts-only / off) when gbrain is detected and
gbrain_sync_mode_prompted is false. Wired into preamble.ts composition.
* test(gbrain-sync): 27-test consolidated suite
test/brain-sync.test.ts covers:
- Config: validation, defaults, GSTACK_HOME env isolation
- Enqueue: no-op gates, skip list, concurrent atomicity, JSON escape
- JSONL merge driver: 3-way + ts-sort + SHA-256 fallback
- Init + sync: canonical file creation, merge driver registration,
push-reject + fetch+merge retry path
- Init refuses different remote (idempotency)
- Cross-machine restore round-trip (machine A write → machine B sees)
- Secret scan across all 6 regex families (AWS, GH, OpenAI, PEM, JWT,
bearer-JSON). --skip-file unblock remediation
- Uninstall removes sync config, preserves user data
- --discover-new idempotence via mtime+size cursor
Behaviors verified via integration smokes during implementation. Known
follow-up: bun-test 5s default timeout needs 30s wrapper for
spawnSync-heavy tests.
* docs(gbrain-sync): user guide + error lookup + README section
docs/gbrain-sync.md: setup walkthrough, privacy modes, cross-machine
workflow, secret protection, two-machine conflict handling, uninstall,
troubleshooting reference.
docs/gbrain-sync-errors.md: problem/cause/fix index for every
user-visible error. Patterned on Rust's error docs + Stripe's API
error reference.
README.md: 'Cross-machine memory with GBrain sync' section near the
top (discovery moment), plus docs-table entry.
* chore: bump version and changelog (v1.7.0.0)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: regenerate SKILL.md files for gbrain-sync preamble block
Re-runs bun run gen:skill-docs after adding generateBrainSyncBlock
to scripts/resolvers/preamble.ts in a2aa8a07. CI check-freshness
caught the drift. All 36 SKILL.md files regenerated with the new
skill-start bash block + privacy-gate prose + skill-end sync
instructions baked in.
* fix(test): session-awareness reads AskUserQuestion Format from a Tier 2+ SKILL.md
The test was reading ROOT/SKILL.md (browse skill, Tier 1) which never
contained '## AskUserQuestion Format' — that section is only emitted
for Tier 2+ skills by scripts/resolvers/preamble.ts. As a result the
agent was prompted with an empty format guide and only emitted
'RECOMMENDATION' intermittently, making the test flaky.
Pre-existing on main (same ROOT/SKILL.md shape there) — surfaced now
because the agent run didn't hit the RECOMMENDATION/recommend/option a
fallback strings in this particular attempt.
Fix: read from office-hours/SKILL.md (Tier 3, always has the section)
with a fallback that scans for the first top-level skill dir whose
SKILL.md contains the header. Future template moves won't break this
test again.
* chore: bump to v1.9.0.0 for gbrain-sync landing
Changes just the VERSION + package.json + CHANGELOG header (1.7.0.0 → 1.9.0.0
and date 2026-04-22 → 2026-04-23). No code changes. User call: land gbrain-sync
as a bigger-signal release above main's 1.6.4.0, skipping 1.8.0.0.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
272 lines
9.0 KiB
Cheetah
272 lines
9.0 KiB
Cheetah
---
|
|
name: context-save
|
|
preamble-tier: 2
|
|
version: 1.0.0
|
|
description: |
|
|
Save working context. Captures git state, decisions made, and remaining work
|
|
so any future session can pick up without losing a beat.
|
|
Use when asked to "save progress", "save state", "context save", or
|
|
"save my work". Pair with /context-restore to resume later.
|
|
Formerly /checkpoint — renamed because Claude Code treats /checkpoint as a
|
|
native rewind alias in current environments, which was shadowing this skill.
|
|
(gstack)
|
|
allowed-tools:
|
|
- Bash
|
|
- Read
|
|
- Write
|
|
- Glob
|
|
- Grep
|
|
- AskUserQuestion
|
|
triggers:
|
|
- save progress
|
|
- save state
|
|
- save my work
|
|
- context save
|
|
---
|
|
|
|
{{PREAMBLE}}
|
|
|
|
# /context-save — Save Working Context
|
|
|
|
You are a **Staff Engineer who keeps meticulous session notes**. Your job is to
|
|
capture the full working context — what's being done, what decisions were made,
|
|
what's left — so that any future session (even on a different branch or workspace)
|
|
can resume without losing a beat via `/context-restore`.
|
|
|
|
**HARD GATE:** Do NOT implement code changes. This skill captures state only.
|
|
|
|
---
|
|
|
|
## Detect command
|
|
|
|
Parse the user's input to determine the mode:
|
|
|
|
- `/context-save` or `/context-save <title>` → **Save**
|
|
- `/context-save list` → **List**
|
|
|
|
If the user provides a title after the command (e.g., `/context-save auth refactor`),
|
|
use it as the title. Otherwise, infer a title from the current work.
|
|
|
|
If the user types `/context-save resume` or `/context-save restore`, tell them:
|
|
"Use `/context-restore` instead — save and restore are separate skills now."
|
|
|
|
---
|
|
|
|
## Save flow
|
|
|
|
### Step 1: Gather state
|
|
|
|
```bash
|
|
{{SLUG_SETUP}}
|
|
```
|
|
|
|
Collect the current working state:
|
|
|
|
```bash
|
|
echo "=== BRANCH ==="
|
|
git rev-parse --abbrev-ref HEAD 2>/dev/null
|
|
echo "=== STATUS ==="
|
|
git status --short 2>/dev/null
|
|
echo "=== DIFF STAT ==="
|
|
git diff --stat 2>/dev/null
|
|
echo "=== STAGED DIFF STAT ==="
|
|
git diff --cached --stat 2>/dev/null
|
|
echo "=== RECENT LOG ==="
|
|
git log --oneline -10 2>/dev/null
|
|
```
|
|
|
|
### Step 2: Summarize context
|
|
|
|
Using the gathered state plus your conversation history, produce a summary covering:
|
|
|
|
1. **What's being worked on** — the high-level goal or feature
|
|
2. **Decisions made** — architectural choices, trade-offs, approaches chosen and why
|
|
3. **Remaining work** — concrete next steps, in priority order
|
|
4. **Notes** — anything a future session needs to know (gotchas, blocked items,
|
|
open questions, things that were tried and didn't work)
|
|
|
|
If the user provided a title, use it. Otherwise, infer a concise title (3-6 words)
|
|
from the work being done.
|
|
|
|
### Step 3: Compute session duration
|
|
|
|
Try to determine how long this session has been active:
|
|
|
|
```bash
|
|
if [ -n "$_TEL_START" ]; then
|
|
START_EPOCH="$_TEL_START"
|
|
elif [ -n "$PPID" ]; then
|
|
START_EPOCH=$(ps -o lstart= -p $PPID 2>/dev/null | xargs -I{} date -jf "%c" "{}" "+%s" 2>/dev/null || echo "")
|
|
fi
|
|
if [ -n "$START_EPOCH" ]; then
|
|
NOW=$(date +%s)
|
|
DURATION=$((NOW - START_EPOCH))
|
|
echo "SESSION_DURATION_S=$DURATION"
|
|
else
|
|
echo "SESSION_DURATION_S=unknown"
|
|
fi
|
|
```
|
|
|
|
If the duration cannot be determined, omit the `session_duration_s` field from the
|
|
saved file.
|
|
|
|
### Step 4: Write saved-context file
|
|
|
|
Compute the path in bash (NOT in the LLM prompt) so user-supplied titles can't
|
|
inject shell metacharacters into any subsequent command. The sanitizer is an
|
|
allowlist: only `a-z 0-9 - .` survive.
|
|
|
|
```bash
|
|
{{SLUG_SETUP}}
|
|
CHECKPOINT_DIR="${GSTACK_HOME:-$HOME/.gstack}/projects/$SLUG/checkpoints"
|
|
mkdir -p "$CHECKPOINT_DIR"
|
|
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
|
# Bash-side title sanitize. Pass the raw title as $1 when running this block.
|
|
# Example: TITLE_RAW="wintermute progress" bash -c '...'
|
|
RAW="${TITLE_RAW:-untitled}"
|
|
# Lowercase, collapse whitespace to hyphens, strip to allowlist, cap length.
|
|
TITLE_SLUG=$(printf '%s' "$RAW" | tr '[:upper:]' '[:lower:]' | tr -s ' \t' '-' | tr -cd 'a-z0-9.-' | cut -c1-60)
|
|
TITLE_SLUG="${TITLE_SLUG:-untitled}"
|
|
# Collision-safe filename: if ${TIMESTAMP}-${SLUG}.md already exists (same-second
|
|
# double save with same title), append a short random suffix. Filenames are
|
|
# append-only — never overwrite.
|
|
FILE="${CHECKPOINT_DIR}/${TIMESTAMP}-${TITLE_SLUG}.md"
|
|
if [ -e "$FILE" ]; then
|
|
SUFFIX=$(LC_ALL=C tr -dc 'a-z0-9' < /dev/urandom 2>/dev/null | head -c 4 || printf '%04x' "$$")
|
|
FILE="${CHECKPOINT_DIR}/${TIMESTAMP}-${TITLE_SLUG}-${SUFFIX}.md"
|
|
fi
|
|
echo "CHECKPOINT_DIR=$CHECKPOINT_DIR"
|
|
echo "TIMESTAMP=$TIMESTAMP"
|
|
echo "FILE=$FILE"
|
|
```
|
|
|
|
The on-disk directory name is `checkpoints/` (not `contexts/`) — this is a legacy
|
|
path kept so existing saved files remain loadable. Users never see it.
|
|
|
|
Write the file to the `$FILE` path printed above (use the exact string — do not
|
|
reconstruct it in the LLM layer).
|
|
|
|
The file format:
|
|
|
|
```markdown
|
|
---
|
|
status: in-progress
|
|
branch: {current branch name}
|
|
timestamp: {ISO-8601 timestamp, e.g. 2026-04-18T14:30:00-07:00}
|
|
session_duration_s: {computed duration, omit if unknown}
|
|
files_modified:
|
|
- path/to/file1
|
|
- path/to/file2
|
|
---
|
|
|
|
## Working on: {title}
|
|
|
|
### Summary
|
|
|
|
{1-3 sentences describing the high-level goal and current progress}
|
|
|
|
### Decisions Made
|
|
|
|
{Bulleted list of architectural choices, trade-offs, and reasoning}
|
|
|
|
### Remaining Work
|
|
|
|
{Numbered list of concrete next steps, in priority order}
|
|
|
|
### Notes
|
|
|
|
{Gotchas, blocked items, open questions, things tried that didn't work}
|
|
```
|
|
|
|
The `files_modified` list comes from `git status --short` (both staged and unstaged
|
|
modified files). Use relative paths from the repo root.
|
|
|
|
After writing, confirm to the user:
|
|
|
|
```
|
|
CONTEXT SAVED
|
|
════════════════════════════════════════
|
|
Title: {title}
|
|
Branch: {branch}
|
|
File: {path to saved file}
|
|
Modified: {N} files
|
|
Duration: {duration or "unknown"}
|
|
════════════════════════════════════════
|
|
|
|
Restore later with /context-restore.
|
|
```
|
|
|
|
---
|
|
|
|
## List flow
|
|
|
|
### Step 1: Gather saved contexts
|
|
|
|
```bash
|
|
{{SLUG_SETUP}}
|
|
CHECKPOINT_DIR="${GSTACK_HOME:-$HOME/.gstack}/projects/$SLUG/checkpoints"
|
|
if [ -d "$CHECKPOINT_DIR" ]; then
|
|
echo "CHECKPOINT_DIR=$CHECKPOINT_DIR"
|
|
# Use find + sort instead of ls -1t: filename YYYYMMDD-HHMMSS prefix is the
|
|
# canonical order (stable across copies/rsync; mtime is not), and empty-result
|
|
# behavior is clean (no files → no output, no "lists cwd" fallback).
|
|
find "$CHECKPOINT_DIR" -maxdepth 1 -name "*.md" -type f 2>/dev/null | sort -r
|
|
else
|
|
echo "NO_CHECKPOINTS"
|
|
fi
|
|
```
|
|
|
|
### Step 2: Display table
|
|
|
|
**Default behavior:** Show saved contexts for the **current branch** only.
|
|
|
|
If the user passes `--all` (e.g., `/context-save list --all`), show contexts
|
|
from **all branches**.
|
|
|
|
Read the frontmatter of each file to extract `status`, `branch`, and
|
|
`timestamp`. Parse the title from the filename (the part after the timestamp).
|
|
|
|
Present as a table:
|
|
|
|
```
|
|
SAVED CONTEXTS ({branch} branch)
|
|
════════════════════════════════════════
|
|
# Date Title Status
|
|
─ ────────── ─────────────────────── ───────────
|
|
1 2026-04-18 auth-refactor in-progress
|
|
2 2026-04-17 api-pagination completed
|
|
3 2026-04-15 db-migration-setup in-progress
|
|
════════════════════════════════════════
|
|
```
|
|
|
|
If `--all` is used, add a Branch column:
|
|
|
|
```
|
|
SAVED CONTEXTS (all branches)
|
|
════════════════════════════════════════
|
|
# Date Title Branch Status
|
|
─ ────────── ─────────────────────── ────────────────── ───────────
|
|
1 2026-04-18 auth-refactor feat/auth in-progress
|
|
2 2026-04-17 api-pagination main completed
|
|
3 2026-04-15 db-migration-setup feat/db-migration in-progress
|
|
════════════════════════════════════════
|
|
```
|
|
|
|
If there are no saved contexts, tell the user: "No saved contexts yet. Run
|
|
`/context-save` to save your current working state."
|
|
|
|
---
|
|
|
|
## Important Rules
|
|
|
|
- **Never modify code.** This skill only reads state and writes the context file.
|
|
- **Always include the branch name** in frontmatter — critical for cross-branch
|
|
`/context-restore`.
|
|
- **Saved files are append-only.** Never overwrite or delete existing files. Each
|
|
save creates a new file.
|
|
- **Infer, don't interrogate.** Use git state and conversation context to fill in
|
|
the file. Only use AskUserQuestion if the title genuinely cannot be inferred.
|
|
- **This is a gstack skill, not a Claude Code built-in.** When the user types
|
|
`/context-save`, invoke this skill via the Skill tool. The old `/checkpoint`
|
|
name collided with Claude Code's native `/rewind` alias — the rename fixed that.
|