現在的主線是哪一個,變成一個查得到的事實(inkstone/ISEP#82)

14 個 open milestone 同時亮著,其中「Mira 現代化」同名活在 5 個 repo,
所以 SOP 說的「那個 active milestone」在現場沒有指涉對象。

- hooks/lib/mainline.py        主線的唯一存放處(一個檔放得下一條),從不打網路
- scripts/mainline             show/list/set/clear/refresh/adopt/has
- hooks/mainline-focus-guard.sh 派了不在主線上的票 ⇒ 攔一次,問補收還是跳線
- hooks/lib/countdown.py       ⏱ 那一行的主線改讀「被標定的」,蓋過「期限最近」的猜測
- hooks/countdown-guard.sh     同一個注入點加第二行 🎯(ISEP#63 那半不動)
- 測試 24 條(離線)+ countdown 原有 20 條仍全綠
This commit is contained in:
Claude Code
2026-08-28 00:30:09 +00:00
committed by Claude
parent dbd7c8c874
commit 21971ea83e
7 changed files with 1020 additions and 12 deletions
+46 -6
View File
@@ -27,6 +27,13 @@ leo 2026-08-28 07:40:「今天我整天上課……**下午四點左右**希
從快取檔讀,**這支從不打網路**UserPromptSubmit 走在每一則訊息的關鍵路徑上,
在那裡打網路 = 每一句話都先等一次 HTTP。快取由 SessionStart 那支負責更新
`scripts/countdown-milestone-refresh.sh`),拿不到就整段不顯示,不編數字。
🔴 **來源有兩個,優先序寫死(inkstone/ISEP#82**
① **被標定的主線**`hooks/lib/mainline.py``scripts/mainline set` 寫的)—— 優先
② 期限最近的那一個(`milestone-due` 快取,`countdown-milestone-refresh.sh` 寫的)
② 是**猜**出來的(「最近到期的大概就是在做的那個」),而 ISEP#82 的整件事就是
「那個 active milestone」在 14 個裡沒有指涉對象。⇒ 一旦有人真的標了主線,
就不准再用猜的那個蓋過它。兩個都沒有 ⇒ 整段不顯示。
"""
import json
import os
@@ -137,10 +144,40 @@ def deadline(now: datetime) -> datetime:
return d
def milestone() -> "tuple[str, datetime] | None":
"""主線 milestone 的期限。快取檔一行:`YYYY-MM-DD|<名稱>`。沒有就沒有。"""
def _mainline():
"""借 `hooks/lib/mainline.py` 讀「被標定的主線」。拿不到就當沒有(fail-open)。
刻意用檔案路徑載入而不是 `import mainline`:本檔自己也是被
`spec_from_file_location` 載進去的,`sys.path` 裡沒有 `hooks/lib`。
"""
try:
import importlib.util
p = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mainline.py")
spec = importlib.util.spec_from_file_location("isep_mainline", p)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
except Exception:
return None
def milestone() -> "tuple[str, datetime | None] | None":
"""主線 milestone 的期限。優先讀被標定的主線,其次讀「期限最近」的快取。
快取檔一行:`YYYY-MM-DD|<名稱>`。兩個都沒有就沒有(回 None,整段不顯示)。
期限可能是 None(被標定的主線沒設期限)——那時只說名字,不編一個日期。
"""
raw = os.environ.get("ISEP_MILESTONE_DUE", "").strip()
name = os.environ.get("ISEP_MILESTONE_NAME", "").strip()
if not raw:
mod = _mainline()
if mod is not None:
try:
seg = mod.countdown_segment()
except Exception:
seg = None
if seg:
return seg
if not raw:
p = os.path.join(state_dir(), "milestone-due")
try:
@@ -184,11 +221,14 @@ def line(session_id: str, transcript_path: str) -> str:
ms = milestone()
if ms:
name, due = ms
gap = due - now
if gap.total_seconds() >= 0:
parts.append("主線 %s%s" % (name, human(gap)))
if due is None:
parts.append("主線 %s(沒設期限)" % name)
else:
parts.append("🔴 主線 %s 已逾期 %s" % (name, human(gap)))
gap = due - now
if gap.total_seconds() >= 0:
parts.append("主線 %s%s" % (name, human(gap)))
else:
parts.append("🔴 主線 %s 已逾期 %s" % (name, human(gap)))
return "".join(parts)