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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `checks/` — warn-only enforcement suite (14 checks: frontmatter, body structure, placeholders, dependency graph, word limits, manifest via `claude plugin validate --strict`, component census, legacy guards, self-containment, spec fixture, ADR status, filename convention); config `checks/enforcement.config.json`, CI job in `validate.yml`
- Enforcement sections appended to all 11 prior ADRs: testable consequences, check mappings, honest not-machine-checkable lines, empty graduation logs
- Two blocking hooks in checked-in `.claude/settings.json`: Stop-gate (suite BLOCKERs bounce the turn) and PreToolUse guard (existing ADRs append-only, Pattern Guide requires an ADR); kill-switches `PRODUCT_DEV_SKIP_STOP_GATE=1` / `PRODUCT_DEV_SKIP_ADR_GUARD=1`
- ADR 0019: CLAUDE.md is the contributor guide; runtime canon lives in the plugin. New warn-level check `claude-md-scope` (links resolve; 1,000-word budget)
- ADR index, generated: `checks/adr_index.py` renders the index of records (status, enforcing checks, amended-by links) from the ADR files and the enforcement config. Two new warn-level checks (ADR 0012 TC-3, TC-4): `adr-index` fails when the committed index is stale, `adr-amendment-link` fails when an ADR says it amends another and the amended one never names it
- `skills-claude-ai/` — Claude.ai Agent Skill ports of product-ideation and product-flow (issue #12): bundled prompts with skill-relative paths, conversation-scoped artifact ledger with `product-dev-artifacts.md` export replacing the `.product-dev/` registry

### Changed
- `CLAUDE.md` is now the contributor guide (ADR 0019): about 650 words of pointers and change rules, down from about 2,000 that restated the skills, the tier model, the plugin tree, and the ADR list. Four runtime rules that never shipped in the plugin stay in a labelled section pending issue #62
- PreToolUse guard: an ADR is protected only once it exists in `HEAD`, so a draft can be revised before its first commit
- README: the hand-kept ADR table is replaced by a pointer to the generated index
- PreToolUse guard: the Bash rule for inline scripts now requires an existing protected file or iteration of the ADR folder, after it blocked two commands that only mentioned the folder in text
- Enforcement suite: first graduation (ADR 0012 amendment, 2026-10-12). Twelve checks moved warn → block after 7+ clean days: `frontmatter-v2`, `name-uniqueness`, `body-structure`, `placeholder-syntax`, `placeholder-resolvability`, `dependency-graph`, `component-census`, `no-legacy`, `spec-fixture`, `adr-status`, `filename-convention`, `objective-parity`. Findings from these now fail CI and bounce the Stop-gate. `manifest`, `word-limit`, and `self-containment` stay at warn, with reasons in the amendment
Expand Down
346 changes: 79 additions & 267 deletions CLAUDE.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

1. Clone the repository
2. Review `docs/adrs/` for architectural decisions
3. Read `CLAUDE.md` for the full workflow specification
3. Read `CLAUDE.md` for the rules that apply to changes in this repo; the workflow itself is specified by the skills under `plugin/skills/`
4. Install the plugin locally: `claude --plugin-dir ./plugin`

## Prompt Authoring
Expand Down
3 changes: 2 additions & 1 deletion checks/enforcement.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
{ "id": "filename-convention", "adr": "CONTRIBUTING", "tc": "CONTRIBUTING:naming-convention", "status": "block", "added": "2026-07-12", "graduated": "2026-10-12" },
{ "id": "objective-parity", "adr": "0014", "tc": "0014:TC-parity", "status": "block", "added": "2026-07-25", "graduated": "2026-10-12" },
{ "id": "adr-index", "adr": "0012", "tc": "0012:TC-3", "status": "warn", "added": "2026-10-12", "graduated": null },
{ "id": "adr-amendment-link", "adr": "0012", "tc": "0012:TC-4", "status": "warn", "added": "2026-10-12", "graduated": null }
{ "id": "adr-amendment-link", "adr": "0012", "tc": "0012:TC-4", "status": "warn", "added": "2026-10-12", "graduated": null },
{ "id": "claude-md-scope", "adr": "0019", "tc": "0019:TC-1,TC-2", "status": "warn", "added": "2026-10-12", "graduated": null }
]
}
30 changes: 26 additions & 4 deletions checks/hooks/pretooluse_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

Protected (see ADR 0012 and its 2026-10-12 amendment):
- docs/adrs/NNNN-*.md that already exist — ADRs are append-only history.
* Creating a NEW ADR file is always allowed (the supersession path).
* Creating a NEW ADR file is always allowed (the supersession path), and
an ADR not yet committed to HEAD is a draft that may be revised freely.
* APPENDING to an existing ADR is allowed: an Edit/Write/MultiEdit whose
result keeps every existing character and only adds text after it. That
is how amendments, graduation-log entries, and supersession notes land.
Expand All @@ -24,6 +25,7 @@
import json
import os
import re
import subprocess
import sys
from pathlib import Path

Expand Down Expand Up @@ -58,6 +60,25 @@ def protected_kind(target, project_dir):
return None


def is_committed(target, project_dir):
"""True when the file exists in HEAD. An ADR that is new in the working
tree is still a draft and may be revised freely; it becomes append-only
once committed. Outside a git repository, or on any git error, assume
committed — the guard fails closed."""
try:
rel = target.resolve().relative_to(project_dir).as_posix()
git = ["git", "-C", str(project_dir)]
head = subprocess.run(git + ["rev-parse", "--verify", "--quiet", "HEAD"],
capture_output=True, timeout=10)
if head.returncode != 0:
return True
found = subprocess.run(git + ["cat-file", "-e", f"HEAD:{rel}"],
capture_output=True, timeout=10)
return found.returncode == 0
except (OSError, ValueError, subprocess.SubprocessError):
return True


def is_pure_append(before, after):
return after != before and after.startswith(before)

Expand Down Expand Up @@ -98,8 +119,8 @@ def guard_file_tool(tool_name, tool_input, project_dir):
"itself is not available to you. " + OWNER_ONLY
)

if not target.exists():
return 0 # new ADR file: always allowed
if not target.exists() or not is_committed(target, project_dir):
return 0 # new ADR file, or a draft not yet committed: always allowed

before = target.read_text()
if tool_name == "Write":
Expand Down Expand Up @@ -154,7 +175,8 @@ def existing_protected(token, bases, project_dir):
candidate = token if os.path.isabs(token) else str(base / token)
for match in glob.glob(candidate) or [candidate]:
p = Path(match)
if p.is_file() and protected_kind(p, project_dir):
kind = protected_kind(p, project_dir) if p.is_file() else None
if kind == "guide" or (kind == "adr" and is_committed(p, project_dir)):
hits.append(p.resolve().relative_to(project_dir))
return hits

Expand Down
25 changes: 25 additions & 0 deletions checks/run_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,30 @@ def check_adr_amendment_link(ctx):
return out


CLAUDE_MD_WORD_BUDGET = 1000


def check_claude_md_scope(ctx):
"""ADR 0019: CLAUDE.md is a contributor guide made of pointers. Its
relative links must resolve (TC-1) and it stays within a word budget so
restated product specification does not creep back (TC-2)."""
out = []
path = ROOT / "CLAUDE.md"
if not path.exists():
out.append(finding("0019", "TC-1", "CLAUDE.md missing — the contributor guide", "CLAUDE.md"))
return out
text = path.read_text()
for target in re.findall(r"\]\(([^)\s]+)\)", text):
if re.match(r"^[a-z][a-z0-9+.-]*:", target) or target.startswith("#"):
continue # external URL or in-page anchor
if not (ROOT / target.split("#", 1)[0]).exists():
out.append(finding("0019", "TC-1", f"link target {target!r} does not exist — fix the pointer or remove it", "CLAUDE.md"))
words = len(text.split())
if words > CLAUDE_MD_WORD_BUDGET:
out.append(finding("0019", "TC-2", f"CLAUDE.md is {words} words — budget {CLAUDE_MD_WORD_BUDGET}. Replace restated content with a link to its source; raising the budget is an amendment to ADR 0019", "CLAUDE.md"))
return out


def check_objective_parity(ctx):
"""ADR 0014: forward (02_objectives) and reverse (07_objective_metrics) passes
both produce `core_objective`. Downstream consumers must be able to substitute
Expand Down Expand Up @@ -464,6 +488,7 @@ def check_filename_convention(ctx):
"objective-parity": check_objective_parity,
"adr-index": check_adr_index,
"adr-amendment-link": check_adr_amendment_link,
"claude-md-scope": check_claude_md_scope,
}


Expand Down
14 changes: 14 additions & 0 deletions checks/tests/test_pretooluse_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,20 @@ def test_inline_script_that_only_mentions_the_adr_folder_in_text_is_allowed(self
"p.write_text(p.read_text() + 'Create docs/adrs/NNNN-short-title.md; see ls docs/adrs/ and docs/adrs/{name}')\nEOF")
self.assertAllowed("Bash", {"command": command})

def test_an_uncommitted_draft_adr_may_be_revised_but_a_committed_one_may_not(self):
def git(*args):
subprocess.run(["git", "-C", str(self.root), "-c", "user.name=t", "-c", "user.email=t@example.invalid",
"-c", "commit.gpgsign=false", *args], check=True, capture_output=True)
git("init", "-q")
git("add", "docs/adrs/0001-example.md")
git("commit", "-q", "-m", "init")
draft = self.root / "docs" / "adrs" / "0002-draft.md"
draft.write_text("# ADR 0002: Draft\n\n## Status\nProposed\n")
self.assertAllowed("Write", {"file_path": str(draft), "content": "# ADR 0002: Rewritten draft\n"})
self.assertAllowed("Bash", {"command": "rm docs/adrs/0002-draft.md"})
self.assertBlocked("Write", {"file_path": str(self.adr), "content": "# rewritten\n"})
self.assertBlocked("Bash", {"command": "rm docs/adrs/0001-example.md"})

def test_bash_may_create_a_new_adr_file(self):
self.assertAllowed("Bash", {"command": "cat > docs/adrs/0002-new.md <<'EOF'\n# ADR 0002\nEOF"})

Expand Down
17 changes: 17 additions & 0 deletions docs/adrs/0006-progressive-disclosure-tiered-engagement.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,20 @@ consecutive clean days — zero findings in every CI run since 2026-07-27.
`checks/enforcement.config.json` is authoritative for current status; the
"warn" label in the Enforcement section above records the status when it was
written.

---

## Amendment (2026-10-12): Where the tier model is implemented (ADR 0019)

The status note at the top says the tier model, escalation signals, and run
conditionality are "implemented in CLAUDE.md". ADR 0019 made CLAUDE.md the
contributor guide and removed its restatement of this model. The
implementation is the `tier`, `run`, and `run_when` frontmatter on each prompt
and the Tier sections of each workflow skill.

One gap is recorded rather than hidden: the escalation procedure in this
ADR's "Skill Behavior" (offer deeper prompts and let the user choose) is
stated in full only in CLAUDE.md's "Runtime rules that exist only here"
section. `product-ideation/SKILL.md` says to insert the Tier 2 prompts, and
`product-flow` and `tech-spec` carry no escalation procedure. Issue #62 tracks
moving it into the plugin.
11 changes: 11 additions & 0 deletions docs/adrs/0012-adr-enforcement-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,3 +304,14 @@ call. A regression test covers the false positive.
**Not machine-checkable:** an amendment that changes another ADR without using
one of the trigger phrases is invisible to TC-4; and the index says which
record to open, not what it means.

---

## Amendment (2026-10-12): A draft ADR is free until committed

Writing ADR 0019 showed a third no-legal-move case in the guard: a new ADR
file became append-only the moment it was first saved, so a draft could not
be corrected before anyone had reviewed it. The guard now protects an ADR
only once it exists in `HEAD`. A file new in the working tree is a draft and
may be revised or removed. Outside a git repository, or on any git error, the
guard assumes the file is committed and stays closed. Covered by a test.
11 changes: 11 additions & 0 deletions docs/adrs/0016-gstack-derived-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,14 @@ no-unsolicited-improvements rule).
- Source review: garrytan/gstack `office-hours`, `plan-ceo-review`,
`autoplan` skills and its per-branch review-log staleness mechanism
(2026-07-24 session).

---

## Amendment (2026-10-12): TC-3 is read against the bundled registry doc (ADR 0019)

TC-3 expects CLAUDE.md's `context.json` schema to document `inputs`. The
schema moved to `plugin/docs/registry-operations.md` with the ADR 0011
amendment of 2026-07-25, and ADR 0019 made CLAUDE.md a contributor guide that
points there. TC-3 now reads: `plugin/docs/registry-operations.md` documents
`inputs` on artifact entries, and `status/SKILL.md` documents the staleness
annotation. Still no dedicated check.
11 changes: 11 additions & 0 deletions docs/adrs/0017-gstack-derived-patterns-second-wave.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,14 @@ Adopt all four, adapted to this framework:
(scope-decision tables), `/learn` (learnings store) — 2026-07-24 session.
- Owner rule: deferred work is tracked as GitHub issues, never in repo
docs (see issues #37–#43 precedent).

---

## Amendment (2026-10-12): TC-4 is read against the bundled registry doc (ADR 0019)

TC-4 expects CLAUDE.md to document `learnings.jsonl`. That documentation
lives in `plugin/docs/registry-operations.md` § Process Learnings since the
ADR 0011 amendment of 2026-07-25, and ADR 0019 made CLAUDE.md a contributor
guide that points there. TC-4 now reads: `plugin/docs/registry-operations.md`
documents `learnings.jsonl` (schema, dedup, recall rules), and all four
workflow skills reference reading it on start. Still no dedicated check.
Loading
Loading