cowork-flow 0.0.45 → 0.0.47

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 (27) hide show
  1. package/README.md +11 -3
  2. package/package.json +1 -1
  3. package/src/cli.js +1 -1
  4. package/src/commands/install-zcode-plugin.js +165 -21
  5. package/template/.cowork-flow/.version +1 -1
  6. package/template/.cowork-flow/scripts/adapters/cli/subagent.py +3 -0
  7. package/template/.cowork-flow/scripts/adapters/cli/task.py +24 -185
  8. package/template/.cowork-flow/scripts/adapters/cli/task_navigation.py +70 -1
  9. package/template/.cowork-flow/scripts/adapters/cli/task_next_runner.py +229 -0
  10. package/template/.cowork-flow/scripts/adapters/cli/task_parser.py +1 -1
  11. package/template/.cowork-flow/scripts/adapters/cli/task_tree_commands.py +68 -0
  12. package/template/.cowork-flow/scripts/infra/skill_manifest.py +25 -2
  13. package/template/.cowork-flow/scripts/runtime/session_state.py +2 -0
  14. package/template/.cowork-flow/scripts/services/task_routing.py +11 -0
  15. package/template/.cowork-flow/spec/contracts/skill-owned-actions.md +14 -5
  16. package/template/.zcode/.zcode-plugin/plugin.json +3 -1
  17. package/template/.zcode/agents/cowork-check.md +27 -0
  18. package/template/.zcode/agents/cowork-implement.md +28 -0
  19. package/template/.zcode/agents/cowork-research.md +21 -0
  20. package/template/.zcode/hooks/hooks.json +12 -6
  21. package/template/.zcode/hooks/inject-context.js +40 -13
  22. package/template/.zcode/hooks/inject-context.selfcheck.mjs +8 -2
  23. package/template/skills/adversarial-review/SKILL.md +17 -0
  24. package/template/skills/runtime-health/SKILL.md +6 -1
  25. package/template/skills/runtime-health/scripts/doctor.py +174 -8
  26. package/template/skills/task-review/SKILL.md +5 -0
  27. package/template/skills/task-review/manifest.json +4 -2
@@ -0,0 +1,229 @@
1
+ #!/usr/bin/env python3
2
+ """Delivery runner for deterministic `task next --run` actions."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import sys
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Callable
11
+
12
+ from adapters.cli import task_navigation
13
+ from adapters.cli.task_support import Colors, colored, resolve_task_dir
14
+ from infra.paths import get_repo_root
15
+ from runtime.session_state import get_active_task
16
+
17
+
18
+ LifecycleCommand = Callable[[argparse.Namespace], int]
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class NextActionHandlers:
23
+ create: LifecycleCommand
24
+ start: LifecycleCommand
25
+ review: LifecycleCommand
26
+ complete: LifecycleCommand
27
+ archive: LifecycleCommand
28
+
29
+
30
+ def namespace_with(args: argparse.Namespace, **overrides) -> argparse.Namespace:
31
+ values = vars(args).copy()
32
+ values.update(overrides)
33
+ return argparse.Namespace(**values)
34
+
35
+
36
+ def _display_task_path(repo_root: Path, task_dir: Path) -> str:
37
+ try:
38
+ return task_dir.resolve().relative_to(repo_root.resolve()).as_posix()
39
+ except ValueError:
40
+ return str(task_dir)
41
+
42
+
43
+ def _next_target_for_run(args: argparse.Namespace, repo_root: Path):
44
+ target_input = getattr(args, "dir", None)
45
+ if target_input:
46
+ task_dir = resolve_task_dir(target_input, repo_root)
47
+ return task_dir, _display_task_path(repo_root, task_dir), False
48
+
49
+ active = get_active_task(repo_root)
50
+ if active.task_path:
51
+ task_dir = repo_root / active.task_path
52
+ return task_dir, active.task_path, True
53
+ return None, None, False
54
+
55
+
56
+ def _next_payload_for_run(args: argparse.Namespace, repo_root: Path) -> dict[str, object]:
57
+ task_dir, task_path, active_target = _next_target_for_run(args, repo_root)
58
+ if task_dir is None or task_path is None:
59
+ return task_navigation.build_navigation_payload(
60
+ args=args,
61
+ status="no_task",
62
+ blockers=[],
63
+ active_target=False,
64
+ task_path=None,
65
+ )
66
+ if not task_dir.is_dir():
67
+ return task_navigation.build_navigation_payload(
68
+ args=args,
69
+ status="stale",
70
+ blockers=[f"task directory not found: {task_path}"],
71
+ active_target=active_target,
72
+ task_path=task_path,
73
+ )
74
+ status = task_navigation._status(repo_root, task_dir)
75
+ blockers = task_navigation._blockers(repo_root, task_dir) if status == "planning" else []
76
+ return task_navigation.build_navigation_payload(
77
+ args=args,
78
+ status=status,
79
+ blockers=blockers,
80
+ active_target=active_target,
81
+ task_path=task_path,
82
+ )
83
+
84
+
85
+ def _run_create_action(
86
+ args: argparse.Namespace,
87
+ action: dict[str, object],
88
+ handlers: NextActionHandlers,
89
+ ) -> int:
90
+ if not getattr(args, "title", None):
91
+ print(
92
+ colored("Error: create_task requires --title", Colors.RED),
93
+ file=sys.stderr,
94
+ )
95
+ print(f"Command: {action.get('command')}", file=sys.stderr)
96
+ return 1
97
+ return handlers.create(
98
+ namespace_with(
99
+ args,
100
+ title=args.title,
101
+ slug=getattr(args, "slug", None),
102
+ assignee=getattr(args, "assignee", None),
103
+ priority=getattr(args, "priority", "P2"),
104
+ description=getattr(args, "description", None),
105
+ parent=getattr(args, "parent", None),
106
+ from_plan=getattr(args, "from_plan", None),
107
+ )
108
+ )
109
+
110
+
111
+ def _create_input_names(args: argparse.Namespace) -> list[str]:
112
+ names = (
113
+ "title",
114
+ "slug",
115
+ "assignee",
116
+ "description",
117
+ "parent",
118
+ "from_plan",
119
+ )
120
+ return [name for name in names if getattr(args, name, None)]
121
+
122
+
123
+ def _validated_next_action(args: argparse.Namespace) -> tuple[dict[str, object] | None, int]:
124
+ payload = _next_payload_for_run(args, get_repo_root())
125
+ action = payload.get("action")
126
+ if not isinstance(action, dict):
127
+ print(
128
+ colored("Error: task next did not return an action", Colors.RED),
129
+ file=sys.stderr,
130
+ )
131
+ return None, 1
132
+ create_inputs = _create_input_names(args)
133
+ action_id = action.get("id")
134
+ if create_inputs and action_id != "create_task":
135
+ print(
136
+ colored(
137
+ f"Error: create_task inputs cannot run {action_id}",
138
+ Colors.RED,
139
+ ),
140
+ file=sys.stderr,
141
+ )
142
+ print(
143
+ "Hint: archive or finish the current task first, then rerun "
144
+ "`task next --run --title ... --slug ... --assignee <name>`.",
145
+ file=sys.stderr,
146
+ )
147
+ return None, 1
148
+ if payload.get("blockers") or action.get("blockers"):
149
+ print(colored("Error: next action is blocked", Colors.RED), file=sys.stderr)
150
+ for blocker in payload.get("blockers", []) or action.get("blockers", []):
151
+ print(f" - {blocker}", file=sys.stderr)
152
+ return None, 1
153
+ if not action.get("runnable"):
154
+ action_id = action.get("id")
155
+ print(
156
+ colored(f"Error: next action is not executable: {action_id}", Colors.RED),
157
+ file=sys.stderr,
158
+ )
159
+ if action.get("command"):
160
+ print(f"Command: {action['command']}", file=sys.stderr)
161
+ return None, 1
162
+ return action, 0
163
+
164
+
165
+ def _resolved_next_action_task(args: argparse.Namespace, action_id: object) -> str | None:
166
+ if action_id == "create_task":
167
+ return None
168
+ _, task_path_value, _ = _next_target_for_run(args, get_repo_root())
169
+ if task_path_value is None:
170
+ print(colored("Error: no task target for action", Colors.RED), file=sys.stderr)
171
+ return None
172
+ return task_path_value
173
+
174
+
175
+ def _dispatch_next_lifecycle_action(
176
+ args: argparse.Namespace,
177
+ action_id: object,
178
+ task_path: str | None,
179
+ action: dict[str, object],
180
+ handlers: NextActionHandlers,
181
+ ) -> int:
182
+ if action_id == "create_task":
183
+ return _run_create_action(args, action, handlers)
184
+ if task_path is None:
185
+ return 1
186
+ if action_id == "start_task":
187
+ return handlers.start(
188
+ namespace_with(
189
+ args,
190
+ dir=task_path,
191
+ auto=bool(getattr(args, "auto", False)),
192
+ approved=bool(getattr(args, "approved", False)),
193
+ )
194
+ )
195
+ if action_id == "request_review":
196
+ return handlers.review(namespace_with(args, dir=task_path))
197
+ if action_id == "complete_task":
198
+ return handlers.complete(namespace_with(args, dir=task_path))
199
+ if action_id == "archive_task":
200
+ return handlers.archive(
201
+ namespace_with(
202
+ args,
203
+ name=Path(str(task_path)).name,
204
+ commit=bool(getattr(args, "commit", False)),
205
+ )
206
+ )
207
+ if action_id == "batch_execute":
208
+ return handlers.start(
209
+ namespace_with(
210
+ args,
211
+ dir=task_path,
212
+ auto=True,
213
+ approved=bool(getattr(args, "approved", False)),
214
+ )
215
+ )
216
+ print(
217
+ colored(f"Error: unsupported next action: {action_id}", Colors.RED),
218
+ file=sys.stderr,
219
+ )
220
+ return 1
221
+
222
+
223
+ def run_next_action(args: argparse.Namespace, handlers: NextActionHandlers) -> int:
224
+ action, error_code = _validated_next_action(args)
225
+ if action is None:
226
+ return error_code
227
+ action_id = action.get("id")
228
+ task_path = _resolved_next_action_task(args, action_id)
229
+ return _dispatch_next_lifecycle_action(args, action_id, task_path, action, handlers)
@@ -14,7 +14,7 @@ USAGE_TEXT = """Task workflow router for cowork-flow
14
14
  Usage:
15
15
  ./.cowork-flow/run task next [dir] [--json] [--intent I]
16
16
  ./.cowork-flow/run task next [dir] --validate
17
- ./.cowork-flow/run task next --list [--mine] [--status S]
17
+ ./.cowork-flow/run task next --list [--json] [--mine] [--status S]
18
18
  ./.cowork-flow/run task next [dir] --run [--intent I]
19
19
 
20
20
  Public contract:
@@ -3,6 +3,7 @@
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
+ import json
6
7
  import sys
7
8
 
8
9
  from services.task_tree import TaskTreeError, TaskTreeService
@@ -140,6 +141,73 @@ def cmd_list(args) -> int:
140
141
  return 0
141
142
 
142
143
 
144
+ def _list_task_records(repo_root, *, mine: bool, status: str | None):
145
+ active_task = get_active_task(repo_root).task_path
146
+ developer = get_developer(repo_root)
147
+ if mine and not developer:
148
+ return None, "No developer set. Run init_developer.py first"
149
+
150
+ service = TaskTreeService(repo_root)
151
+ all_tasks = service.active_nodes()
152
+ records: list[dict[str, object]] = []
153
+
154
+ def append_task(dir_name: str, depth: int = 0) -> None:
155
+ info = all_tasks[dir_name]
156
+ if mine and info.assignee != developer:
157
+ return
158
+ if status and info.status != status:
159
+ return
160
+ relative_path = f"{DIR_WORKFLOW}/{DIR_TASKS}/{dir_name}"
161
+ done, total = service.children_progress(info.children, all_tasks)
162
+ records.append(
163
+ {
164
+ "name": dir_name,
165
+ "path": relative_path,
166
+ "status": info.status,
167
+ "assignee": info.assignee,
168
+ "parent": info.parent,
169
+ "children": list(info.children),
170
+ "childrenDone": done,
171
+ "childrenTotal": total,
172
+ "depth": depth,
173
+ "active": relative_path == active_task,
174
+ }
175
+ )
176
+ for child_name in info.children:
177
+ if child_name in all_tasks:
178
+ append_task(child_name, depth + 1)
179
+
180
+ for dir_name in service.root_names(all_tasks):
181
+ append_task(dir_name)
182
+ return records, None
183
+
184
+
185
+ def cmd_list_json(args) -> int:
186
+ repo_root = get_repo_root()
187
+ records, error = _list_task_records(
188
+ repo_root,
189
+ mine=bool(getattr(args, "mine", False)),
190
+ status=getattr(args, "status", None),
191
+ )
192
+ if error:
193
+ print(colored(f"Error: {error}", Colors.RED), file=sys.stderr)
194
+ return 1
195
+ print(
196
+ json.dumps(
197
+ {
198
+ "tasks": records or [],
199
+ "count": len(records or []),
200
+ "filters": {
201
+ "mine": bool(getattr(args, "mine", False)),
202
+ "status": getattr(args, "status", None),
203
+ },
204
+ },
205
+ ensure_ascii=False,
206
+ )
207
+ )
208
+ return 0
209
+
210
+
143
211
  def cmd_list_archive(args) -> int:
144
212
  repo_root = get_repo_root()
145
213
  month = args.month
@@ -27,6 +27,7 @@ class SkillAction:
27
27
  lifecycle_check: str | None
28
28
  mutates_state: bool
29
29
  command: str | None
30
+ diagnostics_command: str | None
30
31
 
31
32
 
32
33
  @dataclass(frozen=True)
@@ -142,7 +143,14 @@ def _parse_manifest(path: Path, raw: dict[str, Any]) -> SkillManifest:
142
143
  _reject_unknown_fields(
143
144
  item,
144
145
  allowed=frozenset(
145
- {"id", "label", "lifecycleCheck", "mutatesState", "command"}
146
+ {
147
+ "id",
148
+ "label",
149
+ "lifecycleCheck",
150
+ "mutatesState",
151
+ "command",
152
+ "diagnosticsCommand",
153
+ }
146
154
  ),
147
155
  scope="manifest action",
148
156
  path=path,
@@ -162,6 +170,9 @@ def _parse_manifest(path: Path, raw: dict[str, Any]) -> SkillManifest:
162
170
  command = item.get("command")
163
171
  if command is not None and not isinstance(command, str):
164
172
  raise SkillManifestError(f"manifest command is invalid: {path}")
173
+ diagnostics_command = item.get("diagnosticsCommand")
174
+ if diagnostics_command is not None and not isinstance(diagnostics_command, str):
175
+ raise SkillManifestError(f"manifest diagnosticsCommand is invalid: {path}")
165
176
  actions.append(
166
177
  SkillAction(
167
178
  skill=skill,
@@ -170,6 +181,11 @@ def _parse_manifest(path: Path, raw: dict[str, Any]) -> SkillManifest:
170
181
  lifecycle_check=lifecycle_check.strip() if isinstance(lifecycle_check, str) else None,
171
182
  mutates_state=mutates_state,
172
183
  command=command.strip() if isinstance(command, str) else None,
184
+ diagnostics_command=(
185
+ diagnostics_command.strip()
186
+ if isinstance(diagnostics_command, str)
187
+ else None
188
+ ),
173
189
  )
174
190
  )
175
191
 
@@ -333,7 +349,14 @@ def load_skill_manifests(repo_root: Path) -> tuple[SkillManifest, ...]:
333
349
  def _manifest_signature(manifest: SkillManifest) -> tuple[object, ...]:
334
350
  return (
335
351
  tuple(
336
- (item.action_id, item.label, item.lifecycle_check, item.mutates_state, item.command)
352
+ (
353
+ item.action_id,
354
+ item.label,
355
+ item.lifecycle_check,
356
+ item.mutates_state,
357
+ item.command,
358
+ item.diagnostics_command,
359
+ )
337
360
  for item in manifest.actions
338
361
  ),
339
362
  tuple(
@@ -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")),
@@ -47,6 +47,12 @@ def _action_command(action_id: str, task_path: str | None, template: str | None)
47
47
  return None
48
48
 
49
49
 
50
+ def _diagnostics_command(task_path: str | None, template: str | None) -> str | None:
51
+ if template:
52
+ return template.replace("<task-dir>", task_path or "<task-dir>")
53
+ return None
54
+
55
+
50
56
  def _action_contract(
51
57
  *,
52
58
  status: str,
@@ -90,6 +96,10 @@ def _action_contract(
90
96
  "label": owner.label if owner is not None else action_id,
91
97
  "activatedSkill": owner.skill if owner is not None else None,
92
98
  "command": _action_command(action_id, task_path, owner.command if owner else None),
99
+ "diagnosticsCommand": _diagnostics_command(
100
+ task_path,
101
+ owner.diagnostics_command if owner else None,
102
+ ),
93
103
  "mutatesState": action["mutatesState"],
94
104
  "lifecycleCheck": lifecycle_check,
95
105
  "runtimeGate": lifecycle_check,
@@ -141,6 +151,7 @@ def route_request(
141
151
  "nextAction": action["id"],
142
152
  "activatedSkill": action["activatedSkill"],
143
153
  "actionCommand": action["command"],
154
+ "diagnosticsCommand": action["diagnosticsCommand"],
144
155
  "mutatesState": action["mutatesState"],
145
156
  "lifecycleCheck": action["lifecycleCheck"],
146
157
  "runtimeGate": action["runtimeGate"],
@@ -7,7 +7,7 @@ not contain Skill ids, prompt wording, CLI commands, or script paths.
7
7
  Each Skill may declare its own `manifest.json` with:
8
8
 
9
9
  - `actions`: action id, display label, lifecycle check, mutation fact, and an
10
- optional command description;
10
+ optional command description plus optional `diagnosticsCommand`;
11
11
  - `context`: implement/check/debug activation rules by dev type or path;
12
12
  - `commands`: Skill-owned runtime entrypoints.
13
13
 
@@ -28,7 +28,9 @@ resume and result recording are also Skill commands rather than task CLI
28
28
  handlers.
29
29
 
30
30
  Review-oriented Skill commands may provide advisory facts, such as changed-file
31
- coverage, applicable spec sources, and test-intent signals. They must remain
31
+ coverage, applicable spec sources, and test-intent signals. An action-level
32
+ `diagnosticsCommand` may expose these helpers in text and JSON navigation, but
33
+ it does not make the helper runnable as a lifecycle action. They must remain
32
34
  read-only helpers: no task-local review evidence file, lifecycle state mutation,
33
35
  natural-language spec hard gate, or pass/fail completion verdict.
34
36
 
@@ -41,6 +43,13 @@ Skill-id priority tables.
41
43
 
42
44
  Runtime Health has two scopes. An installed project validates its installed
43
45
  runtime, detected host platforms, and installed Skill manifests without
44
- requiring a `template/` directory. A cowork-flow source checkout additionally
45
- validates complete template host assets and template-to-live runtime and Skill
46
- replica parity, including every detected Skill target.
46
+ requiring a `template/` directory. A cowork-flow source checkout distinguishes
47
+ tracked source checkout bootstrap files, ignored local live runtime, and the
48
+ template distribution source. Source checkout health must not force-track the whole `.cowork-flow/` tree: tracked bootstrap files must match the template,
49
+ ignored local live runtime is compared only when present, and Skill replica
50
+ parity is checked for every detected installed Skill target.
51
+
52
+ Runtime Health also reports stale task hygiene read-only. It may warn about
53
+ completed-unarchived tasks, active-unbound tasks, and missing task context, and
54
+ it may include command hints. These warnings must not mutate task lifecycle
55
+ state, write evidence files, or become completion gates.
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "0.0.45",
3
+ "version": "0.0.47",
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
  }