chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)

- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次)
- .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…),
  目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪
- .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/)
- .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*)
- .gitignore 补:备份件(*.bak-*)
- 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
This commit is contained in:
admin committed 2026-10-10 23:13:22 +08:00
1 parent 30b46dbd0c
commit c1b5e4d966
735 files changed
+153192 -2415

No files matched your search

+195 -28
View File
@@ -105,12 +105,61 @@ def _say(msg: str) -> None:
# 台账四态 → 列(🔴 与 board.py::_queue 的 `by` 键**逐字一致**)
COLS = [
("pending", "待执行"),
("running", "执行中"),
("waiting", "待执行"),
("active", "执行中"),
("blocked", "有阻碍"),
("done", "已完成"),
]
# 🔴🔴🔴 **四列装的是「目标」不是「任务」**(2026-10-06 用户定案:
# 「下面的 待执行 执行中 有阻碍 已完成 里面显示的都是目标 目标就是项目能明白吗」)。
# ⇒ 列的内容=**目标本身**(`goal.json` 一条=一个目标=一个项目),按 `lifecycle` 分组。
# ⛔ 改前我用 `tasks.json` 的**台账条目**填这四列 —— 填错了:
# 台账条目是"任务"(`3b-v1` / `dark-tokens` 这种执行记录),不是目标。
# ⚠️ 这两者在本机制里**是不同层级**:目标(goal)= 一件事;任务(task)= 那件事下的一步。
#
# 🔴 **lifecycle 真身取值**(⛔ 别自己另定一套,抄 `collabd.py` 的 `GOAL_LIFE_*`):
# `等待` / `进行中` / `阻碍` / `已完成` / `已暂停`
# ⇒ 映射到本界面四列:等待→waiting|进行中→active|阻碍→blocked|已完成→done;
# `已暂停` **没有对应列**(⛔ 不许硬塞进任一列,会误导)⇒ 归入 `_LIFE_NOCOL`,另行提示。
LIFE_TO_COL = {
"等待": "waiting",
"进行中": "active",
"阻碍": "blocked",
"已完成": "done",
}
_LIFE_NOCOL = ("已暂停",) # 有此项无列,⛔ 不硬塞
# 🔴 别名表**必须与 `collabd.py::_LIFE_ALIAS` 同源**(同族"一条规则两份实现必然漂")。
_LIFE_ALIAS = {"等待": "waiting", "进行中": "active", "阻碍": "blocked",
"已完成": "done", "已暂停": "paused",
"waiting": "等待", "active": "进行中", "blocked": "阻碍",
"done": "已完成", "paused": "已暂停"}
_LIFE_ALL = ("等待", "进行中", "阻碍", "已完成", "已暂停")
def goal_life_of(goal: dict) -> str:
"""把 `goal.json` 的 `lifecycle` **归一到标准五值之一**;认不出 ⇒ 返回 `""`。
🔴🔴 **必须与机制同源**(`collabd.py::goal_life_of` 的口径),否则两边判两个样:
· **剥括号后缀**:实测 `vibe-product` 写过 `"已完成(机器可判部分)"`(手改进去的),
不剥 ⇒ 精确匹配失败 ⇒ 静默回落,这类"读错"在界面上的样子就是**目标凭空消失**。
· 别名容错:接受旧英文写法(`waiting`/`active`/…)。
⚠️ 认不出返回 `""`(⛔ 不猜、不回落成某列)—— 让调用方把它当"状态未知"另行提示,
而不是塞进某个列里冒充正常(同族 P0:「认不出不许静默回落」)。
"""
raw = str((goal or {}).get("lifecycle") or "").strip()
v = raw
if "(" in v:
v = v.split("(", 1)[0].strip()
elif "(" in v:
v = v.split("(", 1)[0].strip()
if v in _LIFE_ALL:
return v
v2 = _LIFE_ALIAS.get(v, v)
if v2 in _LIFE_ALL:
return v2
return ""
def _load_json(p: Path, default=None):
try:
@@ -119,6 +168,41 @@ def _load_json(p: Path, default=None):
return default
def _load_all_goals(inbox: Path) -> list:
"""读一个工作区的**全部目标**:`goal.json`(当前)+ `goals/*.json`(历史归档)。
返回 `[(goal_dict, src), ...]`,`src` = `"cur"` 或归档文件名(去 `.json`)。
🔴🔴🔴 **为什么要读 `goals/`**(2026-10-06 用户第三次纠正,实测坐实):
我连错两次 —— 先以为「一个工作区只有一个目标」,并把这条写进了注释与代码。
真相:**一个工作区可以有多个目标**;`goal.json` 只是**当前那个**。
`vibe-product` 实测:当前 1 个(`规划产品原型项目管理工具`,已完成)
+ 历史 2 个(`把产品规划技能整理成唯一形态` 进行中 /
`补抓跳跃视界暗色主题并升级视觉规范 skill…` 已完成)
⇒ 历史目标必须一并列出来,否则「已完成」列永远少一大截
(这正是用户两次追问「已完成里面明明有一堆内容」的答案)。
⚠️ 排序:**当前目标排最前**,历史按 `declared_at` 新的在前(⛔ 不按文件名,
文件名里的时间戳是**归档时刻**,不是登记时刻,两者可能差很多)。
⚠️ 单个归档文件坏了(非 JSON / 读不出)⇒ **跳过它但记一条**,⛔ 不整批失败
(同族 P0:一个坏文件不许把整个列表拖垮,也不许静默丢)。
"""
out = []
cur = _load_json(inbox / "goal.json", None)
if isinstance(cur, dict) and cur:
out.append((cur, "cur"))
gdir = inbox / "goals"
if gdir.is_dir():
hist = []
for p in sorted(gdir.glob("*.json")):
g = _load_json(p, None)
if isinstance(g, dict) and g:
hist.append((g, p.stem))
hist.sort(key=lambda x: str(x[0].get("declared_at") or ""), reverse=True)
out.extend(hist)
return out
def _acc_is_pass(v) -> bool:
"""一条验收判据算不算过 —— **复用技能真身**(⛔ 不自己写词表;见 pitfalls P0-87)。"""
try:
@@ -213,24 +297,67 @@ def build_project(ws: Path) -> dict:
# ⇒ 有 `title` 就显示 `title`;没 `title` 才回落 `short`;都没有 ⇒ 标「未登记目标」。
name = title or short or ("未登记目标 · %s" % ws.name)
# ── 卡片(台账条目;只取四态里的)
keys = {c[0] for c in COLS}
# ── 卡片 = **目标**(🔴🔴🔴 2026-10-06 用户两次定案)
#
# ⛔ 改前:遍历 `tasks.json`,把**台账条目**当卡片 ⇒ 列里显示的是任务
# (`3b-v1` / `ACCEP-FIX-01` / `dark-tokens`…)⇒ 用户:「已完成里面显示的到底
# 是不是目标 怎么看起来不像」「里面显示的都是目标 目标就是项目」。
# ✅ 现在:**一个目标 = 一张卡**,落哪一列看它自己的 `lifecycle`。
#
# 🔴🔴🔴 **一个工作区并非只有一个目标**(用户第三次纠正,实测坐实):
# · `tmp/supervise-inbox/goal.json` = **当前目标**(恒 1 个)
# · `tmp/supervise-inbox/goals/*.json` = **历史目标归档**(0..N 个,
# 文件名形如 `<标题截断>__YYYYMMDD-HHMM.json`,换目标时旧的那份挪进来)
# ⇒ **全部目标 = 当前 + 历史**,各自按 `lifecycle` 落列。
# 📌 实测 `vibe-product`:当前 1 个(已完成)+ 历史 2 个(进行中 1 / 已完成 1)
# ⇒ 「已完成」列**本来就该有一堆**(历史上做完的目标),这正是用户两次问的
# 「已完成里面明明有一堆内容」。
#
# ⚠️ 没登记目标的区(`agent-product` 实测)⇒ **不产卡片**(⛔ 别造一张"未登记目标"的卡
# 塞进「待执行」冒充目标 —— 那不是目标,是空位)。它的存在由概要/提示区说明。
# ⚠️ `lifecycle` 认不出(空/怪值)⇒ 也**不产卡片**,只计数(同族:⛔ 认不出不许静默塞列)。
goals_all = _load_all_goals(inbox)
cards = []
for tid, t in (tasks or {}).items():
t = t or {}
s = str(t.get("state") or "").strip().lower()
if s not in keys:
continue
cards.append({
"id": tid, "state": s,
"title": str(t.get("title") or tid),
"by": str(t.get("by") or ""),
"line": str(t.get("line") or ""),
"artifact": str(t.get("artifact") or ""),
"block_reason": str(t.get("block_reason") or ""),
"t_start": str(t.get("t_start") or ""),
"t_end": str(t.get("t_end") or ""),
})
life_unknown = []
for g, src in goals_all:
g_title = str(g.get("title") or "").strip()
g_short = str(g.get("short") or "").strip()
if not (g_title or g_short):
continue # 空壳(无标题无短名)⇒ 不是目标
g_name = g_title or g_short
life = goal_life_of(g)
col = LIFE_TO_COL.get(life) if life else None
if col:
# 🔴🔴 **判据必须挂在"该目标"自己身上**(2026-10-06 实测缺陷):
# 历史目标各有自己的 `acceptance_state`(实测:`把产品规划技能整理成唯一形态`
# 有 V1–V4,`补抓跳跃视界暗色主题…` 有 N1–N3),而 `p.acc` 只有**当前目标**的
# ⇒ 点历史目标的卡,详情页会显示**当前目标的判据**(张冠李戴)。
# ✅ 每条目标卡自带 `acc`(该目标自己的判据全集)。
g_acc = g.get("acceptance_state") or {}
g_eff = [k for k in g_acc if not str(k).startswith("_")]
g_items = [{"k": k, "v": str(g_acc[k]), "pass": _acc_is_pass(g_acc[k])}
for k in g_eff]
cards.append({
# 目标短 id + 来源后缀(防同 `short` 的多个目标 id 撞车,跳详情要用)
"id": (g_short or ws.name) + ("@cur" if src == "cur" else "@%s" % src),
"state": col, # ← 列键,来自 **lifecycle**(⛔ 非任务 state)
"title": g_name, # 目标名(=项目名)
"life": life, # 标准五值之一(原样带给前端显示)
"short": g_short,
"current": (src == "cur"), # 🔴 是不是**当前**目标(前端标出来)
"declared_at": str(g.get("declared_at") or "").strip(),
"life_at": str(g.get("lifecycle_at") or "").strip(),
"topics": [str(x) for x in (g.get("topics") or []) if str(x).strip()],
# 🔴 该目标**自己的**判据(⛔ 不是 `p.acc` —— 那是当前目标的)
"acc": {"n": len(g_eff), "pass": sum(1 for i in g_items if i["pass"]),
"items": g_items},
"goal_dir": str(inbox).replace("\\", "/"),
})
else:
# `已暂停` 等"有此项无列"的、以及状态读不出的 ⇒ 记下来另行提示,⛔ 不硬塞进任一列。
life_unknown.append("%s(%s)" % (g_name, life or "状态读不出"))
keys = {c[0] for c in COLS}
by = {k: 0 for k in keys}
for c in cards:
by[c["state"]] += 1
@@ -245,9 +372,27 @@ def build_project(ws: Path) -> dict:
acc_pass = sum(1 for k in eff if _acc_is_pass(acc[k]))
acc_items = [{"k": k, "v": str(acc[k]), "pass": _acc_is_pass(acc[k])} for k in eff]
# ── 工作流分组(按台账的 `line` 聚合)
# ── 工作流分组(按**任务台账**的 `line` 聚合)
# ⚠️ 这一块仍走 `tasks.json`(它本来就是"任务维度"的统计,与四列的目标维度**不冲突**):
# 四列=目标视图;`flows`/`queue`=该目标下的任务细账。两者**分层**,⛔ 不再混用。
flows = {}
for c in cards:
task_cards = []
for tid, t in (tasks or {}).items():
t = t or {}
s = str(t.get("state") or "").strip().lower()
if s not in keys:
continue
task_cards.append({
"id": tid, "state": s,
"title": str(t.get("title") or tid),
"by": str(t.get("by") or ""),
"line": str(t.get("line") or ""),
"artifact": str(t.get("artifact") or ""),
"block_reason": str(t.get("block_reason") or ""),
"t_start": str(t.get("t_start") or ""),
"t_end": str(t.get("t_end") or ""),
})
for c in task_cards:
ln = c["line"] or "(未分组)"
f = flows.setdefault(ln, {"line": ln, "total": 0, "done": 0, "open": 0})
f["total"] += 1
@@ -269,25 +414,47 @@ def build_project(ws: Path) -> dict:
return {
"ws": str(ws).replace("\\", "/"),
"dir": ws.name,
# 🔴 `name` = **当前目标**的名字(单项目视图的标题用它;无目标时是兜底串)
"name": name,
"self": _is_self,
"title": title,
"topics": topics,
"life": str(goal.get("lifecycle") or "").strip(),
# 🔴🔴 `life` / `life_raw` = **当前目标**的状态(归一化值 / 原始值)。
# ⚠️ 一个工作区可有**多个目标**(当前+历史)⇒ 别把这个当成"该区唯一状态";
# 四列的内容看 `cards`(每张卡自带 `life`)。
"life": goal_life_of(goal),
"life_raw": str(goal.get("lifecycle") or "").strip(),
"life_at": str(goal.get("lifecycle_at") or "").strip(),
"declared_at": str(goal.get("declared_at") or "").strip(),
"has_goal": bool(title or short),
# 🔴 有**任何**目标(含历史)就算登记过 —— ⛔ 别只看 `goal.json`
"has_goal": bool(goals_all) and any(
(str(g.get("title") or "").strip() or str(g.get("short") or "").strip())
for g, _ in goals_all),
# 🔴🔴 `n_goals` = 该区目标总数(当前+历史);`n_cur` 恒 1 或 0
"n_goals": len(cards) + len(life_unknown),
"n_current": sum(1 for c in cards if c.get("current")),
# 🔴 `cards` = **目标卡**(列内容;一个目标一张)|`tasks` = 任务细账(详情页用)
"cards": cards,
"tasks": task_cards,
"flows": [flows[k] for k in sorted(flows)],
# 🔴🔴 `queue` = **任务队列**统计(⛔ 不是四列计数!两者语义不同,别混用):
# 来源=`collabd-state.json::queue_info`(机制自己的台账)⇒ ⛔ 界面不重算。
# ⚠️ 改前 `pending/running/blocked/done` 用 `qby.get(x) or by.get(x)` 兜底,
# 而 `by` 当时数的是任务 ⇒ 碰巧对;**现在 `by` 数的是目标**,
# 若再留着这个兜底 ⇒ 会把"目标数"当"任务数"报出去(静默串数)。已去掉兜底。
"queue": {
"total": qi.get("n") or len(cards),
"pending": qby.get("pending") or by.get("pending", 0),
"running": qby.get("running") or by.get("running", 0),
"blocked": qby.get("blocked") or by.get("blocked", 0),
"done": qby.get("done") or by.get("done", 0),
"total": qi.get("n") or 0,
"pending": qby.get("pending") or 0,
"running": qby.get("running") or 0,
"blocked": qby.get("blocked") or 0,
"done": qby.get("done") or 0,
"open": (qby.get("pending") or 0) + (qby.get("running") or 0)
+ (qby.get("blocked") or 0),
},
# 🔴 目标维度计数(=四列各列的张数;一个目标算一个)
"goal_by_col": by,
# 🔴 有目标但**落不进四列**的(`已暂停` / 状态读不出)⇒ 前端另行提示,⛔ 不硬塞列
"life_unknown": life_unknown,
"acc": {"n": len(eff), "pass": acc_pass, "items": acc_items},
"region": str(_region_dir(ws, goal)).replace("\\", "/"),
"port": cfg.get("singleton_port"),