From f27d72d2b8b80717fc71b915f6a46cbb92858d39 Mon Sep 17 00:00:00 2001 From: Justyna Wojtczak Date: Mon, 27 Apr 2026 16:05:41 +0900 Subject: [PATCH 1/3] docs(readme): tighten tagline to name the actual problem MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Your side project died at 80%. Bring it back in one prompt." was cryptic — readers had to guess what 80% referred to (project abandonment? completion?), and nothing in the line connected to the actual problem the tool solves (agent context rot in long sessions). Replace with the literal failure mode users hit in practice: "Your agent forgot the ADR you wrote 30 prompts ago." Names the pain directly, no metaphor decoding required. The next paragraph already explains the mechanism, so the tagline can stay terse. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7ab166b..5615ce6 100644 --- a/README.md +++ b/README.md @@ -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` From e55f0bf72ae1c20a5679fd28f1d317ae89c69558 Mon Sep 17 00:00:00 2001 From: Justyna Wojtczak Date: Mon, 27 Apr 2026 16:08:00 +0900 Subject: [PATCH 2/3] feat(init): auto-update .gitignore so static.md is trackable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A common onboarding stumble: user follows the README, runs `revive init`, then `git add .revive/static.md` — git silently skips the file because their .gitignore has `.revive/*` (or similar) and no exception. The "checked in" workflow the README documents quietly fails. `cmd_init` now calls `maybe_update_gitignore` after writing static.md. Logic: - Skip silently if not in a git repo, no .gitignore exists, or `git check-ignore` says the file is already trackable. - Otherwise append a small canonical block: .revive/* !.revive/static.md with a header comment explaining what gets tracked vs not. - Re-run `git check-ignore`. If a directory-level `.revive/` rule still suppresses the un-ignore (git semantics: `!` cannot rescue a file inside a fully-ignored directory), print a warning telling the user how to fix it. 4 new tests cover: no .gitignore (no-op), `.revive/*` (append + verify trackable), already-correct (idempotent — line count unchanged), `.revive/` directory rule (warning emitted, exit 0 preserved). --- bin/revive | 33 +++++++++++++++++++++++++++++++++ tests/revive.bats | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/bin/revive b/bin/revive index 0f9b6f8..25e132a 100755 --- a/bin/revive +++ b/bin/revive @@ -697,6 +697,39 @@ 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 + + cat >> .gitignore <<'GITIGNORE' + +# revive: .revive/static.md is the curated STATIC layer (checked in); +# other .revive/ contents are local runtime state. +.revive/* +!.revive/static.md +GITIGNORE + + if git check-ignore -q "$STATIC_FILE" 2>/dev/null; then + cat >&2 < .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 warns when a directory-level .revive/ rule blocks the un-ignore" { + printf '.revive/\n' > .gitignore + run "$REVIVE" init + # init still exits 0 — warning is informational, not fatal + [ "$status" -eq 0 ] || return 1 + [[ "$output" == *"still gitignored"* ]] || return 1 +} From 698736b0827ecc8fb36c3c6b1032b711d2d5d9bf Mon Sep 17 00:00:00 2001 From: Justyna Wojtczak Date: Mon, 27 Apr 2026 16:17:58 +0900 Subject: [PATCH 3/3] fix(init/gitignore): handle broad dot-dir + read-only cases (codex) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues codex flagged on the original gitignore-auto patch: [P2] Broad dot-dir rules left static.md untrackable - A repo with `.revive/`, `.revive`, or even `.*` / `.*/` already ignores the parent directory. Git semantics: `!` cannot rescue a file inside a fully-ignored directory. - The canonical block now leads with `!.revive/` to un-ignore the parent first, then re-ignores contents with `.revive/*`, then re-includes static.md with `!.revive/static.md`. - The "warns when a directory-level .revive/ rule blocks" test flipped to "un-ignores even with a directory-level rule" — the former failure mode no longer happens. Added a parallel test for `.*` to cover the broad-rule case. [P3] Read-only .gitignore aborted `revive init` - Under `set -e`, `cat >> .gitignore` on a read-only file killed cmd_init even though the gitignore update is best-effort. - Pre-check writability with `[[ -w .gitignore ]]`. If not writable, print the exact lines the user needs to add and return 0 — static.md is already created at this point. - New test exercises the read-only path: chmod -w .gitignore, trap restores write perm so teardown doesn't break. --- bin/revive | 24 +++++++++++++++++++++--- tests/revive.bats | 33 ++++++++++++++++++++++++++++++--- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/bin/revive b/bin/revive index 25e132a..866e705 100755 --- a/bin/revive +++ b/bin/revive @@ -712,10 +712,29 @@ maybe_update_gitignore() { [[ -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 <> .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 @@ -723,9 +742,8 @@ GITIGNORE if git check-ignore -q "$STATIC_FILE" 2>/dev/null; then cat >&2 < .gitignore run "$REVIVE" init - # init still exits 0 — warning is informational, not fatal [ "$status" -eq 0 ] || return 1 - [[ "$output" == *"still gitignored"* ]] || 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 }