ai_co_discuss 0.1.1 → 0.1.3

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.
@@ -6,7 +6,7 @@
6
6
 
7
7
  1. 使用已安装的 `codiscuss`;未安装时用本仓库 `node runner/codiscuss.mjs`。`codiscuss launch settings` 只读本机软件默认配置,不启动服务或 AI。缺少配置时按仓库 `docs/runner.md` 的“两种讨论入口与启动配置”补齐;用户已提供的软件、目录和上限直接沿用,只询问无法确定的缺项。
8
8
  2. 从当前软件的实际会话信息取得会话 ID,不自造编号。启动文件中 `entry` 使用 `conversation`,`current_session_id` 使用该实际 ID;主持人的 `participants` 覆盖配置必须对应当前实际 CLI / App 客户端,不能仅根据模型名称选适配器。当前会话继续主持,不另建主持聊天。
9
- 3. 站点始终明确指定。每场讨论选择主持人、专家、目标、用户原文、背景和上限;软件路径来自本机默认配置,本次可用 `participants` 覆盖。`cwd` / `discussion_project` 可省略,单个参与者可覆盖。已有会话仍在原目录,设置 cwd 不会迁移它。用户要一起查看某个项目时,把该项目目录设为 `cwd`;项目文件默认要求只读,仅当用户明确要求 AI 修改项目时才设 `"project_access": "write"`,也可在 `participants.NAME.project_access` 单独指定。只读不通过更换项目目录实现;agy 新会话由 runner 设置原生项目权限,已有会话(包括当前主持)仍由提示词约束。答案草稿只使用任务给出的 `draft_directory`,不写到项目或存放凭据的运行目录。
9
+ 3. 站点始终明确指定。每场讨论选择主持人、专家、目标、用户原文、背景和上限;软件路径来自本机默认配置,本次可用 `participants` 覆盖。`cwd` / `discussion_project` 可省略,单个参与者可覆盖。已有会话仍在原目录,设置 cwd 不会迁移它。用户要一起查看某个项目时,把该项目目录设为 `cwd`;项目文件默认要求只读,仅当用户明确要求 AI 修改项目时才设 `"project_access": "write"`,也可在 `participants.NAME.project_access` 单独指定。只读不通过更换项目目录实现;agy 只在讨论服务新建的会话中运行并由原生项目权限限制,不能绑定已有会话或从当前 agy 对话主持全自动讨论。答案草稿只使用任务给出的 `draft_directory`,不写到项目或存放凭据的运行目录。
10
10
 
11
11
  新讨论的 UTF-8 启动文件示例(替换示例内容;软件已在本机配置):
12
12
 
package/README.md CHANGED
@@ -29,9 +29,21 @@ npx --yes --package ai_co_discuss codiscuss install
29
29
 
30
30
  安装摘要为“配置待处理”时,处理列出的路径、旧配置或技能冲突后重跑 `codiscuss setup`;就绪后直接进入[发起第一场讨论](#发起第一场讨论)。已有配置和自定义技能会保留,数据存放在包外,升级或卸载包不会删除。安装就绪不代表真实 AI 已执行过任务。详细选项见 [npm 安装与本机配置](docs/runner.md#npm-安装与本机配置)。
31
31
 
32
+ #### 升级与卸载
33
+
34
+ 已安装过时不要再用 `npx` 升级:npx 会直接运行已安装的旧版本。先停止本机网站,再强制从线上取最新版并重新配置:
35
+
36
+ ```sh
37
+ codiscuss local stop
38
+ npm install --global --prefer-online ai_co_discuss@latest
39
+ codiscuss setup
40
+ ```
41
+
42
+ `--prefer-online` 避免 npm 用本地缓存把 `latest` 解析成旧版本;完成后可用 `npm ls -g ai_co_discuss` 核对版本。卸载时先 `codiscuss local stop`,开启过登录自启的再运行 `codiscuss service autostart off`,最后 `npm uninstall --global ai_co_discuss`。讨论数据、软件配置和已复制的技能不会被删除,位置见 [卸载说明](docs/runner.md#npm-安装与本机配置)。
43
+
32
44
  ### 源码运行方式
33
45
 
34
- 供开发者和未取得安装包的用户使用;已完成包安装可直接跳到下一节。需要 Git、Python 3.10+,使用自动接入还需 Node.js 24+ 和 npm。仓库包含构建好的网页。
46
+ 供开发者使用;已完成包安装可直接跳到下一节。需要 Git、Python 3.10+,使用自动接入还需 Node.js 24+ 和 npm。仓库包含构建好的网页。
35
47
 
36
48
  ```sh
37
49
  git clone https://github.com/huyan1458/ai_co_discuss.git
@@ -79,11 +91,11 @@ codiscuss local connect https://你的站点
79
91
 
80
92
  > 请使用 discuss-host 和 discuss-participant,在 http://127.0.0.1:8765 新建一场全自动讨论,议题是“议题与目标”,我的问题和背景是“用户原文”。由当前 Codex 会话主持,参与者为 Codex 和 Claude,最多 3 轮、60 分钟,到限暂停;沿用本机软件配置。
81
93
 
82
- 将地址、议题和参与者替换为实际内容。AI 主持人使用已保存的软件配置完成建房与启动,**当前实际会话继续主持**;后续安排由本机服务交付,可在网页查看同一场讨论。无需用户手写启动 JSON。当前会话的 CLI / App 类型必须与配置对应;其他接入方式与配置细节见 [两个入口的启动配置](docs/runner.md#两种讨论入口与启动配置)。
94
+ 将地址、议题和参与者替换为实际内容。AI 主持人使用已保存的软件配置完成建房与启动,**当前实际会话继续主持**;后续安排由本机服务交付,可在网页查看同一场讨论。无需用户手写启动 JSON。当前会话的 CLI / App 类型必须与配置对应;agy 不能从自己的对话主持全自动讨论(只在讨论服务新建的会话中运行),请改用其他软件主持或从网页发起。其他接入方式与配置细节见 [两个入口的启动配置](docs/runner.md#两种讨论入口与启动配置)。
83
95
 
84
96
  #### 项目与日常使用
85
97
 
86
- 两个入口都可按场指定项目目录、项目文件只读 / 可写及参与者覆盖配置;不需要项目资料时可省略。CLI 保持指定工作目录,回答草稿与运行数据分开。项目默认要求只读,但限制强度取决于软件:Codex 使用原生沙箱,agy 新会话使用原生项目权限并禁用 MCP;Claude 的已有用户授权仍生效,agy 已有外部会话保留上下文并使用提示词约束。权限冲突处理与实测边界见 [单次执行权限](docs/runner.md#单次执行权限)。
98
+ 两个入口都可按场指定项目目录、项目文件只读 / 可写及参与者覆盖配置;不需要项目资料时可省略。CLI 保持指定工作目录,回答草稿与运行数据分开。项目默认要求只读,但限制强度取决于软件:Codex 使用原生沙箱,agy 只在讨论服务新建的会话中运行并由其独立项目权限限制、禁用 MCP(不绑定已有 agy 会话);Claude 的已有用户授权仍生效。权限冲突处理与实测边界见 [单次执行权限](docs/runner.md#单次执行权限)。
87
99
 
88
100
  | 日常操作 | 命令 |
89
101
  | --- | --- |
package/docs/runner.md CHANGED
@@ -30,7 +30,7 @@ codiscuss local stop
30
30
 
31
31
  `install/setup` 在 stderr 输出中文摘要,包括实际软件配置、Skill 文件就绪数、命令和网站地址;交互终端显示分步进度、多选列表与对齐的结果汇总(遵循 `NO_COLOR`),输出被重定向时保持逐行纯文本。结果 JSON 在 stdout 被重定向或加 `--json` 时写入 stdout,交互终端默认不打印。退出码与 JSON `status` 对应:`0` 为 `ready`(网站与讨论服务已就绪)或 `configured`(显式 `--no-start`);`2` 为 `needs_attention`(配置差异未确认、技能冲突或命令不可用);`1` 为失败。网站启动失败时返回 `start_failed` 与已保存的配置结果,修复后重跑 `setup` 即可。环境预检失败发生在写入前,仅输出错误。配置待处理不撤销已完成部分,也不妨碍启动可用的网站;`configured` 布尔值为 false,不能按成功处理。`ai_verified: false` 表示安装器没有执行真实模型,不证明账号、技能发现或多轮讨论已验收。
32
32
 
33
- 安装器按 [技能配置](skills.md#自动安装完整技能) 复制完整技能。`--skills-home DIR` 可覆盖技能用户根目录;默认当前用户 home。重复安装只更新由安装器管理且未被编辑的版本;冲突保留本地文件,输出实际路径并标记待处理。核对后可移走需替换的旧副本再重跑 `setup`。安装器不调整登录状态;权限只在选中 Gemini 时经确认写入讨论命令与草稿目录规则,见 [单次执行权限](#单次执行权限)。
33
+ 安装器按 [技能配置](skills.md#自动安装完整技能) 复制完整技能。`--skills-home DIR` 可覆盖技能用户根目录;默认当前用户 home。重复安装只更新由安装器管理且未被编辑的版本;冲突保留本地文件,输出实际路径并标记待处理。核对后可移走需替换的旧副本再重跑 `setup`。安装器不调整登录状态,也不向 agy 全局设置写入权限规则;只清理早期版本写入的规则,见 [单次执行权限](#单次执行权限)。
34
34
 
35
35
  安装实现保持固定软件表和本地文件复制。路径查找参考 [npm/node-which](https://github.com/npm/node-which/blob/main/lib/index.js) 的 PATH 顺序与执行权限检查,只覆盖所需命令和后缀;技能复制参考 [OpenAI 单文件安装脚本](https://github.com/openai/skills/blob/main/skills/.system/skill-installer/scripts/install-skill-from-github.py) 的整目录复制与同名保护思路,保留本项目的内容哈希更新判断。不引入这些项目作为依赖,也不增加在线技能下载、插件注册或后台更新。
36
36
 
@@ -38,6 +38,8 @@ codiscuss local stop
38
38
 
39
39
  网站数据库为服务目录下 `local/discuss.sqlite3`,配置、登记表和讨论执行数据也位于包外;服务目录位置及 `DISCUSS_SERVICE_HOME` 见下文。源码运行的 `data/discuss.sqlite3` 不自动迁移。网站仍在而讨论服务已停止时,显式 `local open/start` 会要求网站管理进程恢复服务;不会自动恢复已暂停的房间。`local status` 的 `running` 要求网站和讨论服务同时运行,`website_running` 单独表示网站状态。`local stop` 关闭它创建的网站,并停止它启动或恢复且仍由相同 PID 持有的讨论服务;预先运行或后来独立启动的服务会保留。停止服务会暂停其托管中的活动讨论,数据库保留。日志在 `local/website.log`、`local/supervisor.log` 与原 `service.log`。
40
40
 
41
+ 已安装后升级不要使用 `npx`(它会运行已安装的旧版本):先 `codiscuss local stop`,再 `npm install --global --prefer-online ai_co_discuss@latest`,然后重跑 `codiscuss setup`;`--prefer-online` 避免本地缓存把 `latest` 解析为旧版本。
42
+
41
43
  卸载先 `codiscuss local stop`,按需要另用 `codiscuss service stop` 停止预先独立运行的服务,再执行 `npm uninstall --global ai_co_discuss`;自定义 prefix 时带回相同 `--prefix`。安装器不删除数据库、私有配置或已复制技能。
42
44
 
43
45
  维护者从源码生成安装包时先完成相关测试与网页构建,再 `npm pack --pack-destination <已存在的输出目录>`。包采用运行文件白名单,不含数据库、凭据、`.dev`、前端源码或 node_modules;新增运行文件需同步清单。`npm run test:install` 在临时 prefix/home 中验证真实 npx 安装、命令、技能和本机启停,不触发真实 AI。Windows 已通过隔离测试;macOS/Linux 启动与默认浏览器仍需实机核验。
@@ -202,7 +204,7 @@ Windows 的 `executable` 填原生 `.exe`,不填 npm 的 `.cmd` / `.ps1` 包
202
204
  | --- | --- | --- |
203
205
  | `codex_cli` | `codex exec --json`,从 `thread.started` 读取实际 ID;后续 `exec resume ID`,不使用 `--last` | 首次与续接均显式 `--cd` 到用户指定项目;省略 cwd 时使用本场独立草稿目录。允许非 Git 目录;项目与草稿的写入权限由本次 permission profile 区分。项目名仅作元数据,不关联 App 项目分组 |
204
206
  | `claude` | 现有 `claude -p --output-format json`;后续 `--resume ID` | 沿用上表中的 Claude 规则 |
205
- | `antigravity_cli` | `agy -p --output-format stream-json`,从 `init.conversation_id` 读取实际 ID;后续 `--conversation ID`,不使用 `--continue` | 保持指定 cwd;省略 cwd 时使用本场独立草稿目录。新会话创建独立的原生讨论项目,沿用所选项目的资源目录并设置权限,续接复用该项目;外部已有会话保留原项目和上下文 |
207
+ | `antigravity_cli` | `agy -p --output-format stream-json`,从 `init.conversation_id` 读取实际 ID;后续 `--conversation ID`,不使用 `--continue` | 保持指定 cwd;省略 cwd 时使用本场独立草稿目录。只在讨论服务新建的会话中执行:每位参与者一个独立原生讨论项目,续接复用该项目;不绑定外部已有会话 |
206
208
 
207
209
  两种新适配器都不需要 `source_thread_id` 或 `ipc_directory`。实际 ID 在输出流到达时写入本机绑定和本次执行凭据,随后 AI 按任务文件自行 `execution enter` 并通过讨论 CLI 保存业务结果;程序不把终端回答直接代发。已有 App 绑定不能通过改适配器直接复用,须先暂停并使用独立 `data_directory` 明确重新接入。此路径不承诺 CLI 会话同步到桌面 App。
208
210
 
@@ -210,21 +212,26 @@ Windows 的 `executable` 填原生 `.exe`,不填 npm 的 `.cmd` / `.ps1` 包
210
212
 
211
213
  每次执行只开放本场讨论所需的最小权限,不使用全部放行或绕过审批参数。任务文件的 `cli` 为 `codiscuss`,`subcommands` 列出唯一放行的网站子命令:`room`、`message`、`turn`、`state`、`floor`、`execution`、`health`、`schema`;`service`、`launch`、`run`、`api`、`setup`、`install`、`local` 不放行。提示词要求命令以 `codiscuss` 开头,不用 `python discuss.py`、`cd` 或管道。规则不含路径,重装或更换 Python 无需更新;子进程 PATH 中的 `codiscuss` 须指向本次安装,setup 的持久命令检查会核对。
212
214
 
213
- 项目文件权限 `project_access` 为 `read`(默认)或 `write`:启动文件顶层设本场值,`participants.NAME.project_access` 覆盖单个 AI,空字符串表示沿用本场值。它只随本次启动保存,不能存入本机默认配置;网页新建讨论在“启动”区直接提供项目目录与项目文件选项,单个 AI 的覆盖在“调整启动配置”中。三种 CLI 都在用户指定的 cwd 启动;省略 cwd 时才使用独立草稿目录。agy 新建会话使用原生项目权限;已有外部会话继续使用提示词约束,保留原项目与历史。App 适配器的写权限由 App 自身决定,任务文件始终写明本场要求。
215
+ 项目文件权限 `project_access` 为 `read`(默认)或 `write`:启动文件顶层设本场值,`participants.NAME.project_access` 覆盖单个 AI,空字符串表示沿用本场值。它只随本次启动保存,不能存入本机默认配置;网页新建讨论在“启动”区直接提供项目目录与项目文件选项,单个 AI 的覆盖在“调整启动配置”中。三种 CLI 都在用户指定的 cwd 启动;省略 cwd 时才使用独立草稿目录。agy 的权限全部由其独立讨论项目执行,见下文。App 适配器的写权限由 App 自身决定,任务文件始终写明本场要求。
214
216
 
215
217
  | 适配器 | 本次追加 | 只读 / 可写差异 |
216
218
  | --- | --- | --- |
217
219
  | `claude` | `--add-dir` 运行、草稿与技能目录;每个子命令各一条 `Bash(codiscuss room *)` 与 `PowerShell(codiscuss room *)`,仅对独立草稿增加 `Edit(//<草稿目录>/**)`,再追加用户的 `allowed_tools` | 可写时再加 `Edit(//<项目目录>/**)`;不再对运行目录新增 Edit 放行,已有用户规则仍生效;Windows 路径按 `//c/...` 匹配 |
218
220
  | `codex_cli` | 每次创建唯一名称的 permission profile,通过 `-c default_permissions=...` 与 `permissions.<名称>=...` 传入,`approval_policy="never"`;运行目录只读,独立草稿目录可写 | 无项目或项目只读时继承 `:read-only`,项目可写时继承 `:workspace`;维持原有广泛读取范围和本次联网能力,未启用域名过滤 |
219
- | `antigravity_cli` | 新会话使用独立 `--project UUID` 与 `--mode accept-edits`;项目规则允许讨论命令和草稿写入,禁止运行目录、技能目录、`~/.gemini` 写入及 MCP 工具调用;每次启动检查全局命令权限 | `read` 为项目目录添加 `write_file(目录)` deny,`write` 改为 allow;运行数据禁写不随项目授权放开。旧外部绑定只传原会话 ID,沿用提示词约束,可写时才额外传 `accept-edits` |
221
+ | `antigravity_cli` | 始终使用独立 `--project UUID` 与 `--mode accept-edits`;项目规则允许讨论命令和草稿写入,禁止运行目录、技能目录、`~/.gemini` 写入及 MCP 工具调用;每次交付前检查全局设置 | `read` 为项目目录添加 `write_file(目录)` deny,`write` 改为 allow;运行数据禁写不随项目授权放开 |
220
222
 
221
- agy 的规则按目录前缀递归匹配,不使用 `/**`;Deny > Ask > Allow,因此草稿不得放在只读项目、运行目录、技能或 `.gemini` 中,路径冲突会在交付前报告。原生工具权限不是操作系统沙箱:runner 同时要求 CLI 的 `toolPermission` 为默认 `request-review`,检查 CLI 和共享全局 allow,存在本工具讨论规则以外的 `command(...)` / `unsandboxed_command(...)` 放行时拒绝交付;未授权 shell 在非交互模式下被拒绝。MCP 工具调用统一 deny,避免经 MCP 写入项目。[原生权限文档](https://antigravity.google/docs/permissions?tab=cli)、[CLI 设置](https://antigravity.google/docs/settings?tab=cli)。
223
+ agy 只用一套方法控制权限:
222
224
 
223
- 独立讨论项目保存在 `~/.gemini/config/projects/<UUID>.json`,运行目录的 `agy-project-<参与者摘要>.json` 保存项目与实际会话绑定。所选原项目及用户全局配置不被改写;新项目只沿用资源目录,不继承原项目的命令放行。会话继续使用时保留这两份记录,不将生成的讨论会话手动迁入其他项目。agy 续接已有会话会恢复其原项目,传入另一 `--project` 不能替换它;因此历史绑定和从当前 agy 对话发起的主持保留提示词约束,交付回执以 `permission_enforcement: prompt` 标识,新管理会话为 `native`。缺失已登记的原生项目或会话绑定不匹配时拒绝交付。[项目权限继承](https://antigravity.google/docs/projects?tab=cli)。
225
+ 1. **只在本工具新建的会话中执行**:每位参与者对应一个独立原生讨论项目,权限只写在该项目里。外部已有会话(包括从当前 agy 对话发起主持、在启动配置中填写 `session_id`)会恢复其原项目,无法套用讨论项目权限,因此启动时即拒绝,交付时也再次拒绝。
226
+ 2. **项目内规则**:allow 为受限的讨论命令规则与草稿目录写入(`write` 时加项目目录写入);deny 为项目目录写入(`read` 时)、运行目录、技能目录、`~/.gemini` 写入与 `mcp(*)`。规则按目录前缀递归匹配,不使用 `/**`;Deny > Ask > Allow,因此草稿不得位于受保护目录中,冲突在交付前报告。
227
+ 3. **交付前检查全局设置**:CLI 的 `toolPermission` 须未设置或为 `request-review`;CLI 与共享全局 allow 中不得有任何 `command(...)`、`unsandboxed(...)`、`unsandboxed_command(...)` 或 `*` 放行,否则拒绝交付,因为全局放行会让 shell 绕过项目的文件规则。未授权 shell 在非交互模式下被拒绝。
228
+ 4. **不改用户全局设置**:本工具不向 agy 全局设置写入任何规则;`setup` 只移除早期版本记录在 `agy-permission.json` 中的全局规则(先备份到服务目录 `agy-settings.backup.json`),其他设置与用户规则不变。
224
229
 
225
- Codex 要求 CLI 0.163.0 或更新版本,使用 permission profiles 和本地 `app-server config/read`。配置检查先核对初始化回执中的版本;不支持或无法识别的版本拒绝交付。每次交付前只启动本地配置检查进程,不创建会话、不调用模型;发现已加载的旧 `sandbox_mode` / `sandbox_workspace_write` 或新配置未被采用就拒绝交付,避免旧设置覆盖本次限制。报错后需更新不支持的 CLI 或移除冲突的旧配置;接入程序不自动改写用户全局设置。首次与续接使用同一组权限参数,配置检查进程也随暂停或停止清理。[Codex 权限配置](https://learn.chatgpt.com/docs/permissions)
230
+ 原生工具权限不是操作系统沙箱;工具被拒绝时 agy 仍可能退出码为 0,验收以实际文件和网站结果为准。[原生权限文档](https://antigravity.google/docs/permissions?tab=cli)、[CLI 设置](https://antigravity.google/docs/settings?tab=cli)。
226
231
 
227
- agy 没有单次命令放行参数,新管理会话由上述项目规则放行。兼容旧会话和手动接入的 `setup` 仍可向 `~/.gemini/antigravity-cli/settings.json` 的 `permissions.allow` 写入规则:选中 Gemini 时询问一次(回车同意,`--yes` 视为同意),追加两条规则:只匹配 `codiscuss` 或 `codiscuss.cmd`、可选一个 `--site` 网址及上述子命令的 `command(regex:...)`,参数不得含分号、`&`、`|`、`<`、`>`、美元符号、反引号与圆括号,避免附加命令或 PowerShell 括号表达式;以及 `write_file(<服务目录>/drafts)`(Windows 保留盘符、使用正斜杠)。写入前备份原文件到服务目录 `agy-settings.backup.json`,`agy-permission.json` 记录已写规则;升级后重跑 setup 会替换本工具记录的旧命令规则,并将宽泛 `launches` 写规则替换为 `drafts`,不改其他设置与用户规则。旧会话通过单独 runner 运行时须按任务中的实际草稿目录配置 agy。文件无法解析时不写入并报告待处理;非交互且未加 `--yes` 只报告。
232
+ 独立讨论项目保存在 `~/.gemini/config/projects/<UUID>.json`,运行目录的 `agy-project-<参与者摘要>.json` 保存项目与实际会话绑定。所选原项目及用户全局配置不被改写;新项目只沿用资源目录,不继承原项目的命令放行。项目名为 `CoDiscuss · 讨论标题 · 参与者`,便于在 Antigravity 项目列表中区分。会话继续使用时保留这两份记录,不将生成的讨论会话手动迁入其他项目;讨论收束或存档后仍保留,以便之后在同一会话中继续讨论。只有网站确认房间已被删除(`房间不存在`)时,接入程序才删除该房间建立的讨论项目与记录,不触碰用户自己的项目。缺失已登记的原生项目或会话绑定不匹配时拒绝交付。[项目权限继承](https://antigravity.google/docs/projects?tab=cli)。
233
+
234
+ Codex 要求 CLI 0.163.0 或更新版本,使用 permission profiles 和本地 `app-server config/read`。配置检查先核对初始化回执中的版本;不支持或无法识别的版本拒绝交付。每次交付前只启动本地配置检查进程,不创建会话、不调用模型;发现已加载的旧 `sandbox_mode` / `sandbox_workspace_write` 或新配置未被采用就拒绝交付,避免旧设置覆盖本次限制。报错后需更新不支持的 CLI 或移除冲突的旧配置;接入程序不自动改写用户全局设置。首次与续接使用同一组权限参数,配置检查进程也随暂停或停止清理。[Codex 权限配置](https://learn.chatgpt.com/docs/permissions)
228
235
 
229
236
  2026-10-18 本机实测(本轮权限调整前):Claude 主持、Codex 与 Gemini 专家完成一场最小讨论。Windows 的 Codex / agy 经 PowerShell 调用时优先执行 npm 生成的 `codiscuss.ps1`;Codex elevated 沙箱内即使当前用户为 RemoteSigned 也拒绝 `.ps1`,Codex 自行改用 `cmd.exe` 调用 `codiscuss.cmd` 后成功。`agy` 非交互模式下被拒绝的工具也可能伴随退出码 0,最终仍以网站业务保存为准。`agy` 显式使用 `--print-timeout 0`,由房间的单次执行时限和整场截止控制停止,避免额外的 CLI 默认时限提前截断。
230
237
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai_co_discuss",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "bin": {
5
5
  "codiscuss": "runner/codiscuss.mjs"
6
6
  },
@@ -1,7 +1,7 @@
1
1
  /** Actual software entrypoints; never call model service APIs. */
2
2
  import { spawn, spawnSync } from 'node:child_process';
3
3
  import { createHash, randomUUID } from 'node:crypto';
4
- import { existsSync, readFileSync, readdirSync, writeFileSync, renameSync, mkdirSync } from 'node:fs';
4
+ import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync, renameSync, mkdirSync } from 'node:fs';
5
5
  import { isAbsolute, join, relative, resolve } from 'node:path';
6
6
  import { homedir } from 'node:os';
7
7
  import { createInterface } from 'node:readline';
@@ -144,14 +144,14 @@ export function agy_rule() {
144
144
  /** Read optional native settings; malformed settings must not silently disable a permission check. */
145
145
  function read_json(path) { return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : {}; }
146
146
 
147
- /** Native headless review blocks unapproved shell commands; inherited command grants could bypass it. */
147
+ /** Native headless review blocks unapproved shell commands; any global command grant could bypass the project rules. */
148
148
  function check_agy_shell(user_home) {
149
149
  const cli = read_json(join(user_home, '.gemini', 'antigravity-cli', 'settings.json'));
150
150
  const shared = read_json(join(user_home, '.gemini', 'config', 'config.json'));
151
151
  const grants = [...(cli.permissions?.allow || []), ...(shared.userSettings?.globalPermissionGrants?.allow || [])];
152
152
  if ((cli.toolPermission && cli.toolPermission !== 'request-review')
153
- || grants.some(rule => rule !== agy_rule() && (/^(?:unsandboxed_)?command\(/.test(rule) || rule === '*'))) {
154
- throw new Error('agy 全局命令放行或 toolPermission 会绕过本场文件限制;请保留 request-review,重跑 setup 更新旧管理规则并移除额外命令放行,未交付讨论任务');
153
+ || grants.some(rule => rule === '*' || /^(?:command|unsandboxed|unsandboxed_command)\(/.test(rule))) {
154
+ throw new Error('agy 全局设置放行了命令或改了 toolPermission,会绕过讨论项目的权限;请移除全局 command / unsandboxed 放行并保留 request-review(本工具旧版写入的规则可运行 codiscuss setup 清理),未交付讨论任务');
155
155
  }
156
156
  }
157
157
 
@@ -183,12 +183,14 @@ function agy_project_grants(config, task, resources, user_home) {
183
183
  deny: [...new Set(protected_paths.map(write_rule)), 'mcp(*)'] };
184
184
  }
185
185
 
186
- /** Keep a private native project per participant; existing external conversations retain their original project. */
186
+ /** Run agy only inside a private native project this tool created; external conversations keep their own project, so they are refused. */
187
187
  export function prepare_agy_project(config, binding, task, user_home = homedir()) {
188
188
  const key = createHash('sha256').update(task.participant).digest('hex');
189
189
  const record_path = join(task.directory, `agy-project-${key}.json`);
190
190
  const record = read_json(record_path);
191
- if (binding.session_id && !record.project_id) return null;
191
+ if (binding.session_id && !record.project_id) {
192
+ throw new Error('agy 只在本工具新建的讨论会话中自动执行;该会话不是本工具创建的,无法套用讨论项目权限,未交付讨论任务');
193
+ }
192
194
  if (record.session_id && record.session_id !== binding.session_id) throw new Error('agy 项目记录与会话绑定不符,未交付讨论任务');
193
195
  check_agy_shell(user_home);
194
196
  const root = join(user_home, '.gemini', 'config');
@@ -200,13 +202,30 @@ export function prepare_agy_project(config, binding, task, user_home = homedir()
200
202
  const current = read_json(path);
201
203
  if (record.project_id && current.id !== project_id) throw new Error('agy 讨论项目已丢失或不匹配,未交付讨论任务');
202
204
  mkdirSync(join(root, 'projects'), { recursive: true });
203
- atomic_json(path, { ...current, id: project_id, name: `CoDiscuss · ${task.participant}`,
205
+ const name = ['CoDiscuss', task.room_title, task.participant].filter(Boolean).join(' · ');
206
+ atomic_json(path, { ...current, id: project_id, name,
204
207
  projectResources: { resources }, settings: {}, permissionGrants: { permissionGrants: grants, v2Migrated: true } });
205
208
  const saved = { project_id, session_id: record.session_id || '' };
206
209
  atomic_json(record_path, saved);
207
210
  return { project_id, bind: session_id => atomic_json(record_path, { ...saved, session_id }) };
208
211
  }
209
212
 
213
+ /**
214
+ * Delete the agy projects this room created after the room itself is deleted; the user's own projects are never touched.
215
+ * @returns {number} How many project records were cleared.
216
+ */
217
+ export function remove_agy_projects(directory, user_home = homedir()) {
218
+ if (!existsSync(directory)) return 0;
219
+ const records = readdirSync(directory).filter(name => /^agy-project-[a-f0-9]{64}\.json$/.test(name));
220
+ for (const file of records) {
221
+ const { project_id } = read_json(join(directory, file));
222
+ const path = join(user_home, '.gemini', 'config', 'projects', `${project_id}.json`);
223
+ if (/^[a-f0-9-]{36}$/.test(project_id || '') && read_json(path).id === project_id) rmSync(path);
224
+ rmSync(join(directory, file));
225
+ }
226
+ return records.length;
227
+ }
228
+
210
229
  /**
211
230
  * Allow only listed codiscuss website subcommands and draft writes for this run; user rules are added after.
212
231
  * Claude Code on Windows may run commands through its PowerShell tool instead of Bash, so both are listed.
@@ -328,11 +347,10 @@ function cli_invocation(config, binding, task, user_home) {
328
347
  const project = prepare_agy_project(config, binding, task, user_home);
329
348
  const args = ['--output-format', 'stream-json', '--print-timeout', '0', '--add-dir', task.directory,
330
349
  '--add-dir', task.draft_directory, '--add-dir', task.skills_directory];
331
- if (project || (project_directory(config) && config.project_access === 'write')) args.push('--mode', 'accept-edits');
332
- if (project) args.push('--project', project.project_id);
350
+ args.push('--mode', 'accept-edits', '--project', project.project_id);
333
351
  if (binding.session_id) args.push('--conversation', binding.session_id);
334
352
  args.push('-p', task.prompt);
335
- return { args, project, permission_enforcement: project ? 'native' : 'prompt' };
353
+ return { args, project };
336
354
  }
337
355
 
338
356
  /** Persist an emitted identity before the agent reads its execution credential file. */
@@ -428,8 +446,7 @@ async function cli_delivery(config, binding, task, spawn_process, user_home) {
428
446
  });
429
447
  child.stdin.on('error', error => { if (error.code !== 'EPIPE') task.on_failure(error.message); });
430
448
  child.stdin.end(invocation.input);
431
- return { session_id: state.session_id, status: 'accepted',
432
- ...(invocation.permission_enforcement ? { permission_enforcement: invocation.permission_enforcement } : {}) };
449
+ return { session_id: state.session_id, status: 'accepted' };
433
450
  }
434
451
 
435
452
  /** Queue one immutable request for the official App-managed Sidecar. */
@@ -111,6 +111,8 @@ function launch_config(home, input, room, directory, options) {
111
111
  const cwd = profile.cwd || input.cwd;
112
112
  if (cwd && (!existsSync(cwd) || !statSync(cwd).isDirectory())) throw new Error(`${name}:工作目录不存在或不是文件夹,请核对本次目录配置`);
113
113
  if (input.entry === 'conversation' && name === room.host) profile.session_id = input.current_session_id;
114
+ // agy runs only in sessions this tool creates, so its native discussion project always applies.
115
+ if (profile.adapter === 'antigravity_cli' && profile.session_id) throw new Error(`${name}:agy 不能绑定已有会话或从当前 agy 对话主持全自动讨论;请从网页或其他软件发起,agy 会由讨论服务新建会话`);
114
116
  participants[name] = profile;
115
117
  }
116
118
  return validate_config({ site: input.site, room: room.id || 'preview', token: options.token, participants,
@@ -6,7 +6,7 @@ import { DatabaseSync } from 'node:sqlite';
6
6
  import { fileURLToPath, pathToFileURL } from 'node:url';
7
7
  import { parseArgs } from 'node:util';
8
8
  import { setTimeout as sleep } from 'node:timers/promises';
9
- import { atomic_json, deliver_task, setup_sidecar, sidecar_receipt, TASK_SUBCOMMANDS } from './discussion_adapters.mjs';
9
+ import { atomic_json, deliver_task, remove_agy_projects, setup_sidecar, sidecar_receipt, TASK_SUBCOMMANDS } from './discussion_adapters.mjs';
10
10
 
11
11
  const POLL_MS = 2000;
12
12
  const DELIVERY_RETRY_DELAYS_MS = [2000, 4000];
@@ -189,7 +189,7 @@ export class DiscussionRunner {
189
189
  try {
190
190
  const task = task_files(this.config, event, row.token, binding);
191
191
  invoked = true;
192
- const result = await this.deliver(adapter, binding, { ...task,
192
+ const result = await this.deliver(adapter, binding, { ...task, room_title: this.room_title,
193
193
  on_binding: session => this.store.bind(event.participant, session),
194
194
  on_process: child => { this.children.add(child); child.once('close', () => this.children.delete(child)); },
195
195
  on_output: (path, chunk) => appendFileSync(path, chunk, { mode: 0o600 }),
@@ -256,12 +256,19 @@ export class DiscussionRunner {
256
256
  /** Process one bounded iteration without waking models for empty checks. */
257
257
  async tick() {
258
258
  this.store.context(this.config);
259
- let room = await api(this.config, '');
259
+ let room;
260
+ try { room = await api(this.config, ''); }
261
+ catch (error) {
262
+ // Only a permanently deleted room drops its private agy projects; concluded or archived rooms keep them for later continuation.
263
+ if (/^HTTP 404: 房间不存在/.test(error.message)) remove_agy_projects(this.config.directory);
264
+ throw error;
265
+ }
260
266
  if (room.limits?.reached && (!room.limits.closing || room.limits.reason === 'time_limit') && room.runtime.status === 'running') {
261
267
  const generation = room.runtime.generation;
262
268
  room = await api(this.config, '/limits', { actor: '用户' });
263
269
  if (room.runtime.generation !== generation) this.stop_children();
264
270
  }
271
+ this.room_title = room.title;
265
272
  const run = room.runtime;
266
273
  if (run.run_id) this.store.scope(this.config, run.run_id);
267
274
  if (run.mode !== 'automatic' || run.status !== 'running' || run.expired) {
@@ -1,17 +1,16 @@
1
1
  /** npm bootstrap and per-user setup; never logs in or starts an AI conversation. */
2
2
  import { spawn, spawnSync } from 'node:child_process';
3
- import { accessSync, constants, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from 'node:fs';
3
+ import { accessSync, constants, copyFileSync, existsSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from 'node:fs';
4
4
  import { homedir, tmpdir } from 'node:os';
5
5
  import { basename, delimiter, dirname, isAbsolute, join, resolve } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { parseArgs } from 'node:util';
8
8
 
9
- import { agy_rule, atomic_json } from './discussion_adapters.mjs';
9
+ import { atomic_json } from './discussion_adapters.mjs';
10
10
  import { launch_settings, save_launch_settings } from './discussion_launch.mjs';
11
11
  import { service_home } from './discussion_service.mjs';
12
12
  import { install_skills } from './discussion_skills.mjs';
13
13
  import { ask, checkbox_prompt, intro, outro, pad, short_path, spinner, step, styled, text_width, tone } from './discussion_tui.mjs';
14
- export { agy_rule } from './discussion_adapters.mjs';
15
14
  export { checkbox_prompt } from './discussion_tui.mjs';
16
15
 
17
16
  const ROOT = fileURLToPath(new URL('../', import.meta.url));
@@ -198,62 +197,44 @@ async function choose_profile(client, current, options) {
198
197
  return { client: client.name, status: 'configured', effective: detected };
199
198
  }
200
199
 
201
- /** Allow answer files only; service runtime data never belongs to the managed write rule. */
202
- export function agy_rules(home) {
203
- // Verified with agy 1.4.2 on Windows: the rule matches forward slashes with the drive letter kept.
204
- const drafts = join(resolve(home), 'drafts').replaceAll('\\', '/');
205
- return [agy_rule(), `write_file(${drafts})`];
206
- }
207
-
208
- /** Ask once before adding the rules; --yes counts as consent, other noninteractive runs only report. */
209
- async function agy_consent(path, rules, options) {
210
- if (options.yes) return true;
211
- if (!process.stdin.isTTY) return false;
212
- const answer = await ask(`允许 Gemini(agy)在全自动讨论中执行讨论命令并写讨论草稿?将向 ${path} 的 permissions.allow 追加 ${rules.length} 条只匹配讨论命令与草稿目录的规则`, ' [Y/n]');
213
- return !['n', 'no', '否'].includes(answer.trim().toLowerCase());
214
- }
215
-
216
- /** Keep the installer-managed allow rules current, never touching other settings or user rules. */
217
- export async function allow_agy_command(home, user_home, options) {
218
- const path = join(user_home, ...AGY_SETTINGS);
200
+ /**
201
+ * Remove global agy rules that earlier versions added; agy permissions now live only in each discussion project.
202
+ * Only rules recorded by the installer are removed, after a backup; other settings and user rules stay untouched.
203
+ */
204
+ export function remove_legacy_agy_rules(home, user_home) {
219
205
  const record = join(home, AGY_RULE_RECORD);
220
- const rules = agy_rules(home);
206
+ if (!existsSync(record)) return null;
207
+ const path = join(user_home, ...AGY_SETTINGS);
208
+ const saved = JSON.parse(readFileSync(record, 'utf8'));
209
+ const legacy = saved.rules || (saved.rule ? [saved.rule] : []);
221
210
  let settings = {};
222
211
  try { if (existsSync(path)) settings = JSON.parse(readFileSync(path, 'utf8')); }
223
- catch { return { status: 'invalid', path, rules }; }
212
+ catch { return { client: 'Gemini', status: 'invalid', path }; }
224
213
  const allow = settings.permissions?.allow ?? [];
225
- if (!Array.isArray(allow)) return { status: 'invalid', path, rules };
226
- const saved = existsSync(record) ? JSON.parse(readFileSync(record, 'utf8')) : {};
227
- const previous = saved.rules || (saved.rule ? [saved.rule] : []);
228
- const stale = previous.some(rule => !rules.includes(rule) && allow.includes(rule));
229
- mkdirSync(home, { recursive: true });
230
- if (rules.every(rule => allow.includes(rule)) && !stale) {
231
- if (previous.join('\n') !== rules.join('\n')) atomic_json(record, { rules });
232
- return { status: 'unchanged', path, rules };
214
+ if (!Array.isArray(allow)) return { client: 'Gemini', status: 'invalid', path };
215
+ const next = allow.filter(rule => !legacy.includes(rule));
216
+ if (next.length !== allow.length) {
217
+ copyFileSync(path, join(home, AGY_SETTINGS_BACKUP));
218
+ atomic_json(path, { ...settings, permissions: { ...settings.permissions, allow: next } });
233
219
  }
234
- if (!await agy_consent(path, rules, options)) return { status: options.yes || process.stdin.isTTY ? 'declined' : 'pending', path, rules };
235
- mkdirSync(dirname(path), { recursive: true });
236
- if (existsSync(path)) copyFileSync(path, join(home, AGY_SETTINGS_BACKUP));
237
- const next = [...allow.filter(item => !previous.includes(item) && !rules.includes(item)), ...rules];
238
- atomic_json(path, { ...settings, permissions: { ...settings.permissions, allow: next } });
239
- atomic_json(record, { rules });
240
- return { status: 'allowed', path, rules };
220
+ rmSync(record);
221
+ return { client: 'Gemini', status: next.length !== allow.length ? 'removed' : 'unchanged', path };
241
222
  }
242
223
 
243
224
  /** Save the selected effective profiles and copy complete skills without overwriting edits. */
244
225
  async function configure_clients(home, selected, options, existing) {
245
226
  const participants = { ...existing };
246
- const skills = [], profiles = [], permissions = [];
227
+ const skills = [], profiles = [];
247
228
  const user_home = resolve(options['skills-home'] || homedir());
248
229
  for (const client of selected) {
249
230
  const profile = await choose_profile(client, participants[client.name], options);
250
231
  profiles.push(profile);
251
232
  if (profile.status === 'configured') participants[client.name] = { ...participants[client.name], ...profile.effective };
252
233
  skills.push({ client: client.name, results: install_skills(join(user_home, ...client.skills)) });
253
- if (client.key === 'antigravity') permissions.push({ client: client.name, ...await allow_agy_command(home, user_home, options) });
254
234
  }
255
235
  save_launch_settings(home, { participants });
256
- return { skills, profiles, permissions };
236
+ const legacy = remove_legacy_agy_rules(home, user_home);
237
+ return { skills, profiles, permissions: legacy ? [legacy] : [] };
257
238
  }
258
239
 
259
240
  /** Exclude npm exec's temporary command wrappers when checking persistent availability. */
@@ -292,8 +273,7 @@ function setup_issues(result) {
292
273
  if (skill.status === 'conflict') issues.push(`${group.client} 技能冲突,已保留 ${skill.path};请核对并移走需替换的旧副本,再运行 setup。`);
293
274
  }
294
275
  for (const permission of result.permissions) {
295
- if (permission.status === 'invalid') issues.push(`${permission.client} 设置文件无法解析,未写入讨论命令规则:${permission.path};修复后重新运行 setup。`);
296
- if (['declined', 'pending'].includes(permission.status)) issues.push(`${permission.client} 未放行讨论命令,全自动讨论会被拦截;运行 codiscuss setup --clients antigravity --yes 授权。`);
276
+ if (permission.status === 'invalid') issues.push(`${permission.client} 设置文件无法解析,未能移除旧版全局放行规则:${permission.path};修复后重新运行 setup,否则全自动讨论会拒绝交付给 agy。`);
297
277
  }
298
278
  if (result.command.status !== 'ready') issues.push(result.command.action);
299
279
  return issues;
@@ -345,7 +325,7 @@ function report_setup(result, options = {}) {
345
325
  ? `${profile.client}:仅安装技能,可手动参与,尚不支持全自动接入。`
346
326
  : `${profile.client}:${profile.status === 'configured' ? 'CLI 已配置' : '沿用已有配置'}(${profile.effective.adapter} / ${profile.effective.executable || '未设置路径'})`);
347
327
  for (const permission of result.permissions) {
348
- if (['allowed', 'unchanged'].includes(permission.status)) lines.push(`${permission.client}:已放行讨论命令(${permission.path})`);
328
+ if (permission.status === 'removed') lines.push(`${permission.client}:已移除旧版全局放行规则(${permission.path})`);
349
329
  }
350
330
  for (const group of result.skills) lines.push(`${group.client} Skill:${group.results.filter(skill => skill.status !== 'conflict').length}/${group.results.length} 份文件就绪,目录 ${dirname(group.results[0].path)}`);
351
331
  if (result.command.path) lines.push(`持久命令:${result.command.path}(${result.command.status})`);
@@ -368,7 +348,7 @@ function report_styled(result, options) {
368
348
  for (const profile of result.profiles) rows.push(profile.status === 'manual' ? [profile.client, '仅安装技能,可手动参与', '尚不支持全自动接入']
369
349
  : [profile.client, `${profile.status === 'configured' ? 'CLI 已配置' : '沿用已有配置'} · ${profile.effective.adapter}`, short_path(profile.effective.executable) || '未设置路径']);
370
350
  for (const permission of result.permissions) {
371
- if (['allowed', 'unchanged'].includes(permission.status)) rows.push([permission.client, '已放行讨论命令', short_path(permission.path)]);
351
+ if (permission.status === 'removed') rows.push([permission.client, '已移除旧版全局放行规则', short_path(permission.path)]);
372
352
  }
373
353
  if (result.skills.length) rows.push(['技能', result.skills.map(group => `${group.client} ${group.results.filter(skill => skill.status !== 'conflict').length}/${group.results.length}`).join(' · '), 'discuss-host · discuss-participant · discuss-report']);
374
354
  if (result.command.path) rows.push(['命令', `codiscuss ${tone.dim(`(${result.command.status})`)}`, short_path(result.command.path)]);