mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-05 08:23:59 +01:00
fix(context-restore): head check passes the fence lint, reads frontmatter only, prefers worktree:, and overrides provenance sorting
- The block used a free-text placeholder, <the chosen file>, which the generated-bash lint (test/env-host-fence-conformance.test.ts) refuses on every host. It now uses the allowlisted <the FILE path printed above>, with prose naming the Step 2 choice. - head:, worktree: and project_root: are read from the leading frontmatter only (sed, no positional $N tokens Claude Code would rewrite), so a head: line in a checkpoint's notes no longer counts. - Outside git, the comparison uses the saved worktree: before project_root:, so a save from a nested linked worktree is not compared with the main tree. - The provenance rules now say they are skipped after HEAD_MOVED, HEAD_DIVERGED or HEAD_UNKNOWN, instead of contradicting the head check. - The re-stamp test checks the exit status and that the head moves. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
6e6b1520f7
commit
676a21376d
4 files changed
+44
-15
No files matched your search
+2
-2
@@ -6,10 +6,10 @@
|
||||
|
||||
### Fixed
|
||||
- **`bin/gstack-slug --stamp-checkpoint`** also writes `head:`, the commit the save describes, next to `remote:`, `project_root:` and `worktree:`. A re-stamp replaces it rather than adding a second line.
|
||||
- **`/context-restore`** compares `head:` with `HEAD` before presenting. When commits landed after the save it prints how many and lists up to ten, and puts every Remaining Work item under Verify first. A saved commit that is not in this checkout's history (saved on another branch, rebased, reset, or not fetched) is reported the same way. A malformed `head:` is checked as 40 or 64 hex digits before any git command sees it. Checkpoints saved before this keep their usual sorting with a one-line note.
|
||||
- **`/context-restore`** compares `head:` with `HEAD` before presenting. When commits landed after the save it prints how many and lists up to ten, and puts every Remaining Work item under Verify first. A saved commit that is not in this checkout's history (saved on another branch, rebased, reset, or not fetched) is reported the same way. A malformed `head:` is checked as 40 or 64 hex digits before any git command sees it, `head:` is read from the frontmatter only, and outside git the comparison uses the saved `worktree:` before `project_root:`. Checkpoints saved before this keep their usual sorting with a one-line note.
|
||||
|
||||
### Tests
|
||||
- `test/context-restore-head.test.ts` runs the real save-path, stamp and head-check blocks from the generated SKILL.md files in fixture repos: unchanged, two commits landed, diverged after a reset, resumed from another branch, pre-tracking, and a `head: $(touch pwned)` that must never execute.
|
||||
- `test/context-restore-head.test.ts` runs the real save-path, stamp and head-check blocks from the generated SKILL.md files in fixture repos: unchanged, two commits landed, diverged after a reset, resumed from another branch, a nested linked worktree restored from outside git, a re-stamp after a new commit, pre-tracking, a `head:` that appears only in the body, and a `head: $(touch pwned)` that must never execute.
|
||||
|
||||
## [1.91.186.0] - 2026-10-24
|
||||
|
||||
|
||||
@@ -529,13 +529,15 @@ checkpoint(s) from another project; only this project's are listed." If it print
|
||||
|
||||
**Check whether the branch moved since the save.** The stamp records the commit
|
||||
the save was made at in `head:`. Before presenting, compare it with this
|
||||
checkout:
|
||||
checkout. FILE is the checkpoint path chosen in Step 2, one of the paths Step 1
|
||||
printed:
|
||||
|
||||
```bash
|
||||
FILE="<the chosen file>"
|
||||
SAVED_HEAD=$(grep -m1 '^head:' "$FILE" 2>/dev/null | sed 's/^head:[[:space:]]*//' | tr -d '\r')
|
||||
FILE="<the FILE path printed above>"
|
||||
SAVED_HEAD=$(tr -d '\r' < "$FILE" 2>/dev/null | sed -n '1{/^---$/!q;d;}; /^---$/q; /^head:/{s/^head:[[:space:]]*//p;q;}')
|
||||
REPO_DIR=.
|
||||
git rev-parse --git-dir >/dev/null 2>&1 || REPO_DIR=$(grep -m1 '^project_root:' "$FILE" 2>/dev/null | sed 's/^project_root:[[:space:]]*//' | tr -d '\r')
|
||||
git rev-parse --git-dir >/dev/null 2>&1 || REPO_DIR=$(tr -d '\r' < "$FILE" 2>/dev/null | sed -n '1{/^---$/!q;d;}; /^---$/q; /^worktree:/{s/^worktree:[[:space:]]*//p;q;}')
|
||||
[ -n "$REPO_DIR" ] || REPO_DIR=$(tr -d '\r' < "$FILE" 2>/dev/null | sed -n '1{/^---$/!q;d;}; /^---$/q; /^project_root:/{s/^project_root:[[:space:]]*//p;q;}')
|
||||
CURRENT_HEAD=$(git -C "${REPO_DIR:-.}" rev-parse --verify -q HEAD 2>/dev/null)
|
||||
if [ -z "$SAVED_HEAD" ]; then
|
||||
echo "HEAD_UNTRACKED"
|
||||
@@ -566,7 +568,9 @@ fi
|
||||
and sort as usual.
|
||||
|
||||
**Sort Remaining Work by provenance.** Keep every item's original text and saved
|
||||
order, and drop nothing. Put an item under **Verify first** when it:
|
||||
order, and drop nothing. When the head check printed `HEAD_MOVED`, `HEAD_DIVERGED`
|
||||
or `HEAD_UNKNOWN`, skip the rules below: every item goes under Verify first,
|
||||
whatever its marker. Put an item under **Verify first** when it:
|
||||
- ends in `(path assumed)` or `(code read)`;
|
||||
- ends in `(path run)` but its text reports a failure;
|
||||
- ends in a parenthesised trailing marker that is **not** one of the five English
|
||||
|
||||
@@ -205,13 +205,15 @@ checkpoint(s) from another project; only this project's are listed." If it print
|
||||
|
||||
**Check whether the branch moved since the save.** The stamp records the commit
|
||||
the save was made at in `head:`. Before presenting, compare it with this
|
||||
checkout:
|
||||
checkout. FILE is the checkpoint path chosen in Step 2, one of the paths Step 1
|
||||
printed:
|
||||
|
||||
```bash
|
||||
FILE="<the chosen file>"
|
||||
SAVED_HEAD=$(grep -m1 '^head:' "$FILE" 2>/dev/null | sed 's/^head:[[:space:]]*//' | tr -d '\r')
|
||||
FILE="<the FILE path printed above>"
|
||||
SAVED_HEAD=$(tr -d '\r' < "$FILE" 2>/dev/null | sed -n '1{/^---$/!q;d;}; /^---$/q; /^head:/{s/^head:[[:space:]]*//p;q;}')
|
||||
REPO_DIR=.
|
||||
git rev-parse --git-dir >/dev/null 2>&1 || REPO_DIR=$(grep -m1 '^project_root:' "$FILE" 2>/dev/null | sed 's/^project_root:[[:space:]]*//' | tr -d '\r')
|
||||
git rev-parse --git-dir >/dev/null 2>&1 || REPO_DIR=$(tr -d '\r' < "$FILE" 2>/dev/null | sed -n '1{/^---$/!q;d;}; /^---$/q; /^worktree:/{s/^worktree:[[:space:]]*//p;q;}')
|
||||
[ -n "$REPO_DIR" ] || REPO_DIR=$(tr -d '\r' < "$FILE" 2>/dev/null | sed -n '1{/^---$/!q;d;}; /^---$/q; /^project_root:/{s/^project_root:[[:space:]]*//p;q;}')
|
||||
CURRENT_HEAD=$(git -C "${REPO_DIR:-.}" rev-parse --verify -q HEAD 2>/dev/null)
|
||||
if [ -z "$SAVED_HEAD" ]; then
|
||||
echo "HEAD_UNTRACKED"
|
||||
@@ -242,7 +244,9 @@ fi
|
||||
and sort as usual.
|
||||
|
||||
**Sort Remaining Work by provenance.** Keep every item's original text and saved
|
||||
order, and drop nothing. Put an item under **Verify first** when it:
|
||||
order, and drop nothing. When the head check printed `HEAD_MOVED`, `HEAD_DIVERGED`
|
||||
or `HEAD_UNKNOWN`, skip the rules below: every item goes under Verify first,
|
||||
whatever its marker. Put an item under **Verify first** when it:
|
||||
- ends in `(path assumed)` or `(code read)`;
|
||||
- ends in `(path run)` but its text reports a failure;
|
||||
- ends in a parenthesised trailing marker that is **not** one of the five English
|
||||
|
||||
@@ -93,7 +93,7 @@ function save(cwd: string): string {
|
||||
}
|
||||
|
||||
function headCheck(cwd: string, file: string) {
|
||||
const r = sh(HEAD_BLOCK.replace('FILE="<the chosen file>"', `FILE=${JSON.stringify(file)}`), cwd);
|
||||
const r = sh(HEAD_BLOCK.replace('FILE="<the FILE path printed above>"', `FILE=${JSON.stringify(file)}`), cwd);
|
||||
expect(r.status).toBe(0);
|
||||
return r.stdout;
|
||||
}
|
||||
@@ -113,8 +113,10 @@ describe('context-save records the commit a checkpoint describes', () => {
|
||||
const file = save(app);
|
||||
const fm = fs.readFileSync(file, 'utf-8').split('---')[1];
|
||||
expect(fm).toContain(`\nhead: ${git(app, 'rev-parse', 'HEAD')}\n`);
|
||||
spawnSync('bash', [path.join(ROOT, 'bin', 'gstack-slug'), '--stamp-checkpoint', file], { cwd: app, env: env(), timeout: 30_000 });
|
||||
expect(fs.readFileSync(file, 'utf-8').split('---')[1].match(/^head:/gm)?.length).toBe(1);
|
||||
commit(app, 'later work');
|
||||
const restamp = spawnSync('bash', [path.join(ROOT, 'bin', 'gstack-slug'), '--stamp-checkpoint', file], { cwd: app, env: env(), timeout: 30_000 });
|
||||
expect(restamp.status).toBe(0);
|
||||
expect(fs.readFileSync(file, 'utf-8').split('---')[1].match(/^head:.*$/gm)).toEqual([`head: ${git(app, 'rev-parse', 'HEAD')}`]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -165,6 +167,24 @@ describe('context-restore compares the checkpoint with HEAD', () => {
|
||||
fs.writeFileSync(bogus, '---\nbranch: main\nhead: $(touch pwned)\n---\n');
|
||||
expect(headCheck(app, bogus)).toStartWith('HEAD_UNKNOWN ');
|
||||
expect(fs.existsSync(path.join(app, 'pwned'))).toBe(false);
|
||||
const bodyOnly = path.join(tmp, 'body-only.md');
|
||||
fs.writeFileSync(bodyOnly, `---\nstatus: in-progress\nbranch: main\n---\n\nhead: ${git(app, 'rev-parse', 'HEAD~0')}\n`);
|
||||
expect(headCheck(app, bodyOnly).trim()).toBe('HEAD_UNTRACKED');
|
||||
});
|
||||
|
||||
test('outside git, a save from a nested linked worktree is compared with that worktree, not the main checkout', () => {
|
||||
const app = repo();
|
||||
const wt = path.join(app, '.worktrees', 'side');
|
||||
git(app, 'worktree', 'add', '-q', '-b', 'side', wt);
|
||||
const file = save(wt);
|
||||
expect(fs.readFileSync(file, 'utf-8')).toContain(`\nworktree: ${wt}\n`);
|
||||
commit(app, 'main moves on');
|
||||
commit(app, 'and again');
|
||||
const outside = path.join(tmp, 'outside');
|
||||
fs.mkdirSync(outside);
|
||||
expect(headCheck(outside, file).trim()).toBe('HEAD_UNCHANGED');
|
||||
commit(wt, 'worktree work');
|
||||
expect(headCheck(outside, file)).toStartWith('HEAD_MOVED 1\n');
|
||||
});
|
||||
|
||||
test('the prompt maps every status to what the user sees', () => {
|
||||
@@ -175,6 +195,7 @@ describe('context-restore compares the checkpoint with HEAD', () => {
|
||||
expect(restore).toContain('`<n> commit(s) landed since this save:`');
|
||||
expect(restore).toContain('under **Verify first**');
|
||||
expect(restore).toContain('{commit-tracking lines, if any}');
|
||||
expect(restore).toContain('When the head check printed `HEAD_MOVED`, `HEAD_DIVERGED`\nor `HEAD_UNKNOWN`, skip the rules below: every item goes under Verify first,\nwhatever its marker.');
|
||||
const save = fs.readFileSync(path.join(ROOT, 'context-save/SKILL.md'), 'utf-8');
|
||||
expect(save).toContain('`project_root:`, `worktree:` or `head:` yourself');
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user