cowork-flow 1.2.0 → 1.4.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 +45 -0
- package/README.md +4 -3
- package/package.json +1 -1
- package/template/.claude/hooks/inject-workflow-state.py +12 -98
- package/template/.codex/hooks/inject-workflow-state.py +13 -89
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/scripts/adapters/cli/task.py +4 -1
- package/template/.cowork-flow/scripts/adapters/cli/task_context_commands.py +65 -0
- package/template/.cowork-flow/scripts/adapters/cli/task_parser.py +23 -0
- package/template/.cowork-flow/scripts/adapters/host/claude_code_policy.py +52 -0
- package/template/.cowork-flow/scripts/adapters/host/codex_policy.py +61 -0
- package/template/.cowork-flow/scripts/adapters/host/inject.py +249 -0
- package/template/.cowork-flow/scripts/adapters/host/workflow_state_hook.py +315 -38
- package/template/.cowork-flow/scripts/adapters/host/zcode_policy.py +285 -0
- package/template/.cowork-flow/scripts/adapters/mcp/state_server.py +34 -11
- package/template/.cowork-flow/scripts/runtime/session_state.py +54 -9
- package/template/.cowork-flow/scripts/services/context_discovery.py +18 -5
- package/template/.cowork-flow/scripts/services/fact_view.py +14 -0
- package/template/.cowork-flow/scripts/services/spec_check.py +34 -12
- package/template/.cowork-flow/scripts/services/task_context.py +0 -1
- package/template/.cowork-flow/spec/contracts/context-injection.md +40 -23
- package/template/.cowork-flow/spec/contracts/fact-layer-access.md +43 -8
- package/template/.cowork-flow/spec/contracts/spec-checks.md +40 -8
- package/template/.cowork-flow/spec/contracts/subagent-dispatch.md +21 -0
- package/template/.cowork-flow/spec/runtime/contract-registry.json +12 -0
- package/template/.opencode/plugins/cowork-flow.js +12 -0
- package/template/.zcode/.zcode-plugin/plugin.json +1 -1
- package/template/.zcode/hooks/inject-context.js +95 -971
- package/template/skills/agent-dispatch/SKILL.md +15 -0
- package/template/skills/runtime-health/scripts/doctor.py +90 -0
- package/template/skills/task-planning/SKILL.md +1 -0
- package/template/skills/task-review/SKILL.md +1 -0
- package/template/.zcode/hooks/inject-context.selfcheck.mjs +0 -795
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.4.0 - 2026-09-14
|
|
4
|
+
|
|
5
|
+
### Hook 会话身份绑定(fix(runtime))
|
|
6
|
+
|
|
7
|
+
实弹暴露的多窗口交叉读取问题:zcode hook 会话携带对话自身的 session id,而任务激活发生在 Bash CLI 的显式身份下,导致未绑定的 hook 会话回退到 newest-session 时可能**读到另一个窗口的任务**。本版把绑定与回退语义一起收紧:
|
|
8
|
+
|
|
9
|
+
- **激活即认领**:PostToolUse 在激活类命令(`task start` / `task next <dir> --run` / `resume <dir>`)之后,把 hook 会话自己的 `zcode_<sessionId>` 绑定认领下来(`session_state.claim_active_task`);查询类命令与不可信身份(process-fallback/缺失)永不认领。
|
|
10
|
+
- **回退语义收紧**:`fallback_for_unbound` 仅在**恰好存在一个** main-session 绑定时跟随最新的那个;存在多个绑定时,未绑定 hook 会话渲染 no_task + rebind 提示,绝不交叉读取其他窗口。
|
|
11
|
+
- **会话身份显式化**:workflow-state 头部新增 `session=<contextKey>` 属性,模型可将其作为 `COWORK_FLOW_CONTEXT_ID` 传给后续 CLI 调用,打通 hook 会话与 CLI 身份。
|
|
12
|
+
- **MCP 拒绝隐式冒认**:`task_state` / `task_scope` / `task_list` 在不可信身份下拒绝隐式解析(`identity-untrusted`),不再冒认 process-fallback 任务;CLI `FALLBACK_BINDING_BLOCKER` 语义不变(回归护栏锁定)。
|
|
13
|
+
- 契约文档 context-injection.md 同步记录 session 属性与回退语义;新增 pytest 396 行覆盖(active_task_runtime / inject_entry / mcp_state_server / workflow_state_hook)。
|
|
14
|
+
|
|
15
|
+
## 1.3.0 - 2026-09-09
|
|
16
|
+
|
|
17
|
+
### 跨宿主适配通用化(注入单源化 + MCP 重定位)
|
|
18
|
+
|
|
19
|
+
注入事实逻辑收拢到单一 Python 源,MCP 定位收口为"外部/无 hook 宿主的通用 pull 通道":
|
|
20
|
+
|
|
21
|
+
- **宿主中立入口 `inject.py`**:zcode shim、claude-code、codex 全部走同一入口(`--host` 选择信封与形状),wrapper 瘦身为 ≤20 行委托。digest policy 措辞、registry-warning 抑制(zcode)、preamble、信封缩进全部按 host 参数化,仍由 context-injection.md 契约锁定。
|
|
22
|
+
- **zcode shim 化**:`inject-context.js` 从 ~1000 行事实镜像降为 ~140 行传输层——事件路由、PostToolUse(Bash) 廉价过滤(非生命周期命令零 spawn)、解释器定位(项目 runtime 优先、插件缓存副本兜底)、stdin/stdout 转发。删除 `inject-context.selfcheck.mjs`。matrix 逐字相等测试中的 zcode 线改为经 shim 驱动单源。
|
|
23
|
+
- **每宿主策略模块**:`adapters/host` 拆出 zcode/claude_code/codex policy(digest 措辞、警告静默、rebind 提示、essential-files、PostToolUse 传输、emit 格式化),共享层与入口零 `host ==` 分支;无模块宿主(dsh)走中立默认。字节级契约不变(fingerprint、stage-contract matrix 绿)。
|
|
24
|
+
- **JS-only 行为移植进 Python 单源**:`formatRebindHints`(zcode 无任务体补活动任务列表)、`editScopeWarning`(逐文件 scope 白名单警告,与 spec 警告合并进同一载荷)、missing-task 统一文案(原 "stale" 通用回退改为明确指向任务目录不存在 + 建任务指引,全宿主生效)、`checkEssentialFiles` 缺文件警告(zcode)、无会话身份时的 newest-session 显示回退(zcode,仅显示不改写绑定语义)。
|
|
25
|
+
- **MCP 重定位**:`task_scope` / `task_specs` 补 CLI 同源事实命令(`task scope` / `task specs`),MCP 不再有独占能力;contract-registry 注册 `FACT_LAYER_ACCESS_V1` 契约(digest 行注入"MCP 优先查询事实,CLI 兜底");doctor 新增 MCP 注册健康项(项目级 .mcp.json 存在性 / 全局+项目双重注册提示 / 缺失提示),只读 advisory 不阻断;README 增加两档注册说明(全局受支持默认 + 项目 opt-in 地图)。
|
|
26
|
+
- **修复(1.2.0 遗留,Windows)**:`spec_check._run_command` 三处——含引号命令经 list2cmdline 转义后 cmd.exe 解析失败;`text=True` 未显式 `encoding="utf-8", errors="replace"`,中文 Windows cmd GBK 输出致解码线程崩溃、输出丢失;命令入口缺失在 Windows 被误分类为 violation(现经 `shutil.which` 归 unchecked,契约三态语义恢复一致)。
|
|
27
|
+
- **修复**:legacy runtime context 文件(缺 `runtime_context_id` 字段)注入显示回退到检测到的 id。
|
|
28
|
+
|
|
29
|
+
**行为变更**:delegated 绑定写语义统一为 Python(zcode 注入时新增 bind 写,原 JS 为只读);zcode missing-task 文案与全宿主统一;digest/fingerprint 因注册 `FACT_LAYER_ACCESS_V1` 而变化(预期内,change guard 允许)。
|
|
30
|
+
|
|
31
|
+
### 实弹修正与 delegated 自检协议化(fix)
|
|
32
|
+
|
|
33
|
+
实弹验证(zcode 插件 live proof)暴露四个缺陷并修复:
|
|
34
|
+
|
|
35
|
+
- **未绑定会话的编辑期警告可达**:zcode hook 会话携带对话自身 id、从不绑定(激活发生在 Bash CLI 的显式身份下),empty-session 此前跳过 newest-session 回退——主会话的编辑期 spec/scope 警告完全静默。回退现覆盖 empty-session,`spec_edit_warning` 的 zcode 路由经它放行(claude-code/codex 保持严格身份)。
|
|
36
|
+
- **scope 路径相对化**:`edit_scope_warning` 此前拿原始绝对路径与仓库相对白名单比对,所有编辑都被误标越界;现与 spec 路径同样归一为仓库相对,警告行显示相对路径。
|
|
37
|
+
- **编辑期预算与节流加固**:edit 钳制 3s→2.5s(子进程预算余量);节流只在跑完一次后写入(执行器崩溃不再吞掉节流窗口);`files` 多值列表逐 token 剥引号。
|
|
38
|
+
- **delegated 子代理收到 spec 反馈**:编辑期 spec 警告的静默范围此前在 Python/JS 两侧不一致,现统一为 delegated 会话可达(scope 警告保持仅主会话)。
|
|
39
|
+
|
|
40
|
+
配套协议化(文档与 skill):实弹两轮证实宿主 PostToolUse 只覆盖主会话流——Agent 子代理收不到编辑期反馈。按已验证的 CLI 拉取路径固化:子代理报告前跑 `run spec-check` 自检修复(subagent-dispatch.md、agent-dispatch SKILL);父侧验收把 violation 作为验收阻断项(task-review SKILL,delegated 工作最早的可靠检查点);task-planning 的实现 Verify 命令默认含 spec-check 自检;spec-checks.md 记录缺口、四部分补偿与治理方向(向宿主请求;原生的那一环降级为冗余保险)。
|
|
41
|
+
|
|
42
|
+
**已知合法副作用**:contract fingerprint 更新为 `d1d0e2536e8fa150`(内容派生;Python 与 JS 镜像重算一致)。
|
|
43
|
+
|
|
44
|
+
### 发布门禁加固(release.sh)
|
|
45
|
+
|
|
46
|
+
- 同步 `--force` 自实例镜像时带 AGENTS.md 定制守卫(脏树预检、sync 后从 HEAD 还原);pytest services 门禁前置到 `test:all` 之前;`.gitattributes` 将 `*.version` 钉为 LF(fingerprint 字节比对前提)。
|
|
47
|
+
|
|
3
48
|
## 1.2.0 - 2026-09-08
|
|
4
49
|
|
|
5
50
|
### 规范挂命令(spec 约束前移)
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
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
|
+
| 事实接入 | `run state [task] --json` 事实视图;`run mcp-state` 无依赖 MCP 只读服务(`task_state` / `task_list` / `task_scope` / `task_specs`)供任何 MCP 客户端查询;`task scope` / `task specs` 为 CLI 同源事实命令。 |
|
|
15
15
|
| 运行健康 | `doctor` 诊断 runtime、host assets、Skill replica 和任务 hygiene,不推进生命周期。 |
|
|
16
16
|
| Host 分发 | Host Asset Manifest 驱动 Codex / OpenCode / Claude Code / ZCode / DeepSeek Harness 资产和 obsolete 清理。 |
|
|
17
17
|
| 批处理与讨论 | Batch 发布 Host action;Party Mode 只输出 advisory final facts。 |
|
|
@@ -66,8 +66,9 @@ cowork-flow install-dsh-preset
|
|
|
66
66
|
# 机器级安装实时 workflow-state 注入(推荐:不换预设,任意 DSH 会话生效)
|
|
67
67
|
cowork-flow install-dsh-hook
|
|
68
68
|
|
|
69
|
-
# MCP
|
|
70
|
-
#
|
|
69
|
+
# MCP 客户端接入(可选):全局注册一次,任意项目查询任务事实;
|
|
70
|
+
# 项目级 opt-in(如 claude-code 的 .mcp.json)与注册健康检测见
|
|
71
|
+
# `run doctor`,各客户端配置样例见 docs/mcp-client-setup.md
|
|
71
72
|
cowork-flow mcp-state
|
|
72
73
|
|
|
73
74
|
# 维护者发布前检查
|
package/package.json
CHANGED
|
@@ -1,114 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""Claude Code hook: thin delegate to the host-neutral injection entry.
|
|
3
|
+
|
|
4
|
+
All workflow facts and host shaping (preamble, envelope, exit codes) live in
|
|
5
|
+
adapters/host/inject.py — this file only locates the project runtime next to
|
|
6
|
+
itself (.claude/hooks/ → project root) and forwards with --host claude-code.
|
|
7
|
+
"""
|
|
3
8
|
|
|
4
9
|
from __future__ import annotations
|
|
5
10
|
|
|
6
|
-
import json
|
|
7
|
-
import os
|
|
8
11
|
import sys
|
|
9
12
|
from pathlib import Path
|
|
10
|
-
from typing import Any
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def _configure_stdio() -> None:
|
|
14
|
-
for stream in (sys.stdout, sys.stderr):
|
|
15
|
-
if hasattr(stream, "reconfigure"):
|
|
16
|
-
stream.reconfigure(encoding="utf-8", errors="replace")
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _read_input() -> dict[str, Any]:
|
|
20
|
-
raw = sys.stdin.buffer.read()
|
|
21
|
-
if not raw.strip():
|
|
22
|
-
return {}
|
|
23
|
-
try:
|
|
24
|
-
data = json.loads(raw.decode("utf-8-sig"))
|
|
25
|
-
except (UnicodeDecodeError, json.JSONDecodeError, ValueError):
|
|
26
|
-
return {}
|
|
27
|
-
if not isinstance(data, dict):
|
|
28
|
-
return {}
|
|
29
|
-
if "claude_session_id" not in data:
|
|
30
|
-
session_id = data.get("session_id")
|
|
31
|
-
if isinstance(session_id, str) and session_id.strip():
|
|
32
|
-
data["claude_session_id"] = session_id
|
|
33
|
-
return data
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def _find_root(start: Path) -> Path | None:
|
|
37
|
-
current = start.resolve()
|
|
38
|
-
while True:
|
|
39
|
-
if (current / ".cowork-flow").is_dir():
|
|
40
|
-
return current
|
|
41
|
-
if current == current.parent:
|
|
42
|
-
return None
|
|
43
|
-
current = current.parent
|
|
44
13
|
|
|
45
14
|
|
|
46
15
|
def main() -> int:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return 0
|
|
52
|
-
hook_input = _read_input()
|
|
53
|
-
cwd_value = hook_input.get("cwd")
|
|
54
|
-
cwd = (
|
|
55
|
-
Path(cwd_value)
|
|
56
|
-
if isinstance(cwd_value, str) and cwd_value.strip()
|
|
57
|
-
else Path.cwd()
|
|
58
|
-
)
|
|
59
|
-
root = _find_root(cwd)
|
|
60
|
-
if root is None:
|
|
16
|
+
project_root = Path(__file__).resolve().parents[2]
|
|
17
|
+
scripts_dir = project_root / ".cowork-flow" / "scripts"
|
|
18
|
+
if not scripts_dir.is_dir():
|
|
19
|
+
# Outside any cowork-flow project: stay silent like the entry does.
|
|
61
20
|
return 0
|
|
62
|
-
scripts_dir = root / ".cowork-flow" / "scripts"
|
|
63
21
|
sys.path.insert(0, str(scripts_dir))
|
|
22
|
+
from adapters.host.inject import main as inject_main
|
|
64
23
|
|
|
65
|
-
|
|
66
|
-
hook_input.get("hook_event_name")
|
|
67
|
-
or hook_input.get("hookEventName")
|
|
68
|
-
or "UserPromptSubmit"
|
|
69
|
-
)
|
|
70
|
-
if event_name == "PostToolUse":
|
|
71
|
-
# Editor-phase spec-check short path: at most one advisory line on
|
|
72
|
-
# stderr (exit 2 surfaces it to the model; the edit itself has
|
|
73
|
-
# already happened). Nothing to report exits 0 silently.
|
|
74
|
-
from adapters.host.workflow_state_hook import spec_edit_warning
|
|
75
|
-
|
|
76
|
-
warning = spec_edit_warning(root, hook_input)
|
|
77
|
-
if warning:
|
|
78
|
-
print(warning, file=sys.stderr)
|
|
79
|
-
return 2
|
|
80
|
-
return 0
|
|
81
|
-
|
|
82
|
-
from adapters.host.workflow_state_hook import build_hook_context
|
|
83
|
-
|
|
84
|
-
context = build_hook_context(
|
|
85
|
-
root,
|
|
86
|
-
hook_input,
|
|
87
|
-
host="claude-code",
|
|
88
|
-
adapter="claude-code.hooks",
|
|
89
|
-
preamble=(
|
|
90
|
-
(
|
|
91
|
-
"<claude-code-runtime>\n"
|
|
92
|
-
"hooks: UserPromptSubmit, SessionStart, PostToolUse\n"
|
|
93
|
-
"</claude-code-runtime>"
|
|
94
|
-
),
|
|
95
|
-
),
|
|
96
|
-
session_start=(event_name == "SessionStart"),
|
|
97
|
-
)
|
|
98
|
-
print(
|
|
99
|
-
json.dumps(
|
|
100
|
-
{
|
|
101
|
-
"hookSpecificOutput": {
|
|
102
|
-
"hookEventName": event_name,
|
|
103
|
-
"additionalContext": context,
|
|
104
|
-
}
|
|
105
|
-
},
|
|
106
|
-
ensure_ascii=False,
|
|
107
|
-
)
|
|
108
|
-
)
|
|
109
|
-
return 0
|
|
24
|
+
return inject_main(["--host", "claude-code"])
|
|
110
25
|
|
|
111
26
|
|
|
112
27
|
if __name__ == "__main__":
|
|
113
|
-
_configure_stdio()
|
|
114
28
|
raise SystemExit(main())
|
|
@@ -1,105 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""Codex hook: thin delegate to the host-neutral injection entry.
|
|
3
|
+
|
|
4
|
+
All workflow facts and host shaping (dispatch-mode preamble, envelope,
|
|
5
|
+
session-probe digest semantics) live in adapters/host/inject.py — this file
|
|
6
|
+
only locates the project runtime next to itself (.codex/hooks/ → project
|
|
7
|
+
root) and forwards with --host codex.
|
|
8
|
+
"""
|
|
3
9
|
|
|
4
10
|
from __future__ import annotations
|
|
5
11
|
|
|
6
|
-
import json
|
|
7
|
-
import os
|
|
8
12
|
import sys
|
|
9
13
|
from pathlib import Path
|
|
10
|
-
from typing import Any
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def _configure_stdio() -> None:
|
|
14
|
-
for stream in (sys.stdout, sys.stderr):
|
|
15
|
-
if hasattr(stream, "reconfigure"):
|
|
16
|
-
stream.reconfigure(encoding="utf-8", errors="replace")
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _read_input() -> dict[str, Any]:
|
|
20
|
-
raw = sys.stdin.buffer.read()
|
|
21
|
-
if not raw.strip():
|
|
22
|
-
return {}
|
|
23
|
-
try:
|
|
24
|
-
data = json.loads(raw.decode("utf-8-sig"))
|
|
25
|
-
except (UnicodeDecodeError, json.JSONDecodeError, ValueError):
|
|
26
|
-
return {}
|
|
27
|
-
return data if isinstance(data, dict) else {}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def _find_root(start: Path) -> Path | None:
|
|
31
|
-
current = start.resolve()
|
|
32
|
-
while True:
|
|
33
|
-
if (current / ".cowork-flow").is_dir():
|
|
34
|
-
return current
|
|
35
|
-
if current == current.parent:
|
|
36
|
-
return None
|
|
37
|
-
current = current.parent
|
|
38
14
|
|
|
39
15
|
|
|
40
16
|
def main() -> int:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return 0
|
|
46
|
-
hook_input = _read_input()
|
|
47
|
-
cwd_value = hook_input.get("cwd")
|
|
48
|
-
cwd = (
|
|
49
|
-
Path(cwd_value)
|
|
50
|
-
if isinstance(cwd_value, str) and cwd_value.strip()
|
|
51
|
-
else Path.cwd()
|
|
52
|
-
)
|
|
53
|
-
root = _find_root(cwd)
|
|
54
|
-
if root is None:
|
|
17
|
+
project_root = Path(__file__).resolve().parents[2]
|
|
18
|
+
scripts_dir = project_root / ".cowork-flow" / "scripts"
|
|
19
|
+
if not scripts_dir.is_dir():
|
|
20
|
+
# Outside any cowork-flow project: stay silent like the entry does.
|
|
55
21
|
return 0
|
|
56
|
-
scripts_dir = root / ".cowork-flow" / "scripts"
|
|
57
22
|
sys.path.insert(0, str(scripts_dir))
|
|
58
|
-
from adapters.host.
|
|
59
|
-
build_hook_context,
|
|
60
|
-
codex_dispatch_mode,
|
|
61
|
-
)
|
|
23
|
+
from adapters.host.inject import main as inject_main
|
|
62
24
|
|
|
63
|
-
|
|
64
|
-
event_name = (
|
|
65
|
-
hook_input.get("hook_event_name")
|
|
66
|
-
or hook_input.get("hookEventName")
|
|
67
|
-
or "UserPromptSubmit"
|
|
68
|
-
)
|
|
69
|
-
context = build_hook_context(
|
|
70
|
-
root,
|
|
71
|
-
hook_input,
|
|
72
|
-
host="codex",
|
|
73
|
-
adapter="codex.spawn_agent",
|
|
74
|
-
preamble=(
|
|
75
|
-
f"<codex-dispatch-mode>{dispatch_mode}</codex-dispatch-mode>",
|
|
76
|
-
(
|
|
77
|
-
"<codex-runtime>\n"
|
|
78
|
-
"dispatch_mode_meaning: workflow dispatch hint, not current "
|
|
79
|
-
"thread role\n"
|
|
80
|
-
"runtime_context_identity: formal subagent sessions bind "
|
|
81
|
-
"before workflow-state injection\n"
|
|
82
|
-
"</codex-runtime>"
|
|
83
|
-
),
|
|
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).
|
|
88
|
-
)
|
|
89
|
-
print(
|
|
90
|
-
json.dumps(
|
|
91
|
-
{
|
|
92
|
-
"hookSpecificOutput": {
|
|
93
|
-
"hookEventName": event_name,
|
|
94
|
-
"additionalContext": context,
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
ensure_ascii=False,
|
|
98
|
-
)
|
|
99
|
-
)
|
|
100
|
-
return 0
|
|
25
|
+
return inject_main(["--host", "codex"])
|
|
101
26
|
|
|
102
27
|
|
|
103
28
|
if __name__ == "__main__":
|
|
104
|
-
_configure_stdio()
|
|
105
29
|
raise SystemExit(main())
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.4.0
|
|
@@ -26,7 +26,6 @@ from services.task_context import (
|
|
|
26
26
|
is_skill_path as _is_skill_path,
|
|
27
27
|
iter_jsonl_lines as _iter_jsonl_lines,
|
|
28
28
|
skill_path as _skill_path,
|
|
29
|
-
use_claude_skill_context as _use_claude_skill_context,
|
|
30
29
|
write_jsonl as _write_jsonl,
|
|
31
30
|
)
|
|
32
31
|
from adapters.cli.task_archive_commands import cmd_archive
|
|
@@ -35,6 +34,8 @@ from adapters.cli.task_context_commands import (
|
|
|
35
34
|
cmd_add_context,
|
|
36
35
|
cmd_init_context,
|
|
37
36
|
cmd_list_context,
|
|
37
|
+
cmd_scope,
|
|
38
|
+
cmd_specs,
|
|
38
39
|
cmd_validate,
|
|
39
40
|
)
|
|
40
41
|
from adapters.cli.task_create_command import cmd_create, ensure_tasks_dir
|
|
@@ -124,6 +125,8 @@ WORKER_BLOCKED_COMMANDS = frozenset()
|
|
|
124
125
|
|
|
125
126
|
COMMANDS = {
|
|
126
127
|
"next": cmd_next,
|
|
128
|
+
"scope": cmd_scope,
|
|
129
|
+
"specs": cmd_specs,
|
|
127
130
|
}
|
|
128
131
|
|
|
129
132
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
import argparse
|
|
7
|
+
import json
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
9
10
|
from services.task_context import (
|
|
@@ -244,3 +245,67 @@ def cmd_list_context(args: argparse.Namespace) -> int:
|
|
|
244
245
|
print(f" {colored('->', Colors.YELLOW)} {reason}")
|
|
245
246
|
print()
|
|
246
247
|
return 0
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _resolve_fact_task_dir(
|
|
251
|
+
task_dir_arg: str | None, repo_root: Path
|
|
252
|
+
) -> Path | None:
|
|
253
|
+
"""Explicit dir wins; otherwise the session-bound active task. Returns
|
|
254
|
+
None when nothing is resolvable (the caller reports no-active-task)."""
|
|
255
|
+
if task_dir_arg:
|
|
256
|
+
return resolve_task_dir(task_dir_arg, repo_root)
|
|
257
|
+
from runtime.session_state import get_active_task
|
|
258
|
+
|
|
259
|
+
active = get_active_task(repo_root)
|
|
260
|
+
if not active.task_path:
|
|
261
|
+
return None
|
|
262
|
+
return repo_root / active.task_path
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _print_fact_json(payload: dict) -> None:
|
|
266
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def cmd_scope(args: argparse.Namespace) -> int:
|
|
270
|
+
"""File-scope whitelist for a task (read-only fact query, MCP task_scope
|
|
271
|
+
parity). With --path, prints the per-file inScope verdict instead."""
|
|
272
|
+
repo_root = get_repo_root()
|
|
273
|
+
task_dir = _resolve_fact_task_dir(getattr(args, "dir", None), repo_root)
|
|
274
|
+
if task_dir is None or not task_dir.is_dir():
|
|
275
|
+
_print_fact_json({"error": "no-active-task"})
|
|
276
|
+
return 1
|
|
277
|
+
from services.fact_view import file_scope_whitelist, path_in_scope
|
|
278
|
+
|
|
279
|
+
whitelist = file_scope_whitelist(repo_root, task_dir)
|
|
280
|
+
if getattr(args, "path", None):
|
|
281
|
+
payload = {"taskDir": task_dir.name, **path_in_scope(whitelist, args.path)}
|
|
282
|
+
else:
|
|
283
|
+
payload = {
|
|
284
|
+
"taskDir": task_dir.name,
|
|
285
|
+
"whitelist": whitelist,
|
|
286
|
+
"count": len(whitelist),
|
|
287
|
+
}
|
|
288
|
+
_print_fact_json(payload)
|
|
289
|
+
return 0
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def cmd_specs(args: argparse.Namespace) -> int:
|
|
293
|
+
"""Spec/skill reading list dispatched by task dev_type (read-only fact
|
|
294
|
+
query, MCP task_specs parity)."""
|
|
295
|
+
repo_root = get_repo_root()
|
|
296
|
+
task_dir = _resolve_fact_task_dir(getattr(args, "dir", None), repo_root)
|
|
297
|
+
if task_dir is None or not task_dir.is_dir():
|
|
298
|
+
_print_fact_json({"error": "no-active-task"})
|
|
299
|
+
return 1
|
|
300
|
+
from services.fact_view import spec_entries_for_task
|
|
301
|
+
|
|
302
|
+
dev_type, specs = spec_entries_for_task(repo_root, task_dir)
|
|
303
|
+
_print_fact_json(
|
|
304
|
+
{
|
|
305
|
+
"taskDir": task_dir.name,
|
|
306
|
+
"devType": dev_type,
|
|
307
|
+
"specs": specs,
|
|
308
|
+
"count": len(specs),
|
|
309
|
+
}
|
|
310
|
+
)
|
|
311
|
+
return 0
|
|
@@ -168,6 +168,27 @@ def _add_next_command(subparsers: argparse._SubParsersAction) -> None:
|
|
|
168
168
|
_add_next_create_inputs(next_parser)
|
|
169
169
|
_add_next_runtime_inputs(next_parser)
|
|
170
170
|
|
|
171
|
+
|
|
172
|
+
def _add_scope_command(subparsers: argparse._SubParsersAction) -> None:
|
|
173
|
+
scope_parser = subparsers.add_parser(
|
|
174
|
+
"scope",
|
|
175
|
+
help="File-scope whitelist for a task (read-only fact query)",
|
|
176
|
+
)
|
|
177
|
+
_add_optional_task_dir(scope_parser)
|
|
178
|
+
scope_parser.add_argument(
|
|
179
|
+
"--path",
|
|
180
|
+
help="Check one repo-relative path and print the inScope verdict",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _add_specs_command(subparsers: argparse._SubParsersAction) -> None:
|
|
185
|
+
specs_parser = subparsers.add_parser(
|
|
186
|
+
"specs",
|
|
187
|
+
help="Spec/skill reading list dispatched by task dev_type "
|
|
188
|
+
"(read-only fact query)",
|
|
189
|
+
)
|
|
190
|
+
_add_optional_task_dir(specs_parser)
|
|
191
|
+
|
|
171
192
|
def build_parser() -> argparse.ArgumentParser:
|
|
172
193
|
parser = argparse.ArgumentParser(
|
|
173
194
|
description="Task workflow router for cowork-flow",
|
|
@@ -179,6 +200,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
179
200
|
help="Commands",
|
|
180
201
|
)
|
|
181
202
|
_add_next_command(subparsers)
|
|
203
|
+
_add_scope_command(subparsers)
|
|
204
|
+
_add_specs_command(subparsers)
|
|
182
205
|
return parser
|
|
183
206
|
|
|
184
207
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""claude-code host policy: the claude-code-specific behaviors of the
|
|
3
|
+
workflow hook.
|
|
4
|
+
|
|
5
|
+
Rendering stays in workflow_state_hook.py; this module owns only the
|
|
6
|
+
claude-code transport specifics — the session alias ported from the claude
|
|
7
|
+
wrapper, the runtime preamble block, and the PostToolUse spec-advisory path
|
|
8
|
+
(stderr + exit 2, shared with codex via the neutral helper).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from adapters.host.workflow_state_hook import HostPolicy
|
|
17
|
+
|
|
18
|
+
CLAUDE_PREAMBLE = (
|
|
19
|
+
(
|
|
20
|
+
"<claude-code-runtime>\n"
|
|
21
|
+
"hooks: UserPromptSubmit, SessionStart, PostToolUse\n"
|
|
22
|
+
"</claude-code-runtime>"
|
|
23
|
+
),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def claude_session_alias(data: dict[str, Any]) -> dict[str, Any]:
|
|
28
|
+
# Ported from the claude-code wrapper: bare session_id must resolve as a
|
|
29
|
+
# claude session, not fall through to the codex session_id rule.
|
|
30
|
+
if "claude_session_id" not in data:
|
|
31
|
+
session_id = data.get("session_id")
|
|
32
|
+
if isinstance(session_id, str) and session_id.strip():
|
|
33
|
+
data["claude_session_id"] = session_id
|
|
34
|
+
return data
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def preamble(root: Path) -> tuple[str, ...]:
|
|
38
|
+
return CLAUDE_PREAMBLE
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def post_tool_use(root: Path, hook_input: dict[str, Any]) -> tuple[str, int]:
|
|
42
|
+
from adapters.host.workflow_state_hook import spec_only_post_tool_use
|
|
43
|
+
|
|
44
|
+
return spec_only_post_tool_use(root, hook_input, POLICY)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
POLICY = HostPolicy(
|
|
48
|
+
host="claude-code",
|
|
49
|
+
session_alias=claude_session_alias,
|
|
50
|
+
preamble=preamble,
|
|
51
|
+
post_tool_use=post_tool_use,
|
|
52
|
+
)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""codex host policy: the codex-specific behaviors of the workflow hook.
|
|
3
|
+
|
|
4
|
+
Rendering stays in workflow_state_hook.py; this module owns only the codex
|
|
5
|
+
transport specifics — the dispatch-mode preamble (config-driven, the codex
|
|
6
|
+
wrapper surfaces it above every injection) and the probe-based session-start
|
|
7
|
+
detection (codex registers UserPromptSubmit only, so the digest shape derives
|
|
8
|
+
from the session state file probe). PostToolUse reuses the neutral spec-only
|
|
9
|
+
stderr path shared with claude-code.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from adapters.host.workflow_state_hook import HostPolicy
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def codex_dispatch_mode(root: Path) -> str:
|
|
22
|
+
from adapters.host.workflow_state_hook import _load_common
|
|
23
|
+
|
|
24
|
+
_load_common(root)
|
|
25
|
+
try:
|
|
26
|
+
from infra.config import get_codex_dispatch_mode
|
|
27
|
+
except Exception:
|
|
28
|
+
return "sub-agent"
|
|
29
|
+
try:
|
|
30
|
+
return get_codex_dispatch_mode(root)
|
|
31
|
+
except Exception:
|
|
32
|
+
return "sub-agent"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def preamble(root: Path) -> tuple[str, ...]:
|
|
36
|
+
dispatch_mode = codex_dispatch_mode(root)
|
|
37
|
+
return (
|
|
38
|
+
f"<codex-dispatch-mode>{dispatch_mode}</codex-dispatch-mode>",
|
|
39
|
+
(
|
|
40
|
+
"<codex-runtime>\n"
|
|
41
|
+
"dispatch_mode_meaning: workflow dispatch hint, not current "
|
|
42
|
+
"thread role\n"
|
|
43
|
+
"runtime_context_identity: formal subagent sessions bind "
|
|
44
|
+
"before workflow-state injection\n"
|
|
45
|
+
"</codex-runtime>"
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def post_tool_use(root: Path, hook_input: dict[str, Any]) -> tuple[str, int]:
|
|
51
|
+
from adapters.host.workflow_state_hook import spec_only_post_tool_use
|
|
52
|
+
|
|
53
|
+
return spec_only_post_tool_use(root, hook_input, POLICY)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
POLICY = HostPolicy(
|
|
57
|
+
host="codex",
|
|
58
|
+
session_start_event=None, # probe the session state file instead
|
|
59
|
+
preamble=preamble,
|
|
60
|
+
post_tool_use=post_tool_use,
|
|
61
|
+
)
|