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
|
@@ -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
|
-
|
|
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
|
-
|
|
313
|
-
|
|
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
|
|
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(
|
|
332
|
-
lines.append(f"
|
|
333
|
-
if isinstance(
|
|
334
|
-
lines.append(f"
|
|
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
|
-
"-
|
|
383
|
-
"-
|
|
384
|
-
"-
|
|
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.",
|