cowork-flow 0.0.45 → 0.0.46

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/README.md CHANGED
@@ -39,7 +39,7 @@ template/
39
39
  ├── .codex/ # Codex agents / hooks / config
40
40
  ├── .claude/ # Claude Code settings / agents / hooks
41
41
  ├── .opencode/ # OpenCode agents / commands / plugins
42
- ├── .zcode/ # ⭐ ZCode 插件(hooks + skills + scaffold instructions)
42
+ ├── .zcode/ # ⭐ ZCode 插件(hooks + skills + agents + scaffold instructions)
43
43
  └── .cowork-flow/
44
44
  ├── config.yaml # 项目配置
45
45
  ├── scripts/ # Python 运行时
@@ -103,17 +103,25 @@ Skills 维护在 `template/skills/` 唯一源码,`init` / `sync` 时按目录
103
103
  ```bash
104
104
  cowork-flow install-zcode-plugin # 安装
105
105
  cowork-flow install-zcode-plugin --force # 覆盖已安装
106
+ cowork-flow install-zcode-plugin --force --prune-old # 覆盖并清理旧版本缓存
106
107
  ```
107
108
 
108
- 安装到 `~/.zcode/cli/plugins/cache/zcode-plugins-official/cowork-flow/<version>/`
109
+ 安装到 `~/.zcode/cli/plugins/cache/cowork-flow-local/cowork-flow/<version>/`。安装器会同时写入稳定 marketplace source:`~/.zcode/cli/plugins/cache/marketplaces/cowork-flow-local/marketplace.json`,以及 ZCode 当前使用的活动副本:`~/.zcode/cli/plugins/marketplaces/cowork-flow-local/marketplace.json`。`known_marketplaces.json` 指向稳定 source 目录,避免 ZCode 刷新活动副本时删除自己的 source。
109
110
 
110
- ZCode 插件只安装 hook、skills 和轻量说明文件;`.cowork-flow/` 流程文件仍由显式 `cowork-flow init` / `cowork-flow sync` 在项目根目录管理。插件不会通过 scaffold 创建 `.cowork-flow/`,因此不会在多模块项目的模块目录重复落盘流程文件。
111
+ 安装新版本时,marketplace 中只保留一个 `cowork-flow` entry 并指向最新版本目录;旧版本缓存默认保留,避免正在运行的 ZCode session 仍引用旧插件根目录。需要清理旧版本时显式传 `--prune-old`。
112
+
113
+ ZCode 插件只安装 hook、skills、agents 和轻量说明文件;`.cowork-flow/` 流程文件仍由显式 `cowork-flow init` / `cowork-flow sync` 在项目根目录管理。插件不会通过 scaffold 创建 `.cowork-flow/`,因此不会在多模块项目的模块目录重复落盘流程文件。
111
114
 
112
115
  **Hook 注入内容:**
113
116
  - `workflow-state` — 当前任务状态
114
117
  - `contract-digest` — 合同摘要(SHA256 fingerprint)
115
118
  - `delegated_subtask` — 子代理运行时上下文
116
119
 
120
+ **插件子代理:**
121
+ - `cowork-implement` — 绑定 runtime context 后执行计划内实现
122
+ - `cowork-check` — 绑定 runtime context 后做独立检查
123
+ - `cowork-research` — 绑定 runtime context 后做只读调研
124
+
117
125
  ## 任务流程
118
126
 
119
127
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "0.0.45",
3
+ "version": "0.0.46",
4
4
  "description": "CLI for installing and maintaining cowork-flow collaboration templates.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -12,7 +12,7 @@ const HELP = `cowork-flow
12
12
 
13
13
  Usage:
14
14
  cowork-flow init [target] --platform <codex|opencode|claude-code|all> [--developer <name>] [--dry-run] [--force]
15
- cowork-flow install-zcode-plugin [--force]
15
+ cowork-flow install-zcode-plugin [--dry-run] [--force] [--prune-old]
16
16
  cowork-flow update
17
17
  cowork-flow sync [target] [--dry-run] [--force]
18
18
  cowork-flow --version
@@ -1,17 +1,20 @@
1
- import { cp, mkdir, readFile, writeFile, access, rm } from 'node:fs/promises';
2
- import { join, resolve } from 'node:path';
1
+ import { cp, mkdir, readFile, writeFile, access, rm, readdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
 
5
5
  import { readPackageInfo } from '../lib/package-info.js';
6
6
  import { templateRoot } from '../lib/paths.js';
7
7
 
8
- const ZCODE_MARKETPLACE = 'zcode-plugins-official';
8
+ const ZCODE_MARKETPLACE = 'cowork-flow-local';
9
+ const LEGACY_ZCODE_MARKETPLACE = 'zcode-plugins-official';
9
10
  const PLUGIN_NAME = 'cowork-flow';
11
+ const LOCAL_MARKETPLACE_DESCRIPTION = 'Local marketplace registration for cowork-flow during local development.';
10
12
 
11
13
  function parseArgs(args) {
12
14
  return {
13
15
  dryRun: args.includes('--dry-run'),
14
- force: args.includes('--force')
16
+ force: args.includes('--force'),
17
+ pruneOld: args.includes('--prune-old')
15
18
  };
16
19
  }
17
20
 
@@ -24,9 +27,13 @@ async function pathExists(target) {
24
27
  }
25
28
  }
26
29
 
27
- async function getZCodeCacheDir() {
30
+ function getZCodePluginsRoot() {
28
31
  const base = process.env.ZCODE_HOME || join(homedir(), '.zcode');
29
- return join(base, 'cli', 'plugins', 'cache', ZCODE_MARKETPLACE, PLUGIN_NAME);
32
+ return join(base, 'cli', 'plugins');
33
+ }
34
+
35
+ async function getZCodeCacheDir() {
36
+ return join(getZCodePluginsRoot(), 'cache', ZCODE_MARKETPLACE, PLUGIN_NAME);
30
37
  }
31
38
 
32
39
  async function readJsonSafe(path) {
@@ -46,14 +53,46 @@ function dirname(path) {
46
53
  return idx >= 0 ? path.slice(0, idx) : path;
47
54
  }
48
55
 
49
- async function updateMarketplace(cacheRoot, version) {
50
- const marketPath = resolve(cacheRoot, '..', '..', '..', 'marketplaces', ZCODE_MARKETPLACE, 'marketplace.json');
51
- const market = (await readJsonSafe(marketPath)) || { name: ZCODE_MARKETPLACE, plugins: [], version: 1 };
56
+ function normalizedPath(path) {
57
+ return path.split('\\').join('/');
58
+ }
59
+
60
+ function marketplacePaths(pluginsRoot) {
61
+ return {
62
+ activeMarketplacePath: join(pluginsRoot, 'marketplaces', ZCODE_MARKETPLACE, 'marketplace.json'),
63
+ sourceMarketplacePath: join(pluginsRoot, 'cache', 'marketplaces', ZCODE_MARKETPLACE, 'marketplace.json')
64
+ };
65
+ }
66
+
67
+ async function updateMarketplace(pluginsRoot, cacheRoot, version, manifest = {}) {
68
+ const { activeMarketplacePath, sourceMarketplacePath } = marketplacePaths(pluginsRoot);
69
+ const market = (await readJsonSafe(sourceMarketplacePath))
70
+ || (await readJsonSafe(activeMarketplacePath))
71
+ || {
72
+ name: ZCODE_MARKETPLACE,
73
+ description: LOCAL_MARKETPLACE_DESCRIPTION,
74
+ plugins: [],
75
+ version: 1
76
+ };
77
+ const pluginPath = normalizedPath(join(cacheRoot, version));
78
+
79
+ market.name = ZCODE_MARKETPLACE;
80
+ market.description = market.description || LOCAL_MARKETPLACE_DESCRIPTION;
81
+ market.version = 1;
82
+ if (!Array.isArray(market.plugins)) {
83
+ market.plugins = [];
84
+ }
52
85
 
53
86
  const entry = {
54
- cachePath: join(cacheRoot, version).split('\\').join('/'),
87
+ author: manifest.author || { name: 'fisklikewater' },
88
+ category: 'developer-tools',
89
+ description: manifest.description || 'cowork-flow task lifecycle, hooks, skills, and fixed subagents for ZCode.',
90
+ license: manifest.license || 'MIT',
55
91
  name: PLUGIN_NAME,
56
- source: 'filesystem',
92
+ source: {
93
+ source: 'directory',
94
+ path: pluginPath
95
+ },
57
96
  version
58
97
  };
59
98
 
@@ -64,15 +103,99 @@ async function updateMarketplace(cacheRoot, version) {
64
103
  market.plugins.push(entry);
65
104
  }
66
105
 
67
- await mkdir(dirname(marketPath), { recursive: true });
68
- await writeJsonAtomic(marketPath, market);
106
+ for (const targetPath of [sourceMarketplacePath, activeMarketplacePath]) {
107
+ await mkdir(dirname(targetPath), { recursive: true });
108
+ await writeJsonAtomic(targetPath, market);
109
+ }
110
+ return { activeMarketplacePath, sourceMarketplacePath };
111
+ }
112
+
113
+ async function updateKnownMarketplaces(pluginsRoot) {
114
+ const knownPath = join(pluginsRoot, 'known_marketplaces.json');
115
+ const known = (await readJsonSafe(knownPath)) || {
116
+ version: 1,
117
+ marketplaces: []
118
+ };
119
+ const now = new Date().toISOString();
120
+ const marketplaceDir = join(pluginsRoot, 'cache', 'marketplaces', ZCODE_MARKETPLACE);
121
+ const existingIdx = Array.isArray(known.marketplaces)
122
+ ? known.marketplaces.findIndex((marketplace) => marketplace.id === ZCODE_MARKETPLACE)
123
+ : -1;
124
+ const existing = existingIdx >= 0 ? known.marketplaces[existingIdx] : {};
125
+ const entry = {
126
+ id: ZCODE_MARKETPLACE,
127
+ source: {
128
+ source: 'directory',
129
+ path: normalizedPath(marketplaceDir)
130
+ },
131
+ name: ZCODE_MARKETPLACE,
132
+ description: LOCAL_MARKETPLACE_DESCRIPTION,
133
+ addedAt: existing.addedAt || now,
134
+ pluginCount: 1,
135
+ lastUpdated: now
136
+ };
137
+
138
+ known.version = 1;
139
+ if (!Array.isArray(known.marketplaces)) {
140
+ known.marketplaces = [];
141
+ }
142
+ if (existingIdx >= 0) {
143
+ known.marketplaces[existingIdx] = entry;
144
+ } else {
145
+ known.marketplaces.push(entry);
146
+ }
147
+
148
+ await mkdir(dirname(knownPath), { recursive: true });
149
+ await writeJsonAtomic(knownPath, known);
150
+ return knownPath;
151
+ }
152
+
153
+ async function removeLegacyMarketplaceEntry(pluginsRoot) {
154
+ const legacyPath = join(
155
+ pluginsRoot,
156
+ 'marketplaces',
157
+ LEGACY_ZCODE_MARKETPLACE,
158
+ 'marketplace.json'
159
+ );
160
+ const legacy = await readJsonSafe(legacyPath);
161
+ if (!legacy || !Array.isArray(legacy.plugins)) {
162
+ return false;
163
+ }
164
+
165
+ const filteredPlugins = legacy.plugins.filter((plugin) => plugin.name !== PLUGIN_NAME);
166
+ if (filteredPlugins.length === legacy.plugins.length) {
167
+ return false;
168
+ }
169
+
170
+ legacy.plugins = filteredPlugins;
171
+ await writeJsonAtomic(legacyPath, legacy);
172
+ return true;
173
+ }
174
+
175
+ async function pruneOldVersions(cacheRoot, currentVersion) {
176
+ if (!(await pathExists(cacheRoot))) {
177
+ return [];
178
+ }
179
+
180
+ const removed = [];
181
+ const entries = await readdir(cacheRoot, { withFileTypes: true });
182
+ for (const entry of entries) {
183
+ if (!entry.isDirectory() || entry.name === currentVersion) {
184
+ continue;
185
+ }
186
+ const target = join(cacheRoot, entry.name);
187
+ await rm(target, { recursive: true, force: true });
188
+ removed.push(entry.name);
189
+ }
190
+ return removed;
69
191
  }
70
192
 
71
193
  export async function runInstallZCodePlugin(args = []) {
72
- const { dryRun, force } = parseArgs(args);
194
+ const { dryRun, force, pruneOld } = parseArgs(args);
73
195
  const pluginSrc = join(templateRoot, ".zcode");
74
196
  const skillsSrc = join(templateRoot, "skills");
75
197
  const { version } = await readPackageInfo();
198
+ const manifest = (await readJsonSafe(join(pluginSrc, '.zcode-plugin', 'plugin.json'))) || {};
76
199
 
77
200
  if (!(await pathExists(pluginSrc))) {
78
201
  throw new Error(`ZCode plugin source missing at ${pluginSrc}. Reinstall cowork-flow.`);
@@ -81,19 +204,32 @@ export async function runInstallZCodePlugin(args = []) {
81
204
  throw new Error(`Skills source missing at ${skillsSrc}. Reinstall cowork-flow.`);
82
205
  }
83
206
 
207
+ const pluginsRoot = getZCodePluginsRoot();
84
208
  const cacheRoot = await getZCodeCacheDir();
85
209
  const destDir = join(cacheRoot, version);
86
210
 
87
- if (!force && (await pathExists(destDir))) {
88
- console.log(`cowork-flow ZCode plugin already installed at ${destDir}`);
89
- console.log('Use --force to overwrite.');
90
- return;
91
- }
92
-
93
211
  if (dryRun) {
94
212
  console.log(`[dry-run] Would install ZCode plugin:`);
95
213
  console.log(` Plugin: ${pluginSrc} -> ${destDir}`);
96
214
  console.log(` Skills: ${skillsSrc} -> ${join(destDir, 'skills')}`);
215
+ console.log(` Marketplace source: ${join(pluginsRoot, 'cache', 'marketplaces', ZCODE_MARKETPLACE, 'marketplace.json')}`);
216
+ console.log(` Active marketplace: ${join(pluginsRoot, 'marketplaces', ZCODE_MARKETPLACE, 'marketplace.json')}`);
217
+ console.log(` Known marketplaces: ${join(pluginsRoot, 'known_marketplaces.json')}`);
218
+ return;
219
+ }
220
+
221
+ if (!force && (await pathExists(destDir))) {
222
+ await updateMarketplace(pluginsRoot, cacheRoot, version, manifest);
223
+ await updateKnownMarketplaces(pluginsRoot);
224
+ await removeLegacyMarketplaceEntry(pluginsRoot);
225
+ if (pruneOld) {
226
+ const removed = await pruneOldVersions(cacheRoot, version);
227
+ if (removed.length > 0) {
228
+ console.log(`Pruned old cowork-flow ZCode plugin versions: ${removed.join(', ')}`);
229
+ }
230
+ }
231
+ console.log(`cowork-flow ZCode plugin already installed at ${destDir}`);
232
+ console.log('Use --force to overwrite.');
97
233
  return;
98
234
  }
99
235
 
@@ -121,7 +257,15 @@ export async function runInstallZCodePlugin(args = []) {
121
257
  // Copy canonical skills into the plugin cache so plugin.json "skills" resolves.
122
258
  await cp(skillsSrc, join(destDir, "skills"), { recursive: true, force: true });
123
259
 
124
- await updateMarketplace(cacheRoot, version);
260
+ await updateMarketplace(pluginsRoot, cacheRoot, version, manifest);
261
+ await updateKnownMarketplaces(pluginsRoot);
262
+ await removeLegacyMarketplaceEntry(pluginsRoot);
263
+ if (pruneOld) {
264
+ const removed = await pruneOldVersions(cacheRoot, version);
265
+ if (removed.length > 0) {
266
+ console.log(`Pruned old cowork-flow ZCode plugin versions: ${removed.join(', ')}`);
267
+ }
268
+ }
125
269
 
126
270
  // Write seed file for install tracking
127
271
  await writeJsonAtomic(join(destDir, ".zcode-plugin-seed.json"), {
@@ -1 +1 @@
1
- 0.0.45
1
+ 0.0.46
@@ -72,6 +72,8 @@ def _detect_host() -> str:
72
72
  return "claude-code"
73
73
  if env.get("CODEX_SESSION_ID") or env.get("CODEX_THREAD_ID"):
74
74
  return "codex"
75
+ if env.get("ZCODE_SESSION_ID"):
76
+ return "zcode"
75
77
  if env.get("OPENCODE_SESSION_ID"):
76
78
  return "opencode"
77
79
  return "codex"
@@ -82,6 +84,7 @@ def _detect_adapter(host: str) -> str:
82
84
  "claude-code": "claude-code.hooks",
83
85
  "codex": "codex.spawn_agent",
84
86
  "opencode": "opencode.task",
87
+ "zcode": "zcode.plugin",
85
88
  }
86
89
  return adapters.get(host, "codex.spawn_agent")
87
90
 
@@ -69,6 +69,7 @@ def _resolve_env_context_key() -> str | None:
69
69
  return _sanitize(explicit)
70
70
 
71
71
  for prefix, env_name in (
72
+ ("zcode", "ZCODE_SESSION_ID"),
72
73
  ("opencode", "OPENCODE_SESSION_ID"),
73
74
  ("claude", "CLAUDE_SESSION_ID"),
74
75
  ("claude", "CLAUDE_CODE_SESSION_ID"),
@@ -90,6 +91,7 @@ def _resolve_input_context_key(values: Mapping[str, object] | None) -> str | Non
90
91
  return _sanitize(explicit)
91
92
 
92
93
  for prefix, names in (
94
+ ("zcode", ("ZCODE_SESSION_ID", "zcode_session_id")),
93
95
  ("opencode", ("OPENCODE_SESSION_ID", "opencode_session_id", "sessionID", "sessionId")),
94
96
  ("claude", ("CLAUDE_SESSION_ID", "claude_session_id", "CLAUDE_CODE_SESSION_ID", "claude_code_session_id")),
95
97
  ("codex", ("CODEX_SESSION_ID", "codex_session_id", "session_id")),
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "0.0.45",
3
+ "version": "0.0.46",
4
4
  "description": "AI 协作工作流引擎:任务生命周期、子代理调度、门禁与状态注入。将 cowork-flow 的完整工作流带入 ZCode。",
5
5
  "author": {
6
6
  "name": "fisklikewater"
7
7
  },
8
8
  "license": "MIT",
9
+ "hooks": "hooks/hooks.json",
10
+ "agents": "agents",
9
11
  "skills": "skills"
10
12
  }
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: cowork-check
3
+ description: Cowork-flow check fixed subagent for independent implementation review with runtime context binding.
4
+ ---
5
+
6
+ You are the `cowork-check` fixed subagent for ZCode.
7
+
8
+ Read and apply these plugin Skills before review work:
9
+
10
+ - `skills/agent-dispatch/SKILL.md`
11
+ - `skills/task-review/SKILL.md`
12
+ - `skills/decision-audit/SKILL.md`
13
+ - `skills/spec-sync/SKILL.md`
14
+
15
+ Execution:
16
+
17
+ 1. Follow `agent-dispatch` to bind runtime context. If binding fails, report `needs_context` and stop.
18
+ 2. Read the task directory, `decision-anchor.md`, linked plan, the `check.jsonl` context index, every referenced `file` entry, and current `git diff`; do not write review conclusions to JSONL.
19
+ 3. Apply `task-review` to verify scope, tests, specs, advisory facts, lifecycle facts, and Definition of Done coverage.
20
+ 4. Fix only clearly in-scope issues; otherwise report findings with acceptance IDs, resolutions, `test_intent_review` (test intent review), and verification commands.
21
+
22
+ Rules:
23
+
24
+ - Do not invoke subagents.
25
+ - Do not run task start, finish, archive, unscoped resume, commit, or push.
26
+ - Treat backend/frontend natural-language specs as checklist context, not dynamic hard validators.
27
+ - Do not accept shallow tests that would still pass when target behavior breaks.
@@ -0,0 +1,28 @@
1
+ ---
2
+ name: cowork-implement
3
+ description: Cowork-flow implementation fixed subagent for bounded code changes with runtime context binding.
4
+ ---
5
+
6
+ You are the `cowork-implement` fixed subagent for ZCode.
7
+
8
+ Read and apply these plugin Skills before task work:
9
+
10
+ - `skills/agent-dispatch/SKILL.md`
11
+ - `skills/decision-audit/SKILL.md`
12
+ - `skills/spec-sync/SKILL.md`
13
+ - `skills/test-first/SKILL.md` for behavior changes
14
+
15
+ Execution:
16
+
17
+ 1. Follow `agent-dispatch` to bind runtime context. If binding fails, report `needs_context` and stop.
18
+ 2. Read the task directory, `decision-anchor.md`, optional `info.md`, `implement.jsonl`, every JSONL `file` entry, and the linked plan.
19
+ 3. Follow the plan in order, keeping edits inside assigned scope.
20
+ 4. Use project specs and quality sources as checklist context; runtime gates remain kernel-owned commands.
21
+ 5. Run planned verification and report changed files, acceptance IDs, verification commands, and spec updates or why none were needed.
22
+
23
+ Rules:
24
+
25
+ - Do not invoke subagents.
26
+ - Do not run task start, finish, archive, unscoped resume, commit, or push.
27
+ - Do not create task-local review artifacts, `tdd.jsonl`, or TDD evidence records in `check.jsonl`.
28
+ - Do not claim completion without current verification.
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: cowork-research
3
+ description: Cowork-flow research-only fixed subagent for sourced investigation with runtime context binding.
4
+ ---
5
+
6
+ You are the `cowork-research` fixed subagent for ZCode.
7
+
8
+ Read and apply `skills/agent-dispatch/SKILL.md` before research work.
9
+
10
+ Execution:
11
+
12
+ 1. Follow `agent-dispatch` to bind runtime context. If binding fails, report `needs_context` and stop.
13
+ 2. Read the task directory, assignment, and prompt-named context.
14
+ 3. Write research notes only under the assigned task `research/` directory when asked.
15
+ 4. Report sourced findings, uncertainty, and recommended next action.
16
+
17
+ Rules:
18
+
19
+ - Do not invoke subagents.
20
+ - Do not edit code, specs, task state, or git state.
21
+ - Do not run task start, finish, archive, unscoped resume, commit, or push.
@@ -5,9 +5,12 @@
5
5
  "matcher": "startup|clear|compact",
6
6
  "hooks": [
7
7
  {
8
- "type": "command",
9
- "command": "node ${ZCODE_PLUGIN_ROOT}/hooks/inject-context.js",
10
- "async": false
8
+ "type": "process",
9
+ "command": "node",
10
+ "args": [
11
+ "${ZCODE_PLUGIN_ROOT}/hooks/inject-context.js"
12
+ ],
13
+ "timeoutMs": 5000
11
14
  }
12
15
  ]
13
16
  }
@@ -16,9 +19,12 @@
16
19
  {
17
20
  "hooks": [
18
21
  {
19
- "type": "command",
20
- "command": "node ${ZCODE_PLUGIN_ROOT}/hooks/inject-context.js",
21
- "async": false
22
+ "type": "process",
23
+ "command": "node",
24
+ "args": [
25
+ "${ZCODE_PLUGIN_ROOT}/hooks/inject-context.js"
26
+ ],
27
+ "timeoutMs": 5000
22
28
  }
23
29
  ]
24
30
  }
@@ -29,7 +29,22 @@ const DEFAULT_CONTRACT_REGISTRY = {
29
29
  ],
30
30
  };
31
31
 
32
- function detectEventName() {
32
+ function readHookInput() {
33
+ if (process.stdin.isTTY) return {};
34
+ try {
35
+ const raw = readFileSync(0, "utf8").trim();
36
+ if (!raw) return {};
37
+ const parsed = JSON.parse(raw);
38
+ return parsed && typeof parsed === "object" ? parsed : {};
39
+ } catch {
40
+ return {};
41
+ }
42
+ }
43
+
44
+ function detectEventName(input) {
45
+ if (typeof input?.hook_event_name === "string" && input.hook_event_name.trim()) {
46
+ return input.hook_event_name.trim();
47
+ }
33
48
  if (process.env.CURSOR_PLUGIN_ROOT) return "SessionStart";
34
49
  return "UserPromptSubmit";
35
50
  }
@@ -43,12 +58,9 @@ function outputFormat() {
43
58
  // ---------------------------------------------------------------------------
44
59
  // Find project root
45
60
  // ---------------------------------------------------------------------------
46
- function findProjectRoot() {
47
- const envDir = process.env.ZCODE_PROJECT_DIR;
48
- if (envDir) {
49
- return existsSync(join(envDir, DIR_WORKFLOW)) ? envDir : null;
50
- }
51
- let current = process.cwd();
61
+ function findWorkflowRoot(startDir) {
62
+ if (typeof startDir !== "string" || !startDir.trim()) return null;
63
+ let current = startDir;
52
64
  const root = dirname(current);
53
65
  while (current !== root) {
54
66
  if (existsSync(join(current, DIR_WORKFLOW))) return current;
@@ -57,6 +69,20 @@ function findProjectRoot() {
57
69
  return existsSync(join(current, DIR_WORKFLOW)) ? current : null;
58
70
  }
59
71
 
72
+ function findProjectRoot(input) {
73
+ const candidates = [
74
+ input?.cwd,
75
+ process.env.ZCODE_PROJECT_DIR,
76
+ process.env.CLAUDE_PROJECT_DIR,
77
+ process.cwd(),
78
+ ];
79
+ for (const candidate of candidates) {
80
+ const root = findWorkflowRoot(candidate);
81
+ if (root) return root;
82
+ }
83
+ return null;
84
+ }
85
+
60
86
  // ---------------------------------------------------------------------------
61
87
  // Read active task from .cowork-flow/.runtime/sessions/
62
88
  // ---------------------------------------------------------------------------
@@ -289,14 +315,15 @@ function main() {
289
315
  process.exit(0);
290
316
  }
291
317
 
292
- const event = detectEventName();
318
+ const input = readHookInput();
319
+ const event = detectEventName(input);
293
320
  const format = outputFormat();
294
- const repoRoot = findProjectRoot();
321
+ const repoRoot = findProjectRoot(input);
295
322
 
296
323
  let context;
297
324
 
298
- const envDir = process.env.ZCODE_PROJECT_DIR;
299
- const effectiveRoot = repoRoot || (envDir && existsSync(join(envDir, DIR_WORKFLOW)) ? envDir : null);
325
+ const envDir = process.env.ZCODE_PROJECT_DIR || process.env.CLAUDE_PROJECT_DIR;
326
+ const effectiveRoot = repoRoot || findWorkflowRoot(envDir);
300
327
 
301
328
  if (!effectiveRoot) {
302
329
  context = `<workflow-state>
@@ -308,7 +335,7 @@ Source: cowork-flow-plugin
308
335
  </workflow-state>`;
309
336
  } else {
310
337
  // PRIORITY 1: Detect delegated_subtask from runtime context
311
- const userPrompt = "";
338
+ const userPrompt = typeof input.prompt === "string" ? input.prompt : "";
312
339
  const delegated = detectDelegatedSubtask(effectiveRoot, userPrompt);
313
340
  if (delegated) {
314
341
  context = buildDelegatedSubtask(delegated.contextId, delegated.ctx);
@@ -321,7 +348,7 @@ Source: cowork-flow-plugin
321
348
  }
322
349
 
323
350
  // Inject contract digest
324
- const rootForDigest = effectiveRoot || envDir;
351
+ const rootForDigest = effectiveRoot || envDir || input.cwd;
325
352
  if (rootForDigest) {
326
353
  const contractDigest = buildContractDigest(rootForDigest);
327
354
  context = `${contractDigest}\n\n${context}`;
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { existsSync, writeFileSync, mkdirSync, rmSync } from "fs";
7
7
  import { join, dirname } from "path";
8
- import { execSync } from "child_process";
8
+ import { spawnSync } from "child_process";
9
9
  import assert from "assert/strict";
10
10
 
11
11
  const tmpRoot = join(import.meta.dirname, ".selfcheck-tmp");
@@ -27,12 +27,18 @@ function test(name, fn) {
27
27
  }
28
28
 
29
29
  function runHook(env = {}, stdin) {
30
- return execSync(`node ${hookScript}`, {
30
+ const result = spawnSync(process.execPath, [hookScript], {
31
31
  env: { ...process.env, ...env },
32
32
  encoding: "utf8",
33
33
  cwd: import.meta.dirname,
34
34
  input: stdin,
35
35
  });
36
+ if (result.status !== 0) {
37
+ const error = new Error(result.stderr || `hook exited with ${result.status}`);
38
+ error.status = result.status;
39
+ throw error;
40
+ }
41
+ return result.stdout;
36
42
  }
37
43
 
38
44
  function writeProjectFile(repoRoot, relativePath, content) {