跳到主要内容

每周复盘与待办检查

收藏0

按收集、检查、规划三个阶段复盘一周,找出遗漏待办、停滞项目和没有下一步的承诺。

让 AI 安装

github最新版本 0.1.1更新于 2026-10-21下载 2 次2 个版本

scripts/open_loop_scanner.py

7.8 KB
1#!/usr/bin/env python3
2"""open_loop_scanner.py — Inventory the open loops hiding in a directory.
3
4The GET CLEAR phase of a weekly review starts with evidence, not memory. This script scans a
5directory for three kinds of open loop:
6
7 1. checkbox — unchecked markdown checkboxes: - [ ] / * [ ]
8 2. todo_fixme — TODO / FIXME markers in text and source files
9 3. stale_file — files not modified in --stale-days days (default 14)
10
11Output is an inventory grouped by kind, with counts and per-file locations, so the review walks
12real loops instead of whatever the user happens to remember.
13
14Deterministic logic. No LLM calls, no network. Stdlib only.
15
16Usage:
17 python open_loop_scanner.py --dir ~/notes
18 python open_loop_scanner.py --dir . --stale-days 30 --json
19 python open_loop_scanner.py --sample
20
21Exit codes:
22 0 scan complete (even if zero loops found) — also --sample / --help
23 2 directory not found or not a directory
24"""
25
26import argparse
27import json
28import os
29import re
30import sys
31import time
32from typing import Any, Dict, List
33
34SKIP_DIRS = {".git", ".hg", ".svn", "node_modules", "__pycache__", ".venv", "venv",
35 "dist", "build", ".idea", ".vscode", ".cache"}
36CHECKBOX_EXTS = {".md", ".markdown", ".txt"}
37TEXT_EXTS = CHECKBOX_EXTS | {".py", ".js", ".ts", ".tsx", ".jsx", ".sh", ".rb", ".go",
38 ".rs", ".java", ".c", ".h", ".cpp", ".css", ".html",
39 ".yaml", ".yml", ".toml", ".ini", ".cfg", ".sql"}
40
41CHECKBOX_RE = re.compile(r"^\s*[-*]\s+\[ \]\s+(.*)$")
42TODO_RE = re.compile(r"\b(TODO|FIXME)\b[:\s]*(.*)", re.IGNORECASE)
43
44
45def _snippet(text: str, limit: int = 80) -> str:
46 text = text.strip()
47 return text if len(text) <= limit else text[: limit - 1] + "…"
48
49
50def scan(directory: str, stale_days: int) -> Dict[str, Any]:
51 now = time.time()
52 stale_cutoff = now - stale_days * 86400
53 checkboxes: List[Dict[str, Any]] = []
54 todos: List[Dict[str, Any]] = []
55 stale: List[Dict[str, Any]] = []
56 files_scanned = 0
57
58 for root, dirs, files in os.walk(directory):
59 dirs[:] = sorted(d for d in dirs if d not in SKIP_DIRS and not d.startswith("."))
60 for fname in sorted(files):
61 ext = os.path.splitext(fname)[1].lower()
62 if ext not in TEXT_EXTS:
63 continue
64 path = os.path.join(root, fname)
65 rel = os.path.relpath(path, directory)
66 files_scanned += 1
67
68 try:
69 mtime = os.path.getmtime(path)
70 if mtime < stale_cutoff:
71 stale.append({"file": rel,
72 "days_since_modified": int((now - mtime) // 86400)})
73 with open(path, "r", encoding="utf-8", errors="replace") as f:
74 for lineno, line in enumerate(f, 1):
75 if ext in CHECKBOX_EXTS:
76 m = CHECKBOX_RE.match(line)
77 if m:
78 checkboxes.append({"file": rel, "line": lineno,
79 "text": _snippet(m.group(1))})
80 m = TODO_RE.search(line)
81 if m:
82 todos.append({"file": rel, "line": lineno,
83 "marker": m.group(1).upper(),
84 "text": _snippet(m.group(2))})
85 except OSError:
86 continue
87
88 total = len(checkboxes) + len(todos) + len(stale)
89 return {
90 "directory": os.path.abspath(directory),
91 "stale_days_threshold": stale_days,
92 "files_scanned": files_scanned,
93 "total_open_loops": total,
94 "loops": {
95 "checkbox": {"count": len(checkboxes), "items": checkboxes},
96 "todo_fixme": {"count": len(todos), "items": todos},
97 "stale_file": {"count": len(stale), "items": stale},
98 },
99 }
100
101
102SAMPLE_RESULT: Dict[str, Any] = {
103 "directory": "/home/sample/notes",
104 "stale_days_threshold": 14,
105 "files_scanned": 12,
106 "total_open_loops": 7,
107 "loops": {
108 "checkbox": {
109 "count": 3,
110 "items": [
111 {"file": "projects/website-relaunch.md", "line": 14,
112 "text": "email designer about the hero image"},
113 {"file": "projects/website-relaunch.md", "line": 15,
114 "text": "draft the pricing page copy"},
115 {"file": "inbox.md", "line": 3, "text": "book dentist appointment"},
116 ],
117 },
118 "todo_fixme": {
119 "count": 2,
120 "items": [
121 {"file": "scripts/backup.sh", "line": 22, "marker": "TODO",
122 "text": "rotate old archives after 90 days"},
123 {"file": "notes/tax-prep.md", "line": 8, "marker": "FIXME",
124 "text": "the mileage total is wrong, recount Q2"},
125 ],
126 },
127 "stale_file": {
128 "count": 2,
129 "items": [
130 {"file": "projects/learn-spanish.md", "days_since_modified": 41},
131 {"file": "someday/write-a-novel.md", "days_since_modified": 63},
132 ],
133 },
134 },
135}
136
137KIND_LABEL = {
138 "checkbox": "Unchecked checkboxes (- [ ])",
139 "todo_fixme": "TODO / FIXME markers",
140 "stale_file": "Stale files (untouched past threshold)",
141}
142
143
144def render_human(r: Dict[str, Any]) -> str:
145 out = ["Open Loop Scanner (evidence first, memory second)", "=" * 64]
146 out.append(f" Directory: {r['directory']}")
147 out.append(f" Files scanned: {r['files_scanned']} "
148 f"Stale threshold: {r['stale_days_threshold']} days")
149 out.append(f" TOTAL OPEN LOOPS: {r['total_open_loops']}")
150 for kind in ("checkbox", "todo_fixme", "stale_file"):
151 bucket = r["loops"][kind]
152 out.append(f"\n {KIND_LABEL[kind]} — {bucket['count']}")
153 for item in bucket["items"][:25]:
154 if kind == "stale_file":
155 out.append(f" - {item['file']} ({item['days_since_modified']} days untouched)")
156 elif kind == "todo_fixme":
157 out.append(f" - {item['file']}:{item['line']} [{item['marker']}] {item['text']}")
158 else:
159 out.append(f" - {item['file']}:{item['line']} {item['text']}")
160 if bucket["count"] > 25:
161 out.append(f" … and {bucket['count'] - 25} more")
162 out.append("")
163 out.append(" Feed these into GET CLEAR: collect, clarify, and route every loop")
164 out.append(" to a list — next action, waiting-for, someday/maybe, or trash.")
165 return "\n".join(out)
166
167
168def main(argv: List[str]) -> int:
169 p = argparse.ArgumentParser(
170 description="Scan a directory for open loops: unchecked checkboxes, TODO/FIXME, stale files.",
171 formatter_class=argparse.RawDescriptionHelpFormatter,
172 epilog=__doc__,
173 )
174 p.add_argument("--dir", default=".", help="Directory to scan (default: current directory)")
175 p.add_argument("--stale-days", type=int, default=14,
176 help="Flag files not modified in this many days (default: 14)")
177 p.add_argument("--sample", action="store_true",
178 help="Print a canned deterministic scan result and exit 0")
179 p.add_argument("--json", action="store_true", help="Emit JSON instead of text")
180 args = p.parse_args(argv)
181
182 if args.sample:
183 result = SAMPLE_RESULT
184 else:
185 if not os.path.isdir(args.dir):
186 print(f"error: not a directory: {args.dir}", file=sys.stderr)
187 return 2
188 result = scan(args.dir, args.stale_days)
189
190 if args.json:
191 print(json.dumps(result, indent=2))
192 else:
193 print(render_human(result))
194 return 0
195
196
197if __name__ == "__main__":
198 sys.exit(main(sys.argv[1:]))

文件内容是平台检查时读出的文本,只供查看。平台不会运行包里的任何脚本或程序。