Skip to content
Open
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: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
## [Unreleased]

### Security
- **Cross-repo symlink write guard** (Issue #1019, closes silent cross-repo mutation trap): Adds `_is_cross_repo_protected_write()` helper to `unified_pre_tool.py`. When a session in a consumer repo (e.g., `~/Dev/realign`) follows a `plugins -> ~/Dev/autonomous-dev/plugins` symlink and attempts to write a protected infrastructure file (`hooks/*.py`, `lib/*.py`, `agents/*.md`, etc.), the hook now compares the cwd's resolved repo root against the resolved file's repo root; a mismatch blocks the write regardless of pipeline state. Previously, `_is_pipeline_active()` returning `True` (for the foreign repo's pipeline) caused the edit to be silently allowed, mutating autonomous-dev's source tree. The guard is fail-open: a false negative preserves the existing same-repo pipeline-active check. Wired into: Write/Edit gate (`_check_write_edit_protected`, line ~4254) and Bash gate (`_check_bash_infra_writes`, line ~3518); the Bash gate's pipeline-active short-circuit was refactored from an early return to a per-path `continue` so cross-repo writes are caught even when a pipeline is active. Block message directs to `deploy-all.sh`. 10 new test functions in `tests/unit/hooks/test_cross_repo_symlink_guard.py`: helper unit tests, Write/Edit flow, Bash flow, and deny-cache integration.

- **Phase 3 prompt-injection defense — extend `_wrap_user_input` across GenAI callers** (Issue #1007, follow-up to #960): Adopts the Phase 2 wrapping helper across 8 additional `GenAIAnalyzer` callers (10 sites total), giving the codebase uniform prompt-injection defense at every LLM integration point. New helper `_safe_wrap(text)` in `plugins/autonomous-dev/hooks/genai_utils.py` simplifies adoption (single-line invocation, never raises). Modified callers — Pattern A (kwarg substitution): `complexity_assessor.py` (`feature_description`), `scope_detector.py` (`issue_text`), `issue_scope_detector.py` (`issue_text`), `alignment_assessor.py` (2 sites: `dependencies_sample`/`config_files`/`readme_content`; enum-constrained scalars like `primary_language` left unwrapped), `genai_refactor_analyzer.py` (5 sites covering `doc_content`/`source_content`/`test_source`/`source_under_test`/`function_source`/`references_summary`/`original_analysis`), `security_scan.py` (`line`+`variable_name`; `secret_type` regex-catalog constant unwrapped), `auto_fix_docs.py` (`item_name`; `item_type` constrained literal unwrapped). Pattern B (.format pre-substitution): `feature_completion_detector.py` wraps both `feature` and `evidence` at `.format()` time since `analyzer.analyze()` receives the already-formatted prompt. Audit verified 4 false positives (`error_analyzer.py`, `codebase_analyzer.py`, `enforce_prunable_threshold.py`, `align_project_retrofit.py`) do NOT call `GenAIAnalyzer` and are NOT modified — locked by a parametrized regression test. 26 new test functions: 4 in `tests/unit/hooks/test_genai_utils_safe_wrap.py` (helper guarantees: wraps, escapes, coerces non-string, never raises), 21 in `tests/unit/lib/test_phase3_wrap_adoption.py` (8 wrap-adoption tests parametrized over multi-site files, plus 4 false-positive lock tests), and 1 existing-test adjustment in `tests/unit/lib/test_scope_detector_genai.py` (assertion updated from raw to wrapped form). `docs/INTENT-CLASSIFICATION.md` and `CHANGELOG.md` updated.

### Added
Expand Down
51 changes: 50 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ Development harness for Claude Code. Deterministic enforcement, specialist agent
- **Run `/improve` after `/implement` sessions.** Use `--auto-file` to create GitHub issues.
- **Use `/clear` after each feature.** Prevents context bloat.
- **Deploy with `bash scripts/deploy-all.sh`** — never manual `cp -rf`. Script handles local, remote (Mac Studio), validation, and integrity checks.
- **Don't simplify, redesign, or consolidate agents.** The pipeline, hooks, and enforcement are validated over months of real use. The cost is tokens, not complexity. Complexity is the mechanism.
- **Process applies WHEN doing software development.** The pipeline, hooks, and enforcement exist to ensure thoroughness on real feature work for repos. They should NOT fire on testing, exploration, scratch edits, or non-repo work. The job is to detect *when* software development is happening and apply the right level of process — not to gate every edit. Context-blind enforcement is the bug; intent-aware enforcement is the goal.
- **Hook deadlock protocol**: If a hook blocks legitimate work, surface the block to the user with the proposed bypass and *wait for approval*. Do NOT autonomously create `.claude/.bypass` or set `AUTONOMOUS_DEV_BYPASS=1` — those signals exist for the user to authorize, not for the model to invoke. See [plugins/autonomous-dev/docs/TROUBLESHOOTING.md](plugins/autonomous-dev/docs/TROUBLESHOOTING.md) for bypass mechanics.
- **Plugin source of truth**: `~/Dev/autonomous-dev/plugins/autonomous-dev/` is canonical. Consumer repos (anyclaude, realign, spektiv, homeassistant) receive *copies* via `bash scripts/deploy-all.sh` — never symlinks. A symlink found by `find ~/Dev -maxdepth 4 -name plugins -type l` is a silent cross-repo write trap (Issue #1021); replace with a `deploy-all.sh` copy. See [docs/ARCHITECTURE-OVERVIEW.md](docs/ARCHITECTURE-OVERVIEW.md#distribution-model).

## Build & Test

Expand Down Expand Up @@ -122,3 +124,50 @@ grep -l "search term" ~/.claude/archive/conversations/**/*.jsonl
**Note on `project` values:** Derived from `cwd` basename — worktrees and subdirectories (e.g., `spektiv/frontend`) get their own project row rather than rolling into the parent repo.

**Last Updated**: 2026-04-21
## Documentation Alignment

CLAUDE.md alignment system prevents documentation drift. Automatic validation that CLAUDE.md stays in sync with PROJECT.md and codebase.

**How to check**:
```bash
# Automatic (every commit)
git commit # Pre-commit hook validates CLAUDE.md alignment

# Manual check
python plugins/autonomous-dev/scripts/validate_claude_alignment.py
```

**What gets validated**:
- Version dates (CLAUDE.md shouldn't be older than PROJECT.md)
- Agent counts (documented count matches reality)
- Command availability (documented commands exist)
- Skills status (removed per v2.5.0 guidance)
- Hook documentation (matches implemented hooks)

**If drift detected**:
1. Run validation script to see specific issues
2. Update CLAUDE.md to match actual state
3. Commit the fix
4. Pre-commit hook validates on next commit

## Maintaining Core Philosophy

**The Golden Rule**: UPDATE PROJECT.md FIRST, everything else follows
- PROJECT.md = source of truth for alignment
- orchestrator reads it before any feature work
- Hooks validate against it on every commit

**Priority Updates**:
1. 🔴 ALWAYS: PROJECT.md, orchestrator.md, settings.local.json
2. 🟡 FREQUENTLY: Agent prompts, GenAI prompts, skills
3. 🟢 PERIODICALLY: Documentation, session logs, hooks

**Philosophy Checklist** (before major changes):
- ✅ Trust the model? (GenAI reasoning > rigid Python)
- ✅ Enforce via hooks? (100% reliable blocking)
- ✅ Enhance via agents? (conditional intelligence)
- ✅ PROJECT.md controls alignment? (dynamic scope)
- ✅ Skills used progressively? (no context bloat)
- ✅ Documentation auto-syncs? (no drift)

See `docs/MAINTAINING-PHILOSOPHY.md` for comprehensive guide.
14 changes: 14 additions & 0 deletions docs/ARCHITECTURE-OVERVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,20 @@ autonomous-dev uses a **Diamond Model** — not the traditional TDD pyramid. Acc

---

## Distribution Model

**Canonical source of truth**: `~/Dev/autonomous-dev/plugins/autonomous-dev/`. All other locations are *copies*, distributed via `bash scripts/deploy-all.sh`. Symlinks from consumer repos into this tree are unsupported because they cause silent cross-repo writes (see Issue #1021).

**Per-repo install**: `deploy-all.sh` rsyncs `hooks/`, `commands/`, `agents/`, `lib/`, `templates/`, `config/`, `skills/`, `scripts/` into each consumer repo's `.claude/` directory. Each repo therefore owns its plugin copy; updates require explicit redeploy.

**Global cache**: `deploy-all.sh` also syncs `hooks/`, `lib/`, `config/` to `~/.claude/` so plugin code is reachable from any cwd. Hook *registration* in `~/.claude/settings.json` is opt-in via `--global-settings` (Issue #995).

**Repos in scope**: `LOCAL_REPOS` / `REMOTE_REPOS` env vars in `deploy-all.sh` (default: `autonomous-dev anyclaude realign spektiv homeassistant`).

**Audit invariant**: `find ~/Dev -maxdepth 4 -name plugins -type l` should return zero results. Any symlink found is a cross-repo silent-write trap and must be replaced with a `deploy-all.sh` copy. The `_is_cross_repo_protected_write()` guard in `unified_pre_tool.py` (Issue #1019) now actively blocks Write/Edit/Bash writes that resolve across repo boundaries — so even if a symlink exists, the hook will deny the write before it silently mutates the foreign source tree.

---

## Cross-References

**Related Documentation**:
Expand Down
1 change: 1 addition & 0 deletions docs/HOOKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ This distinction is fundamental: nudges in `systemMessage` are user-readable but

**Infrastructure Protection** (scoped to autonomous-dev repos):
- Write/Edit to `agents/*.md`, `commands/*.md`, `hooks/*.py`, `lib/*.py`, `skills/*/SKILL.md` are blocked outside the `/implement` pipeline
- **Cross-repo symlink guard** (Issue #1019): `_is_cross_repo_protected_write()` additionally blocks any Write/Edit/Bash write that resolves via symlink into a protected path in a *different* repo root, regardless of pipeline state. This closes the silent cross-repo mutation trap where `~/Dev/realign/plugins -> ~/Dev/autonomous-dev/plugins` caused writes in the realign session to silently modify the autonomous-dev source tree. Detection: if `abs_path != abs_path.resolve()` (symlink in play) AND the resolved path is protected infrastructure AND the cwd's owning repo root differs from the resolved file's owning repo root — block. Fails open (false negative preserves existing pipeline-active check). Block message directs to `deploy-all.sh`.
- Scoped to autonomous-dev repos (detected via `_is_autonomous_dev_repo()`) — does not affect user projects
- User-facing docs (`README.md`, `CHANGELOG.md`, `docs/*.md`), config files (`.json`/`.yaml`), and all non-infrastructure paths are unaffected

Expand Down
79 changes: 15 additions & 64 deletions plugins/autonomous-dev/commands/implement.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,39 +119,22 @@ Before EVERY Agent tool dispatch, you MUST run this inline verification. This ca

```bash
python3 -c "
import sys, os, json, time
for _p in ('.claude/lib', 'plugins/autonomous-dev/lib', os.path.expanduser('~/.claude/lib')):
if os.path.isdir(_p):
sys.path.insert(0, _p)
break

# Session-ID fallback chain (Issue #904):
# 1. CLAUDE_SESSION_ID env var (primary — set in-process by Claude Code)
# 2. /tmp/implement_pipeline_state.json['session_id'] (sentinel written at STEP 0)
# — only honored when mtime is within 3600s (avoids cross-pipeline bleed)
# 3. 'unknown' (preserved legacy sentinel — first-boot/pre-STEP-0 case)
def _resolve_session_id():
sid = os.environ.get('CLAUDE_SESSION_ID', '').strip()
if sid and sid != 'unknown':
return sid
sentinel = '/tmp/implement_pipeline_state.json'
try:
if os.path.exists(sentinel):
mtime = os.path.getmtime(sentinel)
if time.time() - mtime < 3600:
with open(sentinel) as _f:
_state = json.load(_f)
_recovered = str(_state.get('session_id', '')).strip()
if _recovered and _recovered != 'unknown':
return _recovered
except (OSError, ValueError, json.JSONDecodeError):
pass
return 'unknown'
import sys, os
for _p in ('.claude/lib', 'plugins/autonomous-dev/lib', os.path.expanduser('~/.claude/lib')):
if os.path.isdir(_p):
sys.path.insert(0, _p)
break
from pipeline_completion_state import resolve_session_id

from agent_ordering_gate import check_ordering_with_session_fallback
result = check_ordering_with_session_fallback(
'TARGET_AGENT',
_resolve_session_id(),
resolve_session_id(),
issue_number=ISSUE_NUMBER_OR_0,
pipeline_mode='MODE'
)
Expand Down Expand Up @@ -237,35 +220,19 @@ save_pipeline(state)
print(f'Pipeline {state.run_id} initialized')
"
python3 -c "
import sys, os, json, time
import json
import sys, os
for _p in ('.claude/lib', 'plugins/autonomous-dev/lib', os.path.expanduser('~/.claude/lib')):
if os.path.isdir(_p):
sys.path.insert(0, _p)
break
from pipeline_state import sign_state
from pipeline_completion_state import resolve_session_id

# Session-ID fallback chain (Issue #904): env → sentinel → 'unknown'.
# Honor a prior-written sentinel when the env var was dropped by a
# subshell, e.g. /implement --resume re-entering STEP 0.
def _resolve_session_id():
sid = os.environ.get('CLAUDE_SESSION_ID', '').strip()
if sid and sid != 'unknown':
return sid
sentinel = '/tmp/implement_pipeline_state.json'
try:
if os.path.exists(sentinel):
mtime = os.path.getmtime(sentinel)
if time.time() - mtime < 3600:
with open(sentinel) as _f:
_state = json.load(_f)
_recovered = str(_state.get('session_id', '')).strip()
if _recovered and _recovered != 'unknown':
return _recovered
except (OSError, ValueError, json.JSONDecodeError):
pass
return 'unknown'

sid = _resolve_session_id()
sid = resolve_session_id()
state = {
'session_start': '$(date +%Y-%m-%dT%H:%M:%S)',
'mode': 'MODE',
Expand Down Expand Up @@ -367,41 +334,25 @@ Read `.claude/PROJECT.md`. If missing: BLOCK ("Run `/setup` or `/align --retrofi

```bash
python3 -c "
import sys, os, json, time
import json
import sys, os
for _p in ('.claude/lib', 'plugins/autonomous-dev/lib', os.path.expanduser('~/.claude/lib')):
if os.path.isdir(_p):
sys.path.insert(0, _p)
break
from pipeline_state import sign_state
from pipeline_completion_state import resolve_session_id

# Session-ID fallback chain (Issue #904): env → sentinel → 'unknown'.
# In a subshell that lost CLAUDE_SESSION_ID (e.g., nested heredoc in a
# pipe), recover the real session_id from the STEP-0 sentinel instead of
# re-signing the state as 'unknown' (which would break HMAC verification).
def _resolve_session_id():
sid = os.environ.get('CLAUDE_SESSION_ID', '').strip()
if sid and sid != 'unknown':
return sid
sentinel = '/tmp/implement_pipeline_state.json'
try:
if os.path.exists(sentinel):
mtime = os.path.getmtime(sentinel)
if time.time() - mtime < 3600:
with open(sentinel) as _f:
_state = json.load(_f)
_recovered = str(_state.get('session_id', '')).strip()
if _recovered and _recovered != 'unknown':
return _recovered
except (OSError, ValueError, json.JSONDecodeError):
pass
return 'unknown'

state_path = '/tmp/implement_pipeline_state.json'
if os.path.exists(state_path):
with open(state_path) as f:
state = json.load(f)
state['alignment_passed'] = True
sid = _resolve_session_id()
sid = resolve_session_id()
state = sign_state(state, sid)
with open(state_path, 'w') as f:
json.dump(state, f)
Expand Down
15 changes: 15 additions & 0 deletions plugins/autonomous-dev/hooks/stop_quality_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,21 @@ def main() -> int:

# Format and output results
output = format_results(results)

# Auto-record pytest-gate completion for active /implement pipelines (Issue #802 wiring).
# 'unknown' guard: only record when a real session_id is resolved, so casual
# end-of-turn pytest passes outside a pipeline don't pollute state.
try:
if results["pytest"].get("ran") and results["pytest"].get("passed") is True:
from hook_stdin import read_stdin_once, extract_session_id
from pipeline_completion_state import resolve_session_id, record_pytest_gate_passed
stdin_data = read_stdin_once()
sid = extract_session_id(stdin_data) or resolve_session_id()
if sid and sid != "unknown":
record_pytest_gate_passed(sid, issue_number=0)
except Exception:
pass # Non-blocking: recording failure must never block the Stop hook

sys.stderr.write(output)

except Exception as e:
Expand Down
Loading
Loading