cowork-flow 0.0.1

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 (101) hide show
  1. package/README.md +193 -0
  2. package/bin/cowork-flow.js +9 -0
  3. package/package.json +28 -0
  4. package/src/cli.js +61 -0
  5. package/src/commands/init.js +88 -0
  6. package/src/commands/sync.js +33 -0
  7. package/src/commands/update.js +60 -0
  8. package/src/lib/copy-template.js +189 -0
  9. package/src/lib/package-info.js +49 -0
  10. package/src/lib/paths.js +6 -0
  11. package/template/.agent/skills/break-loop/SKILL.md +131 -0
  12. package/template/.agent/skills/check-cross-layer/SKILL.md +158 -0
  13. package/template/.agent/skills/finish-work/SKILL.md +163 -0
  14. package/template/.agent/skills/record-session/SKILL.md +78 -0
  15. package/template/.agent/skills/start/SKILL.md +390 -0
  16. package/template/.agent/skills/update-spec/SKILL.md +335 -0
  17. package/template/.cowork-flow/.version +1 -0
  18. package/template/.cowork-flow/changes/archive/.gitkeep +1 -0
  19. package/template/.cowork-flow/config.yaml +49 -0
  20. package/template/.cowork-flow/plans/.gitkeep +1 -0
  21. package/template/.cowork-flow/scripts/add_session.py +441 -0
  22. package/template/.cowork-flow/scripts/change.py +314 -0
  23. package/template/.cowork-flow/scripts/common/__init__.py +84 -0
  24. package/template/.cowork-flow/scripts/common/config.py +128 -0
  25. package/template/.cowork-flow/scripts/common/developer.py +205 -0
  26. package/template/.cowork-flow/scripts/common/files.py +24 -0
  27. package/template/.cowork-flow/scripts/common/git_context.py +628 -0
  28. package/template/.cowork-flow/scripts/common/paths.py +350 -0
  29. package/template/.cowork-flow/scripts/common/task_utils.py +200 -0
  30. package/template/.cowork-flow/scripts/get_context.py +16 -0
  31. package/template/.cowork-flow/scripts/get_developer.py +26 -0
  32. package/template/.cowork-flow/scripts/init_developer.py +51 -0
  33. package/template/.cowork-flow/scripts/task.py +1378 -0
  34. package/template/.cowork-flow/spec/backend/database-guidelines.md +26 -0
  35. package/template/.cowork-flow/spec/backend/directory-structure.md +27 -0
  36. package/template/.cowork-flow/spec/backend/error-handling.md +20 -0
  37. package/template/.cowork-flow/spec/backend/index.md +36 -0
  38. package/template/.cowork-flow/spec/backend/logging-guidelines.md +20 -0
  39. package/template/.cowork-flow/spec/backend/quality-guidelines.md +24 -0
  40. package/template/.cowork-flow/spec/frontend/component-guidelines.md +19 -0
  41. package/template/.cowork-flow/spec/frontend/directory-structure.md +18 -0
  42. package/template/.cowork-flow/spec/frontend/hook-guidelines.md +17 -0
  43. package/template/.cowork-flow/spec/frontend/index.md +28 -0
  44. package/template/.cowork-flow/spec/frontend/quality-guidelines.md +23 -0
  45. package/template/.cowork-flow/spec/frontend/state-management.md +18 -0
  46. package/template/.cowork-flow/spec/frontend/type-safety.md +18 -0
  47. package/template/.cowork-flow/spec/guides/code-reuse-thinking-guide.md +26 -0
  48. package/template/.cowork-flow/spec/guides/cross-layer-thinking-guide.md +21 -0
  49. package/template/.cowork-flow/spec/guides/index.md +31 -0
  50. package/template/.cowork-flow/spec/guides/pre-implementation-checklist.md +15 -0
  51. package/template/.cowork-flow/tasks/.gitkeep +1 -0
  52. package/template/.cowork-flow/tasks/archive/.gitkeep +1 -0
  53. package/template/.cowork-flow/workflow.md +411 -0
  54. package/template/.cowork-flow/workspace/index.md +95 -0
  55. package/template/.superpowers/brainstorming/SKILL.md +164 -0
  56. package/template/.superpowers/brainstorming/scripts/frame-template.html +214 -0
  57. package/template/.superpowers/brainstorming/scripts/helper.js +88 -0
  58. package/template/.superpowers/brainstorming/scripts/server.cjs +354 -0
  59. package/template/.superpowers/brainstorming/scripts/start-server.sh +148 -0
  60. package/template/.superpowers/brainstorming/scripts/stop-server.sh +56 -0
  61. package/template/.superpowers/brainstorming/spec-document-reviewer-prompt.md +49 -0
  62. package/template/.superpowers/brainstorming/visual-companion.md +287 -0
  63. package/template/.superpowers/dispatching-parallel-agents/SKILL.md +182 -0
  64. package/template/.superpowers/executing-plans/SKILL.md +70 -0
  65. package/template/.superpowers/finishing-a-development-branch/SKILL.md +251 -0
  66. package/template/.superpowers/receiving-code-review/SKILL.md +213 -0
  67. package/template/.superpowers/requesting-code-review/SKILL.md +103 -0
  68. package/template/.superpowers/requesting-code-review/code-reviewer.md +168 -0
  69. package/template/.superpowers/subagent-driven-development/SKILL.md +279 -0
  70. package/template/.superpowers/subagent-driven-development/code-quality-reviewer-prompt.md +25 -0
  71. package/template/.superpowers/subagent-driven-development/implementer-prompt.md +113 -0
  72. package/template/.superpowers/subagent-driven-development/spec-reviewer-prompt.md +61 -0
  73. package/template/.superpowers/systematic-debugging/CREATION-LOG.md +119 -0
  74. package/template/.superpowers/systematic-debugging/SKILL.md +296 -0
  75. package/template/.superpowers/systematic-debugging/condition-based-waiting-example.ts +158 -0
  76. package/template/.superpowers/systematic-debugging/condition-based-waiting.md +115 -0
  77. package/template/.superpowers/systematic-debugging/defense-in-depth.md +122 -0
  78. package/template/.superpowers/systematic-debugging/find-polluter.sh +63 -0
  79. package/template/.superpowers/systematic-debugging/root-cause-tracing.md +169 -0
  80. package/template/.superpowers/systematic-debugging/test-academic.md +14 -0
  81. package/template/.superpowers/systematic-debugging/test-pressure-1.md +58 -0
  82. package/template/.superpowers/systematic-debugging/test-pressure-2.md +68 -0
  83. package/template/.superpowers/systematic-debugging/test-pressure-3.md +69 -0
  84. package/template/.superpowers/test-driven-development/SKILL.md +371 -0
  85. package/template/.superpowers/test-driven-development/testing-anti-patterns.md +299 -0
  86. package/template/.superpowers/using-git-worktrees/SKILL.md +215 -0
  87. package/template/.superpowers/using-superpowers/SKILL.md +117 -0
  88. package/template/.superpowers/using-superpowers/references/codex-tools.md +59 -0
  89. package/template/.superpowers/using-superpowers/references/copilot-tools.md +42 -0
  90. package/template/.superpowers/using-superpowers/references/gemini-tools.md +51 -0
  91. package/template/.superpowers/verification-before-completion/SKILL.md +139 -0
  92. package/template/.superpowers/writing-plans/SKILL.md +152 -0
  93. package/template/.superpowers/writing-plans/plan-document-reviewer-prompt.md +49 -0
  94. package/template/.superpowers/writing-skills/SKILL.md +655 -0
  95. package/template/.superpowers/writing-skills/anthropic-best-practices.md +1150 -0
  96. package/template/.superpowers/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
  97. package/template/.superpowers/writing-skills/graphviz-conventions.dot +172 -0
  98. package/template/.superpowers/writing-skills/persuasion-principles.md +187 -0
  99. package/template/.superpowers/writing-skills/render-graphs.js +168 -0
  100. package/template/.superpowers/writing-skills/testing-skills-with-subagents.md +384 -0
  101. package/template/AGENTS.md +116 -0
@@ -0,0 +1,628 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Git and Session Context utilities.
5
+
6
+ Provides:
7
+ output_json - Output context in JSON format
8
+ output_text - Output context in text format
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import subprocess
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+
18
+ from .files import read_json_file as _read_json_file
19
+ from .paths import (
20
+ DIR_SCRIPTS,
21
+ DIR_SPEC,
22
+ DIR_TASKS,
23
+ DIR_WORKFLOW,
24
+ DIR_WORKSPACE,
25
+ FILE_TASK_JSON,
26
+ count_lines,
27
+ get_active_journal_file,
28
+ get_current_task,
29
+ get_developer,
30
+ get_repo_root,
31
+ get_tasks_dir,
32
+ )
33
+
34
+ # =============================================================================
35
+ # Helper Functions
36
+ # =============================================================================
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class GitSnapshot:
41
+ branch: str
42
+ status_count: int
43
+ log_out: str
44
+ recent_commits: list[dict]
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class CurrentTaskSnapshot:
49
+ path: str | None
50
+ data: dict | None
51
+ has_prd: bool
52
+
53
+
54
+ def _run_git_command(args: list[str], cwd: Path | None = None) -> tuple[int, str, str]:
55
+ """Run a git command and return (returncode, stdout, stderr).
56
+
57
+ Uses UTF-8 encoding with -c i18n.logOutputEncoding=UTF-8 to ensure
58
+ consistent output across all platforms (Windows, macOS, Linux).
59
+ """
60
+ try:
61
+ # Force git to output UTF-8 for consistent cross-platform behavior
62
+ git_args = ["git", "-c", "i18n.logOutputEncoding=UTF-8"] + args
63
+ result = subprocess.run(
64
+ git_args,
65
+ cwd=cwd,
66
+ capture_output=True,
67
+ text=True,
68
+ encoding="utf-8",
69
+ errors="replace",
70
+ )
71
+ return result.returncode, result.stdout, result.stderr
72
+ except Exception as e:
73
+ return 1, "", str(e)
74
+
75
+
76
+ def _iter_task_dirs(tasks_dir: Path, sort: bool = True):
77
+ """Yield active task directories."""
78
+ if not tasks_dir.is_dir():
79
+ return
80
+
81
+ dirs = sorted(tasks_dir.iterdir()) if sort else tasks_dir.iterdir()
82
+ for d in dirs:
83
+ if d.is_dir() and d.name != "archive":
84
+ yield d
85
+
86
+
87
+ def _load_task_json_by_dir(tasks_dir: Path, sort: bool = True) -> dict[str, dict]:
88
+ """Load task.json data keyed by task directory name."""
89
+ tasks: dict[str, dict] = {}
90
+ for d in _iter_task_dirs(tasks_dir, sort=sort):
91
+ task_json = d / FILE_TASK_JSON
92
+ if task_json.is_file():
93
+ data = _read_json_file(task_json)
94
+ if data:
95
+ tasks[d.name] = data
96
+ return tasks
97
+
98
+
99
+ def _parse_recent_commits(log_out: str, include_empty_message: bool = True) -> list[dict]:
100
+ """Parse git log --oneline output into JSON-ready commit summaries."""
101
+ commits = []
102
+ for line in log_out.splitlines():
103
+ if not line.strip():
104
+ continue
105
+
106
+ parts = line.split(" ", 1)
107
+ if len(parts) >= 2:
108
+ commits.append({"hash": parts[0], "message": parts[1]})
109
+ elif include_empty_message:
110
+ commits.append({"hash": parts[0], "message": ""})
111
+ return commits
112
+
113
+
114
+ def _get_git_snapshot(
115
+ repo_root: Path,
116
+ include_empty_commit_message: bool = True,
117
+ ) -> GitSnapshot:
118
+ """Collect git data shared by JSON and text renderers."""
119
+ _, branch_out, _ = _run_git_command(["branch", "--show-current"], cwd=repo_root)
120
+ _, status_out, _ = _run_git_command(["status", "--porcelain"], cwd=repo_root)
121
+ _, log_out, _ = _run_git_command(["log", "--oneline", "-5"], cwd=repo_root)
122
+
123
+ status_lines = [line for line in status_out.splitlines() if line.strip()]
124
+ return GitSnapshot(
125
+ branch=branch_out.strip() or "unknown",
126
+ status_count=len(status_lines),
127
+ log_out=log_out,
128
+ recent_commits=_parse_recent_commits(
129
+ log_out,
130
+ include_empty_message=include_empty_commit_message,
131
+ ),
132
+ )
133
+
134
+
135
+ def _git_json(git_snapshot: GitSnapshot) -> dict:
136
+ """Build the git JSON object used by context output."""
137
+ status_count = git_snapshot.status_count
138
+ return {
139
+ "branch": git_snapshot.branch,
140
+ "isClean": status_count == 0,
141
+ "uncommittedChanges": status_count,
142
+ "recentCommits": git_snapshot.recent_commits,
143
+ }
144
+
145
+
146
+ def _append_git_status(lines: list[str], git_snapshot: GitSnapshot, repo_root: Path) -> None:
147
+ """Append the GIT STATUS section to text output."""
148
+ lines.append("## GIT STATUS")
149
+ lines.append(f"Branch: {git_snapshot.branch}")
150
+
151
+ status_count = git_snapshot.status_count
152
+ if status_count == 0:
153
+ lines.append("Working directory: Clean")
154
+ else:
155
+ lines.append(f"Working directory: {status_count} uncommitted change(s)")
156
+ lines.append("")
157
+ lines.append("Changes:")
158
+ _, short_out, _ = _run_git_command(["status", "--short"], cwd=repo_root)
159
+ for line in short_out.splitlines()[:10]:
160
+ lines.append(line)
161
+ lines.append("")
162
+
163
+
164
+ def _append_recent_commits(lines: list[str], git_snapshot: GitSnapshot) -> None:
165
+ """Append the RECENT COMMITS section to text output."""
166
+ lines.append("## RECENT COMMITS")
167
+ log_out = git_snapshot.log_out
168
+ if log_out.strip():
169
+ for line in log_out.splitlines():
170
+ lines.append(line)
171
+ else:
172
+ lines.append("(no commits)")
173
+ lines.append("")
174
+
175
+
176
+ def _get_current_task_snapshot(repo_root: Path) -> CurrentTaskSnapshot:
177
+ """Collect current-task metadata once for text and JSON renderers."""
178
+ current_task = get_current_task(repo_root)
179
+ if not current_task:
180
+ return CurrentTaskSnapshot(path=None, data=None, has_prd=False)
181
+
182
+ current_task_dir = repo_root / current_task
183
+ task_json_path = current_task_dir / FILE_TASK_JSON
184
+ data = None
185
+ if task_json_path.is_file():
186
+ data = _read_json_file(task_json_path)
187
+ return CurrentTaskSnapshot(
188
+ path=current_task,
189
+ data=data,
190
+ has_prd=(current_task_dir / "prd.md").is_file(),
191
+ )
192
+
193
+
194
+ def _current_task_json(snapshot: CurrentTaskSnapshot) -> dict | None:
195
+ """Build the record-mode currentTask JSON object."""
196
+ current_task = snapshot.path
197
+ data = snapshot.data
198
+ if not current_task or not data:
199
+ return None
200
+
201
+ return {
202
+ "path": current_task,
203
+ "name": data.get("name") or data.get("id") or "unknown",
204
+ "status": data.get("status", "unknown"),
205
+ }
206
+
207
+
208
+ def _append_current_task(
209
+ lines: list[str],
210
+ snapshot: CurrentTaskSnapshot,
211
+ include_created: bool = False,
212
+ include_description: bool = False,
213
+ include_prd_hint: bool = False,
214
+ ) -> None:
215
+ """Append the CURRENT TASK section to text output."""
216
+ lines.append("## CURRENT TASK")
217
+ current_task = snapshot.path
218
+ if not current_task:
219
+ lines.append("(none)")
220
+ lines.append("")
221
+ return
222
+
223
+ lines.append(f"Path: {current_task}")
224
+ data = snapshot.data
225
+ if data:
226
+ lines.append(f"Name: {data.get('name') or data.get('id') or 'unknown'}")
227
+ lines.append(f"Status: {data.get('status', 'unknown')}")
228
+ if include_created:
229
+ lines.append(f"Created: {data.get('createdAt', 'unknown')}")
230
+ if include_description:
231
+ description = data.get("description", "")
232
+ if description:
233
+ lines.append(f"Description: {description}")
234
+
235
+ if include_prd_hint and snapshot.has_prd:
236
+ lines.append("")
237
+ lines.append("[!] This task has prd.md - read it for task details")
238
+ lines.append("")
239
+
240
+
241
+ def _load_task_context_by_dir(tasks_dir: Path) -> dict[str, dict]:
242
+ """Load task display data, preserving dirs without task.json as unknown."""
243
+ all_task_data: dict[str, dict] = {}
244
+ for d in _iter_task_dirs(tasks_dir):
245
+ data = _read_json_file(d / FILE_TASK_JSON) or {}
246
+ all_task_data[d.name] = {
247
+ "status": data.get("status", "unknown"),
248
+ "my_status": data.get("status", "planning"),
249
+ "assignee": data.get("assignee", "-"),
250
+ "my_assignee": data.get("assignee", ""),
251
+ "title": data.get("title") or data.get("name") or "unknown",
252
+ "priority": data.get("priority", "P2"),
253
+ "children": data.get("children", []),
254
+ "parent": data.get("parent"),
255
+ }
256
+ return all_task_data
257
+
258
+
259
+ def _task_statuses(task_data_by_dir: dict[str, dict]) -> dict[str, str]:
260
+ """Return task status lookup by directory name."""
261
+ return {
262
+ dir_name: data.get("status", "unknown")
263
+ for dir_name, data in task_data_by_dir.items()
264
+ }
265
+
266
+
267
+ def _children_done_count(children: list[str], statuses: dict[str, str]) -> int:
268
+ """Count completed child tasks."""
269
+ return sum(1 for child in children if statuses.get(child) in ("completed", "done"))
270
+
271
+
272
+ def _children_progress(children: list[str], statuses: dict[str, str]) -> str:
273
+ """Render children progress like '[2/3 done]'."""
274
+ if not children:
275
+ return ""
276
+ return f" [{_children_done_count(children, statuses)}/{len(children)} done]"
277
+
278
+
279
+ # =============================================================================
280
+ # JSON Output
281
+ # =============================================================================
282
+
283
+
284
+ def get_context_json(repo_root: Path | None = None) -> dict:
285
+ """Get context as a dictionary.
286
+
287
+ Args:
288
+ repo_root: Repository root path. Defaults to auto-detected.
289
+
290
+ Returns:
291
+ Context dictionary.
292
+ """
293
+ if repo_root is None:
294
+ repo_root = get_repo_root()
295
+
296
+ developer = get_developer(repo_root)
297
+ tasks_dir = get_tasks_dir(repo_root)
298
+ journal_file = get_active_journal_file(repo_root)
299
+ git_snapshot = _get_git_snapshot(repo_root)
300
+
301
+ journal_lines = 0
302
+ journal_relative = ""
303
+ if journal_file and developer:
304
+ journal_lines = count_lines(journal_file)
305
+ journal_relative = (
306
+ f"{DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/{journal_file.name}"
307
+ )
308
+
309
+ # Tasks
310
+ tasks = []
311
+ for dir_name, data in _load_task_json_by_dir(tasks_dir, sort=False).items():
312
+ tasks.append(
313
+ {
314
+ "dir": dir_name,
315
+ "name": data.get("name") or data.get("id") or "unknown",
316
+ "status": data.get("status", "unknown"),
317
+ "children": data.get("children", []),
318
+ "parent": data.get("parent"),
319
+ }
320
+ )
321
+
322
+ return {
323
+ "developer": developer or "",
324
+ "git": _git_json(git_snapshot),
325
+ "tasks": {
326
+ "active": tasks,
327
+ "directory": f"{DIR_WORKFLOW}/{DIR_TASKS}",
328
+ },
329
+ "journal": {
330
+ "file": journal_relative,
331
+ "lines": journal_lines,
332
+ "nearLimit": journal_lines > 1800,
333
+ },
334
+ }
335
+
336
+
337
+ def output_json(repo_root: Path | None = None) -> None:
338
+ """Output context in JSON format.
339
+
340
+ Args:
341
+ repo_root: Repository root path. Defaults to auto-detected.
342
+ """
343
+ context = get_context_json(repo_root)
344
+ print(json.dumps(context, indent=2, ensure_ascii=False))
345
+
346
+
347
+ # =============================================================================
348
+ # Text Output
349
+ # =============================================================================
350
+
351
+
352
+ def get_context_text(repo_root: Path | None = None) -> str:
353
+ """Get context as formatted text.
354
+
355
+ Args:
356
+ repo_root: Repository root path. Defaults to auto-detected.
357
+
358
+ Returns:
359
+ Formatted text output.
360
+ """
361
+ if repo_root is None:
362
+ repo_root = get_repo_root()
363
+
364
+ lines = []
365
+ lines.append("========================================")
366
+ lines.append("SESSION CONTEXT")
367
+ lines.append("========================================")
368
+ lines.append("")
369
+
370
+ developer = get_developer(repo_root)
371
+
372
+ # Developer section
373
+ lines.append("## DEVELOPER")
374
+ if not developer:
375
+ lines.append(
376
+ f"ERROR: Not initialized. Run: python3 ./{DIR_WORKFLOW}/{DIR_SCRIPTS}/init_developer.py <name>"
377
+ )
378
+ return "\n".join(lines)
379
+
380
+ lines.append(f"Name: {developer}")
381
+ lines.append("")
382
+
383
+ git_snapshot = _get_git_snapshot(repo_root)
384
+ _append_git_status(lines, git_snapshot, repo_root)
385
+ _append_recent_commits(lines, git_snapshot)
386
+ _append_current_task(
387
+ lines,
388
+ _get_current_task_snapshot(repo_root),
389
+ include_created=True,
390
+ include_description=True,
391
+ include_prd_hint=True,
392
+ )
393
+
394
+ # Active tasks
395
+ lines.append("## ACTIVE TASKS")
396
+ tasks_dir = get_tasks_dir(repo_root)
397
+ task_count = 0
398
+
399
+ all_task_data = _load_task_context_by_dir(tasks_dir)
400
+ all_task_statuses = {
401
+ dir_name: data["status"]
402
+ for dir_name, data in all_task_data.items()
403
+ }
404
+
405
+ def _print_task_tree(name: str, indent: int = 0) -> None:
406
+ nonlocal task_count
407
+ info = all_task_data[name]
408
+ progress = _children_progress(info["children"], all_task_statuses)
409
+ prefix = " " * indent
410
+ lines.append(f"{prefix}- {name}/ ({info['status']}){progress} @{info['assignee']}")
411
+ task_count += 1
412
+ for child in info["children"]:
413
+ if child in all_task_data:
414
+ _print_task_tree(child, indent + 1)
415
+
416
+ for dir_name in sorted(all_task_data.keys()):
417
+ if not all_task_data[dir_name]["parent"]:
418
+ _print_task_tree(dir_name)
419
+
420
+ if task_count == 0:
421
+ lines.append("(no active tasks)")
422
+ lines.append(f"Total: {task_count} active task(s)")
423
+ lines.append("")
424
+
425
+ # My tasks
426
+ lines.append("## MY TASKS (Assigned to me)")
427
+ my_task_count = 0
428
+
429
+ for dir_name in sorted(all_task_data.keys()):
430
+ info = all_task_data[dir_name]
431
+ assignee = info["my_assignee"]
432
+ status = info["my_status"]
433
+
434
+ if assignee == developer and status != "done":
435
+ children_list = info["children"]
436
+ progress = _children_progress(children_list, all_task_statuses)
437
+ lines.append(f"- [{info['priority']}] {info['title']} ({status}){progress}")
438
+ my_task_count += 1
439
+
440
+ if my_task_count == 0:
441
+ lines.append("(no tasks assigned to you)")
442
+ lines.append("")
443
+
444
+ # Journal file
445
+ lines.append("## JOURNAL FILE")
446
+ journal_file = get_active_journal_file(repo_root)
447
+ if journal_file:
448
+ journal_lines = count_lines(journal_file)
449
+ relative = f"{DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/{journal_file.name}"
450
+ lines.append(f"Active file: {relative}")
451
+ lines.append(f"Line count: {journal_lines} / 2000")
452
+ if journal_lines > 1800:
453
+ lines.append("[!] WARNING: Approaching 2000 line limit!")
454
+ else:
455
+ lines.append("No journal file found")
456
+ lines.append("")
457
+
458
+ # Paths
459
+ lines.append("## PATHS")
460
+ lines.append(f"Workspace: {DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/")
461
+ lines.append(f"Tasks: {DIR_WORKFLOW}/{DIR_TASKS}/")
462
+ lines.append(f"Spec: {DIR_WORKFLOW}/{DIR_SPEC}/")
463
+ lines.append("")
464
+
465
+ lines.append("========================================")
466
+
467
+ return "\n".join(lines)
468
+
469
+
470
+ def get_context_record_json(repo_root: Path | None = None) -> dict:
471
+ """Get record-mode context as a dictionary.
472
+
473
+ Focused on: my active tasks, git status, current task.
474
+ """
475
+ if repo_root is None:
476
+ repo_root = get_repo_root()
477
+
478
+ developer = get_developer(repo_root)
479
+ tasks_dir = get_tasks_dir(repo_root)
480
+
481
+ git_snapshot = _get_git_snapshot(
482
+ repo_root,
483
+ include_empty_commit_message=False,
484
+ )
485
+
486
+ # My tasks
487
+ my_tasks = []
488
+ task_data_by_dir = _load_task_json_by_dir(tasks_dir)
489
+ all_task_statuses = _task_statuses(task_data_by_dir)
490
+
491
+ for dir_name, data in task_data_by_dir.items():
492
+ if data.get("assignee") == developer:
493
+ children_list = data.get("children", [])
494
+ done = _children_done_count(children_list, all_task_statuses)
495
+ my_tasks.append({
496
+ "dir": dir_name,
497
+ "title": data.get("title") or data.get("name") or "unknown",
498
+ "status": data.get("status", "unknown"),
499
+ "priority": data.get("priority", "P2"),
500
+ "children": children_list,
501
+ "childrenDone": done,
502
+ "parent": data.get("parent"),
503
+ "meta": data.get("meta", {}),
504
+ })
505
+
506
+ return {
507
+ "developer": developer or "",
508
+ "git": _git_json(git_snapshot),
509
+ "myTasks": my_tasks,
510
+ "currentTask": _current_task_json(_get_current_task_snapshot(repo_root)),
511
+ }
512
+
513
+
514
+ def get_context_text_record(repo_root: Path | None = None) -> str:
515
+ """Get context as formatted text for record-session mode.
516
+
517
+ Focused output: MY ACTIVE TASKS first (with [!!!] emphasis),
518
+ then GIT STATUS, RECENT COMMITS, CURRENT TASK.
519
+
520
+ Args:
521
+ repo_root: Repository root path. Defaults to auto-detected.
522
+
523
+ Returns:
524
+ Formatted text output for record-session.
525
+ """
526
+ if repo_root is None:
527
+ repo_root = get_repo_root()
528
+
529
+ lines: list[str] = []
530
+ lines.append("========================================")
531
+ lines.append("SESSION CONTEXT (RECORD MODE)")
532
+ lines.append("========================================")
533
+ lines.append("")
534
+
535
+ developer = get_developer(repo_root)
536
+ if not developer:
537
+ lines.append(
538
+ f"ERROR: Not initialized. Run: python3 ./{DIR_WORKFLOW}/{DIR_SCRIPTS}/init_developer.py <name>"
539
+ )
540
+ return "\n".join(lines)
541
+
542
+ # MY ACTIVE TASKS - first and prominent
543
+ lines.append(f"## [!!!] MY ACTIVE TASKS (Assigned to {developer})")
544
+ lines.append("[!] Review whether any should be archived before recording this session.")
545
+ lines.append("")
546
+
547
+ tasks_dir = get_tasks_dir(repo_root)
548
+ my_task_count = 0
549
+
550
+ # Collect task data for children progress
551
+ task_data_by_dir = _load_task_json_by_dir(tasks_dir)
552
+ all_task_statuses = _task_statuses(task_data_by_dir)
553
+
554
+ for dir_name, data in task_data_by_dir.items():
555
+ assignee = data.get("assignee", "")
556
+ status = data.get("status", "planning")
557
+
558
+ if assignee == developer:
559
+ title = data.get("title") or data.get("name") or "unknown"
560
+ priority = data.get("priority", "P2")
561
+ children_list = data.get("children", [])
562
+ progress = _children_progress(children_list, all_task_statuses)
563
+ lines.append(f"- [{priority}] {title} ({status}){progress} - {dir_name}")
564
+ my_task_count += 1
565
+
566
+ if my_task_count == 0:
567
+ lines.append("(no active tasks assigned to you)")
568
+ lines.append("")
569
+
570
+ git_snapshot = _get_git_snapshot(repo_root)
571
+ _append_git_status(lines, git_snapshot, repo_root)
572
+ _append_recent_commits(lines, git_snapshot)
573
+ _append_current_task(lines, _get_current_task_snapshot(repo_root))
574
+
575
+ lines.append("========================================")
576
+
577
+ return "\n".join(lines)
578
+
579
+
580
+ def output_text(repo_root: Path | None = None) -> None:
581
+ """Output context in text format.
582
+
583
+ Args:
584
+ repo_root: Repository root path. Defaults to auto-detected.
585
+ """
586
+ print(get_context_text(repo_root))
587
+
588
+
589
+ # =============================================================================
590
+ # Main Entry
591
+ # =============================================================================
592
+
593
+
594
+ def main() -> None:
595
+ """CLI entry point."""
596
+ import argparse
597
+
598
+ parser = argparse.ArgumentParser(description="Get Session Context for AI Agent")
599
+ parser.add_argument(
600
+ "--json",
601
+ "-j",
602
+ action="store_true",
603
+ help="Output in JSON format (works with any --mode)",
604
+ )
605
+ parser.add_argument(
606
+ "--mode",
607
+ "-m",
608
+ choices=["default", "record"],
609
+ default="default",
610
+ help="Output mode: default (full context) or record (for record-session)",
611
+ )
612
+
613
+ args = parser.parse_args()
614
+
615
+ if args.mode == "record":
616
+ if args.json:
617
+ print(json.dumps(get_context_record_json(), indent=2, ensure_ascii=False))
618
+ else:
619
+ print(get_context_text_record())
620
+ else:
621
+ if args.json:
622
+ output_json()
623
+ else:
624
+ output_text()
625
+
626
+
627
+ if __name__ == "__main__":
628
+ main()