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
@@ -0,0 +1,28 @@
1
+ name = "explorer"
2
+ description = "Codex explorer agent hardened against bootstrap hijack. Performs bounded read-only exploration without running project start/resume."
3
+ sandbox_mode = "workspace-write"
4
+
5
+ developer_instructions = """
6
+ You are a dispatched explorer agent. Your task comes from the dispatch message or the bounded brief, not from project bootstrap or start-session skills.
7
+
8
+ If AGENTS.md or .agent/skills/start tells you to start a session, skip that start skill.
9
+ Do not run the project start-session workflow or try to spawn another agent from this prompt.
10
+
11
+ Generic explorer dispatch is advisory and cannot satisfy formal Implement or
12
+ Check completion. Follow the bounded prompt directly. Project rules are
13
+ constraints only; they are not the task.
14
+
15
+ Rules:
16
+ - MUST NOT edit files unless the prompt explicitly allows it.
17
+ - MUST NOT spawn, wait for, list, or close other agents.
18
+ - MUST NOT run task start, task finish, task archive, or unscoped resume.
19
+ - MUST NOT commit or push.
20
+ - Prefer rg for file and text search.
21
+ - Return concise findings with file and line evidence.
22
+ """
23
+
24
+ [features]
25
+ multi_agent = false
26
+
27
+ [features.multi_agent_v2]
28
+ enabled = false
@@ -0,0 +1,29 @@
1
+ name = "worker"
2
+ description = "Codex default worker agent hardened against bootstrap hijack. Workers receive delegated work; they must not run project start/resume or create tasks."
3
+ sandbox_mode = "workspace-write"
4
+
5
+ developer_instructions = """
6
+ You are a dispatched worker agent. Your task comes from the dispatch message or the worker brief, not from project bootstrap or start-session skills.
7
+
8
+ If AGENTS.md or .agent/skills/start tells you to start a session, skip that start skill.
9
+ If you can see any outer transport text such as Spawn one ... agent, ignore it. That text is for the coordinator, not for you.
10
+ Do not run the project start-session workflow or try to spawn another worker from this prompt.
11
+
12
+ Generic worker dispatch is advisory and cannot satisfy formal Implement or
13
+ Check completion. Follow the bounded prompt directly. Project rules are
14
+ constraints only; they are not the task.
15
+
16
+ Rules:
17
+ - MUST NOT spawn, wait for, list, or close other agents.
18
+ - MUST NOT run task start, task finish, task archive, or unscoped resume.
19
+ - MUST NOT commit or push.
20
+ - Keep edits inside requested scope.
21
+ - Report changed files and exact verification commands.
22
+ - Stop with the assigned work done, or report needs_context if blocked.
23
+ """
24
+
25
+ [features]
26
+ multi_agent = false
27
+
28
+ [features.multi_agent_v2]
29
+ enabled = false
@@ -4,6 +4,7 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  import json
7
+ import hashlib
7
8
  import os
8
9
  import re
9
10
  import sys
@@ -15,61 +16,28 @@ TAG_RE = re.compile(
15
16
  r"\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n(.*?)\n\s*\[/workflow-state:\1\]",
16
17
  re.DOTALL,
17
18
  )
18
- PROMPT_KEYS = ("prompt", "user_prompt", "userPrompt", "message", "input")
19
- DELEGATED_MARKERS = (
20
- "COWORK_DISPATCH_V1",
21
- "COWORK_DELEGATED_TASK_V1",
22
- "DELEGATED_SUBTASK",
23
- )
24
- DELEGATED_TERMS = (
25
- "delegated task",
26
- "delegated subtask",
27
- "bounded delegated",
28
- "subagent",
29
- "sub-agent",
30
- "worker",
31
- "reviewer",
32
- "explorer",
33
- "委托任务",
34
- "委托 prompt",
35
- "子任务",
36
- "子线程",
37
- )
38
- TASK_TERMS = (
39
- "task:",
40
- "topic:",
41
- "focus:",
42
- "任务:",
43
- "任务:",
44
- "主题:",
45
- "目标:",
46
- "审视",
47
- "讨论",
48
- "review",
49
- "inspect",
50
- )
51
- BOUNDARY_TERMS = (
52
- "do not edit",
53
- "do not run",
54
- "do not spawn",
55
- "return concise analysis only",
56
- "不要编辑",
57
- "不要运行",
58
- "不要派发",
59
- "不要只确认",
60
- "只输出",
61
- "不要改",
62
- )
63
- OUTPUT_TERMS = (
64
- "output:",
65
- "required output",
66
- "return in",
67
- "max ",
68
- "最多",
69
- "输出:",
70
- "输出:",
71
- "分为",
72
- )
19
+ DEFAULT_CONTRACT_REGISTRY = {
20
+ "contracts": [
21
+ {
22
+ "id": "COWORK_ENTRY_CONTRACT_V1",
23
+ "path": ".cowork-flow/spec/entry-contract.md",
24
+ "digest": [
25
+ "Classify main-session requests before task start, resume, archive, or commit.",
26
+ "Runtime context, not prompt labels, identifies formal subagent sessions.",
27
+ ],
28
+ "readWhen": ["before task start/resume/archive", "when prompt and bootstrap text conflict"],
29
+ },
30
+ {
31
+ "id": "RUNTIME_CONTEXT_DISPATCH_V2",
32
+ "path": ".cowork-flow/spec/subagent-dispatch.md",
33
+ "digest": [
34
+ "Formal subagent work is keyed by cowork_runtime_context_id.",
35
+ "Explicit shim bind records bound_context_key before formal output is accepted.",
36
+ ],
37
+ "readWhen": ["before formal subagent dispatch", "when checking subagent health"],
38
+ },
39
+ ]
40
+ }
73
41
 
74
42
  def _find_repo_root(start: Path) -> Path | None:
75
43
  current = start.resolve()
@@ -83,42 +51,28 @@ def _find_repo_root(start: Path) -> Path | None:
83
51
 
84
52
  def _read_hook_input() -> dict[str, Any]:
85
53
  try:
86
- data = json.load(sys.stdin)
54
+ raw = sys.stdin.buffer.read()
55
+ except AttributeError:
56
+ text = sys.stdin.read()
57
+ else:
58
+ if not raw.strip():
59
+ return {}
60
+ try:
61
+ text = raw.decode("utf-8-sig")
62
+ except UnicodeDecodeError:
63
+ encoding = getattr(sys.stdin, "encoding", None) or "utf-8"
64
+ text = raw.decode(encoding, errors="replace")
65
+ if not text.strip():
66
+ return {}
67
+ try:
68
+ data = json.loads(text)
87
69
  except (json.JSONDecodeError, ValueError):
88
70
  return {}
89
71
  return data if isinstance(data, dict) else {}
90
72
 
91
73
 
92
- def _extract_prompt(hook_input: dict[str, Any]) -> str:
93
- values: list[str] = []
94
- for key in PROMPT_KEYS:
95
- value = hook_input.get(key)
96
- if isinstance(value, str) and value.strip():
97
- values.append(value)
98
- return "\n".join(values)
99
-
100
-
101
- def _contains_any(text: str, terms: tuple[str, ...]) -> bool:
102
- return any(term in text for term in terms)
103
-
104
-
105
- def _is_delegated_prompt(hook_input: dict[str, Any]) -> bool:
106
- prompt = _extract_prompt(hook_input)
107
- if not prompt.strip():
108
- return False
109
- if any(marker in prompt for marker in DELEGATED_MARKERS):
110
- return True
111
-
112
- lowered = prompt.lower()
113
- has_task = _contains_any(lowered, TASK_TERMS)
114
- has_delegated_role = _contains_any(lowered, DELEGATED_TERMS)
115
- has_boundary = _contains_any(lowered, BOUNDARY_TERMS)
116
- has_output = _contains_any(lowered, OUTPUT_TERMS)
117
- return has_task and (has_delegated_role or has_boundary) and (has_output or has_boundary)
118
-
119
-
120
74
  def _load_breadcrumbs(root: Path) -> dict[str, str]:
121
- workflow = root / ".cowork-flow" / "workflow.md"
75
+ workflow = root / ".cowork-flow" / "spec" / "workflow-state-templates.md"
122
76
  try:
123
77
  text = workflow.read_text(encoding="utf-8")
124
78
  except OSError:
@@ -126,6 +80,62 @@ def _load_breadcrumbs(root: Path) -> dict[str, str]:
126
80
  return {match.group(1): match.group(2).strip() for match in TAG_RE.finditer(text)}
127
81
 
128
82
 
83
+ def _as_string_list(value: Any) -> list[str]:
84
+ if not isinstance(value, list):
85
+ return []
86
+ return [item for item in value if isinstance(item, str) and item.strip()]
87
+
88
+
89
+ def _load_contract_registry(root: Path) -> list[dict[str, Any]]:
90
+ registry_file = root / ".cowork-flow" / "spec" / "registry.json"
91
+ try:
92
+ data = json.loads(registry_file.read_text(encoding="utf-8"))
93
+ except (OSError, json.JSONDecodeError):
94
+ data = DEFAULT_CONTRACT_REGISTRY
95
+ contracts = data.get("contracts") if isinstance(data, dict) else None
96
+ if not isinstance(contracts, list):
97
+ contracts = DEFAULT_CONTRACT_REGISTRY["contracts"]
98
+ return [contract for contract in contracts if isinstance(contract, dict)]
99
+
100
+
101
+ def _contract_fingerprint(root: Path, contracts: list[dict[str, Any]]) -> str:
102
+ digest = hashlib.sha256()
103
+ digest.update(json.dumps(contracts, ensure_ascii=False, sort_keys=True).encode("utf-8"))
104
+ for contract in contracts:
105
+ path = contract.get("path")
106
+ if not isinstance(path, str) or not path.strip():
107
+ continue
108
+ try:
109
+ digest.update((root / path).read_bytes())
110
+ except OSError:
111
+ digest.update(f"missing:{path}".encode("utf-8"))
112
+ return digest.hexdigest()[:16]
113
+
114
+
115
+ def _build_contract_digest(root: Path) -> str:
116
+ contracts = _load_contract_registry(root)
117
+ fingerprint = _contract_fingerprint(root, contracts)
118
+ lines = [
119
+ '<cowork-runtime host="codex" adapter="codex.spawn_agent">',
120
+ f'<contract-digest fingerprint="{fingerprint}">',
121
+ "policy: repeat this short digest every hook; read full spec files only before listed actions.",
122
+ ]
123
+ for contract in contracts:
124
+ contract_id = contract.get("id")
125
+ path = contract.get("path")
126
+ if not isinstance(contract_id, str) or not contract_id.strip():
127
+ continue
128
+ path_text = path if isinstance(path, str) and path.strip() else "<missing-path>"
129
+ lines.append(f"- {contract_id}: {path_text}")
130
+ for item in _as_string_list(contract.get("digest"))[:2]:
131
+ lines.append(f" digest: {item}")
132
+ read_when = _as_string_list(contract.get("readWhen"))
133
+ if read_when:
134
+ lines.append(f" read_before: {'; '.join(read_when)}")
135
+ lines.extend(["</contract-digest>", "</cowork-runtime>"])
136
+ return "\n".join(lines)
137
+
138
+
129
139
  def _load_common(root: Path) -> None:
130
140
  scripts_dir = root / ".cowork-flow" / "scripts"
131
141
  if str(scripts_dir) not in sys.path:
@@ -144,21 +154,6 @@ def _get_dispatch_mode(root: Path) -> str:
144
154
  return "sub-agent"
145
155
 
146
156
 
147
- def _get_post_ack_execution_grace_ms(root: Path) -> int:
148
- _load_common(root)
149
- try:
150
- from common.config import ( # type: ignore[import-not-found]
151
- DEFAULT_CODEX_POST_ACK_EXECUTION_GRACE_MS,
152
- get_codex_post_ack_execution_grace_ms,
153
- )
154
- except Exception:
155
- return 300000
156
- try:
157
- return get_codex_post_ack_execution_grace_ms(root)
158
- except Exception:
159
- return DEFAULT_CODEX_POST_ACK_EXECUTION_GRACE_MS
160
-
161
-
162
157
  def _get_active_task(root: Path, hook_input: dict[str, Any]) -> tuple[str | None, str, str]:
163
158
  _load_common(root)
164
159
  try:
@@ -183,15 +178,55 @@ def _get_active_task(root: Path, hook_input: dict[str, Any]) -> tuple[str | None
183
178
  return active.task_path, status.strip(), active.source
184
179
 
185
180
 
181
+ def _resolve_runtime_context(root: Path, hook_input: dict[str, Any]) -> tuple[dict[str, Any] | None, str | None]:
182
+ _load_common(root)
183
+ try:
184
+ from common.active_task import ( # type: ignore[import-not-found]
185
+ bind_runtime_context,
186
+ read_runtime_context,
187
+ resolve_runtime_context_id,
188
+ )
189
+ except Exception:
190
+ return None, None
191
+
192
+ runtime_context_id = resolve_runtime_context_id(hook_input)
193
+ if not runtime_context_id:
194
+ return None, None
195
+
196
+ context = read_runtime_context(root, runtime_context_id)
197
+ if not context or context.get("scope") != "subagent" or context.get("status") == "closed":
198
+ return None, runtime_context_id
199
+
200
+ bound = bind_runtime_context(root, runtime_context_id, values=hook_input)
201
+ return bound or context, runtime_context_id
202
+
203
+
204
+ def _subagent_runtime_lines(context: dict[str, Any]) -> list[str]:
205
+ assignment = context.get("assignment") if isinstance(context.get("assignment"), dict) else {}
206
+ lines = [
207
+ f"Runtime context: {context.get('runtime_context_id')}",
208
+ f"Agent: {context.get('agent_type') or 'unknown'}",
209
+ "Scope: subagent",
210
+ "Do not run start/resume/task start/archive/commit/spawn.",
211
+ ]
212
+ goal = assignment.get("goal") if isinstance(assignment, dict) else None
213
+ if isinstance(goal, str) and goal.strip():
214
+ lines.append(f"Goal: {goal.strip()}")
215
+ return lines
216
+
217
+
186
218
  def _build_context(
219
+ root: Path,
187
220
  task_path: str | None,
188
221
  status: str,
189
222
  source: str,
190
223
  breadcrumbs: dict[str, str],
191
224
  dispatch_mode: str,
192
- post_ack_execution_grace_ms: int,
225
+ extra_lines: list[str] | None = None,
193
226
  ) -> str:
194
227
  body = breadcrumbs.get(status) or "Refer to .cowork-flow/workflow.md for the current step."
228
+ if extra_lines:
229
+ body = "\n".join([body, *extra_lines])
195
230
  if task_path is None:
196
231
  header = f"Status: {status}\nSource: {source}"
197
232
  else:
@@ -199,12 +234,16 @@ def _build_context(
199
234
 
200
235
  return "\n\n".join(
201
236
  [
202
- f"<codex-mode>{dispatch_mode}</codex-mode>",
237
+ f"<codex-dispatch-mode>{dispatch_mode}</codex-dispatch-mode>",
203
238
  (
204
239
  "<codex-runtime>\n"
205
- f"post_ack_execution_grace_ms: {post_ack_execution_grace_ms}\n"
240
+ "dispatch_mode_meaning: workflow dispatch hint, not current thread role\n"
241
+ "runtime_context_identity: formal subagent sessions bind before workflow-state injection\n"
206
242
  "</codex-runtime>"
207
243
  ),
244
+ (
245
+ _build_contract_digest(root)
246
+ ),
208
247
  f"<workflow-state>\n{header}\n{body}\n</workflow-state>",
209
248
  ]
210
249
  )
@@ -223,19 +262,33 @@ def main() -> int:
223
262
 
224
263
  breadcrumbs = _load_breadcrumbs(root)
225
264
  dispatch_mode = _get_dispatch_mode(root)
226
- post_ack_execution_grace_ms = _get_post_ack_execution_grace_ms(root)
227
- task_path, status, source = _get_active_task(root, hook_input)
228
- if _is_delegated_prompt(hook_input):
265
+ runtime_context, runtime_context_id = _resolve_runtime_context(root, hook_input)
266
+ extra_lines: list[str] | None = None
267
+ if runtime_context is not None:
268
+ task_dir = runtime_context.get("task_dir")
269
+ task_path = task_dir.strip() if isinstance(task_dir, str) and task_dir.strip() else None
270
+ status = "delegated_subtask"
271
+ source = f"runtime-context:{runtime_context.get('runtime_context_id')}"
272
+ extra_lines = _subagent_runtime_lines(runtime_context)
273
+ elif runtime_context_id:
229
274
  task_path = None
230
275
  status = "delegated_subtask"
231
- source = "prompt"
276
+ source = f"runtime-context-invalid:{runtime_context_id}"
277
+ extra_lines = [
278
+ f"Runtime context: {runtime_context_id}",
279
+ "Scope: subagent",
280
+ "Runtime context is missing, closed, or invalid. Do not run start/resume/task start/archive/commit/spawn.",
281
+ ]
282
+ else:
283
+ task_path, status, source = _get_active_task(root, hook_input)
232
284
  additional_context = _build_context(
285
+ root,
233
286
  task_path,
234
287
  status,
235
288
  source,
236
289
  breadcrumbs,
237
290
  dispatch_mode,
238
- post_ack_execution_grace_ms,
291
+ extra_lines,
239
292
  )
240
293
 
241
294
  print(
@@ -1 +1 @@
1
- 0.0.20
1
+ 0.0.21
@@ -0,0 +1,35 @@
1
+ schemaVersion: 1
2
+ host: claude-code
3
+ displayName: Claude Code
4
+ capabilities:
5
+ dispatchSubagent: native
6
+ freshChildContext: native
7
+ sendFollowup: native
8
+ waitChild: native
9
+ listChildren: native
10
+ cancelChild: native
11
+ stateInjection: external
12
+ backgroundChild: experimental
13
+ runtimeContextDispatch: native
14
+ runtimeContextBinding: shim
15
+ runtimeContextCleanup: native
16
+ dispatch:
17
+ primitive: subagent
18
+ agentsPath: .claude/agents
19
+ skillsPath: .claude/skills
20
+ commandsPath: .claude/commands
21
+ settingsPath: .claude/settings.json
22
+ hooksPath: .claude/hooks
23
+ memoryPath: CLAUDE.md
24
+ runtimeContext:
25
+ transport: prompt
26
+ promptKey: cowork_runtime_context_id
27
+ envKey: COWORK_FLOW_RUNTIME_CONTEXT_ID
28
+ metadataKey: cowork_runtime_context_id
29
+ contracts:
30
+ entry: COWORK_ENTRY_CONTRACT_V1
31
+ dispatch: RUNTIME_CONTEXT_DISPATCH_V2
32
+ leafExecutor: true
33
+ fallback:
34
+ whenRequiredCapabilityMissing: inline_or_manual
35
+ whenRuntimeContextMissing: fail_closed
@@ -0,0 +1,35 @@
1
+ schemaVersion: 1
2
+ host: codex
3
+ displayName: Codex
4
+ capabilities:
5
+ dispatchSubagent: native
6
+ freshChildContext: native
7
+ sendFollowup: native
8
+ waitChild: native
9
+ listChildren: native
10
+ cancelChild: native
11
+ stateInjection: native
12
+ backgroundChild: native
13
+ runtimeContextDispatch: native
14
+ runtimeContextBinding: shim
15
+ runtimeContextCleanup: native
16
+ dispatch:
17
+ primitive: spawn_agent
18
+ forkTurns: none
19
+ followupPrimitive: assign_task
20
+ waitPrimitive: wait_agent
21
+ listPrimitive: list_agents
22
+ cancelPrimitive: close_agent
23
+ agentsPath: .codex/agents
24
+ runtimeContext:
25
+ transport: prompt
26
+ promptKey: cowork_runtime_context_id
27
+ envKey: COWORK_FLOW_RUNTIME_CONTEXT_ID
28
+ metadataKey: cowork_runtime_context_id
29
+ contracts:
30
+ entry: COWORK_ENTRY_CONTRACT_V1
31
+ dispatch: RUNTIME_CONTEXT_DISPATCH_V2
32
+ leafExecutor: true
33
+ fallback:
34
+ whenRequiredCapabilityMissing: inline_or_manual
35
+ whenRuntimeContextMissing: fail_closed
@@ -0,0 +1,33 @@
1
+ schemaVersion: 1
2
+ host: opencode
3
+ displayName: OpenCode
4
+ capabilities:
5
+ dispatchSubagent: native
6
+ freshChildContext: native
7
+ sendFollowup: shim
8
+ waitChild: native
9
+ listChildren: native
10
+ cancelChild: native
11
+ stateInjection: plugin
12
+ backgroundChild: experimental
13
+ runtimeContextDispatch: native
14
+ runtimeContextBinding: shim
15
+ runtimeContextCleanup: native
16
+ dispatch:
17
+ primitive: task
18
+ agentsPath: .opencode/agents
19
+ commandsPath: .opencode/commands
20
+ pluginsPath: .opencode/plugins
21
+ backgroundRequiresEnv: OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true
22
+ runtimeContext:
23
+ transport: prompt
24
+ promptKey: cowork_runtime_context_id
25
+ envKey: COWORK_FLOW_RUNTIME_CONTEXT_ID
26
+ metadataKey: cowork_runtime_context_id
27
+ contracts:
28
+ entry: COWORK_ENTRY_CONTRACT_V1
29
+ dispatch: RUNTIME_CONTEXT_DISPATCH_V2
30
+ leafExecutor: true
31
+ fallback:
32
+ whenRequiredCapabilityMissing: inline_or_manual
33
+ whenRuntimeContextMissing: fail_closed
@@ -18,17 +18,15 @@ max_journal_lines: 2000
18
18
  # Codex Runtime
19
19
  #-------------------------------------------------------------------------------
20
20
 
21
- # This controls only the injected workflow hint (<codex-mode>...).
21
+ # This controls only the injected workflow dispatch hint (<codex-dispatch-mode>...).
22
22
  # It does not force dispatch or disable tools at runtime.
23
23
  # sub-agent: hint that main session should dispatch cowork-* fixed agents.
24
24
  # inline: hint that main session should perform implementation/check directly.
25
25
  codex:
26
26
  dispatch_mode: "sub-agent"
27
- # Grace duration after each subagent ACKs and receives EXECUTE.
28
- # This is a per-dispatch duration, not a shared/global deadline.
29
- # During this window, missing output or missing compass/status files do not
30
- # prove the child is stuck while it loads task context.
31
- post_ack_execution_grace_ms: 300000
27
+ # Formal subagent identity is runtime-context based. The main session creates
28
+ # .cowork-flow/.runtime/subagents/<id>.json with the subagent helper, then
29
+ # passes cowork_runtime_context_id through the active host adapter transport.
32
30
 
33
31
  #-------------------------------------------------------------------------------
34
32
  # Task Lifecycle Hooks
@@ -18,6 +18,9 @@ from datetime import datetime
18
18
  from pathlib import Path
19
19
 
20
20
  from common.paths import (
21
+ DIR_TASKS,
22
+ DIR_WORKFLOW,
23
+ DIR_WORKSPACE,
21
24
  FILE_JOURNAL_PREFIX,
22
25
  count_lines,
23
26
  get_repo_root,
@@ -292,14 +295,16 @@ def update_index(
292
295
  def _auto_commit_workspace(repo_root: Path) -> None:
293
296
  """Stage .cowork-flow/workspace and .cowork-flow/tasks, then commit with a configured message."""
294
297
  commit_msg = get_session_commit_message(repo_root)
298
+ workspace_rel = f"{DIR_WORKFLOW}/{DIR_WORKSPACE}"
299
+ tasks_rel = f"{DIR_WORKFLOW}/{DIR_TASKS}"
295
300
  subprocess.run(
296
- ["git", "add", "-A", f"{DIR_WORKFLOW}/{DIR_WORKSPACE}", f"{DIR_WORKFLOW}/{DIR_TASKS}"],
301
+ ["git", "add", "-A", workspace_rel, tasks_rel],
297
302
  cwd=repo_root,
298
303
  capture_output=True,
299
304
  )
300
305
  # Check if there are staged changes
301
306
  result = subprocess.run(
302
- ["git", "diff", "--cached", "--quiet", "--", ".cowork-flow/workspace", ".cowork-flow/tasks"],
307
+ ["git", "diff", "--cached", "--quiet", "--", workspace_rel, tasks_rel],
303
308
  cwd=repo_root,
304
309
  )
305
310
  if result.returncode == 0:
@@ -322,7 +327,7 @@ def add_session(
322
327
  commit: str = "-",
323
328
  summary: str = "(add summary)",
324
329
  extra_content: str = "(add details)",
325
- auto_commit: bool = True,
330
+ auto_commit: bool = False,
326
331
  ) -> int:
327
332
  """Add a new session."""
328
333
  repo_root = get_repo_root()
@@ -397,7 +402,7 @@ def add_session(
397
402
  print(f" - {target_file.name if target_file else 'journal'}", file=sys.stderr)
398
403
  print(" - index.md", file=sys.stderr)
399
404
 
400
- # Auto-commit workspace changes
405
+ # Auto-commit workspace changes only when explicitly requested.
401
406
  if auto_commit:
402
407
  print("", file=sys.stderr)
403
408
  _auto_commit_workspace(repo_root)
@@ -418,8 +423,16 @@ def main() -> int:
418
423
  parser.add_argument("--commit", default="-", help="Comma-separated commit hashes")
419
424
  parser.add_argument("--summary", default="(add summary)", help="Brief summary")
420
425
  parser.add_argument("--content-file", help="Path to a file containing detailed content")
421
- parser.add_argument("--no-commit", action="store_true",
422
- help="Skip automatic .cowork-flow metadata commit")
426
+ parser.add_argument(
427
+ "--auto-commit",
428
+ action="store_true",
429
+ help="Auto git commit after recording workspace metadata",
430
+ )
431
+ parser.add_argument(
432
+ "--no-commit",
433
+ action="store_true",
434
+ help="Deprecated no-op; add-session no longer commits by default",
435
+ )
423
436
 
424
437
  args = parser.parse_args()
425
438
 
@@ -433,7 +446,7 @@ def main() -> int:
433
446
 
434
447
  return add_session(
435
448
  args.title, args.commit, args.summary, extra_content,
436
- auto_commit=not args.no_commit,
449
+ auto_commit=args.auto_commit and not args.no_commit,
437
450
  )
438
451
 
439
452