cowork-flow 1.7.1 → 1.7.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,26 @@
5
5
  ### Added
6
6
 
7
7
 
8
+ ## [1.7.3] - 2026-10-06
9
+
10
+ ### Fixed
11
+
12
+ - ZCode 插件 hook shim 由 `.js` 改为 **`.mjs`**,并修复项目根向上查找只覆盖两层的缺陷(1.7.2 遗留说明的收口)。修前两个缺陷叠加:shim 安装到宿主插件缓存目录后无 `package.json`,`.js` 按 CommonJS 解析,ESM `import` 只靠 Node ≥23 的语法探测兜住——仓库声明最低 Node 20,这些机器上 shim 直接语法错误,SessionStart / UserPromptSubmit / PostToolUse 注入从未发生;同时 `findWorkflowRoot` 在循环外缓存 `dirname(current)`,向上查找只覆盖起始目录与其父,hook 事件的 cwd 在项目根下两级及以上(例如从子目录打开会话)时定位不到项目运行时,注入同样为空。现在对齐 qoder / kimi 先例:`.mjs` 扩展名 + `resolve` 起步 + 逐级向上直到文件系统根,其余传输行为零变化。升级动作:升级 CLI 后运行 `cwf host add zcode --force` 刷新插件缓存,并重启 ZCode。
13
+
14
+
15
+ ## [1.7.2] - 2026-10-04
16
+
17
+ ### Added
18
+
19
+ - GitHub 现在会在收到 `v*` tag 推送时自动发布 npm:`.github/workflows/publish.yml` 新增 `push: tags` 触发器与按 tag 归一的 `concurrency` 锁(同一 tag 的重复触发串行化而非并发抢发),`scripts/release.sh --no-publish` 改为推送分支与 tag 并把发布交给 CI,`gh release create` 降级为可选的 Release 说明入口。推送 tag 即发布,无需再手工建 Release。
20
+
21
+ ### Fixed
22
+
23
+ - 修复 tag 推送通道的空转:`verify-ubuntu` / `verify-windows` / `publish` 三个 job 的触发条件此前只认 `release` 事件和 `inputs.ref`,`on: push: tags` 触发工作流后三个 job 会被全部跳过——运行显示绿色却不会发布任何内容。三个门禁现在都放行 push 事件;Ubuntu 与 Windows 双平台验证仍是发布的硬前置。新增 `test/package.test.js` 断言锁住这一行为。
24
+ - npm 发布改用 **Trusted Publishing**(OIDC):publish job 声明 `id-token: write`,用 GitHub OIDC token 换取短期 npm 凭据,仓库不再保存长期 token。OIDC 交换需要 npm ≥ 11.5.1,而 Node 20 自带 npm 10,因此仅 publish job 改跑 Node 24(两个验证 job 保持 Node 20,与 `engines` 声明一致)。`setup-node` 保留 `registry-url` 以便 npm 读到 registry 后发起交换,但不提供 `NODE_AUTH_TOKEN`——占位符保持未解析,npm 先试交换、失败才回退。此前用 `NPM_TOKEN` secret 时,账号开启双因素认证会导致 CI 发布以 `EOTP` 失败。
25
+ - `package.json` 的 `bin` 路径去掉 `./` 前缀:npm 发布时会自动清理并告警,导致 tarball 里的 package.json 与源码不一致。
26
+ - Qoder 插件 hook 不再由宿主解析 `python`:改为 **Node 传输 shim**(`command: "node"` + `hooks/inject-context.mjs`),与 ZCode 插件同形态。修前在没有 `python` 命令的机器上(macOS 12.3+、只装 `python3` 的发行版),hook 以 `spawn python ENOENT` 静默失败,SessionStart / UserPromptSubmit 注入从未发生,日志里只有 `Non-blocking hook error(s) … Continuing execution`;Windows 上则依赖 `python.exe` 恰好在 PATH,商店版 Python 等环境同样哑。新 shim 由宿主必备的 Node 运行,内置 `COWORK_FLOW_PYTHON → python3 → python → py -3` 探测链自行定位项目运行时解释器,探测不到时保持 fail-open(exit 0)。升级动作:`cwf host add qoder --force` 刷新插件缓存后重启 Qoder。ZCode 的 `.js` shim(ESM 语法)在安装目录无 `package.json` 时依赖 Node ≥23 的语法探测,仓库声明的最低 Node 20 上会崩溃,建议后续同样改 `.mjs`——本次未动 ZCode。
27
+
8
28
  ## [1.7.1] - 2026-09-30
9
29
 
10
30
  ### Fixed
@@ -269,7 +289,9 @@
269
289
 
270
290
  此版本未保留本地条目正文,详情见 [GitHub Release](https://github.com/fishlikewater/cowork-flow/releases/tag/v0.0.47)。
271
291
 
272
- [Unreleased]: https://github.com/fishlikewater/cowork-flow/compare/v1.7.1...HEAD
292
+ [Unreleased]: https://github.com/fishlikewater/cowork-flow/compare/v1.7.3...HEAD
293
+ [1.7.3]: https://github.com/fishlikewater/cowork-flow/compare/v1.7.2...v1.7.3
294
+ [1.7.2]: https://github.com/fishlikewater/cowork-flow/compare/v1.7.1...v1.7.2
273
295
  [1.7.1]: https://github.com/fishlikewater/cowork-flow/compare/v1.7.0...v1.7.1
274
296
  [1.7.0]: https://github.com/fishlikewater/cowork-flow/compare/v1.6.0...v1.7.0
275
297
  [1.6.0]: https://github.com/fishlikewater/cowork-flow/compare/v1.5.0...v1.6.0
@@ -0,0 +1,167 @@
1
+ # cowork-flow 方向规划:Agent 运行时上下文与协作事实层
2
+
3
+ > 状态:**已批准(Approved)** · 创建:2026-08-27 · 决策记录:注入格式取舍选**方案 A**(结构化事实头 + 人读摘要体,分级降级)
4
+ > 本文档是后续阶段立项的权威输入。任何方向的修订必须先更新本文档再立项。
5
+
6
+ ---
7
+
8
+ ## 0. 状态追踪(防遗忘,后续会话先看这里)
9
+
10
+ | 阶段 | 状态 | 关联任务 / 提交 | 下一动作 |
11
+ |---|---|---|---|
12
+ | 阶段 0:定位与协议收敛 | ✅ 完成 | 08-27-stage0-positioning-protocol(0c7ae64) | — |
13
+ | 阶段 1:事实层 API 化 + 注入结构化 | ✅ 完成 | 1a: 08-28-stage1a-state-fact-view(60753fa);1b: 08-28-stage1b-structured-fact-header(5cb25ea) | — |
14
+ | 阶段 2:多执行者语义 | ✅ 完成 | 08-28-stage2-multi-executor(fd4d6af):executor 归属 / 冲突拦截 / --takeover / 无会话 start / evidence 证据位 | — |
15
+ | 阶段 3:协议生态适配 | ✅ 完成(观察期持续) | 08-29-stage3-mcp-fact-access(a88474e):`run mcp-state` 无依赖 MCP 只读服务 + `fact-layer-access.md` 接入契约 | 观察 AGNTCY 等协议收敛;写工具默认拒绝,需重开契约 |
16
+ | 本规划文档落库 | ✅ 完成 | 08-27-direction-doc | — |
17
+ | 1.5→1.7 宿主广度 + 发布链(七宿主) | ✅ 完成 | CHANGELOG 1.5.0-1.7.2 | — |
18
+ | 1.7.3 维护:ZCode shim .mjs + 项目根查找修复 | ✅ 完成 | 10-04-zcode-shim-mjs(d0716a9) | — |
19
+ | 1.8.0 立项:证据化完成 | 🔄 立项完成,待启动 | 10-05-direction-1-8-0 / 10-05-evidence-completion | 在宿主会话启动主线任务 |
20
+
21
+ **当前结论**:方向路线图阶段 0-3 全部落地。项目当前形态:「多主机 Agent 协作的运行时上下文与协作事实层」——事实视图(CLI + MCP)、结构化注入、跨宿主一致性治理、执行者归属与证据位均已就绪并有测试锁定。后续演进见「不做什么」边界与观察期事项。
22
+
23
+ ---
24
+
25
+ ## 1. 定位(一句话)
26
+
27
+ > **cowork-flow 不约束模型怎么想;它保证任何主机上的任何 agent,在任何会话、任何时刻,从同一份权威事实起步。**
28
+
29
+ | | 旧定位 | 新定位 |
30
+ |---|---|---|
31
+ | 叙事 | 帮你建流程(流程模板) | 运行时上下文与协作事实层 |
32
+ | 手段 | 任务流、门禁、规格文档 | 状态注入、事实一致性、跨宿主治理 |
33
+ | 目的 | 让协作"守规矩" | 让协作"从正确状态起步" |
34
+ | 模型能力上升时 | 约束类价值递减(模型自己会守规矩) | 事实类价值递增(状态是模型编不出来的) |
35
+
36
+ **为什么必须现在调整**:项目的消费方正在从"人"变成"agent"。注入格式还停留在给人看的文本(`<workflow-state>` 的 label:value 行 + 散文,全五宿主一致;唯一严格机器可读的是 fingerprint 属性行)。事实层(task.json+revision、state-snapshot、operations 日志、implement.jsonl、contract-registry、host-assets)已经建起来,但散落成文件约定,没有统一机器出口;决策事实(decision-anchor)完全不在注入体系内。
37
+
38
+ ## 2. 背景:大模型发展对项目的五个冲击
39
+
40
+ 1. **模型能力上升** → "约束模型"价值递减(门禁、防呆、状态机防漂移——模型越强越不需要被管),"喂状态"价值递增(上下文质量成为 agent 质量的第一杠杆)。
41
+ 2. **人机对话 → agent 集群执行**(远程/后台/并行/批处理/CI 触发)→ 会话模型需支持无会话执行者、多执行者协作、委派与收归。
42
+ 3. **上下文工程成为学科**(token 预算、注入时机、摘要压缩、状态新鲜度)→ 项目已有的五宿主 hook 注入、digest 指纹、快照单源正是"运行时上下文中枢",应提升为产品主线。
43
+ 4. **协议标准化**(MCP 成事实标准、hooks.json 类扩展点演化、agent 间协议涌现)→ 契约格式自持、host 适配保持薄层;不自创跨 agent 协议。
44
+ 5. **模型质量波动** → 门禁仍是安全网(每个新版本可能有行为回归),保留但不作为卖点投资。
45
+
46
+ ## 3. 现状资产盘点(2026-08-27 探查实录)
47
+
48
+ ### 3.1 注入层
49
+
50
+ | Host | 注入机制 | 触发时机 | 格式 |
51
+ |---|---|---|---|
52
+ | zcode | 插件 process hook(JS,inject-context.js) | SessionStart 全量 / 每条消息单行指纹 / PostToolUse 生命周期命令刷新 | stdout JSON,内部文本块 |
53
+ | codex | hooks.json 命令(Python) | 仅 UserPromptSubmit | 全量 digest(无 slim) |
54
+ | claude-code | settings.json hook(Python) | SessionStart + 每条消息 | 全量 digest(无 slim) |
55
+ | opencode | 插件(JS) | 每次 prompt 组装 + shell.env | 全量 digest(无 slim) |
56
+ | dsh | 预设插件(内嵌 Python 协议) | session-start / inbox-claimed / 生命周期命令工具结果 | systemPrompt section 文本 |
57
+
58
+ **已知债**:三线零代码共享(zcode JS / opencode JS / python 线)——指纹序列化策略不一致(`JSON.stringify` 不排序 vs `sort_keys` vs `stableStringify`,跨 host 指纹有分叉风险);slim 指纹形态只有 zcode 有;codex 事件名硬编码丢弃事件维度;state-snapshot 只有 zcode 消费(Python 线仍 TAG_RE 解析 templates.md,是已确认的漂移面)。
59
+
60
+ ### 3.2 事实层(任务状态模型)
61
+
62
+ - **权威事实(生命周期内不变)**:id/name、title、creator、assignee、createdAt、priority、description;决策事实 decision-anchor.md(目标/AC/被拒方案/假设/范围);绑定事实 `task.json.meta.planFile`;写入痕迹 `_state.revision`+`operation_id` 与 operations/ 操作日志(追加式)。
63
+ - **流程状态(有明确写者)**:status(planning→in_progress→review→completed,单向禁回退)、completedAt、children/parent、会话绑定(sessions/*.json、subagents/*.json)、state-snapshot.json、归档位置。
64
+ - **死字段待清理**:`commit`(声明存在、无写入方)、`scope`、`relatedFiles`(已被 implement.jsonl 取代)、`subtasks`(无写入方)。
65
+ - **模型**:单主人同步推进(start 必须会话身份;review/complete 已支持无会话);子代理为受缚叶执行者(authority 五项全 false,证据生产不推进状态);并发正确性靠 CAS+锁+UoW 恢复,已成熟。
66
+ - **机器出口现状**:JSON 有 task.json、snapshot、implement.jsonl、sessions/subagents、operations、`task next --json`、contract-registry、host-assets;**仅人读无解析**:decision-anchor.md(只查章节字符串)、plans/*.md(v1 显式不解析)。
67
+
68
+ ### 3.3 治理层
69
+
70
+ - 分层哲学已验证:**机器只裁决事实,判断交给 review 双人证**。
71
+ - 硬契约:host-assets.json(能力矩阵,doctor 交叉校验)、contract-registry.json(指纹清单)、schemas/、lifecycle 文件范围白名单、plan-binding gate、发布链(release:check + CHANGELOG + 三处版本同步)。
72
+ - 软规范:backend/frontend/game(review 强审查、非机器 gate)、references/(loadWhen 驱动)。
73
+ - 模型能力上升时:纯防呆类(guides、error-output-as-data 类)价值递减;跨宿主一致性类(host-assets 校验、分布漂移检测、syncPolicy 保护、registry 指纹)价值递增。
74
+
75
+ ## 4. 为什么这是最优方向(候选否决)
76
+
77
+ | 候选 | 否决理由 |
78
+ |---|---|
79
+ | AgentOps / 可观测性 | 无运行遥测资产,从头建等于换赛道,与工作流引擎定位断裂 |
80
+ | 任务编排引擎 | 与主机自带编排重叠;状态模型翻成分布式系统复杂度爆炸 |
81
+ | 纯治理合规层 | 纯门禁价值随模型自觉性上升递减;无独特性 |
82
+ | ✅ 事实层 + 注入 | 全仓最厚资产都在这;跨主机一致性是任何单主机工具给不了的**结构性差异**;与上下文工程趋势同向且落在可执行的事实层 |
83
+
84
+ ## 5. 三支柱
85
+
86
+ | 支柱 | 内涵 | 已有资产 | 缺口 |
87
+ |---|---|---|---|
88
+ | **事实层** | 权威状态与决策的可查询存储 | task.json+revision、snapshot、operations、implement.jsonl、registry、host-assets | 无统一机器出口;decision-anchor 无解析;死字段 |
89
+ | **注入协议** | 按时机/角色/token 预算把事实注入任意 host | 五宿主注入齐备、digest 指纹、快照信任链、会话隔离 | 人读文本格式;三线拷贝;slim 不对称;决策事实不注入 |
90
+ | **一致性治理** | 跨宿主/跨会话/跨模型的事实一致性 | doctor 交叉校验、同步保护、文件范围门禁、发布链 | 指纹序列化不一致;快照单宿主消费;无跨 host 注入行为断言 |
91
+
92
+ ## 6. 演进路线
93
+
94
+ ### 阶段 0:定位与协议收敛(低风险、高杠杆)
95
+ - 交付:README 定位改写;新增 `spec/contracts/context-injection.md`(冻结块结构、事件时机矩阵、slim 策略、序列化规范,三线按规范对齐);修一致性债(指纹序列化统一 sort_keys、Python 线补 slim、codex 补事件名)。
96
+ - 验收:新增跨 host 注入行为测试——同一仓库五宿主指纹一致、slim 触发一致。
97
+
98
+ ### 阶段 1:事实层 API 化 + 注入结构化
99
+ - 交付:`cowork-flow state <task> --json`(task.json+snapshot+sessions+plan 绑定+registry 汇总为一份事实视图);workflow-state 块升级为**结构化事实头 + 紧凑人读体**(协议允许分级降级,纯文本通道退化为属性行);决策事实条件注入(planning/in_progress 时注入 decision-anchor 要点,完成后收缩)。
100
+ - 验收:任何 host 的 agent 可经上下文与 CLI 拿到"任务是什么、为什么、边界在哪",且机器可解析。
101
+
102
+ ### 阶段 2:多执行者语义
103
+ - 交付:owner/executor 归属字段;无会话推进放开(CLI/CI 触发);多会话协作同一任务的协调(任务级锁/合并协议,复用 CAS+UoW);子代理证据位独立于状态位。
104
+ - 验收:两会话可显式接管同一任务不踩踏;子代理证据可独立推进"证据位"。
105
+
106
+ ### 阶段 3:协议生态适配(观察期,不承诺)
107
+ - 交付:MCP 服务暴露事实层(只读查询先行,`state --json` 的薄壳);观察 AGNTCY 等,不自创协议;保持 adapter 薄。
108
+ - 验收:任何 MCP 客户端可查任务状态;生态协议出现时 2 天内可接入。
109
+
110
+ ## 7. 已拍板取舍
111
+
112
+ - **注入格式(已决策:方案 A)**:结构化事实头 + 人读摘要体双格式,分级降级——结构化优先,通道不允许时退化为属性行/纯摘要。
113
+ - 否决 B(全 JSON 注入):dsh/codex 纯文本通道浪费 token,破坏现有紧凑散文面包屑可读性。
114
+ - 否决 C(保持文本只加 --json 出口):核心价值主张落不到注入本身。
115
+ - **事实层出口(推荐)**:CLI + JSON 协议先行(零依赖),MCP 为阶段 3 的薄适配,不先建。
116
+ - **三线收敛(推荐)**:先"协议规范单源 + 三线按规范实现",代码级共享后议(成本高、收益在协议一致性先兑现)。
117
+
118
+ ## 8. 明确不做什么
119
+
120
+ 1. 不做"更聪明的提示词/AGENTS.md 写作术"——模型能力会追平,且无数据资产。
121
+ 2. 不自创跨 agent 通信标准——协议博弈期,等收敛。
122
+ 3. 不做任务执行引擎——不替代 agent 干活,只保证它从正确状态起步。
123
+ 4. 不深化纯流程门禁——保留为安全网,停止投资。
124
+
125
+ ## 9. 度量指标
126
+
127
+ 1. **跨宿主行为一致**:五宿主指纹/slim 触发有测试断言,0 漂移。
128
+ 2. **新 host 接入成本**:从数天降到小时级(协议规范完备度直接度量)。
129
+ 3. **注入 token 成本**:长会话注入体积不随消息数线性增长(slim 全覆盖)。
130
+ 4. **事实新鲜度事故**:多会话任务污染/踩踏事故率趋零。
131
+
132
+ ## 10. 2026-10 修订:1.8.0 方向(证据化完成)
133
+
134
+ > 修订背景:阶段 0-3 之后,1.5→1.7 三条版本线投入宿主广度(七宿主)与发布链,广度路线边际收益递减;生态出现确定性信号,方向回到事实层纵深。本节为 1.8.0 立项的权威依据。
135
+
136
+ ### 10.1 生态输入(2026-10 复核)
137
+
138
+ 1. **spec-driven development 主流化**:GitHub Spec Kit、AWS Kiro、OpenSpec、Tessl 押同一注——spec 与代码作为可跟踪制品共同演化;市场口径是"builder 们正从自由式 vibe coding 走向结构化工作流"。cowork-flow 的 decision-anchor + plan-binding 在此赛道上,结构性差异是宿主中立、状态在仓库内——定位为这些 SDD 工具**之下的状态层**,不与其竞争。
139
+ 2. **agent 团队 / 后台 agent 兴起**:Claude Code Agent Teams 官方口径 teammates ephemeral by design,靠 git 与文件协调;"持久共享状态"成为公认缺口。cowork-flow 的任务状态在仓库、跨会话可续、executor 归属与冲突拦截(阶段 2 资产)正是该缺口的产品化。
140
+ 3. **质量与隐藏 bug 是 vibe coding 头号风险叙事**:"验证证据"正在成为完成的定义。这与分层哲学(机器裁决事实、review 双人证)同向,但 AC 目前只是 decision-anchor 里的 markdown 勾选框,机器不解析。
141
+
142
+ ### 10.2 1.8.0 主线:证据化完成
143
+
144
+ - **定义**:任务完成从"流程走完"升级为"证据齐全"——decision-anchor 的 AC 成为机器可查询事实,完成门禁校验每个 AC 关联到可验证证据(测试 / 命令输出 / evidence 记录),缺失时阻断或显式放行(对齐 spec-check 的 unchecked 语义)。
145
+ - **最小设计立场**:不发明新格式——AC 沿用 decision-anchor 现有 `- [ ] AC-xxx:` checklist 形态做解析;证据落在任务目录 `evidence.jsonl`(每行 `{ac, kind, ref, recordedAt}`,追加式);无 decision-anchor 或无 AC 的旧任务自动跳过(兼容路径,不产生虚假阻断)。
146
+ - **注入面**:review / 完成阶段注入 AC 覆盖事实行(n/m 有证据);contract digest 随之变化,需同步跨宿主一致性测试与 `spec/contracts/context-injection.md` 契约。
147
+ - **为什么先做它**:自洽(不依赖宿主团队功能节奏)、直接回应"AI 生成代码怎么算完成"的行业焦虑、是候选 B 的前置(evidence 事实格式先定,团队协调才有可信的交接凭证)。
148
+
149
+ ### 10.3 候选与缓行
150
+
151
+ - **候选 B(1.9.0 候选主线):agent-teams 就绪**——把 executor / evidence / takeover 语义补成团队协调原语(任务认领、交接、依赖边),让多 agent 场景把 `.cowork-flow/tasks/` 当共享看板。延后理由:生态节奏不完全可控,且需要 1.8.0 的证据格式做地基。
152
+ - **维持缓行**:MCP 写工具(重开 fact-layer-access 契约才可议)、新宿主接入、自创跨 agent 协议。
153
+ - **新增待决项(维护者拍板,不并入 1.8.0)**:文档英文化 / 双语——npm 周下载约 273(2026-10-03 口径),英文社区是 SDD 讨论主场;与 AGENTS.md「文档语言:中文」约定冲突,单独立项时再议。
154
+
155
+ ### 10.4 度量补充
156
+
157
+ 5. **采用**:npm 周下载与激活项目数——1.8.0 之后用数字验证"深度路线"是否兑现。
158
+
159
+ ## 11. 修订记录
160
+
161
+ - 2026-08-27:创建。基于三路只读探查(注入层/状态模型/治理层)+ 大模型趋势分析;用户拍板方案 A;落库任务 08-27-direction-doc。
162
+ - 2026-08-27:阶段 0 完成并更新状态追踪。交付:README 定位改写、`spec/contracts/context-injection.md` 协议契约、指纹序列化三线统一(跨 host 一致性测试锁定)、Python 线/opencode/dsh slim、codex 事件名。任务 08-27-stage0-positioning-protocol(0c7ae64)。
163
+ - 2026-08-28:阶段 1 完成并更新状态追踪。1a:`run state [task] --json` 事实视图(60753fa)。1b:`<workflow-state>` 属性事实头 + `<decision-anchor>` 决策要点注入,三线一致,协议契约同步(5cb25ea)。
164
+ - 2026-08-28:阶段 2 完成并更新状态追踪。executor 归属(start 写入会话 key / 显式 `--executor`)、`LIFECYCLE-EXECUTOR-001` 冲突拦截(幂等重跑同样拦截)、`--takeover` 接管(含已激活任务)、无会话 CI start、`subagent evidence` 证据位、`run state` Executor 行(fd4d6af)。
165
+ - 2026-08-29:阶段 3 完成并更新状态追踪——路线图阶段 0-3 全部落地。`run mcp-state` 无依赖 MCP stdio 只读服务(task_state / task_list)、`spec/contracts/fact-layer-access.md` 接入契约(只读保证 + 薄 adapter 立场)(a88474e)。观察期事项:AGNTCY 等跨 agent 协议收敛后按薄壳接入;MCP 写工具默认拒绝,提案须先重开 fact-layer-access 契约。
166
+ - 2026-08-29:MCP 全局入口补充——npm CLI 新增 `cowork-flow mcp-state` 透传命令(cwd 向上定位项目运行时、stdio inherit、退出码透传),MCP 客户端全局注册一次即可服务所有项目,消除逐项目 `.mcp.json` 配置(9a1fa86)。
167
+ - 2026-08-29:后续方向 P0-P3 执行完毕。**P0**:1.1.0 发布——dev→master 合并、CHANGELOG 1.1.0 段(含 1.0.0 载体错位注明)、tag v1.1.0 与 GitHub Release 创建;publish CI 首跑曝光 node:full 存量问题(dsh-hook 依赖 gitignored 运行时、Windows 无法 exec shebang 脚本、test runner IPC 崩溃)并全部修复——双平台门禁现已全绿。**发布确认:npm registry `cowork-flow@1.1.0` 已上线(dist-tag latest),AC-004 闭合,发版任务归档。** **P1**:`docs/mcp-client-setup.md`(codex/opencode/claude-code/zcode 真实注册格式)+ `test/mcp-client-matrix.test.js` 客户端等效启动验证。**P1'**:publish.yml 链路实测核对并写入 README 发布节。**P2**:flaky 组合复跑 5 轮未复现(记录为单次现象);doctor 会话卫生无残留。**P3**:观察期边界维持——MCP 写工具默认拒绝,AGNTCY 等协议收敛后按薄壳接入。- 2026-10-05:2026-10 修订落库(任务 10-05-direction-1-8-0)。1.7.3 维护完成(10-04-zcode-shim-mjs,d0716a9:ZCode shim 改 .mjs、findWorkflowRoot off-by-one 修复);1.8.0 主线定为**证据化完成**,主线任务 10-05-evidence-completion 立项(planning 就绪待启动);候选 B(agent-teams 就绪)列为 1.9.0 候选主线;文档英文化列为待决项,待维护者拍板。方向文档自本次起恢复版本控制(087ef72 untrack 后首次入库)。
package/docs/index.md CHANGED
@@ -11,6 +11,7 @@
11
11
  | 了解项目文件如何组织 | [工作原理与扩展点](architecture.md) | 运行时、`template/`、`presets/`、规范检查和扩展边界 |
12
12
  | 参与项目开发 | [贡献指南](../CONTRIBUTING.md) | 本地环境、代码约定、测试和 PR 检查 |
13
13
  | 了解测试与发布流程 | [开发与发布](release.md) | 测试分层、CI、版本发布和维护者命令 |
14
+ | 了解项目方向与版本规划 | [方向规划](direction.md) | 定位声明、演进路线、已拍板取舍和版本主线 |
14
15
  | 查某个版本改了什么 | [更新日志](../CHANGELOG.md) | 按版本记录的功能变化、修复和迁移说明 |
15
16
 
16
17
  ## 常见阅读路径
package/docs/release.md CHANGED
@@ -36,7 +36,7 @@ npm run test:template:full
36
36
  | `npm run release` | 发布 patch 版本 |
37
37
  | `npm run release -- minor` | 发布 minor 版本 |
38
38
  | `npm run release -- --version 0.1.0` | 使用指定版本号,跳过自动 bump |
39
- | `npm run release -- minor --no-publish` | 完成版本、提交和 tag,但不执行 `npm publish` |
39
+ | `npm run release -- minor --no-publish` | 完成版本、提交和 tag,推送 commit 与 tag,但不执行 `npm publish`;由 CI 发布 |
40
40
  | `npm run release -- --dry-run` | 运行发布前检查,停在版本 bump 之前 |
41
41
 
42
42
  `--dry-run` 不是只读命令。它会先要求 Git 工作树和暂存区干净,再执行 `source:refresh` 和 `sync --force`,因此可能刷新仓库中被忽略的运行副本,并临时覆盖后恢复 `AGENTS.md`。它不会改版本文件、提交、打 tag 或发布;如果检查开始时已有本地修改,脚本会在任何刷新前停止。
@@ -48,7 +48,7 @@ npm run test:template:full
48
48
  1. 确认工作树和更新日志符合预期。
49
49
  2. 运行 `npm run release:check` 和 `git diff --check`。
50
50
  3. 稳定性改动按上一节重复模板测试。
51
- 4. 运行 `npm run release -- <release-type>`;CI 通道使用 `--no-publish`。
51
+ 4. 运行 `npm run release -- <release-type>`;CI 通道使用 `--no-publish`,它会把 tag 推给 CI。
52
52
  5. 脚本同步 `package.json`、lockfile、`template/.cowork-flow/.version` 和所有随包插件清单。
53
53
  6. 检查提交与 tag,再按下面的 CI 通道发布。
54
54
 
@@ -58,25 +58,34 @@ npm run test:template:full
58
58
 
59
59
  推荐让 GitHub Actions 执行 `npm publish`:
60
60
 
61
- 1. 在本地运行 `npm run release -- <release-type> --no-publish`,完成检查、版本、提交和 tag。
62
- 2. 先推送分支,再显式推送 tag:
61
+ 1. 在本地运行 `npm run release -- <release-type> --no-publish`。脚本完成检查、版本、提交和 tag,然后推送分支与 tag:
63
62
 
64
63
  ```bash
65
64
  git push
66
65
  git push origin v<v>
67
66
  ```
68
67
 
69
- 3. 创建 GitHub Release:
68
+ 2. 推送 tag 即触发 `.github/workflows/publish.yml`。工作流等待 Ubuntu 和 Windows 验证通过后,只从已存在的 `v<version>` tag 发布,并核对 tag、`package.json` 与 `template/.cowork-flow/.version` 的版本一致。
69
+
70
+ 3. 需要给这个 tag 写 Release 说明时,再手动创建(不是发布的必要步骤):
70
71
 
71
72
  ```bash
72
73
  gh release create v<v>
73
74
  ```
74
75
 
75
- 4. `.github/workflows/publish.yml` 等待 Ubuntu 和 Windows 验证通过后,只从已存在的 `v<version>` tag 发布,并核对 tag、`package.json` 与 `template/.cowork-flow/.version` 的版本一致。
76
-
77
76
  不要在 tag 尚未存在于远端时运行 `gh release create`,否则 GitHub 可能从默认分支最新提交创建 tag,把发布指向错误提交。
78
77
 
79
- PR 和发布使用同一套 core 门禁。只有 publish job 能读取 `NPM_TOKEN`,测试 job 不接触发布凭据。
78
+ 同一 tag 的重复触发会被 concurrency 锁串行化,不会并发抢发;如果该版本已经发到 npm,重复运行会在 `npm publish` 阶段被 npm 拒绝,这是预期行为。上一次 CI 失败需要重发时,用 `workflow_dispatch` 指定同一个 tag 重跑即可。
79
+
80
+ PR 和发布使用同一套 core 门禁。
81
+
82
+ 发布凭据走 **Trusted Publishing**:publish job 声明 `id-token: write`,把 GitHub OIDC token 交给 npm 换取短期凭据,仓库里不保存长期 token。登记在 npm 包页面(Settings → Trusted Publishers),声明仓库 `fishlikewater/cowork-flow` 与 workflow 文件 `publish.yml`。
83
+
84
+ 该交换由 npm CLI 实现(11.5.1 起),而 Node 20 自带 npm 10,因此**只有 publish job 跑在 Node 24**;两个验证 job 仍用 Node 20,与 `package.json` 声明的 `engines` 保持一致。
85
+
86
+ publish job 的 `setup-node` **保留 `registry-url`**,但不提供 `NODE_AUTH_TOKEN`。npm 需要在 `.npmrc` 里读到 registry 才会发起 OIDC 交换——去掉它,发布会在未接触令牌端点的情况下直接以 `ENEEDAUTH` 失败。`setup-node` 写入的 `${NODE_AUTH_TOKEN}` 占位符则**故意不解析**:npm 会带着这个空凭据先尝试交换,只有交换被拒时才回退到它。
87
+
88
+ 早期版本用 `NPM_TOKEN` secret。若账号开启双因素认证,classic token 无法完成交互式 OTP,CI 发布会以 `EOTP` 失败——这也是改用 Trusted Publishing 的原因。验证 job(Ubuntu / Windows)不接触任何发布凭据,只跑 `release:check`。
80
89
 
81
90
  ## 维护者命令
82
91
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
4
4
  "description": "CLI for installing and maintaining cowork-flow collaboration templates.",
5
5
  "keywords": [
6
6
  "ai-agent",
@@ -30,8 +30,8 @@
30
30
  },
31
31
  "type": "module",
32
32
  "bin": {
33
- "cwf": "./bin/cowork-flow.js",
34
- "cowork-flow": "./bin/cowork-flow.js"
33
+ "cwf": "bin/cowork-flow.js",
34
+ "cowork-flow": "bin/cowork-flow.js"
35
35
  },
36
36
  "files": [
37
37
  "bin",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
4
4
  "description": "AI 协作工作流引擎:任务生命周期、子代理调度、门禁与状态注入。将 cowork-flow 的完整工作流带入 Claude Code。",
5
5
  "displayName": "Cowork Flow",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
4
4
  "description": "AI 协作工作流引擎:任务生命周期、子代理调度、门禁与状态注入。将 cowork-flow 的完整工作流带入 Codex。",
5
5
  "author": {
6
6
  "name": "fishlikewater",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
4
4
  "description": "AI 协作工作流引擎:任务生命周期、子代理调度、门禁与状态注入。将 cowork-flow 的完整工作流带入 Kimi Code。",
5
5
  "author": {
6
6
  "name": "fishlikewater",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
4
4
  "description": "AI 协作工作流引擎:任务生命周期、子代理调度、门禁与状态注入。将 cowork-flow 的完整工作流带入 Qoder。",
5
5
  "displayName": "Cowork Flow",
6
6
  "author": {
@@ -5,9 +5,9 @@
5
5
  "hooks": [
6
6
  {
7
7
  "type": "command",
8
- "command": "python",
8
+ "command": "node",
9
9
  "args": [
10
- "${QODER_PLUGIN_ROOT}/hooks/inject-context.py"
10
+ "${QODER_PLUGIN_ROOT}/hooks/inject-context.mjs"
11
11
  ],
12
12
  "timeout": 30,
13
13
  "statusMessage": "cowork-flow: 注入工作流状态"
@@ -20,9 +20,9 @@
20
20
  "hooks": [
21
21
  {
22
22
  "type": "command",
23
- "command": "python",
23
+ "command": "node",
24
24
  "args": [
25
- "${QODER_PLUGIN_ROOT}/hooks/inject-context.py"
25
+ "${QODER_PLUGIN_ROOT}/hooks/inject-context.mjs"
26
26
  ],
27
27
  "timeout": 30,
28
28
  "statusMessage": "cowork-flow: 注入工作流状态"
@@ -36,9 +36,9 @@
36
36
  "hooks": [
37
37
  {
38
38
  "type": "command",
39
- "command": "python",
39
+ "command": "node",
40
40
  "args": [
41
- "${QODER_PLUGIN_ROOT}/hooks/inject-context.py"
41
+ "${QODER_PLUGIN_ROOT}/hooks/inject-context.mjs"
42
42
  ],
43
43
  "timeout": 30,
44
44
  "statusMessage": "cowork-flow: 规范自查"
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * cowork-flow Qoder plugin hook — transport shim.
4
+ *
5
+ * Workflow facts (workflow-state, contract digest, decision
6
+ * anchor, stage contract, scope/spec edits) come from the
7
+ * Python source
8
+ * <scripts>/adapters/host/inject.py; this shim does
9
+ * transport only:
10
+ * 1. project resolution from the hook payload (`cwd`),
11
+ * then QODER_PROJECT_DIR / QODER_WORKING_DIR;
12
+ * 2. interpreter location, COWORK_FLOW_PYTHON -> python3
13
+ * -> python -> py -3 — the hook runs under the host's
14
+ * process PATH, which is not the user's login shell;
15
+ * 3. stdin passthrough to inject.py --host qoder, with
16
+ * stdout forwarded and a failing project runtime
17
+ * absorbed (exit 0, reason on stderr) — Qoder refuses
18
+ * the prompt on a non-zero hook exit, and the hook is
19
+ * machine-level, so it may meet a project runtime
20
+ * older than itself.
21
+ *
22
+ * Deliberately named .mjs: the installed plugin cache has
23
+ * no package.json, so a plain .js entry point is parsed as
24
+ * CommonJS there and an `import` file would only load on
25
+ * Node versions that auto-detect module syntax.
26
+ */
27
+
28
+ import { existsSync, readFileSync } from "fs";
29
+ import { dirname, join, resolve } from "path";
30
+ import { spawnSync } from "child_process";
31
+
32
+ const HOST = "qoder";
33
+ const DIR_WORKFLOW = ".cowork-flow";
34
+ // hooks.json grants 30s per event.
35
+ const SPAWN_TIMEOUT_MS = 25000;
36
+
37
+ function readHookInput() {
38
+ if (process.stdin.isTTY) return { raw: "", parsed: {} };
39
+ let raw = "";
40
+ try {
41
+ raw = readFileSync(0, "utf8");
42
+ } catch {
43
+ return { raw: "", parsed: {} };
44
+ }
45
+ try {
46
+ // Tolerate a UTF-8 BOM and surrounding whitespace: the payload is
47
+ // forwarded verbatim either way, only the parse needs it clean.
48
+ const parsed = JSON.parse(raw.replace(/^\uFEFF/, "").trim());
49
+ return { raw, parsed: parsed && typeof parsed === "object" ? parsed : {} };
50
+ } catch {
51
+ return { raw, parsed: {} };
52
+ }
53
+ }
54
+
55
+ function findWorkflowRoot(startDir) {
56
+ if (typeof startDir !== "string" || !startDir.trim()) return null;
57
+ let current;
58
+ try {
59
+ current = resolve(startDir);
60
+ } catch {
61
+ return null;
62
+ }
63
+ for (;;) {
64
+ if (existsSync(join(current, DIR_WORKFLOW))) return current;
65
+ const parent = dirname(current);
66
+ if (parent === current) return null;
67
+ current = parent;
68
+ }
69
+ }
70
+
71
+ function findProjectRoot(parsedInput) {
72
+ const candidates = [
73
+ parsedInput?.cwd,
74
+ process.env.QODER_PROJECT_DIR,
75
+ process.env.QODER_WORKING_DIR,
76
+ process.cwd(),
77
+ ];
78
+ for (const candidate of candidates) {
79
+ const root = findWorkflowRoot(candidate);
80
+ if (root) return root;
81
+ }
82
+ return null;
83
+ }
84
+
85
+ function pythonCandidates() {
86
+ const list = [];
87
+ const env = (process.env.COWORK_FLOW_PYTHON || "").trim();
88
+ if (env) list.push([env]);
89
+ list.push(["python3"], ["python"]);
90
+ if (process.platform === "win32") list.push(["py", "-3"]);
91
+ return list;
92
+ }
93
+
94
+ function main() {
95
+ if (
96
+ process.env.COWORK_FLOW_HOOKS === "0" ||
97
+ process.env.COWORK_FLOW_DISABLE_HOOKS === "1"
98
+ ) {
99
+ process.exit(0);
100
+ }
101
+
102
+ const { raw, parsed } = readHookInput();
103
+ const root = findProjectRoot(parsed);
104
+ if (!root) process.exit(0);
105
+ const injectScript = join(
106
+ root,
107
+ DIR_WORKFLOW,
108
+ "scripts",
109
+ "adapters",
110
+ "host",
111
+ "inject.py"
112
+ );
113
+ if (!existsSync(injectScript)) process.exit(0);
114
+
115
+ for (const candidate of pythonCandidates()) {
116
+ let result;
117
+ try {
118
+ result = spawnSync(
119
+ candidate[0],
120
+ [...candidate.slice(1), injectScript, "--host", HOST],
121
+ {
122
+ input: raw,
123
+ cwd: root,
124
+ timeout: SPAWN_TIMEOUT_MS,
125
+ encoding: "utf8",
126
+ windowsHide: true,
127
+ }
128
+ );
129
+ } catch {
130
+ continue;
131
+ }
132
+ if (result.error || result.status === null) continue;
133
+ if (result.stdout) process.stdout.write(result.stdout);
134
+ if (result.stderr) process.stderr.write(result.stderr);
135
+ // The interpreter ran; its exit status is the project runtime's verdict,
136
+ // not ours. Forwarding it would let a stale project copy fail the event
137
+ // this hook only decorates.
138
+ if (result.status !== 0) {
139
+ process.stderr.write(
140
+ `cowork-flow qoder hook: inject.py exited ${result.status}; skipping injection\n`
141
+ );
142
+ }
143
+ process.exit(0);
144
+ }
145
+ // No interpreter could run the entry: stay silent rather
146
+ // than break the host's event stream (fail-open).
147
+ process.exit(0);
148
+ }
149
+
150
+ main();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
4
4
  "description": "AI 协作工作流引擎:任务生命周期、子代理调度、门禁与状态注入。将 cowork-flow 的完整工作流带入 ZCode。",
5
5
  "author": {
6
6
  "name": "fishlikewater",
@@ -8,7 +8,7 @@
8
8
  "type": "process",
9
9
  "command": "node",
10
10
  "args": [
11
- "${ZCODE_PLUGIN_ROOT}/hooks/inject-context.js"
11
+ "${ZCODE_PLUGIN_ROOT}/hooks/inject-context.mjs"
12
12
  ],
13
13
  "timeoutMs": 5000
14
14
  }
@@ -22,7 +22,7 @@
22
22
  "type": "process",
23
23
  "command": "node",
24
24
  "args": [
25
- "${ZCODE_PLUGIN_ROOT}/hooks/inject-context.js"
25
+ "${ZCODE_PLUGIN_ROOT}/hooks/inject-context.mjs"
26
26
  ],
27
27
  "timeoutMs": 5000
28
28
  }
@@ -37,7 +37,7 @@
37
37
  "type": "process",
38
38
  "command": "node",
39
39
  "args": [
40
- "${ZCODE_PLUGIN_ROOT}/hooks/inject-context.js"
40
+ "${ZCODE_PLUGIN_ROOT}/hooks/inject-context.mjs"
41
41
  ],
42
42
  "timeoutMs": 5000
43
43
  }
@@ -25,7 +25,7 @@
25
25
  */
26
26
 
27
27
  import { existsSync, readFileSync } from "fs";
28
- import { dirname, join } from "path";
28
+ import { dirname, join, resolve } from "path";
29
29
  import { fileURLToPath } from "url";
30
30
  import { spawnSync } from "child_process";
31
31
 
@@ -54,13 +54,18 @@ function readHookInput() {
54
54
 
55
55
  function findWorkflowRoot(startDir) {
56
56
  if (typeof startDir !== "string" || !startDir.trim()) return null;
57
- let current = startDir;
58
- const root = dirname(current);
59
- while (current !== root) {
57
+ let current;
58
+ try {
59
+ current = resolve(startDir);
60
+ } catch {
61
+ return null;
62
+ }
63
+ for (;;) {
60
64
  if (existsSync(join(current, DIR_WORKFLOW))) return current;
61
- current = dirname(current);
65
+ const parent = dirname(current);
66
+ if (parent === current) return null;
67
+ current = parent;
62
68
  }
63
- return existsSync(join(current, DIR_WORKFLOW)) ? current : null;
64
69
  }
65
70
 
66
71
  function findProjectRoot(parsedInput) {
@@ -1 +1 @@
1
- 1.7.1
1
+ 1.7.3
@@ -33,7 +33,7 @@ ZCODE_DIGEST_POLICY = (
33
33
  "read full spec files only before listed actions."
34
34
  )
35
35
 
36
- # The zcode shim (hooks/inject-context.js) pre-filters Bash
36
+ # The zcode shim (hooks/inject-context.mjs) pre-filters Bash
37
37
  # events with the same rule; this entry keeps the transport
38
38
  # contract correct standalone.
39
39
  LIFECYCLE_BASH_RE = re.compile(r"\brun(?:\.cmd)?\s+(?:task|subagent|resume)\b")
@@ -17,13 +17,13 @@ would make the digest self-referential).
17
17
 
18
18
  | Host | Transport | Shape |
19
19
  |---|---|---|
20
- | zcode | process hook shim (`inject-context.js`) → `inject.py --host zcode`, stdout JSON | `hookSpecificOutput.{hookEventName, additionalContext}` |
20
+ | zcode | process hook shim (`inject-context.mjs`) → `inject.py --host zcode`, stdout JSON | `hookSpecificOutput.{hookEventName, additionalContext}` |
21
21
  | codex | thin wrapper (`inject-workflow-state.py`) → `inject.py --host codex`, stdout JSON | same shape |
22
22
  | claude-code | thin wrapper (`inject-workflow-state.py`) → `inject.py --host claude-code`, stdout JSON | same shape |
23
23
  | opencode | plugin `experimental.chat.system.transform` + `shell.env` | system-prompt section push / env object |
24
24
  | dsh | preset plugin system-prompt section | named section, replace semantics |
25
25
  | kimi-code | user-level `config.toml` (`$KIMI_CODE_HOME`, default `~/.kimi-code/`) `[[hooks]]` row on `UserPromptSubmit` → shim (`cowork-flow-inject.mjs`) → `inject.py --host kimi-code` | bare-text stdout appended to the prompt context, no `hookSpecificOutput` envelope |
26
- | qoder | machine-level plugin `hooks/hooks.json` (`${QODER_PLUGIN_ROOT}/hooks/inject-context.py`) → project `.cowork-flow/scripts/.../inject.py --host qoder`, stdout JSON | `hookSpecificOutput.{hookEventName, additionalContext}` plus a top-level `systemMessage` (a CLI-visible trace; the model context is unchanged); `PostToolUse` is not blockable there, so the edit advisory rides `additionalContext` on exit 0 |
26
+ | qoder | machine-level plugin `hooks/hooks.json` (node, `${QODER_PLUGIN_ROOT}/hooks/inject-context.mjs`) → project `.cowork-flow/scripts/.../inject.py --host qoder`, stdout JSON | `hookSpecificOutput.{hookEventName, additionalContext}` plus a top-level `systemMessage` (a CLI-visible trace; the model context is unchanged); `PostToolUse` is not blockable there, so the edit advisory rides `additionalContext` on exit 0 |
27
27
 
28
28
  The qoder row carries one delta beyond the shared envelope: `systemMessage`,
29
29
  emitted only on the two injecting events (`SessionStart`, `UserPromptSubmit`)
@@ -984,7 +984,7 @@ def check_qoder_plugin(repo_root: Path) -> list[dict[str, str]]:
984
984
  "manifest is on disk there; the host cannot load a missing payload",
985
985
  "cwf host add qoder --force",
986
986
  )
987
- for relative in ("hooks/hooks.json", "hooks/inject-context.py"):
987
+ for relative in ("hooks/hooks.json", "hooks/inject-context.mjs"):
988
988
  if not (install_path / relative).is_file():
989
989
  return _qoder_warning(
990
990
  "PLUGIN-PAYLOAD-INCOMPLETE",
@@ -1,122 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Qoder plugin hook entry: locate the project runtime and forward the payload.
3
-
4
- Installed into the Qoder plugin cache, so this file is never next to the
5
- project it serves. The project is therefore resolved from the hook payload
6
- (`cwd`) rather than from a module-relative path, and the renderers stay in the
7
- project's own `.cowork-flow/scripts` — one copy of the injection logic per
8
- repository, never a stale second copy in the plugin cache.
9
-
10
- Fail-open by design: outside a cowork-flow project, or when the project's own
11
- runtime fails, this exits 0 with no injection. The hook is machine-level, so it
12
- meets project runtimes older than itself; Qoder refuses the prompt on a
13
- non-zero hook exit, and a stale project copy must never do that.
14
- """
15
-
16
- from __future__ import annotations
17
-
18
- import json
19
- import os
20
- import subprocess
21
- import sys
22
- from pathlib import Path
23
-
24
- HOST = "qoder"
25
- WORKFLOW_DIR = ".cowork-flow"
26
- INJECT_RELPATH = ("scripts", "adapters", "host", "inject.py")
27
-
28
-
29
- def _payload_cwd(raw: bytes) -> str | None:
30
- if not raw.strip():
31
- return None
32
- try:
33
- payload = json.loads(raw.decode("utf-8-sig"))
34
- except (UnicodeDecodeError, ValueError):
35
- return None
36
- if isinstance(payload, dict):
37
- cwd = payload.get("cwd")
38
- if isinstance(cwd, str) and cwd.strip():
39
- return cwd
40
- return None
41
-
42
-
43
- def find_workflow_root(start: str | None) -> Path | None:
44
- if not start:
45
- return None
46
- try:
47
- current = Path(start).resolve()
48
- except OSError:
49
- return None
50
- while True:
51
- if (current / WORKFLOW_DIR).is_dir():
52
- return current
53
- parent = current.parent
54
- if parent == current:
55
- return None
56
- current = parent
57
-
58
-
59
- def resolve_root(raw: bytes) -> Path | None:
60
- candidates = [
61
- _payload_cwd(raw),
62
- os.environ.get("QODER_PROJECT_DIR"),
63
- os.environ.get("QODER_WORKING_DIR"),
64
- os.getcwd(),
65
- ]
66
- for candidate in candidates:
67
- root = find_workflow_root(candidate)
68
- if root is not None:
69
- return root
70
- return None
71
-
72
-
73
- def resolve_inject(root: Path) -> Path | None:
74
- inject = root.joinpath(WORKFLOW_DIR, *INJECT_RELPATH)
75
- return inject if inject.is_file() else None
76
-
77
-
78
- def main() -> int:
79
- if (
80
- os.environ.get("COWORK_FLOW_HOOKS") == "0"
81
- or os.environ.get("COWORK_FLOW_DISABLE_HOOKS") == "1"
82
- ):
83
- return 0
84
-
85
- raw = sys.stdin.buffer.read()
86
- root = resolve_root(raw)
87
- if root is None:
88
- return 0
89
- inject = resolve_inject(root)
90
- if inject is None:
91
- return 0
92
-
93
- # The hook is already running under a Python 3 interpreter, so the only
94
- # reason to override it is an explicit COWORK_FLOW_PYTHON; sys.executable
95
- # keeps the hook working where `python` is not on PATH at all.
96
- interpreter = (os.environ.get("COWORK_FLOW_PYTHON") or "").strip() or sys.executable
97
- try:
98
- completed = subprocess.run(
99
- [interpreter, str(inject), "--host", HOST],
100
- input=raw,
101
- cwd=str(root),
102
- timeout=25,
103
- )
104
- except OSError as error:
105
- print(f"cowork-flow qoder hook: {error}", file=sys.stderr)
106
- return 0
107
- except subprocess.TimeoutExpired:
108
- print("cowork-flow qoder hook: inject timed out", file=sys.stderr)
109
- return 0
110
- if completed.returncode != 0:
111
- # Never propagate: Qoder counts a non-zero exit on SessionStart and
112
- # UserPromptSubmit as a blocking hook failure. Keep the reason in the
113
- # run log and leave the event alone.
114
- print(
115
- f"cowork-flow qoder hook: inject.py exited {completed.returncode}; skipping injection",
116
- file=sys.stderr,
117
- )
118
- return 0
119
-
120
-
121
- if __name__ == "__main__":
122
- raise SystemExit(main())