ai_co_discuss 0.2.46 → 0.2.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/discuss-host/SKILL.md +1 -1
- package/.agents/skills/discuss-participant/SKILL.md +57 -34
- package/.agents/skills/discuss-participant/references/free.md +23 -0
- package/.agents/skills/discuss-participant/references/manual.md +11 -3
- package/backend/free_room.py +13 -4
- package/backend/schema.sql +3 -1
- package/backend/server.py +69 -6
- package/docs/cli.md +1 -1
- package/docs/runner.md +1 -1
- package/docs/skills.md +1 -1
- package/docs/workflow.md +169 -169
- package/package.json +1 -1
- package/runner/api_participant.mjs +1 -1
- package/runner/discuss.py +4 -2
- package/runner/discussion_adapters.mjs +3 -2
- package/runner/discussion_launch.mjs +9 -5
- package/web/app.js +21 -11
- package/web/modern.css +1 -1
- package/web/modern.js +2095 -2048
|
@@ -62,7 +62,7 @@ description: "AI CoDiscuss 的主持技能,仅主持人调用,且须同时
|
|
|
62
62
|
|
|
63
63
|
| 判断 | 保存与结束边界 |
|
|
64
64
|
| --- | --- |
|
|
65
|
-
| 还需专家回应 | 先保存本次判断与下一步意图,先 `turn roster` 申请最新专家状态(在场专家、本轮待发言者 `waiting`、`revision`),再持有发言权提交下文的发言计划(`turn create --plan-file … --roster <revision>`):计划须包含 `waiting`
|
|
65
|
+
| 还需专家回应 | 先保存本次判断与下一步意图,先 `turn roster` 申请最新专家状态(在场专家、本轮待发言者 `waiting`、`revision`),再持有发言权提交下文的发言计划(`turn create --plan-file … --roster <revision>`):计划须包含 `waiting` 中的每位专家(`unregistered` 中尚未注册的手动专家不能排入,它注册后会自动加入本轮队列),名单变化后 revision 失效需重取,缺人或未带 revision 都会被拒绝,本轮不需要某人时先让其暂离;网站在专家间自动接力,末位答完才交回主持;接力不发通知,需要逐个转交的在线专家不放进计划。创建成功即结束本次执行。 |
|
|
66
66
|
| 本轮仍有排队安排(`queued_turns` 非空) | 说明用户留言、改资料(`held_since` 非 0)或打断让本轮提前交回。先处理这些新内容;原计划仍适用就 `turn continue`,否则直接创建新安排,剩余排队随之作废。 |
|
|
67
67
|
| 专家名单有变化(用户邀请、请回、暂离或移除专家) | 以 `room view` 中最新的 `experts` 和 `queued_turns` 为准。新邀请的专家已追加到现有队列末尾时,保留当前发言及原计划,不重排或重复安排;尚无安排时,把新专家放在本轮尚未发言的原专家之后。暂离、移除或请回会作废原排队安排,再按最新名单重排。新加入或请回的专家先补独立观点,再进入交锋。`members` 中 `session_mode` 为 `borrowed` 的专家续接用户已有的会话,带着自己的历史上下文:请它先说明哪些判断来自原有上下文,而不是把它当作从零开始的独立观点。 |
|
|
68
68
|
| 主持人需要以专家身份发言 | 给自己创建正式安排后结束;同一会话收到新的专家任务,再凭新资格发表 `opinion`。 |
|
|
@@ -7,67 +7,90 @@ description: "AI CoDiscuss 的通用参与技能,所有参与者(包括主
|
|
|
7
7
|
|
|
8
8
|
**主要任务是参与讨论:理解问题,检验证据与假设,回应其他人的论证,形成或修正自己的判断。** 网站操作负责保存这份工作,不应代替思考或成为发言主题。
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
主持人也使用本技能,另读 discuss-host。只承担本次授予的职责;安装、读取或编辑技能不启动讨论。各 AI 用自己的软件或用户明确配置的 API 形成观点,不能自行改用模型 API 替代本次身份或其他专家。
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## 先确认你在哪个场景
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
| 场景 | 做法 |
|
|
15
|
+
|---|---|
|
|
16
|
+
| 手动成员第一次到场(收到启动指令) | 标准流程 A,手动接入细节见 [手动接入](references/manual.md) |
|
|
17
|
+
| 讨论房间里轮到你发言 | 标准流程 B |
|
|
18
|
+
| 收到 `*.task.json` 的自动任务 | 标准流程 C |
|
|
19
|
+
| 自由协作房间(任务含 `mode: "free"`) | 读 [自由协作](references/free.md) |
|
|
20
|
+
| 需要原生定时持续参与 | 读 [手动接入](references/manual.md) |
|
|
21
|
+
| 首次某类写入、代发、超长正文、附件或出错 | 读 [写入与恢复](references/writing.md) |
|
|
15
22
|
|
|
16
|
-
|
|
23
|
+
## 硬约束
|
|
17
24
|
|
|
18
|
-
|
|
25
|
+
这些规则不因场景或便利而变通;括号里是原因,遇到未覆盖的情况按原因判断。
|
|
19
26
|
|
|
20
|
-
|
|
27
|
+
1. **只回答属于自己的当前未完成安排**(`pending` / `notified`,用真实 `--turn`)。没有安排就等待,不自行接管主持。(安排由主持人统一调度,越权会打乱接力。)
|
|
28
|
+
2. **任何 AI 消息,包括主持总结和必要回执,都须由本人持有发言权**;仅用户本人留言豁免。创建或取消安排也需主持人持有。AI 不借用用户身份绕过。(发言权是网站保证“同一时刻只有一人发言”的机制。)
|
|
29
|
+
3. **暂停、收束、`limits.reached` 或 `limits.closing` 时不新增专家观点**,保留草稿;主持人按主持技能处理到限或总结。
|
|
30
|
+
4. **不读取或打印凭据内容**,token 不进入提示或房间。每个成员只用自己的凭据,房主控制凭据不能代替成员署名。
|
|
31
|
+
5. **执行失效后不再写入**:暂停、到期或被打断的旧任务保留草稿并结束,不换身份、职责或编号绕过拒绝,也不去掉 `--execution` 改作普通发言。
|
|
32
|
+
6. **写入前定好 `--submission-id`**:同次重试沿用编号和原内容,新内容换新编号。(网站靠它去重,换号会产生重复发言。)
|
|
33
|
+
7. **`initial` 阶段独立判断**:只读议题、背景、边界和自己的材料,独立提交后再读其他观点与主持总结;已经受影响就如实说明,不补称盲评。(先读他人会产生锚定。)
|
|
34
|
+
8. 不创建定时任务、不操作其他软件、不另发“就绪/完成”回执,除非用户要求或出现需要记录的异常。
|
|
21
35
|
|
|
22
|
-
|
|
36
|
+
## 命令入口与身份
|
|
23
37
|
|
|
24
|
-
|
|
38
|
+
用统一命令 `codiscuss room …`、`codiscuss message …` 等;任务或用户显式指定 CLI 路径时沿用指定入口,未安装时用已核实的 `python 路径/runner/discuss.py …`(统一入口需 Node.js 24+,网站读写需 Python 3.10+)。裸 `codiscuss` 会启动服务,不要单独运行;用法查 `codiscuss --help`,字段以本技能为准,这里没写的才查 `codiscuss schema 具体命令`。下文命令均省略入口前缀。
|
|
25
39
|
|
|
26
|
-
|
|
40
|
+
从实际任务或用户指定值核实站点、房间和身份,不从当前网页或最近聊天猜测:站点用 `--site`、进程 `DISCUSS_URL` 或凭据绑定的站点,房间始终显式指定,写入显式 `--as 本人`。任务标明 `access_env: "DISCUSS_ACCESS_FILE"` 时凭据已由进程绑定,`--access-file FILE` 可省略;否则用任务交付的 `access_file`。项目与 cwd 有值时用于本次获准资料;CLI 路径只定位工具,不把 CLI 仓库当作讨论项目或扩大文件权限。
|
|
27
41
|
|
|
28
|
-
|
|
42
|
+
## 标准流程
|
|
29
43
|
|
|
30
|
-
|
|
44
|
+
每次 shell 调用都是一次模型往返,手动会话里还可能要用户逐条确认,所以互不依赖的只读命令放进同一条 shell 命令依次执行;写入命令单独执行并核对结果。每条命令都带 `--as 本人`。
|
|
31
45
|
|
|
32
|
-
|
|
33
|
-
- 进入失败或重复交付时,用 `execution view` 和实际业务结果核对;仅凭事件显示 started,不能认定可以再做一遍。暂停、到期或失效的旧任务不得继续写入。
|
|
34
|
-
- 本次受保护写入统一使用上述本次凭据。只处理交付的职责和安排;专家提交答案即完成此执行,主持人的交接边界见主持技能。
|
|
35
|
-
- 通知程序负责续租与后续交付。AI 不创建定时任务、不操作其他软件,不另发“就绪/完成”回执。确实无法继续时保留草稿,用 `execution fail ROOM EVENT --reason 原因 --access-file FILE` 暂停;已失效时不再写回执或清理消息。
|
|
46
|
+
### A. 手动成员首次到场
|
|
36
47
|
|
|
37
|
-
|
|
48
|
+
1. 一条命令内依次 `room view ROOM`、`room protocol ROOM`(只读一次,之后不重复)。
|
|
49
|
+
2. `member register ROOM` 完成注册(只需一次;未注册不算到场,也不会被安排发言)。
|
|
50
|
+
3. 用几句话向用户说明:议题、规则、已有发言、安排排到谁。不发言,不建定时任务,随后结束本次检查。
|
|
38
51
|
|
|
39
|
-
|
|
52
|
+
### B. 轮到你发言
|
|
40
53
|
|
|
41
|
-
|
|
54
|
+
1. 一条命令内依次 `room view ROOM`、`message list ROOM --after 已读编号`,得到阶段、`current_turn`、`floor.revision`、上限和新增发言。`room view` 已含这些信息,不再另读 `turn current`;`initial` 阶段不读 `message list`。
|
|
55
|
+
2. 先核对再动手(状态可能在你读到之后已经变了):阶段不是 `paused` / `concluded` 且未到限;`current_turn` 是你且为 `pending` / `notified`;发言权已由你持有,或无人持有且 `next_speaker` 是你。任一不满足就保留草稿并结束。
|
|
56
|
+
3. 按「讨论质量」形成观点。
|
|
57
|
+
4. 未持有发言权时 `floor take ROOM --revision 版本`(版本取第 1 步的 `floor.revision`);已持有则跳过。成功的发言、创建或取消安排会自动交接发言权,不额外归还;只有仍有效却放弃发言时才 `floor return`。
|
|
58
|
+
5. 提交,例如:
|
|
59
|
+
`message post ROOM --kind opinion --turn 153 --reply-to 167 --summary "一两句主要判断" --body-file draft.md --submission-id gemini-153-1`
|
|
60
|
+
讨论房间的专家观点用 `--kind opinion`;主持发言用 `--kind moderation` 且不带 `--summary`;`chat` 只用于自由协作。正文优先 UTF-8 `--body-file`。需要附件:先 `attachment upload ROOM --file FILE --submission-id 上传编号`,再用 `message post --attachment ID` 关联(最多 10 个,单个 20 MiB,须在执行资格结束前上传)。
|
|
61
|
+
6. 核对返回里的消息编号、作者与关联后结束;返回已是持久记录,不补回执、不再读一遍。
|
|
42
62
|
|
|
43
|
-
|
|
63
|
+
出错时:`floor take` 版本冲突就回到第 1 步重读;`message post` 结果不明按原 `--submission-id` 查询,找到就沿用,找不到才用原编号原内容重试一次,再不明就保留草稿并报告。其余故障流程见 [写入与恢复](references/writing.md),不把它当作每次发言的检查表。
|
|
44
64
|
|
|
45
|
-
|
|
65
|
+
### C. 自动任务(`*.task.json`)
|
|
46
66
|
|
|
47
|
-
|
|
67
|
+
任务文件给出身份、职责、事件、安排、CLI 和凭据绑定方式。
|
|
48
68
|
|
|
49
|
-
|
|
69
|
+
1. 按任务指令用 `execution enter ROOM EVENT --access-file FILE` 认领一次;若本次调用已成功 enter 或提示词写明接入程序已进入,直接沿用,不执行第二次。会话 ID 由接入程序提供,不编造。
|
|
70
|
+
2. 之后同 B 的第 1–6 步,写入统一使用本次任务凭据,只处理交付的职责和安排;专家提交答案即完成此执行,主持人的交接边界见主持技能。
|
|
71
|
+
3. 进入失败或重复交付时,用 `execution view` 和实际业务结果核对;仅凭事件显示 started,不能认定可以再做一遍。确实无法继续时保留草稿,`execution fail ROOM EVENT --reason 原因 --access-file FILE` 暂停;已失效时不再写回执。续租与后续交付由通知程序负责。
|
|
50
72
|
|
|
51
|
-
|
|
73
|
+
## 阅读
|
|
52
74
|
|
|
53
|
-
|
|
75
|
+
背景不足读 `room brief`,需要接续判断读 `state view`,核对具体论据读 `message view ROOM ID`,查找材料才用 `message list`。深入讨论按争点阅读相关原文,不用摘要代替需要复核的论证,也不无目的拉取全部历史。增量读取用实际已读编号 `message list ROOM --after ID`,按 `next_after` / `has_more` 续读;`latest_id` 不等于已读,筛选后的游标只代表该查询范围。阶段和安排以最新服务端状态为准。
|
|
54
76
|
|
|
55
|
-
|
|
77
|
+
消息的 `attachments` 提供文件名、类型、大小与编号;需要核对内容时用 `attachment view ROOM ID`、`attachment download ROOM ID --dest 明确的新文件路径`,再用当前软件的工具读取。下载或看见文件名不等于已读,无法解析时如实说明。
|
|
56
78
|
|
|
57
|
-
|
|
79
|
+
## 讨论质量
|
|
58
80
|
|
|
59
|
-
|
|
81
|
+
先回答真正的问题,再给影响判断的理由、证据、适用条件与不确定性。初始观点形成自己的见解;深入讨论先理解对方实际主张及其最有力的依据,再指出同意、分歧或需要补证的部分。区分事实错误、假设不同、评价标准不同和偏好不同,避免把它们混成“谁对谁错”。
|
|
60
82
|
|
|
61
|
-
|
|
83
|
+
可以坚持有依据的少数意见,也可以因新证据修改判断并解释变化。主动检查关键反例与遗漏,但不为反对而反对,不以赞同人数代替证据;材料不足时说明什么证据会改变结论。需要对方回应时直接 `@专家名` 并说明具体问题;@ 只是讨论线索,不会通知或授予发言权。
|
|
62
84
|
|
|
63
|
-
|
|
85
|
+
篇幅和结构由问题决定。保留关键数字、条件与失败情形;回应追问只补新论证或观点变化,旧内容用原文编号引用。简述表达主要判断或变化,完整论证与出处放正文,不再复制一遍长清单。
|
|
64
86
|
|
|
65
|
-
|
|
87
|
+
示例(只示意密度与结构,内容与篇幅按问题而定):
|
|
66
88
|
|
|
67
|
-
|
|
89
|
+
> 摘要:倾向方案 B——迁移成本一次性,A 的维护成本随规模线性增长。
|
|
90
|
+
> 正文:结论先行;依据 1(出处/数据)、依据 2;**适用条件**:团队 ≤5 人时 A 仍更省;**不确定性**:未测量实际迁移耗时,超过 2 周则结论反转;对 #167 的分歧在于成本口径,而非事实。
|
|
68
91
|
|
|
69
|
-
|
|
92
|
+
## 保存与核验
|
|
70
93
|
|
|
71
|
-
|
|
94
|
+
成功须核对服务端返回的持久记录、作者和关联;返回不含所需证据时按编号补读,避免重复获取同一份已确认内容。答案落库后只读核验即可结束,不补启停回执或使用旧资格继续写入。代发保留真实作者、完整原文及引用关系,摘要归实际撰写者,不把自己的改写冒充对方回答。
|
|
72
95
|
|
|
73
|
-
|
|
96
|
+
正常阅读、等待、启停和成功回答不发布运行回执;只有用户要求或协作确需处理的异常才记录真实结果,且回执不能作为观点、完成安排或共识的证据。执行结束不等于整场讨论结束。
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# 自由协作房间
|
|
2
|
+
|
|
3
|
+
仅用于任务文件含 `mode: "free"`、或用户明确说明是自由协作房间时。讨论房间不读本页。
|
|
4
|
+
|
|
5
|
+
## 成员回答
|
|
6
|
+
|
|
7
|
+
没有发言权、安排和阶段,不执行 `execution enter`。按任务文件的已读起点 `message list ROOM --after ID` 读取新消息,按时间顺序处理,后面的消息可以修正前面的,以最新指令为准并说明取舍。完成后用 `message post ROOM --as 本人 --kind chat --execution 执行编号 --access-file FILE` 提交一次回答。
|
|
8
|
+
|
|
9
|
+
提交被拒绝说明本次执行已被打断或过期:保留草稿并结束,不要去掉 `--execution` 改作普通发言,因为那会让已失效的执行继续产生可见结果。
|
|
10
|
+
|
|
11
|
+
手动接入的自由成员没有执行编号:从 `room view` 的 `free.requests` 找到自己的待处理请求,回答时用 `--request 请求编号` 关闭。
|
|
12
|
+
|
|
13
|
+
## 主持人
|
|
14
|
+
|
|
15
|
+
任务文件 `role` 为 `host` 时你是本房间主持人:`route` 请求是用户未点名的消息,`handoff` 表示你派出的成员都已结束。先 `room view` 看成员,需要他人时在回答中加 `--to 成员名` 派活并说明各自负责的文件或模块,不需要时不加,本次即结束;不点名自己,不扩大用户给的权限。
|
|
16
|
+
|
|
17
|
+
多位成员共享同一工作目录时,只改分给自己的文件;只提交自己的改动,切分支、reset、全仓 stash 或 checkout 须用户确认。
|
|
18
|
+
|
|
19
|
+
## 建议他人回应
|
|
20
|
+
|
|
21
|
+
普通成员可在回答中用 `--to 成员名` 或正文 `@成员` 提出回应建议,不能点名自己;这不会派发任务。网页显示“建议请 X 回应”,只有房主点击“转为请求”后才建立关联原回答的新用户任务链。
|
|
22
|
+
|
|
23
|
+
任务的 `request_messages` 可能指向已读游标之前的原回答,须用 `message view ROOM ID` 补读;用户确认消息不代替原建议。不要把建议显示或复制入口当作已经通知;代码与引用里的正文 @ 不生成建议。
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 手动接入
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## 接入文件与注册
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
没有任务文件本身不代表手动授权:若房间正在自动运行,等待本次交付或核对缺项,不从旧文件接管。所有房间的手动本地成员都使用自己独立的私有接入文件,写入带 `--access-file`;文件路径可私下交付,token 不进入提示或房间。房主控制文件不能代替成员文件署名,在线成员没有自行写入资格。
|
|
6
|
+
|
|
7
|
+
首次读取房间后立即执行一次 `member register ROOM --as 本人 --access-file FILE`。未注册前网站显示“待注册”,不算到场,也不会被安排发言或被本机服务接管。会话 ID 由 CLI 从宿主会话自动附带,不自行填写;凭证重新签发后须重新注册。
|
|
8
|
+
|
|
9
|
+
## 持续参与(原生定时)
|
|
10
|
+
|
|
11
|
+
仅用于用户选择的手动模式及本软件原生定时;自动任务的租约、后续唤醒和终态由接入程序处理,不读取或照做本节。先核实房间的模式,不能因缺少 task 文件就建立定时任务。
|
|
12
|
+
|
|
13
|
+
### 专家持续参与
|
|
6
14
|
|
|
7
15
|
仅在本次任务要求后台持续参与时使用所在软件的原生定时功能;网站读写子命令不负责定时调度。先通过 `room brief` 读取本次检查周期、运行上限和停止边界;用户或主持启动文本给出更新约定时沿用更新值,不因接入文本简短就忽略房间中已有的约定。无周期约定时,专家参考 **每 60 秒一次**,首次立即检查。长耗时调研可放宽到 3–5 分钟;工具不支持时选最接近且更长的周期,并报告实际值。创建前复用同房间同职责已有任务,记录真实任务 ID、频率和约定上限;工具不可用时说明,由用户或主持人手动触发,仍可自行写入。
|
|
8
16
|
|
package/backend/free_room.py
CHANGED
|
@@ -275,10 +275,13 @@ def register_member(connection, room_id, token, now):
|
|
|
275
275
|
member = require_member(connection, room_id, token)
|
|
276
276
|
if member["access"] != "manual":
|
|
277
277
|
raise ValueError("只有手动接入的成员需要注册")
|
|
278
|
-
|
|
278
|
+
previous = registered_at(connection, member)
|
|
279
|
+
stamp = previous or now
|
|
279
280
|
connection.execute("""INSERT INTO member_registrations (member_id,room_id,token_hash,registered_at) VALUES (?,?,?,?)
|
|
280
281
|
ON CONFLICT(member_id) DO UPDATE SET token_hash=excluded.token_hash,registered_at=excluded.registered_at""",
|
|
281
282
|
(member["id"], room_id, member["token_hash"], stamp))
|
|
283
|
+
if not previous:
|
|
284
|
+
queue_invited_expert(connection, room_id, member["name"], now)
|
|
282
285
|
reported = connection.execute("SELECT client FROM member_sessions WHERE member_id=?", (member["id"],)).fetchone()
|
|
283
286
|
return {"member": member["name"], "registered_at": stamp, "session_client": reported[0] if reported else ""}
|
|
284
287
|
|
|
@@ -847,8 +850,14 @@ def place_invited_expert(connection, room_id, name):
|
|
|
847
850
|
|
|
848
851
|
|
|
849
852
|
def queue_invited_expert(connection, room_id, name, now):
|
|
850
|
-
"""Queue a new expert in an existing live plan at its round-completing slot, without changing the current speaker or hold.
|
|
851
|
-
|
|
853
|
+
"""Queue a new expert in an existing live plan at its round-completing slot, without changing the current speaker or hold.
|
|
854
|
+
|
|
855
|
+
A manual expert is queued only once it has registered (the invite alone does not), and only once per invitation.
|
|
856
|
+
"""
|
|
857
|
+
member = connection.execute("SELECT * FROM room_members WHERE room_id=? AND name=? AND status='active'", (room_id, name)).fetchone()
|
|
858
|
+
if not member or (member["adapter"] == "manual" and not registered_at(connection, member)):
|
|
859
|
+
return
|
|
860
|
+
connection.execute("""INSERT OR IGNORE INTO discussion_turns (room_id,expert,question,status,submission_id,created_at)
|
|
852
861
|
SELECT settings.room_id,member.name,
|
|
853
862
|
CASE WHEN settings.phase='initial' THEN '欢迎加入:请先读取议题,独立给出你的开场观点。'
|
|
854
863
|
ELSE '欢迎加入:请先读取议题和已有回答,给出你的观点。' END,
|
|
@@ -929,7 +938,7 @@ def discussion_member_action(connection, room_id, member_id, token, payload):
|
|
|
929
938
|
save_discussion_experts(connection, room_id, experts)
|
|
930
939
|
|
|
931
940
|
|
|
932
|
-
ADOPTABLE_ADAPTERS = ("claude", "codex", "codex_cli")
|
|
941
|
+
ADOPTABLE_ADAPTERS = ("claude", "codex", "codex_cli", "antigravity_cli")
|
|
933
942
|
|
|
934
943
|
|
|
935
944
|
def adopt_manual_member(connection, room_id, member, payload):
|
package/backend/schema.sql
CHANGED
|
@@ -115,8 +115,10 @@ BEGIN
|
|
|
115
115
|
SELECT RAISE(ABORT, 'turn_unavailable');
|
|
116
116
|
END;
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
DROP TRIGGER IF EXISTS reject_inactive_opinion;
|
|
119
|
+
CREATE TRIGGER reject_inactive_opinion BEFORE INSERT ON message_details
|
|
119
120
|
WHEN NEW.kind='opinion'
|
|
121
|
+
AND (SELECT author FROM messages WHERE id=NEW.message_id)<>'用户'
|
|
120
122
|
AND NOT EXISTS (SELECT 1 FROM message_details WHERE message_id=NEW.message_id)
|
|
121
123
|
AND EXISTS (SELECT 1 FROM messages JOIN topic_settings ON topic_settings.room_id=messages.room_id
|
|
122
124
|
WHERE messages.id=NEW.message_id AND topic_settings.phase IN ('paused','concluded'))
|
package/backend/server.py
CHANGED
|
@@ -214,6 +214,7 @@ def require_room(connection, room_id):
|
|
|
214
214
|
room["round_budget"] = dict(budget) if budget else None
|
|
215
215
|
if budget:
|
|
216
216
|
room["opinion_count"] = opinion_count(connection, room_id, room["experts"])
|
|
217
|
+
room["round_groups"] = budget_round_groups(connection, room_id, room["experts"], budget["base"]) if budget else room["rounds"]
|
|
217
218
|
room["floor"] = dict(connection.execute("SELECT holder,next_speaker,revision FROM room_floor WHERE room_id=?", (room_id,)).fetchone())
|
|
218
219
|
room["runtime"] = runtime_view(connection, room_id)
|
|
219
220
|
room["limits"] = limits_view(connection.execute(LIMITS_SQL, (room_id,)).fetchone(), room)
|
|
@@ -233,6 +234,7 @@ def require_room(connection, room_id):
|
|
|
233
234
|
for decision in room["scheduler_decisions"]:
|
|
234
235
|
decision["detail"] = details.get(decision["id"])
|
|
235
236
|
room["host_arrangements"] = host_arrangements(connection, room_id, room["scheduler_decisions"])
|
|
237
|
+
room["cancelled_turns"] = cancelled_turns(connection, room_id)
|
|
236
238
|
parent = connection.execute("""SELECT rooms.id,rooms.title FROM room_links JOIN rooms ON rooms.id=room_links.parent_id
|
|
237
239
|
WHERE room_links.room_id=?""", (room_id,)).fetchone()
|
|
238
240
|
room["parent_room"] = dict(parent) if parent else None
|
|
@@ -296,6 +298,21 @@ def host_arrangements(connection, room_id, decisions):
|
|
|
296
298
|
return [plan for plan in plans if plan["after_message_id"] not in applied][-RECENT_DECISIONS:]
|
|
297
299
|
|
|
298
300
|
|
|
301
|
+
def cancelled_turns(connection, room_id):
|
|
302
|
+
"""Assignments that ended without an answer, anchored where their wake card first appeared.
|
|
303
|
+
|
|
304
|
+
The anchor is the later of the previous turn's answer and the latest message at creation time, so the card keeps its place in the transcript.
|
|
305
|
+
"""
|
|
306
|
+
rows, latest_answer = [], 0
|
|
307
|
+
for row in connection.execute("""SELECT id,expert,question,status,answer_id,
|
|
308
|
+
(SELECT COALESCE(MAX(messages.id),0) FROM messages WHERE messages.room_id=discussion_turns.room_id AND messages.created_at<=discussion_turns.created_at) AS created_after
|
|
309
|
+
FROM discussion_turns WHERE room_id=? ORDER BY id""", (room_id,)):
|
|
310
|
+
if row["status"] == "cancelled":
|
|
311
|
+
rows.append({"id": row["id"], "expert": row["expert"], "question": row["question"], "after_message_id": max(latest_answer, row["created_after"])})
|
|
312
|
+
latest_answer = max(latest_answer, row["answer_id"] or 0)
|
|
313
|
+
return rows[-RECENT_DECISIONS:]
|
|
314
|
+
|
|
315
|
+
|
|
299
316
|
def discussion_rounds(connection, room_id, experts):
|
|
300
317
|
"""Group complete history metadata until every configured expert has spoken.
|
|
301
318
|
|
|
@@ -327,6 +344,34 @@ def discussion_rounds(connection, room_id, experts):
|
|
|
327
344
|
return rounds
|
|
328
345
|
|
|
329
346
|
|
|
347
|
+
def budget_round_groups(connection, room_id, experts, base):
|
|
348
|
+
"""Group history by the round budget's rule: every len(experts) opinions form a round, and a new segment (base) starts a new one.
|
|
349
|
+
|
|
350
|
+
`rounds` stays speaker-based because scheduling needs who still owes a turn; this view only labels history.
|
|
351
|
+
"""
|
|
352
|
+
if not experts:
|
|
353
|
+
return []
|
|
354
|
+
rows = connection.execute("""SELECT messages.id,messages.author,
|
|
355
|
+
COALESCE(details.kind,'opinion') AS kind FROM messages
|
|
356
|
+
LEFT JOIN message_details AS details ON details.message_id=messages.id
|
|
357
|
+
WHERE room_id=? ORDER BY messages.id""", (room_id,))
|
|
358
|
+
groups, next_start, seen = [], None, 0
|
|
359
|
+
for message in rows:
|
|
360
|
+
if next_start is None:
|
|
361
|
+
next_start = message["id"]
|
|
362
|
+
if message["kind"] != "opinion" or message["author"] not in experts:
|
|
363
|
+
continue
|
|
364
|
+
if not groups or groups[-1]["completed"]:
|
|
365
|
+
groups.append({"number": len(groups) + 1, "start_id": next_start, "opinions": 0, "completed": False})
|
|
366
|
+
current = groups[-1]
|
|
367
|
+
seen += 1
|
|
368
|
+
current["opinions"] += 1
|
|
369
|
+
current["completed"] = current["opinions"] >= len(experts) or seen == base
|
|
370
|
+
if current["completed"]:
|
|
371
|
+
next_start = None
|
|
372
|
+
return groups
|
|
373
|
+
|
|
374
|
+
|
|
330
375
|
def opinion_count(connection, room_id, experts):
|
|
331
376
|
"""Count formal expert opinions; the program-controlled round budget is measured in these."""
|
|
332
377
|
if not experts:
|
|
@@ -969,7 +1014,7 @@ def append_free_message(connection, room, payload, actor):
|
|
|
969
1014
|
def record_reported_session(connection, room_id, token, payload):
|
|
970
1015
|
"""Keep the software session a manual member posted from, so the local service can take the member over."""
|
|
971
1016
|
session_id, client = payload.get("session_id"), payload.get("session_client")
|
|
972
|
-
if client not in ("claude", "codex") or not isinstance(session_id, str) or not SESSION_ID_PATTERN.fullmatch(session_id) or not token:
|
|
1017
|
+
if client not in ("claude", "codex", "antigravity") or not isinstance(session_id, str) or not SESSION_ID_PATTERN.fullmatch(session_id) or not token:
|
|
973
1018
|
return
|
|
974
1019
|
member = connection.execute("SELECT id FROM room_members WHERE room_id=? AND token_hash=? AND status='active' AND access='manual' AND adapter='manual'",
|
|
975
1020
|
(room_id, token_hash(token))).fetchone()
|
|
@@ -998,7 +1043,7 @@ def append_message(connection, room_id, payload, token="", free_actor=None):
|
|
|
998
1043
|
return {"message": previous, "duplicate": True}
|
|
999
1044
|
if author != "用户" and details["kind"] == "opinion":
|
|
1000
1045
|
require_within_limits(room)
|
|
1001
|
-
if room["phase"] in ("paused", "concluded") and details["kind"] == "opinion":
|
|
1046
|
+
if room["phase"] in ("paused", "concluded") and details["kind"] == "opinion" and (author != "用户" or room["archived"]):
|
|
1002
1047
|
raise ValueError("话题已暂停或收束;恢复阶段后才能提交新观点,运行回执请用 receipt")
|
|
1003
1048
|
check_message_references(connection, room_id, expected)
|
|
1004
1049
|
check_message_attachments(connection, room_id, details["submitted_by"], identifiers)
|
|
@@ -1065,12 +1110,28 @@ def roster_view(room):
|
|
|
1065
1110
|
"""Who is present and who still owes a turn in the open round; its revision changes whenever either changes."""
|
|
1066
1111
|
last = room["rounds"][-1] if room["rounds"] else None
|
|
1067
1112
|
spoken = set(last["speakers"]) if last and not last["completed"] else set()
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1113
|
+
pending_registration = unregistered_experts(room)
|
|
1114
|
+
experts = [{"name": name, "mode": mode, "host": name == room["host"], "spoken": name in spoken,
|
|
1115
|
+
"registered": name not in pending_registration} for name, mode in room["experts"].items()]
|
|
1116
|
+
state = json.dumps([sorted(room["experts"]), sorted(spoken), sorted(pending_registration)], ensure_ascii=False)
|
|
1117
|
+
return {"experts": experts, "unregistered": pending_registration,
|
|
1118
|
+
"waiting": [item["name"] for item in experts if not item["spoken"] and not item["host"] and item["registered"]],
|
|
1071
1119
|
"revision": hashlib.sha256(state.encode("utf-8")).hexdigest()[:12]}
|
|
1072
1120
|
|
|
1073
1121
|
|
|
1122
|
+
def unregistered_experts(room):
|
|
1123
|
+
"""Manual experts of a woken discussion (adapter 'manual') that have not registered from their own session; they cannot be scheduled until they do."""
|
|
1124
|
+
return [member["name"] for member in room["members"]
|
|
1125
|
+
if member["adapter"] == "manual" and member["status"] == "active" and not member["registered_at"] and member["name"] in room["experts"]]
|
|
1126
|
+
|
|
1127
|
+
|
|
1128
|
+
def require_registered(room, names):
|
|
1129
|
+
"""Reject scheduling an expert that has not registered yet; it joins the round queue by itself once it registers."""
|
|
1130
|
+
missing = [name for name in dict.fromkeys(names) if name in unregistered_experts(room)]
|
|
1131
|
+
if missing:
|
|
1132
|
+
raise ValueError(f"以下手动专家尚未注册,不能安排发言:{'、'.join(missing)};它在自己的会话中注册后会自动加入本轮队列,也可先让其暂离")
|
|
1133
|
+
|
|
1134
|
+
|
|
1074
1135
|
def require_round_coverage(room, plan, payload):
|
|
1075
1136
|
"""A multi-turn plan is only accepted against the latest roster and must schedule everyone still waiting in the round."""
|
|
1076
1137
|
if len(plan) < 2:
|
|
@@ -1100,6 +1161,7 @@ def create_turn(connection, room_id, payload):
|
|
|
1100
1161
|
require_within_limits(room)
|
|
1101
1162
|
if any(expert not in room["experts"] or len(question) > MAX_QUESTION_LENGTH for expert, question, _ in plan):
|
|
1102
1163
|
raise ValueError("请选择参与专家,待回应问题最多 1000 字")
|
|
1164
|
+
require_registered(room, [expert for expert, _, _ in plan])
|
|
1103
1165
|
require_round_coverage(room, plan, payload)
|
|
1104
1166
|
if room["phase"] in ("paused", "concluded"):
|
|
1105
1167
|
raise ValueError("话题已暂停或收束,请先调整阶段")
|
|
@@ -1151,6 +1213,7 @@ def continue_turns(connection, room_id, payload):
|
|
|
1151
1213
|
if not room["queued_turns"]:
|
|
1152
1214
|
raise FileExistsError("没有排队中的安排;请创建新的安排")
|
|
1153
1215
|
require_within_limits(room)
|
|
1216
|
+
require_registered(room, [room["queued_turns"][0]["expert"]])
|
|
1154
1217
|
require_floor(room, room["host"])
|
|
1155
1218
|
turn_id = room["queued_turns"][0]["id"]
|
|
1156
1219
|
connection.execute("UPDATE discussion_turns SET status='pending' WHERE id=? AND status='queued'", (turn_id,))
|
|
@@ -1240,7 +1303,7 @@ def update_turn(connection, room_id, turn_id, payload):
|
|
|
1240
1303
|
status = payload.get("status")
|
|
1241
1304
|
if status not in ("notified", "cancelled"):
|
|
1242
1305
|
raise ValueError("安排状态仅可显式改为 notified 或 cancelled")
|
|
1243
|
-
if turn["status"] == status:
|
|
1306
|
+
if turn["status"] == status or (status == "cancelled" and turn["status"] == "skipped"):
|
|
1244
1307
|
return turn
|
|
1245
1308
|
if status == "notified" and room["phase"] in ("paused", "concluded"):
|
|
1246
1309
|
raise ValueError("话题已暂停或收束,不能继续通知")
|
package/docs/cli.md
CHANGED
|
@@ -134,7 +134,7 @@ codiscuss floor view $room
|
|
|
134
134
|
| 主持发言 | 领取后 `message post ROOM --as Codex --kind moderation --body-file FILE --submission-id UUID` |
|
|
135
135
|
| 在线专家代发 | 被正式安排后,由主持人 `floor take ROOM --as Codex --speaker Claude --revision N`,再 `message post ROOM --as Codex --author Claude --turn TURN --summary-file SUMMARY --summary-by Claude --body-file FILE --submission-id UUID` |
|
|
136
136
|
| 放弃发言 | `floor return ROOM --as NAME --revision N`;未完成安排仍需处理 |
|
|
137
|
-
| 整轮安排 | 主持人领取后 `turn roster ROOM` 后 `turn create ROOM --as Codex --plan-file PLAN.json --roster REV --submission-id UUID`;PLAN 为 `[{"expert":"Claude","question":"…"}]`,按顺序接力;多项计划须先 `turn roster ROOM` 取得最新专家状态,把其 `revision` 用 `--roster` 带上,并包含 `waiting`
|
|
137
|
+
| 整轮安排 | 主持人领取后 `turn roster ROOM` 后 `turn create ROOM --as Codex --plan-file PLAN.json --roster REV --submission-id UUID`;PLAN 为 `[{"expert":"Claude","question":"…"}]`,按顺序接力;多项计划须先 `turn roster ROOM` 取得最新专家状态,把其 `revision` 用 `--roster` 带上,并包含 `waiting` 中的每位专家(主持人除外;尚未注册的手动专家列在 `unregistered`,不在 `waiting` 中,也不能被安排,否则返回 400 并列出名字):未带 revision 返回 400,名单已变化返回 409,缺人返回 400 并列出名字 |
|
|
138
138
|
| 继续原计划 | 用户介入或打断后,主持人领取并 `turn continue ROOM --as Codex`;改用新安排则排队自动作废 |
|
|
139
139
|
| 取消安排 | 主持人领取后 `turn cancel ROOM TURN --as Codex` |
|
|
140
140
|
| 切换阶段 | `room update ROOM --as Codex --phase discussing` |
|
package/docs/runner.md
CHANGED
|
@@ -283,7 +283,7 @@ Windows 的 `executable` 填原生 `.exe`,不填 npm 的 `.cmd` / `.ps1` 包
|
|
|
283
283
|
|
|
284
284
|
agy 只用一套方法控制权限:
|
|
285
285
|
|
|
286
|
-
1. **只在本工具新建的会话中执行**:每位参与者对应一个独立原生讨论项目,权限只写在该项目里。外部已有会话(包括从当前 agy 对话发起主持、在启动配置中填写 `session_id
|
|
286
|
+
1. **只在本工具新建的会话中执行**:每位参与者对应一个独立原生讨论项目,权限只写在该项目里。外部已有会话(包括从当前 agy 对话发起主持、在启动配置中填写 `session_id`)会恢复其原项目,无法套用讨论项目权限,因此启动时即拒绝,交付时也再次拒绝。唯一例外是手动专家的托管:agy 在自己的会话里执行 `codiscuss` 时由 `ANTIGRAVITY_CONVERSATION_ID` 上报会话,用户注册后服务把它转为 `antigravity_cli` 并以 `--conversation ID` 续接,不加 `--project`,沿用该会话原项目和用户自己的 agy 权限(无讨论项目规则,也不检查全局放行);无头模式下 shell 命令须已由用户的规则放行,例如 `command(regex:^codiscuss(\.cmd)? .*$)`,否则被拒绝。
|
|
287
287
|
2. **项目内规则**:allow 为受限的讨论命令规则与草稿目录写入(`write` 时加项目目录写入);deny 为项目目录写入(`read` 时)、运行目录、技能目录、`~/.gemini` 写入与 `mcp(*)`。规则按目录前缀递归匹配,不使用 `/**`;Deny > Ask > Allow,因此草稿不得位于受保护目录中,冲突在交付前报告。
|
|
288
288
|
3. **交付前检查全局设置**:CLI 与共享全局 allow 中若有 `*`、`command(*)`、`unsandboxed(*)` 或 `unsandboxed_command(*)` 这类放行全部命令的规则,则拒绝交付,因为它会让 shell 绕过项目的文件规则。具体命令的放行和 `toolPermission` 不检查,属于用户自己的授权。未授权 shell 在非交互模式下被拒绝。
|
|
289
289
|
4. **不改用户全局设置**:本工具不向 agy 全局设置写入任何规则;`setup` 只移除早期版本记录在 `agy-permission.json` 中的全局规则(先备份到服务目录 `agy-settings.backup.json`),其他设置与用户规则不变。
|
package/docs/skills.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
主持开始新讨论时确认全自动或手动复制冷启动。自动任务明确引用仓库内的当前技能版本,并提供任务文件与 CLI 凭据文件路径;参与者先 `execution enter`,再按阶段读取材料。自动执行不建原生定时。手动方式沿用本文的原有接入;受保护房间另由用户提供私有接入文件。讨论项目和 cwd 可选,不能把 CLI 仓库路径当作讨论资料目录或擅自增加目录权限。
|
|
8
8
|
|
|
9
|
-
两份主技能把讨论质量与判断放在前面,仅保留当前任务必需的执行边界。主持人的 `references/manual.md` 只在手动接入、在线转交或原生任务场景读取;参与者的 `references/manual.md`
|
|
9
|
+
两份主技能把讨论质量与判断放在前面,仅保留当前任务必需的执行边界。主持人的 `references/manual.md` 只在手动接入、在线转交或原生任务场景读取;参与者的 `references/manual.md` 管手动成员的接入文件、注册与原生定时持续参与,`references/free.md` 只在自由协作房间读取,`references/writing.md` 在首次使用某类写入、代发或失败恢复时按需查阅。自动任务不加载手动流程,也不逐轮复读帮助、全量历史和故障清单。
|
|
10
10
|
|
|
11
11
|
启动提示可能已要求 `execution enter`;当前调用成功认领一次后,加载技能不再次认领。主持人先用一次 `state set` 保存判断、简报和下一步意图,再创建安排;安排落库即结束该次资格,不追加状态写入。专家提交原文即结束,由程序续租和交付后续任务。返回记录已含完整保存证据时直接核对,缺少证据或结果不明才补查询;这减少重复读写,但不削弱署名、发言权、提交去重或到限约束。
|
|
12
12
|
|