Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# context-revive

> Your side project died at 80%. Bring it back in one prompt.
> Your agent forgot the ADR you wrote 30 prompts ago.

**Re-inject a dense, deterministic project brief into Claude Code every few
prompts, before context rot degrades the agent.** You edit `.revive/static.md`
Expand Down
51 changes: 51 additions & 0 deletions bin/revive
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,57 @@ cmd_init() {
else
echo "created: $STATIC_FILE"
fi

maybe_update_gitignore
}

# If $STATIC_FILE would currently be gitignored, append the canonical
# exception block to .gitignore so the file can actually be tracked. Stays
# silent when not in a git repo, when there's no .gitignore yet, or when
# static.md is already trackable. If a directory-level `.revive/` rule
# blocks the un-ignore from taking effect, surface a clear warning instead
# of silently leaving the file untracked.
maybe_update_gitignore() {
git rev-parse --git-dir >/dev/null 2>&1 || return 0
[[ -f .gitignore ]] || return 0
git check-ignore -q "$STATIC_FILE" 2>/dev/null || return 0

# Best-effort: a read-only .gitignore must not abort `init`. Surface a
# warning with the exact lines the user needs to add, and continue.
if [[ ! -w .gitignore ]]; then
cat >&2 <<MSG
warning: .gitignore is not writable — skipping auto-update. Add manually:
!.revive/
.revive/*
!.revive/static.md
or run \`git add -f $STATIC_FILE\`.
MSG
return 0
fi

# The block has to un-ignore the parent directory first: git semantics
# don't let \`!\` rescue a file inside a fully-ignored directory. Putting
# \`!.revive/\` first covers \`.revive/\`, \`.revive\`, and broad rules like
# \`.*\` / \`.*/\`. Then we re-ignore the directory contents and re-include
# only static.md.
cat >> .gitignore <<'GITIGNORE'

# revive: .revive/static.md is the curated STATIC layer (checked in);
# other .revive/ contents are local runtime state.
!.revive/
.revive/*
!.revive/static.md
GITIGNORE

if git check-ignore -q "$STATIC_FILE" 2>/dev/null; then
cat >&2 <<MSG
warning: $STATIC_FILE is still gitignored after appending the canonical
exception block. Inspect with \`git check-ignore -v $STATIC_FILE\` to see
which rule is overriding, or run \`git add -f $STATIC_FILE\`.
MSG
else
echo "updated: .gitignore (un-ignored $STATIC_FILE)"
fi
}

# Build a prompt that a coding agent (Claude Code / Cursor / Aider / any
Expand Down
66 changes: 66 additions & 0 deletions tests/revive.bats
Original file line number Diff line number Diff line change
Expand Up @@ -1430,3 +1430,69 @@ JSON
run "$REVIVE" help
[[ "$output" == *"mark-compact"* ]] || return 1
}

# --- init: gitignore auto-update ---

@test "init does nothing to .gitignore when there is none" {
run "$REVIVE" init
[ "$status" -eq 0 ] || return 1
[ ! -f .gitignore ] || return 1
}

@test "init appends exception when .revive/* is gitignored without exception" {
printf '.revive/*\n' > .gitignore
run "$REVIVE" init
[ "$status" -eq 0 ] || return 1
[[ "$output" == *"un-ignored"* ]] || return 1
run cat .gitignore
[[ "$output" == *"!.revive/static.md"* ]] || return 1
# static.md is now actually trackable
run git check-ignore -q .revive/static.md
[ "$status" -ne 0 ] || return 1
}

@test "init leaves .gitignore alone when static.md is already trackable" {
printf '.revive/*\n!.revive/static.md\n' > .gitignore
local before
before=$(wc -l < .gitignore)
run "$REVIVE" init
[ "$status" -eq 0 ] || return 1
local after
after=$(wc -l < .gitignore)
[ "$before" -eq "$after" ] || return 1
}

@test "init un-ignores static.md even with a directory-level .revive/ rule" {
# Codex P2 regression check: a `.revive/` rule used to leave static.md
# untrackable. The canonical block now leads with `!.revive/` so the
# un-ignore works regardless.
printf '.revive/\n' > .gitignore
run "$REVIVE" init
[ "$status" -eq 0 ] || return 1
[[ "$output" == *"un-ignored"* ]] || return 1
run git check-ignore -q .revive/static.md
[ "$status" -ne 0 ] || return 1
}

@test "init un-ignores static.md against a broad dot-dir rule (.*)" {
# Common case: repos that ignore all dot-directories with `.*`. Without
# the leading `!.revive/`, the file-level exception cannot rescue
# static.md from the broader directory ignore.
printf '.*\n' > .gitignore
run "$REVIVE" init
[ "$status" -eq 0 ] || return 1
run git check-ignore -q .revive/static.md
[ "$status" -ne 0 ] || return 1
}

@test "init does not abort when .gitignore is read-only" {
# Codex P3: `cat >> .gitignore` under `set -e` would terminate cmd_init
# if the file is read-only. Best-effort path must warn and continue.
printf '.revive/*\n' > .gitignore
chmod -w .gitignore
trap 'chmod +w .gitignore || true' RETURN
run "$REVIVE" init
[ "$status" -eq 0 ] || return 1
[[ "$output" == *"not writable"* ]] || return 1
[ -f .revive/static.md ] || return 1
}
Loading