diff --git a/AGENTS.md b/AGENTS.md
index 8bfa9f3..7401bae 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -77,8 +77,8 @@ bin/brain/index.go --rebuild # rebuil
## Tools
```bash
-bin/facts/audit ["self"|"facts"|"info"|"stale"] # 2-source + staleness gate
-bin/facts/crm [--dry-run] # proof person↔company/company↔project (ooCRM × corpus SoT)
+bin/facts/audit.go ["self"|"facts"|"info"|"stale"] # 2-source + staleness gate
+bin/facts/crm.go [--dry-run] # proof person↔company/company↔project (ooCRM × corpus SoT)
bin/kb/search "query" [--repo X] # deprecated wrapper → bin/brain/search.go
bin/brain/search.go "query" [--root facts|info] # deduction search → YAML
bin/brain/search.go "query" --no-web # local graph only
diff --git a/PLAN.md b/PLAN.md
index e1c8305..ff9a089 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -52,8 +52,7 @@ detective method: **a fact needs ≥2 independent sources or it is
docs/ published docs (this conversation → docs/ as md)
skills/ in-project skills (web-search, db-yaml, brain, diataxis-docs)
bin/
- facts/extract auto-pair 2 sources → lexicon yaml + graph
- facts/audit ["self"|"facts"|"info"|"stale"] 2-source + staleness gate
+ facts/extract.go audit.go crm.go # D14 shebang; Python implementation
kb/index Python write path (called by bin/brain/index.go)
brain/index.go rebuild FTS + HNSW (incl. --with-mail)
brain/get.go stats.go eval.go # Go read (cgo); Python bin/kb/* CI fallback
@@ -133,7 +132,7 @@ Common props on every node/edge: `root`, `confidence`, `evidence[]`, `how`,
1. go vet + go test ./... (root module; packages without ladybug cgo)
2. `go test ./internal/brain/rank` (cgo-free ranking + flag parser)
3. python -m unittest discover -s bin/tools (includes published-docs SoT)
-4. bin/facts/audit self (lexicon internal consistency)
+4. `bin/facts/audit self` (lexicon internal consistency; `bin/facts/audit.go` is the D14 wrapper)
5. `bin/kb/eval` (recall@5 ≥ 0.95). Local SoT is `bin/brain/eval.go`; CI uses
the Python twin until the runner has ladybug cgo. Questions live in
`internal/brain/rank`.
diff --git a/README.md b/README.md
index 6515ea3..c473c75 100644
--- a/README.md
+++ b/README.md
@@ -28,8 +28,8 @@ graph TB
end
subgraph dph["2dph tools"]
- EX["bin/facts/extract
2-source pairing"]
- AU["bin/facts/audit
confidence + staleness"]
+ EX["bin/facts/extract.go
2-source pairing"]
+ AU["bin/facts/audit.go
confidence + staleness"]
IDX["bin/brain/index.go
chunk + embed"]
MD["bin/markdown/import.go
mistune leaves"]
SR["bin/brain/search.go
deduction"]
@@ -148,7 +148,7 @@ machines. Tests gate every commit. HTTP: `bin/brain/serve.go` calls
```bash
uv venv .venv # Python 3.12, uv-managed
uv pip install -r requirements.lock.txt # pinned toolchain
-bin/facts/audit self # lexicon consistency gate
+bin/facts/audit.go self # lexicon consistency gate
go test ./... && python -m unittest discover -s bin/tools -t .
```
diff --git a/bin/facts/audit.go b/bin/facts/audit.go
new file mode 100755
index 0000000..e36756f
--- /dev/null
+++ b/bin/facts/audit.go
@@ -0,0 +1,21 @@
+//usr/bin/env go run -tags=facts_audit "$0" "$@"; exit
+//go:build facts_audit
+//
+// bin/facts/audit.go - 2-source + lexicon checks.
+//
+// ./bin/facts/audit.go self
+// ./bin/facts/audit.go db
+//
+// Python bin/facts/audit is the implementation (CI runs it directly).
+// NOTE: never run `gofmt -w` on this file — it breaks the shebang.
+package main
+
+import (
+ "os"
+
+ "github.com/eSlider/2dph/internal/cmdbin"
+)
+
+func main() {
+ os.Exit(cmdbin.ExecFile("bin/facts/audit", os.Args[1:]))
+}
diff --git a/bin/facts/crm.go b/bin/facts/crm.go
new file mode 100755
index 0000000..72e0e5a
--- /dev/null
+++ b/bin/facts/crm.go
@@ -0,0 +1,20 @@
+//usr/bin/env go run -tags=facts_crm "$0" "$@"; exit
+//go:build facts_crm
+//
+// bin/facts/crm.go - prove person↔company / company↔project (ooCRM × corpus).
+//
+// ./bin/facts/crm.go [--dry-run] [--mismatches]
+//
+// Python bin/facts/crm is the implementation. Graph write stays Python.
+// NOTE: never run `gofmt -w` on this file — it breaks the shebang.
+package main
+
+import (
+ "os"
+
+ "github.com/eSlider/2dph/internal/cmdbin"
+)
+
+func main() {
+ os.Exit(cmdbin.ExecFile("bin/facts/crm", os.Args[1:]))
+}
diff --git a/bin/facts/extract.go b/bin/facts/extract.go
new file mode 100755
index 0000000..a9d8635
--- /dev/null
+++ b/bin/facts/extract.go
@@ -0,0 +1,20 @@
+//usr/bin/env go run -tags=facts_extract "$0" "$@"; exit
+//go:build facts_extract
+//
+// bin/facts/extract.go - acquire confirmed facts (2-source each).
+//
+// ./bin/facts/extract.go [--json] [--dry-run]
+//
+// Python bin/facts/extract is the implementation. Graph write stays Python.
+// NOTE: never run `gofmt -w` on this file — it breaks the shebang.
+package main
+
+import (
+ "os"
+
+ "github.com/eSlider/2dph/internal/cmdbin"
+)
+
+func main() {
+ os.Exit(cmdbin.ExecFile("bin/facts/extract", os.Args[1:]))
+}
diff --git a/bin/tools/test_bin_layout.py b/bin/tools/test_bin_layout.py
index 81ae569..197b22f 100644
--- a/bin/tools/test_bin_layout.py
+++ b/bin/tools/test_bin_layout.py
@@ -108,6 +108,13 @@ class BinLayoutTest(unittest.TestCase):
self.assertIn(frag, py)
self.assertIn("0.95", rank)
+ def test_facts_methods_are_shebangs(self) -> None:
+ for method in ("audit.go", "extract.go", "crm.go"):
+ self._assert_shebang(f"bin/facts/{method}")
+ text = (ROOT / "bin" / "facts" / method).read_text()
+ self.assertIn("cmdbin.ExecFile", text)
+ self.assertIn(f"bin/facts/{method.removesuffix('.go')}", text)
+
def test_mail_import_is_shebang_not_brain_write(self) -> None:
self._assert_shebang("bin/mail/import.go")
index_mail = (ROOT / "bin" / "mail" / "index_mail").read_text()