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.
- package/README.md +65 -20
- package/package.json +1 -1
- package/src/cli.js +109 -2
- package/src/commands/init.js +61 -4
- package/src/commands/sync.js +10 -1
- package/src/lib/copy-template.js +38 -7
- package/src/lib/platforms.js +73 -0
- package/template/.agent/skills/brainstorming/SKILL.md +13 -9
- package/template/.agent/skills/finish-work/SKILL.md +3 -3
- package/template/.agent/skills/meta/SKILL.md +1 -1
- package/template/.agent/skills/start/SKILL.md +35 -16
- package/template/.agent/skills/writing-plans/SKILL.md +1 -1
- package/template/.claude/agents/cowork-check.md +41 -0
- package/template/.claude/agents/cowork-implement.md +40 -0
- package/template/.claude/agents/cowork-research.md +40 -0
- package/template/.claude/commands/cowork-check.md +27 -0
- package/template/.claude/commands/cowork-implement.md +27 -0
- package/template/.claude/commands/cowork-research.md +27 -0
- package/template/.claude/hooks/inject-workflow-state.py +298 -0
- package/template/.claude/settings.json +24 -0
- package/template/.claude/skills/before-dev/SKILL.md +31 -0
- package/template/.claude/skills/brainstorming/SKILL.md +32 -0
- package/template/.claude/skills/break-loop/SKILL.md +26 -0
- package/template/.claude/skills/check/SKILL.md +29 -0
- package/template/.claude/skills/continue/SKILL.md +29 -0
- package/template/.claude/skills/finish-work/SKILL.md +32 -0
- package/template/.claude/skills/meta/SKILL.md +30 -0
- package/template/.claude/skills/python-design/SKILL.md +27 -0
- package/template/.claude/skills/start/SKILL.md +78 -0
- package/template/.claude/skills/update-spec/SKILL.md +39 -0
- package/template/.claude/skills/writing-plans/SKILL.md +70 -0
- package/template/.codex/agents/cowork-check.toml +17 -22
- package/template/.codex/agents/cowork-implement.toml +18 -23
- package/template/.codex/agents/cowork-research.toml +14 -19
- package/template/.codex/agents/default.toml +27 -0
- package/template/.codex/agents/explorer.toml +28 -0
- package/template/.codex/agents/worker.toml +29 -0
- package/template/.codex/hooks/inject-workflow-state.py +161 -108
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/adapters/claude-code/adapter.yaml +35 -0
- package/template/.cowork-flow/adapters/codex/adapter.yaml +35 -0
- package/template/.cowork-flow/adapters/opencode/adapter.yaml +33 -0
- package/template/.cowork-flow/config.yaml +4 -6
- package/template/.cowork-flow/scripts/add_session.py +20 -7
- package/template/.cowork-flow/scripts/common/active_task.py +212 -1
- package/template/.cowork-flow/scripts/common/config.py +0 -17
- package/template/.cowork-flow/scripts/common/entry_classifier.py +122 -0
- package/template/.cowork-flow/scripts/common/execution_context.py +10 -17
- package/template/.cowork-flow/scripts/common/readiness.py +249 -0
- package/template/.cowork-flow/scripts/doctor.py +418 -40
- package/template/.cowork-flow/scripts/project_context.py +230 -0
- package/template/.cowork-flow/scripts/run.py +3 -0
- package/template/.cowork-flow/scripts/subagent.py +152 -238
- package/template/.cowork-flow/scripts/task.py +330 -17
- package/template/.cowork-flow/spec/adapter.schema.json +132 -0
- package/template/.cowork-flow/spec/capabilities.md +34 -0
- package/template/.cowork-flow/spec/entry-contract.md +45 -0
- package/template/.cowork-flow/spec/registry.json +54 -0
- package/template/.cowork-flow/spec/subagent-dispatch.md +77 -0
- package/template/.cowork-flow/spec/workflow-state-templates.md +66 -0
- package/template/.cowork-flow/workflow.md +158 -203
- package/template/.opencode/agents/cowork-check.md +46 -0
- package/template/.opencode/agents/cowork-implement.md +45 -0
- package/template/.opencode/agents/cowork-research.md +46 -0
- package/template/.opencode/commands/cowork-check.md +26 -0
- package/template/.opencode/commands/cowork-implement.md +26 -0
- package/template/.opencode/commands/cowork-research.md +26 -0
- package/template/.opencode/plugins/cowork-flow.js +317 -0
- package/template/CLAUDE.md +5 -0
- 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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
if
|
|
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 = "
|
|
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
|
-
|
|
291
|
+
extra_lines,
|
|
239
292
|
)
|
|
240
293
|
|
|
241
294
|
print(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.0.
|
|
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
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
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",
|
|
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", "--",
|
|
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 =
|
|
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(
|
|
422
|
-
|
|
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
|
|