@mindfoldhq/trellis 0.3.10 → 0.4.0-beta.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/README.md +19 -5
- package/dist/cli/index.js +5 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +240 -43
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +206 -47
- package/dist/commands/update.js.map +1 -1
- package/dist/configurators/codebuddy.d.ts +11 -0
- package/dist/configurators/codebuddy.d.ts.map +1 -0
- package/dist/configurators/codebuddy.js +58 -0
- package/dist/configurators/codebuddy.js.map +1 -0
- package/dist/configurators/codex.d.ts +7 -4
- package/dist/configurators/codex.d.ts.map +1 -1
- package/dist/configurators/codex.js +40 -10
- package/dist/configurators/codex.js.map +1 -1
- package/dist/configurators/copilot.d.ts +9 -0
- package/dist/configurators/copilot.d.ts.map +1 -0
- package/dist/configurators/copilot.js +34 -0
- package/dist/configurators/copilot.js.map +1 -0
- package/dist/configurators/index.d.ts +11 -1
- package/dist/configurators/index.d.ts.map +1 -1
- package/dist/configurators/index.js +72 -4
- package/dist/configurators/index.js.map +1 -1
- package/dist/configurators/opencode.d.ts +1 -1
- package/dist/configurators/opencode.js +1 -1
- package/dist/configurators/windsurf.d.ts +8 -0
- package/dist/configurators/windsurf.d.ts.map +1 -0
- package/dist/configurators/windsurf.js +18 -0
- package/dist/configurators/windsurf.js.map +1 -0
- package/dist/configurators/workflow.d.ts +6 -2
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +90 -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.4.0-beta.1.json +228 -0
- package/dist/migrations/manifests/0.4.0-beta.10.json +9 -0
- package/dist/migrations/manifests/0.4.0-beta.2.json +9 -0
- package/dist/migrations/manifests/0.4.0-beta.3.json +9 -0
- package/dist/migrations/manifests/0.4.0-beta.4.json +9 -0
- package/dist/migrations/manifests/0.4.0-beta.5.json +9 -0
- package/dist/migrations/manifests/0.4.0-beta.6.json +9 -0
- package/dist/migrations/manifests/0.4.0-beta.7.json +9 -0
- package/dist/migrations/manifests/0.4.0-beta.8.json +34 -0
- package/dist/migrations/manifests/0.4.0-beta.9.json +9 -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 +3 -2
- package/dist/templates/claude/commands/trellis/start.md +8 -4
- package/dist/templates/claude/hooks/inject-subagent-context.py +29 -14
- package/dist/templates/claude/hooks/ralph-loop.py +18 -10
- package/dist/templates/claude/hooks/session-start.py +201 -9
- package/dist/templates/claude/hooks/statusline.py +211 -0
- package/dist/templates/claude/settings.json +4 -0
- package/dist/templates/codebuddy/commands/trellis/before-dev.md +29 -0
- package/dist/templates/codebuddy/commands/trellis/brainstorm.md +487 -0
- package/dist/templates/codebuddy/commands/trellis/break-loop.md +107 -0
- package/dist/templates/codebuddy/commands/trellis/check-cross-layer.md +153 -0
- package/dist/templates/codebuddy/commands/trellis/check.md +25 -0
- package/dist/templates/codebuddy/commands/trellis/create-command.md +154 -0
- package/dist/templates/codebuddy/commands/trellis/finish-work.md +143 -0
- package/dist/templates/codebuddy/commands/trellis/integrate-skill.md +219 -0
- package/dist/templates/codebuddy/commands/trellis/onboard.md +358 -0
- package/dist/templates/codebuddy/commands/trellis/record-session.md +61 -0
- package/dist/templates/codebuddy/commands/trellis/start.md +373 -0
- package/dist/templates/codebuddy/commands/trellis/update-spec.md +354 -0
- package/dist/templates/codebuddy/index.d.ts +25 -0
- package/dist/templates/codebuddy/index.d.ts.map +1 -0
- package/dist/templates/codebuddy/index.js +45 -0
- package/dist/templates/codebuddy/index.js.map +1 -0
- package/dist/templates/codex/agents/check.toml +23 -0
- package/dist/templates/codex/agents/implement.toml +19 -0
- package/dist/templates/codex/agents/research.toml +26 -0
- package/dist/templates/codex/codex-skills/parallel/SKILL.md +194 -0
- package/dist/templates/codex/config.toml +5 -0
- package/dist/templates/codex/hooks/session-start.py +228 -0
- package/dist/templates/codex/hooks.json +16 -0
- package/dist/templates/codex/index.d.ts +27 -5
- package/dist/templates/codex/index.d.ts.map +1 -1
- package/dist/templates/codex/index.js +60 -8
- package/dist/templates/codex/index.js.map +1 -1
- package/dist/templates/codex/skills/before-dev/SKILL.md +34 -0
- package/dist/templates/codex/skills/brainstorm/SKILL.md +1 -1
- package/dist/templates/codex/skills/break-loop/SKILL.md +1 -1
- package/dist/templates/codex/skills/check/SKILL.md +30 -0
- package/dist/templates/codex/skills/check-cross-layer/SKILL.md +1 -1
- package/dist/templates/codex/skills/create-command/SKILL.md +3 -3
- package/dist/templates/codex/skills/finish-work/SKILL.md +1 -1
- package/dist/templates/codex/skills/improve-ut/SKILL.md +69 -0
- package/dist/templates/codex/skills/integrate-skill/SKILL.md +1 -1
- package/dist/templates/codex/skills/onboard/SKILL.md +12 -12
- package/dist/templates/codex/skills/record-session/SKILL.md +4 -3
- package/dist/templates/codex/skills/start/SKILL.md +9 -4
- package/dist/templates/codex/skills/update-spec/SKILL.md +1 -1
- package/dist/templates/copilot/hooks/session-start.py +218 -0
- package/dist/templates/copilot/hooks.json +11 -0
- package/dist/templates/copilot/index.d.ts +23 -0
- package/dist/templates/copilot/index.d.ts.map +1 -0
- package/dist/templates/copilot/index.js +54 -0
- package/dist/templates/copilot/index.js.map +1 -0
- package/dist/templates/copilot/prompts/before-dev.prompt.md +33 -0
- package/dist/templates/copilot/prompts/brainstorm.prompt.md +491 -0
- package/dist/templates/copilot/prompts/break-loop.prompt.md +129 -0
- package/dist/templates/copilot/prompts/check-cross-layer.prompt.md +157 -0
- package/dist/templates/copilot/prompts/check.prompt.md +29 -0
- package/dist/templates/copilot/prompts/create-command.prompt.md +116 -0
- package/dist/templates/copilot/prompts/finish-work.prompt.md +157 -0
- package/dist/templates/copilot/prompts/integrate-skill.prompt.md +223 -0
- package/dist/templates/copilot/prompts/onboard.prompt.md +362 -0
- package/dist/templates/copilot/prompts/parallel.prompt.md +196 -0
- package/dist/templates/copilot/prompts/record-session.prompt.md +66 -0
- package/dist/templates/copilot/prompts/start.prompt.md +397 -0
- package/dist/templates/copilot/prompts/update-spec.prompt.md +358 -0
- 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 +3 -2
- package/dist/templates/cursor/commands/trellis-start.md +7 -16
- package/dist/templates/extract.d.ts +36 -0
- package/dist/templates/extract.d.ts.map +1 -1
- package/dist/templates/extract.js +64 -0
- package/dist/templates/extract.js.map +1 -1
- 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 +3 -2
- 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 +3 -2
- package/dist/templates/iflow/commands/trellis/start.md +8 -4
- package/dist/templates/iflow/hooks/inject-subagent-context.py +29 -14
- package/dist/templates/iflow/hooks/ralph-loop.py +8 -1
- package/dist/templates/iflow/hooks/session-start.py +187 -8
- 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 +3 -2
- 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/brainstorm/SKILL.md +1 -1
- package/dist/templates/kiro/skills/break-loop/SKILL.md +1 -1
- package/dist/templates/kiro/skills/check/SKILL.md +30 -0
- package/dist/templates/kiro/skills/check-cross-layer/SKILL.md +1 -1
- package/dist/templates/kiro/skills/create-command/SKILL.md +3 -3
- package/dist/templates/kiro/skills/finish-work/SKILL.md +1 -1
- package/dist/templates/kiro/skills/integrate-skill/SKILL.md +1 -1
- package/dist/templates/kiro/skills/onboard/SKILL.md +12 -12
- package/dist/templates/kiro/skills/record-session/SKILL.md +4 -3
- package/dist/templates/kiro/skills/start/SKILL.md +9 -4
- package/dist/templates/kiro/skills/update-spec/SKILL.md +1 -1
- package/dist/templates/markdown/agents.md +4 -0
- package/dist/templates/markdown/spec/backend/directory-structure.md +1 -1
- package/dist/templates/markdown/spec/backend/script-conventions.md +93 -0
- package/dist/templates/markdown/workspace-index.md +2 -0
- package/dist/templates/opencode/agents/dispatch.md +21 -21
- 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 +3 -2
- package/dist/templates/opencode/commands/trellis/start.md +8 -3
- package/dist/templates/opencode/lib/trellis-context.js +42 -2
- package/dist/templates/opencode/{plugin → plugins}/inject-subagent-context.js +45 -18
- package/dist/templates/opencode/{plugin → plugins}/session-start.js +156 -28
- package/dist/templates/qoder/skills/before-dev/SKILL.md +34 -0
- package/dist/templates/qoder/skills/brainstorm/SKILL.md +1 -1
- package/dist/templates/qoder/skills/break-loop/SKILL.md +1 -1
- package/dist/templates/qoder/skills/check/SKILL.md +30 -0
- package/dist/templates/qoder/skills/check-cross-layer/SKILL.md +1 -1
- package/dist/templates/qoder/skills/create-command/SKILL.md +3 -3
- package/dist/templates/qoder/skills/finish-work/SKILL.md +1 -1
- package/dist/templates/qoder/skills/integrate-skill/SKILL.md +1 -1
- package/dist/templates/qoder/skills/onboard/SKILL.md +14 -14
- package/dist/templates/qoder/skills/record-session/SKILL.md +4 -3
- package/dist/templates/qoder/skills/start/SKILL.md +9 -4
- package/dist/templates/qoder/skills/update-spec/SKILL.md +1 -1
- 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 +111 -13
- package/dist/templates/trellis/scripts/common/__init__.py +2 -0
- package/dist/templates/trellis/scripts/common/cli_adapter.py +164 -64
- package/dist/templates/trellis/scripts/common/config.py +192 -0
- package/dist/templates/trellis/scripts/common/developer.py +2 -2
- 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 +238 -0
- package/dist/templates/trellis/scripts/common/paths.py +103 -6
- 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 +562 -0
- package/dist/templates/trellis/scripts/common/task_context.py +410 -0
- package/dist/templates/trellis/scripts/common/task_queue.py +27 -98
- package/dist/templates/trellis/scripts/common/task_store.py +536 -0
- package/dist/templates/trellis/scripts/common/task_utils.py +106 -10
- 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 +32 -27
- 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 +9 -32
- package/dist/templates/trellis/scripts/multi_agent/start.py +142 -68
- 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 +51 -976
- package/dist/templates/trellis/scripts-shell-archive/create-bootstrap.sh +1 -1
- package/dist/templates/trellis/workflow.md +38 -38
- package/dist/templates/windsurf/index.d.ts +21 -0
- package/dist/templates/windsurf/index.d.ts.map +1 -0
- package/dist/templates/windsurf/index.js +44 -0
- package/dist/templates/windsurf/index.js.map +1 -0
- package/dist/templates/windsurf/workflows/trellis-before-dev.md +31 -0
- package/dist/templates/windsurf/workflows/trellis-brainstorm.md +491 -0
- package/dist/templates/windsurf/workflows/trellis-break-loop.md +111 -0
- package/dist/templates/windsurf/workflows/trellis-check-cross-layer.md +157 -0
- package/dist/templates/windsurf/workflows/trellis-check.md +27 -0
- package/dist/templates/windsurf/workflows/trellis-create-command.md +154 -0
- package/dist/templates/windsurf/workflows/trellis-finish-work.md +147 -0
- package/dist/templates/windsurf/workflows/trellis-integrate-skill.md +220 -0
- package/dist/templates/windsurf/workflows/trellis-onboard.md +362 -0
- package/dist/templates/windsurf/workflows/trellis-record-session.md +66 -0
- package/dist/templates/windsurf/workflows/trellis-start.md +373 -0
- package/dist/templates/windsurf/workflows/trellis-update-spec.md +358 -0
- package/dist/types/ai-tools.d.ts +15 -3
- package/dist/types/ai-tools.d.ts.map +1 -1
- package/dist/types/ai-tools.js +42 -2
- package/dist/types/ai-tools.js.map +1 -1
- package/dist/types/migration.d.ts +3 -1
- package/dist/types/migration.d.ts.map +1 -1
- package/dist/utils/project-detector.d.ts +28 -0
- package/dist/utils/project-detector.d.ts.map +1 -1
- package/dist/utils/project-detector.js +371 -0
- package/dist/utils/project-detector.js.map +1 -1
- package/dist/utils/template-fetcher.d.ts +19 -6
- package/dist/utils/template-fetcher.d.ts.map +1 -1
- package/dist/utils/template-fetcher.js +99 -17
- 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
|
@@ -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
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Terminal output utilities: colors and structured logging.
|
|
3
|
+
|
|
4
|
+
Single source of truth for Colors and log_* functions
|
|
5
|
+
used across all Trellis scripts.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Colors:
|
|
12
|
+
"""ANSI color codes for terminal output."""
|
|
13
|
+
|
|
14
|
+
RED = "\033[0;31m"
|
|
15
|
+
GREEN = "\033[0;32m"
|
|
16
|
+
YELLOW = "\033[1;33m"
|
|
17
|
+
BLUE = "\033[0;34m"
|
|
18
|
+
CYAN = "\033[0;36m"
|
|
19
|
+
DIM = "\033[2m"
|
|
20
|
+
NC = "\033[0m" # No Color / Reset
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def colored(text: str, color: str) -> str:
|
|
24
|
+
"""Apply ANSI color to text."""
|
|
25
|
+
return f"{color}{text}{Colors.NC}"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def log_info(msg: str) -> None:
|
|
29
|
+
"""Print info-level message with [INFO] prefix."""
|
|
30
|
+
print(f"{Colors.BLUE}[INFO]{Colors.NC} {msg}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def log_success(msg: str) -> None:
|
|
34
|
+
"""Print success message with [SUCCESS] prefix."""
|
|
35
|
+
print(f"{Colors.GREEN}[SUCCESS]{Colors.NC} {msg}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def log_warn(msg: str) -> None:
|
|
39
|
+
"""Print warning message with [WARN] prefix."""
|
|
40
|
+
print(f"{Colors.YELLOW}[WARN]{Colors.NC} {msg}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def log_error(msg: str) -> None:
|
|
44
|
+
"""Print error message with [ERROR] prefix."""
|
|
45
|
+
print(f"{Colors.RED}[ERROR]{Colors.NC} {msg}")
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Package discovery and context output.
|
|
4
|
+
|
|
5
|
+
Provides:
|
|
6
|
+
get_packages_info - Get structured package info
|
|
7
|
+
get_packages_section - Build PACKAGES text section
|
|
8
|
+
get_context_packages_text - Full packages text output (--mode packages)
|
|
9
|
+
get_context_packages_json - Full packages JSON output (--mode packages --json)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from .config import _is_true_config_value, get_default_package, get_packages, get_spec_scope
|
|
17
|
+
from .paths import (
|
|
18
|
+
DIR_SPEC,
|
|
19
|
+
DIR_WORKFLOW,
|
|
20
|
+
get_current_task,
|
|
21
|
+
get_repo_root,
|
|
22
|
+
)
|
|
23
|
+
from .tasks import load_task
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# =============================================================================
|
|
27
|
+
# Internal Helpers
|
|
28
|
+
# =============================================================================
|
|
29
|
+
|
|
30
|
+
def _scan_spec_layers(spec_dir: Path, package: str | None = None) -> list[str]:
|
|
31
|
+
"""Scan spec directory for available layers (subdirectories).
|
|
32
|
+
|
|
33
|
+
For monorepo: scans spec/<package>/
|
|
34
|
+
For single-repo: scans spec/
|
|
35
|
+
"""
|
|
36
|
+
target = spec_dir / package if package else spec_dir
|
|
37
|
+
if not target.is_dir():
|
|
38
|
+
return []
|
|
39
|
+
return sorted(
|
|
40
|
+
d.name for d in target.iterdir() if d.is_dir() and d.name != "guides"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _get_active_task_package(repo_root: Path) -> str | None:
|
|
45
|
+
"""Get the package field from the active task's task.json."""
|
|
46
|
+
current = get_current_task(repo_root)
|
|
47
|
+
if not current:
|
|
48
|
+
return None
|
|
49
|
+
ct = load_task(repo_root / current)
|
|
50
|
+
return ct.package if ct and ct.package else None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _resolve_scope_set(
|
|
54
|
+
packages: dict,
|
|
55
|
+
spec_scope,
|
|
56
|
+
task_pkg: str | None,
|
|
57
|
+
default_pkg: str | None,
|
|
58
|
+
) -> set | None:
|
|
59
|
+
"""Resolve spec_scope to a set of allowed package names, or None for full scan."""
|
|
60
|
+
if not packages:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
if spec_scope is None:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
if isinstance(spec_scope, str) and spec_scope == "active_task":
|
|
67
|
+
if task_pkg and task_pkg in packages:
|
|
68
|
+
return {task_pkg}
|
|
69
|
+
if default_pkg and default_pkg in packages:
|
|
70
|
+
return {default_pkg}
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
if isinstance(spec_scope, list):
|
|
74
|
+
valid = {e for e in spec_scope if e in packages}
|
|
75
|
+
if valid:
|
|
76
|
+
return valid
|
|
77
|
+
# All invalid: fallback
|
|
78
|
+
if task_pkg and task_pkg in packages:
|
|
79
|
+
return {task_pkg}
|
|
80
|
+
if default_pkg and default_pkg in packages:
|
|
81
|
+
return {default_pkg}
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# =============================================================================
|
|
88
|
+
# Public Functions
|
|
89
|
+
# =============================================================================
|
|
90
|
+
|
|
91
|
+
def get_packages_info(repo_root: Path) -> list[dict]:
|
|
92
|
+
"""Get structured package info for monorepo projects.
|
|
93
|
+
|
|
94
|
+
Returns list of dicts with keys: name, path, type, default, specLayers,
|
|
95
|
+
isSubmodule, isGitRepo.
|
|
96
|
+
Returns empty list for single-repo projects.
|
|
97
|
+
"""
|
|
98
|
+
packages = get_packages(repo_root)
|
|
99
|
+
if not packages:
|
|
100
|
+
return []
|
|
101
|
+
|
|
102
|
+
default_pkg = get_default_package(repo_root)
|
|
103
|
+
spec_dir = repo_root / DIR_WORKFLOW / DIR_SPEC
|
|
104
|
+
result = []
|
|
105
|
+
|
|
106
|
+
for pkg_name, pkg_config in packages.items():
|
|
107
|
+
pkg_path = pkg_config.get("path", pkg_name) if isinstance(pkg_config, dict) else str(pkg_config)
|
|
108
|
+
pkg_type = pkg_config.get("type", "local") if isinstance(pkg_config, dict) else "local"
|
|
109
|
+
pkg_git = pkg_config.get("git", False) if isinstance(pkg_config, dict) else False
|
|
110
|
+
layers = _scan_spec_layers(spec_dir, pkg_name)
|
|
111
|
+
|
|
112
|
+
result.append({
|
|
113
|
+
"name": pkg_name,
|
|
114
|
+
"path": pkg_path,
|
|
115
|
+
"type": pkg_type,
|
|
116
|
+
"default": pkg_name == default_pkg,
|
|
117
|
+
"specLayers": layers,
|
|
118
|
+
"isSubmodule": pkg_type == "submodule",
|
|
119
|
+
"isGitRepo": _is_true_config_value(pkg_git),
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
return result
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def get_packages_section(repo_root: Path) -> str:
|
|
126
|
+
"""Build the PACKAGES section for text output."""
|
|
127
|
+
spec_dir = repo_root / DIR_WORKFLOW / DIR_SPEC
|
|
128
|
+
pkg_info = get_packages_info(repo_root)
|
|
129
|
+
|
|
130
|
+
lines: list[str] = []
|
|
131
|
+
lines.append("## PACKAGES")
|
|
132
|
+
|
|
133
|
+
if not pkg_info:
|
|
134
|
+
lines.append("(single-repo mode)")
|
|
135
|
+
layers = _scan_spec_layers(spec_dir)
|
|
136
|
+
if layers:
|
|
137
|
+
lines.append(f"Spec layers: {', '.join(layers)}")
|
|
138
|
+
return "\n".join(lines)
|
|
139
|
+
|
|
140
|
+
default_pkg = get_default_package(repo_root)
|
|
141
|
+
|
|
142
|
+
for pkg in pkg_info:
|
|
143
|
+
layers_str = f" [{', '.join(pkg['specLayers'])}]" if pkg["specLayers"] else ""
|
|
144
|
+
submodule_tag = " (submodule)" if pkg["isSubmodule"] else ""
|
|
145
|
+
git_repo_tag = " (git repo)" if pkg["isGitRepo"] else ""
|
|
146
|
+
default_tag = " *" if pkg["default"] else ""
|
|
147
|
+
lines.append(
|
|
148
|
+
f"- {pkg['name']:<16} {pkg['path']:<20}{layers_str}{submodule_tag}{git_repo_tag}{default_tag}"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if default_pkg:
|
|
152
|
+
lines.append(f"Default package: {default_pkg}")
|
|
153
|
+
|
|
154
|
+
return "\n".join(lines)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def get_context_packages_text(repo_root: Path | None = None) -> str:
|
|
158
|
+
"""Get packages context as formatted text (for --mode packages)."""
|
|
159
|
+
if repo_root is None:
|
|
160
|
+
repo_root = get_repo_root()
|
|
161
|
+
|
|
162
|
+
pkg_info = get_packages_info(repo_root)
|
|
163
|
+
lines: list[str] = []
|
|
164
|
+
|
|
165
|
+
if not pkg_info:
|
|
166
|
+
spec_dir = repo_root / DIR_WORKFLOW / DIR_SPEC
|
|
167
|
+
lines.append("Single-repo project (no packages configured)")
|
|
168
|
+
lines.append("")
|
|
169
|
+
layers = _scan_spec_layers(spec_dir)
|
|
170
|
+
if layers:
|
|
171
|
+
lines.append(f"Spec layers: {', '.join(layers)}")
|
|
172
|
+
return "\n".join(lines)
|
|
173
|
+
|
|
174
|
+
# Resolve scope for annotations
|
|
175
|
+
packages_dict = get_packages(repo_root) or {}
|
|
176
|
+
default_pkg = get_default_package(repo_root)
|
|
177
|
+
spec_scope = get_spec_scope(repo_root)
|
|
178
|
+
task_pkg = _get_active_task_package(repo_root)
|
|
179
|
+
scope_set = _resolve_scope_set(packages_dict, spec_scope, task_pkg, default_pkg)
|
|
180
|
+
|
|
181
|
+
lines.append("## PACKAGES")
|
|
182
|
+
lines.append("")
|
|
183
|
+
for pkg in pkg_info:
|
|
184
|
+
default_tag = " (default)" if pkg["default"] else ""
|
|
185
|
+
type_tag = f" [{pkg['type']}]" if pkg["type"] != "local" else ""
|
|
186
|
+
git_tag = " [git repo]" if pkg["isGitRepo"] else ""
|
|
187
|
+
|
|
188
|
+
# Scope annotation
|
|
189
|
+
scope_tag = ""
|
|
190
|
+
if scope_set is not None and pkg["name"] not in scope_set:
|
|
191
|
+
scope_tag = " (out of scope)"
|
|
192
|
+
|
|
193
|
+
lines.append(f"### {pkg['name']}{default_tag}{type_tag}{git_tag}{scope_tag}")
|
|
194
|
+
lines.append(f"Path: {pkg['path']}")
|
|
195
|
+
if pkg["specLayers"]:
|
|
196
|
+
lines.append(f"Spec layers: {', '.join(pkg['specLayers'])}")
|
|
197
|
+
for layer in pkg["specLayers"]:
|
|
198
|
+
lines.append(f" - .trellis/spec/{pkg['name']}/{layer}/index.md")
|
|
199
|
+
else:
|
|
200
|
+
lines.append("Spec: not configured")
|
|
201
|
+
lines.append("")
|
|
202
|
+
|
|
203
|
+
# Also show shared guides
|
|
204
|
+
guides_dir = repo_root / DIR_WORKFLOW / DIR_SPEC / "guides"
|
|
205
|
+
if guides_dir.is_dir():
|
|
206
|
+
lines.append("### Shared Guides (always included)")
|
|
207
|
+
lines.append("Path: .trellis/spec/guides/index.md")
|
|
208
|
+
lines.append("")
|
|
209
|
+
|
|
210
|
+
return "\n".join(lines)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def get_context_packages_json(repo_root: Path | None = None) -> dict:
|
|
214
|
+
"""Get packages context as a dictionary (for --mode packages --json)."""
|
|
215
|
+
if repo_root is None:
|
|
216
|
+
repo_root = get_repo_root()
|
|
217
|
+
|
|
218
|
+
pkg_info = get_packages_info(repo_root)
|
|
219
|
+
|
|
220
|
+
if not pkg_info:
|
|
221
|
+
spec_dir = repo_root / DIR_WORKFLOW / DIR_SPEC
|
|
222
|
+
layers = _scan_spec_layers(spec_dir)
|
|
223
|
+
return {
|
|
224
|
+
"mode": "single-repo",
|
|
225
|
+
"specLayers": layers,
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
default_pkg = get_default_package(repo_root)
|
|
229
|
+
spec_scope = get_spec_scope(repo_root)
|
|
230
|
+
task_pkg = _get_active_task_package(repo_root)
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
"mode": "monorepo",
|
|
234
|
+
"packages": pkg_info,
|
|
235
|
+
"defaultPackage": default_pkg,
|
|
236
|
+
"specScope": spec_scope,
|
|
237
|
+
"activeTaskPackage": task_pkg,
|
|
238
|
+
}
|
|
@@ -221,6 +221,50 @@ def _get_current_task_file(repo_root: Path | None = None) -> Path:
|
|
|
221
221
|
return repo_root / DIR_WORKFLOW / FILE_CURRENT_TASK
|
|
222
222
|
|
|
223
223
|
|
|
224
|
+
def normalize_task_ref(task_ref: str) -> str:
|
|
225
|
+
"""Normalize a task ref for stable storage in .current-task.
|
|
226
|
+
|
|
227
|
+
Stored refs should prefer repo-relative POSIX paths like
|
|
228
|
+
`.trellis/tasks/03-27-my-task`, even on Windows. Absolute paths are preserved
|
|
229
|
+
unless they can later be converted back to repo-relative form by callers.
|
|
230
|
+
"""
|
|
231
|
+
normalized = task_ref.strip()
|
|
232
|
+
if not normalized:
|
|
233
|
+
return ""
|
|
234
|
+
|
|
235
|
+
path_obj = Path(normalized)
|
|
236
|
+
if path_obj.is_absolute():
|
|
237
|
+
return str(path_obj)
|
|
238
|
+
|
|
239
|
+
normalized = normalized.replace("\\", "/")
|
|
240
|
+
while normalized.startswith("./"):
|
|
241
|
+
normalized = normalized[2:]
|
|
242
|
+
|
|
243
|
+
if normalized.startswith(f"{DIR_TASKS}/"):
|
|
244
|
+
return f"{DIR_WORKFLOW}/{normalized}"
|
|
245
|
+
|
|
246
|
+
return normalized
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def resolve_task_ref(task_ref: str, repo_root: Path | None = None) -> Path | None:
|
|
250
|
+
"""Resolve a task ref from .current-task to an absolute task directory path."""
|
|
251
|
+
if repo_root is None:
|
|
252
|
+
repo_root = get_repo_root()
|
|
253
|
+
|
|
254
|
+
normalized = normalize_task_ref(task_ref)
|
|
255
|
+
if not normalized:
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
path_obj = Path(normalized)
|
|
259
|
+
if path_obj.is_absolute():
|
|
260
|
+
return path_obj
|
|
261
|
+
|
|
262
|
+
if normalized.startswith(f"{DIR_WORKFLOW}/"):
|
|
263
|
+
return repo_root / path_obj
|
|
264
|
+
|
|
265
|
+
return repo_root / DIR_WORKFLOW / DIR_TASKS / path_obj
|
|
266
|
+
|
|
267
|
+
|
|
224
268
|
def get_current_task(repo_root: Path | None = None) -> str | None:
|
|
225
269
|
"""Get current task directory path (relative to repo_root).
|
|
226
270
|
|
|
@@ -236,7 +280,8 @@ def get_current_task(repo_root: Path | None = None) -> str | None:
|
|
|
236
280
|
return None
|
|
237
281
|
|
|
238
282
|
try:
|
|
239
|
-
|
|
283
|
+
content = current_file.read_text(encoding="utf-8").strip()
|
|
284
|
+
return normalize_task_ref(content) if content else None
|
|
240
285
|
except (OSError, IOError):
|
|
241
286
|
return None
|
|
242
287
|
|
|
@@ -255,7 +300,7 @@ def get_current_task_abs(repo_root: Path | None = None) -> Path | None:
|
|
|
255
300
|
|
|
256
301
|
relative = get_current_task(repo_root)
|
|
257
302
|
if relative:
|
|
258
|
-
return repo_root
|
|
303
|
+
return resolve_task_ref(relative, repo_root)
|
|
259
304
|
return None
|
|
260
305
|
|
|
261
306
|
|
|
@@ -272,18 +317,24 @@ def set_current_task(task_path: str, repo_root: Path | None = None) -> bool:
|
|
|
272
317
|
if repo_root is None:
|
|
273
318
|
repo_root = get_repo_root()
|
|
274
319
|
|
|
275
|
-
|
|
320
|
+
normalized = normalize_task_ref(task_path)
|
|
321
|
+
if not normalized:
|
|
276
322
|
return False
|
|
277
323
|
|
|
278
324
|
# Verify task directory exists
|
|
279
|
-
full_path = repo_root
|
|
280
|
-
if not full_path.is_dir():
|
|
325
|
+
full_path = resolve_task_ref(normalized, repo_root)
|
|
326
|
+
if full_path is None or not full_path.is_dir():
|
|
281
327
|
return False
|
|
282
328
|
|
|
329
|
+
try:
|
|
330
|
+
normalized = full_path.relative_to(repo_root).as_posix()
|
|
331
|
+
except ValueError:
|
|
332
|
+
normalized = str(full_path)
|
|
333
|
+
|
|
283
334
|
current_file = _get_current_task_file(repo_root)
|
|
284
335
|
|
|
285
336
|
try:
|
|
286
|
-
current_file.write_text(
|
|
337
|
+
current_file.write_text(normalized, encoding="utf-8")
|
|
287
338
|
return True
|
|
288
339
|
except (OSError, IOError):
|
|
289
340
|
return False
|
|
@@ -333,6 +384,52 @@ def generate_task_date_prefix() -> str:
|
|
|
333
384
|
return datetime.now().strftime("%m-%d")
|
|
334
385
|
|
|
335
386
|
|
|
387
|
+
# =============================================================================
|
|
388
|
+
# Monorepo / Package Paths
|
|
389
|
+
# =============================================================================
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def get_spec_dir(package: str | None = None, repo_root: Path | None = None) -> Path:
|
|
393
|
+
"""Get the spec directory path.
|
|
394
|
+
|
|
395
|
+
Single-repo: .trellis/spec
|
|
396
|
+
Monorepo with package: .trellis/spec/<package>
|
|
397
|
+
|
|
398
|
+
Uses lazy import to avoid circular dependency with config.py.
|
|
399
|
+
"""
|
|
400
|
+
if repo_root is None:
|
|
401
|
+
repo_root = get_repo_root()
|
|
402
|
+
|
|
403
|
+
from .config import get_spec_base
|
|
404
|
+
|
|
405
|
+
base = get_spec_base(package, repo_root)
|
|
406
|
+
return repo_root / DIR_WORKFLOW / base
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def get_package_path(package: str, repo_root: Path | None = None) -> Path | None:
|
|
410
|
+
"""Get a package's source directory absolute path from config.
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
Absolute path to the package directory, or None if not found.
|
|
414
|
+
"""
|
|
415
|
+
if repo_root is None:
|
|
416
|
+
repo_root = get_repo_root()
|
|
417
|
+
|
|
418
|
+
from .config import get_packages
|
|
419
|
+
|
|
420
|
+
packages = get_packages(repo_root)
|
|
421
|
+
if not packages or package not in packages:
|
|
422
|
+
return None
|
|
423
|
+
|
|
424
|
+
info = packages[package]
|
|
425
|
+
if isinstance(info, dict):
|
|
426
|
+
rel_path = info.get("path", package)
|
|
427
|
+
else:
|
|
428
|
+
rel_path = str(info)
|
|
429
|
+
|
|
430
|
+
return repo_root / rel_path
|
|
431
|
+
|
|
432
|
+
|
|
336
433
|
# =============================================================================
|
|
337
434
|
# Main Entry (for testing)
|
|
338
435
|
# =============================================================================
|
|
@@ -19,25 +19,39 @@ Provides:
|
|
|
19
19
|
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
|
|
22
|
-
import json
|
|
23
22
|
from pathlib import Path
|
|
24
23
|
|
|
24
|
+
from .io import read_json, write_json
|
|
25
25
|
|
|
26
|
-
def _read_json_file(path: Path) -> dict | None:
|
|
27
|
-
"""Read and parse a JSON file."""
|
|
28
|
-
try:
|
|
29
|
-
return json.loads(path.read_text(encoding="utf-8"))
|
|
30
|
-
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
|
31
|
-
return None
|
|
32
26
|
|
|
27
|
+
# =============================================================================
|
|
28
|
+
# Internal Helpers (operate on pre-loaded data dict)
|
|
29
|
+
# =============================================================================
|
|
30
|
+
|
|
31
|
+
def _total_phases(data: dict) -> int:
|
|
32
|
+
"""Get total phases from pre-loaded data."""
|
|
33
|
+
next_action = data.get("next_action", [])
|
|
34
|
+
return len(next_action) if isinstance(next_action, list) else 0
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
|
|
37
|
+
def _phase_action(data: dict, phase: int) -> str:
|
|
38
|
+
"""Get action name for a phase from pre-loaded data."""
|
|
39
|
+
next_action = data.get("next_action", [])
|
|
40
|
+
if isinstance(next_action, list):
|
|
41
|
+
for item in next_action:
|
|
42
|
+
if isinstance(item, dict) and item.get("phase") == phase:
|
|
43
|
+
return item.get("action", "unknown")
|
|
44
|
+
return "unknown"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _phase_for_action(data: dict, action: str) -> int:
|
|
48
|
+
"""Get phase number for an action name from pre-loaded data."""
|
|
49
|
+
next_action = data.get("next_action", [])
|
|
50
|
+
if isinstance(next_action, list):
|
|
51
|
+
for item in next_action:
|
|
52
|
+
if isinstance(item, dict) and item.get("action") == action:
|
|
53
|
+
return item.get("phase", 0)
|
|
54
|
+
return 0
|
|
41
55
|
|
|
42
56
|
|
|
43
57
|
# =============================================================================
|
|
@@ -53,7 +67,7 @@ def get_current_phase(task_json: Path) -> int:
|
|
|
53
67
|
Returns:
|
|
54
68
|
Current phase number, or 0 if not found.
|
|
55
69
|
"""
|
|
56
|
-
data =
|
|
70
|
+
data = read_json(task_json)
|
|
57
71
|
if not data:
|
|
58
72
|
return 0
|
|
59
73
|
return data.get("current_phase", 0) or 0
|
|
@@ -68,14 +82,10 @@ def get_total_phases(task_json: Path) -> int:
|
|
|
68
82
|
Returns:
|
|
69
83
|
Total phase count, or 0 if not found.
|
|
70
84
|
"""
|
|
71
|
-
data =
|
|
85
|
+
data = read_json(task_json)
|
|
72
86
|
if not data:
|
|
73
87
|
return 0
|
|
74
|
-
|
|
75
|
-
next_action = data.get("next_action", [])
|
|
76
|
-
if isinstance(next_action, list):
|
|
77
|
-
return len(next_action)
|
|
78
|
-
return 0
|
|
88
|
+
return _total_phases(data)
|
|
79
89
|
|
|
80
90
|
|
|
81
91
|
def get_phase_action(task_json: Path, phase: int) -> str:
|
|
@@ -88,16 +98,10 @@ def get_phase_action(task_json: Path, phase: int) -> str:
|
|
|
88
98
|
Returns:
|
|
89
99
|
Action name, or "unknown" if not found.
|
|
90
100
|
"""
|
|
91
|
-
data =
|
|
101
|
+
data = read_json(task_json)
|
|
92
102
|
if not data:
|
|
93
103
|
return "unknown"
|
|
94
|
-
|
|
95
|
-
next_action = data.get("next_action", [])
|
|
96
|
-
if isinstance(next_action, list):
|
|
97
|
-
for item in next_action:
|
|
98
|
-
if isinstance(item, dict) and item.get("phase") == phase:
|
|
99
|
-
return item.get("action", "unknown")
|
|
100
|
-
return "unknown"
|
|
104
|
+
return _phase_action(data, phase)
|
|
101
105
|
|
|
102
106
|
|
|
103
107
|
def get_phase_info(task_json: Path) -> str:
|
|
@@ -109,18 +113,18 @@ def get_phase_info(task_json: Path) -> str:
|
|
|
109
113
|
Returns:
|
|
110
114
|
Formatted string like "1/4 (implement)".
|
|
111
115
|
"""
|
|
112
|
-
data =
|
|
116
|
+
data = read_json(task_json)
|
|
113
117
|
if not data:
|
|
114
118
|
return "N/A"
|
|
115
119
|
|
|
116
120
|
current_phase = data.get("current_phase", 0) or 0
|
|
117
|
-
|
|
118
|
-
action_name =
|
|
121
|
+
total = _total_phases(data)
|
|
122
|
+
action_name = _phase_action(data, current_phase)
|
|
119
123
|
|
|
120
124
|
if current_phase == 0 or current_phase is None:
|
|
121
|
-
return f"0/{
|
|
125
|
+
return f"0/{total} (pending)"
|
|
122
126
|
else:
|
|
123
|
-
return f"{current_phase}/{
|
|
127
|
+
return f"{current_phase}/{total} ({action_name})"
|
|
124
128
|
|
|
125
129
|
|
|
126
130
|
def set_phase(task_json: Path, phase: int) -> bool:
|
|
@@ -133,12 +137,12 @@ def set_phase(task_json: Path, phase: int) -> bool:
|
|
|
133
137
|
Returns:
|
|
134
138
|
True on success, False on error.
|
|
135
139
|
"""
|
|
136
|
-
data =
|
|
140
|
+
data = read_json(task_json)
|
|
137
141
|
if not data:
|
|
138
142
|
return False
|
|
139
143
|
|
|
140
144
|
data["current_phase"] = phase
|
|
141
|
-
return
|
|
145
|
+
return write_json(task_json, data)
|
|
142
146
|
|
|
143
147
|
|
|
144
148
|
def advance_phase(task_json: Path) -> bool:
|
|
@@ -150,19 +154,19 @@ def advance_phase(task_json: Path) -> bool:
|
|
|
150
154
|
Returns:
|
|
151
155
|
True on success, False on error or at final phase.
|
|
152
156
|
"""
|
|
153
|
-
data =
|
|
157
|
+
data = read_json(task_json)
|
|
154
158
|
if not data:
|
|
155
159
|
return False
|
|
156
160
|
|
|
157
161
|
current = data.get("current_phase", 0) or 0
|
|
158
|
-
total =
|
|
162
|
+
total = _total_phases(data)
|
|
159
163
|
next_phase = current + 1
|
|
160
164
|
|
|
161
165
|
if next_phase > total:
|
|
162
166
|
return False # Already at final phase
|
|
163
167
|
|
|
164
168
|
data["current_phase"] = next_phase
|
|
165
|
-
return
|
|
169
|
+
return write_json(task_json, data)
|
|
166
170
|
|
|
167
171
|
|
|
168
172
|
def get_phase_for_action(task_json: Path, action: str) -> int:
|
|
@@ -175,16 +179,10 @@ def get_phase_for_action(task_json: Path, action: str) -> int:
|
|
|
175
179
|
Returns:
|
|
176
180
|
Phase number, or 0 if not found.
|
|
177
181
|
"""
|
|
178
|
-
data =
|
|
182
|
+
data = read_json(task_json)
|
|
179
183
|
if not data:
|
|
180
184
|
return 0
|
|
181
|
-
|
|
182
|
-
next_action = data.get("next_action", [])
|
|
183
|
-
if isinstance(next_action, list):
|
|
184
|
-
for item in next_action:
|
|
185
|
-
if isinstance(item, dict) and item.get("action") == action:
|
|
186
|
-
return item.get("phase", 0)
|
|
187
|
-
return 0
|
|
185
|
+
return _phase_for_action(data, action)
|
|
188
186
|
|
|
189
187
|
|
|
190
188
|
def map_subagent_to_action(subagent_type: str) -> str:
|
|
@@ -231,8 +229,11 @@ def is_current_action(task_json: Path, action: str) -> bool:
|
|
|
231
229
|
Returns:
|
|
232
230
|
True if current phase matches the action.
|
|
233
231
|
"""
|
|
234
|
-
|
|
235
|
-
|
|
232
|
+
data = read_json(task_json)
|
|
233
|
+
if not data:
|
|
234
|
+
return False
|
|
235
|
+
current = data.get("current_phase", 0) or 0
|
|
236
|
+
action_phase = _phase_for_action(data, action)
|
|
236
237
|
return current == action_phase
|
|
237
238
|
|
|
238
239
|
|