Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions hermes_cli/kanban_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -3146,6 +3146,7 @@ def decompose_triage_task(
"body": "...", # optional
"assignee": "profile-name", # optional, None -> default fallback
"parents": [0, 2], # indices into this same children list
"skills": ["skill-name"], # optional list of skill names
}

Returns the list of created child task ids (in input order) on
Expand Down Expand Up @@ -3233,11 +3234,18 @@ def decompose_triage_task(
title = child["title"].strip()
body = child.get("body")
assignee = _canonical_assignee(child.get("assignee"))
# Extract optional skills from the child dict.
raw_skills = child.get("skills")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should mirror create_task's skills normalization: strip the stored value, dedupe, reject comma-containing names, and reject known toolset names so decomposed children behave like tasks created through the normal path.

skills_val = None
if isinstance(raw_skills, list) and raw_skills:
cleaned = [s for s in raw_skills if isinstance(s, str) and s.strip()]
if cleaned:
skills_val = json.dumps(cleaned)
conn.execute(
"INSERT INTO tasks "
"(id, title, body, assignee, status, workspace_kind, "
" tenant, created_at, created_by) "
"VALUES (?, ?, ?, ?, 'todo', 'scratch', ?, ?, ?)",
" tenant, created_at, created_by, skills) "
"VALUES (?, ?, ?, ?, 'todo', 'scratch', ?, ?, ?, ?)",
(
new_id,
title,
Expand All @@ -3246,6 +3254,7 @@ def decompose_triage_task(
tenant,
now,
(author or "decomposer"),
skills_val,
),
)
_append_event(
Expand Down
13 changes: 12 additions & 1 deletion hermes_cli/kanban_decompose.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@
"title": "<concrete task title, imperative voice, <= 80 chars>",
"body": "<detailed spec for the worker on this child task>",
"assignee": "<profile name from the roster, or null for default>",
"parents": [<int>, ...]
"parents": [<int>, ...],
"skills": ["<skill-name>", ...] # optional — skill bundles to force-load
},
...
]
Expand All @@ -89,6 +90,9 @@
and the system will route to the default_assignee.
- Each child task body is what a fresh worker will read with no other
context — be specific about goal, approach, and acceptance criteria.
- Use "skills" to specify skill bundles the worker should force-load
(e.g. ["github-workflow", "test-driven-development"]). Omit if no
special skills are needed.

When the task is genuinely a single unit of work (no useful decomposition),
return:
Expand Down Expand Up @@ -431,11 +435,18 @@ def decompose_task(
parents = []
# Clean parent indices: drop non-int and out-of-range.
clean_parents = [p for p in parents if isinstance(p, int) and 0 <= p < len(raw_tasks) and p != idx]
# Extract optional skills list from the decomposer response.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This keeps the original string rather than s.strip(), so a decomposer response like " translation " would persist a skill name with spaces unless the DB layer normalizes it later.

raw_skills = entry.get("skills")
if isinstance(raw_skills, list):
skills_list = [s for s in raw_skills if isinstance(s, str) and s.strip()]
else:
skills_list = []
children.append({
"title": title.strip()[:200],
"body": body.strip(),
"assignee": chosen,
"parents": clean_parents,
"skills": skills_list,
})

try:
Expand Down