Parking work is a hold on the graph, not a status and not orchestrator configuration
Context and Problem Statement
Skift has no way to park work. status is only open, done, dropped; ArchiveProject is a one-way read-only door; priority is advisory ordering that skift derives nothing from. So “not this week” can only be said by inflating a priority, and “not this project” can only be said outside skift.
The orchestrator is the obvious place to say it, and the domain model invited that: “Ordering across projects is the orchestrator’s business, because only the orchestrator knows which projects matter this week.” But an orchestrator holding the roster of what matters puts a human decision — sweep everything aside except A, B and C — outside the project management system, where skift ready, a laptop session, and a second orchestrator all disagree with it.
Where does the decision that work is parked belong, and what shape does it take?
Decision Drivers
- Intent is the human’s job and skift is the store for it (vision); a roster of what matters this week is intent
- The failure the vision opens by naming is the true board state living somewhere other than the board
- Stored state is only what cannot be derived (principle 1)
Readymust make no scheduling decision (principle 6), and must not acquire a stage machine (vision, “not a workflow engine”)- A parked subtree must stay parked when a child is filed under it tomorrow
- Work that vanishes from the ready set with no explanation is worse than work that is never parked
Considered Options
- A roster in the orchestrator’s configuration
- A hold node with
blocksedges into the work being parked - A fourth
statusvalue,pausedorheld - A hold as a stored eligibility fact on a node and a project
Decision Outcome
Chosen option: a hold as a stored eligibility fact, on the node or project it is applied to, inherited down the parent relation and derived on every read.
The line the decision rests on:
Priority orders the eligible set. Hold decides what is eligible. Ordering is the orchestrator’s; eligibility is the graph’s.
The original sentence conflated the two. What an orchestrator genuinely owns is arithmetic — how a p0 in a three-day spike interleaves with a p2 in the platform project, given budget, capacity, and which models are free. Combining two project-scoped priority scales needs a rule skift has no basis to choose. Deciding that a project is not this week’s bet is a different question, and it is not arithmetic.
Eligibility was already multi-axis. A leased node leaves the ready set without its status changing, and so does a blocked one. Hold is a third axis of the same kind, which is why it is not a status: a held node is plainly still open, and nothing about it has moved through a stage.
The shipped shape:
| Stored | held_at, held_by, held_reason, hold_group on nodes and on projects |
| Derived | A node is effectively held if it, any ancestor via parent_id, or its project carries a hold. Never written down |
| Who | User principals only. An agent under a person’s uid is that person’s principal (ADR-009); a headless agent in its own trust zone is refused, and the refusal names the next move |
| Reads and writes | Unaffected. Held work is read, decomposed, commented on, and re-linked as usual. Only Claim and ClaimNext refuse |
| Focus | Focus and ClearFocus sweep the complement of a keep-set, server-side, over an organization or within one project |
Focus is a server RPC rather than a client-side expansion for two reasons: a complement sweep over a large workspace exceeds the per-patch operation ceiling, and a semantic operation implemented in each client drifts between client versions. It is declarative and replacing — calling it again in a scope retires the previous sweep in the same transaction — so exactly one sweep can be active per scope and ClearFocus needs no sweep id. That matters because a sweep outlives every session that observes it, unlike a lease, whose fencing token works only because it belongs to one live session. State that outlives sessions must be discoverable, not carried.
A sweep holds only what is not already held, and marks what it holds with a hold_group; a hold placed by hand carries none, and ClearFocus releases only its own group. So an epic parked in March for its own reason survives both a sweep declared today and the clearing of it, with no stacked holds and no reference counting.
Consequences
- Good: What is live is in skift. Every client —
skift ready, an interactive session, one orchestrator or three — gets the same answer, and the reason, the principal, and the time are on the record. - Good: Priority goes back to meaning one thing. Without a hold,
p4has to mean both “low value” and “not now”, and the second meaning is what drives the inflation the priority section already anticipates. - Good: An orchestrator keeps everything that is genuinely its own — interleaving, budget, model choice, concurrency — and holds no facts about the projects it runs.
- Bad: Holding a node parks its downstream cone, and the cone is not itself held. Holding one node can silently stall a large amount of work.
Holdtherefore reports the count it parks, andExplainon a stalled dependent traces to the hold rather than stopping at the blocker. - Bad: A hold is stored state that nothing expires, so a stale hold makes skift quietly wrong about what is live. The reason is required, holds are events, and parked work is listable — but nothing collects them. An expiry is deliberately deferred until stale holds are observed rather than imagined.
- Bad: Readiness gains a recursive ancestor walk, where every other condition is a flat anti-join. It is bounded by the acyclicity guarantee the parent relation already carries and backstopped by the statement timeout the limits table already names.
- Neutral: Archive becomes the terminal point of this axis rather than a separate concept: a permanent hold that also makes the project read-only. That archiving has no inverse in the API is a pre-existing hole, unchanged by this decision.
- Neutral: A held gate stops notifying, because notifiers subscribe to ready user-assigned nodes. Parking an ask silences it, which is what parking means.
Rejected Options
A roster in the orchestrator’s configuration
The work is kept out of the orchestrator’s reach by a list of projects and per-project priority floors in its own config.
- Good: No skift change at all. Ships with the orchestrator, and every filter it needs (
min_priority,labels) already exists onReady. - Bad: The decision lives where no human or other client will look for it, and there is no record of who made it or why. Two orchestrators, or an orchestrator and a person, disagree about what is live. This is the incumbent failure the vision exists to fix, reproduced inside skift’s own consumer.
A hold node with blocks edges
Parking is expressed with the graph it already has: an open node titled “on hold”, with blocks edges into the work being parked, completed or dropped to unpark.
- Good: No schema change, no new concept, atomic in one patch, and the event log and
Explaincome for free. - Bad: A
blocksedge means a dependency, and parking is not one; overloading it makesExplainsay something untrue. Worse, edges attach to the nodes they name, so a child filed under a parked epic tomorrow escapes the park. That is not an ergonomic wart — it is the feature not working.
A fourth status
status grows held beside open, done, and dropped.
- Good: One column, already on every query, no new relation to derive.
- Bad: Status is the stage a node has moved through, and a parked node has not moved. It would make
openmean two things, force every consumer’s status handling to change, and put a stage machine in a store that says it is not a workflow engine. It also cannot express a held project, or inheritance down a subtree, without a second mechanism anyway.
More Information
Related: ADR-009 for the trust zone that makes “user principals only” workable rather than a deadlock, ADR-006 for why an agent hiding work from other agents is the shape of risk being guarded against, ADR-004 for where an organization changes who may hold.
This decision revises two documents rather than only adding to them: the domain model’s priority section no longer sends the roster to the orchestrator, and principle 6 now distinguishes eligibility from ordering.
Revisit when stale holds are observed in practice, which is the argument for an expiring hold; or when a deployment wants a person to park work without being able to unpark someone else’s, which is a policy question rather than a model one.