@mindfoldhq/trellis 0.6.5 → 0.6.6

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.
Files changed (86) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/index.js +1 -0
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/commands/channel/supervisor/stdout.d.ts +7 -5
  5. package/dist/commands/channel/supervisor/stdout.d.ts.map +1 -1
  6. package/dist/commands/channel/supervisor/stdout.js +65 -18
  7. package/dist/commands/channel/supervisor/stdout.js.map +1 -1
  8. package/dist/commands/channel/supervisor.d.ts +13 -0
  9. package/dist/commands/channel/supervisor.d.ts.map +1 -1
  10. package/dist/commands/channel/supervisor.js +36 -16
  11. package/dist/commands/channel/supervisor.js.map +1 -1
  12. package/dist/commands/init.d.ts +1 -0
  13. package/dist/commands/init.d.ts.map +1 -1
  14. package/dist/commands/init.js.map +1 -1
  15. package/dist/commands/uninstall.d.ts.map +1 -1
  16. package/dist/commands/uninstall.js +7 -1
  17. package/dist/commands/uninstall.js.map +1 -1
  18. package/dist/commands/update.d.ts.map +1 -1
  19. package/dist/commands/update.js +60 -31
  20. package/dist/commands/update.js.map +1 -1
  21. package/dist/configurators/copilot.d.ts +1 -0
  22. package/dist/configurators/copilot.d.ts.map +1 -1
  23. package/dist/configurators/copilot.js +4 -1
  24. package/dist/configurators/copilot.js.map +1 -1
  25. package/dist/configurators/index.d.ts.map +1 -1
  26. package/dist/configurators/index.js +7 -1
  27. package/dist/configurators/index.js.map +1 -1
  28. package/dist/configurators/omp.d.ts +11 -0
  29. package/dist/configurators/omp.d.ts.map +1 -0
  30. package/dist/configurators/omp.js +51 -0
  31. package/dist/configurators/omp.js.map +1 -0
  32. package/dist/configurators/shared.d.ts +7 -0
  33. package/dist/configurators/shared.d.ts.map +1 -1
  34. package/dist/configurators/shared.js +27 -0
  35. package/dist/configurators/shared.js.map +1 -1
  36. package/dist/configurators/zcode.d.ts +3 -3
  37. package/dist/configurators/zcode.js +12 -12
  38. package/dist/configurators/zcode.js.map +1 -1
  39. package/dist/migrations/manifests/0.6.6.json +17 -0
  40. package/dist/templates/codex/hooks/session-start.py +6 -6
  41. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-agents.md +1 -1
  42. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-skills-or-commands.md +1 -1
  43. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/bundled-skills.md +2 -1
  44. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/generated-files.md +2 -2
  45. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/agents.md +1 -1
  46. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/overview.md +3 -3
  47. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/platform-map.md +19 -2
  48. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/skills-and-commands.md +1 -1
  49. package/dist/templates/copilot/copilot-instructions.md +11 -0
  50. package/dist/templates/copilot/hooks/session-start.py +6 -6
  51. package/dist/templates/copilot/index.d.ts +6 -1
  52. package/dist/templates/copilot/index.d.ts.map +1 -1
  53. package/dist/templates/copilot/index.js +8 -1
  54. package/dist/templates/copilot/index.js.map +1 -1
  55. package/dist/templates/omp/agents/trellis-check.md +41 -0
  56. package/dist/templates/omp/agents/trellis-implement.md +45 -0
  57. package/dist/templates/omp/agents/trellis-research.md +30 -0
  58. package/dist/templates/omp/extensions/trellis/index.ts.txt +462 -0
  59. package/dist/templates/omp/index.d.ts +4 -0
  60. package/dist/templates/omp/index.d.ts.map +1 -0
  61. package/dist/templates/omp/index.js +9 -0
  62. package/dist/templates/omp/index.js.map +1 -0
  63. package/dist/templates/pi/extensions/trellis/index.ts.txt +47 -21
  64. package/dist/templates/shared-hooks/inject-workflow-state.py +5 -5
  65. package/dist/templates/shared-hooks/session-start.py +7 -7
  66. package/dist/templates/trellis/scripts/add_session.py +65 -17
  67. package/dist/templates/trellis/scripts/common/cli_adapter.py +36 -1
  68. package/dist/templates/trellis/scripts/common/config.py +29 -0
  69. package/dist/templates/trellis/scripts/common/session_context.py +1 -1
  70. package/dist/templates/trellis/scripts/common/task_store.py +78 -19
  71. package/dist/templates/trellis/scripts/task.py +8 -2
  72. package/dist/templates/trellis/workflow.md +13 -13
  73. package/dist/templates/zcode/agents/trellis-research.md +133 -0
  74. package/dist/templates/zcode/index.d.ts +6 -5
  75. package/dist/templates/zcode/index.d.ts.map +1 -1
  76. package/dist/templates/zcode/index.js +6 -5
  77. package/dist/templates/zcode/index.js.map +1 -1
  78. package/dist/types/ai-tools.d.ts +3 -3
  79. package/dist/types/ai-tools.d.ts.map +1 -1
  80. package/dist/types/ai-tools.js +29 -2
  81. package/dist/types/ai-tools.js.map +1 -1
  82. package/dist/utils/uninstall-scrubbers.d.ts +1 -0
  83. package/dist/utils/uninstall-scrubbers.d.ts.map +1 -1
  84. package/dist/utils/uninstall-scrubbers.js +19 -0
  85. package/dist/utils/uninstall-scrubbers.js.map +1 -1
  86. package/package.json +2 -2
@@ -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
- - [OK] (Add test results)
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 = "(Add summary)",
399
- extra_content: str = "(Add details)",
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="(Add summary)", help="Brief summary")
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 = "(Add details)"
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
- # Resolve branch: CLI task.json → git auto-detect → None
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 'trae')"
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
 
@@ -412,7 +412,7 @@ def _get_update_hint(repo_root: Path) -> str | None:
412
412
 
413
413
  return (
414
414
  f"Trellis update available: {current_version} -> {latest_version}, "
415
- "run trellis upgrade"
415
+ "run trellis update"
416
416
  )
417
417
 
418
418
 
@@ -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,
@@ -113,13 +114,13 @@ def _repo_relative_path(path: Path, repo_root: Path) -> str:
113
114
 
114
115
  # Config directories of platforms that consume implement.jsonl / check.jsonl.
115
116
  # 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
- # (Class-1 hook-inject + Class-2 pull-based preludes). Kilo / Antigravity /
118
- # Devin are NOT in this list: they do not consume JSONL.
117
+ # platforms listed in workflow.md's "agent-capable" Skill Routing block.
118
+ # Codex is checked separately because default inline mode does not consume
119
+ # JSONL. Kilo / Antigravity / Devin are NOT in this list either: they load
120
+ # specs through skills instead of JSONL.
119
121
  _SUBAGENT_CONFIG_DIRS: tuple[str, ...] = (
120
122
  ".claude",
121
123
  ".cursor",
122
- ".codex",
123
124
  ".kiro",
124
125
  ".gemini",
125
126
  ".opencode",
@@ -129,7 +130,10 @@ _SUBAGENT_CONFIG_DIRS: tuple[str, ...] = (
129
130
  ".github/copilot",
130
131
  ".pi", # Pi Agent
131
132
  ".trae", # Trae IDE
133
+ ".omp", # Oh My Pi
134
+ ".zcode", # ZCode
132
135
  )
136
+ _CODEX_CONFIG_DIR = ".codex"
133
137
 
134
138
  _SEED_EXAMPLE = (
135
139
  "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. "
@@ -142,13 +146,15 @@ _SEED_EXAMPLE = (
142
146
  def _has_subagent_platform(repo_root: Path) -> bool:
143
147
  """Return True if any sub-agent-capable platform is configured.
144
148
 
145
- Detected by probing well-known config directories at the repo root. Used
146
- only to decide whether ``task.py create`` should seed empty
147
- ``implement.jsonl`` / ``check.jsonl`` files.
149
+ Detected by probing well-known config directories at the repo root. Codex
150
+ only counts when ``codex.dispatch_mode`` explicitly opts into
151
+ ``sub-agent``; inline mode loads context through skills, not JSONL.
148
152
  """
149
153
  for config_dir in _SUBAGENT_CONFIG_DIRS:
150
154
  if (repo_root / config_dir).is_dir():
151
155
  return True
156
+ if (repo_root / _CODEX_CONFIG_DIR).is_dir():
157
+ return get_codex_dispatch_mode(repo_root) == "sub-agent"
152
158
  return False
153
159
 
154
160
 
@@ -240,6 +246,34 @@ def cmd_create(args: argparse.Namespace) -> int:
240
246
  # Create task directory with MM-DD-slug format
241
247
  tasks_dir = get_tasks_dir(repo_root)
242
248
  date_prefix = generate_task_date_prefix()
249
+
250
+ # Guard against date-prefixed --slug (e.g. a full task dir name pasted in),
251
+ # which would otherwise produce MM-DD-MM-DD-slug (issue #377). Only an
252
+ # explicit --slug is guarded; title-derived slugs are left untouched.
253
+ if args.slug:
254
+ m = re.match(r"^(\d{2})-(\d{2})-(.+)$", slug)
255
+ if m and 1 <= int(m.group(1)) <= 12 and 1 <= int(m.group(2)) <= 31:
256
+ slug_prefix = f"{m.group(1)}-{m.group(2)}"
257
+ if slug_prefix == date_prefix:
258
+ slug = m.group(3)
259
+ print(
260
+ colored(
261
+ f'warning: --slug should not include the MM-DD prefix; normalized to "{slug}"',
262
+ Colors.YELLOW,
263
+ ),
264
+ file=sys.stderr,
265
+ )
266
+ else:
267
+ print(
268
+ colored(
269
+ f"Error: --slug starts with a date prefix ({slug_prefix}-), but task.py create always uses today's date ({date_prefix}).",
270
+ Colors.RED,
271
+ ),
272
+ file=sys.stderr,
273
+ )
274
+ print(f"Pass only the slug body, e.g. --slug {m.group(3)}", file=sys.stderr)
275
+ return 1
276
+
243
277
  dir_name = f"{date_prefix}-{slug}"
244
278
  task_dir = tasks_dir / dir_name
245
279
  task_json_path = task_dir / FILE_TASK_JSON
@@ -262,11 +296,21 @@ def cmd_create(args: argparse.Namespace) -> int:
262
296
  _, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root)
263
297
  current_branch = branch_out.strip() or "main"
264
298
 
299
+ description = (args.description or "").strip()
300
+ if not description.strip():
301
+ print(
302
+ colored(
303
+ "warning: task description is empty; pass --description to improve search and later audits.",
304
+ Colors.YELLOW,
305
+ ),
306
+ file=sys.stderr,
307
+ )
308
+
265
309
  task_data = {
266
310
  "id": slug,
267
311
  "name": slug,
268
312
  "title": args.title,
269
- "description": args.description or "",
313
+ "description": description,
270
314
  "status": "planning",
271
315
  "dev_type": None,
272
316
  "scope": None,
@@ -294,7 +338,7 @@ def cmd_create(args: argparse.Namespace) -> int:
294
338
  prd_path = task_dir / "prd.md"
295
339
  if not prd_path.exists():
296
340
  prd_path.write_text(
297
- _default_prd_content(args.title, args.description),
341
+ _default_prd_content(args.title, description),
298
342
  encoding="utf-8",
299
343
  )
300
344
 
@@ -337,16 +381,31 @@ def cmd_create(args: argparse.Namespace) -> int:
337
381
  # outside an AI session) — the task is still created, the user can run
338
382
  # task.py start later. Pointer is session-scoped so this never affects
339
383
  # other AI sessions.
340
- try:
341
- from .active_task import resolve_context_key, set_active_task
342
- if resolve_context_key():
343
- try:
344
- rel_dir = task_dir.relative_to(repo_root).as_posix()
345
- except ValueError:
346
- rel_dir = str(task_dir)
347
- set_active_task(rel_dir, repo_root)
348
- except Exception:
349
- pass
384
+ if getattr(args, "no_start", False):
385
+ print(
386
+ colored(
387
+ "Skipped session activation (--no-start); run task.py start when ready.",
388
+ Colors.YELLOW,
389
+ ),
390
+ file=sys.stderr,
391
+ )
392
+ else:
393
+ try:
394
+ from .active_task import resolve_context_key, set_active_task
395
+ if resolve_context_key():
396
+ try:
397
+ rel_dir = task_dir.relative_to(repo_root).as_posix()
398
+ except ValueError:
399
+ rel_dir = str(task_dir)
400
+ active = set_active_task(rel_dir, repo_root)
401
+ if active:
402
+ print(
403
+ colored(f"Activated task for this session: {active.task_path}", Colors.GREEN),
404
+ file=sys.stderr,
405
+ )
406
+ print(f"Source: {active.source}", file=sys.stderr)
407
+ except Exception:
408
+ pass
350
409
 
351
410
  print(colored(f"Created task: {dir_name}", Colors.GREEN), file=sys.stderr)
352
411
  print("", file=sys.stderr)
@@ -4,7 +4,7 @@
4
4
  Task Management Script.
5
5
 
6
6
  Usage:
7
- python3 task.py create "<title>" [--slug <name>] [--assignee <dev>] [--priority P0|P1|P2|P3] [--parent <dir>] [--package <pkg>]
7
+ python3 task.py create "<title>" [--slug <name>] [--assignee <dev>] [--priority P0|P1|P2|P3] [--parent <dir>] [--package <pkg>] [--no-start]
8
8
  python3 task.py add-context <dir> <file> <path> [reason] # Add jsonl entry
9
9
  python3 task.py validate <dir> # Validate jsonl files
10
10
  python3 task.py list-context <dir> # List jsonl entries
@@ -307,6 +307,7 @@ Usage:
307
307
  python3 task.py create <title> Create new task directory
308
308
  python3 task.py create <title> --package <pkg> Create task for a specific package
309
309
  python3 task.py create <title> --parent <dir> Create task as child of parent
310
+ python3 task.py create <title> --no-start Create without making it active in this session
310
311
  python3 task.py add-context <dir> <jsonl> <path> [reason] Add entry to jsonl
311
312
  python3 task.py validate <dir> Validate jsonl files
312
313
  python3 task.py list-context <dir> List jsonl entries
@@ -392,12 +393,17 @@ def main() -> int:
392
393
  # create
393
394
  p_create = subparsers.add_parser("create", help="Create new task")
394
395
  p_create.add_argument("title", help="Task title")
395
- p_create.add_argument("--slug", "-s", help="Task slug")
396
+ p_create.add_argument("--slug", "-s", help="Task slug without the MM-DD date prefix")
396
397
  p_create.add_argument("--assignee", "-a", help="Assignee developer")
397
398
  p_create.add_argument("--priority", "-p", default="P2", help="Priority (P0-P3)")
398
399
  p_create.add_argument("--description", "-d", help="Task description")
399
400
  p_create.add_argument("--parent", help="Parent task directory (establishes subtask link)")
400
401
  p_create.add_argument("--package", help="Package name for monorepo projects")
402
+ p_create.add_argument(
403
+ "--no-start",
404
+ action="store_true",
405
+ help="Create the task without making it active in this session",
406
+ )
401
407
 
402
408
  # add-context
403
409
  p_add = subparsers.add_parser("add-context", help="Add context entry")