@mindfoldhq/trellis 0.6.7 → 0.6.9

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 (174) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/index.d.ts.map +1 -1
  3. package/dist/cli/index.js +38 -0
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/commands/channel/adapters/codex.d.ts +7 -1
  6. package/dist/commands/channel/adapters/codex.d.ts.map +1 -1
  7. package/dist/commands/channel/adapters/codex.js +19 -2
  8. package/dist/commands/channel/adapters/codex.js.map +1 -1
  9. package/dist/commands/channel/adapters/index.d.ts +3 -1
  10. package/dist/commands/channel/adapters/index.d.ts.map +1 -1
  11. package/dist/commands/channel/adapters/index.js +1 -1
  12. package/dist/commands/channel/adapters/index.js.map +1 -1
  13. package/dist/commands/channel/agent-loader.d.ts +2 -2
  14. package/dist/commands/channel/agent-loader.d.ts.map +1 -1
  15. package/dist/commands/channel/agent-loader.js +9 -5
  16. package/dist/commands/channel/agent-loader.js.map +1 -1
  17. package/dist/commands/channel/context-loader.d.ts +1 -1
  18. package/dist/commands/channel/context-loader.d.ts.map +1 -1
  19. package/dist/commands/channel/context-loader.js +36 -15
  20. package/dist/commands/channel/context-loader.js.map +1 -1
  21. package/dist/commands/channel/context-trust.d.ts +31 -0
  22. package/dist/commands/channel/context-trust.d.ts.map +1 -0
  23. package/dist/commands/channel/context-trust.js +139 -0
  24. package/dist/commands/channel/context-trust.js.map +1 -0
  25. package/dist/commands/channel/index.d.ts.map +1 -1
  26. package/dist/commands/channel/index.js +4 -0
  27. package/dist/commands/channel/index.js.map +1 -1
  28. package/dist/commands/channel/spawn.d.ts +3 -0
  29. package/dist/commands/channel/spawn.d.ts.map +1 -1
  30. package/dist/commands/channel/spawn.js +5 -2
  31. package/dist/commands/channel/spawn.js.map +1 -1
  32. package/dist/commands/channel/supervisor.d.ts +3 -0
  33. package/dist/commands/channel/supervisor.d.ts.map +1 -1
  34. package/dist/commands/channel/supervisor.js +1 -0
  35. package/dist/commands/channel/supervisor.js.map +1 -1
  36. package/dist/commands/init.d.ts +3 -0
  37. package/dist/commands/init.d.ts.map +1 -1
  38. package/dist/commands/init.js.map +1 -1
  39. package/dist/commands/mem.d.ts.map +1 -1
  40. package/dist/commands/mem.js +30 -9
  41. package/dist/commands/mem.js.map +1 -1
  42. package/dist/commands/update.d.ts +12 -1
  43. package/dist/commands/update.d.ts.map +1 -1
  44. package/dist/commands/update.js +75 -7
  45. package/dist/commands/update.js.map +1 -1
  46. package/dist/configurators/codex.d.ts +36 -0
  47. package/dist/configurators/codex.d.ts.map +1 -1
  48. package/dist/configurators/codex.js +111 -13
  49. package/dist/configurators/codex.js.map +1 -1
  50. package/dist/configurators/grok.d.ts +22 -0
  51. package/dist/configurators/grok.d.ts.map +1 -0
  52. package/dist/configurators/grok.js +58 -0
  53. package/dist/configurators/grok.js.map +1 -0
  54. package/dist/configurators/index.d.ts.map +1 -1
  55. package/dist/configurators/index.js +17 -2
  56. package/dist/configurators/index.js.map +1 -1
  57. package/dist/configurators/kimi.d.ts +29 -0
  58. package/dist/configurators/kimi.d.ts.map +1 -0
  59. package/dist/configurators/kimi.js +81 -0
  60. package/dist/configurators/kimi.js.map +1 -0
  61. package/dist/configurators/pi.js +9 -9
  62. package/dist/configurators/pi.js.map +1 -1
  63. package/dist/configurators/shared.d.ts.map +1 -1
  64. package/dist/configurators/shared.js +12 -4
  65. package/dist/configurators/shared.js.map +1 -1
  66. package/dist/configurators/snow.d.ts +31 -0
  67. package/dist/configurators/snow.d.ts.map +1 -0
  68. package/dist/configurators/snow.js +95 -0
  69. package/dist/configurators/snow.js.map +1 -0
  70. package/dist/configurators/workflow.d.ts +14 -0
  71. package/dist/configurators/workflow.d.ts.map +1 -1
  72. package/dist/configurators/workflow.js +37 -1
  73. package/dist/configurators/workflow.js.map +1 -1
  74. package/dist/configurators/zcode.d.ts +9 -4
  75. package/dist/configurators/zcode.d.ts.map +1 -1
  76. package/dist/configurators/zcode.js +41 -12
  77. package/dist/configurators/zcode.js.map +1 -1
  78. package/dist/migrations/manifests/0.5.0-beta.0.json +1 -1
  79. package/dist/migrations/manifests/0.6.8.json +17 -0
  80. package/dist/migrations/manifests/0.6.9.json +9 -0
  81. package/dist/templates/codex/agents/trellis-check.toml +7 -12
  82. package/dist/templates/codex/agents/trellis-implement.toml +7 -12
  83. package/dist/templates/codex/agents/trellis-research.toml +15 -15
  84. package/dist/templates/codex/config.toml +11 -0
  85. package/dist/templates/codex/hooks/session-start.py +9 -3
  86. package/dist/templates/codex/hooks.json +12 -0
  87. package/dist/templates/codex/skills/brainstorm/SKILL.md +40 -13
  88. package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +2 -2
  89. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-skills-or-commands.md +1 -1
  90. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/agents.md +1 -0
  91. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/hooks-and-settings.md +1 -1
  92. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/overview.md +1 -1
  93. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/platform-map.md +10 -4
  94. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/skills-and-commands.md +2 -1
  95. package/dist/templates/common/bundled-skills/trellis-session-insight/SKILL.md +1 -1
  96. package/dist/templates/common/skills/brainstorm.md +40 -13
  97. package/dist/templates/copilot/prompts/brainstorm.prompt.md +40 -13
  98. package/dist/templates/grok/agents/trellis-check.md +105 -0
  99. package/dist/templates/grok/agents/trellis-implement.md +116 -0
  100. package/dist/templates/grok/agents/trellis-research.md +146 -0
  101. package/dist/templates/grok/index.d.ts +16 -0
  102. package/dist/templates/grok/index.d.ts.map +1 -0
  103. package/dist/templates/grok/index.js +19 -0
  104. package/dist/templates/grok/index.js.map +1 -0
  105. package/dist/templates/kimi/agents/trellis-check.md +103 -0
  106. package/dist/templates/kimi/agents/trellis-implement.md +114 -0
  107. package/dist/templates/kimi/agents/trellis-research.md +144 -0
  108. package/dist/templates/kimi/index.d.ts +19 -0
  109. package/dist/templates/kimi/index.d.ts.map +1 -0
  110. package/dist/templates/kimi/index.js +22 -0
  111. package/dist/templates/kimi/index.js.map +1 -0
  112. package/dist/templates/omp/extensions/trellis/index.ts.txt +133 -9
  113. package/dist/templates/opencode/lib/session-utils.js +8 -2
  114. package/dist/templates/opencode/lib/trellis-context.js +263 -43
  115. package/dist/templates/opencode/plugins/inject-subagent-context.js +89 -35
  116. package/dist/templates/opencode/plugins/inject-workflow-state.js +90 -5
  117. package/dist/templates/pi/extensions/trellis/index.ts.txt +278 -24
  118. package/dist/templates/pi/settings.json +0 -3
  119. package/dist/templates/shared-hooks/index.d.ts +11 -5
  120. package/dist/templates/shared-hooks/index.d.ts.map +1 -1
  121. package/dist/templates/shared-hooks/index.js +16 -5
  122. package/dist/templates/shared-hooks/index.js.map +1 -1
  123. package/dist/templates/shared-hooks/inject-subagent-context.py +466 -104
  124. package/dist/templates/shared-hooks/inject-workflow-state.py +86 -29
  125. package/dist/templates/shared-hooks/session-start.py +26 -6
  126. package/dist/templates/snow/SNOW.md +86 -0
  127. package/dist/templates/snow/agents/trellis-check.md +135 -0
  128. package/dist/templates/snow/agents/trellis-implement.md +153 -0
  129. package/dist/templates/snow/agents/trellis-research.md +114 -0
  130. package/dist/templates/snow/hooks/beforeSubAgentStart.json +15 -0
  131. package/dist/templates/snow/hooks/onSessionStart.json +15 -0
  132. package/dist/templates/snow/hooks/onUserMessage.json +15 -0
  133. package/dist/templates/snow/hooks/write-trellis-context.py +644 -0
  134. package/dist/templates/snow/index.d.ts +26 -0
  135. package/dist/templates/snow/index.d.ts.map +1 -0
  136. package/dist/templates/snow/index.js +36 -0
  137. package/dist/templates/snow/index.js.map +1 -0
  138. package/dist/templates/template-utils.d.ts.map +1 -1
  139. package/dist/templates/template-utils.js +5 -1
  140. package/dist/templates/template-utils.js.map +1 -1
  141. package/dist/templates/trellis/config.yaml +55 -7
  142. package/dist/templates/trellis/gitattributes.txt +9 -0
  143. package/dist/templates/trellis/index.d.ts +3 -1
  144. package/dist/templates/trellis/index.d.ts.map +1 -1
  145. package/dist/templates/trellis/index.js +3 -1
  146. package/dist/templates/trellis/index.js.map +1 -1
  147. package/dist/templates/trellis/scripts/add_session.py +94 -28
  148. package/dist/templates/trellis/scripts/common/active_task.py +44 -13
  149. package/dist/templates/trellis/scripts/common/cli_adapter.py +69 -5
  150. package/dist/templates/trellis/scripts/common/config.py +105 -10
  151. package/dist/templates/trellis/scripts/common/git.py +36 -0
  152. package/dist/templates/trellis/scripts/common/io.py +16 -9
  153. package/dist/templates/trellis/scripts/common/task_context.py +101 -7
  154. package/dist/templates/trellis/scripts/common/task_store.py +149 -22
  155. package/dist/templates/trellis/scripts/common/task_utils.py +3 -1
  156. package/dist/templates/trellis/scripts/common/trellis_config.py +2 -1
  157. package/dist/templates/trellis/scripts/common/workflow_phase.py +19 -12
  158. package/dist/templates/trellis/scripts/task.py +106 -10
  159. package/dist/templates/trellis/workflow.md +20 -19
  160. package/dist/templates/zcode/agents/trellis-check.md +7 -0
  161. package/dist/templates/zcode/agents/trellis-implement.md +13 -4
  162. package/dist/templates/zcode/config.json +42 -0
  163. package/dist/templates/zcode/index.d.ts +10 -1
  164. package/dist/templates/zcode/index.d.ts.map +1 -1
  165. package/dist/templates/zcode/index.js +12 -2
  166. package/dist/templates/zcode/index.js.map +1 -1
  167. package/dist/types/ai-tools.d.ts +4 -4
  168. package/dist/types/ai-tools.d.ts.map +1 -1
  169. package/dist/types/ai-tools.js +114 -2
  170. package/dist/types/ai-tools.js.map +1 -1
  171. package/dist/utils/template-fetcher.d.ts.map +1 -1
  172. package/dist/utils/template-fetcher.js +0 -3
  173. package/dist/utils/template-fetcher.js.map +1 -1
  174. package/package.json +2 -2
@@ -9,6 +9,7 @@ Provides:
9
9
  cmd_set_branch - Set git branch for task
10
10
  cmd_set_base_branch - Set PR target branch
11
11
  cmd_set_scope - Set scope for PR title
12
+ cmd_set_meta - Set/overwrite a task metadata key
12
13
  cmd_add_subtask - Link child task to parent
13
14
  cmd_remove_subtask - Unlink child task from parent
14
15
  """
@@ -30,7 +31,7 @@ from .config import (
30
31
  resolve_package,
31
32
  validate_package,
32
33
  )
33
- from .git import run_git
34
+ from .git import branch_exists_locally, resolve_default_branch, run_git
34
35
  from .io import read_json, write_json
35
36
  from .log import Colors, colored
36
37
  from .paths import (
@@ -116,7 +117,7 @@ def _repo_relative_path(path: Path, repo_root: Path) -> str:
116
117
  # Config directories of platforms that consume implement.jsonl / check.jsonl.
117
118
  # Keep in sync with src/types/ai-tools.ts AI_TOOLS entries — these are the
118
119
  # platforms listed in workflow.md's "agent-capable" Skill Routing block.
119
- # Codex is checked separately because default inline mode does not consume
120
+ # Codex is checked separately because explicit inline mode does not consume
120
121
  # JSONL. Kilo / Antigravity / Devin are NOT in this list either: they load
121
122
  # specs through skills instead of JSONL.
122
123
  _SUBAGENT_CONFIG_DIRS: tuple[str, ...] = (
@@ -133,6 +134,8 @@ _SUBAGENT_CONFIG_DIRS: tuple[str, ...] = (
133
134
  ".trae", # Trae IDE
134
135
  ".omp", # Oh My Pi
135
136
  ".zcode", # ZCode
137
+ ".grok", # Grok Build
138
+ ".kimi-code", # Kimi Code
136
139
  )
137
140
  _CODEX_CONFIG_DIR = ".codex"
138
141
 
@@ -148,14 +151,15 @@ def _has_subagent_platform(repo_root: Path) -> bool:
148
151
  """Return True if any sub-agent-capable platform is configured.
149
152
 
150
153
  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.
154
+ counts by default through ``codex.dispatch_mode: auto`` (including the
155
+ legacy ``sub-agent`` alias); explicit inline mode loads context through
156
+ skills, not JSONL.
153
157
  """
154
158
  for config_dir in _SUBAGENT_CONFIG_DIRS:
155
159
  if (repo_root / config_dir).is_dir():
156
160
  return True
157
161
  if (repo_root / _CODEX_CONFIG_DIR).is_dir():
158
- return get_codex_dispatch_mode(repo_root) == "sub-agent"
162
+ return get_codex_dispatch_mode(repo_root) == "auto"
159
163
  return False
160
164
 
161
165
 
@@ -170,6 +174,26 @@ def _write_seed_jsonl(path: Path) -> None:
170
174
  path.write_text(json.dumps(seed, ensure_ascii=False) + "\n", encoding="utf-8")
171
175
 
172
176
 
177
+ def _parse_meta_pairs(pairs: list[str] | None) -> dict[str, str] | None:
178
+ """Parse repeatable ``--meta key=value`` pairs into a dict.
179
+
180
+ Returns ``None`` (after printing an error naming the bad value) on the
181
+ first malformed pair: missing ``=`` or an empty key. Values are stored
182
+ as-is (strings, no nesting, no type coercion).
183
+ """
184
+ meta: dict[str, str] = {}
185
+ for pair in pairs or []:
186
+ key, sep, value = pair.partition("=")
187
+ if not sep or not key:
188
+ print(
189
+ colored(f"Error: malformed --meta value '{pair}' (expected key=value)", Colors.RED),
190
+ file=sys.stderr,
191
+ )
192
+ return None
193
+ meta[key] = value
194
+ return meta
195
+
196
+
173
197
  def _default_prd_content(title: str, description: str | None = None) -> str:
174
198
  """Return the default PRD skeleton created with every task."""
175
199
  goal = (description or "").strip() or "TBD."
@@ -208,6 +232,11 @@ def cmd_create(args: argparse.Namespace) -> int:
208
232
  print(colored("Error: title is required", Colors.RED), file=sys.stderr)
209
233
  return 1
210
234
 
235
+ # Validate --meta (CLI source: fail-fast, before any directory is created)
236
+ meta = _parse_meta_pairs(getattr(args, "meta", None))
237
+ if meta is None:
238
+ return 1
239
+
211
240
  # Validate --package (CLI source: fail-fast)
212
241
  package: str | None = getattr(args, "package", None)
213
242
  if not is_monorepo(repo_root):
@@ -293,9 +322,32 @@ def cmd_create(args: argparse.Namespace) -> int:
293
322
 
294
323
  today = datetime.now().strftime("%Y-%m-%d")
295
324
 
296
- # Record current branch as base_branch (PR target)
325
+ # Record the PR target branch. Prefer the repo's actual default branch
326
+ # (origin/HEAD) so creating a task from a feature branch doesn't
327
+ # mis-stamp that feature branch as the PR target (#399 item 1). Falls
328
+ # back to the checked-out branch when the default can't be resolved
329
+ # (no remote configured, offline, etc.) — the pre-existing behavior.
330
+ # --base-branch lets the caller override both when neither is correct.
297
331
  _, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root)
298
332
  current_branch = branch_out.strip() or "main"
333
+ explicit_base_branch: str | None = getattr(args, "base_branch", None)
334
+ if explicit_base_branch:
335
+ base_branch = explicit_base_branch
336
+ else:
337
+ resolved_base_branch = resolve_default_branch(repo_root)
338
+ if resolved_base_branch:
339
+ base_branch = resolved_base_branch
340
+ else:
341
+ base_branch = current_branch
342
+ print(
343
+ colored(
344
+ f"warning: could not resolve the repository's default branch "
345
+ f"(no remote configured, offline, etc.); stamping base_branch as "
346
+ f"the checked-out branch '{base_branch}'. Pass --base-branch to override.",
347
+ Colors.YELLOW,
348
+ ),
349
+ file=sys.stderr,
350
+ )
299
351
 
300
352
  description = (args.description or "").strip()
301
353
  if not description.strip():
@@ -322,7 +374,7 @@ def cmd_create(args: argparse.Namespace) -> int:
322
374
  "createdAt": today,
323
375
  "completedAt": None,
324
376
  "branch": None,
325
- "base_branch": current_branch,
377
+ "base_branch": base_branch,
326
378
  "worktree_path": None,
327
379
  "commit": None,
328
380
  "pr_url": None,
@@ -331,7 +383,7 @@ def cmd_create(args: argparse.Namespace) -> int:
331
383
  "parent": None,
332
384
  "relatedFiles": [],
333
385
  "notes": "",
334
- "meta": {},
386
+ "meta": meta,
335
387
  }
336
388
 
337
389
  write_json(task_json_path, task_data)
@@ -393,20 +445,46 @@ def cmd_create(args: argparse.Namespace) -> int:
393
445
  else:
394
446
  try:
395
447
  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
448
+ except Exception as exc:
449
+ print(
450
+ colored(f"Warning: session activation unavailable (import failed: {exc})", Colors.YELLOW),
451
+ file=sys.stderr,
452
+ )
453
+ else:
454
+ try:
455
+ context_key = resolve_context_key()
456
+ except Exception as exc:
457
+ print(
458
+ colored(f"Warning: session activation failed (context resolution: {exc})", Colors.YELLOW),
459
+ file=sys.stderr,
460
+ )
461
+ else:
462
+ # No session identity is the normal CLI-outside-an-AI-session
463
+ # case (see comment above) — stay silent, not a failure.
464
+ if context_key:
465
+ try:
466
+ rel_dir = task_dir.relative_to(repo_root).as_posix()
467
+ except ValueError:
468
+ rel_dir = str(task_dir)
469
+ try:
470
+ active = set_active_task(rel_dir, repo_root)
471
+ except Exception as exc:
472
+ print(
473
+ colored(f"Warning: session activation failed (pointer persistence: {exc})", Colors.YELLOW),
474
+ file=sys.stderr,
475
+ )
476
+ else:
477
+ if active:
478
+ print(
479
+ colored(f"Activated task for this session: {active.task_path}", Colors.GREEN),
480
+ file=sys.stderr,
481
+ )
482
+ print(f"Source: {active.source}", file=sys.stderr)
483
+ else:
484
+ print(
485
+ colored("Warning: session activation failed (no pointer returned)", Colors.YELLOW),
486
+ file=sys.stderr,
487
+ )
410
488
 
411
489
  print(colored(f"Created task: {dir_name}", Colors.GREEN), file=sys.stderr)
412
490
  print("", file=sys.stderr)
@@ -478,6 +556,19 @@ def cmd_archive(args: argparse.Namespace) -> int:
478
556
  if task_json_path.is_file():
479
557
  data = read_json(task_json_path)
480
558
  if data:
559
+ # Warn (don't block) when the recorded branch is stale — it was
560
+ # likely already merged and deleted (#399 item 2).
561
+ stored_branch = data.get("branch")
562
+ if stored_branch and not branch_exists_locally(stored_branch, repo_root):
563
+ print(
564
+ colored(
565
+ f"Warning: recorded branch '{stored_branch}' no longer exists locally "
566
+ "(likely merged and deleted).",
567
+ Colors.YELLOW,
568
+ ),
569
+ file=sys.stderr,
570
+ )
571
+
481
572
  data["status"] = "completed"
482
573
  data["completedAt"] = today
483
574
  write_json(task_json_path, data)
@@ -816,3 +907,39 @@ def cmd_set_scope(args: argparse.Namespace) -> int:
816
907
 
817
908
  print(colored(f"✓ Scope set to: {scope}", Colors.GREEN))
818
909
  return 0
910
+
911
+
912
+ # =============================================================================
913
+ # Command: set-meta
914
+ # =============================================================================
915
+
916
+ def cmd_set_meta(args: argparse.Namespace) -> int:
917
+ """Set/overwrite one metadata key on an existing task."""
918
+ repo_root = get_repo_root()
919
+ target_dir = resolve_task_dir(args.dir, repo_root)
920
+ key = args.key
921
+ value = args.value
922
+
923
+ if not key:
924
+ print(colored("Error: Missing arguments", Colors.RED))
925
+ print("Usage: python3 task.py set-meta <task-dir> <key> <value>")
926
+ return 1
927
+
928
+ task_json = target_dir / FILE_TASK_JSON
929
+ if not task_json.is_file():
930
+ print(colored(f"Error: task.json not found at {target_dir}", Colors.RED))
931
+ return 1
932
+
933
+ data = read_json(task_json)
934
+ if not data:
935
+ return 1
936
+
937
+ meta = data.get("meta")
938
+ if not isinstance(meta, dict):
939
+ meta = {}
940
+ meta[key] = value
941
+ data["meta"] = meta
942
+ write_json(task_json, data)
943
+
944
+ print(colored(f"✓ Meta set: {key} = {value}", Colors.GREEN))
945
+ return 0
@@ -88,7 +88,9 @@ def is_within_tasks_dir(task_dir_abs: Path, repo_root: Path | None = None) -> bo
88
88
  tasks_resolved = get_tasks_dir(repo_root).resolve()
89
89
  except (OSError, RuntimeError):
90
90
  return False
91
- return resolved.parent == tasks_resolved
91
+ if resolved.parent != tasks_resolved:
92
+ return False
93
+ return resolved.name != "archive"
92
94
 
93
95
 
94
96
  # =============================================================================
@@ -78,10 +78,11 @@ def _parse_yaml_block(
78
78
  key, _, value = stripped.partition(":")
79
79
  key = key.strip()
80
80
  value = _strip_inline_comment(value).strip()
81
+ was_quoted = len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'")
81
82
  value = _unquote(value)
82
83
  current_list = None
83
84
 
84
- if value:
85
+ if value or was_quoted:
85
86
  target[key] = value
86
87
  i += 1
87
88
  else:
@@ -144,27 +144,34 @@ def _platform_matches(platform: str, block_names: list[str]) -> bool:
144
144
  def resolve_effective_platform(platform: str, config: dict) -> str:
145
145
  """Map ``codex`` to a dispatch-mode-namespaced virtual platform name.
146
146
 
147
- When ``--platform codex`` is passed, return ``"codex-inline"`` (default)
148
- or ``"codex-sub-agent"`` based on ``.trellis/config.yaml`` ``codex.dispatch_mode``.
147
+ When ``--platform codex`` is passed, return ``"codex-sub-agent"`` by
148
+ default or ``"codex-inline"`` when explicitly configured in
149
+ ``.trellis/config.yaml``. ``sub-agent`` remains an alias for ``auto``.
149
150
  ``filter_platform`` then surfaces blocks whose marker lists include the
150
151
  namespaced name (e.g. ``[codex-sub-agent, ...]`` or ``[codex-inline, Kilo,
151
152
  Antigravity, Devin]``).
152
153
 
153
- Default is ``inline`` because Codex sub-agents run with ``fork_turns="none"``
154
- isolation and can't inherit the parent session's task context — inline
155
- keeps the main agent in charge so context isn't lost. Invalid / missing
156
- values also fall back to inline.
154
+ Native Codex context injection supports the ``auto`` default. Invalid
155
+ explicit values fall back to ``inline`` safely; this renderer deliberately
156
+ does not warn because it can run in normal CLI output flows.
157
157
 
158
158
  Other platforms are returned unchanged.
159
159
  """
160
160
  if platform == "codex":
161
- mode = "inline"
161
+ mode = "auto"
162
162
  codex_cfg = config.get("codex") if isinstance(config, dict) else None
163
- if isinstance(codex_cfg, dict):
164
- cfg_mode = codex_cfg.get("dispatch_mode")
165
- if cfg_mode in ("inline", "sub-agent"):
166
- mode = cfg_mode
167
- return f"codex-{mode}"
163
+ if codex_cfg is not None:
164
+ if not isinstance(codex_cfg, dict):
165
+ mode = "inline"
166
+ else:
167
+ cfg_mode = str(codex_cfg.get("dispatch_mode", mode)).strip().lower()
168
+ if cfg_mode == "inline":
169
+ mode = "inline"
170
+ elif cfg_mode in ("auto", "sub-agent"):
171
+ mode = "auto"
172
+ else:
173
+ mode = "inline"
174
+ return "codex-sub-agent" if mode == "auto" else "codex-inline"
168
175
  return platform
169
176
 
170
177
 
@@ -9,11 +9,12 @@ Usage:
9
9
  python3 task.py validate <dir> # Validate jsonl files
10
10
  python3 task.py list-context <dir> # List jsonl entries
11
11
  python3 task.py start <dir> # Set active task
12
- python3 task.py current [--source] # Show active task
12
+ python3 task.py current [--source] [--json] # Show active task
13
13
  python3 task.py finish # Clear active task
14
14
  python3 task.py set-branch <dir> <branch> # Set git branch
15
15
  python3 task.py set-base-branch <dir> <branch> # Set PR target branch
16
16
  python3 task.py set-scope <dir> <scope> # Set scope for PR title
17
+ python3 task.py set-meta <dir> <key> <value> # Set a task metadata key
17
18
  python3 task.py archive <task-dir> # Archive completed task
18
19
  python3 task.py list # List active tasks
19
20
  python3 task.py list-archive [month] # List archived tasks
@@ -24,6 +25,7 @@ Usage:
24
25
  from __future__ import annotations
25
26
 
26
27
  import argparse
28
+ import json
27
29
  import sys
28
30
 
29
31
  from common.log import Colors, colored
@@ -53,6 +55,7 @@ from common.task_store import (
53
55
  cmd_set_branch,
54
56
  cmd_set_base_branch,
55
57
  cmd_set_scope,
58
+ cmd_set_meta,
56
59
  cmd_add_subtask,
57
60
  cmd_remove_subtask,
58
61
  )
@@ -166,6 +169,27 @@ def cmd_current(args: argparse.Namespace) -> int:
166
169
  repo_root = get_repo_root()
167
170
  active = resolve_active_task(repo_root)
168
171
 
172
+ if getattr(args, "json", False):
173
+ task_obj = None
174
+ if active.task_path:
175
+ data = read_json(repo_root / active.task_path / FILE_TASK_JSON) or {}
176
+ task_obj = {
177
+ "dir": active.task_path,
178
+ "id": data.get("id") or data.get("name"),
179
+ "title": data.get("title"),
180
+ "status": data.get("status"),
181
+ "parent": data.get("parent"),
182
+ "children": data.get("children", []),
183
+ "branch": data.get("branch"),
184
+ "base_branch": data.get("base_branch"),
185
+ }
186
+ print(json.dumps({
187
+ "current_task": task_obj,
188
+ "source": active.source,
189
+ "stale": active.stale,
190
+ }, ensure_ascii=False))
191
+ return 0 if active.task_path else 1
192
+
169
193
  if args.source:
170
194
  print(f"Current task: {active.task_path or '(none)'}")
171
195
  print(f"Source: {active.source}")
@@ -184,6 +208,24 @@ def cmd_current(args: argparse.Namespace) -> int:
184
208
  # Command: list
185
209
  # =============================================================================
186
210
 
211
+ def _display_status(t, all_statuses: dict) -> str:
212
+ """Return the status label to show for a task in `list` output.
213
+
214
+ A parent task's stored status stays "planning" until someone runs
215
+ `task.py start` on the parent directly, even while its children are
216
+ actively being worked — a misleading label for anyone scanning the
217
+ list (#399 item 3). Show "active" instead when at least one child is
218
+ past planning; the stored status.json value is left untouched.
219
+ """
220
+ if t.status == "planning" and t.children:
221
+ child_in_flight = any(
222
+ all_statuses.get(c) not in (None, "planning") for c in t.children
223
+ )
224
+ if child_in_flight:
225
+ return "active"
226
+ return t.status
227
+
228
+
187
229
  def cmd_list(args: argparse.Namespace) -> int:
188
230
  """List active tasks."""
189
231
  repo_root = get_repo_root()
@@ -192,6 +234,38 @@ def cmd_list(args: argparse.Namespace) -> int:
192
234
  developer = get_developer(repo_root)
193
235
  filter_mine = args.mine
194
236
  filter_status = args.status
237
+ as_json = getattr(args, "json", False)
238
+
239
+ # Single pass: collect all tasks via shared iterator
240
+ all_tasks = {t.dir_name: t for t in iter_active_tasks(tasks_dir)}
241
+ all_statuses = {name: t.status for name, t in all_tasks.items()}
242
+
243
+ if as_json:
244
+ if filter_mine and not developer:
245
+ print(json.dumps({"error": "No developer set"}), file=sys.stderr)
246
+ return 1
247
+
248
+ items = []
249
+ for dir_name in sorted(all_tasks.keys()):
250
+ t = all_tasks[dir_name]
251
+ if filter_mine and (t.assignee or "-") != developer:
252
+ continue
253
+ if filter_status and t.status != filter_status:
254
+ continue
255
+ items.append({
256
+ "dir": f"{DIR_WORKFLOW}/{DIR_TASKS}/{dir_name}",
257
+ "id": t.raw.get("id") or dir_name,
258
+ "title": t.title,
259
+ "status": t.status,
260
+ "display_status": _display_status(t, all_statuses),
261
+ "priority": t.priority,
262
+ "assignee": t.assignee or None,
263
+ "parent": t.parent,
264
+ "children": list(t.children),
265
+ "package": t.package,
266
+ })
267
+ print(json.dumps({"tasks": items}, ensure_ascii=False))
268
+ return 0
195
269
 
196
270
  if filter_mine:
197
271
  if not developer:
@@ -202,10 +276,6 @@ def cmd_list(args: argparse.Namespace) -> int:
202
276
  print(colored("All active tasks:", Colors.BLUE))
203
277
  print()
204
278
 
205
- # Single pass: collect all tasks via shared iterator
206
- all_tasks = {t.dir_name: t for t in iter_active_tasks(tasks_dir)}
207
- all_statuses = {name: t.status for name, t in all_tasks.items()}
208
-
209
279
  # Display tasks hierarchically
210
280
  count = 0
211
281
 
@@ -228,6 +298,7 @@ def cmd_list(args: argparse.Namespace) -> int:
228
298
 
229
299
  # Children progress
230
300
  progress = children_progress(t.children, all_statuses)
301
+ status_label = _display_status(t, all_statuses)
231
302
 
232
303
  # Package tag
233
304
  pkg_tag = f" @{t.package}" if t.package else ""
@@ -235,9 +306,9 @@ def cmd_list(args: argparse.Namespace) -> int:
235
306
  prefix = " " * indent + " - "
236
307
 
237
308
  if filter_mine:
238
- print(f"{prefix}{dir_name}/ ({t.status}){pkg_tag}{progress}{marker}")
309
+ print(f"{prefix}{dir_name}/ ({status_label}){pkg_tag}{progress}{marker}")
239
310
  else:
240
- print(f"{prefix}{dir_name}/ ({t.status}){pkg_tag}{progress} [{colored(t.assignee or '-', Colors.CYAN)}]{marker}")
311
+ print(f"{prefix}{dir_name}/ ({status_label}){pkg_tag}{progress} [{colored(t.assignee or '-', Colors.CYAN)}]{marker}")
241
312
  count += 1
242
313
 
243
314
  # Print children indented
@@ -245,9 +316,12 @@ def cmd_list(args: argparse.Namespace) -> int:
245
316
  if child_name in all_tasks:
246
317
  _print_task(child_name, indent + 1)
247
318
 
248
- # Display only top-level tasks (those without a parent)
319
+ # Display only top-level tasks: those without a parent, plus orphans
320
+ # whose recorded parent is not (or no longer) in the active set — a
321
+ # dangling parent ref must still render flat instead of disappearing.
249
322
  for dir_name in sorted(all_tasks.keys()):
250
- if not all_tasks[dir_name].parent:
323
+ parent = all_tasks[dir_name].parent
324
+ if not parent or parent not in all_tasks:
251
325
  _print_task(dir_name)
252
326
 
253
327
  if count == 0:
@@ -317,10 +391,11 @@ Usage:
317
391
  python3 task.py set-branch <dir> <branch> Set git branch
318
392
  python3 task.py set-base-branch <dir> <branch> Set PR target branch
319
393
  python3 task.py set-scope <dir> <scope> Set scope for PR title
394
+ python3 task.py set-meta <dir> <key> <value> Set/overwrite a task metadata key
320
395
  python3 task.py archive <task-dir> Archive completed task
321
396
  python3 task.py add-subtask <parent> <child> Link child task to parent
322
397
  python3 task.py remove-subtask <parent> <child> Unlink child from parent
323
- python3 task.py list [--mine] [--status <status>] List tasks
398
+ python3 task.py list [--mine] [--status <status>] [--json] List tasks
324
399
  python3 task.py list-archive [YYYY-MM] List archived tasks
325
400
 
326
401
  Monorepo options:
@@ -329,10 +404,12 @@ Monorepo options:
329
404
  List options:
330
405
  --mine, -m Show only tasks assigned to current developer
331
406
  --status, -s <s> Filter by status (planning, in_progress, review, completed)
407
+ --json Output machine-readable JSON (also available on `current`)
332
408
 
333
409
  Examples:
334
410
  python3 task.py create "Add login feature" --slug add-login
335
411
  python3 task.py create "Add login feature" --slug add-login --package cli
412
+ python3 task.py create "Add login feature" --meta linear=ENG-123 --meta epic=auth
336
413
  python3 task.py create "Child task" --slug child --parent .trellis/tasks/01-21-parent
337
414
  python3 task.py add-context <dir> implement .trellis/spec/cli/backend/auth.md "Auth guidelines"
338
415
  python3 task.py set-branch <dir> task/add-login
@@ -399,6 +476,15 @@ def main() -> int:
399
476
  p_create.add_argument("--description", "-d", help="Task description")
400
477
  p_create.add_argument("--parent", help="Parent task directory (establishes subtask link)")
401
478
  p_create.add_argument("--package", help="Package name for monorepo projects")
479
+ p_create.add_argument(
480
+ "--base-branch",
481
+ help="PR target branch (overrides origin/HEAD detection and the checked-out-branch fallback)",
482
+ )
483
+ p_create.add_argument(
484
+ "--meta",
485
+ action="append",
486
+ help="Task metadata key=value (repeatable)",
487
+ )
402
488
  p_create.add_argument(
403
489
  "--no-start",
404
490
  action="store_true",
@@ -428,6 +514,8 @@ def main() -> int:
428
514
  p_current = subparsers.add_parser("current", help="Show active task")
429
515
  p_current.add_argument("--source", action="store_true",
430
516
  help="Show active task source")
517
+ p_current.add_argument("--json", action="store_true",
518
+ help="Output machine-readable JSON")
431
519
 
432
520
  # finish
433
521
  subparsers.add_parser("finish", help="Clear active task")
@@ -447,6 +535,12 @@ def main() -> int:
447
535
  p_scope.add_argument("dir", help="Task directory")
448
536
  p_scope.add_argument("scope", help="Scope name")
449
537
 
538
+ # set-meta
539
+ p_setmeta = subparsers.add_parser("set-meta", help="Set/overwrite a task metadata key")
540
+ p_setmeta.add_argument("dir", help="Task directory")
541
+ p_setmeta.add_argument("key", help="Metadata key")
542
+ p_setmeta.add_argument("value", help="Metadata value")
543
+
450
544
  # archive
451
545
  p_archive = subparsers.add_parser("archive", help="Archive task")
452
546
  p_archive.add_argument("name", help="Task directory or name")
@@ -456,6 +550,7 @@ def main() -> int:
456
550
  p_list = subparsers.add_parser("list", help="List tasks")
457
551
  p_list.add_argument("--mine", "-m", action="store_true", help="My tasks only")
458
552
  p_list.add_argument("--status", "-s", help="Filter by status")
553
+ p_list.add_argument("--json", action="store_true", help="Output machine-readable JSON")
459
554
 
460
555
  # add-subtask
461
556
  p_addsub = subparsers.add_parser("add-subtask", help="Link child task to parent")
@@ -488,6 +583,7 @@ def main() -> int:
488
583
  "set-branch": cmd_set_branch,
489
584
  "set-base-branch": cmd_set_base_branch,
490
585
  "set-scope": cmd_set_scope,
586
+ "set-meta": cmd_set_meta,
491
587
  "archive": cmd_archive,
492
588
  "add-subtask": cmd_add_subtask,
493
589
  "remove-subtask": cmd_remove_subtask,