cowork-flow 1.0.0 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +11 -2
  3. package/package.json +1 -1
  4. package/presets/dsh/plugins/workflow-state.js +25 -15
  5. package/src/cli.js +6 -0
  6. package/src/commands/mcp-state.js +66 -0
  7. package/template/.claude/hooks/inject-workflow-state.py +6 -5
  8. package/template/.codex/hooks/inject-workflow-state.py +9 -1
  9. package/template/.cowork-flow/.version +1 -1
  10. package/template/.cowork-flow/adapters/claude-code/adapter.yaml +1 -0
  11. package/template/.cowork-flow/adapters/codex/adapter.yaml +1 -0
  12. package/template/.cowork-flow/adapters/dsh/adapter.yaml +1 -0
  13. package/template/.cowork-flow/adapters/opencode/adapter.yaml +1 -0
  14. package/template/.cowork-flow/adapters/zcode/adapter.yaml +1 -0
  15. package/template/.cowork-flow/scripts/adapters/cli/state.py +121 -0
  16. package/template/.cowork-flow/scripts/adapters/cli/subagent.py +28 -0
  17. package/template/.cowork-flow/scripts/adapters/cli/task.py +6 -4
  18. package/template/.cowork-flow/scripts/adapters/cli/task_lifecycle_commands.py +5 -1
  19. package/template/.cowork-flow/scripts/adapters/cli/task_parser.py +9 -0
  20. package/template/.cowork-flow/scripts/adapters/host/workflow_state_hook.py +208 -28
  21. package/template/.cowork-flow/scripts/adapters/mcp/_bootstrap.py +14 -0
  22. package/template/.cowork-flow/scripts/adapters/mcp/state_server.py +305 -0
  23. package/template/.cowork-flow/scripts/infra/git_snapshot.py +49 -10
  24. package/template/.cowork-flow/scripts/infra/skill_manifest.py +3 -1
  25. package/template/.cowork-flow/scripts/run.py +3 -0
  26. package/template/.cowork-flow/scripts/services/context_discovery.py +26 -0
  27. package/template/.cowork-flow/scripts/services/context_paths.py +79 -9
  28. package/template/.cowork-flow/scripts/services/fact_view.py +439 -0
  29. package/template/.cowork-flow/scripts/services/lifecycle_checks.py +34 -3
  30. package/template/.cowork-flow/scripts/services/task_context.py +2 -21
  31. package/template/.cowork-flow/scripts/services/task_lifecycle.py +202 -3
  32. package/template/.cowork-flow/scripts/services/workflow_runtime.py +45 -0
  33. package/template/.cowork-flow/spec/contracts/context-injection.md +208 -0
  34. package/template/.cowork-flow/spec/contracts/decision-anchor.md +7 -0
  35. package/template/.cowork-flow/spec/contracts/fact-layer-access.md +50 -0
  36. package/template/.cowork-flow/spec/runtime/host-assets.json +10 -5
  37. package/template/.cowork-flow/spec/runtime/scope-rules.json +16 -0
  38. package/template/.opencode/plugins/cowork-flow.js +338 -11
  39. package/template/.zcode/.zcode-plugin/plugin.json +1 -1
  40. package/template/.zcode/hooks/hooks.json +1 -1
  41. package/template/.zcode/hooks/inject-context.js +418 -22
  42. package/template/.zcode/hooks/inject-context.selfcheck.mjs +277 -23
package/CHANGELOG.md CHANGED
@@ -1,5 +1,54 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.3 - 2026-08-30
4
+
5
+ > **版本内容载体说明**:1.1.1(守卫修复批次)与 1.1.2(Review 基线 diff)章节内容随本 1.1.3 首次进入 npm 分发——三个批次同属一个发布周期,章节按批次序号记录,包内容以最新版本号为载体(与 1.1.0 承载 1.0.0 章节内容同一惯例)。
6
+
7
+ ### 规则表数据化(scope-rules 单源)
8
+
9
+ - 新增 `.cowork-flow/spec/runtime/scope-rules.json`:scope 过滤规则(allowedTypes/wildcardChars/rejectedSegments/driveLetterPattern/trailingSlashRejectedTypes)与 stage-contract 限制(budget/scopeLimit/specLimit/verifyLimit)从三份复制实现下沉为单一数据文件。
10
+ - Python(context_paths/fact_view)与 zcode/opencode JS 镜像运行时消费同一文件;文件缺失/畸形降级到与默认内容逐字一致的内嵌默认(默认等价由 tests/test_scope_rules.py 与 selfcheck 锁定)。
11
+ - 规则可真实改变行为:wildcardChars 置空 → 通配条目进入 Scope;budget 调小 → 预算降级路径触发。
12
+ - 修复预算兜底 1 字节超限:裁剪切分与闭标签间的换行预留。
13
+ - **CI 修复(delegated 注入崩溃)**:zcode delegated 分支不再以空输入对象重新发现项目根(`findProjectRoot({})`),改为复用 main 已解析的工作流根——在无 `.cowork-flow/` 的目录(干净 checkout、非项目目录)触发 delegated prompt 不再 `join(null)` 崩溃;对应的注入测试显式指定 spawn 工作目录,消除对测试运行 cwd 的隐式依赖(干净 checkout 下此前必红,实测 dev 推送 CI 双平台失败)。
14
+ - **CI 修复(Windows git 降级)**:`git` 二进制不可用(PATH 缺失/未安装)时 `_run_git_command` 捕获 OSError 按 rc!=0 降级——`current_head` 视为无头(不写 baseline)、变更集收集降级 status-only,`task start` 不再崩溃(Windows 的 CreateProcess 在 PATH 缺失时不回退,清空环境的会话测试此前必红;macOS execvp 有默认 PATH 兜底故本地绿)。
15
+
16
+ ## 1.1.2 - 2026-08-30
17
+
18
+ ### Review 基线 diff(堵住提交绕行面)
19
+
20
+ - **基线记录**:`task start`(进入 in_progress)在 task.json `meta.baselineCommit` 记录当前 HEAD,且永不滑动(重复/幂等 start 不覆盖)——任务期间的审查窗口从激活时刻起固定。
21
+ - **变更集合并**:review 门禁以 `baseline..HEAD` diff 与 working-tree status 的并集去重作为变更集——agent 中途 `git commit` 越界文件后,review 的 `unlisted_changed_file` 仍会触发(此前提交即从 status 消失,形成无痕通道)。
22
+ - **降级语义**:无 git 仓库 / HEAD 不存在 / 基线缺失 / diff 失败(如 rebase 孤儿化)→ 降级 status-only,与旧行为一致,不产生错误 blocker。
23
+ - **契约重开**:task-review SKILL 输入语义写明基线变更集与降级行为;已审查任务的增量重审聚焦自基线以来的变化,证据要求不变。
24
+
25
+ ## 1.1.1 - 2026-08-30
26
+
27
+ ### 守卫修复批次(对抗评审后落地)
28
+
29
+ - **hook 崩溃修复**:implement.jsonl 含 `./` 前缀条目时 zcode/opencode 双宿主 hook 从 `TypeError: Assignment to constant variable` 崩溃(整个上下文注入丢失)改为正常输出——`const`→`let` 两处 + 回归 fixture。
30
+ - **stage-contract 预算降级**:超限输入不再产出未闭合的畸形块;按 Verify → Specs → Scope 条目(至少 1 条)逐级降级,收尾标签与 Gates 行恒在;三线同构算法。
31
+ - **MCP 路径隔离**:`task_scope`/`task_specs` 拒绝仓库外路径(`../`、绝对路径)返回 `task-outside-repo`;无 id 的 JSON-RPC 通知(含 initialize/ping/tools/list)不再响应。
32
+ - **JS 白名单语义对齐**:zcode/opencode 过滤规则与 Python `normalize_context_file_scope_entry` 一致(非法 type、`../`、绝对路径、盘符、通配符一律丢弃)——editScopeWarning 与 Scope 行不再对 gate 会标记越界的文件静默放行;spec 指针同规则过滤。
33
+ - **delegated 只读 scope**:子代理注入包删除 `Scope: subagent` 行;stage-contract 以父任务 scope `[read-only]` 变体呈现,Gates 话术同步(不再暗示子代理可自声明 scope)。
34
+ - **异常降级可诊断**:锚点文件非法 UTF-8 时 stage-contract 保留 Scope/Gates 仅丢 Verify(此前整块静默消失);非例行异常在 stderr 留痕。
35
+ - **review 门禁补洞**:implement.jsonl 缺失时 review 产生 `missing_implement_jsonl_file_scope` blocker(此前静默放行,删除 manifest 即绕过白名单)。
36
+ - **矩阵化跨端测试**:`test/fixtures/stage-contract-matrix.json` 单一数据文件驱动三线(python/zcode/opencode)逐字节相等断言,覆盖规范/`./` 前缀/非法边界/超限/emoji/缺锚点/空 scope/delegated 8 类用例;zcode hooks.json matcher(Bash|Edit|Write|MultiEdit)由模板测试锁定;新增 Python 侧矩阵断言。
37
+ - dev_type 畸形值(非字符串)在 task_specs 中按缺省降级;spec 指针忽略 directory 条目;normalizeScopePath 带 trim;zcode 生命周期刷新正则对齐 dsh(task|subagent|resume)。
38
+ - 契约文档如实化:context-injection.md 不再声称三线结构恒等/always emitted,改为差异表 + 矩阵锁定范围 + 残余缺口清单(matcher 依赖 ZCode 运行时工具名、JS 过滤为规则移植)。
39
+
40
+ ## 1.1.0 - 2026-08-29
41
+
42
+ > **版本内容错位说明**:npm registry 上的 1.0.0 tarball 发布于 2026-08-26(仅含发版脚本修复之前的代码)。1.0.0 段下述的里程碑描述以本 1.1.0 为其实际发布载体——阶段 0-3 的全部内容自本版本起进入 npm 分发。
43
+
44
+ ### 方向落地(阶段 0-3,详见 1.0.0 段与 docs/direction.md)
45
+
46
+ - 阶段 0:README 定位改写为「运行时上下文与协作事实层」;注入协议契约 `spec/contracts/context-injection.md`;契约指纹序列化三线统一 + slim 全覆盖 + 跨 host 一致性测试。
47
+ - 阶段 1:`run state [task] --json` 事实视图;`<workflow-state>` 属性事实头 + `<decision-anchor>` 决策要点注入(三线一致)。
48
+ - 阶段 2:`executor` 归属、冲突拦截与 `--takeover`、无会话 CI start、`subagent evidence` 证据位。
49
+ - 阶段 3:`run mcp-state` 无依赖 MCP stdio 只读服务(`task_state` / `task_list`)+ `spec/contracts/fact-layer-access.md` 接入契约。
50
+ - MCP 全局入口:`cowork-flow mcp-state` 透传命令——MCP 客户端全局注册一次即可服务所有 cowork-flow 项目。
51
+
3
52
  ## 1.0.0 - 2026-08-26
4
53
 
5
54
  首个稳定主线发布:核心流程契约、会话模型与宿主矩阵在此版本冻结,后续改动进入语义化版本约束。
@@ -17,6 +66,13 @@
17
66
  - 交互式平台选择器与平台检测断言随五宿主矩阵更新。
18
67
  - release.sh 容错:`--version` 精确模式在版本文件已全部就位(干净工作树)时,`git commit` 的 no-op 空提交不再中止脚本,继续 tag 与 publish;其余 commit 失败仍立即中止。新增回归测试覆盖两条路径。
19
68
  - release.sh 容错:目标发布 tag 已存在且指向当前 HEAD 时跳过创建并继续 publish(上次运行 tag 后 publish 未完成的重跑场景);指向其它提交则中止报错。新增回归测试覆盖两条路径。
69
+ - 方向收敛(阶段 0):README 定位改为「运行时上下文与协作事实层」;新增注入协议契约 `spec/contracts/context-injection.md`(事件时机矩阵、digest 形态规则、序列化规范);契约指纹序列化三线统一(zcode/opencode 稳定排序 + Python 紧凑分隔符,跨 host 指纹一致性测试锁定);Python 线补 slim(SessionStart 全量 / 后续单行指纹,无事件 host 按会话文件首次判定);codex 事件名读取;opencode 首次全量后续单行;dsh 会话开始全量、生命周期命令后单行刷新。
70
+ - 事实层 API 化(阶段 1a):新增 `./.cowork-flow/run state [task] --json` 事实视图——聚合 task.json(含 `_state` 修订)、decision-anchor 结构化要点(目标/验收项/被拒方案名)、plan 绑定、绑定会话与受信快照;无绑定输出 `task: null` 供机器分支。
71
+ - 注入结构化(阶段 1b):`<workflow-state>` 升级为属性事实头(`task`/`status`/`source` 进开标签,body 保留人读面包屑),三线一致并在协议契约冻结;planning/in_progress/review 状态下三线注入紧凑 `<decision-anchor>` 决策要点块(Python 复用 fact_view 解析单源),completed 终态与缺文件不注入。
72
+ - 多执行者语义(阶段 2):task.json 增加 `executor` 归属(start 写入会话 key 或显式 `--executor`);执行者冲突 fail-closed(`LIFECYCLE-EXECUTOR-001`,幂等重跑同样拦截),`--takeover` 显式接管并覆写归属(含已激活任务的幂等接管);`--executor` 允许无会话 CI/无头 start(不建会话绑定);子代理运行时上下文新增 `evidence` 证据位(`subagent evidence <id> --note [--artifact]`,CAS 保护、closed 可补记、不影响任务状态机);`run state` 人读摘要透出 Executor。
73
+ - 生态适配(阶段 3):新增 `./.cowork-flow/run mcp-state`——无依赖 MCP stdio 只读服务(newline-delimited JSON-RPC 2.0),工具 `task_state`(事实视图)与 `task_list`(活动任务概览);接入契约 `spec/contracts/fact-layer-access.md` 冻结只读保证与"不自创跨 agent 协议、adapter 保持薄"立场,写路径仍独占于 CLI 门禁链。
74
+ - 实现阶段守卫三件套:MCP `task_scope`/`task_specs` 只读工具(越界判定与规范清单,宿主无关、Python 单源);`<stage-contract>` 实现契约块三线注入(编辑白名单/规范入口/门禁预告/任务自声明验证命令,≤1200 字符,跨宿主逐字相等测试锁定);zcode 编辑越界实时警告(PostToolUse Edit/Write/MultiEdit 短路径,能力矩阵声明 `editScopeWarning`,其余宿主 fallback 到静态预告)。
75
+ - MCP 全局入口:npm CLI 新增 `cowork-flow mcp-state` 透传命令——从 cwd 向上定位最近 `.cowork-flow/` 并以继承 stdio exec 该项目的 `run mcp-state`;MCP 客户端全局注册一次(`cowork-flow mcp-state`)即可服务所有 cowork-flow 项目,无需逐项目配置。
20
76
 
21
77
  ## 0.0.52 - 2026-08-26
22
78
 
package/README.md CHANGED
@@ -1,16 +1,17 @@
1
1
  # cowork-flow
2
2
 
3
- > 项目协作流程模板 — 把需求、计划、实现、验证串成闭环。
3
+ > 多主机 Agent 协作的运行时上下文与协作事实层 — 让任何主机上的任何 agent,在任何会话、任何时刻,从同一份权威事实起步。
4
4
 
5
5
  ## 一句话
6
6
 
7
- **cowork-flow 不写代码,它帮你建流程。** 把 `template/` 复制到目标项目,立刻获得任务流、规格治理、开发计划和会话记录骨架。
7
+ **cowork-flow 不写代码,它给 agent 喂状态。** 把 `template/` 复制到目标项目,立刻获得运行时状态注入、任务生命周期、决策记录、规格契约与跨宿主的协作事实一致性。
8
8
 
9
9
  ## 当前能力
10
10
 
11
11
  | 领域 | 当前能力 |
12
12
  |---|---|
13
13
  | 任务流程 | `task next --json` 给出下一步 action,`task next --run` 只执行当前 action。 |
14
+ | 事实接入 | `run state [task] --json` 事实视图;`run mcp-state` 无依赖 MCP 只读服务(`task_state` / `task_list`)供任何 MCP 客户端查询。 |
14
15
  | 运行健康 | `doctor` 诊断 runtime、host assets、Skill replica 和任务 hygiene,不推进生命周期。 |
15
16
  | Host 分发 | Host Asset Manifest 驱动 Codex / OpenCode / Claude Code / ZCode / DeepSeek Harness 资产和 obsolete 清理。 |
16
17
  | 批处理与讨论 | Batch 发布 Host action;Party Mode 只输出 advisory final facts。 |
@@ -24,6 +25,7 @@
24
25
  | 理解任务如何流转 | [任务流程](#任务流程) |
25
26
  | 处理故障或漂移 | [支持与故障诊断](#支持与故障诊断) |
26
27
  | 做发布前检查 | [发布](#发布)、[`CHANGELOG.md`](CHANGELOG.md) |
28
+ | 了解项目方向与演进路线 | [`docs/direction.md`](docs/direction.md) |
27
29
  | 接入到自己项目 | [接入原则](#接入原则) |
28
30
 
29
31
  ## 适用 / 不适用
@@ -64,6 +66,10 @@ cowork-flow install-dsh-preset
64
66
  # 机器级安装实时 workflow-state 注入(推荐:不换预设,任意 DSH 会话生效)
65
67
  cowork-flow install-dsh-hook
66
68
 
69
+ # MCP 客户端接入(可选):全局注册一次,任意项目查询任务事实
70
+ # 各客户端配置样例见 docs/mcp-client-setup.md
71
+ cowork-flow mcp-state
72
+
67
73
  # 维护者发布前检查
68
74
  npm run release:check
69
75
  ```
@@ -124,6 +130,7 @@ Skills 维护在 `template/skills/` 唯一源码,`init` / `sync` 时按目录
124
130
  | `install-dsh-preset` | 安装 DSH agent 预设到 `~/.dsh/.agent-presets/cowork-flow/`(整套 agent,可选) |
125
131
  | `install-dsh-hook` | 机器级注册 workflow-state hook 组合行到 `$DSH_HOME/cordis.patch.yml`(当前 DSH 的 agent 提示不收集 host 层 section,实时注入请用预设方式) |
126
132
  | `update [--dry-run]` | 升级 CLI 本身 |
133
+ | `mcp-state` | 全局 MCP 事实入口:从 cwd 向上定位项目运行时并透传 `run mcp-state`(全局注册一次,所有 cowork-flow 项目通用) |
127
134
 
128
135
  ### init 选项
129
136
 
@@ -325,6 +332,8 @@ npm run release -- --version 0.1.0 # 精确发布指定版本(跳过自动 bu
325
332
  5. `git commit` + `git tag`
326
333
  6. `npm publish`
327
334
 
335
+ **CI 发布通道(推荐):** `scripts/release.sh --version <v>` 打好 tag 后,`gh release create v<v>` 触发 `.github/workflows/publish.yml`——Ubuntu/Windows 双平台全量门禁通过后自动 `npm publish`(需仓库 secret `NPM_TOKEN`,权限:publish)。
336
+
328
337
  - 发布说明维护在 `CHANGELOG.md`;发布前更新当前版本段落,并保留 `release:check` 和 `git diff --check` 证据。
329
338
 
330
339
  CI 的 PR 同时运行 Ubuntu core 与 Windows core;发布工作流要求 Ubuntu 与 Windows full verification 均成功后才执行 publish。测试 job 不接触 `NPM_TOKEN`,仅 publish job 使用该 secret。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "1.0.0",
3
+ "version": "1.1.3",
4
4
  "description": "CLI for installing and maintaining cowork-flow collaboration templates.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -51,6 +51,7 @@ import sys
51
51
  from pathlib import Path
52
52
 
53
53
  cwd = sys.argv[1]
54
+ full_digest = len(sys.argv) > 2 and sys.argv[2] != "0"
54
55
  current = Path(cwd).resolve()
55
56
  root = None
56
57
  while True:
@@ -77,6 +78,7 @@ print(build_hook_context(
77
78
  "runtime_context_identity: formal subagent sessions bind before workflow-state injection\\n"
78
79
  "</dsh-runtime>",
79
80
  ),
81
+ session_start=full_digest,
80
82
  ))
81
83
  `;
82
84
 
@@ -127,11 +129,11 @@ function discoverCandidates() {
127
129
  }
128
130
 
129
131
 
130
- function tryPython(candidate, cwd) {
132
+ function tryPython(candidate, cwd, full = true) {
131
133
  return new Promise((resolve) => {
132
134
  execFile(
133
135
  candidate.command,
134
- [...candidate.args, '-c', PYTHON_PROTOCOL, cwd],
136
+ [...candidate.args, '-c', PYTHON_PROTOCOL, cwd, full ? '1' : '0'],
135
137
  EXEC_OPTIONS,
136
138
  (error, stdout) => {
137
139
  if (error) {
@@ -192,10 +194,12 @@ export async function findCoworkRoot(cwd) {
192
194
 
193
195
  /**
194
196
  * Produce the `<workflow-state>` hook context block for a workspace cwd.
195
- * Empty string means "contribute nothing" (no root, hooks disabled, no
196
- * usable interpreter, or a broken runtime).
197
+ * `full` selects the digest shape: true (default) for the full contract
198
+ * block on session-start-like refreshes, false for the single fingerprint
199
+ * line on intra-turn refreshes. Empty string means "contribute nothing"
200
+ * (no root, hooks disabled, no usable interpreter, or a broken runtime).
197
201
  */
198
- export async function runWorkflowState(cwd) {
202
+ export async function runWorkflowState(cwd, full = true) {
199
203
  if (hooksDisabled() || !cwd) {
200
204
  return '';
201
205
  }
@@ -212,14 +216,14 @@ export async function runWorkflowState(cwd) {
212
216
  return '';
213
217
  }
214
218
  if (workingPython !== null) {
215
- const result = await tryPython(workingPython, cwd);
219
+ const result = await tryPython(workingPython, cwd, full);
216
220
  if (!result.missing) {
217
221
  return normalize(result.output);
218
222
  }
219
223
  workingPython = null;
220
224
  }
221
225
  for (const candidate of discoverCandidates()) {
222
- const result = await tryPython(candidate, cwd);
226
+ const result = await tryPython(candidate, cwd, full);
223
227
  if (result.missing) {
224
228
  continue;
225
229
  }
@@ -231,11 +235,15 @@ export async function runWorkflowState(cwd) {
231
235
 
232
236
 
233
237
  export function apply(ctx) {
234
- // One cache entry per agent: { text, inflight, queued }. The WeakMap key
235
- // is the live Agent object the events and the assembly context both carry.
238
+ // One cache entry per agent: { text, inflight, queued, full }.
239
+ // `full` records the digest shape the next refresh should use: session
240
+ // starts and inbox claims re-inject the full contract block, intra-turn
241
+ // refreshes after lifecycle commands only repeat the fingerprint line.
242
+ // The WeakMap key is the live Agent object the events and the assembly
243
+ // context both carry.
236
244
  const states = new WeakMap();
237
245
 
238
- const refresh = (agent) => {
246
+ const refresh = (agent, full = true) => {
239
247
  const cwd = agent && agent.cwd;
240
248
  if (!cwd) {
241
249
  return;
@@ -245,6 +253,7 @@ export function apply(ctx) {
245
253
  entry = { text: '', inflight: false, queued: false };
246
254
  states.set(agent, entry);
247
255
  }
256
+ entry.full = full;
248
257
  if (entry.inflight) {
249
258
  // A refresh arrived while one was running (e.g. several lifecycle
250
259
  // commands back to back). Re-run once after it settles so the latest
@@ -253,7 +262,7 @@ export function apply(ctx) {
253
262
  return;
254
263
  }
255
264
  entry.inflight = true;
256
- runWorkflowState(cwd)
265
+ runWorkflowState(cwd, entry.full)
257
266
  .then((text) => {
258
267
  entry.text = text;
259
268
  })
@@ -264,21 +273,22 @@ export function apply(ctx) {
264
273
  entry.inflight = false;
265
274
  if (entry.queued) {
266
275
  entry.queued = false;
267
- refresh(agent);
276
+ refresh(agent, entry.full);
268
277
  }
269
278
  });
270
279
  };
271
280
 
272
- ctx.on('agent/session-start', (payload) => refresh(payload && payload.agent));
273
- ctx.on('agent/inbox/claimed', (payload) => refresh(payload && payload.agent));
281
+ ctx.on('agent/session-start', (payload) => refresh(payload && payload.agent, true));
282
+ ctx.on('agent/inbox/claimed', (payload) => refresh(payload && payload.agent, true));
274
283
  // Intra-turn refresh: a lifecycle command just settled, so the next prompt
275
284
  // assembly should already see the new state instead of the stale block.
285
+ // Same-session refresh, so only the fingerprint line is repeated.
276
286
  ctx.on('tools/result', (exec) => {
277
287
  const agent = exec && exec.agent;
278
288
  if (!agent || !isLifecycleCommand(exec && exec.arguments)) {
279
289
  return;
280
290
  }
281
- refresh(agent);
291
+ refresh(agent, false);
282
292
  });
283
293
 
284
294
  ctx.effect(() => ctx.systemPrompt.section({
package/src/cli.js CHANGED
@@ -6,6 +6,7 @@ import { runInit } from './commands/init.js';
6
6
  import { runInstallDshHook } from './commands/install-dsh-hook.js';
7
7
  import { runInstallDshPreset } from './commands/install-dsh-preset.js';
8
8
  import { runInstallZCodePlugin } from './commands/install-zcode-plugin.js';
9
+ import { runMcpState } from './commands/mcp-state.js';
9
10
  import { runSourceRefresh } from './commands/source-refresh.js';
10
11
  import { runSync } from './commands/sync.js';
11
12
  import { runUpdate } from './commands/update.js';
@@ -21,6 +22,7 @@ Usage:
21
22
  cowork-flow update
22
23
  cowork-flow sync [target] [--dry-run] [--force]
23
24
  cowork-flow source-refresh [target] [--dry-run]
25
+ cowork-flow mcp-state
24
26
  cowork-flow --version
25
27
  cowork-flow --help
26
28
  `;
@@ -199,6 +201,10 @@ export async function main(argv = process.argv.slice(2), options = {}) {
199
201
  return await runInstallDshHook(args);
200
202
  }
201
203
 
204
+ if (command === 'mcp-state') {
205
+ return await runMcpState(args);
206
+ }
207
+
202
208
  io.writeErr(`Unknown command: ${command}\n`);
203
209
  return 1;
204
210
  } catch (error) {
@@ -0,0 +1,66 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+
5
+ const DIR_WORKFLOW = '.cowork-flow';
6
+
7
+ function defaultIo() {
8
+ return {
9
+ writeOut(message) {
10
+ process.stdout.write(message);
11
+ },
12
+ writeErr(message) {
13
+ process.stderr.write(message);
14
+ },
15
+ };
16
+ }
17
+
18
+ function findNearestWorkflowRoot(startDir) {
19
+ let current = resolve(startDir);
20
+ // Same semantics as the MCP server's root resolution (get_repo_root):
21
+ // walk up to the nearest directory containing .cowork-flow/, so a client
22
+ // launching the globally registered server inside any project subdirectory
23
+ // still lands on the right facts.
24
+ for (;;) {
25
+ if (existsSync(join(current, DIR_WORKFLOW))) {
26
+ return current;
27
+ }
28
+ const parent = resolve(current, '..');
29
+ if (parent === current) {
30
+ return null;
31
+ }
32
+ current = parent;
33
+ }
34
+ }
35
+
36
+ export async function runMcpState(args, options = {}) {
37
+ const io = options.io ?? defaultIo(); const root = findNearestWorkflowRoot(process.cwd());
38
+ if (root === null) {
39
+ io.writeErr(
40
+ 'Error: no .cowork-flow/ found from the current directory; '
41
+ + 'run this inside a cowork-flow project.\n'
42
+ );
43
+ return 1;
44
+ }
45
+
46
+ const runnerName = process.platform === 'win32' ? 'run.cmd' : 'run';
47
+ const runner = join(root, DIR_WORKFLOW, runnerName);
48
+ if (!existsSync(runner)) {
49
+ io.writeErr(`Error: workflow runner not found: ${runner}\n`);
50
+ return 1;
51
+ }
52
+
53
+ return await new Promise((resolveExit) => {
54
+ const child = spawn(runner, ['mcp-state', ...args], {
55
+ stdio: 'inherit',
56
+ cwd: root,
57
+ });
58
+ child.on('error', (error) => {
59
+ io.writeErr(`Error: failed to launch ${runner}: ${error.message}\n`);
60
+ resolveExit(1);
61
+ });
62
+ child.on('exit', (code) => {
63
+ resolveExit(code ?? 1);
64
+ });
65
+ });
66
+ }
@@ -63,6 +63,11 @@ def main() -> int:
63
63
  sys.path.insert(0, str(scripts_dir))
64
64
  from adapters.host.workflow_state_hook import build_hook_context
65
65
 
66
+ event_name = (
67
+ hook_input.get("hook_event_name")
68
+ or hook_input.get("hookEventName")
69
+ or "UserPromptSubmit"
70
+ )
66
71
  context = build_hook_context(
67
72
  root,
68
73
  hook_input,
@@ -75,11 +80,7 @@ def main() -> int:
75
80
  "</claude-code-runtime>"
76
81
  ),
77
82
  ),
78
- )
79
- event_name = (
80
- hook_input.get("hook_event_name")
81
- or hook_input.get("hookEventName")
82
- or "UserPromptSubmit"
83
+ session_start=(event_name == "SessionStart"),
83
84
  )
84
85
  print(
85
86
  json.dumps(
@@ -61,6 +61,11 @@ def main() -> int:
61
61
  )
62
62
 
63
63
  dispatch_mode = codex_dispatch_mode(root)
64
+ event_name = (
65
+ hook_input.get("hook_event_name")
66
+ or hook_input.get("hookEventName")
67
+ or "UserPromptSubmit"
68
+ )
64
69
  context = build_hook_context(
65
70
  root,
66
71
  hook_input,
@@ -77,12 +82,15 @@ def main() -> int:
77
82
  "</codex-runtime>"
78
83
  ),
79
84
  ),
85
+ # Codex registers UserPromptSubmit only, so event_name carries no
86
+ # session-start signal; build_hook_context falls back to the session
87
+ # state file probe (full on first injection, slim afterwards).
80
88
  )
81
89
  print(
82
90
  json.dumps(
83
91
  {
84
92
  "hookSpecificOutput": {
85
- "hookEventName": "UserPromptSubmit",
93
+ "hookEventName": event_name,
86
94
  "additionalContext": context,
87
95
  }
88
96
  },
@@ -1 +1 @@
1
- 1.0.0
1
+ 1.1.3
@@ -13,6 +13,7 @@ capabilities:
13
13
  runtimeContextDispatch: native
14
14
  runtimeContextBinding: shim
15
15
  runtimeContextCleanup: native
16
+ editScopeWarning: unsupported
16
17
  dispatch:
17
18
  primitive: subagent
18
19
  agentsPath: .claude/agents
@@ -13,6 +13,7 @@ capabilities:
13
13
  runtimeContextDispatch: native
14
14
  runtimeContextBinding: shim
15
15
  runtimeContextCleanup: native
16
+ editScopeWarning: unsupported
16
17
  dispatch:
17
18
  primitive: spawn_agent
18
19
  forkTurns: none
@@ -13,6 +13,7 @@ capabilities:
13
13
  runtimeContextDispatch: native
14
14
  runtimeContextBinding: shim
15
15
  runtimeContextCleanup: shim
16
+ editScopeWarning: unsupported
16
17
  dispatch:
17
18
  primitive: subagent
18
19
  skillsPath: .agents/skills
@@ -13,6 +13,7 @@ capabilities:
13
13
  runtimeContextDispatch: native
14
14
  runtimeContextBinding: shim
15
15
  runtimeContextCleanup: native
16
+ editScopeWarning: unsupported
16
17
  dispatch:
17
18
  primitive: task
18
19
  agentsPath: .opencode/agents
@@ -13,6 +13,7 @@ capabilities:
13
13
  runtimeContextDispatch: native
14
14
  runtimeContextBinding: shim
15
15
  runtimeContextCleanup: native
16
+ editScopeWarning: plugin
16
17
  dispatch:
17
18
  primitive: subagent
18
19
  agentsPath: .zcode/agents
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """`run state [task] [--json]` — the fact-layer read entry.
4
+
5
+ Assembles one machine-readable fact view per task (task.json, decision-anchor
6
+ essentials, plan binding, bound sessions, trusted snapshot). See
7
+ services/fact_view.py and docs/direction.md stage 1.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import json
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ if __package__:
18
+ from . import _bootstrap as _bootstrap # noqa: F401
19
+ else:
20
+ import _bootstrap # noqa: F401
21
+
22
+ from infra.paths import get_repo_root
23
+ from runtime.session_state import get_active_task
24
+ from services.fact_view import build_fact_view
25
+ from services.task_repository import TaskRepository, TaskRepositoryError
26
+
27
+
28
+ def _human_summary(view: dict) -> str:
29
+ task = view.get("task") or {}
30
+ lines = [
31
+ f"Task: {view.get('taskPath')}",
32
+ f"Status: {task.get('status')}",
33
+ ]
34
+ executor = task.get("executor")
35
+ if executor:
36
+ lines.append(f"Executor: {executor}")
37
+ anchor = view.get("decisionAnchor") or {}
38
+ if anchor.get("exists"):
39
+ goal = anchor.get("goal") or ""
40
+ lines.append(f"Goal: {goal.splitlines()[0] if goal else ''}")
41
+ criteria = anchor.get("acceptanceCriteria") or []
42
+ lines.append(f"Acceptance criteria: {len(criteria)}")
43
+ rejected = anchor.get("rejectedOptions") or []
44
+ if rejected:
45
+ lines.append(f"Rejected options: {len(rejected)}")
46
+ plan = view.get("plan") or {}
47
+ if plan.get("bound"):
48
+ lines.append(f"Plan: {plan.get('file')}")
49
+ sessions = view.get("sessions") or []
50
+ lines.append(f"Bound sessions: {len(sessions)}")
51
+ snapshot = view.get("snapshot")
52
+ lines.append(
53
+ f"Snapshot: {snapshot.get('status') if snapshot else 'none'}"
54
+ )
55
+ return "\n".join(lines)
56
+
57
+
58
+ def main() -> int:
59
+ parser = argparse.ArgumentParser(
60
+ prog="state",
61
+ description="Show the fact view for a task (machine-readable with --json).",
62
+ )
63
+ parser.add_argument(
64
+ "task",
65
+ nargs="?",
66
+ default=None,
67
+ help="Task directory or name; defaults to the session-bound active task.",
68
+ )
69
+ parser.add_argument(
70
+ "--json",
71
+ action="store_true",
72
+ dest="as_json",
73
+ help="Print the fact view as JSON.",
74
+ )
75
+ args = parser.parse_args()
76
+
77
+ repo_root = get_repo_root()
78
+ target = args.task
79
+ if target is None:
80
+ active = get_active_task(repo_root)
81
+ target = active.task_path
82
+ if not target:
83
+ if args.as_json:
84
+ print(
85
+ json.dumps(
86
+ {
87
+ "schemaVersion": 1,
88
+ "task": None,
89
+ "reason": "no-active-task",
90
+ },
91
+ ensure_ascii=False,
92
+ )
93
+ )
94
+ return 0
95
+ print(
96
+ "No active task bound to this session; pass a task directory "
97
+ "or name.",
98
+ file=sys.stderr,
99
+ )
100
+ return 1
101
+
102
+ try:
103
+ task_dir = TaskRepository(repo_root).resolve(target)
104
+ except TaskRepositoryError as error:
105
+ print(f"Error: {error}", file=sys.stderr)
106
+ return 1
107
+
108
+ if not (Path(task_dir) / "task.json").is_file():
109
+ print(f"Error: task.json not found under {task_dir}", file=sys.stderr)
110
+ return 1
111
+
112
+ view = build_fact_view(repo_root, Path(task_dir))
113
+ if args.as_json:
114
+ print(json.dumps(view, ensure_ascii=False, indent=2))
115
+ else:
116
+ print(_human_summary(view))
117
+ return 0
118
+
119
+
120
+ if __name__ == "__main__":
121
+ sys.exit(main())
@@ -273,6 +273,25 @@ def cmd_bind(args: argparse.Namespace) -> int:
273
273
  return 0
274
274
 
275
275
 
276
+ def cmd_evidence(args: argparse.Namespace) -> int:
277
+ repo_root = get_repo_root()
278
+ try:
279
+ context = RuntimeContextService(repo_root).evidence(
280
+ args.subagent_id,
281
+ note=args.note,
282
+ artifact=getattr(args, "artifact", None),
283
+ )
284
+ except RuntimeContextError as error:
285
+ _print_runtime_error(error)
286
+ return 1
287
+ if context is None:
288
+ print(f"Error: subagent not found: {args.subagent_id}", file=sys.stderr)
289
+ return 1
290
+ entries = context.get("evidence") or []
291
+ print(f"subagent {args.subagent_id} evidence entries={len(entries)}")
292
+ return 0
293
+
294
+
276
295
  def cmd_close(args: argparse.Namespace) -> int:
277
296
  repo_root = get_repo_root()
278
297
  try:
@@ -322,6 +341,15 @@ def build_parser() -> argparse.ArgumentParser:
322
341
  bind.add_argument("context_key")
323
342
  bind.set_defaults(func=cmd_bind)
324
343
 
344
+ evidence = subparsers.add_parser(
345
+ "evidence",
346
+ help="Append an evidence entry to a runtime context (does not move task status)",
347
+ )
348
+ evidence.add_argument("subagent_id")
349
+ evidence.add_argument("--note", required=True)
350
+ evidence.add_argument("--artifact", default=None)
351
+ evidence.set_defaults(func=cmd_evidence)
352
+
325
353
  close = subparsers.add_parser("close", help="Close subagent runtime context")
326
354
  close.add_argument("subagent_id")
327
355
  close.set_defaults(func=cmd_close)
@@ -12,15 +12,17 @@ if __package__:
12
12
  else:
13
13
  import _bootstrap # noqa: F401
14
14
 
15
+ from services.context_discovery import (
16
+ get_implement_backend,
17
+ get_implement_base,
18
+ get_implement_frontend,
19
+ get_implement_spec,
20
+ )
15
21
  from services.task_context import (
16
22
  detect_installed_platforms as _detect_installed_platforms,
17
23
  discover_spec_files as _discover_spec_files,
18
24
  get_check_context,
19
25
  get_debug_context,
20
- get_implement_backend,
21
- get_implement_base,
22
- get_implement_frontend,
23
- get_implement_spec,
24
26
  is_skill_path as _is_skill_path,
25
27
  iter_jsonl_lines as _iter_jsonl_lines,
26
28
  skill_path as _skill_path,
@@ -266,7 +266,11 @@ def cmd_start(args: argparse.Namespace) -> int:
266
266
  if getattr(args, "auto", False):
267
267
  return _run_auto_start(args, full_path, service)
268
268
 
269
- result = service.start(full_path)
269
+ result = service.start(
270
+ full_path,
271
+ executor=getattr(args, "executor", None),
272
+ takeover=bool(getattr(args, "takeover", False)),
273
+ )
270
274
  if not result.ok:
271
275
  return _report_start_failure(result)
272
276