cowork-flow 0.0.19 → 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 (69) 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 +38 -14
  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 +172 -90
  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 -1
  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/entry_classifier.py +122 -0
  47. package/template/.cowork-flow/scripts/common/execution_context.py +10 -17
  48. package/template/.cowork-flow/scripts/common/readiness.py +249 -0
  49. package/template/.cowork-flow/scripts/doctor.py +417 -29
  50. package/template/.cowork-flow/scripts/project_context.py +230 -0
  51. package/template/.cowork-flow/scripts/run.py +3 -0
  52. package/template/.cowork-flow/scripts/subagent.py +152 -238
  53. package/template/.cowork-flow/scripts/task.py +330 -17
  54. package/template/.cowork-flow/spec/adapter.schema.json +132 -0
  55. package/template/.cowork-flow/spec/capabilities.md +34 -0
  56. package/template/.cowork-flow/spec/entry-contract.md +45 -0
  57. package/template/.cowork-flow/spec/registry.json +54 -0
  58. package/template/.cowork-flow/spec/subagent-dispatch.md +77 -0
  59. package/template/.cowork-flow/spec/workflow-state-templates.md +66 -0
  60. package/template/.cowork-flow/workflow.md +154 -190
  61. package/template/.opencode/agents/cowork-check.md +46 -0
  62. package/template/.opencode/agents/cowork-implement.md +45 -0
  63. package/template/.opencode/agents/cowork-research.md +46 -0
  64. package/template/.opencode/commands/cowork-check.md +26 -0
  65. package/template/.opencode/commands/cowork-implement.md +26 -0
  66. package/template/.opencode/commands/cowork-research.md +26 -0
  67. package/template/.opencode/plugins/cowork-flow.js +317 -0
  68. package/template/CLAUDE.md +5 -0
  69. 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:
@@ -168,14 +178,55 @@ def _get_active_task(root: Path, hook_input: dict[str, Any]) -> tuple[str | None
168
178
  return active.task_path, status.strip(), active.source
169
179
 
170
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
+
171
218
  def _build_context(
219
+ root: Path,
172
220
  task_path: str | None,
173
221
  status: str,
174
222
  source: str,
175
223
  breadcrumbs: dict[str, str],
176
224
  dispatch_mode: str,
225
+ extra_lines: list[str] | None = None,
177
226
  ) -> str:
178
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])
179
230
  if task_path is None:
180
231
  header = f"Status: {status}\nSource: {source}"
181
232
  else:
@@ -183,7 +234,16 @@ def _build_context(
183
234
 
184
235
  return "\n\n".join(
185
236
  [
186
- f"<codex-mode>{dispatch_mode}</codex-mode>",
237
+ f"<codex-dispatch-mode>{dispatch_mode}</codex-dispatch-mode>",
238
+ (
239
+ "<codex-runtime>\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"
242
+ "</codex-runtime>"
243
+ ),
244
+ (
245
+ _build_contract_digest(root)
246
+ ),
187
247
  f"<workflow-state>\n{header}\n{body}\n</workflow-state>",
188
248
  ]
189
249
  )
@@ -202,12 +262,34 @@ def main() -> int:
202
262
 
203
263
  breadcrumbs = _load_breadcrumbs(root)
204
264
  dispatch_mode = _get_dispatch_mode(root)
205
- task_path, status, source = _get_active_task(root, hook_input)
206
- 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:
207
274
  task_path = None
208
275
  status = "delegated_subtask"
209
- source = "prompt"
210
- additional_context = _build_context(task_path, status, source, breadcrumbs, dispatch_mode)
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)
284
+ additional_context = _build_context(
285
+ root,
286
+ task_path,
287
+ status,
288
+ source,
289
+ breadcrumbs,
290
+ dispatch_mode,
291
+ extra_lines,
292
+ )
211
293
 
212
294
  print(
213
295
  json.dumps(
@@ -1 +1 @@
1
- 0.0.19
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,12 +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
+ # 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.
27
30
 
28
31
  #-------------------------------------------------------------------------------
29
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