Title
Two dispatch-contract mismatches: (a) goal_mode tasks reject kanban_block with kind unset / capability / transient, but the tool schema documents all four kinds and says "Omit only if none apply"; (b) card skills that a worker profile lacks are skipped with only a log-file warning ("Unknown skill(s)"), silently losing the context the card was authored with
Affected version
- hermes-agent 0.18.0 (
pyproject.toml, [project] version = "0.18.0")
- Install: git checkout of
main @ 3ba5ba89c2479b04466a3cb2f6b14b1f6e8f164c (2026-07-06)
- Platform: macOS (darwin), Python 3.11–3.13 per
requires-python
These are two facets of the same problem — the contract the dispatcher/worker actually enforces differs from the contract published to card authors and to the model — so they are filed together; happy to split if preferred.
Part (a): goal_mode block-kind restriction is undocumented and rejects the schema's own default
The logic, reproduced from the install
tools/kanban_tools.py:182:
_GOAL_MODE_BLOCK_ALLOWED_KINDS = frozenset({"dependency", "needs_input"})
tools/kanban_tools.py:701-714 (inside _handle_block; rationale comment at 691-700 cites Issue #38696):
task = kb.get_task(conn, tid)
if (
task
and task.goal_mode
and kind not in _GOAL_MODE_BLOCK_ALLOWED_KINDS
):
conn.close()
return tool_error(
f"goal_mode tasks can only block with kind in "
f"{sorted(_GOAL_MODE_BLOCK_ALLOWED_KINDS)} (got {kind!r}). "
f"If the task is actually finished or cannot proceed for "
f"another reason, call kanban_complete instead — the "
f"completion judge will evaluate it."
)
Note kind defaults to None (kanban_tools.py:682), and None not in {"dependency","needs_input"} — so omitting kind is rejected on goal_mode tasks.
But the tool schema the model sees — KANBAN_BLOCK_SCHEMA, tools/kanban_tools.py:1290-1332 — tells it the opposite:
- The description advertises all four kinds:
'dependency', 'needs_input', 'capability', 'transient' (lines 1292-1303).
- The
kind property enum lists all four (line 1321) and its description ends "Omit only if none apply." (lines 1322-1326).
"required": ["reason"] (line 1330) — kind is optional per the schema.
Neither the schema, nor the enum, nor any shipped doc mentions that goal_mode changes the rules. The full-kind vocabulary is also the board-level contract: hermes_cli/kanban_db.py:125 VALID_BLOCK_KINDS = {"dependency", "needs_input", "capability", "transient"}.
Minimal repro
- Create a goal_mode card and run a worker on it (or set
HERMES_KANBAN_TASK=<id> and call the tool directly).
- Call exactly what the schema permits:
kanban_block(reason="need repo credentials") # kind omitted
kanban_block(reason="no API access", kind="capability")
Expected vs actual
- Expected (per the published schema): the task blocks;
capability surfaces to a human, omitted kind is accepted.
- Actual:
tool_error("goal_mode tasks can only block with kind in ['dependency', 'needs_input'] (got None)..."). A worker whose lane instructions (written against the schema) block without a kind can never block: with goal_mode on, its only exits are a judge-gated kanban_complete or burning the turn budget. In our deployment this made goal_mode effectively unusable on one lane until we found the restriction by reading the source — and note the gate it defers to ("the completion judge will evaluate it") is itself currently a no-op (see our companion report on the kanban_tools.py:605 tuple-unpack bug), so the two gates combined block the honest path and leave the dishonest one open.
Suggested fix
Any of, in preference order:
- Document the restriction where the model can see it: extend
KANBAN_BLOCK_SCHEMA's kind description with the goal_mode rule, and drop/condition the "Omit only if none apply" sentence.
- Treat an omitted
kind on goal_mode tasks as needs_input (coerce) instead of erroring — the schema marks it optional, so the enforcement should meet the schema halfway.
- Reconsider excluding
capability: a genuine hard wall (no credentials, missing access) is precisely a case the worker cannot resolve and the judge cannot fix; forcing it through kanban_complete invites the judge to reject an honest "cannot proceed".
Part (b): unknown card skills are silently dropped (or crash-loop) with nothing on the board
The logic, reproduced from the install
Cards carry a skills column that the dispatcher passes through to the worker command line — hermes_cli/kanban_db.py:878-881 (dataclass field), 7785-7788:
if task.skills:
for sk in task.skills:
if sk:
cmd.extend(["--skills", sk])
The worker is spawned as hermes -p <assignee> --accept-hooks --skills <sk> ... chat -q <prompt> (kanban_db.py:7771-7797) with a profile-scoped HERMES_HOME (kanban_db.py:7752-7769), so skill names resolve against the assignee profile's skill registry, not the dispatcher's.
At worker startup, cli.py:15827-15848 resolves the names via build_preloaded_skills_prompt (agent/skill_commands.py:695-743, which returns (prompt, loaded, missing); a name is missing when _load_skill_payload — skill_commands.py:138 — finds nothing under the trusted skill roots):
if missing_skills:
missing_display = ", ".join(missing_skills)
if loaded_skills:
logger.warning(
"Unknown skill(s) requested, skipping: %s. "
"Continuing with: %s. "
"List available skills with `hermes skills list`.",
...
)
else:
raise ValueError(f"Unknown skill(s): {missing_display}")
Two failure modes when a card names a skill the assignee profile lacks:
- All named skills missing →
ValueError → the worker process dies at startup, on every dispatch retry, until the failure breaker gives up. The card's own metadata (its skills list) makes it undispatchable to that profile, and nothing validates this at kanban_create/dispatch time (kanban_db.py:2494-2537 normalizes and de-dupes skill names but never checks existence).
- Some skills load → a
logger.warning that lands only in the per-task worker log file (kanban_db.py:7798-7809 redirects worker output to <board-root>/logs/<task-id>.log). The run proceeds without the context the card author considered necessary. No kanban_comment, no card annotation, no dispatch-time error — from the board, the run looks fully provisioned. This is silent context loss: the worker produces output that may pass a judge while missing the exact guidance the skill carried.
Minimal repro
hermes kanban create --title "probe" --body "say hi" \
--skill no-such-skill --assignee <any-profile> # mode 1: crash-loops
hermes kanban create --title "probe2" --body "say hi" \
--skill humanizer --skill no-such-skill --assignee <p> # mode 2: silent skip
hermes kanban dispatch
Observed live in our deployment: cards authored with skills=["ai-radar"] dispatched to a profile that didn't have that skill installed → "Unknown skill(s)" on every spawn; the intended skill context was simply absent from every run. Upstream has hit the crash-loop variant itself: kanban_swarm.py hardcoded a nonexistent avoid-ai-writing skill (#29415; regression test at tests/hermes_cli/test_kanban_cli_dispatch_passthrough.py:119-150).
Expected vs actual
- Expected: skill names on a card are validated against the assignee profile's registry when the card is created or at dispatch time; if a worker must skip a skill at runtime, the skip is surfaced on the board (comment/annotation on the card and/or a dispatch failure), not only in a log file.
- Actual: dispatch happily spawns; missing-skill information exists only inside the worker log; partial-miss runs complete as if fully provisioned.
Suggested fix
- Validate
task.skills against the resolved assignee profile's skill registry in dispatch_once before spawning (the dispatcher already resolves the profile-scoped HERMES_HOME, kanban_db.py:7752-7758, so the right registry is knowable). On failure: block the card with a structured reason instead of spawning, mirroring the existing skills shape validation at kanban_db.py:2494-2537.
- When the CLI skips unknown skills at startup in a kanban-worker context (
HERMES_KANBAN_TASK set), emit a kanban_comment on the card (the comment tool already defaults its author from HERMES_PROFILE, set at kanban_db.py:7769) so the skip is board-visible.
- Keep the current hard-fail when all skills are missing, but make the dispatcher recognize that specific startup error as a card-configuration failure (block, don't retry) rather than burning
max_retries on a deterministic crash.
Workaround we deployed
- (a) Updated our lane skill/instructions so goal_mode workers always block with an explicit allowed kind (
kind="needs_input"); this fix had to precede enabling goal_mode on that lane at all.
- (b) Added a fail-fast pre-spawn validator in our own dispatch wrapper that resolves every card-named skill against the target profile before the worker is launched (dispatch dies loudly with the unresolved names instead of spawning), and moved skill installation into the profile bootstrap (pre-install skill sync plus a drift guard) so profiles can't lack the skills cards reference.
Title
Two dispatch-contract mismatches: (a) goal_mode tasks reject
kanban_blockwithkindunset /capability/transient, but the tool schema documents all four kinds and says "Omit only if none apply"; (b) cardskillsthat a worker profile lacks are skipped with only a log-file warning ("Unknown skill(s)"), silently losing the context the card was authored withAffected version
pyproject.toml,[project] version = "0.18.0")main@3ba5ba89c2479b04466a3cb2f6b14b1f6e8f164c(2026-07-06)requires-pythonThese are two facets of the same problem — the contract the dispatcher/worker actually enforces differs from the contract published to card authors and to the model — so they are filed together; happy to split if preferred.
Part (a): goal_mode block-kind restriction is undocumented and rejects the schema's own default
The logic, reproduced from the install
tools/kanban_tools.py:182:tools/kanban_tools.py:701-714(inside_handle_block; rationale comment at 691-700 cites Issue #38696):Note
kinddefaults toNone(kanban_tools.py:682), andNone not in {"dependency","needs_input"}— so omittingkindis rejected on goal_mode tasks.But the tool schema the model sees —
KANBAN_BLOCK_SCHEMA,tools/kanban_tools.py:1290-1332— tells it the opposite:'dependency','needs_input','capability','transient'(lines 1292-1303).kindproperty enum lists all four (line 1321) and its description ends "Omit only if none apply." (lines 1322-1326)."required": ["reason"](line 1330) —kindis optional per the schema.Neither the schema, nor the enum, nor any shipped doc mentions that goal_mode changes the rules. The full-kind vocabulary is also the board-level contract:
hermes_cli/kanban_db.py:125VALID_BLOCK_KINDS = {"dependency", "needs_input", "capability", "transient"}.Minimal repro
HERMES_KANBAN_TASK=<id>and call the tool directly).Expected vs actual
capabilitysurfaces to a human, omitted kind is accepted.tool_error("goal_mode tasks can only block with kind in ['dependency', 'needs_input'] (got None)..."). A worker whose lane instructions (written against the schema) block without a kind can never block: with goal_mode on, its only exits are a judge-gatedkanban_completeor burning the turn budget. In our deployment this made goal_mode effectively unusable on one lane until we found the restriction by reading the source — and note the gate it defers to ("the completion judge will evaluate it") is itself currently a no-op (see our companion report on thekanban_tools.py:605tuple-unpack bug), so the two gates combined block the honest path and leave the dishonest one open.Suggested fix
Any of, in preference order:
KANBAN_BLOCK_SCHEMA'skinddescription with the goal_mode rule, and drop/condition the "Omit only if none apply" sentence.kindon goal_mode tasks asneeds_input(coerce) instead of erroring — the schema marks it optional, so the enforcement should meet the schema halfway.capability: a genuine hard wall (no credentials, missing access) is precisely a case the worker cannot resolve and the judge cannot fix; forcing it throughkanban_completeinvites the judge to reject an honest "cannot proceed".Part (b): unknown card skills are silently dropped (or crash-loop) with nothing on the board
The logic, reproduced from the install
Cards carry a
skillscolumn that the dispatcher passes through to the worker command line —hermes_cli/kanban_db.py:878-881(dataclass field),7785-7788:The worker is spawned as
hermes -p <assignee> --accept-hooks --skills <sk> ... chat -q <prompt>(kanban_db.py:7771-7797) with a profile-scopedHERMES_HOME(kanban_db.py:7752-7769), so skill names resolve against the assignee profile's skill registry, not the dispatcher's.At worker startup,
cli.py:15827-15848resolves the names viabuild_preloaded_skills_prompt(agent/skill_commands.py:695-743, which returns(prompt, loaded, missing); a name ismissingwhen_load_skill_payload—skill_commands.py:138— finds nothing under the trusted skill roots):Two failure modes when a card names a skill the assignee profile lacks:
ValueError→ the worker process dies at startup, on every dispatch retry, until the failure breaker gives up. The card's own metadata (itsskillslist) makes it undispatchable to that profile, and nothing validates this atkanban_create/dispatch time (kanban_db.py:2494-2537normalizes and de-dupes skill names but never checks existence).logger.warningthat lands only in the per-task worker log file (kanban_db.py:7798-7809redirects worker output to<board-root>/logs/<task-id>.log). The run proceeds without the context the card author considered necessary. Nokanban_comment, no card annotation, no dispatch-time error — from the board, the run looks fully provisioned. This is silent context loss: the worker produces output that may pass a judge while missing the exact guidance the skill carried.Minimal repro
Observed live in our deployment: cards authored with
skills=["ai-radar"]dispatched to a profile that didn't have that skill installed → "Unknown skill(s)" on every spawn; the intended skill context was simply absent from every run. Upstream has hit the crash-loop variant itself:kanban_swarm.pyhardcoded a nonexistentavoid-ai-writingskill (#29415; regression test attests/hermes_cli/test_kanban_cli_dispatch_passthrough.py:119-150).Expected vs actual
Suggested fix
task.skillsagainst the resolved assignee profile's skill registry indispatch_oncebefore spawning (the dispatcher already resolves the profile-scopedHERMES_HOME,kanban_db.py:7752-7758, so the right registry is knowable). On failure: block the card with a structured reason instead of spawning, mirroring the existing skills shape validation atkanban_db.py:2494-2537.HERMES_KANBAN_TASKset), emit akanban_commenton the card (the comment tool already defaults its author fromHERMES_PROFILE, set atkanban_db.py:7769) so the skip is board-visible.max_retrieson a deterministic crash.Workaround we deployed
kind="needs_input"); this fix had to precede enabling goal_mode on that lane at all.