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.
- package/README.md +193 -0
- package/bin/cowork-flow.js +9 -0
- package/package.json +28 -0
- package/src/cli.js +61 -0
- package/src/commands/init.js +88 -0
- package/src/commands/sync.js +33 -0
- package/src/commands/update.js +60 -0
- package/src/lib/copy-template.js +189 -0
- package/src/lib/package-info.js +49 -0
- package/src/lib/paths.js +6 -0
- package/template/.agent/skills/break-loop/SKILL.md +131 -0
- package/template/.agent/skills/check-cross-layer/SKILL.md +158 -0
- package/template/.agent/skills/finish-work/SKILL.md +163 -0
- package/template/.agent/skills/record-session/SKILL.md +78 -0
- package/template/.agent/skills/start/SKILL.md +390 -0
- package/template/.agent/skills/update-spec/SKILL.md +335 -0
- package/template/.cowork-flow/.version +1 -0
- package/template/.cowork-flow/changes/archive/.gitkeep +1 -0
- package/template/.cowork-flow/config.yaml +49 -0
- package/template/.cowork-flow/plans/.gitkeep +1 -0
- package/template/.cowork-flow/scripts/add_session.py +441 -0
- package/template/.cowork-flow/scripts/change.py +314 -0
- package/template/.cowork-flow/scripts/common/__init__.py +84 -0
- package/template/.cowork-flow/scripts/common/config.py +128 -0
- package/template/.cowork-flow/scripts/common/developer.py +205 -0
- package/template/.cowork-flow/scripts/common/files.py +24 -0
- package/template/.cowork-flow/scripts/common/git_context.py +628 -0
- package/template/.cowork-flow/scripts/common/paths.py +350 -0
- package/template/.cowork-flow/scripts/common/task_utils.py +200 -0
- package/template/.cowork-flow/scripts/get_context.py +16 -0
- package/template/.cowork-flow/scripts/get_developer.py +26 -0
- package/template/.cowork-flow/scripts/init_developer.py +51 -0
- package/template/.cowork-flow/scripts/task.py +1378 -0
- package/template/.cowork-flow/spec/backend/database-guidelines.md +26 -0
- package/template/.cowork-flow/spec/backend/directory-structure.md +27 -0
- package/template/.cowork-flow/spec/backend/error-handling.md +20 -0
- package/template/.cowork-flow/spec/backend/index.md +36 -0
- package/template/.cowork-flow/spec/backend/logging-guidelines.md +20 -0
- package/template/.cowork-flow/spec/backend/quality-guidelines.md +24 -0
- package/template/.cowork-flow/spec/frontend/component-guidelines.md +19 -0
- package/template/.cowork-flow/spec/frontend/directory-structure.md +18 -0
- package/template/.cowork-flow/spec/frontend/hook-guidelines.md +17 -0
- package/template/.cowork-flow/spec/frontend/index.md +28 -0
- package/template/.cowork-flow/spec/frontend/quality-guidelines.md +23 -0
- package/template/.cowork-flow/spec/frontend/state-management.md +18 -0
- package/template/.cowork-flow/spec/frontend/type-safety.md +18 -0
- package/template/.cowork-flow/spec/guides/code-reuse-thinking-guide.md +26 -0
- package/template/.cowork-flow/spec/guides/cross-layer-thinking-guide.md +21 -0
- package/template/.cowork-flow/spec/guides/index.md +31 -0
- package/template/.cowork-flow/spec/guides/pre-implementation-checklist.md +15 -0
- package/template/.cowork-flow/tasks/.gitkeep +1 -0
- package/template/.cowork-flow/tasks/archive/.gitkeep +1 -0
- package/template/.cowork-flow/workflow.md +411 -0
- package/template/.cowork-flow/workspace/index.md +95 -0
- package/template/.superpowers/brainstorming/SKILL.md +164 -0
- package/template/.superpowers/brainstorming/scripts/frame-template.html +214 -0
- package/template/.superpowers/brainstorming/scripts/helper.js +88 -0
- package/template/.superpowers/brainstorming/scripts/server.cjs +354 -0
- package/template/.superpowers/brainstorming/scripts/start-server.sh +148 -0
- package/template/.superpowers/brainstorming/scripts/stop-server.sh +56 -0
- package/template/.superpowers/brainstorming/spec-document-reviewer-prompt.md +49 -0
- package/template/.superpowers/brainstorming/visual-companion.md +287 -0
- package/template/.superpowers/dispatching-parallel-agents/SKILL.md +182 -0
- package/template/.superpowers/executing-plans/SKILL.md +70 -0
- package/template/.superpowers/finishing-a-development-branch/SKILL.md +251 -0
- package/template/.superpowers/receiving-code-review/SKILL.md +213 -0
- package/template/.superpowers/requesting-code-review/SKILL.md +103 -0
- package/template/.superpowers/requesting-code-review/code-reviewer.md +168 -0
- package/template/.superpowers/subagent-driven-development/SKILL.md +279 -0
- package/template/.superpowers/subagent-driven-development/code-quality-reviewer-prompt.md +25 -0
- package/template/.superpowers/subagent-driven-development/implementer-prompt.md +113 -0
- package/template/.superpowers/subagent-driven-development/spec-reviewer-prompt.md +61 -0
- package/template/.superpowers/systematic-debugging/CREATION-LOG.md +119 -0
- package/template/.superpowers/systematic-debugging/SKILL.md +296 -0
- package/template/.superpowers/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/template/.superpowers/systematic-debugging/condition-based-waiting.md +115 -0
- package/template/.superpowers/systematic-debugging/defense-in-depth.md +122 -0
- package/template/.superpowers/systematic-debugging/find-polluter.sh +63 -0
- package/template/.superpowers/systematic-debugging/root-cause-tracing.md +169 -0
- package/template/.superpowers/systematic-debugging/test-academic.md +14 -0
- package/template/.superpowers/systematic-debugging/test-pressure-1.md +58 -0
- package/template/.superpowers/systematic-debugging/test-pressure-2.md +68 -0
- package/template/.superpowers/systematic-debugging/test-pressure-3.md +69 -0
- package/template/.superpowers/test-driven-development/SKILL.md +371 -0
- package/template/.superpowers/test-driven-development/testing-anti-patterns.md +299 -0
- package/template/.superpowers/using-git-worktrees/SKILL.md +215 -0
- package/template/.superpowers/using-superpowers/SKILL.md +117 -0
- package/template/.superpowers/using-superpowers/references/codex-tools.md +59 -0
- package/template/.superpowers/using-superpowers/references/copilot-tools.md +42 -0
- package/template/.superpowers/using-superpowers/references/gemini-tools.md +51 -0
- package/template/.superpowers/verification-before-completion/SKILL.md +139 -0
- package/template/.superpowers/writing-plans/SKILL.md +152 -0
- package/template/.superpowers/writing-plans/plan-document-reviewer-prompt.md +49 -0
- package/template/.superpowers/writing-skills/SKILL.md +655 -0
- package/template/.superpowers/writing-skills/anthropic-best-practices.md +1150 -0
- package/template/.superpowers/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
- package/template/.superpowers/writing-skills/graphviz-conventions.dot +172 -0
- package/template/.superpowers/writing-skills/persuasion-principles.md +187 -0
- package/template/.superpowers/writing-skills/render-graphs.js +168 -0
- package/template/.superpowers/writing-skills/testing-skills-with-subagents.md +384 -0
- 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()
|