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
8 changes: 4 additions & 4 deletions scripts/context_saver.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def acquire_lock(lock_path: Path, timeout: float = 10.0):
except FileExistsError:
# Check if lock is stale
try:
content = lock_path.read_text().strip()
content = lock_path.read_text(encoding="utf-8").strip()
pid = int(content)
# Check if process is alive
try:
Expand Down Expand Up @@ -188,7 +188,7 @@ def _merge_narrative(
changes.append("Created new narrative.md")
return "; ".join(changes)

text = path.read_text()
text = path.read_text(encoding="utf-8")
sections = _read_sections(text)

# Current Foci: replace entirely
Expand Down Expand Up @@ -287,7 +287,7 @@ def _append_learnings(path: Path, learnings: list[dict]) -> int:

existing = ""
if path.exists():
existing = path.read_text()
existing = path.read_text(encoding="utf-8")

existing_lower = existing.lower()
today = datetime.now().strftime("%Y-%m-%d")
Expand Down Expand Up @@ -352,7 +352,7 @@ def trigger_git_update(claude_dir: Path):
"""Write trigger file for background git-based narrative update."""
claude_dir.mkdir(parents=True, exist_ok=True)
trigger = claude_dir / ".update-narrative-trigger"
trigger.write_text(f"{os.getpid()}\n{time.time()}\n")
trigger.write_text(f"{os.getpid()}\n{time.time()}\n", encoding="utf-8")


# ---------------------------------------------------------------------------
Expand Down
16 changes: 8 additions & 8 deletions scripts/discover-tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
def extract_description_from_file(filepath: Path) -> str:
"""Extract a short description from a script file's first comment or docstring."""
try:
text = filepath.read_text(errors="replace")
text = filepath.read_text(errors="replace", encoding="utf-8")
lines = text.splitlines()
except (OSError, UnicodeDecodeError):
return ""
Expand Down Expand Up @@ -130,7 +130,7 @@ def discover_makefile_targets(root: Path) -> list[tuple[str, str]]:
return results

try:
text = makefile.read_text(errors="replace")
text = makefile.read_text(errors="replace", encoding="utf-8")
except OSError:
return results

Expand Down Expand Up @@ -159,7 +159,7 @@ def discover_package_json_scripts(root: Path) -> list[tuple[str, str]]:
return results

try:
data = json.loads(pkg.read_text())
data = json.loads(pkg.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return results

Expand Down Expand Up @@ -189,7 +189,7 @@ def discover_pyproject_scripts(root: Path) -> list[tuple[str, str]]:
return results

try:
text = pyproject.read_text()
text = pyproject.read_text(encoding="utf-8")
except OSError:
return results

Expand Down Expand Up @@ -226,7 +226,7 @@ def discover_justfile_targets(root: Path) -> list[tuple[str, str]]:
return results

try:
text = justfile.read_text(errors="replace")
text = justfile.read_text(errors="replace", encoding="utf-8")
except OSError:
return results

Expand Down Expand Up @@ -260,7 +260,7 @@ def discover_taskfile_targets(root: Path) -> list[tuple[str, str]]:
return results

try:
text = taskfile.read_text(errors="replace")
text = taskfile.read_text(errors="replace", encoding="utf-8")
except OSError:
return results

Expand Down Expand Up @@ -297,7 +297,7 @@ def discover_build_commands(root: Path) -> list[tuple[str, str]]:
return results

try:
data = json.loads(manifest.read_text())
data = json.loads(manifest.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return results

Expand Down Expand Up @@ -425,7 +425,7 @@ def main():
claude_dir = root / ".claude"
claude_dir.mkdir(exist_ok=True)
tools_md = claude_dir / "TOOLS.md"
tools_md.write_text(content)
tools_md.write_text(content, encoding="utf-8")
print(f"Wrote {tools_md}", file=sys.stderr)

# Also print to stdout for inspection
Expand Down
2 changes: 1 addition & 1 deletion scripts/extract-context.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def main():
}

if narrative_file.exists():
content = narrative_file.read_text()
content = narrative_file.read_text(encoding="utf-8")
# Truncate sections to prevent context bloat
# Summary: concise overview (300 chars)
# Foci: current work areas (400 chars)
Expand Down
36 changes: 18 additions & 18 deletions scripts/goals.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def find_goal(goal_ref: str) -> Path:
if not directory.exists():
continue
for gf in directory.glob("*.md"):
content = gf.read_text()
content = gf.read_text(encoding="utf-8")
m = re.search(r"^\*\*Slug\*\*:\s*(.+)$", content, re.MULTILINE)
if m and m.group(1).strip() == goal_ref:
slug_matches.append(gf)
Expand Down Expand Up @@ -175,7 +175,7 @@ def find_goal_by_id(goal_id: str) -> Path | None:

def parse_goal(path: Path) -> dict:
"""Parse a goal markdown file into a dict."""
content = path.read_text()
content = path.read_text(encoding="utf-8")
goal = {"path": str(path), "raw": content}

# Parse header fields
Expand Down Expand Up @@ -372,7 +372,7 @@ def migrate_goal(path: Path) -> dict:
if not needs_migration(goal):
return goal

content = path.read_text()
content = path.read_text(encoding="utf-8")

# Add Slug field after ID if missing
if not goal.get("slug"):
Expand Down Expand Up @@ -545,12 +545,12 @@ def goal_list(show_all: bool = False, project_path: str | None = None) -> str:
current_path = claude_dir / ".current-goal"
current_id = ""
if current_path.exists():
current_id, _ = parse_current_goal(current_path.read_text())
current_id, _ = parse_current_goal(current_path.read_text(encoding="utf-8"))

if not index_path.exists():
return "No goals linked to this project. Use 'goal create' or 'goal sync'."

index = json.loads(index_path.read_text())
index = json.loads(index_path.read_text(encoding="utf-8"))
goals = index.get("goals", [])
if not goals:
return "No goals linked to this project."
Expand All @@ -576,10 +576,10 @@ def goal_show(goal_ref: str | None = None, project_path: str | None = None) -> s
current_path = claude_dir / ".current-goal"
if not current_path.exists():
raise ValueError("No current goal set. Use 'goal switch <id>' or 'goal create'.")
goal_ref, _ = parse_current_goal(current_path.read_text())
goal_ref, _ = parse_current_goal(current_path.read_text(encoding="utf-8"))

path = find_goal(goal_ref)
return path.read_text()
return path.read_text(encoding="utf-8")


def goal_switch(goal_ref: str, project_path: str | None = None) -> str:
Expand Down Expand Up @@ -619,7 +619,7 @@ def goal_focus(goal_ref: str | None, step_id: str,
current_path = claude_dir / ".current-goal"
if not current_path.exists():
raise ValueError("No current goal set.")
goal_ref, _ = parse_current_goal(current_path.read_text())
goal_ref, _ = parse_current_goal(current_path.read_text(encoding="utf-8"))

path = find_goal(goal_ref)
goal = parse_goal(path)
Expand All @@ -645,7 +645,7 @@ def goal_focus(goal_ref: str | None, step_id: str,
target_step["current"] = True

# Rebuild file
content = path.read_text()
content = path.read_text(encoding="utf-8")
content = _rebuild_plan_section(content, steps)
content = _update_timestamp(content)
atomic_write(path, content)
Expand Down Expand Up @@ -704,7 +704,7 @@ def goal_update_step(goal_ref: str, step_ref: str | int,
steps[idx]["current"] = True

# Rebuild the file
content = path.read_text()
content = path.read_text(encoding="utf-8")
content = _rebuild_plan_section(content, steps)
content = _update_timestamp(content)
atomic_write(path, content)
Expand Down Expand Up @@ -732,7 +732,7 @@ def goal_add_learning(goal_ref: str, text: str) -> str:
now = datetime.now().strftime("%Y-%m-%d")
entry = f"\n### {now}\n{text}\n"

content = path.read_text()
content = path.read_text(encoding="utf-8")

# Insert before "## Recent Activity"
m = re.search(r"^## Recent Activity", content, re.MULTILINE)
Expand All @@ -757,7 +757,7 @@ def goal_add_commit(goal_ref: str, commit_hash: str, message: str,
project_name = Path.cwd().name
entry = f"- `{commit_hash}` ({project_name}) {now}: {message}"

content = path.read_text()
content = path.read_text(encoding="utf-8")

m = re.search(r"^## Recent Activity\n\n?((?:- .+\n?)*)", content, re.MULTILINE)
if m:
Expand Down Expand Up @@ -818,7 +818,7 @@ def goal_add_step(goal_ref: str, description: str,
if not any(s.get("current") for s in steps):
new_step["current"] = True

content = path.read_text()
content = path.read_text(encoding="utf-8")
content = _rebuild_plan_section(content, steps)
content = _update_timestamp(content)
atomic_write(path, content)
Expand All @@ -833,7 +833,7 @@ def goal_link_project(goal_ref: str, link_path: str,
path = find_goal(goal_ref)
project_path = str(Path(link_path).resolve())

content = path.read_text()
content = path.read_text(encoding="utf-8")

# Check for exact project path match (not substring)
goal = parse_goal(path)
Expand Down Expand Up @@ -864,7 +864,7 @@ def goal_archive(goal_ref: str, project_path: str | None = None) -> str:
path = find_goal(goal_ref)
ensure_dirs()

content = path.read_text()
content = path.read_text(encoding="utf-8")
content = re.sub(r"(\*\*Status\*\*:\s*).+", r"\1archived", content)
content = _update_timestamp(content)

Expand All @@ -876,7 +876,7 @@ def goal_archive(goal_ref: str, project_path: str | None = None) -> str:
claude_dir = get_claude_dir(project_path)
current_path = claude_dir / ".current-goal"
if current_path.exists():
current_id, _ = parse_current_goal(current_path.read_text())
current_id, _ = parse_current_goal(current_path.read_text(encoding="utf-8"))
if current_id == path.stem:
current_path.unlink()

Expand All @@ -888,7 +888,7 @@ def goal_sync(project_path: str | None = None) -> str:
"""Rebuild active-goals.json from goal files. Returns confirmation."""
update_index(project_path=project_path)
claude_dir = get_claude_dir(project_path)
index = json.loads((claude_dir / "active-goals.json").read_text())
index = json.loads((claude_dir / "active-goals.json").read_text(encoding="utf-8"))
count = len(index.get("goals", []))
return f"Synced: {count} goal(s) linked to this project."

Expand All @@ -907,7 +907,7 @@ def goal_context(project_path: str | None = None) -> dict:
if not current_path.exists():
return {}

goal_uuid, focused_step_id = parse_current_goal(current_path.read_text())
goal_uuid, focused_step_id = parse_current_goal(current_path.read_text(encoding="utf-8"))

try:
path = find_goal(goal_uuid)
Expand Down
25 changes: 17 additions & 8 deletions scripts/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@
import tree_sitter_rust as tsrust
from tree_sitter import Language, Parser, Node

# Windows fix: the console codec defaults to the system locale (e.g. GBK on
# zh-CN), which cannot encode characters such as U+1F4DD used in the generated
# map. Reconfigure stdio to UTF-8 so `print(repo_map)` does not crash.
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass


# Cache format version - bump when Symbol structure or file selection changes
CACHE_VERSION = 6 # v6: Added .mm (Objective-C++) and .metal file support
Expand Down Expand Up @@ -129,7 +138,7 @@ def _load(self) -> None:
if not self.cache_path.exists():
return
try:
data = json.loads(self.cache_path.read_text())
data = json.loads(self.cache_path.read_text(encoding="utf-8"))
if data.get("version") != CACHE_VERSION:
return # Invalidate cache on version mismatch
self.found_file_count = data.get("found_file_count")
Expand All @@ -148,8 +157,8 @@ def save(self) -> None:
self.cache_path.parent.mkdir(parents=True, exist_ok=True)
# Write to temp file then rename for atomicity
tmp_path = self.cache_path.with_suffix(".tmp")
tmp_path.write_text(json.dumps(data))
tmp_path.rename(self.cache_path)
tmp_path.write_text(json.dumps(data), encoding="utf-8")
os.replace(tmp_path, self.cache_path)
self._dirty_count = 0

def save_if_needed(self) -> None:
Expand Down Expand Up @@ -1114,7 +1123,7 @@ def update_progress(status: str, completed: int = 0, total: int = 0, symbols: in
"timestamp": time.time(),
}
try:
progress_path.write_text(json.dumps(progress_data))
progress_path.write_text(json.dumps(progress_data), encoding="utf-8")
except IOError:
pass

Expand Down Expand Up @@ -1183,8 +1192,8 @@ def update_progress(status: str, completed: int = 0, total: int = 0, symbols: in
# Write to .in-progress first, then rename atomically
in_progress_path = claude_dir / "repo-map.md.in-progress"
final_path = claude_dir / "repo-map.md"
in_progress_path.write_text(repo_map)
in_progress_path.rename(final_path)
in_progress_path.write_text(repo_map, encoding="utf-8")
os.replace(in_progress_path, final_path)

# Write final progress status
progress_path = claude_dir / "repo-map-progress.json"
Expand All @@ -1197,13 +1206,13 @@ def update_progress(status: str, completed: int = 0, total: int = 0, symbols: in
"symbols_found": len(all_symbols),
"timestamp": time.time(),
}
progress_path.write_text(json.dumps(progress_data))
progress_path.write_text(json.dumps(progress_data), encoding="utf-8")

# Clean up PID file if we're the process that wrote it
pid_file = claude_dir / ".indexing-pid"
if pid_file.exists():
try:
stored_pid = int(pid_file.read_text().strip())
stored_pid = int(pid_file.read_text(encoding="utf-8").strip())
if stored_pid == os.getpid():
pid_file.unlink()
except (ValueError, OSError):
Expand Down
4 changes: 2 additions & 2 deletions scripts/process-guardian.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def read_pids(pidfile: Path) -> list[tuple[int, str]]:
if not pidfile.exists():
return []
pids = []
for line in pidfile.read_text().splitlines():
for line in pidfile.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
Expand Down Expand Up @@ -127,7 +127,7 @@ def handle_signal(signum, frame):
if len(alive) < len(pids) and pidfile.exists():
pidfile.write_text(
"\n".join(f"{pid}:{label}" for pid, label in alive) + "\n"
)
, encoding="utf-8")

# If no children left to watch, exit
if not alive:
Expand Down
8 changes: 4 additions & 4 deletions scripts/readme.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,11 @@ def main():
print("Run story.py first.", file=sys.stderr)
sys.exit(1)

narrative = narrative_file.read_text()
narrative = narrative_file.read_text(encoding="utf-8")

existing_readme = None
if args.update and readme_file.exists():
existing_readme = readme_file.read_text()
existing_readme = readme_file.read_text(encoding="utf-8")
print("Updating existing README...", file=sys.stderr)
else:
print("Generating new README...", file=sys.stderr)
Expand All @@ -175,10 +175,10 @@ def main():
else:
if readme_file.exists():
backup = project_root / "README.md.bak"
backup.write_text(readme_file.read_text())
backup.write_text(readme_file.read_text(), encoding="utf-8")
print(f"Backup saved to {backup}", file=sys.stderr)

readme_file.write_text(readme)
readme_file.write_text(readme, encoding="utf-8")
print(f"README saved to {readme_file}", file=sys.stderr)
print(readme)

Expand Down
Loading