ADR-0027: iris task + iris goal CLI — implementation shape¶
Status: Accepted (2026-06-10) Context: Personal-assistant upgrade Phase 2 Track 2A. Parent ADRs: ADR-0005 (Task vocabulary + soft migration), ADR-0014 (Tasks subsystem implementation shape).
Context¶
Phase 0 Track 5 (ADR-0014) shipped src/iris_harness/tasks/ with Task,
Goal, TaskStore, dedup_keys, and typed event payloads. The
store had no user-facing surface — there was no way for a human to
inspect what producer skills (email-followup in Track 2B,
finance-bills in Phase 4, calendar-prep in Phase 6) were writing.
Track 2A adds that surface: two Typer subgroups (iris task and
iris goal) covering CRUD, the manual-grouping mechanism from
ADR-0005, and goal-task linkage rendered for show. This ADR
records the non-obvious calls made while implementing those CLIs.
Decision¶
Ten implementation decisions:
-
Inline in
src/iris_harness/main.pyunder two newtyper.Typersubapps (task_app,goal_app). Matches the existing pattern foremail_app,vault_app,auth_app. Rejected: a newsrc/iris_harness/cli/task_cmds.pymodule — would be the first CLI subgroup not living inline; consistency wins until main.py itself needs to be split. -
UUID prefix resolution at the CLI boundary — accept any prefix ≥ 4 chars, resolve to a unique row via SQL
LIKE 'prefix%' LIMIT 5. Exit 2 on<4chars, exit 1 on no match or ambiguity. Rejected: requiring full 36-char UUIDs (operationally hostile after copy-paste fromtask list); short sequential IDs (would force a separate human-id column on the store and break the existing event payload contract). -
Default
--statusfilter oniris task listisopen. The brief's "Open tasks" slot pulls the same default. Users pass--status all(or a specific status) to widen. Rejected: defaulting toall— would render terminal noise on every casual list invocation. -
iris task updaterejects terminal status transitions (done,dropped) with exit 2 and a message routing the user tocomplete/drop. Reason: terminal transitions on TaskStore have side effects (completed_atset + dedicated typed events fired) and the CLI must not duplicate that logic. Forcingcomplete/dropkeeps the side-effect path canonical. -
iris task update --clear-dueflag instead of empty--due "". Typer treats an empty option as "not provided", so there's no string-sentinel way to express "clear the existing value". An explicit boolean flag is unambiguous and self-documenting. -
iris task group <a> <b> ... --under "title"creates a new parent task then re-parents every listed child viaTaskStore.update(child, parent_task_id=parent.id). Tree-only per ADR-0005 — children that already had a parent are re-parented to the new one (no DAG). Rejected: rejecting already-parented children (would force the user to ungroup first); making--underaccept an existing task id (saves a call but conflates two distinct operations —move-intoversuscreate-and-collect). -
iris goal achieveonly — no separateiris goal pauseoriris goal drop. Goals already supportpaused,dropped, andachievedviaTaskStore.update_goal, but the only terminal one we surface from the CLI isachieved. Pause and drop are reachable from the store API (and from a future harness tool); the CLI surface stays small until evidence shows the other two are user-asked. -
List markers in
iris goal showuse(x)/( ), not[x]/[ ]. Rich treats[x]as a markup tag and silently strips it. Parentheses render identically across console widths and don't collide with Rich's tag parser. -
Rich tables for all multi-row output; single-row results render as plain
console.printlines. Matches the email-CLI convention and keeps--print --jsonportable (Rich tables strip cleanly when piped). -
Added
TaskStore.get_by_dedup_keyas a public wrapper over the existing private_get_by_dedup_key. The Track 2B auto-resolver needs to look up the followup task byfollowup_key(provider, thread_id)without depending on raw SQL or private method access. Same row-visibility rule (dropped tasks don't surface) as the private helper — ADR-0014 decision #3 remains intact.
Alternatives considered¶
-
CLI under
src/iris_harness/cli/task_cmds.py. Cleaner separation frommain.py. Rejected: every other CLI subgroup is inline; doing this one differently fragments the surface. -
iris tasks(plural) command name. Rejected: Typer subgroups conventionally use the singular noun (vault,auth,email). The plural would be cosmetic divergence. -
iris tshort alias. Rejected:tis too easily confused withtab, terminal aliases, etc. Users can alias locally. -
Live
iris task watchmode that streamstask.*events. Rejected for MVP: useful, but the brief covers the common "what's the state right now?" question and a watch mode needs a stable event-replay story that Phase 2 doesn't have. -
JSON output via
--json. Rejected for MVP: the harness reads via the SkillRegistry tool surface (Track 2C), not the CLI. Operators pipingiris task listthroughjqis a real future case; revisit when somebody asks.
Consequences¶
Positive:
- The Phase 0 substrate now has a usable surface — every task /
goal mutation a producer skill performs is inspectable, and a
human can
add/complete/drop/groupto manage their own work alongside producer output. - Track 2B (email-followup) and Track 2C (brief integration)
both inherit the prefix-resolution convention via the new
_short_idrendering, keeping ID display consistent across the CLI and the brief slots. - 15 CLI tests in
tests/unit/test_cli/test_main_task.pycover CRUD, prefix resolution edge cases,--clear-due, group reparenting, and goal-task linkage rendered bygoal show.
Negative:
- Inline CLI commands push
src/iris_harness/main.pypast 3000 lines. Continues the trend (the file is now the load-bearing CLI registry); a future split is a larger refactor than was warranted for this track. - Prefix resolution opens a small window for "ambiguous prefix" errors as the task list grows. The 4-char minimum keeps the collision probability low (~1 in 65k per task) but operators with thousands of tasks may need to type longer prefixes.
Reversibility: high. Each command is its own function; the
shape of any one can change without touching the others. Adding
--json or iris t aliases is a one-commit addition.
Related ADRs¶
- ADR-0005 (parent) — Task vocabulary + soft migration.
- ADR-0014 (sibling) — TaskStore + dedup_keys + event payloads this CLI sits on.
- ADR-0028 — email-followup skill that produces followup tasks this CLI now exposes.
- ADR-0029 — brief integration that consumes the same TaskStore
via the
iris-tasksbuiltin skill.