Tasks: Make the CLI/TUI experience seamless
The last spec artifact. A DAG derived from the approved
design.mdandtesting-plan.md. Each task's_Test:_names a row of the plan's matrix. Single repository (the-loop graph repos issue-25 --clear); everything lands on PR #26.
There are 22 tasks in five layers: foundations, then workflow behaviour, then commands, then surfaces, then proof and docs. Every task is test-first: write the named test, watch it fail, make it pass. The commit records red → green. Security-relevant tasks name their negative test.
Task list
Layer A: foundations (no dependencies between them)
- [x] 1.
tiny_harness/home.py: configuration resolution and safe default creationConfigSource,resolve_config_path,default_config_text,ensure_default_configas in design §tiny_harness/home.py.- The creation steps:
lstat, owner check,mkdir 0o700,O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW|O_CLOEXECat0o600. Every refusal is aConfigError(path). geteuidis injectable for tests.- Depends on: none
- Requirements: R2.1–2.8, R11.1; abuse cases 7, 8
- Test: T1
tests/unit/test_home.py(resolution, default text loads asSettingswith absolute paths, existing file untouched); T8tests/security/test_home_paths.py(symlink home, symlink file, foreign owner, modes)
- [x] 2. Status extension module and sub-state on
with_stateharness/core/status.py:STATUS_EXT_URI,STATUS_EXT_KEY,SubState,StatusExtensionData.HarnessTask.with_state(state, sub_state=None)writes the key, or deletes it.HarnessTask.sub_statereads it.- Depends on: none
- Requirements: R4.1, R4.3, R3.3
- Test: T1
tests/unit/core/test_status.py
- [x] 3. Task extension fields, lifecycle derivation, and the server-controlled refusal
TaskExtensionDatagainslifecycle,modelandreasoning_effort.HarnessTask.lifecyclederivesdelegated/conversationwhen the field is absent.extension_from_envelopeandapply_task_enveloperefuse an envelope that sets any of the three.- Regenerate
docs/a2a/ext/task.json. - Depends on: none
- Requirements: R3, R7.3; abuse case 1
- Test: T1
tests/unit/core/test_task.py; T8tests/security/test_server_controlled_fields.py; T10tests/unit/core/test_task_compat.py
- [x] 4. Model configuration and
ModelCatalog[openai]and[anthropic]gainmodelsandreasoning_effort. A validator rejects an emptymodels, or one missingmodel(exit 2,openai.models).ModelCatalogprovidesdefault,selectable,default_effort,context_window(model)andsettings_for(task).- Depends on: none
- Requirements: R7.1, R7.5, R8.1, R8.5
- Test: T1
tests/unit/test_config_openai.py,tests/unit/models/test_catalog.py; T10tests/unit/test_config.py(old file unchanged)
- [x] 5. Command primitives: hook operation, contexts, registry, name filter
Operation.COMMAND_INVOKED.CommandInvokedPre/In/Post,CommandSpec,TaskSettings,SpawnIntent,CommandResult,TaskSnapshot.EntityKind.COMMAND.CommandRegistry.register(spec, handler, priority=500)raisesCommandErroron a bad or duplicate name._ForCommandpasses through every other name, and any context whose result is already set.- The command-text parser.
- Depends on: none
- Requirements: R6.1, R6.3, R6.5, R6a.1–6a.5, R6a.7
- Test: T1
tests/unit/commands/test_registry.py,tests/unit/commands/test_parse.py
Layer B: workflow behaviour
- [x] 6. Turn end parks conversation tasks;
ask_participantsub-stateOutcomeKindgainswaiting_for_message.Operations.await_next_messagein both the in-process and the workflow implementations.CoreLoop._finishbranches on the lifecycle.set_status(..., sub_state=)writes the status-update metadata.askpassesawaiting-answer.- The executor's first-message path sets
lifecycle="conversation". - Depends on: 2, 3
- Requirements: R3.1–3.3, R3.6, R4.1
- Test: T1
tests/unit/core/test_loop.py; T2tests/integration/durable/test_conversation_lifecycle.py(Scenario: A reply leaves a conversation task awaiting the next message;…resumes the same task and clears the sub-state;ask_participant parks the task awaiting an answer;A delegated sub-task still completes when its loop ends)
- [x] 7.
spawn_childsets the lifecycle and inherits settingsdelegatedforspawn_subtask.conversation, with no first message, for a/sub-taskspawn.- The parent's
modelandreasoning_effortare copied into every local child. - Depends on: 3, 6
- Requirements: R3.4, R7.3, R9.2
- Test: T2
Scenario: Sub-tasks inherit the parent's model and effort
- [x] 8. Per-request model and effort through every model call
LLMRequest.modelandLLMRequest.reasoning_effort.- The request builders (
context.py,compaction.py,help.py) usesettings_for. - The adapters map effort: Responses
reasoning, Chatreasoning_effort, Anthropicoutput_config.effort. request.model or self._model.assemble_contextuses the per-model window.- Span attributes.
- Depends on: 3, 4
- Requirements: R7.3, R8.3, R8.5; NFR observability
- Test: T1
tests/unit/models/test_openai_effort.py,test_openai_chat_effort.py,test_anthropic_effort.py(byte-identical when unset); T2Scenario: /model and /effort change the next model request, compaction and help included
- [x] 9. Intake classification and the control envelope
EventEnvelope.kindgainscontrol(withaction: Literal["complete"]andextra="forbid").IntakeOut.commandandIntakeOut.control.- A command or control writes an audit row and adds no history.
- A watcher's control is not accepted.
- Depends on: 3, 5
- Requirements: R5.1, R6.2, R6.3; abuse cases 4, 9
- Test: T1
tests/unit/service/test_intake_classify.py; T8tests/security/test_command_roles.py(watcher control;/unknown hiis ordinary)
- [x] 10.
run_commandactivity and the workflow dispatcherActivityName.RUN_COMMAND,CommandInandCommandOut. The activity runs the chain throughOperationRunner: a pre-abort becomes a refusal, an exception becomes/<name> failed: …, and no result becomes "has no handler"._dispatch()runs beforeCoreLoop, and at the loop's drain points.- The loop starts only when the ordinary queue or the notes are non-empty.
- Results are applied in the order settings, spawn, reply, with the
TaskRefconfirmation andtask_refmetadata. _run_participantdispatches first.- Depends on: 6, 7, 9
- Requirements: R6.2, R6.6, R9.1, R10.2, R3.4
- Test: T2
tests/integration/durable/test_commands.py(A command runs without invoking the model and the state is unchanged;…during a running turn…;A failing command…;A participant task still completes on the assignee's answer, and a command does not)
- [x] 11. Participant completion and cancel hardening
- The inbox update validator authorizes a control: sender present, on the task, not a
watcher. Only then does it set the interrupt. _complete(actor)runsdecide_completionwithTaskCompleteIn.requested_by. COMPLETE finishes the task. WAIT emits a refusal naming the unresolved sub-tasks and re-emits the state.- The executor's terminal refusal names the state, for
executeandcancelalike. - A
watcher's cancel is refused. - Depends on: 9, 10
- Requirements: R5.1–5.4, R5.6; abuse cases 3, 4
- Test: T2
tests/integration/durable/test_complete.py(A participant completes a task and every subscriber sees COMPLETED;Completing a task interrupts the turn in progress;Completing or cancelling a terminal task names its state;Completion is refused while a sub-task is open); T8tests/security/test_complete_roles.py(non-member, watcher idle and mid-turn)
- The inbox update validator authorizes a control: sender present, on the task, not a
Layer C: commands
- [x] 12. Plugin
commands.jsonand command-owned hooksPluginLoader.loadresolvescommands.jsonbefore its hook loop, and the hook loop skips command-ownedHookDefs.- Command-owned hooks are registered only through
_ForCommand. Their points must be exactlycommand.invoked.in. - Skips: bad name, unknown hook, extra points, shared hook, duplicate.
ComponentKind.command. Schema atdocs/plugins/commands.json.- Depends on: 5
- Requirements: R6a.6–6a.8; abuse case 10
- Test: T1
tests/unit/plugins/test_commands_json.py; T8tests/security/test_remote_command_executor.py(sees only its own command, scrubbed)
- [x] 13. Built-in commands and the role
preexecutor/model,/effort,/sub-taskand/status, as pure handlers overCommandInvokedIn+ModelCatalog.- The built-in
preexecutor at priority 100 refuses/model <x>,/effort <x>and/sub-taskfrom awatcher, and any command from a sender with no role. - The built-in plugin registers these at
DEFAULT_BODY_PRIORITY. build_runtime(builtin_commands=True).- Depends on: 4, 5, 10
- Requirements: R6.8, R7.2–7.4, R8.2–8.4, R9.1, R9.3, R10.1; abuse cases 1, 2, 4, 5
- Test: T1
tests/unit/commands/test_builtins.py; T2Scenario: /sub-task creates a linked conversation task and replies with its id,Scenario: A plugin command overrides a built-in and a pre hook can refuse it,Scenario: A remote command executor that fails leaves the task unchanged; T8tests/security/test_command_arguments.py(unlisted model, bad effort, hostile arguments)
- [x] 14. Card: status and commands extensions
SUPPORTED_EXTENSIONSandEXTENSION_DESCRIPTIONSgain status (required: false) and commands, the latter withparams.commandsfrom the registry.- Schemas:
docs/a2a/ext/status.json(a string, not a closed enum),status/v1.md,commands.json. - Depends on: 2, 5, 13
- Requirements: R4.2–4.4, R6.7
- Test: T3
tests/contract/test_card.pysnapshot,tests/contract/test_ext_schemas.py
- [x] 15. CLI: bare command, config wiring, help epilog
add_subparsers(required=False). No subcommand dispatches totui.mainresolves the configuration and callsensure_default_configfor the default origin, beforeSettings.load.- A
parse_known_argspre-pass builds the epilog.--helpcreates nothing. - Depends on: 1
- Requirements: R1.1–1.5, R2.1–2.4
- Test: T1
tests/unit/service/test_cli.py
- [x] 16. SDK parity and the public API
- Exports:
tiny_harness.home, the commands package, the sub-state constants and task completion. running_harness(..., plugins=…, commands=…, builtin_commands=…).- Regenerate the API snapshots.
- Depends on: 1, 5, 11, 13
- Requirements: R11.1–11.4
- Test: T3
tests/contract/test_*_api.py(UPDATE_API_SNAPSHOTS=1once, then green); T1Settings.load(None)andrunning_harnessnever touchHOME
- Exports:
Layer D: surfaces
- [x] 17. TUI: controls, sub-state,
/completion, sub-task navigationA2AClientPort.completeand.commands.SdkClientandScriptedClientimplement both.- Bindings:
ctrl+d,ctrl+c,f4, andenteron a sub-task row. - The top bar shows the state and the sub-state.
SuggestFromListon the composer, plus a hint line.- The task stack, and a
ListViewof sub-tasks. - The prototype is the visual contract.
- Depends on: 11, 14
- Requirements: R5.5–5.7, R6.7, R9.4
- Test: T5/T6
tests/ui/test_tui_keys.py,tests/ui/test_tui_snapshots.py(new states); T9 keys only
- [x] 18. Web UI: controls, sub-state, command listbox, sub-task links
a2a.ts:completeandcommands.model.ts:subState.- The header: Complete, Cancel, the sub-state badge and ← Parent, in an
actionsgroup. - The composer combobox and listbox.
- Sub-task links (
?task=). - The fixture gains the new states.
- The prototype is the visual contract.
- Depends on: 11, 14
- Requirements: R5.5–5.7, R6.7, R9.4; NFR accessibility
- Test: T1
src/model.test.ts,src/App.test.tsx; T5/T6e2e/visual.spec.ts(light, dark, phone, listbox, completed); T9e2e/a11y.spec.ts
Layer E: proof and documentation
- [x] 19. Abuse-case suite completeness and the rate limit
- Check that each abuse case 1–10 has a passing negative test.
- Add case 6:
/sub-taskpastrate_limit_per_minutegets 429. - Depends on: 1, 3, 9, 11, 12, 13
- Requirements: Security considerations, abuse cases 1–10
- Test: T8
uv run pytest tests/security
- [x] 20. E2E demo flow
- The offline Ollama demo expects
INPUT_REQUIRED · awaiting-next-message, a second message on the same task,/statuswith no model call, then complete. - The demo's own scripted client is updated.
- Depends on: 10, 11, 13, 15
- Requirements: R1.3, R3, R5, R10; NFR behaviour change
- Test: T4
uv run pytest tests/e2e/test_demo_ollama.py -m e2e
- The offline Ollama demo expects
- [x] 21. Documentation and the decision record
- README and the getting-started guide: install →
tiny-harness→ the default file; the conversation lifecycle; commands; Complete. docs/a2a/extensions.md: the status and commands extensions and the control envelope.- The capability docs:
configuration,tasks-and-plans,a2a-server,surfaces-and-renderers,models,hooks,plugins,entities-and-registry, each with a history row. docs/decisions/decision-007.mdplus its index row.evidence/documentation.md.- Depends on: 13, 14, 15, 16, 17, 18
- Requirements: NFR documentation
- Test: markdownlint (pre-commit) and
cd docs && bun run docs:build
- README and the getting-started guide: install →
- [x] 22. Verification hand-off
- Run the whole suite once, the way CI runs it, so that verification starts green:
uv run pre-commit run --all-files,uv run pytest tests/integration tests/contract tests/security tests/ui, and the webbuild,testand Playwright projects. - Depends on: 1–21
- Requirements: all
- Test: T1–T3, T5, T6, T8, T9 commands from
testing-plan.md§ Verification activities
- Run the whole suite once, the way CI runs it, so that verification starts green:
Dependency graph (DAG)
Critical path: 3 → 6 → 7 → 10 → 11 → 17/18 → 21 → 22. Tasks 1, 4, 8, 12 and 15 are off it and can be done whenever their inputs are ready.
Checkpoints
- After each task: its named tests, plus
uv run pre-commit run --files <changed>(ruff, pyright strict, unit tests, markdownlint). The commit message carries the task number and the red → green test. - After Layer B (task 11):
uv run pytest tests/integration/durable. The lifecycle, dispatch and completion scenarios must be green before commands build on them. - After Layer C (task 16):
uv run pytest tests/contract tests/security. - After Layer D (task 18):
uv run pytest tests/uiand the webtest,test:visualandtest:a11yscripts. - After task 22: the
verificationnode runstesting-plan.md. It ticks each activity and records its evidence underevidence/. The review phases follow: self, critic, and the security review gate inevidence/security-review.md.
Review comments
Appended by the-loop's
record-feedbackhook when a human gate approves with comments (issue-109). Append-only and attributed: an approval never silently discards a reviewer's suggestions, and the feedback travels with the document it concerns rather than living in a side-channel tracker.