session-mechanism:两个看板定名(会话看板 / 项目看板)+ 包内代码一律相对路径

用户令(两句,同一轮):
  ①「会话技能中 能用相对路径的都要用相对路径」
  ②「把会话的 项目目标看板 改成 项目看板,
     项目看板 和 会话技能的任务看板 都重新命名 会话看板、项目看板」

## 一、改名:项目管理界面 → 项目看板
* 目录:boards/项目管理界面/ → boards/项目看板/(会话看板不变)
* 包内引用:路径性(3 行)+ 展示名(6 处,含 argparse description、启动横幅、
  错误日志前缀、模块 docstring、页面 <title> 与 .brand)
* HTML 标题:<title>项目管理 · 执行任务</title> → <title>项目看板 · 执行任务</title>

## 二、相对路径(新立第 11 条禁令)
判据=「这个根能不能从『我自己的位置』推出来」:
* 能(本包自己的目录)⇒ 从 __file__ 上跳推导
* 不能(工作区根 / 配置目录 / 文档库根)⇒ 读本包 env/roots.env
* 推不出来 ⇒ 如实报错 + 留痕(⛔ 不许静默扫空)

落点(共 4 处代码级硬编码,全部清掉):
* boards/项目看板/project-board.py:新增 _sm_load_roots();
  技能根改 HERE.parent.parent;WS_ROOTS 从 DSH_AIROOT 推导;
  推不出根时打警告 + 写 logs/project-board.err.log(原为静默扫出 0 个项目)
* scripts/selftest.py:生产区定位改为扫 DSH_AIROOT 下兄弟区(⛔ 不写死区名);
  两处测试假路径去掉盘符

⛔ 文档/注释里的绝对路径保持原样(举例、历史取证照写 —— 那是证据)。

## 验收
* 换机场景实测(技能包拷到 /tmp、不带 env/roots.env):
  无任何配置 ⇒ 如实报错;只给 DSH_AIROOT ⇒ 正常扫到 4 个项目
* selftest --filter 看板:PASS 110 / FAIL 0(报告型 1 条不计入)
* 项目看板服务:pid 40776 · HTTP 200 · <title>项目看板 · 执行任务</title>
  · /api/projects 出数据 · /healthz ok
* py_compile 三份改动文件全过
This commit is contained in:
admin committed 2026-10-14 16:33:28 +08:00
1 parent 52566c79f9
commit fe22f4cebe
5 files changed
+121 -19

No files matched your search

+14 -3
View File
@@ -68,7 +68,7 @@ agent_created: true
⚠️ 那篇里还有**文档四条规则**(分类索引/结论在最前/历史倒排·新的在前/单条 ≤6 KB)——
**⛔ 只写进工作区日志的规矩 = 没立**(10-04 实证:一条红线立在对的地方,换会话照样读不到)。
## 🔴🔴 第 0 步:加载门槛(2026-10-05 立 · ⛔ 这是**第一条**,在十条禁令之前)
## 🔴🔴 第 0 步:加载门槛(2026-10-05 立 · ⛔ 这是**第一条**,在十一条禁令之前)
> **判据(一句话)**:**用户说了触发句 ⇒ 第 0 步是先加载本技能,然后才动手。⛔ 没有"我大概知道"这条捷径。**
@@ -111,7 +111,7 @@ agent_created: true
- **⛔ 装完不停手**:装机 ≠ 完成目标。装完**继续**把触发句那个目标接下去(起常驻 → 查目标 → 派这一轮任务会话)。
⛔ 不许在装完或 `--verify` 全绿处停下、回头问"要不要继续干"—— 那是把已授权的事重新要签字。
## 🚨 十条禁令(2026-10-05 立 · ⛔ 每条都栽过,栽一次就重来一遍)
## 🚨 十一条禁令(2026-10-05 立 · 2026-10-14 加第 11 条 · ⛔ 每条都栽过,栽一次就重来一遍)
> 用户 2026-10-05 原话:「**你不要给我知道了,你给我记下来,每次都是知道了,知道了,过一会儿又忘了**」。
> ⇒ 所以这些话不放日志、不放事后复盘,**放在每次加载都会读到的地方**。⛔ 改任何东西之前先过一遍。
@@ -126,6 +126,10 @@ agent_created: true
8、⛔ **凡"东西自己反复触发",第一步读它的调度定义**(触发条件/重复间隔),⛔ 别从现象反推。
9、⛔ **验证通过的结论,落点只能是技能**(`SKILL.md` 第一屏 或 `references/`),⛔ 不许只写工作区日志。
10、⛔ **用户说"你知道了"时,⛔ 不许回答"知道了"** —— 去把它写成上面这种条文。
11、⛔ **包内代码不许留盘符字面量**(2026-10-14 用户令:「能用相对路径的都要用相对路径」)——
引用**本包自己的**文件一律从 `__file__` 推导;**推不出来的**(工作区根/配置目录/文档库根)读本包 `env/roots.env`。
⛔ 也不许"推不出来就回落到一个猜的盘符" —— 要**如实报错 + 留痕**(静默扫空与"本机真没有"表现一样,最难查)。
📌 判据见 §0「路径一律用相对」。
## 🗺️ 现状地图(2026-10-05 · ⛔ 看机制**先读这一屏**,读完再往下)
@@ -426,7 +430,14 @@ python .workbuddy/collab/collabd.py --set-life 进行中 --by "主会话"
🔴🔴 **未装即装(2026-10-14 用户令)**:用户说「使用项目机制完成目标:XXXX」而本区没装 ⇒ **先 `--apply --workspace <本区根>` 装好再干活**(判据与停止线见上面「第 0 步的连体动作」)。
🔴 **`--workspace` 要点名本区绝对路径**:`roots.env` 里 `DSH_WS_ROOT` 可能指向别的工作区(实测本机指向 `ai1net-dsh-server`)⇒ ⛔ 不让它顶替,装到别区 = 静默错归属。
🔴 **包内目录怎么分(2026-10-11 用户令:「脚本是脚本 配置是配置」)**:**每台机器各不同**的配置一律放本包 **`env/`**(`env/roots.env` 本机生成、不入库;`env/roots.env.example` 是入库样例);`scripts/` 只留通用代码。仓根 `.gitignore` 只放行 `env/**/*.example`。
🔴🔴 **看板一律进 `boards/<看板名>/`(2026-10-14 用户令:「把这两个看板都纳入会话技能中,分两个看板文件夹」)**:**自带页面的看板程序**(页面 + 脚本 + 启动器)**整包**放本包 `boards/` 下一个看板一个文件夹 ⇒ ⛔ 别把看板的 `.py` 散在 `scripts/`、页面散在 `assets/`(那样一个看板的东西分两处,改的时候必漏)。现有两个:`boards/会话看板/`(会话架构看板 · 端口 20099)/`boards/项目管理界面/`(项目+任务看板 · 端口 20100)。🔑 判据:**装进来的是"一个能跑起来的界面"⇒ 进 `boards/`;是"被别的程序调用的机制代码"⇒ 进 `scripts/`;是"纯样式或模板"⇒ 进 `assets/`**。
🔴🔴 **看板一律进 `boards/<看板名>/`(2026-10-14 用户令:「把这两个看板都纳入会话技能中,分两个看板文件夹」)**:**自带页面的看板程序**(页面 + 脚本 + 启动器)**整包**放本包 `boards/` 下一个看板一个文件夹 ⇒ ⛔ 别把看板的 `.py` 散在 `scripts/`、页面散在 `assets/`(那样一个看板的东西分两处,改的时候必漏)。现有两个:`boards/会话看板/`(会话架构看板 · 端口 20099)/`boards/项目看板/`(项目+任务看板 · 端口 20100)。🔑 判据:**装进来的是"一个能跑起来的界面"⇒ 进 `boards/`;是"被别的程序调用的机制代码"⇒ 进 `scripts/`;是"纯样式或模板"⇒ 进 `assets/`**。
🔴🔴 **路径一律用相对(2026-10-14 用户令:「会话技能中 能用相对路径的都要用相对路径」)**:包内脚本引用**自己的**文件(`scripts/`、`boards/`、`references/`、`assets/`)一律从 `__file__` 上跳推导,⛔ **不写盘符、不写用户目录**。🔑 **判据问一句:「这个根能不能从『我自己的位置』推出来?」**
- **能**(本包自己的目录)⇒ 推导。例:`boards/项目看板/project-board.py` 取包根=`HERE.parent.parent`(上跳两级),⛔ 不写 `"E:/ProgramData/.workbuddy/skills/session-mechanism"`。
- **不能**(工作区根、文档库根、宿主配置目录 —— 技能包跟它们**没有任何相对关系**)⇒ 那是「本机各不同的配置」⇒ **读本包 `env/roots.env`**(`DSH_AIROOT`/`DSH_WS_ROOT`/`CODEBUDDY_CONFIG_DIR`/`DSH_DOCS_ROOT`),⛔ 不在代码里留盘符字面量。
- **推不出来 ⇒ 如实报错 + 留痕**(⛔ 不许静默扫空 —— "扫不到"与"本机真没有"表现完全一样,是最难查的一类假象)。
- ⚠️ **文档/注释里的绝对路径不在此列**(举例、历史取证、报障记录照写 —— 那是证据,改了反而不忠实)。本条只管**会真正被执行的代码**。
- 📌 落点:`boards/项目看板/project-board.py`(`_sm_load_roots()` + 技能根上跳两级 + `WS_ROOTS` 从 `DSH_AIROOT` 推导)、`scripts/selftest.py`(生产区定位改为扫 `DSH_AIROOT` 下兄弟区,⛔ 不写死某个区名)。
**⓪ 我要动手(建判据/写验收/报"已完成"/任何带"现在·已·还差"的结论)** ⇒ **先读 `references/00-动手前必过.md`**(🔴 **动手层**,2026-10-04 建,只有三条动作:① 查状态**问程序自己**,⛔ 不许 glob/手拼路径 ② 引用任何读数**先看它什么时候写的** ③ 写判据**先让基线全绿**再谈抓得住 —— 每条都对应当天真实翻车,且都已落判据不许退化)。
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""项目管理界面的**启动器**(计划任务动作指向它,⛔ 不许直起 `project-board.py`)。
"""项目看板的**启动器**(计划任务动作指向它,⛔ 不许直起 `project-board.py`)。
## 为什么必须有这一层(同族 P0-73/74)
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>项目管理 · 执行任务</title>
<title>项目看板 · 执行任务</title>
<style>
:root{
--bg:#0f1115; --panel:#171a21; --panel2:#1e222b; --line:#2a2f3a;
@@ -249,7 +249,7 @@
<body>
<header>
<div class="hrow">
<div class="brand">项目管理</div>
<div class="brand">项目看板</div>
<select id="psel" aria-label="选择工作区" title="选择工作区"></select>
<div class="spacer"></div>
<span class="stamp" id="stamp">加载中…</span>
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""项目管理界面 —— 管**本机所有工作区**的项目(照 dsh-task-board 的「项目 × 卡片」模式)。
"""项目看板 —— 管**本机所有工作区**的项目(照 dsh-task-board 的「项目 × 卡片」模式)。
## 这是什么(用户原话)
@@ -64,18 +64,71 @@ from pathlib import Path
HERE = Path(__file__).resolve().parent
PAGE = HERE / "project-board.html"
# 🔴 扫哪些根目录找工作区(`;` 分隔;可用环境变量覆盖)
WS_ROOTS = [Path(p) for p in (
os.environ.get("DSH_PM_BOARD_ROOTS") or "E:/ProgramData/AIProject"
).split(";") if p.strip()]
def _sm_load_roots():
"""把本包 `env/roots.env` 注入进程 env(宿主已有的**不动** ⇒ `setdefault`)。
🔴🔴 2026-10-14(用户令:「会话技能中 能用相对路径的都要用相对路径」):
本文件原先把两处根写死成 `E:/ProgramData/...` 绝对路径 ⇒ 换机 / 换用户目录就全错。
正解分两半(判据=**这个根能不能从"我自己的位置"推出来**):
· **能**(本包自己的目录,如 `scripts/`、`boards/`)⇒ 从 `__file__` 上跳推导,⛔ 不写死;
· **不能**(工作区根 —— 技能包在配置目录下,跟工作区没有任何相对关系)
⇒ 属"本机各不同的配置" ⇒ 读本包 `env/roots.env`(`install.py` 生成,不入库)。
⚠️ 从 `boards/项目看板/` 到包根要上跳**两级**。
"""
import os as _os
_here = _os.path.dirname(_os.path.abspath(__file__))
for _up in range(5):
_base = _os.path.normpath(_os.path.join(_here, *([".."] * _up)))
for _rel in (("env", "roots.env"), ("roots.env",)):
_p = _os.path.join(_base, *_rel)
if _os.path.isfile(_p):
try:
with open(_p, encoding="utf-8") as _f:
for _ln in _f:
_ln = _ln.strip()
if _ln and not _ln.startswith("#") and "=" in _ln:
_k, _v = _ln.split("=", 1)
_os.environ.setdefault(_k.strip(), _v.strip())
except Exception:
pass
return _p
return ""
_sm_load_roots()
# 🔴 扫哪些根目录找工作区(`;` 分隔;可用环境变量覆盖)。
# ⚠️ 2026-10-14:默认值原写死 `E:/ProgramData/AIProject` ⇒ 改为**按序推导**(全部相对,⛔ 无盘符):
# ① 宿主 env / `env/roots.env` 的 `DSH_AIROOT`(工作区根的父目录 —— 唯一权威来源)
# ② `DSH_PM_BOARD_SELF_WS`(本看板服务的那个工作区)的父目录
# ③ 都取不到 ⇒ **空**,并由下面的 `_ws_roots()` 在真正要用时**如实报错**,
# ⛔ 不做"兜底猜一个盘符"那种事(猜错 ⇒ 扫到别人的工作区,静默出错数据)。
def _ws_roots() -> list:
raw = (os.environ.get("DSH_PM_BOARD_ROOTS") or "").strip()
if not raw:
raw = (os.environ.get("DSH_AIROOT") or "").strip()
if not raw:
_self = (os.environ.get("DSH_PM_BOARD_SELF_WS") or "").strip()
if _self:
try:
raw = str(Path(_self).resolve().parent)
except Exception:
raw = ""
return [Path(p.strip()) for p in raw.split(";") if p.strip()]
WS_ROOTS = _ws_roots()
# 技能自带的新建入口(⛔ 不自己另写一套建区逻辑 —— 同族"一条规则两份实现"必漂)
# ⚠️ 2026-10-14:看板整包搬进 `boards/`(用户令「开个 boards 文件夹放两个看板」)⇒
# **看板真身**与**机制脚本**不再同目录,⛔ 别再用一个常量指两处。
# · `SKILL_BOARD_DIR` = 会话看板(`board.py::acc_is_pass` 的取用处)
# · `SKILL_SCRIPTS` = 机制脚本(`init_workspace.py` 的取用处)
_SKILL_ROOT = Path(os.environ.get("DSH_SKILL_ROOT")
or r"E:/ProgramData/.workbuddy/skills/session-mechanism")
# 🔴 技能根=**从本文件位置上跳两级**(`boards/项目看板/` → `boards/` → 包根)——
# 纯相对,⛔ 不再写死盘符(2026-10-14 用户令「能用相对路径的都要用相对路径」)。
# 宿主 env `DSH_SKILL_ROOT` 仍优先(部署方显式指定时以它为准)。
_SKILL_ROOT = Path(os.environ.get("DSH_SKILL_ROOT") or HERE.parent.parent)
SKILL_BOARD_DIR = Path(os.environ.get("DSH_SKILL_BOARD_DIR")
or str(_SKILL_ROOT / "boards" / "会话看板"))
SKILL_SCRIPTS = Path(os.environ.get("DSH_SKILL_SCRIPTS")
@@ -258,6 +311,24 @@ def scan_workspaces() -> list:
"""
installed, skipped = [], []
seen = set()
if not WS_ROOTS:
# 🔴 2026-10-14:默认值去掉了写死盘符 ⇒ "推不出来"成了一种真实可能
# (换机 / 没跑过 `install.py` / `env/roots.env` 丢了)。
# ⇒ **如实报错 + 留痕**,⛔ 不许静默扫出"0 个项目"
# (那与"本机真的没有工作区"表现完全一样)。
_say("⛔ 推不出工作区根 ⇒ 一个区都扫不到。请设 `DSH_AIROOT`(工作区根的父目录),"
"或跑一次 `python install.py --apply --workspace <本区>`。")
try:
_d = _SKILL_ROOT / "logs"
_d.mkdir(parents=True, exist_ok=True)
with open(_d / "project-board.err.log", "a",
encoding="utf-8", errors="replace") as _f:
_f.write("[项目看板] ⛔ 推不出工作区根(`DSH_AIROOT` / "
"`DSH_PM_BOARD_ROOTS` / `DSH_PM_BOARD_SELF_WS` 全空,"
"且 `env/roots.env` 缺 `DSH_AIROOT`)\n")
except Exception:
pass
return installed, skipped
for root in WS_ROOTS:
if not root.is_dir():
continue
@@ -717,7 +788,7 @@ class Handler(BaseHTTPRequestHandler):
def main() -> int:
ap = argparse.ArgumentParser(description="项目管理界面(本机所有工作区 · 增删查)")
ap = argparse.ArgumentParser(description="项目看板(本机所有工作区 · 增删查)")
ap.add_argument("--serve", type=int, default=0, metavar="PORT",
help="起 HTTP 服务(默认不启,只做一次扫描并打印摘要)")
ap.add_argument("--once", action="store_true", help="扫一次并打印摘要")
@@ -725,7 +796,7 @@ def main() -> int:
if a.serve:
srv = ThreadingHTTPServer(("127.0.0.1", a.serve), Handler)
_say("项目管理界面已起:http://127.0.0.1:%d/ (根目录 %s)"
_say("项目看板已起:http://127.0.0.1:%d/ (根目录 %s)"
% (a.serve, ", ".join(str(r) for r in WS_ROOTS)))
try:
srv.serve_forever()
+24 -4
View File
@@ -3112,7 +3112,10 @@ def t_ws_attribution():
# ⛔ 弱锚点的危险:把 `attribution` 从源码里删掉但留个同名注释,它照样绿。
_rep = {}
try:
_rep = ev.env_check("E:/ProgramData/__selftest_no_such_ws__")
# 🔴 2026-10-14:传的是**故意不存在**的路径(验"读不出来要如实报")。
# ⛔ 去掉盘符 —— 用纯名字即可,判据只关心"它读不出来",
# 带盘符反而把这行读成"本机路径"(用户令:能用相对路径的都用相对路径)。
_rep = ev.env_check("__selftest_no_such_ws__")
except Exception as _e:
R.append(("⛔ `env_check` 真跑失败(%r)⇒ 后三条判据无法验" % (_e,), False))
_att = (_rep.get("attribution") or {}) if isinstance(_rep, dict) else {}
@@ -3660,7 +3663,9 @@ def t_all_sessions_idle_execution_judge():
m = imp()
import sqlite3
WS_FAKE = "E:/__selftest_ws__"
# 🔴 2026-10-14:假工作区路径**不带盘符** —— 它只用于内存库比对,
# 盘符是噪音(用户令:能用相对路径的都用相对路径)。
WS_FAKE = "__selftest_ws__"
ME_FAKE = "aaaaaaaa-0000-4000-8000-000000000001"
OTHER = "bbbbbbbb-0000-4000-8000-000000000002"
DONE = "cccccccc-0000-4000-8000-000000000003"
@@ -4122,8 +4127,23 @@ def t_execution_doc():
_cands = []
if os.environ.get("COLLABD_PROD_CONFIG"):
_cands.append(Path(os.environ["COLLABD_PROD_CONFIG"]))
_cands.append(Path(r"E:/ProgramData/AIProject/ai1net-dsh-server"
r"/.workbuddy/collab/collabd.config.json"))
# 🔴🔴 2026-10-14(用户令:「能用相对路径的都要用相对路径」):
# 原来这里兜底**写死** `E:/ProgramData/AIProject/ai1net-dsh-server` ⇒ 换机必假红。
# 改成**扫 `DSH_AIROOT` 下的兄弟区**(取第一个装了机制的)—— ⛔ 不写死某个区名:
# 判据="哪个区是生产区"对本脚本没有意义,它只要一份**真实**的部署配置来读 `workspace`;
# 写死区名反而把"换机后区名不同"这件事变成永久假红。
# ⚠️ `DSH_AIROOT` 由 `env/roots.env` 提供(它自己就是"本机各不同的配置",正解)。
_airo = (os.environ.get("DSH_AIROOT") or "").strip()
if _airo:
try:
for _d in sorted(Path(_airo).iterdir()):
if not _d.is_dir() or _d.name.startswith((".", "_")):
continue
_p = _d / ".workbuddy" / "collab" / "collabd.config.json"
if _p.is_file():
_cands.append(_p)
except Exception:
pass
for _cand in _cands:
try:
_cfg = _j.loads(_cand.read_text(encoding="utf-8"))