cowork-flow 0.0.47 → 0.0.49
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/CHANGELOG.md +41 -0
- package/README.md +102 -23
- package/package.json +8 -3
- package/src/cli.js +6 -0
- package/src/commands/init.js +0 -83
- package/src/commands/source-refresh.js +40 -0
- package/src/commands/sync.js +6 -0
- package/src/commands/update.js +59 -6
- package/src/lib/copy-template.js +200 -50
- package/src/lib/host-assets.js +367 -64
- package/src/lib/plan-applier.js +47 -0
- package/template/.claude/hooks/inject-workflow-state.py +7 -0
- package/template/.codex/hooks/inject-workflow-state.py +7 -0
- package/template/.codex/hooks.json +1 -1
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/config.yaml +0 -11
- package/template/.cowork-flow/scripts/adapters/cli/init_developer.py +0 -1
- package/template/.cowork-flow/scripts/adapters/cli/subagent.py +7 -4
- package/template/.cowork-flow/scripts/adapters/cli/task.py +8 -484
- package/template/.cowork-flow/scripts/adapters/cli/task_archive_commands.py +15 -134
- package/template/.cowork-flow/scripts/adapters/cli/task_context_commands.py +7 -1
- package/template/.cowork-flow/scripts/adapters/cli/task_lifecycle_commands.py +388 -0
- package/template/.cowork-flow/scripts/adapters/cli/task_navigation.py +163 -2
- package/template/.cowork-flow/scripts/adapters/git/git_context.py +16 -193
- package/template/.cowork-flow/scripts/adapters/host/host_manifest.py +170 -84
- package/template/.cowork-flow/scripts/infra/config.py +0 -17
- package/template/.cowork-flow/scripts/infra/developer_profile.py +0 -95
- package/template/.cowork-flow/scripts/infra/paths.py +0 -79
- package/template/.cowork-flow/scripts/infra/skill_manifest.py +23 -3
- package/template/.cowork-flow/scripts/infra/storage/operation_log.py +40 -0
- package/template/.cowork-flow/scripts/infra/storage/state_store.py +356 -1
- package/template/.cowork-flow/scripts/kernel/workflow_route.py +15 -15
- package/template/.cowork-flow/scripts/run.py +0 -5
- package/template/.cowork-flow/scripts/services/context_discovery.py +156 -0
- package/template/.cowork-flow/scripts/services/context_jsonl.py +119 -0
- package/template/.cowork-flow/scripts/services/context_paths.py +186 -0
- package/template/.cowork-flow/scripts/services/lifecycle_checks.py +186 -69
- package/template/.cowork-flow/scripts/services/lifecycle_policy.py +128 -0
- package/template/.cowork-flow/scripts/services/plan_binding.py +146 -0
- package/template/.cowork-flow/scripts/services/readiness.py +3 -170
- package/template/.cowork-flow/scripts/services/task_archive.py +115 -24
- package/template/.cowork-flow/scripts/services/task_context.py +103 -365
- package/template/.cowork-flow/scripts/services/task_creation.py +40 -5
- package/template/.cowork-flow/scripts/services/task_lifecycle.py +155 -18
- package/template/.cowork-flow/scripts/services/task_routing.py +80 -67
- package/template/.cowork-flow/scripts/services/task_utils.py +0 -146
- package/template/.cowork-flow/scripts/services/workflow_runtime.py +1 -1
- package/template/.cowork-flow/spec/contracts/capabilities.md +23 -0
- package/template/.cowork-flow/spec/contracts/decision-anchor.md +2 -2
- package/template/.cowork-flow/spec/contracts/error-output-as-data.md +8 -0
- package/template/.cowork-flow/spec/contracts/index.md +1 -0
- package/template/.cowork-flow/spec/contracts/party-mode-v2-board.md +20 -0
- package/template/.cowork-flow/spec/contracts/plan-binding.md +50 -0
- package/template/.cowork-flow/spec/contracts/skill-owned-actions.md +31 -7
- package/template/.cowork-flow/spec/guides/index.md +3 -1
- package/template/.cowork-flow/spec/index.md +5 -4
- package/template/.cowork-flow/spec/references/definition-of-done.md +2 -3
- package/template/.cowork-flow/spec/references/orchestration-patterns.md +1 -1
- package/template/.cowork-flow/spec/runtime/contract-registry.json +14 -0
- package/template/.cowork-flow/spec/runtime/host-assets.json +73 -3
- package/template/.cowork-flow/spec/runtime/index.md +1 -1
- package/template/.cowork-flow/spec/schemas/host-assets.schema.json +150 -8
- package/template/.zcode/.zcode-plugin/plugin.json +1 -1
- package/template/skills/batch-execution/SKILL.md +37 -3
- package/template/skills/batch-execution/scripts/batch_action.py +4 -0
- package/template/skills/batch-execution/scripts/batch_execution.py +120 -499
- package/template/skills/batch-execution/scripts/batch_recovery.py +94 -0
- package/template/skills/batch-execution/scripts/batch_state_machine.py +201 -0
- package/template/skills/batch-execution/scripts/batch_verification.py +326 -0
- package/template/skills/brainstorming/SKILL.md +8 -7
- package/template/skills/cowork-flow/SKILL.md +3 -0
- package/template/skills/party-mode/scripts/party_action_contract.py +113 -0
- package/template/skills/party-mode/scripts/party_actions.py +95 -0
- package/template/skills/party-mode/scripts/party_board_store.py +148 -0
- package/template/skills/party-mode/scripts/party_mode_v2.py +345 -317
- package/template/skills/party-mode/scripts/party_reports.py +177 -0
- package/template/skills/party-mode/scripts/party_state_machine.py +189 -0
- package/template/skills/runtime-health/SKILL.md +14 -2
- package/template/skills/runtime-health/scripts/doctor.py +278 -41
- package/template/skills/task-planning/SKILL.md +105 -61
- package/template/skills/task-review/SKILL.md +4 -4
- package/template/skills/task-review/scripts/review_check.py +277 -20
- package/template/skills/test-first/SKILL.md +18 -3
- package/template/skills/test-first/manifest.json +8 -4
- package/template/.cowork-flow/changes/archive/.gitkeep +0 -1
- package/template/.cowork-flow/scripts/adapters/cli/add_session.py +0 -465
- package/template/.cowork-flow/scripts/adapters/cli/change.py +0 -412
- package/template/.cowork-flow/workspace/index.md +0 -102
- package/template/.zcode/scaffold/AGENTS.md +0 -145
- package/template/.zcode/scaffold/CLAUDE.md +0 -5
|
@@ -15,7 +15,8 @@ from services.readiness import task_readiness_blockers
|
|
|
15
15
|
from services.task_repository import TaskRepository, TaskRepositoryError
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
from services.task_routing import _default_intent
|
|
18
|
+
from services.task_routing import _default_intent
|
|
19
|
+
from services.task_routing import route_request as _route_request
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
RECOVERABLE_TASK_STATUSES = {
|
|
@@ -25,6 +26,85 @@ RECOVERABLE_TASK_STATUSES = {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
def _default_action_command(
|
|
30
|
+
task_path: str | None,
|
|
31
|
+
*,
|
|
32
|
+
create: bool = False,
|
|
33
|
+
) -> str:
|
|
34
|
+
parts = ["./.cowork-flow/run", "task", "next"]
|
|
35
|
+
if task_path:
|
|
36
|
+
parts.append(task_path)
|
|
37
|
+
parts.append("--run")
|
|
38
|
+
if create:
|
|
39
|
+
parts.extend([
|
|
40
|
+
"--title",
|
|
41
|
+
'"<title>"',
|
|
42
|
+
"--slug",
|
|
43
|
+
"<task-name>",
|
|
44
|
+
"--assignee",
|
|
45
|
+
"<name>",
|
|
46
|
+
])
|
|
47
|
+
return " ".join(parts)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _render_command(action_id: str, task_path: str | None, template: object) -> str | None:
|
|
51
|
+
if isinstance(template, str) and template.strip():
|
|
52
|
+
return template.replace("<task-dir>", task_path or "<task-dir>")
|
|
53
|
+
if action_id in {"start_task", "implement_change"}:
|
|
54
|
+
return _default_action_command(task_path)
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _render_diagnostics(task_path: str | None, template: object) -> str | None:
|
|
59
|
+
if isinstance(template, str) and template.strip():
|
|
60
|
+
return template.replace("<task-dir>", task_path or "<task-dir>")
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _render_adapter_commands(payload: dict[str, object], task_path: str | None) -> dict[str, object]:
|
|
65
|
+
action = payload.get("action")
|
|
66
|
+
if not isinstance(action, dict):
|
|
67
|
+
return payload
|
|
68
|
+
action_id = str(action.get("id"))
|
|
69
|
+
command = _render_command(action_id, task_path, action.get("command"))
|
|
70
|
+
diagnostics = _render_diagnostics(task_path, action.get("diagnosticsCommand"))
|
|
71
|
+
action["command"] = command
|
|
72
|
+
action["diagnosticsCommand"] = diagnostics
|
|
73
|
+
payload["actionCommand"] = command
|
|
74
|
+
payload["diagnosticsCommand"] = diagnostics
|
|
75
|
+
return payload
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _attach_runtime_gate_alias(payload: dict[str, object]) -> dict[str, object]:
|
|
79
|
+
lifecycle_check = payload.get("lifecycleCheck")
|
|
80
|
+
payload["runtimeGate"] = lifecycle_check
|
|
81
|
+
action = payload.get("action")
|
|
82
|
+
if isinstance(action, dict):
|
|
83
|
+
action["runtimeGate"] = action.get("lifecycleCheck")
|
|
84
|
+
return payload
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def route_request(
|
|
88
|
+
status: str,
|
|
89
|
+
intent: str,
|
|
90
|
+
context: str,
|
|
91
|
+
blockers: tuple[str, ...] | list[str],
|
|
92
|
+
active_target: bool,
|
|
93
|
+
task_path: str | None = None,
|
|
94
|
+
repo_root: Path | None = None,
|
|
95
|
+
) -> dict[str, object]:
|
|
96
|
+
payload = _route_request(
|
|
97
|
+
status=status,
|
|
98
|
+
intent=intent,
|
|
99
|
+
context=context,
|
|
100
|
+
blockers=blockers,
|
|
101
|
+
active_target=active_target,
|
|
102
|
+
task_path=task_path,
|
|
103
|
+
repo_root=repo_root,
|
|
104
|
+
)
|
|
105
|
+
return _attach_runtime_gate_alias(_render_adapter_commands(payload, task_path))
|
|
106
|
+
|
|
107
|
+
|
|
28
108
|
|
|
29
109
|
def _display(repo_root: Path, task_dir: Path) -> str:
|
|
30
110
|
try:
|
|
@@ -85,6 +165,76 @@ def _attach_recovery(payload: dict[str, object], repo_root: Path) -> dict[str, o
|
|
|
85
165
|
return payload
|
|
86
166
|
|
|
87
167
|
|
|
168
|
+
def _add_read_first_entry(
|
|
169
|
+
entries: list[dict[str, str]],
|
|
170
|
+
seen: set[str],
|
|
171
|
+
file_path: str,
|
|
172
|
+
reason: str,
|
|
173
|
+
) -> None:
|
|
174
|
+
normalized = file_path.replace("\\", "/").strip()
|
|
175
|
+
if not normalized or normalized in seen:
|
|
176
|
+
return
|
|
177
|
+
seen.add(normalized)
|
|
178
|
+
entry = {"file": normalized}
|
|
179
|
+
if reason.strip():
|
|
180
|
+
entry["reason"] = reason.strip()
|
|
181
|
+
entries.append(entry)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _attach_implement_read_first(
|
|
185
|
+
payload: dict[str, object],
|
|
186
|
+
repo_root: Path,
|
|
187
|
+
task_path: str | None,
|
|
188
|
+
) -> dict[str, object]:
|
|
189
|
+
action = payload.get("action")
|
|
190
|
+
if not isinstance(action, dict) or action.get("id") != "implement_change":
|
|
191
|
+
return payload
|
|
192
|
+
if not task_path:
|
|
193
|
+
return payload
|
|
194
|
+
|
|
195
|
+
task_dir = repo_root / task_path
|
|
196
|
+
if not task_dir.is_dir():
|
|
197
|
+
return payload
|
|
198
|
+
|
|
199
|
+
entries: list[dict[str, str]] = []
|
|
200
|
+
seen: set[str] = set()
|
|
201
|
+
task_prefix = task_path.rstrip("/")
|
|
202
|
+
_add_read_first_entry(
|
|
203
|
+
entries,
|
|
204
|
+
seen,
|
|
205
|
+
f"{task_prefix}/decision-anchor.md",
|
|
206
|
+
"Task decision anchor",
|
|
207
|
+
)
|
|
208
|
+
try:
|
|
209
|
+
task_data = TaskRepository(repo_root).load(task_dir)
|
|
210
|
+
except TaskRepositoryError:
|
|
211
|
+
task_data = {}
|
|
212
|
+
meta = task_data.get("meta") if isinstance(task_data, dict) else None
|
|
213
|
+
plan_file = meta.get("planFile") if isinstance(meta, dict) else None
|
|
214
|
+
if isinstance(plan_file, str):
|
|
215
|
+
_add_read_first_entry(entries, seen, plan_file, "Linked implementation plan")
|
|
216
|
+
_add_read_first_entry(
|
|
217
|
+
entries,
|
|
218
|
+
seen,
|
|
219
|
+
f"{task_prefix}/implement.jsonl",
|
|
220
|
+
"Implementation context index",
|
|
221
|
+
)
|
|
222
|
+
for context_entry in TaskContextService(repo_root).entries(task_dir, "implement"):
|
|
223
|
+
file_value = context_entry.get("file")
|
|
224
|
+
if not isinstance(file_value, str):
|
|
225
|
+
continue
|
|
226
|
+
reason = context_entry.get("reason")
|
|
227
|
+
_add_read_first_entry(
|
|
228
|
+
entries,
|
|
229
|
+
seen,
|
|
230
|
+
file_value,
|
|
231
|
+
reason if isinstance(reason, str) else "Implementation context",
|
|
232
|
+
)
|
|
233
|
+
if entries:
|
|
234
|
+
payload["readFirst"] = entries
|
|
235
|
+
return payload
|
|
236
|
+
|
|
237
|
+
|
|
88
238
|
def _print_recovery(payload: dict[str, object]) -> None:
|
|
89
239
|
recovery = payload.get("recovery")
|
|
90
240
|
if not isinstance(recovery, dict):
|
|
@@ -126,20 +276,24 @@ def build_navigation_payload(
|
|
|
126
276
|
blockers: list[str],
|
|
127
277
|
active_target: bool,
|
|
128
278
|
task_path: str | None = None,
|
|
279
|
+
repo_root: Path | None = None,
|
|
129
280
|
) -> dict[str, object]:
|
|
130
281
|
intent = getattr(args, "intent", None) or _default_intent(
|
|
131
282
|
status,
|
|
132
283
|
blockers,
|
|
133
284
|
active_target,
|
|
134
285
|
)
|
|
135
|
-
|
|
286
|
+
root = repo_root if repo_root is not None else get_repo_root()
|
|
287
|
+
payload = route_request(
|
|
136
288
|
status=status,
|
|
137
289
|
intent=intent,
|
|
138
290
|
context=_routing_context(args),
|
|
139
291
|
blockers=blockers,
|
|
140
292
|
active_target=active_target,
|
|
141
293
|
task_path=task_path,
|
|
294
|
+
repo_root=root,
|
|
142
295
|
)
|
|
296
|
+
return _attach_implement_read_first(payload, root, task_path)
|
|
143
297
|
|
|
144
298
|
|
|
145
299
|
def _print_json_route(
|
|
@@ -149,6 +303,7 @@ def _print_json_route(
|
|
|
149
303
|
blockers: list[str],
|
|
150
304
|
active_target: bool,
|
|
151
305
|
task_path: str | None = None,
|
|
306
|
+
repo_root: Path | None = None,
|
|
152
307
|
) -> None:
|
|
153
308
|
payload = build_navigation_payload(
|
|
154
309
|
args=args,
|
|
@@ -156,6 +311,7 @@ def _print_json_route(
|
|
|
156
311
|
blockers=blockers,
|
|
157
312
|
active_target=active_target,
|
|
158
313
|
task_path=task_path,
|
|
314
|
+
repo_root=repo_root,
|
|
159
315
|
)
|
|
160
316
|
print(json.dumps(payload, ensure_ascii=False, sort_keys=False))
|
|
161
317
|
|
|
@@ -194,6 +350,7 @@ def _navigation_target(args, repo_root: Path, structured: bool):
|
|
|
194
350
|
blockers=[],
|
|
195
351
|
active_target=False,
|
|
196
352
|
task_path=None,
|
|
353
|
+
repo_root=repo_root,
|
|
197
354
|
)
|
|
198
355
|
_attach_recovery(payload, repo_root)
|
|
199
356
|
print(json.dumps(payload, ensure_ascii=False, sort_keys=False))
|
|
@@ -206,6 +363,7 @@ def _navigation_target(args, repo_root: Path, structured: bool):
|
|
|
206
363
|
task_path=None,
|
|
207
364
|
blockers=[],
|
|
208
365
|
active_target=False,
|
|
366
|
+
repo_root=repo_root,
|
|
209
367
|
)
|
|
210
368
|
_attach_recovery(payload, repo_root)
|
|
211
369
|
_print_text_payload(payload)
|
|
@@ -222,6 +380,7 @@ def _print_stale_route(args, task_path: str, source: str, active_target: bool) -
|
|
|
222
380
|
blockers=blockers,
|
|
223
381
|
active_target=active_target,
|
|
224
382
|
task_path=task_path,
|
|
383
|
+
repo_root=get_repo_root(),
|
|
225
384
|
)
|
|
226
385
|
return
|
|
227
386
|
print("Status: stale")
|
|
@@ -252,6 +411,7 @@ def _print_text_route(
|
|
|
252
411
|
task_path=task_path,
|
|
253
412
|
blockers=blockers,
|
|
254
413
|
active_target=active_target,
|
|
414
|
+
repo_root=get_repo_root(),
|
|
255
415
|
)
|
|
256
416
|
_print_text_payload(payload)
|
|
257
417
|
|
|
@@ -281,6 +441,7 @@ def cmd_next(args) -> int:
|
|
|
281
441
|
blockers=blockers,
|
|
282
442
|
active_target=active_target,
|
|
283
443
|
task_path=task_path,
|
|
444
|
+
repo_root=repo_root,
|
|
284
445
|
)
|
|
285
446
|
return 0
|
|
286
447
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
"""
|
|
4
|
-
Git and
|
|
4
|
+
Git and task context utilities.
|
|
5
5
|
|
|
6
6
|
Provides:
|
|
7
7
|
output_json - Output context in JSON format
|
|
@@ -22,10 +22,7 @@ from infra.paths import (
|
|
|
22
22
|
DIR_SPEC,
|
|
23
23
|
DIR_TASKS,
|
|
24
24
|
DIR_WORKFLOW,
|
|
25
|
-
DIR_WORKSPACE,
|
|
26
25
|
FILE_TASK_JSON,
|
|
27
|
-
count_lines,
|
|
28
|
-
get_active_journal_file,
|
|
29
26
|
get_developer,
|
|
30
27
|
get_repo_root,
|
|
31
28
|
get_tasks_dir,
|
|
@@ -263,8 +260,8 @@ def _build_resume_checklist(
|
|
|
263
260
|
notes: list[str] = []
|
|
264
261
|
|
|
265
262
|
if not snapshot.path:
|
|
266
|
-
notes.append("No active task
|
|
267
|
-
notes.append("Do not bulk-read `.cowork-flow/spec
|
|
263
|
+
notes.append("No active task in the current context. Use `task next --run --title <title>` or `task next <task-dir> --run` with COWORK_FLOW_CONTEXT_ID.")
|
|
264
|
+
notes.append("Do not bulk-read `.cowork-flow/spec/`; read details only after a task is selected.")
|
|
268
265
|
return {"commands": commands, "readFiles": read_files, "notes": notes}
|
|
269
266
|
|
|
270
267
|
active_task = snapshot.path
|
|
@@ -275,31 +272,18 @@ def _build_resume_checklist(
|
|
|
275
272
|
|
|
276
273
|
read_files.extend(_task_plan_references(repo_root, snapshot))
|
|
277
274
|
notes.append("Read current plan status only when continuing implementation.")
|
|
278
|
-
notes.append("Do not bulk-read `.cowork-flow/spec
|
|
275
|
+
notes.append("Do not bulk-read `.cowork-flow/spec/`; follow JSONL references on demand.")
|
|
279
276
|
|
|
280
277
|
return {"commands": commands, "readFiles": read_files, "notes": notes}
|
|
281
278
|
|
|
282
279
|
|
|
283
|
-
def _active_task_json(snapshot: ActiveTaskSnapshot) -> dict | None:
|
|
284
|
-
"""Build the record-mode activeTask JSON object."""
|
|
285
|
-
active_task = snapshot.path
|
|
286
|
-
data = snapshot.data
|
|
287
|
-
if not active_task or not data:
|
|
288
|
-
return None
|
|
289
|
-
|
|
290
|
-
return {
|
|
291
|
-
"path": active_task,
|
|
292
|
-
"name": data.get("name") or data.get("id") or "unknown",
|
|
293
|
-
"status": data.get("status", "unknown"),
|
|
294
|
-
}
|
|
295
|
-
|
|
296
280
|
|
|
297
281
|
def _append_resume_checklist(
|
|
298
282
|
lines: list[str],
|
|
299
283
|
repo_root: Path,
|
|
300
284
|
snapshot: ActiveTaskSnapshot,
|
|
301
285
|
) -> None:
|
|
302
|
-
"""
|
|
286
|
+
"""Append the minimal resume checklist to text output."""
|
|
303
287
|
lines.append("## RESUME CHECKLIST")
|
|
304
288
|
checklist = _build_resume_checklist(repo_root, snapshot)
|
|
305
289
|
commands = checklist["commands"]
|
|
@@ -308,8 +292,8 @@ def _append_resume_checklist(
|
|
|
308
292
|
lines.append(f"- Recovery entrypoint (rerun only if context is stale): {commands[0]}")
|
|
309
293
|
|
|
310
294
|
if not snapshot.path:
|
|
311
|
-
lines.append("- No active task
|
|
312
|
-
lines.append("- Do not bulk-read .cowork-flow/spec
|
|
295
|
+
lines.append("- No active task in the current context. Use `task next --run --title <title>` or `task next <task-dir> --run` with COWORK_FLOW_CONTEXT_ID.")
|
|
296
|
+
lines.append("- Do not bulk-read .cowork-flow/spec/; choose a task first.")
|
|
313
297
|
lines.append("")
|
|
314
298
|
return
|
|
315
299
|
|
|
@@ -329,7 +313,7 @@ def _append_resume_checklist(
|
|
|
329
313
|
else:
|
|
330
314
|
lines.append("- No plan reference found in task context; do not search all plans unless needed.")
|
|
331
315
|
|
|
332
|
-
lines.append("- Do not bulk-read .cowork-flow/spec
|
|
316
|
+
lines.append("- Do not bulk-read .cowork-flow/spec/; follow JSONL references on demand.")
|
|
333
317
|
lines.append("")
|
|
334
318
|
|
|
335
319
|
|
|
@@ -458,47 +442,15 @@ def _append_my_tasks(lines: list[str], developer: str, tasks_dir: Path) -> None:
|
|
|
458
442
|
lines.append("")
|
|
459
443
|
|
|
460
444
|
|
|
461
|
-
def _append_journal_file(lines: list[str], repo_root: Path, developer: str) -> None:
|
|
462
|
-
"""Append journal file information to text output."""
|
|
463
|
-
lines.append("## JOURNAL FILE")
|
|
464
|
-
journal_file = get_active_journal_file(repo_root)
|
|
465
|
-
if journal_file:
|
|
466
|
-
journal_lines = count_lines(journal_file)
|
|
467
|
-
relative = f"{DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/{journal_file.name}"
|
|
468
|
-
lines.append(f"Active file: {relative}")
|
|
469
|
-
lines.append(f"Line count: {journal_lines} / 2000")
|
|
470
|
-
if journal_lines > 1800:
|
|
471
|
-
lines.append("[!] WARNING: Approaching 2000 line limit!")
|
|
472
|
-
else:
|
|
473
|
-
lines.append("No journal file found")
|
|
474
|
-
lines.append("")
|
|
475
445
|
|
|
476
|
-
|
|
477
|
-
def _append_paths(lines: list[str], developer: str) -> None:
|
|
446
|
+
def _append_paths(lines: list[str]) -> None:
|
|
478
447
|
"""Append standard cowork-flow paths."""
|
|
479
448
|
lines.append("## PATHS")
|
|
480
|
-
lines.append(f"Workspace: {DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/")
|
|
481
449
|
lines.append(f"Tasks: {DIR_WORKFLOW}/{DIR_TASKS}/")
|
|
482
450
|
lines.append(f"Spec: {DIR_WORKFLOW}/{DIR_SPEC}/")
|
|
483
451
|
lines.append("")
|
|
484
452
|
|
|
485
453
|
|
|
486
|
-
def _context_journal_json(repo_root: Path, developer: str | None) -> dict:
|
|
487
|
-
"""Build journal context for JSON output."""
|
|
488
|
-
journal_file = get_active_journal_file(repo_root)
|
|
489
|
-
journal_lines = 0
|
|
490
|
-
journal_relative = ""
|
|
491
|
-
if journal_file and developer:
|
|
492
|
-
journal_lines = count_lines(journal_file)
|
|
493
|
-
journal_relative = (
|
|
494
|
-
f"{DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/{journal_file.name}"
|
|
495
|
-
)
|
|
496
|
-
return {
|
|
497
|
-
"file": journal_relative,
|
|
498
|
-
"lines": journal_lines,
|
|
499
|
-
"nearLimit": journal_lines > 1800,
|
|
500
|
-
}
|
|
501
|
-
|
|
502
454
|
|
|
503
455
|
def _context_tasks_json(tasks_dir: Path) -> list[dict]:
|
|
504
456
|
"""Build active task list for JSON output."""
|
|
@@ -545,7 +497,6 @@ def get_context_json(repo_root: Path | None = None) -> dict:
|
|
|
545
497
|
"active": _context_tasks_json(tasks_dir),
|
|
546
498
|
"directory": f"{DIR_WORKFLOW}/{DIR_TASKS}",
|
|
547
499
|
},
|
|
548
|
-
"journal": _context_journal_json(repo_root, developer),
|
|
549
500
|
"resumeChecklist": _build_resume_checklist(repo_root, active_task_snapshot),
|
|
550
501
|
}
|
|
551
502
|
|
|
@@ -607,8 +558,7 @@ def _append_default_context_sections(
|
|
|
607
558
|
tasks_dir = get_tasks_dir(repo_root)
|
|
608
559
|
_append_active_tasks(lines, tasks_dir)
|
|
609
560
|
_append_my_tasks(lines, developer, tasks_dir)
|
|
610
|
-
|
|
611
|
-
_append_paths(lines, developer)
|
|
561
|
+
_append_paths(lines)
|
|
612
562
|
|
|
613
563
|
|
|
614
564
|
def get_context_text(repo_root: Path | None = None) -> str:
|
|
@@ -623,7 +573,7 @@ def get_context_text(repo_root: Path | None = None) -> str:
|
|
|
623
573
|
if repo_root is None:
|
|
624
574
|
repo_root = get_repo_root()
|
|
625
575
|
|
|
626
|
-
lines = _new_context_lines("
|
|
576
|
+
lines = _new_context_lines("COWORK-FLOW CONTEXT")
|
|
627
577
|
developer = get_developer(repo_root)
|
|
628
578
|
if not _append_developer(lines, developer):
|
|
629
579
|
return "\n".join(lines)
|
|
@@ -634,119 +584,6 @@ def get_context_text(repo_root: Path | None = None) -> str:
|
|
|
634
584
|
return "\n".join(lines)
|
|
635
585
|
|
|
636
586
|
|
|
637
|
-
def get_context_record_json(repo_root: Path | None = None) -> dict:
|
|
638
|
-
"""Get record-mode context as a dictionary.
|
|
639
|
-
|
|
640
|
-
Focused on: my active tasks, git status, active task.
|
|
641
|
-
"""
|
|
642
|
-
if repo_root is None:
|
|
643
|
-
repo_root = get_repo_root()
|
|
644
|
-
|
|
645
|
-
developer = get_developer(repo_root)
|
|
646
|
-
git_snapshot = _get_git_snapshot(
|
|
647
|
-
repo_root,
|
|
648
|
-
include_empty_commit_message=False,
|
|
649
|
-
)
|
|
650
|
-
|
|
651
|
-
active_task_snapshot = _get_active_task_snapshot(repo_root)
|
|
652
|
-
|
|
653
|
-
return {
|
|
654
|
-
"developer": developer or "",
|
|
655
|
-
"git": _git_json(git_snapshot),
|
|
656
|
-
"myTasks": _record_my_tasks_json(get_tasks_dir(repo_root), developer),
|
|
657
|
-
"activeTask": _active_task_json(active_task_snapshot),
|
|
658
|
-
"resumeChecklist": _build_resume_checklist(repo_root, active_task_snapshot),
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
def _record_my_tasks_json(tasks_dir: Path, developer: str | None) -> list[dict]:
|
|
663
|
-
"""Build record-mode task list for JSON output."""
|
|
664
|
-
my_tasks = []
|
|
665
|
-
task_data_by_dir = _load_task_json_by_dir(tasks_dir)
|
|
666
|
-
all_task_statuses = _task_statuses(task_data_by_dir)
|
|
667
|
-
for dir_name, data in task_data_by_dir.items():
|
|
668
|
-
if data.get("assignee") != developer:
|
|
669
|
-
continue
|
|
670
|
-
children_list = data.get("children", [])
|
|
671
|
-
my_tasks.append({
|
|
672
|
-
"dir": dir_name,
|
|
673
|
-
"title": data.get("title") or data.get("name") or "unknown",
|
|
674
|
-
"status": data.get("status", "unknown"),
|
|
675
|
-
"priority": data.get("priority", "P2"),
|
|
676
|
-
"children": children_list,
|
|
677
|
-
"childrenDone": _children_done_count(children_list, all_task_statuses),
|
|
678
|
-
"parent": data.get("parent"),
|
|
679
|
-
"meta": data.get("meta", {}),
|
|
680
|
-
})
|
|
681
|
-
return my_tasks
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
def _append_record_my_tasks(
|
|
685
|
-
lines: list[str],
|
|
686
|
-
developer: str,
|
|
687
|
-
tasks_dir: Path,
|
|
688
|
-
) -> None:
|
|
689
|
-
"""Append prominent record-mode tasks assigned to the developer."""
|
|
690
|
-
lines.append(f"## [!!!] MY ACTIVE TASKS (Assigned to {developer})")
|
|
691
|
-
lines.append("[!] Review whether any should be archived before recording this session.")
|
|
692
|
-
lines.append("")
|
|
693
|
-
|
|
694
|
-
my_task_count = 0
|
|
695
|
-
task_data_by_dir = _load_task_json_by_dir(tasks_dir)
|
|
696
|
-
all_task_statuses = _task_statuses(task_data_by_dir)
|
|
697
|
-
for dir_name, data in task_data_by_dir.items():
|
|
698
|
-
if data.get("assignee", "") != developer:
|
|
699
|
-
continue
|
|
700
|
-
title = data.get("title") or data.get("name") or "unknown"
|
|
701
|
-
priority = data.get("priority", "P2")
|
|
702
|
-
status = data.get("status", "planning")
|
|
703
|
-
progress = _children_progress(data.get("children", []), all_task_statuses)
|
|
704
|
-
lines.append(f"- [{priority}] {title} ({status}){progress} - {dir_name}")
|
|
705
|
-
my_task_count += 1
|
|
706
|
-
|
|
707
|
-
if my_task_count == 0:
|
|
708
|
-
lines.append("(no active tasks assigned to you)")
|
|
709
|
-
lines.append("")
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
def _append_record_context_sections(lines: list[str], repo_root: Path) -> None:
|
|
713
|
-
git_snapshot = _get_git_snapshot(repo_root)
|
|
714
|
-
_append_git_status(lines, git_snapshot, repo_root)
|
|
715
|
-
_append_recent_commits(lines, git_snapshot)
|
|
716
|
-
active_task_snapshot = _get_active_task_snapshot(repo_root)
|
|
717
|
-
_append_active_task(lines, active_task_snapshot)
|
|
718
|
-
_append_resume_checklist(lines, repo_root, active_task_snapshot)
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
def get_context_text_record(repo_root: Path | None = None) -> str:
|
|
722
|
-
"""Get context as formatted text for session recording mode.
|
|
723
|
-
|
|
724
|
-
Focused output: MY ACTIVE TASKS first (with [!!!] emphasis),
|
|
725
|
-
then GIT STATUS, RECENT COMMITS, ACTIVE TASK.
|
|
726
|
-
|
|
727
|
-
Args:
|
|
728
|
-
repo_root: Repository root path. Defaults to auto-detected.
|
|
729
|
-
|
|
730
|
-
Returns:
|
|
731
|
-
Formatted text output for session recording.
|
|
732
|
-
"""
|
|
733
|
-
if repo_root is None:
|
|
734
|
-
repo_root = get_repo_root()
|
|
735
|
-
|
|
736
|
-
lines = _new_context_lines("SESSION CONTEXT (RECORD MODE)")
|
|
737
|
-
developer = get_developer(repo_root)
|
|
738
|
-
if not developer:
|
|
739
|
-
lines.append(
|
|
740
|
-
f"ERROR: Not initialized. Run: ./{DIR_WORKFLOW}/run init-developer <name>"
|
|
741
|
-
)
|
|
742
|
-
return "\n".join(lines)
|
|
743
|
-
|
|
744
|
-
_append_record_my_tasks(lines, developer, get_tasks_dir(repo_root))
|
|
745
|
-
_append_record_context_sections(lines, repo_root)
|
|
746
|
-
lines.append("========================================")
|
|
747
|
-
|
|
748
|
-
return "\n".join(lines)
|
|
749
|
-
|
|
750
587
|
|
|
751
588
|
def output_text(repo_root: Path | None = None) -> None:
|
|
752
589
|
"""Output context in text format.
|
|
@@ -766,33 +603,19 @@ def main() -> None:
|
|
|
766
603
|
"""CLI entry point."""
|
|
767
604
|
import argparse
|
|
768
605
|
|
|
769
|
-
parser = argparse.ArgumentParser(description="Get
|
|
606
|
+
parser = argparse.ArgumentParser(description="Get cowork-flow context for AI Agent")
|
|
770
607
|
parser.add_argument(
|
|
771
608
|
"--json",
|
|
772
609
|
"-j",
|
|
773
610
|
action="store_true",
|
|
774
|
-
help="Output in JSON format
|
|
611
|
+
help="Output in JSON format",
|
|
775
612
|
)
|
|
776
|
-
parser.add_argument(
|
|
777
|
-
"--mode",
|
|
778
|
-
"-m",
|
|
779
|
-
choices=["default", "record"],
|
|
780
|
-
default="default",
|
|
781
|
-
help="Output mode: default (full context) or record (for session recording)",
|
|
782
|
-
)
|
|
783
|
-
|
|
784
613
|
args = parser.parse_args()
|
|
785
614
|
|
|
786
|
-
if args.
|
|
787
|
-
|
|
788
|
-
print(json.dumps(get_context_record_json(), indent=2, ensure_ascii=False))
|
|
789
|
-
else:
|
|
790
|
-
print(get_context_text_record())
|
|
615
|
+
if args.json:
|
|
616
|
+
output_json()
|
|
791
617
|
else:
|
|
792
|
-
|
|
793
|
-
output_json()
|
|
794
|
-
else:
|
|
795
|
-
output_text()
|
|
618
|
+
output_text()
|
|
796
619
|
|
|
797
620
|
|
|
798
621
|
if __name__ == "__main__":
|