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:
1 parent
30b46dbd0c
commit
c1b5e4d966
735 files changed
+153192
-2415
No files matched your search
+195
-28
@@ -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"),
|
||||
|
||||
Reference in new issue
Block a user