Tasks: one rule for where a work item's attributes live
The last spec artifact (requirements → design → testing plan → tasks). A DAG of implementation tasks derived from the approved design and testing plan.
Task list
[x] 1. Declare the rule as data
cli/the_loop/state.py: anATTRIBUTEStable besideGENERATED_PATHS— one entry per top-level key of the three per-work-item files, with its kind (pointer, human decision, remote entity, operator ledger, machine handle, derived) and the file the rule allows it in- Depends on: none
- Requirements: R1.1, R1.5, R6.1
- Test: T1 —
test_state_portability.py(red→green)
[x] 2.
work-item-state.jsonv2cli/the_loop/graph/state.py: dropsession; addsessionPerPr,model,effortandpullRequests[](ref,repository,number,url,stateDir,state,linkedAt,linkedBy);link_pridempotent by ref,set_pr_state; a v1 file'ssessionis ignored on load and absent on the next save;repositoryvalidated throughrepo_state_key- Depends on: 1
- Requirements: R2.1, R2.2, R3.1, R3.3, R4.1
- Test: T2, T3 (red→green)
[x] 3.
session: inheritresolves through the session registrycli/the_loop/graph/runtime.py:resolve_sessionasks the registry for the live endpoint of this state's ref (outer: the work item; inner: the PR) and falls back tofresh-with-artifacts;cli/the_loop/graphlink.py:_bind_sessionandon_closestop writing into the state file- Depends on: 2
- Requirements: R3.1, R3.2, R3.3
- Test: T3, T4 (red→green)
[x] 4. The frozen choices move into the work item's file
cli/the_loop/graph/hooks/selection.py: the freeze writessessionPerPr,modelandeffortintoWorkItemStatebesideskips/optIns/surface; it stops emittingfrozenGraphcli/the_loop/webhook/dispatcher.py:_tmux_forand_resolved_choiceread the state file in the checkout the session record names, falling back to the portablegraphsection for a work item frozen before this change, then to the operator's defaults;_record_frozen_graphis removed- Depends on: 2
- Requirements: R4.1–R4.5, abuse cases 1–2
- Test: T9, T10 (red→green)
[x] 5. The portable record's sections
cli/the_loop/workitem.py:SECTIONSdropsGRAPH, gainsCHANNELSandPULL_REQUESTS;owner_of(ref)scans the pull-request maps;index.jsonlists one entry per work item naming its PR refscli/the_loop/control.py:record_frozen_graphremoved,frozen_graphkept as the legacy reader only- Depends on: 4
- Requirements: R4.2, R8.3, R10.1, R10.7
- Test: T11 (red→green)
[x] 6. One portable record per work item
cli/the_loop/poller/poller.py:PollStatetakes(owner, ref)—owner == refwritespoll, otherwisepullRequests[ref]; the owner is resolved before any write (session registry → portable maps → the router's linkage on the listed item), and a PR that resolves to nothing is a work item of its owncli/the_loop/webhook/dispatcher.py: a PR with an owner records its upstream state through GraphLink and drops its nested ledger; noendedon a PR record- Depends on: 5
- Requirements: R10.1–R10.6
- Test: T23, T6 (red→green)
[x] 7. The local record is a map of sessions keyed by ref
cli/the_loop/sessions/registry.py:{workItem, channels?, sessions{ref → handles}};record_owningis a key lookup;session_for,link_pull_request,save_endpoint,close_endpointandtouchkeep their signatures; a v1 record (top-level handles +pullRequests[]) loads into the map and is rewritten as v2; nothing about a pull request but its ref is kept- Depends on: 1
- Requirements: R5.1, R5.2, R5.5, R8.1
- Test: T24, T12, T5 (red→green)
[x] 8. The channel binding is the operator's; the cursor is the machine's
cli/the_loop/channels/state.py:threadsandconversationsare no longer written,cursorskeeps onlychannel:*; the old maps are exposed read-only aslegacy_bindings()cli/the_loop/channels/slack.py:thread_for/bindgo through the portable record'schannels.slack;cursor/advancethrough the work item's local record; the thread → work item index is built in memory from the portable records- Depends on: 5, 7
- Requirements: R2.3, R5.1, R5.3, R5.4, R7.3, abuse case 6
- Test: T7, T8, T19 (red→green)
[x] 9. The control plane stops joining two identities — amended: the join stays
GET /api/v1/work-itemsnow serves one record per work item, pull requests included, so the issue-302 reconciliation has nothing to do in the ordinary case- It is kept (design D9, amended in implementation): a pull request given its own portable record before this change still has one, and R8.1 deletes no record — removing the join would draw those twice. Inert when there is nothing to reconcile
nodes[]comes fromthe-loop check, which the client already calls- Depends on: 6
- Requirements: R4.5, R10.1
- Test: T13, T22
[x] 10. Lifecycle: cleanup, reset, upgrade
cli/the_loop/cleanup.py,reset.py: cleanup deletes the local record (cursors with it) and keepschannels; reset clears every portable section includingchannelsandpullRequestscommands/upgrade-the-loop.md: report the three retired locations and the per-PR portable records as no longer read; delete nothing- Depends on: 6, 7, 8
- Requirements: R7.2, R8.1, R8.2, R10.6
- Test: T14, T23
[x] 11. The hand-off scenario
cli/tests/test_state_root_integration.py: the design's worked example on two state roots — the three pull requests and the thread survive; the new machine routes a PR event with no provider call- Depends on: 6, 7, 8
- Requirements: R2.1–R2.3
- Test: T13 (red→green)
[x] 12. Docs, decision record and capability docs
docs/cli/state.mdrewritten by the classification (attribute tables); the decision record superseding decision-046's file-level rule; capability docscli,process-graph,interactive-sessions,channels,control-plane,webhook-triggers,spec-workflow- Depends on: 9, 10
- Requirements: R9.1–R9.4
- Test: T15, T22
[x] 13. Verification
- Execute the testing plan; record results, evidence and the security round
- Depends on: 11, 12
- Requirements: all
- Test: T22