跳到主要内容

每周复盘与待办检查

收藏0

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

让 AI 安装

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

scripts/weekly_review_gate.py

9.5 KB
1#!/usr/bin/env python3
2"""weekly_review_gate.py — Gate a GTD weekly review: completion %, named gaps, one honest verdict.
3
4Encodes David Allen's weekly-review checklist as three phases and ten numbered steps:
5
6 GET CLEAR 1. Collect loose inputs (papers, receipts, notes, downloads)
7 2. Process inboxes to zero (clarify, don't do)
8 3. Empty your head (mind sweep — capture everything still riding in RAM)
9 GET CURRENT 4. Review next-action lists (mark done, prune dead, surface stuck)
10 5. Review previous calendar (missed/spawned commitments become actions)
11 6. Review upcoming calendar (prepare, don't react)
12 7. Review waiting-for list (chase, re-date, or drop each item)
13 8. Review project lists (every active project has exactly one next action)
14 GET CREATIVE 9. Review someday/maybe (activate, keep, or kill)
15 10. Capture new ideas (add to the system while your head is clear)
16
17The gate's one hard rule: ALL FIVE GET CURRENT steps (4-8) are mandatory. A GET CURRENT step that
18is neither done nor explicitly skipped with a reason ALWAYS forces INCOMPLETE — a review that
19skims the core is a guilt ritual, not a review.
20
21Deterministic logic. No LLM calls. Stdlib only.
22
23Usage:
24 python weekly_review_gate.py --list
25 python weekly_review_gate.py --done "1,2,3,4,5,6,7,8,10" --skip "9:no someday list yet"
26 python weekly_review_gate.py --done "1,4,6" --json
27 python weekly_review_gate.py --sample
28
29Exit codes:
30 0 review COMPLETE (every step done or skipped with a reason) — also --list / --sample / --help
31 2 review INCOMPLETE (at least one step neither done nor skipped)
32 3 bad input (unknown step number, malformed --done/--skip, step both done and skipped)
33"""
34
35import argparse
36import json
37import sys
38from typing import Any, Dict, List
39
40STEPS = [
41 (1, "GET CLEAR", "Collect loose inputs (papers, receipts, notes, downloads)"),
42 (2, "GET CLEAR", "Process inboxes to zero (clarify, don't do)"),
43 (3, "GET CLEAR", "Empty your head (mind sweep — capture everything still in RAM)"),
44 (4, "GET CURRENT", "Review next-action lists (mark done, prune dead, surface stuck)"),
45 (5, "GET CURRENT", "Review previous calendar (missed/spawned commitments become actions)"),
46 (6, "GET CURRENT", "Review upcoming calendar (prepare, don't react)"),
47 (7, "GET CURRENT", "Review waiting-for list (chase, re-date, or drop each item)"),
48 (8, "GET CURRENT", "Review project lists (every active project has one next action)"),
49 (9, "GET CREATIVE", "Review someday/maybe (activate, keep, or kill)"),
50 (10, "GET CREATIVE", "Capture new ideas (add them while your head is clear)"),
51]
52
53MANDATORY_PHASE = "GET CURRENT"
54PHASE_ORDER = ["GET CLEAR", "GET CURRENT", "GET CREATIVE"]
55
56
57def parse_done(raw: str) -> List[int]:
58 if not raw or not raw.strip():
59 return []
60 nums = []
61 for part in raw.split(","):
62 part = part.strip()
63 if not part:
64 continue
65 if not part.isdigit():
66 raise ValueError(f"--done entries must be step numbers, got: {part!r}")
67 nums.append(int(part))
68 return sorted(set(nums))
69
70
71def parse_skips(raw_list: List[str]) -> Dict[int, str]:
72 skips: Dict[int, str] = {}
73 for raw in raw_list or []:
74 if ":" not in raw:
75 raise ValueError(f"--skip must be 'N:reason', got: {raw!r}")
76 num_s, reason = raw.split(":", 1)
77 num_s, reason = num_s.strip(), reason.strip()
78 if not num_s.isdigit():
79 raise ValueError(f"--skip step must be a number, got: {num_s!r}")
80 if not reason:
81 raise ValueError(f"--skip requires a non-empty reason: {raw!r}")
82 skips[int(num_s)] = reason
83 return skips
84
85
86def evaluate(done: List[int], skips: Dict[int, str]) -> Dict[str, Any]:
87 valid = {n for n, _, _ in STEPS}
88 for n in done:
89 if n not in valid:
90 raise ValueError(f"unknown step number in --done: {n} (valid: 1-10)")
91 for n in skips:
92 if n not in valid:
93 raise ValueError(f"unknown step number in --skip: {n} (valid: 1-10)")
94 overlap = set(done) & set(skips)
95 if overlap:
96 raise ValueError(f"step(s) both done and skipped: {sorted(overlap)}")
97
98 rows = []
99 missing: List[Dict[str, Any]] = []
100 for n, phase, label in STEPS:
101 if n in done:
102 status = "DONE"
103 elif n in skips:
104 status = "SKIPPED"
105 else:
106 status = "MISSING"
107 missing.append({"step": n, "phase": phase, "label": label})
108 rows.append({"step": n, "phase": phase, "label": label, "status": status,
109 "skip_reason": skips.get(n)})
110
111 considered = len(STEPS) - len(skips)
112 completion_pct = round(100.0 * len(done) / considered, 1) if considered else 100.0
113
114 missing_mandatory = [m for m in missing if m["phase"] == MANDATORY_PHASE]
115 skipped_mandatory = [n for n in skips if 4 <= n <= 8]
116
117 verdict = "COMPLETE" if not missing else "INCOMPLETE"
118 forced = bool(missing_mandatory)
119
120 warnings = []
121 if skipped_mandatory:
122 warnings.append(
123 f"GET CURRENT step(s) {sorted(skipped_mandatory)} skipped with a reason — "
124 "allowed, but the core of the review was not fully walked. Do them first next week."
125 )
126 if forced:
127 warnings.append(
128 "Unskipped GET CURRENT step(s) missing — this ALWAYS forces INCOMPLETE, "
129 "regardless of completion %."
130 )
131
132 return {
133 "steps": rows,
134 "done_count": len(done),
135 "skipped_count": len(skips),
136 "missing_count": len(missing),
137 "completion_pct": completion_pct,
138 "missing_steps": missing,
139 "missing_mandatory": missing_mandatory,
140 "mandatory_gate_forced_incomplete": forced,
141 "warnings": warnings,
142 "verdict": verdict,
143 "exit_code": 0 if verdict == "COMPLETE" else 2,
144 }
145
146
147def render_list() -> str:
148 out = ["GTD Weekly Review — the ten steps", "=" * 64]
149 for phase in PHASE_ORDER:
150 tag = " (all mandatory)" if phase == MANDATORY_PHASE else ""
151 out.append(f"\n{phase}{tag}")
152 for n, p, label in STEPS:
153 if p == phase:
154 out.append(f" {n:>2}. {label}")
155 out.append("\nMark progress with --done \"1,2,3\" and --skip \"N:reason\".")
156 return "\n".join(out)
157
158
159def render_human(r: Dict[str, Any]) -> str:
160 out = ["Weekly Review Gate (GET CLEAR -> GET CURRENT -> GET CREATIVE)", "=" * 64]
161 current_phase = None
162 for row in r["steps"]:
163 if row["phase"] != current_phase:
164 current_phase = row["phase"]
165 tag = " (mandatory)" if current_phase == MANDATORY_PHASE else ""
166 out.append(f"\n {current_phase}{tag}")
167 mark = {"DONE": "[x]", "SKIPPED": "[s]", "MISSING": "[ ]"}[row["status"]]
168 line = f" {mark} {row['step']:>2}. {row['label']}"
169 if row["status"] == "SKIPPED":
170 line += f" (skipped: {row['skip_reason']})"
171 out.append(line)
172 out.append("")
173 out.append(f" Completion: {r['completion_pct']}% "
174 f"(done {r['done_count']} · skipped {r['skipped_count']} · missing {r['missing_count']})")
175 if r["missing_steps"]:
176 out.append("\n Missing steps (do these to finish):")
177 for m in r["missing_steps"]:
178 star = " <- MANDATORY" if m["phase"] == MANDATORY_PHASE else ""
179 out.append(f" - {m['step']}. [{m['phase']}] {m['label']}{star}")
180 for w in r["warnings"]:
181 out.append(f"\n WARNING: {w}")
182 out.append("")
183 out.append(f" VERDICT: {r['verdict']}" +
184 (" (mandatory GET CURRENT gate forced this)" if r["mandatory_gate_forced_incomplete"] else ""))
185 return "\n".join(out)
186
187
188def main(argv: List[str]) -> int:
189 p = argparse.ArgumentParser(
190 description="Gate a GTD weekly review: completion %, named gaps, COMPLETE/INCOMPLETE verdict.",
191 formatter_class=argparse.RawDescriptionHelpFormatter,
192 epilog=__doc__,
193 )
194 p.add_argument("--done", default="", help='Comma-separated step numbers completed, e.g. "1,3,5"')
195 p.add_argument("--skip", action="append", default=[],
196 help='Skip a step with a reason, e.g. --skip "9:no someday list yet" (repeatable)')
197 p.add_argument("--list", action="store_true", help="Show the numbered ten-step checklist and exit")
198 p.add_argument("--sample", action="store_true",
199 help="Run an embedded example review (always exits 0; the sample shows an INCOMPLETE verdict)")
200 p.add_argument("--json", action="store_true", help="Emit JSON instead of text")
201 args = p.parse_args(argv)
202
203 if args.list:
204 print(render_list())
205 return 0
206
207 try:
208 if args.sample:
209 done = parse_done("1,2,3,4,6,7,8")
210 skips = parse_skips(["9:no someday list yet"])
211 else:
212 done = parse_done(args.done)
213 skips = parse_skips(args.skip)
214 result = evaluate(done, skips)
215 except ValueError as e:
216 print(f"error: {e}", file=sys.stderr)
217 return 3
218
219 if args.json:
220 print(json.dumps(result, indent=2))
221 else:
222 print(render_human(result))
223
224 if args.sample:
225 return 0 # sample is illustrative; documented to always exit 0
226 return result["exit_code"]
227
228
229if __name__ == "__main__":
230 sys.exit(main(sys.argv[1:]))

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