Skip to content

Commit 580adba

Browse files
justiclaude
andauthored
feat: add DIFFERENTIATORS section + require business goals in PURPOSE (#14)
Two complementary changes, both driven by real-use feedback: ## 1. PURPOSE must include business goals (v0.1.14 Deliverable 1 update) Previous PURPOSE spec: "product one-liner + essential philosophy". A gh repo description (1-sentence marketing tagline) passed the "substantive" test and was preserved — which meant users got terse PURPOSE that doesn't tell an agent WHY the project exists or what success looks like. New spec requires all three: 1. WHAT the project is (product pitch) 2. WHY it exists — business goal / success criterion 3. Essential philosophy or hard constraint Marketing-tagline PURPOSE now classifies as placeholder and gets regenerated by `revive suggest`. The placeholder-marker list explicitly flags "marketing-style 1-liner that doesn't spell out why it exists / what success looks like" as a placeholder indicator. ## 2. New STATIC section: DIFFERENTIATORS Competitive positioning and deliberate design choices that contradict what an "obvious" project in the space would do. This class of signal prevents an agent from suggesting "why not just use X?" for an X you already evaluated and rejected. Format: "<alternative> → <our choice/rationale>" or "unlike X, we Y because Z". Brief layout: PURPOSE → DIFFERENTIATORS → INVARIANTS → GOTCHAS. Full integration: - `revive init` scaffolds DIFFERENTIATORS placeholder alongside the existing sections. - `revive show` suppresses placeholder DIFFERENTIATORS bullets the same way it already suppresses placeholder INVARIANTS/GOTCHAS. - `revive init --force` preserves user-edited DIFFERENTIATORS alongside INVARIANTS/GOTCHAS when regenerating PURPOSE. - `revive suggest` prompt: new Deliverable 2 (DIFFERENTIATORS); INVARIANTS → 3, GOTCHAS → 4. Placeholder markers list updated. Output-format example and STEP 2 ordering include the new section. Agent is explicitly told: "If you genuinely don't see meaningful differentiation signals in the artefacts (ADRs comparing approaches, README 'vs' sections, rejected-alternatives notes), leave this section with its placeholder — better empty than filler." ## Tests (+7, 100 total) - PURPOSE deliverable requires business goals - init scaffolds DIFFERENTIATORS + correct section ordering - show skips placeholder DIFFERENTIATORS bullets - show keeps substantive DIFFERENTIATORS bullets - init --force preserves substantive DIFFERENTIATORS across PURPOSE regen - suggest prompt introduces DIFFERENTIATORS as Deliverable 2 - suggest prompt lists DIFFERENTIATORS placeholder marker Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 63c0625 commit 580adba

2 files changed

Lines changed: 207 additions & 55 deletions

File tree

bin/revive

Lines changed: 105 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# https://github.com/justi/context-revive
44
set -euo pipefail
55

6-
VERSION="0.1.13"
6+
VERSION="0.1.14"
77
STATIC_FILE=".revive/static.md"
88
COMMANDS_FILE=".revive/commands.md"
99
BRIEF_CHAR_BUDGET=2200
@@ -20,12 +20,14 @@ usage() {
2020
Usage: revive <command> [options]
2121
2222
Commands:
23-
init Generate .revive/static.md (PURPOSE, INVARIANTS, GOTCHAS).
24-
Use `init --force` to regenerate PURPOSE from the chain
25-
while preserving user-edited INVARIANTS and GOTCHAS.
23+
init Generate .revive/static.md (PURPOSE, DIFFERENTIATORS,
24+
INVARIANTS, GOTCHAS). Use `init --force` to regenerate
25+
PURPOSE from the chain while preserving user-edited
26+
DIFFERENTIATORS, INVARIANTS, and GOTCHAS.
2627
suggest Print an LLM-ready prompt that, when pasted into any
27-
coding agent, generates PURPOSE + INVARIANTS + GOTCHAS
28-
for this project. Tailored to the actual files in the repo.
28+
coding agent, generates PURPOSE + DIFFERENTIATORS +
29+
INVARIANTS + GOTCHAS for this project. Tailored to the
30+
actual files in the repo.
2931
show Print the brief that would be injected (force-emit)
3032
refresh Hook entry point — cadence-gated, silent on failure
3133
install-hook Wire UserPromptSubmit into .claude/settings.json
@@ -341,7 +343,7 @@ EOF
341343
}
342344
NR_buf = 0
343345
}
344-
/^(PURPOSE|INVARIANTS|GOTCHAS):/ { flush(); buf[++NR_buf] = $0; next }
346+
/^(PURPOSE|DIFFERENTIATORS|INVARIANTS|GOTCHAS):/ { flush(); buf[++NR_buf] = $0; next }
345347
{ buf[++NR_buf] = $0 }
346348
END { flush() }
347349
' "$STATIC_FILE"
@@ -620,40 +622,51 @@ cmd_init() {
620622
exit 1
621623
fi
622624

623-
# Preserve user-edited INVARIANTS and GOTCHAS across a --force regenerate;
624-
# only PURPOSE is replaced. This is the common flow after upgrading
625-
# revive — the chain got smarter, user rules haven't changed.
625+
# Preserve user-edited DIFFERENTIATORS / INVARIANTS / GOTCHAS across a
626+
# --force regenerate; only PURPOSE is replaced. This is the common flow
627+
# after upgrading revive — the chain got smarter, user rules haven't
628+
# changed.
629+
local differentiators=" - (what sets this project apart; edit this file)"
626630
local invariants=" - (top-5 architectural rules; edit this file)"
627631
local gotchas=" - (landmines you keep stepping on; edit this file)"
628632
if (( force )) && [[ -f "$STATIC_FILE" ]]; then
629633
local preserved
630634
preserved=$(awk '
631-
/^INVARIANTS:/ { cap="inv"; next }
632-
/^GOTCHAS:/ { cap="got"; next }
633-
/^[A-Z][A-Z_]*:/ { cap="" }
634-
cap == "inv" { inv = inv $0 "\n" }
635-
cap == "got" { got = got $0 "\n" }
636-
END { printf "INV_START\n%sINV_END\nGOT_START\n%sGOT_END\n", inv, got }
635+
/^DIFFERENTIATORS:/ { cap="diff"; next }
636+
/^INVARIANTS:/ { cap="inv"; next }
637+
/^GOTCHAS:/ { cap="got"; next }
638+
/^[A-Z][A-Z_]*:/ { cap="" }
639+
cap == "diff" { diff = diff $0 "\n" }
640+
cap == "inv" { inv = inv $0 "\n" }
641+
cap == "got" { got = got $0 "\n" }
642+
END {
643+
printf "DIFF_START\n%sDIFF_END\nINV_START\n%sINV_END\nGOT_START\n%sGOT_END\n",
644+
diff, inv, got
645+
}
637646
' "$STATIC_FILE")
638-
local saved_inv saved_got
647+
local saved_diff saved_inv saved_got
648+
saved_diff=$(awk '/^DIFF_START$/{p=1;next} /^DIFF_END$/{p=0} p' <<<"$preserved" | sed '/^$/d')
639649
saved_inv=$(awk '/^INV_START$/{p=1;next} /^INV_END$/{p=0} p' <<<"$preserved" | sed '/^$/d')
640650
saved_got=$(awk '/^GOT_START$/{p=1;next} /^GOT_END$/{p=0} p' <<<"$preserved" | sed '/^$/d')
641-
[[ -n "$saved_inv" ]] && invariants="$saved_inv"
642-
[[ -n "$saved_got" ]] && gotchas="$saved_got"
651+
[[ -n "$saved_diff" ]] && differentiators="$saved_diff"
652+
[[ -n "$saved_inv" ]] && invariants="$saved_inv"
653+
[[ -n "$saved_got" ]] && gotchas="$saved_got"
643654
fi
644655

645656
mkdir -p "$(dirname "$STATIC_FILE")"
646657
local purpose
647658
purpose=$(compute_purpose)
648659
{
649660
printf 'PURPOSE: %s\n' "$purpose"
661+
echo "DIFFERENTIATORS:"
662+
printf '%s\n' "$differentiators"
650663
echo "INVARIANTS:"
651664
printf '%s\n' "$invariants"
652665
echo "GOTCHAS:"
653666
printf '%s\n' "$gotchas"
654667
} > "$STATIC_FILE"
655668
if (( force )); then
656-
echo "regenerated: $STATIC_FILE (INVARIANTS/GOTCHAS preserved)"
669+
echo "regenerated: $STATIC_FILE (DIFFERENTIATORS/INVARIANTS/GOTCHAS preserved)"
657670
else
658671
echo "created: $STATIC_FILE"
659672
fi
@@ -699,17 +712,24 @@ PROMPT_META
699712
cat <<'PROMPT_HEADER'
700713
Read the following artefacts in the current project, then fill any
701714
placeholder sections of .revive/static.md. Treat existing human-
702-
curated content as sacred: PURPOSE, INVARIANTS, and GOTCHAS that
703-
already contain real content must be preserved verbatim — do not
704-
rewrite substantive sections just because you can.
715+
curated content as sacred: PURPOSE, DIFFERENTIATORS, INVARIANTS,
716+
and GOTCHAS that already contain real content must be preserved
717+
verbatim — do not rewrite substantive sections just because you can.
705718
706719
Regenerate ONLY sections that are still in their `revive init`
707720
placeholder state. Recognise a placeholder by these markers:
708721
709722
- PURPOSE: literal `(describe this project in 1-3 sentences)`,
710-
OR a single short fragment clearly extracted from a manifest/
711-
README without context (a one-sentence pitch under ~80 chars
712-
that doesn't frame the project).
723+
OR a marketing-style 1-liner that states WHAT the project does
724+
but doesn't spell out WHY it exists / what success looks like.
725+
A gh repo description or README tagline typically falls in this
726+
category — it's readable, but doesn't give an agent enough to
727+
understand the business value or operating priorities.
728+
Rule of thumb: if the current PURPOSE could fit on a single
729+
marketing banner and a reader couldn't answer "what does success
730+
for this project look like?" from it alone, treat as placeholder.
731+
- DIFFERENTIATORS: bullets containing `(what sets this project
732+
apart; edit this file)` or `(none defined)`.
713733
- INVARIANTS: bullets containing `(top-5 architectural rules;
714734
edit this file)` or `(none defined)`.
715735
- GOTCHAS: bullets containing `(landmines you keep stepping on;
@@ -752,30 +772,63 @@ Strict rules (violations are failures, not stylistic preferences):
752772
incident, OMIT it. Placeholder > false confidence.
753773
754774
Deliverable 1 — PURPOSE (single line, ≤ 400 chars):
755-
A tight 2-3 sentence summary that answers:
756-
- what the project IS (product one-liner)
757-
- its essential philosophy or hard constraint (what makes it different)
758-
Include just enough context that someone reading only the brief could
759-
pick up work without reading README/CLAUDE.md first.
775+
A tight 2-3 sentence summary that answers ALL THREE of:
776+
1. WHAT the project is (product one-liner)
777+
2. WHY it exists — business goal or success criterion the project
778+
is optimizing for (e.g. "cut X hours from weekly ops",
779+
"unlock audience Y", "prevent incident class Z", "reclaim
780+
DORMANT side projects")
781+
3. Its essential philosophy or hard constraint (what makes it
782+
different, the one thing every design decision must respect)
783+
784+
All three must be present. A gh-style 1-liner that covers only (1)
785+
is NOT enough — it's a marketing tagline, not operating context.
760786
761787
Bad examples:
762-
- "A Flutter app." (useless, inferable)
763-
- single terse fragment under 60 chars (too terse)
788+
- "A Flutter app." (only 1, useless)
789+
- "Never forget a side project." (only 1, marketing tagline)
764790
- 800-char pasted README intro (too long, buries signal)
765791
766792
Good examples (generic):
767793
- "A CLI that turns Markdown invoices into tax-compliant PDFs.
768-
Zero-config for sole traders in EU/UK; one-flag override for
769-
custom templates."
770-
- "Background-job scheduler for Go services. Unlike cron,
771-
schedules are defined in code and survive deploys; job state
772-
lives in the same Postgres the app already uses."
794+
Saves sole traders ~3h/month of invoicing; success = zero
795+
manual edits to the generated PDF. Zero-config by default;
796+
customization only via explicit template flag."
797+
- "Background-job scheduler for Go services. Goal: replace ops-
798+
managed cron with code-defined schedules that survive
799+
deploys; success = zero surprise job failures after a deploy.
800+
Constraint: job state lives in the app's own Postgres — no
801+
new infrastructure."
802+
803+
Deliverable 2 — DIFFERENTIATORS (up to 5, or empty):
804+
What makes this project different from the obvious alternatives.
805+
Competitive positioning, deliberate design choices that contradict
806+
what another project in the space would do. This class of signal
807+
prevents an agent from suggesting "why not just use X?" for an X
808+
you've already evaluated and rejected.
809+
810+
Each bullet: "<alternative> → <what we do instead>" or "unlike
811+
<alternative>, we <choice> because <reason>".
773812
774-
If the CURRENT PURPOSE line in .revive/static.md is already a good
775-
2-3 sentence summary (clearly readable, frames the project well),
776-
keep it verbatim — don't rewrite for its own sake.
813+
Bad examples:
814+
- "We're open source." (not differentiation)
815+
- "Better than the rest." (empty marketing)
816+
- Anything an agent can discover by reading competitor READMEs.
777817
778-
Deliverable 2 — INVARIANTS (up to 5):
818+
Good examples:
819+
- "Unlike cron — schedules defined in code, survive deploys; no
820+
ops-managed crontab to drift from the repo."
821+
- "repomix / code2prompt: one-shot dump → we inject small briefs
822+
on cadence, no token flood."
823+
- "Cursor Rules: static prepend → we regenerate DYNAMIC sections
824+
per refresh from git + fs; recent work stays visible."
825+
826+
If you genuinely don't see meaningful differentiation signals in
827+
the artefacts (ADRs comparing approaches, README "vs" sections,
828+
CLAUDE.md rejected-alternatives notes), leave this section with
829+
its placeholder — better empty than filler.
830+
831+
Deliverable 3 — INVARIANTS (up to 5):
779832
Architectural or operational rules whose breakage causes non-obvious
780833
damage. Examples of the *class* of content to produce:
781834
- decisions captured in ADRs that contradict "obvious" approaches
@@ -794,7 +847,7 @@ Deliverable 2 — INVARIANTS (up to 5):
794847
- "Never run data migrations on Monday; the cron vacuum window
795848
overlaps and rollbacks stall."
796849
797-
Deliverable 3 — GOTCHAS (3 to 5):
850+
Deliverable 4 — GOTCHAS (3 to 5):
798851
Landmines whose fix is not obvious from code alone. Dependency quirks,
799852
rate limits, migrations needing manual steps, config that looks like a
800853
bug but is deliberate, platform-specific behaviour.
@@ -813,10 +866,13 @@ Deliverable 3 — GOTCHAS (3 to 5):
813866
Quality bar: 3 strong rules beat 5 weak ones. Same for gotchas.
814867
815868
Required output format (exact indentation, no code fences, no
816-
commentary). PURPOSE is a single line; INVARIANTS/GOTCHAS are bullet
817-
lists with two-space indent:
869+
commentary). PURPOSE is a single line; DIFFERENTIATORS, INVARIANTS,
870+
and GOTCHAS are bullet lists with two-space indent:
818871
819872
PURPOSE: <single 2-3 sentence summary, ≤ 400 chars>
873+
DIFFERENTIATORS:
874+
- <alternative → our choice / rationale>
875+
- <...>
820876
INVARIANTS:
821877
- <rule 1>
822878
- <rule 2>
@@ -839,10 +895,10 @@ Do two things, in order — do NOT stop after step 1:
839895
840896
STEP 2 — Open .revive/static.md and edit it in place: replace
841897
ONLY the sections you identified as placeholders. Preserve file
842-
ordering (PURPOSE → INVARIANTS → GOTCHAS). Do NOT touch sections
843-
you decided to preserve. Use your editor tool (Edit / Write /
844-
str_replace — whatever your agent framework exposes); do NOT
845-
ask me to paste manually.
898+
ordering (PURPOSE → DIFFERENTIATORS → INVARIANTS → GOTCHAS).
899+
Do NOT touch sections you decided to preserve. Use your editor
900+
tool (Edit / Write / str_replace — whatever your agent framework
901+
exposes); do NOT ask me to paste manually.
846902
847903
After step 2, confirm with a one-line summary of which sections
848904
were rewritten and which were preserved, plus the final line

tests/revive.bats

Lines changed: 102 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -916,20 +916,116 @@ EOF
916916
}
917917

918918
@test "suggest prompt requests PURPOSE as Deliverable 1 (v0.1.13)" {
919+
# After v0.1.14 DIFFERENTIATORS slots in as Deliverable 2 — keep this
920+
# test narrow: PURPOSE must be Deliverable 1.
919921
run "$REVIVE" suggest
920922
[ "$status" -eq 0 ]
921-
[[ "$output" == *"Deliverable 1 — PURPOSE"* ]] || return 1
922-
[[ "$output" == *"Deliverable 2 — INVARIANTS"* ]] || return 1
923-
[[ "$output" == *"Deliverable 3 — GOTCHAS"* ]] || return 1
923+
[[ "$output" == *"Deliverable 1 — PURPOSE"* ]] || return 1
924+
}
925+
926+
@test "suggest PURPOSE deliverable requires business goals (v0.1.14)" {
927+
# Marketing one-liners are no longer "substantive" — PURPOSE must
928+
# cover what + why (business goal) + essential constraint.
929+
run "$REVIVE" suggest
930+
[ "$status" -eq 0 ]
931+
printf '%s\n' "$output" | grep -qF 'business goal or success criterion' || return 1
932+
printf '%s\n' "$output" | grep -qF 'All three must be present' || return 1
933+
# Placeholder classification must explicitly cover marketing-tagline state
934+
printf '%s\n' "$output" | grep -qF "what success" || return 1
935+
}
936+
937+
# DIFFERENTIATORS section (v0.1.14)
938+
939+
@test "init scaffolds DIFFERENTIATORS section alongside others" {
940+
run "$REVIVE" init
941+
[ "$status" -eq 0 ]
942+
run cat .revive/static.md
943+
[[ "$output" == *"DIFFERENTIATORS:"* ]] || return 1
944+
[[ "$output" == *"what sets this project apart"* ]] || return 1
945+
# section ordering: PURPOSE → DIFFERENTIATORS → INVARIANTS → GOTCHAS
946+
local purp_line diff_line inv_line got_line
947+
purp_line=$(printf '%s\n' "$output" | grep -n '^PURPOSE:' | cut -d: -f1)
948+
diff_line=$(printf '%s\n' "$output" | grep -n '^DIFFERENTIATORS:' | cut -d: -f1)
949+
inv_line=$(printf '%s\n' "$output" | grep -n '^INVARIANTS:' | cut -d: -f1)
950+
got_line=$(printf '%s\n' "$output" | grep -n '^GOTCHAS:' | cut -d: -f1)
951+
[ "$purp_line" -lt "$diff_line" ] || return 1
952+
[ "$diff_line" -lt "$inv_line" ] || return 1
953+
[ "$inv_line" -lt "$got_line" ] || return 1
954+
}
955+
956+
@test "show skips placeholder DIFFERENTIATORS bullets" {
957+
"$REVIVE" init
958+
run "$REVIVE" show
959+
# header stays for structure, placeholder bullet must not leak
960+
[[ "$output" == *"DIFFERENTIATORS:"* ]] || return 1
961+
[[ "$output" != *"what sets this project"* ]] || return 1
962+
}
963+
964+
@test "show keeps substantive DIFFERENTIATORS bullets intact" {
965+
mkdir -p .revive
966+
cat > .revive/static.md <<'EOF'
967+
PURPOSE: Test project.
968+
DIFFERENTIATORS:
969+
- Unlike cron — code-defined schedules that survive deploys.
970+
- Unlike SaaS schedulers — zero new infrastructure.
971+
INVARIANTS:
972+
GOTCHAS:
973+
EOF
974+
run "$REVIVE" show
975+
[[ "$output" == *"Unlike cron"* ]] || return 1
976+
[[ "$output" == *"zero new infrastructure"* ]] || return 1
977+
}
978+
979+
@test "init --force preserves substantive DIFFERENTIATORS across PURPOSE regen" {
980+
mkdir -p .revive
981+
cat > .revive/static.md <<'EOF'
982+
PURPOSE: old stale purpose
983+
DIFFERENTIATORS:
984+
- Custom differentiator 1.
985+
- Custom differentiator 2.
986+
INVARIANTS:
987+
- user rule 1
988+
GOTCHAS:
989+
- user gotcha 1
990+
EOF
991+
printf '{"description":"fresh purpose"}\n' > package.json
992+
run "$REVIVE" init --force
993+
[ "$status" -eq 0 ]
994+
[[ "$output" == *"DIFFERENTIATORS/INVARIANTS/GOTCHAS preserved"* ]] || return 1
995+
run cat .revive/static.md
996+
[[ "$output" == *"Custom differentiator 1."* ]] || return 1
997+
[[ "$output" == *"Custom differentiator 2."* ]] || return 1
998+
[[ "$output" == *"user rule 1"* ]] || return 1
999+
[[ "$output" == *"user gotcha 1"* ]] || return 1
1000+
[[ "$output" == *"fresh purpose"* ]] || return 1
1001+
}
1002+
1003+
@test "suggest prompt introduces DIFFERENTIATORS as Deliverable 2" {
1004+
run "$REVIVE" suggest
1005+
[ "$status" -eq 0 ]
1006+
[[ "$output" == *"Deliverable 1 — PURPOSE"* ]] || return 1
1007+
[[ "$output" == *"Deliverable 2 — DIFFERENTIATORS"* ]] || return 1
1008+
[[ "$output" == *"Deliverable 3 — INVARIANTS"* ]] || return 1
1009+
[[ "$output" == *"Deliverable 4 — GOTCHAS"* ]] || return 1
1010+
}
1011+
1012+
@test "suggest prompt lists DIFFERENTIATORS placeholder marker" {
1013+
# "what sets this project apart" wraps across two lines in the prompt;
1014+
# assert the left half which is line-complete.
1015+
run "$REVIVE" suggest
1016+
[ "$status" -eq 0 ]
1017+
printf '%s\n' "$output" | grep -qF 'what sets this project' || return 1
9241018
}
9251019

9261020
@test "suggest prompt tells agent to preserve substantive sections across the board" {
927-
# Semantics: existing human-curated content in PURPOSE / INVARIANTS /
928-
# GOTCHAS must survive a re-run. Only placeholder sections get rewritten.
1021+
# Semantics: existing human-curated content in any STATIC section must
1022+
# survive a re-run. Only placeholder sections get rewritten.
1023+
# (String "preserved verbatim" is line-wrapped in the prompt; check
1024+
# for the sentinel words on their own lines.)
9291025
run "$REVIVE" suggest
9301026
[ "$status" -eq 0 ]
9311027
printf '%s\n' "$output" | grep -qF 'Treat existing human-' || return 1
932-
printf '%s\n' "$output" | grep -qF 'preserved verbatim' || return 1
1028+
printf '%s\n' "$output" | grep -qF 'verbatim' || return 1
9331029
printf '%s\n' "$output" | grep -qF 'placeholder state' || return 1
9341030
}
9351031

0 commit comments

Comments
 (0)