雲端要拿得到票、主線與通知:白名單住 ISEP、主線檔隨 repo 走、leo21c 讀放寫擋(inkstone/ISEP#130)
- docs/permissions-allow.json + scripts/settings-allow-sync:四個 Gitea 正門工具的權限白名單一份, setup script 裝完 plugin 寫一次、SessionStart 每次再對一次(只加不減、冪等) - hooks/lib/mainline.py/scripts/mainline:家目錄沒主線就讀 InkStoneCo/system-dev/mainline.json; set/adopt/clear 兩份一起寫,refresh 只寫家目錄 - hooks/leo21c-write-guard.sh:唯讀 -d(tr/cut/sort…)先剪掉再判、notify_leo trigger 放行 (與 prod-write-guard 同一份白名單)、改法段改印 09-02 起的 youlin 子網域;補第一支測試(26 條) - prod-write-guard/main-and-prod-push-guard/kbdb-live-exam:認得 youlin 新子網域 arcrun-yuga3bse - scripts/ticket:收件 repo 寫 inkstone/ISEP 不再 404(org 寫錯當場講) - scripts/isep-notify:有 TELEGRAM_BOT_TOKEN/TELEGRAM_CHAT_ID 先走 Bot API 直送(雲端唯一通的路) - 測試:A31–A35 共 79 條;README/plugin.json/hooks-inventory 數字實數(61 支、85 條、53 支腳本) 假設(記在這裡等 review):權限規則的形狀沿用 leo 09-07 親手加、實測有效的那四條; 「分類器真的不擋」要雲端一趟 run 的 permission_denials 才驗得到,本 PR 驗不了。 版本:待總管定版(plugin.json 仍 0.22.0)。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DTZ9QtvjY7MNxjfQbexAm7
This commit is contained in:
+91
-12
@@ -58,14 +58,57 @@ def path() -> str:
|
||||
return os.path.join(state_dir(), STATE_NAME)
|
||||
|
||||
|
||||
def load():
|
||||
"""讀主線。沒有/壞掉 ⇒ None(=現在沒有主線),**不是例外**。
|
||||
# ── repo 裡的那一份(inkstone/ISEP#130)──────────────────────────────
|
||||
#
|
||||
# 主線原本只住在 `~/.claude/isep-countdown/mainline.json`——**這台機器的家目錄**。
|
||||
# 雲端 session 是另一台機器:家目錄是空的,clone 下來的 repo 才是它唯一拿得到的東西。
|
||||
# 2026-09-05 leo:「routine 每天早上開啟後抓不到任務」——不是 Gitea 沒票,
|
||||
# 是雲端根本不知道主線是哪一條。
|
||||
# ⇒ `inkstone/InkStoneCo` 的 `system-dev/mainline.json` 是**隨 repo 走的那一份**
|
||||
# (PR inkstone/InkStoneCo#118 放進去的)。這裡的規矩:
|
||||
# · 讀:家目錄那份沒有 ⇒ 讀 repo 那份(雲端就是這個形狀)。
|
||||
# 兩份都在 ⇒ 用 `set_at` 較新的(誰後標的誰算數,不分本機雲端)。
|
||||
# · 寫:`set`/`adopt`/`clear` 這種「主線是誰」的改動 ⇒ 兩份一起寫(`mirror=True`)。
|
||||
# `refresh`/閘順手學到成員 ⇒ 只寫家目錄那份——否則每開一個 session
|
||||
# InkStoneCo 的工作樹就髒一次,而髒的 diff 沒有人會去 commit。
|
||||
# · repo 那份要 commit/開 PR 才到得了雲端;這支不碰 git。
|
||||
REPO_REL = os.path.join("system-dev", STATE_NAME)
|
||||
|
||||
「沒有主線」是一個正常狀態,不是故障:ISEP#82 驗收第 4 條寫死了
|
||||
「沒有任何主線時不能整組壞掉」。所以這支從不 raise。
|
||||
"""
|
||||
|
||||
def repo_candidates():
|
||||
"""repo 那一份可能在哪:明指 > 專案根 > 專案根底下的 InkStoneCo/(雲端薄殼的形狀)> cwd 同兩層。"""
|
||||
out = []
|
||||
forced = os.environ.get("ISEP_MAINLINE_REPO_FILE", "").strip()
|
||||
if forced == "none":
|
||||
return [] # 測試用:把「repo 那份」整個拿掉,外界定住
|
||||
if forced:
|
||||
out.append(forced)
|
||||
roots = []
|
||||
for r in (os.environ.get("CLAUDE_PROJECT_DIR", "").strip(), os.getcwd()):
|
||||
if r:
|
||||
roots += [r, os.path.join(r, "InkStoneCo")]
|
||||
for r in roots:
|
||||
p = os.path.join(r, REPO_REL)
|
||||
if p not in out:
|
||||
out.append(p)
|
||||
return out
|
||||
|
||||
|
||||
def repo_path(for_write=False):
|
||||
"""讀:第一個**存在**的檔。寫:第一個 `system-dev/` 目錄存在的位置(檔可以還沒有)。
|
||||
找不到 ⇒ None(雲端沒 clone InkStoneCo、或 cwd 在別的 repo 時都是正常狀態)。"""
|
||||
for p in repo_candidates():
|
||||
if for_write:
|
||||
if os.path.isdir(os.path.dirname(p)):
|
||||
return p
|
||||
elif os.path.isfile(p):
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def _read(p):
|
||||
try:
|
||||
with open(path()) as f:
|
||||
with open(p) as f:
|
||||
d = json.load(f)
|
||||
except Exception:
|
||||
return None
|
||||
@@ -74,23 +117,59 @@ def load():
|
||||
return d
|
||||
|
||||
|
||||
def save(d) -> bool:
|
||||
def load():
|
||||
"""讀主線。沒有/壞掉 ⇒ None(=現在沒有主線),**不是例外**。
|
||||
|
||||
「沒有主線」是一個正常狀態,不是故障:ISEP#82 驗收第 4 條寫死了
|
||||
「沒有任何主線時不能整組壞掉」。所以這支從不 raise。
|
||||
|
||||
家目錄那份與 repo 那份都在時,`set_at` 較新的算數(格式固定 `YYYY-MM-DD HH:MM`,
|
||||
字串比大小就是時間比大小);一樣新 ⇒ 家目錄那份(它才有 refresh 過的進度)。
|
||||
"""
|
||||
local = _read(path())
|
||||
rp = repo_path()
|
||||
remote = _read(rp) if rp else None
|
||||
if local and remote:
|
||||
return remote if (remote.get("set_at") or "") > (local.get("set_at") or "") else local
|
||||
return local or remote
|
||||
|
||||
|
||||
def save(d, mirror=False) -> bool:
|
||||
"""寫家目錄那份;`mirror=True` 時同時寫 repo 那份(只有「主線是誰」變了才這樣叫)。
|
||||
回 True/False 只看家目錄那份——repo 那份寫不寫得進去用 `repo_path(for_write=True)` 另外問。"""
|
||||
try:
|
||||
with open(path(), "w") as f:
|
||||
json.dump(d, f, ensure_ascii=False, indent=2)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
if mirror:
|
||||
rp = repo_path(for_write=True)
|
||||
if rp:
|
||||
try:
|
||||
with open(rp, "w") as f:
|
||||
json.dump(d, f, ensure_ascii=False, indent=2)
|
||||
f.write("\n")
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
|
||||
def clear() -> bool:
|
||||
def clear(mirror=False) -> bool:
|
||||
ok = True
|
||||
try:
|
||||
os.remove(path())
|
||||
return True
|
||||
except FileNotFoundError:
|
||||
return True
|
||||
pass
|
||||
except Exception:
|
||||
return False
|
||||
ok = False
|
||||
if mirror:
|
||||
rp = repo_path()
|
||||
if rp:
|
||||
try:
|
||||
os.remove(rp)
|
||||
except Exception:
|
||||
ok = False
|
||||
return ok
|
||||
|
||||
|
||||
def parse_ts(raw):
|
||||
|
||||
Reference in New Issue
Block a user