cowork-flow 2.0.0 → 2.2.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.
- package/CHANGELOG.md +31 -1
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/docs/direction.md +3 -2
- package/docs/hosts.md +1 -1
- package/docs/index.md +1 -1
- package/docs/release.md +1 -1
- package/package.json +6 -6
- package/presets/claude-code/.claude-plugin/plugin.json +1 -1
- package/presets/codex/.codex-plugin/plugin.json +1 -1
- package/presets/dsh/plugins/workflow-state.js +42 -9
- package/presets/kimi-code/.kimi-plugin/plugin.json +1 -1
- package/presets/kimi-code/hooks/cowork-flow-inject.mjs +28 -2
- package/presets/opencode/cowork-flow/plugin-core.js +28 -1
- package/presets/qoder/.qoder-plugin/plugin.json +1 -1
- package/presets/qoder/hooks/inject-context.mjs +28 -2
- package/presets/zcode/.zcode-plugin/plugin.json +1 -1
- package/presets/zcode/hooks/inject-context.mjs +28 -3
- package/src/commands/install-dsh-hook.js +1 -6
- package/src/commands/install-dsh-preset.js +1 -6
- package/src/commands/install-zcode-plugin.js +2 -3
- package/src/lib/asset-plan.js +1 -1
- package/src/lib/dsh-home.js +6 -0
- package/src/lib/plan-applier.js +1 -6
- package/template/.claude/agents/cowork-check.md +1 -1
- package/template/.claude/agents/cowork-implement.md +1 -1
- package/template/.claude/agents/cowork-research.md +1 -1
- package/template/.codex/agents/cowork-check.toml +1 -1
- package/template/.codex/agents/cowork-implement.toml +1 -1
- package/template/.codex/agents/cowork-research.toml +1 -1
- package/template/.codex/agents/default.toml +1 -1
- package/template/.codex/agents/explorer.toml +1 -1
- package/template/.codex/agents/worker.toml +1 -1
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/run +2 -2
- package/template/.cowork-flow/run.cmd +3 -3
- package/template/.cowork-flow/spec/contracts/context-injection.md +4 -4
- package/template/.cowork-flow/spec/contracts/decision-anchor.md +7 -1
- package/template/.kimi-code/agents/cowork-check.md +1 -1
- package/template/.kimi-code/agents/cowork-implement.md +1 -1
- package/template/.kimi-code/agents/cowork-research.md +1 -1
- package/template/.opencode/agents/cowork-check.md +1 -1
- package/template/.opencode/agents/cowork-implement.md +1 -1
- package/template/.opencode/agents/cowork-research.md +1 -1
- package/template/.opencode/cowork-flow/plugin-core.js +28 -1
- package/template/skills/runtime-health/scripts/doctor.py +83 -9
- package/template/skills/task-planning/SKILL.md +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,34 @@
|
|
|
4
4
|
|
|
5
5
|
### Added
|
|
6
6
|
|
|
7
|
+
## [2.2.0] - 2026-10-20
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **计划与任务验证段纳入规范门禁检查**:`task-planning` 的计划 `Final Verification`、任务规则与 Self-Review 要求按 `dev_type` 逐条核对 `.cowork-flow/spec/<dev_type>/` 适用规范与项目门禁——改动被规范覆盖时,把决定命令或 `规范核对:` 行写进计划的 Final Verification 与任务简报的 Verification Command,标 `pass` / `not_applicable` 并写理由;`decision-anchor` 契约的 `## 验证命令` 同步要求该检查行,并明确为 review 强提醒而非 `spec-check` 硬门禁,120 字符/10 行限制不变。
|
|
12
|
+
- **模板示例统一展示规范核对行**:`decision-anchor` Schema 的 `## 验证命令` 示例新增 `规范核对:` 行;`task-planning` 任务简报的 `Verification Command` 示例块后补充写法说明,避免新建计划/任务漏写规范检查项。
|
|
13
|
+
|
|
14
|
+
## [2.1.0] - 2026-10-19
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **发布前漂移门禁接入 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` 套件(此前只跑两条具名用例)。
|
|
19
|
+
- **发布门禁补"活副本资产漂移"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 .` 修复指引。
|
|
20
|
+
- **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 级。
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **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` 重装。
|
|
25
|
+
- **清理 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`)把点名已删子命令的禁令改为"禁止执行任务生命周期转换"。
|
|
26
|
+
- **`assetDriftVerdict` 缺键改为 fail-closed**:readiness 少 `wouldCopy`/`wouldRemoveObsolete`(或给非数组)时不再静默判"无漂移",改判门禁失效并点名缺失键(`cannot judge drift`),并把逐 key 单钉、e2e fail 断言补进测试。
|
|
27
|
+
- **dsh 候选可用性映射抽为 `candidateUnavailable` 并恢复 Windows 覆盖**:`ENOENT`/退出码 3 → 换下一个候选;其余非零退出(含超时)→ 候选按"可用但协议失败、不注入"处理(语义不变,仅抽函数加注释);新增跨平台单测钉住四种输入,`test/dsh-hook.test.js` 两条过期的 Windows skip 移除(取证:node 探测 WindowsApps `python3` 别名得 `ENOENT`、回落到真实 `python` 3.12)。
|
|
28
|
+
- **重复工具函数收敛**:`sha256` 只留 `src/lib/asset-plan.js` 的导出,`src/lib/plan-applier.js` 改为复用;`getDshHome` 抽到新增的 `src/lib/dsh-home.js`,两个 dsh 安装器共用。
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- **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 上一致。
|
|
33
|
+
- **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` 运行。
|
|
34
|
+
|
|
7
35
|
## [2.0.0] - 2026-10-16
|
|
8
36
|
|
|
9
37
|
### Added
|
|
@@ -365,7 +393,9 @@
|
|
|
365
393
|
|
|
366
394
|
此版本未保留本地条目正文,详情见 [GitHub Release](https://github.com/fishlikewater/cowork-flow/releases/tag/v0.0.47)。
|
|
367
395
|
|
|
368
|
-
[Unreleased]: https://github.com/fishlikewater/cowork-flow/compare/v2.
|
|
396
|
+
[Unreleased]: https://github.com/fishlikewater/cowork-flow/compare/v2.2.0...HEAD
|
|
397
|
+
[2.2.0]: https://github.com/fishlikewater/cowork-flow/compare/v2.1.0...v2.2.0
|
|
398
|
+
[2.1.0]: https://github.com/fishlikewater/cowork-flow/compare/v2.0.0...v2.1.0
|
|
369
399
|
[2.0.0]: https://github.com/fishlikewater/cowork-flow/compare/v1.9.2...v2.0.0
|
|
370
400
|
[1.9.2]: https://github.com/fishlikewater/cowork-flow/compare/v1.9.1...v1.9.2
|
|
371
401
|
[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
|
|
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-
|
|
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.
|
|
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
|
|
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`
|
|
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.
|
|
3
|
+
"version": "2.2.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"
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
376
|
+
resolvePromise({ missing: candidateUnavailable(error), output: '' });
|
|
344
377
|
return;
|
|
345
378
|
}
|
|
346
379
|
resolvePromise({ missing: false, output: stdout.trim() });
|
|
@@ -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
|
|
121
|
-
//
|
|
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
|
|
|
@@ -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
|
|
146
|
-
//
|
|
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
|
|
|
@@ -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
|
|
184
|
-
//
|
|
185
|
-
//
|
|
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
|
|
254
|
-
//
|
|
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)) {
|
package/src/lib/asset-plan.js
CHANGED
|
@@ -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
|
|
package/src/lib/plan-applier.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
1
|
+
2.2.0
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
set -u
|
|
3
3
|
|
|
4
|
-
MIN_PYTHON_LABEL="Python 3.
|
|
5
|
-
VERSION_CHECK='import sys; raise SystemExit(0 if sys.version_info >= (3,
|
|
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.
|
|
5
|
-
set "VERSION_CHECK=import sys; raise SystemExit(0 if sys.version_info >= (3,
|
|
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.
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
@@ -33,6 +33,7 @@ translated heading would simply stop being read.
|
|
|
33
33
|
|
|
34
34
|
## 验证命令
|
|
35
35
|
- `<command>` → <expected>
|
|
36
|
+
- 规范核对:逐条核对 `.cowork-flow/spec/<dev_type>/` 适用规范 → pass / not_applicable(附理由)
|
|
36
37
|
```
|
|
37
38
|
|
|
38
39
|
## Section requirements
|
|
@@ -44,7 +45,7 @@ translated heading would simply stop being read.
|
|
|
44
45
|
| `## 被拒方案` | optional | required for brainstorming tasks, so a rejected option is not reconsidered during execution |
|
|
45
46
|
| `## 关键假设` | optional | filled in for complex tasks to surface implicit constraints; not read by the runtime parser |
|
|
46
47
|
| `## 范围边界` | optional | prevents scope creep |
|
|
47
|
-
| `## 验证命令` | optional | filled in for complex tasks, as the basis for automated verification |
|
|
48
|
+
| `## 验证命令` | optional | filled in for complex tasks, as the basis for automated verification; when the changed files are covered by `.cowork-flow/spec/backend/` or `frontend/` rules, it must also carry the spec/gate check for the task's `dev_type` (deciding command, or clause-by-clause `pass` / `not_applicable` with reason) |
|
|
48
49
|
|
|
49
50
|
### Machine-readable line formats (for injection)
|
|
50
51
|
|
|
@@ -54,6 +55,11 @@ fact view (`services/fact_view.parse_decision_anchor`). Line-format conventions:
|
|
|
54
55
|
- `## 验证命令`: one command per line (an optional `- ` prefix), truncated to 120
|
|
55
56
|
characters, at most 10 lines; the implementing agent runs them itself
|
|
56
57
|
(self-declared, self-checked) and the reviewer verifies the execution evidence.
|
|
58
|
+
When the changed files are covered by `.cowork-flow/spec/<dev_type>/` rules, the
|
|
59
|
+
section must also carry the spec/gate check for the task's `dev_type`: a deciding
|
|
60
|
+
command, or a `规范核对:` line reviewed clause by clause (`pass` /
|
|
61
|
+
`not_applicable` + reason). This is a strong review reminder, not a `spec-check`
|
|
62
|
+
hard gate.
|
|
57
63
|
- `## 范围边界`: only the first non-empty line is taken, truncated to 160
|
|
58
64
|
characters.
|
|
59
65
|
|
|
@@ -24,6 +24,6 @@ Execution:
|
|
|
24
24
|
Rules:
|
|
25
25
|
|
|
26
26
|
- Do not invoke subagents.
|
|
27
|
-
- Do not run task
|
|
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
|
|
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.
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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"):
|
|
@@ -45,7 +45,7 @@ Start with a concise brief:
|
|
|
45
45
|
| **Task Type** | Tiny / Normal / High-risk: <reason> |
|
|
46
46
|
| **Strategy** | Serial / Parallel: <why> |
|
|
47
47
|
| **Success Criteria** | AC-001 / AC-002 / AC-003 |
|
|
48
|
-
| **Final Verification** | `<command>` -> exit 0;
|
|
48
|
+
| **Final Verification** | `<command>` -> exit 0; clause-by-clause check against applicable `.cowork-flow/spec/<dev_type>/` rules and project gates -> `pass` / `not_applicable` per clause |
|
|
49
49
|
| **Primary Risk** | Low / Medium / High: <one sentence> |
|
|
50
50
|
|
|
51
51
|
## Goal
|
|
@@ -116,6 +116,8 @@ Then list task briefs, followed by integrated verification and completion checks
|
|
|
116
116
|
<command>
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
+
When changed files are covered by `.cowork-flow/spec/<dev_type>/` rules, add the deciding command or a spec/gate check line (`pass` / `not_applicable` with reason; format example in `decision-anchor.md`) here; it is a strong review reminder, not a hard gate.
|
|
120
|
+
|
|
119
121
|
**Completion Conditions**
|
|
120
122
|
- <2-4 checkable conditions>
|
|
121
123
|
|
|
@@ -128,7 +130,7 @@ Then list task briefs, followed by integrated verification and completion checks
|
|
|
128
130
|
|
|
129
131
|
- For Tiny tasks, keep the brief short but still name exact file boundaries, completion conditions, and verification commands.
|
|
130
132
|
- For behavior changes, include a failing test before implementation when behavior can be tested.
|
|
131
|
-
- For implementation tasks whose changed files are covered by `.cowork-flow/spec/` requirements, name the covered spec files in the plan
|
|
133
|
+
- For implementation tasks whose changed files are covered by `.cowork-flow/spec/` requirements, name the covered spec files in the plan and add the clause check to the task's `Verification Command` and the plan's `Final Verification`: run the deciding command when one exists, otherwise check clause by clause and record `pass` / `not_applicable` with reason. This is a strong review reminder, not a separate hard gate.
|
|
132
134
|
- Do not add shallow tests just to satisfy process. Avoid tests that only assert existence, mirror implementation details, count mock calls without behavior, or snapshot empty structure.
|
|
133
135
|
- For complex problems, test depth first: business invariants, cross-layer contracts, state transitions, error boundaries, and real regression paths before narrow unit cases.
|
|
134
136
|
- Map behavior-changing tests to stable `decision-anchor.md` acceptance IDs when useful; do not create `tdd.jsonl` or write TDD evidence records into `check.jsonl`.
|
|
@@ -190,3 +192,4 @@ Before handoff:
|
|
|
190
192
|
3. Confirm every Normal or High-risk task has file boundaries, key symbols, implementation notes, test proof, completion conditions, and prohibited drift.
|
|
191
193
|
4. Check names, paths, command syntax, and expected outputs.
|
|
192
194
|
5. Confirm the plan says to stop and report back when code facts conflict with the plan, a plan-external file becomes necessary, or a test fails for an unplanned reason.
|
|
195
|
+
6. Confirm Final Verification carries the applicable `.cowork-flow/spec/<dev_type>/` clause check, or states why no spec file applies.
|