@mindfoldhq/trellis 0.6.5 → 0.6.7
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 +1 -1
- package/dist/cli/index.js +1 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/channel/guard.d.ts.map +1 -1
- package/dist/commands/channel/guard.js +7 -2
- package/dist/commands/channel/guard.js.map +1 -1
- package/dist/commands/channel/store/paths.d.ts +14 -0
- package/dist/commands/channel/store/paths.d.ts.map +1 -1
- package/dist/commands/channel/store/paths.js +26 -0
- package/dist/commands/channel/store/paths.js.map +1 -1
- package/dist/commands/channel/supervisor/stdout.d.ts +7 -5
- package/dist/commands/channel/supervisor/stdout.d.ts.map +1 -1
- package/dist/commands/channel/supervisor/stdout.js +65 -18
- package/dist/commands/channel/supervisor/stdout.js.map +1 -1
- package/dist/commands/channel/supervisor.d.ts +13 -0
- package/dist/commands/channel/supervisor.d.ts.map +1 -1
- package/dist/commands/channel/supervisor.js +36 -16
- package/dist/commands/channel/supervisor.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.map +1 -1
- package/dist/commands/uninstall.d.ts +9 -0
- package/dist/commands/uninstall.d.ts.map +1 -1
- package/dist/commands/uninstall.js +74 -4
- package/dist/commands/uninstall.js.map +1 -1
- package/dist/commands/update.d.ts +29 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +128 -58
- package/dist/commands/update.js.map +1 -1
- package/dist/configurators/copilot.d.ts +1 -0
- package/dist/configurators/copilot.d.ts.map +1 -1
- package/dist/configurators/copilot.js +4 -1
- package/dist/configurators/copilot.js.map +1 -1
- package/dist/configurators/index.d.ts.map +1 -1
- package/dist/configurators/index.js +7 -1
- package/dist/configurators/index.js.map +1 -1
- package/dist/configurators/omp.d.ts +11 -0
- package/dist/configurators/omp.d.ts.map +1 -0
- package/dist/configurators/omp.js +51 -0
- package/dist/configurators/omp.js.map +1 -0
- package/dist/configurators/shared.d.ts +7 -0
- package/dist/configurators/shared.d.ts.map +1 -1
- package/dist/configurators/shared.js +27 -0
- package/dist/configurators/shared.js.map +1 -1
- package/dist/configurators/zcode.d.ts +3 -3
- package/dist/configurators/zcode.js +12 -12
- package/dist/configurators/zcode.js.map +1 -1
- package/dist/migrations/manifests/0.6.6.json +17 -0
- package/dist/migrations/manifests/0.6.7.json +9 -0
- package/dist/templates/codex/hooks/session-start.py +6 -6
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-agents.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-skills-or-commands.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/bundled-skills.md +2 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/generated-files.md +2 -2
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/agents.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/overview.md +3 -3
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/platform-map.md +19 -2
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/skills-and-commands.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-session-insight/SKILL.md +1 -1
- package/dist/templates/copilot/copilot-instructions.md +11 -0
- package/dist/templates/copilot/hooks/session-start.py +6 -6
- package/dist/templates/copilot/index.d.ts +6 -1
- package/dist/templates/copilot/index.d.ts.map +1 -1
- package/dist/templates/copilot/index.js +8 -1
- package/dist/templates/copilot/index.js.map +1 -1
- package/dist/templates/omp/agents/trellis-check.md +41 -0
- package/dist/templates/omp/agents/trellis-implement.md +45 -0
- package/dist/templates/omp/agents/trellis-research.md +30 -0
- package/dist/templates/omp/extensions/trellis/index.ts.txt +462 -0
- package/dist/templates/omp/index.d.ts +4 -0
- package/dist/templates/omp/index.d.ts.map +1 -0
- package/dist/templates/omp/index.js +9 -0
- package/dist/templates/omp/index.js.map +1 -0
- package/dist/templates/pi/extensions/trellis/index.ts.txt +47 -21
- package/dist/templates/shared-hooks/inject-workflow-state.py +5 -5
- package/dist/templates/shared-hooks/session-start.py +7 -7
- package/dist/templates/trellis/scripts/add_session.py +65 -17
- package/dist/templates/trellis/scripts/common/cli_adapter.py +36 -1
- package/dist/templates/trellis/scripts/common/config.py +29 -0
- package/dist/templates/trellis/scripts/common/io.py +20 -3
- package/dist/templates/trellis/scripts/common/session_context.py +1 -1
- package/dist/templates/trellis/scripts/common/task_store.py +90 -19
- package/dist/templates/trellis/scripts/common/task_utils.py +22 -0
- package/dist/templates/trellis/scripts/task.py +8 -2
- package/dist/templates/trellis/workflow.md +13 -13
- package/dist/templates/zcode/agents/trellis-research.md +133 -0
- package/dist/templates/zcode/index.d.ts +6 -5
- package/dist/templates/zcode/index.d.ts.map +1 -1
- package/dist/templates/zcode/index.js +6 -5
- package/dist/templates/zcode/index.js.map +1 -1
- package/dist/types/ai-tools.d.ts +3 -3
- package/dist/types/ai-tools.d.ts.map +1 -1
- package/dist/types/ai-tools.js +29 -2
- package/dist/types/ai-tools.js.map +1 -1
- package/dist/utils/atomic-write.d.ts +12 -0
- package/dist/utils/atomic-write.d.ts.map +1 -0
- package/dist/utils/atomic-write.js +30 -0
- package/dist/utils/atomic-write.js.map +1 -0
- package/dist/utils/file-writer.d.ts.map +1 -1
- package/dist/utils/file-writer.js +6 -5
- package/dist/utils/file-writer.js.map +1 -1
- package/dist/utils/registry-config.d.ts.map +1 -1
- package/dist/utils/registry-config.js +3 -2
- package/dist/utils/registry-config.js.map +1 -1
- package/dist/utils/template-fetcher.d.ts.map +1 -1
- package/dist/utils/template-fetcher.js +36 -4
- package/dist/utils/template-fetcher.js.map +1 -1
- package/dist/utils/template-hash.d.ts.map +1 -1
- package/dist/utils/template-hash.js +2 -1
- package/dist/utils/template-hash.js.map +1 -1
- package/dist/utils/uninstall-scrubbers.d.ts +1 -0
- package/dist/utils/uninstall-scrubbers.d.ts.map +1 -1
- package/dist/utils/uninstall-scrubbers.js +19 -0
- package/dist/utils/uninstall-scrubbers.js.map +1 -1
- package/package.json +2 -2
|
@@ -53,12 +53,12 @@ if sys.platform.startswith("win"):
|
|
|
53
53
|
try:
|
|
54
54
|
_stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
|
|
55
55
|
except Exception:
|
|
56
|
-
pass
|
|
56
|
+
pass # Optional Windows stream setup; keep hook startup non-fatal.
|
|
57
57
|
elif hasattr(_stream, "detach"):
|
|
58
58
|
try:
|
|
59
59
|
setattr(sys, _stream_name, _io.TextIOWrapper(_stream.detach(), encoding="utf-8", errors="replace"))
|
|
60
60
|
except Exception:
|
|
61
|
-
pass
|
|
61
|
+
pass # Optional Windows stream setup; keep hook startup non-fatal.
|
|
62
62
|
from typing import Optional
|
|
63
63
|
|
|
64
64
|
|
|
@@ -316,12 +316,12 @@ def _load_hook_input() -> dict:
|
|
|
316
316
|
short daemon read preserves that path while failing closed to `{}` for
|
|
317
317
|
non-piping hosts.
|
|
318
318
|
"""
|
|
319
|
-
result_queue: "queue.Queue[str |
|
|
319
|
+
result_queue: "queue.Queue[str | Exception]" = queue.Queue(maxsize=1)
|
|
320
320
|
|
|
321
321
|
def _read() -> None:
|
|
322
322
|
try:
|
|
323
323
|
result_queue.put(sys.stdin.read())
|
|
324
|
-
except
|
|
324
|
+
except Exception as exc:
|
|
325
325
|
result_queue.put(exc)
|
|
326
326
|
|
|
327
327
|
reader = threading.Thread(target=_read, daemon=True)
|
|
@@ -331,7 +331,7 @@ def _load_hook_input() -> dict:
|
|
|
331
331
|
except queue.Empty:
|
|
332
332
|
return {}
|
|
333
333
|
|
|
334
|
-
if isinstance(raw,
|
|
334
|
+
if isinstance(raw, Exception):
|
|
335
335
|
return {}
|
|
336
336
|
try:
|
|
337
337
|
data = json.loads(raw) if raw.strip() else {}
|
|
@@ -87,12 +87,12 @@ if sys.platform.startswith("win"):
|
|
|
87
87
|
try:
|
|
88
88
|
_stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
|
|
89
89
|
except Exception:
|
|
90
|
-
pass
|
|
90
|
+
pass # Optional Windows stream setup; keep hook startup non-fatal.
|
|
91
91
|
elif hasattr(_stream, "detach"):
|
|
92
92
|
try:
|
|
93
93
|
setattr(sys, _stream_name, _io.TextIOWrapper(_stream.detach(), encoding="utf-8", errors="replace"))
|
|
94
94
|
except Exception:
|
|
95
|
-
pass
|
|
95
|
+
pass # Optional Windows stream setup; keep hook startup non-fatal.
|
|
96
96
|
|
|
97
97
|
|
|
98
98
|
|
|
@@ -249,7 +249,7 @@ def _persist_context_key_for_bash(context_key: str | None) -> None:
|
|
|
249
249
|
with open(env_file, "a", encoding="utf-8") as handle:
|
|
250
250
|
handle.write(f"export TRELLIS_CONTEXT_ID={shlex.quote(context_key)}\n")
|
|
251
251
|
except OSError:
|
|
252
|
-
pass
|
|
252
|
+
pass # Optional shell bridge; keep session-start non-fatal.
|
|
253
253
|
|
|
254
254
|
|
|
255
255
|
def _resolve_active_task(trellis_dir: Path, input_data: dict):
|
|
@@ -351,7 +351,7 @@ def _get_task_status(trellis_dir: Path, input_data: dict) -> str:
|
|
|
351
351
|
try:
|
|
352
352
|
task_data = json.loads(task_json_path.read_text(encoding="utf-8"))
|
|
353
353
|
except (json.JSONDecodeError, PermissionError):
|
|
354
|
-
pass
|
|
354
|
+
pass # Optional task metadata; fall back to generic status.
|
|
355
355
|
|
|
356
356
|
task_title = task_data.get("title", task_ref)
|
|
357
357
|
task_status = task_data.get("status", "unknown")
|
|
@@ -453,7 +453,7 @@ def _load_trellis_config(trellis_dir: Path, input_data: dict) -> tuple:
|
|
|
453
453
|
if isinstance(tp, str) and tp:
|
|
454
454
|
task_pkg = tp
|
|
455
455
|
except (json.JSONDecodeError, OSError):
|
|
456
|
-
pass
|
|
456
|
+
pass # Optional package metadata; fall back to default scope.
|
|
457
457
|
|
|
458
458
|
default_pkg = get_default_package(repo_root)
|
|
459
459
|
return is_mono, packages, scope, task_pkg, default_pkg
|
|
@@ -630,7 +630,7 @@ def _build_compact_current_state(
|
|
|
630
630
|
if isinstance(data, dict):
|
|
631
631
|
status = str(data.get("status") or "unknown")
|
|
632
632
|
except (json.JSONDecodeError, OSError):
|
|
633
|
-
pass
|
|
633
|
+
pass # Optional task metadata; fall back to generic status.
|
|
634
634
|
lines.append(f"Current task: {_repo_relative(repo_root, task_dir)}; status={status}.")
|
|
635
635
|
else:
|
|
636
636
|
lines.append("Current task: none.")
|
|
@@ -642,7 +642,7 @@ def _build_compact_current_state(
|
|
|
642
642
|
f"Active tasks: {task_count} total. Use `python3 ./.trellis/scripts/task.py list --mine` only if needed."
|
|
643
643
|
)
|
|
644
644
|
except Exception:
|
|
645
|
-
pass
|
|
645
|
+
pass # Optional task summary; keep compact state available.
|
|
646
646
|
|
|
647
647
|
if get_active_journal_file and count_lines:
|
|
648
648
|
journal = get_active_journal_file(repo_root)
|
|
@@ -14,7 +14,7 @@ Usage:
|
|
|
14
14
|
|
|
15
15
|
Branch resolution order:
|
|
16
16
|
1. --branch CLI arg (explicit)
|
|
17
|
-
2. task.json branch field (from active task)
|
|
17
|
+
2. task.json branch field (from active task, if still exists)
|
|
18
18
|
3. git branch --show-current (auto-detect)
|
|
19
19
|
4. None (omitted gracefully)
|
|
20
20
|
"""
|
|
@@ -44,6 +44,7 @@ from common.safe_commit import (
|
|
|
44
44
|
safe_trellis_paths_to_add,
|
|
45
45
|
)
|
|
46
46
|
from common.tasks import load_task
|
|
47
|
+
from common.types import TaskInfo
|
|
47
48
|
from common.config import (
|
|
48
49
|
get_packages,
|
|
49
50
|
get_session_auto_commit,
|
|
@@ -55,6 +56,12 @@ from common.config import (
|
|
|
55
56
|
)
|
|
56
57
|
|
|
57
58
|
|
|
59
|
+
DEFAULT_MAIN_CHANGES = (
|
|
60
|
+
"- Detailed change bullets were not supplied; see the summary above."
|
|
61
|
+
)
|
|
62
|
+
DEFAULT_TESTING = "- Validation was not recorded for this session."
|
|
63
|
+
|
|
64
|
+
|
|
58
65
|
# =============================================================================
|
|
59
66
|
# Helper Functions
|
|
60
67
|
# =============================================================================
|
|
@@ -126,6 +133,56 @@ def count_journal_files(dev_dir: Path, active_num: int) -> str:
|
|
|
126
133
|
return "\n".join(result_lines)
|
|
127
134
|
|
|
128
135
|
|
|
136
|
+
def get_current_git_branch(repo_root: Path) -> str | None:
|
|
137
|
+
"""Return the current checkout branch, or None for detached/non-git states."""
|
|
138
|
+
rc, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root)
|
|
139
|
+
if rc != 0:
|
|
140
|
+
return None
|
|
141
|
+
detected = branch_out.strip()
|
|
142
|
+
return detected or None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def branch_ref_exists(repo_root: Path, branch: str) -> bool:
|
|
146
|
+
"""Return True when branch exists locally or as the local origin ref."""
|
|
147
|
+
for ref in (f"refs/heads/{branch}", f"refs/remotes/origin/{branch}"):
|
|
148
|
+
rc, _, _ = run_git(["show-ref", "--verify", "--quiet", ref], cwd=repo_root)
|
|
149
|
+
if rc == 0:
|
|
150
|
+
return True
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def resolve_session_branch(
|
|
155
|
+
repo_root: Path,
|
|
156
|
+
cli_branch: str | None,
|
|
157
|
+
task_data: TaskInfo | None,
|
|
158
|
+
) -> str | None:
|
|
159
|
+
"""Resolve journal branch without trusting stale task.json branch fields."""
|
|
160
|
+
if cli_branch:
|
|
161
|
+
return cli_branch
|
|
162
|
+
|
|
163
|
+
current_branch = get_current_git_branch(repo_root)
|
|
164
|
+
raw_task_branch = task_data.raw.get("branch") if task_data else None
|
|
165
|
+
task_branch = raw_task_branch.strip() if isinstance(raw_task_branch, str) else ""
|
|
166
|
+
if not task_branch:
|
|
167
|
+
return current_branch
|
|
168
|
+
|
|
169
|
+
if branch_ref_exists(repo_root, task_branch):
|
|
170
|
+
return task_branch
|
|
171
|
+
|
|
172
|
+
if current_branch:
|
|
173
|
+
print(
|
|
174
|
+
f"Warning: task.json branch '{task_branch}' no longer exists locally or as origin/{task_branch}; using current branch '{current_branch}'.",
|
|
175
|
+
file=sys.stderr,
|
|
176
|
+
)
|
|
177
|
+
return current_branch
|
|
178
|
+
|
|
179
|
+
print(
|
|
180
|
+
f"Warning: task.json branch '{task_branch}' no longer exists locally or as origin/{task_branch}; omitting branch.",
|
|
181
|
+
file=sys.stderr,
|
|
182
|
+
)
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
|
|
129
186
|
def create_new_journal_file(
|
|
130
187
|
dev_dir: Path, num: int, developer: str, today: str, max_lines: int = 2000,
|
|
131
188
|
) -> Path:
|
|
@@ -154,6 +211,7 @@ def generate_session_content(
|
|
|
154
211
|
today: str,
|
|
155
212
|
package: str | None = None,
|
|
156
213
|
branch: str | None = None,
|
|
214
|
+
testing_content: str = DEFAULT_TESTING,
|
|
157
215
|
) -> str:
|
|
158
216
|
"""Generate session content."""
|
|
159
217
|
if commit and commit != "-":
|
|
@@ -189,7 +247,7 @@ def generate_session_content(
|
|
|
189
247
|
|
|
190
248
|
### Testing
|
|
191
249
|
|
|
192
|
-
|
|
250
|
+
{testing_content}
|
|
193
251
|
|
|
194
252
|
### Status
|
|
195
253
|
|
|
@@ -395,8 +453,8 @@ def _auto_commit_workspace(repo_root: Path) -> None:
|
|
|
395
453
|
def add_session(
|
|
396
454
|
title: str,
|
|
397
455
|
commit: str = "-",
|
|
398
|
-
summary: str = "
|
|
399
|
-
extra_content: str =
|
|
456
|
+
summary: str = "Session summary was not supplied.",
|
|
457
|
+
extra_content: str = DEFAULT_MAIN_CHANGES,
|
|
400
458
|
auto_commit: bool = True,
|
|
401
459
|
package: str | None = None,
|
|
402
460
|
branch: str | None = None,
|
|
@@ -503,7 +561,7 @@ def main() -> int:
|
|
|
503
561
|
)
|
|
504
562
|
parser.add_argument("--title", required=True, help="Session title")
|
|
505
563
|
parser.add_argument("--commit", default="-", help="Comma-separated commit hashes")
|
|
506
|
-
parser.add_argument("--summary", default="
|
|
564
|
+
parser.add_argument("--summary", default="Session summary was not supplied.", help="Brief summary")
|
|
507
565
|
parser.add_argument("--content-file", help="Path to file with detailed content")
|
|
508
566
|
parser.add_argument("--package", help="Package name tag (e.g., cli, docs-site)")
|
|
509
567
|
parser.add_argument("--branch", help="Branch name (auto-detected if omitted)")
|
|
@@ -514,7 +572,7 @@ def main() -> int:
|
|
|
514
572
|
|
|
515
573
|
args = parser.parse_args()
|
|
516
574
|
|
|
517
|
-
extra_content =
|
|
575
|
+
extra_content = DEFAULT_MAIN_CHANGES
|
|
518
576
|
if args.content_file:
|
|
519
577
|
content_path = Path(args.content_file)
|
|
520
578
|
if content_path.is_file():
|
|
@@ -543,17 +601,7 @@ def main() -> int:
|
|
|
543
601
|
task_package = task_data.package if task_data else None
|
|
544
602
|
package = resolve_package(task_package, repo_root)
|
|
545
603
|
|
|
546
|
-
|
|
547
|
-
branch = args.branch
|
|
548
|
-
|
|
549
|
-
if not branch:
|
|
550
|
-
if task_data and task_data.raw.get("branch"):
|
|
551
|
-
branch = task_data.raw["branch"]
|
|
552
|
-
else:
|
|
553
|
-
_, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root)
|
|
554
|
-
detected = branch_out.strip()
|
|
555
|
-
if detected:
|
|
556
|
-
branch = detected
|
|
604
|
+
branch = resolve_session_branch(repo_root, args.branch, task_data)
|
|
557
605
|
|
|
558
606
|
return add_session(
|
|
559
607
|
args.title, args.commit, args.summary, extra_content,
|
|
@@ -55,6 +55,7 @@ Platform = Literal[
|
|
|
55
55
|
"droid",
|
|
56
56
|
"pi",
|
|
57
57
|
"trae",
|
|
58
|
+
"omp",
|
|
58
59
|
]
|
|
59
60
|
|
|
60
61
|
|
|
@@ -131,6 +132,8 @@ class CLIAdapter:
|
|
|
131
132
|
return ".pi"
|
|
132
133
|
elif self.platform == "trae":
|
|
133
134
|
return ".trae"
|
|
135
|
+
elif self.platform == "omp":
|
|
136
|
+
return ".omp"
|
|
134
137
|
else:
|
|
135
138
|
return ".claude"
|
|
136
139
|
|
|
@@ -188,6 +191,17 @@ class CLIAdapter:
|
|
|
188
191
|
filename = filename[:-3]
|
|
189
192
|
return prompts_dir / f"trellis-{filename}.md"
|
|
190
193
|
return prompts_dir / Path(*parts)
|
|
194
|
+
if self.platform == "omp":
|
|
195
|
+
commands_dir = self.get_config_dir(project_root) / "commands"
|
|
196
|
+
if not parts:
|
|
197
|
+
return commands_dir
|
|
198
|
+
if len(parts) >= 2 and parts[0] == "trellis":
|
|
199
|
+
filename = parts[-1]
|
|
200
|
+
if filename.endswith(".md"):
|
|
201
|
+
filename = filename[:-3]
|
|
202
|
+
return commands_dir / f"trellis-{filename}.md"
|
|
203
|
+
return commands_dir / Path(*parts)
|
|
204
|
+
|
|
191
205
|
|
|
192
206
|
if self.platform == "devin":
|
|
193
207
|
workflow_dir = self.get_config_dir(project_root) / "workflows"
|
|
@@ -272,6 +286,8 @@ class CLIAdapter:
|
|
|
272
286
|
return f".factory/commands/trellis/{name}.md"
|
|
273
287
|
elif self.platform == "pi":
|
|
274
288
|
return f".pi/prompts/trellis-{name}.md"
|
|
289
|
+
elif self.platform == "omp":
|
|
290
|
+
return f".omp/commands/trellis-{name}.md"
|
|
275
291
|
else:
|
|
276
292
|
return f"{self.config_dir_name}/commands/trellis/{name}.md"
|
|
277
293
|
|
|
@@ -311,6 +327,8 @@ class CLIAdapter:
|
|
|
311
327
|
return {}
|
|
312
328
|
elif self.platform == "trae":
|
|
313
329
|
return {}
|
|
330
|
+
elif self.platform == "omp":
|
|
331
|
+
return {}
|
|
314
332
|
else:
|
|
315
333
|
return {"CLAUDE_NON_INTERACTIVE": "1"}
|
|
316
334
|
|
|
@@ -400,6 +418,10 @@ class CLIAdapter:
|
|
|
400
418
|
raise ValueError(
|
|
401
419
|
"Trae is IDE-only; CLI agent run is not supported."
|
|
402
420
|
)
|
|
421
|
+
elif self.platform == "omp":
|
|
422
|
+
raise ValueError(
|
|
423
|
+
"OMP uses native task tool for agent runs; CLI agent run is not supported."
|
|
424
|
+
)
|
|
403
425
|
|
|
404
426
|
else: # claude
|
|
405
427
|
cmd = ["claude", "-p"]
|
|
@@ -470,6 +492,10 @@ class CLIAdapter:
|
|
|
470
492
|
raise ValueError(
|
|
471
493
|
"Trae is IDE-only; CLI resume is not supported."
|
|
472
494
|
)
|
|
495
|
+
elif self.platform == "omp":
|
|
496
|
+
raise ValueError(
|
|
497
|
+
"OMP uses native task tool for agent runs; CLI resume is not supported."
|
|
498
|
+
)
|
|
473
499
|
else:
|
|
474
500
|
return ["claude", "--resume", session_id]
|
|
475
501
|
|
|
@@ -546,6 +572,8 @@ class CLIAdapter:
|
|
|
546
572
|
return "pi"
|
|
547
573
|
elif self.platform == "trae":
|
|
548
574
|
return "trae"
|
|
575
|
+
elif self.platform == "omp":
|
|
576
|
+
return "omp"
|
|
549
577
|
else:
|
|
550
578
|
return "claude"
|
|
551
579
|
|
|
@@ -642,9 +670,10 @@ def get_cli_adapter(platform: str = "claude") -> CLIAdapter:
|
|
|
642
670
|
"droid",
|
|
643
671
|
"pi",
|
|
644
672
|
"trae",
|
|
673
|
+
"omp",
|
|
645
674
|
):
|
|
646
675
|
raise ValueError(
|
|
647
|
-
f"Unsupported platform: {platform} (must be 'claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'devin', 'qoder', 'codebuddy', 'copilot', 'droid', 'pi', or '
|
|
676
|
+
f"Unsupported platform: {platform} (must be 'claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'devin', 'qoder', 'codebuddy', 'copilot', 'droid', 'pi', 'trae', or 'omp')"
|
|
648
677
|
)
|
|
649
678
|
|
|
650
679
|
return CLIAdapter(platform=platform) # type: ignore
|
|
@@ -668,6 +697,7 @@ _ALL_PLATFORM_CONFIG_DIRS = (
|
|
|
668
697
|
".factory",
|
|
669
698
|
".pi",
|
|
670
699
|
".trae",
|
|
700
|
+
".omp",
|
|
671
701
|
)
|
|
672
702
|
"""Platform-specific config directory names used by detect_platform exclusion
|
|
673
703
|
checks. `.agents/skills/` is NOT listed here: it is a shared cross-platform
|
|
@@ -737,6 +767,7 @@ def detect_platform(project_root: Path) -> Platform:
|
|
|
737
767
|
"droid",
|
|
738
768
|
"pi",
|
|
739
769
|
"trae",
|
|
770
|
+
"omp",
|
|
740
771
|
):
|
|
741
772
|
return env_platform # type: ignore
|
|
742
773
|
|
|
@@ -817,6 +848,10 @@ def detect_platform(project_root: Path) -> Platform:
|
|
|
817
848
|
if (project_root / ".trae").is_dir():
|
|
818
849
|
return "trae"
|
|
819
850
|
|
|
851
|
+
# Check for .omp directory (OMP-specific)
|
|
852
|
+
if (project_root / ".omp").is_dir():
|
|
853
|
+
return "omp"
|
|
854
|
+
|
|
820
855
|
# Fallback: checkout only has the Codex shared-skills layer
|
|
821
856
|
# (.agents/skills/trellis-* dirs) and no explicit platform config dir.
|
|
822
857
|
# Happens on fresh clones where .codex/ is gitignored/absent but the
|
|
@@ -167,6 +167,7 @@ def _next_content_line(lines: list[str], start: int) -> tuple[int, str]:
|
|
|
167
167
|
DEFAULT_SESSION_COMMIT_MESSAGE = "chore: record journal"
|
|
168
168
|
DEFAULT_MAX_JOURNAL_LINES = 2000
|
|
169
169
|
DEFAULT_SESSION_AUTO_COMMIT = True
|
|
170
|
+
DEFAULT_CODEX_DISPATCH_MODE = "inline"
|
|
170
171
|
|
|
171
172
|
CONFIG_FILE = "config.yaml"
|
|
172
173
|
|
|
@@ -243,6 +244,34 @@ def get_session_auto_commit(repo_root: Path | None = None) -> bool:
|
|
|
243
244
|
return DEFAULT_SESSION_AUTO_COMMIT
|
|
244
245
|
|
|
245
246
|
|
|
247
|
+
def get_codex_dispatch_mode(repo_root: Path | None = None) -> str:
|
|
248
|
+
"""Return Codex dispatch mode.
|
|
249
|
+
|
|
250
|
+
Default is ``inline``. ``sub-agent`` is an explicit opt-in because Codex
|
|
251
|
+
sub-agents do not inherit the parent session context.
|
|
252
|
+
"""
|
|
253
|
+
config = _load_config(repo_root)
|
|
254
|
+
codex = config.get("codex")
|
|
255
|
+
if codex is None:
|
|
256
|
+
return DEFAULT_CODEX_DISPATCH_MODE
|
|
257
|
+
if not isinstance(codex, dict):
|
|
258
|
+
print(
|
|
259
|
+
f"[WARN] invalid codex config: {codex!r}; using {DEFAULT_CODEX_DISPATCH_MODE}",
|
|
260
|
+
file=sys.stderr,
|
|
261
|
+
)
|
|
262
|
+
return DEFAULT_CODEX_DISPATCH_MODE
|
|
263
|
+
|
|
264
|
+
raw = codex.get("dispatch_mode", DEFAULT_CODEX_DISPATCH_MODE)
|
|
265
|
+
mode = str(raw).strip().lower()
|
|
266
|
+
if mode in ("inline", "sub-agent"):
|
|
267
|
+
return mode
|
|
268
|
+
print(
|
|
269
|
+
f"[WARN] invalid codex.dispatch_mode value: {raw!r}; using {DEFAULT_CODEX_DISPATCH_MODE}",
|
|
270
|
+
file=sys.stderr,
|
|
271
|
+
)
|
|
272
|
+
return DEFAULT_CODEX_DISPATCH_MODE
|
|
273
|
+
|
|
274
|
+
|
|
246
275
|
def get_hooks(event: str, repo_root: Path | None = None) -> list[str]:
|
|
247
276
|
"""Get hook commands for a lifecycle event.
|
|
248
277
|
|
|
@@ -8,6 +8,8 @@ for JSON file operations across all Trellis scripts.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import json
|
|
11
|
+
import os
|
|
12
|
+
import tempfile
|
|
11
13
|
from pathlib import Path
|
|
12
14
|
|
|
13
15
|
|
|
@@ -25,13 +27,28 @@ def read_json(path: Path) -> dict | None:
|
|
|
25
27
|
def write_json(path: Path, data: dict) -> bool:
|
|
26
28
|
"""Write dict to JSON file with pretty formatting.
|
|
27
29
|
|
|
30
|
+
The write is atomic: content goes to a temp file in the same directory
|
|
31
|
+
and is then renamed over the target. A crash or Ctrl-C mid-write leaves
|
|
32
|
+
the existing file intact rather than truncated, so a corrupted task.json
|
|
33
|
+
can never make a task silently vanish from `task.py list`.
|
|
34
|
+
|
|
28
35
|
Returns True on success, False on error.
|
|
29
36
|
"""
|
|
37
|
+
payload = json.dumps(data, indent=2, ensure_ascii=False)
|
|
30
38
|
try:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
encoding="utf-8",
|
|
39
|
+
fd, tmp = tempfile.mkstemp(
|
|
40
|
+
dir=str(path.parent), prefix=f".{path.name}.", suffix=".tmp"
|
|
34
41
|
)
|
|
42
|
+
try:
|
|
43
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
44
|
+
f.write(payload)
|
|
45
|
+
os.replace(tmp, path)
|
|
46
|
+
except BaseException:
|
|
47
|
+
try:
|
|
48
|
+
os.unlink(tmp)
|
|
49
|
+
except OSError:
|
|
50
|
+
pass
|
|
51
|
+
raise
|
|
35
52
|
return True
|
|
36
53
|
except (OSError, IOError):
|
|
37
54
|
return False
|
|
@@ -23,6 +23,7 @@ from datetime import datetime
|
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
|
|
25
25
|
from .config import (
|
|
26
|
+
get_codex_dispatch_mode,
|
|
26
27
|
get_packages,
|
|
27
28
|
get_session_auto_commit,
|
|
28
29
|
is_monorepo,
|
|
@@ -50,6 +51,7 @@ from .safe_commit import (
|
|
|
50
51
|
from .task_utils import (
|
|
51
52
|
archive_task_complete,
|
|
52
53
|
find_task_by_name,
|
|
54
|
+
is_within_tasks_dir,
|
|
53
55
|
resolve_task_dir,
|
|
54
56
|
run_task_hooks,
|
|
55
57
|
)
|
|
@@ -113,13 +115,13 @@ def _repo_relative_path(path: Path, repo_root: Path) -> str:
|
|
|
113
115
|
|
|
114
116
|
# Config directories of platforms that consume implement.jsonl / check.jsonl.
|
|
115
117
|
# Keep in sync with src/types/ai-tools.ts AI_TOOLS entries — these are the
|
|
116
|
-
# platforms listed in workflow.md's "agent-capable" Skill Routing block
|
|
117
|
-
#
|
|
118
|
-
# Devin are NOT in this list: they
|
|
118
|
+
# platforms listed in workflow.md's "agent-capable" Skill Routing block.
|
|
119
|
+
# Codex is checked separately because default inline mode does not consume
|
|
120
|
+
# JSONL. Kilo / Antigravity / Devin are NOT in this list either: they load
|
|
121
|
+
# specs through skills instead of JSONL.
|
|
119
122
|
_SUBAGENT_CONFIG_DIRS: tuple[str, ...] = (
|
|
120
123
|
".claude",
|
|
121
124
|
".cursor",
|
|
122
|
-
".codex",
|
|
123
125
|
".kiro",
|
|
124
126
|
".gemini",
|
|
125
127
|
".opencode",
|
|
@@ -129,7 +131,10 @@ _SUBAGENT_CONFIG_DIRS: tuple[str, ...] = (
|
|
|
129
131
|
".github/copilot",
|
|
130
132
|
".pi", # Pi Agent
|
|
131
133
|
".trae", # Trae IDE
|
|
134
|
+
".omp", # Oh My Pi
|
|
135
|
+
".zcode", # ZCode
|
|
132
136
|
)
|
|
137
|
+
_CODEX_CONFIG_DIR = ".codex"
|
|
133
138
|
|
|
134
139
|
_SEED_EXAMPLE = (
|
|
135
140
|
"Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. "
|
|
@@ -142,13 +147,15 @@ _SEED_EXAMPLE = (
|
|
|
142
147
|
def _has_subagent_platform(repo_root: Path) -> bool:
|
|
143
148
|
"""Return True if any sub-agent-capable platform is configured.
|
|
144
149
|
|
|
145
|
-
Detected by probing well-known config directories at the repo root.
|
|
146
|
-
only
|
|
147
|
-
``
|
|
150
|
+
Detected by probing well-known config directories at the repo root. Codex
|
|
151
|
+
only counts when ``codex.dispatch_mode`` explicitly opts into
|
|
152
|
+
``sub-agent``; inline mode loads context through skills, not JSONL.
|
|
148
153
|
"""
|
|
149
154
|
for config_dir in _SUBAGENT_CONFIG_DIRS:
|
|
150
155
|
if (repo_root / config_dir).is_dir():
|
|
151
156
|
return True
|
|
157
|
+
if (repo_root / _CODEX_CONFIG_DIR).is_dir():
|
|
158
|
+
return get_codex_dispatch_mode(repo_root) == "sub-agent"
|
|
152
159
|
return False
|
|
153
160
|
|
|
154
161
|
|
|
@@ -240,6 +247,34 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
240
247
|
# Create task directory with MM-DD-slug format
|
|
241
248
|
tasks_dir = get_tasks_dir(repo_root)
|
|
242
249
|
date_prefix = generate_task_date_prefix()
|
|
250
|
+
|
|
251
|
+
# Guard against date-prefixed --slug (e.g. a full task dir name pasted in),
|
|
252
|
+
# which would otherwise produce MM-DD-MM-DD-slug (issue #377). Only an
|
|
253
|
+
# explicit --slug is guarded; title-derived slugs are left untouched.
|
|
254
|
+
if args.slug:
|
|
255
|
+
m = re.match(r"^(\d{2})-(\d{2})-(.+)$", slug)
|
|
256
|
+
if m and 1 <= int(m.group(1)) <= 12 and 1 <= int(m.group(2)) <= 31:
|
|
257
|
+
slug_prefix = f"{m.group(1)}-{m.group(2)}"
|
|
258
|
+
if slug_prefix == date_prefix:
|
|
259
|
+
slug = m.group(3)
|
|
260
|
+
print(
|
|
261
|
+
colored(
|
|
262
|
+
f'warning: --slug should not include the MM-DD prefix; normalized to "{slug}"',
|
|
263
|
+
Colors.YELLOW,
|
|
264
|
+
),
|
|
265
|
+
file=sys.stderr,
|
|
266
|
+
)
|
|
267
|
+
else:
|
|
268
|
+
print(
|
|
269
|
+
colored(
|
|
270
|
+
f"Error: --slug starts with a date prefix ({slug_prefix}-), but task.py create always uses today's date ({date_prefix}).",
|
|
271
|
+
Colors.RED,
|
|
272
|
+
),
|
|
273
|
+
file=sys.stderr,
|
|
274
|
+
)
|
|
275
|
+
print(f"Pass only the slug body, e.g. --slug {m.group(3)}", file=sys.stderr)
|
|
276
|
+
return 1
|
|
277
|
+
|
|
243
278
|
dir_name = f"{date_prefix}-{slug}"
|
|
244
279
|
task_dir = tasks_dir / dir_name
|
|
245
280
|
task_json_path = task_dir / FILE_TASK_JSON
|
|
@@ -262,11 +297,21 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
262
297
|
_, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root)
|
|
263
298
|
current_branch = branch_out.strip() or "main"
|
|
264
299
|
|
|
300
|
+
description = (args.description or "").strip()
|
|
301
|
+
if not description.strip():
|
|
302
|
+
print(
|
|
303
|
+
colored(
|
|
304
|
+
"warning: task description is empty; pass --description to improve search and later audits.",
|
|
305
|
+
Colors.YELLOW,
|
|
306
|
+
),
|
|
307
|
+
file=sys.stderr,
|
|
308
|
+
)
|
|
309
|
+
|
|
265
310
|
task_data = {
|
|
266
311
|
"id": slug,
|
|
267
312
|
"name": slug,
|
|
268
313
|
"title": args.title,
|
|
269
|
-
"description":
|
|
314
|
+
"description": description,
|
|
270
315
|
"status": "planning",
|
|
271
316
|
"dev_type": None,
|
|
272
317
|
"scope": None,
|
|
@@ -294,7 +339,7 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
294
339
|
prd_path = task_dir / "prd.md"
|
|
295
340
|
if not prd_path.exists():
|
|
296
341
|
prd_path.write_text(
|
|
297
|
-
_default_prd_content(args.title,
|
|
342
|
+
_default_prd_content(args.title, description),
|
|
298
343
|
encoding="utf-8",
|
|
299
344
|
)
|
|
300
345
|
|
|
@@ -337,16 +382,31 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
337
382
|
# outside an AI session) — the task is still created, the user can run
|
|
338
383
|
# task.py start later. Pointer is session-scoped so this never affects
|
|
339
384
|
# other AI sessions.
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
385
|
+
if getattr(args, "no_start", False):
|
|
386
|
+
print(
|
|
387
|
+
colored(
|
|
388
|
+
"Skipped session activation (--no-start); run task.py start when ready.",
|
|
389
|
+
Colors.YELLOW,
|
|
390
|
+
),
|
|
391
|
+
file=sys.stderr,
|
|
392
|
+
)
|
|
393
|
+
else:
|
|
394
|
+
try:
|
|
395
|
+
from .active_task import resolve_context_key, set_active_task
|
|
396
|
+
if resolve_context_key():
|
|
397
|
+
try:
|
|
398
|
+
rel_dir = task_dir.relative_to(repo_root).as_posix()
|
|
399
|
+
except ValueError:
|
|
400
|
+
rel_dir = str(task_dir)
|
|
401
|
+
active = set_active_task(rel_dir, repo_root)
|
|
402
|
+
if active:
|
|
403
|
+
print(
|
|
404
|
+
colored(f"Activated task for this session: {active.task_path}", Colors.GREEN),
|
|
405
|
+
file=sys.stderr,
|
|
406
|
+
)
|
|
407
|
+
print(f"Source: {active.source}", file=sys.stderr)
|
|
408
|
+
except Exception:
|
|
409
|
+
pass
|
|
350
410
|
|
|
351
411
|
print(colored(f"Created task: {dir_name}", Colors.GREEN), file=sys.stderr)
|
|
352
412
|
print("", file=sys.stderr)
|
|
@@ -396,6 +456,17 @@ def cmd_archive(args: argparse.Namespace) -> int:
|
|
|
396
456
|
print(f" - {t.dir_name}/", file=sys.stderr)
|
|
397
457
|
return 1
|
|
398
458
|
|
|
459
|
+
# Refuse to archive anything that isn't a real task directly under
|
|
460
|
+
# .trellis/tasks/. A mistyped name (e.g. "src") resolves to repo_root/src,
|
|
461
|
+
# which is a dir but not a task — without this guard archive would move the
|
|
462
|
+
# user's source directory out of the repo.
|
|
463
|
+
if not is_within_tasks_dir(task_dir, repo_root):
|
|
464
|
+
print(colored(
|
|
465
|
+
f"Error: refusing to archive '{task_name}': "
|
|
466
|
+
f"{task_dir} is not a task under {tasks_dir}",
|
|
467
|
+
Colors.RED), file=sys.stderr)
|
|
468
|
+
return 1
|
|
469
|
+
|
|
399
470
|
dir_name = task_dir.name
|
|
400
471
|
task_json_path = task_dir / FILE_TASK_JSON
|
|
401
472
|
|
|
@@ -69,6 +69,28 @@ def is_safe_task_path(task_path: str, repo_root: Path | None = None) -> bool:
|
|
|
69
69
|
return True
|
|
70
70
|
|
|
71
71
|
|
|
72
|
+
def is_within_tasks_dir(task_dir_abs: Path, repo_root: Path | None = None) -> bool:
|
|
73
|
+
"""Check that a resolved task directory really is a task under the tasks dir.
|
|
74
|
+
|
|
75
|
+
A real task lives directly at ``.trellis/tasks/<name>``. This returns True
|
|
76
|
+
only when ``task_dir_abs`` is an immediate child of the tasks directory.
|
|
77
|
+
|
|
78
|
+
Guards archive: ``resolve_task_dir`` falls back to ``repo_root/<name>`` for
|
|
79
|
+
an unknown name, so a mistyped ``task.py archive src`` resolves to the real
|
|
80
|
+
``src/`` source directory. Without this check archive would ``shutil.move``
|
|
81
|
+
it out of the repo. Also rejects the tasks dir itself and anything nested
|
|
82
|
+
under ``archive/`` (already-archived tasks).
|
|
83
|
+
"""
|
|
84
|
+
if repo_root is None:
|
|
85
|
+
repo_root = get_repo_root()
|
|
86
|
+
try:
|
|
87
|
+
resolved = task_dir_abs.resolve()
|
|
88
|
+
tasks_resolved = get_tasks_dir(repo_root).resolve()
|
|
89
|
+
except (OSError, RuntimeError):
|
|
90
|
+
return False
|
|
91
|
+
return resolved.parent == tasks_resolved
|
|
92
|
+
|
|
93
|
+
|
|
72
94
|
# =============================================================================
|
|
73
95
|
# Task Lookup
|
|
74
96
|
# =============================================================================
|