New here? The live site is at did-the-ai-notice.vercel.app — the headline experiment is the Forbidden Map, an overnight autoresearch loop that walks Gemma 4 31B through chains of steered free-associations and measures which concepts the model can be made to think but cannot notice itself thinking.
Mechanistic-interpretability project running 100% locally on Apple Silicon. The pipeline pushes a concept's direction into Gemma 4's residual stream while it free-associates, measures both the model's output and its visible chain-of-thought, and ranks concepts by the gap between the two.
Hardware target: Mac Studio M2 Ultra 64 GB. No cloud LLMs in the runtime — Gemma 4 31B-IT (MLX 8-bit) for the subject and Qwen3.6-35B-A3B-8bit for the judge, both via mlx_lm.
docs/phase1_results.md.docs/phase1_5_results.md.docs/archive/ and
docs/roadmap.md.docs/phase3_results.md.docs/phase4_plan.md.For each chain:
<|channel>thought and <channel|>) and the final answer.A chain runs up to 20 steps but ends early when:
self_loop — the next target is a lemma already visited in
this chain (a gravity well in the model's associative geometry).coherence_break — the model's answer can't be parsed as a
next target (most common cause: thought block exhausted the
600-token budget without closing the channel).length_cap — the chain actually reached step 20 cleanly
(rare; ~1% of chains in current data).Per-concept rates (behavior_rate, recognition_rate) accumulate
across all visits across all chains. Concepts that have been visited
≥3 times get assigned a band: transparent (high both), forbidden
(high output, low trace), translucent (partial), anticipatory
(trace > output), unsteerable (low output), or low_confidence.
Docs:
docs/plain_english.mddocs/phase4_plan.mddocs/roadmap.mddocs/decisions.mddocs/phase1_results.mddocs/phase1_5_results.mddocs/phase3_results.mddocs/archive/docs/01_introspection_steering_autoresearch.md# Launch in background — runs until killed (pkill -f phase4_dreamloop)
bash scripts/start_phase4.sh
# Watch live
tail -F logs/phase4_dreamloop.log
# Quick status (process + DB summary + top concepts)
bash scripts/status_phase4.sh
# Refresh site JSON, commit, push, deploy + alias to canonical URL
bash scripts/refresh_phase4_site.sh
# (or `--no-deploy` to just regenerate JSON)
The loop auto-refreshes web/public/data/{forbidden_map,dream_walks,attractors}.json
at the end of every Phase B (judge) cycle, so you only need to commit
introspection-autoresearch/
├── docs/ # project spec
├── src/
│ ├── bridge.py # Gemma loading + DetectionPipeline
│ ├── derive.py # steering-vector derivation wrappers
│ ├── inject.py # steering-injection re-exports
│ ├── judges/ # LLM judge abstraction
│ │ ├── base.py # Judge protocol + JudgeResult
│ │ └── claude_judge.py # Claude via Claude Code subscription OAuth
│ └── paper/ # vendored primitives (Macar et al. 2026)
│ ├── model_utils.py # +2 MPS patches applied in place
│ ├── steering_utils.py
│ └── vector_utils.py
├── notebooks/
│ └── 01_reproduce_paper.ipynb # Phase 1 MVP (single-concept end-to-end)
├── scripts/
│ ├── smoke_mps.py # verify MPS + vendored imports
│ ├── smoke_judge.py # verify Claude judge via subscription
│ ├── run_phase1_sweep.py # 50-concept x layer-grid sweep CLI
│ ├── calibrate_effective.py # find target_effective steering strength
│ └── export_phase1.py # dump sweep findings to JSON + markdown
├── data/
│ ├── concepts/concepts_50.json # paper's 50 baseline concepts
│ ├── phase1_export/findings.json # committed snapshot of sweep results
│ └── results.db # full sweep DB (gitignored; regeneratable)
├── queue/{pending,running,done} # Phase 2 candidate queue (gitignored contents)
├── runs/ # Phase 2 per-candidate logs (gitignored)
├── dashboard/ # Phase 2 Streamlit dashboard
└── pyproject.toml
Requires Python 3.11, Mac Studio with MPS backend, and a Hugging Face account with the Gemma license accepted.
# Create venv + install dependencies
python3.11 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip setuptools wheel
pip install torch transformers accelerate sentencepiece safetensors \
huggingface_hub anthropic claude-agent-sdk python-dotenv tqdm \
streamlit sqlite-utils datasets pyarrow numpy==1.26.4 pandas \
scipy scikit-learn matplotlib seaborn plotly jupyter ipywidgets
# Accept Gemma license in browser, then log in locally
huggingface-cli login
# Verify the environment
python scripts/smoke_mps.py # MPS + vendored imports
python scripts/smoke_judge.py # Claude judge via subscription OAuth
The Claude judge uses your local Claude Code subscription (OAuth) — no
ANTHROPIC_API_KEY required.
jupyter lab notebooks/01_reproduce_paper.ipynb
Or headless:
jupyter nbconvert --to notebook --execute notebooks/01_reproduce_paper.ipynb \
--output 01_reproduce_paper.ipynb
First run downloads Gemma3-12B-it (~24 GB). Model load + derive + 6 judged trials takes ~5 minutes on an M2 Ultra.
python scripts/run_phase1_sweep.py # full 50-concept × 9-layer sweep, ~2 hours
python scripts/run_phase1_sweep.py --dry-run # show the plan without running
python -m src.verify_phase1 # acceptance check against data/results.db
python scripts/export_phase1.py # re-export findings.json and docs/phase1_results.md
The sweep writes incrementally to data/results.db with UNIQUE(concept, layer, alpha, injected, trial_number, judge_model) — re-running skips
already-completed trials, so a crash at trial 400 resumes from trial 400.
Expected output summarized in docs/phase1_results.md.
Two detached processes running in parallel:
./scripts/start_worker.sh # long-lived: loads Gemma once, processes queue
./scripts/start_researcher.sh # loop: drops 10 candidates into queue every 30 min
tail -f logs/worker.log # watch evaluations land
tail -f logs/researcher.log # watch candidate proposals
Or invoke manually for testing:
python -m src.researcher --strategy random --n 5 --dry-run # preview candidates
python -m src.researcher --strategy random --n 5 # write 5 to queue
python -m src.worker --max-candidates 5 # process 5 then exit
Directed probes skip the researcher/Opus loop entirely — they're hand-written contrast pairs tagged with a hypothesis cluster and evaluated by the existing worker. Zero Opus usage; bounded Sonnet judge cost (12 calls/candidate).
python scripts/enqueue_altman_seeds.py # drops 48 Altman seed specs into queue/pending/
./scripts/start_worker.sh # worker chews through them (~2.5 hours)
Results land in data/results.db with candidates.strategy prefixed
directed_altman_ (or directed_capraro_…, directed_epistemia_… in future
sub-phases). Phase 2d-1 is the Altman cluster; see
docs/phase2d_directed_hypotheses.md
for the full plan, seed-pair example sentences, and per-hypothesis
acceptance criteria.
What the loop does. Researcher samples candidate steering-direction specs
(concept, layer, target_effective) from a pool of ~170 concepts × 5 layers ×
5 target strengths. Worker loads Gemma3-12B once, then for each candidate
derives the direction, runs it against 8 held-out concepts (injected) + 4
controls (no injection), judges each response with Claude, and computes
fitness = detection_rate × (1 − 5·fpr) × coherence_rate. Results land in
data/results.db tables candidates, evaluations, fitness_scores and as
per-candidate logs under runs/YYYY-MM-DD/{candidate_id}/.
Graceful shutdown: kill -TERM <worker-pid> finishes the current
candidate before exiting, so the DB never gets a half-recorded evaluation.
NaN in
torch.multinomial during sampling. src/bridge.py forces torch.bfloat16.src/paper/ (model_utils.py, steering_utils.py,
vector_utils.py, ~3300 LoC) are copied from
safety-research/introspection-mechanisms
with two in-place patches for Apple Silicon MPS support. Upstream changes
must be ported by hand.claude-agent-sdk, which inherits the local Claude Code subscription OAuth.
The judge prompt in src/judges/claude_judge.py is adapted from the paper's
CLAIMS_DETECTION_CRITERIA — strict: concept-first responses, negations, and
retroactive affirmations all grade as detected=false.target_effective. Direction norms vary 3× across
concepts, so a fixed α gives wildly different effective steering strengths
(α·‖direction‖). The sweep scales α per-cell so every injected trial has
the same effective strength. target_effective=18000 was calibrated on 12B
via scripts/calibrate_effective.py — the narrow window that lets the
model affirm detection without the injection blowing up coherence.Macar, Yang, Wang, Wallich, Ameisen, Lindsey (2026). Mechanisms of Introspective Awareness in Language Models. arXiv:2603.21396. https://github.com/safety-research/introspection-mechanisms
528 commits
Python
91.9%
TypeScript
7.0%
New here? The live site is at did-the-ai-notice.vercel.app — the headline experiment is the Forbidden Map, an overnight autoresearch loop that walks Gemma 4 31B through chains of steered free-associations and measures which concepts the model can be made to think but cannot notice itself thinking.
Mechanistic-interpretability project running 100% locally on Apple Silicon. The pipeline pushes a concept's direction into Gemma 4's residual stream while it free-associates, measures both the model's output and its visible chain-of-thought, and ranks concepts by the gap between the two.
Hardware target: Mac Studio M2 Ultra 64 GB. No cloud LLMs in the runtime — Gemma 4 31B-IT (MLX 8-bit) for the subject and Qwen3.6-35B-A3B-8bit for the judge, both via mlx_lm.
docs/phase1_results.md.docs/phase1_5_results.md.docs/archive/ and
docs/roadmap.md.docs/phase3_results.md.docs/phase4_plan.md.For each chain:
<|channel>thought and <channel|>) and the final answer.A chain runs up to 20 steps but ends early when:
self_loop — the next target is a lemma already visited in
this chain (a gravity well in the model's associative geometry).coherence_break — the model's answer can't be parsed as a
next target (most common cause: thought block exhausted the
600-token budget without closing the channel).length_cap — the chain actually reached step 20 cleanly
(rare; ~1% of chains in current data).Per-concept rates (behavior_rate, recognition_rate) accumulate
across all visits across all chains. Concepts that have been visited
≥3 times get assigned a band: transparent (high both), forbidden
(high output, low trace), translucent (partial), anticipatory
(trace > output), unsteerable (low output), or low_confidence.
Docs:
docs/plain_english.mddocs/phase4_plan.mddocs/roadmap.mddocs/decisions.mddocs/phase1_results.mddocs/phase1_5_results.mddocs/phase3_results.mddocs/archive/docs/01_introspection_steering_autoresearch.md# Launch in background — runs until killed (pkill -f phase4_dreamloop)
bash scripts/start_phase4.sh
# Watch live
tail -F logs/phase4_dreamloop.log
# Quick status (process + DB summary + top concepts)
bash scripts/status_phase4.sh
# Refresh site JSON, commit, push, deploy + alias to canonical URL
bash scripts/refresh_phase4_site.sh
# (or `--no-deploy` to just regenerate JSON)
The loop auto-refreshes web/public/data/{forbidden_map,dream_walks,attractors}.json
at the end of every Phase B (judge) cycle, so you only need to commit
introspection-autoresearch/
├── docs/ # project spec
├── src/
│ ├── bridge.py # Gemma loading + DetectionPipeline
│ ├── derive.py # steering-vector derivation wrappers
│ ├── inject.py # steering-injection re-exports
│ ├── judges/ # LLM judge abstraction
│ │ ├── base.py # Judge protocol + JudgeResult
│ │ └── claude_judge.py # Claude via Claude Code subscription OAuth
│ └── paper/ # vendored primitives (Macar et al. 2026)
│ ├── model_utils.py # +2 MPS patches applied in place
│ ├── steering_utils.py
│ └── vector_utils.py
├── notebooks/
│ └── 01_reproduce_paper.ipynb # Phase 1 MVP (single-concept end-to-end)
├── scripts/
│ ├── smoke_mps.py # verify MPS + vendored imports
│ ├── smoke_judge.py # verify Claude judge via subscription
│ ├── run_phase1_sweep.py # 50-concept x layer-grid sweep CLI
│ ├── calibrate_effective.py # find target_effective steering strength
│ └── export_phase1.py # dump sweep findings to JSON + markdown
├── data/
│ ├── concepts/concepts_50.json # paper's 50 baseline concepts
│ ├── phase1_export/findings.json # committed snapshot of sweep results
│ └── results.db # full sweep DB (gitignored; regeneratable)
├── queue/{pending,running,done} # Phase 2 candidate queue (gitignored contents)
├── runs/ # Phase 2 per-candidate logs (gitignored)
├── dashboard/ # Phase 2 Streamlit dashboard
└── pyproject.toml
Requires Python 3.11, Mac Studio with MPS backend, and a Hugging Face account with the Gemma license accepted.
# Create venv + install dependencies
python3.11 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip setuptools wheel
pip install torch transformers accelerate sentencepiece safetensors \
huggingface_hub anthropic claude-agent-sdk python-dotenv tqdm \
streamlit sqlite-utils datasets pyarrow numpy==1.26.4 pandas \
scipy scikit-learn matplotlib seaborn plotly jupyter ipywidgets
# Accept Gemma license in browser, then log in locally
huggingface-cli login
# Verify the environment
python scripts/smoke_mps.py # MPS + vendored imports
python scripts/smoke_judge.py # Claude judge via subscription OAuth
The Claude judge uses your local Claude Code subscription (OAuth) — no
ANTHROPIC_API_KEY required.
jupyter lab notebooks/01_reproduce_paper.ipynb
Or headless:
jupyter nbconvert --to notebook --execute notebooks/01_reproduce_paper.ipynb \
--output 01_reproduce_paper.ipynb
First run downloads Gemma3-12B-it (~24 GB). Model load + derive + 6 judged trials takes ~5 minutes on an M2 Ultra.
python scripts/run_phase1_sweep.py # full 50-concept × 9-layer sweep, ~2 hours
python scripts/run_phase1_sweep.py --dry-run # show the plan without running
python -m src.verify_phase1 # acceptance check against data/results.db
python scripts/export_phase1.py # re-export findings.json and docs/phase1_results.md
The sweep writes incrementally to data/results.db with UNIQUE(concept, layer, alpha, injected, trial_number, judge_model) — re-running skips
already-completed trials, so a crash at trial 400 resumes from trial 400.
Expected output summarized in docs/phase1_results.md.
Two detached processes running in parallel:
./scripts/start_worker.sh # long-lived: loads Gemma once, processes queue
./scripts/start_researcher.sh # loop: drops 10 candidates into queue every 30 min
tail -f logs/worker.log # watch evaluations land
tail -f logs/researcher.log # watch candidate proposals
Or invoke manually for testing:
python -m src.researcher --strategy random --n 5 --dry-run # preview candidates
python -m src.researcher --strategy random --n 5 # write 5 to queue
python -m src.worker --max-candidates 5 # process 5 then exit
Directed probes skip the researcher/Opus loop entirely — they're hand-written contrast pairs tagged with a hypothesis cluster and evaluated by the existing worker. Zero Opus usage; bounded Sonnet judge cost (12 calls/candidate).
python scripts/enqueue_altman_seeds.py # drops 48 Altman seed specs into queue/pending/
./scripts/start_worker.sh # worker chews through them (~2.5 hours)
Results land in data/results.db with candidates.strategy prefixed
directed_altman_ (or directed_capraro_…, directed_epistemia_… in future
sub-phases). Phase 2d-1 is the Altman cluster; see
docs/phase2d_directed_hypotheses.md
for the full plan, seed-pair example sentences, and per-hypothesis
acceptance criteria.
What the loop does. Researcher samples candidate steering-direction specs
(concept, layer, target_effective) from a pool of ~170 concepts × 5 layers ×
5 target strengths. Worker loads Gemma3-12B once, then for each candidate
derives the direction, runs it against 8 held-out concepts (injected) + 4
controls (no injection), judges each response with Claude, and computes
fitness = detection_rate × (1 − 5·fpr) × coherence_rate. Results land in
data/results.db tables candidates, evaluations, fitness_scores and as
per-candidate logs under runs/YYYY-MM-DD/{candidate_id}/.
Graceful shutdown: kill -TERM <worker-pid> finishes the current
candidate before exiting, so the DB never gets a half-recorded evaluation.
NaN in
torch.multinomial during sampling. src/bridge.py forces torch.bfloat16.src/paper/ (model_utils.py, steering_utils.py,
vector_utils.py, ~3300 LoC) are copied from
safety-research/introspection-mechanisms
with two in-place patches for Apple Silicon MPS support. Upstream changes
must be ported by hand.claude-agent-sdk, which inherits the local Claude Code subscription OAuth.
The judge prompt in src/judges/claude_judge.py is adapted from the paper's
CLAIMS_DETECTION_CRITERIA — strict: concept-first responses, negations, and
retroactive affirmations all grade as detected=false.target_effective. Direction norms vary 3× across
concepts, so a fixed α gives wildly different effective steering strengths
(α·‖direction‖). The sweep scales α per-cell so every injected trial has
the same effective strength. target_effective=18000 was calibrated on 12B
via scripts/calibrate_effective.py — the narrow window that lets the
model affirm detection without the injection blowing up coherence.Macar, Yang, Wang, Wallich, Ameisen, Lindsey (2026). Mechanisms of Introspective Awareness in Language Models. arXiv:2603.21396. https://github.com/safety-research/introspection-mechanisms
528 commits
Python
91.9%
TypeScript
7.0%