cowork-flow 0.0.51 → 1.0.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 +87 -25
- package/README.md +26 -5
- package/package.json +1 -1
- package/presets/dsh/plugins/workflow-state.js +46 -1
- package/src/cli.js +6 -0
- package/src/commands/install-dsh-hook.js +225 -0
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/adapters/zcode/adapter.yaml +30 -0
- package/template/.cowork-flow/scripts/adapters/cli/task_lifecycle_commands.py +28 -1
- package/template/.cowork-flow/scripts/adapters/cli/task_navigation.py +15 -4
- package/template/.cowork-flow/scripts/adapters/cli/task_next_runner.py +20 -3
- package/template/.cowork-flow/scripts/infra/skill_manifest.py +11 -2
- package/template/.cowork-flow/scripts/runtime/session_state.py +45 -15
- package/template/.cowork-flow/scripts/services/task_lifecycle.py +30 -1
- package/template/.cowork-flow/spec/runtime/host-assets.json +32 -0
- package/template/.dsh/README.md +19 -2
- package/template/.zcode/.zcode-plugin/plugin.json +1 -1
- package/template/.zcode/hooks/hooks.json +15 -0
- package/template/.zcode/hooks/inject-context.js +177 -22
- package/template/.zcode/hooks/inject-context.selfcheck.mjs +309 -4
- package/template/AGENTS.md +3 -2
- package/template/skills/runtime-health/scripts/doctor.py +109 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,41 +1,103 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.0
|
|
3
|
+
## 1.0.0 - 2026-08-26
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
首个稳定主线发布:核心流程契约、会话模型与宿主矩阵在此版本冻结,后续改动进入语义化版本约束。
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- Added this changelog to summarize release highlights and publish-time verification notes.
|
|
7
|
+
### 稳定性声明
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
- **宿主矩阵冻结**:`codex` / `opencode` / `claude-code` / `dsh` / `zcode` 五宿主经 host-assets 注册、adapter 一致性校验与 doctor 全链路覆盖;zcode 插件资产与工作区流程资产均为单源分发(`.zcode/` 走插件市场,技能落盘 `.cowork-flow/skills/` 供内核解析且不进入提示层)。
|
|
10
|
+
- **状态注入协议冻结**:`<workflow-state>` 块 + contract-digest(SessionStart 全量 / 逐消息指纹行)+ 生命周期快照(`.runtime/state-snapshot.json`,与状态转换同单元原子提交)构成宿主 hook 的标准输入;`build_hook_context` 共享协议具备直接单测覆盖。
|
|
11
|
+
- **会话模型冻结**:按会话身份(host session id / 显式 context id / hook sessionId)绑定;显式身份无绑定时判 `no_task` 并列可改绑任务,无身份请求才走全局最新有效兜底;进程级 fallback 绑定不再自动跟随。
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
- Added Batch execution inspect facts, stronger Host action result validation, and documented Batch recovery behavior.
|
|
14
|
-
- Normalized runtime-health and task-review issue facts so review output is easier to consume.
|
|
13
|
+
### 自 0.0.52 以来的变更
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
- 会话绑定安全加固:进程 fallback 会话键(`ZCODE_PROCESS_LABEL`)带 provenance 标记,导航、`--run` 派发、review/complete 目标解析与命令行收尾拒绝自动跟随 fallback 绑定,要求显式任务目录;trusted 身份(显式 env、宿主 session env、hook sessionId)保持完整绑定语义。
|
|
16
|
+
- 对抗性审查修正批次:显式 `COWORK_FLOW_CONTEXT_ID` 按裸键解析与 CLI 对齐;PostToolUse 刷新过滤支持 `cd` + 裸 `run` 与 Windows `run.cmd` 命令形态;legacy cursor 宿主保留完整 contract-digest;改绑提示排除已完成的终态任务;doctor 会话卫生检查对无时区时间戳降级为告警而非崩溃。
|
|
17
|
+
- 交互式平台选择器与平台检测断言随五宿主矩阵更新。
|
|
18
|
+
- release.sh 容错:`--version` 精确模式在版本文件已全部就位(干净工作树)时,`git commit` 的 no-op 空提交不再中止脚本,继续 tag 与 publish;其余 commit 失败仍立即中止。新增回归测试覆盖两条路径。
|
|
19
|
+
- release.sh 容错:目标发布 tag 已存在且指向当前 HEAD 时跳过创建并继续 publish(上次运行 tag 后 publish 未完成的重跑场景);指向其它提交则中止报错。新增回归测试覆盖两条路径。
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
- Protected the ZCode multi-module scaffold boundary so plugin scaffolding does not vendor `.cowork-flow/` runtime files into module directories.
|
|
21
|
+
## 0.0.52 - 2026-08-26
|
|
20
22
|
|
|
21
|
-
###
|
|
23
|
+
### ZCode 宿主与 hook 体验
|
|
22
24
|
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
+
- 修复 hook 过期会话污染:按会话身份选取活动任务,全局兜底跳过失效绑定与 subagent 会话;显式身份无绑定时对齐 CLI 判 `no_task` 并列出可改绑活动任务。
|
|
26
|
+
- 注册 zcode 为一等宿主平台:`host-assets.json` platforms 条目 + `adapters/zcode/adapter.yaml`,alias 解析、平台检测、adapter 一致性校验与 doctor 全链路覆盖;`.zcode/` 维持插件分发不落工程。
|
|
27
|
+
- 纯 zcode 工程生命周期可用:技能落盘 `.cowork-flow/skills/`(宿主提示层仍由插件单源提供),内核 `skill_roots()` 解析 action owner;`detectAny` 增加 adapter 标记修复 sync 静默失养。
|
|
28
|
+
- contract digest 注入瘦身:SessionStart(含 compact/clear)注入完整块,UserPromptSubmit 仅重复指纹行。
|
|
29
|
+
- 新增 PostToolUse(Bash) 轮内刷新:生命周期命令落定后立即注入最新 workflow-state 与指纹行,无关命令零输出。
|
|
25
30
|
|
|
26
|
-
###
|
|
31
|
+
### 运行时与诊断
|
|
27
32
|
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
- Added the product support and troubleshooting playbook for task routing, runtime binding, Batch, Host asset drift, Party Mode, and release checks.
|
|
33
|
+
- 生命周期转换在提交单元内原子写 `.runtime/state-snapshot.json`;hook 在快照与所选任务一致时采用快照面包屑键,缺失或不一致回退 status 推导。Python 共享 hook 协议(build_hook_context)补直接单测。
|
|
34
|
+
- doctor 新增会话卫生检查:报告失效任务绑定、超龄未活跃、不可读的运行时会话文件。
|
|
31
35
|
|
|
32
|
-
###
|
|
36
|
+
### 文档
|
|
33
37
|
|
|
34
|
-
-
|
|
35
|
-
- Formatting gate: `git diff --check`
|
|
36
|
-
- Windows note: POSIX-shell-only release tests may be reported as skipped when POSIX shell is unavailable; skips must remain visible and must not be reported as passes.
|
|
38
|
+
- AGENTS.md 0.1 明确注入块优先、勿重复运行导航器;README 平台清单与技能分发表同步 zcode。
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
## 0.0.51 - 2026-08-15
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
### DSH host 接入
|
|
43
|
+
|
|
44
|
+
- 注册 DeepSeek Harness(dsh)host adapter 与平台标签映射(dsh_ context keys)。
|
|
45
|
+
- 新增 `install-dsh-preset` 分发 DSH agent 预设;预设内置 workflow-state hook 插件,向系统提示注入与其它宿主同构的 `<workflow-state>` 块,每条用户消息刷新,生命周期命令落定后轮内刷新。
|
|
46
|
+
- 补齐协议失败静默降级边界测试;记录 DSH 子代理绑定 field-test 路径(subagent init/bind/close)。
|
|
47
|
+
|
|
48
|
+
### 计划绑定
|
|
49
|
+
|
|
50
|
+
- `--from-plan` 支持绑定计划到 planning 任务(plan binding lite 收口),补 --from-plan help 与实际行为一致。
|
|
51
|
+
- 归档任务快照绑定计划(snapshot bound plan into archived task)。
|
|
52
|
+
|
|
53
|
+
### 运行时与发布
|
|
54
|
+
|
|
55
|
+
- `platform_from_context_key` 单源化并修复 zcode 平台漂移;zcode 会话上下文确定性解析;slug 前缀与任务 id/name 归一。
|
|
56
|
+
- 共享 PYTHONPATH bootstrap;批量动作与 codex hook 在 Windows 通过 cmd wrapper 运行。
|
|
57
|
+
- CI 在 Ubuntu/Windows 双平台跑全量 pytest;发布流程先同步 Skill replicas 再过全量门禁。
|
|
58
|
+
|
|
59
|
+
## 0.0.50 - 2026-08-10
|
|
60
|
+
|
|
61
|
+
### 文档
|
|
62
|
+
|
|
63
|
+
- task-review 技能把用户自定义 spec 明确为绑定义务(binding obligations)。
|
|
64
|
+
|
|
65
|
+
## 0.0.49 - 2026-08-08
|
|
66
|
+
|
|
67
|
+
### 计划与任务
|
|
68
|
+
|
|
69
|
+
- 引入 plan binding lite:任务元数据绑定计划文件,Normal/High-risk 任务启动前校验计划与 decision anchor 就绪。
|
|
70
|
+
- `task next` 暴露实现优先读上下文(implement read-first);test-first 技能强化 red-green 指引。
|
|
71
|
+
|
|
72
|
+
### 运行时与架构
|
|
73
|
+
|
|
74
|
+
- 新增源码检出 source-refresh 命令;任务上下文服务模块化,lifecycle CLI adapter 瘦身;架构护栏测试固化边界。
|
|
75
|
+
- 状态恢复诊断增强;host manifest 契约对齐;批量与 party 编排器模块化。
|
|
76
|
+
- 保持 python 3.9 runner 兼容。
|
|
77
|
+
|
|
78
|
+
### CI / 发布
|
|
79
|
+
|
|
80
|
+
- 增加 Windows 发布信心门禁;发布验证测试稳定化。
|
|
81
|
+
|
|
82
|
+
## 0.0.48 - 2026-08-05
|
|
83
|
+
|
|
84
|
+
### 运行时与流程
|
|
85
|
+
|
|
86
|
+
- 拆分 lifecycle 命令;route 契约收紧;runtime context 生命周期硬化,恢复元数据错误 fail-closed;subagent 运行错误码透出。
|
|
87
|
+
- 移除 legacy changes control plane 与 add-session journal 工作流;guides 指引归入规划阶段。
|
|
88
|
+
|
|
89
|
+
### Party Mode 与 Batch
|
|
90
|
+
|
|
91
|
+
- 抽取 Party board 存储层;final report facts 丰富;host action fallback 对齐 capability matrix。
|
|
92
|
+
- Batch 增加 inspect facts 与 host action 结果校验;恢复契约文档化。
|
|
93
|
+
|
|
94
|
+
### Host 资产与健康
|
|
95
|
+
|
|
96
|
+
- 新增 host capability matrix 契约;host-assets sync 策略集中;防止 ZCode scaffold 向模块目录泄漏流程文件。
|
|
97
|
+
- task-review issue 与 runtime-health envelope 归一,review/health 输出更易消费。
|
|
98
|
+
|
|
99
|
+
### 文档
|
|
100
|
+
|
|
101
|
+
- README 项目概览刷新、任务流程图;产品故障排查 playbook;changelog 发布就绪说明。
|
|
102
|
+
|
|
103
|
+
## 0.0.47 - 2026-08-05
|
package/README.md
CHANGED
|
@@ -58,14 +58,17 @@ cowork-flow update --dry-run
|
|
|
58
58
|
# 安装 ZCode 插件(可选)
|
|
59
59
|
cowork-flow install-zcode-plugin
|
|
60
60
|
|
|
61
|
-
# 安装 DSH
|
|
61
|
+
# 安装 DSH 预设(可选,整套 agent)
|
|
62
62
|
cowork-flow install-dsh-preset
|
|
63
63
|
|
|
64
|
+
# 机器级安装实时 workflow-state 注入(推荐:不换预设,任意 DSH 会话生效)
|
|
65
|
+
cowork-flow install-dsh-hook
|
|
66
|
+
|
|
64
67
|
# 维护者发布前检查
|
|
65
68
|
npm run release:check
|
|
66
69
|
```
|
|
67
70
|
|
|
68
|
-
平台选项:`codex` / `opencode` / `claude-code` / `dsh` / `all`(逗号分隔)
|
|
71
|
+
平台选项:`codex` / `opencode` / `claude-code` / `dsh` / `zcode` / `all`(逗号分隔)
|
|
69
72
|
|
|
70
73
|
## 仓库结构
|
|
71
74
|
|
|
@@ -106,6 +109,7 @@ Skills 维护在 `template/skills/` 唯一源码,`init` / `sync` 时按目录
|
|
|
106
109
|
| `codex` / `opencode` | `.agents/skills/` |
|
|
107
110
|
| `dsh` | `.agents/skills/` |
|
|
108
111
|
| `claude-code` | `.claude/skills/` |
|
|
112
|
+
| `zcode` | `.cowork-flow/skills/`(内核 owner 解析用;系统提示层仍由 ZCode 插件单源提供,不重复加载) |
|
|
109
113
|
|
|
110
114
|
分发动作:`adversarial-review`、`agent-dispatch`、`batch-execution`、`brainstorming`、`cowork-flow`、`cowork-flow-maintenance`、`decision-audit`、`failure-analysis`、`game-design`、`party-mode`、`python-runtime-design`、`runtime-health`、`spec-sync`、`task-planning`、`task-review`、`test-first`
|
|
111
115
|
|
|
@@ -117,14 +121,15 @@ Skills 维护在 `template/skills/` 唯一源码,`init` / `sync` 时按目录
|
|
|
117
121
|
| `sync <path> [--dry-run]` | 同步已初始化项目的模板和技能 |
|
|
118
122
|
| `source-refresh [path] [--dry-run]` | 维护者刷新 source checkout 的 ignored live runtime 与 Host Skill replica |
|
|
119
123
|
| `install-zcode-plugin` | 安装 ZCode 插件到全局缓存 |
|
|
120
|
-
| `install-dsh-preset` | 安装 DSH agent 预设到 `~/.dsh/.agent-presets/cowork-flow
|
|
124
|
+
| `install-dsh-preset` | 安装 DSH agent 预设到 `~/.dsh/.agent-presets/cowork-flow/`(整套 agent,可选) |
|
|
125
|
+
| `install-dsh-hook` | 机器级注册 workflow-state hook 组合行到 `$DSH_HOME/cordis.patch.yml`(当前 DSH 的 agent 提示不收集 host 层 section,实时注入请用预设方式) |
|
|
121
126
|
| `update [--dry-run]` | 升级 CLI 本身 |
|
|
122
127
|
|
|
123
128
|
### init 选项
|
|
124
129
|
|
|
125
130
|
| 选项 | 说明 |
|
|
126
131
|
|---|---|
|
|
127
|
-
| `--platform <p>` | 平台:`codex` / `opencode` / `claude-code` / `dsh` / `all` |
|
|
132
|
+
| `--platform <p>` | 平台:`codex` / `opencode` / `claude-code` / `dsh` / `zcode` / `all` |
|
|
128
133
|
| `--developer <n>` | 开发者名称 |
|
|
129
134
|
| `--force` | 覆盖已有文件 |
|
|
130
135
|
| `--dry-run` | 预览不写入 |
|
|
@@ -167,7 +172,7 @@ ZCode 插件只安装 hook、skills、agents 和轻量说明文件;`.cowork-fl
|
|
|
167
172
|
|
|
168
173
|
**Hook 注入内容:**
|
|
169
174
|
- `workflow-state` — 当前任务状态
|
|
170
|
-
- `contract-digest` —
|
|
175
|
+
- `contract-digest` — 合同摘要:SessionStart 注入完整块,后续消息仅重复 SHA256 fingerprint
|
|
171
176
|
- `delegated_subtask` — 子代理运行时上下文
|
|
172
177
|
|
|
173
178
|
**插件子代理:**
|
|
@@ -175,6 +180,21 @@ ZCode 插件只安装 hook、skills、agents 和轻量说明文件;`.cowork-fl
|
|
|
175
180
|
- `cowork-check` — 绑定 runtime context 后做独立检查
|
|
176
181
|
- `cowork-research` — 绑定 runtime context 后做只读调研
|
|
177
182
|
|
|
183
|
+
## DSH 接入
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
cowork-flow init ./my-project --platform dsh # 项目资产:AGENTS.md + .agents/skills/ + .dsh 标记
|
|
187
|
+
cowork-flow install-dsh-hook # 机器级:注册 hook 组合行(实时注入见下方说明)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`install-dsh-hook` 把 `workflow-state.js` 插件作为 `insert:` patch 注册到 `$DSH_HOME/cordis.patch.yml`(未设置 `DSH_HOME` 时默认 `~/.dsh`),组合层面可被 `dsh --dump-config` 验证。经实测(DSH 0.1.1-rc.1),**agent 提示组装不收集 host 层 section**:该组合行不会在会话系统提示中产生 `<workflow-state>` 块。当前 DSH 版本下实时注入仍需预设方式(`install-dsh-preset`);本命令保留为组合层面的幂等注册能力(卸载见下),待 DSH 支持 agent-scope patch / workspace 级组合后可直接生效(见 `docs/dsh-upstream-proposal.md`)。
|
|
191
|
+
|
|
192
|
+
在未安装 cowork-flow 的项目里 hook 完全无感:JS 侧根目录预检直接短路——不注入内容、不启动 Python 进程。全局开关(环境变量):`COWORK_FLOW_HOOKS=0` / `COWORK_FLOW_DISABLE_HOOKS=1`。卸载:`cowork-flow install-dsh-hook --uninstall`(`--force` 同时删除插件文件)。
|
|
193
|
+
|
|
194
|
+
> 安装或更新后需要**重启 DSH**(`cordis.patch.yml` 在启动时组合,新增/变更不会被热加载)。
|
|
195
|
+
|
|
196
|
+
> 使用预设(`install-dsh-preset`)时无需再运行 `install-dsh-hook`——预设已内置同一 hook。
|
|
197
|
+
|
|
178
198
|
## DSH 预设
|
|
179
199
|
|
|
180
200
|
```bash
|
|
@@ -294,6 +314,7 @@ npm run release:check # 发布信心门禁;当前等价于 test:all
|
|
|
294
314
|
```bash
|
|
295
315
|
npm run release # patch
|
|
296
316
|
npm run release -- minor # minor
|
|
317
|
+
npm run release -- --version 0.1.0 # 精确发布指定版本(跳过自动 bump)
|
|
297
318
|
```
|
|
298
319
|
|
|
299
320
|
**发布流程:**
|
package/package.json
CHANGED
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
// falls back to running the navigator manually.
|
|
23
23
|
|
|
24
24
|
import { execFile } from 'node:child_process';
|
|
25
|
+
import { stat } from 'node:fs/promises';
|
|
26
|
+
import { dirname, join, resolve } from 'node:path';
|
|
25
27
|
|
|
26
28
|
export const name = 'workflow-state-hook';
|
|
27
29
|
export const inject = ['systemPrompt'];
|
|
@@ -35,6 +37,11 @@ const EXEC_OPTIONS = {
|
|
|
35
37
|
maxBuffer: 1024 * 1024,
|
|
36
38
|
};
|
|
37
39
|
const NO_ROOT_MARKER = '__COWORK_FLOW_NO_ROOT__';
|
|
40
|
+
// How long a "no .cowork-flow root" verdict is trusted for one cwd. Bounded
|
|
41
|
+
// on purpose: a project that installs cowork-flow mid-session becomes visible
|
|
42
|
+
// after at most one TTL, at the cost of at most one interpreter probe.
|
|
43
|
+
const NO_ROOT_TTL_MS = 30_000;
|
|
44
|
+
const noRootCache = new Map();
|
|
38
45
|
|
|
39
46
|
// Python 3.9+ protocol reused from the host hook adapters. It resolves the
|
|
40
47
|
// `.cowork-flow` root from the given cwd, loads the shared
|
|
@@ -157,13 +164,51 @@ export function resetWorkingPython() {
|
|
|
157
164
|
}
|
|
158
165
|
|
|
159
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Nearest ancestor of `cwd` whose `.cowork-flow` entry is a directory, or
|
|
169
|
+
* `null` when no such ancestor exists. Mirrors the root resolution inside
|
|
170
|
+
* the Python protocol so a project without cowork-flow never pays for an
|
|
171
|
+
* interpreter spawn at all.
|
|
172
|
+
*/
|
|
173
|
+
export async function findCoworkRoot(cwd) {
|
|
174
|
+
let current = resolve(cwd);
|
|
175
|
+
for (;;) {
|
|
176
|
+
try {
|
|
177
|
+
const entry = await stat(join(current, '.cowork-flow'));
|
|
178
|
+
if (entry.isDirectory()) {
|
|
179
|
+
return current;
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
// Missing or inaccessible — keep walking up.
|
|
183
|
+
}
|
|
184
|
+
const parent = dirname(current);
|
|
185
|
+
if (parent === current) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
current = parent;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
160
193
|
/**
|
|
161
194
|
* Produce the `<workflow-state>` hook context block for a workspace cwd.
|
|
162
195
|
* Empty string means "contribute nothing" (no root, hooks disabled, no
|
|
163
196
|
* usable interpreter, or a broken runtime).
|
|
164
197
|
*/
|
|
165
198
|
export async function runWorkflowState(cwd) {
|
|
166
|
-
if (hooksDisabled()) {
|
|
199
|
+
if (hooksDisabled() || !cwd) {
|
|
200
|
+
return '';
|
|
201
|
+
}
|
|
202
|
+
const resolved = resolve(cwd);
|
|
203
|
+
const cached = noRootCache.get(resolved);
|
|
204
|
+
if (cached !== undefined && cached > Date.now()) {
|
|
205
|
+
return '';
|
|
206
|
+
}
|
|
207
|
+
if ((await findCoworkRoot(resolved)) === null) {
|
|
208
|
+
if (noRootCache.size > 1000) {
|
|
209
|
+
noRootCache.clear();
|
|
210
|
+
}
|
|
211
|
+
noRootCache.set(resolved, Date.now() + NO_ROOT_TTL_MS);
|
|
167
212
|
return '';
|
|
168
213
|
}
|
|
169
214
|
if (workingPython !== null) {
|
package/src/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { emitKeypressEvents } from 'node:readline';
|
|
|
3
3
|
import { createInterface } from 'node:readline/promises';
|
|
4
4
|
|
|
5
5
|
import { runInit } from './commands/init.js';
|
|
6
|
+
import { runInstallDshHook } from './commands/install-dsh-hook.js';
|
|
6
7
|
import { runInstallDshPreset } from './commands/install-dsh-preset.js';
|
|
7
8
|
import { runInstallZCodePlugin } from './commands/install-zcode-plugin.js';
|
|
8
9
|
import { runSourceRefresh } from './commands/source-refresh.js';
|
|
@@ -16,6 +17,7 @@ Usage:
|
|
|
16
17
|
cowork-flow init [target] --platform <codex|opencode|claude-code|all> [--developer <name>] [--dry-run] [--force]
|
|
17
18
|
cowork-flow install-zcode-plugin [--dry-run] [--force] [--prune-old]
|
|
18
19
|
cowork-flow install-dsh-preset [--dry-run] [--force]
|
|
20
|
+
cowork-flow install-dsh-hook [--dry-run] [--force] [--uninstall]
|
|
19
21
|
cowork-flow update
|
|
20
22
|
cowork-flow sync [target] [--dry-run] [--force]
|
|
21
23
|
cowork-flow source-refresh [target] [--dry-run]
|
|
@@ -193,6 +195,10 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
|
|
193
195
|
return await runInstallDshPreset(args);
|
|
194
196
|
}
|
|
195
197
|
|
|
198
|
+
if (command === 'install-dsh-hook') {
|
|
199
|
+
return await runInstallDshHook(args);
|
|
200
|
+
}
|
|
201
|
+
|
|
196
202
|
io.writeErr(`Unknown command: ${command}\n`);
|
|
197
203
|
return 1;
|
|
198
204
|
} catch (error) {
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { access, copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
import { packageRoot } from '../lib/paths.js';
|
|
6
|
+
|
|
7
|
+
const PLUGIN_SRC = join(packageRoot, 'presets', 'dsh', 'plugins', 'workflow-state.js');
|
|
8
|
+
const ROW_ID = 'workflow-state-hook';
|
|
9
|
+
const MANAGED_MARK = '# cowork-flow: managed workflow-state-hook row. Run "cowork-flow install-dsh-hook" to change it.';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
function parseArgs(args) {
|
|
13
|
+
return {
|
|
14
|
+
dryRun: args.includes('--dry-run'),
|
|
15
|
+
force: args.includes('--force'),
|
|
16
|
+
uninstall: args.includes('--uninstall')
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
function getDshHome() {
|
|
22
|
+
return process.env.DSH_HOME || join(homedir(), '.dsh');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async function pathExists(target) {
|
|
27
|
+
try {
|
|
28
|
+
await access(target);
|
|
29
|
+
return true;
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async function readIfExists(file) {
|
|
37
|
+
try {
|
|
38
|
+
return await readFile(file, 'utf8');
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
function hookRowBlock(pluginPath) {
|
|
46
|
+
// cordis.patch.yml is a TOP-LEVEL YAML ARRAY of patch objects. A bare row
|
|
47
|
+
// would be read as a "modify existing entry" patch and skipped with
|
|
48
|
+
// "entry not found"; a new row therefore needs one id-less patch whose
|
|
49
|
+
// insert list appends rows at the top level of the composed entry list.
|
|
50
|
+
return [
|
|
51
|
+
MANAGED_MARK,
|
|
52
|
+
'- insert:',
|
|
53
|
+
' - id: ' + ROW_ID,
|
|
54
|
+
' name: ' + JSON.stringify(pluginPath),
|
|
55
|
+
].join('\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Locate the managed hook block inside a patch file's text.
|
|
61
|
+
*
|
|
62
|
+
* The block is the marker comment (when present), the id-less insert patch
|
|
63
|
+
* line, and the hook row with its indented continuation lines. Returns null
|
|
64
|
+
* when no such row exists. The patch is edited as text on purpose: a YAML
|
|
65
|
+
* round-trip would destroy !!js expressions used by DSH compositions.
|
|
66
|
+
*/
|
|
67
|
+
function findManagedBlock(text) {
|
|
68
|
+
const lines = text.split('\n');
|
|
69
|
+
const start = lines.findIndex((line) => line.trim() === '- id: ' + ROW_ID);
|
|
70
|
+
if (start === -1) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
let markStart = start;
|
|
74
|
+
while (markStart > 0) {
|
|
75
|
+
const previous = lines[markStart - 1].trim();
|
|
76
|
+
if (
|
|
77
|
+
previous === MANAGED_MARK.trim()
|
|
78
|
+
|| previous.startsWith('# cowork-flow: managed workflow-state-hook')
|
|
79
|
+
|| previous === '- insert:'
|
|
80
|
+
|| previous === 'insert:'
|
|
81
|
+
) {
|
|
82
|
+
markStart -= 1;
|
|
83
|
+
} else {
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
let end = start + 1;
|
|
88
|
+
while (end < lines.length && (lines[end].startsWith(' ') || lines[end].startsWith('\t'))) {
|
|
89
|
+
end += 1;
|
|
90
|
+
}
|
|
91
|
+
// The block excludes any following blank separator: replacement keeps the
|
|
92
|
+
// file's own line endings byte-stable, removal folds the leftover blank.
|
|
93
|
+
return { start: markStart, end };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
function patchWithRow(text, pluginPath) {
|
|
98
|
+
const block = hookRowBlock(pluginPath);
|
|
99
|
+
const range = findManagedBlock(text);
|
|
100
|
+
if (range !== null) {
|
|
101
|
+
const lines = text.split('\n');
|
|
102
|
+
return lines.slice(0, range.start)
|
|
103
|
+
.concat(block.split('\n'))
|
|
104
|
+
.concat(lines.slice(range.end))
|
|
105
|
+
.join('\n');
|
|
106
|
+
}
|
|
107
|
+
const base = text.replace(/\s+$/, '');
|
|
108
|
+
return (base + '\n' + block + '\n').replace(/^\n+/, '');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
function patchWithoutRow(text) {
|
|
113
|
+
const range = findManagedBlock(text);
|
|
114
|
+
if (range === null) {
|
|
115
|
+
return text;
|
|
116
|
+
}
|
|
117
|
+
const lines = text.split('\n');
|
|
118
|
+
const kept = lines.slice(0, range.start).concat(lines.slice(range.end));
|
|
119
|
+
const merged = kept.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
120
|
+
if (merged.trim() === '') {
|
|
121
|
+
return '';
|
|
122
|
+
}
|
|
123
|
+
return merged.replace(/\s+$/, '') + '\n';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
async function upsertRow(patchFile, pluginPath) {
|
|
128
|
+
const before = (await readIfExists(patchFile)) ?? '';
|
|
129
|
+
const after = patchWithRow(before, pluginPath);
|
|
130
|
+
if (after === before) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
await writeFile(patchFile, after, 'utf8');
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
async function removeRow(patchFile) {
|
|
139
|
+
const before = await readIfExists(patchFile);
|
|
140
|
+
if (before === null) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
const after = patchWithoutRow(before);
|
|
144
|
+
if (after === before) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
if (after === '') {
|
|
148
|
+
// Nothing of the user's own patch remains: drop the file instead of
|
|
149
|
+
// leaving an empty patch that shadows nothing.
|
|
150
|
+
await rm(patchFile, { force: true });
|
|
151
|
+
} else {
|
|
152
|
+
await writeFile(patchFile, after, 'utf8');
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Install (or uninstall) the machine-level DSH workflow-state hook.
|
|
160
|
+
*
|
|
161
|
+
* Registers the shared workflow-state.js plugin as an id-less insert patch
|
|
162
|
+
* in $DSH_HOME/cordis.patch.yml, which DSH applies to every profile and
|
|
163
|
+
* agent session at boot. Any preset keeps working unchanged; the hook
|
|
164
|
+
* contributes nothing (and, since the plugin pre-checks for a .cowork-flow
|
|
165
|
+
* root, does not even spawn an interpreter) in projects that do not run
|
|
166
|
+
* cowork-flow.
|
|
167
|
+
*/
|
|
168
|
+
export async function runInstallDshHook(args = []) {
|
|
169
|
+
const { dryRun, force, uninstall } = parseArgs(args);
|
|
170
|
+
|
|
171
|
+
if (!(await pathExists(PLUGIN_SRC))) {
|
|
172
|
+
throw new Error('workflow-state hook source missing at ' + PLUGIN_SRC + '. Reinstall cowork-flow.');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const home = getDshHome();
|
|
176
|
+
const pluginDest = join(home, 'plugins', 'cowork-flow', 'workflow-state.js');
|
|
177
|
+
const patchFile = join(home, 'cordis.patch.yml');
|
|
178
|
+
|
|
179
|
+
if (dryRun) {
|
|
180
|
+
if (uninstall) {
|
|
181
|
+
console.log('[dry-run] Would uninstall workflow-state hook:');
|
|
182
|
+
console.log(' Remove managed row from: ' + patchFile);
|
|
183
|
+
if (force) {
|
|
184
|
+
console.log(' Remove plugin file: ' + pluginDest);
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
console.log('[dry-run] Would install workflow-state hook:');
|
|
188
|
+
console.log(' Plugin: ' + PLUGIN_SRC + ' -> ' + pluginDest);
|
|
189
|
+
console.log(' Patch insert: ' + patchFile + ' (id: ' + ROW_ID + ')');
|
|
190
|
+
}
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (uninstall) {
|
|
195
|
+
const removed = await removeRow(patchFile);
|
|
196
|
+
if (removed === null) {
|
|
197
|
+
console.log('No patch file at ' + patchFile + '; nothing to uninstall.');
|
|
198
|
+
} else if (removed === false) {
|
|
199
|
+
console.log('No managed hook row found in ' + patchFile + '.');
|
|
200
|
+
} else {
|
|
201
|
+
console.log('✓ Workflow-state hook row removed from ' + patchFile);
|
|
202
|
+
}
|
|
203
|
+
if (force) {
|
|
204
|
+
await rm(pluginDest, { force: true });
|
|
205
|
+
console.log('✓ Removed plugin file ' + pluginDest);
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
await mkdir(dirname(pluginDest), { recursive: true });
|
|
211
|
+
await copyFile(PLUGIN_SRC, pluginDest);
|
|
212
|
+
const changed = await upsertRow(patchFile, pluginDest);
|
|
213
|
+
|
|
214
|
+
console.log('✓ cowork-flow workflow-state hook installed to ' + pluginDest);
|
|
215
|
+
console.log(' Registered insert row "' + ROW_ID + '" in ' + patchFile + (changed === false ? ' (already up to date)' : ''));
|
|
216
|
+
console.log(' Restart DSH for the composition to load (installed at boot-time).');
|
|
217
|
+
console.log(' Note: current DSH builds compose this row but do not surface host-level sections');
|
|
218
|
+
console.log(' in agent prompts; use "cowork-flow install-dsh-preset" for real-time injection.');
|
|
219
|
+
|
|
220
|
+
const presetComposition = join(home, '.agent-presets', 'cowork-flow', 'agent.cordis.yml');
|
|
221
|
+
if (await pathExists(presetComposition)) {
|
|
222
|
+
console.log(' Note: the cowork-flow agent preset is also installed and already bundles this hook.');
|
|
223
|
+
console.log(' Keep only one to avoid duplicate injection: remove the preset, or run "cowork-flow install-dsh-hook --uninstall".');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.0
|
|
1
|
+
1.0.0
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
schemaVersion: 1
|
|
2
|
+
host: zcode
|
|
3
|
+
displayName: ZCode
|
|
4
|
+
capabilities:
|
|
5
|
+
dispatchSubagent: native
|
|
6
|
+
freshChildContext: native
|
|
7
|
+
sendFollowup: native
|
|
8
|
+
waitChild: native
|
|
9
|
+
listChildren: shim
|
|
10
|
+
cancelChild: native
|
|
11
|
+
stateInjection: external
|
|
12
|
+
backgroundChild: native
|
|
13
|
+
runtimeContextDispatch: native
|
|
14
|
+
runtimeContextBinding: shim
|
|
15
|
+
runtimeContextCleanup: native
|
|
16
|
+
dispatch:
|
|
17
|
+
primitive: subagent
|
|
18
|
+
agentsPath: .zcode/agents
|
|
19
|
+
pluginManifest: .zcode/.zcode-plugin/plugin.json
|
|
20
|
+
runtimeContext:
|
|
21
|
+
transport: prompt
|
|
22
|
+
promptKey: cowork_runtime_context_id
|
|
23
|
+
envKey: COWORK_FLOW_RUNTIME_CONTEXT_ID
|
|
24
|
+
metadataKey: cowork_runtime_context_id
|
|
25
|
+
contracts:
|
|
26
|
+
dispatch: RUNTIME_CONTEXT_DISPATCH_V2
|
|
27
|
+
leafExecutor: true
|
|
28
|
+
fallback:
|
|
29
|
+
whenRequiredCapabilityMissing: inline_or_manual
|
|
30
|
+
whenRuntimeContextMissing: fail_closed
|
|
@@ -26,7 +26,11 @@ from infra.paths import (
|
|
|
26
26
|
FILE_TASK_JSON,
|
|
27
27
|
get_repo_root,
|
|
28
28
|
)
|
|
29
|
-
from runtime.session_state import
|
|
29
|
+
from runtime.session_state import (
|
|
30
|
+
PROVENANCE_PROCESS_FALLBACK,
|
|
31
|
+
clear_active_task,
|
|
32
|
+
get_active_task,
|
|
33
|
+
)
|
|
30
34
|
|
|
31
35
|
|
|
32
36
|
def _report_check_block(title: str, blockers: tuple[str, ...]) -> int:
|
|
@@ -71,6 +75,19 @@ def _resolve_status_task_dir(
|
|
|
71
75
|
file=sys.stderr,
|
|
72
76
|
)
|
|
73
77
|
return None
|
|
78
|
+
if (
|
|
79
|
+
active.task_path
|
|
80
|
+
and active.provenance == PROVENANCE_PROCESS_FALLBACK
|
|
81
|
+
):
|
|
82
|
+
print(
|
|
83
|
+
colored(
|
|
84
|
+
"Error: Session binding is process-fallback (shared); "
|
|
85
|
+
"pass a task dir explicitly.",
|
|
86
|
+
Colors.RED,
|
|
87
|
+
),
|
|
88
|
+
file=sys.stderr,
|
|
89
|
+
)
|
|
90
|
+
return None
|
|
74
91
|
if not active.task_path:
|
|
75
92
|
print(
|
|
76
93
|
colored(
|
|
@@ -342,6 +359,16 @@ def cmd_finish(args: argparse.Namespace) -> int:
|
|
|
342
359
|
"""Clear the active task for this session."""
|
|
343
360
|
repo_root = get_repo_root()
|
|
344
361
|
active = get_active_task(repo_root)
|
|
362
|
+
if active.task_path and active.provenance == PROVENANCE_PROCESS_FALLBACK:
|
|
363
|
+
print(
|
|
364
|
+
colored(
|
|
365
|
+
"Error: Refusing to clear a session binding resolved from "
|
|
366
|
+
"process-fallback identity; archive the task to clear it.",
|
|
367
|
+
Colors.RED,
|
|
368
|
+
),
|
|
369
|
+
file=sys.stderr,
|
|
370
|
+
)
|
|
371
|
+
return 1
|
|
345
372
|
if not active.task_path:
|
|
346
373
|
print(
|
|
347
374
|
colored(
|