diff --git a/hermes_cli/kanban_db.py b/hermes_cli/kanban_db.py index 5c2d0324083..faea0e41acd 100644 --- a/hermes_cli/kanban_db.py +++ b/hermes_cli/kanban_db.py @@ -2798,6 +2798,7 @@ def decompose_triage_task( root_assignee: Optional[str], children: list[dict], author: Optional[str] = None, + auto_promote: bool = True, ) -> Optional[list[str]]: """Fan a triage task out into child tasks and promote the root to ``todo``. @@ -2983,8 +2984,11 @@ def decompose_triage_task( # Outside the write_txn: promote parent-free children to 'ready' # so the dispatcher picks them up on its next tick. Same pattern - # specify_triage_task uses. - recompute_ready(conn) + # specify_triage_task uses. When auto_promote is False children + # stay in 'todo' until the user manually promotes them — useful + # for manual-review-first workflows. + if auto_promote: + recompute_ready(conn) return child_ids diff --git a/hermes_cli/kanban_decompose.py b/hermes_cli/kanban_decompose.py index 2ebe3f04c6e..cbfaa842b07 100644 --- a/hermes_cli/kanban_decompose.py +++ b/hermes_cli/kanban_decompose.py @@ -271,6 +271,8 @@ def decompose_task( cfg = _load_config() orchestrator = _resolve_orchestrator_profile(cfg) default_assignee = _resolve_default_assignee(cfg) + kanban_cfg = cfg.get("kanban", {}) if isinstance(cfg, dict) else {} + auto_promote = bool(kanban_cfg.get("auto_promote_children", True)) roster, valid_names = _build_roster() try: @@ -410,6 +412,7 @@ def decompose_task( root_assignee=orchestrator, children=children, author=audit_author, + auto_promote=auto_promote, ) except ValueError as exc: return DecomposeOutcome(task_id, False, f"DB rejected graph: {exc}") diff --git a/plugins/kanban/dashboard/plugin_api.py b/plugins/kanban/dashboard/plugin_api.py index 92a9d75366c..f617042fa95 100644 --- a/plugins/kanban/dashboard/plugin_api.py +++ b/plugins/kanban/dashboard/plugin_api.py @@ -1701,6 +1701,7 @@ class OrchestrationSettingsBody(BaseModel): orchestrator_profile: Optional[str] = None default_assignee: Optional[str] = None auto_decompose: Optional[bool] = None + auto_promote_children: Optional[bool] = None @router.get("/orchestration") @@ -1716,6 +1717,7 @@ def get_orchestration_settings(): explicit_orch = (kanban_cfg.get("orchestrator_profile") or "").strip() explicit_default = (kanban_cfg.get("default_assignee") or "").strip() auto_decompose = bool(kanban_cfg.get("auto_decompose", True)) + auto_promote_children = bool(kanban_cfg.get("auto_promote_children", True)) # Resolve fallbacks the same way the decomposer does. resolved_orch = explicit_orch @@ -1738,6 +1740,7 @@ def get_orchestration_settings(): "orchestrator_profile": explicit_orch, "default_assignee": explicit_default, "auto_decompose": auto_decompose, + "auto_promote_children": auto_promote_children, "resolved_orchestrator_profile": resolved_orch, "resolved_default_assignee": resolved_default, "active_profile": active_default, @@ -1803,6 +1806,9 @@ def set_orchestration_settings(payload: OrchestrationSettingsBody): if payload.auto_decompose is not None: kanban_section["auto_decompose"] = bool(payload.auto_decompose) + if payload.auto_promote_children is not None: + kanban_section["auto_promote_children"] = bool(payload.auto_promote_children) + try: save_config(cfg) except Exception as exc: