description: Hermes Kanban's lifecycle and dispatcher, with supercode's session lanes
params:
dispatch_in_gateway: true
dispatch_interval_seconds: 60
dispatch_stale_timeout_seconds: null
failure_limit: 2
claim_ttl_seconds: 900
max_in_progress: null
max_in_progress_per_profile: null
max_spawn: null
default_assignee: null
review_dispatch: true
review_skills:
- sdlc-review
auto_decompose: true
auto_decompose_per_tick: 3
reconcile_orphans: true
block_recurrence_limit: 2
rate_limit_cooldown_seconds: 300
recent_success_seconds: 3600
active_pr_seconds: 86400
blocker_pattern: (?i)\b(quota|rate[\s_-]?limit|429|403|auth\w*|unauthorized|forbidden|billing|subscription|access[\s_]denied|permission[\s_]denied|invalid[\s_]api[\s_]key)\b
pr_pattern: (?i)https?://github\.com/[^/\s]+/[^/\s]+/pull/\d+
session_grace_seconds: 120
goal_mode_default: false
goal_max_turns: 20
heartbeat_quiet_seconds: 1800
managers: []
machine_offline_grace_seconds: 3600
machines: {}
roles:
implementer: card.assignee ?? params.default_assignee
reviewer: card.assignee ?? params.default_assignee
creator: card.created_by
triager: card.assignee ?? params.default_assignee
initial:
- guard: event.initial_status == 'blocked'
target: blocked
actions:
- assign:
field: goal_mode
value: '{{event.goal ?? (event.subtask_of == null && params.goal_mode_default)}}'
- guard: event.triage
target: triage
actions:
- assign:
field: goal_mode
value: '{{event.goal ?? (event.subtask_of == null && params.goal_mode_default)}}'
- guard: '!card.parents.all(p, p.status in [''done'', ''archived''])'
target: todo
actions:
- assign:
field: goal_mode
value: '{{event.goal ?? (event.subtask_of == null && params.goal_mode_default)}}'
- target: ready
actions:
- assign:
field: goal_mode
value: '{{event.goal ?? (event.subtask_of == null && params.goal_mode_default)}}'
statuses:
triage:
stored_as: triage
description: a rough idea waiting to be specified (Hermes asks its auxiliary model; here a session writes it)
run_role: triager
resume_as: triage
todo:
stored_as: todo
description: waiting on its parents
always:
- guard: card.arc != null && card.arc.status in ['done', 'archived']
target: blocked
actions:
- assign:
field: block_kind
value: needs_input
- record:
kind: blocked
payload:
reason: its arc {{card.arc.id}} ended with it open
kind: needs_input
source_status: ready
- guard: card.parents.all(p, p.status in ['done', 'archived'])
target: ready
actions:
- record: promoted
ready:
stored_as: ready
description: waiting for the dispatcher to start it
always:
- guard: card.arc != null && card.arc.status in ['done', 'archived']
target: blocked
actions:
- assign:
field: block_kind
value: needs_input
- record:
kind: blocked
payload:
reason: its arc {{card.arc.id}} ended with it open
kind: needs_input
source_status: ready
- guard: '!card.parents.all(p, p.status in [''done'', ''archived''])'
target: todo
running:
stored_as: running
description: its implementer is working it
run_role: implementer
resume_as: ready
review:
stored_as: review
description: its implementer asked for review
run_role: reviewer
resume_as: review
exit:
- close_session: reviewer
blocked:
stored_as: blocked
description: waiting on a person, a capability, or a transient cause
always:
- guard: card.last_block_event != 'blocked' && card.counters.failures < (card.max_retries ?? params.failure_limit) &&
card.parents.all(p, p.status in ['done', 'archived'])
target: '{{card.resume_status ?? ''ready''}}'
actions:
- record: promoted
scheduled:
stored_as: scheduled
description: set aside until someone brings it back
done:
stored_as: done
description: finished
archived:
stored_as: archived
description: out of the way
dispatch:
serve: params.dispatch_in_gateway
tick: params.dispatch_interval_seconds
start: start
eligible: card.current_run_id == null && (card.claim_lock == null && card.subtask_of == null && !(run.local && (host.memory_pressure
== 'critical' || (host.memory_pressure == 'elevated' && tick.local_started >= 1))))
order:
- 'card.status == ''review'' ? 1 : 0'
- -card.priority
- card.created_at
limits:
- description: the home runs this many cards at most across its boards (kanban.max_in_progress)
group: '''home'''
counts: card.stored_as == 'running'
max: params.max_in_progress ?? host.max_in_progress
reserve:
for: card.status == 'review'
slots: 1
- description: the board runs this many cards at most (kanban.max_spawn)
group: card.board
counts: card.stored_as == 'running'
max: params.max_spawn
- description: the profile's capacity (worker.capacity, else kanban.max_in_progress_per_profile)
group: card.assignee
counts: card.stored_as == 'running'
max: lane.capacity ?? params.max_in_progress_per_profile
- description: the machine's capacity; an idle session on a done or blocked card holds its place until closed (ours)
group: card.machine
counts: card.stored_as == 'running' || card.session_live
max: params.machines[card.machine].capacity
- description: triage sessions started per tick (kanban.auto_decompose_per_tick)
group: '''triage'''
counts: card.status == 'triage' && card.current_run_id != null && card.started_this_tick
max: params.auto_decompose_per_tick
notify:
creator:
completed: '✔ {{card.assignee ? ''@'' + card.assignee + '' '' : ''''}}card {{card.id}} done — {{card.title}}{{event.summary
? ''
'' + cut(event.summary, 300) : ''''}}'
blocked: '⏸ {{card.assignee ? ''@'' + card.assignee + '' '' : ''''}}card {{card.id}} blocked{{event.reason ? '': '' +
cut(event.reason, 300) : ''''}}'
gave_up: '✖ {{card.assignee ? ''@'' + card.assignee + '' '' : ''''}}card {{card.id}} gave up after {{event.failures}}
failures{{event.error ? ''
'' + cut(event.error, 300) : ''''}}'
crashed: '✖ {{card.assignee ? ''@'' + card.assignee + '' '' : ''''}}card {{card.id}}: its worker exited without completing
it; the board will retry'
timed_out: '⏱ {{card.assignee ? ''@'' + card.assignee + '' '' : ''''}}card {{card.id}} timed out (max_runtime {{event.limit_seconds}}s);
the board will retry'
status: '🔄 {{card.assignee ? ''@'' + card.assignee + '' '' : ''''}}card {{card.id}} → {{event.status}}'
block_loop_detected: '🛑 {{card.assignee ? ''@'' + card.assignee + '' '' : ''''}}card {{card.id}} went to triage: blocked
{{event.recurrences}}x for the same cause{{event.reason ? '': '' + cut(event.reason, 200) : ''''}}'
review_requested: '👀 {{card.assignee ? ''@'' + card.assignee + '' '' : ''''}}card {{card.id}} ready for review — {{card.title}}{{event.summary
? ''
'' + cut(event.summary, 300) : ''''}}'
changes_requested: '🛑 card {{card.id}}: review asked for changes: {{cut(event.reason ?? ''reviewer feedback'', 300)}}'
session_started: '▶ {{card.assignee ? ''@'' + card.assignee + '' '' : ''''}}card {{card.id}} — {{card.title}}: the board
started session {{event.session_id}} on {{event.machine}} (pane {{event.pane}}; {{event.address}}).'
session_resumed: '▶ {{card.assignee ? ''@'' + card.assignee + '' '' : ''''}}card {{card.id}} — {{card.title}}: the board
resumed session {{event.session_id}} on {{event.machine}} (pane {{event.pane}}; {{event.address}}).'
session_replaced: '▶ {{card.assignee ? ''@'' + card.assignee + '' '' : ''''}}card {{card.id}} — {{card.title}}: the board
could not resume the card''s last session and started session {{event.session_id}} on {{event.machine}} (pane {{event.pane}};
{{event.address}}).'
session_adopted: '▶ {{card.assignee ? ''@'' + card.assignee + '' '' : ''''}}card {{card.id}} — {{card.title}}: the board
found session {{event.session_id}} still live on {{event.machine}} and kept it ({{event.address}}).'
session_lost: '✖ {{card.assignee ? ''@'' + card.assignee + '' '' : ''''}}card {{card.id}}: session {{event.session_id}}
is gone from {{event.machine}}; the board resumes it, and replaces it from the card only when it cannot.'
spawn_failed: '✖ {{card.assignee ? ''@'' + card.assignee + '' '' : ''''}}card {{card.id}}: its session did not start:
{{cut(event.error, 300)}}'
worker:
commented: '💬 card {{card.id}}, {{event.author}}:
{{comment.body}}'
assigned: card {{card.id}} is now assigned to {{event.assignee ?? 'nobody'}}
machine_set: card {{card.id}} now runs on {{event.machine ?? 'no machine'}}
blocked: '⏸ {{card.assignee ? ''@'' + card.assignee + '' '' : ''''}}card {{card.id}} blocked{{event.reason ? '': '' +
cut(event.reason, 300) : ''''}}'
unblocked: ▶ card {{card.id}} unblocked
completed: '✔ {{card.assignee ? ''@'' + card.assignee + '' '' : ''''}}card {{card.id}} done — {{card.title}}{{event.summary
? ''
'' + cut(event.summary, 300) : ''''}}'
archived: card {{card.id}} archived
changes_requested: '🛑 card {{card.id}}: review asked for changes: {{cut(event.reason ?? ''reviewer feedback'', 300)}}'
linked: 'card {{card.id}}: dependency linked ({{event.parent}} → {{event.child}})'
unlinked: 'card {{card.id}}: dependency unlinked ({{event.parent}} → {{event.child}})'
promoted_manual: card {{card.id}} promoted to ready by {{event.actor}}
subtask_added: 'card {{card.id}} has a new subtask {{event.subtask}}: {{event.title}}'
session_adopted: '▶ card {{card.id}} — {{card.title}} is running again, with you: carry it on from where it stands (`supercode
workflow show {{card.id}}` reads it).'
dependency:
completed: '✔ {{card.assignee ? ''@'' + card.assignee + '' '' : ''''}}card {{card.id}} done — {{card.title}}{{event.summary
? ''
'' + cut(event.summary, 300) : ''''}}'
blocked: '⏸ {{card.assignee ? ''@'' + card.assignee + '' '' : ''''}}card {{card.id}} blocked{{event.reason ? '': '' +
cut(event.reason, 300) : ''''}}'
unblocked: ▶ card {{card.id}} unblocked
review_requested: '👀 {{card.assignee ? ''@'' + card.assignee + '' '' : ''''}}card {{card.id}} ready for review — {{card.title}}{{event.summary
? ''
'' + cut(event.summary, 300) : ''''}}'
changes_requested: '🛑 card {{card.id}}: review asked for changes: {{cut(event.reason ?? ''reviewer feedback'', 300)}}'
commented: '💬 card {{card.id}}, {{event.author}}:
{{comment.body}}'
chat:
completed: '✔ {{card.assignee ? ''@'' + card.assignee + '' '' : ''''}}card {{card.id}} done — {{card.title}}{{event.summary
? ''
'' + cut(event.summary, 300) : ''''}}'
blocked: '⏸ {{card.assignee ? ''@'' + card.assignee + '' '' : ''''}}card {{card.id}} blocked{{event.reason ? '': '' +
cut(event.reason, 300) : ''''}}'
gave_up: '✖ {{card.assignee ? ''@'' + card.assignee + '' '' : ''''}}card {{card.id}} gave up after {{event.failures}}
failures{{event.error ? ''
'' + cut(event.error, 300) : ''''}}'
crashed: '✖ {{card.assignee ? ''@'' + card.assignee + '' '' : ''''}}card {{card.id}}: its worker exited without completing
it; the board will retry'
timed_out: '⏱ {{card.assignee ? ''@'' + card.assignee + '' '' : ''''}}card {{card.id}} timed out (max_runtime {{event.limit_seconds}}s);
the board will retry'
status: '🔄 {{card.assignee ? ''@'' + card.assignee + '' '' : ''''}}card {{card.id}} → {{event.status}}'
block_loop_detected: '🛑 {{card.assignee ? ''@'' + card.assignee + '' '' : ''''}}card {{card.id}} went to triage: blocked
{{event.recurrences}}x for the same cause{{event.reason ? '': '' + cut(event.reason, 200) : ''''}}'
review_requested: '👀 {{card.assignee ? ''@'' + card.assignee + '' '' : ''''}}card {{card.id}} ready for review — {{card.title}}{{event.summary
? ''
'' + cut(event.summary, 300) : ''''}}'
changes_requested: '🛑 card {{card.id}}: review asked for changes: {{cut(event.reason ?? ''reviewer feedback'', 300)}}'
verbs:
specify:
- target: todo
actions:
- specify: '{{event}}'
- record: specified
guard: (card.current_run_id == null) && (true)
from:
- triage
- by:
- implementer
target: todo
actions:
- end_run:
outcome: specified
- specify: '{{event}}'
- record: specified
guard: (card.current_run_id != null) && (true)
from:
- triage
decompose:
- target: todo
actions:
- decompose: '{{event}}'
- record: decomposed
guard: (card.current_run_id == null) && (true)
from:
- triage
- by:
- implementer
target: todo
actions:
- end_run:
outcome: decomposed
- decompose: '{{event}}'
- record: decomposed
guard: (card.current_run_id != null) && (true)
from:
- triage
start:
- by:
- platform
guard: (card.current_run_id == null) && (params.auto_decompose)
target: triage
actions:
- claim: implementer
from:
- triage
- by:
- manager
target: triage
actions:
- claim: implementer
guard: (card.current_run_id == null) && (true)
from:
- triage
- by:
- platform
validate:
- expr: '!(card.last_run.outcome == ''rate_limited'' && now - card.last_run.ended_at < params.rate_limit_cooldown_seconds)'
message: rate_limit_cooldown
- expr: '!matches(card.last_failure_error, params.blocker_pattern)'
message: blocker_auth
- expr: '!(card.last_completed != null && now - card.last_completed.ended_at < params.recent_success_seconds && card.events_since(card.last_completed.ended_at,
[''status'', ''promoted'', ''unblocked'', ''reclaimed'']).size() == 0)'
message: recent_success
- expr: '!card.comments_since(now - params.active_pr_seconds).exists(c, matches(c.body, params.pr_pattern))'
message: active_pr
target: running
actions:
- claim: implementer
from:
- ready
- by:
- platform
guard: (card.current_run_id == null) && (params.review_dispatch)
validate:
- expr: '!(card.last_run.outcome == ''rate_limited'' && now - card.last_run.ended_at < params.rate_limit_cooldown_seconds)'
message: rate_limit_cooldown
- expr: '!matches(card.last_failure_error, params.blocker_pattern)'
message: blocker_auth
target: review
actions:
- claim: reviewer
from:
- review
block:
- by:
- implementer
- manager
description: 'you cannot go on: `"<why>" --kind needs_input|capability|transient|dependency`'
target: blocked
actions:
- end_run:
outcome: blocked
- assign:
field: block_kind
value: '{{event.kind}}'
- record: blocked
guard: (card.current_run_id != null) && (true)
from:
- triage
- guard: event.kind == 'dependency'
target: todo
actions:
- assign:
field: block_kind
value: '{{event.kind}}'
- record: dependency_wait
from:
- ready
- guard: card.block_kind == event.kind && card.counters.block_recurrences + 1 >= params.block_recurrence_limit
target: triage
actions:
- count: block_recurrences
- record: block_loop_detected
from:
- ready
- target: blocked
actions:
- count: block_recurrences
- assign:
field: block_kind
value: '{{event.kind}}'
- record: blocked
from:
- ready
- by:
- implementer
- manager
description: 'you cannot go on and nothing you wait on will wake you (a wait with a known end is a heartbeat `--until`):
`"<why>" --kind needs_input|capability|transient|dependency`'
guard: event.kind == 'dependency'
target: todo
actions:
- end_run:
outcome: blocked
- assign:
field: block_kind
value: '{{event.kind}}'
- record: dependency_wait
from:
- running
- by:
- implementer
- manager
guard: card.block_kind == event.kind && card.counters.block_recurrences + 1 >= params.block_recurrence_limit
target: triage
actions:
- end_run:
outcome: blocked
- count: block_recurrences
- record: block_loop_detected
from:
- running
- by:
- implementer
- manager
target: blocked
actions:
- end_run:
outcome: blocked
- count: block_recurrences
- assign:
field: block_kind
value: '{{event.kind}}'
- record: blocked
from:
- running
- target: blocked
actions:
- assign:
field: block_kind
value: '{{event.kind}}'
- record: blocked
guard: (card.current_run_id == null) && (true)
from:
- review
- by:
- reviewer
- manager
description: 'you cannot go on and nothing you wait on will wake you (a wait with a known end is a heartbeat `--until`):
`"<why>" --kind needs_input|capability|transient|dependency`'
target: blocked
actions:
- end_run:
outcome: blocked
- assign:
field: block_kind
value: '{{event.kind}}'
- record: blocked
guard: (card.current_run_id != null) && (true)
from:
- review
- by:
- implementer
- manager
guard: event.kind == 'dependency'
target: todo
actions:
- assign:
field: block_kind
value: '{{event.kind}}'
- record:
kind: dependency_wait
payload:
reason: '{{event.reason}}'
kind: dependency
source_status: '{{card.resume_status}}'
from:
- blocked
- by:
- implementer
- manager
description: 'the block is another: `"<why>" --kind needs_input|capability|transient|dependency`'
actions:
- assign:
field: block_kind
value: '{{event.kind}}'
- record:
kind: blocked
payload:
reason: '{{event.reason}}'
kind: '{{event.kind}}'
source_status: '{{card.resume_status}}'
from:
- blocked
promote:
- validate:
- expr: event.force || card.parents.all(p, p.status in ['done', 'archived'])
message: parents not done (force to override)
target: ready
actions:
- record: promoted_manual
from:
- todo
- validate:
- expr: event.force || card.parents.all(p, p.status in ['done', 'archived'])
message: parents not done (force to override)
target: ready
actions:
- record: promoted_manual
from:
- blocked
schedule:
- target: scheduled
actions:
- record: scheduled
from:
- todo
- target: scheduled
actions:
- record: scheduled
from:
- ready
- by:
- manager
target: scheduled
actions:
- end_run:
outcome: scheduled
- record: scheduled
from:
- running
- target: scheduled
actions:
- record: scheduled
from:
- blocked
request_review:
- target: review
actions:
- assign:
field: assignee
value: '{{event.reviewer ?? (card.last_reviewer in lanes ? card.last_reviewer : null) ?? card.assignee}}'
- record: review_requested
from:
- ready
- by:
- implementer
- manager
description: 'the work is ready for review: `--summary "<what to review>"`'
validate:
- expr: event.run_id == card.current_run_id || event.force
message: the card is running under a live claim; its worker requests review, or pass --force
target: review
actions:
- end_run:
outcome: review_requested
- assign:
field: assignee
value: '{{event.reviewer ?? (card.last_reviewer in lanes ? card.last_reviewer : null) ?? card.assignee}}'
- record: review_requested
from:
- running
complete:
- validate:
- expr: card.parents.all(p, p.status in ['done', 'archived'])
message: the card waits on parents that are not done
target: done
actions:
- end_run:
outcome: completed
- set_result: '{{event}}'
- reset:
- failures
- block_recurrences
- assign:
field: last_failure_error
value: '{{null}}'
- assign:
field: block_kind
value: '{{null}}'
- cleanup_workspace: null
- record: completed
- check_references: '{{event}}'
from:
- ready
- by:
- implementer
- manager
description: 'the work is done: `--summary "<what you did and how it was verified>"`'
validate:
- expr: event.run_id == null || event.run_id == card.current_run_id
message: that run is no longer the card's current run
- expr: card.parents.all(p, p.status in ['done', 'archived'])
message: the card waits on parents that are not done
target: done
actions:
- end_run:
outcome: completed
- set_result: '{{event}}'
- reset:
- failures
- block_recurrences
- assign:
field: last_failure_error
value: '{{null}}'
- assign:
field: block_kind
value: '{{null}}'
- cleanup_workspace: null
- record: completed
- check_references: '{{event}}'
from:
- running
- by:
- reviewer
- manager
description: 'the work is done: `--summary "<what you did and how it was verified>"`'
validate:
- expr: card.parents.all(p, p.status in ['done', 'archived'])
message: the card waits on parents that are not done
target: done
actions:
- end_run:
outcome: completed
summary: '{{event.summary ?? ''Review approved without additional evidence.''}}'
- set_result: '{{event}}'
- reset:
- failures
- block_recurrences
- assign:
field: last_failure_error
value: '{{null}}'
- assign:
field: block_kind
value: '{{null}}'
- cleanup_workspace: null
- record: completed
- check_references: '{{event}}'
guard: (card.current_run_id == null) && (true)
from:
- review
- by:
- reviewer
- manager
description: 'the work is done: `--summary "<what you did and how it was verified>"`'
validate:
- expr: card.parents.all(p, p.status in ['done', 'archived'])
message: the card waits on parents that are not done
target: done
actions:
- end_run:
outcome: completed
- set_result: '{{event}}'
- reset:
- failures
- block_recurrences
- assign:
field: last_failure_error
value: '{{null}}'
- assign:
field: block_kind
value: '{{null}}'
- cleanup_workspace: null
- record: completed
- check_references: '{{event}}'
guard: (card.current_run_id != null) && (true)
from:
- review
- validate:
- expr: card.parents.all(p, p.status in ['done', 'archived'])
message: the card waits on parents that are not done
target: done
actions:
- set_result: '{{event}}'
- reset:
- failures
- block_recurrences
- assign:
field: last_failure_error
value: '{{null}}'
- assign:
field: block_kind
value: '{{null}}'
- record: completed
- check_references: '{{event}}'
from:
- blocked
reclaim:
- by:
- manager
target: ready
actions:
- end_run:
outcome: reclaimed
- reset:
- failures
- record: reclaimed
from:
- running
request_changes:
- by:
- reviewer
- manager
target: '{{card.parents.all(p, p.status in [''done'', ''archived'']) ? ''ready'' : ''todo''}}'
actions:
- assign:
field: assignee
value: '{{card.implementer}}'
- record: changes_requested
guard: (card.current_run_id == null) && (true)
from:
- review
- by:
- reviewer
- manager
description: 'send it back to its implementer: `"<what must change>"`'
target: '{{card.parents.all(p, p.status in [''done'', ''archived'']) ? ''ready'' : ''todo''}}'
actions:
- end_run:
outcome: changes_requested
- assign:
field: assignee
value: '{{card.implementer}}'
- record: changes_requested
guard: (card.current_run_id != null) && (true)
from:
- review
reopen_review:
- by:
- manager
target: '{{card.parents.all(p, p.status in [''done'', ''archived'']) ? ''ready'' : ''todo''}}'
actions:
- assign:
field: assignee
value: '{{card.implementer}}'
- record: review_reopened
guard: (card.current_run_id == null) && (true)
from:
- review
unblock:
- by:
- implementer
description: 'what you waited on arrived: `--reason "<what arrived>"`'
guard: card.counters.failures < (card.max_retries ?? params.failure_limit)
target: '{{card.parents.all(p, p.status in [''done'', ''archived'']) ? (card.resume_status ?? ''ready'') : ''todo''}}'
actions:
- record: unblocked
from:
- blocked
- target: '{{card.parents.all(p, p.status in [''done'', ''archived'']) ? (card.resume_status ?? ''ready'') : ''todo''}}'
actions:
- reset:
- failures
- assign:
field: last_failure_error
value: '{{null}}'
- record: unblocked
from:
- blocked
- target: ready
actions:
- record: unblocked
from:
- scheduled
edit:
- actions:
- set_result: '{{event}}'
- record: edited
from:
- done
reopen:
- target: '{{card.parents.all(p, p.status in [''done'', ''archived'']) ? ''ready'' : ''todo''}}'
actions:
- invalidate_descendants: null
- record: status
from:
- done
- target: '{{card.parents.all(p, p.status in [''done'', ''archived'']) ? ''ready'' : ''todo''}}'
actions:
- invalidate_descendants: null
- record: status
from:
- archived
comment:
- actions:
- record: commented
assign:
- validate:
- expr: '!(card.status in [''running'', ''reviewing''])'
message: the card is running; reclaim it before reassigning
guard: event.assignee != card.assignee
actions:
- assign:
field: assignee
value: '{{event.assignee}}'
- reset:
- failures
- assign:
field: last_failure_error
value: '{{null}}'
- record: assigned
- actions:
- record: assigned
set_model:
- actions:
- assign:
field: model_override
value: '{{event.model}}'
- assign:
field: provider_override
value: '{{event.provider}}'
- record: model_override_set
set_goal:
- actions:
- assign:
field: goal_mode
value: '{{event.goal}}'
- assign:
field: goal_max_turns
value: '{{event.max_turns}}'
- record: goal_set
link:
- actions:
- link: '{{event.parent}}'
- record: linked
unlink:
- actions:
- unlink: '{{event.parent}}'
- record: unlinked
archive:
- target: archived
actions:
- end_run:
outcome: reclaimed
summary: task archived with run still active
- record: archived
adopt:
- by:
- manager
validate:
- expr: card.status in ['todo', 'ready', 'blocked', 'scheduled', 'triage', 'review']
message: only an open card takes a session
target: running
actions:
- claim: implementer
run:
initial: active
roles:
triager:
role: implementer
session: worker
prompt_file: prompts/triager.md
implementer:
role: implementer
session: worker
limits:
runtime: card.max_runtime_seconds
stale: 'run.surface == ''headless'' ? params.dispatch_stale_timeout_seconds : null'
grace: params.session_grace_seconds
prompt_file: prompts/implementer.md
reviewer:
role: reviewer
session: reviewer
skills:
- sdlc-review
limits:
runtime: card.max_runtime_seconds
grace: params.session_grace_seconds
prompt_file: prompts/reviewer.md
states:
active: {}
triaging_spawn_failed_0: {}
triaging_spawn_failed_1: {}
triaging_session_idle_1: {}
triaging_machine_offline_0: {}
triaging_session_lost_0: {}
triaging_orphaned_0: {}
triaging_exited_0: {}
running_exited_0: {}
running_exited_1: {}
running_exited_2: {}
running_exited_3: {}
running_spawn_failed_0: {}
running_spawn_failed_1: {}
running_timed_out_0: {}
running_timed_out_1: {}
running_orphaned_0: {}
running_stale_0: {}
running_session_lost_0: {}
running_session_lost_1: {}
running_session_lost_2: {}
running_machine_offline_0: {}
running_session_idle_1: {}
reviewing_exited_0: {}
reviewing_spawn_failed_0: {}
reviewing_spawn_failed_1: {}
reviewing_timed_out_0: {}
reviewing_session_lost_0: {}
reviewing_orphaned_0: {}
reviewing_session_idle_1: {}
reviewing_machine_offline_0: {}
'on':
heartbeat:
- by:
- implementer
description: 'still working on a long step, or waiting on something outside (a person, a run): the board leaves you
be for a while, or until the time you name (`--note "<what>" [--until <time|2h>]`)'
guard: card.status == 'triage' && ((card.current_run_id != null) && (true))
actions:
- extend_claim: null
- reset:
- goal_turns
- record: heartbeat
- by:
- implementer
description: 'still working on a long step, or waiting on something outside (a person, a run): the board leaves you
be for a while, or until the time you name (`--note "<what>" [--until <time|2h>]`)'
guard: card.status == 'running' && (true)
actions:
- extend_claim: null
- reset:
- goal_turns
- record: heartbeat
- by:
- reviewer
description: 'still working on a long step, or waiting on something outside (a person, a run): the board leaves you
be for a while, or until the time you name (`--note "<what>" [--until <time|2h>]`)'
guard: card.status == 'review' && ((card.current_run_id != null) && (true))
actions:
- extend_claim: null
- reset:
- goal_turns
- record: heartbeat
spawn_failed:
- by:
- platform
guard: card.status == 'triage' && ((card.current_run_id != null) && (card.counters.failures + 1 >= (card.max_retries
?? params.failure_limit)))
target: triaging_spawn_failed_0
actions:
- end_run:
outcome: spawn_failed
- count: failures
- record: spawn_failed
- by:
- platform
guard: card.status == 'triage' && ((card.current_run_id != null) && (true))
target: triaging_spawn_failed_1
actions:
- end_run:
outcome: spawn_failed
- count: failures
- record: spawn_failed
- by:
- platform
guard: card.status == 'running' && (card.counters.failures + 1 >= (card.max_retries ?? params.failure_limit))
target: running_spawn_failed_0
actions:
- end_run:
outcome: spawn_failed
- count: failures
- record: spawn_failed
- by:
- platform
guard: card.status == 'running' && (true)
target: running_spawn_failed_1
actions:
- end_run:
outcome: spawn_failed
- count: failures
- record: spawn_failed
- by:
- platform
guard: card.status == 'review' && ((card.current_run_id != null) && (card.counters.failures + 1 >= (card.max_retries
?? params.failure_limit)))
target: reviewing_spawn_failed_0
actions:
- end_run:
outcome: spawn_failed
- count: failures
- record: spawn_failed
- by:
- platform
guard: card.status == 'review' && ((card.current_run_id != null) && (true))
target: reviewing_spawn_failed_1
actions:
- end_run:
outcome: spawn_failed
- count: failures
- record: spawn_failed
session_idle:
- by:
- platform
guard: card.status == 'triage' && ((card.current_run_id != null) && (card.last_heartbeat_at != null && now < (card.last_events.heartbeat.payload.until
?? card.last_heartbeat_at + params.heartbeat_quiet_seconds)))
- by:
- platform
guard: card.status == 'triage' && ((card.current_run_id != null) && (event.turns >= (card.goal_max_turns ?? params.goal_max_turns)))
target: triaging_session_idle_1
actions:
- end_run:
outcome: blocked
- assign:
field: block_kind
value: needs_input
- by:
- platform
guard: card.status == 'triage' && ((card.current_run_id != null) && (true))
actions:
- count: goal_turns
- send_to_actor: 'Card {{card.id}} ({{card.title}}) is waiting for you to decide ({{event.turns + 1}} of {{card.goal_max_turns
?? params.goal_max_turns}}).
{{doors}}'
- record:
kind: decision_asked
payload:
turn: '{{event.turns + 1}}'
budget: '{{card.goal_max_turns ?? params.goal_max_turns}}'
- by:
- platform
guard: card.status == 'running' && (card.last_heartbeat_at != null && now < (card.last_events.heartbeat.payload.until
?? card.last_heartbeat_at + params.heartbeat_quiet_seconds))
- by:
- platform
guard: card.status == 'running' && ((card.goal_mode || !event.worked) && event.turns >= (card.goal_max_turns ?? params.goal_max_turns))
target: running_session_idle_1
actions:
- end_run:
outcome: blocked
- assign:
field: block_kind
value: needs_input
- by:
- platform
guard: card.status == 'running' && (card.goal_mode)
actions:
- count: goal_turns
- send_to_actor: 'Card {{card.id}} ({{card.title}}) is not done yet: keep working toward it (goal turn {{event.turns
+ 1}} of {{card.goal_max_turns ?? params.goal_max_turns}}).
{{doors}}'
- record:
kind: goal_continued
payload:
turn: '{{event.turns + 1}}'
budget: '{{card.goal_max_turns ?? params.goal_max_turns}}'
- by:
- platform
guard: card.status == 'running' && (true)
actions:
- reset: '{{event.worked ? [''goal_turns''] : []}}'
- count: goal_turns
- send_to_actor: 'Card {{card.id}} ({{card.title}}) is still running and you are idle: decide ({{(event.worked ? 0 :
event.turns) + 1}} of {{card.goal_max_turns ?? params.goal_max_turns}}). If there is more to do, keep working. A
session that stays idle without deciding has its card blocked for its creator.
{{doors}}'
- record:
kind: decision_asked
payload:
turn: '{{(event.worked ? 0 : event.turns) + 1}}'
budget: '{{card.goal_max_turns ?? params.goal_max_turns}}'
- by:
- platform
guard: card.status == 'review' && ((card.current_run_id != null) && (card.last_heartbeat_at != null && now < (card.last_events.heartbeat.payload.until
?? card.last_heartbeat_at + params.heartbeat_quiet_seconds)))
- by:
- platform
guard: card.status == 'review' && ((card.current_run_id != null) && (event.turns >= (card.goal_max_turns ?? params.goal_max_turns)))
target: reviewing_session_idle_1
actions:
- end_run:
outcome: blocked
- assign:
field: block_kind
value: needs_input
- by:
- platform
guard: card.status == 'review' && ((card.current_run_id != null) && (true))
actions:
- count: goal_turns
- send_to_actor: 'Card {{card.id}} ({{card.title}}) is waiting for you to decide ({{event.turns + 1}} of {{card.goal_max_turns
?? params.goal_max_turns}}).
{{doors}}'
- record:
kind: decision_asked
payload:
turn: '{{event.turns + 1}}'
budget: '{{card.goal_max_turns ?? params.goal_max_turns}}'
machine_offline:
- by:
- platform
guard: card.status == 'triage' && ((card.current_run_id != null) && (event.seconds >= params.machine_offline_grace_seconds))
target: triaging_machine_offline_0
actions:
- end_run:
outcome: reclaimed
- assign:
field: block_kind
value: capability
- by:
- platform
guard: card.status == 'running' && (event.seconds >= params.machine_offline_grace_seconds)
target: running_machine_offline_0
actions:
- end_run:
outcome: reclaimed
- assign:
field: block_kind
value: capability
- by:
- platform
guard: card.status == 'review' && ((card.current_run_id != null) && (event.seconds >= params.machine_offline_grace_seconds))
target: reviewing_machine_offline_0
actions:
- end_run:
outcome: reclaimed
- assign:
field: block_kind
value: capability
session_lost:
- by:
- platform
guard: card.status == 'triage' && ((card.current_run_id != null) && (true))
target: triaging_session_lost_0
actions:
- end_run:
outcome: reclaimed
- record: session_lost
- by:
- platform
guard: card.status == 'running' && (event.never_came_up && card.counters.failures + 1 >= (card.max_retries ?? params.failure_limit))
target: running_session_lost_0
actions:
- end_run:
outcome: reclaimed
- count: failures
- record: session_lost
- by:
- platform
guard: card.status == 'running' && (event.never_came_up)
target: running_session_lost_1
actions:
- end_run:
outcome: reclaimed
- count: failures
- record: session_lost
- by:
- platform
guard: card.status == 'running' && (true)
target: running_session_lost_2
actions:
- end_run:
outcome: reclaimed
- record: session_lost
- by:
- platform
guard: card.status == 'review' && ((card.current_run_id != null) && (true))
target: reviewing_session_lost_0
actions:
- end_run:
outcome: reclaimed
- record: session_lost
orphaned:
- by:
- platform
guard: card.status == 'triage' && ((card.current_run_id != null) && (params.reconcile_orphans))
target: triaging_orphaned_0
actions:
- end_run:
outcome: reclaimed
- record: reconciled
- comment: 'Requeued: the card was running with no claim and nothing live behind it.'
- by:
- platform
guard: card.status == 'running' && (params.reconcile_orphans)
target: running_orphaned_0
actions:
- end_run:
outcome: reclaimed
- record: reconciled
- comment: 'Requeued: the card was running with no claim and nothing live behind it.'
- by:
- platform
guard: card.status == 'review' && ((card.current_run_id != null) && (params.reconcile_orphans))
target: reviewing_orphaned_0
actions:
- end_run:
outcome: reclaimed
- record: reconciled
- comment: 'Requeued: the card was running with no claim and nothing live behind it.'
exited:
- by:
- platform
guard: card.status == 'triage' && ((card.current_run_id != null) && (true))
target: triaging_exited_0
actions:
- end_run:
outcome: crashed
- record: crashed
- by:
- platform
guard: card.status == 'running' && (event.code == 75)
target: running_exited_0
actions:
- end_run:
outcome: rate_limited
- record: rate_limited
- by:
- platform
guard: card.status == 'running' && (event.code == 0)
target: running_exited_1
actions:
- end_run:
outcome: protocol_violation
- count: failures
- record: crashed
- by:
- platform
guard: card.status == 'running' && (card.counters.failures + 1 >= (card.max_retries ?? params.failure_limit))
target: running_exited_2
actions:
- end_run:
outcome: '{{event.code == 0 ? ''protocol_violation'' : ''crashed''}}'
- count: failures
- record: crashed
- by:
- platform
guard: card.status == 'running' && (true)
target: running_exited_3
actions:
- end_run:
outcome: '{{event.code == 0 ? ''protocol_violation'' : ''crashed''}}'
- count: failures
- record: crashed
- by:
- platform
guard: card.status == 'review' && ((card.current_run_id != null) && (true))
target: reviewing_exited_0
actions:
- end_run:
outcome: crashed
- count: failures
- record: crashed
timed_out:
- by:
- platform
guard: card.status == 'running' && (card.counters.failures + 1 >= (card.max_retries ?? params.failure_limit))
target: running_timed_out_0
actions:
- stop_run: null
- end_run:
outcome: timed_out
- count: failures
- record: timed_out
- by:
- platform
guard: card.status == 'running' && (true)
target: running_timed_out_1
actions:
- stop_run: null
- end_run:
outcome: timed_out
- count: failures
- record: timed_out
- by:
- platform
guard: card.status == 'review' && ((card.current_run_id != null) && (true))
target: reviewing_timed_out_0
actions:
- stop_run: null
- end_run:
outcome: timed_out
- count: failures
- record: timed_out
stale:
- by:
- platform
guard: card.status == 'running' && (true)
target: running_stale_0
actions:
- stop_run: null
- end_run:
outcome: stale
- record: stale
outcomes:
triaging_spawn_failed_0:
- target: blocked
actions:
- record: gave_up
triaging_spawn_failed_1:
- target: triage
actions: []
triaging_session_idle_1:
- target: blocked
actions:
- record:
kind: blocked
payload:
reason: its triage session went idle {{event.turns}} times without deciding; review it
kind: needs_input
source_status: triage
triaging_machine_offline_0:
- target: blocked
actions:
- record:
kind: blocked
payload:
reason: machine {{event.machine}} has been off the fleet for {{event.seconds}}s
kind: capability
source_status: triage
triaging_session_lost_0:
- target: triage
actions: []
triaging_orphaned_0:
- target: triage
actions: []
triaging_exited_0:
- target: triage
actions: []
running_exited_0:
- target: ready
actions: []
running_exited_1:
- target: blocked
actions:
- record: gave_up
running_exited_2:
- target: blocked
actions:
- record: gave_up
running_exited_3:
- target: ready
actions: []
running_spawn_failed_0:
- target: blocked
actions:
- record: gave_up
running_spawn_failed_1:
- target: ready
actions: []
running_timed_out_0:
- target: blocked
actions:
- record: gave_up
running_timed_out_1:
- target: ready
actions: []
running_orphaned_0:
- target: ready
actions: []
running_stale_0:
- target: ready
actions: []
running_session_lost_0:
- target: blocked
actions:
- record: gave_up
running_session_lost_1:
- target: ready
actions: []
running_session_lost_2:
- target: ready
actions: []
running_machine_offline_0:
- target: blocked
actions:
- record:
kind: blocked
payload:
reason: machine {{event.machine}} has been off the fleet for {{event.seconds}}s
kind: capability
source_status: ready
running_session_idle_1:
- target: blocked
actions:
- record:
kind: blocked
payload:
reason: its session went idle {{event.turns}} times without deciding done or blocked; review it
kind: needs_input
source_status: ready
reviewing_exited_0:
- target: review
actions: []
reviewing_spawn_failed_0:
- target: blocked
actions:
- record: gave_up
reviewing_spawn_failed_1:
- target: review
actions: []
reviewing_timed_out_0:
- target: review
actions: []
reviewing_session_lost_0:
- target: review
actions: []
reviewing_orphaned_0:
- target: review
actions: []
reviewing_session_idle_1:
- target: blocked
actions:
- record:
kind: blocked
payload:
reason: its reviewer went idle {{event.turns}} times without deciding; review it
kind: needs_input
source_status: review
reviewing_machine_offline_0:
- target: blocked
actions:
- record:
kind: blocked
payload:
reason: machine {{event.machine}} has been off the fleet for {{event.seconds}}s
kind: capability
source_status: review
stored_as:
- guard: card.current_run_id != null
status: running
losses:
- role-specific prompts
- machines, panes and resume
- run supervision nudges
- notice subscriptions
- pause flag lowers to blocked
- archived loses done versus cancelled