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
27 changes: 27 additions & 0 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,33 @@ python3 -m factlog doctor # checks Python 3.11+ and pyrewire
python3 -m factlog init --target ~/wiki # scaffold the KB layout
```

### Windows Python executable

On Windows, the `python3` command can point to the Microsoft Store stub instead
of a real Python executable. In that state, `python` or `py` may work while the
plugin's bundled scripts fail.

Check these first:

```powershell
python3 --version
python --version
py -0p
```

If `python3 --version` only prints `Python`, fails, or opens Microsoft Store,
tell factlog which Python to use. For a venv:

```powershell
py -3.12 -m venv .venv
.\.venv\Scripts\python.exe -m pip install -e <path-to-factlog-repo>
$env:FACTLOG_PYTHON = (Resolve-Path .\.venv\Scripts\python.exe).Path
```

The plugin hooks and skill commands use
`${CLAUDE_PLUGIN_ROOT}/tools/factlog_python.sh`, which resolves Python 3.11+ in
this order: `$FACTLOG_PYTHON`, `python3`, `python`, then `py`.

If your Python is externally managed (PEP 668), pip will refuse to install into it; `setup` prints venv guidance instead of forcing the install. Create and activate a venv, then re-run `setup`:

```bash
Expand Down
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,33 @@ python3 -m factlog doctor # checks Python 3.11+ and pyrewire
python3 -m factlog init --target ~/wiki # scaffold the KB layout
```

### Windows Python 실행 파일

Windows에서는 `python3` 명령이 실제 Python이 아니라 Microsoft Store stub을
가리킬 수 있습니다. 이 경우 `python` 또는 `py`는 정상이어도 플러그인의 번들
스크립트가 실패할 수 있습니다.

먼저 다음을 확인하십시오.

```powershell
python3 --version
python --version
py -0p
```

`python3 --version`이 `Python`만 출력하고 실패하거나 Microsoft Store를 여는
상태라면, factlog가 사용할 Python을 명시하십시오. venv를 쓰는 경우:

```powershell
py -3.12 -m venv .venv
.\.venv\Scripts\python.exe -m pip install -e <path-to-factlog-repo>
$env:FACTLOG_PYTHON = (Resolve-Path .\.venv\Scripts\python.exe).Path
```

플러그인의 hook과 skill 명령은 `${CLAUDE_PLUGIN_ROOT}/tools/factlog_python.sh`
를 통해 `$FACTLOG_PYTHON`, `python3`, `python`, `py` 순서로 Python 3.11+
실행 파일을 찾습니다.

여러분의 Python이 외부 관리(PEP 668) 상태라면 pip이 그 안으로의 설치를
거부합니다. 이때 `setup` 은 설치를 강행하는 대신 venv 안내를 출력합니다. venv를
만들어 활성화한 뒤 `setup` 을 다시 실행하십시오.
Expand Down
6 changes: 3 additions & 3 deletions factlog/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1243,7 +1243,7 @@ def _find_requirements():


def _install_requirements(requirements) -> int:
"""Attempt ``python3 -m pip install -r <requirements>``.
"""Attempt ``sys.executable -m pip install -r <requirements>``.

PEP 668 handling: if pip refuses because the environment is
externally-managed, DO NOT pass --break-system-packages. Print actionable
Expand Down Expand Up @@ -1278,9 +1278,9 @@ def _install_requirements(requirements) -> int:
"--break-system-packages. Create and activate a virtual environment,\n"
"then re-run setup:\n"
"\n"
" python3 -m venv ~/.factlog-venv\n"
" python -m venv ~/.factlog-venv\n"
" source ~/.factlog-venv/bin/activate\n"
" python3 -m factlog setup --target <kb>\n",
" python -m factlog setup --target <kb>\n",
file=sys.stderr,
)
else:
Expand Down
46 changes: 30 additions & 16 deletions hooks/gate_check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,27 +34,33 @@
# KB root: set FACTLOG_ROOT to the knowledge-base root for sound path matching;
# falls back to the current working directory when unset.
#
# Fail-closed: if python3 is unavailable or target-path extraction fails for an
# engine-input-shaped payload, the gate denies rather than silently allowing.
# Fail-closed: if a usable Python 3.11+ is unavailable or target-path extraction
# fails for an engine-input-shaped payload, the gate denies rather than silently
# allowing.

set -euo pipefail

payload="$(cat)"
payload="$(</dev/stdin)"

# Determine the KB root: prefer FACTLOG_ROOT, fall back to cwd.
KB_ROOT="${FACTLOG_ROOT:-.}"

# python3 is required for JSON parsing and portable path/mtime handling.
HOOK_DIR="$(cd "${BASH_SOURCE[0]%/*}" && pwd)"
PYTHON_RUNNER_SCRIPT="${FACTLOG_PYTHON_RUNNER:-"$HOOK_DIR/../tools/factlog_python.sh"}"
PYTHON_RUNNER=( "${BASH:-bash}" "$PYTHON_RUNNER_SCRIPT" )

# Python 3.11+ is required for JSON parsing and portable path/mtime handling.
# Fail closed: without it we cannot evaluate the predicate safely.
if ! command -v python3 &>/dev/null; then
echo "[factlog GATE] DENIED: python3 is required to evaluate the gate predicate." >&2
if ! "${PYTHON_RUNNER[@]}" -c 'import sys' >/dev/null 2>&1; then
echo "[factlog GATE] DENIED: usable Python 3.11+ is required to evaluate the gate predicate." >&2
echo " Set FACTLOG_PYTHON to a venv/system python if python3 is unavailable or is a Windows Store stub." >&2
exit 2
fi

# Extract the tool target from the hook payload.
# Claude Code sends the tool input as JSON on stdin.
# The relevant field is "file_path" for Write and "file_path" for Edit.
target_path="$(printf '%s' "$payload" | python3 -c \
target_path="$(printf '%s' "$payload" | "${PYTHON_RUNNER[@]}" -c \
"import json,sys; d=json.load(sys.stdin); print(d.get('file_path','') or d.get('path',''))" \
2>/dev/null || true)"

Expand All @@ -68,12 +74,12 @@ fi
# Normalise: check whether the target is facts/accepted.dl or facts/query.dl
# under the KB root. Match both absolute and relative paths.
#
# Use python3 for portable path canonicalisation — realpath -m is GNU-only and
# is not available on macOS/BSD. python3 os.path.realpath resolves symlinks and
# Use Python for portable path canonicalisation — realpath -m is GNU-only and
# is not available on macOS/BSD. os.path.realpath resolves symlinks and
# normalises . / .. segments on all platforms without requiring the path to
# exist (matching realpath -m semantics).
_canon() {
python3 -c "import os,sys; print(os.path.realpath(os.path.abspath(os.path.expanduser(sys.argv[1]))))" "$1" 2>/dev/null || printf '%s' "$1"
"${PYTHON_RUNNER[@]}" -c "import os,sys; print(os.path.realpath(os.path.abspath(os.path.expanduser(sys.argv[1]))))" "$1" 2>/dev/null || printf '%s' "$1"
}

abs_target="$(_canon "$target_path")"
Expand Down Expand Up @@ -111,30 +117,38 @@ fi
if [ ! -f "$report" ]; then
echo "[factlog GATE] DENIED: facts/logic_report.txt does not exist." >&2
echo " An engine input already exists but no report supersedes it." >&2
echo " Run /factlog check (python3 \"\${CLAUDE_PLUGIN_ROOT}\"/tools/run_logic_check.py)" >&2
echo " Run /factlog check (\"\${CLAUDE_PLUGIN_ROOT}\"/tools/factlog_python.sh \"\${CLAUDE_PLUGIN_ROOT}\"/tools/run_logic_check.py)" >&2
echo " to produce a fresh report before editing engine inputs." >&2
exit 2
fi

_mtime() {
local value
if value="$(stat -c %Y "$1" 2>/dev/null)" || value="$(stat -f %m "$1" 2>/dev/null)"; then
printf '%s\n' "$value"
return 0
fi
echo "[factlog GATE] DENIED: could not read mtime for $1" >&2
exit 2
}

# Find the most recently modified engine input file that exists.
# python3 availability is already guaranteed by the fail-closed check at the
# top of this script, so the mtime probes below call it unconditionally.
newest_input_mtime=0
for f in "$accepted" "$query"; do
if [ -f "$f" ]; then
mtime="$(python3 -c 'import os,sys; print(int(os.path.getmtime(sys.argv[1])))' "$f" 2>/dev/null || echo 0)"
mtime="$(_mtime "$f")"
if [ "$mtime" -gt "$newest_input_mtime" ]; then
newest_input_mtime="$mtime"
fi
fi
done

report_mtime="$(python3 -c 'import os,sys; print(int(os.path.getmtime(sys.argv[1])))' "$report" 2>/dev/null || echo 0)"
report_mtime="$(_mtime "$report")"

if [ "$report_mtime" -lt "$newest_input_mtime" ]; then
echo "[factlog GATE] DENIED: facts/logic_report.txt is stale." >&2
echo " The report predates the last modification to facts/accepted.dl or facts/query.dl." >&2
echo " Run /factlog check (python3 \"\${CLAUDE_PLUGIN_ROOT}\"/tools/run_logic_check.py)" >&2
echo " Run /factlog check (\"\${CLAUDE_PLUGIN_ROOT}\"/tools/factlog_python.sh \"\${CLAUDE_PLUGIN_ROOT}\"/tools/run_logic_check.py)" >&2
echo " to refresh the report before editing engine inputs." >&2
exit 2
fi
Expand Down
4 changes: 2 additions & 2 deletions hooks/gate_reminder.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
# delivery plan (T3) using a PreToolUse deny on the relevant action. A Stop hook
# cannot block, so enforcement must sit on a tool action, not on completion.

payload="$(cat)"
payload="$(</dev/stdin)"

if printf '%s' "$payload" | grep -Eq 'facts/(query\.dl|candidates\.csv|accepted\.dl)|policy/logic-policy\.dl'; then
echo "[factlog] An engine input was edited. Run the logic check before concluding:" >&2
echo " python3 \"\${CLAUDE_PLUGIN_ROOT}\"/tools/run_logic_check.py" >&2
echo " \"\${CLAUDE_PLUGIN_ROOT}\"/tools/factlog_python.sh \"\${CLAUDE_PLUGIN_ROOT}\"/tools/run_logic_check.py" >&2
echo " then show facts/logic_report.txt verbatim. Candidates are not engine input until confirmed." >&2
fi

Expand Down
36 changes: 18 additions & 18 deletions skills/factlog/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ description: >-
logic check, and attempt gated self-correction. Use when the user asks to
"sync facts", "check the wiki", "run factlog", "verify facts", or update a
knowledge base from its source documents.
allowed-tools: Bash(python3 *) Read Edit Write Grep Glob
allowed-tools: Bash(*factlog_python.sh *) Bash(python3 *) Bash(python *) Bash(py *) Read Edit Write Grep Glob
---

# factlog — Agent Bridge
Expand All @@ -23,7 +23,7 @@ gate is also backed by a plugin hook (`hooks/hooks.json`).

1. Treat every fact/query you generate as `candidate`/draft — never promote it
to engine input yourself.
2. Always run `python3 ${CLAUDE_PLUGIN_ROOT}/tools/run_logic_check.py` and show
2. Always run `"${CLAUDE_PLUGIN_ROOT}/tools/factlog_python.sh" "${CLAUDE_PLUGIN_ROOT}/tools/run_logic_check.py"` and show
the resulting `facts/logic_report.txt` **verbatim** before stating any
conclusion.
3. If the report shows `errors > 0`, return to the human instead of concluding.
Expand Down Expand Up @@ -61,14 +61,14 @@ the four operating commands below — it is the first thing to do after
separate terminal:

```bash
python3 -m factlog setup --target <kb>
"${CLAUDE_PLUGIN_ROOT}/tools/factlog_python.sh" -m factlog setup --target <kb>
```

In order, `setup`:

1. Runs the `doctor` checks and reports Python / pyrewire status.
2. If pyrewire is missing or `< 1.0.1`, installs it via
`python3 -m pip install -r <requirements.txt>` (located via
`"${CLAUDE_PLUGIN_ROOT}/tools/factlog_python.sh" -m pip install -r <requirements.txt>` (located via
`$CLAUDE_PLUGIN_ROOT` if set, else the package root). If pyrewire already
satisfies the floor, the install is skipped.
3. Runs the KB `init` for `--target` (scaffolds `sources/`, `facts/`,
Expand All @@ -84,9 +84,9 @@ will refuse to install into it. `setup` does **not** override this with
Create and activate a virtual environment, then re-run:

```bash
python3 -m venv ~/.factlog-venv
"${CLAUDE_PLUGIN_ROOT}/tools/factlog_python.sh" -m venv ~/.factlog-venv
source ~/.factlog-venv/bin/activate
python3 -m factlog setup --target <kb>
"${CLAUDE_PLUGIN_ROOT}/tools/factlog_python.sh" -m factlog setup --target <kb>
```

After `setup` succeeds, use the four operating commands — `/factlog sync`,
Expand All @@ -106,7 +106,7 @@ step is extraction.
### Step 1 — Place the source

- A binary/office file (`.docx`, `.pdf`, ...): run
`python3 -m factlog ingest <path> --target "$FACTLOG_ROOT"` (or `--scan`)
`"${CLAUDE_PLUGIN_ROOT}/tools/factlog_python.sh" -m factlog ingest <path> --target "$FACTLOG_ROOT"` (or `--scan`)
→ it writes a text conversion into `runs/sources/`.
- Free text or a text file: place it under `sources/<name>` (text is read
verbatim by extraction).
Expand All @@ -120,7 +120,7 @@ new source and write candidate rows to `runs/<iso>-<slug>.json` — identical to
### Step 3 — Finalise deterministically (one command)

```bash
python3 "${CLAUDE_PLUGIN_ROOT}/tools/finalize.py" --target "$FACTLOG_ROOT"
"${CLAUDE_PLUGIN_ROOT}/tools/factlog_python.sh" "${CLAUDE_PLUGIN_ROOT}/tools/finalize.py" --target "$FACTLOG_ROOT"
```

`finalize.py` chains the deterministic engine steps — `merge_candidates` →
Expand Down Expand Up @@ -211,7 +211,7 @@ Extraction reads `sources/` files as text, so binary/office originals
first:

```bash
python3 -m factlog ingest --scan --target "$FACTLOG_ROOT"
"${CLAUDE_PLUGIN_ROOT}/tools/factlog_python.sh" -m factlog ingest --scan --target "$FACTLOG_ROOT"
```

`--scan` auto-discovers every binary file under `sources/` and writes a text
Expand Down Expand Up @@ -281,7 +281,7 @@ Run merge_candidates.py to normalise, deduplicate, write `facts/candidates.csv`,
regenerate `pages/`, and update `decisions/open-questions.md`:

```bash
python3 "${CLAUDE_PLUGIN_ROOT}/tools/merge_candidates.py" --wiki "$FACTLOG_ROOT"
"${CLAUDE_PLUGIN_ROOT}/tools/factlog_python.sh" "${CLAUDE_PLUGIN_ROOT}/tools/merge_candidates.py" --wiki "$FACTLOG_ROOT"
```

The script reads all `runs/*.json` files (see `--input` for a custom glob).
Expand Down Expand Up @@ -369,7 +369,7 @@ check, and display the full report verbatim.
### Step 1 — Compile accepted facts

```bash
python3 "${CLAUDE_PLUGIN_ROOT}/tools/compile_facts.py"
"${CLAUDE_PLUGIN_ROOT}/tools/factlog_python.sh" "${CLAUDE_PLUGIN_ROOT}/tools/compile_facts.py"
```

Reads `facts/candidates.csv`, filters rows with `status` in
Expand All @@ -388,7 +388,7 @@ human decisions are preserved across re-merge.
### Step 2 — Run the logic check

```bash
python3 "${CLAUDE_PLUGIN_ROOT}/tools/run_logic_check.py"
"${CLAUDE_PLUGIN_ROOT}/tools/factlog_python.sh" "${CLAUDE_PLUGIN_ROOT}/tools/run_logic_check.py"
```

Runs the wirelog/pyrewire engine over `facts/accepted.dl`,
Expand Down Expand Up @@ -426,7 +426,7 @@ A free-text wiki cannot tell you what it *failed* to capture. Run the coverage
critic to surface sources the KB has not extracted any facts from:

```bash
python3 "${CLAUDE_PLUGIN_ROOT}/tools/coverage.py" --wiki "$FACTLOG_ROOT"
"${CLAUDE_PLUGIN_ROOT}/tools/factlog_python.sh" "${CLAUDE_PLUGIN_ROOT}/tools/coverage.py" --wiki "$FACTLOG_ROOT"
```

It reports, per source file under `sources/` and `runs/sources/`, how many
Expand Down Expand Up @@ -536,7 +536,7 @@ ok, reason = validate_candidate_query(proposed_query_line, facts)
After any write to `facts/query.dl`, immediately run:

```bash
python3 "${CLAUDE_PLUGIN_ROOT}/tools/run_logic_check.py"
"${CLAUDE_PLUGIN_ROOT}/tools/factlog_python.sh" "${CLAUDE_PLUGIN_ROOT}/tools/run_logic_check.py"
```

Show the new `facts/logic_report.txt` verbatim. This is the final evidence for
Expand Down Expand Up @@ -565,7 +565,7 @@ including the `review_required("<verbatim question>")?` fallback.
### Step 2 — Classify deterministically

```bash
python3 "${CLAUDE_PLUGIN_ROOT}/tools/ask_router.py" validate "<draft>" --target "$FACTLOG_ROOT"
"${CLAUDE_PLUGIN_ROOT}/tools/factlog_python.sh" "${CLAUDE_PLUGIN_ROOT}/tools/ask_router.py" validate "<draft>" --target "$FACTLOG_ROOT"
```

This prints JSON `{ok, code, reason, route, negative, predicate}`. **Branch on
Expand All @@ -589,7 +589,7 @@ correct, so retrying is pointless.
### Step 3a — Engine answer (VERIFIED)

```bash
python3 "${CLAUDE_PLUGIN_ROOT}/tools/ask_router.py" render "<draft>" --target "$FACTLOG_ROOT"
"${CLAUDE_PLUGIN_ROOT}/tools/factlog_python.sh" "${CLAUDE_PLUGIN_ROOT}/tools/ask_router.py" render "<draft>" --target "$FACTLOG_ROOT"
```

Show the `VERIFIED — engine` block verbatim (positive rows, or `rows: 0` /
Expand All @@ -614,7 +614,7 @@ statuses), use `factlog provenance <subject> [relation] [object]`.
### Step 3b — Wiki exploration (UNVERIFIED)

```bash
python3 "${CLAUDE_PLUGIN_ROOT}/tools/ask_router.py" wiki "<question>" --reason "<why>" --target "$FACTLOG_ROOT"
"${CLAUDE_PLUGIN_ROOT}/tools/factlog_python.sh" "${CLAUDE_PLUGIN_ROOT}/tools/ask_router.py" wiki "<question>" --reason "<why>" --target "$FACTLOG_ROOT"
```

Show the `UNVERIFIED — wiki exploration` block verbatim (cited `sources/` /
Expand All @@ -627,7 +627,7 @@ present wiki excerpts as confirmed facts. Optionally record the unanswered
question for later review (a non-engine-input sink, never `facts/query.dl`):

```bash
python3 "${CLAUDE_PLUGIN_ROOT}/tools/ask_router.py" note "<question>" --target "$FACTLOG_ROOT"
"${CLAUDE_PLUGIN_ROOT}/tools/factlog_python.sh" "${CLAUDE_PLUGIN_ROOT}/tools/ask_router.py" note "<question>" --target "$FACTLOG_ROOT"
```

---
Expand Down
Loading