@mindfoldhq/trellis 0.3.10-beta.0 → 0.3.10
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 +0 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/init.d.ts +0 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +31 -203
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +6 -154
- package/dist/commands/update.js.map +1 -1
- package/dist/configurators/workflow.d.ts +2 -6
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +58 -88
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/migrations/index.d.ts +0 -1
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +0 -2
- package/dist/migrations/index.js.map +1 -1
- package/dist/migrations/manifests/0.3.10.json +9 -0
- package/dist/templates/claude/agents/dispatch.md +2 -1
- package/dist/templates/claude/agents/implement.md +3 -2
- package/dist/templates/claude/commands/trellis/before-backend-dev.md +13 -0
- package/dist/templates/claude/commands/trellis/before-frontend-dev.md +13 -0
- package/dist/templates/claude/commands/trellis/check-backend.md +13 -0
- package/dist/templates/claude/commands/trellis/check-frontend.md +13 -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 +2 -1
- package/dist/templates/claude/commands/trellis/record-session.md +2 -2
- package/dist/templates/claude/commands/trellis/start.md +4 -8
- package/dist/templates/claude/hooks/inject-subagent-context.py +13 -21
- package/dist/templates/claude/hooks/session-start.py +2 -170
- package/dist/templates/codex/skills/before-backend-dev/SKILL.md +18 -0
- package/dist/templates/codex/skills/before-frontend-dev/SKILL.md +18 -0
- package/dist/templates/codex/skills/check-backend/SKILL.md +18 -0
- package/dist/templates/codex/skills/check-frontend/SKILL.md +18 -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 +2 -2
- package/dist/templates/codex/skills/start/SKILL.md +3 -8
- package/dist/templates/cursor/commands/trellis-before-backend-dev.md +13 -0
- package/dist/templates/cursor/commands/trellis-before-frontend-dev.md +13 -0
- package/dist/templates/cursor/commands/trellis-check-backend.md +13 -0
- package/dist/templates/cursor/commands/trellis-check-frontend.md +13 -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 +2 -2
- package/dist/templates/cursor/commands/trellis-start.md +16 -7
- package/dist/templates/gemini/commands/trellis/before-backend-dev.toml +17 -0
- package/dist/templates/gemini/commands/trellis/before-frontend-dev.toml +17 -0
- package/dist/templates/gemini/commands/trellis/check-backend.toml +17 -0
- package/dist/templates/gemini/commands/trellis/check-frontend.toml +17 -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 +2 -2
- package/dist/templates/gemini/commands/trellis/start.toml +4 -9
- package/dist/templates/iflow/agents/dispatch.md +2 -1
- package/dist/templates/iflow/agents/implement.md +3 -2
- package/dist/templates/iflow/commands/trellis/before-backend-dev.md +13 -0
- package/dist/templates/iflow/commands/trellis/before-frontend-dev.md +13 -0
- package/dist/templates/iflow/commands/trellis/check-backend.md +13 -0
- package/dist/templates/iflow/commands/trellis/check-frontend.md +13 -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 +2 -1
- package/dist/templates/iflow/commands/trellis/record-session.md +2 -2
- package/dist/templates/iflow/commands/trellis/start.md +4 -8
- package/dist/templates/iflow/hooks/inject-subagent-context.py +13 -21
- package/dist/templates/iflow/hooks/session-start.py +1 -156
- package/dist/templates/kilo/workflows/before-backend-dev.md +13 -0
- package/dist/templates/kilo/workflows/before-frontend-dev.md +13 -0
- package/dist/templates/kilo/workflows/check-backend.md +13 -0
- package/dist/templates/kilo/workflows/check-frontend.md +13 -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 +2 -1
- package/dist/templates/kilo/workflows/record-session.md +2 -2
- package/dist/templates/kilo/workflows/start.md +3 -8
- package/dist/templates/kiro/skills/before-backend-dev/SKILL.md +18 -0
- package/dist/templates/kiro/skills/before-frontend-dev/SKILL.md +18 -0
- package/dist/templates/kiro/skills/check-backend/SKILL.md +18 -0
- package/dist/templates/kiro/skills/check-frontend/SKILL.md +18 -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 +2 -2
- package/dist/templates/kiro/skills/start/SKILL.md +3 -8
- package/dist/templates/markdown/spec/backend/script-conventions.md +0 -93
- package/dist/templates/opencode/agents/dispatch.md +2 -1
- package/dist/templates/opencode/agents/implement.md +2 -2
- package/dist/templates/opencode/agents/research.md +2 -1
- package/dist/templates/opencode/commands/trellis/before-backend-dev.md +13 -0
- package/dist/templates/opencode/commands/trellis/before-frontend-dev.md +13 -0
- package/dist/templates/opencode/commands/trellis/check-backend.md +13 -0
- package/dist/templates/opencode/commands/trellis/check-frontend.md +13 -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 +2 -1
- package/dist/templates/opencode/commands/trellis/record-session.md +2 -2
- package/dist/templates/opencode/commands/trellis/start.md +3 -8
- package/dist/templates/opencode/plugin/inject-subagent-context.js +18 -45
- package/dist/templates/opencode/plugin/session-start.js +1 -149
- package/dist/templates/qoder/skills/before-backend-dev/SKILL.md +18 -0
- package/dist/templates/qoder/skills/before-frontend-dev/SKILL.md +18 -0
- package/dist/templates/qoder/skills/check-backend/SKILL.md +18 -0
- package/dist/templates/qoder/skills/check-frontend/SKILL.md +18 -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 +2 -2
- package/dist/templates/qoder/skills/start/SKILL.md +3 -8
- package/dist/templates/trellis/config.yaml +0 -20
- package/dist/templates/trellis/index.d.ts +0 -11
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +0 -22
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/add_session.py +7 -52
- package/dist/templates/trellis/scripts/common/cli_adapter.py +45 -33
- package/dist/templates/trellis/scripts/common/config.py +0 -152
- package/dist/templates/trellis/scripts/common/git_context.py +586 -23
- package/dist/templates/trellis/scripts/common/paths.py +0 -46
- package/dist/templates/trellis/scripts/common/phase.py +49 -50
- package/dist/templates/trellis/scripts/common/registry.py +72 -41
- package/dist/templates/trellis/scripts/common/task_queue.py +98 -27
- package/dist/templates/trellis/scripts/common/task_utils.py +6 -96
- package/dist/templates/trellis/scripts/create_bootstrap.py +26 -31
- package/dist/templates/trellis/scripts/multi_agent/cleanup.py +48 -43
- package/dist/templates/trellis/scripts/multi_agent/create_pr.py +45 -336
- package/dist/templates/trellis/scripts/multi_agent/plan.py +26 -2
- package/dist/templates/trellis/scripts/multi_agent/start.py +57 -126
- package/dist/templates/trellis/scripts/multi_agent/status.py +753 -12
- package/dist/templates/trellis/scripts/task.py +975 -50
- package/dist/templates/trellis/workflow.md +34 -21
- package/dist/types/migration.d.ts +1 -3
- package/dist/types/migration.d.ts.map +1 -1
- package/dist/utils/project-detector.d.ts +0 -23
- package/dist/utils/project-detector.d.ts.map +1 -1
- package/dist/utils/project-detector.js +0 -364
- package/dist/utils/project-detector.js.map +1 -1
- package/dist/utils/template-fetcher.d.ts +10 -2
- package/dist/utils/template-fetcher.d.ts.map +1 -1
- package/dist/utils/template-fetcher.js +43 -12
- package/dist/utils/template-fetcher.js.map +1 -1
- package/package.json +1 -1
- package/dist/migrations/manifests/0.4.0-beta.1.json +0 -228
- package/dist/templates/claude/commands/trellis/before-dev.md +0 -29
- package/dist/templates/claude/commands/trellis/check.md +0 -25
- package/dist/templates/codex/skills/before-dev/SKILL.md +0 -34
- package/dist/templates/codex/skills/check/SKILL.md +0 -30
- package/dist/templates/cursor/commands/trellis-before-dev.md +0 -29
- package/dist/templates/cursor/commands/trellis-check.md +0 -25
- package/dist/templates/gemini/commands/trellis/before-dev.toml +0 -33
- package/dist/templates/gemini/commands/trellis/check.toml +0 -29
- package/dist/templates/iflow/commands/trellis/before-dev.md +0 -29
- package/dist/templates/iflow/commands/trellis/check.md +0 -25
- package/dist/templates/kilo/workflows/before-dev.md +0 -29
- package/dist/templates/kilo/workflows/check.md +0 -25
- package/dist/templates/kiro/skills/before-dev/SKILL.md +0 -34
- package/dist/templates/kiro/skills/check/SKILL.md +0 -30
- package/dist/templates/opencode/commands/trellis/before-dev.md +0 -29
- package/dist/templates/opencode/commands/trellis/check.md +0 -25
- package/dist/templates/qoder/skills/before-dev/SKILL.md +0 -34
- package/dist/templates/qoder/skills/check/SKILL.md +0 -30
- package/dist/templates/trellis/scripts/common/git.py +0 -31
- package/dist/templates/trellis/scripts/common/io.py +0 -37
- package/dist/templates/trellis/scripts/common/log.py +0 -45
- package/dist/templates/trellis/scripts/common/packages_context.py +0 -233
- package/dist/templates/trellis/scripts/common/session_context.py +0 -466
- package/dist/templates/trellis/scripts/common/task_context.py +0 -384
- package/dist/templates/trellis/scripts/common/task_store.py +0 -534
- package/dist/templates/trellis/scripts/common/tasks.py +0 -109
- package/dist/templates/trellis/scripts/common/types.py +0 -112
- package/dist/templates/trellis/scripts/hooks/linear_sync.py +0 -243
- package/dist/templates/trellis/scripts/multi_agent/_bootstrap.py +0 -17
- package/dist/templates/trellis/scripts/multi_agent/status_display.py +0 -542
- package/dist/templates/trellis/scripts/multi_agent/status_monitor.py +0 -225
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
"""
|
|
4
4
|
Git and Session Context utilities.
|
|
5
5
|
|
|
6
|
-
Entry shim — delegates to session_context and packages_context.
|
|
7
|
-
|
|
8
6
|
Provides:
|
|
9
7
|
output_json - Output context in JSON format
|
|
10
8
|
output_text - Output context in text format
|
|
@@ -13,29 +11,599 @@ Provides:
|
|
|
13
11
|
from __future__ import annotations
|
|
14
12
|
|
|
15
13
|
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
|
-
|
|
17
|
+
from .paths import (
|
|
18
|
+
DIR_SCRIPTS,
|
|
19
|
+
DIR_SPEC,
|
|
20
|
+
DIR_TASKS,
|
|
21
|
+
DIR_WORKFLOW,
|
|
22
|
+
DIR_WORKSPACE,
|
|
23
|
+
FILE_TASK_JSON,
|
|
24
|
+
count_lines,
|
|
25
|
+
get_active_journal_file,
|
|
26
|
+
get_current_task,
|
|
27
|
+
get_developer,
|
|
28
|
+
get_repo_root,
|
|
29
|
+
get_tasks_dir,
|
|
29
30
|
)
|
|
30
31
|
|
|
31
|
-
#
|
|
32
|
-
|
|
32
|
+
# =============================================================================
|
|
33
|
+
# Helper Functions
|
|
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))
|
|
33
600
|
|
|
34
601
|
|
|
35
602
|
# =============================================================================
|
|
36
603
|
# Main Entry
|
|
37
604
|
# =============================================================================
|
|
38
605
|
|
|
606
|
+
|
|
39
607
|
def main() -> None:
|
|
40
608
|
"""CLI entry point."""
|
|
41
609
|
import argparse
|
|
@@ -50,9 +618,9 @@ def main() -> None:
|
|
|
50
618
|
parser.add_argument(
|
|
51
619
|
"--mode",
|
|
52
620
|
"-m",
|
|
53
|
-
choices=["default", "record"
|
|
621
|
+
choices=["default", "record"],
|
|
54
622
|
default="default",
|
|
55
|
-
help="Output mode: default (full context)
|
|
623
|
+
help="Output mode: default (full context) or record (for record-session)",
|
|
56
624
|
)
|
|
57
625
|
|
|
58
626
|
args = parser.parse_args()
|
|
@@ -62,11 +630,6 @@ def main() -> None:
|
|
|
62
630
|
print(json.dumps(get_context_record_json(), indent=2, ensure_ascii=False))
|
|
63
631
|
else:
|
|
64
632
|
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())
|
|
70
633
|
else:
|
|
71
634
|
if args.json:
|
|
72
635
|
output_json()
|
|
@@ -333,52 +333,6 @@ def generate_task_date_prefix() -> str:
|
|
|
333
333
|
return datetime.now().strftime("%m-%d")
|
|
334
334
|
|
|
335
335
|
|
|
336
|
-
# =============================================================================
|
|
337
|
-
# Monorepo / Package Paths
|
|
338
|
-
# =============================================================================
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
def get_spec_dir(package: str | None = None, repo_root: Path | None = None) -> Path:
|
|
342
|
-
"""Get the spec directory path.
|
|
343
|
-
|
|
344
|
-
Single-repo: .trellis/spec
|
|
345
|
-
Monorepo with package: .trellis/spec/<package>
|
|
346
|
-
|
|
347
|
-
Uses lazy import to avoid circular dependency with config.py.
|
|
348
|
-
"""
|
|
349
|
-
if repo_root is None:
|
|
350
|
-
repo_root = get_repo_root()
|
|
351
|
-
|
|
352
|
-
from .config import get_spec_base
|
|
353
|
-
|
|
354
|
-
base = get_spec_base(package, repo_root)
|
|
355
|
-
return repo_root / DIR_WORKFLOW / base
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
def get_package_path(package: str, repo_root: Path | None = None) -> Path | None:
|
|
359
|
-
"""Get a package's source directory absolute path from config.
|
|
360
|
-
|
|
361
|
-
Returns:
|
|
362
|
-
Absolute path to the package directory, or None if not found.
|
|
363
|
-
"""
|
|
364
|
-
if repo_root is None:
|
|
365
|
-
repo_root = get_repo_root()
|
|
366
|
-
|
|
367
|
-
from .config import get_packages
|
|
368
|
-
|
|
369
|
-
packages = get_packages(repo_root)
|
|
370
|
-
if not packages or package not in packages:
|
|
371
|
-
return None
|
|
372
|
-
|
|
373
|
-
info = packages[package]
|
|
374
|
-
if isinstance(info, dict):
|
|
375
|
-
rel_path = info.get("path", package)
|
|
376
|
-
else:
|
|
377
|
-
rel_path = str(info)
|
|
378
|
-
|
|
379
|
-
return repo_root / rel_path
|
|
380
|
-
|
|
381
|
-
|
|
382
336
|
# =============================================================================
|
|
383
337
|
# Main Entry (for testing)
|
|
384
338
|
# =============================================================================
|