@mindfoldhq/trellis 0.3.8 → 0.3.10-beta.0
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/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +203 -31
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +154 -6
- package/dist/commands/update.js.map +1 -1
- package/dist/configurators/workflow.d.ts +6 -2
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +88 -58
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +2 -0
- package/dist/migrations/index.js.map +1 -1
- package/dist/migrations/manifests/0.3.9.json +9 -0
- package/dist/migrations/manifests/0.4.0-beta.1.json +228 -0
- package/dist/templates/claude/agents/dispatch.md +1 -2
- package/dist/templates/claude/agents/implement.md +2 -3
- package/dist/templates/claude/commands/trellis/before-dev.md +29 -0
- package/dist/templates/claude/commands/trellis/check.md +25 -0
- package/dist/templates/claude/commands/trellis/create-command.md +2 -2
- package/dist/templates/claude/commands/trellis/onboard.md +13 -13
- package/dist/templates/claude/commands/trellis/parallel.md +1 -2
- package/dist/templates/claude/commands/trellis/record-session.md +1 -1
- package/dist/templates/claude/commands/trellis/start.md +8 -4
- package/dist/templates/claude/hooks/inject-subagent-context.py +21 -13
- package/dist/templates/claude/hooks/session-start.py +170 -2
- package/dist/templates/codex/skills/before-dev/SKILL.md +34 -0
- package/dist/templates/codex/skills/check/SKILL.md +30 -0
- package/dist/templates/codex/skills/create-command/SKILL.md +2 -2
- package/dist/templates/codex/skills/onboard/SKILL.md +11 -11
- package/dist/templates/codex/skills/record-session/SKILL.md +1 -1
- package/dist/templates/codex/skills/start/SKILL.md +8 -3
- package/dist/templates/cursor/commands/trellis-before-dev.md +29 -0
- package/dist/templates/cursor/commands/trellis-check.md +25 -0
- package/dist/templates/cursor/commands/trellis-create-command.md +2 -2
- package/dist/templates/cursor/commands/trellis-onboard.md +13 -13
- package/dist/templates/cursor/commands/trellis-record-session.md +1 -1
- package/dist/templates/cursor/commands/trellis-start.md +7 -16
- package/dist/templates/gemini/commands/trellis/before-dev.toml +33 -0
- package/dist/templates/gemini/commands/trellis/check.toml +29 -0
- package/dist/templates/gemini/commands/trellis/create-command.toml +2 -2
- package/dist/templates/gemini/commands/trellis/onboard.toml +2 -2
- package/dist/templates/gemini/commands/trellis/record-session.toml +1 -1
- package/dist/templates/gemini/commands/trellis/start.toml +9 -4
- package/dist/templates/iflow/agents/dispatch.md +1 -2
- package/dist/templates/iflow/agents/implement.md +2 -3
- package/dist/templates/iflow/commands/trellis/before-dev.md +29 -0
- package/dist/templates/iflow/commands/trellis/check.md +25 -0
- package/dist/templates/iflow/commands/trellis/create-command.md +2 -2
- package/dist/templates/iflow/commands/trellis/onboard.md +13 -13
- package/dist/templates/iflow/commands/trellis/parallel.md +1 -2
- package/dist/templates/iflow/commands/trellis/record-session.md +1 -1
- package/dist/templates/iflow/commands/trellis/start.md +8 -4
- package/dist/templates/iflow/hooks/inject-subagent-context.py +21 -13
- package/dist/templates/iflow/hooks/session-start.py +156 -1
- package/dist/templates/iflow/settings.json +2 -2
- package/dist/templates/kilo/workflows/before-dev.md +29 -0
- package/dist/templates/kilo/workflows/check.md +25 -0
- package/dist/templates/kilo/workflows/create-command.md +2 -2
- package/dist/templates/kilo/workflows/onboard.md +13 -13
- package/dist/templates/kilo/workflows/parallel.md +1 -2
- package/dist/templates/kilo/workflows/record-session.md +1 -1
- package/dist/templates/kilo/workflows/start.md +8 -3
- package/dist/templates/kiro/skills/before-dev/SKILL.md +34 -0
- package/dist/templates/kiro/skills/check/SKILL.md +30 -0
- package/dist/templates/kiro/skills/create-command/SKILL.md +2 -2
- package/dist/templates/kiro/skills/onboard/SKILL.md +11 -11
- package/dist/templates/kiro/skills/record-session/SKILL.md +1 -1
- package/dist/templates/kiro/skills/start/SKILL.md +8 -3
- package/dist/templates/markdown/spec/backend/script-conventions.md +93 -0
- package/dist/templates/opencode/agents/dispatch.md +1 -2
- package/dist/templates/opencode/agents/implement.md +2 -2
- package/dist/templates/opencode/agents/research.md +1 -2
- package/dist/templates/opencode/commands/trellis/before-dev.md +29 -0
- package/dist/templates/opencode/commands/trellis/check.md +25 -0
- package/dist/templates/opencode/commands/trellis/create-command.md +2 -2
- package/dist/templates/opencode/commands/trellis/onboard.md +13 -13
- package/dist/templates/opencode/commands/trellis/parallel.md +1 -2
- package/dist/templates/opencode/commands/trellis/record-session.md +1 -1
- package/dist/templates/opencode/commands/trellis/start.md +8 -3
- package/dist/templates/opencode/plugin/inject-subagent-context.js +45 -18
- package/dist/templates/opencode/plugin/session-start.js +149 -1
- package/dist/templates/qoder/skills/before-dev/SKILL.md +34 -0
- package/dist/templates/qoder/skills/check/SKILL.md +30 -0
- package/dist/templates/qoder/skills/create-command/SKILL.md +2 -2
- package/dist/templates/qoder/skills/onboard/SKILL.md +13 -13
- package/dist/templates/qoder/skills/record-session/SKILL.md +1 -1
- package/dist/templates/qoder/skills/start/SKILL.md +8 -3
- package/dist/templates/trellis/config.yaml +20 -0
- package/dist/templates/trellis/index.d.ts +11 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +22 -0
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/add_session.py +52 -7
- package/dist/templates/trellis/scripts/common/cli_adapter.py +33 -45
- package/dist/templates/trellis/scripts/common/config.py +152 -0
- package/dist/templates/trellis/scripts/common/git.py +31 -0
- package/dist/templates/trellis/scripts/common/git_context.py +23 -586
- package/dist/templates/trellis/scripts/common/io.py +37 -0
- package/dist/templates/trellis/scripts/common/log.py +45 -0
- package/dist/templates/trellis/scripts/common/packages_context.py +233 -0
- package/dist/templates/trellis/scripts/common/paths.py +46 -0
- package/dist/templates/trellis/scripts/common/phase.py +50 -49
- package/dist/templates/trellis/scripts/common/registry.py +41 -72
- package/dist/templates/trellis/scripts/common/session_context.py +466 -0
- package/dist/templates/trellis/scripts/common/task_context.py +384 -0
- package/dist/templates/trellis/scripts/common/task_queue.py +27 -98
- package/dist/templates/trellis/scripts/common/task_store.py +534 -0
- package/dist/templates/trellis/scripts/common/task_utils.py +96 -6
- package/dist/templates/trellis/scripts/common/tasks.py +109 -0
- package/dist/templates/trellis/scripts/common/types.py +112 -0
- package/dist/templates/trellis/scripts/create_bootstrap.py +31 -26
- package/dist/templates/trellis/scripts/hooks/linear_sync.py +243 -0
- package/dist/templates/trellis/scripts/multi_agent/_bootstrap.py +17 -0
- package/dist/templates/trellis/scripts/multi_agent/cleanup.py +43 -48
- package/dist/templates/trellis/scripts/multi_agent/create_pr.py +336 -45
- package/dist/templates/trellis/scripts/multi_agent/plan.py +2 -26
- package/dist/templates/trellis/scripts/multi_agent/start.py +126 -57
- package/dist/templates/trellis/scripts/multi_agent/status.py +12 -753
- package/dist/templates/trellis/scripts/multi_agent/status_display.py +542 -0
- package/dist/templates/trellis/scripts/multi_agent/status_monitor.py +225 -0
- package/dist/templates/trellis/scripts/task.py +50 -975
- package/dist/templates/trellis/workflow.md +21 -34
- package/dist/types/migration.d.ts +3 -1
- package/dist/types/migration.d.ts.map +1 -1
- package/dist/utils/project-detector.d.ts +23 -0
- package/dist/utils/project-detector.d.ts.map +1 -1
- package/dist/utils/project-detector.js +364 -0
- package/dist/utils/project-detector.js.map +1 -1
- package/dist/utils/template-fetcher.d.ts +2 -2
- package/dist/utils/template-fetcher.d.ts.map +1 -1
- package/dist/utils/template-fetcher.js +5 -5
- package/dist/utils/template-fetcher.js.map +1 -1
- package/package.json +1 -1
- package/dist/templates/claude/commands/trellis/before-backend-dev.md +0 -13
- package/dist/templates/claude/commands/trellis/before-frontend-dev.md +0 -13
- package/dist/templates/claude/commands/trellis/check-backend.md +0 -13
- package/dist/templates/claude/commands/trellis/check-frontend.md +0 -13
- package/dist/templates/codex/skills/before-backend-dev/SKILL.md +0 -18
- package/dist/templates/codex/skills/before-frontend-dev/SKILL.md +0 -18
- package/dist/templates/codex/skills/check-backend/SKILL.md +0 -18
- package/dist/templates/codex/skills/check-frontend/SKILL.md +0 -18
- package/dist/templates/cursor/commands/trellis-before-backend-dev.md +0 -13
- package/dist/templates/cursor/commands/trellis-before-frontend-dev.md +0 -13
- package/dist/templates/cursor/commands/trellis-check-backend.md +0 -13
- package/dist/templates/cursor/commands/trellis-check-frontend.md +0 -13
- package/dist/templates/gemini/commands/trellis/before-backend-dev.toml +0 -17
- package/dist/templates/gemini/commands/trellis/before-frontend-dev.toml +0 -17
- package/dist/templates/gemini/commands/trellis/check-backend.toml +0 -17
- package/dist/templates/gemini/commands/trellis/check-frontend.toml +0 -17
- package/dist/templates/iflow/commands/trellis/before-backend-dev.md +0 -13
- package/dist/templates/iflow/commands/trellis/before-frontend-dev.md +0 -13
- package/dist/templates/iflow/commands/trellis/check-backend.md +0 -13
- package/dist/templates/iflow/commands/trellis/check-frontend.md +0 -13
- package/dist/templates/kilo/workflows/before-backend-dev.md +0 -13
- package/dist/templates/kilo/workflows/before-frontend-dev.md +0 -13
- package/dist/templates/kilo/workflows/check-backend.md +0 -13
- package/dist/templates/kilo/workflows/check-frontend.md +0 -13
- package/dist/templates/kiro/skills/before-backend-dev/SKILL.md +0 -18
- package/dist/templates/kiro/skills/before-frontend-dev/SKILL.md +0 -18
- package/dist/templates/kiro/skills/check-backend/SKILL.md +0 -18
- package/dist/templates/kiro/skills/check-frontend/SKILL.md +0 -18
- package/dist/templates/opencode/commands/trellis/before-backend-dev.md +0 -13
- package/dist/templates/opencode/commands/trellis/before-frontend-dev.md +0 -13
- package/dist/templates/opencode/commands/trellis/check-backend.md +0 -13
- package/dist/templates/opencode/commands/trellis/check-frontend.md +0 -13
- package/dist/templates/qoder/skills/before-backend-dev/SKILL.md +0 -18
- package/dist/templates/qoder/skills/before-frontend-dev/SKILL.md +0 -18
- package/dist/templates/qoder/skills/check-backend/SKILL.md +0 -18
- package/dist/templates/qoder/skills/check-frontend/SKILL.md +0 -18
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
"""
|
|
4
4
|
Git and Session Context utilities.
|
|
5
5
|
|
|
6
|
+
Entry shim — delegates to session_context and packages_context.
|
|
7
|
+
|
|
6
8
|
Provides:
|
|
7
9
|
output_json - Output context in JSON format
|
|
8
10
|
output_text - Output context in text format
|
|
@@ -11,599 +13,29 @@ Provides:
|
|
|
11
13
|
from __future__ import annotations
|
|
12
14
|
|
|
13
15
|
import json
|
|
14
|
-
import subprocess
|
|
15
|
-
from pathlib import Path
|
|
16
16
|
|
|
17
|
-
from .
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
get_tasks_dir,
|
|
17
|
+
from .git import run_git
|
|
18
|
+
from .session_context import (
|
|
19
|
+
get_context_json,
|
|
20
|
+
get_context_text,
|
|
21
|
+
get_context_record_json,
|
|
22
|
+
get_context_text_record,
|
|
23
|
+
output_json,
|
|
24
|
+
output_text,
|
|
25
|
+
)
|
|
26
|
+
from .packages_context import (
|
|
27
|
+
get_context_packages_text,
|
|
28
|
+
get_context_packages_json,
|
|
30
29
|
)
|
|
31
30
|
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
# =============================================================================
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def _run_git_command(args: list[str], cwd: Path | None = None) -> tuple[int, str, str]:
|
|
38
|
-
"""Run a git command and return (returncode, stdout, stderr).
|
|
39
|
-
|
|
40
|
-
Uses UTF-8 encoding with -c i18n.logOutputEncoding=UTF-8 to ensure
|
|
41
|
-
consistent output across all platforms (Windows, macOS, Linux).
|
|
42
|
-
"""
|
|
43
|
-
try:
|
|
44
|
-
# Force git to output UTF-8 for consistent cross-platform behavior
|
|
45
|
-
git_args = ["git", "-c", "i18n.logOutputEncoding=UTF-8"] + args
|
|
46
|
-
result = subprocess.run(
|
|
47
|
-
git_args,
|
|
48
|
-
cwd=cwd,
|
|
49
|
-
capture_output=True,
|
|
50
|
-
text=True,
|
|
51
|
-
encoding="utf-8",
|
|
52
|
-
errors="replace",
|
|
53
|
-
)
|
|
54
|
-
return result.returncode, result.stdout, result.stderr
|
|
55
|
-
except Exception as e:
|
|
56
|
-
return 1, "", str(e)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def _read_json_file(path: Path) -> dict | None:
|
|
60
|
-
"""Read and parse a JSON file."""
|
|
61
|
-
try:
|
|
62
|
-
return json.loads(path.read_text(encoding="utf-8"))
|
|
63
|
-
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
|
64
|
-
return None
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
# =============================================================================
|
|
68
|
-
# JSON Output
|
|
69
|
-
# =============================================================================
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def get_context_json(repo_root: Path | None = None) -> dict:
|
|
73
|
-
"""Get context as a dictionary.
|
|
74
|
-
|
|
75
|
-
Args:
|
|
76
|
-
repo_root: Repository root path. Defaults to auto-detected.
|
|
77
|
-
|
|
78
|
-
Returns:
|
|
79
|
-
Context dictionary.
|
|
80
|
-
"""
|
|
81
|
-
if repo_root is None:
|
|
82
|
-
repo_root = get_repo_root()
|
|
83
|
-
|
|
84
|
-
developer = get_developer(repo_root)
|
|
85
|
-
tasks_dir = get_tasks_dir(repo_root)
|
|
86
|
-
journal_file = get_active_journal_file(repo_root)
|
|
87
|
-
|
|
88
|
-
journal_lines = 0
|
|
89
|
-
journal_relative = ""
|
|
90
|
-
if journal_file and developer:
|
|
91
|
-
journal_lines = count_lines(journal_file)
|
|
92
|
-
journal_relative = (
|
|
93
|
-
f"{DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/{journal_file.name}"
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
# Git info
|
|
97
|
-
_, branch_out, _ = _run_git_command(["branch", "--show-current"], cwd=repo_root)
|
|
98
|
-
branch = branch_out.strip() or "unknown"
|
|
99
|
-
|
|
100
|
-
_, status_out, _ = _run_git_command(["status", "--porcelain"], cwd=repo_root)
|
|
101
|
-
git_status_count = len([line for line in status_out.splitlines() if line.strip()])
|
|
102
|
-
is_clean = git_status_count == 0
|
|
103
|
-
|
|
104
|
-
# Recent commits
|
|
105
|
-
_, log_out, _ = _run_git_command(["log", "--oneline", "-5"], cwd=repo_root)
|
|
106
|
-
commits = []
|
|
107
|
-
for line in log_out.splitlines():
|
|
108
|
-
if line.strip():
|
|
109
|
-
parts = line.split(" ", 1)
|
|
110
|
-
if len(parts) >= 2:
|
|
111
|
-
commits.append({"hash": parts[0], "message": parts[1]})
|
|
112
|
-
elif len(parts) == 1:
|
|
113
|
-
commits.append({"hash": parts[0], "message": ""})
|
|
114
|
-
|
|
115
|
-
# Tasks
|
|
116
|
-
tasks = []
|
|
117
|
-
if tasks_dir.is_dir():
|
|
118
|
-
for d in tasks_dir.iterdir():
|
|
119
|
-
if d.is_dir() and d.name != "archive":
|
|
120
|
-
task_json_path = d / FILE_TASK_JSON
|
|
121
|
-
if task_json_path.is_file():
|
|
122
|
-
data = _read_json_file(task_json_path)
|
|
123
|
-
if data:
|
|
124
|
-
tasks.append(
|
|
125
|
-
{
|
|
126
|
-
"dir": d.name,
|
|
127
|
-
"name": data.get("name") or data.get("id") or "unknown",
|
|
128
|
-
"status": data.get("status", "unknown"),
|
|
129
|
-
"children": data.get("children", []),
|
|
130
|
-
"parent": data.get("parent"),
|
|
131
|
-
}
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
return {
|
|
135
|
-
"developer": developer or "",
|
|
136
|
-
"git": {
|
|
137
|
-
"branch": branch,
|
|
138
|
-
"isClean": is_clean,
|
|
139
|
-
"uncommittedChanges": git_status_count,
|
|
140
|
-
"recentCommits": commits,
|
|
141
|
-
},
|
|
142
|
-
"tasks": {
|
|
143
|
-
"active": tasks,
|
|
144
|
-
"directory": f"{DIR_WORKFLOW}/{DIR_TASKS}",
|
|
145
|
-
},
|
|
146
|
-
"journal": {
|
|
147
|
-
"file": journal_relative,
|
|
148
|
-
"lines": journal_lines,
|
|
149
|
-
"nearLimit": journal_lines > 1800,
|
|
150
|
-
},
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
def output_json(repo_root: Path | None = None) -> None:
|
|
155
|
-
"""Output context in JSON format.
|
|
156
|
-
|
|
157
|
-
Args:
|
|
158
|
-
repo_root: Repository root path. Defaults to auto-detected.
|
|
159
|
-
"""
|
|
160
|
-
context = get_context_json(repo_root)
|
|
161
|
-
print(json.dumps(context, indent=2, ensure_ascii=False))
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
# =============================================================================
|
|
165
|
-
# Text Output
|
|
166
|
-
# =============================================================================
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
def get_context_text(repo_root: Path | None = None) -> str:
|
|
170
|
-
"""Get context as formatted text.
|
|
171
|
-
|
|
172
|
-
Args:
|
|
173
|
-
repo_root: Repository root path. Defaults to auto-detected.
|
|
174
|
-
|
|
175
|
-
Returns:
|
|
176
|
-
Formatted text output.
|
|
177
|
-
"""
|
|
178
|
-
if repo_root is None:
|
|
179
|
-
repo_root = get_repo_root()
|
|
180
|
-
|
|
181
|
-
lines = []
|
|
182
|
-
lines.append("========================================")
|
|
183
|
-
lines.append("SESSION CONTEXT")
|
|
184
|
-
lines.append("========================================")
|
|
185
|
-
lines.append("")
|
|
186
|
-
|
|
187
|
-
developer = get_developer(repo_root)
|
|
188
|
-
|
|
189
|
-
# Developer section
|
|
190
|
-
lines.append("## DEVELOPER")
|
|
191
|
-
if not developer:
|
|
192
|
-
lines.append(
|
|
193
|
-
f"ERROR: Not initialized. Run: python3 ./{DIR_WORKFLOW}/{DIR_SCRIPTS}/init_developer.py <name>"
|
|
194
|
-
)
|
|
195
|
-
return "\n".join(lines)
|
|
196
|
-
|
|
197
|
-
lines.append(f"Name: {developer}")
|
|
198
|
-
lines.append("")
|
|
199
|
-
|
|
200
|
-
# Git status
|
|
201
|
-
lines.append("## GIT STATUS")
|
|
202
|
-
_, branch_out, _ = _run_git_command(["branch", "--show-current"], cwd=repo_root)
|
|
203
|
-
branch = branch_out.strip() or "unknown"
|
|
204
|
-
lines.append(f"Branch: {branch}")
|
|
205
|
-
|
|
206
|
-
_, status_out, _ = _run_git_command(["status", "--porcelain"], cwd=repo_root)
|
|
207
|
-
status_lines = [line for line in status_out.splitlines() if line.strip()]
|
|
208
|
-
status_count = len(status_lines)
|
|
209
|
-
|
|
210
|
-
if status_count == 0:
|
|
211
|
-
lines.append("Working directory: Clean")
|
|
212
|
-
else:
|
|
213
|
-
lines.append(f"Working directory: {status_count} uncommitted change(s)")
|
|
214
|
-
lines.append("")
|
|
215
|
-
lines.append("Changes:")
|
|
216
|
-
_, short_out, _ = _run_git_command(["status", "--short"], cwd=repo_root)
|
|
217
|
-
for line in short_out.splitlines()[:10]:
|
|
218
|
-
lines.append(line)
|
|
219
|
-
lines.append("")
|
|
220
|
-
|
|
221
|
-
# Recent commits
|
|
222
|
-
lines.append("## RECENT COMMITS")
|
|
223
|
-
_, log_out, _ = _run_git_command(["log", "--oneline", "-5"], cwd=repo_root)
|
|
224
|
-
if log_out.strip():
|
|
225
|
-
for line in log_out.splitlines():
|
|
226
|
-
lines.append(line)
|
|
227
|
-
else:
|
|
228
|
-
lines.append("(no commits)")
|
|
229
|
-
lines.append("")
|
|
230
|
-
|
|
231
|
-
# Current task
|
|
232
|
-
lines.append("## CURRENT TASK")
|
|
233
|
-
current_task = get_current_task(repo_root)
|
|
234
|
-
if current_task:
|
|
235
|
-
current_task_dir = repo_root / current_task
|
|
236
|
-
task_json_path = current_task_dir / FILE_TASK_JSON
|
|
237
|
-
lines.append(f"Path: {current_task}")
|
|
238
|
-
|
|
239
|
-
if task_json_path.is_file():
|
|
240
|
-
data = _read_json_file(task_json_path)
|
|
241
|
-
if data:
|
|
242
|
-
t_name = data.get("name") or data.get("id") or "unknown"
|
|
243
|
-
t_status = data.get("status", "unknown")
|
|
244
|
-
t_created = data.get("createdAt", "unknown")
|
|
245
|
-
t_desc = data.get("description", "")
|
|
246
|
-
|
|
247
|
-
lines.append(f"Name: {t_name}")
|
|
248
|
-
lines.append(f"Status: {t_status}")
|
|
249
|
-
lines.append(f"Created: {t_created}")
|
|
250
|
-
if t_desc:
|
|
251
|
-
lines.append(f"Description: {t_desc}")
|
|
252
|
-
|
|
253
|
-
# Check for prd.md
|
|
254
|
-
prd_file = current_task_dir / "prd.md"
|
|
255
|
-
if prd_file.is_file():
|
|
256
|
-
lines.append("")
|
|
257
|
-
lines.append("[!] This task has prd.md - read it for task details")
|
|
258
|
-
else:
|
|
259
|
-
lines.append("(none)")
|
|
260
|
-
lines.append("")
|
|
261
|
-
|
|
262
|
-
# Active tasks
|
|
263
|
-
lines.append("## ACTIVE TASKS")
|
|
264
|
-
tasks_dir = get_tasks_dir(repo_root)
|
|
265
|
-
task_count = 0
|
|
266
|
-
|
|
267
|
-
# Collect all task data for hierarchy display
|
|
268
|
-
all_task_data: dict[str, dict] = {}
|
|
269
|
-
if tasks_dir.is_dir():
|
|
270
|
-
for d in sorted(tasks_dir.iterdir()):
|
|
271
|
-
if d.is_dir() and d.name != "archive":
|
|
272
|
-
dir_name = d.name
|
|
273
|
-
t_json = d / FILE_TASK_JSON
|
|
274
|
-
status = "unknown"
|
|
275
|
-
assignee = "-"
|
|
276
|
-
children: list[str] = []
|
|
277
|
-
parent: str | None = None
|
|
278
|
-
|
|
279
|
-
if t_json.is_file():
|
|
280
|
-
data = _read_json_file(t_json)
|
|
281
|
-
if data:
|
|
282
|
-
status = data.get("status", "unknown")
|
|
283
|
-
assignee = data.get("assignee", "-")
|
|
284
|
-
children = data.get("children", [])
|
|
285
|
-
parent = data.get("parent")
|
|
286
|
-
|
|
287
|
-
all_task_data[dir_name] = {
|
|
288
|
-
"status": status,
|
|
289
|
-
"assignee": assignee,
|
|
290
|
-
"children": children,
|
|
291
|
-
"parent": parent,
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
def _children_progress(children_list: list[str]) -> str:
|
|
295
|
-
if not children_list:
|
|
296
|
-
return ""
|
|
297
|
-
done = 0
|
|
298
|
-
for c in children_list:
|
|
299
|
-
if c in all_task_data and all_task_data[c]["status"] in ("completed", "done"):
|
|
300
|
-
done += 1
|
|
301
|
-
return f" [{done}/{len(children_list)} done]"
|
|
302
|
-
|
|
303
|
-
def _print_task_tree(name: str, indent: int = 0) -> None:
|
|
304
|
-
nonlocal task_count
|
|
305
|
-
info = all_task_data[name]
|
|
306
|
-
progress = _children_progress(info["children"]) if info["children"] else ""
|
|
307
|
-
prefix = " " * indent
|
|
308
|
-
lines.append(f"{prefix}- {name}/ ({info['status']}){progress} @{info['assignee']}")
|
|
309
|
-
task_count += 1
|
|
310
|
-
for child in info["children"]:
|
|
311
|
-
if child in all_task_data:
|
|
312
|
-
_print_task_tree(child, indent + 1)
|
|
313
|
-
|
|
314
|
-
for dir_name in sorted(all_task_data.keys()):
|
|
315
|
-
if not all_task_data[dir_name]["parent"]:
|
|
316
|
-
_print_task_tree(dir_name)
|
|
317
|
-
|
|
318
|
-
if task_count == 0:
|
|
319
|
-
lines.append("(no active tasks)")
|
|
320
|
-
lines.append(f"Total: {task_count} active task(s)")
|
|
321
|
-
lines.append("")
|
|
322
|
-
|
|
323
|
-
# My tasks
|
|
324
|
-
lines.append("## MY TASKS (Assigned to me)")
|
|
325
|
-
my_task_count = 0
|
|
326
|
-
|
|
327
|
-
if tasks_dir.is_dir():
|
|
328
|
-
for d in sorted(tasks_dir.iterdir()):
|
|
329
|
-
if d.is_dir() and d.name != "archive":
|
|
330
|
-
t_json = d / FILE_TASK_JSON
|
|
331
|
-
if t_json.is_file():
|
|
332
|
-
data = _read_json_file(t_json)
|
|
333
|
-
if data:
|
|
334
|
-
assignee = data.get("assignee", "")
|
|
335
|
-
status = data.get("status", "planning")
|
|
336
|
-
|
|
337
|
-
if assignee == developer and status != "done":
|
|
338
|
-
title = data.get("title") or data.get("name") or "unknown"
|
|
339
|
-
priority = data.get("priority", "P2")
|
|
340
|
-
children_list = data.get("children", [])
|
|
341
|
-
progress = _children_progress(children_list) if children_list else ""
|
|
342
|
-
lines.append(f"- [{priority}] {title} ({status}){progress}")
|
|
343
|
-
my_task_count += 1
|
|
344
|
-
|
|
345
|
-
if my_task_count == 0:
|
|
346
|
-
lines.append("(no tasks assigned to you)")
|
|
347
|
-
lines.append("")
|
|
348
|
-
|
|
349
|
-
# Journal file
|
|
350
|
-
lines.append("## JOURNAL FILE")
|
|
351
|
-
journal_file = get_active_journal_file(repo_root)
|
|
352
|
-
if journal_file:
|
|
353
|
-
journal_lines = count_lines(journal_file)
|
|
354
|
-
relative = f"{DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/{journal_file.name}"
|
|
355
|
-
lines.append(f"Active file: {relative}")
|
|
356
|
-
lines.append(f"Line count: {journal_lines} / 2000")
|
|
357
|
-
if journal_lines > 1800:
|
|
358
|
-
lines.append("[!] WARNING: Approaching 2000 line limit!")
|
|
359
|
-
else:
|
|
360
|
-
lines.append("No journal file found")
|
|
361
|
-
lines.append("")
|
|
362
|
-
|
|
363
|
-
# Paths
|
|
364
|
-
lines.append("## PATHS")
|
|
365
|
-
lines.append(f"Workspace: {DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/")
|
|
366
|
-
lines.append(f"Tasks: {DIR_WORKFLOW}/{DIR_TASKS}/")
|
|
367
|
-
lines.append(f"Spec: {DIR_WORKFLOW}/{DIR_SPEC}/")
|
|
368
|
-
lines.append("")
|
|
369
|
-
|
|
370
|
-
lines.append("========================================")
|
|
371
|
-
|
|
372
|
-
return "\n".join(lines)
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
def get_context_record_json(repo_root: Path | None = None) -> dict:
|
|
376
|
-
"""Get record-mode context as a dictionary.
|
|
377
|
-
|
|
378
|
-
Focused on: my active tasks, git status, current task.
|
|
379
|
-
"""
|
|
380
|
-
if repo_root is None:
|
|
381
|
-
repo_root = get_repo_root()
|
|
382
|
-
|
|
383
|
-
developer = get_developer(repo_root)
|
|
384
|
-
tasks_dir = get_tasks_dir(repo_root)
|
|
385
|
-
|
|
386
|
-
# Git info
|
|
387
|
-
_, branch_out, _ = _run_git_command(["branch", "--show-current"], cwd=repo_root)
|
|
388
|
-
branch = branch_out.strip() or "unknown"
|
|
389
|
-
|
|
390
|
-
_, status_out, _ = _run_git_command(["status", "--porcelain"], cwd=repo_root)
|
|
391
|
-
git_status_count = len([line for line in status_out.splitlines() if line.strip()])
|
|
392
|
-
|
|
393
|
-
_, log_out, _ = _run_git_command(["log", "--oneline", "-5"], cwd=repo_root)
|
|
394
|
-
commits = []
|
|
395
|
-
for line in log_out.splitlines():
|
|
396
|
-
if line.strip():
|
|
397
|
-
parts = line.split(" ", 1)
|
|
398
|
-
if len(parts) >= 2:
|
|
399
|
-
commits.append({"hash": parts[0], "message": parts[1]})
|
|
400
|
-
|
|
401
|
-
# My tasks
|
|
402
|
-
my_tasks = []
|
|
403
|
-
all_task_statuses: dict[str, str] = {}
|
|
404
|
-
if tasks_dir.is_dir():
|
|
405
|
-
for d in sorted(tasks_dir.iterdir()):
|
|
406
|
-
if d.is_dir() and d.name != "archive":
|
|
407
|
-
t_json = d / FILE_TASK_JSON
|
|
408
|
-
if t_json.is_file():
|
|
409
|
-
data = _read_json_file(t_json)
|
|
410
|
-
if data:
|
|
411
|
-
all_task_statuses[d.name] = data.get("status", "unknown")
|
|
412
|
-
|
|
413
|
-
if tasks_dir.is_dir():
|
|
414
|
-
for d in sorted(tasks_dir.iterdir()):
|
|
415
|
-
if d.is_dir() and d.name != "archive":
|
|
416
|
-
t_json = d / FILE_TASK_JSON
|
|
417
|
-
if t_json.is_file():
|
|
418
|
-
data = _read_json_file(t_json)
|
|
419
|
-
if data and data.get("assignee") == developer:
|
|
420
|
-
children_list = data.get("children", [])
|
|
421
|
-
done = sum(1 for c in children_list if all_task_statuses.get(c) in ("completed", "done"))
|
|
422
|
-
my_tasks.append({
|
|
423
|
-
"dir": d.name,
|
|
424
|
-
"title": data.get("title") or data.get("name") or "unknown",
|
|
425
|
-
"status": data.get("status", "unknown"),
|
|
426
|
-
"priority": data.get("priority", "P2"),
|
|
427
|
-
"children": children_list,
|
|
428
|
-
"childrenDone": done,
|
|
429
|
-
"parent": data.get("parent"),
|
|
430
|
-
"meta": data.get("meta", {}),
|
|
431
|
-
})
|
|
432
|
-
|
|
433
|
-
# Current task
|
|
434
|
-
current_task_info = None
|
|
435
|
-
current_task = get_current_task(repo_root)
|
|
436
|
-
if current_task:
|
|
437
|
-
task_json_path = (repo_root / current_task) / FILE_TASK_JSON
|
|
438
|
-
if task_json_path.is_file():
|
|
439
|
-
data = _read_json_file(task_json_path)
|
|
440
|
-
if data:
|
|
441
|
-
current_task_info = {
|
|
442
|
-
"path": current_task,
|
|
443
|
-
"name": data.get("name") or data.get("id") or "unknown",
|
|
444
|
-
"status": data.get("status", "unknown"),
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
return {
|
|
448
|
-
"developer": developer or "",
|
|
449
|
-
"git": {
|
|
450
|
-
"branch": branch,
|
|
451
|
-
"isClean": git_status_count == 0,
|
|
452
|
-
"uncommittedChanges": git_status_count,
|
|
453
|
-
"recentCommits": commits,
|
|
454
|
-
},
|
|
455
|
-
"myTasks": my_tasks,
|
|
456
|
-
"currentTask": current_task_info,
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
def get_context_text_record(repo_root: Path | None = None) -> str:
|
|
461
|
-
"""Get context as formatted text for record-session mode.
|
|
462
|
-
|
|
463
|
-
Focused output: MY ACTIVE TASKS first (with [!!!] emphasis),
|
|
464
|
-
then GIT STATUS, RECENT COMMITS, CURRENT TASK.
|
|
465
|
-
|
|
466
|
-
Args:
|
|
467
|
-
repo_root: Repository root path. Defaults to auto-detected.
|
|
468
|
-
|
|
469
|
-
Returns:
|
|
470
|
-
Formatted text output for record-session.
|
|
471
|
-
"""
|
|
472
|
-
if repo_root is None:
|
|
473
|
-
repo_root = get_repo_root()
|
|
474
|
-
|
|
475
|
-
lines: list[str] = []
|
|
476
|
-
lines.append("========================================")
|
|
477
|
-
lines.append("SESSION CONTEXT (RECORD MODE)")
|
|
478
|
-
lines.append("========================================")
|
|
479
|
-
lines.append("")
|
|
480
|
-
|
|
481
|
-
developer = get_developer(repo_root)
|
|
482
|
-
if not developer:
|
|
483
|
-
lines.append(
|
|
484
|
-
f"ERROR: Not initialized. Run: python3 ./{DIR_WORKFLOW}/{DIR_SCRIPTS}/init_developer.py <name>"
|
|
485
|
-
)
|
|
486
|
-
return "\n".join(lines)
|
|
487
|
-
|
|
488
|
-
# MY ACTIVE TASKS — first and prominent
|
|
489
|
-
lines.append(f"## [!!!] MY ACTIVE TASKS (Assigned to {developer})")
|
|
490
|
-
lines.append("[!] Review whether any should be archived before recording this session.")
|
|
491
|
-
lines.append("")
|
|
492
|
-
|
|
493
|
-
tasks_dir = get_tasks_dir(repo_root)
|
|
494
|
-
my_task_count = 0
|
|
495
|
-
|
|
496
|
-
# Collect task data for children progress
|
|
497
|
-
all_task_statuses: dict[str, str] = {}
|
|
498
|
-
if tasks_dir.is_dir():
|
|
499
|
-
for d in sorted(tasks_dir.iterdir()):
|
|
500
|
-
if d.is_dir() and d.name != "archive":
|
|
501
|
-
t_json = d / FILE_TASK_JSON
|
|
502
|
-
if t_json.is_file():
|
|
503
|
-
data = _read_json_file(t_json)
|
|
504
|
-
if data:
|
|
505
|
-
all_task_statuses[d.name] = data.get("status", "unknown")
|
|
506
|
-
|
|
507
|
-
def _record_children_progress(children_list: list[str]) -> str:
|
|
508
|
-
if not children_list:
|
|
509
|
-
return ""
|
|
510
|
-
done = 0
|
|
511
|
-
for c in children_list:
|
|
512
|
-
if all_task_statuses.get(c) in ("completed", "done"):
|
|
513
|
-
done += 1
|
|
514
|
-
return f" [{done}/{len(children_list)} done]"
|
|
515
|
-
|
|
516
|
-
if tasks_dir.is_dir():
|
|
517
|
-
for d in sorted(tasks_dir.iterdir()):
|
|
518
|
-
if d.is_dir() and d.name != "archive":
|
|
519
|
-
t_json = d / FILE_TASK_JSON
|
|
520
|
-
if t_json.is_file():
|
|
521
|
-
data = _read_json_file(t_json)
|
|
522
|
-
if data:
|
|
523
|
-
assignee = data.get("assignee", "")
|
|
524
|
-
status = data.get("status", "planning")
|
|
525
|
-
|
|
526
|
-
if assignee == developer:
|
|
527
|
-
title = data.get("title") or data.get("name") or "unknown"
|
|
528
|
-
priority = data.get("priority", "P2")
|
|
529
|
-
children_list = data.get("children", [])
|
|
530
|
-
progress = _record_children_progress(children_list) if children_list else ""
|
|
531
|
-
lines.append(f"- [{priority}] {title} ({status}){progress} — {d.name}")
|
|
532
|
-
my_task_count += 1
|
|
533
|
-
|
|
534
|
-
if my_task_count == 0:
|
|
535
|
-
lines.append("(no active tasks assigned to you)")
|
|
536
|
-
lines.append("")
|
|
537
|
-
|
|
538
|
-
# GIT STATUS
|
|
539
|
-
lines.append("## GIT STATUS")
|
|
540
|
-
_, branch_out, _ = _run_git_command(["branch", "--show-current"], cwd=repo_root)
|
|
541
|
-
branch = branch_out.strip() or "unknown"
|
|
542
|
-
lines.append(f"Branch: {branch}")
|
|
543
|
-
|
|
544
|
-
_, status_out, _ = _run_git_command(["status", "--porcelain"], cwd=repo_root)
|
|
545
|
-
status_lines = [line for line in status_out.splitlines() if line.strip()]
|
|
546
|
-
status_count = len(status_lines)
|
|
547
|
-
|
|
548
|
-
if status_count == 0:
|
|
549
|
-
lines.append("Working directory: Clean")
|
|
550
|
-
else:
|
|
551
|
-
lines.append(f"Working directory: {status_count} uncommitted change(s)")
|
|
552
|
-
lines.append("")
|
|
553
|
-
lines.append("Changes:")
|
|
554
|
-
_, short_out, _ = _run_git_command(["status", "--short"], cwd=repo_root)
|
|
555
|
-
for line in short_out.splitlines()[:10]:
|
|
556
|
-
lines.append(line)
|
|
557
|
-
lines.append("")
|
|
558
|
-
|
|
559
|
-
# RECENT COMMITS
|
|
560
|
-
lines.append("## RECENT COMMITS")
|
|
561
|
-
_, log_out, _ = _run_git_command(["log", "--oneline", "-5"], cwd=repo_root)
|
|
562
|
-
if log_out.strip():
|
|
563
|
-
for line in log_out.splitlines():
|
|
564
|
-
lines.append(line)
|
|
565
|
-
else:
|
|
566
|
-
lines.append("(no commits)")
|
|
567
|
-
lines.append("")
|
|
568
|
-
|
|
569
|
-
# CURRENT TASK
|
|
570
|
-
lines.append("## CURRENT TASK")
|
|
571
|
-
current_task = get_current_task(repo_root)
|
|
572
|
-
if current_task:
|
|
573
|
-
current_task_dir = repo_root / current_task
|
|
574
|
-
task_json_path = current_task_dir / FILE_TASK_JSON
|
|
575
|
-
lines.append(f"Path: {current_task}")
|
|
576
|
-
|
|
577
|
-
if task_json_path.is_file():
|
|
578
|
-
data = _read_json_file(task_json_path)
|
|
579
|
-
if data:
|
|
580
|
-
t_name = data.get("name") or data.get("id") or "unknown"
|
|
581
|
-
t_status = data.get("status", "unknown")
|
|
582
|
-
lines.append(f"Name: {t_name}")
|
|
583
|
-
lines.append(f"Status: {t_status}")
|
|
584
|
-
else:
|
|
585
|
-
lines.append("(none)")
|
|
586
|
-
lines.append("")
|
|
587
|
-
|
|
588
|
-
lines.append("========================================")
|
|
589
|
-
|
|
590
|
-
return "\n".join(lines)
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
def output_text(repo_root: Path | None = None) -> None:
|
|
594
|
-
"""Output context in text format.
|
|
595
|
-
|
|
596
|
-
Args:
|
|
597
|
-
repo_root: Repository root path. Defaults to auto-detected.
|
|
598
|
-
"""
|
|
599
|
-
print(get_context_text(repo_root))
|
|
31
|
+
# Backward-compatible alias — external modules import this name
|
|
32
|
+
_run_git_command = run_git
|
|
600
33
|
|
|
601
34
|
|
|
602
35
|
# =============================================================================
|
|
603
36
|
# Main Entry
|
|
604
37
|
# =============================================================================
|
|
605
38
|
|
|
606
|
-
|
|
607
39
|
def main() -> None:
|
|
608
40
|
"""CLI entry point."""
|
|
609
41
|
import argparse
|
|
@@ -618,9 +50,9 @@ def main() -> None:
|
|
|
618
50
|
parser.add_argument(
|
|
619
51
|
"--mode",
|
|
620
52
|
"-m",
|
|
621
|
-
choices=["default", "record"],
|
|
53
|
+
choices=["default", "record", "packages"],
|
|
622
54
|
default="default",
|
|
623
|
-
help="Output mode: default (full context)
|
|
55
|
+
help="Output mode: default (full context), record (for record-session), packages (package info only)",
|
|
624
56
|
)
|
|
625
57
|
|
|
626
58
|
args = parser.parse_args()
|
|
@@ -630,6 +62,11 @@ def main() -> None:
|
|
|
630
62
|
print(json.dumps(get_context_record_json(), indent=2, ensure_ascii=False))
|
|
631
63
|
else:
|
|
632
64
|
print(get_context_text_record())
|
|
65
|
+
elif args.mode == "packages":
|
|
66
|
+
if args.json:
|
|
67
|
+
print(json.dumps(get_context_packages_json(), indent=2, ensure_ascii=False))
|
|
68
|
+
else:
|
|
69
|
+
print(get_context_packages_text())
|
|
633
70
|
else:
|
|
634
71
|
if args.json:
|
|
635
72
|
output_json()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JSON file I/O utilities.
|
|
3
|
+
|
|
4
|
+
Provides read_json and write_json as the single source of truth
|
|
5
|
+
for JSON file operations across all Trellis scripts.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def read_json(path: Path) -> dict | None:
|
|
15
|
+
"""Read and parse a JSON file.
|
|
16
|
+
|
|
17
|
+
Returns None if the file doesn't exist, is invalid JSON, or can't be read.
|
|
18
|
+
"""
|
|
19
|
+
try:
|
|
20
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
21
|
+
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def write_json(path: Path, data: dict) -> bool:
|
|
26
|
+
"""Write dict to JSON file with pretty formatting.
|
|
27
|
+
|
|
28
|
+
Returns True on success, False on error.
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
path.write_text(
|
|
32
|
+
json.dumps(data, indent=2, ensure_ascii=False),
|
|
33
|
+
encoding="utf-8",
|
|
34
|
+
)
|
|
35
|
+
return True
|
|
36
|
+
except (OSError, IOError):
|
|
37
|
+
return False
|