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.
Files changed (90) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +102 -23
  3. package/package.json +8 -3
  4. package/src/cli.js +6 -0
  5. package/src/commands/init.js +0 -83
  6. package/src/commands/source-refresh.js +40 -0
  7. package/src/commands/sync.js +6 -0
  8. package/src/commands/update.js +59 -6
  9. package/src/lib/copy-template.js +200 -50
  10. package/src/lib/host-assets.js +367 -64
  11. package/src/lib/plan-applier.js +47 -0
  12. package/template/.claude/hooks/inject-workflow-state.py +7 -0
  13. package/template/.codex/hooks/inject-workflow-state.py +7 -0
  14. package/template/.codex/hooks.json +1 -1
  15. package/template/.cowork-flow/.version +1 -1
  16. package/template/.cowork-flow/config.yaml +0 -11
  17. package/template/.cowork-flow/scripts/adapters/cli/init_developer.py +0 -1
  18. package/template/.cowork-flow/scripts/adapters/cli/subagent.py +7 -4
  19. package/template/.cowork-flow/scripts/adapters/cli/task.py +8 -484
  20. package/template/.cowork-flow/scripts/adapters/cli/task_archive_commands.py +15 -134
  21. package/template/.cowork-flow/scripts/adapters/cli/task_context_commands.py +7 -1
  22. package/template/.cowork-flow/scripts/adapters/cli/task_lifecycle_commands.py +388 -0
  23. package/template/.cowork-flow/scripts/adapters/cli/task_navigation.py +163 -2
  24. package/template/.cowork-flow/scripts/adapters/git/git_context.py +16 -193
  25. package/template/.cowork-flow/scripts/adapters/host/host_manifest.py +170 -84
  26. package/template/.cowork-flow/scripts/infra/config.py +0 -17
  27. package/template/.cowork-flow/scripts/infra/developer_profile.py +0 -95
  28. package/template/.cowork-flow/scripts/infra/paths.py +0 -79
  29. package/template/.cowork-flow/scripts/infra/skill_manifest.py +23 -3
  30. package/template/.cowork-flow/scripts/infra/storage/operation_log.py +40 -0
  31. package/template/.cowork-flow/scripts/infra/storage/state_store.py +356 -1
  32. package/template/.cowork-flow/scripts/kernel/workflow_route.py +15 -15
  33. package/template/.cowork-flow/scripts/run.py +0 -5
  34. package/template/.cowork-flow/scripts/services/context_discovery.py +156 -0
  35. package/template/.cowork-flow/scripts/services/context_jsonl.py +119 -0
  36. package/template/.cowork-flow/scripts/services/context_paths.py +186 -0
  37. package/template/.cowork-flow/scripts/services/lifecycle_checks.py +186 -69
  38. package/template/.cowork-flow/scripts/services/lifecycle_policy.py +128 -0
  39. package/template/.cowork-flow/scripts/services/plan_binding.py +146 -0
  40. package/template/.cowork-flow/scripts/services/readiness.py +3 -170
  41. package/template/.cowork-flow/scripts/services/task_archive.py +115 -24
  42. package/template/.cowork-flow/scripts/services/task_context.py +103 -365
  43. package/template/.cowork-flow/scripts/services/task_creation.py +40 -5
  44. package/template/.cowork-flow/scripts/services/task_lifecycle.py +155 -18
  45. package/template/.cowork-flow/scripts/services/task_routing.py +80 -67
  46. package/template/.cowork-flow/scripts/services/task_utils.py +0 -146
  47. package/template/.cowork-flow/scripts/services/workflow_runtime.py +1 -1
  48. package/template/.cowork-flow/spec/contracts/capabilities.md +23 -0
  49. package/template/.cowork-flow/spec/contracts/decision-anchor.md +2 -2
  50. package/template/.cowork-flow/spec/contracts/error-output-as-data.md +8 -0
  51. package/template/.cowork-flow/spec/contracts/index.md +1 -0
  52. package/template/.cowork-flow/spec/contracts/party-mode-v2-board.md +20 -0
  53. package/template/.cowork-flow/spec/contracts/plan-binding.md +50 -0
  54. package/template/.cowork-flow/spec/contracts/skill-owned-actions.md +31 -7
  55. package/template/.cowork-flow/spec/guides/index.md +3 -1
  56. package/template/.cowork-flow/spec/index.md +5 -4
  57. package/template/.cowork-flow/spec/references/definition-of-done.md +2 -3
  58. package/template/.cowork-flow/spec/references/orchestration-patterns.md +1 -1
  59. package/template/.cowork-flow/spec/runtime/contract-registry.json +14 -0
  60. package/template/.cowork-flow/spec/runtime/host-assets.json +73 -3
  61. package/template/.cowork-flow/spec/runtime/index.md +1 -1
  62. package/template/.cowork-flow/spec/schemas/host-assets.schema.json +150 -8
  63. package/template/.zcode/.zcode-plugin/plugin.json +1 -1
  64. package/template/skills/batch-execution/SKILL.md +37 -3
  65. package/template/skills/batch-execution/scripts/batch_action.py +4 -0
  66. package/template/skills/batch-execution/scripts/batch_execution.py +120 -499
  67. package/template/skills/batch-execution/scripts/batch_recovery.py +94 -0
  68. package/template/skills/batch-execution/scripts/batch_state_machine.py +201 -0
  69. package/template/skills/batch-execution/scripts/batch_verification.py +326 -0
  70. package/template/skills/brainstorming/SKILL.md +8 -7
  71. package/template/skills/cowork-flow/SKILL.md +3 -0
  72. package/template/skills/party-mode/scripts/party_action_contract.py +113 -0
  73. package/template/skills/party-mode/scripts/party_actions.py +95 -0
  74. package/template/skills/party-mode/scripts/party_board_store.py +148 -0
  75. package/template/skills/party-mode/scripts/party_mode_v2.py +345 -317
  76. package/template/skills/party-mode/scripts/party_reports.py +177 -0
  77. package/template/skills/party-mode/scripts/party_state_machine.py +189 -0
  78. package/template/skills/runtime-health/SKILL.md +14 -2
  79. package/template/skills/runtime-health/scripts/doctor.py +278 -41
  80. package/template/skills/task-planning/SKILL.md +105 -61
  81. package/template/skills/task-review/SKILL.md +4 -4
  82. package/template/skills/task-review/scripts/review_check.py +277 -20
  83. package/template/skills/test-first/SKILL.md +18 -3
  84. package/template/skills/test-first/manifest.json +8 -4
  85. package/template/.cowork-flow/changes/archive/.gitkeep +0 -1
  86. package/template/.cowork-flow/scripts/adapters/cli/add_session.py +0 -465
  87. package/template/.cowork-flow/scripts/adapters/cli/change.py +0 -412
  88. package/template/.cowork-flow/workspace/index.md +0 -102
  89. package/template/.zcode/scaffold/AGENTS.md +0 -145
  90. 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, route_request
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
- return route_request(
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 Session Context utilities.
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 for this session. Use `task next --run --title <title>` or `task next <task-dir> --run` with COWORK_FLOW_CONTEXT_ID.")
267
- notes.append("Do not bulk-read `.cowork-flow/spec/` or workspace journals; read details only after a task is selected.")
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/` or workspace journals; follow JSONL references on demand.")
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 for this session. Use `task next --run --title <title>` or `task next <task-dir> --run` with COWORK_FLOW_CONTEXT_ID.")
312
- lines.append("- Do not bulk-read .cowork-flow/spec/ or workspace journals; choose a task first.")
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/ or workspace journals; follow JSONL references on demand.")
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
- _append_journal_file(lines, repo_root, developer)
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("SESSION CONTEXT")
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 Session Context for AI Agent")
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 (works with any --mode)",
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.mode == "record":
787
- if args.json:
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
- if args.json:
793
- output_json()
794
- else:
795
- output_text()
618
+ output_text()
796
619
 
797
620
 
798
621
  if __name__ == "__main__":