cowork-flow 0.0.20 → 0.0.21

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 (70) hide show
  1. package/README.md +65 -20
  2. package/package.json +1 -1
  3. package/src/cli.js +109 -2
  4. package/src/commands/init.js +61 -4
  5. package/src/commands/sync.js +10 -1
  6. package/src/lib/copy-template.js +38 -7
  7. package/src/lib/platforms.js +73 -0
  8. package/template/.agent/skills/brainstorming/SKILL.md +13 -9
  9. package/template/.agent/skills/finish-work/SKILL.md +3 -3
  10. package/template/.agent/skills/meta/SKILL.md +1 -1
  11. package/template/.agent/skills/start/SKILL.md +35 -16
  12. package/template/.agent/skills/writing-plans/SKILL.md +1 -1
  13. package/template/.claude/agents/cowork-check.md +41 -0
  14. package/template/.claude/agents/cowork-implement.md +40 -0
  15. package/template/.claude/agents/cowork-research.md +40 -0
  16. package/template/.claude/commands/cowork-check.md +27 -0
  17. package/template/.claude/commands/cowork-implement.md +27 -0
  18. package/template/.claude/commands/cowork-research.md +27 -0
  19. package/template/.claude/hooks/inject-workflow-state.py +298 -0
  20. package/template/.claude/settings.json +24 -0
  21. package/template/.claude/skills/before-dev/SKILL.md +31 -0
  22. package/template/.claude/skills/brainstorming/SKILL.md +32 -0
  23. package/template/.claude/skills/break-loop/SKILL.md +26 -0
  24. package/template/.claude/skills/check/SKILL.md +29 -0
  25. package/template/.claude/skills/continue/SKILL.md +29 -0
  26. package/template/.claude/skills/finish-work/SKILL.md +32 -0
  27. package/template/.claude/skills/meta/SKILL.md +30 -0
  28. package/template/.claude/skills/python-design/SKILL.md +27 -0
  29. package/template/.claude/skills/start/SKILL.md +78 -0
  30. package/template/.claude/skills/update-spec/SKILL.md +39 -0
  31. package/template/.claude/skills/writing-plans/SKILL.md +70 -0
  32. package/template/.codex/agents/cowork-check.toml +17 -22
  33. package/template/.codex/agents/cowork-implement.toml +18 -23
  34. package/template/.codex/agents/cowork-research.toml +14 -19
  35. package/template/.codex/agents/default.toml +27 -0
  36. package/template/.codex/agents/explorer.toml +28 -0
  37. package/template/.codex/agents/worker.toml +29 -0
  38. package/template/.codex/hooks/inject-workflow-state.py +161 -108
  39. package/template/.cowork-flow/.version +1 -1
  40. package/template/.cowork-flow/adapters/claude-code/adapter.yaml +35 -0
  41. package/template/.cowork-flow/adapters/codex/adapter.yaml +35 -0
  42. package/template/.cowork-flow/adapters/opencode/adapter.yaml +33 -0
  43. package/template/.cowork-flow/config.yaml +4 -6
  44. package/template/.cowork-flow/scripts/add_session.py +20 -7
  45. package/template/.cowork-flow/scripts/common/active_task.py +212 -1
  46. package/template/.cowork-flow/scripts/common/config.py +0 -17
  47. package/template/.cowork-flow/scripts/common/entry_classifier.py +122 -0
  48. package/template/.cowork-flow/scripts/common/execution_context.py +10 -17
  49. package/template/.cowork-flow/scripts/common/readiness.py +249 -0
  50. package/template/.cowork-flow/scripts/doctor.py +418 -40
  51. package/template/.cowork-flow/scripts/project_context.py +230 -0
  52. package/template/.cowork-flow/scripts/run.py +3 -0
  53. package/template/.cowork-flow/scripts/subagent.py +152 -238
  54. package/template/.cowork-flow/scripts/task.py +330 -17
  55. package/template/.cowork-flow/spec/adapter.schema.json +132 -0
  56. package/template/.cowork-flow/spec/capabilities.md +34 -0
  57. package/template/.cowork-flow/spec/entry-contract.md +45 -0
  58. package/template/.cowork-flow/spec/registry.json +54 -0
  59. package/template/.cowork-flow/spec/subagent-dispatch.md +77 -0
  60. package/template/.cowork-flow/spec/workflow-state-templates.md +66 -0
  61. package/template/.cowork-flow/workflow.md +158 -203
  62. package/template/.opencode/agents/cowork-check.md +46 -0
  63. package/template/.opencode/agents/cowork-implement.md +45 -0
  64. package/template/.opencode/agents/cowork-research.md +46 -0
  65. package/template/.opencode/commands/cowork-check.md +26 -0
  66. package/template/.opencode/commands/cowork-implement.md +26 -0
  67. package/template/.opencode/commands/cowork-research.md +26 -0
  68. package/template/.opencode/plugins/cowork-flow.js +317 -0
  69. package/template/CLAUDE.md +5 -0
  70. package/template/.agent/skills/entry-boundary/SKILL.md +0 -81
@@ -13,7 +13,18 @@ from .paths import DIR_WORKFLOW
13
13
 
14
14
  DIR_RUNTIME = ".runtime"
15
15
  DIR_SESSIONS = "sessions"
16
+ DIR_SUBAGENTS = "subagents"
16
17
  FIELD_ACTIVE_TASK_PATH = "active_task_path"
18
+ FIELD_RUNTIME_CONTEXT_ID = "runtime_context_id"
19
+ FIELD_SCOPE = "scope"
20
+ SCOPE_MAIN = "main"
21
+ SCOPE_SUBAGENT = "subagent"
22
+ RUNTIME_CONTEXT_PROMPT_RE = re.compile(
23
+ r"(?im)^\s*cowork_runtime_context_id\s*:\s*([A-Za-z0-9._-]+)\s*$"
24
+ )
25
+ HOST_CONTEXT_PROMPT_RE = re.compile(
26
+ r"(?im)^\s*cowork_host_context_key\s*:\s*([A-Za-z0-9._-]+)\s*$"
27
+ )
17
28
 
18
29
 
19
30
  @dataclass(frozen=True)
@@ -38,11 +49,23 @@ def _first_input_value(values: Mapping[str, object] | None, names: tuple[str, ..
38
49
  return None
39
50
 
40
51
 
52
+ def _first_prompt_value(values: Mapping[str, object] | None) -> str | None:
53
+ return _first_input_value(values, ("prompt", "user_prompt", "userPrompt", "message", "input"))
54
+
55
+
41
56
  def resolve_context_key(values: Mapping[str, object] | None = None) -> str | None:
42
57
  explicit = os.environ.get("COWORK_FLOW_CONTEXT_ID")
43
58
  if explicit and explicit.strip():
44
59
  return _sanitize(explicit)
45
60
 
61
+ opencode_session = os.environ.get("OPENCODE_SESSION_ID")
62
+ if opencode_session and opencode_session.strip():
63
+ return f"opencode_{_sanitize(opencode_session)}"
64
+
65
+ claude_session = os.environ.get("CLAUDE_SESSION_ID")
66
+ if claude_session and claude_session.strip():
67
+ return f"claude_{_sanitize(claude_session)}"
68
+
46
69
  codex_session = os.environ.get("CODEX_SESSION_ID")
47
70
  if codex_session and codex_session.strip():
48
71
  return f"codex_{_sanitize(codex_session)}"
@@ -62,6 +85,26 @@ def resolve_context_key(values: Mapping[str, object] | None = None) -> str | Non
62
85
  if input_explicit:
63
86
  return _sanitize(input_explicit)
64
87
 
88
+ input_opencode_session = _first_input_value(
89
+ values,
90
+ (
91
+ "OPENCODE_SESSION_ID",
92
+ "opencode_session_id",
93
+ ),
94
+ )
95
+ if input_opencode_session:
96
+ return f"opencode_{_sanitize(input_opencode_session)}"
97
+
98
+ input_claude_session = _first_input_value(
99
+ values,
100
+ (
101
+ "CLAUDE_SESSION_ID",
102
+ "claude_session_id",
103
+ ),
104
+ )
105
+ if input_claude_session:
106
+ return f"claude_{_sanitize(input_claude_session)}"
107
+
65
108
  input_session = _first_input_value(
66
109
  values,
67
110
  (
@@ -92,6 +135,10 @@ def sessions_dir(repo_root: Path) -> Path:
92
135
  return repo_root / DIR_WORKFLOW / DIR_RUNTIME / DIR_SESSIONS
93
136
 
94
137
 
138
+ def subagent_contexts_dir(repo_root: Path) -> Path:
139
+ return repo_root / DIR_WORKFLOW / DIR_RUNTIME / DIR_SUBAGENTS
140
+
141
+
95
142
  def _now() -> str:
96
143
  return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
97
144
 
@@ -100,6 +147,14 @@ def _session_path(repo_root: Path, context_key: str) -> Path:
100
147
  return sessions_dir(repo_root) / f"{context_key}.json"
101
148
 
102
149
 
150
+ def logical_subagent_context_key(runtime_context_id: str) -> str:
151
+ return f"subagent_{_sanitize(runtime_context_id)}"
152
+
153
+
154
+ def runtime_context_path(repo_root: Path, runtime_context_id: str) -> Path:
155
+ return subagent_contexts_dir(repo_root) / f"{_sanitize(runtime_context_id)}.json"
156
+
157
+
103
158
  def _read_json(path: Path) -> dict:
104
159
  try:
105
160
  data = json.loads(path.read_text(encoding="utf-8"))
@@ -113,6 +168,161 @@ def _write_json(path: Path, data: dict) -> None:
113
168
  path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
114
169
 
115
170
 
171
+ def _platform_from_context_key(context_key: str) -> str:
172
+ if context_key.startswith("codex_"):
173
+ return "codex"
174
+ if context_key.startswith("opencode_"):
175
+ return "opencode"
176
+ if context_key.startswith("claude_"):
177
+ return "claude-code"
178
+ return "manual"
179
+
180
+
181
+ def resolve_runtime_context_id(values: Mapping[str, object] | None = None) -> str | None:
182
+ explicit = os.environ.get("COWORK_FLOW_RUNTIME_CONTEXT_ID")
183
+ if explicit and explicit.strip():
184
+ return _sanitize(explicit)
185
+
186
+ input_explicit = _first_input_value(
187
+ values,
188
+ (
189
+ "COWORK_FLOW_RUNTIME_CONTEXT_ID",
190
+ "cowork_runtime_context_id",
191
+ "runtime_context_id",
192
+ ),
193
+ )
194
+ if input_explicit:
195
+ return _sanitize(input_explicit)
196
+
197
+ prompt = _first_prompt_value(values)
198
+ if prompt:
199
+ match = RUNTIME_CONTEXT_PROMPT_RE.search(prompt)
200
+ if match:
201
+ return _sanitize(match.group(1))
202
+
203
+ return None
204
+
205
+ def resolve_host_context_key(values: Mapping[str, object] | None = None) -> str | None:
206
+ explicit = os.environ.get("COWORK_FLOW_HOST_CONTEXT_KEY")
207
+ if explicit and explicit.strip():
208
+ return _sanitize(explicit)
209
+
210
+ input_explicit = _first_input_value(
211
+ values,
212
+ (
213
+ "cowork_host_context_key",
214
+ "host_context_key",
215
+ "COWORK_FLOW_HOST_CONTEXT_KEY",
216
+ ),
217
+ )
218
+ if input_explicit:
219
+ return _sanitize(input_explicit)
220
+
221
+ prompt = _first_prompt_value(values)
222
+ if prompt:
223
+ match = HOST_CONTEXT_PROMPT_RE.search(prompt)
224
+ if match:
225
+ return _sanitize(match.group(1))
226
+
227
+ return None
228
+
229
+
230
+ def read_runtime_context(repo_root: Path, runtime_context_id: str) -> dict:
231
+ return _read_json(runtime_context_path(repo_root, runtime_context_id))
232
+
233
+
234
+ def write_runtime_context(repo_root: Path, runtime_context_id: str, data: dict) -> None:
235
+ _write_json(runtime_context_path(repo_root, runtime_context_id), data)
236
+
237
+
238
+ def write_subagent_logical_session(
239
+ repo_root: Path,
240
+ runtime_context_id: str,
241
+ task_path: str | None,
242
+ platform: str,
243
+ status: str = "pending_bind",
244
+ ) -> str:
245
+ context_key = logical_subagent_context_key(runtime_context_id)
246
+ data: dict[str, object] = {
247
+ "schema_version": 2,
248
+ FIELD_SCOPE: SCOPE_SUBAGENT,
249
+ FIELD_RUNTIME_CONTEXT_ID: runtime_context_id,
250
+ "platform": platform,
251
+ "status": status,
252
+ "last_seen_at": _now(),
253
+ }
254
+ if task_path:
255
+ data[FIELD_ACTIVE_TASK_PATH] = task_path.replace("\\", "/")
256
+ _write_json(_session_path(repo_root, context_key), data)
257
+ return context_key
258
+
259
+
260
+ def bind_runtime_context(
261
+ repo_root: Path,
262
+ runtime_context_id: str,
263
+ host_context_key: str | None = None,
264
+ values: Mapping[str, object] | None = None,
265
+ ) -> dict | None:
266
+ context = read_runtime_context(repo_root, runtime_context_id)
267
+ if not context or context.get(FIELD_SCOPE) != SCOPE_SUBAGENT:
268
+ return None
269
+ if context.get("status") == "closed":
270
+ return None
271
+
272
+ resolved_key = (
273
+ _sanitize(host_context_key)
274
+ if host_context_key
275
+ else resolve_host_context_key(values) or resolve_context_key(values)
276
+ )
277
+ if not resolved_key:
278
+ return None
279
+
280
+ existing_key = context.get("bound_context_key")
281
+ if isinstance(existing_key, str) and existing_key.strip() and existing_key != resolved_key:
282
+ return None
283
+
284
+ task_path = context.get("task_dir")
285
+ session: dict[str, object] = {
286
+ "schema_version": 2,
287
+ FIELD_SCOPE: SCOPE_SUBAGENT,
288
+ FIELD_RUNTIME_CONTEXT_ID: runtime_context_id,
289
+ "platform": _platform_from_context_key(resolved_key),
290
+ "status": "bound",
291
+ "last_seen_at": _now(),
292
+ }
293
+ if isinstance(task_path, str) and task_path.strip():
294
+ session[FIELD_ACTIVE_TASK_PATH] = task_path.strip()
295
+ _write_json(_session_path(repo_root, resolved_key), session)
296
+
297
+ context["status"] = "bound"
298
+ context["bound_context_key"] = resolved_key
299
+ if not context.get("bound_at"):
300
+ context["bound_at"] = _now()
301
+ context["last_seen_at"] = _now()
302
+ write_runtime_context(repo_root, runtime_context_id, context)
303
+ return context
304
+
305
+
306
+ def close_runtime_context(repo_root: Path, runtime_context_id: str) -> bool:
307
+ context = read_runtime_context(repo_root, runtime_context_id)
308
+ if not context:
309
+ return False
310
+
311
+ bound_context_key = context.get("bound_context_key")
312
+ for context_key in (bound_context_key, logical_subagent_context_key(runtime_context_id)):
313
+ if isinstance(context_key, str) and context_key.strip():
314
+ try:
315
+ _session_path(repo_root, context_key).unlink()
316
+ except FileNotFoundError:
317
+ pass
318
+
319
+ context["status"] = "closed"
320
+ context["closed_at"] = _now()
321
+ context["last_seen_at"] = _now()
322
+ write_runtime_context(repo_root, runtime_context_id, context)
323
+ return True
324
+
325
+
116
326
  def set_active_task(repo_root: Path, task_path: str) -> ActiveTask | None:
117
327
  context_key = resolve_context_key()
118
328
  if not context_key:
@@ -125,7 +335,8 @@ def set_active_task(repo_root: Path, task_path: str) -> ActiveTask | None:
125
335
  _session_path(repo_root, context_key),
126
336
  {
127
337
  FIELD_ACTIVE_TASK_PATH: normalized,
128
- "platform": "codex" if context_key.startswith("codex_") else "manual",
338
+ FIELD_SCOPE: SCOPE_MAIN,
339
+ "platform": _platform_from_context_key(context_key),
129
340
  "last_seen_at": _now(),
130
341
  },
131
342
  )
@@ -16,7 +16,6 @@ from .paths import DIR_WORKFLOW, get_repo_root
16
16
  DEFAULT_SESSION_COMMIT_MESSAGE = "chore: record journal"
17
17
  DEFAULT_MAX_JOURNAL_LINES = 2000
18
18
  DEFAULT_CODEX_DISPATCH_MODE = "sub-agent"
19
- DEFAULT_CODEX_POST_ACK_EXECUTION_GRACE_MS = 300000
20
19
 
21
20
  CONFIG_FILE = "config.yaml"
22
21
 
@@ -140,19 +139,3 @@ def get_codex_dispatch_mode(repo_root: Path | None = None) -> str:
140
139
  if mode in {"sub-agent", "inline"}:
141
140
  return str(mode)
142
141
  return DEFAULT_CODEX_DISPATCH_MODE
143
-
144
-
145
- def get_codex_post_ack_execution_grace_ms(repo_root: Path | None = None) -> int:
146
- """Get the post-ACK execution grace duration for Codex subagent dispatch."""
147
- config = _load_config(repo_root)
148
- codex = config.get("codex")
149
- if not isinstance(codex, dict):
150
- return DEFAULT_CODEX_POST_ACK_EXECUTION_GRACE_MS
151
- value = codex.get("post_ack_execution_grace_ms", DEFAULT_CODEX_POST_ACK_EXECUTION_GRACE_MS)
152
- try:
153
- parsed = int(value)
154
- except (ValueError, TypeError):
155
- return DEFAULT_CODEX_POST_ACK_EXECUTION_GRACE_MS
156
- if parsed <= 0:
157
- return DEFAULT_CODEX_POST_ACK_EXECUTION_GRACE_MS
158
- return parsed
@@ -0,0 +1,122 @@
1
+ """Classify cowork-flow hook entries before workflow-state injection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ class EntryKind:
9
+ MAIN_SESSION = "MAIN_SESSION"
10
+ READ_ONLY = "READ_ONLY"
11
+ COMMAND_ONLY = "COMMAND_ONLY"
12
+ UNKNOWN = "UNKNOWN"
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class Classification:
17
+ entry_kind: str
18
+ confidence: float
19
+ source: str
20
+
21
+
22
+ PROMPT_KEYS = ("prompt", "user_prompt", "userPrompt", "message", "input")
23
+
24
+ TASK_TERMS = (
25
+ "task:",
26
+ "topic:",
27
+ "focus:",
28
+ "任务:",
29
+ "任务:",
30
+ "主题:",
31
+ "目标:",
32
+ "审视",
33
+ "讨论",
34
+ "review",
35
+ "inspect",
36
+ )
37
+
38
+ READ_ONLY_TERMS = (
39
+ "explain",
40
+ "what does",
41
+ "what is",
42
+ "why does",
43
+ "为什么",
44
+ "原因",
45
+ "解释",
46
+ "说明",
47
+ "read-only",
48
+ "inspect only",
49
+ "analysis only",
50
+ "只看",
51
+ "只读",
52
+ "分析一下",
53
+ )
54
+
55
+ COMMAND_ONLY_TERMS = (
56
+ "git status",
57
+ "git diff",
58
+ "npm run",
59
+ ".cowork-flow/run",
60
+ ".cowork-flow\\run.cmd",
61
+ )
62
+
63
+ MAIN_SESSION_TERMS = (
64
+ "main session",
65
+ "main agent",
66
+ "not a subagent",
67
+ "not a sub-agent",
68
+ "run full cowork-flow start",
69
+ "run full project start",
70
+ "继续",
71
+ "实现",
72
+ "提交",
73
+ "归档",
74
+ "修复",
75
+ "主会话",
76
+ "主 agent",
77
+ "主代理",
78
+ "不是子代理",
79
+ "不是子任务",
80
+ "start task",
81
+ "resume",
82
+ "implement",
83
+ "commit",
84
+ "archive",
85
+ "fix",
86
+ )
87
+
88
+
89
+ def extract_prompt(hook_input: dict) -> str:
90
+ values: list[str] = []
91
+ for key in PROMPT_KEYS:
92
+ value = hook_input.get(key)
93
+ if isinstance(value, str) and value.strip():
94
+ values.append(value)
95
+ return "\n".join(values)
96
+
97
+
98
+ def _contains_any(text: str, terms: tuple[str, ...]) -> bool:
99
+ return any(term in text for term in terms)
100
+
101
+
102
+ def classify_entry(hook_input: dict) -> Classification:
103
+ prompt = extract_prompt(hook_input)
104
+ if not prompt.strip():
105
+ return Classification(EntryKind.UNKNOWN, 0.0, "empty_prompt")
106
+
107
+ lowered = prompt.lower()
108
+ has_task = _contains_any(lowered, TASK_TERMS)
109
+
110
+ if _contains_any(lowered, READ_ONLY_TERMS) and not _contains_any(lowered, MAIN_SESSION_TERMS):
111
+ return Classification(EntryKind.READ_ONLY, 0.6, "read_only")
112
+
113
+ if _contains_any(lowered, COMMAND_ONLY_TERMS) and not has_task:
114
+ return Classification(EntryKind.COMMAND_ONLY, 0.6, "command_only")
115
+
116
+ if _contains_any(lowered, MAIN_SESSION_TERMS):
117
+ return Classification(EntryKind.MAIN_SESSION, 0.55, "main_session_heuristic")
118
+
119
+ if has_task:
120
+ return Classification(EntryKind.UNKNOWN, 0.35, "task_prompt_unbound")
121
+
122
+ return Classification(EntryKind.UNKNOWN, 0.3, "unclassified")
@@ -309,17 +309,15 @@ def build_subagent_resume_text(
309
309
  forbidden_actions = context_data.get("forbiddenActions")
310
310
  status_file = context_data.get("statusFile")
311
311
  events_file = context_data.get("eventsFile")
312
- dispatch_id = context_data.get("dispatchId")
313
- ack_token = context_data.get("ackToken")
314
- dispatch_status = context_data.get("dispatchStatus")
312
+ runtime_context_id = context_data.get("runtimeContextId") or context_data.get("runtime_context_id")
313
+ runtime_context_status = context_data.get("runtimeContextStatus")
315
314
  agent_type = context_data.get("agentType")
316
315
  dispatch_reliability = context_data.get("dispatchReliability")
317
- expected_ack = context_data.get("expectedAck")
318
316
  lines = [
319
317
  "========================================",
320
318
  "COWORK-FLOW SUBAGENT RESUME",
321
319
  "========================================",
322
- "Use this only for a delegated subagent's own scoped recovery.",
320
+ "Use this only for a runtime-context subagent's own scoped recovery.",
323
321
  "Do not switch back into the coordinator workflow from this entrypoint.",
324
322
  "",
325
323
  "## SUBAGENT CONTEXT",
@@ -328,18 +326,14 @@ def build_subagent_resume_text(
328
326
  f"Role: {context.role or 'unknown'}",
329
327
  f"Goal: {context.goal or 'unknown'}",
330
328
  ]
331
- if isinstance(dispatch_id, str) and dispatch_id.strip():
332
- lines.append(f"Dispatch ID: {dispatch_id}")
333
- if isinstance(dispatch_status, str) and dispatch_status.strip():
334
- lines.append(f"Dispatch status: {dispatch_status}")
329
+ if isinstance(runtime_context_id, str) and runtime_context_id.strip():
330
+ lines.append(f"Runtime context: {runtime_context_id}")
331
+ if isinstance(runtime_context_status, str) and runtime_context_status.strip():
332
+ lines.append(f"Runtime context status: {runtime_context_status}")
335
333
  if isinstance(agent_type, str) and agent_type.strip():
336
334
  lines.append(f"Agent type: {agent_type}")
337
335
  if isinstance(dispatch_reliability, str) and dispatch_reliability.strip():
338
336
  lines.append(f"Dispatch reliability: {dispatch_reliability}")
339
- if isinstance(ack_token, str) and ack_token.strip():
340
- lines.append(f"ACK token: {ack_token}")
341
- if isinstance(expected_ack, str) and expected_ack.strip():
342
- lines.append(f"Expected ACK: {expected_ack}")
343
337
  if context.context_file:
344
338
  lines.append(f"Context file: {context.context_file}")
345
339
  if isinstance(allowed_context, list) and allowed_context:
@@ -379,10 +373,9 @@ def build_subagent_resume_text(
379
373
  lines.extend([
380
374
  "",
381
375
  "## RULES",
382
- "- If you have not acknowledged this dispatch, first return only: COWORK_ACK <dispatch_id> <ack_token>.",
383
- "- Execute only after the coordinator sends EXECUTE <dispatch_id> for this same dispatch.",
384
- "- If a message names a different dispatch_id, report needs_context and do not execute it.",
385
- "- Generic worker dispatch is best-effort only; no ACK means the coordinator must close or retry it.",
376
+ "- Execute only when the bound runtime context exists, is open, and names this agent type.",
377
+ "- If runtime context is missing, closed, invalid, or mismatched, report needs_context and do not execute it.",
378
+ "- Generic worker dispatch is advisory only and cannot complete formal Implement or Check.",
386
379
  "- Read only prompt-named files and allowed context unless you ask for more context.",
387
380
  "- Do not run task start, task finish, task archive, or unscoped resume.",
388
381
  "- Stop only with success, needs_context, or blocked status evidence.",