mirror of https://github.com/garrytan/gstack.git
Merge 8a5c998461 into 2beb636f7c
This commit is contained in:
commit
0f89264099
|
|
@ -23,8 +23,8 @@ triggers:
|
|||
## When to invoke this skill
|
||||
|
||||
Loads the most recent
|
||||
saved state (across all branches by default) so you can pick up where you
|
||||
left off — even across Conductor workspace handoffs.
|
||||
saved state (preferring the current branch, falling back across branches) so
|
||||
you can pick up where you left off — even across Conductor workspace handoffs.
|
||||
Use when asked to "resume", "restore context", "where was I", or
|
||||
"pick up where I left off". Pair with /context-save.
|
||||
Formerly /checkpoint resume — renamed because Claude Code treats /checkpoint
|
||||
|
|
@ -791,13 +791,19 @@ context and present it clearly so the user can resume work without losing a beat
|
|||
**HARD GATE:** Do NOT implement code changes. This skill only reads saved
|
||||
context files and presents the summary.
|
||||
|
||||
**Default: load the most recent saved context across ALL branches.** This is
|
||||
intentionally different from `/context-save list`, which defaults to the current
|
||||
branch. `/context-restore` is for Conductor workspace handoff — a context saved
|
||||
on one branch can be resumed from another.
|
||||
**Default: prefer the most recent checkpoint saved on the CURRENT branch; if
|
||||
this branch has none, fall back to the most recent across ALL branches.** The
|
||||
fallback is for Conductor workspace handoff — a context saved on one branch can
|
||||
be resumed from another. The current-branch preference exists because every
|
||||
worktree of a repo shares one checkpoints directory (same origin-derived slug),
|
||||
so without it `/context-restore` in one worktree could silently load a sibling
|
||||
worktree's newer checkpoint.
|
||||
|
||||
**Do NOT filter the candidate set by current branch.** The `list` flow does
|
||||
that; `/context-restore` does not.
|
||||
**Do NOT hard-filter the candidate set to the current branch** — other-branch
|
||||
checkpoints stay in the set as a fallback. They are just ordered *after* the
|
||||
current branch's own, so a current-branch save is never shadowed by a newer
|
||||
sibling-worktree save. (`/context-save list` is the flow that hard-scopes to one
|
||||
branch.)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -828,27 +834,53 @@ else
|
|||
# copies/rsync). Filesystem mtime drifts and is not authoritative.
|
||||
# 2. On macOS, `find ... | xargs ls -1t` with zero results falls back to
|
||||
# listing cwd. `sort -r` on empty input cleanly returns nothing.
|
||||
# Cap at 20 most recent: a user with 10k saved files shouldn't blow the
|
||||
# context window just listing them. /context-save list handles pagination.
|
||||
FILES=$(find "$CHECKPOINT_DIR" -maxdepth 1 -name "*.md" -type f 2>/dev/null | sort -r | head -20)
|
||||
if [ -z "$FILES" ]; then
|
||||
# Scan the 200 newest so a current-branch checkpoint sitting below a burst of
|
||||
# sibling-worktree saves can still be found; the result is capped at 20 below.
|
||||
ALL=$(find "$CHECKPOINT_DIR" -maxdepth 1 -name "*.md" -type f 2>/dev/null | sort -r | head -200)
|
||||
if [ -z "$ALL" ]; then
|
||||
echo "NO_CHECKPOINTS"
|
||||
else
|
||||
# Order current-branch checkpoints first, other branches after. A git branch
|
||||
# is checked out in at most one worktree, and all worktrees of a repo share
|
||||
# one checkpoints dir (same origin-derived slug), so without this preference
|
||||
# `/context-restore` in worktree A could load worktree B's newer checkpoint.
|
||||
# Cross-branch resume (Conductor handoff) is preserved as the fallback: when
|
||||
# the current branch has no checkpoint, the full newest-first set is used.
|
||||
# CURRENT_BRANCH may be pre-set (tests); otherwise resolve it from git.
|
||||
: "${CURRENT_BRANCH:=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)}"
|
||||
SAME=""; OTHER=""
|
||||
while IFS= read -r f; do
|
||||
[ -n "$f" ] || continue
|
||||
b=$(grep -m1 '^branch:' "$f" 2>/dev/null | sed 's/^branch:[[:space:]]*//')
|
||||
if [ -n "$CURRENT_BRANCH" ] && [ "$b" = "$CURRENT_BRANCH" ]; then
|
||||
SAME="${SAME}${f}
|
||||
"
|
||||
else
|
||||
OTHER="${OTHER}${f}
|
||||
"
|
||||
fi
|
||||
done <<EOF
|
||||
$ALL
|
||||
EOF
|
||||
# Cap at 20: a user with 10k saved files shouldn't blow the context window.
|
||||
FILES=$(printf '%s%s' "$SAME" "$OTHER" | grep -v '^[[:space:]]*$' | head -20)
|
||||
echo "$FILES"
|
||||
fi
|
||||
fi
|
||||
```
|
||||
|
||||
**Candidates include every `.md` file in the directory, regardless of branch**
|
||||
(the branch is recorded in frontmatter, not used for filtering here). This
|
||||
enables Conductor workspace handoff.
|
||||
**Candidates include every `.md` file in the directory**, but they are ordered
|
||||
**current-branch-first** (the branch is read from each file's `branch:`
|
||||
frontmatter). Other-branch files stay in the set as a fallback, which preserves
|
||||
Conductor workspace handoff when the current branch has no checkpoint of its own.
|
||||
|
||||
### Step 2: Load the right file
|
||||
|
||||
- If the user specified a title fragment or number: find the matching file among
|
||||
the candidates.
|
||||
- Otherwise: load the **first file returned by the `sort -r` above** — that is
|
||||
the newest `YYYYMMDD-HHMMSS` prefix, which is the canonical "most recent."
|
||||
- Otherwise: load the **first file returned by Step 1 above** — that is the
|
||||
newest `YYYYMMDD-HHMMSS` checkpoint for the current branch, or, if the current
|
||||
branch has none, the newest across all branches.
|
||||
|
||||
Read the chosen file and present a summary:
|
||||
|
||||
|
|
@ -900,9 +932,10 @@ state, then `/context-restore` will find it."
|
|||
## Important Rules
|
||||
|
||||
- **Never modify code.** This skill only reads saved files and presents them.
|
||||
- **Always search across all branches by default.** Cross-branch resume is the
|
||||
whole point. Only filter by branch if the user explicitly asks via a
|
||||
title-fragment match that happens to be branch-specific.
|
||||
- **Prefer the current branch's own checkpoint, but keep all branches in the
|
||||
fallback set.** Cross-branch resume (Conductor handoff) still works when the
|
||||
current branch has no checkpoint; it just no longer lets a sibling worktree's
|
||||
newer save shadow this branch's own.
|
||||
- **"Most recent" means the filename `YYYYMMDD-HHMMSS` prefix**, not
|
||||
`ls -1t` (filesystem mtime). Filenames are stable across file-system
|
||||
operations; mtime is not.
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ preamble-tier: 2
|
|||
version: 1.0.0
|
||||
description: |
|
||||
Restore working context saved earlier by /context-save. Loads the most recent
|
||||
saved state (across all branches by default) so you can pick up where you
|
||||
left off — even across Conductor workspace handoffs.
|
||||
saved state (preferring the current branch, falling back across branches) so
|
||||
you can pick up where you left off — even across Conductor workspace handoffs.
|
||||
Use when asked to "resume", "restore context", "where was I", or
|
||||
"pick up where I left off". Pair with /context-save.
|
||||
Formerly /checkpoint resume — renamed because Claude Code treats /checkpoint
|
||||
|
|
@ -35,13 +35,19 @@ context and present it clearly so the user can resume work without losing a beat
|
|||
**HARD GATE:** Do NOT implement code changes. This skill only reads saved
|
||||
context files and presents the summary.
|
||||
|
||||
**Default: load the most recent saved context across ALL branches.** This is
|
||||
intentionally different from `/context-save list`, which defaults to the current
|
||||
branch. `/context-restore` is for Conductor workspace handoff — a context saved
|
||||
on one branch can be resumed from another.
|
||||
**Default: prefer the most recent checkpoint saved on the CURRENT branch; if
|
||||
this branch has none, fall back to the most recent across ALL branches.** The
|
||||
fallback is for Conductor workspace handoff — a context saved on one branch can
|
||||
be resumed from another. The current-branch preference exists because every
|
||||
worktree of a repo shares one checkpoints directory (same origin-derived slug),
|
||||
so without it `/context-restore` in one worktree could silently load a sibling
|
||||
worktree's newer checkpoint.
|
||||
|
||||
**Do NOT filter the candidate set by current branch.** The `list` flow does
|
||||
that; `/context-restore` does not.
|
||||
**Do NOT hard-filter the candidate set to the current branch** — other-branch
|
||||
checkpoints stay in the set as a fallback. They are just ordered *after* the
|
||||
current branch's own, so a current-branch save is never shadowed by a newer
|
||||
sibling-worktree save. (`/context-save list` is the flow that hard-scopes to one
|
||||
branch.)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -72,27 +78,53 @@ else
|
|||
# copies/rsync). Filesystem mtime drifts and is not authoritative.
|
||||
# 2. On macOS, `find ... | xargs ls -1t` with zero results falls back to
|
||||
# listing cwd. `sort -r` on empty input cleanly returns nothing.
|
||||
# Cap at 20 most recent: a user with 10k saved files shouldn't blow the
|
||||
# context window just listing them. /context-save list handles pagination.
|
||||
FILES=$(find "$CHECKPOINT_DIR" -maxdepth 1 -name "*.md" -type f 2>/dev/null | sort -r | head -20)
|
||||
if [ -z "$FILES" ]; then
|
||||
# Scan the 200 newest so a current-branch checkpoint sitting below a burst of
|
||||
# sibling-worktree saves can still be found; the result is capped at 20 below.
|
||||
ALL=$(find "$CHECKPOINT_DIR" -maxdepth 1 -name "*.md" -type f 2>/dev/null | sort -r | head -200)
|
||||
if [ -z "$ALL" ]; then
|
||||
echo "NO_CHECKPOINTS"
|
||||
else
|
||||
# Order current-branch checkpoints first, other branches after. A git branch
|
||||
# is checked out in at most one worktree, and all worktrees of a repo share
|
||||
# one checkpoints dir (same origin-derived slug), so without this preference
|
||||
# `/context-restore` in worktree A could load worktree B's newer checkpoint.
|
||||
# Cross-branch resume (Conductor handoff) is preserved as the fallback: when
|
||||
# the current branch has no checkpoint, the full newest-first set is used.
|
||||
# CURRENT_BRANCH may be pre-set (tests); otherwise resolve it from git.
|
||||
: "${CURRENT_BRANCH:=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)}"
|
||||
SAME=""; OTHER=""
|
||||
while IFS= read -r f; do
|
||||
[ -n "$f" ] || continue
|
||||
b=$(grep -m1 '^branch:' "$f" 2>/dev/null | sed 's/^branch:[[:space:]]*//')
|
||||
if [ -n "$CURRENT_BRANCH" ] && [ "$b" = "$CURRENT_BRANCH" ]; then
|
||||
SAME="${SAME}${f}
|
||||
"
|
||||
else
|
||||
OTHER="${OTHER}${f}
|
||||
"
|
||||
fi
|
||||
done <<EOF
|
||||
$ALL
|
||||
EOF
|
||||
# Cap at 20: a user with 10k saved files shouldn't blow the context window.
|
||||
FILES=$(printf '%s%s' "$SAME" "$OTHER" | grep -v '^[[:space:]]*$' | head -20)
|
||||
echo "$FILES"
|
||||
fi
|
||||
fi
|
||||
```
|
||||
|
||||
**Candidates include every `.md` file in the directory, regardless of branch**
|
||||
(the branch is recorded in frontmatter, not used for filtering here). This
|
||||
enables Conductor workspace handoff.
|
||||
**Candidates include every `.md` file in the directory**, but they are ordered
|
||||
**current-branch-first** (the branch is read from each file's `branch:`
|
||||
frontmatter). Other-branch files stay in the set as a fallback, which preserves
|
||||
Conductor workspace handoff when the current branch has no checkpoint of its own.
|
||||
|
||||
### Step 2: Load the right file
|
||||
|
||||
- If the user specified a title fragment or number: find the matching file among
|
||||
the candidates.
|
||||
- Otherwise: load the **first file returned by the `sort -r` above** — that is
|
||||
the newest `YYYYMMDD-HHMMSS` prefix, which is the canonical "most recent."
|
||||
- Otherwise: load the **first file returned by Step 1 above** — that is the
|
||||
newest `YYYYMMDD-HHMMSS` checkpoint for the current branch, or, if the current
|
||||
branch has none, the newest across all branches.
|
||||
|
||||
Read the chosen file and present a summary:
|
||||
|
||||
|
|
@ -144,9 +176,10 @@ state, then `/context-restore` will find it."
|
|||
## Important Rules
|
||||
|
||||
- **Never modify code.** This skill only reads saved files and presents them.
|
||||
- **Always search across all branches by default.** Cross-branch resume is the
|
||||
whole point. Only filter by branch if the user explicitly asks via a
|
||||
title-fragment match that happens to be branch-specific.
|
||||
- **Prefer the current branch's own checkpoint, but keep all branches in the
|
||||
fallback set.** Cross-branch resume (Conductor handoff) still works when the
|
||||
current branch has no checkpoint; it just no longer lets a sibling worktree's
|
||||
newer save shadow this branch's own.
|
||||
- **"Most recent" means the filename `YYYYMMDD-HHMMSS` prefix**, not
|
||||
`ls -1t` (filesystem mtime). Filenames are stable across file-system
|
||||
operations; mtime is not.
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
},
|
||||
"context-restore": {
|
||||
"lead": "Restore working context saved earlier by /context-save.",
|
||||
"routing": "Loads the most recent\nsaved state (across all branches by default) so you can pick up where you\nleft off — even across Conductor workspace handoffs.\nUse when asked to \"resume\", \"restore context\", \"where was I\", or\n\"pick up where I left off\". Pair with /context-save.\nFormerly /checkpoint resume — renamed because Claude Code treats /checkpoint\nas a native rewind alias in current environments.",
|
||||
"routing": "Loads the most recent\nsaved state (preferring the current branch, falling back across branches) so\nyou can pick up where you left off — even across Conductor workspace handoffs.\nUse when asked to \"resume\", \"restore context\", \"where was I\", or\n\"pick up where I left off\". Pair with /context-save.\nFormerly /checkpoint resume — renamed because Claude Code treats /checkpoint\nas a native rewind alias in current environments.",
|
||||
"voice_line": null
|
||||
},
|
||||
"context-save": {
|
||||
|
|
|
|||
|
|
@ -36,15 +36,33 @@ echo "TITLE_SLUG=$TITLE_SLUG"
|
|||
echo "FILE=$FILE"
|
||||
`;
|
||||
|
||||
// The exact find + sort + head used by context-restore/SKILL.md Step 1.
|
||||
// The exact selection used by context-restore/SKILL.md Step 1: scan newest 200,
|
||||
// order current-branch checkpoints first (fallback: all branches), cap at 20.
|
||||
// CURRENT_BRANCH is injected via env in tests; the skill resolves it from git.
|
||||
const RESTORE_FIND_BASH = `
|
||||
if [ ! -d "$CHECKPOINT_DIR" ]; then
|
||||
echo "NO_CHECKPOINTS"
|
||||
else
|
||||
FILES=$(find "$CHECKPOINT_DIR" -maxdepth 1 -name "*.md" -type f 2>/dev/null | sort -r | head -20)
|
||||
if [ -z "$FILES" ]; then
|
||||
ALL=$(find "$CHECKPOINT_DIR" -maxdepth 1 -name "*.md" -type f 2>/dev/null | sort -r | head -200)
|
||||
if [ -z "$ALL" ]; then
|
||||
echo "NO_CHECKPOINTS"
|
||||
else
|
||||
: "\${CURRENT_BRANCH:=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)}"
|
||||
SAME=""; OTHER=""
|
||||
while IFS= read -r f; do
|
||||
[ -n "$f" ] || continue
|
||||
b=$(grep -m1 '^branch:' "$f" 2>/dev/null | sed 's/^branch:[[:space:]]*//')
|
||||
if [ -n "$CURRENT_BRANCH" ] && [ "$b" = "$CURRENT_BRANCH" ]; then
|
||||
SAME="\${SAME}\${f}
|
||||
"
|
||||
else
|
||||
OTHER="\${OTHER}\${f}
|
||||
"
|
||||
fi
|
||||
done <<EOF
|
||||
$ALL
|
||||
EOF
|
||||
FILES=$(printf '%s%s' "$SAME" "$OTHER" | grep -v '^[[:space:]]*$' | head -20)
|
||||
echo "$FILES"
|
||||
fi
|
||||
fi
|
||||
|
|
@ -312,6 +330,73 @@ describe('context-restore: find + sort + head cap', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ─── Current-branch preference (#2052) ──────────────────────────────────────
|
||||
//
|
||||
// All worktrees of a repo share one origin-derived slug → one checkpoints dir.
|
||||
// Restore must prefer the CURRENT branch's own checkpoint so a sibling
|
||||
// worktree's newer save can't shadow it, while still falling back across
|
||||
// branches (Conductor handoff) when the current branch has none.
|
||||
|
||||
describe('context-restore: current-branch preference (#2052)', () => {
|
||||
let tmp: string;
|
||||
beforeEach(() => { tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ctx-branch-')); });
|
||||
afterEach(() => { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} });
|
||||
|
||||
function writeCheckpoint(ts: string, branch: string | null): string {
|
||||
const file = `${tmp}/${ts}-work.md`;
|
||||
const fm = branch === null
|
||||
? `---\nstatus: in-progress\n---\n`
|
||||
: `---\nstatus: in-progress\nbranch: ${branch}\n---\n`;
|
||||
fs.writeFileSync(file, fm);
|
||||
return file;
|
||||
}
|
||||
|
||||
function firstCandidate(currentBranch?: string): string {
|
||||
const env: Record<string, string> = { CHECKPOINT_DIR: tmp };
|
||||
if (currentBranch !== undefined) env.CURRENT_BRANCH = currentBranch;
|
||||
const out = runBash(RESTORE_FIND_BASH, env).stdout;
|
||||
return out.trim().split('\n').filter(Boolean)[0] ?? '';
|
||||
}
|
||||
|
||||
test('the bug: current-branch save is NOT shadowed by a newer sibling-worktree save', () => {
|
||||
const mine = writeCheckpoint('20260101-120000', 'feature-a'); // older, my branch
|
||||
writeCheckpoint('20260619-120000', 'feature-b'); // newer, sibling worktree
|
||||
// On feature-a, restore must load feature-a's own (older) checkpoint.
|
||||
expect(firstCandidate('feature-a')).toBe(mine);
|
||||
});
|
||||
|
||||
test('fallback: current branch has no checkpoint → newest across all branches (Conductor handoff)', () => {
|
||||
writeCheckpoint('20260101-120000', 'feature-a');
|
||||
const newest = writeCheckpoint('20260619-120000', 'feature-b');
|
||||
// On feature-c (no own checkpoint), cross-branch resume still works.
|
||||
expect(firstCandidate('feature-c')).toBe(newest);
|
||||
});
|
||||
|
||||
test('back-compat: empty current branch (non-git) → newest across all', () => {
|
||||
writeCheckpoint('20260101-120000', 'feature-a');
|
||||
const newest = writeCheckpoint('20260619-120000', 'feature-b');
|
||||
expect(firstCandidate('')).toBe(newest);
|
||||
});
|
||||
|
||||
test('checkpoints without a branch frontmatter still rank as fallback, never lost', () => {
|
||||
const mine = writeCheckpoint('20260101-120000', 'feature-a');
|
||||
writeCheckpoint('20260301-120000', null); // legacy save, no branch field
|
||||
const out = runBash(RESTORE_FIND_BASH, { CHECKPOINT_DIR: tmp, CURRENT_BRANCH: 'feature-a' }).stdout;
|
||||
const lines = out.trim().split('\n').filter(Boolean);
|
||||
expect(lines[0]).toBe(mine); // current branch first
|
||||
expect(lines.length).toBe(2); // legacy file is still present
|
||||
});
|
||||
|
||||
test('within the current branch, ordering stays newest-first', () => {
|
||||
const older = writeCheckpoint('20260101-120000', 'feature-a');
|
||||
const newer = writeCheckpoint('20260619-120000', 'feature-a');
|
||||
const out = runBash(RESTORE_FIND_BASH, { CHECKPOINT_DIR: tmp, CURRENT_BRANCH: 'feature-a' }).stdout;
|
||||
const lines = out.trim().split('\n').filter(Boolean);
|
||||
expect(lines[0]).toBe(newer);
|
||||
expect(lines[1]).toBe(older);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Migration HOME guard ──────────────────────────────────────────────────
|
||||
|
||||
describe('migration v1.1.3.0: HOME guard', () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue