cowork-flow 2.0.0 → 2.1.0

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/README.en.md +1 -1
  3. package/README.md +1 -1
  4. package/docs/direction.md +3 -2
  5. package/docs/hosts.md +1 -1
  6. package/docs/index.md +1 -1
  7. package/docs/release.md +1 -1
  8. package/package.json +6 -6
  9. package/presets/claude-code/.claude-plugin/plugin.json +1 -1
  10. package/presets/codex/.codex-plugin/plugin.json +1 -1
  11. package/presets/dsh/plugins/workflow-state.js +42 -9
  12. package/presets/kimi-code/.kimi-plugin/plugin.json +1 -1
  13. package/presets/kimi-code/hooks/cowork-flow-inject.mjs +28 -2
  14. package/presets/opencode/cowork-flow/plugin-core.js +28 -1
  15. package/presets/qoder/.qoder-plugin/plugin.json +1 -1
  16. package/presets/qoder/hooks/inject-context.mjs +28 -2
  17. package/presets/zcode/.zcode-plugin/plugin.json +1 -1
  18. package/presets/zcode/hooks/inject-context.mjs +28 -3
  19. package/src/commands/install-dsh-hook.js +1 -6
  20. package/src/commands/install-dsh-preset.js +1 -6
  21. package/src/commands/install-zcode-plugin.js +2 -3
  22. package/src/lib/asset-plan.js +1 -1
  23. package/src/lib/dsh-home.js +6 -0
  24. package/src/lib/plan-applier.js +1 -6
  25. package/template/.claude/agents/cowork-check.md +1 -1
  26. package/template/.claude/agents/cowork-implement.md +1 -1
  27. package/template/.claude/agents/cowork-research.md +1 -1
  28. package/template/.codex/agents/cowork-check.toml +1 -1
  29. package/template/.codex/agents/cowork-implement.toml +1 -1
  30. package/template/.codex/agents/cowork-research.toml +1 -1
  31. package/template/.codex/agents/default.toml +1 -1
  32. package/template/.codex/agents/explorer.toml +1 -1
  33. package/template/.codex/agents/worker.toml +1 -1
  34. package/template/.cowork-flow/.version +1 -1
  35. package/template/.cowork-flow/run +2 -2
  36. package/template/.cowork-flow/run.cmd +3 -3
  37. package/template/.cowork-flow/spec/contracts/context-injection.md +4 -4
  38. package/template/.kimi-code/agents/cowork-check.md +1 -1
  39. package/template/.kimi-code/agents/cowork-implement.md +1 -1
  40. package/template/.kimi-code/agents/cowork-research.md +1 -1
  41. package/template/.opencode/agents/cowork-check.md +1 -1
  42. package/template/.opencode/agents/cowork-implement.md +1 -1
  43. package/template/.opencode/agents/cowork-research.md +1 -1
  44. package/template/.opencode/cowork-flow/plugin-core.js +28 -1
  45. package/template/skills/runtime-health/scripts/doctor.py +83 -9
package/CHANGELOG.md CHANGED
@@ -4,6 +4,27 @@
4
4
 
5
5
  ### Added
6
6
 
7
+ ## [2.1.0] - 2026-10-19
8
+
9
+ ### Added
10
+
11
+ - **发布前漂移门禁接入 release.sh,测试范围与超时收敛**:发布 pre-flight 在 `source:refresh`(会覆写活副本)之前强制跑 `node scripts/release-gates.js`,自实例漂移即中止并打印修复指引;新增 `pytest.ini`(`testpaths = tests`)把 `python -m pytest -q` 的收集范围钉在 `tests/`,所有 node 测试脚本统一 `--test-timeout=600000`,`test:windows:core` 补上 Windows 全量 `init/sync` 套件(此前只跑两条具名用例)。
12
+ - **发布门禁补"活副本资产漂移"verdict**:`release-gates` 新增 `self-instance-asset-drift`——以 `node bin/cowork-flow.js project sync . --dry-run` 的 readiness 为准,`wouldCopy`/`wouldRemoveObsolete` 非空即 fail 并点名文件(`wouldSkipProtected`/`wouldKeep` 只报告不阻断)。覆盖 `.claude/**`、`.codex/**`、skills 等 `dev refresh` 看不到的活副本,避免发布第 5 步 `sync --force` 静默覆写;`scripts/release.sh` 失败提示补 `node bin/cowork-flow.js project sync .` 修复指引。
13
+ - **doctor 增补 zcode 插件新鲜度检查**:`check_zcode_plugin` 以 payload 盖章清单 `.zcode-plugin/plugin.json` 的版本对比项目 `.cowork-flow/.version` → `PLUGIN-STALE`(hint `cwf host add zcode --force`);未声明 zcode 静默,声明未安装报 `PLUGIN-NOT-INSTALLED`,清单不可读报 `PLUGIN-UNKNOWN-VERSION`。与 opencode/qoder 同为 advisory,不进 error 级。
14
+
15
+ ### Changed
16
+
17
+ - **Python 解释器下限统一 3.10,五个宿主 shim 加解释器探针守卫**:`template/.cowork-flow/run`、`run.cmd` 的版本判据与提示从 3.9 升到 3.10(与运行时协议一致);`presets/{opencode,zcode,qoder,kimi-code,dsh}` 的注入 shim 逐个候选加同源版本探针(`sys.version_info >= (3, 10)`),不可用即跳过、全不可用则 stderr 一行说明并 fail-open(exit 0),探针超时视为无裁决、不跳过。**迁移**:shim 在宿主载荷内,需 `cwf host add <host> --force` 重装。
18
+ - **清理 spec-check 删除后的文档与宿主提示残留**:README(中英)、`docs/{index,hosts,direction}.md`、`context-injection.md` 里"规范检查/规范门禁"的过时措辞改为"同步策略与受保护文件",并订正机器级 shim 的 fail-open 描述(现为四个机器级 shim;项目级 Edit 期 hook 自 2.0.0 起同样 advisory、恒 exit 0);15 处宿主 agent 提示(`.claude`/`.codex`/`.kimi-code`/`.opencode` 的 `cowork-*` 与 codex 的 `default`/`explorer`/`worker`)把点名已删子命令的禁令改为"禁止执行任务生命周期转换"。
19
+ - **`assetDriftVerdict` 缺键改为 fail-closed**:readiness 少 `wouldCopy`/`wouldRemoveObsolete`(或给非数组)时不再静默判"无漂移",改判门禁失效并点名缺失键(`cannot judge drift`),并把逐 key 单钉、e2e fail 断言补进测试。
20
+ - **dsh 候选可用性映射抽为 `candidateUnavailable` 并恢复 Windows 覆盖**:`ENOENT`/退出码 3 → 换下一个候选;其余非零退出(含超时)→ 候选按"可用但协议失败、不注入"处理(语义不变,仅抽函数加注释);新增跨平台单测钉住四种输入,`test/dsh-hook.test.js` 两条过期的 Windows skip 移除(取证:node 探测 WindowsApps `python3` 别名得 `ENOENT`、回落到真实 `python` 3.12)。
21
+ - **重复工具函数收敛**:`sha256` 只留 `src/lib/asset-plan.js` 的导出,`src/lib/plan-applier.js` 改为复用;`getDshHome` 抽到新增的 `src/lib/dsh-home.js`,两个 dsh 安装器共用。
22
+
23
+ ### Fixed
24
+
25
+ - **dsh 完整协议 stdout 固定 UTF-8,恢复非 CJK 控制台下的工作流注入**:`<workflow-state>` 块含中文文案,Windows 默认 ANSI 代码页(CI runner 为 cp1252)下 `print()` 抛 `UnicodeEncodeError`,调用方按既有语义读作"解释器能跑但协议失败 → 不注入",dsh 会话静默丢失整块注入——上面移除 Windows skip 后两条用例在 runner 上暴露的正是这个缺陷(原 skip 注释归因于 WindowsApps `python3` 桩,实测不符)。修法对齐同文件 edit-scope 协议与 `inject.py` 的既有写法,在完整协议开头补 `sys.stdout.reconfigure(encoding="utf-8", errors="replace", newline="")`;新增回归用例在 `PYTHONIOENCODING=ascii` 下钉住该契约(红证:改动前该用例失败)。顺带把 `newline=""` 的 LF 语义带进来,块字节在 Windows 与 POSIX 上一致。
26
+ - **release 编排用例补 `scripts/release-gates.js` 桩**:`0c81b29` 给 `release.sh` 加的首道漂移门禁在 Linux 上必飞——夹具只复制 `release.sh`,`node scripts/release-gates.js` 报 `Cannot find module` 被读作漂移阻断,3 条用例红(was: 该门禁已无任何用例钉住其接线)。夹具补同款 `process.exit(0)` 桩,并在"defaults to patch"用例断言门禁先于 `source:refresh` 运行。
27
+
7
28
  ## [2.0.0] - 2026-10-16
8
29
 
9
30
  ### Added
@@ -365,7 +386,8 @@
365
386
 
366
387
  此版本未保留本地条目正文,详情见 [GitHub Release](https://github.com/fishlikewater/cowork-flow/releases/tag/v0.0.47)。
367
388
 
368
- [Unreleased]: https://github.com/fishlikewater/cowork-flow/compare/v2.0.0...HEAD
389
+ [Unreleased]: https://github.com/fishlikewater/cowork-flow/compare/v2.1.0...HEAD
390
+ [2.1.0]: https://github.com/fishlikewater/cowork-flow/compare/v2.0.0...v2.1.0
369
391
  [2.0.0]: https://github.com/fishlikewater/cowork-flow/compare/v1.9.2...v2.0.0
370
392
  [1.9.2]: https://github.com/fishlikewater/cowork-flow/compare/v1.9.1...v1.9.2
371
393
  [1.9.1]: https://github.com/fishlikewater/cowork-flow/compare/v1.9.0...v1.9.1
package/README.en.md CHANGED
@@ -169,7 +169,7 @@ The detailed documentation is written in Chinese.
169
169
  | Document | Contents |
170
170
  |---|---|
171
171
  | [Documentation index](docs/index.md) | Entry point for every document |
172
- | [Architecture and extension](docs/architecture.md) | How the project files are organized, how spec checks work |
172
+ | [Architecture and extension](docs/architecture.md) | How the project files are organized, how the sync policy and protected files work |
173
173
  | [Host integration](docs/hosts.md) | Install, restart, troubleshoot, and uninstall for each host |
174
174
  | [Release and maintenance](docs/release.md) | Tests, CI, and maintainer commands |
175
175
  | [Contributing](CONTRIBUTING.md) | Local setup, commit conventions, and PR checks |
package/README.md CHANGED
@@ -149,7 +149,7 @@ cwf project sync .
149
149
  | 文档 | 内容 |
150
150
  |---|---|
151
151
  | [文档索引](docs/index.md) | 全部文档入口 |
152
- | [架构与扩展](docs/architecture.md) | 项目文件如何组织、规范检查如何工作 |
152
+ | [架构与扩展](docs/architecture.md) | 项目文件如何组织、同步策略与受保护文件 |
153
153
  | [宿主接入](docs/hosts.md) | 各宿主的安装、重启、排障和卸载方式 |
154
154
  | [发布与维护](docs/release.md) | 测试、CI 和维护者命令 |
155
155
  | [参与开发](CONTRIBUTING.md) | 本地开发、提交和 PR 检查 |
package/docs/direction.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # cowork-flow 方向规划:Agent 运行时上下文与协作事实层
2
2
 
3
- > 状态:**已批准** · 创建:2026-08-27 · 最近更新:2026-10-13
3
+ > 状态:**已批准** · 创建:2026-08-27 · 最近更新:2026-10-19
4
4
  > 本文档是后续阶段立项的权威输入。任何方向的修订必须先更新本文档,再立项。
5
5
  > 已完成阶段的实施细节与提交记录见 [CHANGELOG](../CHANGELOG.md);本文只保留仍然生效的定位、取舍、边界和待决项。
6
6
 
@@ -46,7 +46,8 @@
46
46
  ## 5. 版本线
47
47
 
48
48
  - **1.8.x 已发布**:证据化完成(AC 事实化、`evidence.jsonl`、完成门禁)。
49
- - **1.9.0 主线已实现,待发布**:多执行者协作事实——看板归属可见、依赖边、交接凭证。详见 [CHANGELOG](../CHANGELOG.md) 的 `[Unreleased]` 段。
49
+ - **1.9.x 已发布**:多执行者协作事实——看板归属可见、依赖边、交接凭证。
50
+ - **2.0.0 已发布**:spec-check 机制破坏性删除、编辑期越界警告覆盖各宿主、注入容量放宽(Verify 10 条 / 块预算 3400)。详见 [CHANGELOG](../CHANGELOG.md) 的 `[2.0.0]` 段。
50
51
  - 各版本的交付明细一律以 CHANGELOG 为准,本文不重复。
51
52
 
52
53
  ## 6. 待决项与缓行
package/docs/hosts.md CHANGED
@@ -51,7 +51,7 @@ cwf host remove <host>
51
51
 
52
52
  `--force` 只在确认需要覆盖已有安装时使用。安装器遇到不属于自己的文件时默认拒绝覆盖或删除;`--dry-run` 会执行同样的归属检查。
53
53
 
54
- 机器级 hook 与项目运行时各自升级,而它会在任何工作区触发——包括运行时比它旧的项目。三个机器级注入 shim(Qoder、ZCode、Kimi Code)因此是 **fail-open** 的:项目运行时失败时只在宿主运行日志留原因并返回 0,不注入,也不把「hook 失败」变成「拒绝你的输入」;保留非零退出的只有项目级 Edit 期规范门禁(Claude Code / Codex),它与所调用的运行时同源。旧项目上注入会静默不生效,跑一次 `cwf project sync .` 即可恢复。
54
+ 机器级 hook 与项目运行时各自升级,而它会在任何工作区触发——包括运行时比它旧的项目。四个机器级注入 shim(Qoder、ZCode、Kimi Code、OpenCode)因此是 **fail-open** 的:项目运行时失败时只在宿主运行日志留原因并返回 0,不注入,也不把「hook 失败」变成「拒绝你的输入」;项目级 Edit 期 hook(Claude Code / Codex)自 2.0.0 起同样不再返回非零退出,一律 advisory。旧项目上注入会静默不生效,跑一次 `cwf project sync .` 即可恢复。
55
55
 
56
56
  ## Codex
57
57
 
package/docs/index.md CHANGED
@@ -8,7 +8,7 @@
8
8
  |---|---|---|
9
9
  | 安装、初始化项目或开始一个任务 | [README](../README.md) | 最短使用路径、日常命令和常见问题 |
10
10
  | 把 cowork-flow 接到某个 AI 编码宿主 | [宿主接入](hosts.md) | 各宿主的安装组件、重启要求、升级、卸载和排障方式 |
11
- | 了解项目文件如何组织 | [架构与扩展](architecture.md) | 运行时、`template/`、`presets/`、规范检查和扩展边界 |
11
+ | 了解项目文件如何组织 | [架构与扩展](architecture.md) | 运行时、`template/`、`presets/`、同步策略和扩展边界 |
12
12
  | 参与项目开发 | [参与开发](../CONTRIBUTING.md) | 本地环境、代码约定、测试和 PR 检查 |
13
13
  | 了解测试与发布流程 | [发布与维护](release.md) | 测试分层、CI、版本发布和维护者命令 |
14
14
  | 了解项目方向与版本规划 | [方向规划](direction.md) | 定位声明、已拍板取舍、版本主线与待决项 |
package/docs/release.md CHANGED
@@ -17,7 +17,7 @@
17
17
  | `npm run test:windows:core` | 涉及 Windows、路径、换行或发布行为 |
18
18
  | `python -m pytest -q` | 改动 `template/.cowork-flow/`、`tests/` 或 Python 契约 |
19
19
  | `npm run test:all` | 准备发布时的完整 Node、模板和打包检查 |
20
- | `npm run release:check` | 发布信心门禁:`test:all` 加上测试表达不了的检查——自实例相对 `template/` 的漂移,以及 `doctor --all` 的 error 级问题 |
20
+ | `npm run release:check` | 发布信心门禁:`test:all` 加上测试表达不了的检查——自实例与活副本资产(`.claude/**`、`.codex/**`、skills)相对 `template/` 的漂移,以及 `doctor --all` 的 error 级问题 |
21
21
 
22
22
  平台前提不满足时,测试可能明确跳过。跳过项必须原样记录,不能当作通过。
23
23
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "CLI for installing and maintaining cowork-flow collaboration templates.",
5
5
  "keywords": [
6
6
  "ai-agent",
@@ -48,10 +48,10 @@
48
48
  ],
49
49
  "scripts": {
50
50
  "test": "npm run test:fast",
51
- "test:smoke": "node --test test/smoke.test.js test/tarball-install.test.js",
52
- "test:fast": "node --test test/asset-plan.test.js test/cli.test.js test/cli-registry.test.js test/claude-code-plugin.test.js test/codex-plugin.test.js test/compliance-e2e.test.js test/contract-fingerprint.test.js test/copy-template.obsolete-safety.test.js test/docs.test.js test/host-assets.test.js test/host-command.test.js test/kimi-hook.test.js test/kimi-installed-registry.test.js test/kimi-plugin.test.js test/opencode-plugin-installer.test.js test/opencode-plugin.test.js test/package.test.js test/plugin-metadata.test.js test/plugin-payload.test.js test/qoder-plugin.test.js test/smoke.test.js test/stage-contract.test.js test/template-runner.test.js test/update.test.js test/zcode-plugin.test.js",
53
- "test:integration": "node --test --test-name-pattern=\"^(init copies the template into a new target directory|sync updates safe template files and preserves protected files)$\" test/init.test.js test/sync.test.js",
54
- "test:node:full": "node --test",
51
+ "test:smoke": "node --test --test-timeout=600000 test/smoke.test.js test/tarball-install.test.js",
52
+ "test:fast": "node --test --test-timeout=600000 test/asset-plan.test.js test/cli.test.js test/cli-registry.test.js test/claude-code-plugin.test.js test/codex-plugin.test.js test/compliance-e2e.test.js test/contract-fingerprint.test.js test/copy-template.obsolete-safety.test.js test/docs.test.js test/host-assets.test.js test/host-command.test.js test/kimi-hook.test.js test/kimi-installed-registry.test.js test/kimi-plugin.test.js test/opencode-plugin-installer.test.js test/opencode-plugin.test.js test/package.test.js test/plugin-metadata.test.js test/plugin-payload.test.js test/qoder-plugin.test.js test/smoke.test.js test/stage-contract.test.js test/template-runner.test.js test/update.test.js test/zcode-plugin.test.js",
53
+ "test:integration": "node --test --test-timeout=600000 --test-name-pattern=\"^(init copies the template into a new target directory|sync updates safe template files and preserves protected files)$\" test/init.test.js test/sync.test.js",
54
+ "test:node:full": "node --test --test-timeout=600000",
55
55
  "test:template": "node scripts/run-template-tests.js --suite core",
56
56
  "test:template:full": "node scripts/run-template-tests.js --suite full",
57
57
  "pack:check": "node scripts/pack-check.js",
@@ -61,7 +61,7 @@
61
61
  "test:all": "npm run test:node:full && npm run test:template:full && npm run pack:check",
62
62
  "release:check": "npm run test:all && node scripts/release-gates.js",
63
63
  "release": "sh scripts/release.sh",
64
- "test:windows:core": "npm run test:fast && npm run test:integration && node --test test/dsh-home-patch.test.js test/dsh-hook.test.js test/dsh-preset.test.js test/dsh-preset-bundle.test.js test/mcp-client-matrix.test.js test/mcp-state-command.test.js test/tarball-install.test.js && npm run pack:check && npm run test:template"
64
+ "test:windows:core": "npm run test:fast && npm run test:integration && node --test --test-timeout=600000 test/dsh-home-patch.test.js test/dsh-hook.test.js test/dsh-preset.test.js test/dsh-preset-bundle.test.js test/mcp-client-matrix.test.js test/mcp-state-command.test.js test/tarball-install.test.js && node --test --test-timeout=600000 test/init.test.js test/sync.test.js && npm run pack:check && npm run test:template"
65
65
  },
66
66
  "engines": {
67
67
  "node": ">=20"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
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": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "AI 协作工作流引擎:任务生命周期、子代理调度、门禁与状态注入。将 cowork-flow 的完整工作流带入 Codex。",
5
5
  "author": {
6
6
  "name": "fishlikewater",
@@ -52,14 +52,28 @@ const NO_ROOT_MARKER = '__COWORK_FLOW_NO_ROOT__';
52
52
  const NO_ROOT_TTL_MS = 30_000;
53
53
  const noRootCache = new Map();
54
54
 
55
- // Python 3.9+ protocol reused from the host hook adapters.
55
+ // Python 3.10+ protocol reused from the host hook adapters.
56
56
  // It resolves the `.cowork-flow` root from the given cwd,
57
57
  // loads the shared workflow_state_hook module, and prints
58
- // the full hook context block.
58
+ // the full hook context block. The version guard runs first
59
+ // and exits 3 so the caller can tell "interpreter below the
60
+ // floor" (try the next candidate) apart from "the runtime
61
+ // failed" (contribute nothing).
59
62
  const PYTHON_PROTOCOL = `\
60
63
  import sys
64
+
65
+ if sys.version_info < (3, 10):
66
+ raise SystemExit(3)
67
+
61
68
  from pathlib import Path
62
69
 
70
+ # The block carries CJK copy, so stdout must be UTF-8 whatever the console
71
+ # codepage says: on a cp1252 console (the CI runner's default) print()
72
+ # raises, and the caller reads the traceback as "interpreter ran, protocol
73
+ # failed" and injects nothing. newline="" keeps the payload bytes identical
74
+ # across platforms, like the edit-scope protocol below.
75
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace", newline="")
76
+
63
77
  cwd = sys.argv[1]
64
78
  full_digest = len(sys.argv) > 2 and sys.argv[2] != "0"
65
79
  current = Path(cwd).resolve()
@@ -105,9 +119,15 @@ const EDIT_EXEC_OPTIONS = {
105
119
  // Editor-phase scope protocol: same root discovery, then the shared
106
120
  // edit-scope entry the other hosts reach through their PostToolUse
107
121
  // hook, so DSH sessions get the identical single-line advisory. Prints
108
- // at most one line; exits 0 either way.
122
+ // at most one line; exits 0 either way. The version guard exits 3 (an
123
+ // interpreter below the floor, not a runtime verdict), which the
124
+ // caller treats like a missing binary: try the next candidate.
109
125
  const PYTHON_EDIT_SCOPE_PROTOCOL = `\
110
126
  import sys
127
+
128
+ if sys.version_info < (3, 10):
129
+ raise SystemExit(3)
130
+
111
131
  from pathlib import Path
112
132
 
113
133
  # The advisory line starts with a character outside the Windows ANSI
@@ -190,6 +210,16 @@ function discoverCandidates() {
190
210
  }
191
211
 
192
212
 
213
+ // ENOENT = the interpreter does not exist, exit code 3 = it is older than
214
+ // the protocol's floor; both mean "try the next candidate". Any other
215
+ // failure — a different non-zero exit or a timeout — means the interpreter
216
+ // ran but the protocol failed: the candidate works, the runtime does not,
217
+ // so it is not skipped. `attempt`'s edit-scope variant shares this mapping.
218
+ export function candidateUnavailable(error) {
219
+ return error.code === 'ENOENT' || error.code === 3;
220
+ }
221
+
222
+
193
223
  function tryPython(candidate, cwd, full = true) {
194
224
  return new Promise((resolve) => {
195
225
  execFile(
@@ -198,11 +228,7 @@ function tryPython(candidate, cwd, full = true) {
198
228
  EXEC_OPTIONS,
199
229
  (error, stdout) => {
200
230
  if (error) {
201
- // ENOENT = the interpreter does not exist (try
202
- // the next one). Any other failure means the
203
- // interpreter ran but the protocol failed — the
204
- // candidate works, the runtime does not.
205
- resolve({ missing: error.code === 'ENOENT', output: '' });
231
+ resolve({ missing: candidateUnavailable(error), output: '' });
206
232
  return;
207
233
  }
208
234
  resolve({ missing: false, output: stdout.trim() });
@@ -296,6 +322,13 @@ export async function runWorkflowState(cwd, full = true) {
296
322
  workingPython = candidate;
297
323
  return normalize(result.output);
298
324
  }
325
+ // Nothing in the candidate list could run the protocol: contribute
326
+ // nothing, but say why once — a Python below the floor (or none at
327
+ // all) is a misconfiguration the session owner can fix, and total
328
+ // silence hides it.
329
+ process.stderr.write(
330
+ 'cowork-flow dsh hook: no Python 3.10+ interpreter found; skipping workflow-state injection\n'
331
+ );
299
332
  return '';
300
333
  }
301
334
 
@@ -340,7 +373,7 @@ export async function runEditScopeCheck(cwd, filePath) {
340
373
  EDIT_EXEC_OPTIONS,
341
374
  (error, stdout) => {
342
375
  if (error) {
343
- resolvePromise({ missing: error.code === 'ENOENT', output: '' });
376
+ resolvePromise({ missing: candidateUnavailable(error), output: '' });
344
377
  return;
345
378
  }
346
379
  resolvePromise({ missing: false, output: stdout.trim() });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "AI 协作工作流引擎:任务生命周期、子代理调度、门禁与状态注入。将 cowork-flow 的完整工作流带入 Kimi Code。",
5
5
  "author": {
6
6
  "name": "fishlikewater",
@@ -34,6 +34,27 @@ const INJECT_RELATIVE = join(
34
34
  );
35
35
  const HOST = "kimi-code";
36
36
  const SPAWN_TIMEOUT_MS = 20000; // config.toml grants the hook 30s
37
+ // The .cowork-flow/run floor, reused here: a candidate that cannot run
38
+ // Python 3.10+ is skipped like a missing binary, so a stale `python3`
39
+ // never hides a working `python` further down the list.
40
+ const MIN_PYTHON_CHECK = "import sys; raise SystemExit(0 if sys.version_info >= (3, 10) else 1)";
41
+
42
+ function interpreterIsUsable(candidate) {
43
+ try {
44
+ const probe = spawnSync(
45
+ candidate[0],
46
+ [...candidate.slice(1), "-c", MIN_PYTHON_CHECK],
47
+ { timeout: SPAWN_TIMEOUT_MS, windowsHide: true }
48
+ );
49
+ // A probe killed by its own timeout reached no verdict — a loaded machine,
50
+ // not a bad interpreter — so let the real spawn decide; it carries the
51
+ // same timeout and its own diagnostics.
52
+ if (probe.error && probe.error.code === "ETIMEDOUT") return true;
53
+ return !probe.error && probe.status === 0;
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
37
58
 
38
59
  function readHookInput() {
39
60
  if (process.stdin.isTTY) return { raw: "", parsed: {} };
@@ -89,6 +110,7 @@ function main() {
89
110
 
90
111
  const injectScript = join(root, INJECT_RELATIVE);
91
112
  for (const candidate of pythonCandidates()) {
113
+ if (!interpreterIsUsable(candidate)) continue;
92
114
  let result;
93
115
  try {
94
116
  result = spawnSync(
@@ -117,8 +139,12 @@ function main() {
117
139
  }
118
140
  process.exit(0);
119
141
  }
120
- // No interpreter could run the entry: stay silent rather than break the
121
- // user's submit (fail-open, same contract as the other host shims).
142
+ // No candidate could run the entry: stay fail-open, but say why once
143
+ // instead of hiding a misconfiguration (a Python below the floor, or
144
+ // none at all).
145
+ process.stderr.write(
146
+ "cowork-flow kimi-code hook: no Python 3.10+ interpreter found; skipping injection\n"
147
+ );
122
148
  process.exit(0);
123
149
  }
124
150
 
@@ -16,7 +16,7 @@
16
16
  // install, so the adapter's relative import resolves in
17
17
  // both.
18
18
 
19
- import { spawn } from "node:child_process"
19
+ import { spawn, spawnSync } from "node:child_process"
20
20
  import { existsSync } from "node:fs"
21
21
  import { dirname, resolve } from "node:path"
22
22
  import { fileURLToPath } from "node:url"
@@ -24,6 +24,27 @@ import { fileURLToPath } from "node:url"
24
24
  const DIR_WORKFLOW = ".cowork-flow"
25
25
  // One prompt assembly must not wait longer than a host hook event.
26
26
  const INJECT_TIMEOUT_MS = 4800
27
+ // The .cowork-flow/run floor, reused here: a candidate that cannot run
28
+ // Python 3.10+ is skipped like a missing binary, so a stale `python3`
29
+ // never hides a working `python` further down the list.
30
+ const MIN_PYTHON_CHECK =
31
+ "import sys; raise SystemExit(0 if sys.version_info >= (3, 10) else 1)"
32
+
33
+ function interpreterIsUsable(candidate) {
34
+ try {
35
+ const probe = spawnSync(candidate[0], [...candidate.slice(1), "-c", MIN_PYTHON_CHECK], {
36
+ timeout: INJECT_TIMEOUT_MS,
37
+ windowsHide: true,
38
+ })
39
+ // A probe killed by its own timeout reached no verdict — a loaded machine,
40
+ // not a bad interpreter — so let the real spawn decide; it carries the
41
+ // same timeout and its own diagnostics.
42
+ if (probe.error && probe.error.code === "ETIMEDOUT") return true
43
+ return !probe.error && probe.status === 0
44
+ } catch {
45
+ return false
46
+ }
47
+ }
27
48
 
28
49
  // Last-resort repository root: this module lives at
29
50
  // <payload>/cowork-flow/plugin-core.js, next to plugins/,
@@ -197,12 +218,18 @@ async function renderViaInject(input, overrides = null) {
197
218
  cwd: inputCwd(input) || (root === pluginRoot ? null : root) || process.cwd(),
198
219
  }
199
220
  for (const candidate of pythonCandidates()) {
221
+ if (!interpreterIsUsable(candidate)) {
222
+ continue
223
+ }
200
224
  const result = await runInject(candidate, script, payload)
201
225
  if (!result.ran) {
202
226
  continue
203
227
  }
204
228
  return result.stdout.trim()
205
229
  }
230
+ console.error(
231
+ "cowork-flow opencode plugin: no Python 3.10+ interpreter found; skipping injection"
232
+ )
206
233
  return ""
207
234
  }
208
235
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "AI 协作工作流引擎:任务生命周期、子代理调度、门禁与状态注入。将 cowork-flow 的完整工作流带入 Qoder。",
5
5
  "displayName": "Cowork Flow",
6
6
  "author": {
@@ -33,6 +33,27 @@ const HOST = "qoder";
33
33
  const DIR_WORKFLOW = ".cowork-flow";
34
34
  // hooks.json grants 30s per event.
35
35
  const SPAWN_TIMEOUT_MS = 25000;
36
+ // The .cowork-flow/run floor, reused here: a candidate that cannot run
37
+ // Python 3.10+ is skipped like a missing binary, so a stale `python3`
38
+ // never hides a working `python` further down the list.
39
+ const MIN_PYTHON_CHECK = "import sys; raise SystemExit(0 if sys.version_info >= (3, 10) else 1)";
40
+
41
+ function interpreterIsUsable(candidate) {
42
+ try {
43
+ const probe = spawnSync(
44
+ candidate[0],
45
+ [...candidate.slice(1), "-c", MIN_PYTHON_CHECK],
46
+ { timeout: SPAWN_TIMEOUT_MS, windowsHide: true }
47
+ );
48
+ // A probe killed by its own timeout reached no verdict — a loaded machine,
49
+ // not a bad interpreter — so let the real spawn decide; it carries the
50
+ // same timeout and its own diagnostics.
51
+ if (probe.error && probe.error.code === "ETIMEDOUT") return true;
52
+ return !probe.error && probe.status === 0;
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
36
57
 
37
58
  function readHookInput() {
38
59
  if (process.stdin.isTTY) return { raw: "", parsed: {} };
@@ -113,6 +134,7 @@ function main() {
113
134
  if (!existsSync(injectScript)) process.exit(0);
114
135
 
115
136
  for (const candidate of pythonCandidates()) {
137
+ if (!interpreterIsUsable(candidate)) continue;
116
138
  let result;
117
139
  try {
118
140
  result = spawnSync(
@@ -142,8 +164,12 @@ function main() {
142
164
  }
143
165
  process.exit(0);
144
166
  }
145
- // No interpreter could run the entry: stay silent rather
146
- // than break the host's event stream (fail-open).
167
+ // No candidate could run the entry: stay fail-open, but say why once
168
+ // instead of hiding a misconfiguration (a Python below the floor, or
169
+ // none at all).
170
+ process.stderr.write(
171
+ "cowork-flow qoder hook: no Python 3.10+ interpreter found; skipping injection\n"
172
+ );
147
173
  process.exit(0);
148
174
  }
149
175
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "AI 协作工作流引擎:任务生命周期、子代理调度、门禁与状态注入。将 cowork-flow 的完整工作流带入 ZCode。",
5
5
  "author": {
6
6
  "name": "fishlikewater",
@@ -35,6 +35,27 @@ const DIR_WORKFLOW = ".cowork-flow";
35
35
  const LIFECYCLE_BASH_RE = /\brun(?:\.cmd)?\s+(?:task|subagent|resume)\b/;
36
36
  // hooks.json grants 5s per event.
37
37
  const SPAWN_TIMEOUT_MS = 4800;
38
+ // The .cowork-flow/run floor, reused here: a candidate that cannot run
39
+ // Python 3.10+ is skipped like a missing binary, so a stale `python3`
40
+ // never hides a working `python` further down the list.
41
+ const MIN_PYTHON_CHECK = "import sys; raise SystemExit(0 if sys.version_info >= (3, 10) else 1)";
42
+
43
+ function interpreterIsUsable(candidate) {
44
+ try {
45
+ const probe = spawnSync(
46
+ candidate[0],
47
+ [...candidate.slice(1), "-c", MIN_PYTHON_CHECK],
48
+ { timeout: SPAWN_TIMEOUT_MS, windowsHide: true }
49
+ );
50
+ // A probe killed by its own timeout reached no verdict — a loaded machine,
51
+ // not a bad interpreter — so let the real spawn decide; it carries the
52
+ // same timeout and its own diagnostics.
53
+ if (probe.error && probe.error.code === "ETIMEDOUT") return true;
54
+ return !probe.error && probe.status === 0;
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
38
59
 
39
60
  function readHookInput() {
40
61
  if (process.stdin.isTTY) return { raw: "", parsed: {} };
@@ -152,6 +173,7 @@ function main() {
152
173
  if (!injectScript) process.exit(0);
153
174
 
154
175
  for (const candidate of pythonCandidates()) {
176
+ if (!interpreterIsUsable(candidate)) continue;
155
177
  let result;
156
178
  try {
157
179
  result = spawnSync(
@@ -180,9 +202,12 @@ function main() {
180
202
  }
181
203
  process.exit(0);
182
204
  }
183
- // No interpreter could run the entry: stay silent rather
184
- // than break the host's event stream (fail-open, like
185
- // the spec-edit path).
205
+ // No candidate could run the entry: stay fail-open, but say why once
206
+ // instead of hiding a misconfiguration (a Python below the floor, or
207
+ // none at all).
208
+ process.stderr.write(
209
+ "cowork-flow zcode hook: no Python 3.10+ interpreter found; skipping injection\n"
210
+ );
186
211
  process.exit(0);
187
212
  }
188
213
 
@@ -1,7 +1,7 @@
1
1
  import { copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
2
  import { dirname, join } from 'node:path';
3
- import { homedir } from 'node:os';
4
3
 
4
+ import { getDshHome } from '../lib/dsh-home.js';
5
5
  import { parseInstallArgs, pathExists } from '../lib/install-support.js';
6
6
  import { packageRoot } from '../lib/paths.js';
7
7
  import { removeManagedBlock, upsertManagedBlock } from '../lib/payload-install.js';
@@ -19,11 +19,6 @@ const MANAGED_MARK = '# cowork-flow: managed workflow-state-hook row. Run "cowor
19
19
  export const FLAGS = ['--dry-run', '--force', '--uninstall'];
20
20
 
21
21
 
22
- function getDshHome() {
23
- return process.env.DSH_HOME || join(homedir(), '.dsh');
24
- }
25
-
26
-
27
22
  async function readIfExists(file) {
28
23
  try {
29
24
  return await readFile(file, 'utf8');
@@ -1,8 +1,8 @@
1
1
  import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
- import { homedir } from 'node:os';
4
3
  import { pathToFileURL } from 'node:url';
5
4
 
5
+ import { getDshHome } from '../lib/dsh-home.js';
6
6
  import { parseInstallArgs, pathExists, readJsonFile, writeJsonAtomic } from '../lib/install-support.js';
7
7
  import { packageRoot } from '../lib/paths.js';
8
8
  import { readPackageInfo } from '../lib/package-info.js';
@@ -34,11 +34,6 @@ const DESKTOP_PERSONA_KEY = /(- id: persona\n(?:.*\n)*? {4})text: /;
34
34
  export const FLAGS = ['--dry-run', '--force', '--uninstall'];
35
35
 
36
36
 
37
- function getDshHome() {
38
- return process.env.DSH_HOME || join(homedir(), '.dsh');
39
- }
40
-
41
-
42
37
  function getDshPresetRoot() {
43
38
  return join(getDshHome(), '.agent-presets');
44
39
  }
@@ -250,9 +250,8 @@ async function materializePlugin({ pluginSrc, destDir, cacheRoot, manifest, vers
250
250
  // folder; keep workflow runtime files out of scaffold.
251
251
  await rm(join(destDir, "scaffold", ".cowork-flow"), { recursive: true, force: true });
252
252
 
253
- // The presets/zcode/hooks/runtime/scripts copy is stale;
254
- // overwrite it with the authoritative version from the
255
- // template.
253
+ // The preset ships no runtime payload, so materialize
254
+ // the authoritative workflow scripts from the template.
256
255
  const mainScriptsSrc = join(templateRoot, ".cowork-flow", "scripts");
257
256
  const pluginScriptsDest = join(destDir, "hooks", "runtime", "scripts");
258
257
  if (await pathExists(mainScriptsSrc)) {
@@ -5,7 +5,7 @@ import { resolve } from 'node:path';
5
5
  const VERSION_PATH = '.cowork-flow/.version';
6
6
  const MATERIALIZED_ACTIONS = new Set(['create', 'update']);
7
7
 
8
- function sha256(content) {
8
+ export function sha256(content) {
9
9
  return createHash('sha256').update(content).digest('hex');
10
10
  }
11
11
 
@@ -0,0 +1,6 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+
4
+ export function getDshHome() {
5
+ return process.env.DSH_HOME || join(homedir(), '.dsh');
6
+ }
@@ -1,8 +1,7 @@
1
- import { createHash } from 'node:crypto';
2
1
  import * as defaultFileSystem from 'node:fs/promises';
3
2
  import { basename, dirname, join, resolve, sep } from 'node:path';
4
3
 
5
- import { planActions } from './asset-plan.js';
4
+ import { planActions, sha256 } from './asset-plan.js';
6
5
 
7
6
  const MATERIALIZED_ACTIONS = new Set(['create', 'update']);
8
7
  const VERSION_PATH = '.cowork-flow/.version';
@@ -15,10 +14,6 @@ const TRANSACTION_STATUSES = new Set([
15
14
  'committed'
16
15
  ]);
17
16
 
18
- function sha256(content) {
19
- return createHash('sha256').update(content).digest('hex');
20
- }
21
-
22
17
  async function pathExists(fileSystem, path) {
23
18
  try {
24
19
  await fileSystem.access(path);
@@ -23,6 +23,6 @@ Execution:
23
23
  Rules:
24
24
 
25
25
  - Do not use the Task tool or invoke subagents.
26
- - Do not run task start, finish, archive, unscoped resume, commit, or push.
26
+ - Do not run task lifecycle transitions, unscoped resume, commit, or push.
27
27
  - Treat backend/frontend natural-language specs as checklist context, not dynamic hard validators.
28
28
  - Do not accept shallow tests that would still pass when target behavior breaks.
@@ -24,6 +24,6 @@ Execution:
24
24
  Rules:
25
25
 
26
26
  - Do not use the Task tool or invoke subagents.
27
- - Do not run task start, finish, archive, unscoped resume, commit, or push.
27
+ - Do not run task lifecycle transitions, unscoped resume, commit, or push.
28
28
  - Do not create task-local review artifacts, `tdd.jsonl`, or TDD evidence records in `check.jsonl`.
29
29
  - Do not claim completion without current verification.
@@ -19,4 +19,4 @@ Rules:
19
19
 
20
20
  - Do not use the Task tool or invoke subagents.
21
21
  - Do not edit code, specs, task state, or git state.
22
- - Do not run task start, finish, archive, unscoped resume, commit, or push.
22
+ - Do not run task lifecycle transitions, unscoped resume, commit, or push.
@@ -19,7 +19,7 @@ Execution:
19
19
 
20
20
  Rules:
21
21
  - MUST NOT spawn, wait for, list, or close other agents.
22
- - MUST NOT run task start, finish, archive, unscoped resume, commit, or push.
22
+ - MUST NOT run task lifecycle transitions, unscoped resume, commit, or push.
23
23
  - Treat backend/frontend natural-language specs as checklist context, not dynamic hard validators.
24
24
  - Do not accept shallow tests that would still pass when target behavior breaks.
25
25
  """
@@ -20,7 +20,7 @@ Execution:
20
20
 
21
21
  Rules:
22
22
  - MUST NOT spawn, wait for, list, or close other agents.
23
- - MUST NOT run task start, finish, archive, unscoped resume, commit, or push.
23
+ - MUST NOT run task lifecycle transitions, unscoped resume, commit, or push.
24
24
  - Do not create task-local review artifacts, `tdd.jsonl`, or TDD evidence records in `check.jsonl`.
25
25
  - Do not claim completion without current verification.
26
26
  """
@@ -16,7 +16,7 @@ Execution:
16
16
  Rules:
17
17
  - MUST NOT spawn, wait for, list, or close other agents.
18
18
  - MUST NOT edit code, specs, task state, or git state.
19
- - MUST NOT run task start, finish, archive, unscoped resume, commit, or push.
19
+ - MUST NOT run task lifecycle transitions, unscoped resume, commit, or push.
20
20
  """
21
21
 
22
22
  [features]
@@ -15,7 +15,7 @@ constraints only; they are not the task.
15
15
 
16
16
  Rules:
17
17
  - MUST NOT spawn, wait for, list, or close other agents.
18
- - MUST NOT run task start, task finish, task archive, or unscoped resume.
18
+ - MUST NOT run task lifecycle transitions or unscoped resume.
19
19
  - MUST NOT commit or push.
20
20
  - Keep edits inside requested scope.
21
21
  - Report changed files and exact verification commands.
@@ -16,7 +16,7 @@ constraints only; they are not the task.
16
16
  Rules:
17
17
  - MUST NOT edit files unless the prompt explicitly allows it.
18
18
  - MUST NOT spawn, wait for, list, or close other agents.
19
- - MUST NOT run task start, task finish, task archive, or unscoped resume.
19
+ - MUST NOT run task lifecycle transitions or unscoped resume.
20
20
  - MUST NOT commit or push.
21
21
  - Prefer rg for file and text search.
22
22
  - Return concise findings with file and line evidence.
@@ -16,7 +16,7 @@ constraints only; they are not the task.
16
16
 
17
17
  Rules:
18
18
  - MUST NOT spawn, wait for, list, or close other agents.
19
- - MUST NOT run task start, task finish, task archive, or unscoped resume.
19
+ - MUST NOT run task lifecycle transitions or unscoped resume.
20
20
  - MUST NOT commit or push.
21
21
  - Keep edits inside requested scope.
22
22
  - Report changed files and exact verification commands.
@@ -1 +1 @@
1
- 2.0.0
1
+ 2.1.0
@@ -1,8 +1,8 @@
1
1
  #!/bin/sh
2
2
  set -u
3
3
 
4
- MIN_PYTHON_LABEL="Python 3.9+"
5
- VERSION_CHECK='import sys; raise SystemExit(0 if sys.version_info >= (3, 9) else 1)'
4
+ MIN_PYTHON_LABEL="Python 3.10+"
5
+ VERSION_CHECK='import sys; raise SystemExit(0 if sys.version_info >= (3, 10) else 1)'
6
6
  WORKFLOW_DIR=$(CDPATH= cd "$(dirname "$0")" && pwd) || exit 1
7
7
  RUNNER_SCRIPT="$WORKFLOW_DIR/scripts/run.py"
8
8
 
@@ -1,8 +1,8 @@
1
1
  @echo off
2
2
  setlocal EnableExtensions
3
3
 
4
- set "MIN_PYTHON_LABEL=Python 3.9+"
5
- set "VERSION_CHECK=import sys; raise SystemExit(0 if sys.version_info >= (3, 9) else 1)"
4
+ set "MIN_PYTHON_LABEL=Python 3.10+"
5
+ set "VERSION_CHECK=import sys; raise SystemExit(0 if sys.version_info >= (3, 10) else 1)"
6
6
  set "WORKFLOW_DIR=%~dp0"
7
7
  set "RUNNER_SCRIPT=%WORKFLOW_DIR%scripts\run.py"
8
8
  set "SELECTED_PYTHON="
@@ -46,7 +46,7 @@ if not errorlevel 1 (
46
46
  )
47
47
 
48
48
  echo Error: %ENV_NAME% does not point to a usable %MIN_PYTHON_LABEL% interpreter: %ENV_VALUE% 1>&2
49
- echo Set %ENV_NAME% to an executable Python 3.9+ interpreter path. 1>&2
49
+ echo Set %ENV_NAME% to an executable Python 3.10+ interpreter path. 1>&2
50
50
  exit /b 127
51
51
 
52
52
  :select_python
@@ -49,10 +49,10 @@ ZCode, Kimi Code, OpenCode) are **fail-open**: they run in every workspace, so
49
49
  they meet project runtimes older than themselves, and a project-side failure — an
50
50
  argparse rejection of the host argument, a crash, a timeout — must not fail the
51
51
  event they only decorate. Each keeps the child's message on stderr (the host's
52
- run log) and completes the event with nothing injected. Exit-status blocking is reserved
53
- for the project-level edit gate (`spec_only_post_tool_use`, installed by
54
- `init` / `sync` for claude-code and codex): that wrapper sits next to the
55
- runtime it calls, so hook and runtime cannot skew.
52
+ run log) and completes the event with nothing injected. Exit-status blocking
53
+ was removed in 2.0.0: the project-level edit hooks (`init` / `sync` install
54
+ them for claude-code and codex next to the runtime they call) are advisory as
55
+ well and always exit 0.
56
56
 
57
57
  The string block inside every transport follows the same shape: an optional
58
58
  runtime preamble, the contract digest (full block or fingerprint line, see
@@ -24,6 +24,6 @@ Execution:
24
24
  Rules:
25
25
 
26
26
  - Do not invoke subagents.
27
- - Do not run task start, finish, archive, unscoped resume, commit, or push.
27
+ - Do not run task lifecycle transitions, unscoped resume, commit, or push.
28
28
  - Treat backend/frontend natural-language specs as checklist context, not dynamic hard validators.
29
29
  - Do not accept shallow tests that would still pass when target behavior breaks.
@@ -25,6 +25,6 @@ Execution:
25
25
  Rules:
26
26
 
27
27
  - Do not invoke subagents.
28
- - Do not run task start, finish, archive, unscoped resume, commit, or push.
28
+ - Do not run task lifecycle transitions, unscoped resume, commit, or push.
29
29
  - Do not create task-local review artifacts, `tdd.jsonl`, or TDD evidence records in `check.jsonl`.
30
30
  - Do not claim completion without current verification.
@@ -20,4 +20,4 @@ Rules:
20
20
 
21
21
  - Do not invoke subagents.
22
22
  - Do not edit code, specs, task state, or git state.
23
- - Do not run task start, finish, archive, unscoped resume, commit, or push.
23
+ - Do not run task lifecycle transitions, unscoped resume, commit, or push.
@@ -28,6 +28,6 @@ Execution:
28
28
  Rules:
29
29
 
30
30
  - Do not use the `task` tool or invoke subagents.
31
- - Do not run task start, finish, archive, unscoped resume, commit, or push.
31
+ - Do not run task lifecycle transitions, unscoped resume, commit, or push.
32
32
  - Treat backend/frontend natural-language specs as checklist context, not dynamic hard validators.
33
33
  - Do not accept shallow tests that would still pass when target behavior breaks.
@@ -29,6 +29,6 @@ Execution:
29
29
  Rules:
30
30
 
31
31
  - Do not use the `task` tool or invoke subagents.
32
- - Do not run task start, finish, archive, unscoped resume, commit, or push.
32
+ - Do not run task lifecycle transitions, unscoped resume, commit, or push.
33
33
  - Do not create task-local review artifacts, `tdd.jsonl`, or TDD evidence records in `check.jsonl`.
34
34
  - Do not claim completion without current verification.
@@ -26,4 +26,4 @@ Rules:
26
26
 
27
27
  - Do not use the `task` tool or invoke subagents.
28
28
  - Do not edit code, specs, task state, or git state.
29
- - Do not run task start, finish, archive, unscoped resume, commit, or push.
29
+ - Do not run task lifecycle transitions, unscoped resume, commit, or push.
@@ -16,7 +16,7 @@
16
16
  // install, so the adapter's relative import resolves in
17
17
  // both.
18
18
 
19
- import { spawn } from "node:child_process"
19
+ import { spawn, spawnSync } from "node:child_process"
20
20
  import { existsSync } from "node:fs"
21
21
  import { dirname, resolve } from "node:path"
22
22
  import { fileURLToPath } from "node:url"
@@ -24,6 +24,27 @@ import { fileURLToPath } from "node:url"
24
24
  const DIR_WORKFLOW = ".cowork-flow"
25
25
  // One prompt assembly must not wait longer than a host hook event.
26
26
  const INJECT_TIMEOUT_MS = 4800
27
+ // The .cowork-flow/run floor, reused here: a candidate that cannot run
28
+ // Python 3.10+ is skipped like a missing binary, so a stale `python3`
29
+ // never hides a working `python` further down the list.
30
+ const MIN_PYTHON_CHECK =
31
+ "import sys; raise SystemExit(0 if sys.version_info >= (3, 10) else 1)"
32
+
33
+ function interpreterIsUsable(candidate) {
34
+ try {
35
+ const probe = spawnSync(candidate[0], [...candidate.slice(1), "-c", MIN_PYTHON_CHECK], {
36
+ timeout: INJECT_TIMEOUT_MS,
37
+ windowsHide: true,
38
+ })
39
+ // A probe killed by its own timeout reached no verdict — a loaded machine,
40
+ // not a bad interpreter — so let the real spawn decide; it carries the
41
+ // same timeout and its own diagnostics.
42
+ if (probe.error && probe.error.code === "ETIMEDOUT") return true
43
+ return !probe.error && probe.status === 0
44
+ } catch {
45
+ return false
46
+ }
47
+ }
27
48
 
28
49
  // Last-resort repository root: this module lives at
29
50
  // <payload>/cowork-flow/plugin-core.js, next to plugins/,
@@ -197,12 +218,18 @@ async function renderViaInject(input, overrides = null) {
197
218
  cwd: inputCwd(input) || (root === pluginRoot ? null : root) || process.cwd(),
198
219
  }
199
220
  for (const candidate of pythonCandidates()) {
221
+ if (!interpreterIsUsable(candidate)) {
222
+ continue
223
+ }
200
224
  const result = await runInject(candidate, script, payload)
201
225
  if (!result.ran) {
202
226
  continue
203
227
  }
204
228
  return result.stdout.trim()
205
229
  }
230
+ console.error(
231
+ "cowork-flow opencode plugin: no Python 3.10+ interpreter found; skipping injection"
232
+ )
206
233
  return ""
207
234
  }
208
235
 
@@ -1052,6 +1052,82 @@ def check_qoder_plugin(repo_root: Path) -> list[dict[str, str]]:
1052
1052
  return []
1053
1053
 
1054
1054
 
1055
+ _ZCODE_PLUGIN_CONTRACT = "runtime-health:zcode-plugin"
1056
+
1057
+
1058
+ def _zcode_warning(code: str, path: Path, message: str, hint: str) -> list[dict[str, str]]:
1059
+ return [
1060
+ _issue(
1061
+ code=code,
1062
+ severity="warning",
1063
+ path=str(path),
1064
+ message=message,
1065
+ command_hint=hint,
1066
+ contract=_ZCODE_PLUGIN_CONTRACT,
1067
+ )
1068
+ ]
1069
+
1070
+
1071
+ def _zcode_marketplace_path() -> Path:
1072
+ home = Path(os.environ.get("ZCODE_HOME") or (Path.home() / ".zcode"))
1073
+ return (
1074
+ home
1075
+ / "cli"
1076
+ / "plugins"
1077
+ / "marketplaces"
1078
+ / "cowork-flow-local"
1079
+ / "marketplace.json"
1080
+ )
1081
+
1082
+
1083
+ def check_zcode_plugin(repo_root: Path) -> list[dict[str, str]]:
1084
+ """ZCode plugin health. Advisory, and silent while the project never selected
1085
+ the ZCode host: the plugin is a machine-level asset installed once into the
1086
+ ZCode config directory, so it never updates through sync or npm.
1087
+
1088
+ Staleness is judged on the payload's stamped manifest version rather than
1089
+ per file: the payload also carries a materialized copy of the project
1090
+ runtime, and that copy may legitimately differ from this project."""
1091
+ adapter = repo_root / DIR_WORKFLOW / "adapters" / "zcode" / "adapter.yaml"
1092
+ if not adapter.is_file():
1093
+ return []
1094
+
1095
+ marketplace = _zcode_marketplace_path()
1096
+ payload = _machine_plugin_payload("zcode")
1097
+ if payload is None:
1098
+ return _zcode_warning(
1099
+ "PLUGIN-NOT-INSTALLED",
1100
+ marketplace,
1101
+ "this project declares the ZCode host, but no cowork-flow plugin is "
1102
+ "installed in the ZCode config directory, so ZCode sessions inject no "
1103
+ "workflow context",
1104
+ "cwf host add zcode",
1105
+ )
1106
+
1107
+ manifest_relative = _payload_manifest("zcode", repo_root) or ".zcode-plugin/plugin.json"
1108
+ manifest = payload / manifest_relative
1109
+ recorded = _marker_version(manifest)
1110
+ if recorded is None:
1111
+ return _zcode_warning(
1112
+ "PLUGIN-UNKNOWN-VERSION",
1113
+ manifest,
1114
+ "the installed ZCode plugin carries no readable manifest version, so "
1115
+ "its injection logic may predate the current release",
1116
+ "cwf host add zcode --force",
1117
+ )
1118
+ project_version = _project_version(repo_root)
1119
+ if project_version and recorded != project_version:
1120
+ return _zcode_warning(
1121
+ "PLUGIN-STALE",
1122
+ manifest,
1123
+ f"the ZCode plugin was installed from {recorded} but this project "
1124
+ f"runs {project_version}; the plugin does not update with sync or npm, "
1125
+ "so injection may lag the project runtime",
1126
+ "cwf host add zcode --force",
1127
+ )
1128
+ return []
1129
+
1130
+
1055
1131
  _CLAUDE_CODE_PLUGIN_CONTRACT = "runtime-health:claude-code-plugin"
1056
1132
  _CLAUDE_CODE_PLUGIN_NAME = "cowork-flow"
1057
1133
  _CLAUDE_CODE_SKILL = "skills/cowork-flow-bootstrap/SKILL.md"
@@ -1478,15 +1554,7 @@ def _machine_plugin_payload(platform_id: str) -> Path | None:
1478
1554
  payload = Path(str(entry.get("installPath") or "")) if entry else None
1479
1555
  return payload if payload is not None and payload.is_dir() else None
1480
1556
  if platform_id == "zcode":
1481
- home = Path(os.environ.get("ZCODE_HOME") or (Path.home() / ".zcode"))
1482
- marketplace = (
1483
- home
1484
- / "cli"
1485
- / "plugins"
1486
- / "marketplaces"
1487
- / "cowork-flow-local"
1488
- / "marketplace.json"
1489
- )
1557
+ marketplace = _zcode_marketplace_path()
1490
1558
  try:
1491
1559
  data = json.loads(marketplace.read_text(encoding="utf-8"))
1492
1560
  except (OSError, json.JSONDecodeError, ValueError):
@@ -1615,6 +1683,7 @@ def _all_check_result(repo_root: Path) -> dict[str, object]:
1615
1683
  codex_plugin_issues = check_codex_plugin(repo_root)
1616
1684
  claude_code_plugin_issues = check_claude_code_plugin(repo_root)
1617
1685
  opencode_plugin_issues = check_opencode_plugin(repo_root)
1686
+ zcode_plugin_issues = check_zcode_plugin(repo_root)
1618
1687
  kimi_code_plugin_issues = check_kimi_code_plugin(repo_root)
1619
1688
  skill_delivery_issues = check_skill_delivery(repo_root)
1620
1689
  errors: list[dict[str, object]] = []
@@ -1643,6 +1712,7 @@ def _all_check_result(repo_root: Path) -> dict[str, object]:
1643
1712
  "codexPlugin": codex_plugin_issues,
1644
1713
  "claudeCodePlugin": claude_code_plugin_issues,
1645
1714
  "opencodePlugin": opencode_plugin_issues,
1715
+ "zcodePlugin": zcode_plugin_issues,
1646
1716
  "kimiCodePlugin": kimi_code_plugin_issues,
1647
1717
  "skillDelivery": skill_delivery_issues,
1648
1718
  },
@@ -1684,6 +1754,10 @@ def _run_checks(repo_root: Path, *, structured: bool = False) -> int:
1684
1754
  print(f"OpenCode plugin ({issue['code']}): {issue['message']}")
1685
1755
  if issue.get("commandHint"):
1686
1756
  print(f" fix: {issue['commandHint']}")
1757
+ for issue in result["issues"]["zcodePlugin"]:
1758
+ print(f"ZCode plugin ({issue['code']}): {issue['message']}")
1759
+ if issue.get("commandHint"):
1760
+ print(f" fix: {issue['commandHint']}")
1687
1761
  for issue in result["issues"]["kimiCodePlugin"]:
1688
1762
  print(f"Kimi Code plugin ({issue['code']}): {issue['message']}")
1689
1763
  if issue.get("commandHint"):